Conversation
Declarative type registry mirroring lib/config/schema.zsh: - 10 syncable types in parallel arrays (label/kind/bundles) - Bundle resolver expands all/customizations/claude-config/preferences - _core_account_sync_resolve_includes mixes types+bundles, subtracts excludes Engine skeleton documents the 5-function strategy contract per type. Dispatcher skeleton parses --include/--exclude/--dry-run/--yes/--force. Refs: docs/plans/2026-05-02-sync-overhaul-design.md §3, §5.2, §7.1.
Per-invocation backup dir at <dst>/.ckipper-sync-backups/<UTC-ts>-from-<src>/ with 0700 perms; 0600 on backed-up files. JSON manifest with version, source, target, timestamp, files[]. Per-target rollback reads manifest and reverses each entry (create→rm, overwrite→restore). Undo wraps rollback + cleans backup dir on success. Refs: docs/plans/2026-05-02-sync-overhaul-design.md §7.4-§7.6.
Three structured (JSON-key merge) types share json_validate + json_atomic_write helpers. mcp enumerates .mcpServers entries; settings enumerates jq leaf paths excluding .hooks; prefs operates on the registry via _core_config_get/_core_config_set. Plan-level fixes: - settings_enumerate jq filter rewritten (the original paths(scalars+objects+arrays) addition does not work — replaced with a paths walk that filters out array-index components and object-typed leaves). - Renamed local var `path` to `id` in settings strategy: zsh ties lowercase `path` to `$PATH` as an array, corrupting the env when the function does `local path=...`. Refs: docs/plans/2026-05-02-sync-overhaul-design.md §3, §7.1.
files_flat covers claude-md, agents, commands, output-styles via shared helpers parameterized by _CKIPPER_SYNC_FILES_FLAT_PATH; per-type wrappers satisfy the strategy naming convention. Comparison via sha256. files_dir covers skills with cp -a (preserves symlinks) and a recursive content hash that compares symlinks by target path, regular dirs by concatenated per-file hashes. Plan-level fix: renamed local var `status` to `cmp_status` in summary functions — `status` is read-only in zsh (alias for $?). Same rename applied to mcp/settings strategies. Also renamed `path` to `target` in _core_sync_dir_hash (zsh ties lowercase `path` to $PATH). Refs: docs/plans/2026-05-02-sync-overhaul-design.md §3, §7.1.
statusline detects internal vs external script (path resolves under $src or not). Internal: backup + copy script + rewrite .command path. External: settings reference only. hooks filters $src/hooks/* against $CKIPPER_DIR/hooks/ (install allowlist) so only user-written hooks enumerate. Apply copies script + rewrites paired settings.hooks entries with src→dst path substitution. Plan-level fixes: - statusline replaced `local -n obj_ref` (bash nameref idiom that works inconsistently in zsh subshells) with a function that prints the rewritten JSON instead. - hooks rewrote the merge jq filter — original used `--slurpfile` (yields a 1-elem array) but indexed it as an object directly. Split into a filter + merge two-step, both expressed as ordinary jq. Refs: docs/plans/2026-05-02-sync-overhaul-design.md §3, §7.1.
- _core_account_sync_assert_dst_idle hard-refuses when Claude is running on destination (`--force` bypass) - _core_account_sync_validate_pair rejects src==tgt - _core_account_sync_build_change_set walks each requested type via the strategy contract (enumerate then compare per item) - _core_account_sync_apply_target reads change set on stdin, calls apply per row, rolls back from backup dir on any failure - _core_account_sync_apply_one bridges strategy contract to manifest schema; prefs uses account names, others use dirs - _core_account_sync_manifest_rel maps (type,id) → manifest path field — mcp→.claude.json, settings/statusline→settings.json, prefs→accounts.json, files→id Plan-level fix: renamed local var `status` to `change_status` — `status` is read-only in zsh (alias for $?), causing the read-loop and apply-one parameter binding to fail. Refs: docs/plans/2026-05-02-sync-overhaul-design.md §7.3, §8 #5.
Summary renderer reads change-set on stdin, groups by type, prints [+] new / [~] overwrite badges with strategy-supplied summaries and the backup dir path. Drill-down picker filters to overwrite-only rows and dispatches to <type>_diff via _core_prompt_choose. Plan-level fix: renamed local var `status` to `change_status` / `cmp_status` in render funcs (read-only collision again). Refs: docs/plans/2026-05-02-sync-overhaul-design.md §6.
Source picker (single), target picker (multi-select via gum --no-limit), type picker (multi-select). All honor CKIPPER_NO_GUM via fallback to read prompts. Refs: docs/plans/2026-05-02-sync-overhaul-design.md §5.3, §6.
ckipper.zsh sources the new sync subsystem (registry, backup, engine, preview, interactive, dispatcher, then strategy modules) instead of the deleted lib/account/sync.zsh. lib/account/dispatcher.zsh routes `sync` to the new _ckipper_account_sync_dispatch. The other arms (add/list/default/ remove/rename/sync-hooks) keep their existing case branch. Old lib/account/sync.zsh (--mcp/--settings/--all flag surface, no preview, no backup) and its tests are deleted now rather than waiting for Phase 12.1: their tests reference functions whose orchestration was moved into the new sync/ module, so leaving them in causes the test suite to break. Integration tests cover --include mcp --yes (apply), --dry-run (no apply), unregistered-source rejection, and src==tgt rejection. Refs: docs/plans/2026-05-02-sync-overhaul-design.md §5, §7.2-7.6.
The 4 ckipper-managed safety hooks (bash-guardrails, protect-claude-config, docker-context, notify-bell) live at $CKIPPER_DIR/hooks/ and are deployed to every account by construction. That's install→all redeploy, NOT peer-to-peer sync — the rename to redeploy-hooks makes the semantics explicit so the new `account sync ... --include hooks` (user-hook-only, peer-to-peer) doesn't conflict. - _ckipper_account_sync_hooks_for → _ckipper_account_redeploy_hooks_for - _ckipper_account_sync_hooks → _ckipper_account_redeploy_hooks - account dispatcher arm 'sync-hooks' → 'redeploy-hooks' - help text rewritten to flag the install-vs-peer distinction - account-management.zsh + aliases_test.bats updated to track renames Refs: docs/plans/2026-05-02-sync-overhaul-design.md §2 (non-goals), §7.2 (rename rationale).
…ase 11) After _ckipper_setup_apply_account returns, _ckipper_setup_offer_initial_sync checks the registry account count and prompts the user to sync from an existing account into the new one. Silently skipped on first account. Refs: docs/plans/2026-05-02-sync-overhaul-design.md §9.
- Tab completion bumped to v7: adds redeploy-hooks subcommand, sync flags (--include/--exclude/--dry-run/--yes/--force), and multi-target account-name completion at arg4. - README: new 'Sync state between accounts' section with the 10 syncable types, common commands, named bundles, safeguards/undo, and a sync-vs-redeploy-hooks distinction. The MCP-per-account section now points readers at the new sync section instead of describing a flag surface that no longer exists. - CHANGELOG: dedicated 'Sync system overhaul' block under Unreleased — every new capability + the rename + the removed flags listed. - CONTRIBUTING.md unchanged (no stale sync references). Refs: docs/plans/2026-05-02-sync-overhaul-design.md §11.
Critical:
- statusline: detect interpreter-prefix commands like "bash <src>/script.sh"
by walking every whitespace token, not just the first. The apply
function's path rewrite switched from sub() (anchored, regex) to
literal split+join — safe with paths that contain regex metacharacters.
- preview UX wired end-to-end. _summary functions are now called per
changeset row to populate the summaries TSV. The Apply prompt is a
3-way Apply / View changes / Abort chooser; View loops through the
drill-down picker until Apply or Abort. _ckipper_account_sync_count_changes
prints the trailing "N changes (a new, b overwrite)" line per §6.1.
Important:
- _CKIPPER_LEGACY_COMMANDS[sync-hooks] now points at the renamed
redeploy-hooks subcommand.
- Drill-down id/display fix: items file is the source of truth for the
original id, recovered via _drill_down_resolve_id(items, type, display).
Without this, files-based diffs would resolve to display ('foo.md')
instead of id ('agents/foo.md').
- Per-target rollback safety: manifest_append fires BEFORE the apply
call. A strategy that backs up + writes + then errors mid-write now
has its entry in the manifest, so rollback restores from the backup
dir. Test added that simulates a crashing apply and verifies the
destination is restored to its pre-sync state.
- Renamed every _core_* function defined in lib/account/sync/ to
_ckipper_account_sync_* (55 functions). Per .claude/rules/shell-conventions.md,
_core_* is reserved for lib/core/. Sed pass: _core_account_sync_ →
_ckipper_account_sync_, then _core_sync_ → _ckipper_account_sync_.
- Added lint-merge-guards rule that catches future _core_* definitions
outside lib/core/. The guard would have failed this PR's pre-fix
state — it now guards against the same drift recurring.
Code review fixes pushed (
|
Code reviewFound 5 issues:
Ckipper/lib/account/sync/dispatcher.zsh Lines 150 to 158 in 7df8a43 Ckipper/lib/account/sync/preview.zsh Lines 88 to 103 in 7df8a43
Ckipper/lib/account/sync/engine.zsh Lines 168 to 181 in 7df8a43
Ckipper/lib/account/sync/dispatcher.zsh Lines 36 to 55 in 7df8a43
Lines 44 to 47 in 7df8a43
Ckipper/lib/account/sync/dispatcher.zsh Lines 244 to 260 in 7df8a43 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Five issues from the latest code review of PR #37: Critical - preview_prompt stdout pollution: when the user picked "View changes" then "Apply", $action ended up as "<diff text>...apply" not "apply", so the [[ "$action" == "apply" ]] gate at finalize silently skipped the apply. Two distinct causes: 1. drill_down_loop wrote diffs and the press-enter prompt to stdout, which was captured by the outer $() in run_one_target. 2. `local choice` inside preview_prompt's loop printed `choice='...'` on every iteration after the first — zsh re-declares behaviour. Fix: redirect drill_down_loop output to stderr; hoist `local choice=""` outside the loop; same hoist for `local _ack=""` in drill_down_loop. Regression test added to dispatcher_test.bats. 3-parameter cap (.claude/rules/code-style.md) - Introduced _SYNC_CTX associative array as the per-target context object. apply_one 8→3, finalize 7→1, drill_down_show 6→1, drill_down_loop 5→1, preview_prompt 4→0. Keys: src_dir, dst_dir, src_name, dst_name, backup_dir, changeset, summaries, items. Nesting cap (.claude/rules/code-style.md) - Extracted _ckipper_account_sync_accumulate_positional from parse_args's case `*)` arm, dropping the function from 3 nesting levels to 2. undo_dispatch doc-header (.claude/rules/shell-conventions.md) - Added the missing `Errors (stderr):` line for the "Unknown flag" and "Usage:" messages. CHANGELOG - Removed the false note claiming `lib/account/sync.zsh` is "intentionally untouched" — the file was deleted in this PR (commit ea61ee6) and replaced with the lib/account/sync/ tree. Verification - 445 bats + 6 pytest, all green - make lint-merge-guards green
Code reviewFound 16 issues. Verified by 5 parallel reviewers + a validator agent against Bugs (correctness / data-loss risk)
Ckipper/lib/account/sync/strategies/structured.zsh Lines 62 to 72 in 0c3724f Ckipper/lib/account/sync/strategies/structured.zsh Lines 148 to 159 in 0c3724f
Ckipper/lib/account/sync/strategies/hooks.zsh Lines 143 to 161 in 0c3724f Ckipper/lib/account/sync/strategies/statusline.zsh Lines 131 to 137 in 0c3724f
Ckipper/lib/account/sync/strategies/statusline.zsh Lines 124 to 137 in 0c3724f Ckipper/lib/account/sync/engine.zsh Lines 177 to 209 in 0c3724f
Ckipper/lib/account/sync/engine.zsh Lines 200 to 209 in 0c3724f Ckipper/lib/account/sync/backup.zsh Lines 44 to 59 in 0c3724f
Ckipper/lib/account/sync/dispatcher.zsh Lines 156 to 182 in 0c3724f
Ckipper/lib/account/dispatcher.zsh Lines 15 to 43 in 0c3724f Lines 19 to 44 in 0c3724f
Ckipper/lib/account/sync/backup.zsh Lines 80 to 90 in 0c3724f CLAUDE.md compliance — 3-param cap
Ckipper/lib/account/sync/engine.zsh Lines 117 to 130 in 0c3724f
Ckipper/lib/account/sync/backup.zsh Lines 80 to 90 in 0c3724f Ckipper/lib/account/sync/backup.zsh Lines 133 to 145 in 0c3724f
Ckipper/lib/account/sync/strategies/statusline.zsh Lines 124 to 137 in 0c3724f Ckipper/lib/account/sync/strategies/hooks.zsh Lines 143 to 161 in 0c3724f
Ckipper/lib/account/sync/dispatcher.zsh Lines 186 to 194 in 0c3724f Ckipper/lib/account/sync/preview.zsh Lines 44 to 60 in 0c3724f
Ckipper/lib/account/sync/engine.zsh Lines 29 to 40 in 0c3724f CLAUDE.md compliance — other
Ckipper/lib/account/sync/preview.zsh Lines 30 to 38 in 0c3724f
Test gaps
Ckipper/lib/account/sync/dispatcher.zsh Lines 142 to 154 in 0c3724f Ckipper/lib/account/sync/engine.zsh Lines 195 to 209 in 0c3724f UX / doc accuracy
Ckipper/lib/account/sync/dispatcher.zsh Lines 173 to 182 in 0c3724f Ckipper/lib/account/dispatcher.zsh Lines 77 to 95 in 0c3724f 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Seven verified bugs from the latest review, each with a regression test that watched the bug fail before the fix landed. - structured: mcp_compare/settings_compare returned "overwrite" when the destination JSON file was absent (jq -> empty, not "null"); manifest recorded op=overwrite and rollback would not delete the new file. Treat empty as the same case as "null". - hooks: filter_src jq gsub treated $src as a regex pattern; switched to literal split($src) | join($dst) so paths with `.`/`-` (e.g. ~/.claude-personal) don't mangle unrelated commands. - statusline: copy_and_rewrite now appends a manifest entry for the copied script file. Mid-write crash + rollback used to leave the script orphaned because manifest_rel only emitted settings.json. - backup: backup_file is now idempotent — a second call for the same rel within one invocation is a no-op so the original-state snapshot survives when settings + statusline both write settings.json. - dispatcher: skip assert_dst_idle for --dry-run; preview-only runs shouldn't be blocked by a still-open Claude session. - account dispatcher: 'sync-hooks' now prints a rename hint pointing at 'redeploy-hooks' (mirrors the top-level legacy-command pattern in ckipper.zsh). Removed the misleading "[hidden but callable]" doc and the contradictory CHANGELOG line, plus the stale comment about a non-existent fallback function. - backup: manifest_append cleans up its tmp file on jq failure (was using `&&` chain that left the tmp file in the backup dir).
Six confirmed defects in the new sync engine, all uncovered by a fresh multi-agent review. Each fix is paired with a regression test that fails on develop and passes after the change. E. apply_one used to derive op (create|overwrite) from change_status. A new SUB-KEY (e.g. one new MCP server) maps to change_status="new" even when the destination FILE already exists with unrelated content. The manifest then recorded op=create, and rollback's `rm -rf $live` would destroy the entire .claude.json on `sync undo`. Fix: derive op from `[[ ! -e $live ]]` at apply time, where $live is computed via a new live_path helper. (lib/account/sync/engine.zsh, backup.zsh) G. prefs rollback resolved the live path as `$dst_dir/$rel`, which for prefs is the destination ACCOUNT dir, not $CKIPPER_REGISTRY. Rollback wrote a stray `accounts.json` into the account dir and silently left the registry corrupted. Fix: live_path returns $CKIPPER_REGISTRY for type=prefs; rollback_target reads the type field from the manifest and forwards it to rollback_one. (lib/account/sync/backup.zsh, engine.zsh) A. assert_dst_idle filtered `_core_running_claude_processes` output via `grep -F $dst_dir`, but `pgrep -lx claude` only emits "<PID> claude" on macOS — no env, no argv. The grep could never match and the safeguard was dead code. macOS does not expose foreign processes' `CLAUDE_CONFIG_DIR` env var, so dst-specific filtering is not achievable; revert to refusing on any running Claude CLI. (lib/account/sync/engine.zsh, CHANGELOG.md, dispatcher.zsh help text) B. undo_dispatch read the module-level _SYNC_FORCE directly, which leaks across invocations: a prior `sync ... --force` left _SYNC_FORCE=true, and a subsequent `sync undo` (no --force) silently bypassed the running-Claude refusal because parse_args resets the flag only on the sync path. Fix: undo_dispatch uses a local force var. (lib/account/sync/dispatcher.zsh) D. statusline_compare lacked the empty-string guard that mcp_compare and settings_compare have. When the destination's settings.json was missing entirely, jq exited non-zero and d="" — the [[ "$d" == "null" ]] guard missed and compare returned "overwrite" instead of "new", mislabeling the preview UI. (lib/account/sync/strategies/statusline.zsh) F. hooks_apply mutates settings.json (adding the paired .hooks entry) but the engine's apply_one only recorded a manifest entry for the script file (manifest_rel returns "<id>" for hooks). The settings.json mutation was untracked, so rollback after a hook sync left a phantom .hooks entry pointing at a deleted script. Fix: hooks_apply now appends a settings.json manifest entry alongside the script entry; duplicate entries from multi-hook syncs are harmless because rollback is idempotent. (lib/account/sync/strategies/hooks.zsh) Tests: 455 → 467 (+12 regression tests across all 6 fixes). All green.
Fourth-round code reviewSpawned a 5-agent review pass focused on functional bugs that would prevent the sync system from working. Six confirmed defects (validated against the source, then fixed in commit 56f2837):
Each fix has a regression test that fails on develop and passes after the change. Test count: 455 → 467 (+12). All green; 🤖 Generated with Claude Code - If this review was useful, please react with 👍. Otherwise, react with 👎. |
#37) Consolidations driven by a parallel reuse / quality / efficiency review. - Extract lib/account/sync/_shared.zsh with hash_file / hash_dir / diff_line_stats / json_status. Removes the cross-strategy reach where hooks.zsh called _file_hash defined in files_flat.zsh, and the three copies of the new|unchanged|overwrite block in mcp/settings/statusline compare functions. - Refactor run_one_target (28 → 16 body lines) by extracting prepare_target_artifacts. Both helpers under the 25-line cap. - Replace [[ \"\$type\" == \"prefs\" ]] hardcoding (4 sites in engine.zsh + preview.zsh) with the new _CKIPPER_SYNC_TYPE_USES_NAMES registry array — adding a name-typed sync type now requires only a registry entry, not engine + preview edits. - Fix O(N²) preview summary lookup in render_summary — read summaries TSV into an assoc-array once instead of awk-scanning per row. - Replace 2 inline gum-detect blocks in interactive.zsh with _core_prompt_use_gum. - Delete stale §6.1 references in preview.zsh and dispatcher.zsh comments. Test files updated to source _shared.zsh in the same load order as production. 467/467 bats tests pass; make lint clean.
Code reviewFound 3 issues:
Ckipper/lib/account/sync/strategies/structured.zsh Lines 135 to 141 in 8adf336
Ckipper/lib/account/sync/preview.zsh Lines 29 to 36 in 8adf336
Lines 121 to 127 in 8adf336 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
- settings strategy: enumerator now also excludes `.statusLine.*` paths (in addition to `.hooks`). Without this, `--include settings` (or `--include claude-config`) silently planted the source's absolute `statusLine.command` path on the destination, breaking the destination's statusline. The dedicated `statusline` strategy owns path-rewrite + internal-script copy, so it must be the only enumerator for that subtree. Updated `structured_test.bats` to assert exclusion. - preview: extract magic number 26 → `_CKIPPER_SYNC_DISPLAY_COL_WIDTH`, matching the existing `_CKIPPER_SYNC_DIVIDER_WIDTH=45` pattern. - rename sweep cleanup: update stale `sync-hooks` → `redeploy-hooks` references missed in the original rename — `install.sh` (echo + comments), `doctor.zsh` WARN message, and `templates/settings-template.json` comment.
Summary
lib/account/sync.zsh(narrow--mcp/--settings/--allflag surface) with a polymorphic strategy-based sync system covering 10 syncable types.--include,--exclude,--dry-run,--yes,--force).account sync undofor restore.--force).account sync-hooks→account redeploy-hooksto disambiguate install→all redeploy from the new peer-to-peer user-hook sync (--include hooks).Architecture
Strategy pattern via declarative parallel arrays in
lib/account/sync/registry.zsh(mirrorslib/config/schema.zsh). Engine inengine.zshis type-agnostic — iterates the registry and dispatches to_ckipper_account_sync_<type>_{enumerate,compare,summary,diff,apply}functions per the strategy contract. Adding a new type is 3 array rows + 5 strategy functions; no engine edits.Safeguards
<dst>/.ckipper-sync-backups/<UTC-ts>-from-<src>/.ckipper-sync-manifest.jsonpowerssync undomktemp+mvjq -e .against tmpfile; refuses to commit invalid JSON--forceoverride required--yesrequires explicit intentTest plan
make lint— greenmake test-unit— 438 bats + 6 pytest, all greenmake lint-merge-guards— green (_ckipper_account_sync_*pinned tolib/account/sync/)ckipper account sync(no args) launches the wizardckipper account sync personal work --include mcpsyncs servers and shows backup pathckipper account sync undo workrestores from latest backupckipper account redeploy-hooksstill works as beforeNotes for the reviewer
--mcp/--settings/--allflag surface is removed entirely (no aliases or shims)..claude/rules/: ≤25 lines, ≤2 nesting levels, ≤3 params (modulo the strategy-contract apply functions which take 4: src/dst/id/backup_dir — that's the engine's interface, intentional).shell-conventions.md.lib/account/5 files,lib/account/sync/6 files,lib/account/sync/strategies/5 files (cap is 10, test files don't count).local path=...corrupts$PATH(lowercase tied to array);local status=...fails (read-only alias for$?). Renamed toid/targetandcmp_status/change_statusrespectively across strategy functions.Plan / design references
docs/plans/2026-05-02-sync-overhaul-design.mddocs/plans/2026-05-02-sync-overhaul-implementation-plan.md(Both gitignored per repo policy; included here as references for the reviewer.)