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
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');
7
- const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks } = require('./settings-merge');
8
- const { create: createMetadata, read: readMetadata } = require('./metadata');
9
- const { setupDb } = require('./db-setup');
6
+ const { copyTemplates, recordSkip, isProjectOwnedSeed, classifyFileOwnership } = require('./copy');
7
+ const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks, mergeCcDevHooks, mergeSkillOverrides } = require('./settings-merge');
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');
@@ -442,255 +442,10 @@ function generateAgentWrappers(projectDir) {
442
442
  return wanted.size;
443
443
  }
444
444
 
445
- // MODULES is the manifest: every template path here is copied into a
446
- // consumer's project on install. A skill/hook/script that exists under
447
- // templates/ but is NOT listed in any module never ships — that is the
448
- // orphan bug that left the whole built-in-memory layer uninstalled
449
- // before v0.27.2 (test/manifest-integrity guards against recurrence).
450
- //
451
- // Intentional orphans (maintainer-only, must NOT ship to consumers) are
452
- // allowlisted in test/manifest-integrity. Currently: skills/cc-publish
453
- // (CC-source-repo release tooling, like scripts/migrate-all-consumers.js).
454
- const MODULES = {
455
- 'session-loop': {
456
- name: 'Session Loop (orient + debrief)',
457
- description: 'The briefing cycle. Claude starts each session informed, ends by preparing the next briefing.',
458
- mandatory: true,
459
- templates: [
460
- 'skills/orient',
461
- 'skills/orient-quick',
462
- 'skills/debrief',
463
- 'skills/debrief-quick',
464
- 'skills/session-handoff',
465
- // Instruction phases — always ship, overriding the default skip-phases rule in copy.js
466
- 'skills/debrief/phases/audit-pattern-capture.md',
467
- 'skills/debrief/phases/methodology-capture.md',
468
- 'skills/debrief/phases/record-lessons.md',
469
- 'skills/debrief/phases/upstream-feedback.md',
470
- 'skills/debrief/phases/qa-handoff-sweep.md',
471
- 'skills/menu',
472
- ],
473
- },
474
- 'hooks': {
475
- name: 'Git Guardrails + Telemetry',
476
- description: 'Block destructive git ops (force push, hard reset). Track skill usage via JSONL telemetry.',
477
- mandatory: false,
478
- default: true,
479
- lean: true,
480
- templates: ['hooks/git-guardrails.sh', 'hooks/cc-upstream-guard.sh', 'hooks/skill-telemetry.sh', 'hooks/skill-tool-telemetry.sh', 'hooks/work-tracker-guard.sh', 'hooks/action-quality-gate.sh', 'hooks/action-completion-gate.sh', 'hooks/memory-index-guard.sh', 'scripts/cc-drift-check.cjs', 'scripts/skill-usage.mjs'],
481
- },
482
- 'work-tracking': {
483
- name: 'Work Tracking (pib-db or markdown)',
484
- description: 'Track work items for orient/debrief. Default: SQLite (pib-db). Also supports markdown (tasks.md) or external systems. Choice made during /onboard.',
485
- mandatory: false,
486
- default: true,
487
- 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'],
489
- needsDb: true,
490
- },
491
- 'planning': {
492
- name: 'Planning + Execution (plan + execute)',
493
- description: 'Structured planning with cabinet critique — members weigh in before you build.',
494
- mandatory: false,
495
- default: true,
496
- 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'],
498
- },
499
- 'compliance': {
500
- name: 'Compliance Stack (rules + enforcement)',
501
- description: 'Scoped instructions that load by path. Enforcement pipeline for promoting patterns to rules.',
502
- mandatory: false,
503
- default: true,
504
- 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'],
506
- },
507
- 'memory': {
508
- name: 'Built-In Memory (cc-remember + reader + validator)',
509
- description: 'Curated write/validate layer over Claude Code\'s built-in file memory. /cc-remember writes indexed memories, /memory browses them, validate-memory.mjs guards MEMORY.md integrity. Replaced the retired omega engine in v0.27.',
510
- mandatory: false,
511
- default: true,
512
- // lean:true is load-bearing — it keeps the skill triad, scripts, and
513
- // rule installing together as an atomic unit. compliance is lean:false,
514
- // so the memory-capture rule must NOT live there or lean installs get a
515
- // partial, incoherent set.
516
- lean: true,
517
- templates: [
518
- 'skills/cc-remember',
519
- 'skills/memory',
520
- 'rules/memory-capture.md',
521
- 'scripts/write-memory-file.mjs',
522
- 'scripts/validate-memory.mjs',
523
- // project-context.cjs ships as a co-located sibling: the two .mjs
524
- // scripts require('./project-context.cjs') and consumers have no lib/.
525
- 'scripts/project-context.cjs',
526
- ],
527
- },
528
- 'audit': {
529
- name: 'Audit Loop (audit + triage + cabinet)',
530
- description: '27 expert cabinet members review your project. Convene the full cabinet or just one committee.',
531
- mandatory: false,
532
- default: true,
533
- lean: true,
534
- templates: [
535
- 'skills/audit', 'skills/pulse', 'skills/triage-audit', 'skills/cabinet',
536
- 'cabinet', 'briefing',
537
- 'skills/cabinet-accessibility', 'skills/cabinet-anti-confirmation',
538
- 'skills/cabinet-architecture', 'skills/cabinet-automation',
539
- 'skills/cabinet-boundary-man',
540
- 'skills/cabinet-anthropic-insider', 'skills/cabinet-cc-health',
541
- 'skills/cabinet-data-integrity',
542
- 'skills/cabinet-debugger', 'skills/cabinet-deployment',
543
- 'skills/cabinet-historian',
544
- 'skills/cabinet-organized-mind', 'skills/cabinet-process-therapist',
545
- 'skills/cabinet-qa', 'skills/cabinet-record-keeper',
546
- 'skills/cabinet-roster-check', 'skills/cabinet-security',
547
- 'skills/cabinet-seo',
548
- 'skills/cabinet-small-screen', 'skills/cabinet-speed-freak',
549
- 'skills/cabinet-system-advocate', 'skills/cabinet-technical-debt',
550
- 'skills/cabinet-usability', 'skills/cabinet-workflow-cop',
551
- 'skills/cabinet-framework-quality', 'skills/cabinet-goal-alignment',
552
- 'skills/cabinet-information-design', 'skills/cabinet-mantine-quality',
553
- 'skills/cabinet-ui-experimentalist', 'skills/cabinet-user-advocate',
554
- 'skills/cabinet-vision',
555
- 'skills/cabinet-narrative-architect', 'skills/cabinet-interactive-storyteller',
556
- 'skills/cabinet-elegance',
557
- 'scripts/merge-findings.js', 'scripts/load-triage-history.js',
558
- 'scripts/triage-server.mjs', 'scripts/triage-ui.html',
559
- 'scripts/finding-schema.json', 'scripts/resolve-committees.cjs',
560
- 'scripts/review-server.mjs', 'scripts/review-ui.html',
561
- 'workflows/deliberative-audit.js', 'cabinet/critique-contract.md',
562
- ],
563
- },
564
- 'lifecycle': {
565
- name: 'Lifecycle (onboard + seed + cc-upgrade + link)',
566
- description: 'Onboarding prepares the briefings. Seed proposes new cabinet members when you adopt new tech.',
567
- mandatory: false,
568
- default: true,
569
- lean: true,
570
- templates: ['skills/onboard', 'skills/seed', 'skills/cc-upgrade', 'skills/cc-link', 'skills/cc-unlink', 'skills/cc-extract', 'skills/cc-feedback', 'cabinet/elicitation-methods.md'],
571
- },
572
- 'validate': {
573
- name: 'Validate',
574
- description: 'Structural validation checks with unified summary. Define your own validators.',
575
- mandatory: false,
576
- default: true,
577
- lean: false,
578
- templates: ['skills/validate', 'scripts/skill-validator.sh'],
579
- },
580
- 'verify': {
581
- name: 'Verification Harness (Cucumber + Playwright)',
582
- description: 'Walkthrough verification with human-in-the-loop verdict pauses. Replaces flat AC lists with re-runnable user-journey scenarios. Includes /verify skeleton skill + cabinet-verify npm runtime + opt-in integration phases for /plan, /execute, /debrief.',
583
- mandatory: false,
584
- default: false,
585
- lean: false,
586
- templates: [
587
- 'skills/verify',
588
- 'skills/plan/phases/verify-plan.md',
589
- 'skills/execute/phases/verify-emit.md',
590
- 'skills/debrief/phases/verify-coverage.md',
591
- 'skills/orient/phases/verify-backfill.md',
592
- ],
593
- postInstall: 'verify-setup',
594
- },
595
- 'site-audit': {
596
- name: 'Site Audit (deployed-site quality)',
597
- description: '14-check audit for deployed websites: performance, accessibility (3 engines), security, SEO, DNS, privacy, sustainability. Standalone HTML report + comparison mode.',
598
- mandatory: false,
599
- default: false,
600
- lean: false,
601
- templates: [
602
- 'skills/cc-site-audit',
603
- 'site-audit-runtime',
604
- ],
605
- postInstall: 'site-audit-setup',
606
- },
607
- engagement: {
608
- name: 'Engagement management (with secure credential handoff)',
609
- description: 'Ongoing client-engagement management built on pib-db: per-recipient packets (rendered projections of the work backlog), role-gated billing, client feedback flowing back as events, and secure credential handoff (encrypted capture via OS dialog, pluggable email/MCP/file transport). Three primary skills across consultant and client sides, plus redirect stubs. Requires work-tracking (the pib-db adapter is the engine\'s data source).',
610
- mandatory: false,
611
- default: false,
612
- lean: false,
613
- requires: ['work-tracking'],
614
- postInstall: 'engagement-setup',
615
- templates: [
616
- 'skills/collab-client',
617
- 'skills/collab-consultant',
618
- 'skills/engagement-help',
619
- 'skills/engagement-create',
620
- 'skills/setup-accounts',
621
- 'skills/guide',
622
- 'engagement',
623
- ],
624
- },
625
- watchtower: {
626
- name: 'Watchtower (continuous background state)',
627
- description: 'Replaces orient/debrief with continuous background processing. Three rings (mechanical cron, Claude intelligence, session-aware), ambient state injection via SessionStart hook, and an inbox for extracted knowledge and signals. Sessions start informed with minimal context cost.',
628
- mandatory: false,
629
- default: false,
630
- lean: false,
631
- templates: [
632
- 'scripts/watchtower-validate.mjs',
633
- 'cabinet/watchtower-contracts.md',
634
- 'scripts/watchtower-lib.mjs',
635
- 'scripts/watchtower-queue.mjs',
636
- 'scripts/watchtower-routines.mjs',
637
- 'scripts/watchtower-advisories.mjs',
638
- 'cabinet/advisories-state-schema.md',
639
- 'skills/inbox',
640
- 'hooks/watchtower-session-start.sh',
641
- 'scripts/watchtower-build-context.mjs',
642
- 'scripts/watchtower-ring1.mjs',
643
- 'scripts/watchtower-ring1-runner.sh',
644
- 'skills/watchtower',
645
- 'watchtower/com.claude-cabinet.watchtower-ring1.plist',
646
- 'watchtower/watchtower-ring1.service',
647
- 'watchtower/watchtower-ring1.timer',
648
- 'scripts/watchtower-ring2.mjs',
649
- 'scripts/watchtower-ring2-runner.sh',
650
- 'watchtower/com.claude-cabinet.watchtower-ring2-fast.plist',
651
- 'watchtower/com.claude-cabinet.watchtower-ring2-slow.plist',
652
- 'watchtower/watchtower-ring2-fast.service',
653
- 'watchtower/watchtower-ring2-fast.timer',
654
- 'watchtower/watchtower-ring2-slow.service',
655
- 'watchtower/watchtower-ring2-slow.timer',
656
- 'hooks/watchtower-session-end.sh',
657
- 'scripts/watchtower-ring3-close.mjs',
658
- 'scripts/watchtower-status.sh',
659
- 'skills/briefing',
660
- 'skills/threads',
661
- 'skills/qa-handoff',
662
- 'skills/qa-drain',
663
- ],
664
- },
665
- mux: {
666
- name: 'Mux (tmux project manager)',
667
- description: 'Multi-project terminal manager. Desks (tmux sessions), auto-worktrees, trail logging, sticky notes, portal color-switching, and an MCP server for Claude. Installs to user-level paths (~/.local/bin/, ~/.config/mux/).',
668
- mandatory: false,
669
- default: false,
670
- lean: false,
671
- postInstall: 'mux-setup',
672
- templates: ['skills/orient/phases/dx-captures.md', 'skills/dx-feedback'],
673
- },
674
- 'engagement-server': {
675
- name: 'Engagement Server',
676
- description: 'Central multi-engagement API server. Deploys once to Railway/Fly, serves all client engagements. User-level infrastructure.',
677
- mandatory: false,
678
- default: false,
679
- lean: false,
680
- requires: ['engagement', 'work-tracking'],
681
- postInstall: 'engagement-server-setup',
682
- templates: [],
683
- },
684
- 'bash-compress': {
685
- name: 'Bash Output Compression Hook',
686
- description: 'PostToolUse hook that compresses noisy Bash stdout (git status walls, npm/yarn install output, find/ls dumps) to reclaim context in long sessions. Off by default. stderr and error/warning lines pass through verbatim; every rewrite carries a visible [compressed] marker; fail-open on any error. Requires the hooks module (it wires into .claude/settings.json).',
687
- mandatory: false,
688
- default: false,
689
- lean: false,
690
- requires: ['hooks'],
691
- templates: ['hooks/bash-output-compress.sh'],
692
- },
693
- };
445
+ // The MODULES manifest (the module template-path registry) lives in its
446
+ // own file so this ~2000-line orchestrator stays navigable (architecture-0005).
447
+ // Re-exported below for back-compat with importers of `require('../lib/cli').MODULES`.
448
+ const { MODULES } = require('./modules');
694
449
 
695
450
  /** Recursively collect all relative file paths under a directory. */
696
451
  function walkDir(dir, base) {
@@ -708,6 +463,94 @@ function walkDir(dir, base) {
708
463
  return results;
709
464
  }
710
465
 
466
+ // Map a module template path to its install-time manifest key (the path
467
+ // relative to the project root). Top-level so both the install copy loop and
468
+ // computeRemovals derive the key the same way — single source of truth.
469
+ function manifestPath(tmpl) {
470
+ if (tmpl.startsWith('scripts/')) return tmpl;
471
+ return '.claude/' + tmpl;
472
+ }
473
+
474
+ /**
475
+ * Expand a declared frozen-module list by its `requires` closure (act:c1013967).
476
+ * Freezing a module must also pin the modules it depends on, or a frozen
477
+ * caller can be left bound to a refreshed callee contract (the cross-runtime
478
+ * version-skew failure: green in the dev tree, broken on a fresh install).
479
+ * E.g. freezing `engagement` (requires work-tracking) also pins work-tracking.
480
+ * Returns a Set of module keys. Unknown keys are ignored (validated elsewhere).
481
+ */
482
+ function expandFrozenClosure(frozenKeys, modules = MODULES) {
483
+ const closure = new Set();
484
+ const queue = [...(frozenKeys || [])];
485
+ while (queue.length) {
486
+ const key = queue.shift();
487
+ if (closure.has(key)) continue;
488
+ closure.add(key);
489
+ const mod = modules[key];
490
+ for (const req of (mod && mod.requires) || []) {
491
+ if (!closure.has(req)) queue.push(req);
492
+ }
493
+ }
494
+ return closure;
495
+ }
496
+
497
+ /**
498
+ * Classify which manifest entries should be removed because they are no longer
499
+ * shipped upstream — the pure core of the cleanup/reconcile loop, extracted so
500
+ * it is unit-testable in isolation (the inline loop never was). Returns
501
+ * `{ toRemove, kept }`: `toRemove` is the list of project-relative paths whose
502
+ * files exist on disk and are safe to delete; `kept` carries `{path, reason}`
503
+ * for files deliberately spared, which the caller logs.
504
+ *
505
+ * The four safeguards from the original loop are preserved exactly: S1 only
506
+ * delete known-template paths; S2 only from selected modules; project-owned
507
+ * seeds and non-CC project skills are never deleted. A FROZEN file needs no
508
+ * special case here — it was RETAINED in `allManifest` (at its on-disk hash),
509
+ * so it never reaches the `!allManifest[oldPath]` test; a frozen file removed
510
+ * upstream is kept by S1 ("non-template").
511
+ */
512
+ function computeRemovals({ existingManifest, allManifest, selectedModules, templateRoot, projectDir, modules = MODULES }) {
513
+ const collectPaths = (modKeys) => {
514
+ const set = new Set();
515
+ for (const modKey of modKeys) {
516
+ const mod = modules[modKey];
517
+ for (const tmpl of (mod && mod.templates) || []) {
518
+ const srcPath = path.join(templateRoot, tmpl);
519
+ if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) {
520
+ for (const rel of walkDir(srcPath)) set.add(manifestPath(tmpl) + '/' + rel);
521
+ } else if (fs.existsSync(srcPath)) {
522
+ set.add(manifestPath(tmpl));
523
+ }
524
+ }
525
+ }
526
+ return set;
527
+ };
528
+
529
+ const allTemplatePaths = collectPaths(Object.keys(modules));
530
+ const selectedTemplatePaths = collectPaths(selectedModules);
531
+
532
+ const ccSkillNames = new Set();
533
+ for (const mod of Object.values(modules)) {
534
+ for (const tmpl of mod.templates || []) {
535
+ if (tmpl.startsWith('skills/')) ccSkillNames.add(tmpl.split('/')[1]);
536
+ }
537
+ }
538
+
539
+ const toRemove = [];
540
+ const kept = [];
541
+ for (const oldPath of Object.keys(existingManifest)) {
542
+ if (allManifest[oldPath]) continue;
543
+ if (/\/phases\//.test(oldPath)) continue; // user-customizable
544
+ if (isProjectOwnedSeed(oldPath)) { kept.push({ path: oldPath, reason: 'project-owned seed' }); continue; }
545
+ if (!allTemplatePaths.has(oldPath)) { kept.push({ path: oldPath, reason: 'non-template file' }); continue; }
546
+ if (!selectedTemplatePaths.has(oldPath)) continue; // deselected module — leave it
547
+ const skillDirMatch = oldPath.match(/^\.claude\/skills\/([^/]+)\//);
548
+ if (skillDirMatch && !ccSkillNames.has(skillDirMatch[1])) { kept.push({ path: oldPath, reason: 'project skill' }); continue; }
549
+ if (fs.existsSync(path.join(projectDir, oldPath))) toRemove.push(oldPath);
550
+ }
551
+ return { toRemove, kept };
552
+ }
553
+
711
554
  // Signals that a directory contains a real project (not just empty)
712
555
  const PROJECT_SIGNALS = [
713
556
  'package.json', 'Cargo.toml', 'requirements.txt', 'pyproject.toml',
@@ -730,6 +573,13 @@ function detectProjectState(dir) {
730
573
  return 'empty';
731
574
  }
732
575
 
576
+ // A `--freeze`/`--unfreeze` value token vs the positional target dir. Module
577
+ // keys are simple kebab-case identifiers; a flag or any path-like token
578
+ // (separator, or . / ..) is the target directory, never a module list.
579
+ function looksLikeModuleArg(s) {
580
+ return Boolean(s) && !s.startsWith('-') && !s.includes('/') && !s.includes('\\') && s !== '.' && s !== '..';
581
+ }
582
+
733
583
  function parseArgs(argv) {
734
584
  const args = argv.slice(2);
735
585
  const flags = {
@@ -752,11 +602,32 @@ function parseArgs(argv) {
752
602
  else if (arg === '--help' || arg === '-h') flags.help = true;
753
603
  else if (arg === '--reset') flags.reset = true;
754
604
  else if (arg === '--force') flags.force = true;
605
+ else if (arg === '--reindex') flags.reindex = true;
755
606
  else if (arg === '--migrate-memory') flags.migrateMemory = true;
756
607
  else if (arg === '--unmigrate-memory') flags.unmigrateMemory = true;
608
+ else if (arg === '--list-frozen') flags.listFrozen = true;
757
609
  else if (arg === '--modules' && i + 1 < args.length) {
758
610
  flags.modules = args[++i].split(',').map(s => s.trim()).filter(Boolean);
759
611
  }
612
+ // --freeze <mods> / --unfreeze [mods]: edit .ccrc.json frozenModules and
613
+ // exit. Consume the next token as the module list ONLY when it looks like
614
+ // one — a flag, or a path-like token (contains a separator, or is . / ..),
615
+ // is the target directory, not a value. Module keys are simple kebab-case
616
+ // identifiers and never contain a path separator, so this can't swallow a
617
+ // real module while it does stop `--unfreeze /path/to/project` from being
618
+ // misread as a module (which would silently fail to clear-all). An empty
619
+ // --freeze is an error (dispatch warns, never a silent no-op); an empty
620
+ // --unfreeze clears all.
621
+ else if (arg === '--freeze') {
622
+ flags.freeze = looksLikeModuleArg(args[i + 1])
623
+ ? args[++i].split(',').map(s => s.trim()).filter(Boolean)
624
+ : [];
625
+ }
626
+ else if (arg === '--unfreeze') {
627
+ flags.unfreeze = looksLikeModuleArg(args[i + 1])
628
+ ? args[++i].split(',').map(s => s.trim()).filter(Boolean)
629
+ : []; // clear all
630
+ }
760
631
  else if (arg === '--frontier-model' && i + 1 < args.length) {
761
632
  // Empty/whitespace values are treated as absent: '' is a substring of
762
633
  // every model ID, which would match everything and leave the watchdog
@@ -782,6 +653,16 @@ function printHelp() {
782
653
  --modules <keys> Comma-separated module keys to install (e.g., 'verify').
783
654
  Mandatory modules are always included.
784
655
  --dry-run Show what would be copied without writing
656
+ --reindex Regenerate .claude/skills/_index.json in place (picks up
657
+ directives-project.yaml mandate/routine edits without a full
658
+ reinstall) and exit
659
+ --freeze <keys> Pin module(s) (comma-separated) in .ccrc.json so the
660
+ NEXT reinstall holds their CC-owned files at the current
661
+ version (e.g. --freeze engagement while a client engagement
662
+ is live). Also pins the requires-closure. Edits config, exits.
663
+ --unfreeze [keys] Clear the freeze for the given module(s), or all if no
664
+ keys. Re-run the installer to refresh the unfrozen module(s).
665
+ --list-frozen Show which modules are frozen (+ their closure) and exit.
785
666
  --reset Remove Claude Cabinet files (uses manifest for safety)
786
667
  --force With --reset: remove even customized files;
787
668
  with --migrate-memory: override already-migrated guard
@@ -823,6 +704,80 @@ async function run() {
823
704
  return;
824
705
  }
825
706
 
707
+ // Standalone skill-index regen (act:98d41fa2). Advisor standing-mandates and
708
+ // routine declarations are read from .claude/skills/_index.json, which is
709
+ // otherwise regenerated only by a full install — coupling every overlay
710
+ // change (e.g. adding a session-close mandate to directives-project.yaml) to
711
+ // an upgrade cycle. This regenerates the index in place so a change is
712
+ // verifiable immediately, no reinstall needed.
713
+ if (flags.reindex) {
714
+ const projectDir = path.resolve(flags.targetDir);
715
+ if (!fs.existsSync(path.join(projectDir, '.claude', 'skills'))) {
716
+ console.log(` No .claude/skills/ in ${projectDir} — nothing to reindex.`);
717
+ return;
718
+ }
719
+ const n = generateSkillIndex(projectDir);
720
+ console.log(` 📇 Reindexed ${n} skill${n === 1 ? '' : 's'} in .claude/skills/_index.json`);
721
+ return;
722
+ }
723
+
724
+ // Standalone freeze management (act:c1013967) — edit .ccrc.json frozenModules
725
+ // and exit, no install. Freeze pins a module's CC-owned files across a
726
+ // reinstall (the "reinstall maginnis without disturbing Ed's engagement" fix);
727
+ // the install body reads frozenModules + expands the requires closure.
728
+ if (flags.listFrozen || flags.freeze || flags.unfreeze) {
729
+ const projectDir = path.resolve(flags.targetDir);
730
+ const current = readFrozenModules(projectDir);
731
+
732
+ const describe = (mods) => {
733
+ if (mods.length === 0) { console.log(' No modules are frozen.'); return; }
734
+ const closure = expandFrozenClosure(mods, MODULES);
735
+ const pulledIn = [...closure].filter(k => !mods.includes(k));
736
+ console.log(` ❄ Frozen modules: ${mods.join(', ')}`);
737
+ if (pulledIn.length) console.log(` + required closure (also pinned): ${pulledIn.join(', ')}`);
738
+ console.log(' A reinstall holds these modules’ CC-owned files at the current version.');
739
+ };
740
+
741
+ if (flags.listFrozen) {
742
+ describe(current);
743
+ return;
744
+ }
745
+
746
+ if (flags.freeze) {
747
+ if (flags.freeze.length === 0) {
748
+ console.log(' ✗ --freeze needs at least one module key, e.g. --freeze engagement');
749
+ return;
750
+ }
751
+ const known = Object.keys(MODULES);
752
+ const unknown = flags.freeze.filter(m => !known.includes(m));
753
+ if (unknown.length) console.log(` ⚠ Unknown module key(s) ignored: ${unknown.join(', ')} (known: ${known.join(', ')})`);
754
+ const valid = flags.freeze.filter(m => known.includes(m));
755
+ const next = [...new Set([...current, ...valid])];
756
+ if (flags.dryRun) {
757
+ console.log(` [dry run] Would set frozenModules to: ${next.join(', ') || '(none)'}`);
758
+ } else {
759
+ setFrozenModules(projectDir, next);
760
+ console.log(' ✓ Updated .ccrc.json frozenModules.');
761
+ }
762
+ describe(next);
763
+ return;
764
+ }
765
+
766
+ if (flags.unfreeze) {
767
+ const next = flags.unfreeze.length === 0
768
+ ? [] // --unfreeze → clear all
769
+ : current.filter(m => !flags.unfreeze.includes(m));
770
+ if (flags.dryRun) {
771
+ console.log(` [dry run] Would set frozenModules to: ${next.join(', ') || '(none)'}`);
772
+ } else {
773
+ setFrozenModules(projectDir, next);
774
+ console.log(' ✓ Updated .ccrc.json frozenModules. Re-run the installer to refresh the unfrozen module(s).');
775
+ }
776
+ describe(next);
777
+ return;
778
+ }
779
+ }
780
+
826
781
  if (flags.migrateMemory) {
827
782
  const { migrateMemoryCmd } = require('./migrate-memory-cmd');
828
783
  const projectDir = path.resolve(flags.targetDir);
@@ -1151,18 +1106,24 @@ async function run() {
1151
1106
  return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
1152
1107
  }
1153
1108
 
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
- }
1109
+ // Frozen modules (act:c1013967): the operator pinned these in .ccrc.json so
1110
+ // a reinstall holds their CC-owned files at the current version. Expand by
1111
+ // the `requires` closure (freezing engagement also pins work-tracking, so a
1112
+ // frozen caller is never left bound to a refreshed callee), then intersect
1113
+ // with what is actually installed. `effectiveFrozen` gates ALL freeze
1114
+ // behavior — never the raw declared list — so a typo'd / uninstalled / dead
1115
+ // key can't silently pin global state or claim protection it isn't giving.
1116
+ const declaredFrozen = readFrozenModules(projectDir);
1117
+ const effectiveFrozen = new Set(
1118
+ [...expandFrozenClosure(declaredFrozen, MODULES)].filter(
1119
+ (k) => MODULES[k] && selectedModules.includes(k)
1120
+ )
1121
+ );
1122
+ const frozenCounts = {}; // modKey -> # files held this install
1163
1123
 
1164
1124
  for (const modKey of selectedModules) {
1165
1125
  const mod = MODULES[modKey];
1126
+ const frozen = effectiveFrozen.has(modKey);
1166
1127
  for (const tmpl of mod.templates) {
1167
1128
  const srcPath = path.join(templateRoot, tmpl);
1168
1129
  if (!fs.existsSync(srcPath)) {
@@ -1211,7 +1172,9 @@ async function run() {
1211
1172
  projectRoot: projectDir,
1212
1173
  existingManifest,
1213
1174
  instructionPhases: instrPhases,
1175
+ frozen,
1214
1176
  });
1177
+ if (results.frozen.length) frozenCounts[modKey] = (frozenCounts[modKey] || 0) + results.frozen.length;
1215
1178
  totalCopied += results.copied.length;
1216
1179
  totalSkipped += results.skipped.length;
1217
1180
  totalOverwritten += results.overwritten.length;
@@ -1229,53 +1192,88 @@ async function run() {
1229
1192
  const incoming = fs.readFileSync(srcPath, 'utf8');
1230
1193
  const incomingHash = hashContent(incoming);
1231
1194
  const mPath = manifestPath(tmpl);
1195
+ const destExists = fs.existsSync(destPath);
1196
+ const existingContent = destExists ? fs.readFileSync(destPath, 'utf8') : null;
1197
+ // Instruction phases explicitly listed in MODULES are CC-owned and must
1198
+ // always overwrite (they're not customizable); other phase files are
1199
+ // preserved when the operator has edited them.
1200
+ const isPhaseFile = tmpl.includes('/phases/') || tmpl.includes('\\phases\\');
1201
+ const isInstructionPhase = isPhaseFile && mod.templates.includes(tmpl);
1202
+
1203
+ // Single decision spine — the ownership cascade lives in copy.js
1204
+ // (classifyFileOwnership) so this branch can't drift from the directory
1205
+ // walk. The leak this closes: the hand-inlined copy here was missing
1206
+ // the project-owned-seed guard (elegance-0002, act:a6377c82).
1207
+ const decision = classifyFileOwnership({
1208
+ relPath: tmpl,
1209
+ destExists,
1210
+ existingContent,
1211
+ incoming,
1212
+ isPhaseFile,
1213
+ isInstructionPhase,
1214
+ frozen,
1215
+ isCcOwned: Boolean(existingManifest[mPath]),
1216
+ skipConflicts: flags.yes || dirState === 'existing-install',
1217
+ });
1232
1218
 
1233
- if (fs.existsSync(destPath)) {
1234
- const existingContent = fs.readFileSync(destPath, 'utf8');
1235
- if (existingContent === incoming) {
1219
+ switch (decision.action) {
1220
+ case 'seed':
1221
+ // Project-owned seed (act:2001bf54): seed if absent, NEVER
1222
+ // overwrite, ALWAYS omit from the manifest.
1223
+ if (!destExists) {
1224
+ if (!flags.dryRun) fs.copyFileSync(srcPath, destPath);
1225
+ totalCopied++;
1226
+ } else {
1227
+ totalSkipped++;
1228
+ }
1229
+ recordSkip(allManifest, mPath);
1230
+ continue;
1231
+ case 'preserve-phase':
1232
+ console.log(` Preserved customized phase: ${tmpl}`);
1233
+ totalSkipped++;
1234
+ recordSkip(allManifest, mPath);
1235
+ continue;
1236
+ case 'skip-identical':
1236
1237
  totalSkipped++;
1237
1238
  recordSkip(allManifest, mPath, { identical: true, incomingHash });
1238
1239
  continue;
1239
- }
1240
-
1241
- // Phase file customization guard — mirrors copy.js:48-59 for the
1242
- // single-file install path. If the destination path is a phase
1243
- // file and the on-disk content differs from upstream (operator
1244
- // edited it), preserve it. This keeps the documented "customize
1245
- // by editing the phase file" affordance intact across upgrades.
1246
- // Exception: instruction phases explicitly listed in MODULES are
1247
- // CC-owned and must always overwrite (they're not customizable).
1248
- const isPhaseFile = tmpl.includes('/phases/') || tmpl.includes('\\phases\\');
1249
- const isInstructionPhase = isPhaseFile && mod.templates.includes(tmpl);
1250
- if (isPhaseFile && !isInstructionPhase && existingContent.trim() !== '' && existingContent.trim() !== incoming.trim()) {
1251
- console.log(` Preserved customized phase: ${tmpl}`);
1240
+ case 'freeze-hold':
1252
1241
  totalSkipped++;
1253
- // Customized phase = project-owned content omit from manifest
1254
- // (recordSkip in copy.js — omission means "not ours").
1242
+ frozenCounts[modKey] = (frozenCounts[modKey] || 0) + 1;
1243
+ allManifest[mPath] = decision.manifestHash;
1244
+ console.log(` ❄ Frozen (held): ${path.relative(projectDir, destPath)}`);
1245
+ continue;
1246
+ case 'overwrite':
1247
+ if (!flags.dryRun) fs.copyFileSync(srcPath, destPath);
1248
+ totalOverwritten++;
1249
+ // Log single-file overwrites too — the directory path (copy.js)
1250
+ // already does. Without this, scripts/ updates are invisible in
1251
+ // install output, masking whether a changed script propagated.
1252
+ console.log(` Updated: ${path.relative(projectDir, destPath)}`);
1253
+ allManifest[mPath] = incomingHash;
1254
+ continue;
1255
+ case 'skip-omit':
1256
+ totalSkipped++;
1257
+ // Project-created file → omit from manifest entirely. Ownership
1258
+ // classification is manifest-PRESENCE-based, so recording ANY hash
1259
+ // here would mark the file upstream-owned and the NEXT install
1260
+ // would silently overwrite it (act:bf21c95b).
1255
1261
  recordSkip(allManifest, mPath);
1256
1262
  continue;
1257
- }
1258
-
1259
- if (flags.yes || dirState === 'existing-install') {
1260
- // If file is in the old manifest, it's upstream-managed overwrite.
1261
- // If not, it's project-created skip.
1262
- if (existingManifest[mPath]) {
1263
- if (!flags.dryRun) fs.copyFileSync(srcPath, destPath);
1264
- totalOverwritten++;
1265
- // Log single-file overwrites too — the directory path (copy.js)
1266
- // already does. Without this, scripts/ updates are invisible in
1267
- // install output, masking whether a changed script propagated.
1268
- console.log(` Updated: ${path.relative(projectDir, destPath)}`);
1269
- allManifest[mPath] = incomingHash;
1270
- } else {
1271
- totalSkipped++;
1272
- // Project-created file → omit from manifest entirely. Ownership
1273
- // classification is manifest-PRESENCE-based, so recording ANY
1274
- // hash here would mark the file upstream-owned and the NEXT
1275
- // install would silently overwrite it (act:bf21c95b).
1276
- recordSkip(allManifest, mPath);
1277
- }
1278
- } else {
1263
+ case 'freeze-hold-new':
1264
+ // New file in a frozen module: hold it back (don't create) so the
1265
+ // module stays internally consistent at its pinned version. Report
1266
+ // only never a manifest entry (no on-disk content to hash).
1267
+ frozenCounts[modKey] = (frozenCounts[modKey] || 0) + 1;
1268
+ console.log(` ❄ Frozen (held new): ${path.relative(projectDir, destPath)}`);
1269
+ continue;
1270
+ case 'copy-new':
1271
+ if (!flags.dryRun) fs.copyFileSync(srcPath, destPath);
1272
+ totalCopied++;
1273
+ allManifest[mPath] = incomingHash;
1274
+ continue;
1275
+ case 'prompt':
1276
+ default: {
1279
1277
  const response = await prompts({
1280
1278
  type: 'select',
1281
1279
  name: 'action',
@@ -1296,11 +1294,8 @@ async function run() {
1296
1294
  // the manifest so it is never mistaken for upstream content.
1297
1295
  recordSkip(allManifest, mPath);
1298
1296
  }
1297
+ continue;
1299
1298
  }
1300
- } else {
1301
- if (!flags.dryRun) fs.copyFileSync(srcPath, destPath);
1302
- totalCopied++;
1303
- allManifest[mPath] = incomingHash;
1304
1299
  }
1305
1300
  }
1306
1301
  }
@@ -1308,6 +1303,28 @@ async function run() {
1308
1303
 
1309
1304
  console.log(` 📁 Files: ${totalCopied} copied, ${totalOverwritten} overwritten, ${totalSkipped} unchanged`);
1310
1305
 
1306
+ // --- Frozen-module report (act:c1013967) ---
1307
+ // Plain-English, per-module: surface WHICH modules were held and how many
1308
+ // files, so a gitignored freeze can never quietly change install behavior.
1309
+ // Note in plain words when the requires-closure pinned a module the operator
1310
+ // didn't name directly.
1311
+ if (effectiveFrozen.size > 0) {
1312
+ const declaredSet = new Set(declaredFrozen);
1313
+ for (const modKey of effectiveFrozen) {
1314
+ const n = frozenCounts[modKey] || 0;
1315
+ let reason = '';
1316
+ if (!declaredSet.has(modKey)) {
1317
+ const namedBy = [...declaredSet].find(d => expandFrozenClosure([d], MODULES).has(modKey) && d !== modKey);
1318
+ if (namedBy) reason = ` (pinned because ${namedBy} requires it)`;
1319
+ }
1320
+ if (n > 0) {
1321
+ console.log(` ❄ Frozen: ${n} file${n === 1 ? '' : 's'} in module '${modKey}'${reason} held at current version — unfreeze to update.`);
1322
+ } else {
1323
+ console.log(` ❄ Frozen: module '${modKey}'${reason} — no CC-owned files differed (nothing to hold this run).`);
1324
+ }
1325
+ }
1326
+ }
1327
+
1311
1328
  // --- Make hook scripts executable ---
1312
1329
  const hooksDir = path.join(claudeDir, 'hooks');
1313
1330
  if (!flags.dryRun && fs.existsSync(hooksDir)) {
@@ -1339,6 +1356,27 @@ async function run() {
1339
1356
  mergeBashCompressHooks(settingsPath);
1340
1357
  console.log(' ⚙️ Registered bash-output compression PostToolUse hook');
1341
1358
  }
1359
+
1360
+ // Repo-local maintainer hooks — no-op unless this IS the CC source repo
1361
+ // (dogfood). Registers the node --check PostToolUse gate durably so a
1362
+ // reinstall can't silently regenerate it away (architecture-0004).
1363
+ if (mergeCcDevHooks(projectDir)) {
1364
+ console.log(' ⚙️ Registered repo-local node --check PostToolUse hook (CC source repo)');
1365
+ }
1366
+
1367
+ // cabinet-* skills are ~half the skill-listing budget and are spawned by
1368
+ // agentType (name), never auto-invoked by description — so set them
1369
+ // "name-only" to free budget for the routing skills that need full trigger
1370
+ // keywords (act:bee0fa16). Only-if-absent, so a user override is preserved.
1371
+ if (selectedModules.includes('audit')) {
1372
+ const cabinetSkills = (MODULES.audit.templates || [])
1373
+ .map((t) => (t.match(/^skills\/(cabinet-[^/]+)$/) || [])[1])
1374
+ .filter(Boolean);
1375
+ const added = mergeSkillOverrides(projectDir, cabinetSkills);
1376
+ if (added > 0) {
1377
+ console.log(` ⚙️ Set ${added} cabinet-* skill${added === 1 ? '' : 's'} to name-only (frees skill-listing budget)`);
1378
+ }
1379
+ }
1342
1380
  }
1343
1381
 
1344
1382
  // --- Heal user-level ~/.claude/settings.json ---
@@ -1378,10 +1416,34 @@ async function run() {
1378
1416
  }
1379
1417
 
1380
1418
  // --- Set up database ---
1419
+ // dbSchemaVersion is recorded into .ccrc.json so a future reinstall can
1420
+ // tell, WITHOUT loading better-sqlite3, that no migration is pending and
1421
+ // skip the init step entirely (lib/db-setup.js shouldSkipInit).
1422
+ let dbSchemaVersion;
1381
1423
  if (includeDb && selectedModules.includes('work-tracking') && !flags.dryRun) {
1382
1424
  try {
1383
- const dbResults = setupDb(projectDir);
1384
- for (const r of dbResults) console.log(` 🗄️ ${r}`);
1425
+ // Freeze insurance (act:c1013967): when a module is frozen and a schema
1426
+ // migration is actually pending, back up pib.db before db-setup runs so
1427
+ // the operator has true one-file rollback for the store holding live
1428
+ // engagement data. Reuses db-setup's own skip decision (no logic
1429
+ // duplication): if work-tracking is itself frozen, its pib-db-lib.mjs
1430
+ // stayed old → bundled == recorded → no migration → no backup. The
1431
+ // existsSync guard means a fresh install (no pib.db yet) still creates
1432
+ // the db normally; there's just nothing to back up.
1433
+ if (effectiveFrozen.size > 0) {
1434
+ const dbPath = path.join(projectDir, 'pib.db');
1435
+ const recordedVersion = (readMetadata(projectDir) || {}).dbSchemaVersion;
1436
+ const bundledVersion = readBundledSchemaVersion(path.join(projectDir, 'scripts'));
1437
+ const willMigrate = !shouldSkipInit({ dbExists: fs.existsSync(dbPath), recordedVersion, bundledVersion });
1438
+ if (willMigrate && fs.existsSync(dbPath)) {
1439
+ const backupPath = `${dbPath}.cc-bak-${new Date().toISOString().replace(/[:.]/g, '-')}`;
1440
+ fs.copyFileSync(dbPath, backupPath);
1441
+ console.log(` ❄ Backed up pib.db → ${path.basename(backupPath)} before migration (freeze active).`);
1442
+ }
1443
+ }
1444
+ const dbResult = setupDb(projectDir);
1445
+ for (const r of dbResult.results) console.log(` 🗄️ ${r}`);
1446
+ if (dbResult.schemaVersion != null) dbSchemaVersion = dbResult.schemaVersion;
1385
1447
  } catch (err) {
1386
1448
  console.log(` ⚠ Database setup failed: ${err.message}`);
1387
1449
  console.log(' You can set it up later: node scripts/pib-db.mjs init');
@@ -1487,78 +1549,26 @@ async function run() {
1487
1549
  // S3: Itemize — list all deletions and confirm before proceeding
1488
1550
  // S4: Backup — copy files to .cc-backup/<timestamp>/ before deletion
1489
1551
  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
- }
1552
+ // Classification is the pure `computeRemovals` (top-level, unit-tested): S1
1553
+ // only known-template paths, S2 only selected modules, plus the
1554
+ // project-owned-seed and project-skill guards. A FROZEN file needs no case
1555
+ // here retain-in-manifest (the freeze copy path) kept it in allManifest,
1556
+ // so it never reaches the removal test; a frozen file removed upstream is
1557
+ // kept by S1 ("non-template"). The S3 itemize/confirm + S4 backup/delete
1558
+ // stay inline below.
1559
+ const { toRemove, kept } = computeRemovals({
1560
+ existingManifest,
1561
+ allManifest,
1562
+ selectedModules,
1563
+ templateRoot,
1564
+ projectDir,
1565
+ });
1566
+ for (const k of kept) {
1567
+ // 'deselected module' files are silently left (matching the prior loop's
1568
+ // bare `continue` for that S2 case); the rest log their keep reason.
1569
+ if (k.reason === 'project-owned seed') console.log(` Keeping project-owned seed: ${k.path}`);
1570
+ else if (k.reason === 'non-template file') console.log(` Keeping non-template file: ${k.path}`);
1571
+ else if (k.reason === 'project skill') console.log(` Keeping project skill: ${k.path}`);
1562
1572
  }
1563
1573
 
1564
1574
  // S3: Itemize and confirm before deleting
@@ -1629,6 +1639,7 @@ async function run() {
1629
1639
  skipped: skippedModules,
1630
1640
  version: VERSION,
1631
1641
  manifest: allManifest,
1642
+ dbSchemaVersion,
1632
1643
  });
1633
1644
  console.log(' 📝 Created .ccrc.json');
1634
1645
  }
@@ -1684,9 +1695,13 @@ async function run() {
1684
1695
  } else {
1685
1696
  console.log(' 📋 Registered in project registry');
1686
1697
  }
1687
- // Self-announcing: print the effective designation on every run.
1698
+ // Self-announcing: print the effective designation on every run, and
1699
+ // the inert-state notice when none is set — a never-configured
1700
+ // watchdog must not read the same as a passing one (act:77f7b7b2).
1688
1701
  if (registry.frontierModel) {
1689
1702
  console.log(` 🛰 Frontier model: ${registry.frontierModel} (visibility watchdog — /orient + SessionStart warn on mismatch; nothing is pinned)`);
1703
+ } else {
1704
+ console.log(' 🛰 Frontier model: none designated — watchdog inert. Set with: npx create-claude-cabinet --frontier-model <value>');
1690
1705
  }
1691
1706
  } catch (err) {
1692
1707
  // Non-fatal — registry is nice-to-have
@@ -1710,4 +1725,4 @@ async function run() {
1710
1725
  console.log('');
1711
1726
  }
1712
1727
 
1713
- module.exports = { run, MODULES, generateAgentWrappers };
1728
+ module.exports = { run, MODULES, generateAgentWrappers, generateSkillIndex, expandFrozenClosure, computeRemovals, manifestPath };