create-claude-cabinet 0.38.0 → 0.39.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 (70) hide show
  1. package/README.md +4 -4
  2. package/lib/cli.js +63 -1
  3. package/lib/engagement-server-setup.js +189 -0
  4. package/lib/mux-setup.js +189 -0
  5. package/lib/settings-merge.js +56 -1
  6. package/package.json +1 -1
  7. package/templates/cabinet/committees.yaml +1 -0
  8. package/templates/cabinet/watchtower-contracts.md +87 -0
  9. package/templates/engagement/capture-and-encrypt.mjs +14 -5
  10. package/templates/engagement/engagement-schema.md +94 -8
  11. package/templates/engagement/engagement-transport.mjs +99 -2
  12. package/templates/engagement/sql-constants.mjs +1 -1
  13. package/templates/engagement-server/Dockerfile +12 -0
  14. package/templates/engagement-server/engage-server.mjs +211 -0
  15. package/templates/engagement-server/package.json +12 -0
  16. package/templates/engagement-server/railway.toml +10 -0
  17. package/templates/engagement-server/schema.sql +46 -0
  18. package/templates/engagement-server/server.mjs +412 -0
  19. package/templates/hooks/watchtower-session-end.sh +74 -0
  20. package/templates/hooks/watchtower-session-start.sh +41 -0
  21. package/templates/mux/bin/mux +1119 -0
  22. package/templates/mux/config/_mux +70 -0
  23. package/templates/mux/config/context-help.py +49 -0
  24. package/templates/mux/config/dashboard-wrapper.sh +16 -0
  25. package/templates/mux/config/dashboard.py +122 -0
  26. package/templates/mux/config/dx-wrapper.sh +12 -0
  27. package/templates/mux/config/help.txt +38 -0
  28. package/templates/mux/config/manage-dx.py +105 -0
  29. package/templates/mux/config/manage-notes.py +73 -0
  30. package/templates/mux/config/mux-server.py +295 -0
  31. package/templates/mux/config/mux.bash +43 -0
  32. package/templates/mux/config/muxlib.py +395 -0
  33. package/templates/mux/config/show-dx.py +89 -0
  34. package/templates/mux/config/show-notes.py +87 -0
  35. package/templates/mux/config/sidebar-wrapper.sh +56 -0
  36. package/templates/mux/config/status-indicators.sh +44 -0
  37. package/templates/mux/config/status-popup.py +200 -0
  38. package/templates/mux/config/status-set.sh +44 -0
  39. package/templates/mux/config/trail-add.sh +49 -0
  40. package/templates/mux/config/trail-popup.py +210 -0
  41. package/templates/scripts/watchtower-build-context.mjs +248 -0
  42. package/templates/scripts/watchtower-lib.mjs +78 -0
  43. package/templates/scripts/watchtower-queue.mjs +265 -0
  44. package/templates/scripts/watchtower-ring1-runner.sh +77 -0
  45. package/templates/scripts/watchtower-ring1.mjs +669 -0
  46. package/templates/scripts/watchtower-ring2-runner.sh +92 -0
  47. package/templates/scripts/watchtower-ring2.mjs +1066 -0
  48. package/templates/scripts/watchtower-ring3-close.mjs +972 -0
  49. package/templates/scripts/watchtower-validate.mjs +134 -0
  50. package/templates/skills/briefing/SKILL.md +242 -0
  51. package/templates/skills/cabinet-elegance/SKILL.md +284 -0
  52. package/templates/skills/collab-client/SKILL.md +181 -46
  53. package/templates/skills/collab-consultant/SKILL.md +313 -75
  54. package/templates/skills/decisions/SKILL.md +164 -0
  55. package/templates/skills/orient/phases/dx-captures.md +52 -0
  56. package/templates/skills/setup-accounts/SKILL.md +88 -6
  57. package/templates/skills/watchtower/SKILL.md +252 -0
  58. package/templates/watchtower/com.claude-cabinet.watchtower-ring1.plist +38 -0
  59. package/templates/watchtower/com.claude-cabinet.watchtower-ring2-fast.plist +39 -0
  60. package/templates/watchtower/com.claude-cabinet.watchtower-ring2-slow.plist +39 -0
  61. package/templates/watchtower/config.json.template +25 -0
  62. package/templates/watchtower/queue/items/item.json.schema +78 -0
  63. package/templates/watchtower/state/projects/project.md.template +13 -0
  64. package/templates/watchtower/state/summary.md.template +18 -0
  65. package/templates/watchtower/watchtower-ring1.service +16 -0
  66. package/templates/watchtower/watchtower-ring1.timer +15 -0
  67. package/templates/watchtower/watchtower-ring2-fast.service +16 -0
  68. package/templates/watchtower/watchtower-ring2-fast.timer +15 -0
  69. package/templates/watchtower/watchtower-ring2-slow.service +16 -0
  70. package/templates/watchtower/watchtower-ring2-slow.timer +15 -0
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Claude Cabinet
2
2
 
3
3
  A cabinet of expert advisors for your Claude Code project. One command
4
- gives Claude a memory, 31 domain experts, a planning process, and the
4
+ gives Claude a memory, 32 domain experts, a planning process, and the
5
5
  habit of starting sessions informed and ending them properly.
6
6
 
7
7
  Built by a guy who'd rather talk to Claude than write code. Most of it
@@ -12,7 +12,7 @@ was built by Claude. I just complained until it (mostly) worked.
12
12
  Your project gets a cabinet — specialist advisors who each own a domain
13
13
  and weigh in when their expertise matters:
14
14
 
15
- - **Cabinet members** — 31 domain experts (security, accessibility,
15
+ - **Cabinet members** — 32 domain experts (security, accessibility,
16
16
  architecture, QA, etc.) who review your project and surface what
17
17
  you'd miss alone
18
18
  - **Briefings** — project context members read before weighing in
@@ -78,7 +78,7 @@ left off.
78
78
 
79
79
  ### The Cabinet (included in lean)
80
80
 
81
- 31 expert cabinet members who each own a domain and stay in their lane.
81
+ 32 expert cabinet members who each own a domain and stay in their lane.
82
82
  **Speed-freak** watches performance. **Boundary-man** catches edge cases.
83
83
  **Record-keeper** flags when docs drift from code. **Workflow-cop**
84
84
  evaluates whether your process actually works. Each member has a
@@ -240,7 +240,7 @@ source code.
240
240
  ```
241
241
  .claude/
242
242
  ├── skills/ # orient, debrief, plan, execute, audit, etc.
243
- │ └── cabinet-*/ # 31 cabinet member definitions
243
+ │ └── cabinet-*/ # 32 cabinet member definitions
244
244
  ├── cabinet/ # committees, lifecycle, composition patterns
245
245
  │ # (incl. pib-db-access.md, pib-db-triggers.md)
246
246
  ├── briefing/ # project briefing templates
package/lib/cli.js CHANGED
@@ -4,12 +4,14 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const crypto = require('crypto');
6
6
  const { copyTemplates } = require('./copy');
7
- const { mergeSettings, healUserSettings } = require('./settings-merge');
7
+ const { mergeSettings, healUserSettings, mergeWatchtowerHooks } = require('./settings-merge');
8
8
  const { create: createMetadata, read: readMetadata } = require('./metadata');
9
9
  const { setupDb } = require('./db-setup');
10
10
  const { setupVerifyRuntime } = require('./verify-setup');
11
11
  const { setupSiteAuditRuntime } = require('./site-audit-setup');
12
12
  const { setupEngagement } = require('./engagement-setup');
13
+ const { setupMux } = require('./mux-setup');
14
+ const { setupEngagementServer } = require('./engagement-server-setup');
13
15
  const { reset } = require('./reset');
14
16
 
15
17
  const VERSION = require('../package.json').version;
@@ -543,6 +545,7 @@ const MODULES = {
543
545
  'skills/cabinet-ui-experimentalist', 'skills/cabinet-user-advocate',
544
546
  'skills/cabinet-vision',
545
547
  'skills/cabinet-narrative-architect', 'skills/cabinet-interactive-storyteller',
548
+ 'skills/cabinet-elegance',
546
549
  'scripts/merge-findings.js', 'scripts/load-triage-history.js',
547
550
  'scripts/triage-server.mjs', 'scripts/triage-ui.html',
548
551
  'scripts/finding-schema.json', 'scripts/resolve-committees.cjs',
@@ -618,6 +621,58 @@ const MODULES = {
618
621
  'engagement',
619
622
  ],
620
623
  },
624
+ watchtower: {
625
+ name: 'Watchtower (continuous background state)',
626
+ description: 'Replaces orient/debrief with continuous background processing. Three rings (mechanical cron, Claude intelligence, session-aware), ambient state injection via SessionStart hook, and an asynchronous decision queue. Sessions start informed with minimal context cost.',
627
+ mandatory: false,
628
+ default: false,
629
+ lean: false,
630
+ templates: [
631
+ 'scripts/watchtower-validate.mjs',
632
+ 'cabinet/watchtower-contracts.md',
633
+ 'scripts/watchtower-lib.mjs',
634
+ 'scripts/watchtower-queue.mjs',
635
+ 'skills/decisions',
636
+ 'hooks/watchtower-session-start.sh',
637
+ 'scripts/watchtower-build-context.mjs',
638
+ 'scripts/watchtower-ring1.mjs',
639
+ 'scripts/watchtower-ring1-runner.sh',
640
+ 'skills/watchtower',
641
+ 'watchtower/com.claude-cabinet.watchtower-ring1.plist',
642
+ 'watchtower/watchtower-ring1.service',
643
+ 'watchtower/watchtower-ring1.timer',
644
+ 'scripts/watchtower-ring2.mjs',
645
+ 'scripts/watchtower-ring2-runner.sh',
646
+ 'watchtower/com.claude-cabinet.watchtower-ring2-fast.plist',
647
+ 'watchtower/com.claude-cabinet.watchtower-ring2-slow.plist',
648
+ 'watchtower/watchtower-ring2-fast.service',
649
+ 'watchtower/watchtower-ring2-fast.timer',
650
+ 'watchtower/watchtower-ring2-slow.service',
651
+ 'watchtower/watchtower-ring2-slow.timer',
652
+ 'hooks/watchtower-session-end.sh',
653
+ 'scripts/watchtower-ring3-close.mjs',
654
+ 'skills/briefing',
655
+ ],
656
+ },
657
+ mux: {
658
+ name: 'Mux (tmux project manager)',
659
+ 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/).',
660
+ mandatory: false,
661
+ default: false,
662
+ lean: false,
663
+ postInstall: 'mux-setup',
664
+ templates: ['skills/orient/phases/dx-captures.md'],
665
+ },
666
+ 'engagement-server': {
667
+ name: 'Engagement Server',
668
+ description: 'Central multi-engagement API server. Deploys once to Railway/Fly, serves all client engagements. User-level infrastructure.',
669
+ mandatory: false,
670
+ default: false,
671
+ lean: false,
672
+ requires: ['engagement', 'work-tracking'],
673
+ postInstall: 'engagement-server-setup',
674
+ templates: [],
675
+ },
621
676
  };
622
677
 
623
678
  /** Recursively collect all relative file paths under a directory. */
@@ -1225,6 +1280,11 @@ async function run() {
1225
1280
  if (selectedModules.includes('hooks') && !flags.dryRun) {
1226
1281
  const settingsPath = mergeSettings(projectDir, { includeDb });
1227
1282
  console.log(` ⚙️ Merged hooks into ${path.relative(projectDir, settingsPath)}`);
1283
+
1284
+ if (selectedModules.includes('watchtower')) {
1285
+ mergeWatchtowerHooks(settingsPath);
1286
+ console.log(' ⚙️ Registered watchtower SessionStart/SessionEnd hooks');
1287
+ }
1228
1288
  }
1229
1289
 
1230
1290
  // --- Heal user-level ~/.claude/settings.json ---
@@ -1276,6 +1336,8 @@ async function run() {
1276
1336
  'verify-setup': setupVerifyRuntime,
1277
1337
  'site-audit-setup': setupSiteAuditRuntime,
1278
1338
  'engagement-setup': setupEngagement,
1339
+ 'mux-setup': setupMux,
1340
+ 'engagement-server-setup': setupEngagementServer,
1279
1341
  };
1280
1342
  for (const moduleKey of selectedModules) {
1281
1343
  const mod = MODULES[moduleKey];
@@ -0,0 +1,189 @@
1
+ /**
2
+ * engagement-server-setup.js — install engagement server to user-level paths.
3
+ *
4
+ * Follows the mux-setup.js pattern: user-level infrastructure managed by CC,
5
+ * tracked via global manifest at ~/.claude-cabinet/global-manifest.json.
6
+ *
7
+ * Installs to ~/.claude-cabinet/engagement-server/. The user deploys to
8
+ * Railway (or similar) separately.
9
+ *
10
+ * Version semantics:
11
+ * - First install: copy all managed files, create data dir, write .cc-version
12
+ * - Same version: skip with log
13
+ * - Newer CC version: upgrade managed files, preserve data dir
14
+ * - Older CC version than installed: skip (don't downgrade)
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const crypto = require('crypto');
21
+
22
+ const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
23
+ const GLOBAL_MANIFEST_PATH = path.join(CC_HOME, 'global-manifest.json');
24
+ const INSTALL_DIR = path.join(CC_HOME, 'engagement-server');
25
+ const VERSION_FILE = path.join(INSTALL_DIR, '.cc-version');
26
+
27
+ const MANAGED_FILES = [
28
+ 'server.mjs',
29
+ 'engage-server.mjs',
30
+ 'schema.sql',
31
+ 'package.json',
32
+ 'Dockerfile',
33
+ 'railway.toml',
34
+ ];
35
+
36
+ const DATA_DIRS = [
37
+ path.join(INSTALL_DIR, 'data'),
38
+ path.join(INSTALL_DIR, 'migrations'),
39
+ ];
40
+
41
+ function sha256(content) {
42
+ return crypto.createHash('sha256').update(content).digest('hex');
43
+ }
44
+
45
+ function compareVersions(a, b) {
46
+ const pa = a.split('.').map(Number);
47
+ const pb = b.split('.').map(Number);
48
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
49
+ const va = pa[i] || 0;
50
+ const vb = pb[i] || 0;
51
+ if (va > vb) return 1;
52
+ if (va < vb) return -1;
53
+ }
54
+ return 0;
55
+ }
56
+
57
+ function readGlobalManifest() {
58
+ if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
59
+ try {
60
+ return JSON.parse(fs.readFileSync(GLOBAL_MANIFEST_PATH, 'utf8'));
61
+ } catch {
62
+ return { files: {} };
63
+ }
64
+ }
65
+
66
+ function writeGlobalManifest(manifest) {
67
+ fs.mkdirSync(path.dirname(GLOBAL_MANIFEST_PATH), { recursive: true });
68
+ const tmp = GLOBAL_MANIFEST_PATH + '.tmp';
69
+ fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2));
70
+ fs.renameSync(tmp, GLOBAL_MANIFEST_PATH);
71
+ }
72
+
73
+ function readInstalledVersion() {
74
+ if (!fs.existsSync(VERSION_FILE)) return null;
75
+ try {
76
+ return fs.readFileSync(VERSION_FILE, 'utf8').trim();
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ function writeVersion(version) {
83
+ fs.mkdirSync(path.dirname(VERSION_FILE), { recursive: true });
84
+ fs.writeFileSync(VERSION_FILE, version + '\n');
85
+ }
86
+
87
+ /**
88
+ * @param {Object} opts
89
+ * @param {boolean} [opts.dryRun]
90
+ * @param {string} [opts.projectDir]
91
+ * @returns {{ results: string[], status: string }}
92
+ */
93
+ function setupEngagementServer(opts = {}) {
94
+ const dryRun = !!opts.dryRun;
95
+ const results = [];
96
+
97
+ const ccVersion = require('../package.json').version;
98
+ const templateDir = path.resolve(__dirname, '..', 'templates', 'engagement-server');
99
+
100
+ if (!fs.existsSync(templateDir)) {
101
+ throw new Error(`engagement-server-setup: ${templateDir} not found.`);
102
+ }
103
+
104
+ const installedVersion = readInstalledVersion();
105
+
106
+ if (installedVersion) {
107
+ const cmp = compareVersions(ccVersion, installedVersion);
108
+ if (cmp === 0) {
109
+ results.push(`engagement-server ${installedVersion} already installed — skipping`);
110
+ return { results, status: 'skipped' };
111
+ }
112
+ if (cmp < 0) {
113
+ results.push(`engagement-server ${installedVersion} is newer than CC ${ccVersion} — skipping (won't downgrade)`);
114
+ return { results, status: 'skipped' };
115
+ }
116
+ results.push(`Upgrading engagement-server from ${installedVersion} to ${ccVersion}`);
117
+ } else {
118
+ results.push(`Installing engagement-server ${ccVersion}`);
119
+ }
120
+
121
+ // Create data directories (preserved across upgrades)
122
+ for (const dir of DATA_DIRS) {
123
+ if (!fs.existsSync(dir)) {
124
+ if (dryRun) {
125
+ results.push(` [dry-run] mkdir -p ${dir}`);
126
+ } else {
127
+ fs.mkdirSync(dir, { recursive: true });
128
+ }
129
+ }
130
+ }
131
+
132
+ // Copy managed files
133
+ const manifest = readGlobalManifest();
134
+ let copiedCount = 0;
135
+
136
+ for (const file of MANAGED_FILES) {
137
+ const srcPath = path.join(templateDir, file);
138
+ const destPath = path.join(INSTALL_DIR, file);
139
+
140
+ if (!fs.existsSync(srcPath)) {
141
+ results.push(` ⚠ Template missing: ${file}`);
142
+ continue;
143
+ }
144
+
145
+ const content = fs.readFileSync(srcPath);
146
+ const hash = sha256(content);
147
+
148
+ if (manifest.files[destPath] === hash) {
149
+ continue;
150
+ }
151
+
152
+ if (dryRun) {
153
+ results.push(` [dry-run] ${file} → ${destPath}`);
154
+ } else {
155
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
156
+ fs.writeFileSync(destPath, content);
157
+ manifest.files[destPath] = hash;
158
+ }
159
+ copiedCount++;
160
+ }
161
+
162
+ if (!dryRun) {
163
+ manifest.version = ccVersion;
164
+ manifest.installedAt = new Date().toISOString();
165
+ writeGlobalManifest(manifest);
166
+ writeVersion(ccVersion);
167
+ }
168
+
169
+ if (copiedCount > 0) {
170
+ results.push(` Copied ${copiedCount} file(s) to ${INSTALL_DIR}`);
171
+ }
172
+
173
+ if (!installedVersion) {
174
+ results.push('');
175
+ results.push(' Next steps:');
176
+ results.push(` cd ${INSTALL_DIR}`);
177
+ results.push(' npm install');
178
+ results.push(' railway init # or railway link');
179
+ results.push(' railway volume add -m /app/data');
180
+ results.push(' railway up --detach');
181
+ } else {
182
+ results.push('');
183
+ results.push(` Re-deploy: cd ${INSTALL_DIR} && railway up --detach`);
184
+ }
185
+
186
+ return { results, status: installedVersion ? 'upgraded' : 'installed' };
187
+ }
188
+
189
+ module.exports = { setupEngagementServer };
@@ -0,0 +1,189 @@
1
+ /**
2
+ * mux-setup.js — install mux files to user-level paths.
3
+ *
4
+ * First user-level module in CC. Unlike project-scoped templates that
5
+ * copy to .claude/ and scripts/, mux installs to ~/.local/bin/,
6
+ * ~/.config/mux/, and ~/.local/share/mux/. Tracked via a global
7
+ * manifest at ~/.claude-cabinet/global-manifest.json.
8
+ *
9
+ * Version semantics:
10
+ * - First install: copy all managed files, write .cc-version
11
+ * - Same version: skip with log
12
+ * - Newer CC version: upgrade managed files, preserve data dirs
13
+ * - Older CC version than installed: skip (don't downgrade)
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+ const crypto = require('crypto');
20
+
21
+ const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
22
+ const GLOBAL_MANIFEST_PATH = path.join(CC_HOME, 'global-manifest.json');
23
+ const MUX_VERSION_FILE = path.join(os.homedir(), '.config', 'mux', '.cc-version');
24
+
25
+ const MANAGED_FILES = [
26
+ { src: 'bin/mux', dest: path.join(os.homedir(), '.local', 'bin', 'mux'), mode: 0o755 },
27
+ { src: 'config/muxlib.py', dest: path.join(os.homedir(), '.config', 'mux', 'muxlib.py') },
28
+ { src: 'config/trail-add.sh', dest: path.join(os.homedir(), '.config', 'mux', 'trail-add.sh'), mode: 0o755 },
29
+ { src: 'config/status-set.sh', dest: path.join(os.homedir(), '.config', 'mux', 'status-set.sh'), mode: 0o755 },
30
+ { src: 'config/trail-popup.py', dest: path.join(os.homedir(), '.config', 'mux', 'trail-popup.py') },
31
+ { src: 'config/status-popup.py', dest: path.join(os.homedir(), '.config', 'mux', 'status-popup.py') },
32
+ { src: 'config/dashboard.py', dest: path.join(os.homedir(), '.config', 'mux', 'dashboard.py') },
33
+ { src: 'config/dashboard-wrapper.sh', dest: path.join(os.homedir(), '.config', 'mux', 'dashboard-wrapper.sh'), mode: 0o755 },
34
+ { src: 'config/sidebar-wrapper.sh', dest: path.join(os.homedir(), '.config', 'mux', 'sidebar-wrapper.sh'), mode: 0o755 },
35
+ { src: 'config/status-indicators.sh', dest: path.join(os.homedir(), '.config', 'mux', 'status-indicators.sh'), mode: 0o755 },
36
+ { src: 'config/context-help.py', dest: path.join(os.homedir(), '.config', 'mux', 'context-help.py') },
37
+ { src: 'config/show-notes.py', dest: path.join(os.homedir(), '.config', 'mux', 'show-notes.py') },
38
+ { src: 'config/manage-notes.py', dest: path.join(os.homedir(), '.config', 'mux', 'manage-notes.py') },
39
+ { src: 'config/manage-dx.py', dest: path.join(os.homedir(), '.config', 'mux', 'manage-dx.py') },
40
+ { src: 'config/show-dx.py', dest: path.join(os.homedir(), '.config', 'mux', 'show-dx.py') },
41
+ { src: 'config/mux-server.py', dest: path.join(os.homedir(), '.config', 'mux', 'mux-server.py') },
42
+ { src: 'config/help.txt', dest: path.join(os.homedir(), '.config', 'mux', 'help.txt') },
43
+ { src: 'config/_mux', dest: path.join(os.homedir(), '.config', 'mux', '_mux') },
44
+ { src: 'config/mux.bash', dest: path.join(os.homedir(), '.config', 'mux', 'mux.bash') },
45
+ ];
46
+
47
+ const DATA_DIRS = [
48
+ path.join(os.homedir(), '.local', 'share', 'mux', 'trails'),
49
+ path.join(os.homedir(), '.local', 'share', 'mux', 'status'),
50
+ path.join(os.homedir(), '.config', 'mux', 'notes'),
51
+ path.join(os.homedir(), '.config', 'mux', 'dx'),
52
+ path.join(os.homedir(), '.config', 'mux', 'pending-prompts'),
53
+ ];
54
+
55
+ function sha256(content) {
56
+ return crypto.createHash('sha256').update(content).digest('hex');
57
+ }
58
+
59
+ function compareVersions(a, b) {
60
+ const pa = a.split('.').map(Number);
61
+ const pb = b.split('.').map(Number);
62
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
63
+ const va = pa[i] || 0;
64
+ const vb = pb[i] || 0;
65
+ if (va > vb) return 1;
66
+ if (va < vb) return -1;
67
+ }
68
+ return 0;
69
+ }
70
+
71
+ function readGlobalManifest() {
72
+ if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
73
+ try {
74
+ return JSON.parse(fs.readFileSync(GLOBAL_MANIFEST_PATH, 'utf8'));
75
+ } catch {
76
+ return { files: {} };
77
+ }
78
+ }
79
+
80
+ function writeGlobalManifest(manifest) {
81
+ fs.mkdirSync(path.dirname(GLOBAL_MANIFEST_PATH), { recursive: true });
82
+ const tmp = GLOBAL_MANIFEST_PATH + '.tmp';
83
+ fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2));
84
+ fs.renameSync(tmp, GLOBAL_MANIFEST_PATH);
85
+ }
86
+
87
+ function readInstalledVersion() {
88
+ if (!fs.existsSync(MUX_VERSION_FILE)) return null;
89
+ try {
90
+ return fs.readFileSync(MUX_VERSION_FILE, 'utf8').trim();
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function writeVersion(version) {
97
+ fs.mkdirSync(path.dirname(MUX_VERSION_FILE), { recursive: true });
98
+ fs.writeFileSync(MUX_VERSION_FILE, version + '\n');
99
+ }
100
+
101
+ /**
102
+ * @param {Object} opts
103
+ * @param {boolean} [opts.dryRun]
104
+ * @param {string} [opts.projectDir] — consumer project dir (unused for mux)
105
+ * @returns {{ results: string[], status: string }}
106
+ */
107
+ function setupMux(opts = {}) {
108
+ const dryRun = !!opts.dryRun;
109
+ const results = [];
110
+
111
+ const ccVersion = require('../package.json').version;
112
+ const templateDir = path.resolve(__dirname, '..', 'templates', 'mux');
113
+
114
+ if (!fs.existsSync(templateDir)) {
115
+ throw new Error(`mux-setup: ${templateDir} not found.`);
116
+ }
117
+
118
+ const installedVersion = readInstalledVersion();
119
+
120
+ if (installedVersion) {
121
+ const cmp = compareVersions(ccVersion, installedVersion);
122
+ if (cmp === 0) {
123
+ results.push(`mux ${installedVersion} already installed — skipping`);
124
+ return { results, status: 'skipped' };
125
+ }
126
+ if (cmp < 0) {
127
+ results.push(`mux ${installedVersion} is newer than CC ${ccVersion} — skipping (won't downgrade)`);
128
+ return { results, status: 'skipped' };
129
+ }
130
+ results.push(`Upgrading mux from ${installedVersion} to ${ccVersion}`);
131
+ } else {
132
+ results.push(`Installing mux ${ccVersion}`);
133
+ }
134
+
135
+ // Create data directories
136
+ for (const dir of DATA_DIRS) {
137
+ if (!fs.existsSync(dir)) {
138
+ if (dryRun) {
139
+ results.push(` [dry-run] mkdir -p ${dir}`);
140
+ } else {
141
+ fs.mkdirSync(dir, { recursive: true });
142
+ }
143
+ }
144
+ }
145
+
146
+ // Copy managed files
147
+ const manifest = readGlobalManifest();
148
+ let copiedCount = 0;
149
+
150
+ for (const file of MANAGED_FILES) {
151
+ const srcPath = path.join(templateDir, file.src);
152
+ if (!fs.existsSync(srcPath)) {
153
+ results.push(` ⚠ Template missing: ${file.src}`);
154
+ continue;
155
+ }
156
+
157
+ const content = fs.readFileSync(srcPath);
158
+ const hash = sha256(content);
159
+
160
+ if (manifest.files[file.dest] === hash) {
161
+ continue; // file unchanged
162
+ }
163
+
164
+ if (dryRun) {
165
+ results.push(` [dry-run] ${file.src} → ${file.dest}`);
166
+ } else {
167
+ fs.mkdirSync(path.dirname(file.dest), { recursive: true });
168
+ fs.writeFileSync(file.dest, content);
169
+ if (file.mode) {
170
+ fs.chmodSync(file.dest, file.mode);
171
+ }
172
+ manifest.files[file.dest] = hash;
173
+ }
174
+ copiedCount++;
175
+ }
176
+
177
+ if (!dryRun) {
178
+ manifest.version = ccVersion;
179
+ manifest.installedAt = new Date().toISOString();
180
+ writeGlobalManifest(manifest);
181
+ writeVersion(ccVersion);
182
+ }
183
+
184
+ results.push(` ${copiedCount} file${copiedCount !== 1 ? 's' : ''} installed to user paths`);
185
+
186
+ return { results, status: installedVersion ? 'upgraded' : 'installed' };
187
+ }
188
+
189
+ module.exports = { setupMux };
@@ -82,6 +82,31 @@ const DEFAULT_HOOKS = {
82
82
  ],
83
83
  };
84
84
 
85
+ const WATCHTOWER_HOOKS = {
86
+ SessionStart: [
87
+ {
88
+ matcher: '',
89
+ hooks: [
90
+ {
91
+ type: 'command',
92
+ command: '$HOME/.claude-cabinet/watchtower/hooks/watchtower-session-start.sh',
93
+ },
94
+ ],
95
+ },
96
+ ],
97
+ SessionEnd: [
98
+ {
99
+ matcher: '',
100
+ hooks: [
101
+ {
102
+ type: 'command',
103
+ command: '$HOME/.claude-cabinet/watchtower/hooks/watchtower-session-end.sh',
104
+ },
105
+ ],
106
+ },
107
+ ],
108
+ };
109
+
85
110
  // Legacy hook script names that should be stripped on any merge.
86
111
  // Centralizes cleanup so a user who skips --migrate-memory but runs
87
112
  // any other CC operation still gets omega-era hooks pruned.
@@ -209,4 +234,34 @@ function healUserSettings() {
209
234
  return removed;
210
235
  }
211
236
 
212
- module.exports = { mergeSettings, healUserSettings, DEFAULT_HOOKS, LEGACY_HOOK_COMMANDS };
237
+ /**
238
+ * Merge watchtower-specific hooks into project settings.
239
+ * Called from the watchtower module's install path in cli.js — only
240
+ * registers SessionStart/SessionEnd hooks when watchtower is installed.
241
+ */
242
+ function mergeWatchtowerHooks(settingsPath) {
243
+ if (!fs.existsSync(settingsPath)) return;
244
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
245
+ if (!settings.hooks) settings.hooks = {};
246
+
247
+ for (const [event, newHooks] of Object.entries(WATCHTOWER_HOOKS)) {
248
+ if (!settings.hooks[event]) {
249
+ settings.hooks[event] = newHooks;
250
+ } else {
251
+ for (const newHook of newHooks) {
252
+ const hookKey = h => h.command || h.prompt || '';
253
+ const existingKeys = settings.hooks[event].flatMap(h =>
254
+ h.hooks.map(hh => hookKey(hh))
255
+ );
256
+ const newKeys = newHook.hooks.map(h => hookKey(h));
257
+ if (!newKeys.every(k => existingKeys.includes(k))) {
258
+ settings.hooks[event].push(newHook);
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
265
+ }
266
+
267
+ module.exports = { mergeSettings, healUserSettings, mergeWatchtowerHooks, DEFAULT_HOOKS, WATCHTOWER_HOOKS, LEGACY_HOOK_COMMANDS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-cabinet": "bin/create-claude-cabinet.js"
@@ -30,6 +30,7 @@ committees:
30
30
  - architecture
31
31
  - automation
32
32
  - boundary-man
33
+ - elegance
33
34
 
34
35
  health:
35
36
  name: "System Health"
@@ -0,0 +1,87 @@
1
+ # Watchtower Contracts
2
+
3
+ Shared contracts for all watchtower components. Every ring, hook, and
4
+ skill that reads or writes watchtower state files must follow these.
5
+
6
+ ## Atomic Writes
7
+
8
+ All shared files use temp+rename. No exceptions. Write to a `.tmp`
9
+ sibling, then `fs.renameSync()`. This prevents partial reads when a
10
+ ring is writing while a SessionStart hook is reading.
11
+
12
+ ```javascript
13
+ const tmp = filePath + '.tmp';
14
+ fs.writeFileSync(tmp, content);
15
+ fs.renameSync(tmp, filePath);
16
+ ```
17
+
18
+ ## No-Index Convention
19
+
20
+ The decision queue uses directory listing, not an index file. To list
21
+ pending items, read `queue/items/`, open each `.json`, filter by
22
+ status. No manifest, no index.json.
23
+
24
+ Rationale: one fewer file to keep consistent. Directory listing is
25
+ O(n) but n is small (decision queues rarely exceed 50 items). If
26
+ listing exceeds 2 seconds or 500 items, revisit via `act:2b638b02`.
27
+
28
+ ## Schema Versioning
29
+
30
+ Every JSON file carries `schema_version`. Readers guard on version
31
+ and emit clear errors on unrecognized versions. Never silent wrong
32
+ behavior — if a reader sees `schema_version: 2` and only understands
33
+ `1`, it must fail loudly, not silently misparse.
34
+
35
+ ```javascript
36
+ if (item.schema_version !== 1) {
37
+ throw new Error(`Unsupported schema version ${item.schema_version} in ${filePath}`);
38
+ }
39
+ ```
40
+
41
+ ## Session-ID Join Key
42
+
43
+ Session IDs are the join key across queue items (via `transcript_ref`),
44
+ messages (Plan 8), and Ring 3 cursors (Plan 7). This is a load-bearing
45
+ assumption documented here so that if session IDs prove to be the wrong
46
+ unit (e.g., Claude Code changes ID semantics), the migration scope is
47
+ known: queue items, messages, and cursors all need updating.
48
+
49
+ Validate when Ring 3 first writes real data.
50
+
51
+ ## Summary Hard Cap
52
+
53
+ `state/summary.md` must not exceed 30 lines. This is the SessionStart
54
+ injection payload — it competes with other context for the model's
55
+ attention window. If content exceeds 30 lines, truncation order:
56
+
57
+ 1. Drop Health section detail (keep one-line summary)
58
+ 2. Drop Portfolio Pulse detail for quiet projects
59
+ 3. Never truncate "What Needs Attention" or "Where You Left Off"
60
+
61
+ ## Enrichment Directory
62
+
63
+ Per-item enrichment lives in `queue/items/<id>/enrichment/`. Four
64
+ standard files:
65
+
66
+ | File | Written by | Content |
67
+ |------|-----------|---------|
68
+ | `code-context.md` | Ring 2 fast | Actual code being discussed |
69
+ | `related-decisions.md` | Ring 2 fast | Prior decisions bearing on this |
70
+ | `memory-refs.md` | Ring 2 fast | Relevant memory entries |
71
+ | `options-analysis.md` | Ring 2 fast | Pros/cons with evidence |
72
+
73
+ `/decisions` reads these when `enrichment_status` is `"complete"`.
74
+ Missing files degrade gracefully (null in the read result).
75
+
76
+ ## Deferred Schemas
77
+
78
+ The following are specified in project notes but NOT formalized until
79
+ their owning plan ships:
80
+
81
+ | Schema | Owner | Ships with |
82
+ |--------|-------|-----------|
83
+ | `messages/<session-id>.json` | Plan 8 | Session-directed messages |
84
+ | `ring3/cursors/<session-id>.json` | Plan 7 | Ring 3 live mode |
85
+ | `hooks/<ring>-<phase>.d/` | Plan 9 | Lifecycle hooks |
86
+ | `logs/<ring>.log` | Plan 4 | Ring 1 runner |
87
+ | `lock/<ring>.pid` | Plan 4 | Ring 1 runner |