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
package/lib/cli.js CHANGED
@@ -3,15 +3,16 @@ 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');
13
13
  const { setupMux } = require('./mux-setup');
14
14
  const { setupEngagementServer } = require('./engagement-server-setup');
15
+ const { refreshWatchtowerRuntime } = require('./watchtower-setup');
15
16
  const { reset } = require('./reset');
16
17
 
17
18
  const VERSION = require('../package.json').version;
@@ -467,6 +468,20 @@ const MODULES = {
467
468
  'skills/debrief/phases/record-lessons.md',
468
469
  'skills/debrief/phases/upstream-feedback.md',
469
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',
470
485
  'skills/menu',
471
486
  ],
472
487
  },
@@ -484,7 +499,7 @@ const MODULES = {
484
499
  mandatory: false,
485
500
  default: true,
486
501
  lean: false,
487
- 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'],
488
503
  needsDb: true,
489
504
  },
490
505
  'planning': {
@@ -493,7 +508,7 @@ const MODULES = {
493
508
  mandatory: false,
494
509
  default: true,
495
510
  lean: true,
496
- 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'],
497
512
  },
498
513
  'compliance': {
499
514
  name: 'Compliance Stack (rules + enforcement)',
@@ -501,7 +516,7 @@ const MODULES = {
501
516
  mandatory: false,
502
517
  default: true,
503
518
  lean: false,
504
- 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'],
505
520
  },
506
521
  'memory': {
507
522
  name: 'Built-In Memory (cc-remember + reader + validator)',
@@ -517,6 +532,7 @@ const MODULES = {
517
532
  'skills/cc-remember',
518
533
  'skills/memory',
519
534
  'rules/memory-capture.md',
535
+ 'cabinet/memory-lifecycle-contract.md',
520
536
  'scripts/write-memory-file.mjs',
521
537
  'scripts/validate-memory.mjs',
522
538
  // project-context.cjs ships as a co-located sibling: the two .mjs
@@ -554,6 +570,7 @@ const MODULES = {
554
570
  'skills/cabinet-narrative-architect', 'skills/cabinet-interactive-storyteller',
555
571
  'skills/cabinet-elegance',
556
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)
557
574
  'scripts/triage-server.mjs', 'scripts/triage-ui.html',
558
575
  'scripts/finding-schema.json', 'scripts/resolve-committees.cjs',
559
576
  'scripts/review-server.mjs', 'scripts/review-ui.html',
@@ -632,9 +649,18 @@ const MODULES = {
632
649
  'cabinet/watchtower-contracts.md',
633
650
  'scripts/watchtower-lib.mjs',
634
651
  'scripts/watchtower-queue.mjs',
652
+ 'scripts/watchtower-routines.mjs',
653
+ 'scripts/watchtower-hook-runner.mjs',
654
+ 'scripts/watchtower-phase-shim.mjs',
655
+ 'scripts/watchtower-sync.mjs',
656
+ 'scripts/watchtower-advisories.mjs',
657
+ 'cabinet/advisories-state-schema.md',
635
658
  'skills/inbox',
636
659
  'hooks/watchtower-session-start.sh',
637
660
  'scripts/watchtower-build-context.mjs',
661
+ 'scripts/watchtower-snapshot.mjs',
662
+ 'scripts/watchtower-narrative-corpus.mjs',
663
+ 'scripts/watchtower-cross-ring-reader.mjs',
638
664
  'scripts/watchtower-ring1.mjs',
639
665
  'scripts/watchtower-ring1-runner.sh',
640
666
  'skills/watchtower',
@@ -651,10 +677,15 @@ const MODULES = {
651
677
  'watchtower/watchtower-ring2-slow.timer',
652
678
  'hooks/watchtower-session-end.sh',
653
679
  'scripts/watchtower-ring3-close.mjs',
680
+ 'scripts/watchtower-ring4.mjs',
681
+ 'scripts/watchtower-ring4-runner.sh',
654
682
  'scripts/watchtower-status.sh',
655
683
  'skills/briefing',
656
684
  'skills/threads',
685
+ 'skills/catch-up',
657
686
  'skills/qa-handoff',
687
+ 'skills/qa-drain',
688
+ 'skills/close',
658
689
  ],
659
690
  },
660
691
  mux: {
@@ -703,6 +734,94 @@ function walkDir(dir, base) {
703
734
  return results;
704
735
  }
705
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
+
706
825
  // Signals that a directory contains a real project (not just empty)
707
826
  const PROJECT_SIGNALS = [
708
827
  'package.json', 'Cargo.toml', 'requirements.txt', 'pyproject.toml',
@@ -725,6 +844,13 @@ function detectProjectState(dir) {
725
844
  return 'empty';
726
845
  }
727
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
+
728
854
  function parseArgs(argv) {
729
855
  const args = argv.slice(2);
730
856
  const flags = {
@@ -747,11 +873,32 @@ function parseArgs(argv) {
747
873
  else if (arg === '--help' || arg === '-h') flags.help = true;
748
874
  else if (arg === '--reset') flags.reset = true;
749
875
  else if (arg === '--force') flags.force = true;
876
+ else if (arg === '--reindex') flags.reindex = true;
750
877
  else if (arg === '--migrate-memory') flags.migrateMemory = true;
751
878
  else if (arg === '--unmigrate-memory') flags.unmigrateMemory = true;
879
+ else if (arg === '--list-frozen') flags.listFrozen = true;
752
880
  else if (arg === '--modules' && i + 1 < args.length) {
753
881
  flags.modules = args[++i].split(',').map(s => s.trim()).filter(Boolean);
754
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
+ }
755
902
  else if (arg === '--frontier-model' && i + 1 < args.length) {
756
903
  // Empty/whitespace values are treated as absent: '' is a substring of
757
904
  // every model ID, which would match everything and leave the watchdog
@@ -777,6 +924,16 @@ function printHelp() {
777
924
  --modules <keys> Comma-separated module keys to install (e.g., 'verify').
778
925
  Mandatory modules are always included.
779
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.
780
937
  --reset Remove Claude Cabinet files (uses manifest for safety)
781
938
  --force With --reset: remove even customized files;
782
939
  with --migrate-memory: override already-migrated guard
@@ -818,6 +975,80 @@ async function run() {
818
975
  return;
819
976
  }
820
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
+
821
1052
  if (flags.migrateMemory) {
822
1053
  const { migrateMemoryCmd } = require('./migrate-memory-cmd');
823
1054
  const projectDir = path.resolve(flags.targetDir);
@@ -1146,18 +1377,24 @@ async function run() {
1146
1377
  return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
1147
1378
  }
1148
1379
 
1149
- // Compute the relative path from projectDir for manifest entries
1150
- function manifestPath(tmpl) {
1151
- if (tmpl.startsWith('skills/') || tmpl.startsWith('hooks/') || tmpl.startsWith('rules/')) {
1152
- return '.claude/' + tmpl;
1153
- } else if (tmpl.startsWith('scripts/')) {
1154
- return tmpl;
1155
- }
1156
- return '.claude/' + tmpl;
1157
- }
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
1158
1394
 
1159
1395
  for (const modKey of selectedModules) {
1160
1396
  const mod = MODULES[modKey];
1397
+ const frozen = effectiveFrozen.has(modKey);
1161
1398
  for (const tmpl of mod.templates) {
1162
1399
  const srcPath = path.join(templateRoot, tmpl);
1163
1400
  if (!fs.existsSync(srcPath)) {
@@ -1206,7 +1443,9 @@ async function run() {
1206
1443
  projectRoot: projectDir,
1207
1444
  existingManifest,
1208
1445
  instructionPhases: instrPhases,
1446
+ frozen,
1209
1447
  });
1448
+ if (results.frozen.length) frozenCounts[modKey] = (frozenCounts[modKey] || 0) + results.frozen.length;
1210
1449
  totalCopied += results.copied.length;
1211
1450
  totalSkipped += results.skipped.length;
1212
1451
  totalOverwritten += results.overwritten.length;
@@ -1251,6 +1490,22 @@ async function run() {
1251
1490
  continue;
1252
1491
  }
1253
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
+
1254
1509
  if (flags.yes || dirState === 'existing-install') {
1255
1510
  // If file is in the old manifest, it's upstream-managed — overwrite.
1256
1511
  // If not, it's project-created — skip.
@@ -1293,6 +1548,14 @@ async function run() {
1293
1548
  }
1294
1549
  }
1295
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
+ }
1296
1559
  if (!flags.dryRun) fs.copyFileSync(srcPath, destPath);
1297
1560
  totalCopied++;
1298
1561
  allManifest[mPath] = incomingHash;
@@ -1303,6 +1566,28 @@ async function run() {
1303
1566
 
1304
1567
  console.log(` 📁 Files: ${totalCopied} copied, ${totalOverwritten} overwritten, ${totalSkipped} unchanged`);
1305
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
+
1306
1591
  // --- Make hook scripts executable ---
1307
1592
  const hooksDir = path.join(claudeDir, 'hooks');
1308
1593
  if (!flags.dryRun && fs.existsSync(hooksDir)) {
@@ -1373,10 +1658,34 @@ async function run() {
1373
1658
  }
1374
1659
 
1375
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;
1376
1665
  if (includeDb && selectedModules.includes('work-tracking') && !flags.dryRun) {
1377
1666
  try {
1378
- const dbResults = setupDb(projectDir);
1379
- 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;
1380
1689
  } catch (err) {
1381
1690
  console.log(` ⚠ Database setup failed: ${err.message}`);
1382
1691
  console.log(' You can set it up later: node scripts/pib-db.mjs init');
@@ -1411,6 +1720,27 @@ async function run() {
1411
1720
  }
1412
1721
  }
1413
1722
 
1723
+ // --- Refresh the GLOBAL watchtower runtime (content-aware) ---
1724
+ // The watchtower module copies its files into the PROJECT, but the global
1725
+ // runtime at ~/.claude-cabinet/watchtower/ is set up only by the one-time
1726
+ // `/watchtower install` SKILL.md step. Without this, a reinstall left the
1727
+ // global runtime scripts/docs/hooks stale and never delivered newly shipped
1728
+ // ones. refreshWatchtowerRuntime is REFRESH-ONLY: it no-ops (status
1729
+ // 'absent', zero writes) when no runtime exists yet, so it's safe to call
1730
+ // unconditionally whenever the module is selected.
1731
+ if (selectedModules.includes('watchtower')) {
1732
+ try {
1733
+ const result = refreshWatchtowerRuntime({ dryRun: !!flags.dryRun });
1734
+ if (result.status !== 'absent') {
1735
+ console.log('');
1736
+ for (const r of result.results || []) console.log(` 📋 ${r}`);
1737
+ }
1738
+ } catch (err) {
1739
+ console.log(` ⚠ watchtower runtime refresh failed: ${err.message}`);
1740
+ console.log(' Re-run the installer to retry.');
1741
+ }
1742
+ }
1743
+
1414
1744
  // --- Manifest key migration (act:d1f16bee) ---
1415
1745
  // When CC renames directories (e.g., perspectives/ → cabinet-*/), old manifest
1416
1746
  // keys no longer match new template paths. Migrate keys BEFORE cleanup so the
@@ -1461,78 +1791,26 @@ async function run() {
1461
1791
  // S3: Itemize — list all deletions and confirm before proceeding
1462
1792
  // S4: Backup — copy files to .cc-backup/<timestamp>/ before deletion
1463
1793
  if (Object.keys(existingManifest).length > 0) {
1464
- // S1: Build complete template path set (ALL modules) for classification
1465
- const allTemplatePaths = new Set();
1466
- for (const mod of Object.values(MODULES)) {
1467
- for (const tmpl of mod.templates || []) {
1468
- const srcPath = path.join(templateRoot, tmpl);
1469
- if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) {
1470
- for (const rel of walkDir(srcPath)) {
1471
- allTemplatePaths.add(manifestPath(tmpl) + '/' + rel);
1472
- }
1473
- } else if (fs.existsSync(srcPath)) {
1474
- allTemplatePaths.add(manifestPath(tmpl));
1475
- }
1476
- }
1477
- }
1478
-
1479
- // S2: Build selected-module template paths for scoping
1480
- const selectedTemplatePaths = new Set();
1481
- for (const modKey of selectedModules) {
1482
- const mod = MODULES[modKey];
1483
- for (const tmpl of mod.templates || []) {
1484
- const srcPath = path.join(templateRoot, tmpl);
1485
- if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) {
1486
- for (const rel of walkDir(srcPath)) {
1487
- selectedTemplatePaths.add(manifestPath(tmpl) + '/' + rel);
1488
- }
1489
- } else if (fs.existsSync(srcPath)) {
1490
- selectedTemplatePaths.add(manifestPath(tmpl));
1491
- }
1492
- }
1493
- }
1494
-
1495
- // Build CC template skill name set for project-skill guard (act:4ac281ba)
1496
- const ccSkillNames = new Set();
1497
- for (const mod of Object.values(MODULES)) {
1498
- for (const tmpl of mod.templates || []) {
1499
- if (tmpl.startsWith('skills/')) {
1500
- const skillName = tmpl.split('/')[1];
1501
- ccSkillNames.add(skillName);
1502
- }
1503
- }
1504
- }
1505
-
1506
- // Collect files that would be removed
1507
- const toRemove = [];
1508
- for (const oldPath of Object.keys(existingManifest)) {
1509
- if (!allManifest[oldPath]) {
1510
- // Skip phase files — they may be user-customized
1511
- if (/\/phases\//.test(oldPath)) continue;
1512
-
1513
- // S1: Only delete if the path maps to a known CC template
1514
- if (!allTemplatePaths.has(oldPath)) {
1515
- console.log(` Keeping non-template file: ${oldPath}`);
1516
- continue;
1517
- }
1518
-
1519
- // S2: Only delete from selected modules (don't purge deselected modules)
1520
- if (!selectedTemplatePaths.has(oldPath)) {
1521
- continue;
1522
- }
1523
-
1524
- // Project-skill guard: never delete skills CC didn't create
1525
- const skillDirMatch = oldPath.match(/^\.claude\/skills\/([^/]+)\//);
1526
- if (skillDirMatch && !ccSkillNames.has(skillDirMatch[1])) {
1527
- console.log(` Keeping project skill: ${oldPath}`);
1528
- continue;
1529
- }
1530
-
1531
- const fullPath = path.join(projectDir, oldPath);
1532
- if (fs.existsSync(fullPath)) {
1533
- toRemove.push(oldPath);
1534
- }
1535
- }
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}`);
1536
1814
  }
1537
1815
 
1538
1816
  // S3: Itemize and confirm before deleting
@@ -1603,6 +1881,7 @@ async function run() {
1603
1881
  skipped: skippedModules,
1604
1882
  version: VERSION,
1605
1883
  manifest: allManifest,
1884
+ dbSchemaVersion,
1606
1885
  });
1607
1886
  console.log(' 📝 Created .ccrc.json');
1608
1887
  }
@@ -1658,9 +1937,13 @@ async function run() {
1658
1937
  } else {
1659
1938
  console.log(' 📋 Registered in project registry');
1660
1939
  }
1661
- // 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).
1662
1943
  if (registry.frontierModel) {
1663
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>');
1664
1947
  }
1665
1948
  } catch (err) {
1666
1949
  // Non-fatal — registry is nice-to-have
@@ -1684,4 +1967,4 @@ async function run() {
1684
1967
  console.log('');
1685
1968
  }
1686
1969
 
1687
- module.exports = { run, MODULES, generateAgentWrappers };
1970
+ module.exports = { run, MODULES, generateAgentWrappers, generateSkillIndex, expandFrozenClosure, computeRemovals, manifestPath };