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
|
@@ -194,16 +194,23 @@ test('parseDormantSkills: extracts dead/stale, caps, computes hasFindings', () =
|
|
|
194
194
|
assert.equal(parseDormantSkills({ dead: [], stale: [] }).hasFindings, false);
|
|
195
195
|
assert.equal(parseDormantSkills(null).hasFindings, false);
|
|
196
196
|
|
|
197
|
-
// cap
|
|
197
|
+
// cap bounds the DISPLAY list, never the count (act:ea23b3a5). It used to be
|
|
198
|
+
// both, and the title was built from the sliced array — so every project ever
|
|
199
|
+
// measured reported exactly "12 dead", a saturated constant wearing the
|
|
200
|
+
// costume of a measurement.
|
|
198
201
|
const many = { dead: Array.from({ length: 20 }, (_, i) => ({ skill: `d${i}` })), stale: [] };
|
|
199
|
-
|
|
202
|
+
const capped = parseDormantSkills(many, { cap: 5 });
|
|
203
|
+
assert.equal(capped.dead.length, 5, 'names are bounded');
|
|
204
|
+
assert.equal(capped.dead_count, 20, 'the count is TRUE');
|
|
205
|
+
assert.equal(parseDormantSkills(reader).dead_count, 2);
|
|
206
|
+
assert.equal(parseDormantSkills(reader).stale_count, 1);
|
|
200
207
|
});
|
|
201
208
|
|
|
202
209
|
// =====================================================================
|
|
203
210
|
// Orchestrator (injected deps — hermetic)
|
|
204
211
|
// =====================================================================
|
|
205
212
|
|
|
206
|
-
test('reviewCabinetRoster:
|
|
213
|
+
test('reviewCabinetRoster: only uncovered-tech FILES; dormant/briefing become ambient metrics (act:ea23b3a5)', async () => {
|
|
207
214
|
clearQueue();
|
|
208
215
|
rmSync(join(root, 'state', 'roster-review.json'), { force: true });
|
|
209
216
|
|
|
@@ -219,38 +226,85 @@ test('reviewCabinetRoster: files the three roster findings, stamps state, dedups
|
|
|
219
226
|
|
|
220
227
|
await reviewCabinetRoster(config, deps);
|
|
221
228
|
|
|
229
|
+
// uncovered-tech PROPOSES A CABINET SEAT — a real decision, so it stays a
|
|
230
|
+
// queue item. The other two are measurements and belong in ambient state.
|
|
222
231
|
const items = listQueue();
|
|
223
|
-
|
|
224
|
-
assert.deepEqual(kinds, ['dormant-skill', 'stale-briefing', 'uncovered-tech']);
|
|
225
|
-
// every finding is an ungated advisor-finding from ring2-slow
|
|
232
|
+
assert.deepEqual(items.map(i => i.evidence?.roster_kind).sort(), ['uncovered-tech']);
|
|
226
233
|
for (const it of items) {
|
|
227
234
|
assert.equal(it.category, 'advisor-finding');
|
|
228
235
|
assert.equal(it.filed_by, 'ring2-slow');
|
|
229
236
|
assert.equal(it.project, 'roster-proj');
|
|
230
237
|
}
|
|
231
238
|
assert.equal(calls.ask, 1, 'one coverage call');
|
|
232
|
-
assert.equal(calls.reader, 1, 'one reader call');
|
|
239
|
+
assert.equal(calls.reader, 1, 'one reader call — the measurement still happens');
|
|
233
240
|
|
|
234
|
-
// cadence stamped + project recorded
|
|
241
|
+
// cadence stamped + project recorded + metrics landed in the ONE trailing write
|
|
235
242
|
const state = JSON.parse(readFileSync(join(root, 'state', 'roster-review.json'), 'utf8'));
|
|
236
243
|
assert.ok(state.last_run);
|
|
237
244
|
assert.ok(state.projects_checked['roster-proj']);
|
|
238
|
-
|
|
239
|
-
|
|
245
|
+
const m = state.metrics['roster-proj'];
|
|
246
|
+
assert.ok(m, 'metrics must survive reviewCabinetRoster\'s wholesale state rewrite');
|
|
247
|
+
assert.equal(m.dormant.dead_count, 1);
|
|
248
|
+
assert.deepEqual(m.dormant.dead, ['dead-skill'], 'names, not just a count');
|
|
249
|
+
assert.equal(m.stale_briefings.length, 1);
|
|
250
|
+
assert.ok(m.measured_at);
|
|
251
|
+
assert.equal(m.previous, null, 'first measurement has no prior to compare against');
|
|
252
|
+
|
|
253
|
+
// 2nd run, same now → cadence gate blocks
|
|
240
254
|
await reviewCabinetRoster(config, deps);
|
|
241
255
|
assert.equal(calls.ask, 1, 'cadence gate prevents a second coverage call');
|
|
242
|
-
assert.equal(listQueue().length,
|
|
256
|
+
assert.equal(listQueue().length, 1, 'no duplicate items while cadence holds');
|
|
243
257
|
|
|
244
|
-
// 3rd run
|
|
245
|
-
//
|
|
258
|
+
// 3rd run past the interval: the uncovered-tech dedup still holds, and the
|
|
259
|
+
// measurement carries its prior forward so Ring 1 can render on change.
|
|
246
260
|
const later = {
|
|
247
261
|
...deps,
|
|
248
262
|
now: deps.now + (ROSTER_REVIEW_INTERVAL_DAYS + 1) * DAY,
|
|
249
263
|
ask: async () => { calls.ask++; return 'GAP: WebGL — still uncovered'; },
|
|
250
|
-
runSkillUsage: async () => { calls.reader++; return { days: 30, dead: [{ skill: 'dead-skill' }], stale: [] }; },
|
|
264
|
+
runSkillUsage: async () => { calls.reader++; return { days: 30, dead: [{ skill: 'dead-skill' }, { skill: 'another' }], stale: [] }; },
|
|
251
265
|
};
|
|
252
266
|
await reviewCabinetRoster(config, later);
|
|
253
|
-
assert.equal(listQueue().length,
|
|
267
|
+
assert.equal(listQueue().length, 1, 'dedup prevents duplicate roster findings');
|
|
268
|
+
const state2 = JSON.parse(readFileSync(join(root, 'state', 'roster-review.json'), 'utf8'));
|
|
269
|
+
assert.equal(state2.metrics['roster-proj'].dormant.dead_count, 2);
|
|
270
|
+
assert.deepEqual(state2.metrics['roster-proj'].previous, { dead_count: 1, stale_count: 0 });
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('reviewCabinetRoster retires pending dormant-skill / stale-briefing items once', async () => {
|
|
274
|
+
// The migration the change carries with it: fileRosterFinding and its dedup
|
|
275
|
+
// were the ONLY code that ever touched these items. Without this they would
|
|
276
|
+
// display a stale count for up to 30 days beside a live ambient line saying
|
|
277
|
+
// something else — dual existence.
|
|
278
|
+
clearQueue();
|
|
279
|
+
rmSync(join(root, 'state', 'roster-review.json'), { force: true });
|
|
280
|
+
|
|
281
|
+
const q = await import('../watchtower-queue.mjs');
|
|
282
|
+
const keep = q.createItem({
|
|
283
|
+
project: 'roster-proj', project_path: '/tmp/x', filed_by: 'ring2-slow',
|
|
284
|
+
category: 'advisor-finding', urgency: 'low', title: 'Roster coverage gap: roster-proj (3)',
|
|
285
|
+
summary: 's', evidence: { roster_kind: 'uncovered-tech' },
|
|
286
|
+
});
|
|
287
|
+
const retireA = q.createItem({
|
|
288
|
+
project: 'roster-proj', project_path: '/tmp/x', filed_by: 'ring2-slow',
|
|
289
|
+
category: 'advisor-finding', urgency: 'low', title: 'Dormant skills: roster-proj (12 dead, 11 stale)',
|
|
290
|
+
summary: 's', evidence: { roster_kind: 'dormant-skill' },
|
|
291
|
+
});
|
|
292
|
+
const retireB = q.createItem({
|
|
293
|
+
project: 'roster-proj', project_path: '/tmp/x', filed_by: 'ring2-slow',
|
|
294
|
+
category: 'advisor-finding', urgency: 'low', title: 'Stale cabinet briefings: roster-proj (2)',
|
|
295
|
+
summary: 's', evidence: { roster_kind: 'stale-briefing' },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const retired = ring2.retireRosterFindingItems();
|
|
299
|
+
assert.deepEqual(retired.sort(), [retireA, retireB].sort());
|
|
300
|
+
|
|
301
|
+
const statuses = Object.fromEntries(listQueue().map(i => [i.id, i.status]));
|
|
302
|
+
assert.equal(statuses[keep], 'pending', 'the decision item is untouched');
|
|
303
|
+
assert.equal(statuses[retireA], 'superseded');
|
|
304
|
+
assert.equal(statuses[retireB], 'superseded');
|
|
305
|
+
|
|
306
|
+
// Idempotent: a second pass finds nothing left pending.
|
|
307
|
+
assert.deepEqual(ring2.retireRosterFindingItems(), []);
|
|
254
308
|
});
|
|
255
309
|
|
|
256
310
|
test('reviewCabinetRoster: COVERAGE OK + no stale briefings + no dormant skills → no items', async () => {
|
|
@@ -307,5 +361,10 @@ test('reviewCabinetRoster: a thrown reader does not abort the other two checks',
|
|
|
307
361
|
});
|
|
308
362
|
|
|
309
363
|
const kinds = listQueue().map(i => i.evidence?.roster_kind).sort();
|
|
310
|
-
assert.deepEqual(kinds, ['
|
|
364
|
+
assert.deepEqual(kinds, ['uncovered-tech'], 'reader failure isolated; the coverage decision still files');
|
|
365
|
+
|
|
366
|
+
// and the briefing measurement still lands in ambient state
|
|
367
|
+
const state = JSON.parse(readFileSync(join(root, 'state', 'roster-review.json'), 'utf8'));
|
|
368
|
+
assert.equal(state.metrics['partial-proj'].stale_briefings.length, 1);
|
|
369
|
+
assert.equal(state.metrics['partial-proj'].dormant, undefined, 'the thrown reader contributed nothing');
|
|
311
370
|
});
|
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
// Phase 2c
|
|
2
|
-
//
|
|
1
|
+
// Phase 2c emit guard — the confidence inversion (act:ea23b3a5) that REPLACED
|
|
2
|
+
// the create-vs-complete day-window filter (act:9eebbac4).
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
4
|
+
// What this file used to assert, and why it no longer does. act:9eebbac4 read
|
|
5
|
+
// "an action created this session whose only evidence is its own creation is
|
|
6
|
+
// not a completion candidate" and suppressed on it — but EXEMPTED
|
|
7
|
+
// confidence: 'high', on the theory that high confidence means the transcript
|
|
8
|
+
// shows the work itself done. A full-corpus measurement on 2026-07-26 (all 223
|
|
9
|
+
// items the category had ever filed, each joined against the cited action's own
|
|
10
|
+
// project db) inverted that theory:
|
|
11
|
+
//
|
|
12
|
+
// high 12 acted-on / 28 dismissed-noise 30% precision
|
|
13
|
+
// medium+low 48 acted-on / 32 dismissed-noise 60% precision
|
|
14
|
+
//
|
|
15
|
+
// same-day creation + medium/low (the filter SUPPRESSED this) 74%
|
|
16
|
+
// same-day creation + high (the filter EXEMPTED this) 33%
|
|
17
|
+
//
|
|
18
|
+
// Split at the filter's 2026-07-12 ship date, the category went 0-for-23 — zero
|
|
19
|
+
// acted-on, 19 dismissed as noise. So the filter was deleted and the exempted
|
|
20
|
+
// bucket is now the suppressed one. Twelve of the nineteen dismissals in the
|
|
21
|
+
// 2026-07-26 tail triage were rated 'high'; that cohort is the fixture below.
|
|
14
22
|
//
|
|
15
23
|
// Hermetic: the guard takes a fake prepared statement; no sqlite, no model.
|
|
16
24
|
|
|
17
25
|
import { test } from 'node:test';
|
|
18
26
|
import assert from 'node:assert/strict';
|
|
19
|
-
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
|
27
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
20
28
|
import { join } from 'node:path';
|
|
21
29
|
import { tmpdir } from 'node:os';
|
|
22
30
|
|
|
@@ -24,142 +32,123 @@ const root = mkdtempSync(join(tmpdir(), 'ring3-completion-filter-'));
|
|
|
24
32
|
process.env.WATCHTOWER_DIR = root;
|
|
25
33
|
mkdirSync(join(root, 'queue', 'items'), { recursive: true });
|
|
26
34
|
|
|
27
|
-
const { completionReviewEmitGuard } =
|
|
35
|
+
const { completionReviewEmitGuard, verifyCompletionQuote, decodeTranscriptText, preprocessTranscript } =
|
|
36
|
+
await import('../watchtower-ring3-close.mjs');
|
|
28
37
|
|
|
29
38
|
test.after(() => rmSync(root, { recursive: true, force: true }));
|
|
30
39
|
|
|
31
|
-
const SESSION_DATE = '2026-07-10';
|
|
32
40
|
const stmtReturning = (row) => ({ get: () => row });
|
|
33
|
-
const
|
|
34
|
-
const OPEN_CREATED_EARLIER = { status: 'open', created: '2026-07-01' };
|
|
41
|
+
const OPEN = { status: 'open' };
|
|
35
42
|
|
|
36
43
|
// ---------------------------------------------------------------------------
|
|
37
|
-
// The
|
|
44
|
+
// The confidence inversion fires — positive confirmation, not a green fallback
|
|
38
45
|
// ---------------------------------------------------------------------------
|
|
39
46
|
|
|
40
|
-
test(
|
|
47
|
+
test("high confidence → suppressed, with a reason naming the calibration", () => {
|
|
41
48
|
const guard = completionReviewEmitGuard('act:aaaa1111', {
|
|
42
|
-
statusStmt: stmtReturning(
|
|
43
|
-
|
|
44
|
-
confidence: 'medium',
|
|
49
|
+
statusStmt: stmtReturning(OPEN),
|
|
50
|
+
confidence: 'high',
|
|
45
51
|
});
|
|
46
52
|
assert.equal(guard.emit, false);
|
|
47
|
-
assert.match(guard.reason, /
|
|
48
|
-
assert.match(guard.reason, /
|
|
53
|
+
assert.match(guard.reason, /anti-predictive/);
|
|
54
|
+
assert.match(guard.reason, /calibration note/);
|
|
49
55
|
});
|
|
50
56
|
|
|
51
|
-
test('
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
test('confidence matching is case-insensitive — a deviant "High" is still suppressed', () => {
|
|
58
|
+
// Direction matters: this arm is deliberately fail-CLOSED (the one such arm
|
|
59
|
+
// in the guard), so a malformed spelling must not become an escape hatch.
|
|
60
|
+
const guard = completionReviewEmitGuard('act:aaaa2222', {
|
|
61
|
+
statusStmt: stmtReturning(OPEN),
|
|
62
|
+
confidence: 'High',
|
|
56
63
|
});
|
|
57
64
|
assert.equal(guard.emit, false);
|
|
58
65
|
});
|
|
59
66
|
|
|
60
|
-
test('
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
confidence: 'High',
|
|
76
|
-
});
|
|
77
|
-
assert.equal(guard.emit, true, 'malformed spelling must not flip fail-open into fail-closed');
|
|
67
|
+
test('the 2026-07-26 tail cohort: 12 of 19 dismissals were high → at most 7 still emit', () => {
|
|
68
|
+
// The calibration corpus itself, as the fixture. Confidences taken from the
|
|
69
|
+
// 19 items dismissed noise on 2026-07-26 (queue items, resolution_type
|
|
70
|
+
// 'noise', category completion-review): 12 high, 4 medium, 3 low.
|
|
71
|
+
const cohort = [
|
|
72
|
+
...Array(12).fill('high'),
|
|
73
|
+
...Array(4).fill('medium'),
|
|
74
|
+
...Array(3).fill('low'),
|
|
75
|
+
];
|
|
76
|
+
const emitted = cohort.filter((confidence, i) =>
|
|
77
|
+
completionReviewEmitGuard(`act:cohort${String(i).padStart(2, '0')}`, {
|
|
78
|
+
statusStmt: stmtReturning(OPEN),
|
|
79
|
+
confidence,
|
|
80
|
+
}).emit).length;
|
|
81
|
+
assert.equal(emitted, 7, 'the high bucket is suppressed; the 7 medium/low still file');
|
|
78
82
|
});
|
|
79
83
|
|
|
80
84
|
// ---------------------------------------------------------------------------
|
|
81
|
-
// The
|
|
85
|
+
// The cohort the deleted filter used to suppress now files — this is the point
|
|
82
86
|
// ---------------------------------------------------------------------------
|
|
83
87
|
|
|
84
|
-
test('
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
test('an action created THIS session at medium/low confidence now EMITS (74% cohort restored)', () => {
|
|
89
|
+
// act:9eebbac4 suppressed exactly this. Measured at 20 acted-on / 7 noise —
|
|
90
|
+
// the highest-precision cell in the whole corpus.
|
|
91
|
+
for (const confidence of ['medium', 'low']) {
|
|
92
|
+
const guard = completionReviewEmitGuard('act:bbbb1111', {
|
|
93
|
+
statusStmt: stmtReturning({ status: 'open', created: '2026-07-10' }),
|
|
94
|
+
confidence,
|
|
95
|
+
});
|
|
96
|
+
assert.equal(guard.emit, true, `${confidence} must emit regardless of the created date`);
|
|
97
|
+
}
|
|
91
98
|
});
|
|
92
99
|
|
|
93
|
-
test('
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
test('the guard no longer reads a created date at all', () => {
|
|
101
|
+
// A row carrying no `created` column and a row created today are
|
|
102
|
+
// indistinguishable to the guard — proof the day-window logic is gone rather
|
|
103
|
+
// than merely unreachable.
|
|
104
|
+
const withCreated = completionReviewEmitGuard('act:bbbb2222', {
|
|
105
|
+
statusStmt: stmtReturning({ status: 'open', created: new Date().toISOString().slice(0, 10) }),
|
|
106
|
+
confidence: 'medium',
|
|
98
107
|
});
|
|
99
|
-
|
|
108
|
+
const without = completionReviewEmitGuard('act:bbbb2222', {
|
|
109
|
+
statusStmt: stmtReturning({ status: 'open' }),
|
|
110
|
+
confidence: 'medium',
|
|
111
|
+
});
|
|
112
|
+
assert.deepEqual(withCreated, without);
|
|
113
|
+
assert.equal(withCreated.emit, true);
|
|
100
114
|
});
|
|
101
115
|
|
|
102
116
|
// ---------------------------------------------------------------------------
|
|
103
|
-
// Fail-open discipline
|
|
117
|
+
// Fail-open discipline elsewhere in the guard is unchanged
|
|
104
118
|
// ---------------------------------------------------------------------------
|
|
105
119
|
|
|
106
|
-
test('
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// EVERY below-high candidate for the run (fail-closed inversion).
|
|
110
|
-
const guard = completionReviewEmitGuard('act:9999ffff', {
|
|
111
|
-
statusStmt: stmtReturning(OPEN_CREATED_IN_SESSION),
|
|
112
|
-
sessionDate: '1752000000',
|
|
120
|
+
test('thrown lookup → emits (fail-open, the status re-check discipline)', () => {
|
|
121
|
+
const guard = completionReviewEmitGuard('act:ffff6666', {
|
|
122
|
+
statusStmt: { get: () => { throw new Error('database is locked'); } },
|
|
113
123
|
confidence: 'medium',
|
|
114
124
|
});
|
|
115
125
|
assert.equal(guard.emit, true);
|
|
116
126
|
});
|
|
117
127
|
|
|
118
|
-
test('no
|
|
128
|
+
test('no confidence supplied → the inversion self-skips and the item emits', () => {
|
|
119
129
|
const guard = completionReviewEmitGuard('act:dddd4444', {
|
|
120
|
-
statusStmt: stmtReturning(
|
|
121
|
-
|
|
122
|
-
confidence: 'medium',
|
|
123
|
-
});
|
|
124
|
-
assert.equal(guard.emit, true);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test('row without a created value (schema gap) → emits', () => {
|
|
128
|
-
const guard = completionReviewEmitGuard('act:eeee5555', {
|
|
129
|
-
statusStmt: stmtReturning({ status: 'open' }),
|
|
130
|
-
sessionDate: SESSION_DATE,
|
|
131
|
-
confidence: 'medium',
|
|
132
|
-
});
|
|
133
|
-
assert.equal(guard.emit, true);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('thrown lookup → emits (fail-open, matching the status re-check discipline)', () => {
|
|
137
|
-
const guard = completionReviewEmitGuard('act:ffff6666', {
|
|
138
|
-
statusStmt: { get: () => { throw new Error('database is locked'); } },
|
|
139
|
-
sessionDate: SESSION_DATE,
|
|
140
|
-
confidence: 'medium',
|
|
130
|
+
statusStmt: stmtReturning(OPEN),
|
|
131
|
+
existingItems: [],
|
|
141
132
|
});
|
|
142
133
|
assert.equal(guard.emit, true);
|
|
143
134
|
});
|
|
144
135
|
|
|
145
136
|
// ---------------------------------------------------------------------------
|
|
146
|
-
// Guard ordering
|
|
137
|
+
// Guard ordering
|
|
147
138
|
// ---------------------------------------------------------------------------
|
|
148
139
|
|
|
149
|
-
test('status re-check wins first: a done action reports status, not
|
|
140
|
+
test('status re-check wins first: a done action reports status, not confidence', () => {
|
|
150
141
|
const guard = completionReviewEmitGuard('act:1111aaaa', {
|
|
151
|
-
statusStmt: stmtReturning({ status: 'done'
|
|
152
|
-
|
|
153
|
-
confidence: 'medium',
|
|
142
|
+
statusStmt: stmtReturning({ status: 'done' }),
|
|
143
|
+
confidence: 'high',
|
|
154
144
|
});
|
|
155
145
|
assert.equal(guard.emit, false);
|
|
156
146
|
assert.match(guard.reason, /status now 'done'/);
|
|
157
147
|
});
|
|
158
148
|
|
|
159
|
-
test('per-fid dedup still runs after the
|
|
149
|
+
test('per-fid dedup still runs after the confidence gate passes', () => {
|
|
160
150
|
const guard = completionReviewEmitGuard('act:2222bbbb', {
|
|
161
|
-
statusStmt: stmtReturning(
|
|
162
|
-
sessionDate: SESSION_DATE,
|
|
151
|
+
statusStmt: stmtReturning(OPEN),
|
|
163
152
|
confidence: 'medium',
|
|
164
153
|
existingItems: [{ id: 'dec-x1', status: 'pending', plan_fid: 'act:2222bbbb' }],
|
|
165
154
|
});
|
|
@@ -167,10 +156,83 @@ test('per-fid dedup still runs after the filter passes', () => {
|
|
|
167
156
|
assert.match(guard.reason, /existing completion-review item dec-x1/);
|
|
168
157
|
});
|
|
169
158
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Quote verification — INSTRUMENTATION, and it must stay that way
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
//
|
|
163
|
+
// The haystack is built by calling the REAL preprocessTranscript, not by
|
|
164
|
+
// hand-writing plain prose. That is the whole point: its output is one
|
|
165
|
+
// JSON.stringify'd message per line, so a hand-written fixture would test a
|
|
166
|
+
// string the production code never sees.
|
|
167
|
+
|
|
168
|
+
let fixtureSeq = 0;
|
|
169
|
+
function realHaystack(messages) {
|
|
170
|
+
// preprocessTranscript reads a Claude Code transcript FILE, so the fixture is
|
|
171
|
+
// written to disk in the nested `{ type, message: { role, content } }` shape
|
|
172
|
+
// it normalizes — no shortcut around the production code path.
|
|
173
|
+
const path = join(root, `transcript-${fixtureSeq++}.jsonl`);
|
|
174
|
+
writeFileSync(path, messages.map((m) => JSON.stringify({ type: m.role, message: m })).join('\n'));
|
|
175
|
+
// preprocessTranscript returns { compressed, originalTokenEstimate,
|
|
176
|
+
// compressedTokenEstimate } — the haystack is the `compressed` field, which
|
|
177
|
+
// is what Phase 2c slices and hands the model.
|
|
178
|
+
return preprocessTranscript(path).compressed;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
test('a multi-line prose quote verifies against a real preprocessed transcript', () => {
|
|
182
|
+
// The case a naive substring test against the raw JSONL would REJECT: the
|
|
183
|
+
// real newline inside this message is the two characters \ and n once
|
|
184
|
+
// serialized, which survives whitespace normalization and breaks containment.
|
|
185
|
+
const haystack = realHaystack([
|
|
186
|
+
{ role: 'assistant', content: 'Implemented the guard and pushed.\n\nAll 32 tests pass, nothing left here.' },
|
|
187
|
+
]);
|
|
188
|
+
const check = verifyCompletionQuote(
|
|
189
|
+
'Implemented the guard and pushed. All 32 tests pass, nothing left here.', haystack);
|
|
190
|
+
assert.equal(check.verified, true, 'decoding must happen before matching');
|
|
191
|
+
assert.equal(check.reason, 'found');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('a quote absent from the transcript does not verify', () => {
|
|
195
|
+
const haystack = realHaystack([{ role: 'assistant', content: 'Still working through the migration.' }]);
|
|
196
|
+
const check = verifyCompletionQuote('Shipped it, everything is green and merged to main.', haystack);
|
|
197
|
+
assert.equal(check.verified, false);
|
|
198
|
+
assert.equal(check.reason, 'not-found');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('an empty or trivially short quote reports absent rather than matching', () => {
|
|
202
|
+
const haystack = realHaystack([{ role: 'assistant', content: 'done' }]);
|
|
203
|
+
for (const q of ['', null, undefined, 'done']) {
|
|
204
|
+
assert.equal(verifyCompletionQuote(q, haystack).reason, 'absent-or-too-short');
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('a serialized tool-use argument is NOT quotable evidence', () => {
|
|
209
|
+
// Every TodoWrite call leaves its input_summary in the haystack. That blob is
|
|
210
|
+
// the single most quotable false positive in the corpus, so decodeTranscriptText
|
|
211
|
+
// drops tool_use blocks — a model quoting one gets no credit.
|
|
212
|
+
const haystack = realHaystack([{
|
|
213
|
+
role: 'assistant',
|
|
214
|
+
content: [{
|
|
215
|
+
type: 'tool_use',
|
|
216
|
+
name: 'TodoWrite',
|
|
217
|
+
input: { todos: [{ content: 'Wire the guard into Phase 2c', status: 'completed' }] },
|
|
218
|
+
}],
|
|
219
|
+
}]);
|
|
220
|
+
const check = verifyCompletionQuote('Wire the guard into Phase 2c', haystack);
|
|
221
|
+
assert.equal(check.verified, false, 'tool arguments must not read as completion evidence');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('the quote NEVER changes the emit decision', () => {
|
|
225
|
+
// The load-bearing assertion of this whole section: instrumentation only.
|
|
226
|
+
const base = { statusStmt: stmtReturning(OPEN), confidence: 'medium' };
|
|
227
|
+
const withoutQuote = completionReviewEmitGuard('act:5555eeee', base);
|
|
228
|
+
const withQuote = completionReviewEmitGuard('act:5555eeee', { ...base, quote: 'anything at all' });
|
|
229
|
+
assert.deepEqual(withoutQuote, withQuote);
|
|
230
|
+
assert.equal(withQuote.emit, true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('decodeTranscriptText skips unparseable lines instead of raw-matching them', () => {
|
|
234
|
+
const decoded = decodeTranscriptText('{"content":"kept"}\nnot json at all\n{"content":"also kept"}');
|
|
235
|
+
assert.match(decoded, /kept/);
|
|
236
|
+
assert.match(decoded, /also kept/);
|
|
237
|
+
assert.doesNotMatch(decoded, /not json at all/);
|
|
176
238
|
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Roster measurements as ambient state (act:ea23b3a5) — the Ring 1 render half.
|
|
2
|
+
//
|
|
3
|
+
// Ring 2 measures weekly into state/roster-review.json; Ring 1 renders into the
|
|
4
|
+
// per-project state file's Standing Issues, because Ring 1 owns that file. Same
|
|
5
|
+
// division as the recall canary (ring1: "the canary is Ring 2's, the render is
|
|
6
|
+
// Ring 1's").
|
|
7
|
+
//
|
|
8
|
+
// Two rules the render must hold, both learned the hard way:
|
|
9
|
+
// - the COUNT is true and the NAMES are what the operator can act on. The old
|
|
10
|
+
// queue item reported a saturated display cap — every project read exactly
|
|
11
|
+
// "12 dead", forever.
|
|
12
|
+
// - render ON CHANGE only. An unchanging number in a file read at every
|
|
13
|
+
// briefing is furniture by the third read; the queue item at least had a
|
|
14
|
+
// dismiss verb, and ambient state has none.
|
|
15
|
+
//
|
|
16
|
+
// Pure function, no I/O.
|
|
17
|
+
|
|
18
|
+
import { test } from 'node:test';
|
|
19
|
+
import assert from 'node:assert/strict';
|
|
20
|
+
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
23
|
+
|
|
24
|
+
const root = mkdtempSync(join(tmpdir(), 'roster-ambient-'));
|
|
25
|
+
process.env.WATCHTOWER_DIR = root;
|
|
26
|
+
mkdirSync(join(root, 'state'), { recursive: true });
|
|
27
|
+
|
|
28
|
+
const { renderRosterEntries, assembleProjectState } = await import('../watchtower-ring1.mjs');
|
|
29
|
+
|
|
30
|
+
test.after(() => rmSync(root, { recursive: true, force: true }));
|
|
31
|
+
|
|
32
|
+
const MEASURED = '2026-07-21T10:00:00.000Z';
|
|
33
|
+
|
|
34
|
+
test('a first measurement renders, with true counts and the skill names', () => {
|
|
35
|
+
const lines = renderRosterEntries({
|
|
36
|
+
measured_at: MEASURED,
|
|
37
|
+
dormant: { dead_count: 14, stale_count: 11, dead: ['audit', 'retire-me', 'verify-learn'], stale: [], days: 30 },
|
|
38
|
+
previous: null,
|
|
39
|
+
});
|
|
40
|
+
assert.equal(lines.length, 1);
|
|
41
|
+
assert.match(lines[0], /dormant skills: 14 never invoked/);
|
|
42
|
+
assert.match(lines[0], /11 stale >30d/);
|
|
43
|
+
assert.match(lines[0], /audit, retire-me, verify-learn/, 'names are the actionable half');
|
|
44
|
+
assert.match(lines[0], /measured 2026-07-21/, 'as-of stamp — this is a weekly measurement, not live');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('an UNCHANGED count renders nothing', () => {
|
|
48
|
+
const lines = renderRosterEntries({
|
|
49
|
+
measured_at: MEASURED,
|
|
50
|
+
dormant: { dead_count: 12, stale_count: 4, dead: ['a'], stale: [], days: 30 },
|
|
51
|
+
previous: { dead_count: 12, stale_count: 4 },
|
|
52
|
+
});
|
|
53
|
+
assert.deepEqual(lines, [], 'a number that never moves never needs a human');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('a CHANGED count renders and shows the movement', () => {
|
|
57
|
+
const lines = renderRosterEntries({
|
|
58
|
+
measured_at: MEASURED,
|
|
59
|
+
dormant: { dead_count: 14, stale_count: 4, dead: ['a'], stale: [], days: 30 },
|
|
60
|
+
previous: { dead_count: 11, stale_count: 4 },
|
|
61
|
+
});
|
|
62
|
+
assert.equal(lines.length, 1);
|
|
63
|
+
assert.match(lines[0], /14 never invoked \(was 11\)/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('a change in only the stale count still renders', () => {
|
|
67
|
+
const lines = renderRosterEntries({
|
|
68
|
+
measured_at: MEASURED,
|
|
69
|
+
dormant: { dead_count: 12, stale_count: 9, dead: ['a'], stale: [], days: 30 },
|
|
70
|
+
previous: { dead_count: 12, stale_count: 4 },
|
|
71
|
+
});
|
|
72
|
+
assert.equal(lines.length, 1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('zero dormant skills render nothing at all', () => {
|
|
76
|
+
assert.deepEqual(renderRosterEntries({
|
|
77
|
+
measured_at: MEASURED,
|
|
78
|
+
dormant: { dead_count: 0, stale_count: 0, dead: [], stale: [], days: 30 },
|
|
79
|
+
previous: null,
|
|
80
|
+
}), []);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('the name list is capped but the count is not', () => {
|
|
84
|
+
const dead = Array.from({ length: 20 }, (_, i) => `skill-${i}`);
|
|
85
|
+
const lines = renderRosterEntries({
|
|
86
|
+
measured_at: MEASURED,
|
|
87
|
+
dormant: { dead_count: 20, stale_count: 0, dead, stale: [], days: 30 },
|
|
88
|
+
previous: null,
|
|
89
|
+
}, 3);
|
|
90
|
+
assert.match(lines[0], /dormant skills: 20 never invoked/, 'the count is true');
|
|
91
|
+
assert.match(lines[0], /skill-0, skill-1, skill-2, \+17 more/, 'the enumeration is bounded');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('stale briefings render their own line with the oldest age', () => {
|
|
95
|
+
const lines = renderRosterEntries({
|
|
96
|
+
measured_at: MEASURED,
|
|
97
|
+
stale_briefings: [
|
|
98
|
+
{ name: '_briefing-architecture.md', ageDaysBehind: 63 },
|
|
99
|
+
{ name: '_briefing-cabinet.md', ageDaysBehind: 12 },
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
assert.equal(lines.length, 1);
|
|
103
|
+
assert.match(lines[0], /stale cabinet briefings: 2, oldest ~63d behind/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('absent, empty, or malformed metrics render nothing rather than throwing', () => {
|
|
107
|
+
for (const input of [null, undefined, {}, 'nonsense', 42, { dormant: null }, { stale_briefings: [] }]) {
|
|
108
|
+
assert.deepEqual(renderRosterEntries(input), [], `input ${JSON.stringify(input)}`);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('the lines reach Standing Issues in the assembled project state', () => {
|
|
113
|
+
const state = assembleProjectState({
|
|
114
|
+
name: 'proj', path: '/tmp/proj',
|
|
115
|
+
pib: { openActions: 3, flaggedCount: 0 },
|
|
116
|
+
roster: {
|
|
117
|
+
measured_at: MEASURED,
|
|
118
|
+
dormant: { dead_count: 14, stale_count: 2, dead: ['audit'], stale: [], days: 30 },
|
|
119
|
+
previous: { dead_count: 11, stale_count: 2 },
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
assert.match(state, /## Standing Issues/);
|
|
123
|
+
assert.match(state, /- dormant skills: 14 never invoked \(was 11\)/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('a project with no roster entry renders no roster lines', () => {
|
|
127
|
+
const state = assembleProjectState({
|
|
128
|
+
name: 'proj', path: '/tmp/proj', pib: { openActions: 0, flaggedCount: 0 }, roster: null,
|
|
129
|
+
});
|
|
130
|
+
assert.doesNotMatch(state, /dormant skills/);
|
|
131
|
+
assert.match(state, /## Standing Issues/);
|
|
132
|
+
});
|