create-claude-cabinet 0.52.0 → 0.53.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/lib/cli.js +2 -2
- package/package.json +1 -1
- package/templates/CLAUDE.md +164 -20
- package/templates/cabinet/watchtower-contracts.md +202 -10
- package/templates/mux/bin/mux +5 -0
- package/templates/mux/config/mux-server.py +7 -5
- package/templates/scripts/__tests__/category-expiry.test.mjs +130 -0
- package/templates/scripts/__tests__/detector-registry.test.mjs +41 -7
- package/templates/scripts/__tests__/extraction-classification.test.mjs +87 -0
- package/templates/scripts/__tests__/friction-actionability.test.mjs +180 -0
- package/templates/scripts/__tests__/memory-budget-debt.test.mjs +264 -0
- package/templates/scripts/__tests__/ring2-pattern-gate.test.mjs +315 -0
- package/templates/scripts/__tests__/ring2-roster-review.test.mjs +75 -16
- package/templates/scripts/__tests__/ring3-completion-filter.test.mjs +167 -105
- package/templates/scripts/__tests__/roster-ambient-render.test.mjs +132 -0
- package/templates/scripts/__tests__/routine-dispatch.test.mjs +29 -1
- package/templates/scripts/__tests__/routine-moment-expiry.test.mjs +192 -0
- package/templates/scripts/skill-validator.sh +7 -4
- package/templates/scripts/watchtower-lib.mjs +58 -5
- package/templates/scripts/watchtower-queue.mjs +54 -7
- package/templates/scripts/watchtower-ring1.mjs +74 -0
- package/templates/scripts/watchtower-ring2.mjs +442 -81
- package/templates/scripts/watchtower-ring3-close.mjs +271 -82
- package/templates/scripts/watchtower-routines.mjs +107 -1
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +35 -2
- package/templates/skills/cabinet-anti-confirmation/SKILL.md +24 -0
- package/templates/skills/cabinet-cc-health/SKILL.md +22 -0
- package/templates/skills/cabinet-data-integrity/SKILL.md +19 -0
- package/templates/skills/cabinet-debugger/SKILL.md +36 -0
- package/templates/skills/cabinet-deployment/SKILL.md +22 -0
- package/templates/skills/cabinet-elegance/SKILL.md +12 -0
- package/templates/skills/cabinet-goal-alignment/SKILL.md +11 -0
- package/templates/skills/cabinet-historian/SKILL.md +33 -0
- package/templates/skills/cabinet-organized-mind/SKILL.md +11 -0
- package/templates/skills/cabinet-process-therapist/SKILL.md +132 -6
- package/templates/skills/cabinet-qa/SKILL.md +63 -0
- package/templates/skills/cabinet-roster-check/SKILL.md +12 -0
- package/templates/skills/cabinet-workflow-cop/SKILL.md +60 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Memory-budget standing debt + validator-backed retraction (act:ea23b3a5).
|
|
2
|
+
//
|
|
3
|
+
// Two live defects, one pass:
|
|
4
|
+
// (a) the alarm re-filed as a fresh event every 7 days — the 2026-07-26
|
|
5
|
+
// triage found one maginnis budget violation restated three times across
|
|
6
|
+
// three categories;
|
|
7
|
+
// (b) it never retracted. Those alarms sat pending for days after MEMORY.md
|
|
8
|
+
// was fixed (verified 74 lines / 16.6 KB against a 200 / 25 KB budget),
|
|
9
|
+
// while Ring 2 slow was already running the validator that knew.
|
|
10
|
+
//
|
|
11
|
+
// Both halves now live in Ring 2 beside the verdict. Hermetic: every queue
|
|
12
|
+
// dependency is injected, so no fixture queue and no subprocess.
|
|
13
|
+
|
|
14
|
+
import { test } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { mkdtempSync, mkdirSync, rmSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
|
|
20
|
+
const root = mkdtempSync(join(tmpdir(), 'memory-budget-debt-'));
|
|
21
|
+
process.env.WATCHTOWER_DIR = root;
|
|
22
|
+
mkdirSync(join(root, 'queue', 'items'), { recursive: true });
|
|
23
|
+
const stateDir = join(root, 'state');
|
|
24
|
+
mkdirSync(stateDir, { recursive: true });
|
|
25
|
+
|
|
26
|
+
const ring2 = await import('../watchtower-ring2.mjs');
|
|
27
|
+
|
|
28
|
+
test.after(() => rmSync(root, { recursive: true, force: true }));
|
|
29
|
+
|
|
30
|
+
// --- a tiny in-memory queue double, shaped like the real verbs -------------
|
|
31
|
+
|
|
32
|
+
function makeQueue(seed = []) {
|
|
33
|
+
const items = [...seed];
|
|
34
|
+
let seq = 0;
|
|
35
|
+
return {
|
|
36
|
+
items,
|
|
37
|
+
listPending: ({ project, category }) => items.filter((i) =>
|
|
38
|
+
i.status === 'pending'
|
|
39
|
+
&& (!project || i.project === project)
|
|
40
|
+
&& (!category || i.category === category)),
|
|
41
|
+
// createItem returns the new item's id STRING, matching the real one.
|
|
42
|
+
createItem: (spec) => {
|
|
43
|
+
const id = `dec-new${seq++}`;
|
|
44
|
+
items.push({ ...spec, id, status: 'pending', filed_at: new Date().toISOString() });
|
|
45
|
+
return id;
|
|
46
|
+
},
|
|
47
|
+
annotateItemEvidence: (id, patch) => {
|
|
48
|
+
const item = items.find((i) => i.id === id);
|
|
49
|
+
if (!item || item.status !== 'pending') return null;
|
|
50
|
+
item.evidence = { ...(item.evidence || {}), ...patch };
|
|
51
|
+
return { item, changed: true };
|
|
52
|
+
},
|
|
53
|
+
supersedeItem: (id, { reason }) => {
|
|
54
|
+
const item = items.find((i) => i.id === id);
|
|
55
|
+
if (item) { item.status = 'superseded'; item.resolution_notes = reason; }
|
|
56
|
+
return item || null;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const VIOLATIONS = ['MEMORY.md is 293 lines (budget 200)', 'MEMORY.md is 43.6KB (budget 25KB)'];
|
|
62
|
+
const violating = (project) => [{ project, violations: VIOLATIONS, status: 'violations' }];
|
|
63
|
+
// Pre-seed the streak so the call under test is already at the
|
|
64
|
+
// 3-consecutive-run gate (the gate itself is unchanged behaviour).
|
|
65
|
+
function seedStreak(project, count) {
|
|
66
|
+
writeFileSync(join(stateDir, 'memory-violation-streaks.json'),
|
|
67
|
+
JSON.stringify({ [project]: { count, last_filed: null } }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Aggregation
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
test('first violating run past the streak gate files exactly one item', () => {
|
|
75
|
+
const q = makeQueue();
|
|
76
|
+
seedStreak('proj-a', 2); // +1 this run = 3, the threshold
|
|
77
|
+
ring2.surfacePersistentViolations(violating('proj-a'), { 'proj-a': { path: '/tmp/a' } }, stateDir,
|
|
78
|
+
{ ...q, today: '2026-07-26' });
|
|
79
|
+
assert.equal(q.items.length, 1);
|
|
80
|
+
const ev = q.items[0].evidence;
|
|
81
|
+
assert.equal(ev.debt_class, 'memory-budget');
|
|
82
|
+
assert.deepEqual(ev.days, ['2026-07-26']);
|
|
83
|
+
assert.equal(ev.session_count, 1);
|
|
84
|
+
assert.equal(ev.first_seen, '2026-07-26');
|
|
85
|
+
assert.deepEqual(ev.violations, VIOLATIONS);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('a SECOND day APPENDS to the existing item — no second item', () => {
|
|
89
|
+
const q = makeQueue();
|
|
90
|
+
seedStreak('proj-b', 2);
|
|
91
|
+
ring2.surfacePersistentViolations(violating('proj-b'), {}, stateDir, { ...q, today: '2026-07-26' });
|
|
92
|
+
ring2.surfacePersistentViolations(violating('proj-b'), {}, stateDir, { ...q, today: '2026-07-27' });
|
|
93
|
+
|
|
94
|
+
assert.equal(q.items.filter((i) => i.status === 'pending').length, 1, 'still ONE pending item');
|
|
95
|
+
const ev = q.items[0].evidence;
|
|
96
|
+
assert.deepEqual(ev.days, ['2026-07-26', '2026-07-27']);
|
|
97
|
+
assert.equal(ev.session_count, 2, 'the spelling itemSessionWeight already reads');
|
|
98
|
+
assert.equal(ev.first_seen, '2026-07-26', 'first_seen holds the oldest occurrence');
|
|
99
|
+
assert.equal(ev.last_seen, '2026-07-27');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('replay guard: a second run on the SAME day changes nothing', () => {
|
|
103
|
+
const q = makeQueue();
|
|
104
|
+
seedStreak('proj-c', 2);
|
|
105
|
+
ring2.surfacePersistentViolations(violating('proj-c'), {}, stateDir, { ...q, today: '2026-07-26' });
|
|
106
|
+
ring2.surfacePersistentViolations(violating('proj-c'), {}, stateDir, { ...q, today: '2026-07-26' });
|
|
107
|
+
ring2.surfacePersistentViolations(violating('proj-c'), {}, stateDir, { ...q, today: '2026-07-26' });
|
|
108
|
+
assert.equal(q.items.length, 1);
|
|
109
|
+
assert.deepEqual(q.items[0].evidence.days, ['2026-07-26']);
|
|
110
|
+
assert.equal(q.items[0].evidence.session_count, 1,
|
|
111
|
+
'Ring 2 slow runs ~48x/day — the day key is what keeps this from counting cron ticks');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a LEGACY item (no debt_class, evidence {violations, streak}) is found and appended to', () => {
|
|
115
|
+
// Every memory-budget item on disk when this shipped has this shape. A
|
|
116
|
+
// debt_class-only selector would silently miss the pile that motivated the fix.
|
|
117
|
+
const legacy = {
|
|
118
|
+
id: 'dec-legacy1', project: 'proj-d', category: 'watchtower-health', status: 'pending',
|
|
119
|
+
title: '[AGING] Memory budget violation: proj-d', filed_at: '2026-07-10T00:00:00Z',
|
|
120
|
+
evidence: { violations: ['old'], streak: 5 },
|
|
121
|
+
};
|
|
122
|
+
const q = makeQueue([legacy]);
|
|
123
|
+
seedStreak('proj-d', 2);
|
|
124
|
+
ring2.surfacePersistentViolations(violating('proj-d'), {}, stateDir, { ...q, today: '2026-07-26' });
|
|
125
|
+
|
|
126
|
+
assert.equal(q.items.filter((i) => i.status === 'pending').length, 1, 'no duplicate filed');
|
|
127
|
+
assert.equal(q.items[0].id, 'dec-legacy1');
|
|
128
|
+
assert.equal(q.items[0].evidence.debt_class, 'memory-budget', 'the legacy item is upgraded in place');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('two pending items collapse: oldest accumulates, newer is superseded naming it', () => {
|
|
132
|
+
const older = {
|
|
133
|
+
id: 'dec-old', project: 'proj-e', category: 'watchtower-health', status: 'pending',
|
|
134
|
+
title: 'Memory budget violation: proj-e', filed_at: '2026-07-01T00:00:00Z', evidence: {},
|
|
135
|
+
};
|
|
136
|
+
const newer = {
|
|
137
|
+
id: 'dec-new', project: 'proj-e', category: 'watchtower-health', status: 'pending',
|
|
138
|
+
title: 'Memory budget violation: proj-e', filed_at: '2026-07-20T00:00:00Z', evidence: {},
|
|
139
|
+
};
|
|
140
|
+
const q = makeQueue([newer, older]); // deliberately out of order
|
|
141
|
+
seedStreak('proj-e', 2);
|
|
142
|
+
ring2.surfacePersistentViolations(violating('proj-e'), {}, stateDir, { ...q, today: '2026-07-26' });
|
|
143
|
+
|
|
144
|
+
assert.equal(q.items.find((i) => i.id === 'dec-old').status, 'pending');
|
|
145
|
+
assert.equal(q.items.find((i) => i.id === 'dec-new').status, 'superseded');
|
|
146
|
+
assert.match(q.items.find((i) => i.id === 'dec-new').resolution_notes, /dec-old/);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('an unrelated watchtower-health item is never absorbed', () => {
|
|
150
|
+
const other = {
|
|
151
|
+
id: 'dec-other', project: 'proj-f', category: 'watchtower-health', status: 'pending',
|
|
152
|
+
title: 'Ring 3 has not run in 3 days', filed_at: '2026-07-01T00:00:00Z', evidence: {},
|
|
153
|
+
};
|
|
154
|
+
const q = makeQueue([other]);
|
|
155
|
+
seedStreak('proj-f', 2);
|
|
156
|
+
ring2.surfacePersistentViolations(violating('proj-f'), {}, stateDir, { ...q, today: '2026-07-26' });
|
|
157
|
+
assert.equal(q.items.find((i) => i.id === 'dec-other').status, 'pending',
|
|
158
|
+
'category alone must not decide absorption — that would eat a sibling detector');
|
|
159
|
+
assert.equal(q.items.filter((i) => i.status === 'pending').length, 2);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('below the streak threshold nothing is filed at all', () => {
|
|
163
|
+
const q = makeQueue();
|
|
164
|
+
seedStreak('proj-g', 0);
|
|
165
|
+
ring2.surfacePersistentViolations(violating('proj-g'), {}, stateDir, { ...q, today: '2026-07-26' });
|
|
166
|
+
assert.equal(q.items.length, 0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('the selector matches both shapes and nothing else', () => {
|
|
170
|
+
assert.equal(ring2.isMemoryBudgetItem({
|
|
171
|
+
category: 'watchtower-health', evidence: { debt_class: 'memory-budget' } }), true);
|
|
172
|
+
assert.equal(ring2.isMemoryBudgetItem({
|
|
173
|
+
category: 'watchtower-health', title: 'Memory budget violation: x' }), true);
|
|
174
|
+
assert.equal(ring2.isMemoryBudgetItem({
|
|
175
|
+
category: 'watchtower-health', title: '[AGING] Memory budget violation: x' }), true,
|
|
176
|
+
'the [AGING] rewrite is exactly what broke the old startsWith check');
|
|
177
|
+
assert.equal(ring2.isMemoryBudgetItem({
|
|
178
|
+
category: 'watchtower-health', title: 'Ring 2 stalled' }), false);
|
|
179
|
+
assert.equal(ring2.isMemoryBudgetItem({
|
|
180
|
+
category: 'knowledge-extraction', evidence: { debt_class: 'memory-budget' } }), false);
|
|
181
|
+
assert.equal(ring2.isMemoryBudgetItem(null), false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Retraction — fail toward KEEPING
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
function retractionQueue(items) {
|
|
189
|
+
const calls = [];
|
|
190
|
+
return {
|
|
191
|
+
calls,
|
|
192
|
+
listPending: ({ project, category }) => items.filter((i) =>
|
|
193
|
+
i.status === 'pending' && i.project === project && i.category === category),
|
|
194
|
+
autoReconcileItem: (id, opts) => { calls.push({ id, ...opts }); return { id }; },
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const PENDING_ALARM = [{
|
|
199
|
+
id: 'dec-alarm', project: 'proj-x', category: 'watchtower-health', status: 'pending',
|
|
200
|
+
title: 'Memory budget violation: proj-x', filed_at: '2026-07-10T00:00:00Z',
|
|
201
|
+
evidence: { debt_class: 'memory-budget' },
|
|
202
|
+
}];
|
|
203
|
+
|
|
204
|
+
test('a passing project retracts its alarm as a machine act', () => {
|
|
205
|
+
const q = retractionQueue(PENDING_ALARM);
|
|
206
|
+
const n = ring2.retractClearedMemoryBudgetItems([{ project: 'proj-x', status: 'pass' }], q);
|
|
207
|
+
assert.equal(n, 1);
|
|
208
|
+
assert.equal(q.calls[0].id, 'dec-alarm');
|
|
209
|
+
assert.equal(q.calls[0].actor, 'ring2-slow', 'excluded from engagement buckets portfolio-wide');
|
|
210
|
+
assert.equal(q.calls[0].resolution, 'validator-passes');
|
|
211
|
+
assert.equal(q.calls[0].evidence.validator_status, 'pass');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('a LEGACY-shaped alarm retracts too', () => {
|
|
215
|
+
const legacy = [{
|
|
216
|
+
id: 'dec-legacy2', project: 'proj-y', category: 'watchtower-health', status: 'pending',
|
|
217
|
+
title: '[AGING] Memory budget violation: proj-y', evidence: { violations: [], streak: 9 },
|
|
218
|
+
}];
|
|
219
|
+
const q = retractionQueue(legacy);
|
|
220
|
+
assert.equal(ring2.retractClearedMemoryBudgetItems([{ project: 'proj-y' }], q), 1,
|
|
221
|
+
'the three items that motivated this fix carry no debt_class');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('violations, skipped, and an empty pass list retract nothing', () => {
|
|
225
|
+
for (const passList of [[], undefined, null]) {
|
|
226
|
+
const q = retractionQueue(PENDING_ALARM);
|
|
227
|
+
assert.equal(ring2.retractClearedMemoryBudgetItems(passList, q), 0);
|
|
228
|
+
assert.equal(q.calls.length, 0);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('a throwing reconciler keeps the item — never a swallowed retraction', () => {
|
|
233
|
+
const q = {
|
|
234
|
+
listPending: () => PENDING_ALARM,
|
|
235
|
+
autoReconcileItem: () => { throw new Error('queue is locked'); },
|
|
236
|
+
};
|
|
237
|
+
assert.equal(ring2.retractClearedMemoryBudgetItems([{ project: 'proj-x' }], q), 0,
|
|
238
|
+
'fail toward KEEPING: an error is not evidence the condition cleared');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('a race (item no longer pending) counts as not-retracted, not as an error', () => {
|
|
242
|
+
const q = { listPending: () => PENDING_ALARM, autoReconcileItem: () => null };
|
|
243
|
+
assert.equal(ring2.retractClearedMemoryBudgetItems([{ project: 'proj-x' }], q), 0);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('the streak resets on a pass — the anti-oscillation floor', () => {
|
|
247
|
+
// Retract, then a violating run: the project must fail three consecutive
|
|
248
|
+
// runs again before anything refiles, so retract→refile cannot cycle every
|
|
249
|
+
// 30 minutes.
|
|
250
|
+
const q = makeQueue();
|
|
251
|
+
seedStreak('proj-z', 9);
|
|
252
|
+
ring2.surfacePersistentViolations([], {}, stateDir, { ...q, today: '2026-07-26' }); // now passing
|
|
253
|
+
const streaks = JSON.parse(readFileSync(join(stateDir, 'memory-violation-streaks.json'), 'utf8'));
|
|
254
|
+
assert.equal(streaks['proj-z'], undefined, 'streak cleared');
|
|
255
|
+
|
|
256
|
+
ring2.surfacePersistentViolations(violating('proj-z'), {}, stateDir, { ...q, today: '2026-07-27' });
|
|
257
|
+
assert.equal(q.items.length, 0, 'one violating run after a pass is not enough to refile');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('the state file is always written, even with no violations', () => {
|
|
261
|
+
const q = makeQueue();
|
|
262
|
+
ring2.surfacePersistentViolations([], {}, stateDir, { ...q, today: '2026-07-26' });
|
|
263
|
+
assert.ok(existsSync(join(stateDir, 'memory-violation-streaks.json')));
|
|
264
|
+
});
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// Pattern-promotion emission-quality gate (act:09184ad7) — hermetic tests.
|
|
2
|
+
//
|
|
3
|
+
// Background: the 2026-07-26 consolidation of 144 pending pattern-promotion
|
|
4
|
+
// items found 17 misfiled ledger/status fragments (wave tables, dependency
|
|
5
|
+
// graphs, setup recipes, a raw transcript snippet) that ring2-slow had filed
|
|
6
|
+
// as "patterns", plus 6 non-durable observations (no-gap positives,
|
|
7
|
+
// truncated-transcript disclaimers). Three layers landed:
|
|
8
|
+
// 1. patternHasRequiredShape — mechanical Evidence/Gap shape gate at the
|
|
9
|
+
// scanAuditPatterns filing site (calibrated: 120/120 legitimate bodies
|
|
10
|
+
// pass, 17/17 fragments fail).
|
|
11
|
+
// 2. QUALITY_PATTERN_SYSTEM_PROMPT exclusions in Ring 3 Phase 2e — the
|
|
12
|
+
// classes that carry well-formed Evidence/Gap blocks and can only be
|
|
13
|
+
// filtered at generation time.
|
|
14
|
+
// 3. runDraftAnnotationSweep pattern-promotion fold pass — near-duplicate
|
|
15
|
+
// variants arrive pre-annotated, grouped per target member.
|
|
16
|
+
//
|
|
17
|
+
// Hermetic per ring2-roster-review.test.mjs: WATCHTOWER_DIR points at a temp
|
|
18
|
+
// dir set BEFORE the dynamic import; orchestrators are driven entirely
|
|
19
|
+
// through injected deps — no Anthropic call, no real queue.
|
|
20
|
+
|
|
21
|
+
import { test } from 'node:test';
|
|
22
|
+
import assert from 'node:assert/strict';
|
|
23
|
+
import {
|
|
24
|
+
mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, utimesSync,
|
|
25
|
+
} from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { tmpdir } from 'node:os';
|
|
28
|
+
|
|
29
|
+
const root = mkdtempSync(join(tmpdir(), 'ring2-pattern-gate-'));
|
|
30
|
+
process.env.WATCHTOWER_DIR = root;
|
|
31
|
+
|
|
32
|
+
const ring2 = await import('../watchtower-ring2.mjs');
|
|
33
|
+
const {
|
|
34
|
+
patternHasRequiredShape,
|
|
35
|
+
parseAuditPatterns,
|
|
36
|
+
scanAuditPatterns,
|
|
37
|
+
runDraftAnnotationSweep,
|
|
38
|
+
} = ring2;
|
|
39
|
+
const { QUALITY_PATTERN_SYSTEM_PROMPT } = await import('../watchtower-ring3-close.mjs');
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Fixtures — representative of the five misfiled-fragment shapes dismissed in
|
|
43
|
+
// the 2026-07-26 consolidation (dec-fd619cbf, dec-e965c928, dec-67a6a869,
|
|
44
|
+
// dec-84eabbea, dec-67be0c26) and of legitimate pattern bodies.
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
const FRAGMENT_WAVE_TABLE = `| Wave | Actions | Status |
|
|
48
|
+
|------|---------|--------|
|
|
49
|
+
| 1 | 7 | DONE |
|
|
50
|
+
| 2 | 12 | 10/12 done (2 remaining) |
|
|
51
|
+
|
|
52
|
+
**Total:** 46 actions (17 done, 29 open)`;
|
|
53
|
+
|
|
54
|
+
const FRAGMENT_DEP_GRAPH = `\`\`\`
|
|
55
|
+
Wave 2 remaining:
|
|
56
|
+
act:862d8333 ---- independent
|
|
57
|
+
act:205551ba ---- independent (large)
|
|
58
|
+
\`\`\``;
|
|
59
|
+
|
|
60
|
+
const FRAGMENT_RECIPE = `1. Set action in-progress
|
|
61
|
+
2. Build in worktree (docker-compose, no local Ruby)
|
|
62
|
+
3. RSpec pre-check (\`bash -c\` wrapper)
|
|
63
|
+
4. Merge to main`;
|
|
64
|
+
|
|
65
|
+
const FRAGMENT_TRANSCRIPT = `- timelog.md: hours not yet logged (ask operator)
|
|
66
|
+
</tool_response>
|
|
67
|
+
|
|
68
|
+
<tool_call>
|
|
69
|
+
{"name": "Bash", "input": {"command": "cat reviews/map.md"}}
|
|
70
|
+
</tool_call>`;
|
|
71
|
+
|
|
72
|
+
const FRAGMENT_FID_LIST = `- act:88fe16f4 — EmailTemplate key-vs-name
|
|
73
|
+
- act:767ed43a — e-sign multi-campaign SigningEvent model`;
|
|
74
|
+
|
|
75
|
+
const LEGIT_BODY = `**Evidence:** The assistant repeatedly makes concrete technical decisions
|
|
76
|
+
before getting explicit approval, then presents them as fait accompli.
|
|
77
|
+
|
|
78
|
+
**Gap:** The assistant conflates "I have a recommendation" with "I have a
|
|
79
|
+
mandate." The session needed a cleaner checkpoint.`;
|
|
80
|
+
|
|
81
|
+
// Carries well-formed blocks but is a transcript-artifact observation — the
|
|
82
|
+
// shape gate deliberately PASSES it; the prompt exclusions own this class.
|
|
83
|
+
const SESSION_SPECIFIC_BODY = `**Evidence:** The transcript cuts off mid-sentence — the session was still
|
|
84
|
+
working when the record ended.
|
|
85
|
+
|
|
86
|
+
**Gap:** Pattern analysis on a truncated transcript can only assess the
|
|
87
|
+
portion observed.`;
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// 1. The shape gate
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
test('patternHasRequiredShape: rejects every misfiled-fragment shape', () => {
|
|
94
|
+
for (const body of [
|
|
95
|
+
FRAGMENT_WAVE_TABLE, FRAGMENT_DEP_GRAPH, FRAGMENT_RECIPE,
|
|
96
|
+
FRAGMENT_TRANSCRIPT, FRAGMENT_FID_LIST,
|
|
97
|
+
]) {
|
|
98
|
+
assert.equal(patternHasRequiredShape(body), false);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('patternHasRequiredShape: accepts well-formed Evidence/Gap bodies', () => {
|
|
103
|
+
assert.equal(patternHasRequiredShape(LEGIT_BODY), true);
|
|
104
|
+
// The boundary is deliberate: well-formed session-specific observations
|
|
105
|
+
// pass the mechanical gate — the prompt exclusions are the layer for them.
|
|
106
|
+
assert.equal(patternHasRequiredShape(SESSION_SPECIFIC_BODY), true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('patternHasRequiredShape: requires BOTH blocks and a string body', () => {
|
|
110
|
+
assert.equal(patternHasRequiredShape('**Evidence:** something observed'), false);
|
|
111
|
+
assert.equal(patternHasRequiredShape('**Gap:** something missing'), false);
|
|
112
|
+
assert.equal(patternHasRequiredShape(''), false);
|
|
113
|
+
assert.equal(patternHasRequiredShape(undefined), false);
|
|
114
|
+
assert.equal(patternHasRequiredShape(null), false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// 2. Parse + gate integration over a realistic audit-patterns.md
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
const PATTERNS_MD = `# Quality Patterns
|
|
122
|
+
|
|
123
|
+
### 2026-07-20
|
|
124
|
+
|
|
125
|
+
## Unilateral Decisions Dressed as Consultation
|
|
126
|
+
|
|
127
|
+
${LEGIT_BODY}
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Waves Overview
|
|
132
|
+
|
|
133
|
+
${FRAGMENT_WAVE_TABLE}
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
test('parseAuditPatterns + gate: only the well-formed section would file', () => {
|
|
137
|
+
const parsed = parseAuditPatterns(PATTERNS_MD);
|
|
138
|
+
assert.equal(parsed.length, 2);
|
|
139
|
+
const verdicts = parsed.map((p) => patternHasRequiredShape(p.body));
|
|
140
|
+
assert.deepEqual(verdicts, [true, false]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// 3. scanAuditPatterns — gate wired at the filing site
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
function makeScanFixture(name) {
|
|
148
|
+
const dir = join(root, name);
|
|
149
|
+
mkdirSync(join(dir, 'state'), { recursive: true });
|
|
150
|
+
writeFileSync(join(dir, 'state', 'audit-patterns.md'), PATTERNS_MD);
|
|
151
|
+
const member = {
|
|
152
|
+
name: 'process-therapist', description: 'skill ecosystem health',
|
|
153
|
+
project: 'alpha', project_path: '/tmp/alpha',
|
|
154
|
+
};
|
|
155
|
+
const calls = { routed: [], created: [] };
|
|
156
|
+
const deps = {
|
|
157
|
+
watchtowerDir: dir,
|
|
158
|
+
membersFn: () => [member],
|
|
159
|
+
routeFn: (pattern) => { calls.routed.push(pattern.title); return member; },
|
|
160
|
+
createItemFn: (payload) => { calls.created.push(payload); return 'dec-test0001'; },
|
|
161
|
+
listPendingFn: () => [],
|
|
162
|
+
};
|
|
163
|
+
return { dir, calls, deps };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
test('scanAuditPatterns: files the well-formed pattern, rejects the fragment before routing', async () => {
|
|
167
|
+
const { dir, calls, deps } = makeScanFixture('scan-basic');
|
|
168
|
+
await scanAuditPatterns({ projects: {} }, deps);
|
|
169
|
+
|
|
170
|
+
// Only the legit section was routed and filed — the fragment never cost a
|
|
171
|
+
// routing call.
|
|
172
|
+
assert.deepEqual(calls.routed, ['Unilateral Decisions Dressed as Consultation']);
|
|
173
|
+
assert.equal(calls.created.length, 1);
|
|
174
|
+
assert.equal(calls.created[0].category, 'pattern-promotion');
|
|
175
|
+
assert.match(calls.created[0].title, /Unilateral Decisions/);
|
|
176
|
+
|
|
177
|
+
// Rejection is counted loudly in persisted state, and BOTH hashes are
|
|
178
|
+
// marked processed so the fragment is not retried every run.
|
|
179
|
+
const state = JSON.parse(readFileSync(join(dir, 'state', 'pattern-detection.json'), 'utf8'));
|
|
180
|
+
assert.equal(state.last_scan_rejected, 1);
|
|
181
|
+
assert.equal(state.rejected_malformed_total, 1);
|
|
182
|
+
assert.equal(state.processed_hashes.length, 2);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('scanAuditPatterns: rejected fragments are not reprocessed; the total accumulates', async () => {
|
|
186
|
+
const { dir, calls, deps } = makeScanFixture('scan-accumulate');
|
|
187
|
+
await scanAuditPatterns({ projects: {} }, deps);
|
|
188
|
+
assert.equal(calls.created.length, 1);
|
|
189
|
+
|
|
190
|
+
// Unchanged file → mtime gate short-circuits, nothing new happens.
|
|
191
|
+
await scanAuditPatterns({ projects: {} }, deps);
|
|
192
|
+
assert.equal(calls.routed.length, 1);
|
|
193
|
+
assert.equal(calls.created.length, 1);
|
|
194
|
+
|
|
195
|
+
// A NEW malformed section arrives → rejected, total accumulates to 2,
|
|
196
|
+
// and the already-filed pattern is not re-filed.
|
|
197
|
+
const patternsPath = join(dir, 'state', 'audit-patterns.md');
|
|
198
|
+
writeFileSync(patternsPath, `${PATTERNS_MD}\n---\n\n## Gate Setup Recipe\n\n${FRAGMENT_RECIPE}\n`);
|
|
199
|
+
const future = new Date(Date.now() + 60_000);
|
|
200
|
+
utimesSync(patternsPath, future, future);
|
|
201
|
+
await scanAuditPatterns({ projects: {} }, deps);
|
|
202
|
+
|
|
203
|
+
assert.equal(calls.routed.length, 1);
|
|
204
|
+
assert.equal(calls.created.length, 1);
|
|
205
|
+
const state = JSON.parse(readFileSync(join(dir, 'state', 'pattern-detection.json'), 'utf8'));
|
|
206
|
+
assert.equal(state.last_scan_rejected, 1);
|
|
207
|
+
assert.equal(state.rejected_malformed_total, 2);
|
|
208
|
+
assert.equal(state.processed_hashes.length, 3);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// 4. The Phase 2e prompt exclusions (guard against silent removal)
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
test('QUALITY_PATTERN_SYSTEM_PROMPT: names the format and all three exclusions', () => {
|
|
216
|
+
const p = QUALITY_PATTERN_SYSTEM_PROMPT;
|
|
217
|
+
// The mechanical half the shape gate depends on.
|
|
218
|
+
assert.ok(p.includes('**Evidence:**'));
|
|
219
|
+
assert.ok(p.includes('**Gap:**'));
|
|
220
|
+
assert.ok(p.includes('No recurring patterns detected'));
|
|
221
|
+
// The three prompt-only exclusions (act:09184ad7).
|
|
222
|
+
assert.match(p, /"no gap" observation is not a pattern/);
|
|
223
|
+
assert.match(p, /transcript artifact itself/);
|
|
224
|
+
assert.match(p, /not a document excerpt/);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// 5. Pattern-promotion folds in the draft-annotation sweep
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
function patternItem(id, project, member, title) {
|
|
232
|
+
return {
|
|
233
|
+
id,
|
|
234
|
+
project,
|
|
235
|
+
category: 'pattern-promotion',
|
|
236
|
+
evidence: {
|
|
237
|
+
target_member: member,
|
|
238
|
+
pattern_title: title,
|
|
239
|
+
pattern_text: `**Evidence:** ${title} observed repeatedly.\n\n**Gap:** ${title} unaddressed.`,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
test('runDraftAnnotationSweep: folds near-duplicate patterns within one member, never across members', () => {
|
|
245
|
+
const sweepDir = join(root, 'sweep');
|
|
246
|
+
mkdirSync(join(sweepDir, 'state'), { recursive: true });
|
|
247
|
+
|
|
248
|
+
// Two near-identical process-therapist variants (the live fold shape from
|
|
249
|
+
// the consolidation: "incomplete orientation before execution" filed
|
|
250
|
+
// twice), plus a THIRD item with the same title targeted at a DIFFERENT
|
|
251
|
+
// member — same lexical similarity, must not be treated as a duplicate.
|
|
252
|
+
const items = [
|
|
253
|
+
patternItem('dec-aaaa0001', 'alpha', 'process-therapist', 'Incomplete Orientation Before Execution'),
|
|
254
|
+
patternItem('dec-aaaa0002', 'alpha', 'process-therapist', 'Pattern 1: Incomplete Orientation Before Execution'),
|
|
255
|
+
patternItem('dec-aaaa0003', 'alpha', 'workflow-cop', 'Incomplete Orientation Before Execution'),
|
|
256
|
+
];
|
|
257
|
+
const annotations = [];
|
|
258
|
+
const summary = runDraftAnnotationSweep(
|
|
259
|
+
{ projects: { alpha: { path: '/tmp/alpha' } } },
|
|
260
|
+
{
|
|
261
|
+
watchtowerDir: sweepDir,
|
|
262
|
+
listPendingFn: ({ project, category } = {}) => {
|
|
263
|
+
if (category === 'pattern-promotion') {
|
|
264
|
+
return project ? items.filter((i) => i.project === project) : items;
|
|
265
|
+
}
|
|
266
|
+
return [];
|
|
267
|
+
},
|
|
268
|
+
annotateFn: (id, patch) => { annotations.push({ id, patch }); return { item: { id }, changed: true }; },
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const annotatedIds = annotations.map((a) => a.id).sort();
|
|
273
|
+
assert.deepEqual(annotatedIds, ['dec-aaaa0001', 'dec-aaaa0002']);
|
|
274
|
+
const forFirst = annotations.find((a) => a.id === 'dec-aaaa0001');
|
|
275
|
+
assert.deepEqual(forFirst.patch.possible_duplicate_of, ['dec-aaaa0002']);
|
|
276
|
+
|
|
277
|
+
assert.equal(summary.pattern_items_scanned, 3);
|
|
278
|
+
assert.equal(summary.pattern_fold_annotated, 2);
|
|
279
|
+
assert.equal(summary.projects_scanned, 1);
|
|
280
|
+
|
|
281
|
+
// Positive-confirmation sidecar carries the new counters.
|
|
282
|
+
const sidecar = JSON.parse(readFileSync(join(sweepDir, 'state', 'draft-annotations-health.json'), 'utf8'));
|
|
283
|
+
assert.equal(sidecar.pattern_items_scanned, 3);
|
|
284
|
+
assert.equal(sidecar.pattern_fold_annotated, 2);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('runDraftAnnotationSweep: patterns outside config are counted, never silently invisible', () => {
|
|
288
|
+
const sweepDir = join(root, 'sweep-unscanned');
|
|
289
|
+
mkdirSync(join(sweepDir, 'state'), { recursive: true });
|
|
290
|
+
const outside = patternItem('dec-bbbb0001', 'orphan-project', 'qa', 'Some Pattern Title Here');
|
|
291
|
+
const summary = runDraftAnnotationSweep(
|
|
292
|
+
{ projects: { alpha: { path: '/tmp/alpha' } } },
|
|
293
|
+
{
|
|
294
|
+
watchtowerDir: sweepDir,
|
|
295
|
+
listPendingFn: ({ project, category } = {}) => {
|
|
296
|
+
if (category === 'pattern-promotion' && !project) return [outside];
|
|
297
|
+
return [];
|
|
298
|
+
},
|
|
299
|
+
annotateFn: () => { throw new Error('must not annotate anything'); },
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
assert.equal(summary.unscanned_pattern_items, 1);
|
|
303
|
+
assert.equal(summary.pattern_fold_annotated, 0);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('runDraftAnnotationSweep: sidecar still written when there is nothing to sweep', () => {
|
|
307
|
+
const sweepDir = join(root, 'sweep-empty');
|
|
308
|
+
mkdirSync(join(sweepDir, 'state'), { recursive: true });
|
|
309
|
+
const summary = runDraftAnnotationSweep(
|
|
310
|
+
{ projects: { alpha: { path: '/tmp/alpha' } } },
|
|
311
|
+
{ watchtowerDir: sweepDir, listPendingFn: () => [], annotateFn: () => null },
|
|
312
|
+
);
|
|
313
|
+
assert.equal(summary.pattern_items_scanned, 0);
|
|
314
|
+
assert.ok(existsSync(join(sweepDir, 'state', 'draft-annotations-health.json')));
|
|
315
|
+
});
|