@warren-dean/code-kit 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +31 -0
- package/README.md +35 -28
- package/bin/code-kit.mjs +114 -14
- package/hooks/guard-bash.mjs +84 -29
- package/hooks/lane-audit.mjs +17 -6
- package/hooks/lib/config.mjs +33 -5
- package/hooks/lib/diff.mjs +5 -1
- package/hooks/lib/harness.mjs +131 -0
- package/hooks/lib/next.mjs +13 -2
- package/hooks/lib/plan.mjs +18 -7
- package/hooks/lib/registry.mjs +37 -3
- package/hooks/lib/reviews.mjs +39 -0
- package/hooks/lib/rules.mjs +66 -4
- package/hooks/lib/shell.mjs +94 -0
- package/hooks/lib/trace.mjs +4 -2
- package/hooks/lib/verify.mjs +74 -3
- package/hooks/mod/hold.mjs +25 -0
- package/hooks/mod/register.mjs +129 -4
- package/hooks/mod/register.test.ts +305 -4
- package/hooks/mod/view.mjs +355 -11
- package/package.json +1 -1
- package/skills/init/SKILL.md +10 -2
- package/skills/review/SKILL.md +9 -1
- package/templates/agent.md +1 -1
package/hooks/mod/view.mjs
CHANGED
|
@@ -34,6 +34,7 @@ export function readyFrom(next) {
|
|
|
34
34
|
* One lane's row (PANE-2): the story it's on, its branch and its state.
|
|
35
35
|
* building a branch for the story and, if it has commits, the lane's agent still at work on it;
|
|
36
36
|
* in review commits on the branch (status's `review`) and the agent no longer at work;
|
|
37
|
+
* sent back a review sent the branch back, and the lane hasn't committed its fix yet;
|
|
37
38
|
* blocked the lane's next story waits on another;
|
|
38
39
|
* idle none of these.
|
|
39
40
|
* `status` can't tell a finished branch from one still being built, so the agent at work decides.
|
|
@@ -41,7 +42,9 @@ export function readyFrom(next) {
|
|
|
41
42
|
function laneRow(name, agent, stories, atWork) {
|
|
42
43
|
const own = stories.filter((s) => s.lane === name);
|
|
43
44
|
const active = Boolean(agent && atWork.has(agent));
|
|
44
|
-
const story = own.find((s) => ['in progress', 'review'].includes(s.state));
|
|
45
|
+
const story = own.find((s) => ['in progress', 'review', 'sent back'].includes(s.state));
|
|
46
|
+
if (story?.state === 'sent back')
|
|
47
|
+
return { name, agent, active, story, branch: story.branch, state: 'sent back' };
|
|
45
48
|
if (story) {
|
|
46
49
|
const state = story.state === 'review' && !active ? 'in review' : 'building';
|
|
47
50
|
return { name, agent, active, story, branch: story.branch, state };
|
|
@@ -58,7 +61,8 @@ function laneRow(name, agent, stories, atWork) {
|
|
|
58
61
|
* subagent types at work:
|
|
59
62
|
* { kind: 'none' } no .claude/code-kit.json
|
|
60
63
|
* { kind: 'invalid', problems } a config that doesn't validate
|
|
61
|
-
* { kind: 'ok', lanes, ready, planGaps }
|
|
64
|
+
* { kind: 'ok', lanes, ready, planGaps, stories } a row per lane, what's ready, the plan's gaps (or
|
|
65
|
+
* null), and each story's id, lane and state
|
|
62
66
|
*/
|
|
63
67
|
export function projectState(check, status, next = null, atWork = new Set()) {
|
|
64
68
|
if (!check || check.exists === false) return { kind: 'none' };
|
|
@@ -74,13 +78,28 @@ export function projectState(check, status, next = null, atWork = new Set()) {
|
|
|
74
78
|
const story = stories.find((s) => s.id === id);
|
|
75
79
|
return { id, title: story?.title ?? '', lane: story?.lane ?? null };
|
|
76
80
|
});
|
|
77
|
-
return {
|
|
81
|
+
return {
|
|
82
|
+
kind: 'ok',
|
|
83
|
+
lanes,
|
|
84
|
+
ready,
|
|
85
|
+
planGaps: status ? (status.problems ?? []).length : null,
|
|
86
|
+
stories: stories.map((s) => ({ id: s.id, lane: s.lane, state: s.state })),
|
|
87
|
+
};
|
|
78
88
|
}
|
|
79
89
|
|
|
80
|
-
const STATE_COLOR = {
|
|
90
|
+
const STATE_COLOR = {
|
|
91
|
+
building: 'blue',
|
|
92
|
+
'in review': 'yellow',
|
|
93
|
+
'sent back': 'magenta',
|
|
94
|
+
blocked: 'red',
|
|
95
|
+
idle: undefined,
|
|
96
|
+
};
|
|
81
97
|
|
|
82
|
-
/**
|
|
83
|
-
|
|
98
|
+
/**
|
|
99
|
+
* The Lanes pane's body, drawn with the elements `$.ui.resolve(e)` returned; `usage` is
|
|
100
|
+
* usageSummary's, `plan` planOf's (USE-2), either null before there is any.
|
|
101
|
+
*/
|
|
102
|
+
export function lanesPane(state, { Box, Text }, usage = null, plan = null) {
|
|
84
103
|
const text = (value, style = {}) => Text({ ...style, children: [value] });
|
|
85
104
|
if (!state || state.kind === 'none') return text(NOT_CODE_KIT, { dimColor: true });
|
|
86
105
|
if (state.kind === 'invalid')
|
|
@@ -99,6 +118,9 @@ export function lanesPane(state, { Box, Text }) {
|
|
|
99
118
|
text(l.name.padEnd(width), { bold: true }),
|
|
100
119
|
text(l.state.padEnd(9), STATE_COLOR[l.state] ? { color: STATE_COLOR[l.state] } : {}),
|
|
101
120
|
text(l.agent ?? 'the main session', { dimColor: true }),
|
|
121
|
+
...(usage?.lanes[l.name]
|
|
122
|
+
? [text(tokens(usage.lanes[l.name]), { dimColor: true })]
|
|
123
|
+
: []),
|
|
102
124
|
],
|
|
103
125
|
}),
|
|
104
126
|
...(l.story
|
|
@@ -143,10 +165,59 @@ export function lanesPane(state, { Box, Text }) {
|
|
|
143
165
|
...notes,
|
|
144
166
|
Box({ flexDirection: 'column', children: rows }),
|
|
145
167
|
Box({ flexDirection: 'column', children: ready }),
|
|
168
|
+
...usageSection(usage, plan, text, Box),
|
|
146
169
|
],
|
|
147
170
|
});
|
|
148
171
|
}
|
|
149
172
|
|
|
173
|
+
/** USE-2: the plan's 5-hour use as a bar, each story's tokens, and the background agents' share. */
|
|
174
|
+
function usageSection(usage, plan, text, Box) {
|
|
175
|
+
const lines = [];
|
|
176
|
+
if (plan?.percent != null)
|
|
177
|
+
lines.push(
|
|
178
|
+
Box({
|
|
179
|
+
key: 'usage-plan',
|
|
180
|
+
flexDirection: 'row',
|
|
181
|
+
columnGap: 2,
|
|
182
|
+
children: [
|
|
183
|
+
text('Plan'),
|
|
184
|
+
text(planBar(plan.percent), { color: plan.paused ? 'red' : 'green' }),
|
|
185
|
+
text(`${plan.percent}% of the 5-hour window`, { dimColor: true }),
|
|
186
|
+
...(plan.paused ? [text('background agents paused', { color: 'red' })] : []),
|
|
187
|
+
],
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
if (usage?.total) {
|
|
191
|
+
const flagged = new Map(usage.outliers.map((o) => [o.id, o.ratio]));
|
|
192
|
+
for (const [id, n] of Object.entries(usage.stories).sort((a, b) => b[1] - a[1]))
|
|
193
|
+
lines.push(
|
|
194
|
+
Box({
|
|
195
|
+
key: `usage-${id}`,
|
|
196
|
+
flexDirection: 'row',
|
|
197
|
+
columnGap: 2,
|
|
198
|
+
children: [
|
|
199
|
+
text(id),
|
|
200
|
+
text(tokens(n), { dimColor: true }),
|
|
201
|
+
...(flagged.has(id)
|
|
202
|
+
? [text(`${flagged.get(id)}× the usual`, { color: 'yellow' })]
|
|
203
|
+
: []),
|
|
204
|
+
],
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
if (usage.background)
|
|
208
|
+
lines.push(
|
|
209
|
+
text(
|
|
210
|
+
`Background agents ${tokens(usage.background)} (${Math.round((usage.background / usage.total) * 100)}%)`,
|
|
211
|
+
{ dimColor: true },
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
lines.push(text(`This session ${tokens(usage.total)} tokens`, { dimColor: true }));
|
|
215
|
+
}
|
|
216
|
+
return lines.length
|
|
217
|
+
? [Box({ flexDirection: 'column', children: [text('Usage', { bold: true }), ...lines] })]
|
|
218
|
+
: [];
|
|
219
|
+
}
|
|
220
|
+
|
|
150
221
|
// --- the band, and the person's acts -----------------------------------------------------------
|
|
151
222
|
|
|
152
223
|
export const APPROVE_ID = 'code-kit-approve';
|
|
@@ -176,9 +247,18 @@ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
|
176
247
|
/**
|
|
177
248
|
* The band's lines (BAND-2): one per kind of thing waiting, each with its actions, and none when
|
|
178
249
|
* nothing waits. `reviewing` holds the stories the person has asked the lead to review (ACT-3);
|
|
179
|
-
* `notice` is why the person's last act failed, until they dismiss it.
|
|
250
|
+
* `notice` is why the person's last act failed, until they dismiss it. `usage` (usageSummary's) flags
|
|
251
|
+
* outliers (USE-3), and `plan` (planOf's) says when background agents are paused (USE-4).
|
|
180
252
|
*/
|
|
181
|
-
export function bandLines({
|
|
253
|
+
export function bandLines({
|
|
254
|
+
state,
|
|
255
|
+
requests,
|
|
256
|
+
stops,
|
|
257
|
+
reviewing = new Set(),
|
|
258
|
+
notice = null,
|
|
259
|
+
usage = null,
|
|
260
|
+
plan = null,
|
|
261
|
+
}) {
|
|
182
262
|
const lines = [];
|
|
183
263
|
if (notice)
|
|
184
264
|
lines.push({ kind: 'notice', text: notice, actions: [{ id: 'dismiss', label: 'Dismiss' }] });
|
|
@@ -215,6 +295,21 @@ export function bandLines({ state, requests, stops, reviewing = new Set(), notic
|
|
|
215
295
|
text: `Finish check failing: ${stops[0].title}${stops.length > 1 ? ` (and ${stops.length - 1} more)` : ''}`,
|
|
216
296
|
actions: [{ id: 'lanes', label: 'Lanes' }],
|
|
217
297
|
});
|
|
298
|
+
if (usage?.outliers.length) {
|
|
299
|
+
const [top] = usage.outliers;
|
|
300
|
+
const more = usage.outliers.length - 1;
|
|
301
|
+
lines.push({
|
|
302
|
+
kind: 'usage',
|
|
303
|
+
text: `${top.id} has used ${top.ratio}× the usual${more ? ` (and ${more} more)` : ''}`,
|
|
304
|
+
actions: [{ id: 'lanes', label: 'Lanes' }],
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (plan?.paused)
|
|
308
|
+
lines.push({
|
|
309
|
+
kind: 'paused',
|
|
310
|
+
text: `background agents paused: plan at ${plan.percent}%`,
|
|
311
|
+
actions: [],
|
|
312
|
+
});
|
|
218
313
|
// BAND-4: a digit for every action, in the order they're drawn.
|
|
219
314
|
let digit = 0;
|
|
220
315
|
for (const line of lines)
|
|
@@ -224,7 +319,14 @@ export function bandLines({ state, requests, stops, reviewing = new Set(), notic
|
|
|
224
319
|
|
|
225
320
|
/** The band, drawn with the elements `$.ui.resolve(e)` returned; `onAction(id, line)` acts. */
|
|
226
321
|
export function band(lines, { Box, Text, Button }, onAction) {
|
|
227
|
-
const COLOR = {
|
|
322
|
+
const COLOR = {
|
|
323
|
+
notice: 'red',
|
|
324
|
+
approvals: 'yellow',
|
|
325
|
+
review: 'blue',
|
|
326
|
+
finish: 'red',
|
|
327
|
+
usage: 'yellow',
|
|
328
|
+
paused: 'yellow',
|
|
329
|
+
};
|
|
228
330
|
return Box({
|
|
229
331
|
flexDirection: 'column',
|
|
230
332
|
children: lines.map((line) =>
|
|
@@ -263,7 +365,7 @@ export function approvePane(
|
|
|
263
365
|
Text({ bold: true, children: [`Approve ${request.names.join(', ')}`] }),
|
|
264
366
|
Text({
|
|
265
367
|
children: [
|
|
266
|
-
`For ${request.lane ? `the ${request.lane} lane` : 'any agent'}, for 60 minutes. Asked for: ${request.what}`,
|
|
368
|
+
`For ${request.lane ? `the ${request.lane} lane` : 'any agent'}, ${request.names.every((n) => n.startsWith('dep-')) ? 'until the install is committed (at most 7 days)' : 'for 60 minutes'}. Asked for: ${request.what}`,
|
|
267
369
|
],
|
|
268
370
|
}),
|
|
269
371
|
Input({
|
|
@@ -342,13 +444,32 @@ export function refusalCard(raw) {
|
|
|
342
444
|
request: request(`write ${m[1]}`),
|
|
343
445
|
raw: text,
|
|
344
446
|
};
|
|
447
|
+
m = first.match(/^(\S+) is (.+); committing it needs a person's approval in force\./);
|
|
448
|
+
if (m)
|
|
449
|
+
return {
|
|
450
|
+
kind: 'approval',
|
|
451
|
+
title: `Commit refused: ${m[1]}`,
|
|
452
|
+
why: `It's ${m[2]}.`,
|
|
453
|
+
todo: 'A person approves it, here or with the `!` command, for 60 minutes.',
|
|
454
|
+
request: request(`commit ${m[1]}`),
|
|
455
|
+
raw: text,
|
|
456
|
+
};
|
|
457
|
+
if (/^the \.claude kit is changed only by the lead or with a person's approval\./.test(first))
|
|
458
|
+
return {
|
|
459
|
+
kind: 'kit',
|
|
460
|
+
title: 'Kit edit refused',
|
|
461
|
+
why: "The .claude kit is changed only by the lead or with a person's approval.",
|
|
462
|
+
todo: 'A person approves it, here or with the `!` command, for 60 minutes.',
|
|
463
|
+
request: request('change the .claude kit'),
|
|
464
|
+
raw: text,
|
|
465
|
+
};
|
|
345
466
|
m = first.match(/^a new dependency \((.+?)\) needs a person's approval\./);
|
|
346
467
|
if (m)
|
|
347
468
|
return {
|
|
348
469
|
kind: 'dependency',
|
|
349
470
|
title: `Install refused: ${m[1]}`,
|
|
350
471
|
why: "A new dependency needs a person's approval.",
|
|
351
|
-
todo: 'A person approves it, here or with the `!` command,
|
|
472
|
+
todo: 'A person approves it, here or with the `!` command, until the install is committed (at most 7 days).',
|
|
352
473
|
request: request(command ?? `install ${m[1]}`),
|
|
353
474
|
raw: text,
|
|
354
475
|
};
|
|
@@ -416,3 +537,226 @@ export function approvalsText(requests) {
|
|
|
416
537
|
];
|
|
417
538
|
return lines.join('\n');
|
|
418
539
|
}
|
|
540
|
+
|
|
541
|
+
// --- the harness settings (SET-2) --------------------------------------------------------------
|
|
542
|
+
|
|
543
|
+
export const SETTINGS_ID = 'code-kit-settings';
|
|
544
|
+
const CHOICES = {
|
|
545
|
+
autonomy: ['autonomous', 'propose', 'off'],
|
|
546
|
+
'agents.reviewer.model': ['', 'haiku', 'sonnet', 'opus'],
|
|
547
|
+
};
|
|
548
|
+
const shown = (v) =>
|
|
549
|
+
v === null || v === '' ? "the session's" : v === true ? 'on' : v === false ? 'off' : String(v);
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* The Settings view: each setting from `code-kit settings --json` with a control, and, once one is
|
|
553
|
+
* changed, a confirmation asking the person's reason before anything is written.
|
|
554
|
+
*/
|
|
555
|
+
export function settingsView(rows, pending, { Box, Text, Select, Input, Button }, handlers) {
|
|
556
|
+
const control = (r) => {
|
|
557
|
+
const listed = CHOICES[r.key] ?? (typeof r.default === 'boolean' ? [true, false] : null);
|
|
558
|
+
// A value the list doesn't name (a model given by its id) is still shown, as the one chosen.
|
|
559
|
+
const choices =
|
|
560
|
+
listed && !listed.map(String).includes(String(r.value ?? '')) ? [...listed, r.value] : listed;
|
|
561
|
+
if (choices)
|
|
562
|
+
return Select({
|
|
563
|
+
key: `set-${r.key}`,
|
|
564
|
+
label: '',
|
|
565
|
+
value: String(r.value ?? ''),
|
|
566
|
+
options: choices.map((c) => ({ value: String(c), label: shown(c) })),
|
|
567
|
+
onSelect: (v) => handlers.onChoose(r.key, v === '' ? 'null' : v),
|
|
568
|
+
});
|
|
569
|
+
return Input({
|
|
570
|
+
key: `set-${r.key}`,
|
|
571
|
+
label: '',
|
|
572
|
+
value: String(r.value),
|
|
573
|
+
submitLabel: 'Set',
|
|
574
|
+
onSubmit: (v) => handlers.onChoose(r.key, v.trim()),
|
|
575
|
+
});
|
|
576
|
+
};
|
|
577
|
+
const rowsView = rows.map((r) =>
|
|
578
|
+
Box({
|
|
579
|
+
key: `setting-${r.key}`,
|
|
580
|
+
flexDirection: 'column',
|
|
581
|
+
children: [
|
|
582
|
+
Box({
|
|
583
|
+
flexDirection: 'row',
|
|
584
|
+
columnGap: 2,
|
|
585
|
+
children: [
|
|
586
|
+
Box({ width: 28, children: [Text({ bold: true, children: [r.key] })] }),
|
|
587
|
+
control(r),
|
|
588
|
+
...(r.value !== r.default
|
|
589
|
+
? [Text({ dimColor: true, children: [`default ${shown(r.default)}`] })]
|
|
590
|
+
: []),
|
|
591
|
+
],
|
|
592
|
+
}),
|
|
593
|
+
Text({ dimColor: true, children: [` ${r.about}`] }),
|
|
594
|
+
],
|
|
595
|
+
}),
|
|
596
|
+
);
|
|
597
|
+
const confirm = pending
|
|
598
|
+
? [
|
|
599
|
+
Box({
|
|
600
|
+
key: 'settings-confirm',
|
|
601
|
+
flexDirection: 'column',
|
|
602
|
+
borderStyle: 'round',
|
|
603
|
+
paddingX: 1,
|
|
604
|
+
children: [
|
|
605
|
+
Text({
|
|
606
|
+
bold: true,
|
|
607
|
+
children: [
|
|
608
|
+
`Set ${pending.key} to ${shown(pending.value === 'null' ? null : pending.value)}?`,
|
|
609
|
+
],
|
|
610
|
+
}),
|
|
611
|
+
Text({
|
|
612
|
+
dimColor: true,
|
|
613
|
+
children: [
|
|
614
|
+
'It goes into .claude/code-kit.json, and the approval log keeps your reason.',
|
|
615
|
+
],
|
|
616
|
+
}),
|
|
617
|
+
Input({
|
|
618
|
+
key: 'settings-reason',
|
|
619
|
+
label: 'Reason',
|
|
620
|
+
value: pending.reason,
|
|
621
|
+
submitLabel: 'Change it',
|
|
622
|
+
autoFocus: true,
|
|
623
|
+
onInput: handlers.onReason,
|
|
624
|
+
onSubmit: handlers.onConfirm,
|
|
625
|
+
}),
|
|
626
|
+
...(pending.error ? [Text({ color: 'red', children: [pending.error] })] : []),
|
|
627
|
+
Box({
|
|
628
|
+
flexDirection: 'row',
|
|
629
|
+
columnGap: 2,
|
|
630
|
+
children: [
|
|
631
|
+
Button({
|
|
632
|
+
key: 'settings-ok',
|
|
633
|
+
label: 'Change it',
|
|
634
|
+
onPress: () => handlers.onConfirm(pending.reason),
|
|
635
|
+
}),
|
|
636
|
+
Button({ key: 'settings-cancel', label: 'Cancel', onPress: handlers.onCancel }),
|
|
637
|
+
],
|
|
638
|
+
}),
|
|
639
|
+
],
|
|
640
|
+
}),
|
|
641
|
+
]
|
|
642
|
+
: [];
|
|
643
|
+
return Box({
|
|
644
|
+
flexDirection: 'column',
|
|
645
|
+
rowGap: 1,
|
|
646
|
+
children: [
|
|
647
|
+
Text({ bold: true, color: 'cyan', children: ['HARNESS SETTINGS'] }),
|
|
648
|
+
...confirm,
|
|
649
|
+
Box({ flexDirection: 'column', children: rowsView }),
|
|
650
|
+
Text({
|
|
651
|
+
dimColor: true,
|
|
652
|
+
children: ['Lanes, paths, layers and protected paths change through /code-kit:init.'],
|
|
653
|
+
}),
|
|
654
|
+
],
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// --- usage per lane (USE-1 to USE-4) -----------------------------------------------------------
|
|
659
|
+
|
|
660
|
+
const STORY_ID = /\bST-\d+\b/;
|
|
661
|
+
|
|
662
|
+
/** A model request's tokens: input (cache writes included), output and cache reads. */
|
|
663
|
+
export function tokensOf(usage) {
|
|
664
|
+
return {
|
|
665
|
+
input: (usage?.input_tokens ?? 0) + (usage?.cache_creation_input_tokens ?? 0),
|
|
666
|
+
output: usage?.output_tokens ?? 0,
|
|
667
|
+
cache: usage?.cache_read_input_tokens ?? 0,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Whom a model request is for (USE-1): the lead when no agent made it; a lane's agent by its type,
|
|
673
|
+
* on the story its brief names or else the story its lane is on; any other agent is background.
|
|
674
|
+
* `agent` is the `$.agent.list()` entry, or null when the list no longer has it.
|
|
675
|
+
*/
|
|
676
|
+
export function attribution(agentId, agent, check, state) {
|
|
677
|
+
const laneStory = (name) => state?.lanes?.find((l) => l.name === name)?.story?.id ?? null;
|
|
678
|
+
if (!agentId) return { key: 'lead', lane: LEAD, story: laneStory(LEAD), background: false };
|
|
679
|
+
const lane = Object.entries(check?.lanes ?? {}).find(([, l]) => l.agent === agent?.type)?.[0];
|
|
680
|
+
const named = agent?.description?.match(STORY_ID)?.[0] ?? null;
|
|
681
|
+
if (!lane) return { key: agentId, lane: null, story: named, background: true };
|
|
682
|
+
return { key: agentId, lane, story: named ?? laneStory(lane), background: false };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** The ledger with one request added: entries by agent and story, their tokens summed. */
|
|
686
|
+
export function withUsage(ledger, who, usage) {
|
|
687
|
+
const id = `${who.key}|${who.story ?? ''}`;
|
|
688
|
+
const t = tokensOf(usage);
|
|
689
|
+
const was = ledger[id] ?? { ...who, input: 0, output: 0, cache: 0 };
|
|
690
|
+
return {
|
|
691
|
+
...ledger,
|
|
692
|
+
[id]: {
|
|
693
|
+
...was,
|
|
694
|
+
input: was.input + t.input,
|
|
695
|
+
output: was.output + t.output,
|
|
696
|
+
cache: was.cache + t.cache,
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const total = (e) => e.input + e.output + e.cache;
|
|
702
|
+
const median = (xs) => {
|
|
703
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
704
|
+
const m = Math.floor(s.length / 2);
|
|
705
|
+
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
/** The stories finished: done, or their lane's branch waiting for review. */
|
|
709
|
+
export function finishedStories(state) {
|
|
710
|
+
if (state?.kind !== 'ok') return new Set();
|
|
711
|
+
return new Set([
|
|
712
|
+
...(state.stories ?? []).filter((s) => s.state === 'done').map((s) => s.id),
|
|
713
|
+
...state.lanes.filter((l) => l.state === 'in review').map((l) => l.story.id),
|
|
714
|
+
]);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* The session's usage rolled up (USE-1, USE-2): tokens per lane, per story and for the background
|
|
719
|
+
* agents, and the outliers (USE-3), stories past three times the median of the finished ones once
|
|
720
|
+
* at least three have finished.
|
|
721
|
+
*/
|
|
722
|
+
export function usageSummary(ledger, finished = new Set()) {
|
|
723
|
+
const lanes = {};
|
|
724
|
+
const stories = {};
|
|
725
|
+
let background = 0;
|
|
726
|
+
let all = 0;
|
|
727
|
+
for (const e of Object.values(ledger ?? {})) {
|
|
728
|
+
const n = total(e);
|
|
729
|
+
all += n;
|
|
730
|
+
if (e.background) background += n;
|
|
731
|
+
else lanes[e.lane] = (lanes[e.lane] ?? 0) + n;
|
|
732
|
+
if (e.story) stories[e.story] = (stories[e.story] ?? 0) + n;
|
|
733
|
+
}
|
|
734
|
+
const done = Object.entries(stories).filter(([id]) => finished.has(id));
|
|
735
|
+
const usual = done.length >= 3 ? median(done.map(([, n]) => n)) : null;
|
|
736
|
+
const outliers = usual
|
|
737
|
+
? Object.entries(stories)
|
|
738
|
+
.filter(([, n]) => n > 3 * usual)
|
|
739
|
+
.map(([id, n]) => ({ id, ratio: Math.round((n / usual) * 10) / 10 }))
|
|
740
|
+
.sort((a, b) => b.ratio - a.ratio)
|
|
741
|
+
: [];
|
|
742
|
+
return { lanes, stories, background, total: all, outliers };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** The plan's 5-hour use from the session's limits (null off a subscription), and the pause (USE-4). */
|
|
746
|
+
export function planOf(rateLimits, pauseAtPercent = 80) {
|
|
747
|
+
const percent = (rateLimits ?? []).find((r) => r.kind === 'five_hour')?.percentUsed ?? null;
|
|
748
|
+
return { percent, paused: percent !== null && percent > pauseAtPercent };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** Tokens as a person reads them: 950, 12k, 1.4M. */
|
|
752
|
+
export function tokens(n) {
|
|
753
|
+
if (n < 1000) return String(n);
|
|
754
|
+
if (n < 1e6) return `${Math.round(n / 1000)}k`;
|
|
755
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** The plan's use as a bar of ten cells. */
|
|
759
|
+
export function planBar(percent) {
|
|
760
|
+
const full = Math.max(0, Math.min(10, Math.round(percent / 10)));
|
|
761
|
+
return `${'█'.repeat(full)}${'░'.repeat(10 - full)}`;
|
|
762
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warren-dean/code-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Keeps AI coding agents inside your project's rules: lanes, layer rules, protected paths and proof before finish, enforced by Claude Code hooks and in CI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Warren Langeveldt",
|
package/skills/init/SKILL.md
CHANGED
|
@@ -31,7 +31,7 @@ Say which one applies and why before you start.
|
|
|
31
31
|
- **Design-first rules:** screens designed before they are built, and where approvals are listed.
|
|
32
32
|
- **Commands:** test, lint, typecheck, database and deploy commands, and which must never run locally.
|
|
33
33
|
2. **The repository.** Map the real layout: `git ls-files | cut -d/ -f1-3 | sort | uniq -c`, package manifests (`package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`), workspace files, existing lint, format and test scripts, CI workflows and CODEOWNERS.
|
|
34
|
-
3. **Existing kit.** If the project has `.claude/hooks`, agents or skills, read them. The kit replaces local hooks; agents and skills are kept and adapted, not rewritten.
|
|
34
|
+
3. **Existing kit.** If the project has `.claude/hooks`, agents or skills, read them. The kit replaces local hooks; agents and skills are kept and adapted, not rewritten. On an update (section 4), skills that came from the docs' recipes follow those recipes as they are now.
|
|
35
35
|
4. **Other tools.** Run `adapters --config .claude/code-kit.draft.json` once the draft exists. It lists the tools code-kit has an adapter for (such as Context Graph, found by its `.ctx/` folder), what each adds to the rules, and the setup each needs. Don't copy an adapter's paths into the config; they're added automatically while the tool is present.
|
|
36
36
|
|
|
37
37
|
**Brownfield, in addition:**
|
|
@@ -97,6 +97,8 @@ Draft, alongside the config:
|
|
|
97
97
|
- Drop the spec-check lines if the config has no `docs.specs`.
|
|
98
98
|
- **CLAUDE.md:** add the section from `${CLAUDE_PLUGIN_ROOT}/templates/claude-md.md`.
|
|
99
99
|
- **Project skills** (recipes like "new migration" or "new endpoint"): list the recipes the docs describe step by step, and offer to write them. Don't write them unasked, and never write an empty one.
|
|
100
|
+
- Write each as `.claude/skills/<name>/SKILL.md`, saying in its description which recipe in which doc it comes from, so an update can find it again.
|
|
101
|
+
- List it under **Skills to use** on every lane agent whose paths the recipe touches, not only one: a recipe that adds a migration and the endpoint that uses it belongs to both lanes. A recipe only the lead follows goes in the CLAUDE.md section instead.
|
|
100
102
|
|
|
101
103
|
Show the person:
|
|
102
104
|
|
|
@@ -144,10 +146,16 @@ Re-run whenever the build has moved on: new apps or packages, folders that moved
|
|
|
144
146
|
|
|
145
147
|
4. **Also propose:**
|
|
146
148
|
- agent files for new lanes, and updates to agents whose paths changed;
|
|
149
|
+
- **project skills**, by reading the recipes in the docs again and comparing them with `.claude/skills/`:
|
|
150
|
+
- a new skill for each recipe the docs now describe that has none;
|
|
151
|
+
- an update to each skill whose recipe changed, showing what changed;
|
|
152
|
+
- removing a skill whose recipe is gone from the docs (ask first: the person may want it kept);
|
|
153
|
+
- the matching **Skills to use** changes on every lane agent whose paths each recipe touches;
|
|
154
|
+
- skills the person wrote themselves, with no recipe behind them, stay as they are;
|
|
147
155
|
- a smaller baseline when `baseline` shows violations were fixed. It only ever shrinks.
|
|
148
156
|
5. **On approval:**
|
|
149
157
|
1. Replace `.claude/code-kit.json` with the draft and delete the draft.
|
|
150
|
-
2. Write or update the agents.
|
|
158
|
+
2. Write or update the agents, and write, update or remove the project skills as approved.
|
|
151
159
|
3. Run `baseline --write` if it shrank.
|
|
152
160
|
4. Run `check` and `unowned`.
|
|
153
161
|
5. Commit on a branch.
|
package/skills/review/SKILL.md
CHANGED
|
@@ -50,8 +50,16 @@ Read the story, every requirement it cites, and the full diff (`git diff <base>.
|
|
|
50
50
|
|
|
51
51
|
Report the table, `verify`'s result and one of:
|
|
52
52
|
|
|
53
|
+
- **Files any agent may write** (`anyActor`, such as a lockfile or recorded decisions) have no owning lane, but they have a reviewer: `trace <file>` names it (the lead unless the config names a lane). Review those changes as that reviewer would, or ask that lane's agent.
|
|
53
54
|
- **Merge.** Everything passes. The lead merges the branch into its own branch (`git merge --no-ff <branch>`), sets the story's `**Status:**` to `done` in the plan, and commits. `main` and the other protected branches are merged by a person, through a pull request, after `verify` passes in CI. When the branch is merged, remove its worktree.
|
|
54
|
-
- **Send back.** List each problem, where it is, and what "fixed" looks like.
|
|
55
|
+
- **Send back.** List each problem, where it is, and what "fixed" looks like. Record the send-back, so `status` and `next` read the branch as waiting for its fix rather than finished:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/code-kit.mjs" sent-back <branch> --reason "<the problems, in one line>"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Send the list to the same lane agent if it's still running (SendMessage), or dispatch the story again with the list added to the brief. Set the story's status to `in progress`.
|
|
62
|
+
|
|
55
63
|
- **Ask the person.** When the right answer is a product or scope decision (the spec is silent, or the lane found a real conflict), put the question to the person with your recommendation. Don't merge around it.
|
|
56
64
|
|
|
57
65
|
Keep the report short: the verdict first, then the table, then the problems.
|
package/templates/agent.md
CHANGED
|
@@ -9,7 +9,7 @@ You are the {{lane}} engineer for this repository.
|
|
|
9
9
|
**Owned paths:** {{lane paths from .claude/code-kit.json}}
|
|
10
10
|
**Layers:** {{the layers in these paths, and what each may depend on}}
|
|
11
11
|
**Specs:** {{the spec files this lane reads}}
|
|
12
|
-
**Skills to use:** spec-check{{, project
|
|
12
|
+
**Skills to use:** spec-check{{, every project skill whose recipe touches this lane's paths}}
|
|
13
13
|
**Prove your work with:** `{{the command that proves this lane's work}}`
|
|
14
14
|
|
|
15
15
|
Lane rules:
|