create-claude-cabinet 0.45.0 → 0.47.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 +6 -10
- package/lib/cli.js +374 -91
- package/lib/copy.js +108 -6
- package/lib/db-setup.js +122 -17
- package/lib/engagement-server-setup.js +34 -9
- package/lib/metadata.js +51 -2
- package/lib/migrate-from-omega.js +13 -1
- package/lib/mux-setup.js +33 -9
- package/lib/settings-merge.js +52 -10
- package/lib/watchtower-setup.js +230 -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 +15 -1
- package/templates/cabinet/composition-patterns.md +4 -3
- package/templates/cabinet/memory-lifecycle-contract.md +135 -0
- package/templates/cabinet/pib-db-access.md +13 -0
- package/templates/cabinet/skill-output-conventions.md +35 -1
- package/templates/cabinet/watchtower-contracts.md +503 -1
- package/templates/cabinet/worktree-invocation-contract.md +87 -0
- package/templates/engagement/OVERVIEW.md +12 -0
- package/templates/engagement/__tests__/engagement.test.mjs +177 -2
- package/templates/engagement/__tests__/invoice-doc.test.mjs +195 -0
- package/templates/engagement/engagement-preview.mjs +100 -0
- package/templates/engagement/engagement-schema.md +240 -0
- package/templates/engagement/engagement.mjs +391 -11
- package/templates/engagement/pib-db-patches/pib-db-lib.mjs +10 -1
- package/templates/engagement/pib-db-patches/pib-db-mcp-server.mjs +5 -44
- package/templates/engagement/pib-db-patches/pib-db.mjs +5 -2
- package/templates/hooks/action-completion-gate.sh +9 -3
- package/templates/hooks/memory-index-guard.sh +17 -11
- package/templates/hooks/watchtower-session-start.sh +24 -2
- package/templates/mcp/pib-db.json +1 -4
- package/templates/mux/__tests__/mux-fail-loud.fixture.sh +44 -0
- package/templates/mux/__tests__/station-liveness.fixture.sh +234 -0
- package/templates/mux/__tests__/station-liveness.test.mjs +47 -0
- package/templates/mux/bin/mux +289 -56
- package/templates/mux/config/help.txt +1 -0
- package/templates/rules/acknowledge-when-corrected.md +33 -0
- package/templates/rules/maintainability.md +11 -0
- package/templates/rules/memory-capture.md +26 -4
- package/templates/rules/plan-before-bulk-or-irreversible-actions.md +48 -0
- package/templates/rules/verify-before-asserting.md +79 -0
- package/templates/scripts/__tests__/advisor-pass.test.mjs +238 -0
- package/templates/scripts/__tests__/advisories.test.mjs +262 -0
- package/templates/scripts/__tests__/ahead-check-origin.test.mjs +355 -0
- package/templates/scripts/__tests__/batch-disposition.test.mjs +235 -0
- package/templates/scripts/__tests__/claude-churn-authored.test.mjs +90 -0
- package/templates/scripts/__tests__/cross-ring-reader.test.mjs +505 -0
- package/templates/scripts/__tests__/dx-captures-briefing.test.mjs +169 -0
- package/templates/scripts/__tests__/feedback-outbox-flush.test.mjs +232 -0
- package/templates/scripts/__tests__/hook-runner.test.mjs +247 -0
- package/templates/scripts/__tests__/narrative-corpus.test.mjs +247 -0
- package/templates/scripts/__tests__/phase-shim.test.mjs +112 -0
- package/templates/scripts/__tests__/qa-handoff-gate.test.mjs +68 -0
- package/templates/scripts/__tests__/qa-handoff-merge-state.test.mjs +162 -0
- package/templates/scripts/__tests__/resolve-cli.test.mjs +314 -0
- package/templates/scripts/__tests__/ring-state-ownership.test.mjs +228 -3
- package/templates/scripts/__tests__/ring1-content-detector.test.mjs +168 -0
- package/templates/scripts/__tests__/ring1-flagged-actions.test.mjs +135 -0
- package/templates/scripts/__tests__/ring1-script-drift.test.mjs +126 -0
- package/templates/scripts/__tests__/ring2-queue-reader.test.mjs +80 -0
- package/templates/scripts/__tests__/ring2-recall-canary.test.mjs +150 -0
- package/templates/scripts/__tests__/ring2-roster-review.test.mjs +311 -0
- package/templates/scripts/__tests__/ring2-thread-context.test.mjs +189 -0
- package/templates/scripts/__tests__/ring3-chunk-merge.test.mjs +122 -0
- package/templates/scripts/__tests__/ring3-close-lenses.test.mjs +359 -0
- package/templates/scripts/__tests__/ring3-dedup.test.mjs +467 -0
- package/templates/scripts/__tests__/ring3-memory-titles.test.mjs +187 -0
- package/templates/scripts/__tests__/ring3-novelty-rescue.test.mjs +148 -0
- package/templates/scripts/__tests__/ring3-recent-slice.test.mjs +101 -0
- package/templates/scripts/__tests__/ring4-reconcile.test.mjs +393 -0
- package/templates/scripts/__tests__/routine-dispatch.test.mjs +312 -0
- package/templates/scripts/__tests__/shared-thread-reader.test.mjs +187 -0
- package/templates/scripts/__tests__/suppression-ledger.test.mjs +148 -0
- package/templates/scripts/__tests__/verify-backfill.test.mjs +103 -0
- package/templates/scripts/__tests__/verify-coverage.test.mjs +80 -0
- package/templates/scripts/__tests__/watchtower-snapshot.test.mjs +261 -0
- package/templates/scripts/__tests__/watchtower-sync.test.mjs +311 -0
- package/templates/scripts/load-triage-history.js +5 -2
- package/templates/scripts/pib-db-mcp-server.mjs +5 -44
- package/templates/scripts/pib-db-path.mjs +61 -0
- package/templates/scripts/pib-db.mjs +5 -2
- package/templates/scripts/validate-memory.mjs +214 -16
- package/templates/scripts/watchtower-advisories.mjs +309 -0
- package/templates/scripts/watchtower-build-context.mjs +301 -18
- package/templates/scripts/watchtower-cross-ring-reader.mjs +700 -0
- package/templates/scripts/watchtower-hook-runner.mjs +422 -0
- package/templates/scripts/watchtower-lib.mjs +447 -2
- package/templates/scripts/watchtower-narrative-corpus.mjs +385 -0
- package/templates/scripts/watchtower-phase-shim.mjs +171 -0
- package/templates/scripts/watchtower-queue.mjs +484 -1
- package/templates/scripts/watchtower-ring1.mjs +533 -53
- package/templates/scripts/watchtower-ring2.mjs +787 -48
- package/templates/scripts/watchtower-ring3-close.mjs +1357 -99
- package/templates/scripts/watchtower-ring4-runner.sh +85 -0
- package/templates/scripts/watchtower-ring4.mjs +753 -0
- package/templates/scripts/watchtower-routines.mjs +358 -0
- package/templates/scripts/watchtower-snapshot.mjs +452 -0
- package/templates/scripts/watchtower-status.sh +1 -1
- package/templates/scripts/watchtower-sync.mjs +393 -0
- package/templates/skills/audit/SKILL.md +5 -1
- package/templates/skills/briefing/SKILL.md +584 -236
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +14 -6
- package/templates/skills/cabinet-historian/SKILL.md +14 -11
- package/templates/skills/cabinet-process-therapist/SKILL.md +28 -0
- package/templates/skills/cabinet-security/SKILL.md +11 -0
- package/templates/skills/cabinet-system-advocate/SKILL.md +22 -21
- package/templates/skills/cabinet-user-advocate/SKILL.md +13 -7
- package/templates/skills/catch-up/SKILL.md +113 -0
- package/templates/skills/cc-publish/SKILL.md +148 -25
- package/templates/skills/cc-remember/SKILL.md +45 -0
- package/templates/skills/close/SKILL.md +107 -0
- package/templates/skills/collab-client/SKILL.md +22 -5
- package/templates/skills/collab-consultant/SKILL.md +110 -2
- package/templates/skills/debrief/SKILL.md +148 -16
- package/templates/skills/debrief-classic/SKILL.md +696 -0
- package/templates/skills/debrief-classic/calibration.md +44 -0
- package/templates/skills/debrief-classic/phases/audit-pattern-capture.md +78 -0
- package/templates/skills/debrief-classic/phases/auto-maintenance.md +48 -0
- package/templates/skills/debrief-classic/phases/checklist-feedback.md +123 -0
- package/templates/skills/debrief-classic/phases/close-work.md +163 -0
- package/templates/skills/debrief-classic/phases/health-checks.md +54 -0
- package/templates/skills/debrief-classic/phases/inventory.md +40 -0
- package/templates/skills/debrief-classic/phases/loose-ends.md +52 -0
- package/templates/skills/debrief-classic/phases/methodology-capture.md +223 -0
- package/templates/skills/debrief-classic/phases/qa-handoff-sweep.md +78 -0
- package/templates/skills/debrief-classic/phases/record-lessons.md +177 -0
- package/templates/skills/debrief-classic/phases/report.md +59 -0
- package/templates/skills/debrief-classic/phases/update-state.md +48 -0
- package/templates/skills/debrief-classic/phases/upstream-feedback.md +185 -0
- package/templates/skills/debrief-classic/phases/verify-coverage.md +101 -0
- package/templates/skills/execute/SKILL.md +77 -6
- package/templates/skills/execute/phases/commit-and-deploy.md +8 -0
- package/templates/skills/execute-group/SKILL.md +23 -26
- package/templates/skills/generate-plan-groups/SKILL.md +20 -0
- package/templates/skills/inbox/SKILL.md +124 -7
- package/templates/skills/memory/SKILL.md +22 -6
- package/templates/skills/orient/SKILL.md +140 -52
- package/templates/skills/orient-classic/SKILL.md +770 -0
- package/templates/skills/orient-classic/phases/auto-maintenance.md +52 -0
- package/templates/skills/orient-classic/phases/briefing.md +53 -0
- package/templates/skills/orient-classic/phases/cabinet.md +46 -0
- package/templates/skills/orient-classic/phases/checklist-status.md +54 -0
- package/templates/skills/orient-classic/phases/context.md +88 -0
- package/templates/skills/orient-classic/phases/data-sync.md +35 -0
- package/templates/skills/orient-classic/phases/deferred-check.md +55 -0
- package/templates/skills/orient-classic/phases/dx-captures.md +53 -0
- package/templates/skills/orient-classic/phases/health-checks.md +50 -0
- package/templates/skills/orient-classic/phases/verify-backfill.md +109 -0
- package/templates/skills/orient-classic/phases/work-scan.md +69 -0
- package/templates/skills/plan/SKILL.md +8 -0
- package/templates/skills/qa-drain/SKILL.md +157 -0
- package/templates/skills/qa-handoff/SKILL.md +287 -46
- package/templates/skills/session-handoff/SKILL.md +175 -6
- package/templates/skills/setup-accounts/SKILL.md +38 -16
- package/templates/skills/threads/SKILL.md +4 -0
- package/templates/skills/triage-audit/SKILL.md +6 -0
- package/templates/skills/validate/phases/validators.md +41 -0
- package/templates/skills/watchtower/SKILL.md +199 -2
- package/templates/verify-runtime/CONVENTIONS.md +9 -0
- package/templates/verify-runtime/README.md +37 -0
- package/templates/watchtower/config.json.template +10 -2
- package/templates/watchtower/queue/items/item.json.schema +1 -1
- package/templates/workflows/execute-group-complete.js +10 -1
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// watchtower-sync.mjs — content-based 3-tier hash-diff + auto-heal for the
|
|
4
|
+
// watchtower script set.
|
|
5
|
+
//
|
|
6
|
+
// The watchtower runtime that launchd/cron actually executes lives at
|
|
7
|
+
// ~/.claude-cabinet/watchtower/. Getting a one-file fix there durably means
|
|
8
|
+
// keeping THREE tiers in step:
|
|
9
|
+
//
|
|
10
|
+
// 1. template — upstream source of truth (`<cc-source>/templates/`).
|
|
11
|
+
// Present only in the CC source repo (or the npm package during install).
|
|
12
|
+
// 2. tracked — the project's committed copies (`<project>/scripts/` +
|
|
13
|
+
// `<project>/.claude/`), read by the one-time `/watchtower install` step.
|
|
14
|
+
// 3. runtime — `~/.claude-cabinet/watchtower/`, what the daemon runs.
|
|
15
|
+
//
|
|
16
|
+
// Before this tool a single fix had to be hand-copied to all three; miss the
|
|
17
|
+
// runtime and the fix is committed but not running, miss the tracked copy and
|
|
18
|
+
// the next install silently reverts it. This module hash-diffs the tiers from
|
|
19
|
+
// their ACTUAL on-disk bytes (never a cached manifest, which can itself go
|
|
20
|
+
// stale) and either reports the divergence (`--check`, read-only) or heals it
|
|
21
|
+
// (`--heal`) by propagating the authoritative tier downward.
|
|
22
|
+
//
|
|
23
|
+
// Authority is by contract, not by mtime: template wins when present, else the
|
|
24
|
+
// tracked copy. The runtime is ALWAYS a heal target, never a source — and the
|
|
25
|
+
// template tier is NEVER written. So healing only ever flows downstream.
|
|
26
|
+
//
|
|
27
|
+
// The managed file SET is DISCOVERED by scanning the tiers (union of what's on
|
|
28
|
+
// disk), so this drift-detector cannot itself accumulate a stale hardcoded
|
|
29
|
+
// list — the one failure mode the source feedback warned about. The only fixed
|
|
30
|
+
// knobs are the category→subdir map and the two non-`watchtower-*` doc names.
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
readFileSync, writeFileSync, existsSync, readdirSync,
|
|
34
|
+
mkdirSync, renameSync, chmodSync,
|
|
35
|
+
} from 'fs';
|
|
36
|
+
import { join, dirname } from 'path';
|
|
37
|
+
import { homedir } from 'os';
|
|
38
|
+
import { createHash } from 'crypto';
|
|
39
|
+
import { pathToFileURL } from 'url';
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Tier layout — the ONE place the per-tier subdir for each category is encoded.
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// - scripts live in scripts/ in every tier
|
|
45
|
+
// - hooks are templates/hooks/ upstream but .claude/hooks/ when tracked
|
|
46
|
+
// - cabinet docs are templates/cabinet/ upstream but .claude/cabinet/ tracked
|
|
47
|
+
// In the runtime they collapse to flat scripts/ hooks/ cabinet/ subdirs.
|
|
48
|
+
const CATEGORY_SUBDIRS = {
|
|
49
|
+
script: { template: 'scripts', tracked: 'scripts', runtime: 'scripts' },
|
|
50
|
+
hook: { template: 'hooks', tracked: '.claude/hooks', runtime: 'hooks' },
|
|
51
|
+
cabinet: { template: 'cabinet', tracked: '.claude/cabinet', runtime: 'cabinet' },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const TIERS = ['template', 'tracked', 'runtime'];
|
|
55
|
+
|
|
56
|
+
// Which files in each category are "ours" to keep in sync.
|
|
57
|
+
const HOOK_FILES = ['watchtower-session-start.sh', 'watchtower-session-end.sh'];
|
|
58
|
+
const CABINET_FILES = ['advisories-state-schema.md', 'watchtower-contracts.md'];
|
|
59
|
+
|
|
60
|
+
function isManagedScript(name) {
|
|
61
|
+
return /^watchtower-.*\.(mjs|sh)$/.test(name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Files that must be executable in the runtime/tracked tiers.
|
|
65
|
+
function isExecutable(category, name) {
|
|
66
|
+
if (category === 'hook') return true;
|
|
67
|
+
if (category === 'script' && name.endsWith('.sh')) return true;
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function tierDir(root, category, tier) {
|
|
72
|
+
return join(root, CATEGORY_SUBDIRS[category][tier]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function hashBytes(buf) {
|
|
76
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// discoverManaged — union of managed files present across the supplied tiers.
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// A file present in ANY tier participates, so a script that exists upstream but
|
|
83
|
+
// is missing downstream (or an orphan that lingers only in the runtime) is
|
|
84
|
+
// surfaced rather than silently ignored.
|
|
85
|
+
export function discoverManaged(roots) {
|
|
86
|
+
const seen = new Map(); // `${category}/${name}` -> { category, name }
|
|
87
|
+
for (const category of ['script', 'hook', 'cabinet']) {
|
|
88
|
+
for (const tier of TIERS) {
|
|
89
|
+
const root = roots[tier];
|
|
90
|
+
if (!root) continue;
|
|
91
|
+
const dir = tierDir(root, category, tier);
|
|
92
|
+
if (!existsSync(dir)) continue;
|
|
93
|
+
let names;
|
|
94
|
+
try { names = readdirSync(dir); } catch { continue; }
|
|
95
|
+
for (const name of names) {
|
|
96
|
+
let ok = false;
|
|
97
|
+
if (category === 'script') ok = isManagedScript(name);
|
|
98
|
+
else if (category === 'hook') ok = HOOK_FILES.includes(name);
|
|
99
|
+
else if (category === 'cabinet') ok = CABINET_FILES.includes(name);
|
|
100
|
+
if (ok) seen.set(`${category}/${name}`, { category, name });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return [...seen.values()].sort((a, b) =>
|
|
105
|
+
`${a.category}/${a.name}`.localeCompare(`${b.category}/${b.name}`));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// template wins when present, else the tracked copy. Runtime is never a source.
|
|
109
|
+
function authoritativeTier(roots) {
|
|
110
|
+
if (roots.template) return 'template';
|
|
111
|
+
if (roots.tracked) return 'tracked';
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readTier(root, category, tier, name) {
|
|
116
|
+
if (!root) return { tier, present: false, path: null };
|
|
117
|
+
const path = join(tierDir(root, category, tier), name);
|
|
118
|
+
if (!existsSync(path)) return { tier, present: false, path };
|
|
119
|
+
try {
|
|
120
|
+
return { tier, present: true, path, hash: hashBytes(readFileSync(path)) };
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return { tier, present: false, path, error: err.message };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// analyze — per-file 3-tier classification + heal plan (no writes).
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
export function analyze(roots) {
|
|
130
|
+
const authTier = authoritativeTier(roots);
|
|
131
|
+
const managed = discoverManaged(roots);
|
|
132
|
+
const files = [];
|
|
133
|
+
|
|
134
|
+
for (const { category, name } of managed) {
|
|
135
|
+
const tiers = {};
|
|
136
|
+
for (const tier of TIERS) {
|
|
137
|
+
if (roots[tier]) tiers[tier] = readTier(roots[tier], category, tier, name);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const auth = authTier ? tiers[authTier] : null;
|
|
141
|
+
const healTargets = [];
|
|
142
|
+
let status;
|
|
143
|
+
|
|
144
|
+
if (!authTier) {
|
|
145
|
+
// Only the runtime is present — nothing to verify against.
|
|
146
|
+
status = 'unverifiable';
|
|
147
|
+
} else if (!auth || !auth.present) {
|
|
148
|
+
// File lingers in a downstream tier but is gone from the source.
|
|
149
|
+
status = 'orphan';
|
|
150
|
+
} else {
|
|
151
|
+
for (const tier of TIERS) {
|
|
152
|
+
if (tier === authTier) continue;
|
|
153
|
+
const t = tiers[tier];
|
|
154
|
+
if (!t) continue; // tier root absent — not a target
|
|
155
|
+
if (!t.present) healTargets.push({ tier, reason: 'missing' });
|
|
156
|
+
else if (t.hash !== auth.hash) healTargets.push({ tier, reason: 'drift' });
|
|
157
|
+
}
|
|
158
|
+
if (healTargets.length === 0) status = 'in-sync';
|
|
159
|
+
else if (healTargets.some((h) => h.reason === 'drift')) status = 'drift';
|
|
160
|
+
else status = 'missing';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
files.push({ category, name, authTier, status, tiers, healTargets });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const summary = {
|
|
167
|
+
total: files.length,
|
|
168
|
+
inSync: files.filter((f) => f.status === 'in-sync').length,
|
|
169
|
+
drift: files.filter((f) => f.status === 'drift').length,
|
|
170
|
+
missing: files.filter((f) => f.status === 'missing').length,
|
|
171
|
+
orphan: files.filter((f) => f.status === 'orphan').length,
|
|
172
|
+
unverifiable: files.filter((f) => f.status === 'unverifiable').length,
|
|
173
|
+
};
|
|
174
|
+
summary.clean = summary.drift === 0 && summary.missing === 0
|
|
175
|
+
&& summary.orphan === 0 && summary.unverifiable === 0;
|
|
176
|
+
|
|
177
|
+
return { authTier, roots, files, summary };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// heal — propagate the authoritative tier into drifted/missing targets.
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Atomic (tmp + rename), preserves exec bits, NEVER writes the template tier.
|
|
184
|
+
// Orphans are reported, never auto-deleted (deletion is the riskier direction;
|
|
185
|
+
// a human removes a retired runtime script deliberately).
|
|
186
|
+
export function heal(analysis, opts = {}) {
|
|
187
|
+
const dryRun = !!opts.dryRun;
|
|
188
|
+
const healed = [];
|
|
189
|
+
|
|
190
|
+
for (const f of analysis.files) {
|
|
191
|
+
if (!f.healTargets.length) continue;
|
|
192
|
+
const auth = f.tiers[f.authTier];
|
|
193
|
+
if (!auth || !auth.present) continue; // defensive: nothing to copy
|
|
194
|
+
const content = readFileSync(auth.path);
|
|
195
|
+
|
|
196
|
+
for (const target of f.healTargets) {
|
|
197
|
+
if (target.tier === 'template') continue; // never write upstream
|
|
198
|
+
const root = analysis.roots[target.tier];
|
|
199
|
+
if (!root) continue;
|
|
200
|
+
const destPath = join(tierDir(root, f.category, target.tier), f.name);
|
|
201
|
+
const record = {
|
|
202
|
+
category: f.category, name: f.name, tier: target.tier,
|
|
203
|
+
reason: target.reason, from: f.authTier, path: destPath,
|
|
204
|
+
};
|
|
205
|
+
if (!dryRun) {
|
|
206
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
207
|
+
const tmp = destPath + '.tmp';
|
|
208
|
+
writeFileSync(tmp, content);
|
|
209
|
+
renameSync(tmp, destPath);
|
|
210
|
+
if (isExecutable(f.category, f.name)) chmodSync(destPath, 0o755);
|
|
211
|
+
}
|
|
212
|
+
healed.push(record);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { healed, dryRun };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// resolveRoots — default tier roots from cwd/env, overridable per flag.
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// template : <cwd>/templates only when its scripts/ exists (the CC SOURCE repo).
|
|
223
|
+
// A consumer with no local templates is legitimately 2-tier.
|
|
224
|
+
// tracked : an explicit dir, else <cwd> when it looks like a watchtower project
|
|
225
|
+
// (scripts/watchtower-lib.mjs present).
|
|
226
|
+
// runtime : an explicit dir, else $WATCHTOWER_DIR, else ~/.claude-cabinet/watchtower.
|
|
227
|
+
export function resolveRoots(opts = {}) {
|
|
228
|
+
const cwd = opts.cwd || process.cwd();
|
|
229
|
+
const env = opts.env || process.env;
|
|
230
|
+
|
|
231
|
+
let template = opts.template;
|
|
232
|
+
if (template === undefined) {
|
|
233
|
+
const cand = join(cwd, 'templates');
|
|
234
|
+
template = existsSync(join(cand, 'scripts')) ? cand : null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let tracked = opts.tracked;
|
|
238
|
+
if (tracked === undefined) {
|
|
239
|
+
tracked = existsSync(join(cwd, 'scripts', 'watchtower-lib.mjs')) ? cwd : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let runtime = opts.runtime;
|
|
243
|
+
if (runtime === undefined) {
|
|
244
|
+
runtime = env.WATCHTOWER_DIR
|
|
245
|
+
|| join(homedir(), '.claude-cabinet', 'watchtower');
|
|
246
|
+
}
|
|
247
|
+
// A runtime root that doesn't exist on disk is treated as absent so the
|
|
248
|
+
// refresh-only contract holds (nothing to diff against a missing runtime).
|
|
249
|
+
if (runtime && !existsSync(runtime)) runtime = null;
|
|
250
|
+
|
|
251
|
+
return { template: template || null, tracked: tracked || null, runtime: runtime || null };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Human-readable report
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
function tierLabel(tier) {
|
|
258
|
+
return { template: 'template', tracked: 'tracked', runtime: 'runtime' }[tier] || tier;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function renderReport(analysis, { heal: healResult } = {}) {
|
|
262
|
+
const lines = [];
|
|
263
|
+
const { roots, authTier, summary } = analysis;
|
|
264
|
+
lines.push('Watchtower script sync — 3-tier hash diff');
|
|
265
|
+
lines.push(` template: ${roots.template || '(absent)'}`);
|
|
266
|
+
lines.push(` tracked: ${roots.tracked || '(absent)'}`);
|
|
267
|
+
lines.push(` runtime: ${roots.runtime || '(absent)'}`);
|
|
268
|
+
lines.push(` authoritative tier: ${authTier || '(none — cannot verify)'}`);
|
|
269
|
+
lines.push('');
|
|
270
|
+
|
|
271
|
+
const problems = analysis.files.filter((f) => f.status !== 'in-sync');
|
|
272
|
+
if (!problems.length) {
|
|
273
|
+
lines.push(` ✓ all ${summary.total} watchtower files in sync across tiers`);
|
|
274
|
+
} else {
|
|
275
|
+
for (const f of problems) {
|
|
276
|
+
if (f.status === 'orphan') {
|
|
277
|
+
const where = TIERS.filter((t) => f.tiers[t]?.present).map(tierLabel).join(', ');
|
|
278
|
+
lines.push(` ⚠ orphan: ${f.name} — absent from ${tierLabel(f.authTier)}, lingers in ${where} (remove by hand if retired)`);
|
|
279
|
+
} else if (f.status === 'unverifiable') {
|
|
280
|
+
lines.push(` ? unverifiable: ${f.name} — no source tier present`);
|
|
281
|
+
} else {
|
|
282
|
+
const targets = f.healTargets
|
|
283
|
+
.map((h) => `${tierLabel(h.tier)} (${h.reason})`).join(', ');
|
|
284
|
+
lines.push(` ✗ ${f.status}: ${f.name} → heal ${targets} from ${tierLabel(f.authTier)}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
lines.push('');
|
|
290
|
+
lines.push(` ${summary.inSync}/${summary.total} in sync · ${summary.drift} drift · ${summary.missing} missing · ${summary.orphan} orphan` +
|
|
291
|
+
(summary.unverifiable ? ` · ${summary.unverifiable} unverifiable` : ''));
|
|
292
|
+
|
|
293
|
+
if (healResult) {
|
|
294
|
+
lines.push('');
|
|
295
|
+
if (!healResult.healed.length) {
|
|
296
|
+
lines.push(' nothing to heal');
|
|
297
|
+
} else {
|
|
298
|
+
lines.push(healResult.dryRun ? ' [dry-run] would heal:' : ' healed:');
|
|
299
|
+
for (const h of healResult.healed) {
|
|
300
|
+
lines.push(` ${h.reason === 'missing' ? '+' : '~'} ${tierLabel(h.tier)}/${h.name} ← ${tierLabel(h.from)}`);
|
|
301
|
+
}
|
|
302
|
+
const trackedHealed = healResult.healed.some((h) => h.tier === 'tracked');
|
|
303
|
+
if (trackedHealed && !healResult.dryRun) {
|
|
304
|
+
lines.push(' (tracked-tier files changed — commit them so the fix is durable)');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return lines.join('\n');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// CLI
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
function parseArgs(argv) {
|
|
316
|
+
const opts = { mode: 'check', json: false, dryRun: false };
|
|
317
|
+
for (let i = 0; i < argv.length; i++) {
|
|
318
|
+
const a = argv[i];
|
|
319
|
+
if (a === '--check') opts.mode = 'check';
|
|
320
|
+
else if (a === '--heal') opts.mode = 'heal';
|
|
321
|
+
else if (a === '--json') opts.json = true;
|
|
322
|
+
else if (a === '--dry-run') opts.dryRun = true;
|
|
323
|
+
else if (a === '--template') opts.template = argv[++i];
|
|
324
|
+
else if (a === '--tracked') opts.tracked = argv[++i];
|
|
325
|
+
else if (a === '--runtime') opts.runtime = argv[++i];
|
|
326
|
+
else if (a === '--help' || a === '-h') opts.help = true;
|
|
327
|
+
}
|
|
328
|
+
return opts;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const HELP = `watchtower-sync — keep the watchtower script tiers in step
|
|
332
|
+
|
|
333
|
+
Usage:
|
|
334
|
+
node watchtower-sync.mjs [--check|--heal] [--json] [--dry-run] [roots]
|
|
335
|
+
|
|
336
|
+
Modes:
|
|
337
|
+
--check (default) read-only: hash-diff the tiers and report divergence
|
|
338
|
+
--heal propagate the authoritative tier into drifted/missing copies
|
|
339
|
+
|
|
340
|
+
Root overrides (else auto-detected from cwd / $WATCHTOWER_DIR):
|
|
341
|
+
--template <dir> upstream templates/ root (CC source repo only)
|
|
342
|
+
--tracked <dir> the project root holding committed scripts/ + .claude/
|
|
343
|
+
--runtime <dir> the live runtime root (~/.claude-cabinet/watchtower)
|
|
344
|
+
|
|
345
|
+
--json emit the structured analysis instead of the text report
|
|
346
|
+
--dry-run with --heal, show what would be written without writing
|
|
347
|
+
|
|
348
|
+
Exit codes: 0 = clean (or healed), 2 = drift/missing/orphan detected (--check).`;
|
|
349
|
+
|
|
350
|
+
export function main(argv) {
|
|
351
|
+
const opts = parseArgs(argv);
|
|
352
|
+
if (opts.help) {
|
|
353
|
+
process.stdout.write(HELP + '\n');
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const roots = resolveRoots(opts);
|
|
358
|
+
const analysis = analyze(roots);
|
|
359
|
+
|
|
360
|
+
let healResult = null;
|
|
361
|
+
if (opts.mode === 'heal') {
|
|
362
|
+
healResult = heal(analysis, { dryRun: opts.dryRun });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (opts.json) {
|
|
366
|
+
process.stdout.write(JSON.stringify({
|
|
367
|
+
authTier: analysis.authTier,
|
|
368
|
+
roots: analysis.roots,
|
|
369
|
+
summary: analysis.summary,
|
|
370
|
+
files: analysis.files.map((f) => ({
|
|
371
|
+
category: f.category, name: f.name, status: f.status,
|
|
372
|
+
authTier: f.authTier,
|
|
373
|
+
healTargets: f.healTargets,
|
|
374
|
+
tiers: Object.fromEntries(Object.entries(f.tiers).map(([t, v]) =>
|
|
375
|
+
[t, { present: v.present, hash: v.hash || null }])),
|
|
376
|
+
})),
|
|
377
|
+
heal: healResult,
|
|
378
|
+
}, null, 2) + '\n');
|
|
379
|
+
} else {
|
|
380
|
+
process.stdout.write(renderReport(analysis, { heal: healResult }) + '\n');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// After a heal, re-evaluate to report the post-heal verdict.
|
|
384
|
+
if (opts.mode === 'heal' && !opts.dryRun) {
|
|
385
|
+
return analyze(roots).summary.clean ? 0 : 2;
|
|
386
|
+
}
|
|
387
|
+
return analysis.summary.clean ? 0 : 2;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Run as CLI when invoked directly (not when imported by tests).
|
|
391
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
392
|
+
process.exit(main(process.argv.slice(2)));
|
|
393
|
+
}
|
|
@@ -259,7 +259,11 @@ CLI.
|
|
|
259
259
|
the run directory (or `scripts/merge-findings.js <run-dir> --db`) if
|
|
260
260
|
pib-db is initialized
|
|
261
261
|
5. Present findings summary: total count, breakdown by severity, by
|
|
262
|
-
cabinet member, and highlight any critical findings
|
|
262
|
+
cabinet member, and highlight any critical findings — in plain
|
|
263
|
+
English (see `cabinet/skill-output-conventions.md` §9). The findings
|
|
264
|
+
*records* keep exact file:line locations and evidence; the *summary*
|
|
265
|
+
you speak leads with what matters and what's at stake, not a path
|
|
266
|
+
dump.
|
|
263
267
|
|
|
264
268
|
After persisting, remind the user about triage: findings need human
|
|
265
269
|
judgment before they drive action. Use `/triage-audit` to review and
|