@warren-dean/code-kit 0.7.0 → 0.8.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.
@@ -97,6 +97,8 @@ function project() {
97
97
  allowed: false, // an approval in force: the hooks beneath let the call through
98
98
  bashRuns: 0,
99
99
  delegateExit: 1,
100
+ delegateRefusal:
101
+ 'This project delegates no approvals to the lead ("approvals.delegate" isn\'t set). Ask the person.',
100
102
  delegated: [] as string[][],
101
103
  holds: [] as string[][],
102
104
  draft: '', // what the person has typed in the prompt
@@ -148,6 +150,10 @@ function project() {
148
150
  },
149
151
  } as any,
150
152
  sent: [] as { to: any; text: string }[],
153
+ loopPaused: false, // the pause recorded by `code-kit loop`
154
+ map: null as any, // `code-kit map <story> --json`
155
+ trail: null as any, // `code-kit trail --json`
156
+ journal: { events: [] as any[] }, // `code-kit journal --json`
151
157
  holdWaits: false, // false: the hold ends at once, as if timed out (tests about cards, not hold)
152
158
  waiting: new Map<string, (answer: string) => void>(),
153
159
  };
@@ -185,6 +191,15 @@ function stub(on: any, w: World) {
185
191
  if (sub === 'adapters') return ran(0, JSON.stringify(w.adapters));
186
192
  if (sub === 'timeline')
187
193
  return ran(0, JSON.stringify({ base: 'main', now: 0, stories: w.times }));
194
+ if (sub === 'map') return ran(w.map ? 0 : 1, JSON.stringify(w.map));
195
+ if (sub === 'trail') return ran(w.trail ? 0 : 1, JSON.stringify(w.trail));
196
+ if (sub === 'journal') return ran(0, JSON.stringify(w.journal));
197
+ if (sub === 'loop' && ['pause', 'resume'].includes(argv[3])) {
198
+ w.acts.push([...argv.slice(2)]);
199
+ w.loopPaused = argv[3] === 'pause';
200
+ return ran(0, '');
201
+ }
202
+ if (sub === 'loop') return ran(0, JSON.stringify({ paused: w.loopPaused, at: null }));
188
203
  if (sub === 'sent-back') {
189
204
  w.acts.push([...argv.slice(2)]);
190
205
  return ran(0, `Recorded: ${argv[3]} was sent back.`);
@@ -198,7 +213,7 @@ function stub(on: any, w: World) {
198
213
  if (sub === 'approve' && argv.includes('--delegated')) {
199
214
  w.delegated.push([...argv.slice(2)]);
200
215
  if (w.delegateExit === 0) w.allowed = true;
201
- return ran(w.delegateExit, '', w.delegateExit ? 'Nothing was approved.' : '');
216
+ return ran(w.delegateExit, '', w.delegateExit ? w.delegateRefusal : '');
202
217
  }
203
218
  if (sub === 'hold') {
204
219
  const id = argv[3];
@@ -437,7 +452,6 @@ test('PANE-2 each lane shows its agent, story, branch and state', async ($, on)
437
452
  await lanes($);
438
453
  const ui = await pane($, PANE);
439
454
  expect(await ui.find({ key: 'lane-web' })).toBeDefined();
440
- expect(await ui.find({ type: 'Text', text: /web-engineer/ })).toBeDefined();
441
455
  expect(await ui.find({ type: 'Text', text: 'ST-4 Booking form · web/st-4' })).toBeDefined();
442
456
  expect(await ui.find({ type: 'Text', text: /building/ })).toBeDefined();
443
457
  expect(await ui.find({ type: 'Text', text: /idle/ })).toBeDefined();
@@ -613,8 +627,20 @@ test('ACT-1 a grant that fails approves nothing, and the band says why', async (
613
627
  await after.unmount();
614
628
  });
615
629
 
630
+ test("DECIDE-1 while the loop runs the build, a lane's finished work never waits in the band for the person", async ($, on) => {
631
+ const w = project();
632
+ w.stories[0] = { ...w.stories[0], state: 'review' };
633
+ await start($, on, w);
634
+ const band = await bandUi($);
635
+ expect(await band.find({ key: 'band-review' })).toBeUndefined();
636
+ expect(await band.find({ key: 'band-merge' })).toBeUndefined();
637
+ await band.unmount();
638
+ });
639
+
616
640
  test('ACT-3 Review asks the lead to run the review skill on the branch', async ($, on) => {
617
641
  const w = project();
642
+ // The person runs the build themselves: reviews and merges wait in the band for them.
643
+ (w.check as any).harness = { autonomy: 'off' };
618
644
  w.stories[0] = { ...w.stories[0], state: 'review' };
619
645
  await start($, on, w);
620
646
  const ui = await bandUi($);
@@ -629,6 +655,8 @@ test('ACT-3 Review asks the lead to run the review skill on the branch', async (
629
655
 
630
656
  test('ACT-4 Merge confirms, then merges as the person', async ($, on) => {
631
657
  const w = project();
658
+ // The person runs the build themselves: reviews and merges wait in the band for them.
659
+ (w.check as any).harness = { autonomy: 'off' };
632
660
  w.stories[0] = { ...w.stories[0], state: 'review' };
633
661
  await start($, on, w);
634
662
  const ui = await bandUi($);
@@ -643,6 +671,8 @@ test('ACT-4 Merge confirms, then merges as the person', async ($, on) => {
643
671
 
644
672
  test('ACT-4 a branch that fails verify is not merged, and the problems are shown', async ($, on) => {
645
673
  const w = project();
674
+ // The person runs the build themselves: reviews and merges wait in the band for them.
675
+ (w.check as any).harness = { autonomy: 'off' };
646
676
  w.stories[0] = { ...w.stories[0], state: 'review' };
647
677
  w.mergeExit = 1;
648
678
  await start($, on, w);
@@ -657,6 +687,8 @@ test('ACT-4 a branch that fails verify is not merged, and the problems are shown
657
687
 
658
688
  test('ACT-4 cancelling the confirmation merges nothing', async ($, on) => {
659
689
  const w = project();
690
+ // The person runs the build themselves: reviews and merges wait in the band for them.
691
+ (w.check as any).harness = { autonomy: 'off' };
660
692
  w.stories[0] = { ...w.stories[0], state: 'review' };
661
693
  w.answer = 'Cancel';
662
694
  await start($, on, w);
@@ -690,6 +722,8 @@ test('BAND-2 a failing finish check shows, with Lanes opening the pane', async (
690
722
 
691
723
  test('ACT-3 the story shows as being reviewed until the lead has reported', async ($, on) => {
692
724
  const w = project();
725
+ // The person runs the build themselves: reviews and merges wait in the band for them.
726
+ (w.check as any).harness = { autonomy: 'off' };
693
727
  w.stories[0] = { ...w.stories[0], state: 'review' };
694
728
  await start($, on, w);
695
729
  const ui = await bandUi($);
@@ -1190,6 +1224,117 @@ test('HOLD-1 and HOLD-2 a lane installing a new package is held, and the band as
1190
1224
  w.waiting.get('tu-h1')?.('timed out');
1191
1225
  });
1192
1226
 
1227
+ test('DECIDE-2 a decision in the band says which story it is for, and d opens it as a card with its lineage', async ($, on) => {
1228
+ const w = project();
1229
+ w.refusal = webInstall;
1230
+ w.stories[0] = { ...w.stories[0], requirements: ['BOOK-1'] };
1231
+ w.requirements = [{ id: 'BOOK-1', title: 'Request a booking', state: 'in progress' }];
1232
+ w.journal = {
1233
+ events: [
1234
+ { at: 1, kind: 'request', text: 'You asked for “Bookings”: guests book a room online.' },
1235
+ {
1236
+ at: 2,
1237
+ kind: 'dispatch',
1238
+ story: 'ST-4',
1239
+ text: 'ST-4 Booking form (BOOK-1) was handed to the web lane.',
1240
+ },
1241
+ ],
1242
+ };
1243
+ w.agents = [{ id: 'a1', type: 'web-engineer', description: 'ST-4', status: 'running' }];
1244
+ const clock = await start($, on, w);
1245
+ await lanes($);
1246
+ await heldCall($, w, clock);
1247
+ const band = await bandUi($);
1248
+ expect(await band.find({ type: 'Text', text: 'for ST-4 Booking form (BOOK-1)' })).toBeDefined();
1249
+ expect((await band.find({ key: 'band-decide' }))?.props.hotkey).toBe('d');
1250
+ await press($, 'band-decide');
1251
+ await band.unmount();
1252
+ const ui = await pane($, PANE);
1253
+ const card = await ui.find({ key: 'decision-card' });
1254
+ expect(card?.text).toMatch(/ST-4 Booking form · web\/st-4/);
1255
+ expect(card?.text).toMatch(/BOOK-1 Request a booking/);
1256
+ expect(card?.text).toMatch(/npm install dayjs/);
1257
+ expect(card?.text).toMatch(/ST-4 Booking form \(BOOK-1\) was handed to the web lane/);
1258
+ expect(card?.text).toMatch(
1259
+ /Blocked: a new dependency \(dayjs\) needs a person's approval\. You haven't let the lead approve these itself\./,
1260
+ );
1261
+ expect((await ui.find({ key: 'decision-option-approveHeld' }))?.text).toMatch(
1262
+ /The held call runs now, and the agent carries on/,
1263
+ );
1264
+ await ui.unmount();
1265
+ w.waiting.get('tu-h1')?.('timed out');
1266
+ });
1267
+
1268
+ test('DECIDE-3 an option on the card answers the decision, as the band would', async ($, on) => {
1269
+ const w = project();
1270
+ w.refusal = webInstall;
1271
+ w.agents = [{ id: 'a1', type: 'web-engineer', description: 'ST-4', status: 'running' }];
1272
+ const clock = await start($, on, w);
1273
+ await lanes($);
1274
+ const { call } = await heldCall($, w, clock);
1275
+ const band = await bandUi($);
1276
+ await press($, 'band-decide');
1277
+ await band.unmount();
1278
+ const ui = await pane($, PANE);
1279
+ await press($, 'decide-refuseHeld', PANE);
1280
+ expect(refused(await call)).toBe(true);
1281
+ await ui.unmount();
1282
+ const after = await bandUi($);
1283
+ expect(await after.find({ key: 'band-decide' })).toBeUndefined();
1284
+ await after.unmount();
1285
+ });
1286
+
1287
+ test('DECIDE-3 notes are labelled note, not you, and need no decision', async ($, on) => {
1288
+ const w = project();
1289
+ w.limits = [{ kind: 'five_hour', percentUsed: 91 }];
1290
+ await start($, on, w);
1291
+ const band = await bandUi($);
1292
+ expect(await band.find({ type: 'Text', text: 'you' })).toBeUndefined();
1293
+ expect(await band.find({ key: 'band-decide' })).toBeUndefined();
1294
+ await band.unmount();
1295
+ });
1296
+
1297
+ test('JOURNAL-1 the Journal tab tells the lineage, from the request to now, with the loop and your choices in it', async ($, on) => {
1298
+ const w = project();
1299
+ w.journal = {
1300
+ events: [
1301
+ {
1302
+ at: Date.parse('2026-10-01T09:00:00Z'),
1303
+ kind: 'request',
1304
+ text: 'You asked for “Bookings”.',
1305
+ },
1306
+ {
1307
+ at: Date.parse('2026-10-01T09:30:00Z'),
1308
+ kind: 'dispatch',
1309
+ story: 'ST-4',
1310
+ text: 'ST-4 Booking form was handed to the web lane.',
1311
+ },
1312
+ {
1313
+ at: Date.parse('2026-10-02T10:00:00Z'),
1314
+ kind: 'approval',
1315
+ who: 'person',
1316
+ text: 'You approved adding the package dayjs: “dates”.',
1317
+ },
1318
+ {
1319
+ at: Date.parse('2026-10-02T11:00:00Z'),
1320
+ kind: 'merge',
1321
+ story: 'ST-4',
1322
+ text: 'ST-4 Booking form was merged into main.',
1323
+ },
1324
+ ],
1325
+ };
1326
+ await start($, on, w);
1327
+ await lanes($);
1328
+ const ui = await pane($, PANE);
1329
+ expect((await ui.find({ key: 'tab-journal' }))?.props.hotkey).toBe('3');
1330
+ await press($, 'tab-journal', PANE);
1331
+ const journal = await ui.find({ key: 'journal' });
1332
+ expect(journal?.text).toMatch(
1333
+ /2026-10-01[\s\S]*You asked for “Bookings”[\s\S]*handed to the web lane[\s\S]*2026-10-02[\s\S]*You approved adding the package dayjs[\s\S]*merged into main/,
1334
+ );
1335
+ await ui.unmount();
1336
+ });
1337
+
1193
1338
  test('HOLD-3 on Approve the approval is written as the pane writes it, and the same call runs', async ($, on) => {
1194
1339
  const w = project();
1195
1340
  w.refusal = webInstall;
@@ -1361,7 +1506,7 @@ test('LOOP-1 and LOOP-2 with the lead idle, the loop dispatches every ready stor
1361
1506
  ]);
1362
1507
  await lanes($);
1363
1508
  const ui = await pane($, PANE);
1364
- expect((await ui.find({ key: 'step-0' }))?.text).toMatch(/dispatch\s*ST-9 ST-10 ST-11 ST-12/);
1509
+ expect((await ui.find({ key: 'loop-step-0' }))?.text).toMatch(/dispatch ST-9 ST-10 ST-11 ST-12/);
1365
1510
  await ui.unmount();
1366
1511
  });
1367
1512
 
@@ -1477,6 +1622,29 @@ test('LOOP-6 Pause stops new steps until Resume', async ($, on) => {
1477
1622
  ]);
1478
1623
  });
1479
1624
 
1625
+ test('LOOP-6 the pause is recorded for other tools, and a new session starts with the loop running', async ($, on) => {
1626
+ const w = project();
1627
+ w.loopPaused = true; // left by an earlier session
1628
+ w.next = { step: 'review', args: 'ST-4', then: [] };
1629
+ const clock = await start($, on, w);
1630
+ expect(w.acts).toContainEqual(['loop', 'resume']);
1631
+ expect(w.loopPaused).toBe(false);
1632
+ await leadTurn($);
1633
+ await settle(clock);
1634
+ w.acts = [];
1635
+ let ui = await bandUi($);
1636
+ await press($, 'band-pause');
1637
+ await ui.unmount();
1638
+ expect(w.acts).toEqual([['loop', 'pause']]);
1639
+ ui = await bandUi($);
1640
+ await press($, 'band-resume');
1641
+ await ui.unmount();
1642
+ expect(w.acts).toEqual([
1643
+ ['loop', 'pause'],
1644
+ ['loop', 'resume'],
1645
+ ]);
1646
+ });
1647
+
1480
1648
  test('LOOP-3 a quiet lane agent is nudged at 5 minutes, restarted through the lead at 10, and flagged after 2 restarts', async ($, on) => {
1481
1649
  const w = project();
1482
1650
  w.next = { step: 'wait', args: '', then: [] };
@@ -1515,6 +1683,51 @@ test('LOOP-3 a quiet lane agent is nudged at 5 minutes, restarted through the le
1515
1683
  await band.unmount();
1516
1684
  });
1517
1685
 
1686
+ test('LOOP-3 an agent whose work is done is never nudged or restarted: it handed back, or its branch waits in the merge queue', async ($, on) => {
1687
+ const w = project();
1688
+ w.next = { step: 'wait', args: '', then: [] };
1689
+ w.agents = [
1690
+ { id: 'a1', type: 'web-engineer', description: 'ST-4 booking form', status: 'running' },
1691
+ ];
1692
+ const clock = await start($, on, w);
1693
+ await clock.advance(2000);
1694
+ // The agent hands its work back, but stays listed as running while it waits for messages.
1695
+ await $.turn.complete({
1696
+ turnId: 't-a1',
1697
+ agentId: 'a1',
1698
+ answer: 'ST-4 done.',
1699
+ durationMs: 1,
1700
+ isAborted: false,
1701
+ } as any);
1702
+ await clock.advance(11 * 60000);
1703
+ expect(w.sent).toEqual([]);
1704
+ expect(w.prompts).toEqual([]);
1705
+ // Another, whose story's branch is in the merge queue, isn't stalled either.
1706
+ w.agents = [
1707
+ { id: 'a2', type: 'web-engineer', description: 'ST-4 booking form', status: 'running' },
1708
+ ];
1709
+ w.queue = [{ branch: 'web/st-4', passed: '2026-10-11T00:00:00Z', state: 'waiting' }];
1710
+ await clock.advance(11 * 60000);
1711
+ expect(w.sent).toEqual([]);
1712
+ expect(w.prompts).toEqual([]);
1713
+ });
1714
+
1715
+ test('LOOP-3 until the person has chosen how hands-off the loop is, a restart is offered, not taken', async ($, on) => {
1716
+ const w = project();
1717
+ (w.check as any).harnessSet = [];
1718
+ w.next = { step: 'wait', args: '', then: [] };
1719
+ w.agents = [
1720
+ { id: 'a1', type: 'web-engineer', description: 'ST-4 booking form', status: 'running' },
1721
+ ];
1722
+ const clock = await start($, on, w);
1723
+ await clock.advance(10 * 60000 + 4000);
1724
+ expect(w.prompts).toEqual([]);
1725
+ const band = await bandUi($);
1726
+ expect(await band.find({ type: 'Text', text: /Next: Restart ST-4/ })).toBeDefined();
1727
+ expect(await band.find({ key: 'band-keepGoing' })).toBeDefined();
1728
+ await band.unmount();
1729
+ });
1730
+
1518
1731
  test('LOOP-3 an agent waiting on a held call (or running a long command) is never quiet', async ($, on) => {
1519
1732
  const w = project();
1520
1733
  w.next = { step: 'wait', args: '', then: [] };
@@ -1675,6 +1888,161 @@ test("REVW-2 the reviewer's model is the setting's, registered again when it cha
1675
1888
 
1676
1889
  // --- the merge queue --------------------------------------------------------------------------------
1677
1890
 
1891
+ // --- the code map and the trail -----------------------------------------------------------------
1892
+
1893
+ const storyMap = () => ({
1894
+ story: { id: 'ST-4', title: 'Booking form', lane: 'web', state: 'in progress' },
1895
+ layers: ['domain', 'services', 'tests'],
1896
+ files: [
1897
+ {
1898
+ path: 'apps/web/domain/booking.ts',
1899
+ layer: 'domain',
1900
+ lane: 'web',
1901
+ imports: [],
1902
+ adapters: {
1903
+ 'context-graph': { card: { state: 'current', text: 'A booking and its rules.' } },
1904
+ },
1905
+ },
1906
+ {
1907
+ path: 'apps/web/services/book.ts',
1908
+ layer: 'services',
1909
+ lane: 'web',
1910
+ imports: ['apps/web/domain/booking.ts'],
1911
+ adapters: { 'context-graph': { card: { state: 'stale', text: 'Books a slot.' } } },
1912
+ },
1913
+ {
1914
+ path: 'apps/web/e2e/book.spec.ts',
1915
+ layer: 'tests',
1916
+ lane: 'web',
1917
+ imports: ['apps/web/services/book.ts'],
1918
+ adapters: { 'context-graph': { card: { state: 'missing', text: '' } } },
1919
+ },
1920
+ ],
1921
+ open: { command: 'graph' },
1922
+ });
1923
+
1924
+ test("MAP-1 the Code map shows the selected lane's story by layer, its imports, and the selected file's card", async ($, on) => {
1925
+ const w = project();
1926
+ w.map = storyMap();
1927
+ const clock = await start($, on, w);
1928
+ await lanes($);
1929
+ const ui = await pane($, PANE);
1930
+ await press($, 'tab-code', PANE);
1931
+ await clock.advance(1);
1932
+ expect((await ui.find({ key: 'map-story-ST-4' }))?.props.variant).toBe('primary');
1933
+ const drawn = (await ui.find({ key: 'code-map' }))?.text ?? '';
1934
+ for (const name of [
1935
+ 'domain',
1936
+ 'services',
1937
+ 'tests',
1938
+ 'booking.ts',
1939
+ 'book.ts',
1940
+ 'book.spec.ts',
1941
+ '✓ why written',
1942
+ 'why out of date',
1943
+ 'no why yet',
1944
+ ])
1945
+ expect(drawn).toContain(name);
1946
+ // An import between neighbouring layers is drawn as a line.
1947
+ expect(drawn).toMatch(/[─┐┘└┌]/);
1948
+ expect((await ui.find({ key: 'code-file' }))?.text).toMatch(
1949
+ /apps\/web\/domain\/booking.ts.*A booking and its rules\..*imported by book.ts/s,
1950
+ );
1951
+ await press($, 'code-next', PANE);
1952
+ expect((await ui.find({ key: 'code-file' }))?.text).toMatch(
1953
+ /book.ts.*Books a slot\..*changed since its why was written.*imports booking.ts.*imported by book.spec.ts/s,
1954
+ );
1955
+ await press($, 'code-open', PANE);
1956
+ expect(w.commands).toContainEqual({ command: 'graph', args: 'apps/web/services/book.ts' });
1957
+ await ui.unmount();
1958
+ });
1959
+
1960
+ test('MAP-2 a file a lane reads or edits glows in the code map, in the lane, and stops after a while', async ($, on) => {
1961
+ const w = project();
1962
+ w.map = storyMap();
1963
+ w.agents = [{ id: 'a1', type: 'web-engineer', description: 'ST-4', status: 'running' }];
1964
+ const clock = await start($, on, w);
1965
+ await lanes($);
1966
+ const ui = await pane($, PANE);
1967
+ await press($, 'tab-code', PANE);
1968
+ await $.tool.call({ tool: 'Bash', tool_use_id: 'tu-w', command: 'ls', agentId: 'a1' } as any);
1969
+ await clock.advance(2000);
1970
+ await $.tool
1971
+ .call({
1972
+ tool: 'Read',
1973
+ tool_use_id: 'tu-r',
1974
+ file_path: '/repo/.claude/worktrees/agent-a1/apps/web/services/book.ts',
1975
+ agentId: 'a1',
1976
+ } as any)
1977
+ .catch(() => {});
1978
+ await clock.advance(1);
1979
+ expect((await ui.find({ key: 'code-map' }))?.text).toContain('web reading');
1980
+ // The box fills with the lane's colour, the status written over it.
1981
+ const filled = (await ui.findAll({ type: 'Text' })).find((t: any) => /web reading/.test(t.text));
1982
+ expect(filled?.props.backgroundColor).toMatch(/^#[0-9a-f]{6}$/);
1983
+ expect(await ui.find({ key: 'code-legend' })).toBeDefined();
1984
+ await clock.advance(30000);
1985
+ expect((await ui.find({ key: 'code-map' }))?.text).not.toContain('web reading');
1986
+ await ui.unmount();
1987
+ });
1988
+
1989
+ test('TRAIL-1 the Trail tab follows each requirement of the spec to its stories, code and tests, with gaps in red', async ($, on) => {
1990
+ const w = project();
1991
+ w.requirements = [
1992
+ {
1993
+ id: 'BOOK-1',
1994
+ title: 'Request',
1995
+ file: 'docs/specs/01-booking.md',
1996
+ state: 'in progress',
1997
+ stories: ['ST-4'],
1998
+ tests: ['apps/web/e2e/book.spec.ts'],
1999
+ },
2000
+ {
2001
+ id: 'BOOK-2',
2002
+ title: 'Cancel',
2003
+ file: 'docs/specs/01-booking.md',
2004
+ state: 'no story',
2005
+ stories: [],
2006
+ tests: [],
2007
+ },
2008
+ ];
2009
+ w.trail = {
2010
+ base: 'main',
2011
+ requirements: [
2012
+ {
2013
+ id: 'BOOK-1',
2014
+ title: 'Request',
2015
+ spec: '01-booking.md',
2016
+ state: 'in progress',
2017
+ stories: [{ id: 'ST-4', state: 'in progress', lane: 'web' }],
2018
+ files: ['apps/web/services/book.ts'],
2019
+ tests: ['apps/web/e2e/book.spec.ts'],
2020
+ },
2021
+ {
2022
+ id: 'BOOK-2',
2023
+ title: 'Cancel',
2024
+ spec: '01-booking.md',
2025
+ state: 'no story',
2026
+ stories: [],
2027
+ files: [],
2028
+ tests: [],
2029
+ },
2030
+ ],
2031
+ };
2032
+ const clock = await start($, on, w);
2033
+ await lanes($);
2034
+ const ui = await pane($, PANE);
2035
+ await press($, 'tab-map', PANE);
2036
+ await clock.advance(1);
2037
+ expect((await ui.find({ key: 'trail-BOOK-1' }))?.text).toMatch(
2038
+ /BOOK-1\s*Request\s*─+\s*● ST-4\s*─+\s*1 file\s*─+\s*✓ book\.spec\.ts/,
2039
+ );
2040
+ const gap = await ui.find({ key: 'trail-BOOK-2' });
2041
+ expect(gap?.text).toMatch(/BOOK-2.*╌+\s*no story.*╌+\s*no code.*╌+\s*no test names it/);
2042
+ expect((await ui.find({ key: 'map-files' }))?.text).toMatch(/book.ts/);
2043
+ await ui.unmount();
2044
+ });
2045
+
1678
2046
  test('MQ-2 where the lead merges, the loop prompts it to merge the head of the queue', async ($, on) => {
1679
2047
  const w = project();
1680
2048
  w.next = { step: 'merge', args: 'web/st-4', then: [] };
@@ -1698,18 +2066,31 @@ test('MQ-2 where the person merges, the head of the queue waits in the band for
1698
2066
  expect(w.opened).toContain(RESULT);
1699
2067
  });
1700
2068
 
1701
- test('MQ-2 the first merge, before the base has code-kit rules committed, is offered to the person', async ($, on) => {
2069
+ test("SETUP-2 with no rules on the base yet, the lead puts them there under autonomy: the band doesn't ask the person to merge", async ($, on) => {
1702
2070
  const w = project();
1703
2071
  (w.check as any).firstMerge = true;
1704
2072
  w.queue = [{ branch: 'lead/st-1', passed: '2026-10-10T00:00:00Z', state: 'waiting' }];
2073
+ w.next = { step: 'rules', args: 'main', then: [] };
1705
2074
  await start($, on, w);
1706
2075
  const ui = await bandUi($);
1707
- expect(
1708
- await ui.find({
1709
- type: 'Text',
1710
- text: 'lead/st-1 is next to merge: the first merge is yours, then the lead merges',
1711
- }),
1712
- ).toBeDefined();
2076
+ expect(await ui.find({ type: 'Text', text: /next to merge/ })).toBeUndefined();
2077
+ await ui.unmount();
2078
+ });
2079
+
2080
+ test("SETUP-2 with the loop off, the merge is the person's, and its card says how the rules get onto main", async ($, on) => {
2081
+ const w = project();
2082
+ (w.check as any).firstMerge = true;
2083
+ (w.check as any).harness = { autonomy: 'off' };
2084
+ w.queue = [{ branch: 'lead/st-1', passed: '2026-10-10T00:00:00Z', state: 'waiting' }];
2085
+ await start($, on, w);
2086
+ const band = await bandUi($);
2087
+ expect(await band.find({ type: 'Text', text: 'lead/st-1 is next to merge' })).toBeDefined();
2088
+ await press($, 'band-decide');
2089
+ await band.unmount();
2090
+ const ui = await pane($, PANE);
2091
+ expect((await ui.find({ key: 'decision-card' }))?.text).toMatch(
2092
+ /code-kit queue rules puts them there/,
2093
+ );
1713
2094
  await ui.unmount();
1714
2095
  });
1715
2096
 
@@ -1784,6 +2165,46 @@ test('VIEW-1 the header counts the stories by state, and a count filters the lan
1784
2165
  await ui.unmount();
1785
2166
  });
1786
2167
 
2168
+ test('MISSION-1 the band shows the goal, what the lead is doing, the next step and each agent at work', async ($, on) => {
2169
+ const w = project();
2170
+ w.stories = w.stories.map((x: any) => ({ ...x, milestone: 'Milestone 2: Bookings' }));
2171
+ w.stories[0] = { ...w.stories[0], state: 'done' };
2172
+ w.next = { step: 'review', args: 'ST-9', then: [{ step: 'dispatch', args: 'ST-10' }] };
2173
+ w.agents = [{ id: 'a1', type: 'web-engineer', description: 'ST-4', status: 'running' }];
2174
+ const clock = await start($, on, w);
2175
+ await leadTurn($);
2176
+ await settle(clock);
2177
+ await $.tool.call({
2178
+ tool: 'Bash',
2179
+ tool_use_id: 'tu-m',
2180
+ command: 'npm run test:e2e',
2181
+ agentId: 'a1',
2182
+ } as any);
2183
+ await $.turn.start({ turnId: 't-busy', text: '' } as any);
2184
+ await clock.advance(3000);
2185
+ const ui = await bandUi($);
2186
+ const rows = (await ui.findAll({ type: 'Text' })).map((t: any) => t.text);
2187
+ expect(rows).toContain('Milestone 2: Bookings · 1 of 4 stories done');
2188
+ expect(rows.some((t: string) => /^lead at work · loop on · last: review ST-9$/.test(t))).toBe(
2189
+ true,
2190
+ );
2191
+ expect(rows).toContain('Review ST-9, then Dispatch ST-10');
2192
+ expect(rows).toContain('web ');
2193
+ expect(rows).toContain('Bash npm run test:e2e');
2194
+ expect(await ui.find({ key: 'band-pause' })).toBeDefined();
2195
+ await ui.unmount();
2196
+ });
2197
+
2198
+ test('MISSION-1 with nothing happening the band has no mission rows', async ($, on) => {
2199
+ const w = project();
2200
+ w.next = { step: 'wait', args: '', then: [] };
2201
+ await start($, on, w);
2202
+ const ui = await bandUi($);
2203
+ const rows = (await ui.findAll({ type: 'Text' })).map((t: any) => t.text);
2204
+ expect(rows.some((t: string) => /^(goal|now|next) +$/.test(t))).toBe(false);
2205
+ await ui.unmount();
2206
+ });
2207
+
1787
2208
  test("VIEW-4 each lane shows its agent's last tool call and the time since, amber once quiet", async ($, on) => {
1788
2209
  const w = project();
1789
2210
  w.next = { step: 'wait', args: '', then: [] };
@@ -1817,7 +2238,9 @@ test('VIEW-5 what needs the person is pinned at the top with its letter: a held
1817
2238
  await heldCall($, w, clock);
1818
2239
  await lanes($);
1819
2240
  const ui = await pane($, PANE);
1820
- expect((await ui.find({ key: 'need-held' }))?.text).toMatch(/web wants to install dayjs/);
2241
+ expect((await ui.find({ key: 'need-held' }))?.text).toMatch(
2242
+ /web wants to install dayjs · 2:00for ST-4 Booking form/,
2243
+ );
1821
2244
  expect((await ui.find({ key: 'need-approveHeld' }))?.props.hotkey).toBe('a');
1822
2245
  await press($, 'need-approveHeld', PANE);
1823
2246
  expect(w.opened).toContain(APPROVE);
@@ -1825,13 +2248,14 @@ test('VIEW-5 what needs the person is pinned at the top with its letter: a held
1825
2248
  w.waiting.get('tu-h1')?.('timed out');
1826
2249
  });
1827
2250
 
1828
- test('VIEW-6 tabs on 1 to 3, j and k move between lanes, and the selected lane has its keys', async ($, on) => {
2251
+ test('VIEW-6 tabs on 1 to 7, j and k move between lanes, and the selected lane has its keys', async ($, on) => {
1829
2252
  const w = project();
1830
2253
  w.agents = [{ id: 'a1', type: 'web-engineer', description: 'ST-4', status: 'running' }];
1831
2254
  await start($, on, w);
1832
2255
  await lanes($);
1833
2256
  const ui = await pane($, PANE);
1834
- expect((await ui.find({ key: 'tab-queue' }))?.props.hotkey).toBe('2');
2257
+ expect((await ui.find({ key: 'tab-queue' }))?.props.hotkey).toBe('6');
2258
+ expect((await ui.find({ key: 'tab-code' }))?.props.hotkey).toBe('4');
1835
2259
  await press($, 'tab-queue', PANE);
1836
2260
  expect(await ui.find({ type: 'Text', text: 'The merge queue is empty.' })).toBeDefined();
1837
2261
  await press($, 'tab-lanes', PANE);
@@ -2010,7 +2434,8 @@ test('TRACE-1 the map: a cell per requirement, a row per spec, and a legend with
2010
2434
  await start($, on, w);
2011
2435
  await lanes($);
2012
2436
  const ui = await pane($, PANE);
2013
- expect((await ui.find({ key: 'tab-map' }))?.props.hotkey).toBe('4');
2437
+ expect((await ui.find({ key: 'tab-map' }))?.props.label).toBe('Trail');
2438
+ expect((await ui.find({ key: 'tab-map' }))?.props.hotkey).toBe('5');
2014
2439
  await press($, 'tab-map', PANE);
2015
2440
  const legend = (await ui.find({ key: 'map-legend' }))?.text ?? '';
2016
2441
  expect(legend).toMatch(/■ 20 done and tested/);
@@ -2227,12 +2652,13 @@ test('JOIN-2 each lane shows its edits without understanding, red above zero, an
2227
2652
  await lanes($);
2228
2653
  const ui = await pane($, PANE);
2229
2654
  expect((await ui.find({ key: 'lane-web' }))?.text).toMatch(
2230
- /1 edit without understanding\s*2 cards owed/,
2655
+ /1 edited without understanding\s*2 cards owed/,
2231
2656
  );
2232
- expect((await ui.find({ type: 'Text', text: '1 edit without understanding' }))?.props.color).toBe(
2233
- 'red',
2234
- );
2235
- expect((await ui.find({ key: 'lane-api' }))?.text).toMatch(/0 edits without understanding/);
2657
+ expect(
2658
+ (await ui.find({ type: 'Text', text: '1 edited without understanding' }))?.props.color,
2659
+ ).toBe('red');
2660
+ // LANES-2: a lane whose facts are all zero shows none of them.
2661
+ expect((await ui.find({ key: 'lane-api' }))?.text).not.toMatch(/understanding|owed/);
2236
2662
  await ui.unmount();
2237
2663
  });
2238
2664
 
@@ -2410,9 +2836,13 @@ test('VIEW-6 a selected idle lane shows what it owns and its stories, Enter open
2410
2836
  expect((await ui.find({ key: 'count-idle' }))?.props.label).toMatch(/· \d+ idle/);
2411
2837
  for (let i = 0; i < 3; i++) await press($, 'lane-next', PANE);
2412
2838
  const api = (await ui.find({ key: 'lane-api' }))?.text ?? '';
2413
- expect(api).toMatch(/owns apps\/api\/\*\*/);
2414
- expect(api).toMatch(/ST-1 Schema · done/);
2415
- expect(api).toMatch(/ST-2 Endpoints · blocked on ST-9/);
2839
+ expect(api).toMatch(/owns\s*apps\/api\/\*\*/);
2840
+ expect(api).toMatch(/agent\s*api-engineer/);
2841
+ // LANES-3: the lane's stories as cards.
2842
+ expect((await ui.find({ key: 'card-ST-1' }))?.text).toMatch(/ST-1\s*merged\s*Schema/);
2843
+ expect((await ui.find({ key: 'card-ST-2' }))?.text).toMatch(
2844
+ /ST-2\s*blocked\s*Endpoints.*waits on ST-9/,
2845
+ );
2416
2846
  expect((await ui.find({ key: 'open-story' }))?.props.label).toBe('Open ST-2');
2417
2847
  await press($, 'open-story', PANE);
2418
2848
  expect(await ui.find({ key: 'story-head' })).toBeDefined();
@@ -2431,7 +2861,7 @@ test('VIEW-6 the filter is one row, its key beside the box', async ($, on) => {
2431
2861
  await ui.unmount();
2432
2862
  });
2433
2863
 
2434
- test('the lead reads working while its turn runs, and idle once it ends; an agent at work outside a story too', async ($, on) => {
2864
+ test('the lead reads orchestrating while its turn runs, and idle once it ends; an agent at work outside a story too', async ($, on) => {
2435
2865
  const w = project();
2436
2866
  w.agents = [
2437
2867
  { id: 'a9', type: 'api-engineer', description: 'look at the logs', status: 'running' },
@@ -2441,8 +2871,9 @@ test('the lead reads working while its turn runs, and idle once it ends; an agen
2441
2871
  const ui = await pane($, PANE);
2442
2872
  expect((await ui.find({ key: 'lane-api' }))?.text).toMatch(/api\s*working/);
2443
2873
  await $.turn.start({ turnId: 't-1', text: '' } as any);
2444
- expect((await ui.find({ key: 'lane-lead' }))?.text).toMatch(/lead\s*working/);
2445
- expect((await ui.find({ key: 'count-working' }))?.props.label).toBe('▶ 2 working');
2874
+ // LANES-2: the lead mid-turn is orchestrating, not a lane at work on nothing.
2875
+ expect((await ui.find({ key: 'lane-lead' }))?.text).toMatch(/lead\s*orchestrating/);
2876
+ expect((await ui.find({ key: 'count-working' }))?.props.label).toBe('▶ 1 working');
2446
2877
  await $.turn.complete({ turnId: 't-1', answer: '', durationMs: 1, isAborted: false } as any);
2447
2878
  await settle(clock);
2448
2879
  expect((await ui.find({ key: 'lane-lead' }))?.text).toMatch(/lead\s*idle/);