@warren-dean/code-kit 0.13.0 → 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 +10 -0
- package/README.md +7 -2
- package/bin/code-kit.mjs +26 -6
- package/hooks/guard-bash.mjs +5 -0
- package/hooks/lib/loop-state.mjs +49 -9
- package/hooks/mod/decisions.mjs +75 -0
- package/hooks/mod/loop.mjs +74 -0
- package/hooks/mod/mission.mjs +19 -15
- package/hooks/mod/register.mjs +283 -14
- package/hooks/mod/register.test.ts +335 -8
- package/hooks/mod/view.mjs +66 -4
- package/package.json +1 -1
|
@@ -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
|
+
"version": "0.14.0",
|
|
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.
|
|
3
|
+
"version": "0.14.0",
|
|
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,16 @@
|
|
|
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.14.0 (2026-10-16)
|
|
6
|
+
|
|
7
|
+
The lead loop stays in one session, listens to the lead before prompting it again, and asks your choices in Claude Code's own question dialog.
|
|
8
|
+
|
|
9
|
+
- Only one session in a project runs the loop. Opening a second session there, for anything else, used to have code-kit prompt that session's lead too. The first session whose loop acts now holds a lead lease in `.claude/state/loop.json` and renews it while it runs. Any other session leaves its lead alone, and its band says "the loop runs in another session" with Lead from here to move the loop over. A new session also no longer clears a pause the running session made. `code-kit loop lead` and `code-kit loop release` record the lease, and the hooks refuse both to agents.
|
|
10
|
+
- The loop reads the lead's last reply before prompting again, with a side call over the lead's own conversation. When the reply waits on you (a question, a design to look at, an approval), or you stopped the lead's turn, nothing more is prompted until you answer or speak to the lead. The band says what the lead is waiting on, and its question is asked in Claude Code's dialog with the answers it offered. A step the lead didn't take waits in the band with the lead's reason and Ask again. Before, the loop kept re-sending the same step and then asked you whether to ask again, without knowing what the lead had said.
|
|
11
|
+
- A design story is handed to the lead once a session. While you and the lead design it, the loop leaves it to you instead of prompting the design step again each time a screen is approved.
|
|
12
|
+
- Choices are asked in Claude Code's own question dialog while the lead is idle: how hands-off the loop is, a step under `propose`, a repeated or blocked step, a stalled lane, a stale claim, and a question from the lead with answers to pick from. Each is asked once; dismissed, it stays in the band and the Decisions tab. Approvals, send-backs and settings, which need your reason, stay in the pane.
|
|
13
|
+
- The card for a step still next after two prompts now says why it's yours (the lead was asked twice and something stopped it), where it used to say you'd asked to approve each step.
|
|
14
|
+
|
|
5
15
|
## 0.13.0 (2026-10-14)
|
|
6
16
|
|
|
7
17
|
A fix proves its bug, tests can't be weakened without you, and a passing `verify` says exactly what it covered.
|
package/README.md
CHANGED
|
@@ -331,8 +331,12 @@ Each button has a digit: type it in an empty prompt to press it. The band keeps
|
|
|
331
331
|
The loop keeps the build moving between your prompts, under `harness.autonomy`. `/code-kit:init` asks you how hands-off you want it. Until you've chosen, its first step waits in the band ("code-kit can keep the build moving between your prompts") with **Keep going**, **Ask each time** and **Off**, and your answer is recorded as your setting.
|
|
332
332
|
|
|
333
333
|
- **Each step.** Once the lead has finished a turn, with the lead idle and nothing typed in the prompt, the loop asks `code-kit next` and prompts the lead to dispatch the ready stories, review a finished one, or build its own. It never prompts the same step twice while nothing has changed, and never a story an agent is still on: one its lane agent is building, or a reviewer is reviewing, waits until that agent finishes ("waiting: ST-10 is still with its agent").
|
|
334
|
-
- **
|
|
335
|
-
- **
|
|
334
|
+
- **It reads the lead's reply first.** Before prompting again, the loop reads the lead's last reply with a side call over the lead's own conversation. A reply that waits on you (a question, a design to look at, an approval), or a turn you stopped, holds the loop until you answer or speak to the lead: the band says what the lead waits on, and its question is asked in Claude Code's own question dialog. A step the lead didn't take waits in the band with its reason and **Ask again**.
|
|
335
|
+
- **A step that doesn't move.** One the lead was prompted with twice in a row that is still next goes to you instead: "Merge lead/st-1: still next after 2 prompts. Ask the lead again?" with **Ask again**.
|
|
336
|
+
- **Designs.** A design story is handed to the lead once a session; while you and the lead design it, the loop leaves it to you.
|
|
337
|
+
- **One session runs it.** The first session whose loop acts runs it for the project. Another session you open there, for any reason, never prompts its lead: its band reads "the loop runs in another session", with **Lead from here** to move the loop to it.
|
|
338
|
+
- **Modes.** **Pause** and **Resume** stop and start it. Under `propose`, each step waits for **Go**; under `off`, the loop does nothing. When every story is done, the band says so and the loop stops.
|
|
339
|
+
- **Choices in Claude Code's dialog.** A decision you settle by choosing (how hands-off the loop is, a step to take, a stalled lane, a stale claim, a question from the lead with answers offered) is asked in Claude Code's own question dialog while the lead is idle, once; dismissed, it stays in the band and the Decisions tab. Approvals, send-backs and settings, which need your reason, stay in the pane.
|
|
336
340
|
- **Stalled lanes.** A lane agent with no tool call for `harness.stall.nudgeMinutes` (5) is messaged, unless its work is done (it handed back, or its branch is in review or the merge queue). At `stall.restartMinutes` (10) the lead is asked to stop it and dispatch its story again, at most `stall.maxRestarts` (2) times, and then the band flags the lane.
|
|
337
341
|
- **The reviewer agent**, with `harness.agents.reviewer.on`. A finished branch is first reviewed in the background by code-kit's reviewer agent, which the lead starts: it runs `verify` and the review checklist, read-only, and grades each finding blocker, concern or nit. The lead's review prompt then carries the findings, and a blocker means the branch is sent back. The pane shows each review, running with its clock or what it found. If the reviewer hasn't reported within 15 minutes, or the plan's 5-hour use is past `harness.background.pauseAtPercent`, the lead reviews without it.
|
|
338
342
|
- **The merge queue** (`code-kit queue`). A branch the lead's review passes joins the queue. Branches merge one at a time, in the order they passed, each verified against the base as it is then: the lead merges where `approvals.delegate.merge` is on under `autonomous`, otherwise the band shows the head with **Merge** for you. Who may merge is read from the config committed on the base, so a branch can't grant itself merges. The queue records the commit each review passed, and merges exactly that commit: a branch that moves on after its review leaves the queue (`moved`) for the lead to review it again. A head that conflicts or fails verify goes back to its lane. A merged lane's worktree is removed when it holds nothing uncommitted. The pane lists the queue, and lanes whose branch waits in it read `queued`.
|
|
@@ -442,6 +446,7 @@ code-kit queue rules # the lead puts th
|
|
|
442
446
|
code-kit queue merge --delegated|--person # merge the head at the commit its review passed, verified against the base as it is now; a conflict or failed verify sends it back to its lane, and a branch that moved since its review leaves the queue to be reviewed again
|
|
443
447
|
code-kit loop [--json] # whether the person paused the lead loop; other tools' background work pauses with it
|
|
444
448
|
code-kit loop pause|resume # the person's act, as Pause and Resume in the band
|
|
449
|
+
code-kit loop lead|release --session <id> [--take] # which session runs the loop: the mod's, and Lead from here in the band
|
|
445
450
|
code-kit story <id> [--json] # one story's requirements, spec-check report, diff against the base and verify (without checks), for the drill-down
|
|
446
451
|
code-kit timeline [--json] # when each story's work happened: its branch made, last commit, merge, send-backs and approvals
|
|
447
452
|
code-kit journal [--json] # the build's lineage: the request, its decisions, the spec, each story handed out, sent back and merged, each approval and who gave it
|
package/bin/code-kit.mjs
CHANGED
|
@@ -79,7 +79,7 @@ import { PROJECT, projectMap, storyMap, trail } from '../hooks/lib/storymap.mjs'
|
|
|
79
79
|
import { journal } from '../hooks/lib/journal.mjs';
|
|
80
80
|
import { dequeue, enqueue, lastMerged, readQueue, settle, waiting } from '../hooks/lib/queue.mjs';
|
|
81
81
|
import { SETTINGS, parseSetting, settingOf, withSetting } from '../hooks/lib/harness.mjs';
|
|
82
|
-
import { readLoop, writeLoop } from '../hooks/lib/loop-state.mjs';
|
|
82
|
+
import { claimLead, readLoop, releaseLead, writeLoop } from '../hooks/lib/loop-state.mjs';
|
|
83
83
|
import {
|
|
84
84
|
claimChanges,
|
|
85
85
|
storyOfBranch,
|
|
@@ -162,6 +162,7 @@ const FLAGS = [
|
|
|
162
162
|
'--peek',
|
|
163
163
|
'--off',
|
|
164
164
|
'--blocks',
|
|
165
|
+
'--take',
|
|
165
166
|
];
|
|
166
167
|
const flag = (name) => args.includes(name);
|
|
167
168
|
const writeBaseline = flag('--write');
|
|
@@ -1814,6 +1815,8 @@ function reportCommand() {
|
|
|
1814
1815
|
/**
|
|
1815
1816
|
* The lead loop's pause (LOOP-6): `loop` says whether the person paused it; `loop pause|resume`, the
|
|
1816
1817
|
* person's act from the code-kit band, records it, so other tools' background work pauses with it.
|
|
1818
|
+
* LOOP-10: `loop lead` takes or renews the lease of the one session that runs the loop, and `loop
|
|
1819
|
+
* release` gives it up; the mod runs both, and the hooks refuse all four to agents.
|
|
1817
1820
|
*/
|
|
1818
1821
|
function loopCommand() {
|
|
1819
1822
|
const root = resolve('.');
|
|
@@ -1822,14 +1825,31 @@ function loopCommand() {
|
|
|
1822
1825
|
writeLoop(root, sub === 'pause');
|
|
1823
1826
|
return out(sub === 'pause' ? 'The lead loop is paused.' : 'The lead loop is running.');
|
|
1824
1827
|
}
|
|
1825
|
-
|
|
1828
|
+
// LOOP-10: one session in the project runs the loop; the mod takes and renews the lease.
|
|
1829
|
+
if (sub === 'lead' || sub === 'release') {
|
|
1830
|
+
if (!sessionId) die(`code-kit loop ${sub} needs the session: --session <id>.`);
|
|
1831
|
+
if (sub === 'release')
|
|
1832
|
+
return out(
|
|
1833
|
+
releaseLead(root, sessionId)
|
|
1834
|
+
? 'This session no longer runs the lead loop.'
|
|
1835
|
+
: "This session wasn't running the lead loop.",
|
|
1836
|
+
);
|
|
1837
|
+
const r = claimLead(root, sessionId, { take: flag('--take') });
|
|
1838
|
+
if (!r.ok)
|
|
1839
|
+
die(
|
|
1840
|
+
`Another session (${r.lead.session}, renewed ${r.lead.at}) runs the lead loop in this project. Leave it there, or take it over with --take.`,
|
|
1841
|
+
);
|
|
1842
|
+
return out('This session runs the lead loop.');
|
|
1843
|
+
}
|
|
1844
|
+
if (sub !== undefined)
|
|
1845
|
+
die(
|
|
1846
|
+
'code-kit loop [--json] | loop pause | loop resume | loop lead --session <id> [--take] | loop release --session <id>',
|
|
1847
|
+
);
|
|
1826
1848
|
const state = readLoop(root);
|
|
1827
1849
|
out(
|
|
1828
1850
|
flag('--json')
|
|
1829
1851
|
? JSON.stringify(state)
|
|
1830
|
-
: state.paused
|
|
1831
|
-
? `The lead loop is paused (since ${state.at}).`
|
|
1832
|
-
: 'The lead loop is running.',
|
|
1852
|
+
: `${state.paused ? `The lead loop is paused (since ${state.at}).` : 'The lead loop is running.'}${state.lead ? ` Session ${state.lead.session} runs it.` : ''}`,
|
|
1833
1853
|
);
|
|
1834
1854
|
}
|
|
1835
1855
|
|
|
@@ -1881,7 +1901,7 @@ function usage() {
|
|
|
1881
1901
|
' | settings [--json] | settings set <key> <value> --reason "…" [--via pane]\n' +
|
|
1882
1902
|
' | hold <id> [--minutes N] | hold <id> --answer approved|refused\n' +
|
|
1883
1903
|
' | queue [--json] | queue add|drop <branch> | queue rules | queue merge --delegated|--person\n' +
|
|
1884
|
-
' | loop [--json] | loop pause|resume\n' +
|
|
1904
|
+
' | loop [--json] | loop pause|resume | loop lead|release --session id [--take]\n' +
|
|
1885
1905
|
' | map <story>|project [--json] | trail [--json] | journal [--json]\n' +
|
|
1886
1906
|
' | story <id> [--json] | timeline [--json]\n' +
|
|
1887
1907
|
' | team [--json] [--fresh] | team since [--json] [--peek]\n' +
|
package/hooks/guard-bash.mjs
CHANGED
|
@@ -186,6 +186,11 @@ if (runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+loop\s+(pause|resume)\b`)))
|
|
|
186
186
|
block(
|
|
187
187
|
`Blocked: pausing or resuming the lead loop is the person's own act, from the code-kit band or their terminal. Ask them, and stop.\nCommand: ${cmd}`,
|
|
188
188
|
);
|
|
189
|
+
// LOOP-10: which session runs the loop is the mod's to record, and moving it the person's.
|
|
190
|
+
if (runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+loop\s+(lead|release)\b`)))
|
|
191
|
+
block(
|
|
192
|
+
`Blocked: which session runs the lead loop is recorded by the code-kit band, and moving it is the person's own act (Lead from here, in the band). Carry on with your work.\nCommand: ${cmd}`,
|
|
193
|
+
);
|
|
189
194
|
// TEAM-9: handing someone's claim to someone else is the tech lead's own decision too.
|
|
190
195
|
if (runs(cmd, new RegExp(String.raw`^${CODE_KIT}\s+team\s+reassign\b`)))
|
|
191
196
|
block(
|
package/hooks/lib/loop-state.mjs
CHANGED
|
@@ -1,26 +1,66 @@
|
|
|
1
|
-
//
|
|
2
|
-
// tools sharing the project can hold their own background work while it's paused
|
|
3
|
-
//
|
|
1
|
+
// The lead loop's state, kept beside the merge queue (spec 05): whether the person has paused it
|
|
2
|
+
// (LOOP-6), so other tools sharing the project can hold their own background work while it's paused;
|
|
3
|
+
// and which session runs it (LOOP-10), so a second session opened in the project never prompts its
|
|
4
|
+
// lead too. The mod records both through the CLI.
|
|
4
5
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
6
|
import { dirname, join } from 'node:path';
|
|
6
7
|
import { projectWorktrees } from './rules.mjs';
|
|
7
8
|
|
|
8
9
|
export const LOOP_FILE = '.claude/state/loop.json';
|
|
10
|
+
/** LOOP-10: a lead session renews its lease well within this; one not renewed for this long has ended. */
|
|
11
|
+
export const LEAD_LEASE_MS = 90000;
|
|
9
12
|
|
|
10
13
|
const loopFile = (root) => join(projectWorktrees(root)[0] ?? root, LOOP_FILE);
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
export function readLoop(root) {
|
|
15
|
+
function readRaw(root) {
|
|
14
16
|
try {
|
|
15
17
|
const state = JSON.parse(readFileSync(loopFile(root), 'utf8'));
|
|
16
|
-
return
|
|
18
|
+
return state && typeof state === 'object' ? state : {};
|
|
17
19
|
} catch {
|
|
18
|
-
return {
|
|
20
|
+
return {};
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
function writeRaw(root, state) {
|
|
23
25
|
const file = loopFile(root);
|
|
24
26
|
mkdirSync(dirname(file), { recursive: true });
|
|
25
|
-
writeFileSync(file, `${JSON.stringify(
|
|
27
|
+
writeFileSync(file, `${JSON.stringify(state)}\n`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The session holding the lead lease, while it holds it: { session, at }, or null. */
|
|
31
|
+
const liveLead = (state, now) => {
|
|
32
|
+
const lead = state.lead;
|
|
33
|
+
if (!lead?.session || !lead.at) return null;
|
|
34
|
+
return now - Date.parse(lead.at) < LEAD_LEASE_MS ? { session: lead.session, at: lead.at } : null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** { paused, at, lead }: not paused when nothing was recorded; `lead` is the live lead session or null. */
|
|
38
|
+
export function readLoop(root, now = Date.now()) {
|
|
39
|
+
const state = readRaw(root);
|
|
40
|
+
return { paused: state.paused === true, at: state.at ?? null, lead: liveLead(state, now) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function writeLoop(root, paused, now = Date.now()) {
|
|
44
|
+
writeRaw(root, { ...readRaw(root), paused, at: new Date(now).toISOString() });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* LOOP-10: `session` takes or renews the lead lease. Another live session keeps it unless `take` (the
|
|
49
|
+
* person moved the loop here): { ok: true } or { ok: false, lead } naming the session that has it.
|
|
50
|
+
*/
|
|
51
|
+
export function claimLead(root, session, { take = false } = {}, now = Date.now()) {
|
|
52
|
+
const state = readRaw(root);
|
|
53
|
+
const lead = liveLead(state, now);
|
|
54
|
+
if (lead && lead.session !== session && !take) return { ok: false, lead };
|
|
55
|
+
writeRaw(root, { ...state, lead: { session, at: new Date(now).toISOString() } });
|
|
56
|
+
return { ok: true, lead: null };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** LOOP-10: the lead session ends: its lease goes, so the next session to act takes the loop at once. */
|
|
60
|
+
export function releaseLead(root, session) {
|
|
61
|
+
const state = readRaw(root);
|
|
62
|
+
if (state.lead?.session !== session) return false;
|
|
63
|
+
const { lead: _lead, ...rest } = state;
|
|
64
|
+
writeRaw(root, rest);
|
|
65
|
+
return true;
|
|
26
66
|
}
|
package/hooks/mod/decisions.mjs
CHANGED
|
@@ -22,6 +22,7 @@ export const DECISION_KINDS = new Set([
|
|
|
22
22
|
'brief',
|
|
23
23
|
'denied',
|
|
24
24
|
'report',
|
|
25
|
+
'waiting',
|
|
25
26
|
]);
|
|
26
27
|
|
|
27
28
|
/** What each answer does, in the person's words. */
|
|
@@ -54,6 +55,10 @@ const EFFECT = {
|
|
|
54
55
|
'Puts it in your prompt after !, so you run it yourself; press Enter to run it, then Done it.',
|
|
55
56
|
deniedDone: 'The lead is told you ran it, or allowed it, and carries on.',
|
|
56
57
|
deniedNo: "The lead is told you won't run it now, and carries on with other work.",
|
|
58
|
+
answerLead: 'The lead is told your answer and carries on.',
|
|
59
|
+
answerLeadText: 'Type your answer; the lead is told it and carries on.',
|
|
60
|
+
askAgain: 'The lead is asked to take this step again now.',
|
|
61
|
+
carryOn: 'The loop goes on with whatever is next.',
|
|
57
62
|
sendReport:
|
|
58
63
|
"Files it on code-kit's GitHub under your account (a comment instead, if it's filed already).",
|
|
59
64
|
discardReport: "It's not sent, and goes.",
|
|
@@ -133,6 +138,18 @@ function optionsOf(line) {
|
|
|
133
138
|
{ id: 'sendReport', label: 'Send' },
|
|
134
139
|
{ id: 'discardReport', label: 'Discard' },
|
|
135
140
|
];
|
|
141
|
+
// LOOP-11: what the lead waits on, read from its reply: its answers, or the person's own words.
|
|
142
|
+
if (line.kind === 'waiting' && line.waiting.kind === 'person' && line.waiting.question)
|
|
143
|
+
return [
|
|
144
|
+
...line.waiting.options.map((o, i) => ({
|
|
145
|
+
id: `answerLead-${i}`,
|
|
146
|
+
act: 'answerLead',
|
|
147
|
+
label: o.label,
|
|
148
|
+
effect: o.effect,
|
|
149
|
+
line: { ...line, choice: o.label },
|
|
150
|
+
})),
|
|
151
|
+
{ id: 'answerLeadText', label: line.waiting.options.length ? 'Something else…' : 'Answer…' },
|
|
152
|
+
];
|
|
136
153
|
return line.actions;
|
|
137
154
|
}
|
|
138
155
|
|
|
@@ -149,6 +166,7 @@ const TAG = {
|
|
|
149
166
|
brief: 'your brief',
|
|
150
167
|
denied: 'refused',
|
|
151
168
|
report: 'bug report',
|
|
169
|
+
waiting: 'the lead',
|
|
152
170
|
};
|
|
153
171
|
|
|
154
172
|
/** At most `n` characters on one line, cut with an ellipsis. */
|
|
@@ -200,6 +218,8 @@ function headlineOf(line, story) {
|
|
|
200
218
|
};
|
|
201
219
|
if (line.kind === 'report')
|
|
202
220
|
return { headline: line.report.title, sub: "drafted by an agent, for code-kit's GitHub" };
|
|
221
|
+
if (line.kind === 'waiting' && line.waiting.question)
|
|
222
|
+
return { headline: line.waiting.question, sub: 'from the lead, which waits on it' };
|
|
203
223
|
return { headline: line.text, sub: story ? `${story.id} ${story.title ?? ''}` : '' };
|
|
204
224
|
}
|
|
205
225
|
|
|
@@ -245,6 +265,12 @@ function whyOf(line, check) {
|
|
|
245
265
|
return `It's an open question in ${line.brief.path}, left for you: what gets built depends on your answer. Not sure what it means? Ask about it when you answer.`;
|
|
246
266
|
if (line.kind === 'denied')
|
|
247
267
|
return `Claude Code's own permission check refused it${line.denial.reason ? ` (${line.denial.reason})` : ''}, which code-kit can't approve past: only you can run it or allow it.`;
|
|
268
|
+
if (line.kind === 'waiting' && line.waiting.kind === 'person')
|
|
269
|
+
return "The lead's last reply waits on you, so the loop doesn't prompt it again until you answer, here or in the chat.";
|
|
270
|
+
if (line.kind === 'waiting' && line.waiting.kind === 'stopped')
|
|
271
|
+
return 'You stopped the lead mid-turn, so the loop leaves the lead to you until you say.';
|
|
272
|
+
if (line.kind === 'waiting' && line.waiting.kind === 'blocked')
|
|
273
|
+
return "The lead was asked to take this step and didn't, so the loop won't ask again until you say.";
|
|
248
274
|
if (line.kind === 'report')
|
|
249
275
|
return "code-kit got in an agent's way. It's sent under your GitHub account to a public repository, so read what it says first.";
|
|
250
276
|
return null;
|
|
@@ -363,6 +389,7 @@ export function decisionCards(lines, context = {}) {
|
|
|
363
389
|
: null) ??
|
|
364
390
|
(line.pull ? `Pull request #${line.pull.number} on ${line.pull.branch}` : null) ??
|
|
365
391
|
(line.question?.blocks ? `${line.question.story} waits on your answer.` : null) ??
|
|
392
|
+
(line.waiting?.label ? `The loop's last step: ${line.waiting.label}` : null) ??
|
|
366
393
|
(line.denial ? clip(line.denial.command ?? line.denial.what, 400) : null) ??
|
|
367
394
|
(line.report ? line.report.body : null),
|
|
368
395
|
settled,
|
|
@@ -381,3 +408,51 @@ export function contextOf(card) {
|
|
|
381
408
|
const reqs = card.requirements.map((r) => r.id).join(', ');
|
|
382
409
|
return `for ${card.story.id} ${card.story.title}${reqs ? ` (${reqs})` : ''}`;
|
|
383
410
|
}
|
|
411
|
+
|
|
412
|
+
/** DECIDE-4: the kinds of decision settled with a choice, asked in Claude Code's own question dialog. */
|
|
413
|
+
export const NATIVE_KINDS = new Set(['proposal', 'stalled', 'teamStale', 'question', 'waiting']);
|
|
414
|
+
|
|
415
|
+
/** One sentence ended as a question asks. */
|
|
416
|
+
const asked = (text, question) => `${String(text).replace(/[.\s]+$/, '')}. ${question}`;
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* DECIDE-4: a decision card as Claude Code's question dialog asks it: { key, question, header, options:
|
|
420
|
+
* [{ label, act, line }], typed }, or null for one that needs the pane (a reason to give, an answer to
|
|
421
|
+
* type with no choices offered). An option without `act` leaves the decision where it is; `typed` names
|
|
422
|
+
* the act that takes the person's own words, typed under the dialog's Other.
|
|
423
|
+
*/
|
|
424
|
+
export function nativeChoice(card) {
|
|
425
|
+
if (!NATIVE_KINDS.has(card.kind)) return null;
|
|
426
|
+
const line = card.line;
|
|
427
|
+
const w = line.waiting;
|
|
428
|
+
// What the person stopped, or a reply waiting on them with no question in it, is theirs to pick up
|
|
429
|
+
// in the chat: no dialog over what they're about to type.
|
|
430
|
+
if (card.kind === 'waiting' && !(w?.kind === 'person' && w.question) && w?.kind !== 'blocked')
|
|
431
|
+
return null;
|
|
432
|
+
const options = card.options
|
|
433
|
+
.filter((o) => !o.label.endsWith('…'))
|
|
434
|
+
.slice(0, 4)
|
|
435
|
+
.map((o) => ({ label: o.label, act: o.act ?? o.id, line: o.line ?? line }));
|
|
436
|
+
if (!options.length) return null;
|
|
437
|
+
if (options.length === 1) options.push({ label: 'Not now', act: null, line });
|
|
438
|
+
const question =
|
|
439
|
+
card.kind === 'proposal' && line.proposal?.first
|
|
440
|
+
? asked(line.text, 'How much should it do on its own?')
|
|
441
|
+
: card.kind === 'proposal' && line.proposal?.repeated
|
|
442
|
+
? `${card.why} ${line.text}`
|
|
443
|
+
: card.kind === 'stalled'
|
|
444
|
+
? asked(`${line.text}. ${card.why}`, 'Restart it?')
|
|
445
|
+
: card.kind === 'teamStale'
|
|
446
|
+
? asked(line.text, 'Keep your claim?')
|
|
447
|
+
: card.kind === 'waiting' && w.kind === 'blocked'
|
|
448
|
+
? asked(line.text, 'Ask the lead again?')
|
|
449
|
+
: card.headline;
|
|
450
|
+
return {
|
|
451
|
+
key: `${card.kind}:${card.headline}:${options.map((o) => o.label).join('|')}`,
|
|
452
|
+
question: /\?$/.test(question) ? question : `${question}?`,
|
|
453
|
+
header: card.tag.slice(0, 12),
|
|
454
|
+
options,
|
|
455
|
+
typed:
|
|
456
|
+
card.kind === 'question' ? 'answerTyped' : card.kind === 'waiting' ? 'answerLeadTyped' : null,
|
|
457
|
+
};
|
|
458
|
+
}
|
package/hooks/mod/loop.mjs
CHANGED
|
@@ -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 });
|