@warren-dean/code-kit 0.12.2 → 0.14.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 +21 -0
- package/README.md +113 -40
- package/SECURITY.md +11 -2
- package/bin/code-kit.mjs +87 -23
- package/hooks/guard-bash.mjs +40 -0
- package/hooks/lib/checks.mjs +19 -6
- package/hooks/lib/config.mjs +8 -0
- package/hooks/lib/loop-state.mjs +49 -9
- 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/verify.mjs +53 -7
- package/hooks/mod/decisions.mjs +99 -4
- package/hooks/mod/loop.mjs +75 -1
- package/hooks/mod/mission.mjs +19 -15
- package/hooks/mod/panes.mjs +11 -1
- package/hooks/mod/register.mjs +361 -35
- package/hooks/mod/register.test.ts +508 -70
- package/hooks/mod/view.mjs +161 -28
- 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/loop.mjs
CHANGED
|
@@ -75,7 +75,7 @@ export function leadPrompt(step, { cli = 'code-kit' } = {}) {
|
|
|
75
75
|
// MQ-2: the head of the merge queue, where the lead merges within the delegated rules.
|
|
76
76
|
if (step.step === 'merge')
|
|
77
77
|
return ids.length
|
|
78
|
-
? `Merge ${ids[0]}, next in the merge queue: run node "${cli}" queue merge --delegated. It verifies the branch against the base as it is now, and sends it back to its lane if it conflicts or fails.`
|
|
78
|
+
? `Merge ${ids[0]}, next in the merge queue: run node "${cli}" queue merge --delegated. It verifies the branch against the base as it is now, and sends it back to its lane if it conflicts or fails; a branch that moved after its review leaves the queue for you to review again.`
|
|
79
79
|
: null;
|
|
80
80
|
// SETUP-2: the base has no rules yet; the lead puts the approved ones there, then merges as usual.
|
|
81
81
|
if (step.step === 'rules')
|
|
@@ -148,3 +148,77 @@ export function restartPrompt({ type, id, story, minutes }) {
|
|
|
148
148
|
/** The message a quiet agent gets (LOOP-3). */
|
|
149
149
|
export const nudgeText = (minutes) =>
|
|
150
150
|
`No tool call for ${minutes} minutes: report where you are, or carry on.`;
|
|
151
|
+
|
|
152
|
+
/** LOOP-11: what a lead turn left the build at, as the loop reads the lead's reply. */
|
|
153
|
+
export const OUTCOMES = new Set(['done', 'started', 'needs-person', 'blocked']);
|
|
154
|
+
const ANSWER_SHOWN = 4000;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* LOOP-11: the side call that reads the lead's last reply before the loop prompts it again: over the
|
|
158
|
+
* lead's own conversation (so it knows the whole exchange), with the reply itself for a call that
|
|
159
|
+
* can't fork. `prompt` is the loop's own prompt for that turn, or null when the person started it.
|
|
160
|
+
*/
|
|
161
|
+
export function outcomePrompt({ prompt = null, answer = '' }) {
|
|
162
|
+
const reply = String(answer ?? '').trim();
|
|
163
|
+
const shown =
|
|
164
|
+
reply.length > ANSWER_SHOWN ? `${reply.slice(0, ANSWER_SHOWN)}\n[… cut short]` : reply;
|
|
165
|
+
return [
|
|
166
|
+
prompt
|
|
167
|
+
? `code-kit's lead loop prompted you: "${prompt}"`
|
|
168
|
+
: "The person prompted you, and code-kit's lead loop is about to prompt you with the build's next step.",
|
|
169
|
+
`Your reply ended your turn. It read:\n"""\n${shown}\n"""`,
|
|
170
|
+
'Say where that reply left things, so the loop knows whether to prompt you again. Answer with one JSON object and nothing else:',
|
|
171
|
+
'{"outcome": "done" | "started" | "needs-person" | "blocked", "question": "…", "options": [{"label": "…", "effect": "…"}], "reason": "…"}',
|
|
172
|
+
'- done: you finished what was asked, or there was nothing to do.',
|
|
173
|
+
'- started: you started work that carries on without you, such as lane agents.',
|
|
174
|
+
"- needs-person: you asked the person something, or wait on their answer, approval or feedback before you go on. question: what you wait on, as one question; options: 2 to 4 answers they could give (a label of at most five words, and what it does), or [] when it's theirs to put in words.",
|
|
175
|
+
"- blocked: you didn't take the step, for a reason no answer from the person settles now. reason: why, in a sentence.",
|
|
176
|
+
].join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* LOOP-11: the side call's answer: { outcome, question, options: [{ label, effect }], reason }, or
|
|
181
|
+
* null when it isn't one (the loop then goes on as before).
|
|
182
|
+
*/
|
|
183
|
+
export function readOutcome(text) {
|
|
184
|
+
const raw = String(text ?? '');
|
|
185
|
+
const json = raw.slice(raw.indexOf('{'), raw.lastIndexOf('}') + 1);
|
|
186
|
+
let parsed;
|
|
187
|
+
try {
|
|
188
|
+
parsed = JSON.parse(json);
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (!parsed || !OUTCOMES.has(parsed.outcome)) return null;
|
|
193
|
+
const words = (s, n) =>
|
|
194
|
+
typeof s === 'string' && s.trim() ? s.replace(/\s+/g, ' ').trim().slice(0, n) : null;
|
|
195
|
+
const options = (Array.isArray(parsed.options) ? parsed.options : [])
|
|
196
|
+
.map((o) => ({ label: words(o?.label, 40), effect: words(o?.effect, 200) }))
|
|
197
|
+
.filter((o) => o.label)
|
|
198
|
+
.slice(0, 4);
|
|
199
|
+
return {
|
|
200
|
+
outcome: parsed.outcome,
|
|
201
|
+
question: words(parsed.question, 400),
|
|
202
|
+
options: options.length >= 2 ? options : [],
|
|
203
|
+
reason: words(parsed.reason, 300),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* LOOP-12: a design is made with the person, so the loop hands a design story to the lead once; while
|
|
209
|
+
* the stories `next` names have all been handed over, it leaves them to the person and the lead.
|
|
210
|
+
*/
|
|
211
|
+
export const designHandedOver = (step, handed) =>
|
|
212
|
+
step?.step === 'design' &&
|
|
213
|
+
(step.args ?? '')
|
|
214
|
+
.split(/\s+/)
|
|
215
|
+
.filter(Boolean)
|
|
216
|
+
.every((id) => handed.has(id));
|
|
217
|
+
|
|
218
|
+
/** LOOP-11: the lead's prompt with the person's answer to what it waited on. */
|
|
219
|
+
export const answeredPrompt = (question, answer) => {
|
|
220
|
+
const said = /[.!?]$/.test(answer) ? answer : `${answer}.`;
|
|
221
|
+
return question
|
|
222
|
+
? `The person answered what you asked ("${question}"): ${said} Carry on with it.`
|
|
223
|
+
: `The person says: ${said} Carry on with it.`;
|
|
224
|
+
};
|
package/hooks/mod/mission.mjs
CHANGED
|
@@ -50,7 +50,7 @@ export function missionLines({
|
|
|
50
50
|
}) {
|
|
51
51
|
if (state?.kind !== 'ok') return [];
|
|
52
52
|
const working = state.lanes.filter((l) => l.name !== 'lead' && l.active && live[l.name]);
|
|
53
|
-
const loopOn = Boolean(loop?.started) && !['off', 'stopped'].includes(loop?.state);
|
|
53
|
+
const loopOn = Boolean(loop?.started) && !['off', 'stopped', 'elsewhere'].includes(loop?.state);
|
|
54
54
|
if (!leadBusy && !working.length && !loopOn) return [];
|
|
55
55
|
const rows = [];
|
|
56
56
|
const goal = goalOf(state.stories);
|
|
@@ -64,25 +64,29 @@ export function missionLines({
|
|
|
64
64
|
? `lead ${live.lead?.tool ? `at work: ${live.lead.tool}` : 'at work'}`
|
|
65
65
|
: 'lead idle';
|
|
66
66
|
const loopWord =
|
|
67
|
-
loop?.state === '
|
|
68
|
-
? 'loop
|
|
69
|
-
: loop?.state === '
|
|
70
|
-
? 'loop
|
|
71
|
-
: loop?.state === '
|
|
72
|
-
? '
|
|
73
|
-
:
|
|
74
|
-
?
|
|
75
|
-
:
|
|
67
|
+
loop?.state === 'elsewhere'
|
|
68
|
+
? 'loop in another session'
|
|
69
|
+
: loop?.state === 'paused'
|
|
70
|
+
? 'loop paused'
|
|
71
|
+
: loop?.state === 'off'
|
|
72
|
+
? 'loop off'
|
|
73
|
+
: loop?.state === 'done'
|
|
74
|
+
? 'milestone done'
|
|
75
|
+
: loopOn
|
|
76
|
+
? `loop ${loop.autonomy === 'propose' ? 'proposing' : 'on'}${loop.last ? ` · last: ${loop.last}` : ''}`
|
|
77
|
+
: null;
|
|
76
78
|
rows.push({
|
|
77
79
|
kind: 'now',
|
|
78
80
|
label: 'now',
|
|
79
81
|
text: [lead, loopWord].filter(Boolean).join(' · '),
|
|
80
82
|
actions:
|
|
81
|
-
loop?.state === '
|
|
82
|
-
? [{ id: '
|
|
83
|
-
:
|
|
84
|
-
? [{ id: '
|
|
85
|
-
:
|
|
83
|
+
loop?.state === 'elsewhere'
|
|
84
|
+
? [{ id: 'leadHere', label: 'Lead from here' }]
|
|
85
|
+
: loop?.state === 'paused'
|
|
86
|
+
? [{ id: 'resume', label: 'Resume' }]
|
|
87
|
+
: loopOn && loop?.state === 'running'
|
|
88
|
+
? [{ id: 'pause', label: 'Pause' }]
|
|
89
|
+
: [],
|
|
86
90
|
});
|
|
87
91
|
const next = nextText(nextStep);
|
|
88
92
|
if (next && loop?.state !== 'done') rows.push({ kind: 'next', label: 'next', text: next });
|
package/hooks/mod/panes.mjs
CHANGED
|
@@ -1357,7 +1357,17 @@ export function decisionsTab(cards, ui, { Box, Text, Button }, on) {
|
|
|
1357
1357
|
paddingX: 1,
|
|
1358
1358
|
rowGap: 0,
|
|
1359
1359
|
children: [
|
|
1360
|
-
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
|
+
),
|
|
1361
1371
|
...(card.asked ? [row('You asked for', card.asked)] : []),
|
|
1362
1372
|
...(card.story
|
|
1363
1373
|
? [
|