@warren-dean/code-kit 0.20.0 → 0.22.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +75 -50
- package/bin/code-kit.mjs +364 -25
- package/hooks/guard-agent.mjs +36 -0
- package/hooks/guard-bash.mjs +67 -13
- package/hooks/guard-paths.mjs +14 -2
- package/hooks/hooks.json +20 -0
- package/hooks/lane-audit.mjs +38 -1
- package/hooks/lib/adapters/hub/client.mjs +15 -0
- package/hooks/lib/checks.mjs +5 -1
- package/hooks/lib/config.mjs +149 -6
- package/hooks/lib/decisionstore.mjs +89 -0
- package/hooks/lib/harness.mjs +7 -0
- package/hooks/lib/hook.mjs +7 -0
- package/hooks/lib/instructions.mjs +46 -13
- package/hooks/lib/laneclaims.mjs +89 -0
- package/hooks/lib/layers.mjs +11 -0
- package/hooks/lib/loop-state.mjs +20 -2
- package/hooks/lib/next.mjs +34 -1
- package/hooks/lib/plan.mjs +3 -1
- package/hooks/lib/questions.mjs +23 -5
- package/hooks/lib/regression.mjs +2 -1
- package/hooks/lib/rules.mjs +20 -13
- package/hooks/lib/schedule.mjs +86 -0
- package/hooks/lib/sessions.mjs +223 -0
- package/hooks/lib/snapshot.mjs +80 -2
- package/hooks/lib/standards.mjs +450 -0
- package/hooks/lib/team.mjs +9 -2
- package/hooks/lib/verify.mjs +23 -1
- package/hooks/mod/decisions.mjs +1 -28
- package/hooks/mod/loop.mjs +8 -0
- package/hooks/mod/mission.mjs +21 -19
- package/hooks/mod/register.mjs +117 -40
- package/hooks/mod/register.test.ts +248 -73
- package/hooks/mod/review.mjs +1 -0
- package/hooks/mod/view.mjs +98 -47
- package/hooks/post-edit-check.mjs +8 -1
- package/hooks/session.mjs +24 -0
- package/hooks/stop-check.mjs +23 -3
- package/package.json +1 -1
- package/skills/decide/SKILL.md +1 -1
- package/skills/dispatch/SKILL.md +2 -2
- package/skills/init/SKILL.md +17 -3
- package/skills/lead/SKILL.md +28 -0
- package/skills/next/SKILL.md +2 -2
- package/skills/review/SKILL.md +2 -1
- package/skills/spec-design/SKILL.md +10 -7
- package/templates/agent.md +1 -0
- package/templates/claude-md.md +5 -1
- package/templates/plan.md +2 -1
|
@@ -87,6 +87,10 @@ function project() {
|
|
|
87
87
|
acts: [] as string[][],
|
|
88
88
|
pulses: [] as any[],
|
|
89
89
|
runs: 0,
|
|
90
|
+
laneClaims: [] as any[],
|
|
91
|
+
role: null as string | null, // SESS-2: this session's role, as `loop --json --session` says
|
|
92
|
+
task: null as any, // SESS-4: a working session's task
|
|
93
|
+
decisions: null as any, // OWN-1: `decisions --json --session`: { mine, elsewhere }
|
|
90
94
|
settings: [
|
|
91
95
|
{
|
|
92
96
|
key: 'autonomy',
|
|
@@ -226,6 +230,19 @@ function stub(on: any, w: World) {
|
|
|
226
230
|
if (sub === 'verify')
|
|
227
231
|
return ran(w.verifyExit, w.verifyExit ? '' : w.verifyOut, w.verifyExit ? w.verifyOut : '');
|
|
228
232
|
if (sub === 'stops') return ran(0, JSON.stringify(w.stops));
|
|
233
|
+
if (sub === 'task') return ran(0, JSON.stringify(w.task));
|
|
234
|
+
if (sub === 'decisions')
|
|
235
|
+
return w.decisions ? ran(0, JSON.stringify(w.decisions)) : ran(1, '', 'unexpected decisions');
|
|
236
|
+
if (sub === 'sessions') {
|
|
237
|
+
w.acts.push([...argv.slice(2)]);
|
|
238
|
+
return ran(0, 'Seen.');
|
|
239
|
+
}
|
|
240
|
+
if (sub === 'lanes' && argv[3] === 'release') {
|
|
241
|
+
w.acts.push([...argv.slice(2)]);
|
|
242
|
+
w.laneClaims = w.laneClaims.filter((c: any) => c.lane !== argv[4]);
|
|
243
|
+
return ran(0, `Released the ${argv[4]} lane.`);
|
|
244
|
+
}
|
|
245
|
+
if (sub === 'lanes') return ran(0, JSON.stringify(w.laneClaims));
|
|
229
246
|
if (sub === 'story') return ran(0, JSON.stringify(w.story));
|
|
230
247
|
if (sub === 'adapters') return ran(0, JSON.stringify(w.adapters));
|
|
231
248
|
if (sub === 'timeline')
|
|
@@ -258,6 +275,9 @@ function stub(on: any, w: World) {
|
|
|
258
275
|
}
|
|
259
276
|
if (sub === 'loop' && argv[3] === 'lead') {
|
|
260
277
|
const session = argv[argv.indexOf('--session') + 1];
|
|
278
|
+
// SESS-2: a working session leads only when the person moves the loop to it.
|
|
279
|
+
if (w.role === 'working' && !argv.includes('--take'))
|
|
280
|
+
return ran(1, '', 'No session leads this project now, and this is a working session.');
|
|
261
281
|
if (w.leadSession && w.leadSession !== session && !argv.includes('--take'))
|
|
262
282
|
return ran(1, '', `Another session (${w.leadSession}) runs the lead loop in this project.`);
|
|
263
283
|
w.leadSession = session;
|
|
@@ -276,6 +296,7 @@ function stub(on: any, w: World) {
|
|
|
276
296
|
paused: w.loopPaused,
|
|
277
297
|
at: null,
|
|
278
298
|
lead: w.leadSession ? { session: w.leadSession, at: '2026-10-16T00:00:00.000Z' } : null,
|
|
299
|
+
...(w.role ? { role: w.role } : {}),
|
|
279
300
|
}),
|
|
280
301
|
);
|
|
281
302
|
if (sub === 'sent-back') {
|
|
@@ -622,7 +643,8 @@ test('PANE-4 with nothing changed it reads the project every 10 seconds while th
|
|
|
622
643
|
// The fifth reads it all (check, status, next, requests, stops, queue, timeline, the lead's questions,
|
|
623
644
|
// the reports waiting, and which session runs the loop, while this one doesn't) between two looks at git.
|
|
624
645
|
await clock.advance(2000);
|
|
625
|
-
|
|
646
|
+
// OWN-1: and this session's decisions.
|
|
647
|
+
expect(w.runs - before).toBe(17);
|
|
626
648
|
});
|
|
627
649
|
|
|
628
650
|
test('CARD-4 outside a code-kit project, /lanes says so, opens nothing, and there is no band', async ($, on) => {
|
|
@@ -1204,6 +1226,57 @@ test('SET-2 a number is typed; one the CLI refuses shows why and changes nothing
|
|
|
1204
1226
|
await ui.unmount();
|
|
1205
1227
|
});
|
|
1206
1228
|
|
|
1229
|
+
test("WHO-3 the Approvals section says who decides each, and a change is the person's, with a reason", async ($, on) => {
|
|
1230
|
+
const w = project();
|
|
1231
|
+
w.settings.push(
|
|
1232
|
+
{
|
|
1233
|
+
key: 'approvals.schema',
|
|
1234
|
+
value: 'lead',
|
|
1235
|
+
default: 'lead',
|
|
1236
|
+
about: 'Who approves a lane\'s change to what\'s protected as "schema"',
|
|
1237
|
+
section: 'Approvals',
|
|
1238
|
+
choices: ['lead', 'person'],
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
key: 'approvals.merge',
|
|
1242
|
+
value: 'lead',
|
|
1243
|
+
default: 'lead',
|
|
1244
|
+
about: 'Who merges a reviewed branch into the base',
|
|
1245
|
+
section: 'Approvals',
|
|
1246
|
+
choices: ['lead', 'person'],
|
|
1247
|
+
},
|
|
1248
|
+
);
|
|
1249
|
+
await start($, on, w);
|
|
1250
|
+
await harness($);
|
|
1251
|
+
const ui = await pane($, SETTINGS);
|
|
1252
|
+
expect(await ui.find({ type: 'Text', text: 'APPROVALS' })).toBeDefined();
|
|
1253
|
+
expect(await ui.find({ type: 'Text', text: 'schema' })).toBeDefined();
|
|
1254
|
+
expect((await ui.find({ key: 'set-approvals.schema' }))?.props.value).toBe('lead');
|
|
1255
|
+
await $.ui.select({
|
|
1256
|
+
plugin: 'code-kit',
|
|
1257
|
+
key: 'set-approvals.schema',
|
|
1258
|
+
value: 'person',
|
|
1259
|
+
requestId: SETTINGS,
|
|
1260
|
+
});
|
|
1261
|
+
expect(await ui.find({ type: 'Text', text: /Set approvals\.schema to you\?/ })).toBeDefined();
|
|
1262
|
+
expect(w.acts).toEqual([]);
|
|
1263
|
+
await $.ui.input({ plugin: 'code-kit', key: 'settings-reason', text: 'I review schema changes' });
|
|
1264
|
+
expect(w.acts).toEqual([
|
|
1265
|
+
[
|
|
1266
|
+
'settings',
|
|
1267
|
+
'set',
|
|
1268
|
+
'approvals.schema',
|
|
1269
|
+
'person',
|
|
1270
|
+
'--reason',
|
|
1271
|
+
'I review schema changes',
|
|
1272
|
+
'--via',
|
|
1273
|
+
'pane',
|
|
1274
|
+
],
|
|
1275
|
+
]);
|
|
1276
|
+
expect((await ui.find({ key: 'set-approvals.schema' }))?.props.value).toBe('person');
|
|
1277
|
+
await ui.unmount();
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1207
1280
|
test("SET-2 the reviewer's model can go back to the session's", async ($, on) => {
|
|
1208
1281
|
const w = project();
|
|
1209
1282
|
w.settings[3].value = 'haiku';
|
|
@@ -1708,70 +1781,30 @@ test('ASK-2 and ASK-4 an answer closes its decision at once, though the lead hea
|
|
|
1708
1781
|
await ui.unmount();
|
|
1709
1782
|
});
|
|
1710
1783
|
|
|
1711
|
-
test(
|
|
1784
|
+
test('OWN-2 sixty calls Claude Code refused, each followed by the lead finding another way, are no decisions: the Lanes pane shows the latest as activity', async ($, on) => {
|
|
1712
1785
|
const w = project();
|
|
1713
|
-
await start($, on, w);
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1786
|
+
const clock = await start($, on, w);
|
|
1787
|
+
for (let i = 1; i <= 60; i++) {
|
|
1788
|
+
await $.classic.PermissionDenied({
|
|
1789
|
+
tool_name: 'Bash',
|
|
1790
|
+
tool_input: { command: `pnpm db:migrate --step ${i}` },
|
|
1791
|
+
tool_use_id: `tu-${i}`,
|
|
1792
|
+
reason: 'Permission for this action was denied by the Claude Code auto mode classifier.',
|
|
1793
|
+
});
|
|
1794
|
+
await $.tool.call({ tool: 'Bash', tool_use_id: `ok-${i}`, command: 'pnpm test' } as any);
|
|
1795
|
+
}
|
|
1796
|
+
await settle(clock);
|
|
1720
1797
|
const band = await bandUi($);
|
|
1721
|
-
expect(
|
|
1722
|
-
|
|
1723
|
-
).toBeDefined();
|
|
1724
|
-
await press($, 'band-runDenied');
|
|
1725
|
-
expect(w.fills).toEqual(['! pnpm db:generate && pnpm db:migrate']);
|
|
1726
|
-
await press($, 'band-decide');
|
|
1798
|
+
expect(await band.find({ type: 'Text', text: /refused/ })).toBeUndefined();
|
|
1799
|
+
expect(await band.find({ key: 'band-decide' })).toBeUndefined();
|
|
1727
1800
|
await band.unmount();
|
|
1801
|
+
expect(w.asked).toEqual([]);
|
|
1802
|
+
await lanes($);
|
|
1728
1803
|
const ui = await pane($, PANE);
|
|
1729
|
-
expect(
|
|
1730
|
-
|
|
1731
|
-
type: 'Text',
|
|
1732
|
-
text: /^Claude Code's own permission check refused it \(Permission for this action/,
|
|
1733
|
-
}),
|
|
1734
|
-
).toBeDefined();
|
|
1735
|
-
await press($, 'decide-deniedDone', PANE);
|
|
1736
|
-
expect(w.prompts.at(-1)).toMatch(
|
|
1737
|
-
/^The person ran it themselves, or allowed it: pnpm db:generate && pnpm db:migrate/,
|
|
1804
|
+
expect((await ui.find({ key: 'lane-lead' }))?.text).toMatch(
|
|
1805
|
+
/Claude Code refused `pnpm db:migrate/,
|
|
1738
1806
|
);
|
|
1739
1807
|
await ui.unmount();
|
|
1740
|
-
const after = await bandUi($);
|
|
1741
|
-
expect(await after.find({ type: 'Text', text: /Claude Code refused/ })).toBeUndefined();
|
|
1742
|
-
await after.unmount();
|
|
1743
|
-
});
|
|
1744
|
-
|
|
1745
|
-
test('PERM-1 a refused call that later goes through no longer waits for the person', async ($, on) => {
|
|
1746
|
-
const w = project();
|
|
1747
|
-
await start($, on, w);
|
|
1748
|
-
const refused = (id: string, command: string) =>
|
|
1749
|
-
$.classic.PermissionDenied({
|
|
1750
|
-
tool_name: 'Bash',
|
|
1751
|
-
tool_input: { command },
|
|
1752
|
-
tool_use_id: id,
|
|
1753
|
-
reason: 'Permission for this action was denied by the Claude Code auto mode classifier.',
|
|
1754
|
-
});
|
|
1755
|
-
await refused('tu-1', 'pnpm db:generate');
|
|
1756
|
-
await refused('tu-2', 'pnpm db:migrate');
|
|
1757
|
-
// another command going through leaves both waiting
|
|
1758
|
-
await $.tool.call({ tool: 'Bash', tool_use_id: 'tu-3', command: 'pnpm test' } as any);
|
|
1759
|
-
let band = await bandUi($);
|
|
1760
|
-
expect(await band.find({ type: 'Text', text: /^2 calls refused by Claude Code/ })).toBeDefined();
|
|
1761
|
-
await band.unmount();
|
|
1762
|
-
// the lead retries one and it goes through: only the other still waits
|
|
1763
|
-
await $.tool.call({ tool: 'Bash', tool_use_id: 'tu-4', command: 'pnpm db:generate' } as any);
|
|
1764
|
-
band = await bandUi($);
|
|
1765
|
-
expect(
|
|
1766
|
-
await band.find({ type: 'Text', text: "Claude Code refused the lead's `pnpm db:migrate`" }),
|
|
1767
|
-
).toBeDefined();
|
|
1768
|
-
await band.unmount();
|
|
1769
|
-
await $.tool.call({ tool: 'Bash', tool_use_id: 'tu-5', command: 'pnpm db:migrate' } as any);
|
|
1770
|
-
band = await bandUi($);
|
|
1771
|
-
expect(
|
|
1772
|
-
await band.find({ type: 'Text', text: /Claude Code refused|refused by Claude Code/ }),
|
|
1773
|
-
).toBeUndefined();
|
|
1774
|
-
await band.unmount();
|
|
1775
1808
|
});
|
|
1776
1809
|
|
|
1777
1810
|
test('CARD-8 two approvals waiting are two decisions, each approved or dropped on its own', async ($, on) => {
|
|
@@ -2172,31 +2205,32 @@ test("LOOP-1 review and the lead's own stories are prompted with their skill", a
|
|
|
2172
2205
|
expect(w.prompts[1]).toMatch(/^Build ST-6 yourself.*lead\/st-6/);
|
|
2173
2206
|
});
|
|
2174
2207
|
|
|
2175
|
-
test('LOOP-8 a step the lead was asked for
|
|
2208
|
+
test('LOOP-8 and WHO-4 a step the lead was asked for three times and that is still next is a note, while lanes keep committing', async ($, on) => {
|
|
2176
2209
|
const w = project();
|
|
2177
2210
|
w.next = { step: 'merge', args: 'lead/st-1', then: [] };
|
|
2178
2211
|
const clock = await start($, on, w);
|
|
2179
|
-
for (const [i, turn] of ['t-1', 't-2', 't-3', 't-4'].entries()) {
|
|
2212
|
+
for (const [i, turn] of ['t-1', 't-2', 't-3', 't-4', 't-5'].entries()) {
|
|
2180
2213
|
// A lane commits between the lead's turns: the project moves, but not this step.
|
|
2181
2214
|
w.refs += ` c${i} refs/heads/core/st-${i}\n`;
|
|
2182
2215
|
await leadTurn($, turn);
|
|
2183
2216
|
await settle(clock);
|
|
2184
2217
|
}
|
|
2185
|
-
expect(w.prompts).toHaveLength(
|
|
2218
|
+
expect(w.prompts).toHaveLength(3);
|
|
2186
2219
|
expect(w.prompts.every((p: string) => p.startsWith('Merge lead/st-1'))).toBe(true);
|
|
2220
|
+
expect(w.asked).toEqual([]);
|
|
2187
2221
|
const ui = await bandUi($);
|
|
2188
2222
|
expect(
|
|
2189
2223
|
await ui.find({
|
|
2190
2224
|
type: 'Text',
|
|
2191
|
-
text: 'Merge lead/st-1: still next after
|
|
2225
|
+
text: 'Merge lead/st-1: still next after 3 prompts to the lead',
|
|
2192
2226
|
}),
|
|
2193
2227
|
).toBeDefined();
|
|
2194
2228
|
await press($, 'band-go');
|
|
2195
2229
|
await ui.unmount();
|
|
2196
|
-
expect(w.prompts).toHaveLength(
|
|
2230
|
+
expect(w.prompts).toHaveLength(4);
|
|
2197
2231
|
// Once the step changes, the loop goes on by itself.
|
|
2198
2232
|
w.next = { step: 'dispatch', args: 'ST-9', then: [] };
|
|
2199
|
-
await leadTurn($, 't-
|
|
2233
|
+
await leadTurn($, 't-6');
|
|
2200
2234
|
await settle(clock);
|
|
2201
2235
|
expect(w.prompts.at(-1)).toBe('Dispatch ST-9: run /code-kit:dispatch ST-9.');
|
|
2202
2236
|
});
|
|
@@ -2277,7 +2311,7 @@ test('LOOP-6 the pause is recorded for other tools, and a new session starts wit
|
|
|
2277
2311
|
// It steps the clock through tens of minutes of the mod's 2-second refresh, each tick a real round of
|
|
2278
2312
|
// stubbed CLI runs: about 1.5 to 3 seconds locally, and more on a shared CI runner.
|
|
2279
2313
|
test(
|
|
2280
|
-
'LOOP-3 a quiet lane agent is nudged at 5 minutes, restarted through the lead at 10, and
|
|
2314
|
+
'LOOP-3 and WHO-4 a quiet lane agent is nudged at 5 minutes, restarted through the lead at 10, and handed to the lead after 2 restarts',
|
|
2281
2315
|
{ timeoutMs: 30000 },
|
|
2282
2316
|
async ($, on) => {
|
|
2283
2317
|
const w = project();
|
|
@@ -2312,14 +2346,51 @@ test(
|
|
|
2312
2346
|
];
|
|
2313
2347
|
await clock.advance(10 * 60000 + 2000);
|
|
2314
2348
|
}
|
|
2315
|
-
|
|
2349
|
+
// WHO-4: past its restarts, the lead is asked how the story goes on; the person sees a note.
|
|
2350
|
+
expect(w.prompts).toHaveLength(3);
|
|
2351
|
+
expect(w.prompts[2]).toMatch(
|
|
2352
|
+
/^web-engineer \(agent a3\) on ST-4 has stalled again after 2 restarts.*split ST-4 into smaller stories/,
|
|
2353
|
+
);
|
|
2316
2354
|
const band = await bandUi($);
|
|
2317
|
-
expect(
|
|
2318
|
-
|
|
2355
|
+
expect(
|
|
2356
|
+
await band.find({
|
|
2357
|
+
type: 'Text',
|
|
2358
|
+
text: 'web stalled 3 times on ST-4: the lead decides how it goes on',
|
|
2359
|
+
}),
|
|
2360
|
+
).toBeDefined();
|
|
2361
|
+
expect(await band.find({ key: 'band-resumeStall' })).toBeUndefined();
|
|
2362
|
+
expect(w.asked).toEqual([]);
|
|
2319
2363
|
await band.unmount();
|
|
2320
2364
|
},
|
|
2321
2365
|
);
|
|
2322
2366
|
|
|
2367
|
+
test('SCHED-1 a lane this session claimed, with no agent of it running, is freed after a minute', async ($, on) => {
|
|
2368
|
+
const w = project();
|
|
2369
|
+
w.next = { step: 'wait', args: '', then: [] };
|
|
2370
|
+
const since = new Date(0).toISOString();
|
|
2371
|
+
w.laneClaims = [
|
|
2372
|
+
{ lane: 'web', story: 'ST-4', session: 's1', since },
|
|
2373
|
+
{ lane: 'api', story: 'ST-8', session: 'someone-else', since },
|
|
2374
|
+
];
|
|
2375
|
+
w.agents = [];
|
|
2376
|
+
const clock = await start($, on, w);
|
|
2377
|
+
await clock.advance(64000);
|
|
2378
|
+
// Its own claim only: another session's lane is that session's to free.
|
|
2379
|
+
expect(w.acts.filter((a: string[]) => a[0] === 'lanes')).toEqual([['lanes', 'release', 'web']]);
|
|
2380
|
+
});
|
|
2381
|
+
|
|
2382
|
+
test('SCHED-1 a lane whose agent still runs keeps its claim', async ($, on) => {
|
|
2383
|
+
const w = project();
|
|
2384
|
+
w.next = { step: 'wait', args: '', then: [] };
|
|
2385
|
+
w.laneClaims = [{ lane: 'web', story: 'ST-4', session: 's1', since: new Date(0).toISOString() }];
|
|
2386
|
+
w.agents = [
|
|
2387
|
+
{ id: 'a1', type: 'web-engineer', description: 'ST-4 booking form', status: 'running' },
|
|
2388
|
+
];
|
|
2389
|
+
const clock = await start($, on, w);
|
|
2390
|
+
await clock.advance(64000);
|
|
2391
|
+
expect(w.acts.filter((a: string[]) => a[0] === 'lanes')).toEqual([]);
|
|
2392
|
+
});
|
|
2393
|
+
|
|
2323
2394
|
test('LEAN-5 a lane agent past its context limit is told to wrap up, and its story goes on with a fresh agent', async ($, on) => {
|
|
2324
2395
|
const w = project();
|
|
2325
2396
|
w.next = { step: 'wait', args: '', then: [] };
|
|
@@ -2488,13 +2559,63 @@ test('LOOP-10 a second session opened in the project never prompts its lead; Lea
|
|
|
2488
2559
|
await clock.advance(4000);
|
|
2489
2560
|
expect(w.prompts).toEqual([]);
|
|
2490
2561
|
const ui = await bandUi($);
|
|
2491
|
-
expect(await ui.find({ type: 'Text', text: 'the
|
|
2562
|
+
expect(await ui.find({ type: 'Text', text: 'the lead is in another session' })).toBeDefined();
|
|
2492
2563
|
await press($, 'band-leadHere');
|
|
2493
2564
|
await ui.unmount();
|
|
2494
2565
|
expect(w.leadSession).toBe('s1');
|
|
2495
2566
|
expect(w.prompts).toEqual(['Dispatch ST-9: run /code-kit:dispatch ST-9.']);
|
|
2496
2567
|
});
|
|
2497
2568
|
|
|
2569
|
+
test('SESS-2 and SESS-3 a working session never prompts its lead, asks nothing, and its band holds only its own, with its task', async ($, on) => {
|
|
2570
|
+
const w = project();
|
|
2571
|
+
w.leadSession = 's-lead';
|
|
2572
|
+
w.role = 'working';
|
|
2573
|
+
w.task = { title: 'Integration readiness', paths: ['src/integrations/**'] };
|
|
2574
|
+
w.questions = [
|
|
2575
|
+
{ id: 'Q-1', question: 'Which payment provider?', state: 'open', options: [], story: null },
|
|
2576
|
+
];
|
|
2577
|
+
w.queue = [{ branch: 'web/st-4', state: 'waiting' }];
|
|
2578
|
+
const clock = await start($, on, w);
|
|
2579
|
+
await leadTurn($);
|
|
2580
|
+
await settle(clock);
|
|
2581
|
+
await clock.advance(4000);
|
|
2582
|
+
expect(w.prompts).toEqual([]);
|
|
2583
|
+
expect(w.asked).toEqual([]);
|
|
2584
|
+
const ui = await bandUi($);
|
|
2585
|
+
expect(await ui.find({ type: 'Text', text: 'the lead is in another session' })).toBeDefined();
|
|
2586
|
+
expect(
|
|
2587
|
+
await ui.find({
|
|
2588
|
+
type: 'Text',
|
|
2589
|
+
text: 'your task: Integration readiness · leasing src/integrations/**',
|
|
2590
|
+
}),
|
|
2591
|
+
).toBeDefined();
|
|
2592
|
+
expect(await ui.find({ type: 'Text', text: /web\/st-4 is next to merge/ })).toBeUndefined();
|
|
2593
|
+
await ui.unmount();
|
|
2594
|
+
});
|
|
2595
|
+
|
|
2596
|
+
test('SESS-2 with no session leading, a working session says so and offers Lead from here', async ($, on) => {
|
|
2597
|
+
const w = project();
|
|
2598
|
+
w.role = 'working';
|
|
2599
|
+
const clock = await start($, on, w);
|
|
2600
|
+
await leadTurn($);
|
|
2601
|
+
await settle(clock);
|
|
2602
|
+
expect(w.prompts).toEqual([]);
|
|
2603
|
+
const ui = await bandUi($);
|
|
2604
|
+
expect(
|
|
2605
|
+
await ui.find({ type: 'Text', text: 'no session leads the project: the loop waits for one' }),
|
|
2606
|
+
).toBeDefined();
|
|
2607
|
+
expect(await ui.find({ key: 'band-leadHere' })).toBeDefined();
|
|
2608
|
+
await ui.unmount();
|
|
2609
|
+
});
|
|
2610
|
+
|
|
2611
|
+
test('SESS-1 the mod says the session is still there every minute', async ($, on) => {
|
|
2612
|
+
const w = project();
|
|
2613
|
+
w.next = { step: 'wait', args: '', then: [] };
|
|
2614
|
+
const clock = await start($, on, w);
|
|
2615
|
+
await clock.advance(64000);
|
|
2616
|
+
expect(w.acts).toContainEqual(['sessions', 'beat', '--session', 's1']);
|
|
2617
|
+
});
|
|
2618
|
+
|
|
2498
2619
|
test("LOOP-10 a new session leaves a live lead session's pause alone", async ($, on) => {
|
|
2499
2620
|
const w = project();
|
|
2500
2621
|
w.loopPaused = true;
|
|
@@ -2783,7 +2904,7 @@ test('LOOP-11 a turn the person stopped holds the loop, with no dialog, until th
|
|
|
2783
2904
|
expect(w.prompts.at(-1)).toBe('Review ST-4: run /code-kit:review ST-4.');
|
|
2784
2905
|
});
|
|
2785
2906
|
|
|
2786
|
-
test("LOOP-11 a step the lead didn't take
|
|
2907
|
+
test("LOOP-11 and WHO-4 a step the lead didn't take is a note with its reason; Ask again prompts it, and a new step goes on", async ($, on) => {
|
|
2787
2908
|
const w = project();
|
|
2788
2909
|
w.next = { step: 'merge', args: 'lead/st-1', then: [] };
|
|
2789
2910
|
w.sideReply = unread;
|
|
@@ -2804,8 +2925,8 @@ test("LOOP-11 a step the lead didn't take waits with its reason; Ask again promp
|
|
|
2804
2925
|
await leadTurn($, 't-3');
|
|
2805
2926
|
await settle(clock);
|
|
2806
2927
|
expect(w.prompts).toHaveLength(1);
|
|
2807
|
-
|
|
2808
|
-
expect(w.asked.
|
|
2928
|
+
// WHO-4: it's a note, not a decision: nothing is asked in the dialog.
|
|
2929
|
+
expect(w.asked.some((a: any) => /didn't take it/.test(a.question))).toBe(false);
|
|
2809
2930
|
const ui = await bandUi($);
|
|
2810
2931
|
expect(
|
|
2811
2932
|
await ui.find({
|
|
@@ -2900,6 +3021,56 @@ test("DECIDE-4 the lead's question is asked in the dialog: an option answers it,
|
|
|
2900
3021
|
await ui.unmount();
|
|
2901
3022
|
});
|
|
2902
3023
|
|
|
3024
|
+
test("OWN-1 a question another session's lead asked isn't shown, asked or answered here: only a count", async ($, on) => {
|
|
3025
|
+
const w = project();
|
|
3026
|
+
const theirs = { ...question(), id: 'Q-2', session: 's-other' };
|
|
3027
|
+
w.questions = [theirs];
|
|
3028
|
+
w.decisions = { mine: [], elsewhere: [{ session: 's-other', member: 'kim/s-other', count: 1 }] };
|
|
3029
|
+
w.next = { step: 'wait', args: '', then: [] };
|
|
3030
|
+
const clock = await start($, on, w);
|
|
3031
|
+
await clock.advance(4000);
|
|
3032
|
+
expect(w.asked).toEqual([]);
|
|
3033
|
+
expect(w.notified).toEqual([]);
|
|
3034
|
+
const ui = await bandUi($);
|
|
3035
|
+
expect(await ui.find({ type: 'Text', text: /Which weather provider/ })).toBeUndefined();
|
|
3036
|
+
expect(
|
|
3037
|
+
await ui.find({ type: 'Text', text: '1 decision in session kim/s-other, answered there' }),
|
|
3038
|
+
).toBeDefined();
|
|
3039
|
+
await ui.unmount();
|
|
3040
|
+
});
|
|
3041
|
+
|
|
3042
|
+
test("OWN-1 and OWN-5 this session's own question is asked in its dialog, and the answer goes to its own lead", async ($, on) => {
|
|
3043
|
+
const w = project();
|
|
3044
|
+
const mine = { ...question(), session: 's1' };
|
|
3045
|
+
w.questions = [mine];
|
|
3046
|
+
w.decisions = {
|
|
3047
|
+
mine: [{ id: mine.id, kind: 'answer', session: 's1', source: mine }],
|
|
3048
|
+
elsewhere: [],
|
|
3049
|
+
};
|
|
3050
|
+
w.next = { step: 'wait', args: '', then: [] };
|
|
3051
|
+
const clock = await start($, on, w);
|
|
3052
|
+
await clock.advance(4000);
|
|
3053
|
+
expect(w.asked).toHaveLength(1);
|
|
3054
|
+
expect(w.asked[0].question).toMatch(/^Which weather provider/);
|
|
3055
|
+
});
|
|
3056
|
+
|
|
3057
|
+
test('OWN-5 a working session asks its own question in its dialog, though another session leads', async ($, on) => {
|
|
3058
|
+
const w = project();
|
|
3059
|
+
w.role = 'working';
|
|
3060
|
+
w.leadSession = 's-lead';
|
|
3061
|
+
const mine = { ...question(), session: 's1' };
|
|
3062
|
+
w.questions = [mine];
|
|
3063
|
+
w.decisions = {
|
|
3064
|
+
mine: [{ id: mine.id, kind: 'answer', session: 's1', source: mine }],
|
|
3065
|
+
elsewhere: [],
|
|
3066
|
+
};
|
|
3067
|
+
w.next = { step: 'wait', args: '', then: [] };
|
|
3068
|
+
const clock = await start($, on, w);
|
|
3069
|
+
await clock.advance(4000);
|
|
3070
|
+
expect(w.prompts).toEqual([]);
|
|
3071
|
+
expect(w.asked).toHaveLength(1);
|
|
3072
|
+
});
|
|
3073
|
+
|
|
2903
3074
|
test("DECIDE-4 an option chosen in the dialog, or the person's own words typed there, answer the lead's question", async ($, on) => {
|
|
2904
3075
|
const w = project();
|
|
2905
3076
|
w.questions = [question()];
|
|
@@ -3000,6 +3171,10 @@ test('REVW-1 with the reviewer on, a finished branch has the lead start one in t
|
|
|
3000
3171
|
name: 'reviewer',
|
|
3001
3172
|
tools: ['Read', 'Grep', 'Glob', 'Bash'],
|
|
3002
3173
|
});
|
|
3174
|
+
// STD-4: it checks the changed files against the project's standards, grading what each costs.
|
|
3175
|
+
expect(w.registered[0].prompt).toMatch(
|
|
3176
|
+
/standards --json --for <changed files>.*A broken required standard is a blocker, a broken recommended one a concern, advice at most a nit; cite the standard's id as the rule/,
|
|
3177
|
+
);
|
|
3003
3178
|
await leadTurn($);
|
|
3004
3179
|
await settle(clock);
|
|
3005
3180
|
expect(w.prompts[0]).toMatch(
|
package/hooks/mod/review.mjs
CHANGED
|
@@ -22,6 +22,7 @@ export function reviewerSpec({ cli, checklist, model }) {
|
|
|
22
22
|
`1. Run \`node "${cli}" verify --branch <branch>\` from the branch's worktree if it has one (\`git worktree list\`), otherwise in a throwaway worktree (\`git worktree add .claude/worktrees/reviewer-<n> <branch>\`, removed when you're done). A failing verify is always a blocker.`,
|
|
23
23
|
`2. Read ${checklist} and apply its review checklist to the branch's changes against its base, and to the story's requirements. Skip its steps that act (recording a send-back, merging): those are the lead's.`,
|
|
24
24
|
"3. Run the project's test commands that cover the changed files.",
|
|
25
|
+
`4. Check the changed files against the project's standards for their layers: \`node "${cli}" standards --json --for <changed files>\`. Look for breaches of each one marked checked (an enforced one, the hooks already refuse). A broken required standard is a blocker, a broken recommended one a concern, advice at most a nit; cite the standard's id as the rule.`,
|
|
25
26
|
'',
|
|
26
27
|
'Grade each finding: blocker (it must not merge as it is: a failing check, a requirement not met, a rule broken), concern (worth fixing; the lead decides), nit (small). Say where (path:line), what is wrong and what fixed looks like, and the requirement or rule it cites.',
|
|
27
28
|
'',
|