kaizen-agent 0.6.0 → 0.7.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/README.md +3 -0
- package/cli/dashboard.ts +221 -59
- package/cli/install.ts +41 -15
- package/cli/settings.ts +54 -6
- package/commands/kaizen-full.md +5 -0
- package/commands/kaizen-init.md +1 -1
- package/commands/kaizen-lite.md +5 -0
- package/package.json +1 -1
- package/skills/kaizen/SKILL.md +93 -12
- package/skills/kaizen/adapters.md +3 -2
- package/skills/kaizen/config.default.yml +30 -0
- package/skills/kaizen/spec-build.md +14 -0
- package/skills/kaizen/spec-main.md +3 -1
- package/skills/kaizen/spec-plan.md +13 -0
- package/skills/kaizen/spec-review.md +17 -0
- package/skills/kaizen/spec.md +7 -1
- package/skills/kaizen-help/SKILL.md +4 -1
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
|
-
|
|
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
|
|
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
|
|
191
|
-
if (
|
|
192
|
-
await runDetail(runs[
|
|
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
|
|
262
|
-
(n) => full[n] === null);
|
|
263
|
-
if (
|
|
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[
|
|
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,52 @@ 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 =
|
|
303
|
-
const at =
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
+
stdout.write(" " + c.cyan(line.padEnd(gutter)) + cut(beside[i] ?? "", cols - gutter - 2) + "\n");
|
|
314
417
|
}
|
|
315
418
|
} else {
|
|
316
419
|
stdout.write(` ${c.bold("kaizen")} ${c.dim(version(repo))} ${c.dim(state ? tilde(dirname(state)) : "no project here")}\n`);
|
|
@@ -318,18 +421,28 @@ export async function dashboard(
|
|
|
318
421
|
}
|
|
319
422
|
|
|
320
423
|
if (state) {
|
|
424
|
+
// Across every project, not only this one. Where more than one is known each
|
|
425
|
+
// row says which, since "waiting on approvals.plan" means nothing on its own.
|
|
426
|
+
const many = new Set(seen.runs.map((r) => r.where)).size > 1;
|
|
427
|
+
const from = (w: string) => (many ? c.dim(w + " ") : "");
|
|
428
|
+
const wait = seen.runs.filter((r) => r.awaiting && r.stage !== "abandoned");
|
|
429
|
+
const fly = seen.runs.filter((r) => !r.awaiting && !["done", "abandoned"].includes(r.stage));
|
|
430
|
+
const fin = seen.runs.filter((r) => r.stage === "done");
|
|
431
|
+
|
|
321
432
|
const runLines = [
|
|
322
|
-
...
|
|
323
|
-
...
|
|
324
|
-
...(
|
|
433
|
+
...wait.map((r) => `${c.amber("●")} ${from(r.where)}${short(r.id)} ${c.dim(r.awaiting ?? "")}`),
|
|
434
|
+
...fly.map((r) => `${c.cyan("●")} ${from(r.where)}${short(r.id)} ${c.dim(r.stage)}`),
|
|
435
|
+
...(fin.length ? [c.dim(`● ${fin.length} done`)] : []),
|
|
325
436
|
];
|
|
326
437
|
if (!runLines.length) runLines.push(c.dim("none yet"));
|
|
327
438
|
|
|
328
|
-
|
|
439
|
+
// parseItem's `where` is the file the finding is in; the project is a separate
|
|
440
|
+
// thing and both belong on the row.
|
|
441
|
+
const items = seen.items.map((i) => ({ ...parseItem(i.text), project: i.where }));
|
|
329
442
|
const backLines = items.slice(0, 6).map((it) => {
|
|
330
443
|
const sev = it.severity ? (it.severity === "critical" || it.severity === "high"
|
|
331
444
|
? c.amber(it.severity) : c.dim(it.severity)) + " " : "";
|
|
332
|
-
return sev + (it.where ? c.dim(it.where) + " " : "") + it.text;
|
|
445
|
+
return from(it.project) + sev + (it.where ? c.dim(it.where) + " " : "") + it.text;
|
|
333
446
|
});
|
|
334
447
|
if (!backLines.length) backLines.push(c.dim("nothing open"));
|
|
335
448
|
else if (items.length > 6) backLines.push(c.dim(`… ${items.length - 6} more`));
|
|
@@ -406,6 +519,50 @@ export async function dashboard(
|
|
|
406
519
|
}
|
|
407
520
|
}
|
|
408
521
|
|
|
522
|
+
// The list of projects kaizen knows about. Appended to as kaizen sets one up or is
|
|
523
|
+
// opened inside one, and seeded by the Find projects action. Plain lines so it can be
|
|
524
|
+
// read and corrected in an editor; blanks and # lines ignored.
|
|
525
|
+
const REGISTRY = join(home, ".kaizen", "projects");
|
|
526
|
+
|
|
527
|
+
export function knownProjects(): string[] {
|
|
528
|
+
if (!existsSync(REGISTRY)) return [];
|
|
529
|
+
return readFileSync(REGISTRY, "utf8").split("\n")
|
|
530
|
+
.map((l) => l.trim())
|
|
531
|
+
.filter((l) => l && !l.startsWith("#"));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Never fails the command that triggered it: a read-only home is not a reason for a
|
|
535
|
+
// dashboard to stop opening.
|
|
536
|
+
export function remember(projectDir: string) {
|
|
537
|
+
try {
|
|
538
|
+
if (knownProjects().includes(projectDir)) return;
|
|
539
|
+
mkdirSync(dirname(REGISTRY), { recursive: true });
|
|
540
|
+
appendFileSync(REGISTRY, projectDir + "\n");
|
|
541
|
+
} catch { /* nothing here is worth an error */ }
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Bounded on purpose. Depth is what keeps this from becoming a filesystem walk, and
|
|
545
|
+
// the skips are the directories that make one slow: node_modules, and anything dotted
|
|
546
|
+
// (which includes .git, and every state directory that is not a project of its own).
|
|
547
|
+
function findProjects(root: string, depth = 4): string[] {
|
|
548
|
+
const found: string[] = [];
|
|
549
|
+
const walk = (dir: string, left: number) => {
|
|
550
|
+
let entries: string[];
|
|
551
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
552
|
+
// $HOME holds ~/.kaizen, the global state, which is not a project and must not
|
|
553
|
+
// stop the walk before it has looked at anything.
|
|
554
|
+
if (dir !== root && entries.includes(".kaizen")) { found.push(dir); return; }
|
|
555
|
+
if (left === 0) return;
|
|
556
|
+
for (const name of entries) {
|
|
557
|
+
if (name.startsWith(".") || name === "node_modules") continue;
|
|
558
|
+
try { if (statSync(join(dir, name)).isDirectory()) walk(join(dir, name), left - 1); }
|
|
559
|
+
catch { /* unreadable or vanished mid-walk */ }
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
walk(root, depth);
|
|
563
|
+
return found;
|
|
564
|
+
}
|
|
565
|
+
|
|
409
566
|
function locate() {
|
|
410
567
|
let dir = process.cwd();
|
|
411
568
|
for (;;) {
|
|
@@ -444,6 +601,11 @@ function backlog(state: string) {
|
|
|
444
601
|
function version(repo: string) {
|
|
445
602
|
try { return JSON.parse(readFileSync(join(repo, "package.json"), "utf8")).version; } catch { return ""; }
|
|
446
603
|
}
|
|
604
|
+
// The global state directory is not a project and has no parent worth naming.
|
|
605
|
+
function label(stateDir: string) {
|
|
606
|
+
return stateDir === join(home, ".kaizen") ? "global" : tilde(dirname(stateDir));
|
|
607
|
+
}
|
|
608
|
+
|
|
447
609
|
function tilde(p: string) { return p.startsWith(home) ? "~" + p.slice(home.length) : p; }
|
|
448
610
|
|
|
449
611
|
// The first few real lines of a file's body, blank lines and headings dropped.
|
|
@@ -531,7 +693,7 @@ function panel(title: string, lines: string[], inner: number) {
|
|
|
531
693
|
const body = cut(line, inner - 2);
|
|
532
694
|
out.push(`${DIM}│${OFF} ${body}${" ".repeat(Math.max(0, inner - 2 - vis(body)))} ${DIM}│${OFF}`);
|
|
533
695
|
}
|
|
534
|
-
out.push(`${DIM}╰${"─".repeat(inner)}╯${OFF}`);
|
|
696
|
+
out.push(`${DIM}╰${"─".repeat(Math.max(0, inner))}╯${OFF}`);
|
|
535
697
|
return out;
|
|
536
698
|
}
|
|
537
699
|
|
package/cli/install.ts
CHANGED
|
@@ -287,10 +287,10 @@ async function chooseRoot() {
|
|
|
287
287
|
if (args.has("--project")) return process.cwd();
|
|
288
288
|
if (args.has("--global") || !interactive) return home;
|
|
289
289
|
// A project install only means something in a project. Standing in the home
|
|
290
|
-
// directory it is the global install under another name
|
|
291
|
-
//
|
|
290
|
+
// directory it is the global install under another name; any other folder is
|
|
291
|
+
// a project worth installing into, git repository or not.
|
|
292
292
|
const cwd = process.cwd();
|
|
293
|
-
if (cwd === home
|
|
293
|
+
if (cwd === home) return home;
|
|
294
294
|
|
|
295
295
|
const names = (r: string) => found(r).map((a) => a.name).join(", ");
|
|
296
296
|
return select("Install kaizen for", [
|
|
@@ -390,22 +390,43 @@ function installLauncher() {
|
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
const
|
|
393
|
+
const inProject = process.cwd() !== home;
|
|
394
394
|
const kaizenDir = join(process.cwd(), ".kaizen");
|
|
395
395
|
|
|
396
|
-
|
|
396
|
+
// A .kaizen/ in a parent already owns this folder's state, and setting up a child
|
|
397
|
+
// would shadow it, hiding the parent's runs and leaving the child out of the root
|
|
398
|
+
// .gitignore. Deliberately not shared with cli/dashboard.ts locate(): that one
|
|
399
|
+
// resolves which state directory to read and so falls back to a global ~/.kaizen,
|
|
400
|
+
// while this one decides whether a folder is already owned and must stop *before*
|
|
401
|
+
// $HOME — a global ~/.kaizen would otherwise suppress the setup offer for every
|
|
402
|
+
// non-repo folder under the home directory. Do not re-sync the two walks.
|
|
403
|
+
function kaizenAbove() {
|
|
404
|
+
let dir = process.cwd();
|
|
405
|
+
for (;;) {
|
|
406
|
+
if (dir === home) return false;
|
|
407
|
+
if (existsSync(join(dir, ".kaizen"))) return true;
|
|
408
|
+
if (existsSync(join(dir, ".git"))) return false;
|
|
409
|
+
const up = dirname(dir);
|
|
410
|
+
if (up === dir) return false;
|
|
411
|
+
dir = up;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (inProject && !kaizenAbove()) {
|
|
397
416
|
const now = interactive
|
|
398
417
|
? await select(`Set up ${basename(process.cwd())}/ for kaizen now`, [
|
|
399
|
-
|
|
418
|
+
// "Not now" leads: the offer now appears in any folder outside home, so the
|
|
419
|
+
// safe answer is the one under the cursor.
|
|
400
420
|
{ label: "Not now", hint: "the first /kaizen run will do it", value: false },
|
|
421
|
+
{ label: "Yes", hint: "writes .kaizen/ and a CLAUDE.md line so it runs by default", value: true },
|
|
401
422
|
])
|
|
402
423
|
: false;
|
|
403
|
-
if (now)
|
|
424
|
+
if (now) initProject();
|
|
404
425
|
} else if (existsSync(kaizenDir)) {
|
|
405
426
|
step(`${basename(process.cwd())}/.kaizen already set up`);
|
|
406
427
|
}
|
|
407
428
|
|
|
408
|
-
function
|
|
429
|
+
function initProject() {
|
|
409
430
|
mkdirSync(kaizenDir, { recursive: true });
|
|
410
431
|
for (const f of readdirSync(join(repo, "skills/kaizen")).filter((f) => f.startsWith("spec"))) {
|
|
411
432
|
copyFileSync(join(repo, "skills/kaizen", f), join(kaizenDir, f));
|
|
@@ -413,13 +434,18 @@ function initRepo() {
|
|
|
413
434
|
copyFileSync(join(repo, "skills/kaizen/config.default.yml"), join(kaizenDir, "config.yml"));
|
|
414
435
|
|
|
415
436
|
// Run state is local; the workflow itself is shared, so those files stay tracked.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
437
|
+
// Nothing to ignore where there is no git, so a plain folder gets no .gitignore.
|
|
438
|
+
let ignored = false;
|
|
439
|
+
if (existsSync(join(process.cwd(), ".git"))) {
|
|
440
|
+
const rules = ".kaizen/*\n!.kaizen/spec*.md\n!.kaizen/config.yml\n";
|
|
441
|
+
const gi = join(process.cwd(), ".gitignore");
|
|
442
|
+
const current = existsSync(gi) ? readFileSync(gi, "utf8") : "";
|
|
443
|
+
if (!current.includes(".kaizen/*")) {
|
|
444
|
+
appendFileSync(gi, (current && !current.endsWith("\n") ? "\n" : "") + "\n# kaizen run state\n" + rules);
|
|
445
|
+
ignored = true;
|
|
446
|
+
}
|
|
421
447
|
}
|
|
422
|
-
step("wrote .kaizen/ and the gitignore entry");
|
|
448
|
+
step(ignored ? "wrote .kaizen/ and the gitignore entry" : "wrote .kaizen/");
|
|
423
449
|
writePointer();
|
|
424
450
|
}
|
|
425
451
|
|
|
@@ -427,7 +453,7 @@ function initRepo() {
|
|
|
427
453
|
// ("add a discount cap") does not reach kaizen unless the project says it should.
|
|
428
454
|
// One line in the agent instructions is what makes it the default here.
|
|
429
455
|
function writePointer() {
|
|
430
|
-
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\
|
|
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`;
|
|
431
457
|
|
|
432
458
|
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
433
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
|
-
|
|
90
|
-
|
|
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
|
|
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
|
|
package/commands/kaizen-init.md
CHANGED
package/package.json
CHANGED
package/skills/kaizen/SKILL.md
CHANGED
|
@@ -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 |
|
|
@@ -47,7 +49,7 @@ Porting the workflow to Codex or Antigravity is covered in [`adapters.md`](adapt
|
|
|
47
49
|
| `/kaizen reject <reason>` | Reject it; the reason is fed back to the stage that produced it |
|
|
48
50
|
| `/kaizen abort` | Mark the current run abandoned |
|
|
49
51
|
| `/kaizen config` | Show this project's settings and change them, one picker per setting |
|
|
50
|
-
| `/kaizen init` |
|
|
52
|
+
| `/kaizen init` | Set up this project folder: `.kaizen/` with the `spec*.md` files, config, and — in a git repo — the gitignore entry |
|
|
51
53
|
| `/kaizen install` | Install the workflow itself globally or per-project (see below) |
|
|
52
54
|
|
|
53
55
|
If the user typed `/kaizen` with no argument and a run is in progress, treat it as
|
|
@@ -55,12 +57,14 @@ If the user typed `/kaizen` with no argument and a run is in progress, treat it
|
|
|
55
57
|
|
|
56
58
|
## Before doing anything
|
|
57
59
|
|
|
58
|
-
1. Locate the state directory: `.kaizen/` in the
|
|
59
|
-
keyed by working directory. If neither exists and the user is starting
|
|
60
|
-
`/kaizen init` first: it creates `.kaizen/` with a copy of the `spec*.md`
|
|
61
|
-
the gitignore entry
|
|
60
|
+
1. Locate the state directory: `.kaizen/` in the project folder's root, else
|
|
61
|
+
`~/.kaizen/` keyed by working directory. If neither exists and the user is starting
|
|
62
|
+
work, run `/kaizen init` first: it creates `.kaizen/` with a copy of the `spec*.md`
|
|
63
|
+
files, a config, and — where the folder is a git repository — the gitignore entry
|
|
64
|
+
(see [`adapters.md`](adapters.md)).
|
|
62
65
|
2. Read `.kaizen/config.yml`, falling back to `config.default.yml` in this skill for
|
|
63
|
-
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.
|
|
64
68
|
3. Read `.kaizen/runs/<current>/state.json` if a run is active. Never assume the
|
|
65
69
|
stage from conversation memory — the run may have been advanced by another tool
|
|
66
70
|
or another session.
|
|
@@ -78,6 +82,7 @@ whichever the user picks.
|
|
|
78
82
|
| Setting | Values | What it decides |
|
|
79
83
|
|---|---|---|
|
|
80
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 |
|
|
81
86
|
| `build.executor` | `subagent`, `inline`, `ask` | Who carries out the approved plan |
|
|
82
87
|
| `approvals.plan` | `true`, `false` | Stop and show the plan before anything is built |
|
|
83
88
|
| `approvals.review` | `true`, `false` | Stop after the review, before the run is called done |
|
|
@@ -109,6 +114,31 @@ picker with thirty options.
|
|
|
109
114
|
someone who would rather see the whole file at once. Mention it when the user is
|
|
110
115
|
changing several at a time.
|
|
111
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
|
+
|
|
112
142
|
## Modes
|
|
113
143
|
|
|
114
144
|
Set by `mode` in config, overridable per invocation.
|
|
@@ -214,6 +244,37 @@ Each line is the run id, its stage, and what it is awaiting. Grouping is derived
|
|
|
214
244
|
`state.json` at read time, never from the directory name — run directories are flat and
|
|
215
245
|
are never renamed or moved, so a path written into another file cannot break.
|
|
216
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
|
+
|
|
217
278
|
## Running a stage
|
|
218
279
|
|
|
219
280
|
**If the tool you are running in has no subagents** — Codex, and anything else that
|
|
@@ -223,8 +284,16 @@ only its declared inputs, write its artifact before moving on, and when reviewin
|
|
|
223
284
|
judge the work on its own terms rather than defending the reasoning you used writing
|
|
224
285
|
it. Everything else in this file holds unchanged.
|
|
225
286
|
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
228
297
|
paste plans or diffs into the prompt, since the file is the shared source of truth
|
|
229
298
|
across tools. Name its spec files in the prompt: core `spec.md` plus its own stage
|
|
230
299
|
file, never the whole set.
|
|
@@ -244,6 +313,13 @@ conversation the work happened in.
|
|
|
244
313
|
After a subagent returns, update `state.json` yourself in the main thread. Subagents
|
|
245
314
|
report; the main thread owns the state machine.
|
|
246
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
|
+
|
|
247
323
|
**Scope precisely, don't scope narrow.** Each subagent starts cold — no context from
|
|
248
324
|
you carries over except what the prompt names. List exactly the files this stage's
|
|
249
325
|
task actually touches; don't default to "read everything" for safety. But cutting a
|
|
@@ -280,12 +356,15 @@ shown automatically.
|
|
|
280
356
|
state.json
|
|
281
357
|
```
|
|
282
358
|
|
|
283
|
-
`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:
|
|
284
362
|
|
|
285
363
|
```json
|
|
286
364
|
{
|
|
287
365
|
"id": "2026-09-02-add-oauth",
|
|
288
366
|
"mode": "approve",
|
|
367
|
+
"runner": "full",
|
|
289
368
|
"stage": "review",
|
|
290
369
|
"awaiting": "approvals.review",
|
|
291
370
|
"iteration": 1,
|
|
@@ -293,9 +372,11 @@ shown automatically.
|
|
|
293
372
|
}
|
|
294
373
|
```
|
|
295
374
|
|
|
296
|
-
`init` adds a gitignore entry that ignores run state but
|
|
297
|
-
ignores `.kaizen/*` and un-ignores `.kaizen/spec*.md` and
|
|
298
|
-
are meant to be committed (see
|
|
375
|
+
In a git repository, `init` also adds a gitignore entry that ignores run state but
|
|
376
|
+
keeps the workflow: it ignores `.kaizen/*` and un-ignores `.kaizen/spec*.md` and
|
|
377
|
+
`.kaizen/config.yml`, which are meant to be committed (see
|
|
378
|
+
[`adapters.md`](adapters.md)). A project folder that is not a repository gets no
|
|
379
|
+
gitignore.
|
|
299
380
|
|
|
300
381
|
## Install
|
|
301
382
|
|
|
@@ -64,8 +64,9 @@ completes. A stale `state.json` is the one way this breaks.
|
|
|
64
64
|
|
|
65
65
|
## Installing the adapter files
|
|
66
66
|
|
|
67
|
-
`/kaizen init` writes `.kaizen/` with the `spec*.md` files
|
|
68
|
-
|
|
67
|
+
`/kaizen init` writes `.kaizen/` with the `spec*.md` files and `config.yml` into the
|
|
68
|
+
project folder, plus the gitignore entry when that folder is a git repository.
|
|
69
|
+
`/kaizen install` additionally offers to append the `AGENTS.md` block above.
|
|
69
70
|
`.kaizen/spec*.md` and `.kaizen/config.yml` should be committed even though run state
|
|
70
71
|
is ignored — the workflow is shared, the run data is not:
|
|
71
72
|
|
|
@@ -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
|
|
@@ -55,12 +68,27 @@ review:
|
|
|
55
68
|
# Additional checks per track, chosen by what the run actually is. The planner may
|
|
56
69
|
# name a track not listed here; the reviewer then derives its checks from the
|
|
57
70
|
# track's "what would make it wrong".
|
|
71
|
+
# Each check below is one question the reviewer asks; the line beside it says which.
|
|
58
72
|
track_checks:
|
|
73
|
+
# security - no new way in, no secret or user data exposed
|
|
74
|
+
# reuse_simplification - nothing rebuilt that the codebase already had
|
|
75
|
+
# test_coverage - the changed behaviour has a check that fails when it breaks
|
|
59
76
|
software: [security, reuse_simplification, test_coverage]
|
|
60
77
|
# blast_radius and regression are universal — see spec-review.md — not listed here.
|
|
78
|
+
# completeness - every part the piece promises is actually present
|
|
79
|
+
# claim_accuracy - every checkable claim traced to a source
|
|
80
|
+
# reader_usability - its intended reader can act on it without asking
|
|
61
81
|
writing: [completeness, claim_accuracy, reader_usability]
|
|
82
|
+
# audience_fit - written for the people who will actually read it
|
|
83
|
+
# voice_consistency - reads as the same voice as the surrounding material
|
|
62
84
|
communication: [claim_accuracy, audience_fit, voice_consistency]
|
|
85
|
+
# prerequisites - everything needed before step one is stated
|
|
86
|
+
# rollback - a way back exists for every step that changes something
|
|
87
|
+
# executability - someone else can run it start to finish unaided
|
|
63
88
|
operations: [prerequisites, rollback, executability]
|
|
89
|
+
# evidence_support - the conclusion follows from evidence shown, not asserted
|
|
90
|
+
# selection_bias - the evidence was not picked to fit the answer
|
|
91
|
+
# alternatives - the readings that were rejected are named and why
|
|
64
92
|
research: [evidence_support, selection_bias, alternatives]
|
|
65
93
|
# Let the reviewer append durable lessons to .kaizen/memory.md.
|
|
66
94
|
write_memory: true
|
|
@@ -90,6 +118,8 @@ state:
|
|
|
90
118
|
|
|
91
119
|
git:
|
|
92
120
|
# Never commit, push, or open a PR unless the user explicitly asks.
|
|
121
|
+
# Ignored when the project is not a git repository.
|
|
93
122
|
auto_commit: false
|
|
94
123
|
# Create a branch before the implement stage when on the default branch.
|
|
124
|
+
# Ignored when the project is not a git repository.
|
|
95
125
|
branch_before_implement: true
|
|
@@ -35,6 +35,9 @@ Rules:
|
|
|
35
35
|
abstraction, a new file, a new section — is allowed, and costs one written line
|
|
36
36
|
saying what it serves. The line is the price of the technique, not a request for
|
|
37
37
|
permission. No line means the reviewer records a finding.
|
|
38
|
+
- The `git.*` settings apply only where the work sits in a git repository. In a plain
|
|
39
|
+
folder there is no branch to make and no commit to skip, and that absence is not a
|
|
40
|
+
finding.
|
|
38
41
|
|
|
39
42
|
**Baseline first, in any track with an existing suite of checks.** Before changing
|
|
40
43
|
anything, run whatever already proves the existing material correct — the test suite,
|
|
@@ -88,3 +91,14 @@ reports nothing at or above the threshold, or `max_iterations` is reached.
|
|
|
88
91
|
A finding the loop could not close is escalated to the user with the reviewer's
|
|
89
92
|
description intact — never quietly downgraded to make the run look clean.
|
|
90
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.
|
|
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
|
+
|
package/skills/kaizen/spec.md
CHANGED
|
@@ -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 |
|
|
@@ -31,7 +33,7 @@ anything.
|
|
|
31
33
|
| `/kaizen reject <reason>` | Reject it; the reason decides revise (back to planner) vs kill (abandon) |
|
|
32
34
|
| `/kaizen abort` | Mark the current run abandoned |
|
|
33
35
|
| `/kaizen config` | Show this project's settings and change them, one picker per setting |
|
|
34
|
-
| `/kaizen init` |
|
|
36
|
+
| `/kaizen init` | Set up this project folder: `.kaizen/` with `spec.md`, config, and — in a git repo — the gitignore entry |
|
|
35
37
|
| `/kaizen install` | Install the workflow itself globally or per-project |
|
|
36
38
|
|
|
37
39
|
`/kaizen` with no argument: `status` if a run is in progress, else ask what to build.
|
|
@@ -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 |
|