create-claude-cabinet 0.46.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 (139) hide show
  1. package/README.md +2 -6
  2. package/lib/cli.js +348 -91
  3. package/lib/copy.js +108 -6
  4. package/lib/db-setup.js +122 -17
  5. package/lib/metadata.js +51 -2
  6. package/lib/settings-merge.js +52 -10
  7. package/lib/watchtower-setup.js +22 -2
  8. package/package.json +1 -1
  9. package/templates/cabinet/advisories-state-schema.md +1 -1
  10. package/templates/cabinet/checklist-stats-schema.md +15 -1
  11. package/templates/cabinet/memory-lifecycle-contract.md +135 -0
  12. package/templates/cabinet/pib-db-access.md +13 -0
  13. package/templates/cabinet/watchtower-contracts.md +414 -0
  14. package/templates/cabinet/worktree-invocation-contract.md +87 -0
  15. package/templates/engagement/OVERVIEW.md +12 -0
  16. package/templates/engagement/__tests__/engagement.test.mjs +177 -2
  17. package/templates/engagement/__tests__/invoice-doc.test.mjs +195 -0
  18. package/templates/engagement/engagement-preview.mjs +100 -0
  19. package/templates/engagement/engagement-schema.md +240 -0
  20. package/templates/engagement/engagement.mjs +391 -11
  21. package/templates/engagement/pib-db-patches/pib-db-mcp-server.mjs +5 -44
  22. package/templates/engagement/pib-db-patches/pib-db.mjs +5 -2
  23. package/templates/hooks/action-completion-gate.sh +9 -3
  24. package/templates/hooks/memory-index-guard.sh +17 -11
  25. package/templates/hooks/watchtower-session-start.sh +24 -2
  26. package/templates/mcp/pib-db.json +1 -4
  27. package/templates/mux/bin/mux +8 -1
  28. package/templates/mux/config/help.txt +1 -0
  29. package/templates/rules/acknowledge-when-corrected.md +33 -0
  30. package/templates/rules/maintainability.md +11 -0
  31. package/templates/rules/memory-capture.md +26 -4
  32. package/templates/rules/plan-before-bulk-or-irreversible-actions.md +48 -0
  33. package/templates/rules/verify-before-asserting.md +79 -0
  34. package/templates/scripts/__tests__/ahead-check-origin.test.mjs +355 -0
  35. package/templates/scripts/__tests__/batch-disposition.test.mjs +98 -0
  36. package/templates/scripts/__tests__/claude-churn-authored.test.mjs +90 -0
  37. package/templates/scripts/__tests__/cross-ring-reader.test.mjs +505 -0
  38. package/templates/scripts/__tests__/dx-captures-briefing.test.mjs +169 -0
  39. package/templates/scripts/__tests__/hook-runner.test.mjs +247 -0
  40. package/templates/scripts/__tests__/narrative-corpus.test.mjs +247 -0
  41. package/templates/scripts/__tests__/phase-shim.test.mjs +112 -0
  42. package/templates/scripts/__tests__/qa-handoff-merge-state.test.mjs +162 -0
  43. package/templates/scripts/__tests__/resolve-cli.test.mjs +314 -0
  44. package/templates/scripts/__tests__/ring-state-ownership.test.mjs +120 -0
  45. package/templates/scripts/__tests__/ring1-content-detector.test.mjs +168 -0
  46. package/templates/scripts/__tests__/ring1-flagged-actions.test.mjs +135 -0
  47. package/templates/scripts/__tests__/ring1-script-drift.test.mjs +126 -0
  48. package/templates/scripts/__tests__/ring2-queue-reader.test.mjs +80 -0
  49. package/templates/scripts/__tests__/ring2-recall-canary.test.mjs +150 -0
  50. package/templates/scripts/__tests__/ring2-roster-review.test.mjs +311 -0
  51. package/templates/scripts/__tests__/ring3-chunk-merge.test.mjs +122 -0
  52. package/templates/scripts/__tests__/ring3-close-lenses.test.mjs +359 -0
  53. package/templates/scripts/__tests__/ring3-dedup.test.mjs +92 -12
  54. package/templates/scripts/__tests__/ring3-memory-titles.test.mjs +187 -0
  55. package/templates/scripts/__tests__/ring3-novelty-rescue.test.mjs +148 -0
  56. package/templates/scripts/__tests__/ring3-recent-slice.test.mjs +101 -0
  57. package/templates/scripts/__tests__/ring4-reconcile.test.mjs +393 -0
  58. package/templates/scripts/__tests__/shared-thread-reader.test.mjs +187 -0
  59. package/templates/scripts/__tests__/suppression-ledger.test.mjs +148 -0
  60. package/templates/scripts/__tests__/verify-backfill.test.mjs +103 -0
  61. package/templates/scripts/__tests__/verify-coverage.test.mjs +80 -0
  62. package/templates/scripts/__tests__/watchtower-snapshot.test.mjs +261 -0
  63. package/templates/scripts/__tests__/watchtower-sync.test.mjs +311 -0
  64. package/templates/scripts/load-triage-history.js +5 -2
  65. package/templates/scripts/pib-db-mcp-server.mjs +5 -44
  66. package/templates/scripts/pib-db-path.mjs +61 -0
  67. package/templates/scripts/pib-db.mjs +5 -2
  68. package/templates/scripts/validate-memory.mjs +214 -16
  69. package/templates/scripts/watchtower-advisories.mjs +7 -3
  70. package/templates/scripts/watchtower-build-context.mjs +192 -8
  71. package/templates/scripts/watchtower-cross-ring-reader.mjs +700 -0
  72. package/templates/scripts/watchtower-hook-runner.mjs +422 -0
  73. package/templates/scripts/watchtower-lib.mjs +273 -4
  74. package/templates/scripts/watchtower-narrative-corpus.mjs +385 -0
  75. package/templates/scripts/watchtower-phase-shim.mjs +171 -0
  76. package/templates/scripts/watchtower-queue.mjs +341 -3
  77. package/templates/scripts/watchtower-ring1.mjs +405 -45
  78. package/templates/scripts/watchtower-ring2.mjs +710 -68
  79. package/templates/scripts/watchtower-ring3-close.mjs +988 -147
  80. package/templates/scripts/watchtower-ring4-runner.sh +85 -0
  81. package/templates/scripts/watchtower-ring4.mjs +753 -0
  82. package/templates/scripts/watchtower-routines.mjs +1 -1
  83. package/templates/scripts/watchtower-snapshot.mjs +452 -0
  84. package/templates/scripts/watchtower-sync.mjs +393 -0
  85. package/templates/skills/briefing/SKILL.md +281 -41
  86. package/templates/skills/cabinet-process-therapist/SKILL.md +28 -0
  87. package/templates/skills/cabinet-security/SKILL.md +11 -0
  88. package/templates/skills/catch-up/SKILL.md +113 -0
  89. package/templates/skills/cc-publish/SKILL.md +53 -16
  90. package/templates/skills/cc-remember/SKILL.md +45 -0
  91. package/templates/skills/close/SKILL.md +107 -0
  92. package/templates/skills/collab-client/SKILL.md +22 -5
  93. package/templates/skills/collab-consultant/SKILL.md +110 -2
  94. package/templates/skills/debrief/SKILL.md +21 -4
  95. package/templates/skills/debrief-classic/SKILL.md +696 -0
  96. package/templates/skills/debrief-classic/calibration.md +44 -0
  97. package/templates/skills/debrief-classic/phases/audit-pattern-capture.md +78 -0
  98. package/templates/skills/debrief-classic/phases/auto-maintenance.md +48 -0
  99. package/templates/skills/debrief-classic/phases/checklist-feedback.md +123 -0
  100. package/templates/skills/debrief-classic/phases/close-work.md +163 -0
  101. package/templates/skills/debrief-classic/phases/health-checks.md +54 -0
  102. package/templates/skills/debrief-classic/phases/inventory.md +40 -0
  103. package/templates/skills/debrief-classic/phases/loose-ends.md +52 -0
  104. package/templates/skills/debrief-classic/phases/methodology-capture.md +223 -0
  105. package/templates/skills/debrief-classic/phases/qa-handoff-sweep.md +78 -0
  106. package/templates/skills/debrief-classic/phases/record-lessons.md +177 -0
  107. package/templates/skills/debrief-classic/phases/report.md +59 -0
  108. package/templates/skills/debrief-classic/phases/update-state.md +48 -0
  109. package/templates/skills/debrief-classic/phases/upstream-feedback.md +185 -0
  110. package/templates/skills/debrief-classic/phases/verify-coverage.md +101 -0
  111. package/templates/skills/execute/SKILL.md +71 -6
  112. package/templates/skills/execute/phases/commit-and-deploy.md +8 -0
  113. package/templates/skills/execute-group/SKILL.md +23 -26
  114. package/templates/skills/generate-plan-groups/SKILL.md +20 -0
  115. package/templates/skills/inbox/SKILL.md +84 -28
  116. package/templates/skills/memory/SKILL.md +22 -6
  117. package/templates/skills/orient/SKILL.md +71 -5
  118. package/templates/skills/orient-classic/SKILL.md +770 -0
  119. package/templates/skills/orient-classic/phases/auto-maintenance.md +52 -0
  120. package/templates/skills/orient-classic/phases/briefing.md +53 -0
  121. package/templates/skills/orient-classic/phases/cabinet.md +46 -0
  122. package/templates/skills/orient-classic/phases/checklist-status.md +54 -0
  123. package/templates/skills/orient-classic/phases/context.md +88 -0
  124. package/templates/skills/orient-classic/phases/data-sync.md +35 -0
  125. package/templates/skills/orient-classic/phases/deferred-check.md +55 -0
  126. package/templates/skills/orient-classic/phases/dx-captures.md +53 -0
  127. package/templates/skills/orient-classic/phases/health-checks.md +50 -0
  128. package/templates/skills/orient-classic/phases/verify-backfill.md +109 -0
  129. package/templates/skills/orient-classic/phases/work-scan.md +69 -0
  130. package/templates/skills/qa-drain/SKILL.md +38 -0
  131. package/templates/skills/qa-handoff/SKILL.md +287 -46
  132. package/templates/skills/setup-accounts/SKILL.md +38 -16
  133. package/templates/skills/threads/SKILL.md +4 -0
  134. package/templates/skills/validate/phases/validators.md +41 -0
  135. package/templates/skills/watchtower/SKILL.md +160 -8
  136. package/templates/verify-runtime/CONVENTIONS.md +9 -0
  137. package/templates/verify-runtime/README.md +37 -0
  138. package/templates/watchtower/config.json.template +8 -2
  139. package/templates/workflows/execute-group-complete.js +10 -1
package/README.md CHANGED
@@ -129,13 +129,9 @@ you already use GitHub Issues, Linear, or something else.
129
129
  Claude Code has built-in file memory, but no guardrails around it.
130
130
  The memory module adds structure:
131
131
 
132
- - **`/cc-remember`** — write a new memory with automatic indexing.
133
- Every memory gets its own file and an entry in `MEMORY.md` so
134
- `/orient` can find it next session.
132
+ - **`/cc-remember`** — write a new memory with automatic indexing. Every memory gets its own file and is made reachable from `MEMORY.md` — by a direct entry or a region pointer — so `/orient` can find it next session.
135
133
  - **`/memory`** — browse and search what Claude remembers.
136
- - **Validation** — `validate-memory.mjs` checks that the index stays
137
- within Claude Code's session-start budget and that every file is
138
- indexed. A PostToolUse hook flags unindexed writes in real time.
134
+ - **Validation** — `validate-memory.mjs` checks that the index stays within Claude Code's session-start budget and that every memory file is reachable from `MEMORY.md`. A PostToolUse hook flags unreachable writes in real time.
139
135
 
140
136
  ### Compliance Stack (full install)
141
137
 
package/lib/cli.js CHANGED
@@ -3,10 +3,10 @@ const path = require('path');
3
3
  const fs = require('fs');
4
4
  const os = require('os');
5
5
  const crypto = require('crypto');
6
- const { copyTemplates, recordSkip } = require('./copy');
6
+ const { copyTemplates, recordSkip, isProjectOwnedSeed, freezeRetain } = require('./copy');
7
7
  const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks } = require('./settings-merge');
8
- const { create: createMetadata, read: readMetadata } = require('./metadata');
9
- const { setupDb } = require('./db-setup');
8
+ const { create: createMetadata, read: readMetadata, readFrozenModules, setFrozenModules } = require('./metadata');
9
+ const { setupDb, shouldSkipInit, readBundledSchemaVersion } = require('./db-setup');
10
10
  const { setupVerifyRuntime } = require('./verify-setup');
11
11
  const { setupSiteAuditRuntime } = require('./site-audit-setup');
12
12
  const { setupEngagement } = require('./engagement-setup');
@@ -468,6 +468,20 @@ const MODULES = {
468
468
  'skills/debrief/phases/record-lessons.md',
469
469
  'skills/debrief/phases/upstream-feedback.md',
470
470
  'skills/debrief/phases/qa-handoff-sweep.md',
471
+ // Reversibility scaffolding (act:6fa60c7d) — standalone classic copies
472
+ // of orient/debrief, kept invokable so the orient/debrief retirement is
473
+ // reversible during the watchtower transition. They do NOT delegate to
474
+ // the live orient/debrief (which will change at retirement); they ARE
475
+ // the old skills under a new name. orient-classic ships SKILL.md only
476
+ // (its phases use skeleton defaults, like live orient); debrief-classic
477
+ // ships its SKILL.md + the same instruction phases as live debrief.
478
+ 'skills/orient-classic',
479
+ 'skills/debrief-classic',
480
+ 'skills/debrief-classic/phases/audit-pattern-capture.md',
481
+ 'skills/debrief-classic/phases/methodology-capture.md',
482
+ 'skills/debrief-classic/phases/record-lessons.md',
483
+ 'skills/debrief-classic/phases/upstream-feedback.md',
484
+ 'skills/debrief-classic/phases/qa-handoff-sweep.md',
471
485
  'skills/menu',
472
486
  ],
473
487
  },
@@ -485,7 +499,7 @@ const MODULES = {
485
499
  mandatory: false,
486
500
  default: true,
487
501
  lean: false,
488
- templates: ['scripts/pib-db.mjs', 'scripts/pib-db-lib.mjs', 'scripts/pib-db-mcp-server.mjs', 'scripts/pib-db-schema.sql', 'scripts/work-tracker-server.mjs', 'scripts/work-tracker-ui.html', 'skills/work-tracker', 'skills/spring-clean'],
502
+ templates: ['scripts/pib-db.mjs', 'scripts/pib-db-path.mjs', 'scripts/pib-db-lib.mjs', 'scripts/pib-db-mcp-server.mjs', 'scripts/pib-db-schema.sql', 'scripts/work-tracker-server.mjs', 'scripts/work-tracker-ui.html', 'skills/work-tracker', 'skills/spring-clean'],
489
503
  needsDb: true,
490
504
  },
491
505
  'planning': {
@@ -494,7 +508,7 @@ const MODULES = {
494
508
  mandatory: false,
495
509
  default: true,
496
510
  lean: true,
497
- templates: ['skills/plan', 'skills/execute', 'skills/execute/phases/post-impl-checklist.md', 'skills/debrief/phases/checklist-feedback.md', 'skills/checklist-discover', 'skills/generate-plan-groups', 'skills/execute-group', 'workflows/execute-group-implement.js', 'workflows/execute-group-complete.js', 'skills/investigate', 'cabinet/checkpoint-protocol.md', 'cabinet/elicitation-methods.md', 'cabinet/qa-dimensions-template.yaml', 'cabinet/checklist-stats-schema.md', 'scripts/qa-dimensions-validator.cjs', 'skills/orient/phases/checklist-status.md', 'skills/audit/phases/checklist-pruning.md'],
511
+ templates: ['skills/plan', 'skills/execute', 'skills/execute/phases/post-impl-checklist.md', 'skills/debrief/phases/checklist-feedback.md', 'skills/checklist-discover', 'skills/generate-plan-groups', 'skills/execute-group', 'workflows/execute-group-implement.js', 'workflows/execute-group-complete.js', 'skills/investigate', 'cabinet/checkpoint-protocol.md', 'cabinet/worktree-invocation-contract.md', 'cabinet/elicitation-methods.md', 'cabinet/qa-dimensions-template.yaml', 'cabinet/checklist-stats-schema.md', 'scripts/qa-dimensions-validator.cjs', 'skills/orient/phases/checklist-status.md', 'skills/audit/phases/checklist-pruning.md'],
498
512
  },
499
513
  'compliance': {
500
514
  name: 'Compliance Stack (rules + enforcement)',
@@ -502,7 +516,7 @@ const MODULES = {
502
516
  mandatory: false,
503
517
  default: true,
504
518
  lean: false,
505
- templates: ['rules/enforcement-pipeline.md', 'rules/maintainability.md', 'rules/markdown-prose.md', 'skills/unwrap', 'memory/patterns/_pattern-template.md', 'memory/patterns/pattern-intelligence-first.md'],
519
+ templates: ['rules/enforcement-pipeline.md', 'rules/maintainability.md', 'rules/markdown-prose.md', 'rules/verify-before-asserting.md', 'rules/plan-before-bulk-or-irreversible-actions.md', 'rules/acknowledge-when-corrected.md', 'skills/unwrap', 'memory/patterns/_pattern-template.md', 'memory/patterns/pattern-intelligence-first.md'],
506
520
  },
507
521
  'memory': {
508
522
  name: 'Built-In Memory (cc-remember + reader + validator)',
@@ -518,6 +532,7 @@ const MODULES = {
518
532
  'skills/cc-remember',
519
533
  'skills/memory',
520
534
  'rules/memory-capture.md',
535
+ 'cabinet/memory-lifecycle-contract.md',
521
536
  'scripts/write-memory-file.mjs',
522
537
  'scripts/validate-memory.mjs',
523
538
  // project-context.cjs ships as a co-located sibling: the two .mjs
@@ -555,6 +570,7 @@ const MODULES = {
555
570
  'skills/cabinet-narrative-architect', 'skills/cabinet-interactive-storyteller',
556
571
  'skills/cabinet-elegance',
557
572
  'scripts/merge-findings.js', 'scripts/load-triage-history.js',
573
+ 'scripts/pib-db-path.mjs', // load-triage-history.js imports it (declared cross-module dep; also shipped by work-tracking)
558
574
  'scripts/triage-server.mjs', 'scripts/triage-ui.html',
559
575
  'scripts/finding-schema.json', 'scripts/resolve-committees.cjs',
560
576
  'scripts/review-server.mjs', 'scripts/review-ui.html',
@@ -634,11 +650,17 @@ const MODULES = {
634
650
  'scripts/watchtower-lib.mjs',
635
651
  'scripts/watchtower-queue.mjs',
636
652
  'scripts/watchtower-routines.mjs',
653
+ 'scripts/watchtower-hook-runner.mjs',
654
+ 'scripts/watchtower-phase-shim.mjs',
655
+ 'scripts/watchtower-sync.mjs',
637
656
  'scripts/watchtower-advisories.mjs',
638
657
  'cabinet/advisories-state-schema.md',
639
658
  'skills/inbox',
640
659
  'hooks/watchtower-session-start.sh',
641
660
  'scripts/watchtower-build-context.mjs',
661
+ 'scripts/watchtower-snapshot.mjs',
662
+ 'scripts/watchtower-narrative-corpus.mjs',
663
+ 'scripts/watchtower-cross-ring-reader.mjs',
642
664
  'scripts/watchtower-ring1.mjs',
643
665
  'scripts/watchtower-ring1-runner.sh',
644
666
  'skills/watchtower',
@@ -655,11 +677,15 @@ const MODULES = {
655
677
  'watchtower/watchtower-ring2-slow.timer',
656
678
  'hooks/watchtower-session-end.sh',
657
679
  'scripts/watchtower-ring3-close.mjs',
680
+ 'scripts/watchtower-ring4.mjs',
681
+ 'scripts/watchtower-ring4-runner.sh',
658
682
  'scripts/watchtower-status.sh',
659
683
  'skills/briefing',
660
684
  'skills/threads',
685
+ 'skills/catch-up',
661
686
  'skills/qa-handoff',
662
687
  'skills/qa-drain',
688
+ 'skills/close',
663
689
  ],
664
690
  },
665
691
  mux: {
@@ -708,6 +734,94 @@ function walkDir(dir, base) {
708
734
  return results;
709
735
  }
710
736
 
737
+ // Map a module template path to its install-time manifest key (the path
738
+ // relative to the project root). Top-level so both the install copy loop and
739
+ // computeRemovals derive the key the same way — single source of truth.
740
+ function manifestPath(tmpl) {
741
+ if (tmpl.startsWith('scripts/')) return tmpl;
742
+ return '.claude/' + tmpl;
743
+ }
744
+
745
+ /**
746
+ * Expand a declared frozen-module list by its `requires` closure (act:c1013967).
747
+ * Freezing a module must also pin the modules it depends on, or a frozen
748
+ * caller can be left bound to a refreshed callee contract (the cross-runtime
749
+ * version-skew failure: green in the dev tree, broken on a fresh install).
750
+ * E.g. freezing `engagement` (requires work-tracking) also pins work-tracking.
751
+ * Returns a Set of module keys. Unknown keys are ignored (validated elsewhere).
752
+ */
753
+ function expandFrozenClosure(frozenKeys, modules = MODULES) {
754
+ const closure = new Set();
755
+ const queue = [...(frozenKeys || [])];
756
+ while (queue.length) {
757
+ const key = queue.shift();
758
+ if (closure.has(key)) continue;
759
+ closure.add(key);
760
+ const mod = modules[key];
761
+ for (const req of (mod && mod.requires) || []) {
762
+ if (!closure.has(req)) queue.push(req);
763
+ }
764
+ }
765
+ return closure;
766
+ }
767
+
768
+ /**
769
+ * Classify which manifest entries should be removed because they are no longer
770
+ * shipped upstream — the pure core of the cleanup/reconcile loop, extracted so
771
+ * it is unit-testable in isolation (the inline loop never was). Returns
772
+ * `{ toRemove, kept }`: `toRemove` is the list of project-relative paths whose
773
+ * files exist on disk and are safe to delete; `kept` carries `{path, reason}`
774
+ * for files deliberately spared, which the caller logs.
775
+ *
776
+ * The four safeguards from the original loop are preserved exactly: S1 only
777
+ * delete known-template paths; S2 only from selected modules; project-owned
778
+ * seeds and non-CC project skills are never deleted. A FROZEN file needs no
779
+ * special case here — it was RETAINED in `allManifest` (at its on-disk hash),
780
+ * so it never reaches the `!allManifest[oldPath]` test; a frozen file removed
781
+ * upstream is kept by S1 ("non-template").
782
+ */
783
+ function computeRemovals({ existingManifest, allManifest, selectedModules, templateRoot, projectDir, modules = MODULES }) {
784
+ const collectPaths = (modKeys) => {
785
+ const set = new Set();
786
+ for (const modKey of modKeys) {
787
+ const mod = modules[modKey];
788
+ for (const tmpl of (mod && mod.templates) || []) {
789
+ const srcPath = path.join(templateRoot, tmpl);
790
+ if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) {
791
+ for (const rel of walkDir(srcPath)) set.add(manifestPath(tmpl) + '/' + rel);
792
+ } else if (fs.existsSync(srcPath)) {
793
+ set.add(manifestPath(tmpl));
794
+ }
795
+ }
796
+ }
797
+ return set;
798
+ };
799
+
800
+ const allTemplatePaths = collectPaths(Object.keys(modules));
801
+ const selectedTemplatePaths = collectPaths(selectedModules);
802
+
803
+ const ccSkillNames = new Set();
804
+ for (const mod of Object.values(modules)) {
805
+ for (const tmpl of mod.templates || []) {
806
+ if (tmpl.startsWith('skills/')) ccSkillNames.add(tmpl.split('/')[1]);
807
+ }
808
+ }
809
+
810
+ const toRemove = [];
811
+ const kept = [];
812
+ for (const oldPath of Object.keys(existingManifest)) {
813
+ if (allManifest[oldPath]) continue;
814
+ if (/\/phases\//.test(oldPath)) continue; // user-customizable
815
+ if (isProjectOwnedSeed(oldPath)) { kept.push({ path: oldPath, reason: 'project-owned seed' }); continue; }
816
+ if (!allTemplatePaths.has(oldPath)) { kept.push({ path: oldPath, reason: 'non-template file' }); continue; }
817
+ if (!selectedTemplatePaths.has(oldPath)) continue; // deselected module — leave it
818
+ const skillDirMatch = oldPath.match(/^\.claude\/skills\/([^/]+)\//);
819
+ if (skillDirMatch && !ccSkillNames.has(skillDirMatch[1])) { kept.push({ path: oldPath, reason: 'project skill' }); continue; }
820
+ if (fs.existsSync(path.join(projectDir, oldPath))) toRemove.push(oldPath);
821
+ }
822
+ return { toRemove, kept };
823
+ }
824
+
711
825
  // Signals that a directory contains a real project (not just empty)
712
826
  const PROJECT_SIGNALS = [
713
827
  'package.json', 'Cargo.toml', 'requirements.txt', 'pyproject.toml',
@@ -730,6 +844,13 @@ function detectProjectState(dir) {
730
844
  return 'empty';
731
845
  }
732
846
 
847
+ // A `--freeze`/`--unfreeze` value token vs the positional target dir. Module
848
+ // keys are simple kebab-case identifiers; a flag or any path-like token
849
+ // (separator, or . / ..) is the target directory, never a module list.
850
+ function looksLikeModuleArg(s) {
851
+ return Boolean(s) && !s.startsWith('-') && !s.includes('/') && !s.includes('\\') && s !== '.' && s !== '..';
852
+ }
853
+
733
854
  function parseArgs(argv) {
734
855
  const args = argv.slice(2);
735
856
  const flags = {
@@ -752,11 +873,32 @@ function parseArgs(argv) {
752
873
  else if (arg === '--help' || arg === '-h') flags.help = true;
753
874
  else if (arg === '--reset') flags.reset = true;
754
875
  else if (arg === '--force') flags.force = true;
876
+ else if (arg === '--reindex') flags.reindex = true;
755
877
  else if (arg === '--migrate-memory') flags.migrateMemory = true;
756
878
  else if (arg === '--unmigrate-memory') flags.unmigrateMemory = true;
879
+ else if (arg === '--list-frozen') flags.listFrozen = true;
757
880
  else if (arg === '--modules' && i + 1 < args.length) {
758
881
  flags.modules = args[++i].split(',').map(s => s.trim()).filter(Boolean);
759
882
  }
883
+ // --freeze <mods> / --unfreeze [mods]: edit .ccrc.json frozenModules and
884
+ // exit. Consume the next token as the module list ONLY when it looks like
885
+ // one — a flag, or a path-like token (contains a separator, or is . / ..),
886
+ // is the target directory, not a value. Module keys are simple kebab-case
887
+ // identifiers and never contain a path separator, so this can't swallow a
888
+ // real module while it does stop `--unfreeze /path/to/project` from being
889
+ // misread as a module (which would silently fail to clear-all). An empty
890
+ // --freeze is an error (dispatch warns, never a silent no-op); an empty
891
+ // --unfreeze clears all.
892
+ else if (arg === '--freeze') {
893
+ flags.freeze = looksLikeModuleArg(args[i + 1])
894
+ ? args[++i].split(',').map(s => s.trim()).filter(Boolean)
895
+ : [];
896
+ }
897
+ else if (arg === '--unfreeze') {
898
+ flags.unfreeze = looksLikeModuleArg(args[i + 1])
899
+ ? args[++i].split(',').map(s => s.trim()).filter(Boolean)
900
+ : []; // clear all
901
+ }
760
902
  else if (arg === '--frontier-model' && i + 1 < args.length) {
761
903
  // Empty/whitespace values are treated as absent: '' is a substring of
762
904
  // every model ID, which would match everything and leave the watchdog
@@ -782,6 +924,16 @@ function printHelp() {
782
924
  --modules <keys> Comma-separated module keys to install (e.g., 'verify').
783
925
  Mandatory modules are always included.
784
926
  --dry-run Show what would be copied without writing
927
+ --reindex Regenerate .claude/skills/_index.json in place (picks up
928
+ directives-project.yaml mandate/routine edits without a full
929
+ reinstall) and exit
930
+ --freeze <keys> Pin module(s) (comma-separated) in .ccrc.json so the
931
+ NEXT reinstall holds their CC-owned files at the current
932
+ version (e.g. --freeze engagement while a client engagement
933
+ is live). Also pins the requires-closure. Edits config, exits.
934
+ --unfreeze [keys] Clear the freeze for the given module(s), or all if no
935
+ keys. Re-run the installer to refresh the unfrozen module(s).
936
+ --list-frozen Show which modules are frozen (+ their closure) and exit.
785
937
  --reset Remove Claude Cabinet files (uses manifest for safety)
786
938
  --force With --reset: remove even customized files;
787
939
  with --migrate-memory: override already-migrated guard
@@ -823,6 +975,80 @@ async function run() {
823
975
  return;
824
976
  }
825
977
 
978
+ // Standalone skill-index regen (act:98d41fa2). Advisor standing-mandates and
979
+ // routine declarations are read from .claude/skills/_index.json, which is
980
+ // otherwise regenerated only by a full install — coupling every overlay
981
+ // change (e.g. adding a session-close mandate to directives-project.yaml) to
982
+ // an upgrade cycle. This regenerates the index in place so a change is
983
+ // verifiable immediately, no reinstall needed.
984
+ if (flags.reindex) {
985
+ const projectDir = path.resolve(flags.targetDir);
986
+ if (!fs.existsSync(path.join(projectDir, '.claude', 'skills'))) {
987
+ console.log(` No .claude/skills/ in ${projectDir} — nothing to reindex.`);
988
+ return;
989
+ }
990
+ const n = generateSkillIndex(projectDir);
991
+ console.log(` 📇 Reindexed ${n} skill${n === 1 ? '' : 's'} in .claude/skills/_index.json`);
992
+ return;
993
+ }
994
+
995
+ // Standalone freeze management (act:c1013967) — edit .ccrc.json frozenModules
996
+ // and exit, no install. Freeze pins a module's CC-owned files across a
997
+ // reinstall (the "reinstall maginnis without disturbing Ed's engagement" fix);
998
+ // the install body reads frozenModules + expands the requires closure.
999
+ if (flags.listFrozen || flags.freeze || flags.unfreeze) {
1000
+ const projectDir = path.resolve(flags.targetDir);
1001
+ const current = readFrozenModules(projectDir);
1002
+
1003
+ const describe = (mods) => {
1004
+ if (mods.length === 0) { console.log(' No modules are frozen.'); return; }
1005
+ const closure = expandFrozenClosure(mods, MODULES);
1006
+ const pulledIn = [...closure].filter(k => !mods.includes(k));
1007
+ console.log(` ❄ Frozen modules: ${mods.join(', ')}`);
1008
+ if (pulledIn.length) console.log(` + required closure (also pinned): ${pulledIn.join(', ')}`);
1009
+ console.log(' A reinstall holds these modules’ CC-owned files at the current version.');
1010
+ };
1011
+
1012
+ if (flags.listFrozen) {
1013
+ describe(current);
1014
+ return;
1015
+ }
1016
+
1017
+ if (flags.freeze) {
1018
+ if (flags.freeze.length === 0) {
1019
+ console.log(' ✗ --freeze needs at least one module key, e.g. --freeze engagement');
1020
+ return;
1021
+ }
1022
+ const known = Object.keys(MODULES);
1023
+ const unknown = flags.freeze.filter(m => !known.includes(m));
1024
+ if (unknown.length) console.log(` ⚠ Unknown module key(s) ignored: ${unknown.join(', ')} (known: ${known.join(', ')})`);
1025
+ const valid = flags.freeze.filter(m => known.includes(m));
1026
+ const next = [...new Set([...current, ...valid])];
1027
+ if (flags.dryRun) {
1028
+ console.log(` [dry run] Would set frozenModules to: ${next.join(', ') || '(none)'}`);
1029
+ } else {
1030
+ setFrozenModules(projectDir, next);
1031
+ console.log(' ✓ Updated .ccrc.json frozenModules.');
1032
+ }
1033
+ describe(next);
1034
+ return;
1035
+ }
1036
+
1037
+ if (flags.unfreeze) {
1038
+ const next = flags.unfreeze.length === 0
1039
+ ? [] // --unfreeze → clear all
1040
+ : current.filter(m => !flags.unfreeze.includes(m));
1041
+ if (flags.dryRun) {
1042
+ console.log(` [dry run] Would set frozenModules to: ${next.join(', ') || '(none)'}`);
1043
+ } else {
1044
+ setFrozenModules(projectDir, next);
1045
+ console.log(' ✓ Updated .ccrc.json frozenModules. Re-run the installer to refresh the unfrozen module(s).');
1046
+ }
1047
+ describe(next);
1048
+ return;
1049
+ }
1050
+ }
1051
+
826
1052
  if (flags.migrateMemory) {
827
1053
  const { migrateMemoryCmd } = require('./migrate-memory-cmd');
828
1054
  const projectDir = path.resolve(flags.targetDir);
@@ -1151,18 +1377,24 @@ async function run() {
1151
1377
  return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
1152
1378
  }
1153
1379
 
1154
- // Compute the relative path from projectDir for manifest entries
1155
- function manifestPath(tmpl) {
1156
- if (tmpl.startsWith('skills/') || tmpl.startsWith('hooks/') || tmpl.startsWith('rules/')) {
1157
- return '.claude/' + tmpl;
1158
- } else if (tmpl.startsWith('scripts/')) {
1159
- return tmpl;
1160
- }
1161
- return '.claude/' + tmpl;
1162
- }
1380
+ // Frozen modules (act:c1013967): the operator pinned these in .ccrc.json so
1381
+ // a reinstall holds their CC-owned files at the current version. Expand by
1382
+ // the `requires` closure (freezing engagement also pins work-tracking, so a
1383
+ // frozen caller is never left bound to a refreshed callee), then intersect
1384
+ // with what is actually installed. `effectiveFrozen` gates ALL freeze
1385
+ // behavior — never the raw declared list — so a typo'd / uninstalled / dead
1386
+ // key can't silently pin global state or claim protection it isn't giving.
1387
+ const declaredFrozen = readFrozenModules(projectDir);
1388
+ const effectiveFrozen = new Set(
1389
+ [...expandFrozenClosure(declaredFrozen, MODULES)].filter(
1390
+ (k) => MODULES[k] && selectedModules.includes(k)
1391
+ )
1392
+ );
1393
+ const frozenCounts = {}; // modKey -> # files held this install
1163
1394
 
1164
1395
  for (const modKey of selectedModules) {
1165
1396
  const mod = MODULES[modKey];
1397
+ const frozen = effectiveFrozen.has(modKey);
1166
1398
  for (const tmpl of mod.templates) {
1167
1399
  const srcPath = path.join(templateRoot, tmpl);
1168
1400
  if (!fs.existsSync(srcPath)) {
@@ -1211,7 +1443,9 @@ async function run() {
1211
1443
  projectRoot: projectDir,
1212
1444
  existingManifest,
1213
1445
  instructionPhases: instrPhases,
1446
+ frozen,
1214
1447
  });
1448
+ if (results.frozen.length) frozenCounts[modKey] = (frozenCounts[modKey] || 0) + results.frozen.length;
1215
1449
  totalCopied += results.copied.length;
1216
1450
  totalSkipped += results.skipped.length;
1217
1451
  totalOverwritten += results.overwritten.length;
@@ -1256,6 +1490,22 @@ async function run() {
1256
1490
  continue;
1257
1491
  }
1258
1492
 
1493
+ // Frozen module (act:c1013967): held instead of overwritten. Mirrors
1494
+ // copy.js's directory path via the shared freezeRetain rule — retain
1495
+ // at the on-disk hash only if already CC-owned; a project-owned file
1496
+ // falls through to the normal skip-and-omit path below.
1497
+ if (frozen) {
1498
+ const decision = freezeRetain({ existing: existingContent, isCcOwned: Boolean(existingManifest[mPath]) });
1499
+ if (decision.manifestHash) {
1500
+ totalSkipped++;
1501
+ frozenCounts[modKey] = (frozenCounts[modKey] || 0) + 1;
1502
+ allManifest[mPath] = decision.manifestHash;
1503
+ console.log(` ❄ Frozen (held): ${path.relative(projectDir, destPath)}`);
1504
+ continue;
1505
+ }
1506
+ // decision.omit → project-owned; fall through.
1507
+ }
1508
+
1259
1509
  if (flags.yes || dirState === 'existing-install') {
1260
1510
  // If file is in the old manifest, it's upstream-managed — overwrite.
1261
1511
  // If not, it's project-created — skip.
@@ -1298,6 +1548,14 @@ async function run() {
1298
1548
  }
1299
1549
  }
1300
1550
  } else {
1551
+ // New file in a frozen module: hold it back (don't create) so the
1552
+ // module stays internally consistent at its pinned version. Report
1553
+ // only — never a manifest entry (no on-disk content to hash).
1554
+ if (frozen) {
1555
+ frozenCounts[modKey] = (frozenCounts[modKey] || 0) + 1;
1556
+ console.log(` ❄ Frozen (held new): ${path.relative(projectDir, destPath)}`);
1557
+ continue;
1558
+ }
1301
1559
  if (!flags.dryRun) fs.copyFileSync(srcPath, destPath);
1302
1560
  totalCopied++;
1303
1561
  allManifest[mPath] = incomingHash;
@@ -1308,6 +1566,28 @@ async function run() {
1308
1566
 
1309
1567
  console.log(` 📁 Files: ${totalCopied} copied, ${totalOverwritten} overwritten, ${totalSkipped} unchanged`);
1310
1568
 
1569
+ // --- Frozen-module report (act:c1013967) ---
1570
+ // Plain-English, per-module: surface WHICH modules were held and how many
1571
+ // files, so a gitignored freeze can never quietly change install behavior.
1572
+ // Note in plain words when the requires-closure pinned a module the operator
1573
+ // didn't name directly.
1574
+ if (effectiveFrozen.size > 0) {
1575
+ const declaredSet = new Set(declaredFrozen);
1576
+ for (const modKey of effectiveFrozen) {
1577
+ const n = frozenCounts[modKey] || 0;
1578
+ let reason = '';
1579
+ if (!declaredSet.has(modKey)) {
1580
+ const namedBy = [...declaredSet].find(d => expandFrozenClosure([d], MODULES).has(modKey) && d !== modKey);
1581
+ if (namedBy) reason = ` (pinned because ${namedBy} requires it)`;
1582
+ }
1583
+ if (n > 0) {
1584
+ console.log(` ❄ Frozen: ${n} file${n === 1 ? '' : 's'} in module '${modKey}'${reason} held at current version — unfreeze to update.`);
1585
+ } else {
1586
+ console.log(` ❄ Frozen: module '${modKey}'${reason} — no CC-owned files differed (nothing to hold this run).`);
1587
+ }
1588
+ }
1589
+ }
1590
+
1311
1591
  // --- Make hook scripts executable ---
1312
1592
  const hooksDir = path.join(claudeDir, 'hooks');
1313
1593
  if (!flags.dryRun && fs.existsSync(hooksDir)) {
@@ -1378,10 +1658,34 @@ async function run() {
1378
1658
  }
1379
1659
 
1380
1660
  // --- Set up database ---
1661
+ // dbSchemaVersion is recorded into .ccrc.json so a future reinstall can
1662
+ // tell, WITHOUT loading better-sqlite3, that no migration is pending and
1663
+ // skip the init step entirely (lib/db-setup.js shouldSkipInit).
1664
+ let dbSchemaVersion;
1381
1665
  if (includeDb && selectedModules.includes('work-tracking') && !flags.dryRun) {
1382
1666
  try {
1383
- const dbResults = setupDb(projectDir);
1384
- for (const r of dbResults) console.log(` 🗄️ ${r}`);
1667
+ // Freeze insurance (act:c1013967): when a module is frozen and a schema
1668
+ // migration is actually pending, back up pib.db before db-setup runs so
1669
+ // the operator has true one-file rollback for the store holding live
1670
+ // engagement data. Reuses db-setup's own skip decision (no logic
1671
+ // duplication): if work-tracking is itself frozen, its pib-db-lib.mjs
1672
+ // stayed old → bundled == recorded → no migration → no backup. The
1673
+ // existsSync guard means a fresh install (no pib.db yet) still creates
1674
+ // the db normally; there's just nothing to back up.
1675
+ if (effectiveFrozen.size > 0) {
1676
+ const dbPath = path.join(projectDir, 'pib.db');
1677
+ const recordedVersion = (readMetadata(projectDir) || {}).dbSchemaVersion;
1678
+ const bundledVersion = readBundledSchemaVersion(path.join(projectDir, 'scripts'));
1679
+ const willMigrate = !shouldSkipInit({ dbExists: fs.existsSync(dbPath), recordedVersion, bundledVersion });
1680
+ if (willMigrate && fs.existsSync(dbPath)) {
1681
+ const backupPath = `${dbPath}.cc-bak-${new Date().toISOString().replace(/[:.]/g, '-')}`;
1682
+ fs.copyFileSync(dbPath, backupPath);
1683
+ console.log(` ❄ Backed up pib.db → ${path.basename(backupPath)} before migration (freeze active).`);
1684
+ }
1685
+ }
1686
+ const dbResult = setupDb(projectDir);
1687
+ for (const r of dbResult.results) console.log(` 🗄️ ${r}`);
1688
+ if (dbResult.schemaVersion != null) dbSchemaVersion = dbResult.schemaVersion;
1385
1689
  } catch (err) {
1386
1690
  console.log(` ⚠ Database setup failed: ${err.message}`);
1387
1691
  console.log(' You can set it up later: node scripts/pib-db.mjs init');
@@ -1487,78 +1791,26 @@ async function run() {
1487
1791
  // S3: Itemize — list all deletions and confirm before proceeding
1488
1792
  // S4: Backup — copy files to .cc-backup/<timestamp>/ before deletion
1489
1793
  if (Object.keys(existingManifest).length > 0) {
1490
- // S1: Build complete template path set (ALL modules) for classification
1491
- const allTemplatePaths = new Set();
1492
- for (const mod of Object.values(MODULES)) {
1493
- for (const tmpl of mod.templates || []) {
1494
- const srcPath = path.join(templateRoot, tmpl);
1495
- if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) {
1496
- for (const rel of walkDir(srcPath)) {
1497
- allTemplatePaths.add(manifestPath(tmpl) + '/' + rel);
1498
- }
1499
- } else if (fs.existsSync(srcPath)) {
1500
- allTemplatePaths.add(manifestPath(tmpl));
1501
- }
1502
- }
1503
- }
1504
-
1505
- // S2: Build selected-module template paths for scoping
1506
- const selectedTemplatePaths = new Set();
1507
- for (const modKey of selectedModules) {
1508
- const mod = MODULES[modKey];
1509
- for (const tmpl of mod.templates || []) {
1510
- const srcPath = path.join(templateRoot, tmpl);
1511
- if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) {
1512
- for (const rel of walkDir(srcPath)) {
1513
- selectedTemplatePaths.add(manifestPath(tmpl) + '/' + rel);
1514
- }
1515
- } else if (fs.existsSync(srcPath)) {
1516
- selectedTemplatePaths.add(manifestPath(tmpl));
1517
- }
1518
- }
1519
- }
1520
-
1521
- // Build CC template skill name set for project-skill guard (act:4ac281ba)
1522
- const ccSkillNames = new Set();
1523
- for (const mod of Object.values(MODULES)) {
1524
- for (const tmpl of mod.templates || []) {
1525
- if (tmpl.startsWith('skills/')) {
1526
- const skillName = tmpl.split('/')[1];
1527
- ccSkillNames.add(skillName);
1528
- }
1529
- }
1530
- }
1531
-
1532
- // Collect files that would be removed
1533
- const toRemove = [];
1534
- for (const oldPath of Object.keys(existingManifest)) {
1535
- if (!allManifest[oldPath]) {
1536
- // Skip phase files — they may be user-customized
1537
- if (/\/phases\//.test(oldPath)) continue;
1538
-
1539
- // S1: Only delete if the path maps to a known CC template
1540
- if (!allTemplatePaths.has(oldPath)) {
1541
- console.log(` Keeping non-template file: ${oldPath}`);
1542
- continue;
1543
- }
1544
-
1545
- // S2: Only delete from selected modules (don't purge deselected modules)
1546
- if (!selectedTemplatePaths.has(oldPath)) {
1547
- continue;
1548
- }
1549
-
1550
- // Project-skill guard: never delete skills CC didn't create
1551
- const skillDirMatch = oldPath.match(/^\.claude\/skills\/([^/]+)\//);
1552
- if (skillDirMatch && !ccSkillNames.has(skillDirMatch[1])) {
1553
- console.log(` Keeping project skill: ${oldPath}`);
1554
- continue;
1555
- }
1556
-
1557
- const fullPath = path.join(projectDir, oldPath);
1558
- if (fs.existsSync(fullPath)) {
1559
- toRemove.push(oldPath);
1560
- }
1561
- }
1794
+ // Classification is the pure `computeRemovals` (top-level, unit-tested): S1
1795
+ // only known-template paths, S2 only selected modules, plus the
1796
+ // project-owned-seed and project-skill guards. A FROZEN file needs no case
1797
+ // here retain-in-manifest (the freeze copy path) kept it in allManifest,
1798
+ // so it never reaches the removal test; a frozen file removed upstream is
1799
+ // kept by S1 ("non-template"). The S3 itemize/confirm + S4 backup/delete
1800
+ // stay inline below.
1801
+ const { toRemove, kept } = computeRemovals({
1802
+ existingManifest,
1803
+ allManifest,
1804
+ selectedModules,
1805
+ templateRoot,
1806
+ projectDir,
1807
+ });
1808
+ for (const k of kept) {
1809
+ // 'deselected module' files are silently left (matching the prior loop's
1810
+ // bare `continue` for that S2 case); the rest log their keep reason.
1811
+ if (k.reason === 'project-owned seed') console.log(` Keeping project-owned seed: ${k.path}`);
1812
+ else if (k.reason === 'non-template file') console.log(` Keeping non-template file: ${k.path}`);
1813
+ else if (k.reason === 'project skill') console.log(` Keeping project skill: ${k.path}`);
1562
1814
  }
1563
1815
 
1564
1816
  // S3: Itemize and confirm before deleting
@@ -1629,6 +1881,7 @@ async function run() {
1629
1881
  skipped: skippedModules,
1630
1882
  version: VERSION,
1631
1883
  manifest: allManifest,
1884
+ dbSchemaVersion,
1632
1885
  });
1633
1886
  console.log(' 📝 Created .ccrc.json');
1634
1887
  }
@@ -1684,9 +1937,13 @@ async function run() {
1684
1937
  } else {
1685
1938
  console.log(' 📋 Registered in project registry');
1686
1939
  }
1687
- // Self-announcing: print the effective designation on every run.
1940
+ // Self-announcing: print the effective designation on every run, and
1941
+ // the inert-state notice when none is set — a never-configured
1942
+ // watchdog must not read the same as a passing one (act:77f7b7b2).
1688
1943
  if (registry.frontierModel) {
1689
1944
  console.log(` 🛰 Frontier model: ${registry.frontierModel} (visibility watchdog — /orient + SessionStart warn on mismatch; nothing is pinned)`);
1945
+ } else {
1946
+ console.log(' 🛰 Frontier model: none designated — watchdog inert. Set with: npx create-claude-cabinet --frontier-model <value>');
1690
1947
  }
1691
1948
  } catch (err) {
1692
1949
  // Non-fatal — registry is nice-to-have
@@ -1710,4 +1967,4 @@ async function run() {
1710
1967
  console.log('');
1711
1968
  }
1712
1969
 
1713
- module.exports = { run, MODULES, generateAgentWrappers };
1970
+ module.exports = { run, MODULES, generateAgentWrappers, generateSkillIndex, expandFrozenClosure, computeRemovals, manifestPath };