create-claude-cabinet 0.46.0 → 0.48.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 (189) hide show
  1. package/README.md +3 -6
  2. package/lib/CLAUDE.md +218 -0
  3. package/lib/cli.js +397 -382
  4. package/lib/copy.js +182 -6
  5. package/lib/db-setup.js +122 -17
  6. package/lib/engagement-server-setup.js +1 -17
  7. package/lib/engagement-setup.js +1 -1
  8. package/lib/installer-gate.js +135 -0
  9. package/lib/metadata.js +51 -2
  10. package/lib/modules.js +292 -0
  11. package/lib/mux-setup.js +1 -17
  12. package/lib/project-context.js +16 -96
  13. package/lib/settings-merge.js +194 -13
  14. package/lib/site-audit-setup.js +23 -7
  15. package/lib/verify-setup.js +20 -9
  16. package/lib/watchtower-setup.js +23 -7
  17. package/package.json +1 -1
  18. package/templates/CLAUDE.md +959 -0
  19. package/templates/briefing/_briefing-template.md +6 -11
  20. package/templates/cabinet/advisories-state-schema.md +1 -1
  21. package/templates/cabinet/checklist-stats-schema.md +15 -1
  22. package/templates/cabinet/eval-protocol.md +27 -10
  23. package/templates/cabinet/memory-lifecycle-contract.md +135 -0
  24. package/templates/cabinet/pib-db-access.md +13 -0
  25. package/templates/cabinet/watchtower-contracts.md +453 -0
  26. package/templates/cabinet/worktree-invocation-contract.md +111 -0
  27. package/templates/engagement/OVERVIEW.md +12 -0
  28. package/templates/engagement/__tests__/checklist-visibility.test.mjs +76 -0
  29. package/templates/engagement/__tests__/engagement.test.mjs +177 -2
  30. package/templates/engagement/__tests__/invoice-doc.test.mjs +195 -0
  31. package/templates/engagement/engagement-checklist.mjs +26 -2
  32. package/templates/engagement/engagement-preview.mjs +100 -0
  33. package/templates/engagement/engagement-schema.md +248 -6
  34. package/templates/engagement/engagement.mjs +391 -11
  35. package/templates/engagement/pib-db-patches/pib-db-lib.mjs +49 -9
  36. package/templates/engagement/pib-db-patches/pib-db-mcp-server.mjs +5 -44
  37. package/templates/engagement/pib-db-patches/pib-db-schema.sql +1 -1
  38. package/templates/engagement/pib-db-patches/pib-db.mjs +9 -3
  39. package/templates/engagement/sql-constants.mjs +9 -1
  40. package/templates/engagement-server/__tests__/cross-tenant-auth.test.mjs +87 -0
  41. package/templates/engagement-server/server.mjs +65 -40
  42. package/templates/hooks/action-completion-gate.sh +19 -3
  43. package/templates/hooks/action-quality-gate.sh +10 -0
  44. package/templates/hooks/memory-index-guard.sh +17 -11
  45. package/templates/hooks/skill-telemetry.sh +10 -5
  46. package/templates/hooks/skill-tool-telemetry.sh +11 -5
  47. package/templates/hooks/watchtower-session-start.sh +24 -2
  48. package/templates/mcp/pib-db.json +1 -4
  49. package/templates/mux/bin/mux +8 -1
  50. package/templates/mux/config/help.txt +1 -0
  51. package/templates/rules/acknowledge-when-corrected.md +33 -0
  52. package/templates/rules/enforcement-pipeline.md +0 -12
  53. package/templates/rules/maintainability.md +11 -0
  54. package/templates/rules/memory-capture.md +26 -4
  55. package/templates/rules/plan-before-bulk-or-irreversible-actions.md +48 -0
  56. package/templates/rules/verify-before-asserting.md +79 -0
  57. package/templates/scripts/__tests__/ahead-check-origin.test.mjs +355 -0
  58. package/templates/scripts/__tests__/api-usage-idle-gate.test.mjs +81 -0
  59. package/templates/scripts/__tests__/batch-disposition.test.mjs +98 -0
  60. package/templates/scripts/__tests__/bsql-loader-verify.test.mjs +47 -0
  61. package/templates/scripts/__tests__/claude-churn-authored.test.mjs +90 -0
  62. package/templates/scripts/__tests__/cross-ring-reader.test.mjs +505 -0
  63. package/templates/scripts/__tests__/dx-captures-briefing.test.mjs +169 -0
  64. package/templates/scripts/__tests__/hook-runner.test.mjs +247 -0
  65. package/templates/scripts/__tests__/inbox-assessment.test.mjs +341 -0
  66. package/templates/scripts/__tests__/memory-reachability.test.mjs +150 -0
  67. package/templates/scripts/__tests__/narrative-corpus.test.mjs +247 -0
  68. package/templates/scripts/__tests__/phase-shim.test.mjs +112 -0
  69. package/templates/scripts/__tests__/qa-handoff-merge-state.test.mjs +162 -0
  70. package/templates/scripts/__tests__/resolve-cli.test.mjs +314 -0
  71. package/templates/scripts/__tests__/ring-state-ownership.test.mjs +120 -0
  72. package/templates/scripts/__tests__/ring1-content-detector.test.mjs +168 -0
  73. package/templates/scripts/__tests__/ring1-flagged-actions.test.mjs +135 -0
  74. package/templates/scripts/__tests__/ring1-pib-error-surfacing.test.mjs +69 -0
  75. package/templates/scripts/__tests__/ring1-script-drift.test.mjs +126 -0
  76. package/templates/scripts/__tests__/ring1-stale-open-counts.test.mjs +143 -0
  77. package/templates/scripts/__tests__/ring2-queue-reader.test.mjs +80 -0
  78. package/templates/scripts/__tests__/ring2-recall-canary.test.mjs +150 -0
  79. package/templates/scripts/__tests__/ring2-roster-review.test.mjs +311 -0
  80. package/templates/scripts/__tests__/ring3-chunk-merge.test.mjs +122 -0
  81. package/templates/scripts/__tests__/ring3-close-lenses.test.mjs +359 -0
  82. package/templates/scripts/__tests__/ring3-dedup.test.mjs +92 -12
  83. package/templates/scripts/__tests__/ring3-memory-titles.test.mjs +187 -0
  84. package/templates/scripts/__tests__/ring3-novelty-rescue.test.mjs +148 -0
  85. package/templates/scripts/__tests__/ring3-recent-slice.test.mjs +101 -0
  86. package/templates/scripts/__tests__/ring3-thread-capture.test.mjs +213 -0
  87. package/templates/scripts/__tests__/ring4-reconcile.test.mjs +393 -0
  88. package/templates/scripts/__tests__/shared-thread-reader.test.mjs +187 -0
  89. package/templates/scripts/__tests__/suppression-ledger.test.mjs +148 -0
  90. package/templates/scripts/__tests__/verify-backfill.test.mjs +103 -0
  91. package/templates/scripts/__tests__/verify-coverage.test.mjs +80 -0
  92. package/templates/scripts/__tests__/watchtower-snapshot.test.mjs +261 -0
  93. package/templates/scripts/__tests__/watchtower-status-ring4.test.mjs +89 -0
  94. package/templates/scripts/__tests__/watchtower-sync.test.mjs +311 -0
  95. package/templates/scripts/audit-coherence-check.mjs +123 -0
  96. package/templates/scripts/audit-synth.mjs +117 -0
  97. package/templates/scripts/load-triage-history.js +5 -2
  98. package/templates/scripts/merge-findings.js +4 -1
  99. package/templates/scripts/patterns-scope-check.mjs +122 -0
  100. package/templates/scripts/pib-db-lib.mjs +61 -11
  101. package/templates/scripts/pib-db-mcp-server.mjs +7 -45
  102. package/templates/scripts/pib-db-path.mjs +61 -0
  103. package/templates/scripts/pib-db.mjs +7 -3
  104. package/templates/scripts/review-server.mjs +7 -12
  105. package/templates/scripts/triage-server.mjs +7 -8
  106. package/templates/scripts/validate-memory.mjs +214 -16
  107. package/templates/scripts/watchtower-advisories.mjs +7 -3
  108. package/templates/scripts/watchtower-build-context.mjs +192 -8
  109. package/templates/scripts/watchtower-cross-ring-reader.mjs +714 -0
  110. package/templates/scripts/watchtower-hook-runner.mjs +422 -0
  111. package/templates/scripts/watchtower-inbox-assessment.mjs +706 -0
  112. package/templates/scripts/watchtower-lib.mjs +555 -8
  113. package/templates/scripts/watchtower-narrative-corpus.mjs +385 -0
  114. package/templates/scripts/watchtower-phase-shim.mjs +171 -0
  115. package/templates/scripts/watchtower-queue.mjs +341 -3
  116. package/templates/scripts/watchtower-ring1.mjs +481 -81
  117. package/templates/scripts/watchtower-ring2.mjs +748 -80
  118. package/templates/scripts/watchtower-ring3-close.mjs +1147 -167
  119. package/templates/scripts/watchtower-ring4-runner.sh +85 -0
  120. package/templates/scripts/watchtower-ring4.mjs +753 -0
  121. package/templates/scripts/watchtower-routines.mjs +1 -1
  122. package/templates/scripts/watchtower-snapshot.mjs +452 -0
  123. package/templates/scripts/watchtower-status.sh +17 -0
  124. package/templates/scripts/watchtower-sync.mjs +393 -0
  125. package/templates/scripts/work-tracker-server.mjs +26 -22
  126. package/templates/skills/audit/SKILL.md +18 -5
  127. package/templates/skills/audit/phases/structural-checks.md +22 -0
  128. package/templates/skills/briefing/SKILL.md +289 -41
  129. package/templates/skills/cabinet-accessibility/SKILL.md +1 -1
  130. package/templates/skills/cabinet-process-therapist/SKILL.md +28 -0
  131. package/templates/skills/cabinet-security/SKILL.md +11 -0
  132. package/templates/skills/catch-up/SKILL.md +113 -0
  133. package/templates/skills/cc-publish/SKILL.md +89 -18
  134. package/templates/skills/cc-remember/SKILL.md +45 -0
  135. package/templates/skills/close/SKILL.md +107 -0
  136. package/templates/skills/collab-client/SKILL.md +22 -5
  137. package/templates/skills/collab-consultant/SKILL.md +110 -2
  138. package/templates/skills/debrief/SKILL.md +21 -4
  139. package/templates/skills/debrief/phases/audit-pattern-capture.md +13 -0
  140. package/templates/skills/debrief-classic/SKILL.md +696 -0
  141. package/templates/skills/debrief-classic/calibration.md +44 -0
  142. package/templates/skills/debrief-classic/phases/audit-pattern-capture.md +78 -0
  143. package/templates/skills/debrief-classic/phases/auto-maintenance.md +48 -0
  144. package/templates/skills/debrief-classic/phases/checklist-feedback.md +123 -0
  145. package/templates/skills/debrief-classic/phases/close-work.md +163 -0
  146. package/templates/skills/debrief-classic/phases/health-checks.md +54 -0
  147. package/templates/skills/debrief-classic/phases/inventory.md +40 -0
  148. package/templates/skills/debrief-classic/phases/loose-ends.md +52 -0
  149. package/templates/skills/debrief-classic/phases/methodology-capture.md +223 -0
  150. package/templates/skills/debrief-classic/phases/qa-handoff-sweep.md +78 -0
  151. package/templates/skills/debrief-classic/phases/record-lessons.md +177 -0
  152. package/templates/skills/debrief-classic/phases/report.md +59 -0
  153. package/templates/skills/debrief-classic/phases/update-state.md +48 -0
  154. package/templates/skills/debrief-classic/phases/upstream-feedback.md +185 -0
  155. package/templates/skills/debrief-classic/phases/verify-coverage.md +101 -0
  156. package/templates/skills/execute/SKILL.md +71 -6
  157. package/templates/skills/execute/phases/commit-and-deploy.md +8 -0
  158. package/templates/skills/execute-group/SKILL.md +23 -26
  159. package/templates/skills/generate-plan-groups/SKILL.md +20 -0
  160. package/templates/skills/inbox/SKILL.md +177 -31
  161. package/templates/skills/memory/SKILL.md +22 -6
  162. package/templates/skills/orient/SKILL.md +75 -5
  163. package/templates/skills/orient-classic/SKILL.md +770 -0
  164. package/templates/skills/orient-classic/phases/auto-maintenance.md +52 -0
  165. package/templates/skills/orient-classic/phases/briefing.md +53 -0
  166. package/templates/skills/orient-classic/phases/cabinet.md +46 -0
  167. package/templates/skills/orient-classic/phases/checklist-status.md +54 -0
  168. package/templates/skills/orient-classic/phases/context.md +88 -0
  169. package/templates/skills/orient-classic/phases/data-sync.md +35 -0
  170. package/templates/skills/orient-classic/phases/deferred-check.md +55 -0
  171. package/templates/skills/orient-classic/phases/dx-captures.md +53 -0
  172. package/templates/skills/orient-classic/phases/health-checks.md +50 -0
  173. package/templates/skills/orient-classic/phases/verify-backfill.md +109 -0
  174. package/templates/skills/orient-classic/phases/work-scan.md +69 -0
  175. package/templates/skills/pulse/SKILL.md +10 -8
  176. package/templates/skills/qa-drain/SKILL.md +38 -0
  177. package/templates/skills/qa-handoff/SKILL.md +287 -46
  178. package/templates/skills/setup-accounts/SKILL.md +38 -16
  179. package/templates/skills/spring-clean/SKILL.md +1 -1
  180. package/templates/skills/spring-clean/phases/execute-decisions.md +1 -1
  181. package/templates/skills/threads/SKILL.md +4 -0
  182. package/templates/skills/validate/phases/validators.md +127 -0
  183. package/templates/skills/watchtower/SKILL.md +174 -30
  184. package/templates/verify-runtime/CONVENTIONS.md +9 -0
  185. package/templates/verify-runtime/README.md +37 -0
  186. package/templates/watchtower/config.json.template +8 -2
  187. package/templates/workflows/deliberative-audit.js +258 -87
  188. package/templates/workflows/execute-group-complete.js +26 -3
  189. package/templates/workflows/execute-group-implement.js +16 -2
@@ -8,7 +8,7 @@ const DEFAULT_HOOKS = {
8
8
  hooks: [
9
9
  {
10
10
  type: 'command',
11
- command: '.claude/hooks/git-guardrails.sh',
11
+ command: '$CLAUDE_PROJECT_DIR/.claude/hooks/git-guardrails.sh',
12
12
  },
13
13
  ],
14
14
  },
@@ -17,7 +17,7 @@ const DEFAULT_HOOKS = {
17
17
  hooks: [
18
18
  {
19
19
  type: 'command',
20
- command: '.claude/hooks/work-tracker-guard.sh',
20
+ command: '$CLAUDE_PROJECT_DIR/.claude/hooks/work-tracker-guard.sh',
21
21
  },
22
22
  ],
23
23
  },
@@ -26,25 +26,30 @@ const DEFAULT_HOOKS = {
26
26
  hooks: [
27
27
  {
28
28
  type: 'command',
29
- command: '.claude/hooks/cc-upstream-guard.sh',
29
+ command: '$CLAUDE_PROJECT_DIR/.claude/hooks/cc-upstream-guard.sh',
30
30
  },
31
31
  ],
32
32
  },
33
33
  {
34
- matcher: 'pib_create_action',
34
+ // pib tools arrive as `mcp__<server>__<tool>`, so the bare
35
+ // `pib_create_action` spelling never matched and this gate was silently
36
+ // dead since it shipped (audit cabinet-anthropic-insider-0001, run
37
+ // 09-20-25 — proven live: all 14 audit actions were created with no AC
38
+ // check). Match the real MCP tool name.
39
+ matcher: 'mcp__pib-db__pib_create_action',
35
40
  hooks: [
36
41
  {
37
42
  type: 'command',
38
- command: '.claude/hooks/action-quality-gate.sh',
43
+ command: '$CLAUDE_PROJECT_DIR/.claude/hooks/action-quality-gate.sh',
39
44
  },
40
45
  ],
41
46
  },
42
47
  {
43
- matcher: 'pib_complete_action',
48
+ matcher: 'mcp__pib-db__pib_complete_action',
44
49
  hooks: [
45
50
  {
46
51
  type: 'command',
47
- command: '.claude/hooks/action-completion-gate.sh',
52
+ command: '$CLAUDE_PROJECT_DIR/.claude/hooks/action-completion-gate.sh',
48
53
  },
49
54
  ],
50
55
  },
@@ -55,7 +60,7 @@ const DEFAULT_HOOKS = {
55
60
  hooks: [
56
61
  {
57
62
  type: 'command',
58
- command: '.claude/hooks/skill-telemetry.sh',
63
+ command: '$CLAUDE_PROJECT_DIR/.claude/hooks/skill-telemetry.sh',
59
64
  },
60
65
  ],
61
66
  },
@@ -66,7 +71,7 @@ const DEFAULT_HOOKS = {
66
71
  hooks: [
67
72
  {
68
73
  type: 'command',
69
- command: '.claude/hooks/skill-tool-telemetry.sh',
74
+ command: '$CLAUDE_PROJECT_DIR/.claude/hooks/skill-tool-telemetry.sh',
70
75
  },
71
76
  ],
72
77
  },
@@ -75,7 +80,7 @@ const DEFAULT_HOOKS = {
75
80
  hooks: [
76
81
  {
77
82
  type: 'command',
78
- command: '.claude/hooks/memory-index-guard.sh',
83
+ command: '$CLAUDE_PROJECT_DIR/.claude/hooks/memory-index-guard.sh',
79
84
  },
80
85
  ],
81
86
  },
@@ -133,7 +138,29 @@ const BASH_COMPRESS_HOOKS = {
133
138
  hooks: [
134
139
  {
135
140
  type: 'command',
136
- command: '.claude/hooks/bash-output-compress.sh',
141
+ command: '$CLAUDE_PROJECT_DIR/.claude/hooks/bash-output-compress.sh',
142
+ },
143
+ ],
144
+ },
145
+ ],
146
+ };
147
+
148
+ // Maintainer-only ("repo-local") dev hooks — registered by mergeCcDevHooks()
149
+ // ONLY when installing into the Claude Cabinet SOURCE repo (package.json
150
+ // name === 'create-claude-cabinet'), never in a consumer install. node-check
151
+ // mechanizes the CLAUDE.md "node -c after CLI edits" rule (architecture-0004).
152
+ // The hook script is a tracked maintainer tool at scripts/node-check-hook.sh —
153
+ // NOT a MODULES template, so it never ships to consumers. Registered durably
154
+ // (re-added on every dogfood reinstall) rather than hand-added to the gitignored
155
+ // settings.json, which the installer would silently regenerate away.
156
+ const CC_DEV_HOOKS = {
157
+ PostToolUse: [
158
+ {
159
+ matcher: 'Edit|Write',
160
+ hooks: [
161
+ {
162
+ type: 'command',
163
+ command: '$CLAUDE_PROJECT_DIR/scripts/node-check-hook.sh',
137
164
  },
138
165
  ],
139
166
  },
@@ -156,6 +183,38 @@ const LEGACY_HOOK_COMMANDS = [
156
183
  'domain-memories.sh',
157
184
  ];
158
185
 
186
+ // CC-owned hook commands ship with a `$CLAUDE_PROJECT_DIR/` prefix so they
187
+ // resolve regardless of the hook's working directory. Older installs wrote them
188
+ // bare-relative (`.claude/hooks/X.sh`), which break ("No such file or
189
+ // directory") whenever a hook fires from a non-root cwd. bareForm() derives that
190
+ // legacy spelling so a reinstall can REPLACE it (drop old, add prefixed) rather
191
+ // than leave a broken duplicate.
192
+ function bareForm(command) {
193
+ return (command || '').replace(/^\$CLAUDE_PROJECT_DIR\//, '');
194
+ }
195
+
196
+ // Every CC-owned hook command in a hook set, in its current spelling.
197
+ function ccHookCommands(hookSet) {
198
+ return Object.values(hookSet)
199
+ .flatMap(entries => entries.flatMap(e => (e.hooks || []).map(h => h.command)));
200
+ }
201
+
202
+ // Remove every sub-hook whose command exactly matches one of `commands`, and
203
+ // prune entries left empty. Exact-match on CC-owned command strings only — a
204
+ // consumer's own hooks (different paths) are never touched.
205
+ function dropHookCommands(settings, commands) {
206
+ const drop = new Set(commands.filter(Boolean));
207
+ for (const [event, entries] of Object.entries(settings.hooks || {})) {
208
+ if (!Array.isArray(entries)) continue;
209
+ settings.hooks[event] = entries
210
+ .map(entry => (entry.hooks && Array.isArray(entry.hooks))
211
+ ? { ...entry, hooks: entry.hooks.filter(h => !drop.has(h.command)) }
212
+ : entry)
213
+ .filter(entry => !entry.hooks || !Array.isArray(entry.hooks) || entry.hooks.length > 0);
214
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
215
+ }
216
+ }
217
+
159
218
  /**
160
219
  * Merge PIB hooks into the project's .claude/settings.json.
161
220
  * Creates the file if it doesn't exist. Preserves existing hooks.
@@ -199,6 +258,21 @@ function mergeSettings(projectDir, { includeDb = true } = {}) {
199
258
  if (!settings.hooks[event]) continue;
200
259
  }
201
260
 
261
+ // Retire superseded CC hook entries so the DEFAULT_HOOKS forms below REPLACE
262
+ // them rather than duplicate — covering TWO kinds of drift:
263
+ // (a) a changed command spelling (bare-relative `.claude/hooks/X.sh` →
264
+ // `$CLAUDE_PROJECT_DIR/.claude/hooks/X.sh`, which resolves from any cwd);
265
+ // (b) a changed MATCHER on an unchanged command (the bare `pib_create_action`
266
+ // matcher → `mcp__pib-db__pib_create_action`). The merge loop below
267
+ // dedups by COMMAND alone, so a stale-matcher entry would otherwise
268
+ // survive untouched while the corrected entry was skipped as a dup —
269
+ // exactly how the pib gate hooks stayed dead across reinstalls.
270
+ // Dropping every CURRENT-spelling CC command (and its bare form) forces a
271
+ // clean re-add with the correct current matcher. Exact-match on CC-owned
272
+ // commands only — consumer-authored hooks are left intact.
273
+ const ccCommands = ccHookCommands(DEFAULT_HOOKS);
274
+ dropHookCommands(settings, [...ccCommands, ...ccCommands.map(bareForm)]);
275
+
202
276
  // Merge each hook event type from DEFAULT_HOOKS
203
277
  for (const [event, newHooks] of Object.entries(DEFAULT_HOOKS)) {
204
278
  if (!settings.hooks[event]) {
@@ -254,7 +328,7 @@ function healUserSettings() {
254
328
  const filtered = entries.filter(entry => {
255
329
  if (!entry.hooks || !Array.isArray(entry.hooks)) return true;
256
330
  const hasRelativeCcHook = entry.hooks.some(h =>
257
- /^\.claude\/hooks\//.test(h.command || '')
331
+ /^(\$CLAUDE_PROJECT_DIR\/)?\.claude\/hooks\//.test(h.command || '')
258
332
  );
259
333
  if (hasRelativeCcHook) removed++;
260
334
  return !hasRelativeCcHook;
@@ -338,6 +412,9 @@ function mergeBashCompressHooks(settingsPath) {
338
412
  const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
339
413
  if (!settings.hooks) settings.hooks = {};
340
414
 
415
+ // Replace any superseded bare-relative form with the prefixed one (see mergeSettings).
416
+ dropHookCommands(settings, ccHookCommands(BASH_COMPRESS_HOOKS).map(bareForm));
417
+
341
418
  for (const [event, newHooks] of Object.entries(BASH_COMPRESS_HOOKS)) {
342
419
  if (!settings.hooks[event]) {
343
420
  settings.hooks[event] = newHooks;
@@ -358,4 +435,108 @@ function mergeBashCompressHooks(settingsPath) {
358
435
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
359
436
  }
360
437
 
361
- module.exports = { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks, DEFAULT_HOOKS, WATCHTOWER_HOOKS, MUX_HOOKS, BASH_COMPRESS_HOOKS, LEGACY_HOOK_COMMANDS };
438
+ /**
439
+ * Register maintainer-only ("repo-local") dev hooks — but ONLY when installing
440
+ * into the Claude Cabinet SOURCE repo itself (target package.json
441
+ * name === 'create-claude-cabinet'). A silent no-op in every consumer project.
442
+ * Idempotent + reinstall-safe: drops the CC-owned dev commands (current + bare
443
+ * spellings) then re-adds them, so a reinstall REPLACES rather than duplicates.
444
+ * Returns true if it registered, false if it no-op'd (not the CC repo).
445
+ */
446
+ function mergeCcDevHooks(projectDir) {
447
+ let pkgName = '';
448
+ try {
449
+ pkgName =
450
+ JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf8'))
451
+ .name || '';
452
+ } catch {
453
+ /* no/invalid package.json → not the CC repo → no-op */
454
+ }
455
+ if (pkgName !== 'create-claude-cabinet') return false;
456
+
457
+ const settingsDir = path.join(projectDir, '.claude');
458
+ const settingsPath = path.join(settingsDir, 'settings.json');
459
+ if (!fs.existsSync(settingsDir)) fs.mkdirSync(settingsDir, { recursive: true });
460
+
461
+ let settings = {};
462
+ if (fs.existsSync(settingsPath)) {
463
+ try {
464
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
465
+ } catch {
466
+ settings = {};
467
+ }
468
+ }
469
+ if (typeof settings !== 'object' || settings === null || Array.isArray(settings)) {
470
+ settings = {};
471
+ }
472
+ if (!settings.hooks) settings.hooks = {};
473
+
474
+ // Drop current + bare spellings so a reinstall replaces, not duplicates.
475
+ const devCommands = ccHookCommands(CC_DEV_HOOKS);
476
+ dropHookCommands(settings, [...devCommands, ...devCommands.map(bareForm)]);
477
+
478
+ for (const [event, newHooks] of Object.entries(CC_DEV_HOOKS)) {
479
+ if (!settings.hooks[event]) settings.hooks[event] = [];
480
+ for (const newHook of newHooks) settings.hooks[event].push(newHook);
481
+ }
482
+
483
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
484
+ return true;
485
+ }
486
+
487
+ /**
488
+ * Set `skillOverrides: "name-only"` for the given skills in .claude/settings.json
489
+ * (act:bee0fa16, cabinet-anthropic-insider-0002). Claude Code loads every skill's
490
+ * name+description into context so the model can auto-invoke by trigger keyword,
491
+ * under a budget of ~1% of the context window; when it overflows, the
492
+ * least-invoked skills' descriptions are dropped first — silently stripping the
493
+ * keywords that make them auto-invocable. cabinet-* skills are ~half that
494
+ * listing and are spawned by name (agentType), never auto-invoked by
495
+ * description, so "name-only" costs them nothing while freeing budget for the
496
+ * routing skills (orient/debrief/briefing/inbox/…) that DO need full keywords.
497
+ *
498
+ * Only-if-absent: an existing skillOverrides entry (a user who deliberately set
499
+ * a cabinet skill back to "on") is never clobbered. Idempotent — a reinstall
500
+ * re-applies only for skills with no entry yet (e.g. a newly-added cabinet
501
+ * member). Creates .claude/settings.json if absent. Returns the count added.
502
+ */
503
+ function mergeSkillOverrides(projectDir, skillNames = []) {
504
+ if (!Array.isArray(skillNames) || skillNames.length === 0) return 0;
505
+ const settingsDir = path.join(projectDir, '.claude');
506
+ const settingsPath = path.join(settingsDir, 'settings.json');
507
+ if (!fs.existsSync(settingsDir)) fs.mkdirSync(settingsDir, { recursive: true });
508
+
509
+ let settings = {};
510
+ if (fs.existsSync(settingsPath)) {
511
+ try {
512
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
513
+ } catch {
514
+ settings = {};
515
+ }
516
+ }
517
+ if (typeof settings !== 'object' || settings === null || Array.isArray(settings)) {
518
+ settings = {};
519
+ }
520
+ if (
521
+ !settings.skillOverrides ||
522
+ typeof settings.skillOverrides !== 'object' ||
523
+ Array.isArray(settings.skillOverrides)
524
+ ) {
525
+ settings.skillOverrides = {};
526
+ }
527
+
528
+ let added = 0;
529
+ for (const name of skillNames) {
530
+ if (!name) continue;
531
+ if (!(name in settings.skillOverrides)) {
532
+ settings.skillOverrides[name] = 'name-only';
533
+ added++;
534
+ }
535
+ }
536
+ if (added > 0) {
537
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
538
+ }
539
+ return added;
540
+ }
541
+
542
+ module.exports = { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks, mergeCcDevHooks, mergeSkillOverrides, DEFAULT_HOOKS, WATCHTOWER_HOOKS, MUX_HOOKS, BASH_COMPRESS_HOOKS, CC_DEV_HOOKS, LEGACY_HOOK_COMMANDS };
@@ -10,12 +10,17 @@ const { execSync } = require('child_process');
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
  const os = require('os');
13
+ const { tarballIsStale, writeSourceHash } = require('./installer-gate');
13
14
 
14
15
  const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
15
16
  const SITE_AUDIT_BASE = path.join(CC_HOME, 'site-audit');
16
17
 
17
18
  function setupSiteAuditRuntime(opts = {}) {
18
19
  const dryRun = !!opts.dryRun;
20
+ const exec = opts.exec || execSync; // injectable for tests
21
+ // The heavy extract+install step (npm install, puppeteer, chromedriver) is
22
+ // injectable so the pack/skip/re-pack gate can be exercised in tests.
23
+ const installTarball = opts.installTarball || extractAndInstall;
19
24
  const runtimeSourceDir =
20
25
  opts.runtimeSourceDir || path.resolve(__dirname, '..', 'templates', 'site-audit-runtime');
21
26
 
@@ -43,20 +48,28 @@ function setupSiteAuditRuntime(opts = {}) {
43
48
  return { installPath: installDir, version, status: 'dry-run', results };
44
49
  }
45
50
 
46
- if (fs.existsSync(tarballPath) && fs.statSync(tarballPath).size > 1024) {
47
- // Tarball exists ensure it's extracted and installed (may have been
48
- // packed but not installed on a prior run)
49
- extractAndInstall(path.join(SITE_AUDIT_BASE, version), tarballPath, results);
51
+ // Idempotency + equal-version fall-through: reuse the tarball ONLY when the
52
+ // runtime SOURCE content is unchanged (hash sidecar matches). A same-version
53
+ // reinstall carrying a template edit re-packs instead of hard-skipping (the
54
+ // tarball parallel to act:df1a1dc3; technical-debt-0003).
55
+ const staleness = tarballIsStale({ tarballPath, installDir, runtimeSourceDir });
56
+ if (!staleness.stale) {
57
+ // Tarball exists and source is unchanged — ensure it's extracted and
58
+ // installed (may have been packed but not installed on a prior run)
59
+ installTarball(path.join(SITE_AUDIT_BASE, version), tarballPath, results);
50
60
  updateCurrentBinSymlink(SITE_AUDIT_BASE, version);
51
61
  writeVersionPointer(versionPointer, version);
52
- results.push(`@claude-cabinet/site-audit@${version} already installed (${tarballPath})`);
62
+ results.push(`@claude-cabinet/site-audit@${version} already installed, source unchanged (${tarballPath})`);
53
63
  return { installPath: installDir, version, status: 'skipped', results };
54
64
  }
65
+ if (staleness.reason === 'source-changed' || staleness.reason === 'no-source-hash') {
66
+ results.push(`@claude-cabinet/site-audit@${version} runtime source changed — re-packing`);
67
+ }
55
68
 
56
69
  if (fs.existsSync(tarballPath)) fs.unlinkSync(tarballPath);
57
70
 
58
71
  fs.mkdirSync(installDir, { recursive: true });
59
- const packStdout = execSync(`npm pack --silent --pack-destination "${installDir}"`, {
72
+ const packStdout = exec(`npm pack --silent --pack-destination "${installDir}"`, {
60
73
  cwd: runtimeSourceDir,
61
74
  encoding: 'utf8',
62
75
  }).trim();
@@ -75,8 +88,11 @@ function setupSiteAuditRuntime(opts = {}) {
75
88
  }
76
89
 
77
90
  // Extract and install so the runtime is runnable (not just a tarball)
78
- extractAndInstall(path.join(SITE_AUDIT_BASE, version), tarballPath, results);
91
+ installTarball(path.join(SITE_AUDIT_BASE, version), tarballPath, results);
79
92
 
93
+ // Record the runtime-source hash so the next same-version reinstall can
94
+ // skip when unchanged and re-pack when it changed.
95
+ writeSourceHash(installDir, staleness.sourceHash);
80
96
  writeVersionPointer(versionPointer, version);
81
97
  updateCurrentBinSymlink(SITE_AUDIT_BASE, version);
82
98
  results.push(`Installed @claude-cabinet/site-audit@${version}`);
@@ -15,6 +15,7 @@ const { execSync } = require('child_process');
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
  const os = require('os');
18
+ const { tarballIsStale, writeSourceHash } = require('./installer-gate');
18
19
 
19
20
  const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
20
21
  const VERIFY_BASE = path.join(CC_HOME, 'verify');
@@ -40,6 +41,7 @@ const VERIFY_BASE = path.join(CC_HOME, 'verify');
40
41
  */
41
42
  function setupVerifyRuntime(opts = {}) {
42
43
  const dryRun = !!opts.dryRun;
44
+ const exec = opts.exec || execSync; // injectable for tests
43
45
  const runtimeSourceDir =
44
46
  opts.runtimeSourceDir || path.resolve(__dirname, '..', 'templates', 'verify-runtime');
45
47
 
@@ -72,21 +74,27 @@ function setupVerifyRuntime(opts = {}) {
72
74
  return { installPath: installDir, version, status: 'dry-run', results };
73
75
  }
74
76
 
75
- // 3. Idempotency check. Existence alone is not enough — an earlier
76
- // stub-touch (e.g., a test fixture) can leave a 0-byte file at the
77
- // tarball path. npm install on the consumer side then fails with
78
- // ENODATA / TAR_BAD_ARCHIVE. Validate the file has non-trivial size
79
- // before treating it as installed.
80
- if (fs.existsSync(tarballPath) && fs.statSync(tarballPath).size > 1024) {
81
- results.push(`cabinet-verify@${version} already installed (${tarballPath})`);
77
+ // 3. Idempotency + equal-version fall-through. A present, non-truncated
78
+ // tarball is reused ONLY when the runtime SOURCE content is unchanged (its
79
+ // hash sidecar still matches). A same-version reinstall that carries a
80
+ // template edit re-packs instead of hard-skipping the tarball parallel
81
+ // to the per-file-manifest fix (act:df1a1dc3; technical-debt-0003). A
82
+ // truncated stub (earlier fixture touch → ENODATA/TAR_BAD_ARCHIVE on the
83
+ // consumer) also counts as stale.
84
+ const staleness = tarballIsStale({ tarballPath, installDir, runtimeSourceDir });
85
+ if (!staleness.stale) {
86
+ results.push(`cabinet-verify@${version} already installed, source unchanged (${tarballPath})`);
82
87
  // Even on skip, ensure the VERSION pointer is up-to-date so a later
83
88
  // version downgrade doesn't leave a stale pointer behind.
84
89
  writeVersionPointer(versionPointer, version);
85
90
  results.push(`Updated VERSION pointer: ${versionPointer}`);
86
91
  return { installPath: installDir, version, status: 'skipped', results };
87
92
  }
93
+ if (staleness.reason === 'source-changed' || staleness.reason === 'no-source-hash') {
94
+ results.push(`cabinet-verify@${version} runtime source changed — re-packing`);
95
+ }
88
96
 
89
- // Remove a 0-byte stub before re-packing so the move below doesn't
97
+ // Remove a stale/stub tarball before re-packing so the move below doesn't
90
98
  // hit a "file exists" surprise.
91
99
  if (fs.existsSync(tarballPath)) {
92
100
  fs.unlinkSync(tarballPath);
@@ -96,7 +104,7 @@ function setupVerifyRuntime(opts = {}) {
96
104
  fs.mkdirSync(installDir, { recursive: true });
97
105
  // Run npm pack in the runtime source dir. --pack-destination writes
98
106
  // the tarball directly to installDir without an intermediate cwd move.
99
- const packStdout = execSync(`npm pack --silent --pack-destination "${installDir}"`, {
107
+ const packStdout = exec(`npm pack --silent --pack-destination "${installDir}"`, {
100
108
  cwd: runtimeSourceDir,
101
109
  encoding: 'utf8',
102
110
  }).trim();
@@ -122,6 +130,9 @@ function setupVerifyRuntime(opts = {}) {
122
130
  );
123
131
  }
124
132
 
133
+ // Record the runtime-source hash so the next same-version reinstall can
134
+ // skip when the source is unchanged, and re-pack when it changed.
135
+ writeSourceHash(installDir, staleness.sourceHash);
125
136
  results.push(`Installed cabinet-verify@${version} to ${tarballPath}`);
126
137
 
127
138
  // 5. Write VERSION pointer
@@ -34,7 +34,7 @@
34
34
  const fs = require('fs');
35
35
  const path = require('path');
36
36
  const os = require('os');
37
- const crypto = require('crypto');
37
+ const { sha256 } = require('./installer-gate');
38
38
 
39
39
  const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
40
40
  const GLOBAL_MANIFEST_PATH = path.join(CC_HOME, 'global-manifest.json');
@@ -76,6 +76,7 @@ function buildManagedFiles() {
76
76
  const shellRunners = [
77
77
  'watchtower-ring1-runner.sh',
78
78
  'watchtower-ring2-runner.sh',
79
+ 'watchtower-ring4-runner.sh',
79
80
  'watchtower-status.sh',
80
81
  ];
81
82
  for (const name of shellRunners) {
@@ -110,10 +111,6 @@ function buildManagedFiles() {
110
111
 
111
112
  const MANAGED_FILES = buildManagedFiles();
112
113
 
113
- function sha256(content) {
114
- return crypto.createHash('sha256').update(content).digest('hex');
115
- }
116
-
117
114
  function readGlobalManifest() {
118
115
  if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
119
116
  try {
@@ -168,8 +165,27 @@ function refreshWatchtowerRuntime(opts = {}) {
168
165
  const content = fs.readFileSync(file.src);
169
166
  const hash = sha256(content);
170
167
 
171
- if (manifest.files[file.dest] === hash) {
172
- continue; // file unchanged
168
+ // Content-aware skip: compare the template hash to the runtime file's
169
+ // ACTUAL on-disk bytes, NOT the cached manifest. The manifest can lie — a
170
+ // hand-edited or externally-replaced runtime file still carries the hash
171
+ // the manifest recorded at last write, so a manifest-only check would skip
172
+ // it and leave the divergence running. Reading the real file makes the
173
+ // refresh self-healing: any runtime file that differs from the template
174
+ // (or is missing) is rewritten, so a reinstall reliably reconverges the
175
+ // runtime onto the template instead of silently leaving it stale.
176
+ let runtimeHash = null;
177
+ if (fs.existsSync(file.dest)) {
178
+ try {
179
+ runtimeHash = sha256(fs.readFileSync(file.dest));
180
+ } catch {
181
+ runtimeHash = null; // unreadable runtime file → treat as divergent
182
+ }
183
+ }
184
+
185
+ if (runtimeHash === hash) {
186
+ // Already in sync on disk. Keep the manifest honest as bookkeeping.
187
+ if (!dryRun) manifest.files[file.dest] = hash;
188
+ continue;
173
189
  }
174
190
 
175
191
  if (dryRun) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
4
4
  "description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-cabinet": "bin/create-claude-cabinet.js"