create-claude-cabinet 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +6 -10
  2. package/lib/cli.js +374 -91
  3. package/lib/copy.js +108 -6
  4. package/lib/db-setup.js +122 -17
  5. package/lib/engagement-server-setup.js +34 -9
  6. package/lib/metadata.js +51 -2
  7. package/lib/migrate-from-omega.js +13 -1
  8. package/lib/mux-setup.js +33 -9
  9. package/lib/settings-merge.js +52 -10
  10. package/lib/watchtower-setup.js +230 -0
  11. package/package.json +5 -1
  12. package/templates/cabinet/_cabinet-member-template.md +8 -3
  13. package/templates/cabinet/advisories-state-schema.md +34 -7
  14. package/templates/cabinet/checklist-stats-schema.md +15 -1
  15. package/templates/cabinet/composition-patterns.md +4 -3
  16. package/templates/cabinet/memory-lifecycle-contract.md +135 -0
  17. package/templates/cabinet/pib-db-access.md +13 -0
  18. package/templates/cabinet/skill-output-conventions.md +35 -1
  19. package/templates/cabinet/watchtower-contracts.md +503 -1
  20. package/templates/cabinet/worktree-invocation-contract.md +87 -0
  21. package/templates/engagement/OVERVIEW.md +12 -0
  22. package/templates/engagement/__tests__/engagement.test.mjs +177 -2
  23. package/templates/engagement/__tests__/invoice-doc.test.mjs +195 -0
  24. package/templates/engagement/engagement-preview.mjs +100 -0
  25. package/templates/engagement/engagement-schema.md +240 -0
  26. package/templates/engagement/engagement.mjs +391 -11
  27. package/templates/engagement/pib-db-patches/pib-db-lib.mjs +10 -1
  28. package/templates/engagement/pib-db-patches/pib-db-mcp-server.mjs +5 -44
  29. package/templates/engagement/pib-db-patches/pib-db.mjs +5 -2
  30. package/templates/hooks/action-completion-gate.sh +9 -3
  31. package/templates/hooks/memory-index-guard.sh +17 -11
  32. package/templates/hooks/watchtower-session-start.sh +24 -2
  33. package/templates/mcp/pib-db.json +1 -4
  34. package/templates/mux/__tests__/mux-fail-loud.fixture.sh +44 -0
  35. package/templates/mux/__tests__/station-liveness.fixture.sh +234 -0
  36. package/templates/mux/__tests__/station-liveness.test.mjs +47 -0
  37. package/templates/mux/bin/mux +289 -56
  38. package/templates/mux/config/help.txt +1 -0
  39. package/templates/rules/acknowledge-when-corrected.md +33 -0
  40. package/templates/rules/maintainability.md +11 -0
  41. package/templates/rules/memory-capture.md +26 -4
  42. package/templates/rules/plan-before-bulk-or-irreversible-actions.md +48 -0
  43. package/templates/rules/verify-before-asserting.md +79 -0
  44. package/templates/scripts/__tests__/advisor-pass.test.mjs +238 -0
  45. package/templates/scripts/__tests__/advisories.test.mjs +262 -0
  46. package/templates/scripts/__tests__/ahead-check-origin.test.mjs +355 -0
  47. package/templates/scripts/__tests__/batch-disposition.test.mjs +235 -0
  48. package/templates/scripts/__tests__/claude-churn-authored.test.mjs +90 -0
  49. package/templates/scripts/__tests__/cross-ring-reader.test.mjs +505 -0
  50. package/templates/scripts/__tests__/dx-captures-briefing.test.mjs +169 -0
  51. package/templates/scripts/__tests__/feedback-outbox-flush.test.mjs +232 -0
  52. package/templates/scripts/__tests__/hook-runner.test.mjs +247 -0
  53. package/templates/scripts/__tests__/narrative-corpus.test.mjs +247 -0
  54. package/templates/scripts/__tests__/phase-shim.test.mjs +112 -0
  55. package/templates/scripts/__tests__/qa-handoff-gate.test.mjs +68 -0
  56. package/templates/scripts/__tests__/qa-handoff-merge-state.test.mjs +162 -0
  57. package/templates/scripts/__tests__/resolve-cli.test.mjs +314 -0
  58. package/templates/scripts/__tests__/ring-state-ownership.test.mjs +228 -3
  59. package/templates/scripts/__tests__/ring1-content-detector.test.mjs +168 -0
  60. package/templates/scripts/__tests__/ring1-flagged-actions.test.mjs +135 -0
  61. package/templates/scripts/__tests__/ring1-script-drift.test.mjs +126 -0
  62. package/templates/scripts/__tests__/ring2-queue-reader.test.mjs +80 -0
  63. package/templates/scripts/__tests__/ring2-recall-canary.test.mjs +150 -0
  64. package/templates/scripts/__tests__/ring2-roster-review.test.mjs +311 -0
  65. package/templates/scripts/__tests__/ring2-thread-context.test.mjs +189 -0
  66. package/templates/scripts/__tests__/ring3-chunk-merge.test.mjs +122 -0
  67. package/templates/scripts/__tests__/ring3-close-lenses.test.mjs +359 -0
  68. package/templates/scripts/__tests__/ring3-dedup.test.mjs +467 -0
  69. package/templates/scripts/__tests__/ring3-memory-titles.test.mjs +187 -0
  70. package/templates/scripts/__tests__/ring3-novelty-rescue.test.mjs +148 -0
  71. package/templates/scripts/__tests__/ring3-recent-slice.test.mjs +101 -0
  72. package/templates/scripts/__tests__/ring4-reconcile.test.mjs +393 -0
  73. package/templates/scripts/__tests__/routine-dispatch.test.mjs +312 -0
  74. package/templates/scripts/__tests__/shared-thread-reader.test.mjs +187 -0
  75. package/templates/scripts/__tests__/suppression-ledger.test.mjs +148 -0
  76. package/templates/scripts/__tests__/verify-backfill.test.mjs +103 -0
  77. package/templates/scripts/__tests__/verify-coverage.test.mjs +80 -0
  78. package/templates/scripts/__tests__/watchtower-snapshot.test.mjs +261 -0
  79. package/templates/scripts/__tests__/watchtower-sync.test.mjs +311 -0
  80. package/templates/scripts/load-triage-history.js +5 -2
  81. package/templates/scripts/pib-db-mcp-server.mjs +5 -44
  82. package/templates/scripts/pib-db-path.mjs +61 -0
  83. package/templates/scripts/pib-db.mjs +5 -2
  84. package/templates/scripts/validate-memory.mjs +214 -16
  85. package/templates/scripts/watchtower-advisories.mjs +309 -0
  86. package/templates/scripts/watchtower-build-context.mjs +301 -18
  87. package/templates/scripts/watchtower-cross-ring-reader.mjs +700 -0
  88. package/templates/scripts/watchtower-hook-runner.mjs +422 -0
  89. package/templates/scripts/watchtower-lib.mjs +447 -2
  90. package/templates/scripts/watchtower-narrative-corpus.mjs +385 -0
  91. package/templates/scripts/watchtower-phase-shim.mjs +171 -0
  92. package/templates/scripts/watchtower-queue.mjs +484 -1
  93. package/templates/scripts/watchtower-ring1.mjs +533 -53
  94. package/templates/scripts/watchtower-ring2.mjs +787 -48
  95. package/templates/scripts/watchtower-ring3-close.mjs +1357 -99
  96. package/templates/scripts/watchtower-ring4-runner.sh +85 -0
  97. package/templates/scripts/watchtower-ring4.mjs +753 -0
  98. package/templates/scripts/watchtower-routines.mjs +358 -0
  99. package/templates/scripts/watchtower-snapshot.mjs +452 -0
  100. package/templates/scripts/watchtower-status.sh +1 -1
  101. package/templates/scripts/watchtower-sync.mjs +393 -0
  102. package/templates/skills/audit/SKILL.md +5 -1
  103. package/templates/skills/briefing/SKILL.md +584 -236
  104. package/templates/skills/cabinet-anthropic-insider/SKILL.md +14 -6
  105. package/templates/skills/cabinet-historian/SKILL.md +14 -11
  106. package/templates/skills/cabinet-process-therapist/SKILL.md +28 -0
  107. package/templates/skills/cabinet-security/SKILL.md +11 -0
  108. package/templates/skills/cabinet-system-advocate/SKILL.md +22 -21
  109. package/templates/skills/cabinet-user-advocate/SKILL.md +13 -7
  110. package/templates/skills/catch-up/SKILL.md +113 -0
  111. package/templates/skills/cc-publish/SKILL.md +148 -25
  112. package/templates/skills/cc-remember/SKILL.md +45 -0
  113. package/templates/skills/close/SKILL.md +107 -0
  114. package/templates/skills/collab-client/SKILL.md +22 -5
  115. package/templates/skills/collab-consultant/SKILL.md +110 -2
  116. package/templates/skills/debrief/SKILL.md +148 -16
  117. package/templates/skills/debrief-classic/SKILL.md +696 -0
  118. package/templates/skills/debrief-classic/calibration.md +44 -0
  119. package/templates/skills/debrief-classic/phases/audit-pattern-capture.md +78 -0
  120. package/templates/skills/debrief-classic/phases/auto-maintenance.md +48 -0
  121. package/templates/skills/debrief-classic/phases/checklist-feedback.md +123 -0
  122. package/templates/skills/debrief-classic/phases/close-work.md +163 -0
  123. package/templates/skills/debrief-classic/phases/health-checks.md +54 -0
  124. package/templates/skills/debrief-classic/phases/inventory.md +40 -0
  125. package/templates/skills/debrief-classic/phases/loose-ends.md +52 -0
  126. package/templates/skills/debrief-classic/phases/methodology-capture.md +223 -0
  127. package/templates/skills/debrief-classic/phases/qa-handoff-sweep.md +78 -0
  128. package/templates/skills/debrief-classic/phases/record-lessons.md +177 -0
  129. package/templates/skills/debrief-classic/phases/report.md +59 -0
  130. package/templates/skills/debrief-classic/phases/update-state.md +48 -0
  131. package/templates/skills/debrief-classic/phases/upstream-feedback.md +185 -0
  132. package/templates/skills/debrief-classic/phases/verify-coverage.md +101 -0
  133. package/templates/skills/execute/SKILL.md +77 -6
  134. package/templates/skills/execute/phases/commit-and-deploy.md +8 -0
  135. package/templates/skills/execute-group/SKILL.md +23 -26
  136. package/templates/skills/generate-plan-groups/SKILL.md +20 -0
  137. package/templates/skills/inbox/SKILL.md +124 -7
  138. package/templates/skills/memory/SKILL.md +22 -6
  139. package/templates/skills/orient/SKILL.md +140 -52
  140. package/templates/skills/orient-classic/SKILL.md +770 -0
  141. package/templates/skills/orient-classic/phases/auto-maintenance.md +52 -0
  142. package/templates/skills/orient-classic/phases/briefing.md +53 -0
  143. package/templates/skills/orient-classic/phases/cabinet.md +46 -0
  144. package/templates/skills/orient-classic/phases/checklist-status.md +54 -0
  145. package/templates/skills/orient-classic/phases/context.md +88 -0
  146. package/templates/skills/orient-classic/phases/data-sync.md +35 -0
  147. package/templates/skills/orient-classic/phases/deferred-check.md +55 -0
  148. package/templates/skills/orient-classic/phases/dx-captures.md +53 -0
  149. package/templates/skills/orient-classic/phases/health-checks.md +50 -0
  150. package/templates/skills/orient-classic/phases/verify-backfill.md +109 -0
  151. package/templates/skills/orient-classic/phases/work-scan.md +69 -0
  152. package/templates/skills/plan/SKILL.md +8 -0
  153. package/templates/skills/qa-drain/SKILL.md +157 -0
  154. package/templates/skills/qa-handoff/SKILL.md +287 -46
  155. package/templates/skills/session-handoff/SKILL.md +175 -6
  156. package/templates/skills/setup-accounts/SKILL.md +38 -16
  157. package/templates/skills/threads/SKILL.md +4 -0
  158. package/templates/skills/triage-audit/SKILL.md +6 -0
  159. package/templates/skills/validate/phases/validators.md +41 -0
  160. package/templates/skills/watchtower/SKILL.md +199 -2
  161. package/templates/verify-runtime/CONVENTIONS.md +9 -0
  162. package/templates/verify-runtime/README.md +37 -0
  163. package/templates/watchtower/config.json.template +10 -2
  164. package/templates/watchtower/queue/items/item.json.schema +1 -1
  165. package/templates/workflows/execute-group-complete.js +10 -1
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env node
2
+
3
+ // watchtower-sync.mjs — content-based 3-tier hash-diff + auto-heal for the
4
+ // watchtower script set.
5
+ //
6
+ // The watchtower runtime that launchd/cron actually executes lives at
7
+ // ~/.claude-cabinet/watchtower/. Getting a one-file fix there durably means
8
+ // keeping THREE tiers in step:
9
+ //
10
+ // 1. template — upstream source of truth (`<cc-source>/templates/`).
11
+ // Present only in the CC source repo (or the npm package during install).
12
+ // 2. tracked — the project's committed copies (`<project>/scripts/` +
13
+ // `<project>/.claude/`), read by the one-time `/watchtower install` step.
14
+ // 3. runtime — `~/.claude-cabinet/watchtower/`, what the daemon runs.
15
+ //
16
+ // Before this tool a single fix had to be hand-copied to all three; miss the
17
+ // runtime and the fix is committed but not running, miss the tracked copy and
18
+ // the next install silently reverts it. This module hash-diffs the tiers from
19
+ // their ACTUAL on-disk bytes (never a cached manifest, which can itself go
20
+ // stale) and either reports the divergence (`--check`, read-only) or heals it
21
+ // (`--heal`) by propagating the authoritative tier downward.
22
+ //
23
+ // Authority is by contract, not by mtime: template wins when present, else the
24
+ // tracked copy. The runtime is ALWAYS a heal target, never a source — and the
25
+ // template tier is NEVER written. So healing only ever flows downstream.
26
+ //
27
+ // The managed file SET is DISCOVERED by scanning the tiers (union of what's on
28
+ // disk), so this drift-detector cannot itself accumulate a stale hardcoded
29
+ // list — the one failure mode the source feedback warned about. The only fixed
30
+ // knobs are the category→subdir map and the two non-`watchtower-*` doc names.
31
+
32
+ import {
33
+ readFileSync, writeFileSync, existsSync, readdirSync,
34
+ mkdirSync, renameSync, chmodSync,
35
+ } from 'fs';
36
+ import { join, dirname } from 'path';
37
+ import { homedir } from 'os';
38
+ import { createHash } from 'crypto';
39
+ import { pathToFileURL } from 'url';
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Tier layout — the ONE place the per-tier subdir for each category is encoded.
43
+ // ---------------------------------------------------------------------------
44
+ // - scripts live in scripts/ in every tier
45
+ // - hooks are templates/hooks/ upstream but .claude/hooks/ when tracked
46
+ // - cabinet docs are templates/cabinet/ upstream but .claude/cabinet/ tracked
47
+ // In the runtime they collapse to flat scripts/ hooks/ cabinet/ subdirs.
48
+ const CATEGORY_SUBDIRS = {
49
+ script: { template: 'scripts', tracked: 'scripts', runtime: 'scripts' },
50
+ hook: { template: 'hooks', tracked: '.claude/hooks', runtime: 'hooks' },
51
+ cabinet: { template: 'cabinet', tracked: '.claude/cabinet', runtime: 'cabinet' },
52
+ };
53
+
54
+ const TIERS = ['template', 'tracked', 'runtime'];
55
+
56
+ // Which files in each category are "ours" to keep in sync.
57
+ const HOOK_FILES = ['watchtower-session-start.sh', 'watchtower-session-end.sh'];
58
+ const CABINET_FILES = ['advisories-state-schema.md', 'watchtower-contracts.md'];
59
+
60
+ function isManagedScript(name) {
61
+ return /^watchtower-.*\.(mjs|sh)$/.test(name);
62
+ }
63
+
64
+ // Files that must be executable in the runtime/tracked tiers.
65
+ function isExecutable(category, name) {
66
+ if (category === 'hook') return true;
67
+ if (category === 'script' && name.endsWith('.sh')) return true;
68
+ return false;
69
+ }
70
+
71
+ function tierDir(root, category, tier) {
72
+ return join(root, CATEGORY_SUBDIRS[category][tier]);
73
+ }
74
+
75
+ function hashBytes(buf) {
76
+ return createHash('sha256').update(buf).digest('hex');
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // discoverManaged — union of managed files present across the supplied tiers.
81
+ // ---------------------------------------------------------------------------
82
+ // A file present in ANY tier participates, so a script that exists upstream but
83
+ // is missing downstream (or an orphan that lingers only in the runtime) is
84
+ // surfaced rather than silently ignored.
85
+ export function discoverManaged(roots) {
86
+ const seen = new Map(); // `${category}/${name}` -> { category, name }
87
+ for (const category of ['script', 'hook', 'cabinet']) {
88
+ for (const tier of TIERS) {
89
+ const root = roots[tier];
90
+ if (!root) continue;
91
+ const dir = tierDir(root, category, tier);
92
+ if (!existsSync(dir)) continue;
93
+ let names;
94
+ try { names = readdirSync(dir); } catch { continue; }
95
+ for (const name of names) {
96
+ let ok = false;
97
+ if (category === 'script') ok = isManagedScript(name);
98
+ else if (category === 'hook') ok = HOOK_FILES.includes(name);
99
+ else if (category === 'cabinet') ok = CABINET_FILES.includes(name);
100
+ if (ok) seen.set(`${category}/${name}`, { category, name });
101
+ }
102
+ }
103
+ }
104
+ return [...seen.values()].sort((a, b) =>
105
+ `${a.category}/${a.name}`.localeCompare(`${b.category}/${b.name}`));
106
+ }
107
+
108
+ // template wins when present, else the tracked copy. Runtime is never a source.
109
+ function authoritativeTier(roots) {
110
+ if (roots.template) return 'template';
111
+ if (roots.tracked) return 'tracked';
112
+ return null;
113
+ }
114
+
115
+ function readTier(root, category, tier, name) {
116
+ if (!root) return { tier, present: false, path: null };
117
+ const path = join(tierDir(root, category, tier), name);
118
+ if (!existsSync(path)) return { tier, present: false, path };
119
+ try {
120
+ return { tier, present: true, path, hash: hashBytes(readFileSync(path)) };
121
+ } catch (err) {
122
+ return { tier, present: false, path, error: err.message };
123
+ }
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // analyze — per-file 3-tier classification + heal plan (no writes).
128
+ // ---------------------------------------------------------------------------
129
+ export function analyze(roots) {
130
+ const authTier = authoritativeTier(roots);
131
+ const managed = discoverManaged(roots);
132
+ const files = [];
133
+
134
+ for (const { category, name } of managed) {
135
+ const tiers = {};
136
+ for (const tier of TIERS) {
137
+ if (roots[tier]) tiers[tier] = readTier(roots[tier], category, tier, name);
138
+ }
139
+
140
+ const auth = authTier ? tiers[authTier] : null;
141
+ const healTargets = [];
142
+ let status;
143
+
144
+ if (!authTier) {
145
+ // Only the runtime is present — nothing to verify against.
146
+ status = 'unverifiable';
147
+ } else if (!auth || !auth.present) {
148
+ // File lingers in a downstream tier but is gone from the source.
149
+ status = 'orphan';
150
+ } else {
151
+ for (const tier of TIERS) {
152
+ if (tier === authTier) continue;
153
+ const t = tiers[tier];
154
+ if (!t) continue; // tier root absent — not a target
155
+ if (!t.present) healTargets.push({ tier, reason: 'missing' });
156
+ else if (t.hash !== auth.hash) healTargets.push({ tier, reason: 'drift' });
157
+ }
158
+ if (healTargets.length === 0) status = 'in-sync';
159
+ else if (healTargets.some((h) => h.reason === 'drift')) status = 'drift';
160
+ else status = 'missing';
161
+ }
162
+
163
+ files.push({ category, name, authTier, status, tiers, healTargets });
164
+ }
165
+
166
+ const summary = {
167
+ total: files.length,
168
+ inSync: files.filter((f) => f.status === 'in-sync').length,
169
+ drift: files.filter((f) => f.status === 'drift').length,
170
+ missing: files.filter((f) => f.status === 'missing').length,
171
+ orphan: files.filter((f) => f.status === 'orphan').length,
172
+ unverifiable: files.filter((f) => f.status === 'unverifiable').length,
173
+ };
174
+ summary.clean = summary.drift === 0 && summary.missing === 0
175
+ && summary.orphan === 0 && summary.unverifiable === 0;
176
+
177
+ return { authTier, roots, files, summary };
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // heal — propagate the authoritative tier into drifted/missing targets.
182
+ // ---------------------------------------------------------------------------
183
+ // Atomic (tmp + rename), preserves exec bits, NEVER writes the template tier.
184
+ // Orphans are reported, never auto-deleted (deletion is the riskier direction;
185
+ // a human removes a retired runtime script deliberately).
186
+ export function heal(analysis, opts = {}) {
187
+ const dryRun = !!opts.dryRun;
188
+ const healed = [];
189
+
190
+ for (const f of analysis.files) {
191
+ if (!f.healTargets.length) continue;
192
+ const auth = f.tiers[f.authTier];
193
+ if (!auth || !auth.present) continue; // defensive: nothing to copy
194
+ const content = readFileSync(auth.path);
195
+
196
+ for (const target of f.healTargets) {
197
+ if (target.tier === 'template') continue; // never write upstream
198
+ const root = analysis.roots[target.tier];
199
+ if (!root) continue;
200
+ const destPath = join(tierDir(root, f.category, target.tier), f.name);
201
+ const record = {
202
+ category: f.category, name: f.name, tier: target.tier,
203
+ reason: target.reason, from: f.authTier, path: destPath,
204
+ };
205
+ if (!dryRun) {
206
+ mkdirSync(dirname(destPath), { recursive: true });
207
+ const tmp = destPath + '.tmp';
208
+ writeFileSync(tmp, content);
209
+ renameSync(tmp, destPath);
210
+ if (isExecutable(f.category, f.name)) chmodSync(destPath, 0o755);
211
+ }
212
+ healed.push(record);
213
+ }
214
+ }
215
+
216
+ return { healed, dryRun };
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // resolveRoots — default tier roots from cwd/env, overridable per flag.
221
+ // ---------------------------------------------------------------------------
222
+ // template : <cwd>/templates only when its scripts/ exists (the CC SOURCE repo).
223
+ // A consumer with no local templates is legitimately 2-tier.
224
+ // tracked : an explicit dir, else <cwd> when it looks like a watchtower project
225
+ // (scripts/watchtower-lib.mjs present).
226
+ // runtime : an explicit dir, else $WATCHTOWER_DIR, else ~/.claude-cabinet/watchtower.
227
+ export function resolveRoots(opts = {}) {
228
+ const cwd = opts.cwd || process.cwd();
229
+ const env = opts.env || process.env;
230
+
231
+ let template = opts.template;
232
+ if (template === undefined) {
233
+ const cand = join(cwd, 'templates');
234
+ template = existsSync(join(cand, 'scripts')) ? cand : null;
235
+ }
236
+
237
+ let tracked = opts.tracked;
238
+ if (tracked === undefined) {
239
+ tracked = existsSync(join(cwd, 'scripts', 'watchtower-lib.mjs')) ? cwd : null;
240
+ }
241
+
242
+ let runtime = opts.runtime;
243
+ if (runtime === undefined) {
244
+ runtime = env.WATCHTOWER_DIR
245
+ || join(homedir(), '.claude-cabinet', 'watchtower');
246
+ }
247
+ // A runtime root that doesn't exist on disk is treated as absent so the
248
+ // refresh-only contract holds (nothing to diff against a missing runtime).
249
+ if (runtime && !existsSync(runtime)) runtime = null;
250
+
251
+ return { template: template || null, tracked: tracked || null, runtime: runtime || null };
252
+ }
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // Human-readable report
256
+ // ---------------------------------------------------------------------------
257
+ function tierLabel(tier) {
258
+ return { template: 'template', tracked: 'tracked', runtime: 'runtime' }[tier] || tier;
259
+ }
260
+
261
+ function renderReport(analysis, { heal: healResult } = {}) {
262
+ const lines = [];
263
+ const { roots, authTier, summary } = analysis;
264
+ lines.push('Watchtower script sync — 3-tier hash diff');
265
+ lines.push(` template: ${roots.template || '(absent)'}`);
266
+ lines.push(` tracked: ${roots.tracked || '(absent)'}`);
267
+ lines.push(` runtime: ${roots.runtime || '(absent)'}`);
268
+ lines.push(` authoritative tier: ${authTier || '(none — cannot verify)'}`);
269
+ lines.push('');
270
+
271
+ const problems = analysis.files.filter((f) => f.status !== 'in-sync');
272
+ if (!problems.length) {
273
+ lines.push(` ✓ all ${summary.total} watchtower files in sync across tiers`);
274
+ } else {
275
+ for (const f of problems) {
276
+ if (f.status === 'orphan') {
277
+ const where = TIERS.filter((t) => f.tiers[t]?.present).map(tierLabel).join(', ');
278
+ lines.push(` ⚠ orphan: ${f.name} — absent from ${tierLabel(f.authTier)}, lingers in ${where} (remove by hand if retired)`);
279
+ } else if (f.status === 'unverifiable') {
280
+ lines.push(` ? unverifiable: ${f.name} — no source tier present`);
281
+ } else {
282
+ const targets = f.healTargets
283
+ .map((h) => `${tierLabel(h.tier)} (${h.reason})`).join(', ');
284
+ lines.push(` ✗ ${f.status}: ${f.name} → heal ${targets} from ${tierLabel(f.authTier)}`);
285
+ }
286
+ }
287
+ }
288
+
289
+ lines.push('');
290
+ lines.push(` ${summary.inSync}/${summary.total} in sync · ${summary.drift} drift · ${summary.missing} missing · ${summary.orphan} orphan` +
291
+ (summary.unverifiable ? ` · ${summary.unverifiable} unverifiable` : ''));
292
+
293
+ if (healResult) {
294
+ lines.push('');
295
+ if (!healResult.healed.length) {
296
+ lines.push(' nothing to heal');
297
+ } else {
298
+ lines.push(healResult.dryRun ? ' [dry-run] would heal:' : ' healed:');
299
+ for (const h of healResult.healed) {
300
+ lines.push(` ${h.reason === 'missing' ? '+' : '~'} ${tierLabel(h.tier)}/${h.name} ← ${tierLabel(h.from)}`);
301
+ }
302
+ const trackedHealed = healResult.healed.some((h) => h.tier === 'tracked');
303
+ if (trackedHealed && !healResult.dryRun) {
304
+ lines.push(' (tracked-tier files changed — commit them so the fix is durable)');
305
+ }
306
+ }
307
+ }
308
+
309
+ return lines.join('\n');
310
+ }
311
+
312
+ // ---------------------------------------------------------------------------
313
+ // CLI
314
+ // ---------------------------------------------------------------------------
315
+ function parseArgs(argv) {
316
+ const opts = { mode: 'check', json: false, dryRun: false };
317
+ for (let i = 0; i < argv.length; i++) {
318
+ const a = argv[i];
319
+ if (a === '--check') opts.mode = 'check';
320
+ else if (a === '--heal') opts.mode = 'heal';
321
+ else if (a === '--json') opts.json = true;
322
+ else if (a === '--dry-run') opts.dryRun = true;
323
+ else if (a === '--template') opts.template = argv[++i];
324
+ else if (a === '--tracked') opts.tracked = argv[++i];
325
+ else if (a === '--runtime') opts.runtime = argv[++i];
326
+ else if (a === '--help' || a === '-h') opts.help = true;
327
+ }
328
+ return opts;
329
+ }
330
+
331
+ const HELP = `watchtower-sync — keep the watchtower script tiers in step
332
+
333
+ Usage:
334
+ node watchtower-sync.mjs [--check|--heal] [--json] [--dry-run] [roots]
335
+
336
+ Modes:
337
+ --check (default) read-only: hash-diff the tiers and report divergence
338
+ --heal propagate the authoritative tier into drifted/missing copies
339
+
340
+ Root overrides (else auto-detected from cwd / $WATCHTOWER_DIR):
341
+ --template <dir> upstream templates/ root (CC source repo only)
342
+ --tracked <dir> the project root holding committed scripts/ + .claude/
343
+ --runtime <dir> the live runtime root (~/.claude-cabinet/watchtower)
344
+
345
+ --json emit the structured analysis instead of the text report
346
+ --dry-run with --heal, show what would be written without writing
347
+
348
+ Exit codes: 0 = clean (or healed), 2 = drift/missing/orphan detected (--check).`;
349
+
350
+ export function main(argv) {
351
+ const opts = parseArgs(argv);
352
+ if (opts.help) {
353
+ process.stdout.write(HELP + '\n');
354
+ return 0;
355
+ }
356
+
357
+ const roots = resolveRoots(opts);
358
+ const analysis = analyze(roots);
359
+
360
+ let healResult = null;
361
+ if (opts.mode === 'heal') {
362
+ healResult = heal(analysis, { dryRun: opts.dryRun });
363
+ }
364
+
365
+ if (opts.json) {
366
+ process.stdout.write(JSON.stringify({
367
+ authTier: analysis.authTier,
368
+ roots: analysis.roots,
369
+ summary: analysis.summary,
370
+ files: analysis.files.map((f) => ({
371
+ category: f.category, name: f.name, status: f.status,
372
+ authTier: f.authTier,
373
+ healTargets: f.healTargets,
374
+ tiers: Object.fromEntries(Object.entries(f.tiers).map(([t, v]) =>
375
+ [t, { present: v.present, hash: v.hash || null }])),
376
+ })),
377
+ heal: healResult,
378
+ }, null, 2) + '\n');
379
+ } else {
380
+ process.stdout.write(renderReport(analysis, { heal: healResult }) + '\n');
381
+ }
382
+
383
+ // After a heal, re-evaluate to report the post-heal verdict.
384
+ if (opts.mode === 'heal' && !opts.dryRun) {
385
+ return analyze(roots).summary.clean ? 0 : 2;
386
+ }
387
+ return analysis.summary.clean ? 0 : 2;
388
+ }
389
+
390
+ // Run as CLI when invoked directly (not when imported by tests).
391
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
392
+ process.exit(main(process.argv.slice(2)));
393
+ }
@@ -259,7 +259,11 @@ CLI.
259
259
  the run directory (or `scripts/merge-findings.js <run-dir> --db`) if
260
260
  pib-db is initialized
261
261
  5. Present findings summary: total count, breakdown by severity, by
262
- cabinet member, and highlight any critical findings
262
+ cabinet member, and highlight any critical findings — in plain
263
+ English (see `cabinet/skill-output-conventions.md` §9). The findings
264
+ *records* keep exact file:line locations and evidence; the *summary*
265
+ you speak leads with what matters and what's at stake, not a path
266
+ dump.
263
267
 
264
268
  After persisting, remind the user about triage: findings need human
265
269
  judgment before they drive action. Use `/triage-audit` to review and