create-claude-cabinet 0.44.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.
- package/README.md +9 -4
- package/lib/cli.js +77 -6
- package/lib/copy.js +56 -10
- package/lib/engagement-server-setup.js +34 -9
- package/lib/migrate-from-omega.js +13 -1
- package/lib/mux-setup.js +34 -9
- package/lib/watchtower-setup.js +210 -0
- package/package.json +5 -1
- package/templates/cabinet/_cabinet-member-template.md +8 -3
- package/templates/cabinet/advisories-state-schema.md +34 -7
- package/templates/cabinet/checklist-stats-schema.md +104 -0
- package/templates/cabinet/checkpoint-protocol.md +17 -5
- package/templates/cabinet/composition-patterns.md +4 -3
- package/templates/cabinet/qa-dimensions-template.yaml +7 -0
- package/templates/cabinet/skill-output-conventions.md +35 -1
- package/templates/cabinet/watchtower-contracts.md +126 -0
- package/templates/engagement/pib-db-patches/pib-db-lib.mjs +14 -2
- package/templates/hooks/action-completion-gate.sh +17 -0
- package/templates/hooks/watchtower-session-start.sh +80 -5
- package/templates/mux/__tests__/claude-carveout.fixture.sh +136 -0
- package/templates/mux/__tests__/claude-carveout.test.mjs +38 -0
- package/templates/mux/__tests__/mux-fail-loud.fixture.sh +298 -0
- package/templates/mux/__tests__/mux-fail-loud.test.mjs +41 -0
- package/templates/mux/__tests__/station-liveness.fixture.sh +234 -0
- package/templates/mux/__tests__/station-liveness.test.mjs +47 -0
- package/templates/mux/__tests__/worktree-dirty-check.fixture.sh +184 -0
- package/templates/mux/__tests__/worktree-dirty-check.test.mjs +35 -0
- package/templates/mux/bin/mux +485 -107
- package/templates/mux/config/worktree-cleanup.sh +55 -9
- package/templates/mux/config/worktree-dirty-check.sh +128 -0
- package/templates/mux/config/worktree-session-health.sh +62 -35
- package/templates/scripts/__tests__/advisor-pass.test.mjs +238 -0
- package/templates/scripts/__tests__/advisories.test.mjs +262 -0
- package/templates/scripts/__tests__/batch-disposition.test.mjs +137 -0
- package/templates/scripts/__tests__/feedback-outbox-flush.test.mjs +232 -0
- package/templates/scripts/__tests__/qa-handoff-aging.e2e.test.mjs +108 -0
- package/templates/scripts/__tests__/qa-handoff-gate.test.mjs +403 -0
- package/templates/scripts/__tests__/resolve-project.test.mjs +144 -0
- package/templates/scripts/__tests__/ring-state-ownership.test.mjs +333 -0
- package/templates/scripts/__tests__/ring2-thread-context.test.mjs +189 -0
- package/templates/scripts/__tests__/ring3-dedup.test.mjs +387 -0
- package/templates/scripts/__tests__/routine-dispatch.test.mjs +312 -0
- package/templates/scripts/pib-db-lib.mjs +4 -1
- package/templates/scripts/pib-db.mjs +4 -1
- package/templates/scripts/validate-memory.mjs +6 -2
- package/templates/scripts/watchtower-advisories.mjs +305 -0
- package/templates/scripts/watchtower-build-context.mjs +122 -19
- package/templates/scripts/watchtower-lib.mjs +441 -2
- package/templates/scripts/watchtower-migrate-keys.mjs +305 -0
- package/templates/scripts/watchtower-queue.mjs +372 -2
- package/templates/scripts/watchtower-ring1.mjs +138 -2
- package/templates/scripts/watchtower-ring2.mjs +122 -23
- package/templates/scripts/watchtower-ring3-close.mjs +558 -137
- package/templates/scripts/watchtower-routines.mjs +358 -0
- package/templates/scripts/watchtower-status.sh +1 -1
- package/templates/skills/audit/SKILL.md +30 -7
- package/templates/skills/audit/phases/checklist-pruning.md +108 -0
- package/templates/skills/briefing/SKILL.md +342 -223
- package/templates/skills/cabinet/SKILL.md +2 -2
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +14 -6
- package/templates/skills/cabinet-historian/SKILL.md +14 -11
- package/templates/skills/cabinet-system-advocate/SKILL.md +22 -21
- package/templates/skills/cabinet-user-advocate/SKILL.md +13 -7
- package/templates/skills/cc-publish/SKILL.md +105 -19
- package/templates/skills/collab-consultant/SKILL.md +1 -1
- package/templates/skills/debrief/SKILL.md +160 -15
- package/templates/skills/debrief/phases/checklist-feedback.md +10 -3
- package/templates/skills/debrief/phases/qa-handoff-sweep.md +78 -0
- package/templates/skills/engagement-create/SKILL.md +1 -1
- package/templates/skills/engagement-help/SKILL.md +1 -1
- package/templates/skills/execute/SKILL.md +7 -1
- package/templates/skills/execute/phases/post-impl-checklist.md +18 -0
- package/templates/skills/execute-group/SKILL.md +76 -24
- package/templates/skills/inbox/SKILL.md +97 -13
- package/templates/skills/orient/SKILL.md +168 -52
- package/templates/skills/orient/phases/checklist-status.md +12 -0
- package/templates/skills/plan/SKILL.md +22 -6
- package/templates/skills/qa-drain/SKILL.md +119 -0
- package/templates/skills/qa-handoff/SKILL.md +132 -5
- package/templates/skills/session-handoff/SKILL.md +334 -0
- package/templates/skills/setup-accounts/SKILL.md +1 -1
- package/templates/skills/triage-audit/SKILL.md +6 -0
- package/templates/skills/unwrap/SKILL.md +1 -1
- package/templates/skills/verify/SKILL.md +2 -2
- package/templates/skills/watchtower/SKILL.md +64 -1
- package/templates/watchtower/config.json.template +3 -1
- package/templates/watchtower/queue/items/item.json.schema +10 -1
- package/templates/workflows/deliberative-audit.js +3 -0
- package/templates/workflows/execute-group-complete.js +93 -16
- package/templates/workflows/execute-group-implement.js +164 -19
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,
|
|
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** —
|
|
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
|
-
|
|
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
|
|
@@ -241,8 +241,13 @@ npx create-claude-cabinet --yes # Accept all defaults
|
|
|
241
241
|
npx create-claude-cabinet --yes --no-db # All defaults, skip database
|
|
242
242
|
npx create-claude-cabinet --dry-run # Preview without writing files
|
|
243
243
|
npx create-claude-cabinet --modules verify --yes # Add an opt-in module (merges, doesn't replace)
|
|
244
|
+
npx create-claude-cabinet --frontier-model claude-fable-5 # Designate your frontier model (watchdog)
|
|
244
245
|
```
|
|
245
246
|
|
|
247
|
+
### Frontier-model watchdog
|
|
248
|
+
|
|
249
|
+
`--frontier-model <model>` records, once, which model your heavy thinking is supposed to run on. The designation is per-operator (stored in `~/.claude/cc-registry.json` under `frontierModel`, not per-project), and the installer prints the effective value on every run. From then on, `/orient` — and, on watchtower installs, the SessionStart hook — compares the session's actual model against it and leads the briefing with a loud warning on mismatch. The key can be an exact model ID (`claude-fable-5`, exact match required) or a family alias (`fable`, matches any model ID containing it). This is **visibility only**: nothing is pinned, blocked, or rerouted — it just makes "you're accidentally on the wrong model" impossible to miss. A stale key after a model-family transition nags loudly by design; update it with the same flag.
|
|
250
|
+
|
|
246
251
|
## What Gets Installed
|
|
247
252
|
|
|
248
253
|
Everything goes into `.claude/` or `scripts/`. Nothing touches your
|
|
@@ -251,7 +256,7 @@ source code.
|
|
|
251
256
|
```
|
|
252
257
|
.claude/
|
|
253
258
|
├── skills/ # orient, debrief, plan, execute, audit, etc.
|
|
254
|
-
│ └── cabinet-*/ #
|
|
259
|
+
│ └── cabinet-*/ # 34 cabinet member definitions
|
|
255
260
|
├── cabinet/ # committees, lifecycle, composition patterns
|
|
256
261
|
│ # (incl. pib-db-access.md, pib-db-triggers.md)
|
|
257
262
|
├── briefing/ # project briefing templates
|
package/lib/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
-
const { copyTemplates } = require('./copy');
|
|
6
|
+
const { copyTemplates, recordSkip } = require('./copy');
|
|
7
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');
|
|
@@ -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;
|
|
@@ -460,11 +461,13 @@ const MODULES = {
|
|
|
460
461
|
'skills/orient-quick',
|
|
461
462
|
'skills/debrief',
|
|
462
463
|
'skills/debrief-quick',
|
|
464
|
+
'skills/session-handoff',
|
|
463
465
|
// Instruction phases — always ship, overriding the default skip-phases rule in copy.js
|
|
464
466
|
'skills/debrief/phases/audit-pattern-capture.md',
|
|
465
467
|
'skills/debrief/phases/methodology-capture.md',
|
|
466
468
|
'skills/debrief/phases/record-lessons.md',
|
|
467
469
|
'skills/debrief/phases/upstream-feedback.md',
|
|
470
|
+
'skills/debrief/phases/qa-handoff-sweep.md',
|
|
468
471
|
'skills/menu',
|
|
469
472
|
],
|
|
470
473
|
},
|
|
@@ -491,7 +494,7 @@ const MODULES = {
|
|
|
491
494
|
mandatory: false,
|
|
492
495
|
default: true,
|
|
493
496
|
lean: true,
|
|
494
|
-
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'],
|
|
497
|
+
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', 'cabinet/checklist-stats-schema.md', 'scripts/qa-dimensions-validator.cjs', 'skills/orient/phases/checklist-status.md', 'skills/audit/phases/checklist-pruning.md'],
|
|
495
498
|
},
|
|
496
499
|
'compliance': {
|
|
497
500
|
name: 'Compliance Stack (rules + enforcement)',
|
|
@@ -630,6 +633,9 @@ const MODULES = {
|
|
|
630
633
|
'cabinet/watchtower-contracts.md',
|
|
631
634
|
'scripts/watchtower-lib.mjs',
|
|
632
635
|
'scripts/watchtower-queue.mjs',
|
|
636
|
+
'scripts/watchtower-routines.mjs',
|
|
637
|
+
'scripts/watchtower-advisories.mjs',
|
|
638
|
+
'cabinet/advisories-state-schema.md',
|
|
633
639
|
'skills/inbox',
|
|
634
640
|
'hooks/watchtower-session-start.sh',
|
|
635
641
|
'scripts/watchtower-build-context.mjs',
|
|
@@ -653,6 +659,7 @@ const MODULES = {
|
|
|
653
659
|
'skills/briefing',
|
|
654
660
|
'skills/threads',
|
|
655
661
|
'skills/qa-handoff',
|
|
662
|
+
'skills/qa-drain',
|
|
656
663
|
],
|
|
657
664
|
},
|
|
658
665
|
mux: {
|
|
@@ -750,6 +757,14 @@ function parseArgs(argv) {
|
|
|
750
757
|
else if (arg === '--modules' && i + 1 < args.length) {
|
|
751
758
|
flags.modules = args[++i].split(',').map(s => s.trim()).filter(Boolean);
|
|
752
759
|
}
|
|
760
|
+
else if (arg === '--frontier-model' && i + 1 < args.length) {
|
|
761
|
+
// Empty/whitespace values are treated as absent: '' is a substring of
|
|
762
|
+
// every model ID, which would match everything and leave the watchdog
|
|
763
|
+
// permanently silent while appearing configured.
|
|
764
|
+
const value = args[++i].trim();
|
|
765
|
+
if (value) flags.frontierModel = value;
|
|
766
|
+
else flags.frontierModelEmpty = true;
|
|
767
|
+
}
|
|
753
768
|
else if (!arg.startsWith('-')) flags.targetDir = arg;
|
|
754
769
|
}
|
|
755
770
|
|
|
@@ -775,6 +790,11 @@ function printHelp() {
|
|
|
775
790
|
disables omega hooks/MCP. Idempotent — safe to re-run.
|
|
776
791
|
Pair with --dry-run to preview.
|
|
777
792
|
--unmigrate-memory Roll back --migrate-memory using its backup dir.
|
|
793
|
+
--frontier-model <model> Designate your frontier model (user-level, stored
|
|
794
|
+
in ~/.claude/cc-registry.json). Visibility only: /orient and
|
|
795
|
+
the watchtower SessionStart hook warn loudly when a session
|
|
796
|
+
runs a different model. Does NOT pin or route anything.
|
|
797
|
+
Accepts an exact ID (claude-fable-5) or a family alias (fable).
|
|
778
798
|
--help, -h Show this help
|
|
779
799
|
|
|
780
800
|
Examples:
|
|
@@ -1214,7 +1234,7 @@ async function run() {
|
|
|
1214
1234
|
const existingContent = fs.readFileSync(destPath, 'utf8');
|
|
1215
1235
|
if (existingContent === incoming) {
|
|
1216
1236
|
totalSkipped++;
|
|
1217
|
-
allManifest
|
|
1237
|
+
recordSkip(allManifest, mPath, { identical: true, incomingHash });
|
|
1218
1238
|
continue;
|
|
1219
1239
|
}
|
|
1220
1240
|
|
|
@@ -1230,7 +1250,9 @@ async function run() {
|
|
|
1230
1250
|
if (isPhaseFile && !isInstructionPhase && existingContent.trim() !== '' && existingContent.trim() !== incoming.trim()) {
|
|
1231
1251
|
console.log(` Preserved customized phase: ${tmpl}`);
|
|
1232
1252
|
totalSkipped++;
|
|
1233
|
-
|
|
1253
|
+
// Customized phase = project-owned content → omit from manifest
|
|
1254
|
+
// (recordSkip in copy.js — omission means "not ours").
|
|
1255
|
+
recordSkip(allManifest, mPath);
|
|
1234
1256
|
continue;
|
|
1235
1257
|
}
|
|
1236
1258
|
|
|
@@ -1240,10 +1262,19 @@ async function run() {
|
|
|
1240
1262
|
if (existingManifest[mPath]) {
|
|
1241
1263
|
if (!flags.dryRun) fs.copyFileSync(srcPath, destPath);
|
|
1242
1264
|
totalOverwritten++;
|
|
1265
|
+
// Log single-file overwrites too — the directory path (copy.js)
|
|
1266
|
+
// already does. Without this, scripts/ updates are invisible in
|
|
1267
|
+
// install output, masking whether a changed script propagated.
|
|
1268
|
+
console.log(` Updated: ${path.relative(projectDir, destPath)}`);
|
|
1269
|
+
allManifest[mPath] = incomingHash;
|
|
1243
1270
|
} else {
|
|
1244
1271
|
totalSkipped++;
|
|
1272
|
+
// Project-created file → omit from manifest entirely. Ownership
|
|
1273
|
+
// classification is manifest-PRESENCE-based, so recording ANY
|
|
1274
|
+
// hash here would mark the file upstream-owned and the NEXT
|
|
1275
|
+
// install would silently overwrite it (act:bf21c95b).
|
|
1276
|
+
recordSkip(allManifest, mPath);
|
|
1245
1277
|
}
|
|
1246
|
-
allManifest[mPath] = incomingHash;
|
|
1247
1278
|
} else {
|
|
1248
1279
|
const response = await prompts({
|
|
1249
1280
|
type: 'select',
|
|
@@ -1258,10 +1289,13 @@ async function run() {
|
|
|
1258
1289
|
if (response.action === 'overwrite') {
|
|
1259
1290
|
if (!flags.dryRun) fs.copyFileSync(srcPath, destPath);
|
|
1260
1291
|
totalOverwritten++;
|
|
1292
|
+
allManifest[mPath] = incomingHash;
|
|
1261
1293
|
} else {
|
|
1262
1294
|
totalSkipped++;
|
|
1295
|
+
// Keep: the user claimed this file → project-owned → omit from
|
|
1296
|
+
// the manifest so it is never mistaken for upstream content.
|
|
1297
|
+
recordSkip(allManifest, mPath);
|
|
1263
1298
|
}
|
|
1264
|
-
allManifest[mPath] = incomingHash;
|
|
1265
1299
|
}
|
|
1266
1300
|
} else {
|
|
1267
1301
|
if (!flags.dryRun) fs.copyFileSync(srcPath, destPath);
|
|
@@ -1382,6 +1416,27 @@ async function run() {
|
|
|
1382
1416
|
}
|
|
1383
1417
|
}
|
|
1384
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
|
+
|
|
1385
1440
|
// --- Manifest key migration (act:d1f16bee) ---
|
|
1386
1441
|
// When CC renames directories (e.g., perspectives/ → cabinet-*/), old manifest
|
|
1387
1442
|
// keys no longer match new template paths. Migrate keys BEFORE cleanup so the
|
|
@@ -1610,6 +1665,18 @@ async function run() {
|
|
|
1610
1665
|
// Register with folder name. /onboard fills in name and description later.
|
|
1611
1666
|
registry.projects.push(entry);
|
|
1612
1667
|
}
|
|
1668
|
+
// --- Frontier-model designation (visibility watchdog) ---
|
|
1669
|
+
// User-level, per-operator key. Read-preserve-rewrite: only the
|
|
1670
|
+
// frontierModel key is touched; every other key rides through.
|
|
1671
|
+
if (flags.frontierModelEmpty) {
|
|
1672
|
+
console.log(' ⚠ Ignoring empty --frontier-model value (an empty key would match every model and silence the watchdog)');
|
|
1673
|
+
}
|
|
1674
|
+
if (flags.frontierModel) {
|
|
1675
|
+
registry.frontierModel = flags.frontierModel;
|
|
1676
|
+
} else if (typeof registry.frontierModel === 'string' && !registry.frontierModel.trim()) {
|
|
1677
|
+
// Heal a hand-edited empty key — treat as absent (see parseArgs note).
|
|
1678
|
+
delete registry.frontierModel;
|
|
1679
|
+
}
|
|
1613
1680
|
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
1614
1681
|
const otherCount = registry.projects.filter(p => p.path !== projectDir).length;
|
|
1615
1682
|
if (otherCount > 0) {
|
|
@@ -1617,6 +1684,10 @@ async function run() {
|
|
|
1617
1684
|
} else {
|
|
1618
1685
|
console.log(' 📋 Registered in project registry');
|
|
1619
1686
|
}
|
|
1687
|
+
// Self-announcing: print the effective designation on every run.
|
|
1688
|
+
if (registry.frontierModel) {
|
|
1689
|
+
console.log(` 🛰 Frontier model: ${registry.frontierModel} (visibility watchdog — /orient + SessionStart warn on mismatch; nothing is pinned)`);
|
|
1690
|
+
}
|
|
1620
1691
|
} catch (err) {
|
|
1621
1692
|
// Non-fatal — registry is nice-to-have
|
|
1622
1693
|
}
|
package/lib/copy.js
CHANGED
|
@@ -7,6 +7,49 @@ function hashContent(content) {
|
|
|
7
7
|
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Record the manifest consequence of SKIPPING a file at install time
|
|
12
|
+
* (act:bf21c95b). The single shared rule for ALL skip sites in BOTH
|
|
13
|
+
* install code paths (lib/copy.js and the single-file branches in
|
|
14
|
+
* lib/cli.js):
|
|
15
|
+
*
|
|
16
|
+
* - A skipped file whose on-disk content differs from the incoming
|
|
17
|
+
* template is NOT upstream content — it is project-owned
|
|
18
|
+
* (project-created, user-kept, or a customized phase). It must be
|
|
19
|
+
* OMITTED from the manifest entirely. An absent entry means "not
|
|
20
|
+
* ours". Recording any hash for it poisons the manifest: ownership
|
|
21
|
+
* classification is manifest-PRESENCE-based, so the next install
|
|
22
|
+
* would classify the file upstream-owned and silently overwrite it
|
|
23
|
+
* (and cc-upstream-guard / cc-drift-check would false-positive on it).
|
|
24
|
+
* - The one exception: a skipped file byte-identical to the incoming
|
|
25
|
+
* template is indistinguishable from upstream content and stays
|
|
26
|
+
* tracked under the template hash.
|
|
27
|
+
*
|
|
28
|
+
* Omission only — never a marker value or an alternate manifest value
|
|
29
|
+
* shape (lesson_shared_json_shape_drift). All manifest consumers
|
|
30
|
+
* (ownership classification, cleanup loop, key migration, cc-drift-check,
|
|
31
|
+
* cc-upstream-guard, lib/reset.js) treat an absent key as "not ours" and
|
|
32
|
+
* leave the file alone.
|
|
33
|
+
*
|
|
34
|
+
* Known limitation (documented, not solved here): manifests already
|
|
35
|
+
* poisoned by past installs cannot retroactively distinguish a recorded
|
|
36
|
+
* project-created file from genuine upstream content. This helper only
|
|
37
|
+
* prevents NEW poisoning.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} manifest manifest object being built for this install
|
|
40
|
+
* @param {string} key manifest key for the skipped file
|
|
41
|
+
* @param {object} [opts]
|
|
42
|
+
* @param {boolean} [opts.identical] on-disk content === incoming template
|
|
43
|
+
* @param {string} [opts.incomingHash] hash of the incoming template content
|
|
44
|
+
*/
|
|
45
|
+
function recordSkip(manifest, key, { identical = false, incomingHash = null } = {}) {
|
|
46
|
+
if (identical && incomingHash) {
|
|
47
|
+
manifest[key] = incomingHash;
|
|
48
|
+
} else {
|
|
49
|
+
delete manifest[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
10
53
|
/**
|
|
11
54
|
* Recursively copy files from src to dest, surfacing conflicts.
|
|
12
55
|
* Returns { copied: string[], skipped: string[], overwritten: string[] }
|
|
@@ -55,7 +98,8 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
|
|
|
55
98
|
const trimmedExisting = existing.trim();
|
|
56
99
|
if (trimmedExisting !== '' && trimmedExisting !== incoming.trim()) {
|
|
57
100
|
results.skipped.push(relPath);
|
|
58
|
-
|
|
101
|
+
// Customized phase = project-owned content → omit from manifest.
|
|
102
|
+
recordSkip(results.manifest, relPath);
|
|
59
103
|
console.log(` Preserved customized phase: ${displayPath}`);
|
|
60
104
|
continue;
|
|
61
105
|
}
|
|
@@ -64,7 +108,7 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
|
|
|
64
108
|
|
|
65
109
|
if (existing === incoming) {
|
|
66
110
|
results.skipped.push(relPath);
|
|
67
|
-
results.manifest
|
|
111
|
+
recordSkip(results.manifest, relPath, { identical: true, incomingHash });
|
|
68
112
|
continue;
|
|
69
113
|
}
|
|
70
114
|
|
|
@@ -82,9 +126,8 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
|
|
|
82
126
|
console.log(` Updated: ${displayPath}`);
|
|
83
127
|
} else {
|
|
84
128
|
results.skipped.push(relPath);
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
results.manifest[relPath] = hashContent(existing);
|
|
129
|
+
// Project-created file → omit from manifest ("not ours").
|
|
130
|
+
recordSkip(results.manifest, relPath);
|
|
88
131
|
}
|
|
89
132
|
continue;
|
|
90
133
|
}
|
|
@@ -101,9 +144,9 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
|
|
|
101
144
|
});
|
|
102
145
|
|
|
103
146
|
if (!response.action) {
|
|
104
|
-
// User cancelled
|
|
147
|
+
// User cancelled → file kept as-is → project-owned → omit.
|
|
105
148
|
results.skipped.push(relPath);
|
|
106
|
-
results.manifest
|
|
149
|
+
recordSkip(results.manifest, relPath);
|
|
107
150
|
continue;
|
|
108
151
|
}
|
|
109
152
|
|
|
@@ -118,17 +161,20 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
|
|
|
118
161
|
if (followUp.overwrite && !dryRun) {
|
|
119
162
|
fs.copyFileSync(srcPath, destPath);
|
|
120
163
|
results.overwritten.push(relPath);
|
|
164
|
+
results.manifest[relPath] = incomingHash;
|
|
121
165
|
} else {
|
|
166
|
+
// Diff shown, user kept their file → project-owned → omit.
|
|
122
167
|
results.skipped.push(relPath);
|
|
168
|
+
recordSkip(results.manifest, relPath);
|
|
123
169
|
}
|
|
124
|
-
results.manifest[relPath] = incomingHash;
|
|
125
170
|
} else if (response.action === 'overwrite') {
|
|
126
171
|
if (!dryRun) fs.copyFileSync(srcPath, destPath);
|
|
127
172
|
results.overwritten.push(relPath);
|
|
128
173
|
results.manifest[relPath] = incomingHash;
|
|
129
174
|
} else {
|
|
175
|
+
// 'Keep existing' → project-owned → omit from manifest.
|
|
130
176
|
results.skipped.push(relPath);
|
|
131
|
-
results.manifest
|
|
177
|
+
recordSkip(results.manifest, relPath);
|
|
132
178
|
}
|
|
133
179
|
} else {
|
|
134
180
|
if (!dryRun) {
|
|
@@ -169,4 +215,4 @@ function showDiff(existing, incoming, relPath) {
|
|
|
169
215
|
console.log('');
|
|
170
216
|
}
|
|
171
217
|
|
|
172
|
-
module.exports = { copyTemplates };
|
|
218
|
+
module.exports = { copyTemplates, recordSkip };
|
|
@@ -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:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 },
|
|
@@ -45,6 +49,7 @@ const MANAGED_FILES = [
|
|
|
45
49
|
{ src: 'config/worktree-session-health.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-session-health.sh'), mode: 0o755 },
|
|
46
50
|
{ src: 'config/worktree-health-popup.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-health-popup.sh'), mode: 0o755 },
|
|
47
51
|
{ src: 'config/worktree-cleanup.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-cleanup.sh'), mode: 0o755 },
|
|
52
|
+
{ src: 'config/worktree-dirty-check.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-dirty-check.sh'), mode: 0o755 },
|
|
48
53
|
{ src: 'config/mux.tmux.conf', dest: path.join(os.homedir(), '.config', 'mux', 'mux.tmux.conf') },
|
|
49
54
|
{ src: 'config/unwrap-copy.py', dest: path.join(os.homedir(), '.config', 'mux', 'unwrap-copy.py'), mode: 0o755 },
|
|
50
55
|
{ src: 'config/screenshot-to-clipboard.sh', dest: path.join(os.homedir(), '.config', 'mux', 'screenshot-to-clipboard.sh'), mode: 0o755 },
|
|
@@ -121,7 +126,7 @@ function setupMux(opts = {}) {
|
|
|
121
126
|
const results = [];
|
|
122
127
|
|
|
123
128
|
const ccVersion = require('../package.json').version;
|
|
124
|
-
const templateDir =
|
|
129
|
+
const templateDir = TEMPLATE_DIR;
|
|
125
130
|
|
|
126
131
|
if (!fs.existsSync(templateDir)) {
|
|
127
132
|
throw new Error(`mux-setup: ${templateDir} not found.`);
|
|
@@ -131,15 +136,19 @@ function setupMux(opts = {}) {
|
|
|
131
136
|
|
|
132
137
|
if (installedVersion) {
|
|
133
138
|
const cmp = compareVersions(ccVersion, installedVersion);
|
|
134
|
-
if (cmp === 0) {
|
|
135
|
-
results.push(`mux ${installedVersion} already installed — skipping`);
|
|
136
|
-
return { results, status: 'skipped' };
|
|
137
|
-
}
|
|
138
139
|
if (cmp < 0) {
|
|
139
140
|
results.push(`mux ${installedVersion} is newer than CC ${ccVersion} — skipping (won't downgrade)`);
|
|
140
141
|
return { results, status: 'skipped' };
|
|
141
142
|
}
|
|
142
|
-
|
|
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
|
+
}
|
|
143
152
|
} else {
|
|
144
153
|
results.push(`Installing mux ${ccVersion}`);
|
|
145
154
|
}
|
|
@@ -199,7 +208,16 @@ function setupMux(opts = {}) {
|
|
|
199
208
|
setupDarwinIntegration({ dryRun, results });
|
|
200
209
|
}
|
|
201
210
|
|
|
202
|
-
|
|
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 };
|
|
203
221
|
}
|
|
204
222
|
|
|
205
223
|
/**
|
|
@@ -307,4 +325,11 @@ function setupDarwinIntegration({ dryRun, results }) {
|
|
|
307
325
|
}
|
|
308
326
|
}
|
|
309
327
|
|
|
310
|
-
|
|
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
|
+
};
|