@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.
@@ -8,6 +8,7 @@ const LEAD = 'lead';
8
8
 
9
9
  /** A CLI run's stdout as JSON, or null when it printed none. */
10
10
  import { BAR, POSE_GLYPH, rasterCells, runs, sprite, spriteSvg, timelineSvg } from './art.mjs';
11
+ import { missionLines, nextText } from './mission.mjs';
11
12
 
12
13
  export function parseJson(stdout) {
13
14
  try {
@@ -94,6 +95,7 @@ export function projectState(check, status, next = null, atWork = new Set()) {
94
95
  worktree: s.worktree ?? null,
95
96
  requirements: s.requirements ?? [],
96
97
  waitingOn: s.waitingOn ?? [],
98
+ milestone: s.milestone ?? null,
97
99
  })),
98
100
  };
99
101
  }
@@ -101,6 +103,7 @@ export function projectState(check, status, next = null, atWork = new Set()) {
101
103
  export const STATE_COLOR = {
102
104
  building: 'blue',
103
105
  working: 'green',
106
+ orchestrating: 'yellow',
104
107
  'in review': 'yellow',
105
108
  'sent back': 'magenta',
106
109
  blocked: 'red',
@@ -112,6 +115,10 @@ export const STATE_COLOR = {
112
115
  * `working` while its agent (or the lead, mid-turn) is at work on nothing the plan names.
113
116
  */
114
117
  export function shownState(lane, queue = []) {
118
+ // LANES-2: the lead mid-turn is orchestrating unless it builds its own story; between turns, a story
119
+ // of its own that waits on another is the story's state, not the lead's.
120
+ if (lane.name === LEAD && lane.active && lane.state !== 'building') return 'orchestrating';
121
+ if (lane.name === LEAD && lane.state === 'blocked') return 'idle';
115
122
  if (lane.state === 'idle' && lane.active) return 'working';
116
123
  const waiting = queue.some(
117
124
  (e) => e.branch === lane.branch && ['waiting', 'merging'].includes(e.state),
@@ -212,11 +219,42 @@ export function lanesPane(
212
219
  .some((s) => s.toLowerCase().includes(needle))),
213
220
  );
214
221
  const LIVE = { quiet: 'yellow', stalled: 'red' };
222
+ // LANES-2: fixed columns that never wrap: name, state, then the story (and the agent's current tool
223
+ // beneath it), then tokens; another tool's facts only when they aren't zero.
224
+ const STATE_WIDTH = 13;
215
225
  const rows = shown.map((l) => {
226
+ const s = shownState(l, loop?.queue);
227
+ const facts = combined?.[l.name];
228
+ const chips = [
229
+ ...(facts?.without
230
+ ? [text(`${facts.without} edited without understanding`, { color: 'red' })]
231
+ : []),
232
+ ...(facts?.owed
233
+ ? [text(`${facts.owed} card${facts.owed === 1 ? '' : 's'} owed`, { color: 'yellow' })]
234
+ : []),
235
+ ...(loop?.laneNotes?.[l.name]
236
+ ? [
237
+ text(loop.laneNotes[l.name], {
238
+ color: loop.laneNotes[l.name] === 'stalled' ? 'red' : 'yellow',
239
+ }),
240
+ ]
241
+ : []),
242
+ ];
243
+ const story = l.story
244
+ ? `${l.story.id} ${l.story.title}${
245
+ l.story.state === 'blocked' && l.story.waitingOn?.length
246
+ ? ` · waits on ${l.story.waitingOn.join(', ')}`
247
+ : l.branch
248
+ ? ` · ${l.branch}`
249
+ : ''
250
+ }`
251
+ : l.name === LEAD
252
+ ? 'the main session'
253
+ : (l.agent ?? '');
216
254
  const lines = [
217
255
  Box({
218
256
  flexDirection: 'row',
219
- columnGap: 2,
257
+ columnGap: 1,
220
258
  children: [
221
259
  text(l.name === selected ? '›' : l.active ? '●' : ' ', {
222
260
  color: l.name === selected ? 'cyan' : 'green',
@@ -226,86 +264,52 @@ export function lanesPane(
226
264
  ...(l.name === selected ? { color: 'cyan' } : {}),
227
265
  }),
228
266
  text(
229
- shownState(l, loop?.queue).padEnd(9),
230
- shownState(l, loop?.queue) === 'queued'
267
+ s.padEnd(STATE_WIDTH),
268
+ s === 'queued'
231
269
  ? { color: 'cyan' }
232
- : STATE_COLOR[shownState(l, loop?.queue)]
233
- ? { color: STATE_COLOR[shownState(l, loop?.queue)] }
234
- : {},
270
+ : STATE_COLOR[s]
271
+ ? { color: STATE_COLOR[s] }
272
+ : { dimColor: true },
235
273
  ),
236
- text(l.agent ?? 'the main session', { dimColor: true }),
237
- ...(live[l.name]
238
- ? [
274
+ Box({
275
+ flexGrow: 1,
276
+ flexShrink: 1,
277
+ children: [text(story, { wrap: 'truncate-end', dimColor: !l.story })],
278
+ }),
279
+ ...chips,
280
+ ...(usage?.lanes[l.name]
281
+ ? [text(tokens(usage.lanes[l.name]).padStart(6), { dimColor: true })]
282
+ : []),
283
+ ],
284
+ }),
285
+ // VIEW-4: what its agent is doing now, and since when, beneath the story.
286
+ ...(live[l.name]
287
+ ? [
288
+ Box({
289
+ flexDirection: 'row',
290
+ columnGap: 1,
291
+ children: [
292
+ text(''.padEnd(width + STATE_WIDTH + 3)),
239
293
  text(`${live[l.name].tool} · ${live[l.name].ago}`, {
240
294
  wrap: 'truncate-end',
241
295
  ...(LIVE[live[l.name].level]
242
296
  ? { color: LIVE[live[l.name].level] }
243
297
  : { dimColor: true }),
244
298
  }),
245
- ]
246
- : []),
247
- ...(loop?.laneNotes?.[l.name]
248
- ? [
249
- text(loop.laneNotes[l.name], {
250
- color: loop.laneNotes[l.name] === 'stalled' ? 'red' : 'yellow',
251
- }),
252
- ]
253
- : []),
254
- ...(usage?.lanes[l.name] ? [text(tokens(usage.lanes[l.name]), { dimColor: true })] : []),
255
- // JOIN-2: with an adapter that knows, the lane's edits without understanding and cards owed.
256
- ...(combined?.[l.name]
257
- ? [
258
- text(
259
- `${combined[l.name].without} edit${combined[l.name].without === 1 ? '' : 's'} without understanding`,
260
- combined[l.name].without ? { color: 'red' } : { dimColor: true },
261
- ),
262
- text(
263
- `${combined[l.name].owed} card${combined[l.name].owed === 1 ? '' : 's'} owed`,
264
- combined[l.name].owed ? { color: 'yellow' } : { dimColor: true },
265
- ),
266
- ]
267
- : []),
268
- ],
269
- }),
270
- ...(l.story
271
- ? [
272
- text(
273
- ` ${l.story.id} ${l.story.title}${l.branch ? ` · ${l.branch}` : ''}${
274
- l.state === 'blocked' ? ` · waits on ${l.story.waitingOn.join(', ')}` : ''
275
- }`,
276
- { dimColor: true, wrap: 'truncate-end' },
277
- ),
278
- ]
279
- : []),
280
- ...bar(l),
281
- // VIEW-6: the selected lane's detail: the paths it owns and its stories, each with its state.
282
- ...(l.name === selected && detail
283
- ? [
284
- text(` owns ${detail.owns.length ? detail.owns.join(', ') : 'nothing yet'}`, {
285
- dimColor: true,
286
- wrap: 'truncate-end',
299
+ ],
287
300
  }),
288
- ...(detail.stories.length
289
- ? detail.stories.map((s) =>
290
- text(
291
- ` ${s.id} ${s.title} · ${s.state}${s.state === 'blocked' && s.waitingOn.length ? ` on ${s.waitingOn.join(', ')}` : ''}`,
292
- {
293
- dimColor: s.state === 'done',
294
- ...(s.state === 'blocked' ? { color: 'red' } : {}),
295
- wrap: 'truncate-end',
296
- },
297
- ),
298
- )
299
- : [text(' no stories in the plan', { dimColor: true })]),
300
301
  ]
301
302
  : []),
303
+ ...bar(l),
304
+ // LANES-3: the selected lane's stories as cards, then what it owns.
305
+ ...(l.name === selected && detail ? laneDetail(detail, els, width) : []),
302
306
  ];
303
307
  return art
304
308
  ? Box({
305
309
  key: `lane-${l.name}`,
306
310
  flexDirection: 'row',
307
311
  columnGap: 1,
308
- children: [character(l), Box({ flexDirection: 'column', children: lines })],
312
+ children: [character(l), Box({ flexDirection: 'column', flexGrow: 1, children: lines })],
309
313
  })
310
314
  : Box({ key: `lane-${l.name}`, flexDirection: 'column', children: lines });
311
315
  });
@@ -335,6 +339,7 @@ export function lanesPane(
335
339
  flexDirection: 'column',
336
340
  rowGap: 1,
337
341
  children: [
342
+ ...loopCard(loop, els),
338
343
  ...notes,
339
344
  Box({
340
345
  flexDirection: 'column',
@@ -342,11 +347,156 @@ export function lanesPane(
342
347
  }),
343
348
  Box({ flexDirection: 'column', children: ready }),
344
349
  ...reviewsSection(loop, text, Box),
345
- ...loopSection(loop, text, Box),
346
350
  ],
347
351
  });
348
352
  }
349
353
 
354
+ const CARD_STATE = {
355
+ done: { label: 'merged', color: 'gray' },
356
+ review: { label: 'in review', color: 'yellow' },
357
+ 'in progress': { label: 'building', color: 'blue' },
358
+ 'sent back': { label: 'sent back', color: 'magenta' },
359
+ blocked: { label: 'blocked', color: 'red' },
360
+ ready: { label: 'ready', color: 'green' },
361
+ todo: { label: 'to do', color: 'gray' },
362
+ };
363
+
364
+ /**
365
+ * LANES-3: a lane's stories as cards (id and state, title, requirements, and what it waits on or its
366
+ * branch), wrapping across the pane, then its agent and the paths it owns. `detail` is { agent, owns,
367
+ * stories }.
368
+ */
369
+ export function laneDetail(detail, { Box, Text }, indent = 0) {
370
+ const text = (value, style = {}) => Text({ ...style, children: [value] });
371
+ const cards = detail.stories.map((s) => {
372
+ const st = CARD_STATE[s.state] ?? { label: s.state, color: 'gray' };
373
+ return Box({
374
+ key: `card-${s.id}`,
375
+ flexDirection: 'column',
376
+ borderStyle: 'round',
377
+ borderColor: st.color,
378
+ ...(s.state === 'done' ? { borderDimColor: true } : {}),
379
+ paddingX: 1,
380
+ width: 30,
381
+ children: [
382
+ Box({
383
+ flexDirection: 'row',
384
+ justifyContent: 'space-between',
385
+ children: [
386
+ text(s.id, { bold: true, dimColor: s.state === 'done' }),
387
+ text(st.label, { color: st.color }),
388
+ ],
389
+ }),
390
+ text(s.title, { wrap: 'truncate-end', dimColor: s.state === 'done' }),
391
+ ...(s.requirements?.length
392
+ ? [text(s.requirements.join(' '), { dimColor: true, wrap: 'truncate-end' })]
393
+ : []),
394
+ ...(s.state === 'blocked' && s.waitingOn?.length
395
+ ? [text(`waits on ${s.waitingOn.join(', ')}`, { color: 'red', wrap: 'truncate-end' })]
396
+ : s.branch && s.state !== 'done'
397
+ ? [text(s.branch, { dimColor: true, wrap: 'truncate-end' })]
398
+ : []),
399
+ ],
400
+ });
401
+ });
402
+ return [
403
+ Box({
404
+ key: 'lane-cards',
405
+ flexDirection: 'row',
406
+ flexWrap: 'wrap',
407
+ columnGap: 1,
408
+ marginLeft: Math.min(indent + 2, 8),
409
+ children: cards.length
410
+ ? cards
411
+ : [text('No stories in the plan for this lane.', { dimColor: true })],
412
+ }),
413
+ Box({
414
+ key: 'lane-owns',
415
+ flexDirection: 'row',
416
+ flexWrap: 'wrap',
417
+ columnGap: 2,
418
+ marginLeft: Math.min(indent + 2, 8),
419
+ children: [
420
+ ...(detail.agent ? [text('agent', { dimColor: true }), text(detail.agent)] : []),
421
+ text('owns', { dimColor: true }),
422
+ ...(detail.owns.length
423
+ ? detail.owns.map((p) => text(p, { color: 'cyan' }))
424
+ : [text('nothing yet', { dimColor: true })]),
425
+ ],
426
+ }),
427
+ ];
428
+ }
429
+
430
+ /**
431
+ * LANES-1: the loop as a card at the top of the Lanes tab: its mode, then the steps it took, the one
432
+ * running and the one it takes next. None while the loop is off and has taken no step.
433
+ */
434
+ export function loopCard(loop, { Box, Text }) {
435
+ if (!loop || (loop.state === 'off' && !loop.steps?.length)) return [];
436
+ const text = (value, style = {}) => Text({ ...style, children: [value] });
437
+ const mode =
438
+ loop.state === 'paused'
439
+ ? { word: 'paused', color: 'yellow' }
440
+ : loop.state === 'off'
441
+ ? { word: 'off', color: 'gray' }
442
+ : loop.state === 'done' || loop.state === 'stopped'
443
+ ? { word: 'milestone done', color: 'green' }
444
+ : {
445
+ word:
446
+ loop.autonomy === 'propose'
447
+ ? 'asks before each step'
448
+ : 'acts while the lead is idle',
449
+ color: 'green',
450
+ };
451
+ // The step running now has its own chip; the ones before it are what the loop did.
452
+ const steps = [...(loop.steps ?? [])].slice(0, loop.current ? -1 : undefined).slice(-4);
453
+ const chip = (key, value, color, extra = {}) =>
454
+ Box({
455
+ key,
456
+ borderStyle: 'single',
457
+ borderColor: color,
458
+ paddingX: 1,
459
+ children: [text(value, { wrap: 'truncate-end', ...extra })],
460
+ });
461
+ return [
462
+ Box({
463
+ key: 'loop-card',
464
+ flexDirection: 'column',
465
+ borderStyle: 'round',
466
+ borderColor: 'gray',
467
+ paddingX: 1,
468
+ children: [
469
+ Box({
470
+ flexDirection: 'row',
471
+ columnGap: 2,
472
+ children: [
473
+ text('loop', { bold: true, color: 'yellow' }),
474
+ text(mode.word, { color: mode.color }),
475
+ ],
476
+ }),
477
+ Box({
478
+ flexDirection: 'row',
479
+ flexWrap: 'wrap',
480
+ columnGap: 1,
481
+ children: [
482
+ ...steps.map((s, i) =>
483
+ chip(
484
+ `loop-step-${i}`,
485
+ `${ago(loop.now - s.at).replace(' ago', '')} ${s.kind} ${s.target}`,
486
+ 'gray',
487
+ { dimColor: true },
488
+ ),
489
+ ),
490
+ ...(loop.nextText
491
+ ? [chip('loop-next', `next: ${loop.nextText}`, 'cyan', { color: 'cyan' })]
492
+ : []),
493
+ ],
494
+ }),
495
+ ],
496
+ }),
497
+ ];
498
+ }
499
+
350
500
  /** How long ago, as a person reads it: now, 40s ago, 12m ago, 2h ago. */
351
501
  export function ago(ms) {
352
502
  const s = Math.max(0, Math.round(ms / 1000));
@@ -425,32 +575,6 @@ export function reviewsSection(loop, text, Box) {
425
575
  ];
426
576
  }
427
577
 
428
- /** The loop's record (spec 05): its state and its latest steps, newest first. */
429
- export function loopSection(loop, text, Box) {
430
- if (!loop || (!loop.steps?.length && loop.state !== 'paused')) return [];
431
- const steps = [...(loop.steps ?? [])].reverse().slice(0, 8);
432
- return [
433
- Box({
434
- flexDirection: 'column',
435
- children: [
436
- text(`Loop · ${loop.state === 'paused' ? 'paused' : loop.autonomy}`, { bold: true }),
437
- ...steps.map((s, i) =>
438
- Box({
439
- key: `step-${i}`,
440
- flexDirection: 'row',
441
- columnGap: 2,
442
- children: [
443
- text(ago(loop.now - s.at).padEnd(8), { dimColor: true }),
444
- text(s.kind.padEnd(8)),
445
- text(s.target, { wrap: 'truncate-end' }),
446
- ],
447
- }),
448
- ),
449
- ],
450
- }),
451
- ];
452
- }
453
-
454
578
  /** USE-2: the plan's 5-hour use as a bar, each story's tokens, and the background agents' share. */
455
579
  export function usageSection(usage, plan, text, Box) {
456
580
  const lines = [];
@@ -560,8 +684,15 @@ export function bandLines({
560
684
  loop = null,
561
685
  queue = [],
562
686
  check = null,
687
+ leadBusy = false,
688
+ live = {},
689
+ nextStep = null,
690
+ combined = null,
563
691
  }) {
564
692
  const lines = [];
693
+ // MISSION-1: the goal, now, next and the agents at work, above what needs the person.
694
+ const mission = missionLines({ state, loop, leadBusy, live, nextStep, combined });
695
+ lines.push(...mission);
565
696
  if (notice)
566
697
  lines.push({ kind: 'notice', text: notice, actions: [{ id: 'dismiss', label: 'Dismiss' }] });
567
698
  if (!state || state.kind !== 'ok') return lines;
@@ -620,7 +751,13 @@ export function bandLines({
620
751
  request: open[0],
621
752
  actions: [{ id: 'approve', label: 'Approve…' }],
622
753
  });
623
- const inReview = state.lanes.filter((l) => l.state === 'in review').map((l) => l.story);
754
+ // DECIDE-1: reviewing a lane's work is the lead's. Only with the loop off, where the person runs the
755
+ // build themselves, does a finished story wait in the band for their Review or Merge; otherwise the
756
+ // loop prompts the lead (or, under propose, offers the step with Go).
757
+ const loopOff = (check?.harness?.autonomy ?? 'autonomous') === 'off';
758
+ const inReview = loopOff
759
+ ? state.lanes.filter((l) => l.state === 'in review').map((l) => l.story)
760
+ : [];
624
761
  if (inReview.length) {
625
762
  const [story] = inReview;
626
763
  const ids = inReview.map((s) => s.id).join(', ');
@@ -661,15 +798,13 @@ export function bandLines({
661
798
  });
662
799
  // MQ-2: where the lead doesn't merge, the head of the queue waits for the person's Merge.
663
800
  const next = queue.find((e) => ['waiting', 'merging'].includes(e.state));
664
- const leadMerges =
665
- check?.delegatesMerge && (check?.harness?.autonomy ?? 'autonomous') === 'autonomous';
801
+ const autonomous = (check?.harness?.autonomy ?? 'autonomous') === 'autonomous';
802
+ // SETUP-2: with no rules on the base yet, the lead puts them there first and then merges.
803
+ const leadMerges = autonomous && (check?.delegatesMerge || check?.firstMerge);
666
804
  if (next && !leadMerges)
667
805
  lines.push({
668
806
  kind: 'queue',
669
- // The first merge brings code-kit's rules onto the base, so it's the person's even under autonomy.
670
- text: check?.firstMerge
671
- ? `${next.branch} is next to merge: the first merge is yours, then the lead merges`
672
- : `${next.branch} is next to merge`,
807
+ text: `${next.branch} is next to merge`,
673
808
  branch: next.branch,
674
809
  actions: [{ id: 'mergeQueue', label: 'Merge' }],
675
810
  });
@@ -680,7 +815,10 @@ export function bandLines({
680
815
  text: `Milestone done: ${plural(loop.doneCount, 'story', 'stories')} merged`,
681
816
  actions: [{ id: 'dismissDone', label: 'Dismiss' }],
682
817
  });
683
- else if (loop?.state === 'paused')
818
+ // With the mission rows drawn, the loop's state and its Pause or Resume are on the `now` row.
819
+ else if (mission.length) {
820
+ // nothing more
821
+ } else if (loop?.state === 'paused')
684
822
  lines.push({ kind: 'loop', text: 'loop paused', actions: [{ id: 'resume', label: 'Resume' }] });
685
823
  else if (loop?.state === 'running' && loop.started)
686
824
  lines.push({
@@ -710,31 +848,101 @@ export function band(lines, { Box, Text, Button }, onAction) {
710
848
  finish: 'red',
711
849
  usage: 'yellow',
712
850
  paused: 'yellow',
851
+ facts: 'yellow',
713
852
  };
714
- return Box({
715
- flexDirection: 'column',
716
- children: lines.map((line) =>
717
- Box({
718
- key: `band-${line.kind}`,
719
- flexDirection: 'row',
720
- columnGap: 2,
853
+ const LIVE = { quiet: 'yellow', stalled: 'red' };
854
+ const LABEL = 8;
855
+ const buttons = (line) =>
856
+ line.actions.map((a) =>
857
+ Button({
858
+ key: `band-${a.id}`,
859
+ label: a.hotkey ? `${a.hotkey} ${a.label}` : a.label,
860
+ hotkey: a.hotkey,
861
+ onPress: () => onAction(a.id, line),
862
+ }),
863
+ );
864
+ // MISSION-2: what waits on the person is labelled "you", once, above its first line.
865
+ let saidYou = false;
866
+ const rows = lines.map((line) => {
867
+ if (line.kind === 'agents')
868
+ return Box({
869
+ key: 'band-agents',
870
+ flexDirection: 'column',
721
871
  children: [
722
- Text({ color: COLOR[line.kind], children: ['code-kit'] }),
723
- Text({ children: [line.text] }),
724
- ...line.actions.map((a) =>
725
- Button({
726
- key: `band-${a.id}`,
727
- label: a.hotkey ? `${a.hotkey} ${a.label}` : a.label,
728
- hotkey: a.hotkey,
729
- onPress: () => onAction(a.id, line),
872
+ ...line.agents.map((a) =>
873
+ Box({
874
+ key: `band-agent-${a.lane}`,
875
+ flexDirection: 'row',
876
+ columnGap: 1,
877
+ children: [
878
+ Text({
879
+ dimColor: true,
880
+ children: [(a === line.agents[0] ? line.label : '').padEnd(LABEL)],
881
+ }),
882
+ Text({ color: a.level ? LIVE[a.level] : 'green', children: ['●'] }),
883
+ Text({ bold: true, children: [a.lane.padEnd(8)] }),
884
+ Text({ wrap: 'truncate-end', children: [a.tool] }),
885
+ Text({ dimColor: true, children: [a.ago] }),
886
+ ],
730
887
  }),
731
888
  ),
889
+ ...(line.more
890
+ ? [Text({ dimColor: true, children: [`${''.padEnd(LABEL)} and ${line.more} more`] })]
891
+ : []),
732
892
  ],
733
- }),
734
- ),
893
+ });
894
+ // DECIDE-3: what needs the person is labelled "you", once; notes (usage, the plan's pause) are "note".
895
+ const note = NOTE_KINDS.has(line.kind);
896
+ const label = line.mission ? line.label : note ? 'note' : saidYou ? '' : 'you';
897
+ if (!line.mission && !note) saidYou = true;
898
+ return Box({
899
+ key: `band-${line.kind}`,
900
+ flexDirection: 'row',
901
+ columnGap: 1,
902
+ children: [
903
+ Text({
904
+ ...(line.mission ? { dimColor: true } : { color: COLOR[line.kind] ?? 'yellow' }),
905
+ children: [label.padEnd(LABEL)],
906
+ }),
907
+ Text({
908
+ wrap: 'truncate-end',
909
+ ...(line.kind === 'facts' ? { color: COLOR.facts } : {}),
910
+ children: [line.text],
911
+ }),
912
+ // DECIDE-2: which story it's for, so it isn't a fragment.
913
+ ...(line.context
914
+ ? [Text({ dimColor: true, wrap: 'truncate-end', children: [line.context] })]
915
+ : []),
916
+ ...buttons(line),
917
+ ],
918
+ });
735
919
  });
920
+ // DECIDE-3: one key opens the decisions as cards, each with its lineage and what each choice does.
921
+ const first = lines.find((l) => !l.mission && !NOTE_KINDS.has(l.kind));
922
+ if (first)
923
+ rows.push(
924
+ Box({
925
+ key: 'band-why',
926
+ flexDirection: 'row',
927
+ columnGap: 1,
928
+ children: [
929
+ Text({ children: [''.padEnd(LABEL)] }),
930
+ Button({
931
+ key: 'band-decide',
932
+ label: 'Why, and what each choice does',
933
+ hotkey: 'd',
934
+ plain: true,
935
+ onPress: () => onAction('decide', first),
936
+ }),
937
+ ],
938
+ }),
939
+ );
940
+ return Box({ flexDirection: 'column', children: rows });
736
941
  }
737
942
 
943
+ /** Band lines that inform rather than ask: they never need the person's answer. */
944
+ const NOTE_KINDS = new Set(['usage', 'paused', 'finish', 'done']);
945
+
738
946
  /** The Approve… confirmation (ACT-1): what it grants, to whom, for how long, and the reason. */
739
947
  export function approvePane(
740
948
  approving,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warren-dean/code-kit",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
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",
@@ -67,7 +67,7 @@ Write `.claude/code-kit.draft.json`. Follow `${CLAUDE_PLUGIN_ROOT}/README.md`
67
67
  - **`shell.restricted`:** commands that affect a shared local service, limited to the lanes that own it.
68
68
  - **`postEdit`:** the project's existing per-file linters and formatters, with `{file}`.
69
69
  - **`checks`:** the typecheck, test, architecture and database commands, each scoped by `files`. Only include commands that pass on the current code, or ones the person chose to keep.
70
- - **`harness.autonomy`:** ask the person how hands-off they want the build. With the code-kit mod (Claude Code 2.1.287 or later), once the plan has stories, a lead loop can keep the work moving between their prompts: it prompts the lead to dispatch ready stories, review finished ones and merge them where merges are delegated. Offer the three choices in plain words: `autonomous` (it keeps going on its own; they can pause it from the band), `propose` (each step waits in the band for their Go), or `off`. Put their answer in the draft as `"harness": { "autonomy": "…" }`. If they'd rather decide later, leave it out: the loop then asks them in the band before its first step.
70
+ - **`harness.autonomy`:** ask the person how hands-off they want the build. With the code-kit mod (Claude Code 2.1.287 or later), once the plan has stories, a lead loop can keep the work moving between their prompts: it prompts the lead to dispatch ready stories, review finished ones and merge them where merges are delegated. Offer the three choices in plain words: `autonomous` (it keeps going on its own; they can pause it from the band), `propose` (each step waits in the band for their Go), or `off`. Put their answer in the draft as `"harness": { "autonomy": "…" }`. If they'd rather decide later, leave it out: the loop then asks them in the band before its first step. With `autonomous`, the lead owns delivery, so also draft `approvals.delegate`: `"merge": true` (the lead merges a reviewed branch once every check passes) and `"dependencies": {}` (it approves a new package within the default limits on licence, popularity and age), so neither comes to the person. Say these are the limits; anything outside them still comes to them as a decision.
71
71
  - **`approvals.lead`:** ask the person whether they'll ever approve from a phone or another chat-only client, where a `!` command arrives as plain text. If so, set it to `true`: the lead then records approvals they give in chat (`code-kit approve`), and runs the `shell.block` commands marked `person: true` when they say so. Mark a `shell.block` command `person: true` only if the person wants to be able to have the lead run it on their say-so. Leave the setting out if the lead will run unattended.
72
72
 
73
73
  Prove the draft:
@@ -124,9 +124,9 @@ Once they approve (with any changes):
124
124
  - make sure the lead owns the workflow file, and add it to `protected` (approval `ci`, why "the CI that enforces the rules"), so a lane can't switch it off;
125
125
  - tell the person to make the `code-kit` check required on the protected branches.
126
126
  7. Run `check` and `unowned` again, without `--config`.
127
- 8. Commit on a branch. The kit records the protected files in `.claude/approval-log.jsonl`.
127
+ 8. Commit the setup straight onto the base branch (usually `main`), not a branch: while the base has no `.claude/code-kit.json` committed, the hooks let the lead make that one commit, so the rules the person just approved are in force on `main` and the lead can merge reviewed work from the start, with no first merge for anyone to make. The kit records the protected files in `.claude/approval-log.jsonl`. If the base already has rules (an update, section 4), commit on a branch as usual.
128
128
 
129
- Report what was set up, what's enforced from now on, how many existing violations the baseline holds, and the open questions. If the base branch had no `.claude/code-kit.json` committed before, say that the first merge into it is the person's, since that merge brings the rules onto it: once the branch has passed review, they press Merge in the code-kit band, or run `! node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" queue merge --person`. After that, the rules committed there decide who merges.
129
+ Report what was set up, what's enforced from now on, how many existing violations the baseline holds, and the open questions. Say who does what from here: the person owns the intent and the rules; the lead owns delivery, so it dispatches, reviews, sends back and merges, and approves within the delegated limits, and only a real decision (scope, something outside those limits, a problem it couldn't solve) comes to the person.
130
130
 
131
131
  If the project has no `.ctx/` folder, end the report with one line about the companion plugin. If Context Graph is already installed (its `/context-graph:init` command is available to you), offer to run it now: "Context Graph is installed but has no graph here yet. `/context-graph:init` gives each lane agent the why behind the files it edits (cards, rules and decisions kept in git), with this spec's requirements and layer rules in them. Shall I run it?" If it isn't installed, don't install it unless the person asks: "Context Graph can give each lane agent the why behind the files it edits (cards, rules and decisions kept in git), with this spec's requirements and layer rules in them: `/plugin marketplace add warrendeanlangeveldt/context-graph`, then `/plugin install context-graph@context-graph`."
132
132