@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +33 -0
- package/README.md +15 -3
- package/bin/code-kit.mjs +171 -3
- package/hooks/guard-bash.mjs +29 -2
- package/hooks/lib/adapters/context-graph.mjs +18 -19
- package/hooks/lib/journal.mjs +224 -0
- package/hooks/lib/loop-state.mjs +26 -0
- package/hooks/lib/next.mjs +13 -3
- package/hooks/lib/plan.mjs +8 -1
- package/hooks/lib/storymap.mjs +155 -0
- package/hooks/mod/codemap.mjs +328 -0
- package/hooks/mod/decisions.mjs +146 -0
- package/hooks/mod/loop.mjs +7 -3
- package/hooks/mod/mission.mjs +116 -0
- package/hooks/mod/panes.mjs +469 -34
- package/hooks/mod/register.mjs +196 -2
- package/hooks/mod/register.test.ts +456 -25
- package/hooks/mod/trail.mjs +152 -0
- package/hooks/mod/view.mjs +324 -116
- package/package.json +1 -1
- package/skills/init/SKILL.md +3 -3
package/hooks/mod/view.mjs
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
230
|
-
|
|
267
|
+
s.padEnd(STATE_WIDTH),
|
|
268
|
+
s === 'queued'
|
|
231
269
|
? { color: 'cyan' }
|
|
232
|
-
: STATE_COLOR[
|
|
233
|
-
? { color: STATE_COLOR[
|
|
234
|
-
: {},
|
|
270
|
+
: STATE_COLOR[s]
|
|
271
|
+
? { color: STATE_COLOR[s] }
|
|
272
|
+
: { dimColor: true },
|
|
235
273
|
),
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
|
665
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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.
|
|
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",
|
package/skills/init/SKILL.md
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|