@warren-dean/code-kit 0.12.1 → 0.13.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 +18 -0
- package/README.md +108 -40
- package/SECURITY.md +11 -2
- package/bin/code-kit.mjs +61 -17
- package/hooks/guard-bash.mjs +35 -0
- package/hooks/lib/checks.mjs +19 -6
- package/hooks/lib/config.mjs +8 -0
- package/hooks/lib/next.mjs +34 -8
- package/hooks/lib/plan.mjs +4 -2
- package/hooks/lib/queue.mjs +25 -6
- package/hooks/lib/regression.mjs +321 -0
- package/hooks/lib/rules.mjs +34 -0
- package/hooks/lib/verify.mjs +53 -7
- package/hooks/mod/decisions.mjs +102 -6
- package/hooks/mod/loop.mjs +1 -1
- package/hooks/mod/panes.mjs +92 -11
- package/hooks/mod/register.mjs +79 -22
- package/hooks/mod/register.test.ts +215 -68
- package/hooks/mod/view.mjs +96 -25
- package/hooks/stop-check.mjs +1 -1
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +1 -0
- package/skills/review/SKILL.md +3 -1
- package/skills/spec-design/SKILL.md +1 -1
- package/templates/agent.md +2 -0
- package/templates/claude-md.md +1 -1
- package/templates/plan.md +1 -1
package/hooks/mod/panes.mjs
CHANGED
|
@@ -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:
|
|
1262
|
-
*
|
|
1263
|
-
*
|
|
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,92 @@ 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:
|
|
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:
|
|
1286
|
-
label:
|
|
1336
|
+
key: 'decision-next',
|
|
1337
|
+
label: 'Next',
|
|
1338
|
+
hotkey: 'j',
|
|
1287
1339
|
plain: true,
|
|
1288
|
-
onPress: () => on.onDecision(
|
|
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: '
|
|
1355
|
+
borderStyle: 'double',
|
|
1296
1356
|
borderColor: 'yellow',
|
|
1297
1357
|
paddingX: 1,
|
|
1298
1358
|
rowGap: 0,
|
|
1299
1359
|
children: [
|
|
1300
|
-
text(
|
|
1360
|
+
text(
|
|
1361
|
+
card.kind === 'brief'
|
|
1362
|
+
? 'An open question in your brief'
|
|
1363
|
+
: card.kind === 'question'
|
|
1364
|
+
? 'A question from the lead'
|
|
1365
|
+
: card.headline,
|
|
1366
|
+
{ bold: true, wrap: 'wrap' },
|
|
1367
|
+
),
|
|
1368
|
+
...(card.details ?? []).map((d) =>
|
|
1369
|
+
row(d.label, d.value, d.label === 'Question' ? { bold: true } : {}),
|
|
1370
|
+
),
|
|
1301
1371
|
...(card.asked ? [row('You asked for', card.asked)] : []),
|
|
1302
1372
|
...(card.story
|
|
1303
1373
|
? [
|
|
@@ -1353,7 +1423,18 @@ export function decisionsTab(cards, ui, { Box, Text, Button }, on) {
|
|
|
1353
1423
|
`${cards.length} decision${cards.length === 1 ? '' : 's'} need${cards.length === 1 ? 's' : ''} you. Everything else is the lead's.`,
|
|
1354
1424
|
{ bold: true },
|
|
1355
1425
|
),
|
|
1356
|
-
|
|
1426
|
+
Box({
|
|
1427
|
+
key: 'decision-kinds',
|
|
1428
|
+
children: [
|
|
1429
|
+
text(
|
|
1430
|
+
[...kinds]
|
|
1431
|
+
.map(([tag, n]) => `${n} ${tag}${n === 1 || tag.endsWith('brief') ? '' : 's'}`)
|
|
1432
|
+
.join(' · '),
|
|
1433
|
+
{ dimColor: true },
|
|
1434
|
+
),
|
|
1435
|
+
],
|
|
1436
|
+
}),
|
|
1437
|
+
...(cards.length > 1 ? [list, keys] : []),
|
|
1357
1438
|
detail,
|
|
1358
1439
|
];
|
|
1359
1440
|
}
|
package/hooks/mod/register.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
answerPane,
|
|
22
22
|
approvalAsidePrompt,
|
|
23
23
|
approvePane,
|
|
24
|
+
questionAsidePrompt,
|
|
24
25
|
commandItself,
|
|
25
26
|
band,
|
|
26
27
|
bandLines,
|
|
@@ -564,12 +565,24 @@ export function register(on) {
|
|
|
564
565
|
answerWith: (line) => act.sendAnswer({ kind: 'question', ...line.question }, line.choice),
|
|
565
566
|
// ASK-2, ASK-4: Answer… opens a box for the person's own words.
|
|
566
567
|
answer: async (line) => {
|
|
567
|
-
answering = {
|
|
568
|
+
answering = {
|
|
569
|
+
kind: 'question',
|
|
570
|
+
...line.question,
|
|
571
|
+
text: '',
|
|
572
|
+
error: null,
|
|
573
|
+
aside: { thread: [], draft: '' },
|
|
574
|
+
};
|
|
568
575
|
await $.ui.open({ id: ANSWER_ID, title: 'Answer', focus: true, closeOnEscape: true });
|
|
569
576
|
$.ui.invalidate('ui.render');
|
|
570
577
|
},
|
|
571
578
|
answerBrief: async (line) => {
|
|
572
|
-
answering = {
|
|
579
|
+
answering = {
|
|
580
|
+
kind: 'brief',
|
|
581
|
+
...line.brief,
|
|
582
|
+
text: '',
|
|
583
|
+
error: null,
|
|
584
|
+
aside: { thread: [], draft: '' },
|
|
585
|
+
};
|
|
573
586
|
await $.ui.open({ id: ANSWER_ID, title: 'Answer', focus: true, closeOnEscape: true });
|
|
574
587
|
$.ui.invalidate('ui.render');
|
|
575
588
|
},
|
|
@@ -613,17 +626,24 @@ export function register(on) {
|
|
|
613
626
|
}
|
|
614
627
|
}
|
|
615
628
|
notice = null;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
text:
|
|
619
|
-
q.kind === 'question'
|
|
620
|
-
? `The person answered ${q.id} (${q.question}): ${said}${q.blocks ? ` ${q.story} no longer waits on them.` : ''} Carry on with it.`
|
|
621
|
-
: `The person answered an open question in ${q.path}, "${q.question}": ${said} Record it in the brief's decision log, take it out of the open questions, and carry on with it.`,
|
|
622
|
-
});
|
|
629
|
+
// Answered, the decision goes at once; the lead hears it as a turn of its own once it's idle, which
|
|
630
|
+
// can be long after (so the prompt isn't waited on).
|
|
623
631
|
if (q.kind === 'brief') {
|
|
624
632
|
briefLater.add(q.question);
|
|
625
633
|
model = { ...model, brief: (model.brief ?? []).filter((b) => b.question !== q.question) };
|
|
626
634
|
}
|
|
635
|
+
if (q.kind === 'question')
|
|
636
|
+
model = { ...model, questions: (model.questions ?? []).filter((x) => x.id !== q.id) };
|
|
637
|
+
$.ui.invalidate('ui.render');
|
|
638
|
+
const said = /[.!?]$/.test(text) ? text : `${text}.`;
|
|
639
|
+
$.prompt
|
|
640
|
+
.submit({
|
|
641
|
+
text:
|
|
642
|
+
q.kind === 'question'
|
|
643
|
+
? `The person answered ${q.id} (${q.question}): ${said}${q.blocks ? ` ${q.story} no longer waits on them.` : ''} Carry on with it.`
|
|
644
|
+
: `The person answered an open question in ${q.path}, "${q.question}": ${said} Record it in the brief's decision log, take it out of the open questions, and carry on with it.`,
|
|
645
|
+
})
|
|
646
|
+
.catch(() => {});
|
|
627
647
|
await act.reload();
|
|
628
648
|
return true;
|
|
629
649
|
},
|
|
@@ -634,16 +654,20 @@ export function register(on) {
|
|
|
634
654
|
deniedDone: async (line) => {
|
|
635
655
|
denied.delete(line.denial.id);
|
|
636
656
|
$.ui.invalidate('ui.render');
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
657
|
+
$.prompt
|
|
658
|
+
.submit({
|
|
659
|
+
text: `The person ran it themselves, or allowed it: ${line.denial.command ?? line.denial.what} (Claude Code had refused it for ${line.denial.who}). Check it worked, and carry on.`,
|
|
660
|
+
})
|
|
661
|
+
.catch(() => {});
|
|
640
662
|
},
|
|
641
663
|
deniedNo: async (line) => {
|
|
642
664
|
denied.delete(line.denial.id);
|
|
643
665
|
$.ui.invalidate('ui.render');
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
666
|
+
$.prompt
|
|
667
|
+
.submit({
|
|
668
|
+
text: `The person won't run ${line.denial.command ?? line.denial.what} now (Claude Code refused it for ${line.denial.who}). Don't try to get around it: carry on with other work, and if a story can't go on without it, ask with code-kit ask --blocks.`,
|
|
669
|
+
})
|
|
670
|
+
.catch(() => {});
|
|
647
671
|
},
|
|
648
672
|
// REPORT-3: the person sends a drafted report to code-kit's GitHub, or discards it.
|
|
649
673
|
sendReport: async (line) => {
|
|
@@ -675,6 +699,40 @@ export function register(on) {
|
|
|
675
699
|
heldId: line.held.id,
|
|
676
700
|
outside: line.held.outside ?? null,
|
|
677
701
|
}),
|
|
702
|
+
// ACT-5: a side call over the lead's own conversation (it shares the prompt cache, and nothing is
|
|
703
|
+
// added to the conversation); before the lead's first reply, from the prompt's facts alone.
|
|
704
|
+
sideCall: async (prompt) => {
|
|
705
|
+
const failed = (err) => ({ isAnswered: false, reason: String(err?.message ?? err) });
|
|
706
|
+
const reply = await $.model.fork({ prompt }).catch(failed);
|
|
707
|
+
if (reply.isAnswered || reply.reason !== 'nothing-to-fork') return reply;
|
|
708
|
+
return $.model.complete({ model: 'sonnet', prompt, maxTokens: 800 }).catch(failed);
|
|
709
|
+
},
|
|
710
|
+
// ASK-5: a question about a question, asked in the answer box before answering.
|
|
711
|
+
askAnswer: async (question) => {
|
|
712
|
+
const q = question.trim();
|
|
713
|
+
if (!answering || !q) return;
|
|
714
|
+
const asked = answering;
|
|
715
|
+
const turn = { question: q, pending: true };
|
|
716
|
+
const thread = asked.aside.thread;
|
|
717
|
+
answering = { ...asked, aside: { thread: [...thread, turn], draft: '' } };
|
|
718
|
+
$.ui.invalidate('ui.render');
|
|
719
|
+
const reply = await act.sideCall(questionAsidePrompt({ q: asked, thread, question: q }));
|
|
720
|
+
if (!answering || answering.question !== asked.question) return;
|
|
721
|
+
const done = reply.isAnswered
|
|
722
|
+
? { question: q, answer: reply.text }
|
|
723
|
+
: {
|
|
724
|
+
question: q,
|
|
725
|
+
error: `No answer: ${reply.reason}${reply.status ? ` (${reply.status})` : ''}`,
|
|
726
|
+
};
|
|
727
|
+
answering = {
|
|
728
|
+
...answering,
|
|
729
|
+
aside: {
|
|
730
|
+
...answering.aside,
|
|
731
|
+
thread: answering.aside.thread.map((t) => (t === turn ? done : t)),
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
$.ui.invalidate('ui.render');
|
|
735
|
+
},
|
|
678
736
|
// ACT-5: a question about the approval, answered in its pane by a side call over the lead's own
|
|
679
737
|
// conversation (it shares the prompt cache, and nothing is added to the conversation); before the
|
|
680
738
|
// lead's first reply, from code-kit's facts alone.
|
|
@@ -694,13 +752,7 @@ export function register(on) {
|
|
|
694
752
|
thread,
|
|
695
753
|
question: q,
|
|
696
754
|
});
|
|
697
|
-
|
|
698
|
-
.fork({ prompt })
|
|
699
|
-
.catch((err) => ({ isAnswered: false, reason: String(err?.message ?? err) }));
|
|
700
|
-
if (!reply.isAnswered && reply.reason === 'nothing-to-fork')
|
|
701
|
-
reply = await $.model
|
|
702
|
-
.complete({ model: 'sonnet', prompt, maxTokens: 800 })
|
|
703
|
-
.catch((err) => ({ isAnswered: false, reason: String(err?.message ?? err) }));
|
|
755
|
+
const reply = await act.sideCall(prompt);
|
|
704
756
|
// Answered for this approval only: a pane closed or moved on meanwhile keeps what it has.
|
|
705
757
|
if (!approving || approving.request !== asked.request) return;
|
|
706
758
|
const done = reply.isAnswered
|
|
@@ -2004,6 +2056,11 @@ export function register(on) {
|
|
|
2004
2056
|
},
|
|
2005
2057
|
onSubmit: (value) => act.confirmAnswer(value),
|
|
2006
2058
|
onCancel: () => act.cancelAnswer(),
|
|
2059
|
+
onAskInput: (value) => {
|
|
2060
|
+
answering = { ...answering, aside: { ...answering.aside, draft: value } };
|
|
2061
|
+
$.ui.invalidate('ui.render');
|
|
2062
|
+
},
|
|
2063
|
+
onAsk: (value) => act.askAnswer(value),
|
|
2007
2064
|
});
|
|
2008
2065
|
});
|
|
2009
2066
|
|