create-claude-cabinet 0.45.0 → 0.46.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 (53) hide show
  1. package/README.md +4 -4
  2. package/lib/cli.js +26 -0
  3. package/lib/engagement-server-setup.js +34 -9
  4. package/lib/migrate-from-omega.js +13 -1
  5. package/lib/mux-setup.js +33 -9
  6. package/lib/watchtower-setup.js +210 -0
  7. package/package.json +5 -1
  8. package/templates/cabinet/_cabinet-member-template.md +8 -3
  9. package/templates/cabinet/advisories-state-schema.md +34 -7
  10. package/templates/cabinet/composition-patterns.md +4 -3
  11. package/templates/cabinet/skill-output-conventions.md +35 -1
  12. package/templates/cabinet/watchtower-contracts.md +89 -1
  13. package/templates/engagement/pib-db-patches/pib-db-lib.mjs +10 -1
  14. package/templates/mux/__tests__/mux-fail-loud.fixture.sh +44 -0
  15. package/templates/mux/__tests__/station-liveness.fixture.sh +234 -0
  16. package/templates/mux/__tests__/station-liveness.test.mjs +47 -0
  17. package/templates/mux/bin/mux +281 -55
  18. package/templates/scripts/__tests__/advisor-pass.test.mjs +238 -0
  19. package/templates/scripts/__tests__/advisories.test.mjs +262 -0
  20. package/templates/scripts/__tests__/batch-disposition.test.mjs +137 -0
  21. package/templates/scripts/__tests__/feedback-outbox-flush.test.mjs +232 -0
  22. package/templates/scripts/__tests__/qa-handoff-gate.test.mjs +68 -0
  23. package/templates/scripts/__tests__/ring-state-ownership.test.mjs +108 -3
  24. package/templates/scripts/__tests__/ring2-thread-context.test.mjs +189 -0
  25. package/templates/scripts/__tests__/ring3-dedup.test.mjs +387 -0
  26. package/templates/scripts/__tests__/routine-dispatch.test.mjs +312 -0
  27. package/templates/scripts/watchtower-advisories.mjs +305 -0
  28. package/templates/scripts/watchtower-build-context.mjs +110 -11
  29. package/templates/scripts/watchtower-lib.mjs +177 -1
  30. package/templates/scripts/watchtower-queue.mjs +146 -1
  31. package/templates/scripts/watchtower-ring1.mjs +129 -9
  32. package/templates/scripts/watchtower-ring2.mjs +118 -21
  33. package/templates/scripts/watchtower-ring3-close.mjs +466 -49
  34. package/templates/scripts/watchtower-routines.mjs +358 -0
  35. package/templates/scripts/watchtower-status.sh +1 -1
  36. package/templates/skills/audit/SKILL.md +5 -1
  37. package/templates/skills/briefing/SKILL.md +342 -234
  38. package/templates/skills/cabinet-anthropic-insider/SKILL.md +14 -6
  39. package/templates/skills/cabinet-historian/SKILL.md +14 -11
  40. package/templates/skills/cabinet-system-advocate/SKILL.md +22 -21
  41. package/templates/skills/cabinet-user-advocate/SKILL.md +13 -7
  42. package/templates/skills/cc-publish/SKILL.md +105 -19
  43. package/templates/skills/debrief/SKILL.md +127 -12
  44. package/templates/skills/execute/SKILL.md +6 -0
  45. package/templates/skills/inbox/SKILL.md +67 -6
  46. package/templates/skills/orient/SKILL.md +69 -47
  47. package/templates/skills/plan/SKILL.md +8 -0
  48. package/templates/skills/qa-drain/SKILL.md +119 -0
  49. package/templates/skills/session-handoff/SKILL.md +175 -6
  50. package/templates/skills/triage-audit/SKILL.md +6 -0
  51. package/templates/skills/watchtower/SKILL.md +46 -1
  52. package/templates/watchtower/config.json.template +3 -1
  53. package/templates/watchtower/queue/items/item.json.schema +1 -1
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, 32 domain experts, a planning process, and the
4
+ gives Claude a memory, 34 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** — 32 domain experts (security, accessibility,
15
+ - **Cabinet members** — 34 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
- 32 expert cabinet members who each own a domain and stay in their lane.
81
+ 34 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
@@ -256,7 +256,7 @@ source code.
256
256
  ```
257
257
  .claude/
258
258
  ├── skills/ # orient, debrief, plan, execute, audit, etc.
259
- │ └── cabinet-*/ # 32 cabinet member definitions
259
+ │ └── cabinet-*/ # 34 cabinet member definitions
260
260
  ├── cabinet/ # committees, lifecycle, composition patterns
261
261
  │ # (incl. pib-db-access.md, pib-db-triggers.md)
262
262
  ├── briefing/ # project briefing templates
package/lib/cli.js CHANGED
@@ -12,6 +12,7 @@ const { setupSiteAuditRuntime } = require('./site-audit-setup');
12
12
  const { setupEngagement } = require('./engagement-setup');
13
13
  const { setupMux } = require('./mux-setup');
14
14
  const { setupEngagementServer } = require('./engagement-server-setup');
15
+ const { refreshWatchtowerRuntime } = require('./watchtower-setup');
15
16
  const { reset } = require('./reset');
16
17
 
17
18
  const VERSION = require('../package.json').version;
@@ -632,6 +633,9 @@ const MODULES = {
632
633
  'cabinet/watchtower-contracts.md',
633
634
  'scripts/watchtower-lib.mjs',
634
635
  'scripts/watchtower-queue.mjs',
636
+ 'scripts/watchtower-routines.mjs',
637
+ 'scripts/watchtower-advisories.mjs',
638
+ 'cabinet/advisories-state-schema.md',
635
639
  'skills/inbox',
636
640
  'hooks/watchtower-session-start.sh',
637
641
  'scripts/watchtower-build-context.mjs',
@@ -655,6 +659,7 @@ const MODULES = {
655
659
  'skills/briefing',
656
660
  'skills/threads',
657
661
  'skills/qa-handoff',
662
+ 'skills/qa-drain',
658
663
  ],
659
664
  },
660
665
  mux: {
@@ -1411,6 +1416,27 @@ async function run() {
1411
1416
  }
1412
1417
  }
1413
1418
 
1419
+ // --- Refresh the GLOBAL watchtower runtime (content-aware) ---
1420
+ // The watchtower module copies its files into the PROJECT, but the global
1421
+ // runtime at ~/.claude-cabinet/watchtower/ is set up only by the one-time
1422
+ // `/watchtower install` SKILL.md step. Without this, a reinstall left the
1423
+ // global runtime scripts/docs/hooks stale and never delivered newly shipped
1424
+ // ones. refreshWatchtowerRuntime is REFRESH-ONLY: it no-ops (status
1425
+ // 'absent', zero writes) when no runtime exists yet, so it's safe to call
1426
+ // unconditionally whenever the module is selected.
1427
+ if (selectedModules.includes('watchtower')) {
1428
+ try {
1429
+ const result = refreshWatchtowerRuntime({ dryRun: !!flags.dryRun });
1430
+ if (result.status !== 'absent') {
1431
+ console.log('');
1432
+ for (const r of result.results || []) console.log(` 📋 ${r}`);
1433
+ }
1434
+ } catch (err) {
1435
+ console.log(` ⚠ watchtower runtime refresh failed: ${err.message}`);
1436
+ console.log(' Re-run the installer to retry.');
1437
+ }
1438
+ }
1439
+
1414
1440
  // --- Manifest key migration (act:d1f16bee) ---
1415
1441
  // When CC renames directories (e.g., perspectives/ → cabinet-*/), old manifest
1416
1442
  // keys no longer match new template paths. Migrate keys BEFORE cleanup so the
@@ -9,7 +9,10 @@
9
9
  *
10
10
  * Version semantics:
11
11
  * - First install: copy all managed files, create data dir, write .cc-version
12
- * - Same version: skip with log
12
+ * - Same version: fall through to the SHA256 manifest hash-compare and
13
+ * copy only files whose hashes differ (dogfood-from-source changes
14
+ * templates without a version bump — equal-version must still
15
+ * propagate; the manifest check makes this cheap and idempotent)
13
16
  * - Newer CC version: upgrade managed files, preserve data dir
14
17
  * - Older CC version than installed: skip (don't downgrade)
15
18
  */
@@ -23,6 +26,7 @@ const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
23
26
  const GLOBAL_MANIFEST_PATH = path.join(CC_HOME, 'global-manifest.json');
24
27
  const INSTALL_DIR = path.join(CC_HOME, 'engagement-server');
25
28
  const VERSION_FILE = path.join(INSTALL_DIR, '.cc-version');
29
+ const TEMPLATE_DIR = path.resolve(__dirname, '..', 'templates', 'engagement-server');
26
30
 
27
31
  const MANAGED_FILES = [
28
32
  'server.mjs',
@@ -99,7 +103,7 @@ function setupEngagementServer(opts = {}) {
99
103
  const results = [];
100
104
 
101
105
  const ccVersion = require('../package.json').version;
102
- const templateDir = path.resolve(__dirname, '..', 'templates', 'engagement-server');
106
+ const templateDir = TEMPLATE_DIR;
103
107
 
104
108
  if (!fs.existsSync(templateDir)) {
105
109
  throw new Error(`engagement-server-setup: ${templateDir} not found.`);
@@ -109,15 +113,19 @@ function setupEngagementServer(opts = {}) {
109
113
 
110
114
  if (installedVersion) {
111
115
  const cmp = compareVersions(ccVersion, installedVersion);
112
- if (cmp === 0) {
113
- results.push(`engagement-server ${installedVersion} already installed — skipping`);
114
- return { results, status: 'skipped' };
115
- }
116
116
  if (cmp < 0) {
117
117
  results.push(`engagement-server ${installedVersion} is newer than CC ${ccVersion} — skipping (won't downgrade)`);
118
118
  return { results, status: 'skipped' };
119
119
  }
120
- results.push(`Upgrading engagement-server from ${installedVersion} to ${ccVersion}`);
120
+ // Equal version (cmp === 0): do NOT early-return. Fall through to the
121
+ // SHA256 manifest hash-compare below — it copies only changed files,
122
+ // so a dogfood-from-source template edit propagates even without a
123
+ // package.json version bump. The hash-compare makes this idempotent.
124
+ if (cmp === 0) {
125
+ results.push(`engagement-server ${installedVersion} already installed — checking for changed files`);
126
+ } else {
127
+ results.push(`Upgrading engagement-server from ${installedVersion} to ${ccVersion}`);
128
+ }
121
129
  } else {
122
130
  results.push(`Installing engagement-server ${ccVersion}`);
123
131
  }
@@ -187,7 +195,24 @@ function setupEngagementServer(opts = {}) {
187
195
  results.push(` Re-deploy: cd ${INSTALL_DIR} && railway up --detach`);
188
196
  }
189
197
 
190
- return { results, status: installedVersion ? 'upgraded' : 'installed' };
198
+ let status;
199
+ if (!installedVersion) {
200
+ status = 'installed';
201
+ } else if (copiedCount > 0) {
202
+ status = 'upgraded';
203
+ } else {
204
+ // Equal-or-newer version, manifest already current — nothing to do.
205
+ status = 'unchanged';
206
+ }
207
+ return { results, status };
191
208
  }
192
209
 
193
- module.exports = { setupEngagementServer };
210
+ // MANAGED_FILES, INSTALL_DIR, and TEMPLATE_DIR are exported for the
211
+ // version-gate test so it can seed a faithful, complete global manifest
212
+ // (single source of truth — the test never re-derives the file list).
213
+ module.exports = {
214
+ setupEngagementServer,
215
+ MANAGED_FILES,
216
+ INSTALL_DIR,
217
+ TEMPLATE_DIR,
218
+ };
@@ -82,6 +82,13 @@ function stripUserPrefix(raw, homeDir) {
82
82
  const segs = tail.split('/');
83
83
  return segs.slice(1).join('/');
84
84
  }
85
+ // Omega project keys are historical strings recorded on whatever machine
86
+ // wrote them — a DB carried from a Mac to a Linux box (or exercised on a
87
+ // Linux CI runner) still holds /Users/<name>/... paths that the current
88
+ // machine's home root won't match. Recognize the conventional home roots
89
+ // structurally so cross-machine migrations classify instead of crashing.
90
+ const foreignHome = raw.match(/^\/(?:Users|home)\/[^/]+\/(.+)$/);
91
+ if (foreignHome) return foreignHome[1];
85
92
  return raw;
86
93
  }
87
94
 
@@ -100,7 +107,12 @@ function canonicalizeProjectKey(raw, ctx = {}) {
100
107
  const slugPath = stripUserPrefix(work, ctx.homeDir);
101
108
  const topSlug = slugPath.split('/')[0];
102
109
 
103
- return { canonical: topSlug || null, kind: 'project' };
110
+ // kind 'project' with a null canonical is an incoherent contract — the
111
+ // bucketing consumer would key a cross-project map on null and crash on
112
+ // slug derivation. An unclassifiable key is unscoped, not a null project.
113
+ if (!topSlug) return { canonical: null, kind: 'unscoped' };
114
+
115
+ return { canonical: topSlug, kind: 'project' };
104
116
  }
105
117
 
106
118
  function resolveCurrentProject(cwd, homeDir) {
package/lib/mux-setup.js CHANGED
@@ -8,7 +8,10 @@
8
8
  *
9
9
  * Version semantics:
10
10
  * - First install: copy all managed files, write .cc-version
11
- * - Same version: skip with log
11
+ * - Same version: fall through to the SHA256 manifest hash-compare and
12
+ * copy only files whose hashes differ (dogfood-from-source changes
13
+ * templates without a version bump — equal-version must still
14
+ * propagate; the manifest check makes this cheap and idempotent)
12
15
  * - Newer CC version: upgrade managed files, preserve data dirs
13
16
  * - Older CC version than installed: skip (don't downgrade)
14
17
  */
@@ -21,6 +24,7 @@ const crypto = require('crypto');
21
24
  const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
22
25
  const GLOBAL_MANIFEST_PATH = path.join(CC_HOME, 'global-manifest.json');
23
26
  const MUX_VERSION_FILE = path.join(os.homedir(), '.config', 'mux', '.cc-version');
27
+ const TEMPLATE_DIR = path.resolve(__dirname, '..', 'templates', 'mux');
24
28
 
25
29
  const MANAGED_FILES = [
26
30
  { src: 'bin/mux', dest: path.join(os.homedir(), '.local', 'bin', 'mux'), mode: 0o755 },
@@ -122,7 +126,7 @@ function setupMux(opts = {}) {
122
126
  const results = [];
123
127
 
124
128
  const ccVersion = require('../package.json').version;
125
- const templateDir = path.resolve(__dirname, '..', 'templates', 'mux');
129
+ const templateDir = TEMPLATE_DIR;
126
130
 
127
131
  if (!fs.existsSync(templateDir)) {
128
132
  throw new Error(`mux-setup: ${templateDir} not found.`);
@@ -132,15 +136,19 @@ function setupMux(opts = {}) {
132
136
 
133
137
  if (installedVersion) {
134
138
  const cmp = compareVersions(ccVersion, installedVersion);
135
- if (cmp === 0) {
136
- results.push(`mux ${installedVersion} already installed — skipping`);
137
- return { results, status: 'skipped' };
138
- }
139
139
  if (cmp < 0) {
140
140
  results.push(`mux ${installedVersion} is newer than CC ${ccVersion} — skipping (won't downgrade)`);
141
141
  return { results, status: 'skipped' };
142
142
  }
143
- results.push(`Upgrading mux from ${installedVersion} to ${ccVersion}`);
143
+ // Equal version (cmp === 0): do NOT early-return. Fall through to the
144
+ // SHA256 manifest hash-compare below — it copies only changed files,
145
+ // so a dogfood-from-source template edit propagates even without a
146
+ // package.json version bump. The hash-compare makes this idempotent.
147
+ if (cmp === 0) {
148
+ results.push(`mux ${installedVersion} already installed — checking for changed files`);
149
+ } else {
150
+ results.push(`Upgrading mux from ${installedVersion} to ${ccVersion}`);
151
+ }
144
152
  } else {
145
153
  results.push(`Installing mux ${ccVersion}`);
146
154
  }
@@ -200,7 +208,16 @@ function setupMux(opts = {}) {
200
208
  setupDarwinIntegration({ dryRun, results });
201
209
  }
202
210
 
203
- return { results, status: installedVersion ? 'upgraded' : 'installed' };
211
+ let status;
212
+ if (!installedVersion) {
213
+ status = 'installed';
214
+ } else if (copiedCount > 0) {
215
+ status = 'upgraded';
216
+ } else {
217
+ // Equal-or-newer version, manifest already current — nothing to do.
218
+ status = 'unchanged';
219
+ }
220
+ return { results, status };
204
221
  }
205
222
 
206
223
  /**
@@ -308,4 +325,11 @@ function setupDarwinIntegration({ dryRun, results }) {
308
325
  }
309
326
  }
310
327
 
311
- module.exports = { setupMux };
328
+ // MANAGED_FILES and TEMPLATE_DIR are exported for the version-gate test so it
329
+ // can seed a faithful, complete global manifest (single source of truth — the
330
+ // test never re-derives the file list).
331
+ module.exports = {
332
+ setupMux,
333
+ MANAGED_FILES,
334
+ TEMPLATE_DIR,
335
+ };
@@ -0,0 +1,210 @@
1
+ /**
2
+ * watchtower-setup.js — keep the GLOBAL watchtower runtime fresh on reinstall.
3
+ *
4
+ * The watchtower module copies its files into the PROJECT (the normal module
5
+ * copy in lib/cli.js). But the GLOBAL runtime at ~/.claude-cabinet/watchtower/
6
+ * is only ever set up by the manual `/watchtower install` SKILL.md one-time
7
+ * step. So `npx create-claude-cabinet` (reinstall) refreshed the project files
8
+ * but NOT the global runtime scripts/docs/hooks — they went stale, and a
9
+ * brand-new runtime script (e.g. watchtower-advisories.mjs) never appeared
10
+ * until a hand-copy.
11
+ *
12
+ * This installer mirrors lib/mux-setup.js's content-aware refresh: a global
13
+ * SHA256 manifest at ~/.claude-cabinet/global-manifest.json records each
14
+ * dest's last-written hash, and only changed-or-new files are copied. Reusing
15
+ * mux's manifest is safe — entries are keyed by absolute dest path, so the
16
+ * watchtower dests (~/.claude-cabinet/watchtower/...) never collide with mux's
17
+ * (~/.config/mux/..., ~/.local/bin/...).
18
+ *
19
+ * REFRESH-ONLY semantics (critical): if the runtime directory does NOT already
20
+ * exist, this returns immediately with status 'absent' and writes nothing.
21
+ * Fresh runtime setup — launchd plist / cron, config.json, migrate-keys, the
22
+ * coherence assertion — is the `/watchtower install` SKILL.md step's job and is
23
+ * NOT replicated here. This function only keeps the code/docs/hooks of an
24
+ * EXISTING runtime current.
25
+ *
26
+ * The .mjs script set is globbed from templates/scripts/watchtower-*.mjs at
27
+ * load time (single source of truth — a newly shipped runtime script is picked
28
+ * up automatically; that "new file appears" case was the watchtower-advisories
29
+ * failure). The shell runners, session hooks, and cabinet docs are mapped
30
+ * explicitly since they live in different template subtrees and land in
31
+ * different runtime subdirs.
32
+ */
33
+
34
+ const fs = require('fs');
35
+ const path = require('path');
36
+ const os = require('os');
37
+ const crypto = require('crypto');
38
+
39
+ const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
40
+ const GLOBAL_MANIFEST_PATH = path.join(CC_HOME, 'global-manifest.json');
41
+ const RUNTIME_DIR = path.join(CC_HOME, 'watchtower');
42
+ const RUNTIME_SCRIPTS_DIR = path.join(RUNTIME_DIR, 'scripts');
43
+ const RUNTIME_HOOKS_DIR = path.join(RUNTIME_DIR, 'hooks');
44
+ const RUNTIME_CABINET_DIR = path.join(RUNTIME_DIR, 'cabinet');
45
+ const TEMPLATE_DIR = path.resolve(__dirname, '..', 'templates');
46
+
47
+ /**
48
+ * Build the MANAGED_FILES list: { src (absolute), dest (absolute), mode? }.
49
+ *
50
+ * - ALL templates/scripts/watchtower-*.mjs → runtime scripts/
51
+ * - the watchtower shell runners under scripts/ → runtime scripts/ (0o755)
52
+ * - templates/hooks/watchtower-session-*.sh → runtime hooks/ (0o755)
53
+ * - the two cabinet docs → runtime cabinet/
54
+ *
55
+ * Globbing the .mjs set keeps it the single source of truth — no hand-picked
56
+ * subset that can drift as new runtime scripts ship.
57
+ */
58
+ function buildManagedFiles() {
59
+ const files = [];
60
+ const scriptsDir = path.join(TEMPLATE_DIR, 'scripts');
61
+
62
+ // All watchtower runtime .mjs scripts (globbed — complete set, no drift).
63
+ if (fs.existsSync(scriptsDir)) {
64
+ for (const name of fs.readdirSync(scriptsDir)) {
65
+ if (/^watchtower-.*\.mjs$/.test(name)) {
66
+ files.push({
67
+ src: path.join(scriptsDir, name),
68
+ dest: path.join(RUNTIME_SCRIPTS_DIR, name),
69
+ });
70
+ }
71
+ }
72
+ }
73
+
74
+ // Shell runners the runtime executes (cron/launchd target these). They live
75
+ // in templates/scripts/ and land beside the .mjs in the runtime scripts/ dir.
76
+ const shellRunners = [
77
+ 'watchtower-ring1-runner.sh',
78
+ 'watchtower-ring2-runner.sh',
79
+ 'watchtower-status.sh',
80
+ ];
81
+ for (const name of shellRunners) {
82
+ files.push({
83
+ src: path.join(scriptsDir, name),
84
+ dest: path.join(RUNTIME_SCRIPTS_DIR, name),
85
+ mode: 0o755,
86
+ });
87
+ }
88
+
89
+ // Session hooks → runtime hooks/.
90
+ const hooksDir = path.join(TEMPLATE_DIR, 'hooks');
91
+ for (const name of ['watchtower-session-start.sh', 'watchtower-session-end.sh']) {
92
+ files.push({
93
+ src: path.join(hooksDir, name),
94
+ dest: path.join(RUNTIME_HOOKS_DIR, name),
95
+ mode: 0o755,
96
+ });
97
+ }
98
+
99
+ // Cabinet docs the runtime / advisory pass reads → runtime cabinet/.
100
+ const cabinetDir = path.join(TEMPLATE_DIR, 'cabinet');
101
+ for (const name of ['advisories-state-schema.md', 'watchtower-contracts.md']) {
102
+ files.push({
103
+ src: path.join(cabinetDir, name),
104
+ dest: path.join(RUNTIME_CABINET_DIR, name),
105
+ });
106
+ }
107
+
108
+ return files;
109
+ }
110
+
111
+ const MANAGED_FILES = buildManagedFiles();
112
+
113
+ function sha256(content) {
114
+ return crypto.createHash('sha256').update(content).digest('hex');
115
+ }
116
+
117
+ function readGlobalManifest() {
118
+ if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
119
+ try {
120
+ const m = JSON.parse(fs.readFileSync(GLOBAL_MANIFEST_PATH, 'utf8'));
121
+ // Guard shape drift: manifest.files is indexed unconditionally below.
122
+ if (typeof m !== 'object' || m === null || Array.isArray(m)) return { files: {} };
123
+ if (typeof m.files !== 'object' || m.files === null || Array.isArray(m.files)) m.files = {};
124
+ return m;
125
+ } catch {
126
+ return { files: {} };
127
+ }
128
+ }
129
+
130
+ function writeGlobalManifest(manifest) {
131
+ fs.mkdirSync(path.dirname(GLOBAL_MANIFEST_PATH), { recursive: true });
132
+ const tmp = GLOBAL_MANIFEST_PATH + '.tmp';
133
+ fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2));
134
+ fs.renameSync(tmp, GLOBAL_MANIFEST_PATH);
135
+ }
136
+
137
+ /**
138
+ * Refresh the code/docs/hooks of an EXISTING watchtower runtime, content-aware.
139
+ *
140
+ * @param {Object} opts
141
+ * @param {boolean} [opts.dryRun]
142
+ * @param {string[]} [opts.results] — human-readable lines are pushed here; a
143
+ * fresh array is created and returned when not supplied.
144
+ * @returns {{ results: string[], status: 'absent'|'unchanged'|'refreshed' }}
145
+ */
146
+ function refreshWatchtowerRuntime(opts = {}) {
147
+ const dryRun = !!opts.dryRun;
148
+ const results = opts.results || [];
149
+
150
+ // Refresh-only: never bootstrap a fresh runtime here. Fresh setup (launchd,
151
+ // cron, config.json, migrate-keys, coherence assertion) is the
152
+ // `/watchtower install` SKILL.md step's job. No runtime dir ⇒ no-op.
153
+ if (!fs.existsSync(RUNTIME_DIR)) {
154
+ return { results, status: 'absent' };
155
+ }
156
+
157
+ results.push('Refreshing watchtower runtime (content-aware)');
158
+
159
+ const manifest = readGlobalManifest();
160
+ let copiedCount = 0;
161
+
162
+ for (const file of MANAGED_FILES) {
163
+ if (!fs.existsSync(file.src)) {
164
+ results.push(` ⚠ Template missing: ${file.src}`);
165
+ continue;
166
+ }
167
+
168
+ const content = fs.readFileSync(file.src);
169
+ const hash = sha256(content);
170
+
171
+ if (manifest.files[file.dest] === hash) {
172
+ continue; // file unchanged
173
+ }
174
+
175
+ if (dryRun) {
176
+ results.push(` [dry-run] ${path.relative(TEMPLATE_DIR, file.src)} → ${file.dest}`);
177
+ } else {
178
+ fs.mkdirSync(path.dirname(file.dest), { recursive: true });
179
+ fs.writeFileSync(file.dest, content);
180
+ if (file.mode) {
181
+ fs.chmodSync(file.dest, file.mode);
182
+ }
183
+ manifest.files[file.dest] = hash;
184
+ }
185
+ copiedCount++;
186
+ }
187
+
188
+ if (!dryRun && copiedCount > 0) {
189
+ manifest.installedAt = new Date().toISOString();
190
+ writeGlobalManifest(manifest);
191
+ }
192
+
193
+ if (copiedCount > 0) {
194
+ results.push(` ${copiedCount} runtime file${copiedCount !== 1 ? 's' : ''} refreshed under ${RUNTIME_DIR}`);
195
+ return { results, status: 'refreshed' };
196
+ }
197
+
198
+ results.push(' watchtower runtime already current');
199
+ return { results, status: 'unchanged' };
200
+ }
201
+
202
+ // MANAGED_FILES, RUNTIME_DIR, and TEMPLATE_DIR are exported for the
203
+ // runtime-refresh test so it can seed a faithful, complete global manifest
204
+ // (single source of truth — the test never re-derives the file list).
205
+ module.exports = {
206
+ refreshWatchtowerRuntime,
207
+ MANAGED_FILES,
208
+ RUNTIME_DIR,
209
+ TEMPLATE_DIR,
210
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.45.0",
3
+ "version": "0.46.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"
@@ -22,6 +22,10 @@
22
22
  ],
23
23
  "author": "Oren Magid",
24
24
  "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/orenmagid/claude-cabinet.git"
28
+ },
25
29
  "engines": {
26
30
  "node": ">=18"
27
31
  },
@@ -27,7 +27,10 @@ briefing:
27
27
  # Common: .claude/cabinet/_briefing-architecture.md, _briefing-jurisdictions.md, _briefing-api.md
28
28
  standing-mandate: audit
29
29
  # Add plan, execute, orient, debrief if this member should activate
30
- # in those contexts. Most members are audit-only.
30
+ # in those contexts. Most members are audit-only. Two session-boundary
31
+ # contexts exist for standing advisors (watchtower installs):
32
+ # session-close (Ring 3's automatic transcript-fed advisor pass) and
33
+ # briefing (/briefing's live advisor panel).
31
34
  tools:
32
35
  # List every external tool/command this member uses.
33
36
  # Format: "tool-name (scope -- what it does)"
@@ -37,8 +40,10 @@ tools:
37
40
  # - grep patterns (all projects -- dead code detection)
38
41
  # Use "tools: []" for pure-reasoning members with no tool stage.
39
42
  directives:
40
- # Only if standing-mandate includes plan, execute, orient, or debrief.
41
- # Each directive is a one-sentence focused task for that context.
43
+ # Only if standing-mandate includes plan, execute, orient, debrief,
44
+ # session-close, or briefing. Each directive is a one-sentence focused
45
+ # task for that context. A mandate without a matching directive is a
46
+ # data error — consumers skip the member and say so.
42
47
  # plan: >
43
48
  # What to evaluate when reviewing a plan.
44
49
  # execute: >
@@ -1,12 +1,18 @@
1
1
  # Advisory dismissal state — schema and rules
2
2
 
3
- Orient surfaces stack-aware advisories (install the Ruby language server, register the Railway MCP, install `hookify`, …). Without memory, every advisory re-nags every session — the same attention-fatigue pattern the watchtower rings were built to eliminate. This file defines the per-project state that gives advisories a memory, and the exact rules orient follows so an advisory is never *permanently* silenced by accident.
3
+ The watchtower **SessionStart context builder** surfaces stack-aware advisories (install the Ruby language server, register the Railway MCP, install `hookify`, …) via `scripts/watchtower-advisories.mjs` (act:f9ea075d). Without memory, every advisory re-nags every session — the same attention-fatigue pattern the watchtower rings were built to eliminate. This file defines the per-project state that gives advisories a memory, and the exact rules the advisory pass follows so an advisory is never *permanently* silenced by accident.
4
+
5
+ > **Actor.** The owner of these rules is `watchtower-advisories.mjs` (`runAdvisoryPass`), called by the context builder when watchtower is installed. On a project WITHOUT watchtower, orient runs a thin one-shot fallback (it may shell the same module via `node scripts/watchtower-advisories.mjs` when present, else surface basic install hints with no persistent dismissal state). The module is the single implementation of the rules below; do not re-encode them anywhere else.
4
6
 
5
7
  ## Where it lives
6
8
 
7
- `.claude/cabinet/advisories-state.json` — **per project, generated at runtime**, NOT shipped as a template. Orient creates it on first write. It must never be added to a module's template array: a shipped stub would overwrite a project's real dismissal history on reinstall (the `.ccrc.json` clobber class of bug). If the file is absent, every advisory is treated as never-seen.
9
+ `.claude/cabinet/advisories-state.json` — **per project, generated at runtime**, NOT shipped as a template. The advisory pass creates it on first write. It must never be added to a module's template array: a shipped stub would overwrite a project's real dismissal history on reinstall (the `.ccrc.json` clobber class of bug). If the file is absent OR malformed JSON, every advisory is treated as never-seen (the reader degrades to `{}`, never throws).
10
+
11
+ > Worktree note: `.claude/cabinet/` is copied per worktree, so dismissal state can diverge between a worktree and its main checkout. That is acceptable — advisories are advisory — and is the reason this is project-local, not user-global. **Path-consistency rule:** the pass reads AND writes the state at the SAME path (the session's `--project-path` cwd). Never read from one path and write to another, or a decline recorded in one place is invisible from the other and the advisory re-nags forever.
12
+
13
+ ## Reserved `_meta` key
8
14
 
9
- > Worktree note: `.claude/cabinet/` is copied per worktree, so dismissal state can diverge between a worktree and its main checkout. That is acceptable advisories are advisory and is the reason this is project-local, not user-global.
15
+ `_meta` is a **reserved top-level key**, NOT an advisory entry. It holds pass-level bookkeeping — currently `{ "last_probe": "<UTC YYYY-MM-DD>" }`, the throttle stamp for the `claude plugin list` install-probe (run at most once/day/checkout; UTC to match `last_shown` and sqlite `date('now')`). Any consumer iterating advisory entries (e.g. `Object.entries(state)`) MUST skip `_meta`. No advisory id may be named `_meta`.
10
16
 
11
17
  ## Schema
12
18
 
@@ -30,9 +36,26 @@ Orient surfaces stack-aware advisories (install the Ruby language server, regist
30
36
  - **`last_shown`** — ISO date of the most recent surfacing.
31
37
  - **`signal`** — *the key field that makes "resurface if the stack changed" actually work.* A short, deterministic fingerprint of the stack indicators present when the advisory was last shown/declined. For a multi-indicator advisory like Ruby (`Gemfile` OR `*.rb`), the fingerprint records *which* indicators were present (e.g. `gemfile` vs `gemfile+rb`), so a later change is detectable. Without this stored snapshot, orient has only the *current* indicators and no baseline to diff against — which is the gap this schema closes.
32
38
 
33
- ## The rules orient follows
39
+ ## Computing the signal (the indicator fingerprint)
40
+
41
+ A signal is a deterministic fingerprint of the stack indicators present now. Two rules keep it stable:
42
+
43
+ - **Sorted tokens.** A multi-indicator advisory joins its present indicators (e.g. Ruby = `gemfile`, `rb`, or `gemfile+rb`) **after sorting** them. Readdir order is not stable; without the sort, `gemfile+rb` and `rb+gemfile` would alternate and spuriously re-arm a declined advisory every session.
44
+ - **Bounded stack scan.** Source-file indicators (`*.ts`, `*.py`, `*.rb`) are detected by a **depth-limited (≤3) walk** with a denylist (`node_modules`, `dist`, `build`, `vendor`, `coverage`, `target`, `out`, plus all dot-dirs) and **early-exit on first match** (existence only). A recursive walk would be pathological on the session-start critical path; a root-only scan would silently miss the common `src/**/*.ts` layout (a TS project with no root `tsconfig.json`).
45
+
46
+ ## The probe is tri-state (the fourth input state)
34
47
 
35
- Before surfacing any advisory, orient computes the advisory's **current signal** (fingerprint of the indicators present now) and reads the stored entry:
48
+ The install-probe (`claude plugin list`) answers "is this plugin installed?" — but it can fail to answer (claude not on PATH, nonzero exit, timeout). Its result is therefore **`true | false | null`**, never a boolean. `null` ("unknown") is a distinct input state and is handled per advisory **kind**:
49
+
50
+ - **probe-suppressed** (the LSP advisories): the signal is on disk (stack files); the probe only *suppresses* (confirms installed → terminal). `installed===null` → **still surface** from the signal/state rules; freeze only the `installed` transition.
51
+ - **probe-gated** (`plugin:hookify`): the surfacing predicate *is* the probe ("hookify not installed"). `installed===null` → predicate unknowable → **suppress this session, do NOT increment count, do NOT write state** (freeze the entry untouched).
52
+ - **no-probe** (`mcp:railway`, `briefing-file`, `registry-orphan`): pure filesystem/config signal; the probe is irrelevant.
53
+
54
+ In all kinds, `installed===true` flips the entry to terminal `installed`.
55
+
56
+ ## The rules the advisory pass follows
57
+
58
+ Before surfacing any advisory, the pass computes the advisory's **current signal** (fingerprint of the indicators present now) and reads the stored entry:
36
59
 
37
60
  1. **No entry / file absent** → surface it. Write `{status:"suggested", count:1, last_shown:today, signal:current}`.
38
61
  2. **`installed`** → never surface (terminal). (Re-probe may flip a `suggested`/`declined` entry to `installed`; never the reverse automatically.)
@@ -64,5 +87,9 @@ Document any new advisory's signal source here when you add it, and call out exp
64
87
  | `lsp:rust` | `Cargo.toml` | `/plugin install rust-analyzer-lsp` |
65
88
  | `lsp:go` | `go.mod` | `/plugin install gopls-lsp` |
66
89
  | `lsp:ruby` | `Gemfile` or `*.rb` | `/plugin install ruby-lsp@claude-plugins-official` (also needs `gem install ruby-lsp` AND `ENABLE_LSP_TOOL=1`) |
67
- | `mcp:railway` | `railway.toml` and no railway key in `~/.claude.json` | local: `railway setup agent -y` · remote: register `mcp.railway.com` (OAuth) |
68
- | `plugin:hookify` | `.claude/rules/enforcement-pipeline.md` exists and hookify not in `claude plugin list` (signal is **static**) | `/plugin install hookify` |
90
+ | `mcp:railway` | `railway.toml` and no railway key in `~/.claude.json` (no-probe) | local: `railway setup agent -y` · remote: register `mcp.railway.com` (OAuth) |
91
+ | `plugin:hookify` | `.claude/rules/enforcement-pipeline.md` exists and hookify not in `claude plugin list` (signal is **static**; probe-gated) | `/plugin install hookify` |
92
+ | `briefing-file` | `.claude/briefing/_briefing.md` is **absent** (signal `missing`, **static**; no-probe) | run `/onboard` to create one |
93
+ | `registry-orphan` | `~/.claude/cc-registry.json` lists project path(s) that no longer exist (signal = sorted orphan-name set, so it re-arms when the registry changes; no-probe) | remove the dead entr(y/ies) from `~/.claude/cc-registry.json` |
94
+
95
+ > Note: `mcp:railway` keys on `railway.toml`, which Ring 1 also marker-checks for deploy detection — these are independent reads of the same file for different purposes; do not consolidate them (the advisory adds the `~/.claude.json` registration predicate Ring 1 lacks).
@@ -107,9 +107,10 @@ to do its own work. One cabinet member consults another mid-evaluation.
107
107
  needs full conversation history) references another cabinet member's known
108
108
  findings — from memory, from audit history, or from prior session output.
109
109
 
110
- **Example:** During debrief, a historian cabinet member is activated to check:
111
- "Has this kind of change been done before? What happened? Are there
112
- lessons from prior sessions relevant to what was just completed?"
110
+ **Example:** At session close (Ring 3's advisor pass), the historian is
111
+ activated to check: "Has this kind of change been done before? What
112
+ happened? Are there lessons from prior sessions relevant to what was
113
+ just completed?"
113
114
 
114
115
  **Example:** During planning, the organized-mind cabinet member might need
115
116
  the historian's input: "Has this kind of information architecture been