@warren-dean/code-kit 0.16.0 → 0.16.1
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 +8 -0
- package/README.md +1 -0
- package/hooks/mod/loop.mjs +73 -9
- package/hooks/mod/register.mjs +77 -4
- package/hooks/mod/register.test.ts +77 -0
- package/hooks/mod/view.mjs +2 -0
- 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.16.
|
|
12
|
+
"version": "0.16.1",
|
|
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.16.
|
|
3
|
+
"version": "0.16.1",
|
|
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,14 @@
|
|
|
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.16.1 (2026-10-16)
|
|
6
|
+
|
|
7
|
+
The lead loop stops asking for stories that are waiting on other work.
|
|
8
|
+
|
|
9
|
+
- When the lead says a story isn't due yet because it waits on work still going, such as a paired story a lane is building, a rebuild or a check running, the loop parks that story. A parked story is left out of every step, so a step that names it with others prompts the lead only for the others. Before, the loop held a declined step only while the step stayed exactly the same, so when one more story became ready, or any lane committed, it prompted the same reviews again.
|
|
10
|
+
- A parked story wakes when what it waits on moves: its own branch or state, the stories the lead named, or the unfinished stories in the plan that depend on it. Other lanes' commits don't wake it. After 30 minutes with nothing moving, the lead is asked once more.
|
|
11
|
+
- The band's next row says what each parked story waits on. A step the lead declined for another reason no longer holds up every other step.
|
|
12
|
+
|
|
5
13
|
## 0.16.0 (2026-10-16)
|
|
6
14
|
|
|
7
15
|
The lead runs the team inside your guardrails, and lanes ask the lead, never you.
|
package/README.md
CHANGED
|
@@ -332,6 +332,7 @@ The loop keeps the build moving between your prompts, under `harness.autonomy`.
|
|
|
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
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
|
+
- **Stories waiting on other work.** When the lead says a story isn't due yet because it waits on work still going (a paired story a lane is building, a rebuild, a check running), the loop parks that story and leaves it out of every step, so a step naming it with others prompts only the others. It wakes when what it waits on moves: its own branch, the stories the lead named, or the unfinished stories that depend on it. Other lanes' commits don't wake it, and after 30 minutes with nothing moving the lead is asked once more.
|
|
335
336
|
- **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
337
|
- **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
338
|
- **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.
|
package/hooks/mod/loop.mjs
CHANGED
|
@@ -40,8 +40,8 @@ export function storiesInFlight(agents) {
|
|
|
40
40
|
* gives way to the next one `next` listed; with nothing left, the loop waits, saying for what, until an
|
|
41
41
|
* agent finishes (which moves the project on).
|
|
42
42
|
*/
|
|
43
|
-
export function withoutInFlight(step, inFlight) {
|
|
44
|
-
if (!step || !inFlight?.size) return step;
|
|
43
|
+
export function withoutInFlight(step, inFlight, parked = new Map()) {
|
|
44
|
+
if (!step || (!inFlight?.size && !parked?.size)) return step;
|
|
45
45
|
const chain = [step, ...(step.then ?? [])].map(({ then: _then, ...s }) => s);
|
|
46
46
|
const kept = [];
|
|
47
47
|
const busy = [];
|
|
@@ -51,19 +51,72 @@ export function withoutInFlight(step, inFlight) {
|
|
|
51
51
|
continue;
|
|
52
52
|
}
|
|
53
53
|
const ids = (s.args ?? '').split(/\s+/).filter(Boolean);
|
|
54
|
-
const
|
|
55
|
-
|
|
54
|
+
const held = (id) => inFlight?.has(id.toUpperCase()) || parked?.has(id.toUpperCase());
|
|
55
|
+
const free = ids.filter((id) => !held(id));
|
|
56
|
+
busy.push(...ids.filter(held));
|
|
56
57
|
if (free.length) kept.push({ ...s, args: free.join(' ') });
|
|
57
58
|
}
|
|
58
59
|
if (kept.length) return { ...kept[0], ...(kept.length > 1 ? { then: kept.slice(1) } : {}) };
|
|
59
|
-
const names = [...new Set(busy)];
|
|
60
|
+
const names = [...new Set(busy.map((id) => id.toUpperCase()))];
|
|
61
|
+
const working = names.filter((id) => inFlight?.has(id));
|
|
62
|
+
// LOOP-13: what each parked story waits on, as the lead said.
|
|
63
|
+
const waits = names
|
|
64
|
+
.filter((id) => !inFlight?.has(id))
|
|
65
|
+
.map(
|
|
66
|
+
(id) =>
|
|
67
|
+
`${id} waits${parked.get(id)?.on?.length ? ` on ${parked.get(id).on.join(', ')}` : ''}`,
|
|
68
|
+
);
|
|
60
69
|
return {
|
|
61
70
|
step: 'wait',
|
|
62
|
-
why:
|
|
71
|
+
why: [
|
|
72
|
+
working.length
|
|
73
|
+
? `${working.join(', ')} ${working.length === 1 ? 'is' : 'are'} still with ${working.length === 1 ? 'its agent' : 'their agents'}`
|
|
74
|
+
: null,
|
|
75
|
+
...waits,
|
|
76
|
+
]
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.join('; ')
|
|
79
|
+
.concat('; the loop goes on when one moves'),
|
|
63
80
|
inFlight: names,
|
|
64
81
|
};
|
|
65
82
|
}
|
|
66
83
|
|
|
84
|
+
/** LOOP-13: how long a parked story waits with nothing it waits on moving, before the lead is asked again. */
|
|
85
|
+
export const PARK_MS = 30 * 60_000;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* LOOP-13: the stories to watch while `id` is parked: itself, the stories the lead said it waits on,
|
|
89
|
+
* and the stories that depend on it and aren't done (a pair the lead merges together).
|
|
90
|
+
*/
|
|
91
|
+
export function parkWatch(id, on = [], stories = []) {
|
|
92
|
+
const dependents = stories
|
|
93
|
+
.filter((s) => (s.dependsOn ?? []).includes(id) && s.state !== 'done')
|
|
94
|
+
.map((s) => s.id);
|
|
95
|
+
return [...new Set([id, ...on, ...dependents])];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* LOOP-13: where the watched stories stand, as one string: each one's state, its branch's commit and
|
|
100
|
+
* whether an agent is at work on it. A parked story wakes when this changes.
|
|
101
|
+
*/
|
|
102
|
+
export function storiesSignature(ids, stories = [], heads = new Map(), inFlight = new Set()) {
|
|
103
|
+
return ids
|
|
104
|
+
.map((id) => {
|
|
105
|
+
const s = stories.find((x) => x.id === id);
|
|
106
|
+
return `${id}:${s?.state ?? '?'}:${s?.branch ? (heads.get(s.branch) ?? '') : ''}:${inFlight.has(id) ? 'busy' : ''}`;
|
|
107
|
+
})
|
|
108
|
+
.join('|');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The story ids a step names, upper-cased: `review ST-158 ST-172` gives ST-158 and ST-172. */
|
|
112
|
+
export const stepStories = (step) =>
|
|
113
|
+
STORY_STEPS.has(step?.stepKind ?? step?.kind ?? step?.step)
|
|
114
|
+
? String(step?.target ?? step?.args ?? '')
|
|
115
|
+
.split(/\s+/)
|
|
116
|
+
.filter((id) => /^ST-\d+$/i.test(id))
|
|
117
|
+
.map((id) => id.toUpperCase())
|
|
118
|
+
: [];
|
|
119
|
+
|
|
67
120
|
/**
|
|
68
121
|
* The prompt for a step from `code-kit next --json` (LOOP-1), or null when it isn't the lead's to
|
|
69
122
|
* take. Every ready story is named: the dispatch skill decides how many lanes go (LOOP-2). A prompt
|
|
@@ -157,7 +210,7 @@ export const nudgeText = (minutes) =>
|
|
|
157
210
|
`No tool call for ${minutes} minutes: report where you are, or carry on.`;
|
|
158
211
|
|
|
159
212
|
/** LOOP-11: what a lead turn left the build at, as the loop reads the lead's reply. */
|
|
160
|
-
export const OUTCOMES = new Set(['done', 'started', 'needs-person', 'blocked']);
|
|
213
|
+
export const OUTCOMES = new Set(['done', 'started', 'needs-person', 'blocked', 'waiting']);
|
|
161
214
|
const ANSWER_SHOWN = 4000;
|
|
162
215
|
|
|
163
216
|
/**
|
|
@@ -175,11 +228,12 @@ export function outcomePrompt({ prompt = null, answer = '' }) {
|
|
|
175
228
|
: "The person prompted you, and code-kit's lead loop is about to prompt you with the build's next step.",
|
|
176
229
|
`Your reply ended your turn. It read:\n"""\n${shown}\n"""`,
|
|
177
230
|
'Say where that reply left things, so the loop knows whether to prompt you again. Answer with one JSON object and nothing else:',
|
|
178
|
-
'{"outcome": "done" | "started" | "needs-person" | "blocked", "question": "…", "options": [{"label": "…", "effect": "…"}], "reason": "…"}',
|
|
231
|
+
'{"outcome": "done" | "started" | "needs-person" | "blocked" | "waiting", "question": "…", "options": [{"label": "…", "effect": "…"}], "reason": "…", "waits": [{"story": "ST-n", "on": ["ST-n"], "why": "…"}]}',
|
|
179
232
|
'- done: you finished what was asked, or there was nothing to do.',
|
|
180
233
|
'- started: you started work that carries on without you, such as lane agents.',
|
|
181
234
|
"- 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.",
|
|
182
|
-
"-
|
|
235
|
+
"- waiting: the step isn't due yet for some or all of its stories: they wait on other work still going (a lane building a story they merge with, a rebuild, a check running). waits: one entry per story that waits, with the stories it waits on (their ST ids, [] when it's other work) and why, in a few words. Use this, not done, when you said a story can't be taken yet.",
|
|
236
|
+
"- blocked: you didn't take the step, for a reason no answer from the person settles now and no work in flight will change. reason: why, in a sentence.",
|
|
183
237
|
].join('\n');
|
|
184
238
|
}
|
|
185
239
|
|
|
@@ -203,11 +257,21 @@ export function readOutcome(text) {
|
|
|
203
257
|
.map((o) => ({ label: words(o?.label, 40), effect: words(o?.effect, 200) }))
|
|
204
258
|
.filter((o) => o.label)
|
|
205
259
|
.slice(0, 4);
|
|
260
|
+
const story = (v) =>
|
|
261
|
+
typeof v === 'string' && /^ST-\d+$/i.test(v.trim()) ? v.trim().toUpperCase() : null;
|
|
262
|
+
const waits = (Array.isArray(parsed.waits) ? parsed.waits : [])
|
|
263
|
+
.map((w) => ({
|
|
264
|
+
story: story(w?.story),
|
|
265
|
+
on: (Array.isArray(w?.on) ? w.on : []).map(story).filter(Boolean),
|
|
266
|
+
why: words(w?.why, 200),
|
|
267
|
+
}))
|
|
268
|
+
.filter((w) => w.story);
|
|
206
269
|
return {
|
|
207
270
|
outcome: parsed.outcome,
|
|
208
271
|
question: words(parsed.question, 400),
|
|
209
272
|
options: options.length >= 2 ? options : [],
|
|
210
273
|
reason: words(parsed.reason, 300),
|
|
274
|
+
waits,
|
|
211
275
|
};
|
|
212
276
|
}
|
|
213
277
|
|
package/hooks/mod/register.mjs
CHANGED
|
@@ -58,12 +58,16 @@ import {
|
|
|
58
58
|
leadPrompt,
|
|
59
59
|
nudgeText,
|
|
60
60
|
outcomePrompt,
|
|
61
|
+
PARK_MS,
|
|
62
|
+
parkWatch,
|
|
61
63
|
readOutcome,
|
|
62
64
|
restartPrompt,
|
|
63
65
|
stallDue,
|
|
64
66
|
stepKey,
|
|
65
67
|
stepLabel,
|
|
68
|
+
stepStories,
|
|
66
69
|
storiesInFlight,
|
|
70
|
+
storiesSignature,
|
|
67
71
|
withoutInFlight,
|
|
68
72
|
} from './loop.mjs';
|
|
69
73
|
import { goalOf, nextText } from './mission.mjs';
|
|
@@ -122,6 +126,8 @@ const loop = {
|
|
|
122
126
|
// LOOP-11, LOOP-12: what holds the loop: { kind: 'person' | 'stopped' | 'blocked' | 'design', key, label, prompt, question, options, reason }
|
|
123
127
|
waiting: null,
|
|
124
128
|
handedDesign: new Set(), // LOOP-12: design stories handed to the lead this session
|
|
129
|
+
// LOOP-13: stories the lead said wait on other work: id → { on, why, watch, sig, at }
|
|
130
|
+
parked: new Map(),
|
|
125
131
|
};
|
|
126
132
|
// LOOP-10: whether this session runs the loop, the session that does when another does, and when this
|
|
127
133
|
// one last renewed its lease.
|
|
@@ -1099,6 +1105,45 @@ export function register(on) {
|
|
|
1099
1105
|
$.ui.invalidate('ui.render');
|
|
1100
1106
|
await act.step('resume');
|
|
1101
1107
|
},
|
|
1108
|
+
// LOOP-13: where the watched stories stand now: their states, branch commits, and agents at work.
|
|
1109
|
+
storiesNow: async () => {
|
|
1110
|
+
const refs = await $.process
|
|
1111
|
+
.run(['git', 'for-each-ref', '--format=%(objectname) %(refname)', 'refs/heads'], { cwd })
|
|
1112
|
+
.catch(() => ({ stdout: '' }));
|
|
1113
|
+
const heads = new Map(
|
|
1114
|
+
String(refs.stdout ?? '')
|
|
1115
|
+
.split('\n')
|
|
1116
|
+
.map((l) => /^\s*(\S+)\s+(?:refs\/heads\/)?(\S+)/.exec(l))
|
|
1117
|
+
.filter(Boolean)
|
|
1118
|
+
.map((m) => [m[2], m[1]]),
|
|
1119
|
+
);
|
|
1120
|
+
const inFlight = storiesInFlight(await $.agent.list().catch(() => []));
|
|
1121
|
+
const stories = model.state?.stories ?? [];
|
|
1122
|
+
return {
|
|
1123
|
+
heads,
|
|
1124
|
+
inFlight,
|
|
1125
|
+
stories,
|
|
1126
|
+
sig: (ids) => storiesSignature(ids, stories, heads, inFlight),
|
|
1127
|
+
};
|
|
1128
|
+
},
|
|
1129
|
+
park: async (waits) => {
|
|
1130
|
+
if (!waits.length) return;
|
|
1131
|
+
const now = await $.clock.now();
|
|
1132
|
+
const seen = await act.storiesNow();
|
|
1133
|
+
for (const w of waits) {
|
|
1134
|
+
const watch = parkWatch(w.id, w.on, seen.stories);
|
|
1135
|
+
loop.parked.set(w.id, { on: w.on, why: w.why, watch, sig: seen.sig(watch), at: now });
|
|
1136
|
+
}
|
|
1137
|
+
$.ui.invalidate('ui.render');
|
|
1138
|
+
},
|
|
1139
|
+
// LOOP-13: a parked story wakes when anything it watches moves, or after PARK_MS with nothing moving.
|
|
1140
|
+
unpark: async () => {
|
|
1141
|
+
if (!loop.parked.size) return;
|
|
1142
|
+
const now = await $.clock.now();
|
|
1143
|
+
const seen = await act.storiesNow();
|
|
1144
|
+
for (const [id, p] of loop.parked)
|
|
1145
|
+
if (now - p.at >= PARK_MS || seen.sig(p.watch) !== p.sig) loop.parked.delete(id);
|
|
1146
|
+
},
|
|
1102
1147
|
// LOOP-11: the lead's last reply, read once before the loop prompts it again. A reply that waits on
|
|
1103
1148
|
// the person, or a turn they stopped, holds the loop until they speak; a step the lead didn't take
|
|
1104
1149
|
// holds that step while it's still next.
|
|
@@ -1145,6 +1190,21 @@ export function register(on) {
|
|
|
1145
1190
|
// Spec 05's data: what came of the step, once known.
|
|
1146
1191
|
const record = sent ? loop.steps.findLast((x) => x.prompt === sent.prompt) : null;
|
|
1147
1192
|
if (record) record.outcome = heard?.outcome ?? 'unread';
|
|
1193
|
+
// LOOP-13: stories the lead said wait on other work are parked, each until what it waits on moves.
|
|
1194
|
+
const ids = stepStories(sent);
|
|
1195
|
+
if (['waiting', 'blocked'].includes(heard?.outcome) && ids.length) {
|
|
1196
|
+
const named = new Map((heard.waits ?? []).map((w) => [w.story, w]));
|
|
1197
|
+
// A blocked step, or a reply that names none, parks all its stories; otherwise those it names.
|
|
1198
|
+
const waiting =
|
|
1199
|
+
heard.outcome === 'blocked' || !named.size ? ids : ids.filter((id) => named.has(id));
|
|
1200
|
+
await act.park(
|
|
1201
|
+
waiting.map((id) => ({
|
|
1202
|
+
id,
|
|
1203
|
+
on: named.get(id)?.on ?? [],
|
|
1204
|
+
why: named.get(id)?.why ?? heard.reason ?? null,
|
|
1205
|
+
})),
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1148
1208
|
if (heard?.outcome === 'needs-person')
|
|
1149
1209
|
loop.waiting = {
|
|
1150
1210
|
kind: 'person',
|
|
@@ -1154,7 +1214,7 @@ export function register(on) {
|
|
|
1154
1214
|
reason: heard.reason,
|
|
1155
1215
|
};
|
|
1156
1216
|
else if (heard?.outcome === 'blocked' && sent)
|
|
1157
|
-
loop.waiting = { kind: 'blocked', ...step, reason: heard.reason };
|
|
1217
|
+
loop.waiting = { kind: 'blocked', ...step, reason: heard.reason, stories: ids };
|
|
1158
1218
|
if (loop.waiting) $.ui.invalidate('ui.render');
|
|
1159
1219
|
},
|
|
1160
1220
|
// LOOP-11: the person's choice from the lead's options, or their own words, told to the lead.
|
|
@@ -1181,11 +1241,13 @@ export function register(on) {
|
|
|
1181
1241
|
const w = loop.waiting;
|
|
1182
1242
|
if (!w?.prompt || leadTurn) return;
|
|
1183
1243
|
loop.waiting = null;
|
|
1244
|
+
for (const id of w.stories ?? []) loop.parked.delete(id);
|
|
1184
1245
|
loop.asked = { key: w.key, times: 0 };
|
|
1185
1246
|
await act.submit(w.prompt, w.stepKind, w.target, { key: w.key, label: w.label });
|
|
1186
1247
|
},
|
|
1187
1248
|
// LOOP-11: the person lets the loop go on.
|
|
1188
1249
|
carryOn: async () => {
|
|
1250
|
+
for (const id of loop.waiting?.stories ?? []) loop.parked.delete(id);
|
|
1189
1251
|
loop.waiting = null;
|
|
1190
1252
|
loop.lastKey = null;
|
|
1191
1253
|
$.ui.invalidate('ui.render');
|
|
@@ -1253,12 +1315,15 @@ export function register(on) {
|
|
|
1253
1315
|
return act.submit(prompt, 'restart', story);
|
|
1254
1316
|
}
|
|
1255
1317
|
const ran = await $.process.run(['node', cli, 'next', '--json'], { cwd });
|
|
1318
|
+
// LOOP-13: a parked story stays out of the step until what it waits on moves.
|
|
1319
|
+
await act.unpark();
|
|
1256
1320
|
// LOOP-9: never a story an agent is already at work on, building or reviewing it.
|
|
1257
1321
|
const step =
|
|
1258
1322
|
ran.exitCode === 0
|
|
1259
1323
|
? withoutInFlight(
|
|
1260
1324
|
parseJson(ran.stdout),
|
|
1261
1325
|
storiesInFlight(await $.agent.list().catch(() => [])),
|
|
1326
|
+
loop.parked,
|
|
1262
1327
|
)
|
|
1263
1328
|
: null;
|
|
1264
1329
|
// MISSION-1: the band shows what the loop takes next.
|
|
@@ -1294,9 +1359,17 @@ export function register(on) {
|
|
|
1294
1359
|
// LOOP-11, LOOP-12: a step the lead didn't take, or designs left to the person and the lead, hold
|
|
1295
1360
|
// that step while it's still next; any other step goes on.
|
|
1296
1361
|
if (loop.waiting) {
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1362
|
+
// LOOP-13: a blocked step's stories are parked, so the line stays while any of them is, and
|
|
1363
|
+
// other steps go on; a step without stories holds while it's still next.
|
|
1364
|
+
const stories = loop.waiting.stories ?? [];
|
|
1365
|
+
const held = stories.length
|
|
1366
|
+
? stories.some((id) => loop.parked.has(id))
|
|
1367
|
+
: loop.waiting.key === key;
|
|
1368
|
+
if (held && !stories.length) return;
|
|
1369
|
+
if (!held) {
|
|
1370
|
+
loop.waiting = null;
|
|
1371
|
+
$.ui.invalidate('ui.render');
|
|
1372
|
+
}
|
|
1300
1373
|
}
|
|
1301
1374
|
if (!prompt) return;
|
|
1302
1375
|
if (designHandedOver(step, loop.handedDesign)) {
|
|
@@ -2510,6 +2510,83 @@ test('LOOP-11 dismissed, what the lead waits on stays in the band, and the loop
|
|
|
2510
2510
|
expect(w.prompts.at(-1)).toBe('Review ST-4: run /code-kit:review ST-4.');
|
|
2511
2511
|
});
|
|
2512
2512
|
|
|
2513
|
+
test('LOOP-13 stories the lead says wait on other work are parked, each until what it waits on moves', async ($, on) => {
|
|
2514
|
+
const w = project();
|
|
2515
|
+
w.stories.push(
|
|
2516
|
+
{
|
|
2517
|
+
id: 'ST-158',
|
|
2518
|
+
title: 'Storage',
|
|
2519
|
+
lane: 'web',
|
|
2520
|
+
state: 'in review',
|
|
2521
|
+
branch: 'web/st-158',
|
|
2522
|
+
waitingOn: [],
|
|
2523
|
+
},
|
|
2524
|
+
{
|
|
2525
|
+
id: 'ST-164',
|
|
2526
|
+
title: 'Callers use the storage',
|
|
2527
|
+
lane: 'api',
|
|
2528
|
+
state: 'in progress',
|
|
2529
|
+
branch: 'api/st-164',
|
|
2530
|
+
dependsOn: ['ST-158'],
|
|
2531
|
+
waitingOn: [],
|
|
2532
|
+
},
|
|
2533
|
+
{
|
|
2534
|
+
id: 'ST-172',
|
|
2535
|
+
title: 'Silent tests',
|
|
2536
|
+
lane: 'core',
|
|
2537
|
+
state: 'in review',
|
|
2538
|
+
branch: 'core/st-172',
|
|
2539
|
+
waitingOn: [],
|
|
2540
|
+
},
|
|
2541
|
+
{
|
|
2542
|
+
id: 'ST-173',
|
|
2543
|
+
title: 'Verdicts',
|
|
2544
|
+
lane: 'lead',
|
|
2545
|
+
state: 'in review',
|
|
2546
|
+
branch: 'lead/st-173',
|
|
2547
|
+
waitingOn: [],
|
|
2548
|
+
},
|
|
2549
|
+
);
|
|
2550
|
+
w.refs += ' s1 refs/heads/web/st-158\n s2 refs/heads/api/st-164\n s3 refs/heads/core/st-172\n';
|
|
2551
|
+
w.next = { step: 'review', args: 'ST-158 ST-172', then: [] };
|
|
2552
|
+
w.sideReply = unread;
|
|
2553
|
+
const clock = await start($, on, w);
|
|
2554
|
+
await leadTurn($);
|
|
2555
|
+
await settle(clock);
|
|
2556
|
+
expect(w.prompts.at(-1)).toBe('Review ST-158, ST-172: run /code-kit:review.');
|
|
2557
|
+
// The lead says neither is due yet; next then names a third story as well.
|
|
2558
|
+
w.sideReply = outcome({
|
|
2559
|
+
outcome: 'waiting',
|
|
2560
|
+
waits: [
|
|
2561
|
+
{ story: 'ST-158', on: [], why: 'it merges with its paired branch' },
|
|
2562
|
+
{ story: 'ST-172', on: [], why: 'its merge checks are re-running' },
|
|
2563
|
+
],
|
|
2564
|
+
});
|
|
2565
|
+
w.next = { step: 'review', args: 'ST-158 ST-172 ST-173', then: [] };
|
|
2566
|
+
await leadSays(
|
|
2567
|
+
$,
|
|
2568
|
+
't-2',
|
|
2569
|
+
'None of these is due: ST-158 merges with ST-164; ST-172 re-runs its checks.',
|
|
2570
|
+
);
|
|
2571
|
+
await settle(clock);
|
|
2572
|
+
// Only the story that isn't waiting is asked for.
|
|
2573
|
+
expect(w.prompts.at(-1)).toBe('Review ST-173: run /code-kit:review ST-173.');
|
|
2574
|
+
const asked = w.prompts.length;
|
|
2575
|
+
// Lanes committing elsewhere don't wake them, and nothing is prompted while both still wait.
|
|
2576
|
+
w.sideReply = unread;
|
|
2577
|
+
w.next = { step: 'review', args: 'ST-158 ST-172', then: [] };
|
|
2578
|
+
w.refs += ' x1 refs/heads/core/st-9\n';
|
|
2579
|
+
await leadTurn($, 't-3');
|
|
2580
|
+
await settle(clock);
|
|
2581
|
+
expect(w.prompts).toHaveLength(asked);
|
|
2582
|
+
// The story building on ST-158 commits: ST-158 wakes, and ST-172 still waits.
|
|
2583
|
+
w.refs = w.refs.replace(' s2 refs/heads/api/st-164', ' s9 refs/heads/api/st-164');
|
|
2584
|
+
await leadTurn($, 't-4');
|
|
2585
|
+
await settle(clock);
|
|
2586
|
+
expect(w.prompts.at(-1)).toBe('Review ST-158: run /code-kit:review ST-158.');
|
|
2587
|
+
expect(w.prompts).toHaveLength(asked + 1);
|
|
2588
|
+
});
|
|
2589
|
+
|
|
2513
2590
|
test('LOOP-11 a turn the person stopped holds the loop, with no dialog, until they speak or press Carry on', async ($, on) => {
|
|
2514
2591
|
const w = project();
|
|
2515
2592
|
w.sideReply = unread;
|
package/hooks/mod/view.mjs
CHANGED
|
@@ -101,6 +101,8 @@ export function projectState(check, status, next = null, atWork = new Set()) {
|
|
|
101
101
|
worktree: s.worktree ?? null,
|
|
102
102
|
requirements: s.requirements ?? [],
|
|
103
103
|
waitingOn: s.waitingOn ?? [],
|
|
104
|
+
// LOOP-13: a parked story watches the stories building on it.
|
|
105
|
+
dependsOn: s.dependsOn ?? [],
|
|
104
106
|
milestone: s.milestone ?? null,
|
|
105
107
|
})),
|
|
106
108
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warren-dean/code-kit",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.1",
|
|
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",
|