create-claude-cabinet 0.39.0 → 0.41.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.
- package/README.md +11 -0
- package/lib/cli.js +24 -3
- package/lib/engagement-server-setup.js +5 -1
- package/lib/metadata.js +9 -2
- package/lib/mux-setup.js +121 -1
- package/lib/settings-merge.js +45 -1
- package/package.json +4 -2
- package/templates/cabinet/_cabinet-member-template.md +8 -20
- package/templates/cabinet/watchtower-contracts.md +75 -3
- package/templates/engagement-server/Dockerfile +2 -0
- package/templates/engagement-server/server.mjs +73 -21
- package/templates/mux/bin/mux +221 -10
- package/templates/mux/config/context-help.py +6 -0
- package/templates/mux/config/help.txt +21 -0
- package/templates/mux/config/mux-server.py +5 -2
- package/templates/mux/config/mux.tmux.conf +27 -0
- package/templates/mux/config/muxlib.py +50 -1
- package/templates/mux/config/screenshot-to-clipboard.sh +45 -0
- package/templates/mux/config/unwrap-copy.py +72 -0
- package/templates/mux/config/worktree-cleanup.sh +86 -0
- package/templates/mux/config/worktree-health-popup.sh +23 -0
- package/templates/mux/config/worktree-session-health.sh +105 -0
- package/templates/rules/maintainability.md +92 -0
- package/templates/rules/memory-capture.md +4 -2
- package/templates/scripts/watchtower-build-context.mjs +118 -15
- package/templates/scripts/watchtower-lib.mjs +41 -1
- package/templates/scripts/watchtower-queue.mjs +21 -11
- package/templates/scripts/watchtower-ring1.mjs +248 -14
- package/templates/scripts/watchtower-ring2.mjs +378 -24
- package/templates/scripts/watchtower-ring3-close.mjs +462 -114
- package/templates/scripts/watchtower-status.sh +266 -0
- package/templates/scripts/watchtower-validate.mjs +3 -3
- package/templates/skills/briefing/SKILL.md +46 -7
- package/templates/skills/cabinet-accessibility/SKILL.md +60 -223
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +65 -296
- package/templates/skills/cabinet-anti-confirmation/SKILL.md +38 -152
- package/templates/skills/cabinet-architecture/SKILL.md +59 -265
- package/templates/skills/cabinet-automation/SKILL.md +77 -398
- package/templates/skills/cabinet-boundary-man/SKILL.md +58 -194
- package/templates/skills/cabinet-cc-health/SKILL.md +64 -462
- package/templates/skills/cabinet-cc-health/migration-reference.md +46 -0
- package/templates/skills/cabinet-data-integrity/SKILL.md +53 -142
- package/templates/skills/cabinet-debugger/SKILL.md +67 -209
- package/templates/skills/cabinet-elegance/SKILL.md +34 -229
- package/templates/skills/cabinet-framework-quality/SKILL.md +78 -387
- package/templates/skills/cabinet-goal-alignment/SKILL.md +64 -218
- package/templates/skills/cabinet-historian/SKILL.md +76 -320
- package/templates/skills/cabinet-information-design/SKILL.md +89 -432
- package/templates/skills/cabinet-interactive-storyteller/SKILL.md +77 -307
- package/templates/skills/cabinet-mantine-quality/SKILL.md +42 -293
- package/templates/skills/cabinet-narrative-architect/SKILL.md +67 -254
- package/templates/skills/cabinet-organized-mind/SKILL.md +90 -340
- package/templates/skills/cabinet-process-therapist/SKILL.md +70 -233
- package/templates/skills/cabinet-qa/SKILL.md +57 -195
- package/templates/skills/cabinet-record-keeper/SKILL.md +59 -170
- package/templates/skills/cabinet-roster-check/SKILL.md +74 -300
- package/templates/skills/cabinet-security/SKILL.md +58 -211
- package/templates/skills/cabinet-small-screen/SKILL.md +38 -138
- package/templates/skills/cabinet-speed-freak/SKILL.md +36 -198
- package/templates/skills/cabinet-system-advocate/SKILL.md +62 -170
- package/templates/skills/cabinet-technical-debt/SKILL.md +36 -176
- package/templates/skills/cabinet-ui-experimentalist/SKILL.md +67 -231
- package/templates/skills/cabinet-usability/SKILL.md +59 -175
- package/templates/skills/cabinet-user-advocate/SKILL.md +69 -290
- package/templates/skills/cabinet-vision/SKILL.md +62 -224
- package/templates/skills/cabinet-workflow-cop/SKILL.md +63 -226
- package/templates/skills/collab-client/SKILL.md +30 -0
- package/templates/skills/collab-consultant/SKILL.md +146 -2
- package/templates/skills/decisions/SKILL.md +7 -158
- package/templates/skills/dx-feedback/SKILL.md +125 -0
- package/templates/skills/inbox/SKILL.md +194 -0
- package/templates/skills/investigate/SKILL.md +2 -2
- package/templates/skills/orient/phases/dx-captures.md +16 -17
- package/templates/skills/plan/phases/verify-plan.md +3 -3
- package/templates/skills/watchtower/SKILL.md +3 -2
- package/templates/watchtower/queue/items/item.json.schema +33 -6
package/README.md
CHANGED
|
@@ -221,6 +221,17 @@ the listed modules to what's already there, it doesn't replace your
|
|
|
221
221
|
module set. Safe to run on a mature project without losing
|
|
222
222
|
customization. You can pass multiple modules: `--modules verify,audit`.
|
|
223
223
|
|
|
224
|
+
### Opt-in Modules
|
|
225
|
+
|
|
226
|
+
| Module | What it does |
|
|
227
|
+
|--------|-------------|
|
|
228
|
+
| **verify** | Cucumber + Playwright walkthrough verification harness |
|
|
229
|
+
| **site-audit** | 14-check deployed-site quality audit with HTML reports |
|
|
230
|
+
| **engagement** | Client engagement management — packets, billing, feedback loops |
|
|
231
|
+
| **engagement-server** | Central multi-engagement API server (Railway/Fly deploy) |
|
|
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, durable tmux bindings, clipboard copy with hard-wrap removal, screenshot-to-clipboard launchd watcher |
|
|
234
|
+
|
|
224
235
|
## CLI Options
|
|
225
236
|
|
|
226
237
|
```
|
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 } = require('./settings-merge');
|
|
7
|
+
const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks } = 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');
|
|
@@ -496,7 +496,7 @@ const MODULES = {
|
|
|
496
496
|
mandatory: false,
|
|
497
497
|
default: true,
|
|
498
498
|
lean: false,
|
|
499
|
-
templates: ['rules/enforcement-pipeline.md', 'memory/patterns/_pattern-template.md', 'memory/patterns/pattern-intelligence-first.md'],
|
|
499
|
+
templates: ['rules/enforcement-pipeline.md', 'rules/maintainability.md', '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)',
|
|
@@ -623,7 +623,7 @@ const MODULES = {
|
|
|
623
623
|
},
|
|
624
624
|
watchtower: {
|
|
625
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
|
|
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 inbox for extracted knowledge and signals. Sessions start informed with minimal context cost.',
|
|
627
627
|
mandatory: false,
|
|
628
628
|
default: false,
|
|
629
629
|
lean: false,
|
|
@@ -632,6 +632,7 @@ const MODULES = {
|
|
|
632
632
|
'cabinet/watchtower-contracts.md',
|
|
633
633
|
'scripts/watchtower-lib.mjs',
|
|
634
634
|
'scripts/watchtower-queue.mjs',
|
|
635
|
+
'skills/inbox',
|
|
635
636
|
'skills/decisions',
|
|
636
637
|
'hooks/watchtower-session-start.sh',
|
|
637
638
|
'scripts/watchtower-build-context.mjs',
|
|
@@ -651,6 +652,7 @@ const MODULES = {
|
|
|
651
652
|
'watchtower/watchtower-ring2-slow.timer',
|
|
652
653
|
'hooks/watchtower-session-end.sh',
|
|
653
654
|
'scripts/watchtower-ring3-close.mjs',
|
|
655
|
+
'scripts/watchtower-status.sh',
|
|
654
656
|
'skills/briefing',
|
|
655
657
|
],
|
|
656
658
|
},
|
|
@@ -1285,6 +1287,11 @@ async function run() {
|
|
|
1285
1287
|
mergeWatchtowerHooks(settingsPath);
|
|
1286
1288
|
console.log(' ⚙️ Registered watchtower SessionStart/SessionEnd hooks');
|
|
1287
1289
|
}
|
|
1290
|
+
|
|
1291
|
+
if (selectedModules.includes('mux')) {
|
|
1292
|
+
mergeMuxHooks(settingsPath);
|
|
1293
|
+
console.log(' ⚙️ Registered mux worktree health SessionStart hook');
|
|
1294
|
+
}
|
|
1288
1295
|
}
|
|
1289
1296
|
|
|
1290
1297
|
// --- Heal user-level ~/.claude/settings.json ---
|
|
@@ -1308,6 +1315,11 @@ async function run() {
|
|
|
1308
1315
|
if (fs.existsSync(mcpJsonPath)) {
|
|
1309
1316
|
existing = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
|
|
1310
1317
|
}
|
|
1318
|
+
// Guard shape drift: .mcpServers set on a top-level array would be
|
|
1319
|
+
// silently dropped on serialize.
|
|
1320
|
+
if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {
|
|
1321
|
+
existing = {};
|
|
1322
|
+
}
|
|
1311
1323
|
if (!existing.mcpServers) existing.mcpServers = {};
|
|
1312
1324
|
Object.assign(existing.mcpServers, mcpConfig.mcpServers);
|
|
1313
1325
|
fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
|
|
@@ -1560,6 +1572,15 @@ async function run() {
|
|
|
1560
1572
|
if (fs.existsSync(registryPath)) {
|
|
1561
1573
|
registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
1562
1574
|
}
|
|
1575
|
+
// Normalize shape drift: a bare top-level array has been observed
|
|
1576
|
+
// (ad-hoc edit dropped the {"projects": ...} wrapper). Re-wrap so
|
|
1577
|
+
// the update below also heals the file on disk.
|
|
1578
|
+
if (Array.isArray(registry)) {
|
|
1579
|
+
registry = { projects: registry };
|
|
1580
|
+
}
|
|
1581
|
+
if (!Array.isArray(registry.projects)) {
|
|
1582
|
+
registry.projects = [];
|
|
1583
|
+
}
|
|
1563
1584
|
const existingIdx = registry.projects.findIndex(p => p.path === projectDir);
|
|
1564
1585
|
const entry = {
|
|
1565
1586
|
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
|
-
|
|
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
|
@@ -42,6 +42,12 @@ const MANAGED_FILES = [
|
|
|
42
42
|
{ src: 'config/help.txt', dest: path.join(os.homedir(), '.config', 'mux', 'help.txt') },
|
|
43
43
|
{ src: 'config/_mux', dest: path.join(os.homedir(), '.config', 'mux', '_mux') },
|
|
44
44
|
{ src: 'config/mux.bash', dest: path.join(os.homedir(), '.config', 'mux', 'mux.bash') },
|
|
45
|
+
{ src: 'config/worktree-session-health.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-session-health.sh'), mode: 0o755 },
|
|
46
|
+
{ src: 'config/worktree-health-popup.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-health-popup.sh'), mode: 0o755 },
|
|
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 },
|
|
45
51
|
];
|
|
46
52
|
|
|
47
53
|
const DATA_DIRS = [
|
|
@@ -50,6 +56,7 @@ const DATA_DIRS = [
|
|
|
50
56
|
path.join(os.homedir(), '.config', 'mux', 'notes'),
|
|
51
57
|
path.join(os.homedir(), '.config', 'mux', 'dx'),
|
|
52
58
|
path.join(os.homedir(), '.config', 'mux', 'pending-prompts'),
|
|
59
|
+
path.join(os.homedir(), '.local', 'share', 'mux', 'wt-health'),
|
|
53
60
|
];
|
|
54
61
|
|
|
55
62
|
function sha256(content) {
|
|
@@ -71,7 +78,11 @@ function compareVersions(a, b) {
|
|
|
71
78
|
function readGlobalManifest() {
|
|
72
79
|
if (!fs.existsSync(GLOBAL_MANIFEST_PATH)) return { files: {} };
|
|
73
80
|
try {
|
|
74
|
-
|
|
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;
|
|
75
86
|
} catch {
|
|
76
87
|
return { files: {} };
|
|
77
88
|
}
|
|
@@ -183,7 +194,116 @@ function setupMux(opts = {}) {
|
|
|
183
194
|
|
|
184
195
|
results.push(` ${copiedCount} file${copiedCount !== 1 ? 's' : ''} installed to user paths`);
|
|
185
196
|
|
|
197
|
+
if (process.platform === 'darwin') {
|
|
198
|
+
setupDarwinIntegration({ dryRun, results });
|
|
199
|
+
}
|
|
200
|
+
|
|
186
201
|
return { results, status: installedVersion ? 'upgraded' : 'installed' };
|
|
187
202
|
}
|
|
188
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
|
+
|
|
189
309
|
module.exports = { setupMux };
|
package/lib/settings-merge.js
CHANGED
|
@@ -107,6 +107,20 @@ const WATCHTOWER_HOOKS = {
|
|
|
107
107
|
],
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
+
const MUX_HOOKS = {
|
|
111
|
+
SessionStart: [
|
|
112
|
+
{
|
|
113
|
+
matcher: '',
|
|
114
|
+
hooks: [
|
|
115
|
+
{
|
|
116
|
+
type: 'command',
|
|
117
|
+
command: '$HOME/.config/mux/worktree-session-health.sh',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
|
|
110
124
|
// Legacy hook script names that should be stripped on any merge.
|
|
111
125
|
// Centralizes cleanup so a user who skips --migrate-memory but runs
|
|
112
126
|
// any other CC operation still gets omega-era hooks pruned.
|
|
@@ -139,6 +153,11 @@ function mergeSettings(projectDir, { includeDb = true } = {}) {
|
|
|
139
153
|
if (fs.existsSync(settingsPath)) {
|
|
140
154
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
141
155
|
}
|
|
156
|
+
// Guard shape drift: setting .hooks on a top-level array would silently
|
|
157
|
+
// drop it on serialize (JSON.stringify ignores non-index array props).
|
|
158
|
+
if (typeof settings !== 'object' || settings === null || Array.isArray(settings)) {
|
|
159
|
+
settings = {};
|
|
160
|
+
}
|
|
142
161
|
|
|
143
162
|
if (!settings.hooks) settings.hooks = {};
|
|
144
163
|
|
|
@@ -264,4 +283,29 @@ function mergeWatchtowerHooks(settingsPath) {
|
|
|
264
283
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
265
284
|
}
|
|
266
285
|
|
|
267
|
-
|
|
286
|
+
function mergeMuxHooks(settingsPath) {
|
|
287
|
+
if (!fs.existsSync(settingsPath)) return;
|
|
288
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
289
|
+
if (!settings.hooks) settings.hooks = {};
|
|
290
|
+
|
|
291
|
+
for (const [event, newHooks] of Object.entries(MUX_HOOKS)) {
|
|
292
|
+
if (!settings.hooks[event]) {
|
|
293
|
+
settings.hooks[event] = newHooks;
|
|
294
|
+
} else {
|
|
295
|
+
for (const newHook of newHooks) {
|
|
296
|
+
const hookKey = h => h.command || h.prompt || '';
|
|
297
|
+
const existingKeys = settings.hooks[event].flatMap(h =>
|
|
298
|
+
h.hooks.map(hh => hookKey(hh))
|
|
299
|
+
);
|
|
300
|
+
const newKeys = newHook.hooks.map(h => hookKey(h));
|
|
301
|
+
if (!newKeys.every(k => existingKeys.includes(k))) {
|
|
302
|
+
settings.hooks[event].push(newHook);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, DEFAULT_HOOKS, WATCHTOWER_HOOKS, MUX_HOOKS, LEGACY_HOOK_COMMANDS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-claude-cabinet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.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",
|
|
@@ -125,33 +125,21 @@ to anchor the boundaries.
|
|
|
125
125
|
|
|
126
126
|
### 7. Historically Problematic Patterns
|
|
127
127
|
|
|
128
|
-
Two-file overlay:
|
|
129
|
-
|
|
130
128
|
```markdown
|
|
131
129
|
## Historically Problematic Patterns
|
|
132
130
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
via field-feedback.
|
|
138
|
-
2. **`patterns-project.md`** in this skill's directory — project-specific
|
|
139
|
-
patterns discovered during audits of this particular project. Project-
|
|
140
|
-
owned, never overwritten by CC upgrades.
|
|
141
|
-
|
|
142
|
-
If `patterns-project.md` exists, read it alongside this section. Both
|
|
143
|
-
inform your analysis equally.
|
|
144
|
-
|
|
145
|
-
**How patterns get here:** A consuming project's audit finds a real issue.
|
|
146
|
-
If the same pattern recurs across projects, it gets promoted upstream via
|
|
147
|
-
field-feedback. The CC maintainer adds it to this section. Project-specific
|
|
148
|
-
patterns that don't generalize stay in `patterns-project.md`.
|
|
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.
|
|
149
135
|
|
|
150
136
|
<!-- Universal patterns below this line -->
|
|
151
137
|
```
|
|
152
138
|
|
|
153
|
-
This section starts empty for new members.
|
|
154
|
-
findings over time — never pre-fill with
|
|
139
|
+
This section starts empty for new members. Universal patterns accumulate
|
|
140
|
+
from real field-feedback findings over time — never pre-fill with
|
|
141
|
+
hypothetical patterns. Project-specific patterns live in
|
|
142
|
+
`patterns-project.md` (project-owned, never overwritten by CC upgrades).
|
|
155
143
|
|
|
156
144
|
## Optional Sections
|
|
157
145
|
|
|
@@ -17,12 +17,12 @@ fs.renameSync(tmp, filePath);
|
|
|
17
17
|
|
|
18
18
|
## No-Index Convention
|
|
19
19
|
|
|
20
|
-
The
|
|
20
|
+
The inbox queue uses directory listing, not an index file. To list
|
|
21
21
|
pending items, read `queue/items/`, open each `.json`, filter by
|
|
22
22
|
status. No manifest, no index.json.
|
|
23
23
|
|
|
24
24
|
Rationale: one fewer file to keep consistent. Directory listing is
|
|
25
|
-
O(n) but n is small (
|
|
25
|
+
O(n) but n is small (inbox items rarely exceed 50 items). If
|
|
26
26
|
listing exceeds 2 seconds or 500 items, revisit via `act:2b638b02`.
|
|
27
27
|
|
|
28
28
|
## Schema Versioning
|
|
@@ -70,7 +70,7 @@ standard files:
|
|
|
70
70
|
| `memory-refs.md` | Ring 2 fast | Relevant memory entries |
|
|
71
71
|
| `options-analysis.md` | Ring 2 fast | Pros/cons with evidence |
|
|
72
72
|
|
|
73
|
-
`/
|
|
73
|
+
`/inbox` reads these when `enrichment_status` is `"complete"`.
|
|
74
74
|
Missing files degrade gracefully (null in the read result).
|
|
75
75
|
|
|
76
76
|
## Deferred Schemas
|
|
@@ -85,3 +85,75 @@ their owning plan ships:
|
|
|
85
85
|
| `hooks/<ring>-<phase>.d/` | Plan 9 | Lifecycle hooks |
|
|
86
86
|
| `logs/<ring>.log` | Plan 4 | Ring 1 runner |
|
|
87
87
|
| `lock/<ring>.pid` | Plan 4 | Ring 1 runner |
|
|
88
|
+
|
|
89
|
+
## Ring 4 — Periodic Truth Reconciliation (design phase)
|
|
90
|
+
|
|
91
|
+
**Status: concept. Needs design session before implementation.**
|
|
92
|
+
|
|
93
|
+
Rings 1–3 operate on individual signals: individual files (R1),
|
|
94
|
+
individual patterns (R2), individual sessions (R3). Nothing catches
|
|
95
|
+
**cumulative drift** — the slow rot where twenty sessions each move
|
|
96
|
+
reality a little further from what documents claim, but no single
|
|
97
|
+
session is the tipping point.
|
|
98
|
+
|
|
99
|
+
Ring 4 is the answer to: "Is what we wrote still true?"
|
|
100
|
+
|
|
101
|
+
### The problem it solves
|
|
102
|
+
|
|
103
|
+
The maginnis project's architecture briefing said "Layer 4 Not built"
|
|
104
|
+
and "tech stack TBD" for three weeks after the platform shipped with
|
|
105
|
+
a Rails 8 app, Mantine frontend, 421 RSpec specs, and staging on
|
|
106
|
+
Railway. No individual session caused the staleness. No individual
|
|
107
|
+
session's debrief would flag it. The drift was invisible until
|
|
108
|
+
someone tried to use the briefing for a real audit and found it
|
|
109
|
+
described a project that no longer existed.
|
|
110
|
+
|
|
111
|
+
This class of problem includes:
|
|
112
|
+
- **Briefing-to-codebase drift** — claims about architecture, tech
|
|
113
|
+
stack, file layout, or project state that no longer match reality
|
|
114
|
+
- **Memory-to-codebase drift** — memory files that reference
|
|
115
|
+
functions, files, or flags that have been renamed or removed
|
|
116
|
+
- **CLAUDE.md drift** — sections describing architecture or
|
|
117
|
+
conventions that have been refactored past
|
|
118
|
+
- **Plan-to-reality drift** — plan files describing work that's been
|
|
119
|
+
done but never marked complete, or describing approaches that were
|
|
120
|
+
abandoned
|
|
121
|
+
|
|
122
|
+
### Cadence and weight
|
|
123
|
+
|
|
124
|
+
Less frequent than R1–R3. Daily or weekly, not per-session or
|
|
125
|
+
per-minute. Heavier per run — reads real code, cross-references
|
|
126
|
+
document claims against codebase state, may invoke Claude for
|
|
127
|
+
semantic comparison. Acceptable cost because it runs rarely and
|
|
128
|
+
catches problems that compound silently.
|
|
129
|
+
|
|
130
|
+
### Relationship to other rings
|
|
131
|
+
|
|
132
|
+
Following the nervous system principle (not a stack): R4 consumes
|
|
133
|
+
signals from R1–R3 (what changed recently, what sessions touched,
|
|
134
|
+
what patterns emerged) to prioritize what to reconcile. R4 produces
|
|
135
|
+
inbox items when drift is detected. R4 does not mutate documents —
|
|
136
|
+
it flags, the user triages and routes.
|
|
137
|
+
|
|
138
|
+
R3 catches "this session broke the briefing." R4 catches "the
|
|
139
|
+
briefing has been slowly wrong for three weeks and nobody noticed."
|
|
140
|
+
They're complementary, not redundant.
|
|
141
|
+
|
|
142
|
+
### Open design questions
|
|
143
|
+
|
|
144
|
+
- **Scope per run:** Reconcile everything every time, or rotate
|
|
145
|
+
through documents on a schedule? Full sweep is thorough but
|
|
146
|
+
expensive; rotation risks missing urgent drift on off-cycle docs.
|
|
147
|
+
- **Drift threshold:** How wrong does a document need to be before
|
|
148
|
+
it's worth flagging? A missing model in the architecture briefing
|
|
149
|
+
is clear; a slightly outdated line count is noise.
|
|
150
|
+
- **Trigger vs schedule:** Pure schedule (weekly), or also triggered
|
|
151
|
+
when R1/R2 detect significant codebase changes (new migration,
|
|
152
|
+
major refactor, new dependency)?
|
|
153
|
+
- **Cross-project scope:** Per-project reconciliation, or also
|
|
154
|
+
cross-project (e.g., a CC template claiming something about
|
|
155
|
+
consumer behavior that's no longer true)?
|
|
156
|
+
- **Relationship to Ring 2 slow tier:** R2 slow already does some
|
|
157
|
+
staleness detection (stale work, memory hygiene). Where's the line
|
|
158
|
+
between R2's "is this work item stale?" and R4's "is this document
|
|
159
|
+
still true?" Is R4 an extension of R2 slow, or genuinely distinct?
|
|
@@ -73,7 +73,7 @@ function hashToken(raw) {
|
|
|
73
73
|
return createHash('sha256').update(raw).digest('hex');
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function
|
|
76
|
+
function resolveLocalToken(tokenHash) {
|
|
77
77
|
return db.prepare(`
|
|
78
78
|
SELECT u.id AS user_id, u.engagement_id, u.role, u.name AS user_name,
|
|
79
79
|
e.auth_mode, e.auth_config
|
|
@@ -84,31 +84,83 @@ function resolveToken(tokenHash) {
|
|
|
84
84
|
`).get(tokenHash);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function getExternalEngagements() {
|
|
88
|
+
return db.prepare(`
|
|
89
|
+
SELECT id, auth_config FROM engagements
|
|
90
|
+
WHERE auth_mode = 'external' AND auth_config IS NOT NULL
|
|
91
|
+
`).all();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveExternalUser(engagementId, email, roleMapping) {
|
|
95
|
+
const user = db.prepare(`
|
|
96
|
+
SELECT id AS user_id, engagement_id, role, name AS user_name
|
|
97
|
+
FROM users
|
|
98
|
+
WHERE engagement_id = ? AND email = ?
|
|
99
|
+
`).get(engagementId, email);
|
|
100
|
+
if (user) return user;
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
async function authenticateRequest(req) {
|
|
88
106
|
const authHeader = req.headers['authorization'];
|
|
89
107
|
if (!authHeader || !authHeader.startsWith('Bearer ')) return null;
|
|
90
108
|
|
|
91
109
|
const raw = authHeader.slice(7);
|
|
92
|
-
const tokenData = resolveToken(hashToken(raw));
|
|
93
|
-
if (!tokenData) return null;
|
|
94
110
|
|
|
95
|
-
|
|
111
|
+
// Path 1: local token lookup (works for all auth modes)
|
|
112
|
+
const localData = resolveLocalToken(hashToken(raw));
|
|
113
|
+
if (localData && localData.auth_mode === 'local') return localData;
|
|
114
|
+
|
|
115
|
+
// Path 2: external auth — forward token to client app's validate_url
|
|
116
|
+
const externals = getExternalEngagements();
|
|
117
|
+
for (const eng of externals) {
|
|
96
118
|
try {
|
|
97
|
-
const config = JSON.parse(
|
|
98
|
-
if (config.validate_url)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
119
|
+
const config = JSON.parse(eng.auth_config);
|
|
120
|
+
if (!config.validate_url) continue;
|
|
121
|
+
|
|
122
|
+
const resp = await fetch(config.validate_url, {
|
|
123
|
+
headers: { 'Authorization': `Bearer ${raw}` },
|
|
124
|
+
signal: AbortSignal.timeout(5000),
|
|
125
|
+
});
|
|
126
|
+
if (!resp.ok) continue;
|
|
127
|
+
|
|
128
|
+
const identity = await resp.json();
|
|
129
|
+
const email = identity.email;
|
|
130
|
+
if (!email) continue;
|
|
131
|
+
|
|
132
|
+
// Map platform role to engagement role via role_mapping
|
|
133
|
+
const roleMapping = config.role_mapping || {};
|
|
134
|
+
const mappedRole = roleMapping[identity.role] || identity.role;
|
|
135
|
+
|
|
136
|
+
// Find matching local user by email for message attribution
|
|
137
|
+
const user = resolveExternalUser(eng.id, email, roleMapping);
|
|
138
|
+
if (user) return { ...user, auth_mode: 'external' };
|
|
139
|
+
|
|
140
|
+
// User authenticated with platform but no local user record —
|
|
141
|
+
// auto-create so messages are properly attributed
|
|
142
|
+
const userId = `usr_${randomBytes(6).toString('hex')}`;
|
|
143
|
+
const engRole = ['consultant', 'client'].includes(mappedRole) ? mappedRole : 'client';
|
|
144
|
+
db.prepare(`INSERT INTO users (id, engagement_id, name, email, role) VALUES (?, ?, ?, ?, ?)`)
|
|
145
|
+
.run(userId, eng.id, email.split('@')[0], email, engRole);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
user_id: userId,
|
|
149
|
+
engagement_id: eng.id,
|
|
150
|
+
role: engRole,
|
|
151
|
+
user_name: email.split('@')[0],
|
|
152
|
+
auth_mode: 'external',
|
|
153
|
+
};
|
|
105
154
|
} catch (err) {
|
|
106
|
-
console.error(`External auth failed: ${err.message}`);
|
|
107
|
-
return { error: 'auth_backend_unavailable' };
|
|
155
|
+
console.error(`External auth failed for engagement ${eng.id}: ${err.message}`);
|
|
108
156
|
}
|
|
109
157
|
}
|
|
110
158
|
|
|
111
|
-
|
|
159
|
+
// Path 3: local token in an external-auth engagement (consultant-side
|
|
160
|
+
// tokens may still be locally managed)
|
|
161
|
+
if (localData) return localData;
|
|
162
|
+
|
|
163
|
+
return null;
|
|
112
164
|
}
|
|
113
165
|
|
|
114
166
|
// ---------------------------------------------------------------------------
|
|
@@ -278,18 +330,18 @@ const server = createServer(async (req, res) => {
|
|
|
278
330
|
const path = url.pathname;
|
|
279
331
|
const method = req.method;
|
|
280
332
|
|
|
333
|
+
// Health check — no auth, before HTTPS enforcement (Railway internal probes lack x-forwarded-proto)
|
|
334
|
+
if (path === '/health' && method === 'GET') {
|
|
335
|
+
logRequest(req, 200, null);
|
|
336
|
+
return json(res, 200, { ok: true, version: SCHEMA_VERSION });
|
|
337
|
+
}
|
|
338
|
+
|
|
281
339
|
// HTTPS enforcement (Railway terminates TLS)
|
|
282
340
|
if (process.env.RAILWAY_ENVIRONMENT && req.headers['x-forwarded-proto'] !== 'https') {
|
|
283
341
|
logRequest(req, 421, null);
|
|
284
342
|
return json(res, 421, { error: 'https_required' });
|
|
285
343
|
}
|
|
286
344
|
|
|
287
|
-
// Health check — no auth
|
|
288
|
-
if (path === '/health' && method === 'GET') {
|
|
289
|
-
logRequest(req, 200, null);
|
|
290
|
-
return json(res, 200, { ok: true, version: SCHEMA_VERSION });
|
|
291
|
-
}
|
|
292
|
-
|
|
293
345
|
// All other routes require auth
|
|
294
346
|
const authHeader = req.headers['authorization'];
|
|
295
347
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|