kaizen-agent 0.7.0 → 0.7.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.
package/README.md CHANGED
@@ -352,6 +352,8 @@ tells you what it concluded, so you correct a sentence instead of filling in a f
352
352
  | `/kaizen <request>` | Start a run |
353
353
  | `/kaizen-plan <request>` | Plan only — stop before anything is built |
354
354
  | `/kaizen-auto <request>` | No approval stops; report at the end |
355
+ | `/kaizen-lite <request>` | Every stage in this session — cheap, and the reviewer has seen the work |
356
+ | `/kaizen-full <request>` | Every stage its own cold agent — the independent review |
355
357
  | `/kaizen-review [target]` | Review something that already exists |
356
358
  | `/kaizen-status` | What is waiting on you, in flight, done |
357
359
  | `/kaizen-approve` | Approve whatever the run is waiting on |
@@ -391,6 +393,7 @@ in one can be finished in another. See [adapters.md](https://github.com/hfadhlul
391
393
  | Setting | Default | Meaning |
392
394
  |---|---|---|
393
395
  | `mode` | `approve` | `approve` stops for you; `auto` never does; `plan-only` stops after the plan |
396
+ | `runner` | `full` | `full` gives each stage its own agent, so the reviewer never saw the work — several times the cost. `lite` runs every stage here, at about the cost of doing it by hand |
394
397
  | `build.executor` | `subagent` | Who does the work: a separate agent, `inline` in the current one, or `ask` each time |
395
398
  | `auto_fix.min_severity` | `high` | Findings this bad or worse get fixed without asking |
396
399
  | `approvals.plan` | `true` | Stop and show the plan before anything is built |
package/cli/dashboard.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  // What `kaizen` shows once it is installed: the state of this project's runs, and
2
2
  // the few things you would have opened a terminal to do.
3
- import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { existsSync, readFileSync, readdirSync, appendFileSync, mkdirSync, statSync } from "node:fs";
4
4
  import { join, dirname, basename } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
 
7
7
  const home = homedir();
8
+ const rgb = (r: number, g: number, b: number, s: string) =>
9
+ `\x1b[38;2;${r};${g};${b}m${s}\x1b[0m`;
10
+
8
11
  const c = {
9
12
  dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
10
13
  bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
@@ -16,29 +19,8 @@ const c = {
16
19
  type Run = { id: string; stage: string; awaiting: string | null };
17
20
 
18
21
  // The mascot, rendered from assets/kaizen.jpg. Braille packs 2x4 dots into one
19
- // character, which is the only way outline art survives a downsample this far;
20
- // sampling brightness onto a character ramp gives grey mush, having no midtones
21
- // to sample. Regenerate with the snippet in assets/tanuki.txt's history.
22
+ // character, which is the only way outline art survives a downsample this far.
22
23
  const TANUKI = [
23
- "⠀⠀⠀⠀⢀⡶⢶⣖⡒⠒⠛⠛⠛⠛⣛⡿⠟⢛⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
24
- "⠀⠀⠀⢀⣾⡇⠀⠀⠉⠛⠲⣤⠴⠛⠁⢀⡴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
25
- "⠀⠀⠀⡾⢹⠇⠀⠀⣠⠴⠋⠁⠀⣠⠖⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
26
- "⠀⠀⣼⠁⣸⣠⡴⠋⢁⡴⠦⣄⡚⢁⣀⣀⣀⣀⡀⢀⣠⠴⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀",
27
- "⠀⣸⢃⡴⠟⠁⠀⣀⢸⡇⠀⠈⠙⠉⠉⠀⠀⠉⠉⠋⠁⠀⢨⡇⠀⠀⠀⠀⠀⠀⠀⠀",
28
- "⣰⡿⠋⢰⡏⢹⠋⠁⠈⣧⠀⠀⢀⡀⠀⠀⠀⠀⢀⡀⠀⠀⣸⠃⠀⠀⠀⠀⠀⠀⠀⠀",
29
- "⠻⠖⠛⢹⡇⠘⣇⠀⢰⠇⣠⠞⢉⡉⢳⡀⢀⡞⢉⡉⠳⣄⠘⣆⠀⠀⢠⡤⣤⢀⡀⠀",
30
- "⠀⠀⠀⠀⣧⠀⠉⣰⣋⡴⠃⠀⠈⠁⡞⣀⣀⢳⠈⠁⠀⠘⢦⣙⣆⠀⣰⠃⠹⡇⣸⣻",
31
- "⠀⠀⠀⠀⠘⣆⠀⠈⠻⣄⠀⠀⠀⠀⠳⢬⡥⠞⠀⠀⠀⠀⣠⠝⠁⢀⡇⠀⠀⣷⢳⠃",
32
- "⠀⠀⠀⠀⠀⠈⢧⡀⠀⠈⠙⠲⠦⣤⣤⣀⣀⣤⣤⠴⠖⠿⠷⠶⠚⠉⣳⠤⠴⠯⠏⠀",
33
- "⠀⠀⠀⠀⠀⠀⠀⠙⠲⣄⠀⠀⠀⣀⡤⠶⠶⢤⣀⠀⠀⠀⠀⣀⣤⠞⣡⠶⠚⠒⢦⡀",
34
- "⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⡼⠁⠀⠀⠀⠀⠈⢧⠀⠀⢾⠉⢀⡾⠻⣄⡀⠀⠈⡇",
35
- "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠐⡇⠀⠀⠀⠀⠀⠀⢸⠆⠀⢸⠚⠉⠀⠀⠀⢯⣀⡼⠃",
36
- "⠀⠀⠀⠀⠀⠀⠀⠀⠀⢷⠀⠀⠻⣄⠀⠀⠀⠀⣠⠟⠀⠀⡾⢤⣤⣀⣤⡤⠶⠋⠀⠀",
37
- "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢧⣄⣠⠤⠶⠒⠒⠻⠧⣄⣠⡼⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀",
38
- ];
39
-
40
- // The same, for windows that cannot spare fifteen rows before the runs begin.
41
- const TANUKI_SMALL = [
42
24
  "⠀⠀⠀⣰⠖⠾⣟⣛⠋⢉⣩⠽⢛⡽⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
43
25
  "⠀⠀⢰⢻⠀⠀⢀⡬⠟⠉⢀⠴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
44
26
  "⠀⢠⠇⣾⡠⠞⢩⠤⢄⡚⣁⣀⣀⣀⢀⣠⠤⡄⠀⠀⠀⠀⠀⠀",
@@ -52,6 +34,13 @@ const TANUKI_SMALL = [
52
34
  "⠀⠀⠀⠀⠀⠀⠀⠳⣀⣨⠥⠤⠶⠾⣅⣀⠞⠉⠉⠉⠉⠀⠀⠀",
53
35
  ];
54
36
 
37
+ // Compact block wordmark (28 columns x 3 rows).
38
+ const WORDMARK = [
39
+ "█ █ ▄▀█ █ ▀▀█ █▀▀ █▄ █",
40
+ "█▀▄ █▀█ █ ▄▀ ██▄ █ ▀█",
41
+ "█ █ █ █ █ █▄▄ █▄▄ █ █",
42
+ ];
43
+
55
44
 
56
45
  export async function dashboard(
57
46
  repo: string,
@@ -60,8 +49,32 @@ export async function dashboard(
60
49
  ) {
61
50
  const { stdin, stdout } = process;
62
51
  const state = locate();
52
+ // Opening a project is what makes it known; nothing else asks the user to register.
53
+ if (state && dirname(state) !== home) remember(dirname(state));
54
+
55
+ // Every known project's runs and open items, tagged with where they came from.
56
+ // draw() runs on every keypress, so this is gathered when the dashboard opens and
57
+ // after an action returns -- never inside draw().
58
+ type Seen = { runs: (Run & { where: string })[]; items: { text: string; where: string }[] };
59
+ let seen: Seen = { runs: [], items: [] };
60
+
61
+ const survey = () => {
62
+ const dirs = [...knownProjects().map((d) => join(d, ".kaizen")), join(home, ".kaizen")]
63
+ .filter((d, i, all) => all.indexOf(d) === i && existsSync(d));
64
+ const runs: Seen["runs"] = [], items: Seen["items"] = [];
65
+ for (const dir of dirs) {
66
+ const where = label(dir);
67
+ for (const r of readRuns(dir)) runs.push({ ...r, where });
68
+ for (const g of allBacklog(dir)) for (const t of g.items) items.push({ text: t, where });
69
+ }
70
+ seen = { runs, items };
71
+ };
72
+ survey();
63
73
 
64
74
  const actions = () => [
75
+ ...(knownProjects().length || !state ? [
76
+ { key: "projects", label: "All projects", hint: "every project kaizen knows about" },
77
+ ] : []),
65
78
  ...(state ? [
66
79
  { key: "runs", label: "Runs", hint: "every run, and what each is waiting on" },
67
80
  { key: "backlog", label: "Backlog", hint: "what runs noticed and did not do" },
@@ -78,10 +91,12 @@ export async function dashboard(
78
91
  for (;;) {
79
92
  const chosen = await menu();
80
93
  if (chosen === "quit") return;
94
+ if (chosen === "projects") { await projectsView(); continue; }
81
95
  if (chosen === "runs") { await runsView(); continue; }
82
96
  if (chosen === "backlog") { await backlogView(); continue; }
83
97
  const lines = await run(chosen);
84
98
  if (lines?.length) await report(lines);
99
+ survey(); // an action may have changed what exists
85
100
  }
86
101
 
87
102
  // An action that has something to say says it here, on its own screen, rather
@@ -115,13 +130,14 @@ export async function dashboard(
115
130
  rows: string[],
116
131
  footer = "enter open · backspace back",
117
132
  skip: (i: number) => boolean = () => false,
118
- ) {
119
- if (!rows.length) { await report([c.bold(title), "", c.dim("nothing here yet")]); return null; }
133
+ extra = "", // keys the caller wants to hear about
134
+ ): Promise<{ index: number | null; key?: string }> {
135
+ if (!rows.length) { await report([c.bold(title), "", c.dim("nothing here yet")]); return { index: null }; }
120
136
 
121
137
  // Headings are rows too, but they are not items: they are not counted, and
122
138
  // moving passes over them rather than landing on them.
123
139
  const pickable = rows.map((_, i) => i).filter((i) => !skip(i));
124
- if (!pickable.length) { await report([c.bold(title), "", c.dim("nothing here yet")]); return null; }
140
+ if (!pickable.length) { await report([c.bold(title), "", c.dim("nothing here yet")]); return { index: null }; }
125
141
  let cursor = 0; // index into pickable
126
142
  const move = (d: number) => { cursor = Math.min(pickable.length - 1, Math.max(0, cursor + d)); };
127
143
  let top = 0;
@@ -146,7 +162,7 @@ export async function dashboard(
146
162
  stdin.setRawMode(true);
147
163
  stdin.resume();
148
164
  draw();
149
- let chosen: number | null = null;
165
+ let chosen: number | null = null, hit: string | undefined;
150
166
  await new Promise<void>((resolve) => {
151
167
  const onData = (chunk: Buffer) => {
152
168
  const keys = chunk.toString();
@@ -159,6 +175,8 @@ export async function dashboard(
159
175
  if (rest.startsWith("\r") || rest.startsWith("\n")) {
160
176
  chosen = pickable[cursor]!; stdin.off("data", onData); return resolve();
161
177
  }
178
+ const k = [...extra].find((x) => rest.startsWith(x));
179
+ if (k) { hit = k; stdin.off("data", onData); return resolve(); }
162
180
  if (rest.startsWith("\x1b[A")) { move(-1); i += 2; }
163
181
  else if (rest.startsWith("\x1b[B")) { move(1); i += 2; }
164
182
  else if (rest.startsWith("k")) move(-1);
@@ -172,12 +190,74 @@ export async function dashboard(
172
190
  stdin.setRawMode(false);
173
191
  stdin.pause();
174
192
  stdout.write("\x1b[?25h\x1b[?1049l");
175
- return chosen;
193
+ return { index: hit ? pickable[cursor]! : chosen, key: hit };
194
+ }
195
+
196
+ // Every project kaizen knows about, with what each is waiting on. The global
197
+ // ~/.kaizen is one of them: it holds runs made outside any project.
198
+ async function projectsView() {
199
+ for (;;) {
200
+ const dirs = [...knownProjects(), join(home, ".kaizen")].filter(
201
+ (d, i, all) => all.indexOf(d) === i);
202
+
203
+ const rows: string[] = [];
204
+ const open: (string | null)[] = [];
205
+ const width = Math.max(...dirs.map((d) => tilde(d).length));
206
+
207
+ for (const dir of dirs) {
208
+ const st = dir.endsWith(".kaizen") ? dir : join(dir, ".kaizen");
209
+ if (!existsSync(st)) {
210
+ // Listed, not dropped: a project that moved should be visible and fixable,
211
+ // and the registry is a file the user can edit.
212
+ rows.push(`${c.dim(tilde(dir).padEnd(width))} ${c.amber("missing")}`);
213
+ open.push(null);
214
+ continue;
215
+ }
216
+ const runs = readRuns(st);
217
+ const waiting = runs.filter((r) => r.awaiting && r.stage !== "abandoned").length;
218
+ const flight = runs.filter((r) => !r.awaiting && !["done", "abandoned"].includes(r.stage)).length;
219
+ const done = runs.filter((r) => r.stage === "done").length;
220
+ const items = allBacklog(st).reduce((n, g) => n + g.items.length, 0);
221
+ const bits = [
222
+ waiting ? c.amber(`${waiting} waiting`) : "",
223
+ flight ? c.cyan(`${flight} running`) : "",
224
+ done ? c.dim(`${done} done`) : "",
225
+ items ? c.dim(`${items} open`) : "",
226
+ ].filter(Boolean);
227
+ rows.push(`${tilde(dir).padEnd(width)} ${bits.join(c.dim(" · ")) || c.dim("nothing yet")}`);
228
+ open.push(st);
229
+ }
230
+
231
+ const { index, key } = await pick("All projects", rows,
232
+ "enter runs · b backlog · s find more · backspace back", () => false, "sb");
233
+ if (index === null) return;
234
+ if (key === "s") { await sync(); continue; } // the list rebuilds on the next pass
235
+ if (!open[index]) continue; // a project whose state has gone
236
+ if (key === "b") await backlogView(open[index]!);
237
+ else await runsView(open[index]!);
238
+ }
239
+ }
240
+
241
+ async function sync() {
242
+ const before = knownProjects().length;
243
+ const started = Date.now();
244
+ const found = findProjects(home);
245
+ for (const dir of found) remember(dir);
246
+ const added = knownProjects().length - before;
247
+ await report([
248
+ c.bold(added ? `Found ${added} project${added === 1 ? "" : "s"}` : "Nothing new"),
249
+ "",
250
+ ...found.map((d) => " " + c.dim(tilde(d))),
251
+ "",
252
+ c.dim(`${found.length} found in ${((Date.now() - started) / 1000).toFixed(1)}s under ${tilde(home)}`),
253
+ c.dim(`the list lives in ${tilde(join(home, ".kaizen", "projects"))} and can be edited`),
254
+ ]);
176
255
  }
177
256
 
178
- async function runsView() {
257
+ async function runsView(from?: string) {
179
258
  for (;;) {
180
- const runs = readRuns(state!);
259
+ const here = from ?? state!;
260
+ const runs = readRuns(here);
181
261
  const width = Math.max(...runs.map((r) => r.id.length));
182
262
  const rows = runs.map((r) => {
183
263
  const mark = r.stage === "done" ? c.dim("done ")
@@ -187,15 +267,15 @@ export async function dashboard(
187
267
  const tail = r.stage === "done" || r.stage === "abandoned" ? "" : (r.awaiting ?? r.stage);
188
268
  return `${mark} ${r.id.padEnd(width)} ${c.dim(tail)}`;
189
269
  });
190
- const i = await pick("Runs", rows);
191
- if (i === null) return;
192
- await runDetail(runs[i]!);
270
+ const { index } = await pick(from ? `Runs — ${label(from)}` : "Runs", rows);
271
+ if (index === null) return;
272
+ await runDetail(runs[index]!, here);
193
273
  }
194
274
  }
195
275
 
196
276
  // What a run is, read off its own files rather than summarised from memory.
197
- async function runDetail(r: Run) {
198
- const dir = join(state!, "runs", r.id);
277
+ async function runDetail(r: Run, from?: string) {
278
+ const dir = join(from ?? state!, "runs", r.id);
199
279
  const lines: string[] = [c.bold(r.id), ""];
200
280
  lines.push(` stage ${r.stage}`);
201
281
  lines.push(` waiting ${r.awaiting ?? c.dim("nothing — it can carry on")}`);
@@ -234,8 +314,8 @@ export async function dashboard(
234
314
  return out + "…\x1b[0m";
235
315
  }
236
316
 
237
- async function backlogView() {
238
- const groups = allBacklog(state!);
317
+ async function backlogView(from?: string) {
318
+ const groups = allBacklog(from ?? state!);
239
319
  const parsed = groups.map((g) => ({ run: g.run, items: g.items.map(parseItem) }));
240
320
  const sevWidth = Math.max(0, ...parsed.flatMap((g) => g.items.map((i) => i.severity?.length ?? 0)));
241
321
  const whereWidth = Math.max(0, ...parsed.flatMap((g) => g.items.map((i) => i.where?.length ?? 0)));
@@ -258,11 +338,11 @@ export async function dashboard(
258
338
  }
259
339
  }
260
340
  for (;;) {
261
- const i = await pick("Backlog — open items", rows, "enter read · backspace back",
262
- (n) => full[n] === null);
263
- if (i === null) return;
341
+ const { index } = await pick(from ? `Backlog — ${label(from)}` : "Backlog — open items",
342
+ rows, "enter read · backspace back", (n) => full[n] === null);
343
+ if (index === null) return;
264
344
  // Rows are cut to the window, so reading one means opening it.
265
- const it = full[i];
345
+ const it = full[index];
266
346
  if (!it) continue;
267
347
  await report([
268
348
  c.bold(it.where ?? "Backlog item"),
@@ -288,29 +368,56 @@ export async function dashboard(
288
368
  async function menu() {
289
369
  const draw = () => {
290
370
  const acts = actions();
291
- const runs = state ? readRuns(state) : [];
292
- const waiting = runs.filter((r) => r.awaiting && r.stage !== "abandoned");
293
- const flight = runs.filter((r) => !r.awaiting && !["done", "abandoned"].includes(r.stage));
294
- const done = runs.filter((r) => r.stage === "done");
295
-
296
371
  stdout.write("\x1b[H\x1b[2J");
297
372
 
298
373
  // The mascot sits beside the header rather than above it; stacked, it pushes
299
374
  // the runs -- the thing you opened this for -- below the fold on a short window.
300
375
  const cols = stdout.columns ?? 80, rows = stdout.rows ?? 24;
301
376
  const wide = cols >= 64;
302
- const art = rows >= 30 ? TANUKI : TANUKI_SMALL;
303
- const at = art === TANUKI ? 6 : 2; // where the text sits against it
377
+ const art = TANUKI;
378
+ const at = 2; // where the text sits against it
379
+
380
+ // Every width below is measured, not guessed: the mascot's own width plus the
381
+ // gutter plus the wordmark's columns is exactly what the wide header occupies,
382
+ // and 2 more is the left margin every line is written with.
383
+ const gutter = Math.max(...art.map((l) => l.length)) + 4;
384
+ const setsWordmark = cols >= gutter + WORDMARK[0]!.length + 2;
304
385
 
305
386
  const beside = Array(art.length).fill("");
306
- beside[at] = `${c.bold("kaizen")} ${c.dim(version(repo))}`;
307
- beside[at + 1] = c.dim(state ? tilde(dirname(state)) : "no project here");
308
- beside[at + 3] = c.dim(agents.join(", "));
387
+ if (setsWordmark) {
388
+ // Against the mascot's middle, with what kaizen is under it and where you are
389
+ // under that: the header says what the tool does before it says where it is.
390
+ const top = 2;
391
+ for (const [i, line] of WORDMARK.entries()) {
392
+ const t = i / (WORDMARK.length - 1);
393
+ beside[top + i] = rgb(
394
+ Math.round(222 - t * 120), Math.round(238 - t * 100), Math.round(255 - t * 30), line);
395
+ }
396
+ // The description is chosen from compact phrasings to fit cleanly beside the mascot.
397
+ const room = cols - gutter - 2 - (version(repo).length + 3);
398
+ const line = ["plan · approve · build · review",
399
+ "plan · approve · build",
400
+ "plan · build · review"].find((d) => d.length <= room);
401
+ beside[top + WORDMARK.length + 1] =
402
+ `${c.dim(version(repo))}${line ? " " + c.dim(line) : ""}`;
403
+ beside[top + WORDMARK.length + 2] = c.dim(state ? tilde(dirname(state)) : "no project here");
404
+ beside[top + WORDMARK.length + 3] = c.dim(agents.join(", "));
405
+ } else {
406
+ beside[at] = `${c.bold("kaizen")} ${c.dim(version(repo))}`;
407
+ beside[at + 1] = c.dim(state ? tilde(dirname(state)) : "no project here");
408
+ beside[at + 3] = c.dim(agents.join(", "));
409
+ }
309
410
  stdout.write("\n");
310
411
  if (wide) {
311
- const gutter = Math.max(...art.map((l) => l.length)) + 4;
412
+ // Cut what sits beside the mascot to the room left over. The agent list is
413
+ // long and grows with every agent installed, and it wrapped here before the
414
+ // wordmark existed -- a wrapped line pushes the whole header down a row.
312
415
  for (const [i, line] of art.entries()) {
313
- stdout.write(" " + c.cyan(line.padEnd(gutter)) + (beside[i] ?? "") + "\n");
416
+ const t = i / (art.length - 1);
417
+ const r = Math.round(235 - t * 135);
418
+ const g = Math.round(245 - t * 110);
419
+ const b = Math.round(255 - t * 20);
420
+ stdout.write(" " + rgb(r, g, b, line.padEnd(gutter)) + cut(beside[i] ?? "", cols - gutter - 2) + "\n");
314
421
  }
315
422
  } else {
316
423
  stdout.write(` ${c.bold("kaizen")} ${c.dim(version(repo))} ${c.dim(state ? tilde(dirname(state)) : "no project here")}\n`);
@@ -318,18 +425,28 @@ export async function dashboard(
318
425
  }
319
426
 
320
427
  if (state) {
428
+ // Across every project, not only this one. Where more than one is known each
429
+ // row says which, since "waiting on approvals.plan" means nothing on its own.
430
+ const many = new Set(seen.runs.map((r) => r.where)).size > 1;
431
+ const from = (w: string) => (many ? c.dim(w + " ") : "");
432
+ const wait = seen.runs.filter((r) => r.awaiting && r.stage !== "abandoned");
433
+ const fly = seen.runs.filter((r) => !r.awaiting && !["done", "abandoned"].includes(r.stage));
434
+ const fin = seen.runs.filter((r) => r.stage === "done");
435
+
321
436
  const runLines = [
322
- ...waiting.map((r) => `${c.amber("●")} ${short(r.id)} ${c.dim(r.awaiting ?? "")}`),
323
- ...flight.map((r) => `${c.cyan("●")} ${short(r.id)} ${c.dim(r.stage)}`),
324
- ...(done.length ? [c.dim(`● ${done.length} done`)] : []),
437
+ ...wait.map((r) => `${c.amber("●")} ${from(r.where)}${short(r.id)} ${c.dim(r.awaiting ?? "")}`),
438
+ ...fly.map((r) => `${c.cyan("●")} ${from(r.where)}${short(r.id)} ${c.dim(r.stage)}`),
439
+ ...(fin.length ? [c.dim(`● ${fin.length} done`)] : []),
325
440
  ];
326
441
  if (!runLines.length) runLines.push(c.dim("none yet"));
327
442
 
328
- const items = allBacklog(state).flatMap((g) => g.items.map(parseItem));
443
+ // parseItem's `where` is the file the finding is in; the project is a separate
444
+ // thing and both belong on the row.
445
+ const items = seen.items.map((i) => ({ ...parseItem(i.text), project: i.where }));
329
446
  const backLines = items.slice(0, 6).map((it) => {
330
447
  const sev = it.severity ? (it.severity === "critical" || it.severity === "high"
331
448
  ? c.amber(it.severity) : c.dim(it.severity)) + " " : "";
332
- return sev + (it.where ? c.dim(it.where) + " " : "") + it.text;
449
+ return from(it.project) + sev + (it.where ? c.dim(it.where) + " " : "") + it.text;
333
450
  });
334
451
  if (!backLines.length) backLines.push(c.dim("nothing open"));
335
452
  else if (items.length > 6) backLines.push(c.dim(`… ${items.length - 6} more`));
@@ -406,6 +523,50 @@ export async function dashboard(
406
523
  }
407
524
  }
408
525
 
526
+ // The list of projects kaizen knows about. Appended to as kaizen sets one up or is
527
+ // opened inside one, and seeded by the Find projects action. Plain lines so it can be
528
+ // read and corrected in an editor; blanks and # lines ignored.
529
+ const REGISTRY = join(home, ".kaizen", "projects");
530
+
531
+ export function knownProjects(): string[] {
532
+ if (!existsSync(REGISTRY)) return [];
533
+ return readFileSync(REGISTRY, "utf8").split("\n")
534
+ .map((l) => l.trim())
535
+ .filter((l) => l && !l.startsWith("#"));
536
+ }
537
+
538
+ // Never fails the command that triggered it: a read-only home is not a reason for a
539
+ // dashboard to stop opening.
540
+ export function remember(projectDir: string) {
541
+ try {
542
+ if (knownProjects().includes(projectDir)) return;
543
+ mkdirSync(dirname(REGISTRY), { recursive: true });
544
+ appendFileSync(REGISTRY, projectDir + "\n");
545
+ } catch { /* nothing here is worth an error */ }
546
+ }
547
+
548
+ // Bounded on purpose. Depth is what keeps this from becoming a filesystem walk, and
549
+ // the skips are the directories that make one slow: node_modules, and anything dotted
550
+ // (which includes .git, and every state directory that is not a project of its own).
551
+ function findProjects(root: string, depth = 4): string[] {
552
+ const found: string[] = [];
553
+ const walk = (dir: string, left: number) => {
554
+ let entries: string[];
555
+ try { entries = readdirSync(dir); } catch { return; }
556
+ // $HOME holds ~/.kaizen, the global state, which is not a project and must not
557
+ // stop the walk before it has looked at anything.
558
+ if (dir !== root && entries.includes(".kaizen")) { found.push(dir); return; }
559
+ if (left === 0) return;
560
+ for (const name of entries) {
561
+ if (name.startsWith(".") || name === "node_modules") continue;
562
+ try { if (statSync(join(dir, name)).isDirectory()) walk(join(dir, name), left - 1); }
563
+ catch { /* unreadable or vanished mid-walk */ }
564
+ }
565
+ };
566
+ walk(root, depth);
567
+ return found;
568
+ }
569
+
409
570
  function locate() {
410
571
  let dir = process.cwd();
411
572
  for (;;) {
@@ -444,6 +605,11 @@ function backlog(state: string) {
444
605
  function version(repo: string) {
445
606
  try { return JSON.parse(readFileSync(join(repo, "package.json"), "utf8")).version; } catch { return ""; }
446
607
  }
608
+ // The global state directory is not a project and has no parent worth naming.
609
+ function label(stateDir: string) {
610
+ return stateDir === join(home, ".kaizen") ? "global" : tilde(dirname(stateDir));
611
+ }
612
+
447
613
  function tilde(p: string) { return p.startsWith(home) ? "~" + p.slice(home.length) : p; }
448
614
 
449
615
  // The first few real lines of a file's body, blank lines and headings dropped.
@@ -531,7 +697,7 @@ function panel(title: string, lines: string[], inner: number) {
531
697
  const body = cut(line, inner - 2);
532
698
  out.push(`${DIM}│${OFF} ${body}${" ".repeat(Math.max(0, inner - 2 - vis(body)))} ${DIM}│${OFF}`);
533
699
  }
534
- out.push(`${DIM}╰${"─".repeat(inner)}╯${OFF}`);
700
+ out.push(`${DIM}╰${"─".repeat(Math.max(0, inner))}╯${OFF}`);
535
701
  return out;
536
702
  }
537
703
 
package/cli/install.ts CHANGED
@@ -453,7 +453,7 @@ function initProject() {
453
453
  // ("add a discount cap") does not reach kaizen unless the project says it should.
454
454
  // One line in the agent instructions is what makes it the default here.
455
455
  function writePointer() {
456
- const block = `\n## Kaizen workflow\n\nNon-trivial work in this repository follows the staged workflow in\n\`.kaizen/spec.md\`: plan, human approval, build, independent review, bounded fix\nloop. Invoke the \`kaizen\` skill for it — do not carry the stages out inline.\n\nNon-trivial means work worth a plan: a change across more than one file, anything\nwith a migration or a rollback, or a document someone else will act on. Answering a\nquestion, reading code, or a one-line fix is not, and should not start a run.\n\nRun state lives in \`.kaizen/runs/<id>/\`. Read \`state.json\` first and continue from\nthe stage it names; never advance a run whose \`awaiting\` field is non-null without a\nrecorded human decision.\n`;
456
+ const block = `\n## Kaizen workflow\n\nNon-trivial work in this repository follows the staged workflow in\n\`.kaizen/spec.md\`: plan, human approval, build, independent review, bounded fix\nloop. Invoke the \`kaizen\` skill for it — do not carry the stages out inline.\n\nA run costs several times what the same change costs in one session, so spend it\nwhere being wrong is expensive: money, data loss, a migration or anything else hard\nto undo, work a customer sees, or a part of the system nobody has read in a year.\n\nWork where being wrong is cheap and obvious does not start a run. Answering a\nquestion, reading code, a one-line fix, a rename the compiler checks — do those\ndirectly. When it is genuinely unclear, ask rather than assuming the expensive path.\n\nRun state lives in \`.kaizen/runs/<id>/\`. Read \`state.json\` first and continue from\nthe stage it names; never advance a run whose \`awaiting\` field is non-null without a\nrecorded human decision.\n`;
457
457
 
458
458
  for (const name of ["CLAUDE.md", "AGENTS.md"]) {
459
459
  const f = join(process.cwd(), name);
package/cli/settings.ts CHANGED
@@ -8,6 +8,7 @@ type Setting = { key: string; values: string[]; help: string };
8
8
 
9
9
  const SETTINGS: Setting[] = [
10
10
  { key: "mode", values: ["approve", "auto", "plan-only", "review-only"], help: "Where a run stops" },
11
+ { key: "runner", values: ["full", "lite"], help: "full: every stage its own cold agent. lite: this session runs them all, cheaper, reviewer has seen the work" },
11
12
  { key: "build.executor", values: ["subagent", "inline", "ask"], help: "Who carries out the approved plan" },
12
13
  { key: "approvals.plan", values: ["true", "false"], help: "Stop and show the plan before anything is built" },
13
14
  { key: "approvals.review", values: ["true", "false"], help: "Stop after the review, before the run is done" },
@@ -86,8 +87,13 @@ export async function settings(repoRoot: string, standalone = true) {
86
87
  const now = read(text, s.key) ?? s.values[0]!;
87
88
  const at = s.values.indexOf(now);
88
89
  const next = s.values[((at < 0 ? 0 : at) + step + s.values.length) % s.values.length]!;
89
- writeFileSync(file, write(text, s.key, next));
90
- saved = c.green(`saved ${s.key} = ${next || '""'}`);
90
+ const updated = write(text, s.key, next, readFileSync(join(repoRoot, "skills/kaizen/config.default.yml"), "utf8"));
91
+ writeFileSync(file, updated);
92
+ // Only claim a save that happened. Reporting one that did not is worse
93
+ // than failing loudly: the setting reads back unchanged and nobody knows why.
94
+ saved = read(updated, s.key) === next
95
+ ? c.green(`saved ${s.key} = ${next || '""'}`)
96
+ : `\x1b[33mcould not write ${s.key}\x1b[0m`;
91
97
  if (!rest.startsWith("\r")) i += 2;
92
98
  } else if (rest.startsWith("k")) active = (active - 1 + SETTINGS.length) % SETTINGS.length;
93
99
  else if (rest.startsWith("j")) active = (active + 1) % SETTINGS.length;
@@ -145,12 +151,54 @@ function read(text: string, key: string) {
145
151
  return v.replace(/^["']|["']$/g, "");
146
152
  }
147
153
 
148
- function write(text: string, key: string, value: string) {
149
- const { lines, index, indent } = lineOf(text, key);
150
- if (index < 0) return text;
154
+ function write(text: string, key: string, value: string, defaults: string) {
151
155
  const name = key.split(".").pop()!;
152
156
  const shown = value === "" ? '""' : value;
153
- lines[index] = `${" ".repeat(indent)}${name}: ${shown}`;
157
+ const { lines, index, indent } = lineOf(text, key);
158
+
159
+ if (index >= 0) {
160
+ lines[index] = `${" ".repeat(indent)}${name}: ${shown}`;
161
+ return lines.join("\n");
162
+ }
163
+
164
+ // A config written before this key existed does not have a line to change. Take
165
+ // the key from the defaults, comment and all, rather than silently doing nothing
166
+ // and reporting a save: an older config is the normal case for anyone who has had
167
+ // kaizen installed for a while.
168
+ return insert(text, key, shown, defaults);
169
+ }
170
+
171
+ function insert(text: string, key: string, shown: string, defaults: string) {
172
+ const parts = key.split(".");
173
+ const name = parts[parts.length - 1]!;
174
+ const indent = (parts.length - 1) * 2;
175
+ const src = defaults.split("\n");
176
+ const { index: at } = lineOf(defaults, key);
177
+
178
+ // The comment block above the key in the defaults explains it; carry it across.
179
+ const block: string[] = [];
180
+ if (at >= 0) {
181
+ let i = at - 1;
182
+ while (i >= 0 && /^\s*#/.test(src[i]!)) { block.unshift(src[i]!); i--; }
183
+ }
184
+ block.push(`${" ".repeat(indent)}${name}: ${shown}`);
185
+
186
+ const lines = text.split("\n");
187
+ if (parts.length === 1) {
188
+ while (lines.length && !lines[lines.length - 1]!.trim()) lines.pop();
189
+ return [...lines, "", ...block, ""].join("\n");
190
+ }
191
+
192
+ // Nested: put it under its parent if the parent is there, else write the parent too.
193
+ const parent = parts.slice(0, -1).join(".");
194
+ const { index: pi } = lineOf(text, parent);
195
+ if (pi < 0) {
196
+ while (lines.length && !lines[lines.length - 1]!.trim()) lines.pop();
197
+ return [...lines, "", `${parts[0]}:`, ...block, ""].join("\n");
198
+ }
199
+ let end = pi + 1;
200
+ while (end < lines.length && (/^\s+\S/.test(lines[end]!) || !lines[end]!.trim())) end++;
201
+ lines.splice(end, 0, ...block);
154
202
  return lines.join("\n");
155
203
  }
156
204
 
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Run kaizen with every stage as its own cold agent — the independent review
3
+ ---
4
+
5
+ Invoke the `kaizen` skill with argument `full $ARGUMENTS`.
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Run kaizen with every stage in this session — cheaper, and the reviewer has seen the work
3
+ ---
4
+
5
+ Invoke the `kaizen` skill with argument `lite $ARGUMENTS`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Staged workflow for AI agents \u2014 code, writing, research, operations: plan, human approval, build, independent review, bounded fix loop.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,6 +39,8 @@ Porting the workflow to Codex or Antigravity is covered in [`adapters.md`](adapt
39
39
  | `/kaizen <request>` | Start a new run in the configured mode (default: `approve`) |
40
40
  | `/kaizen plan <request>` | Start a run and stop after the plan (`plan-only`) |
41
41
  | `/kaizen auto <request>` | Start a run in `auto` mode: every stage back to back, no approval stops |
42
+ | `/kaizen lite <request>` | Run every stage in this session — cheap, and the reviewer has seen the work |
43
+ | `/kaizen full <request>` | Dispatch every stage as its own agent — the reviewer starts cold |
42
44
  | `/kaizen run` | Resume the current run and execute the approved plan |
43
45
  | `/kaizen review [target]` | Review-only: audit existing code, no plan, no implementation (`review-only`) |
44
46
  | `/kaizen status` | List every run grouped waiting-on-you / in flight / done / abandoned, plus the open backlog count |
@@ -61,7 +63,8 @@ If the user typed `/kaizen` with no argument and a run is in progress, treat it
61
63
  files, a config, and — where the folder is a git repository — the gitignore entry
62
64
  (see [`adapters.md`](adapters.md)).
63
65
  2. Read `.kaizen/config.yml`, falling back to `config.default.yml` in this skill for
64
- any key it does not set.
66
+ any key it does not set. `runner` decides whether this run dispatches subagents at
67
+ all; read it before the first stage, not when you reach one.
65
68
  3. Read `.kaizen/runs/<current>/state.json` if a run is active. Never assume the
66
69
  stage from conversation memory — the run may have been advanced by another tool
67
70
  or another session.
@@ -79,6 +82,7 @@ whichever the user picks.
79
82
  | Setting | Values | What it decides |
80
83
  |---|---|---|
81
84
  | `mode` | `approve`, `auto`, `plan-only`, `review-only` | Where a run stops |
85
+ | `runner` | `full`, `lite` | Whether each stage is its own cold agent, or this session runs them all |
82
86
  | `build.executor` | `subagent`, `inline`, `ask` | Who carries out the approved plan |
83
87
  | `approvals.plan` | `true`, `false` | Stop and show the plan before anything is built |
84
88
  | `approvals.review` | `true`, `false` | Stop after the review, before the run is called done |
@@ -110,6 +114,31 @@ picker with thirty options.
110
114
  someone who would rather see the whole file at once. Mention it when the user is
111
115
  changing several at a time.
112
116
 
117
+ ## What is worth a run
118
+
119
+ A run costs several times what the same change costs in one session, because every
120
+ stage starts cold and the reviewer is deliberately kept ignorant of how the work was
121
+ done. That price buys one thing: an agent with nothing to defend, which is the only
122
+ kind that finds the bug in the fix.
123
+
124
+ Spend it where being wrong is expensive — money, data loss, a migration, anything a
125
+ customer sees, anything hard to undo, anything touching a part of the system nobody
126
+ has read in a year. Do not spend it on work where being wrong is cheap and obvious.
127
+ A tool that charges the same for a typo and a payroll change gets turned off for both.
128
+
129
+ **Small changes take the short path.** Where the approved plan touches roughly two
130
+ files or fifty lines, run the stages in this session (`runner: lite`, below)
131
+ rather than dispatching them, then stop after the review: report the findings and let
132
+ the user choose, rather than entering the fix loop. The loop
133
+ exists because a fix to something intricate can be worse than the defect — it is not
134
+ worth two more stages to confirm a one-line message now reads correctly. A finding at
135
+ `high` or above pulls the run back onto the full path whatever its size, since that
136
+ severity is the loop's whole reason for existing.
137
+
138
+ This is a judgement, not a threshold to enforce: fifty lines that change how money is
139
+ rounded are not a small change. Say which path you took and why, in one line, when the
140
+ run starts.
141
+
113
142
  ## Modes
114
143
 
115
144
  Set by `mode` in config, overridable per invocation.
@@ -215,6 +244,37 @@ Each line is the run id, its stage, and what it is awaiting. Grouping is derived
215
244
  `state.json` at read time, never from the directory name — run directories are flat and
216
245
  are never renamed or moved, so a path written into another file cannot break.
217
246
 
247
+ ## Who runs the stages
248
+
249
+ `runner` decides, and it is the difference between a run costing about what the work
250
+ would cost in one session and several times that. Set it for the project with
251
+ `/kaizen config` or `kaizen settings`; override it for one run with `/kaizen lite
252
+ <request>` or `/kaizen full <request>`, which does not change the project's setting.
253
+
254
+ - **`full`** (default) — each stage is dispatched as its own agent, started cold.
255
+ The reviewer has never seen how the work was done, so it has nothing to defend.
256
+ That is the entire reason its findings are worth reading, and it is what catches the
257
+ bug in a fix rather than the bug in the plan.
258
+ - **`lite`** — this session runs every stage itself, in order, as
259
+ [`adapters.md`](adapters.md) already describes for tools that have no subagents.
260
+ Read only the stage's declared inputs, write its artifact before starting the next,
261
+ and when reviewing, judge the work on its own terms rather than defending the
262
+ reasoning you used producing it.
263
+
264
+ Everything else holds in both: the plan, the approval stop, the numbered gate answered
265
+ with evidence, numbered findings, the files, and a run another tool can pick up.
266
+
267
+ **Be honest about what `lite` costs you.** Instructing an agent to be impartial
268
+ about something it just did is weaker than an agent that cannot be partial, because it
269
+ never saw it. A same-session review reliably finds what is visible in the diff. It is
270
+ much less likely to find the defect that requires disbelieving the reasoning that
271
+ produced it — and a fix that is worse than the finding it closes is exactly that shape.
272
+
273
+ So: `lite` where being wrong is cheap and obvious, which is also where the short path
274
+ above applies. `full` where being wrong is expensive. When a run is going to
275
+ touch money, a migration, or anything hard to undo, say which runner it used at the
276
+ final report, so nobody reads a same-session review as an independent one.
277
+
218
278
  ## Running a stage
219
279
 
220
280
  **If the tool you are running in has no subagents** — Codex, and anything else that
@@ -224,8 +284,16 @@ only its declared inputs, write its artifact before moving on, and when reviewin
224
284
  judge the work on its own terms rather than defending the reasoning you used writing
225
285
  it. Everything else in this file holds unchanged.
226
286
 
227
- Each stage runs as a subagent via the Agent tool, with the agent type named below.
228
- Pass it the run directory path and let it read its own inputs from there — do not
287
+ **Check `runner` first.**
288
+
289
+ - `runner: lite` — do not call the Agent tool. You run plan, build and review
290
+ yourself, in that order, reading only each stage's declared inputs and writing its
291
+ artifact before starting the next. The stage table below still applies; you are the
292
+ agent for every row.
293
+ - `runner: full` — dispatch each stage as its own subagent, as follows.
294
+
295
+ Under `full`, each stage runs as a subagent via the Agent tool, with the agent type
296
+ named below. Pass it the run directory path and let it read its own inputs from there — do not
229
297
  paste plans or diffs into the prompt, since the file is the shared source of truth
230
298
  across tools. Name its spec files in the prompt: core `spec.md` plus its own stage
231
299
  file, never the whole set.
@@ -245,6 +313,13 @@ conversation the work happened in.
245
313
  After a subagent returns, update `state.json` yourself in the main thread. Subagents
246
314
  report; the main thread owns the state machine.
247
315
 
316
+ **Name files, do not narrate them.** The prompt says which paths to read, which
317
+ decisions bind the stage, and what to report back. It does not summarize those files:
318
+ the subagent is about to read them, and a summary is the same content paid for twice —
319
+ once in your prompt and once when it opens the file. Asking for analysis the stage's
320
+ own contract does not require costs a stage's worth of tokens for an opinion nobody
321
+ will act on.
322
+
248
323
  **Scope precisely, don't scope narrow.** Each subagent starts cold — no context from
249
324
  you carries over except what the prompt names. List exactly the files this stage's
250
325
  task actually touches; don't default to "read everything" for safety. But cutting a
@@ -281,12 +356,15 @@ shown automatically.
281
356
  state.json
282
357
  ```
283
358
 
284
- `state.json` is the resume point:
359
+ `state.json` is the resume point, and records the runner the run started with, so a
360
+ run picked up later keeps the shape it was reviewed under rather than silently
361
+ changing because a setting moved:
285
362
 
286
363
  ```json
287
364
  {
288
365
  "id": "2026-09-02-add-oauth",
289
366
  "mode": "approve",
367
+ "runner": "full",
290
368
  "stage": "review",
291
369
  "awaiting": "approvals.review",
292
370
  "iteration": 1,
@@ -14,6 +14,19 @@ approvals:
14
14
  # Confirm every individual file edit. Maximum control, slowest.
15
15
  each_file: false
16
16
 
17
+ # How much machinery a run uses.
18
+ # full - each stage is its own agent, started cold. The reviewer never sees how the
19
+ # work was done, which is the only reason its findings are worth reading.
20
+ # Costs several times what the same change costs in one session.
21
+ # lite - the session you are talking to runs every stage itself, in order, keeping
22
+ # each stage to its own declared inputs. Roughly the cost of doing the work
23
+ # by hand, and you watch it happen. The reviewer has seen the work, so
24
+ # expect it to miss what it just did.
25
+ # Everything else is the same either way: the plan, the approval, the gate, the
26
+ # findings, the files, and resuming a run in another tool.
27
+ # Override for one run with `/kaizen lite <request>` or `/kaizen full <request>`.
28
+ runner: full
29
+
17
30
  build:
18
31
  # Who carries out the approved plan.
19
32
  # subagent - dispatch the builder as its own agent. The main thread keeps its
@@ -91,3 +91,14 @@ reports nothing at or above the threshold, or `max_iterations` is reached.
91
91
  A finding the loop could not close is escalated to the user with the reviewer's
92
92
  description intact — never quietly downgraded to make the run look clean.
93
93
 
94
+ **An iteration reads the finding, the work, and the gate.** Not the plan. Fixing a
95
+ named defect needs the reviewer's line, the diff so far, and the gate the result must
96
+ still pass; it does not need the framing, the alternatives rejected, or the out-of-scope
97
+ list, all of which were settled before the finding existed. Read `01-plan.md` only when
98
+ a finding turns on what was approved — a scope question, or a fix that would take the
99
+ work outside it.
100
+
101
+ Fix what the finding names and stop. A second defect noticed while fixing the first is
102
+ a note in the iteration file, not a second fix: it has not been reviewed, and the
103
+ recheck is scoped to what the iteration claims to have changed.
104
+
@@ -82,7 +82,9 @@ subagents at all.
82
82
  Controlled by `approvals.review`, skipped in `auto`.
83
83
 
84
84
  Report: what was built, verification results, findings fixed, findings still open,
85
- and anything the builder noted as out of scope. Then stop. Committing, pushing, or
85
+ and anything the builder noted as out of scope. Where the run used `runner: lite`, say
86
+ so in that report — the reviewer saw the work, and nobody should read a same-session
87
+ review as an independent one because the output looked the same. Then stop. Committing, pushing, or
86
88
  opening a pull request happens only if the user asks.
87
89
 
88
90
  **Open findings are offered by number.** List them with the reviewer's numbering
@@ -76,6 +76,19 @@ The planner reads code but writes none. Its plan must contain:
76
76
  The plan is a proposal, not a decision. It never edits files, runs migrations, or
77
77
  installs packages.
78
78
 
79
+ **Length is a cost the whole run pays.** Every later stage reads this file, and the
80
+ fix loop reads it again per iteration, so a line written once is read five or six
81
+ times. Sections 3 to 6 — the material to reuse, the file manifest, the steps, the
82
+ verification — earn their length: they are what stop every later stage searching the
83
+ material again, and cutting them costs more than it saves. Sections 0, 1, 7 and 9 are
84
+ read once by a human at the approval and should be as short as they can be and still
85
+ be correct: a paragraph of framing, a paragraph of goal, the risks that are real, the
86
+ questions that are open. An alternative rejected gets a line saying what and why, not
87
+ a case against it.
88
+
89
+ A plan whose framing is longer than its steps is a plan that spent the run's budget
90
+ explaining itself.
91
+
79
92
  ---
80
93
 
81
94
  ## Plan approval
@@ -87,3 +87,20 @@ reports nothing at or above the threshold, or `max_iterations` is reached.
87
87
  A finding the loop could not close is escalated to the user with the reviewer's
88
88
  description intact — never quietly downgraded to make the run look clean.
89
89
 
90
+ **A recheck is not a second review.** Re-answer the findings it was meant to close,
91
+ the gate items the change could plausibly touch, and the regression check. Nothing
92
+ else. State which items were skipped and why, in one line, so the reader can disagree
93
+ with the judgement — a skipped item is a claim that the change could not have reached
94
+ it, and that claim is checkable.
95
+
96
+ Answering all of them again is not thoroughness, it is the same evidence gathered
97
+ twice. A fix that touched one line of a message string cannot have broken an item
98
+ about which directories the installer writes to, and re-running that item's live
99
+ setup to prove it costs more than the fix did. Where a change genuinely could have
100
+ reached everything — a rename across the run's files, a shared helper — say so and
101
+ re-answer everything.
102
+
103
+ The exception is the regression check, which runs every iteration whatever the fix
104
+ touched. It is cheap, and it is the one that catches a fix breaking something nobody
105
+ thought to connect it to.
106
+
@@ -101,9 +101,15 @@ sentence and either nods or says "no, this is a blog post". They are never asked
101
101
  categorize their own request up front.
102
102
 
103
103
  Write into `00-request.md`: the verbatim request, the deliverable, what done means,
104
- and what wrong means. Record in `state.json`: `id`, `mode`, `track`,
104
+ and what wrong means. Record in `state.json`: `id`, `mode`, `runner`, `track`,
105
105
  `stage: "plan"`, `awaiting: null`.
106
106
 
107
+ `runner` is read from config at intake and written down here, because it decides
108
+ whether the stages that follow are separate agents or this one. A run resumed later
109
+ keeps the runner it started with: a review done in the same session as the work is a
110
+ different thing from an independent one, and which happened should not depend on what
111
+ the config said the day someone came back to it.
112
+
107
113
  If the request remains ambiguous enough that a plan would be guesswork, ask now. One
108
114
  round of questions here is cheaper than a rejected plan.
109
115
 
@@ -23,6 +23,8 @@ anything.
23
23
  | `/kaizen <request>` | Start a new run in the configured mode (default: `approve`) |
24
24
  | `/kaizen plan <request>` | Start a run and stop after the plan (`plan-only`) |
25
25
  | `/kaizen auto <request>` | Start a run in `auto` mode: every stage back to back, no approval stops |
26
+ | `/kaizen lite <request>` | Run the stages in this session — cheap, reviewer has seen the work |
27
+ | `/kaizen full <request>` | Dispatch each stage as its own cold agent |
26
28
  | `/kaizen run` | Resume the current run and execute the approved plan |
27
29
  | `/kaizen review [target]` | Review-only: audit existing code, no plan, no implementation |
28
30
  | `/kaizen status` | List every run grouped waiting-on-you / in flight / done / abandoned, plus open backlog count |
@@ -74,6 +76,7 @@ Each iteration is written to `runs/<id>/05-iterations/`.
74
76
  | Key | Default | Meaning |
75
77
  |---|---|---|
76
78
  | `mode` | `approve` | Default run mode |
79
+ | `runner` | `full` | `full`: each stage a cold subagent. `lite`: this session runs every stage |
77
80
  | `approvals.plan` | `true` | Stop after the plan |
78
81
  | `approvals.review` | `false` | Stop after the review |
79
82
  | `approvals.each_file` | `false` | Confirm every file edit |