create-claude-cabinet 0.47.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 (76) hide show
  1. package/README.md +1 -0
  2. package/lib/CLAUDE.md +218 -0
  3. package/lib/cli.js +102 -344
  4. package/lib/copy.js +75 -1
  5. package/lib/engagement-server-setup.js +1 -17
  6. package/lib/engagement-setup.js +1 -1
  7. package/lib/installer-gate.js +135 -0
  8. package/lib/modules.js +292 -0
  9. package/lib/mux-setup.js +1 -17
  10. package/lib/project-context.js +16 -96
  11. package/lib/settings-merge.js +148 -9
  12. package/lib/site-audit-setup.js +23 -7
  13. package/lib/verify-setup.js +20 -9
  14. package/lib/watchtower-setup.js +1 -5
  15. package/package.json +1 -1
  16. package/templates/CLAUDE.md +959 -0
  17. package/templates/briefing/_briefing-template.md +6 -11
  18. package/templates/cabinet/eval-protocol.md +27 -10
  19. package/templates/cabinet/watchtower-contracts.md +39 -0
  20. package/templates/cabinet/worktree-invocation-contract.md +24 -0
  21. package/templates/engagement/__tests__/checklist-visibility.test.mjs +76 -0
  22. package/templates/engagement/engagement-checklist.mjs +26 -2
  23. package/templates/engagement/engagement-schema.md +8 -6
  24. package/templates/engagement/pib-db-patches/pib-db-lib.mjs +49 -9
  25. package/templates/engagement/pib-db-patches/pib-db-schema.sql +1 -1
  26. package/templates/engagement/pib-db-patches/pib-db.mjs +4 -1
  27. package/templates/engagement/sql-constants.mjs +9 -1
  28. package/templates/engagement-server/__tests__/cross-tenant-auth.test.mjs +87 -0
  29. package/templates/engagement-server/server.mjs +65 -40
  30. package/templates/hooks/action-completion-gate.sh +10 -0
  31. package/templates/hooks/action-quality-gate.sh +10 -0
  32. package/templates/hooks/skill-telemetry.sh +10 -5
  33. package/templates/hooks/skill-tool-telemetry.sh +11 -5
  34. package/templates/mcp/pib-db.json +1 -1
  35. package/templates/rules/enforcement-pipeline.md +0 -12
  36. package/templates/scripts/__tests__/api-usage-idle-gate.test.mjs +81 -0
  37. package/templates/scripts/__tests__/bsql-loader-verify.test.mjs +47 -0
  38. package/templates/scripts/__tests__/inbox-assessment.test.mjs +341 -0
  39. package/templates/scripts/__tests__/memory-reachability.test.mjs +150 -0
  40. package/templates/scripts/__tests__/ring1-pib-error-surfacing.test.mjs +69 -0
  41. package/templates/scripts/__tests__/ring1-stale-open-counts.test.mjs +143 -0
  42. package/templates/scripts/__tests__/ring3-thread-capture.test.mjs +213 -0
  43. package/templates/scripts/__tests__/watchtower-status-ring4.test.mjs +89 -0
  44. package/templates/scripts/audit-coherence-check.mjs +123 -0
  45. package/templates/scripts/audit-synth.mjs +117 -0
  46. package/templates/scripts/merge-findings.js +4 -1
  47. package/templates/scripts/patterns-scope-check.mjs +122 -0
  48. package/templates/scripts/pib-db-lib.mjs +61 -11
  49. package/templates/scripts/pib-db-mcp-server.mjs +2 -1
  50. package/templates/scripts/pib-db.mjs +2 -1
  51. package/templates/scripts/review-server.mjs +7 -12
  52. package/templates/scripts/triage-server.mjs +7 -8
  53. package/templates/scripts/watchtower-cross-ring-reader.mjs +20 -6
  54. package/templates/scripts/watchtower-inbox-assessment.mjs +706 -0
  55. package/templates/scripts/watchtower-lib.mjs +282 -4
  56. package/templates/scripts/watchtower-ring1.mjs +78 -38
  57. package/templates/scripts/watchtower-ring2.mjs +38 -12
  58. package/templates/scripts/watchtower-ring3-close.mjs +160 -21
  59. package/templates/scripts/watchtower-status.sh +17 -0
  60. package/templates/scripts/work-tracker-server.mjs +26 -22
  61. package/templates/skills/audit/SKILL.md +18 -5
  62. package/templates/skills/audit/phases/structural-checks.md +22 -0
  63. package/templates/skills/briefing/SKILL.md +8 -0
  64. package/templates/skills/cabinet-accessibility/SKILL.md +1 -1
  65. package/templates/skills/cc-publish/SKILL.md +41 -7
  66. package/templates/skills/debrief/phases/audit-pattern-capture.md +13 -0
  67. package/templates/skills/inbox/SKILL.md +95 -5
  68. package/templates/skills/orient/SKILL.md +4 -0
  69. package/templates/skills/pulse/SKILL.md +10 -8
  70. package/templates/skills/spring-clean/SKILL.md +1 -1
  71. package/templates/skills/spring-clean/phases/execute-decisions.md +1 -1
  72. package/templates/skills/validate/phases/validators.md +86 -0
  73. package/templates/skills/watchtower/SKILL.md +18 -26
  74. package/templates/workflows/deliberative-audit.js +258 -87
  75. package/templates/workflows/execute-group-complete.js +16 -2
  76. package/templates/workflows/execute-group-implement.js +16 -2
package/lib/cli.js CHANGED
@@ -3,8 +3,8 @@ 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, isProjectOwnedSeed, freezeRetain } = require('./copy');
7
- const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks } = require('./settings-merge');
6
+ const { copyTemplates, recordSkip, isProjectOwnedSeed, classifyFileOwnership } = require('./copy');
7
+ const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks, mergeCcDevHooks, mergeSkillOverrides } = require('./settings-merge');
8
8
  const { create: createMetadata, read: readMetadata, readFrozenModules, setFrozenModules } = require('./metadata');
9
9
  const { setupDb, shouldSkipInit, readBundledSchemaVersion } = require('./db-setup');
10
10
  const { setupVerifyRuntime } = require('./verify-setup');
@@ -442,281 +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
- // 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',
485
- 'skills/menu',
486
- ],
487
- },
488
- 'hooks': {
489
- name: 'Git Guardrails + Telemetry',
490
- description: 'Block destructive git ops (force push, hard reset). Track skill usage via JSONL telemetry.',
491
- mandatory: false,
492
- default: true,
493
- lean: true,
494
- 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'],
495
- },
496
- 'work-tracking': {
497
- name: 'Work Tracking (pib-db or markdown)',
498
- description: 'Track work items for orient/debrief. Default: SQLite (pib-db). Also supports markdown (tasks.md) or external systems. Choice made during /onboard.',
499
- mandatory: false,
500
- default: true,
501
- lean: false,
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'],
503
- needsDb: true,
504
- },
505
- 'planning': {
506
- name: 'Planning + Execution (plan + execute)',
507
- description: 'Structured planning with cabinet critique — members weigh in before you build.',
508
- mandatory: false,
509
- default: true,
510
- lean: true,
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'],
512
- },
513
- 'compliance': {
514
- name: 'Compliance Stack (rules + enforcement)',
515
- description: 'Scoped instructions that load by path. Enforcement pipeline for promoting patterns to rules.',
516
- mandatory: false,
517
- default: true,
518
- lean: false,
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'],
520
- },
521
- 'memory': {
522
- name: 'Built-In Memory (cc-remember + reader + validator)',
523
- 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.',
524
- mandatory: false,
525
- default: true,
526
- // lean:true is load-bearing — it keeps the skill triad, scripts, and
527
- // rule installing together as an atomic unit. compliance is lean:false,
528
- // so the memory-capture rule must NOT live there or lean installs get a
529
- // partial, incoherent set.
530
- lean: true,
531
- templates: [
532
- 'skills/cc-remember',
533
- 'skills/memory',
534
- 'rules/memory-capture.md',
535
- 'cabinet/memory-lifecycle-contract.md',
536
- 'scripts/write-memory-file.mjs',
537
- 'scripts/validate-memory.mjs',
538
- // project-context.cjs ships as a co-located sibling: the two .mjs
539
- // scripts require('./project-context.cjs') and consumers have no lib/.
540
- 'scripts/project-context.cjs',
541
- ],
542
- },
543
- 'audit': {
544
- name: 'Audit Loop (audit + triage + cabinet)',
545
- description: '27 expert cabinet members review your project. Convene the full cabinet or just one committee.',
546
- mandatory: false,
547
- default: true,
548
- lean: true,
549
- templates: [
550
- 'skills/audit', 'skills/pulse', 'skills/triage-audit', 'skills/cabinet',
551
- 'cabinet', 'briefing',
552
- 'skills/cabinet-accessibility', 'skills/cabinet-anti-confirmation',
553
- 'skills/cabinet-architecture', 'skills/cabinet-automation',
554
- 'skills/cabinet-boundary-man',
555
- 'skills/cabinet-anthropic-insider', 'skills/cabinet-cc-health',
556
- 'skills/cabinet-data-integrity',
557
- 'skills/cabinet-debugger', 'skills/cabinet-deployment',
558
- 'skills/cabinet-historian',
559
- 'skills/cabinet-organized-mind', 'skills/cabinet-process-therapist',
560
- 'skills/cabinet-qa', 'skills/cabinet-record-keeper',
561
- 'skills/cabinet-roster-check', 'skills/cabinet-security',
562
- 'skills/cabinet-seo',
563
- 'skills/cabinet-small-screen', 'skills/cabinet-speed-freak',
564
- 'skills/cabinet-system-advocate', 'skills/cabinet-technical-debt',
565
- 'skills/cabinet-usability', 'skills/cabinet-workflow-cop',
566
- 'skills/cabinet-framework-quality', 'skills/cabinet-goal-alignment',
567
- 'skills/cabinet-information-design', 'skills/cabinet-mantine-quality',
568
- 'skills/cabinet-ui-experimentalist', 'skills/cabinet-user-advocate',
569
- 'skills/cabinet-vision',
570
- 'skills/cabinet-narrative-architect', 'skills/cabinet-interactive-storyteller',
571
- 'skills/cabinet-elegance',
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)
574
- 'scripts/triage-server.mjs', 'scripts/triage-ui.html',
575
- 'scripts/finding-schema.json', 'scripts/resolve-committees.cjs',
576
- 'scripts/review-server.mjs', 'scripts/review-ui.html',
577
- 'workflows/deliberative-audit.js', 'cabinet/critique-contract.md',
578
- ],
579
- },
580
- 'lifecycle': {
581
- name: 'Lifecycle (onboard + seed + cc-upgrade + link)',
582
- description: 'Onboarding prepares the briefings. Seed proposes new cabinet members when you adopt new tech.',
583
- mandatory: false,
584
- default: true,
585
- lean: true,
586
- templates: ['skills/onboard', 'skills/seed', 'skills/cc-upgrade', 'skills/cc-link', 'skills/cc-unlink', 'skills/cc-extract', 'skills/cc-feedback', 'cabinet/elicitation-methods.md'],
587
- },
588
- 'validate': {
589
- name: 'Validate',
590
- description: 'Structural validation checks with unified summary. Define your own validators.',
591
- mandatory: false,
592
- default: true,
593
- lean: false,
594
- templates: ['skills/validate', 'scripts/skill-validator.sh'],
595
- },
596
- 'verify': {
597
- name: 'Verification Harness (Cucumber + Playwright)',
598
- 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.',
599
- mandatory: false,
600
- default: false,
601
- lean: false,
602
- templates: [
603
- 'skills/verify',
604
- 'skills/plan/phases/verify-plan.md',
605
- 'skills/execute/phases/verify-emit.md',
606
- 'skills/debrief/phases/verify-coverage.md',
607
- 'skills/orient/phases/verify-backfill.md',
608
- ],
609
- postInstall: 'verify-setup',
610
- },
611
- 'site-audit': {
612
- name: 'Site Audit (deployed-site quality)',
613
- description: '14-check audit for deployed websites: performance, accessibility (3 engines), security, SEO, DNS, privacy, sustainability. Standalone HTML report + comparison mode.',
614
- mandatory: false,
615
- default: false,
616
- lean: false,
617
- templates: [
618
- 'skills/cc-site-audit',
619
- 'site-audit-runtime',
620
- ],
621
- postInstall: 'site-audit-setup',
622
- },
623
- engagement: {
624
- name: 'Engagement management (with secure credential handoff)',
625
- 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).',
626
- mandatory: false,
627
- default: false,
628
- lean: false,
629
- requires: ['work-tracking'],
630
- postInstall: 'engagement-setup',
631
- templates: [
632
- 'skills/collab-client',
633
- 'skills/collab-consultant',
634
- 'skills/engagement-help',
635
- 'skills/engagement-create',
636
- 'skills/setup-accounts',
637
- 'skills/guide',
638
- 'engagement',
639
- ],
640
- },
641
- watchtower: {
642
- name: 'Watchtower (continuous background state)',
643
- 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.',
644
- mandatory: false,
645
- default: false,
646
- lean: false,
647
- templates: [
648
- 'scripts/watchtower-validate.mjs',
649
- 'cabinet/watchtower-contracts.md',
650
- 'scripts/watchtower-lib.mjs',
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',
658
- 'skills/inbox',
659
- 'hooks/watchtower-session-start.sh',
660
- 'scripts/watchtower-build-context.mjs',
661
- 'scripts/watchtower-snapshot.mjs',
662
- 'scripts/watchtower-narrative-corpus.mjs',
663
- 'scripts/watchtower-cross-ring-reader.mjs',
664
- 'scripts/watchtower-ring1.mjs',
665
- 'scripts/watchtower-ring1-runner.sh',
666
- 'skills/watchtower',
667
- 'watchtower/com.claude-cabinet.watchtower-ring1.plist',
668
- 'watchtower/watchtower-ring1.service',
669
- 'watchtower/watchtower-ring1.timer',
670
- 'scripts/watchtower-ring2.mjs',
671
- 'scripts/watchtower-ring2-runner.sh',
672
- 'watchtower/com.claude-cabinet.watchtower-ring2-fast.plist',
673
- 'watchtower/com.claude-cabinet.watchtower-ring2-slow.plist',
674
- 'watchtower/watchtower-ring2-fast.service',
675
- 'watchtower/watchtower-ring2-fast.timer',
676
- 'watchtower/watchtower-ring2-slow.service',
677
- 'watchtower/watchtower-ring2-slow.timer',
678
- 'hooks/watchtower-session-end.sh',
679
- 'scripts/watchtower-ring3-close.mjs',
680
- 'scripts/watchtower-ring4.mjs',
681
- 'scripts/watchtower-ring4-runner.sh',
682
- 'scripts/watchtower-status.sh',
683
- 'skills/briefing',
684
- 'skills/threads',
685
- 'skills/catch-up',
686
- 'skills/qa-handoff',
687
- 'skills/qa-drain',
688
- 'skills/close',
689
- ],
690
- },
691
- mux: {
692
- name: 'Mux (tmux project manager)',
693
- 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/).',
694
- mandatory: false,
695
- default: false,
696
- lean: false,
697
- postInstall: 'mux-setup',
698
- templates: ['skills/orient/phases/dx-captures.md', 'skills/dx-feedback'],
699
- },
700
- 'engagement-server': {
701
- name: 'Engagement Server',
702
- description: 'Central multi-engagement API server. Deploys once to Railway/Fly, serves all client engagements. User-level infrastructure.',
703
- mandatory: false,
704
- default: false,
705
- lean: false,
706
- requires: ['engagement', 'work-tracking'],
707
- postInstall: 'engagement-server-setup',
708
- templates: [],
709
- },
710
- 'bash-compress': {
711
- name: 'Bash Output Compression Hook',
712
- 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).',
713
- mandatory: false,
714
- default: false,
715
- lean: false,
716
- requires: ['hooks'],
717
- templates: ['hooks/bash-output-compress.sh'],
718
- },
719
- };
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');
720
449
 
721
450
  /** Recursively collect all relative file paths under a directory. */
722
451
  function walkDir(dir, base) {
@@ -1463,69 +1192,88 @@ async function run() {
1463
1192
  const incoming = fs.readFileSync(srcPath, 'utf8');
1464
1193
  const incomingHash = hashContent(incoming);
1465
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
+ });
1466
1218
 
1467
- if (fs.existsSync(destPath)) {
1468
- const existingContent = fs.readFileSync(destPath, 'utf8');
1469
- 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':
1470
1237
  totalSkipped++;
1471
1238
  recordSkip(allManifest, mPath, { identical: true, incomingHash });
1472
1239
  continue;
1473
- }
1474
-
1475
- // Phase file customization guard — mirrors copy.js:48-59 for the
1476
- // single-file install path. If the destination path is a phase
1477
- // file and the on-disk content differs from upstream (operator
1478
- // edited it), preserve it. This keeps the documented "customize
1479
- // by editing the phase file" affordance intact across upgrades.
1480
- // Exception: instruction phases explicitly listed in MODULES are
1481
- // CC-owned and must always overwrite (they're not customizable).
1482
- const isPhaseFile = tmpl.includes('/phases/') || tmpl.includes('\\phases\\');
1483
- const isInstructionPhase = isPhaseFile && mod.templates.includes(tmpl);
1484
- if (isPhaseFile && !isInstructionPhase && existingContent.trim() !== '' && existingContent.trim() !== incoming.trim()) {
1485
- console.log(` Preserved customized phase: ${tmpl}`);
1240
+ case 'freeze-hold':
1486
1241
  totalSkipped++;
1487
- // Customized phase = project-owned content omit from manifest
1488
- // (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).
1489
1261
  recordSkip(allManifest, mPath);
1490
1262
  continue;
1491
- }
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
-
1509
- if (flags.yes || dirState === 'existing-install') {
1510
- // If file is in the old manifest, it's upstream-managed — overwrite.
1511
- // If not, it's project-created — skip.
1512
- if (existingManifest[mPath]) {
1513
- if (!flags.dryRun) fs.copyFileSync(srcPath, destPath);
1514
- totalOverwritten++;
1515
- // Log single-file overwrites too — the directory path (copy.js)
1516
- // already does. Without this, scripts/ updates are invisible in
1517
- // install output, masking whether a changed script propagated.
1518
- console.log(` Updated: ${path.relative(projectDir, destPath)}`);
1519
- allManifest[mPath] = incomingHash;
1520
- } else {
1521
- totalSkipped++;
1522
- // Project-created file → omit from manifest entirely. Ownership
1523
- // classification is manifest-PRESENCE-based, so recording ANY
1524
- // hash here would mark the file upstream-owned and the NEXT
1525
- // install would silently overwrite it (act:bf21c95b).
1526
- recordSkip(allManifest, mPath);
1527
- }
1528
- } 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: {
1529
1277
  const response = await prompts({
1530
1278
  type: 'select',
1531
1279
  name: 'action',
@@ -1546,19 +1294,8 @@ async function run() {
1546
1294
  // the manifest so it is never mistaken for upstream content.
1547
1295
  recordSkip(allManifest, mPath);
1548
1296
  }
1549
- }
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
1297
  continue;
1558
1298
  }
1559
- if (!flags.dryRun) fs.copyFileSync(srcPath, destPath);
1560
- totalCopied++;
1561
- allManifest[mPath] = incomingHash;
1562
1299
  }
1563
1300
  }
1564
1301
  }
@@ -1619,6 +1356,27 @@ async function run() {
1619
1356
  mergeBashCompressHooks(settingsPath);
1620
1357
  console.log(' ⚙️ Registered bash-output compression PostToolUse hook');
1621
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
+ }
1622
1380
  }
1623
1381
 
1624
1382
  // --- Heal user-level ~/.claude/settings.json ---
package/lib/copy.js CHANGED
@@ -104,6 +104,80 @@ function freezeRetain({ existing, isCcOwned }) {
104
104
  return isCcOwned ? { manifestHash: hashContent(existing) } : { omit: true };
105
105
  }
106
106
 
107
+ /**
108
+ * Pure ownership classifier for a SINGLE incoming template file — the
109
+ * non-interactive install cascade in one place (elegance-0002, act:a6377c82).
110
+ *
111
+ * The rule cascade below is the same one the walkAndCopy directory walk runs
112
+ * inline; cli.js's single-file install branch had hand-inlined a copy of it
113
+ * and the copy had already drifted — it was MISSING the project-owned-seed
114
+ * guard (step 1), so the single-file path could adopt/clobber a consumer
115
+ * overlay. Making cli.js delegate to this function removes that "kept in step
116
+ * by comments" fragility for the single-file path; the shared RULES
117
+ * (isProjectOwnedSeed, freezeRetain, recordSkip's omission semantics) already
118
+ * live in one place and are reused here.
119
+ *
120
+ * Order matches walkAndCopy exactly: seed → customized-phase → identical →
121
+ * frozen-hold → skipConflicts(manifest-driven) → interactive-prompt. Purely a
122
+ * decision — the caller performs all I/O, counter, and logging side effects.
123
+ *
124
+ * @returns {{ action: string, manifest: 'omit'|'identical'|'frozen'|'hash'|null, manifestHash?: string }}
125
+ * action ∈ 'seed' | 'preserve-phase' | 'skip-identical' | 'freeze-hold' |
126
+ * 'freeze-hold-new' | 'overwrite' | 'skip-omit' | 'copy-new' | 'prompt'
127
+ */
128
+ function classifyFileOwnership({
129
+ relPath,
130
+ destExists,
131
+ existingContent,
132
+ incoming,
133
+ isPhaseFile = false,
134
+ isInstructionPhase = false,
135
+ frozen = false,
136
+ isCcOwned = false,
137
+ skipConflicts = false,
138
+ }) {
139
+ // 1. Project-owned seed — before any existsSync branch (act:2001bf54).
140
+ if (isProjectOwnedSeed(relPath)) {
141
+ return { action: 'seed', manifest: 'omit' };
142
+ }
143
+ if (destExists) {
144
+ // 2. Customized phase file → preserve (independent of skipPhases). Ahead
145
+ // of the identical check to match the walk; behavior-identical since
146
+ // identical content never trips this guard.
147
+ if (
148
+ isPhaseFile &&
149
+ !isInstructionPhase &&
150
+ existingContent.trim() !== '' &&
151
+ existingContent.trim() !== incoming.trim()
152
+ ) {
153
+ return { action: 'preserve-phase', manifest: 'omit' };
154
+ }
155
+ // 3. Byte-identical → skip, recorded as CC-owned-unchanged.
156
+ if (existingContent === incoming) {
157
+ return { action: 'skip-identical', manifest: 'identical' };
158
+ }
159
+ // 4. Frozen module (act:c1013967): hold a CC-owned file at its on-disk
160
+ // hash; a project-owned file falls through.
161
+ if (frozen) {
162
+ const decision = freezeRetain({ existing: existingContent, isCcOwned });
163
+ if (decision.manifestHash) {
164
+ return { action: 'freeze-hold', manifest: 'frozen', manifestHash: decision.manifestHash };
165
+ }
166
+ }
167
+ // 5. Non-interactive install (--yes / existing-install): manifest-driven.
168
+ if (skipConflicts) {
169
+ return isCcOwned
170
+ ? { action: 'overwrite', manifest: 'hash' }
171
+ : { action: 'skip-omit', manifest: 'omit' };
172
+ }
173
+ // 6. Interactive → the caller prompts.
174
+ return { action: 'prompt', manifest: null };
175
+ }
176
+ // New file (dest absent): a frozen module holds it back; else copy.
177
+ if (frozen) return { action: 'freeze-hold-new', manifest: null };
178
+ return { action: 'copy-new', manifest: 'hash' };
179
+ }
180
+
107
181
  /**
108
182
  * Recursively copy files from src to dest, surfacing conflicts.
109
183
  * Returns { copied: string[], skipped: string[], overwritten: string[] }
@@ -317,4 +391,4 @@ function showDiff(existing, incoming, relPath) {
317
391
  console.log('');
318
392
  }
319
393
 
320
- module.exports = { copyTemplates, recordSkip, isProjectOwnedSeed, freezeRetain };
394
+ module.exports = { copyTemplates, recordSkip, isProjectOwnedSeed, freezeRetain, classifyFileOwnership };
@@ -20,7 +20,7 @@
20
20
  const fs = require('fs');
21
21
  const path = require('path');
22
22
  const os = require('os');
23
- const crypto = require('crypto');
23
+ const { sha256, compareVersions } = require('./installer-gate');
24
24
 
25
25
  const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
26
26
  const GLOBAL_MANIFEST_PATH = path.join(CC_HOME, 'global-manifest.json');
@@ -42,22 +42,6 @@ const DATA_DIRS = [
42
42
  path.join(INSTALL_DIR, 'migrations'),
43
43
  ];
44
44
 
45
- function sha256(content) {
46
- return crypto.createHash('sha256').update(content).digest('hex');
47
- }
48
-
49
- function compareVersions(a, b) {
50
- const pa = a.split('.').map(Number);
51
- const pb = b.split('.').map(Number);
52
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
53
- const va = pa[i] || 0;
54
- const vb = pb[i] || 0;
55
- if (va > vb) return 1;
56
- if (va < vb) return -1;
57
- }
58
- return 0;
59
- }
60
-
61
45
  function readGlobalManifest() {
62
46
  if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
63
47
  try {
@@ -177,7 +177,7 @@ function setupEngagement({ dryRun, projectDir } = {}) {
177
177
  target_fid TEXT,
178
178
  packet_id TEXT,
179
179
  kind TEXT NOT NULL
180
- CHECK(kind IN ('client_feedback','status_push','delegation','approval','note','packet_sent','packet_opened')),
180
+ CHECK(kind IN ('client_feedback','status_push','delegation','approval','note','packet_sent')),
181
181
  author TEXT NOT NULL,
182
182
  verdict TEXT CHECK(verdict IS NULL OR verdict IN ('approve','object','comment','none')),
183
183
  body TEXT CHECK(body IS NULL OR length(body) <= 10000),