create-claude-cabinet 0.40.0 → 0.42.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 (93) hide show
  1. package/README.md +1 -1
  2. package/lib/cli.js +37 -6
  3. package/lib/engagement-server-setup.js +5 -1
  4. package/lib/metadata.js +9 -2
  5. package/lib/mux-setup.js +117 -1
  6. package/lib/settings-merge.js +56 -1
  7. package/package.json +4 -2
  8. package/templates/cabinet/_cabinet-member-template.md +4 -2
  9. package/templates/cabinet/advisories-state-schema.md +68 -0
  10. package/templates/cabinet/elicitation-methods.md +70 -0
  11. package/templates/cabinet/eval-protocol.md +20 -0
  12. package/templates/cabinet/skill-output-conventions.md +21 -3
  13. package/templates/engagement-server/__tests__/e2e-skills.test.mjs +147 -0
  14. package/templates/engagement-server/__tests__/server-harness.mjs +72 -0
  15. package/templates/engagement-server/__tests__/server.test.mjs +181 -0
  16. package/templates/hooks/action-completion-gate.sh +5 -2
  17. package/templates/hooks/action-quality-gate.sh +6 -3
  18. package/templates/hooks/bash-output-compress.sh +147 -0
  19. package/templates/hooks/cc-upstream-guard.sh +7 -3
  20. package/templates/hooks/git-guardrails.sh +7 -3
  21. package/templates/hooks/work-tracker-guard.sh +5 -2
  22. package/templates/mux/bin/mux +31 -6
  23. package/templates/mux/config/context-help.py +6 -0
  24. package/templates/mux/config/help.txt +16 -0
  25. package/templates/mux/config/manage-dx.py +2 -3
  26. package/templates/mux/config/mux.tmux.conf +27 -0
  27. package/templates/mux/config/muxlib.py +25 -4
  28. package/templates/mux/config/screenshot-to-clipboard.sh +45 -0
  29. package/templates/mux/config/show-dx.py +3 -4
  30. package/templates/mux/config/unwrap-copy.py +72 -0
  31. package/templates/rules/enforcement-pipeline.md +1 -1
  32. package/templates/rules/markdown-prose.md +9 -0
  33. package/templates/scripts/skill-usage.mjs +208 -0
  34. package/templates/scripts/watchtower-build-context.mjs +27 -12
  35. package/templates/scripts/watchtower-queue.mjs +8 -2
  36. package/templates/scripts/watchtower-ring1.mjs +37 -8
  37. package/templates/scripts/watchtower-ring2.mjs +99 -5
  38. package/templates/scripts/watchtower-ring3-close.mjs +67 -20
  39. package/templates/scripts/watchtower-status.sh +6 -0
  40. package/templates/skills/cabinet-accessibility/SKILL.md +4 -2
  41. package/templates/skills/cabinet-anthropic-insider/SKILL.md +10 -2
  42. package/templates/skills/cabinet-anti-confirmation/SKILL.md +4 -2
  43. package/templates/skills/cabinet-architecture/SKILL.md +4 -2
  44. package/templates/skills/cabinet-automation/SKILL.md +4 -2
  45. package/templates/skills/cabinet-boundary-man/SKILL.md +4 -2
  46. package/templates/skills/cabinet-cc-health/SKILL.md +4 -2
  47. package/templates/skills/cabinet-data-integrity/SKILL.md +4 -2
  48. package/templates/skills/cabinet-debugger/SKILL.md +4 -2
  49. package/templates/skills/cabinet-deployment/SKILL.md +265 -0
  50. package/templates/skills/cabinet-deployment/phases/scan-scope.md +40 -0
  51. package/templates/skills/cabinet-elegance/SKILL.md +4 -2
  52. package/templates/skills/cabinet-framework-quality/SKILL.md +4 -2
  53. package/templates/skills/cabinet-goal-alignment/SKILL.md +4 -2
  54. package/templates/skills/cabinet-historian/SKILL.md +4 -2
  55. package/templates/skills/cabinet-information-design/SKILL.md +4 -2
  56. package/templates/skills/cabinet-interactive-storyteller/SKILL.md +4 -2
  57. package/templates/skills/cabinet-mantine-quality/SKILL.md +4 -2
  58. package/templates/skills/cabinet-narrative-architect/SKILL.md +4 -2
  59. package/templates/skills/cabinet-organized-mind/SKILL.md +4 -2
  60. package/templates/skills/cabinet-process-therapist/SKILL.md +4 -2
  61. package/templates/skills/cabinet-qa/SKILL.md +4 -2
  62. package/templates/skills/cabinet-record-keeper/SKILL.md +4 -2
  63. package/templates/skills/cabinet-roster-check/SKILL.md +4 -2
  64. package/templates/skills/cabinet-security/SKILL.md +4 -2
  65. package/templates/skills/cabinet-seo/SKILL.md +150 -0
  66. package/templates/skills/cabinet-small-screen/SKILL.md +4 -2
  67. package/templates/skills/cabinet-speed-freak/SKILL.md +4 -2
  68. package/templates/skills/cabinet-system-advocate/SKILL.md +4 -2
  69. package/templates/skills/cabinet-technical-debt/SKILL.md +4 -2
  70. package/templates/skills/cabinet-ui-experimentalist/SKILL.md +4 -2
  71. package/templates/skills/cabinet-usability/SKILL.md +4 -2
  72. package/templates/skills/cabinet-user-advocate/SKILL.md +4 -2
  73. package/templates/skills/cabinet-vision/SKILL.md +11 -2
  74. package/templates/skills/cabinet-workflow-cop/SKILL.md +4 -2
  75. package/templates/skills/cc-link/SKILL.md +1 -0
  76. package/templates/skills/cc-publish/SKILL.md +1 -0
  77. package/templates/skills/cc-remember/SKILL.md +1 -0
  78. package/templates/skills/cc-unlink/SKILL.md +1 -0
  79. package/templates/skills/checklist-discover/SKILL.md +27 -25
  80. package/templates/skills/inbox/SKILL.md +17 -4
  81. package/templates/skills/memory/SKILL.md +1 -0
  82. package/templates/skills/menu/SKILL.md +1 -0
  83. package/templates/skills/onboard/SKILL.md +5 -0
  84. package/templates/skills/orient/SKILL.md +80 -8
  85. package/templates/skills/orient/phases/dx-captures.md +5 -3
  86. package/templates/skills/plan/SKILL.md +60 -1
  87. package/templates/skills/seed/SKILL.md +4 -1
  88. package/templates/skills/threads/SKILL.md +144 -0
  89. package/templates/skills/unwrap/SKILL.md +43 -0
  90. package/templates/skills/watchtower/SKILL.md +1 -1
  91. package/templates/watchtower/queue/items/item.json.schema +30 -3
  92. package/templates/workflows/deliberative-audit.js +38 -15
  93. package/templates/mux/config/__pycache__/muxlib.cpython-314.pyc +0 -0
package/README.md CHANGED
@@ -230,7 +230,7 @@ customization. You can pass multiple modules: `--modules verify,audit`.
230
230
  | **engagement** | Client engagement management — packets, billing, feedback loops |
231
231
  | **engagement-server** | Central multi-engagement API server (Railway/Fly deploy) |
232
232
  | **watchtower** | Continuous background state management replacing orient/debrief |
233
- | **mux** | Multi-project terminal manager — desks, auto-worktrees with shared identity, trail logging, DX captures, portal color-switching |
233
+ | **mux** | Multi-project terminal manager — desks, auto-worktrees with shared identity, trail logging, DX captures, portal color-switching, durable tmux bindings, clipboard copy with hard-wrap removal, screenshot-to-clipboard launchd watcher |
234
234
 
235
235
  ## CLI Options
236
236
 
package/lib/cli.js CHANGED
@@ -4,7 +4,7 @@ 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, mergeWatchtowerHooks, mergeMuxHooks } = require('./settings-merge');
7
+ const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks } = 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');
@@ -471,7 +471,7 @@ const MODULES = {
471
471
  mandatory: false,
472
472
  default: true,
473
473
  lean: true,
474
- 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'],
474
+ 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'],
475
475
  },
476
476
  'work-tracking': {
477
477
  name: 'Work Tracking (pib-db or markdown)',
@@ -488,7 +488,7 @@ const MODULES = {
488
488
  mandatory: false,
489
489
  default: true,
490
490
  lean: true,
491
- 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/qa-dimensions-template.yaml', 'scripts/qa-dimensions-validator.cjs', 'skills/orient/phases/checklist-status.md'],
491
+ templates: ['skills/plan', 'skills/execute', 'skills/execute/phases/post-impl-checklist.md', 'skills/debrief/phases/checklist-feedback.md', 'skills/checklist-discover', 'skills/generate-plan-groups', 'skills/execute-group', 'workflows/execute-group-implement.js', 'workflows/execute-group-complete.js', 'skills/investigate', 'cabinet/checkpoint-protocol.md', 'cabinet/elicitation-methods.md', 'cabinet/qa-dimensions-template.yaml', 'scripts/qa-dimensions-validator.cjs', 'skills/orient/phases/checklist-status.md'],
492
492
  },
493
493
  'compliance': {
494
494
  name: 'Compliance Stack (rules + enforcement)',
@@ -496,7 +496,7 @@ const MODULES = {
496
496
  mandatory: false,
497
497
  default: true,
498
498
  lean: false,
499
- templates: ['rules/enforcement-pipeline.md', 'rules/maintainability.md', 'memory/patterns/_pattern-template.md', 'memory/patterns/pattern-intelligence-first.md'],
499
+ templates: ['rules/enforcement-pipeline.md', 'rules/maintainability.md', 'rules/markdown-prose.md', 'skills/unwrap', 'memory/patterns/_pattern-template.md', 'memory/patterns/pattern-intelligence-first.md'],
500
500
  },
501
501
  'memory': {
502
502
  name: 'Built-In Memory (cc-remember + reader + validator)',
@@ -533,10 +533,12 @@ const MODULES = {
533
533
  'skills/cabinet-boundary-man',
534
534
  'skills/cabinet-anthropic-insider', 'skills/cabinet-cc-health',
535
535
  'skills/cabinet-data-integrity',
536
- 'skills/cabinet-debugger', 'skills/cabinet-historian',
536
+ 'skills/cabinet-debugger', 'skills/cabinet-deployment',
537
+ 'skills/cabinet-historian',
537
538
  'skills/cabinet-organized-mind', 'skills/cabinet-process-therapist',
538
539
  'skills/cabinet-qa', 'skills/cabinet-record-keeper',
539
540
  'skills/cabinet-roster-check', 'skills/cabinet-security',
541
+ 'skills/cabinet-seo',
540
542
  'skills/cabinet-small-screen', 'skills/cabinet-speed-freak',
541
543
  'skills/cabinet-system-advocate', 'skills/cabinet-technical-debt',
542
544
  'skills/cabinet-usability', 'skills/cabinet-workflow-cop',
@@ -559,7 +561,7 @@ const MODULES = {
559
561
  mandatory: false,
560
562
  default: true,
561
563
  lean: true,
562
- templates: ['skills/onboard', 'skills/seed', 'skills/cc-upgrade', 'skills/cc-link', 'skills/cc-unlink', 'skills/cc-extract', 'skills/cc-feedback'],
564
+ templates: ['skills/onboard', 'skills/seed', 'skills/cc-upgrade', 'skills/cc-link', 'skills/cc-unlink', 'skills/cc-extract', 'skills/cc-feedback', 'cabinet/elicitation-methods.md'],
563
565
  },
564
566
  'validate': {
565
567
  name: 'Validate',
@@ -654,6 +656,7 @@ const MODULES = {
654
656
  'scripts/watchtower-ring3-close.mjs',
655
657
  'scripts/watchtower-status.sh',
656
658
  'skills/briefing',
659
+ 'skills/threads',
657
660
  ],
658
661
  },
659
662
  mux: {
@@ -675,6 +678,15 @@ const MODULES = {
675
678
  postInstall: 'engagement-server-setup',
676
679
  templates: [],
677
680
  },
681
+ 'bash-compress': {
682
+ name: 'Bash Output Compression Hook',
683
+ 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).',
684
+ mandatory: false,
685
+ default: false,
686
+ lean: false,
687
+ requires: ['hooks'],
688
+ templates: ['hooks/bash-output-compress.sh'],
689
+ },
678
690
  };
679
691
 
680
692
  /** Recursively collect all relative file paths under a directory. */
@@ -1292,6 +1304,11 @@ async function run() {
1292
1304
  mergeMuxHooks(settingsPath);
1293
1305
  console.log(' ⚙️ Registered mux worktree health SessionStart hook');
1294
1306
  }
1307
+
1308
+ if (selectedModules.includes('bash-compress')) {
1309
+ mergeBashCompressHooks(settingsPath);
1310
+ console.log(' ⚙️ Registered bash-output compression PostToolUse hook');
1311
+ }
1295
1312
  }
1296
1313
 
1297
1314
  // --- Heal user-level ~/.claude/settings.json ---
@@ -1315,6 +1332,11 @@ async function run() {
1315
1332
  if (fs.existsSync(mcpJsonPath)) {
1316
1333
  existing = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
1317
1334
  }
1335
+ // Guard shape drift: .mcpServers set on a top-level array would be
1336
+ // silently dropped on serialize.
1337
+ if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {
1338
+ existing = {};
1339
+ }
1318
1340
  if (!existing.mcpServers) existing.mcpServers = {};
1319
1341
  Object.assign(existing.mcpServers, mcpConfig.mcpServers);
1320
1342
  fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
@@ -1567,6 +1589,15 @@ async function run() {
1567
1589
  if (fs.existsSync(registryPath)) {
1568
1590
  registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
1569
1591
  }
1592
+ // Normalize shape drift: a bare top-level array has been observed
1593
+ // (ad-hoc edit dropped the {"projects": ...} wrapper). Re-wrap so
1594
+ // the update below also heals the file on disk.
1595
+ if (Array.isArray(registry)) {
1596
+ registry = { projects: registry };
1597
+ }
1598
+ if (!Array.isArray(registry.projects)) {
1599
+ registry.projects = [];
1600
+ }
1570
1601
  const existingIdx = registry.projects.findIndex(p => p.path === projectDir);
1571
1602
  const entry = {
1572
1603
  path: projectDir,
@@ -57,7 +57,11 @@ function compareVersions(a, b) {
57
57
  function readGlobalManifest() {
58
58
  if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
59
59
  try {
60
- return JSON.parse(fs.readFileSync(GLOBAL_MANIFEST_PATH, 'utf8'));
60
+ const m = JSON.parse(fs.readFileSync(GLOBAL_MANIFEST_PATH, 'utf8'));
61
+ // Guard shape drift: manifest.files is indexed unconditionally below.
62
+ if (typeof m !== 'object' || m === null || Array.isArray(m)) return { files: {} };
63
+ if (typeof m.files !== 'object' || m.files === null || Array.isArray(m.files)) m.files = {};
64
+ return m;
61
65
  } catch {
62
66
  return { files: {} };
63
67
  }
package/lib/metadata.js CHANGED
@@ -10,13 +10,20 @@ function metadataPath(projectDir) {
10
10
 
11
11
  function read(projectDir) {
12
12
  const file = metadataPath(projectDir);
13
- if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf8'));
13
+ if (fs.existsSync(file)) return normalize(JSON.parse(fs.readFileSync(file, 'utf8')));
14
14
  // Fall back to legacy manifest from pre-v0.6.0 installs
15
15
  const legacyFile = path.join(projectDir, LEGACY_METADATA_FILE);
16
- if (fs.existsSync(legacyFile)) return JSON.parse(fs.readFileSync(legacyFile, 'utf8'));
16
+ if (fs.existsSync(legacyFile)) return normalize(JSON.parse(fs.readFileSync(legacyFile, 'utf8')));
17
17
  return null;
18
18
  }
19
19
 
20
+ // Guard shape drift: callers key into .modules/.manifest/.version — a
21
+ // non-object top level must read as "no metadata", not crash the installer.
22
+ function normalize(data) {
23
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) return null;
24
+ return data;
25
+ }
26
+
20
27
  function write(projectDir, data) {
21
28
  const file = metadataPath(projectDir);
22
29
  fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n');
package/lib/mux-setup.js CHANGED
@@ -45,6 +45,9 @@ const MANAGED_FILES = [
45
45
  { src: 'config/worktree-session-health.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-session-health.sh'), mode: 0o755 },
46
46
  { src: 'config/worktree-health-popup.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-health-popup.sh'), mode: 0o755 },
47
47
  { src: 'config/worktree-cleanup.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-cleanup.sh'), mode: 0o755 },
48
+ { src: 'config/mux.tmux.conf', dest: path.join(os.homedir(), '.config', 'mux', 'mux.tmux.conf') },
49
+ { src: 'config/unwrap-copy.py', dest: path.join(os.homedir(), '.config', 'mux', 'unwrap-copy.py'), mode: 0o755 },
50
+ { src: 'config/screenshot-to-clipboard.sh', dest: path.join(os.homedir(), '.config', 'mux', 'screenshot-to-clipboard.sh'), mode: 0o755 },
48
51
  ];
49
52
 
50
53
  const DATA_DIRS = [
@@ -75,7 +78,11 @@ function compareVersions(a, b) {
75
78
  function readGlobalManifest() {
76
79
  if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
77
80
  try {
78
- return JSON.parse(fs.readFileSync(GLOBAL_MANIFEST_PATH, 'utf8'));
81
+ const m = JSON.parse(fs.readFileSync(GLOBAL_MANIFEST_PATH, 'utf8'));
82
+ // Guard shape drift: manifest.files is indexed unconditionally below.
83
+ if (typeof m !== 'object' || m === null || Array.isArray(m)) return { files: {} };
84
+ if (typeof m.files !== 'object' || m.files === null || Array.isArray(m.files)) m.files = {};
85
+ return m;
79
86
  } catch {
80
87
  return { files: {} };
81
88
  }
@@ -187,7 +194,116 @@ function setupMux(opts = {}) {
187
194
 
188
195
  results.push(` ${copiedCount} file${copiedCount !== 1 ? 's' : ''} installed to user paths`);
189
196
 
197
+ if (process.platform === 'darwin') {
198
+ setupDarwinIntegration({ dryRun, results });
199
+ }
200
+
190
201
  return { results, status: installedVersion ? 'upgraded' : 'installed' };
191
202
  }
192
203
 
204
+ /**
205
+ * macOS-specific wiring: tmux.conf source line, live binding reload,
206
+ * and the screenshot-to-clipboard launchd watcher. All steps are
207
+ * idempotent and individually fault-tolerant — a failure in one is
208
+ * reported but never aborts the install.
209
+ */
210
+ function setupDarwinIntegration({ dryRun, results }) {
211
+ const { execSync } = require('child_process');
212
+ const run = (cmd) => execSync(cmd, { stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
213
+
214
+ // 1. ~/.tmux.conf sources mux.tmux.conf so mux bindings survive tmux
215
+ // server restarts (inline bind-key calls from bin/mux evaporate).
216
+ const tmuxConf = path.join(os.homedir(), '.tmux.conf');
217
+ const sourceLine = 'source-file -q ~/.config/mux/mux.tmux.conf';
218
+ try {
219
+ const existing = fs.existsSync(tmuxConf) ? fs.readFileSync(tmuxConf, 'utf8') : '';
220
+ if (!existing.includes('mux.tmux.conf')) {
221
+ if (dryRun) {
222
+ results.push(` [dry-run] append "${sourceLine}" to ~/.tmux.conf`);
223
+ } else {
224
+ const block = `\n# mux-managed bindings (CC) — keep this line; mux upgrades edit the sourced file\n${sourceLine}\n`;
225
+ fs.appendFileSync(tmuxConf, block);
226
+ results.push(' ~/.tmux.conf now sources mux.tmux.conf');
227
+ }
228
+ }
229
+ } catch (err) {
230
+ results.push(` ⚠ Could not update ~/.tmux.conf: ${err.message}`);
231
+ }
232
+
233
+ // 2. Apply bindings to a running tmux server immediately.
234
+ if (!dryRun) {
235
+ try {
236
+ run('tmux source-file ~/.config/mux/mux.tmux.conf');
237
+ results.push(' mux.tmux.conf applied to running tmux server');
238
+ } catch {
239
+ // No server running — bindings load at next server start via ~/.tmux.conf.
240
+ }
241
+ }
242
+
243
+ // 3. Screenshot-to-clipboard launchd watcher. Watches the macOS
244
+ // screenshot folder and puts each new screenshot on the clipboard
245
+ // as a file reference.
246
+ const label = 'com.mux.screenshot-to-clipboard';
247
+ const agentDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
248
+ const plistPath = path.join(agentDir, `${label}.plist`);
249
+ const legacyLabel = 'com.orenmagid.screenshot-to-clipboard';
250
+ const legacyPlist = path.join(agentDir, `${legacyLabel}.plist`);
251
+ const scriptPath = path.join(os.homedir(), '.config', 'mux', 'screenshot-to-clipboard.sh');
252
+
253
+ let shotDir = path.join(os.homedir(), 'Desktop');
254
+ try {
255
+ const loc = run('defaults read com.apple.screencapture location');
256
+ if (loc) shotDir = loc.replace(/^~/, os.homedir());
257
+ } catch {
258
+ /* default location */
259
+ }
260
+
261
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
262
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
263
+ <plist version="1.0">
264
+ <dict>
265
+ <key>Label</key>
266
+ <string>${label}</string>
267
+ <key>ProgramArguments</key>
268
+ <array>
269
+ <string>/bin/bash</string>
270
+ <string>${scriptPath}</string>
271
+ </array>
272
+ <key>WatchPaths</key>
273
+ <array>
274
+ <string>${shotDir}</string>
275
+ </array>
276
+ </dict>
277
+ </plist>
278
+ `;
279
+
280
+ if (dryRun) {
281
+ results.push(` [dry-run] install launchd watcher ${label} (watching ${shotDir})`);
282
+ return;
283
+ }
284
+
285
+ try {
286
+ const uid = run('id -u');
287
+
288
+ // Migrate: unload + remove a pre-CC hand-rolled watcher so the two
289
+ // never double-fire. The old script file is left in place.
290
+ if (fs.existsSync(legacyPlist)) {
291
+ try { run(`launchctl bootout gui/${uid}/${legacyLabel}`); } catch { /* not loaded */ }
292
+ fs.unlinkSync(legacyPlist);
293
+ results.push(` migrated legacy watcher (${legacyLabel} removed)`);
294
+ }
295
+
296
+ fs.mkdirSync(agentDir, { recursive: true });
297
+ const hadPlist = fs.existsSync(plistPath);
298
+ fs.writeFileSync(plistPath, plist);
299
+ if (hadPlist) {
300
+ try { run(`launchctl bootout gui/${uid}/${label}`); } catch { /* not loaded */ }
301
+ }
302
+ run(`launchctl bootstrap gui/${uid} ${plistPath}`);
303
+ results.push(` screenshot-to-clipboard watcher loaded (watching ${shotDir})`);
304
+ } catch (err) {
305
+ results.push(` ⚠ launchd watcher setup failed: ${err.message}`);
306
+ }
307
+ }
308
+
193
309
  module.exports = { setupMux };
@@ -121,6 +121,25 @@ const MUX_HOOKS = {
121
121
  ],
122
122
  };
123
123
 
124
+ // Opt-in bash-output compression. PostToolUse hook on Bash that compresses
125
+ // known-noisy stdout (git status walls, npm/yarn install output) to reclaim
126
+ // context. Off by default; registered only when the `bash-compress` module
127
+ // is selected. The hook itself is fail-open (passes output through untouched
128
+ // on any error) — see templates/hooks/bash-output-compress.sh.
129
+ const BASH_COMPRESS_HOOKS = {
130
+ PostToolUse: [
131
+ {
132
+ matcher: 'Bash',
133
+ hooks: [
134
+ {
135
+ type: 'command',
136
+ command: '.claude/hooks/bash-output-compress.sh',
137
+ },
138
+ ],
139
+ },
140
+ ],
141
+ };
142
+
124
143
  // Legacy hook script names that should be stripped on any merge.
125
144
  // Centralizes cleanup so a user who skips --migrate-memory but runs
126
145
  // any other CC operation still gets omega-era hooks pruned.
@@ -153,6 +172,11 @@ function mergeSettings(projectDir, { includeDb = true } = {}) {
153
172
  if (fs.existsSync(settingsPath)) {
154
173
  settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
155
174
  }
175
+ // Guard shape drift: setting .hooks on a top-level array would silently
176
+ // drop it on serialize (JSON.stringify ignores non-index array props).
177
+ if (typeof settings !== 'object' || settings === null || Array.isArray(settings)) {
178
+ settings = {};
179
+ }
156
180
 
157
181
  if (!settings.hooks) settings.hooks = {};
158
182
 
@@ -303,4 +327,35 @@ function mergeMuxHooks(settingsPath) {
303
327
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
304
328
  }
305
329
 
306
- module.exports = { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, DEFAULT_HOOKS, WATCHTOWER_HOOKS, MUX_HOOKS, LEGACY_HOOK_COMMANDS };
330
+ /**
331
+ * Merge the opt-in bash-output compression hook into project settings.
332
+ * Called from the bash-compress module's install path in cli.js — only
333
+ * registers the PostToolUse Bash hook when that module is selected.
334
+ * Idempotent (de-dupes by command path).
335
+ */
336
+ function mergeBashCompressHooks(settingsPath) {
337
+ if (!fs.existsSync(settingsPath)) return;
338
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
339
+ if (!settings.hooks) settings.hooks = {};
340
+
341
+ for (const [event, newHooks] of Object.entries(BASH_COMPRESS_HOOKS)) {
342
+ if (!settings.hooks[event]) {
343
+ settings.hooks[event] = newHooks;
344
+ } else {
345
+ for (const newHook of newHooks) {
346
+ const hookKey = h => h.command || h.prompt || '';
347
+ const existingKeys = settings.hooks[event].flatMap(h =>
348
+ h.hooks.map(hh => hookKey(hh))
349
+ );
350
+ const newKeys = newHook.hooks.map(h => hookKey(h));
351
+ if (!newKeys.every(k => existingKeys.includes(k))) {
352
+ settings.hooks[event].push(newHook);
353
+ }
354
+ }
355
+ }
356
+ }
357
+
358
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
359
+ }
360
+
361
+ module.exports = { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks, DEFAULT_HOOKS, WATCHTOWER_HOOKS, MUX_HOOKS, BASH_COMPRESS_HOOKS, LEGACY_HOOK_COMMANDS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.40.0",
3
+ "version": "0.42.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"
@@ -8,7 +8,9 @@
8
8
  "files": [
9
9
  "bin/",
10
10
  "lib/",
11
- "templates/"
11
+ "templates/",
12
+ "!**/__pycache__",
13
+ "!**/*.pyc"
12
14
  ],
13
15
  "keywords": [
14
16
  "claude",
@@ -128,8 +128,10 @@ to anchor the boundaries.
128
128
  ```markdown
129
129
  ## Historically Problematic Patterns
130
130
 
131
- Read `patterns-project.md` in this skill directory for project-specific
132
- patterns from prior audits. Apply alongside universal patterns below.
131
+ If `patterns-project.md` exists in this skill directory, read it for
132
+ project-specific patterns from prior audits and apply alongside the
133
+ universal patterns below. Absent is normal — it is seeded by debrief
134
+ when recurring findings accumulate.
133
135
 
134
136
  <!-- Universal patterns below this line -->
135
137
  ```
@@ -0,0 +1,68 @@
1
+ # Advisory dismissal state — schema and rules
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.
4
+
5
+ ## Where it lives
6
+
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.
8
+
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.
10
+
11
+ ## Schema
12
+
13
+ ```json
14
+ {
15
+ "<advisoryId>": {
16
+ "status": "suggested" | "declined" | "installed",
17
+ "count": 2,
18
+ "last_shown": "2026-06-07",
19
+ "signal": "gemfile+rb"
20
+ }
21
+ }
22
+ ```
23
+
24
+ - **`advisoryId`** — a stable id per advisory, e.g. `lsp:ruby`, `lsp:typescript`, `mcp:railway`, `plugin:hookify`.
25
+ - **`status`**
26
+ - `suggested` — shown, not yet acted on.
27
+ - `declined` — the user explicitly waved it off.
28
+ - `installed` — the thing is present (probe confirmed). **Terminal** — never surface again.
29
+ - **`count`** — how many sessions it has been surfaced while still actionable. Drives the "stop nagging" rule.
30
+ - **`last_shown`** — ISO date of the most recent surfacing.
31
+ - **`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
+
33
+ ## The rules orient follows
34
+
35
+ Before surfacing any advisory, orient computes the advisory's **current signal** (fingerprint of the indicators present now) and reads the stored entry:
36
+
37
+ 1. **No entry / file absent** → surface it. Write `{status:"suggested", count:1, last_shown:today, signal:current}`.
38
+ 2. **`installed`** → never surface (terminal). (Re-probe may flip a `suggested`/`declined` entry to `installed`; never the reverse automatically.)
39
+ 3. **`declined`**
40
+ - current signal **==** stored signal → **silent.** (Optionally surfaced in `/pulse` only.)
41
+ - current signal **!=** stored signal → the stack changed since the user declined → **re-surface exactly once.** Reset to `{status:"suggested", count:1, signal:current}`.
42
+ 4. **`suggested`**
43
+ - `count < 2` and signal unchanged → surface again, `count++`, `last_shown=today`.
44
+ - `count >= 2` and signal unchanged → **go quiet** (mention only in `/pulse`). Do not keep incrementing.
45
+ - signal **!=** stored signal (at any count) → the stack changed → reset to `{count:1, signal:current}` and surface.
46
+
47
+ ### The anti-trap guarantee
48
+
49
+ **Any change in the stack signal resets an advisory to actionable** — so no advisory is permanently invisible while the thing it suggests is still relevant *and the project keeps evolving*.
50
+
51
+ The one deliberately-sticky case: an advisory whose signal **never changes** once it fires. Example: `plugin:hookify`, keyed on the existence of `.claude/rules/enforcement-pipeline.md` — a file that, once created, stays. A `declined` hookify therefore stays declined. That is intended (the user said no, and nothing about the project changed to revisit it), but it must remain **escapable, not a black hole**:
52
+
53
+ - It is still listed in `/pulse` (quiet, not gone).
54
+ - Clearing its entry from `advisories-state.json` (or setting `status` back to `suggested`) re-arms it.
55
+
56
+ Document any new advisory's signal source here when you add it, and call out explicitly if its signal is static (like hookify) so the sticky behavior is a known property, not a surprise.
57
+
58
+ ## Advisory ids in use
59
+
60
+ | advisoryId | indicator(s) → signal | install action shown (advisory only — orient never runs it) |
61
+ |---|---|---|
62
+ | `lsp:typescript` | `tsconfig.json` or `*.ts` | `/plugin install typescript-lsp` |
63
+ | `lsp:python` | `pyproject.toml` / `requirements.txt` / `*.py` | `/plugin install pyright-lsp` |
64
+ | `lsp:rust` | `Cargo.toml` | `/plugin install rust-analyzer-lsp` |
65
+ | `lsp:go` | `go.mod` | `/plugin install gopls-lsp` |
66
+ | `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` |
@@ -0,0 +1,70 @@
1
+ # Elicitation Methods — structured ways to draw out what the user knows
2
+
3
+ CC's interviewing moments (onboard, seed, `/plan` scoping, checklist-discover, debrief) improvise their questioning. This is the shared shelf of structured elicitation techniques skills can consult when they need to draw something out — surfacing a hidden assumption, pressure-testing a plan, widening the option space. It sits on the same shelf as `skill-output-conventions.md`: a reference skills *cite*, not a phase they run.
4
+
5
+ Derived from the BMAD-METHOD advanced-elicitation method set (Apache-2.0; see attribution at the end), curated and adapted to CC's constraints. Several entries are classic requirements-elicitation / creative-thinking craft that BMAD also draws on; those are noted.
6
+
7
+ ## How to use this file
8
+
9
+ A skill at an interview step consults this file to *choose how to ask* — it does not run a menu. Pick the one or two techniques that fit the moment and the gap you're trying to close, then ask. The technique shapes the question; the conversation stays a conversation.
10
+
11
+ ## Fit criteria (why these and not the other 70-odd)
12
+
13
+ A technique earns a place here only if it meets all four:
14
+
15
+ 1. **Conversational register** — it works as plain dialogue, not a worksheet or a numbered menu (terminal prose constraint).
16
+ 2. **One question at a time** — it can be run as a sequence of single questions, never a batch. This is a CC hard rule (`CLAUDE.md`): write interview questions one at a time, never batched.
17
+ 3. **Fits a named CC moment** — onboard, seed, `/plan` scoping, checklist-discover, or debrief.
18
+ 4. **No persona-roleplay dependency** — it doesn't require the user (or Claude) to adopt and switch between named personas to function.
19
+
20
+ ## The methods
21
+
22
+ ### First-Principles Thinking
23
+ *Moments: /plan scoping, onboard.* Strip away how it's done today and rebuild from what the thing actually needs to do. Use when a plan is anchored on an existing implementation and you suspect the real requirement is simpler or different.
24
+ - Ask: *"Ignore how this works today — what does it actually need to accomplish, at minimum?"* then, one at a time, *"Which of those are truly required versus inherited from the current approach?"*
25
+
26
+ ### Pre-mortem
27
+ *Moments: /plan scoping, checklist-discover.* Assume the work shipped and failed; reason backward to the cause. Surfaces risks and edge cases the optimistic framing hides. (BMAD-named.)
28
+ - Ask: *"Imagine this shipped and quietly failed a month later — what's the single most likely reason?"* then *"What would we have needed to know up front to prevent that?"*
29
+
30
+ ### Inversion
31
+ *Moments: /plan, checklist-discover.* Ask how to *guarantee* failure, then avoid those things. Often easier to enumerate than success conditions. (BMAD-named.)
32
+ - Ask: *"What's the surest way to make this go wrong?"* then *"Which of those are we closest to doing by accident?"*
33
+
34
+ ### Assumption Surfacing
35
+ *Moments: /plan scoping (pairs with the plan-completeness `[NEEDS CLARIFICATION]` marker), investigate.* Name the unspoken assumptions a plan rests on so the shaky ones get checked before building. (Classic requirements-elicitation craft.)
36
+ - Ask: *"What are we assuming is true here that we haven't actually verified?"* then take them one at a time: *"How would we confirm that one cheaply?"*
37
+
38
+ ### Socratic Questioning
39
+ *Moments: any.* Challenge a claim with "why?" and "how do you know?" until it rests on something solid. Use sparingly — it's a scalpel, not a default. (BMAD-named.)
40
+ - Ask: *"What makes you confident that's the right call?"* then follow the answer down one level at a time.
41
+
42
+ ### Constraint Removal
43
+ *Moments: onboard (vision), seed (member design), /plan (widen options).* Drop a constraint, see what becomes possible, then add it back deliberately. Widens the option space when thinking feels boxed in. (BMAD-named.)
44
+ - Ask: *"If [time / scope / the existing schema] weren't a limit, what would you do instead?"* then *"What's the smallest version of that we could actually do?"*
45
+
46
+ ### Stakeholder Lens
47
+ *Moments: onboard (who is served), seed (whose perspective the member encodes).* Re-ask the question from one stakeholder's point of view at a time — the user, a future maintainer, the end customer. One lens per question; never a round-table battery. (Adapted from BMAD's Stakeholder Mapping to honor the one-question rule.)
48
+ - Ask: *"From the end user's point of view, what would make this a win?"* then, next turn, *"Now from the person who maintains it a year from now — same question."*
49
+
50
+ ### Analogical Reasoning
51
+ *Moments: seed (member design), onboard (mental model).* Find the closest parallel in another domain and borrow its lessons. Good for naming a fuzzy concept or designing something with no obvious precedent. (BMAD-named.)
52
+ - Ask: *"What existing thing — in or out of software — is this most like?"* then *"What does that parallel get right that we should copy, and where does it break down?"*
53
+
54
+ ### Five Whys
55
+ *Moments: investigate, /plan problem-framing, debrief.* Trace a stated problem to its root by asking "why" about each answer in turn. Naturally one-at-a-time. (Classic root-cause craft BMAD draws on.)
56
+ - Ask: *"Why is that a problem?"* — and about each answer, *"And why is that?"* — usually three to five levels reaches the root.
57
+
58
+ ### Expand or Contract for Audience
59
+ *Moments: checklist-discover, onboard, debrief presentation.* Deliberately widen or narrow the level of detail to fit who the output serves. Use when scope or depth feels mismatched to the audience. (BMAD-named.)
60
+ - Ask: *"Who reads this, and do they need more breadth or more depth than we have?"* then adjust one dimension at a time.
61
+
62
+ ## Considered, not kept
63
+
64
+ - **Red Team vs Blue Team** — a multi-round attack/defend battery; the full technique needs adversarial persona-switching and several exchanges. The useful core (steelman the opposing case) is covered by Socratic Questioning and Inversion as single questions.
65
+ - **Six Thinking Hats** — a six-perspective battery requiring sequential persona adoption; fails the no-persona-roleplay and one-question criteria. The Stakeholder Lens covers the salvageable part, one lens at a time.
66
+ - **Any numbered-menu / "pick 1-9" selection flows** — BMAD presents methods as an interactive numbered menu; that interaction model fails the terminal-prose constraint. CC skills choose a technique themselves and just ask.
67
+
68
+ ## Attribution
69
+
70
+ Several methods here are derived from the BMAD-METHOD advanced-elicitation method set (`bmad-code-org/BMAD-METHOD`, Apache License 2.0). Method *names and descriptions* that originate there are marked "(BMAD-named)" above; the adaptations to one-question-at-a-time conversational flow, the CC-moment mapping, and the classic-craft additions are CC's own. BMAD is Apache-2.0 licensed; this derived reference preserves that attribution.
@@ -70,6 +70,26 @@ Also check:
70
70
  If a skill hasn't been invoked 3 times in the last month, that itself is
71
71
  a finding (coverage gap or trigger problem).
72
72
 
73
+ **Invocation data — don't eyeball it.** The hooks module's skill-telemetry
74
+ hooks log every skill run to `~/.claude/telemetry/telemetry.jsonl`. Read it
75
+ with the dead-skill reader instead of guessing:
76
+
77
+ ```
78
+ node scripts/skill-usage.mjs # human report (dead / stale / active)
79
+ node scripts/skill-usage.mjs --quiet # prints only if there's something to flag
80
+ node scripts/skill-usage.mjs --json # structured, for programmatic checks
81
+ node scripts/skill-usage.mjs --days 60 # widen the stale threshold
82
+ ```
83
+
84
+ It cross-references installed user-invocable skills against the telemetry and
85
+ surfaces **DEAD** (never invoked — removal or trigger-phrase candidates) and
86
+ **STALE** (not invoked within the threshold). Telemetry is global across
87
+ projects, so a skill flagged DEAD was invoked in *no* project — a strong
88
+ signal. Cabinet members and other `user-invocable: false` skills are excluded
89
+ (they run as agents, not slash/Skill calls, so they never appear). Treat the
90
+ output as candidates for judgment, not a verdict — a never-invoked skill may
91
+ have bad trigger phrasing rather than no purpose.
92
+
73
93
  ### 3. Score Each Assertion
74
94
 
75
95
  For each assertion, review the sampled executions and score:
@@ -53,9 +53,27 @@ behaves unexpectedly.
53
53
  pick more than one.
54
54
  - **"Other" is auto-added** by the harness. Never add an "Other" /
55
55
  "Something else" option manually — it duplicates.
56
- - **No reliable preview/comparison field.** The official schema has no
57
- dependable preview surface; don't build conventions on preview
58
- behavior or expect side-by-side rendering.
56
+ - **A dialog swallows same-turn prose. Never pair one with an
57
+ explanation the user must read.** When AskUserQuestion fires, the
58
+ dialog takes over the screen — any prose streamed in the same turn
59
+ is effectively invisible at decision time. Two compliant shapes
60
+ (user-confirmed 2026-06-06, after being bitten twice in one day):
61
+ 1. **Explanation-first (default for read-then-decide loops):** end
62
+ the turn with the full explanation as prose and a plain-text
63
+ question ("Accept, edit, or skip?"). Take the user's prose
64
+ answer. No dialog at all — this also satisfies the bounded-list
65
+ caveat below for sequential same-shaped decisions.
66
+ 2. **Self-sufficient dialog (for small, comparable artifacts):**
67
+ the dialog carries ALL decision content itself. Single-select
68
+ options support a `preview` field (markdown, side-by-side) —
69
+ put the artifact in the preview, attach the SAME preview to
70
+ every option so it stays visible whichever option is focused,
71
+ tradeoffs in option `description`s. Same-turn prose: one-line
72
+ pointer max. If the content doesn't fit a preview pane, you're
73
+ in shape 1.
74
+ (Updated 2026-06-06: earlier guidance said preview was
75
+ undependable; the harness now renders it reliably for single-select
76
+ questions. multiSelect still has no preview.)
59
77
  - **Unavailable in Task-spawned subagents.** Agents launched via the
60
78
  Task tool (execute-group worktree agents, Workflow agents,
61
79
  `context:fork`) cannot call AskUserQuestion — they must use prose.