@warren-dean/code-kit 0.13.0 → 0.14.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 +10 -0
- package/README.md +7 -2
- package/bin/code-kit.mjs +26 -6
- package/hooks/guard-bash.mjs +5 -0
- package/hooks/lib/loop-state.mjs +49 -9
- package/hooks/mod/decisions.mjs +75 -0
- package/hooks/mod/loop.mjs +74 -0
- package/hooks/mod/mission.mjs +19 -15
- package/hooks/mod/register.mjs +283 -14
- package/hooks/mod/register.test.ts +335 -8
- package/hooks/mod/view.mjs +66 -4
- package/package.json +1 -1
|
@@ -152,6 +152,10 @@ function project() {
|
|
|
152
152
|
} as any,
|
|
153
153
|
sent: [] as { to: any; text: string }[],
|
|
154
154
|
loopPaused: false, // the pause recorded by `code-kit loop`
|
|
155
|
+
leadSession: null as string | null, // LOOP-10: the session holding the lead lease
|
|
156
|
+
leadClaims: 0,
|
|
157
|
+
asked: [] as { question: string; options: string[]; header?: string }[], // dialogs the mod opened
|
|
158
|
+
typed: null as string | null, // what the person types under the dialog's Other, if anything
|
|
155
159
|
map: null as any, // `code-kit map <story> --json`
|
|
156
160
|
trail: null as any, // `code-kit trail --json`
|
|
157
161
|
journal: { events: [] as any[] }, // `code-kit journal --json`
|
|
@@ -244,7 +248,28 @@ function stub(on: any, w: World) {
|
|
|
244
248
|
w.loopPaused = argv[3] === 'pause';
|
|
245
249
|
return ran(0, '');
|
|
246
250
|
}
|
|
247
|
-
if (sub === 'loop'
|
|
251
|
+
if (sub === 'loop' && argv[3] === 'lead') {
|
|
252
|
+
const session = argv[argv.indexOf('--session') + 1];
|
|
253
|
+
if (w.leadSession && w.leadSession !== session && !argv.includes('--take'))
|
|
254
|
+
return ran(1, '', `Another session (${w.leadSession}) runs the lead loop in this project.`);
|
|
255
|
+
w.leadSession = session;
|
|
256
|
+
w.leadClaims += 1;
|
|
257
|
+
return ran(0, 'This session runs the lead loop.');
|
|
258
|
+
}
|
|
259
|
+
if (sub === 'loop' && argv[3] === 'release') {
|
|
260
|
+
w.acts.push([...argv.slice(2)]);
|
|
261
|
+
if (w.leadSession === argv[argv.indexOf('--session') + 1]) w.leadSession = null;
|
|
262
|
+
return ran(0, '');
|
|
263
|
+
}
|
|
264
|
+
if (sub === 'loop')
|
|
265
|
+
return ran(
|
|
266
|
+
0,
|
|
267
|
+
JSON.stringify({
|
|
268
|
+
paused: w.loopPaused,
|
|
269
|
+
at: null,
|
|
270
|
+
lead: w.leadSession ? { session: w.leadSession, at: '2026-10-16T00:00:00.000Z' } : null,
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
248
273
|
if (sub === 'sent-back') {
|
|
249
274
|
w.acts.push([...argv.slice(2)]);
|
|
250
275
|
return ran(0, `Recorded: ${argv[3]} was sent back.`);
|
|
@@ -371,6 +396,7 @@ function stub(on: any, w: World) {
|
|
|
371
396
|
});
|
|
372
397
|
on('turn.start', ($: any, e: any) => e);
|
|
373
398
|
on('turn.complete', () => ({ text: '' }));
|
|
399
|
+
on('session.end', ($: any, e: any) => ({ sessionId: e.sessionId }));
|
|
374
400
|
on('session.usage', () => ({ value: { startedAt: 0, context: {}, rateLimits: w.limits } }));
|
|
375
401
|
on('session.measure', ($: any, e: any) => ({ changed: e.changed }));
|
|
376
402
|
// A model request, answered with the usage the test gave it.
|
|
@@ -397,10 +423,16 @@ function stub(on: any, w: World) {
|
|
|
397
423
|
if (w.leadBusy) return new Promise(() => {});
|
|
398
424
|
return { text: e.text };
|
|
399
425
|
});
|
|
400
|
-
// $.ui.ask is the AskUserQuestion dialog: the person picks
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
426
|
+
// $.ui.ask is the AskUserQuestion dialog: the person picks w.answer where it's one of its labels, types
|
|
427
|
+
// w.typed under Other, or else dismisses it.
|
|
428
|
+
on('tool.call', { tool: 'AskUserQuestion' }, ($: any, e: any) => {
|
|
429
|
+
const q = e.questions[0];
|
|
430
|
+
const labels = (q.options ?? []).map((o: any) => o.label);
|
|
431
|
+
w.asked.push({ question: q.question, options: labels, header: q.header });
|
|
432
|
+
const answer = labels.includes(w.answer) ? w.answer : w.typed;
|
|
433
|
+
if (answer == null) return { deny: 'The person dismissed the question.' };
|
|
434
|
+
return { result: { questions: e.questions, answers: { [q.question]: answer } } };
|
|
435
|
+
});
|
|
404
436
|
// Another plugin's command, as the mod asks for it (Context Graph's /graph).
|
|
405
437
|
on('command.run', ($: any, e: any) => {
|
|
406
438
|
w.commands.push({ command: e.command, args: e.args });
|
|
@@ -559,10 +591,10 @@ test('PANE-4 with nothing changed it reads the project every 10 seconds while th
|
|
|
559
591
|
const before = w.runs;
|
|
560
592
|
await clock.advance(8000); // four quiet ticks: git only
|
|
561
593
|
expect(w.runs - before).toBe(4);
|
|
562
|
-
// The fifth reads it all (check, status, next, requests, stops, queue, timeline, the lead's questions
|
|
563
|
-
// and the
|
|
594
|
+
// The fifth reads it all (check, status, next, requests, stops, queue, timeline, the lead's questions,
|
|
595
|
+
// the reports waiting, and which session runs the loop, while this one doesn't) between two looks at git.
|
|
564
596
|
await clock.advance(2000);
|
|
565
|
-
expect(w.runs - before).toBe(
|
|
597
|
+
expect(w.runs - before).toBe(16);
|
|
566
598
|
});
|
|
567
599
|
|
|
568
600
|
test('CARD-4 outside a code-kit project, /lanes says so, opens nothing, and there is no band', async ($, on) => {
|
|
@@ -2295,6 +2327,301 @@ test('LOOP-7 when every story is done the loop stops, and the band says so until
|
|
|
2295
2327
|
await ui.unmount();
|
|
2296
2328
|
});
|
|
2297
2329
|
|
|
2330
|
+
/** The lead's turn, ending with what it said; `isAborted` when the person stopped it. */
|
|
2331
|
+
async function leadSays($: any, id: string, answer: string, isAborted = false) {
|
|
2332
|
+
await $.turn.start({ turnId: id, text: '' } as any);
|
|
2333
|
+
await $.turn.complete({
|
|
2334
|
+
turnId: id,
|
|
2335
|
+
answer,
|
|
2336
|
+
durationMs: 1,
|
|
2337
|
+
isAborted,
|
|
2338
|
+
reason: isAborted ? 'aborted' : 'answer',
|
|
2339
|
+
} as any);
|
|
2340
|
+
}
|
|
2341
|
+
/** What the person types and sends to the lead. */
|
|
2342
|
+
const personSays = ($: any, text: string) =>
|
|
2343
|
+
$.prompt.submit({ text, origin: { kind: 'composer' }, wait: false } as any);
|
|
2344
|
+
/** The side call's reading of the lead's reply (LOOP-11). */
|
|
2345
|
+
const outcome = (o: any) => ({ isAnswered: true, text: JSON.stringify(o), usage: null });
|
|
2346
|
+
const unread = { isAnswered: true, text: 'Nothing to classify.', usage: null };
|
|
2347
|
+
|
|
2348
|
+
test('LOOP-10 a second session opened in the project never prompts its lead; Lead from here moves the loop to it', async ($, on) => {
|
|
2349
|
+
const w = project();
|
|
2350
|
+
w.leadSession = 's-other'; // a working session already runs the loop
|
|
2351
|
+
const clock = await start($, on, w);
|
|
2352
|
+
await leadTurn($);
|
|
2353
|
+
await settle(clock);
|
|
2354
|
+
await clock.advance(4000);
|
|
2355
|
+
expect(w.prompts).toEqual([]);
|
|
2356
|
+
const ui = await bandUi($);
|
|
2357
|
+
expect(await ui.find({ type: 'Text', text: 'the loop runs in another session' })).toBeDefined();
|
|
2358
|
+
await press($, 'band-leadHere');
|
|
2359
|
+
await ui.unmount();
|
|
2360
|
+
expect(w.leadSession).toBe('s1');
|
|
2361
|
+
expect(w.prompts).toEqual(['Dispatch ST-9: run /code-kit:dispatch ST-9.']);
|
|
2362
|
+
});
|
|
2363
|
+
|
|
2364
|
+
test("LOOP-10 a new session leaves a live lead session's pause alone", async ($, on) => {
|
|
2365
|
+
const w = project();
|
|
2366
|
+
w.loopPaused = true;
|
|
2367
|
+
w.leadSession = 's-other';
|
|
2368
|
+
await start($, on, w);
|
|
2369
|
+
expect(w.acts).not.toContainEqual(['loop', 'resume']);
|
|
2370
|
+
expect(w.loopPaused).toBe(true);
|
|
2371
|
+
});
|
|
2372
|
+
|
|
2373
|
+
test('LOOP-10 the lead session renews its lease while it runs, and releases it as it ends', async ($, on) => {
|
|
2374
|
+
const w = project();
|
|
2375
|
+
const clock = await start($, on, w);
|
|
2376
|
+
await leadTurn($);
|
|
2377
|
+
await settle(clock);
|
|
2378
|
+
expect(w.leadSession).toBe('s1');
|
|
2379
|
+
const claims = w.leadClaims;
|
|
2380
|
+
await clock.advance(32000);
|
|
2381
|
+
expect(w.leadClaims).toBeGreaterThan(claims);
|
|
2382
|
+
await $.session.end({ reason: 'prompt_input_exit', sessionId: 's1', resume: {} } as any);
|
|
2383
|
+
expect(w.acts).toContainEqual(['loop', 'release', '--session', 's1']);
|
|
2384
|
+
expect(w.leadSession).toBe(null);
|
|
2385
|
+
});
|
|
2386
|
+
|
|
2387
|
+
test("LOOP-11 a reply that waits on the person holds the loop; the dialog asks the lead's question and the answer goes to the lead", async ($, on) => {
|
|
2388
|
+
const w = project();
|
|
2389
|
+
w.sideReply = unread;
|
|
2390
|
+
const clock = await start($, on, w);
|
|
2391
|
+
await leadTurn($);
|
|
2392
|
+
await settle(clock);
|
|
2393
|
+
expect(w.prompts).toEqual(['Dispatch ST-9: run /code-kit:dispatch ST-9.']);
|
|
2394
|
+
w.sideReply = outcome({
|
|
2395
|
+
outcome: 'needs-person',
|
|
2396
|
+
question: 'Should ST-9 show prices with GST?',
|
|
2397
|
+
options: [
|
|
2398
|
+
{ label: 'Include GST', effect: 'Every price shows GST.' },
|
|
2399
|
+
{ label: 'Exclude GST', effect: 'GST is added at checkout.' },
|
|
2400
|
+
],
|
|
2401
|
+
reason: 'a pricing rule only the person can set',
|
|
2402
|
+
});
|
|
2403
|
+
w.next = { step: 'review', args: 'ST-4', then: [] };
|
|
2404
|
+
w.answer = 'Include GST';
|
|
2405
|
+
await leadSays($, 't-2', 'Before I dispatch ST-9: should its prices include GST?');
|
|
2406
|
+
await settle(clock);
|
|
2407
|
+
// The side call read the reply over the lead's own conversation, with the step it answered.
|
|
2408
|
+
expect(w.sideAsks.at(-1).fork).toBe(true);
|
|
2409
|
+
expect(w.sideAsks.at(-1).prompt).toMatch(/prompted you: "Dispatch ST-9/);
|
|
2410
|
+
expect(w.sideAsks.at(-1).prompt).toMatch(/should its prices include GST\?/);
|
|
2411
|
+
expect(w.asked.at(-1)).toEqual({
|
|
2412
|
+
question: 'Should ST-9 show prices with GST?',
|
|
2413
|
+
options: ['Include GST', 'Exclude GST'],
|
|
2414
|
+
header: 'the lead',
|
|
2415
|
+
});
|
|
2416
|
+
expect(w.prompts.slice(1)).toEqual([
|
|
2417
|
+
'The person answered what you asked ("Should ST-9 show prices with GST?"): Include GST. Carry on with it.',
|
|
2418
|
+
]);
|
|
2419
|
+
});
|
|
2420
|
+
|
|
2421
|
+
test('LOOP-11 dismissed, what the lead waits on stays in the band, and the loop prompts again only once the person has spoken', async ($, on) => {
|
|
2422
|
+
const w = project();
|
|
2423
|
+
w.sideReply = unread;
|
|
2424
|
+
const clock = await start($, on, w);
|
|
2425
|
+
await leadTurn($);
|
|
2426
|
+
await settle(clock);
|
|
2427
|
+
w.sideReply = outcome({
|
|
2428
|
+
outcome: 'needs-person',
|
|
2429
|
+
question: 'Which plan names should the pricing page use?',
|
|
2430
|
+
options: [
|
|
2431
|
+
{ label: 'Basic and Pro', effect: 'Two plans.' },
|
|
2432
|
+
{ label: 'Starter, Team, Business', effect: 'Three plans.' },
|
|
2433
|
+
],
|
|
2434
|
+
});
|
|
2435
|
+
w.next = { step: 'review', args: 'ST-4', then: [] };
|
|
2436
|
+
await leadSays($, 't-2', 'Which plan names do you want on the pricing page?');
|
|
2437
|
+
await settle(clock);
|
|
2438
|
+
await clock.advance(6000);
|
|
2439
|
+
expect(w.prompts).toHaveLength(1);
|
|
2440
|
+
expect(w.asked).toHaveLength(1); // asked once, and dismissed: not asked again
|
|
2441
|
+
const ui = await bandUi($);
|
|
2442
|
+
expect(
|
|
2443
|
+
await ui.find({
|
|
2444
|
+
type: 'Text',
|
|
2445
|
+
text: 'The lead waits on you: Which plan names should the pricing page use?',
|
|
2446
|
+
}),
|
|
2447
|
+
).toBeDefined();
|
|
2448
|
+
await ui.unmount();
|
|
2449
|
+
// The person answers in the chat; the lead's reply to them doesn't wait on them.
|
|
2450
|
+
await personSays($, 'Basic and Pro.');
|
|
2451
|
+
w.sideReply = outcome({ outcome: 'done' });
|
|
2452
|
+
await leadSays($, 't-3', 'Updated the plan names.');
|
|
2453
|
+
await settle(clock);
|
|
2454
|
+
expect(w.prompts.at(-1)).toBe('Review ST-4: run /code-kit:review ST-4.');
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2457
|
+
test('LOOP-11 a turn the person stopped holds the loop, with no dialog, until they speak or press Carry on', async ($, on) => {
|
|
2458
|
+
const w = project();
|
|
2459
|
+
w.sideReply = unread;
|
|
2460
|
+
const clock = await start($, on, w);
|
|
2461
|
+
await leadTurn($);
|
|
2462
|
+
await settle(clock);
|
|
2463
|
+
w.next = { step: 'review', args: 'ST-4', then: [] };
|
|
2464
|
+
await leadSays($, 't-2', '', true);
|
|
2465
|
+
await settle(clock);
|
|
2466
|
+
await clock.advance(4000);
|
|
2467
|
+
expect(w.prompts).toHaveLength(1);
|
|
2468
|
+
expect(w.asked).toEqual([]);
|
|
2469
|
+
const ui = await bandUi($);
|
|
2470
|
+
expect(
|
|
2471
|
+
await ui.find({ type: 'Text', text: "You stopped the lead's turn: the loop waits for you" }),
|
|
2472
|
+
).toBeDefined();
|
|
2473
|
+
await press($, 'band-carryOn');
|
|
2474
|
+
await ui.unmount();
|
|
2475
|
+
expect(w.prompts.at(-1)).toBe('Review ST-4: run /code-kit:review ST-4.');
|
|
2476
|
+
});
|
|
2477
|
+
|
|
2478
|
+
test("LOOP-11 a step the lead didn't take waits with its reason; Ask again prompts it, and a new step goes on", async ($, on) => {
|
|
2479
|
+
const w = project();
|
|
2480
|
+
w.next = { step: 'merge', args: 'lead/st-1', then: [] };
|
|
2481
|
+
w.sideReply = unread;
|
|
2482
|
+
const clock = await start($, on, w);
|
|
2483
|
+
await leadTurn($);
|
|
2484
|
+
await settle(clock);
|
|
2485
|
+
expect(w.prompts).toHaveLength(1);
|
|
2486
|
+
w.sideReply = outcome({
|
|
2487
|
+
outcome: 'blocked',
|
|
2488
|
+
reason: 'main has a failing check that lead/st-1 does not touch',
|
|
2489
|
+
});
|
|
2490
|
+
w.refs += ' c1 refs/heads/core/st-1\n';
|
|
2491
|
+
await leadSays($, 't-2', "I can't merge lead/st-1 while main's checks fail.");
|
|
2492
|
+
await settle(clock);
|
|
2493
|
+
// The person's own turn, and lanes committing, don't make the step worth prompting again.
|
|
2494
|
+
w.sideReply = unread;
|
|
2495
|
+
w.refs += ' c2 refs/heads/core/st-2\n';
|
|
2496
|
+
await leadTurn($, 't-3');
|
|
2497
|
+
await settle(clock);
|
|
2498
|
+
expect(w.prompts).toHaveLength(1);
|
|
2499
|
+
expect(w.asked.at(-1)?.options).toEqual(['Ask again', 'Not now']);
|
|
2500
|
+
expect(w.asked.at(-1)?.question).toMatch(/the lead didn't take it.*Ask the lead again\?$/);
|
|
2501
|
+
const ui = await bandUi($);
|
|
2502
|
+
expect(
|
|
2503
|
+
await ui.find({
|
|
2504
|
+
type: 'Text',
|
|
2505
|
+
text: "Merge lead/st-1: the lead didn't take it: main has a failing check that lead/st-1 does not touch",
|
|
2506
|
+
}),
|
|
2507
|
+
).toBeDefined();
|
|
2508
|
+
await press($, 'band-askAgain');
|
|
2509
|
+
await ui.unmount();
|
|
2510
|
+
expect(w.prompts).toHaveLength(2);
|
|
2511
|
+
expect(w.prompts[1]).toMatch(/^Merge lead\/st-1/);
|
|
2512
|
+
// A new step isn't held by the old one.
|
|
2513
|
+
w.sideReply = outcome({ outcome: 'blocked', reason: 'still failing' });
|
|
2514
|
+
await leadSays($, 't-4', 'Still failing.');
|
|
2515
|
+
await settle(clock);
|
|
2516
|
+
w.next = { step: 'dispatch', args: 'ST-9', then: [] };
|
|
2517
|
+
w.sideReply = unread;
|
|
2518
|
+
await leadTurn($, 't-5');
|
|
2519
|
+
await settle(clock);
|
|
2520
|
+
expect(w.prompts.at(-1)).toBe('Dispatch ST-9: run /code-kit:dispatch ST-9.');
|
|
2521
|
+
});
|
|
2522
|
+
|
|
2523
|
+
test('LOOP-12 a design story is handed to the lead once; the loop leaves designing it to the person and the lead', async ($, on) => {
|
|
2524
|
+
const w = project();
|
|
2525
|
+
w.next = { step: 'design', args: 'ST-21 ST-22', then: [] };
|
|
2526
|
+
w.sideReply = unread;
|
|
2527
|
+
const clock = await start($, on, w);
|
|
2528
|
+
await leadTurn($);
|
|
2529
|
+
await settle(clock);
|
|
2530
|
+
expect(w.prompts).toEqual([
|
|
2531
|
+
'Design ST-21, ST-22 before any of their screens are built: run /code-kit:design ST-21 ST-22.',
|
|
2532
|
+
]);
|
|
2533
|
+
// The person and the lead go back and forth; ST-21's approved design changes the project.
|
|
2534
|
+
for (const turn of ['t-2', 't-3']) {
|
|
2535
|
+
await personSays($, 'Make the header calmer.');
|
|
2536
|
+
w.refs += ` ${turn} refs/heads/design/${turn}\n`;
|
|
2537
|
+
await leadTurn($, turn);
|
|
2538
|
+
await settle(clock);
|
|
2539
|
+
}
|
|
2540
|
+
expect(w.prompts.filter((p: string) => p.startsWith('Design'))).toHaveLength(1);
|
|
2541
|
+
const ui = await bandUi($);
|
|
2542
|
+
expect(
|
|
2543
|
+
await ui.find({
|
|
2544
|
+
type: 'Text',
|
|
2545
|
+
text: "Design ST-21, ST-22: with you and the lead, so the loop doesn't prompt it again",
|
|
2546
|
+
}),
|
|
2547
|
+
).toBeDefined();
|
|
2548
|
+
await ui.unmount();
|
|
2549
|
+
// A design story not handed over yet is.
|
|
2550
|
+
w.next = { step: 'design', args: 'ST-22 ST-30', then: [] };
|
|
2551
|
+
await leadTurn($, 't-4');
|
|
2552
|
+
await settle(clock);
|
|
2553
|
+
expect(w.prompts.at(-1)).toBe(
|
|
2554
|
+
'Design ST-22, ST-30 before any of their screens are built: run /code-kit:design ST-22 ST-30.',
|
|
2555
|
+
);
|
|
2556
|
+
});
|
|
2557
|
+
|
|
2558
|
+
test("DECIDE-4 the loop's first offer is asked in Claude Code's own dialog; Keep going records it and takes the step", async ($, on) => {
|
|
2559
|
+
const w = project();
|
|
2560
|
+
(w.check as any).harnessSet = [];
|
|
2561
|
+
w.sideReply = unread;
|
|
2562
|
+
w.answer = 'Keep going';
|
|
2563
|
+
const clock = await start($, on, w);
|
|
2564
|
+
await leadTurn($);
|
|
2565
|
+
await settle(clock);
|
|
2566
|
+
expect(w.asked[0].options).toEqual(['Keep going', 'Ask each time', 'Off']);
|
|
2567
|
+
expect(w.asked[0].question).toMatch(
|
|
2568
|
+
/^code-kit can keep the build moving between your prompts\. Next: Dispatch ST-9\. How much should it do on its own\?$/,
|
|
2569
|
+
);
|
|
2570
|
+
expect(w.acts.some((a) => a.join(' ').startsWith('settings set autonomy autonomous'))).toBe(true);
|
|
2571
|
+
expect(w.prompts).toEqual(['Dispatch ST-9: run /code-kit:dispatch ST-9.']);
|
|
2572
|
+
});
|
|
2573
|
+
|
|
2574
|
+
test("DECIDE-4 the lead's question is asked in the dialog: an option answers it, and a dismissed one isn't asked again", async ($, on) => {
|
|
2575
|
+
const w = project();
|
|
2576
|
+
w.questions = [question()];
|
|
2577
|
+
w.next = { step: 'wait', args: '', then: [] };
|
|
2578
|
+
const clock = await start($, on, w);
|
|
2579
|
+
await clock.advance(4000);
|
|
2580
|
+
expect(w.asked).toHaveLength(1);
|
|
2581
|
+
expect(w.asked[0]).toEqual({
|
|
2582
|
+
question: 'Which weather provider: the Bureau of Meteorology or a commercial service?',
|
|
2583
|
+
options: ['Bureau of Meteorology', 'Commercial'],
|
|
2584
|
+
header: 'question',
|
|
2585
|
+
});
|
|
2586
|
+
await clock.advance(6000);
|
|
2587
|
+
expect(w.asked).toHaveLength(1);
|
|
2588
|
+
const ui = await bandUi($);
|
|
2589
|
+
expect(
|
|
2590
|
+
await ui.find({ type: 'Text', text: /^The lead asks: Which weather provider/ }),
|
|
2591
|
+
).toBeDefined();
|
|
2592
|
+
await ui.unmount();
|
|
2593
|
+
});
|
|
2594
|
+
|
|
2595
|
+
test("DECIDE-4 an option chosen in the dialog, or the person's own words typed there, answer the lead's question", async ($, on) => {
|
|
2596
|
+
const w = project();
|
|
2597
|
+
w.questions = [question()];
|
|
2598
|
+
w.next = { step: 'wait', args: '', then: [] };
|
|
2599
|
+
w.typed = 'Both, the Bureau first';
|
|
2600
|
+
const clock = await start($, on, w);
|
|
2601
|
+
await clock.advance(4000);
|
|
2602
|
+
expect(w.acts).toContainEqual([
|
|
2603
|
+
'ask',
|
|
2604
|
+
'answer',
|
|
2605
|
+
'Q-1',
|
|
2606
|
+
'Both, the Bureau first',
|
|
2607
|
+
'--via',
|
|
2608
|
+
'pane',
|
|
2609
|
+
]);
|
|
2610
|
+
expect(w.prompts.at(-1)).toMatch(
|
|
2611
|
+
/^The person answered Q-1 .*: Both, the Bureau first\. ST-4 no longer waits/,
|
|
2612
|
+
);
|
|
2613
|
+
});
|
|
2614
|
+
|
|
2615
|
+
test('DECIDE-4 nothing is asked in a session another one runs the loop for', async ($, on) => {
|
|
2616
|
+
const w = project();
|
|
2617
|
+
w.leadSession = 's-other';
|
|
2618
|
+
w.questions = [question()];
|
|
2619
|
+
w.next = { step: 'wait', args: '', then: [] };
|
|
2620
|
+
const clock = await start($, on, w);
|
|
2621
|
+
await clock.advance(6000);
|
|
2622
|
+
expect(w.asked).toEqual([]);
|
|
2623
|
+
});
|
|
2624
|
+
|
|
2298
2625
|
// --- the reviewer -----------------------------------------------------------------------------------
|
|
2299
2626
|
|
|
2300
2627
|
/** ST-4 finished on web/st-4, the reviewer on. */
|
package/hooks/mod/view.mjs
CHANGED
|
@@ -788,6 +788,7 @@ export function bandLines({
|
|
|
788
788
|
lines.push({
|
|
789
789
|
kind: 'proposal',
|
|
790
790
|
text: `code-kit can keep the build moving between your prompts. Next: ${loop.proposal.label}.`,
|
|
791
|
+
proposal: loop.proposal,
|
|
791
792
|
actions: [
|
|
792
793
|
{ id: 'keepGoing', label: 'Keep going' },
|
|
793
794
|
{ id: 'askEach', label: 'Ask each time' },
|
|
@@ -801,8 +802,12 @@ export function bandLines({
|
|
|
801
802
|
text: loop.proposal.repeated
|
|
802
803
|
? `${loop.proposal.label}: still next after ${loop.proposal.repeated} prompts. Ask the lead again?`
|
|
803
804
|
: `${loop.proposal.label}?`,
|
|
804
|
-
|
|
805
|
+
proposal: loop.proposal,
|
|
806
|
+
actions: [{ id: 'go', label: loop.proposal.repeated ? 'Ask again' : 'Go' }],
|
|
805
807
|
});
|
|
808
|
+
// LOOP-11, LOOP-12: what the loop read in the lead's reply holds it until the person says.
|
|
809
|
+
const waitingLine = waitingOf(loop?.waiting);
|
|
810
|
+
if (waitingLine) lines.push(waitingLine);
|
|
806
811
|
// A held call's request is asked on its own line above, not again here.
|
|
807
812
|
// CARD-2: counted as decisions, not files: one approval for a lane covers every file it was asked for.
|
|
808
813
|
const asks = approvalAsks((requests?.open ?? []).filter((r) => !held.some((h) => sameAsk(h, r))));
|
|
@@ -915,7 +920,14 @@ export function bandLines({
|
|
|
915
920
|
// With the mission rows drawn, the loop's state and its Pause or Resume are on the `now` row.
|
|
916
921
|
else if (mission.length) {
|
|
917
922
|
// nothing more
|
|
918
|
-
} else if (loop?.state === '
|
|
923
|
+
} else if (loop?.state === 'elsewhere')
|
|
924
|
+
// LOOP-10: another session in this project runs the loop; this one never prompts its lead.
|
|
925
|
+
lines.push({
|
|
926
|
+
kind: 'loop',
|
|
927
|
+
text: 'the loop runs in another session',
|
|
928
|
+
actions: [{ id: 'leadHere', label: 'Lead from here' }],
|
|
929
|
+
});
|
|
930
|
+
else if (loop?.state === 'paused')
|
|
919
931
|
lines.push({ kind: 'loop', text: 'loop paused', actions: [{ id: 'resume', label: 'Resume' }] });
|
|
920
932
|
else if (loop?.state === 'running' && loop.started)
|
|
921
933
|
lines.push({
|
|
@@ -930,6 +942,48 @@ export function bandLines({
|
|
|
930
942
|
return lines;
|
|
931
943
|
}
|
|
932
944
|
|
|
945
|
+
/**
|
|
946
|
+
* LOOP-11, LOOP-12: the band's line for what the loop waits on, or null: the lead waiting on the
|
|
947
|
+
* person (its question, when the reply asked one), a turn the person stopped, a step the lead didn't
|
|
948
|
+
* take, or design stories left to the person and the lead.
|
|
949
|
+
*/
|
|
950
|
+
export function waitingOf(waiting) {
|
|
951
|
+
if (!waiting) return null;
|
|
952
|
+
if (waiting.kind === 'person')
|
|
953
|
+
return {
|
|
954
|
+
kind: 'waiting',
|
|
955
|
+
text: waiting.question
|
|
956
|
+
? `The lead waits on you: ${waiting.question}`
|
|
957
|
+
: `The lead waits on you${waiting.reason ? `: ${waiting.reason}` : ''}`,
|
|
958
|
+
waiting,
|
|
959
|
+
actions: waiting.question
|
|
960
|
+
? [{ id: 'openDecision', label: 'Answer…' }]
|
|
961
|
+
: [{ id: 'carryOn', label: 'Carry on' }],
|
|
962
|
+
};
|
|
963
|
+
if (waiting.kind === 'stopped')
|
|
964
|
+
return {
|
|
965
|
+
kind: 'waiting',
|
|
966
|
+
text: "You stopped the lead's turn: the loop waits for you",
|
|
967
|
+
waiting,
|
|
968
|
+
actions: [{ id: 'carryOn', label: 'Carry on' }],
|
|
969
|
+
};
|
|
970
|
+
if (waiting.kind === 'blocked')
|
|
971
|
+
return {
|
|
972
|
+
kind: 'waiting',
|
|
973
|
+
text: `${waiting.label}: the lead didn't take it${waiting.reason ? `: ${waiting.reason}` : ''}`,
|
|
974
|
+
waiting,
|
|
975
|
+
actions: [{ id: 'askAgain', label: 'Ask again' }],
|
|
976
|
+
};
|
|
977
|
+
if (waiting.kind === 'design')
|
|
978
|
+
return {
|
|
979
|
+
kind: 'designing',
|
|
980
|
+
text: `${waiting.label}: with you and the lead, so the loop doesn't prompt it again`,
|
|
981
|
+
waiting,
|
|
982
|
+
actions: [{ id: 'askAgain', label: 'Ask the lead' }],
|
|
983
|
+
};
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
|
|
933
987
|
/** The band, drawn with the elements `$.ui.resolve(e)` returned; `onAction(id, line)` acts. */
|
|
934
988
|
export function band(lines, { Box, Text, Button }, onAction) {
|
|
935
989
|
const COLOR = {
|
|
@@ -938,6 +992,8 @@ export function band(lines, { Box, Text, Button }, onAction) {
|
|
|
938
992
|
queue: 'blue',
|
|
939
993
|
stalled: 'red',
|
|
940
994
|
proposal: 'cyan',
|
|
995
|
+
waiting: 'cyan',
|
|
996
|
+
designing: 'blue',
|
|
941
997
|
loop: 'green',
|
|
942
998
|
done: 'green',
|
|
943
999
|
approvals: 'yellow',
|
|
@@ -1143,7 +1199,11 @@ export function questionAsidePrompt({ q, thread = [], question }) {
|
|
|
1143
1199
|
return [
|
|
1144
1200
|
"The person is deciding how to answer a question in code-kit's Decisions pane, and asks you about it first. Answer in a few short sentences, in plain words: what the question means, why it matters for the build, what the choices are and what each leads to, and what you'd recommend if you know enough to. Use what you know from this conversation, the project's docs you've read, and the facts below. If you don't know, say so; don't guess. This answer goes to that pane, not into the conversation: don't act on it.",
|
|
1145
1201
|
'',
|
|
1146
|
-
q.kind === 'brief'
|
|
1202
|
+
q.kind === 'brief'
|
|
1203
|
+
? `An open question in ${q.path}:`
|
|
1204
|
+
: q.kind === 'lead'
|
|
1205
|
+
? 'What the lead asked in its last reply, and waits on:'
|
|
1206
|
+
: `A question the lead asked (${q.id}):`,
|
|
1147
1207
|
`- ${q.question}`,
|
|
1148
1208
|
...(q.owner ? [`- who decides: ${q.owner}`] : []),
|
|
1149
1209
|
...(q.kind === 'brief' && q.blocks ? [`- what it holds up: ${q.blocks}`] : []),
|
|
@@ -1224,7 +1284,9 @@ export function answerPane(
|
|
|
1224
1284
|
children: [
|
|
1225
1285
|
kind === 'brief'
|
|
1226
1286
|
? `An open question in ${answering.path}. The lead records your answer in the brief's decisions and builds on it.`
|
|
1227
|
-
:
|
|
1287
|
+
: kind === 'lead'
|
|
1288
|
+
? 'From the lead, which waits on it. Your answer is sent to the lead.'
|
|
1289
|
+
: `${answering.id}, from the lead${answering.story ? `, for ${answering.story}` : ''}. Your answer is recorded and sent to the lead.`,
|
|
1228
1290
|
],
|
|
1229
1291
|
}),
|
|
1230
1292
|
...(kind === 'brief' && (answering.owner || answering.blocks)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warren-dean/code-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Keeps AI coding agents inside your project's rules: lanes, layer rules, protected paths and proof before finish, enforced by Claude Code hooks and in CI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Warren Langeveldt",
|