@warren-dean/code-kit 0.21.0 → 0.22.1

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.
Files changed (45) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +20 -0
  4. package/README.md +46 -23
  5. package/bin/code-kit.mjs +271 -21
  6. package/hooks/guard-agent.mjs +36 -0
  7. package/hooks/guard-bash.mjs +23 -11
  8. package/hooks/guard-paths.mjs +11 -0
  9. package/hooks/hooks.json +20 -0
  10. package/hooks/lane-audit.mjs +38 -1
  11. package/hooks/lib/adapters/hub/client.mjs +177 -25
  12. package/hooks/lib/config.mjs +148 -5
  13. package/hooks/lib/decisionstore.mjs +89 -0
  14. package/hooks/lib/harness.mjs +7 -0
  15. package/hooks/lib/hook.mjs +7 -0
  16. package/hooks/lib/instructions.mjs +1 -1
  17. package/hooks/lib/laneclaims.mjs +89 -0
  18. package/hooks/lib/loop-state.mjs +20 -2
  19. package/hooks/lib/next.mjs +34 -1
  20. package/hooks/lib/plan.mjs +3 -1
  21. package/hooks/lib/questions.mjs +23 -5
  22. package/hooks/lib/regression.mjs +2 -1
  23. package/hooks/lib/rules.mjs +18 -12
  24. package/hooks/lib/schedule.mjs +86 -0
  25. package/hooks/lib/sessions.mjs +223 -0
  26. package/hooks/lib/snapshot.mjs +9 -3
  27. package/hooks/mod/decisions.mjs +1 -28
  28. package/hooks/mod/loop.mjs +8 -0
  29. package/hooks/mod/mission.mjs +21 -19
  30. package/hooks/mod/register.mjs +118 -41
  31. package/hooks/mod/register.test.ts +244 -73
  32. package/hooks/mod/view.mjs +98 -47
  33. package/hooks/post-edit-check.mjs +3 -0
  34. package/hooks/session.mjs +24 -0
  35. package/hooks/stop-check.mjs +12 -0
  36. package/package.json +1 -1
  37. package/skills/decide/SKILL.md +1 -1
  38. package/skills/dispatch/SKILL.md +2 -2
  39. package/skills/init/SKILL.md +1 -1
  40. package/skills/lead/SKILL.md +28 -0
  41. package/skills/next/SKILL.md +1 -1
  42. package/skills/spec-design/SKILL.md +2 -1
  43. package/skills/team-init/SKILL.md +1 -1
  44. package/templates/claude-md.md +3 -1
  45. 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
- expect(w.runs - before).toBe(16);
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("PERM-1 a call Claude Code's own permission check refused waits for the person, who runs it or says not now", async ($, on) => {
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
- await $.classic.PermissionDenied({
1715
- tool_name: 'Bash',
1716
- tool_input: { command: 'pnpm db:generate && pnpm db:migrate' },
1717
- tool_use_id: 'tu-1',
1718
- reason: 'Permission for this action was denied by the Claude Code auto mode classifier.',
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
- await band.find({ type: 'Text', text: "Claude Code refused the lead's `pnpm db:generate`" }),
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
- await ui.find({
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 twice and that is still next goes to the person, while lanes keep committing', async ($, on) => {
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(2);
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 2 prompts. Ask the lead again?',
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(3);
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-5');
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 flagged after 2 restarts',
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
- expect(w.prompts).toHaveLength(2);
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(await band.find({ type: 'Text', text: 'web stalled 3 times on ST-4' })).toBeDefined();
2318
- expect(await band.find({ key: 'band-resumeStall' })).toBeDefined();
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 loop runs in another session' })).toBeDefined();
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 waits with its reason; Ask again prompts it, and a new step goes on", async ($, on) => {
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
- expect(w.asked.at(-1)?.options).toEqual(['Ask again', 'Not now']);
2808
- expect(w.asked.at(-1)?.question).toMatch(/the lead didn't take it.*Ask the lead again\?$/);
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()];