@warren-dean/code-kit 0.12.1 → 0.12.2

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.
@@ -9,7 +9,7 @@
9
9
  "name": "code-kit",
10
10
  "source": "./",
11
11
  "description": "Keeps AI coding agents inside your project's rules, from idea to merged code: spec design, lanes, layer rules, protected paths with an approval log, spec-check and design gates, proof-before-finish checks, dispatch, review, build status and CI verification, configured per project from its docs",
12
- "version": "0.12.1",
12
+ "version": "0.12.2",
13
13
  "category": "development",
14
14
  "tags": ["guardrails", "architecture", "lanes", "layers", "spec-driven", "ci", "multi-agent"],
15
15
  "license": "MIT"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-kit",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "Keeps AI coding agents inside your project's rules, from idea to merged code: spec design, lanes, layer rules, protected paths with an approval log, spec-check and design gates, proof-before-finish checks, dispatch, review, build status and CI verification, configured per project from its docs",
5
5
  "author": {
6
6
  "name": "Warren Langeveldt"
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  What changed in each release of code-kit, newest first. Versions follow the plugin manifest and the npm package `@warren-dean/code-kit`. Each release is also on [GitHub Releases](https://github.com/warrendeanlangeveldt/code-kit/releases).
4
4
 
5
+ ## 0.12.2 (2026-10-13)
6
+
7
+ The Decisions tab is readable, and old package requests stop waiting.
8
+
9
+ - **Decisions as short cards.** The tab says how many of each kind wait ("10 approvals · 2 questions"), then lists each as a card: its kind, a one-line headline ("Approve fastify, jose, drizzle-orm and 4 more") and who it's for, with commands cut at their first pipe instead of whole scripts. Five show at a time; **j** and **k** move, and the selected one opens in full below.
10
+ - **Package requests close once installed.** A request for a package closes once the package is in a committed `package.json`, whoever installed it, so installs already done no longer wait for you.
11
+
5
12
  ## 0.12.1 (2026-10-13)
6
13
 
7
14
  Ask about an approval before you give it.
package/README.md CHANGED
@@ -290,7 +290,7 @@ With Claude Code 2.1.287 or later, code-kit's mod shows the project in the sessi
290
290
  - a story using more than three times the median of the stories finished this session (once three have finished): **Lanes** opens the pane;
291
291
  - background agents paused, once the plan’s 5-hour use passes `harness.background.pauseAtPercent` (80% unless set).
292
292
 
293
- Each button has a digit: type it in an empty prompt to press it. The band keeps one line per kind ("2 approvals waiting"); the Decisions tab has a card for each decision.
293
+ Each button has a digit: type it in an empty prompt to press it. The band keeps one line per kind ("2 approvals waiting"); the Decisions tab says how many of each kind, then lists a short card for each decision, five at a time (**j** and **k** move), with the selected one in full below.
294
294
 
295
295
  - **`/lanes`** opens the Lanes pane. Its first line counts the stories by state (building, in review, queued, sent back, blocked, ready, merged), with the loop's state and the plan's 5-hour use; press a count to show only those lanes. Beneath it, whatever needs you (held calls, approvals, reviews, merges, stalled lanes, outliers) is pinned with its key. Tabs on **1** to **7**: **Lanes** opens on the loop as a card: its mode, the steps it took, the one running and the next. Then each lane on fixed columns: its name, state and story, its agent's current or last tool call with the time since beneath (amber once quiet, red when stalled), and its tokens; the lead reads orchestrating while its turn runs. A selected lane shows its stories as cards (state, requirements, what each waits on or its branch), then its agent and the paths it owns. **Decisions**, each decision that needs you as a card: what you asked for, the story and its requirements, what was happening, what's already settled, why the lead couldn't settle it, and each option with what it does; **Journal**, the build from your request to now in plain sentences, by day: the request and the decisions in the brief, the specification, each story handed out, sent back and merged, each approval and who gave it, each milestone finished, and the loop's steps this session, yours in yellow. Each lane has its own small pixel character, tinted in the lane's colour, whose pose shows its state: working (animated while its agent works), waiting on you, quiet, stalled, done or resting; the lead's wears a crown. Under each lane, its story's timeline on a shared time axis: building, then in review, with send-backs (↩), approvals (◆) and the merge (✓) marked where they fell; **Code map**, the whole project's folders by layer when no story is at work, or a story's files in a column per layer with the imports between them: a file fills with a lane's colour while that lane reads or edits it, and says whether its why (its Context Graph card) is written, out of date or not written yet; **j**/**k** select a file to see its card and what it imports and what imports it, and **o** opens it in Context Graph (box-drawing text in the terminal, an SVG on Desktop); **Trail**, every requirement in the specs as a cell by spec, its glyph and colour saying whether it's done and tested, done but untested, in progress, todo, without a story or removed, with a legend counting each. **h**/**l** and **j**/**k** move between cells and specs (or pick one from the list), showing the requirement's stories, their branches, the code they changed and the tests that name it; beneath the cells, the spec's trail drawn as a flow, each requirement to its stories, code and tests, with a missing link dashed in red; **Queue**, the merge queue; and **Usage**. **j** and **k** move between lanes, and the selected lane's acts are on **a** (approve), **r** (review), **s** (send back, with your reason), **m** (merge), **n** (nudge its agent) and **x** (stop it); **p** pauses or resumes the loop, and **f** filters the lanes by name, agent or story. **Enter** (or **o**) opens the selected lane's story: its requirements beside its spec-check report, the reviewer's findings by severity (each a way into the diff), `verify`'s problems, its usage and loop steps, and its diff against the base, file by file, with the acts its state allows. **b** or Escape goes back to the lanes. Inline (when the terminal can't dock it beside the transcript), it shows the counts, what needs you and a line per lane. It stays current within 2 seconds of a commit, a branch change, a plan edit or an agent starting or stopping. `/lanes` again, or Escape, closes it.
296
296
  - **Refusals** a code-kit hook gives an agent are drawn as cards: what was refused, the rule, what to do, and Approve… where your approval would allow it. The full text is a press away.
@@ -415,6 +415,7 @@ export function openRequests(root, now = Date.now()) {
415
415
  // allowed), or the file it named was committed since by someone who could.
416
416
  if (r.names.every((n) => grantedSince(root, given, n, r))) continue;
417
417
  if (committedSince(root, r)) continue;
418
+ if (installedSince(root, r)) continue;
418
419
  open.push(r);
419
420
  }
420
421
  return open;
@@ -439,6 +440,39 @@ function grantedSince(root, given, name, r) {
439
440
  });
440
441
  }
441
442
 
443
+ /**
444
+ * CARD-7: whether every package a dependency request named has since been added to a committed
445
+ * manifest, on any branch: the install happened (approved, or run by the person themselves), so it no
446
+ * longer waits.
447
+ */
448
+ function installedSince(root, r) {
449
+ if (!r.names.length || !r.names.every((n) => n.startsWith(DEPENDENCY_APPROVAL))) return false;
450
+ return r.names.every((name) => {
451
+ const pkg = name.slice(DEPENDENCY_APPROVAL.length).replaceAll('+', '/');
452
+ try {
453
+ const out = execFileSync(
454
+ 'git',
455
+ [
456
+ '-C',
457
+ root,
458
+ 'log',
459
+ '--all',
460
+ '-1',
461
+ '--format=%ct',
462
+ `--since=@${Math.floor(Date.parse(r.at) / 1000)}`,
463
+ `-S"${pkg}"`,
464
+ '--',
465
+ ':(glob)**/package.json',
466
+ ],
467
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
468
+ );
469
+ return Boolean(out.trim());
470
+ } catch {
471
+ return false;
472
+ }
473
+ });
474
+ }
475
+
442
476
  /** Whether the file a request named has been committed, on any branch, since it was asked. */
443
477
  function committedSince(root, r) {
444
478
  const path = String(r.what);
@@ -2,7 +2,7 @@
2
2
  // the person, each with its lineage. The band's "you" lines say what needs them; a decision card says why:
3
3
  // what they asked for, the story and requirements it belongs to, what the agent was doing, what was already
4
4
  // settled, why the lead couldn't settle this itself, and each option with what happens next.
5
- import { askWords } from './view.mjs';
5
+ import { approvalLabel, askWords, commandItself } from './view.mjs';
6
6
 
7
7
  /** The kinds of band line that are the person's to decide; the rest are notes. */
8
8
  export const DECISION_KINDS = new Set([
@@ -136,6 +136,70 @@ function optionsOf(line) {
136
136
  return line.actions;
137
137
  }
138
138
 
139
+ /** A kind of decision as its card's tag says it. */
140
+ const TAG = {
141
+ held: 'held call',
142
+ approvals: 'approval',
143
+ stalled: 'stalled',
144
+ proposal: 'next step',
145
+ queue: 'merge',
146
+ review: 'review',
147
+ notice: 'notice',
148
+ question: 'question',
149
+ brief: 'your brief',
150
+ denied: 'refused',
151
+ report: 'bug report',
152
+ };
153
+
154
+ /** At most `n` characters on one line, cut with an ellipsis. */
155
+ const clip = (text, n) => {
156
+ const one = String(text ?? '')
157
+ .replace(/\s+/g, ' ')
158
+ .trim();
159
+ return one.length > n ? `${one.slice(0, n - 1)}…` : one;
160
+ };
161
+
162
+ /** Names as a person reads them, at most three, then how many more. */
163
+ const someNames = (names) => {
164
+ const shown = names.slice(0, 3).map(approvalLabel).join(', ');
165
+ return names.length > 3 ? `${shown} and ${names.length - 3} more` : shown;
166
+ };
167
+
168
+ /**
169
+ * CARD-9: a decision's headline and the line under it, short enough for a card in a list:
170
+ * { tag, headline, sub }.
171
+ */
172
+ function headlineOf(line, story) {
173
+ const forStory = story ? ` · ${story.id}` : '';
174
+ if (line.kind === 'approvals') {
175
+ const who = line.request.lane ? `the ${line.request.lane} lane` : line.request.actor;
176
+ return {
177
+ headline: `Approve ${someNames(line.request.names)}`,
178
+ sub: `for ${who}${forStory} · ${clip(commandItself(line.files?.[0] ?? line.request.what), 70)}`,
179
+ };
180
+ }
181
+ if (line.kind === 'held')
182
+ return {
183
+ headline: `${line.held.who} wants to ${line.text.replace(/^.*? wants to /, '').replace(/ · \d+:\d+$/, '')}`,
184
+ sub: `held${forStory} · ${line.text.match(/\d+:\d+$/)?.[0] ?? ''} left`,
185
+ };
186
+ if (line.kind === 'question')
187
+ return {
188
+ headline: line.question.question,
189
+ sub: `from the lead${forStory}${line.question.blocks ? ', which waits on it' : ''}`,
190
+ };
191
+ if (line.kind === 'brief')
192
+ return { headline: line.brief.question, sub: `open in ${line.brief.path}` };
193
+ if (line.kind === 'denied')
194
+ return {
195
+ headline: `Claude Code refused ${line.denial.who}'s call`,
196
+ sub: clip(line.denial.command ?? line.denial.what, 80),
197
+ };
198
+ if (line.kind === 'report')
199
+ return { headline: line.report.title, sub: "drafted by an agent, for code-kit's GitHub" };
200
+ return { headline: line.text, sub: story ? `${story.id} ${story.title ?? ''}` : '' };
201
+ }
202
+
139
203
  /** Why a line needs the person, when the line itself doesn't say. */
140
204
  function whyOf(line, check) {
141
205
  if (line.kind === 'held') {
@@ -243,9 +307,13 @@ export function decisionCards(lines, context = {}) {
243
307
  `${a.name} is approved${a.lane ? ` for the ${a.lane} lane` : ''}${a.reason ? `: ${a.reason}` : ''}`,
244
308
  ),
245
309
  ].slice(-5);
310
+ const { headline, sub } = headlineOf(line, story);
246
311
  return {
247
312
  id: `${line.kind}-${i}`,
248
313
  kind: line.kind,
314
+ tag: TAG[line.kind] ?? 'decision',
315
+ headline: clip(headline, 100),
316
+ sub,
249
317
  line,
250
318
  // The band line it came from: one line can stand for several decisions (CARD-8).
251
319
  source,
@@ -260,14 +328,22 @@ export function decisionCards(lines, context = {}) {
260
328
  })),
261
329
  doing:
262
330
  line.held?.command ??
263
- (line.request ? askWords(line.request, line.files ?? [line.request.what]) : null) ??
331
+ (line.request
332
+ ? clip(
333
+ askWords(
334
+ line.request,
335
+ (line.files ?? [line.request.what]).map((f) => commandItself(f)),
336
+ ),
337
+ 300,
338
+ )
339
+ : null) ??
264
340
  (line.proposal?.label ? `Next step: ${line.proposal.label}` : null) ??
265
341
  (line.intent
266
342
  ? `Intent #${line.intent.id}: ${line.intent.about}${line.intent.touches.length ? ` (touches ${line.intent.touches.join(', ')})` : ''}`
267
343
  : null) ??
268
344
  (line.pull ? `Pull request #${line.pull.number} on ${line.pull.branch}` : null) ??
269
345
  (line.question?.blocks ? `${line.question.story} waits on your answer.` : null) ??
270
- (line.denial ? (line.denial.command ?? line.denial.what) : null) ??
346
+ (line.denial ? clip(line.denial.command ?? line.denial.what, 400) : null) ??
271
347
  (line.report ? line.report.body : null),
272
348
  settled,
273
349
  why: whyOf(line, check),
@@ -1257,10 +1257,15 @@ export function codeTab(view, { Box, Text, Button, Svg }, on) {
1257
1257
  return [picker, drawing, legend, detail, keys];
1258
1258
  }
1259
1259
 
1260
+ /** CARD-9: how many decision cards the list shows at once, around the selected one. */
1261
+ const CARDS_SHOWN = 5;
1262
+
1260
1263
  /**
1261
- * DECIDE-3: the Decisions tab: each decision that needs the person, the selected one as a card with its
1262
- * lineage (what they asked for, the story and requirements, what was happening, what's settled, why it
1263
- * needs them) and its options, each saying what it does. `on`: onDecision(i), onNeed(id, line).
1264
+ * DECIDE-3, CARD-9: the Decisions tab: how many decisions need the person, by kind; then each as a short
1265
+ * card (its kind, a one-line headline, and who or what it's for), a few at a time around the selected
1266
+ * one, with j and k to move; then the selected one in full, with its lineage (what they asked for, the
1267
+ * story and requirements, what was happening, what's settled, why it needs them) and its options, each
1268
+ * saying what it does. `on`: onDecision(i), onNeed(id, line).
1264
1269
  */
1265
1270
  export function decisionsTab(cards, ui, { Box, Text, Button }, on) {
1266
1271
  const text = (value, style = {}) => Text({ ...style, children: [value] });
@@ -1277,27 +1282,82 @@ export function decisionsTab(cards, ui, { Box, Text, Button }, on) {
1277
1282
  text(value, { wrap: 'wrap', ...style }),
1278
1283
  ],
1279
1284
  });
1285
+ const kinds = new Map();
1286
+ for (const c of cards) kinds.set(c.tag, (kinds.get(c.tag) ?? 0) + 1);
1287
+ const from = Math.max(0, Math.min(at - Math.floor(CARDS_SHOWN / 2), cards.length - CARDS_SHOWN));
1288
+ const shown = cards.slice(from, from + CARDS_SHOWN);
1280
1289
  const list = Box({
1281
1290
  key: 'decision-list',
1282
1291
  flexDirection: 'column',
1283
- children: cards.map((c, i) =>
1292
+ children: [
1293
+ ...(from > 0 ? [text(` ↑ ${from} more`, { key: 'decision-above', dimColor: true })] : []),
1294
+ ...shown.map((c, n) => {
1295
+ const i = from + n;
1296
+ return Box({
1297
+ key: `decision-item-${i}`,
1298
+ flexDirection: 'column',
1299
+ borderStyle: 'round',
1300
+ borderColor: i === at ? 'yellow' : 'gray',
1301
+ paddingX: 1,
1302
+ children: [
1303
+ Box({
1304
+ flexDirection: 'row',
1305
+ columnGap: 1,
1306
+ children: [
1307
+ text(c.tag.toUpperCase(), { color: i === at ? 'yellow' : 'cyan', bold: true }),
1308
+ Button({
1309
+ key: `decision-pick-${i}`,
1310
+ label: c.headline,
1311
+ plain: true,
1312
+ onPress: () => on.onDecision(i),
1313
+ }),
1314
+ ],
1315
+ }),
1316
+ ...(c.sub ? [text(c.sub, { dimColor: true, wrap: 'truncate-end' })] : []),
1317
+ ],
1318
+ });
1319
+ }),
1320
+ ...(from + CARDS_SHOWN < cards.length
1321
+ ? [
1322
+ text(` ↓ ${cards.length - from - CARDS_SHOWN} more`, {
1323
+ key: 'decision-below',
1324
+ dimColor: true,
1325
+ }),
1326
+ ]
1327
+ : []),
1328
+ ],
1329
+ });
1330
+ const keys = Box({
1331
+ key: 'decision-keys',
1332
+ flexDirection: 'row',
1333
+ columnGap: 2,
1334
+ children: [
1284
1335
  Button({
1285
- key: `decision-pick-${i}`,
1286
- label: `${i === at ? '›' : ' '} ${c.title}`,
1336
+ key: 'decision-next',
1337
+ label: 'Next',
1338
+ hotkey: 'j',
1287
1339
  plain: true,
1288
- onPress: () => on.onDecision(i),
1340
+ onPress: () => on.onDecision(Math.min(at + 1, cards.length - 1)),
1289
1341
  }),
1290
- ),
1342
+ Button({
1343
+ key: 'decision-prev',
1344
+ label: 'Previous',
1345
+ hotkey: 'k',
1346
+ plain: true,
1347
+ onPress: () => on.onDecision(Math.max(at - 1, 0)),
1348
+ }),
1349
+ text(`${at + 1} of ${cards.length}`, { dimColor: true }),
1350
+ ],
1291
1351
  });
1292
1352
  const detail = Box({
1293
1353
  key: 'decision-card',
1294
1354
  flexDirection: 'column',
1295
- borderStyle: 'round',
1355
+ borderStyle: 'double',
1296
1356
  borderColor: 'yellow',
1297
1357
  paddingX: 1,
1298
1358
  rowGap: 0,
1299
1359
  children: [
1300
- text(card.title, { bold: true }),
1360
+ text(card.headline, { bold: true, wrap: 'wrap' }),
1301
1361
  ...(card.asked ? [row('You asked for', card.asked)] : []),
1302
1362
  ...(card.story
1303
1363
  ? [
@@ -1353,7 +1413,18 @@ export function decisionsTab(cards, ui, { Box, Text, Button }, on) {
1353
1413
  `${cards.length} decision${cards.length === 1 ? '' : 's'} need${cards.length === 1 ? 's' : ''} you. Everything else is the lead's.`,
1354
1414
  { bold: true },
1355
1415
  ),
1356
- ...(cards.length > 1 ? [list] : []),
1416
+ Box({
1417
+ key: 'decision-kinds',
1418
+ children: [
1419
+ text(
1420
+ [...kinds]
1421
+ .map(([tag, n]) => `${n} ${tag}${n === 1 || tag.endsWith('brief') ? '' : 's'}`)
1422
+ .join(' · '),
1423
+ { dimColor: true },
1424
+ ),
1425
+ ],
1426
+ }),
1427
+ ...(cards.length > 1 ? [list, keys] : []),
1357
1428
  detail,
1358
1429
  ];
1359
1430
  }
@@ -1521,13 +1521,10 @@ test("ASK-4 the brief's open questions are the person's decisions: answered to t
1521
1521
  await press($, 'band-openDecision');
1522
1522
  await band.unmount();
1523
1523
  const ui = await pane($, PANE);
1524
- expect(
1525
- await ui.find({ type: 'Text', text: 'From your brief: Accounts: AWS, Supabase, Fly' }),
1526
- ).toBeDefined();
1524
+ expect(await ui.find({ type: 'Text', text: 'Accounts: AWS, Supabase, Fly' })).toBeDefined();
1525
+ expect(await ui.find({ type: 'Text', text: 'open in docs/brief.md' })).toBeDefined();
1527
1526
  await press($, 'decide-laterBrief', PANE);
1528
- expect(
1529
- await ui.find({ type: 'Text', text: 'From your brief: The product domain' }),
1530
- ).toBeDefined();
1527
+ expect(await ui.find({ type: 'Text', text: 'The product domain' })).toBeDefined();
1531
1528
  await press($, 'decide-answerBrief', PANE);
1532
1529
  const box = await pane($, 'code-kit-answer');
1533
1530
  await $.ui.input({ plugin: 'code-kit', key: 'answer-text', text: 'myaila.com.au' });
@@ -1602,6 +1599,45 @@ test('CARD-8 two approvals waiting are two decisions, each approved or dropped o
1602
1599
  await ui.unmount();
1603
1600
  });
1604
1601
 
1602
+ test('CARD-9 many decisions read as short cards, a few at a time, with the selected one in full', async ($, on) => {
1603
+ const w = project();
1604
+ w.requests = Array.from({ length: 8 }, (_, i) => ({
1605
+ ...dayjs,
1606
+ at: `2026-10-06T00:0${i}:00.000Z`,
1607
+ names: [`dep-pkg${i}`, 'dep-a', 'dep-b', 'dep-c'],
1608
+ what: `pnpm --filter @app/web add pkg${i} a b c 2>&1 | tail -20 && cat > tsconfig.json <<'EOF'\n{ "x": 1 }\nEOF`,
1609
+ }));
1610
+ w.questions = [question()];
1611
+ await start($, on, w);
1612
+ const band = await bandUi($);
1613
+ await press($, 'band-decide');
1614
+ await band.unmount();
1615
+ const ui = await pane($, PANE);
1616
+ expect((await ui.find({ key: 'decision-kinds' }))?.text).toBe('8 approvals · 1 question');
1617
+ // Five cards at a time, each a headline and a line under it; the command cut at its pipe, on one line.
1618
+ expect(await ui.find({ key: 'decision-item-4' })).toBeDefined();
1619
+ expect(await ui.find({ key: 'decision-item-5' })).toBeUndefined();
1620
+ expect(await ui.find({ type: 'Text', text: ' ↓ 4 more' })).toBeDefined();
1621
+ expect((await ui.find({ key: 'decision-pick-0' }))?.props.label).toBe(
1622
+ 'Approve pkg0, a, b and 1 more',
1623
+ );
1624
+ expect(
1625
+ await ui.find({
1626
+ type: 'Text',
1627
+ text: 'for the web lane · ST-4 · pnpm --filter @app/web add pkg0 a b c',
1628
+ }),
1629
+ ).toBeDefined();
1630
+ expect((await ui.findAll({ type: 'Text' })).some((t: any) => /tsconfig|EOF/.test(t.text))).toBe(
1631
+ false,
1632
+ );
1633
+ await press($, 'decision-next', PANE);
1634
+ await press($, 'decision-next', PANE);
1635
+ await press($, 'decision-next', PANE);
1636
+ expect(await ui.find({ type: 'Text', text: /↑ \d+ more/ })).toBeDefined();
1637
+ expect((await ui.findAll({ type: 'Text' })).some((t: any) => t.text === '4 of 9')).toBe(true);
1638
+ await ui.unmount();
1639
+ });
1640
+
1605
1641
  test('REPORT-3 a bug report an agent drafted waits for the person, who reads it and sends it, or discards it', async ($, on) => {
1606
1642
  const w = project();
1607
1643
  w.reports = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warren-dean/code-kit",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
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",