kaizen-agent 1.5.3 → 1.5.5
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 +8 -4
- package/agents/kaizen-builder.md +2 -0
- package/agents/kaizen-planner.md +9 -2
- package/cli/dashboard.ts +15 -4
- package/cli/install.ts +37 -1
- package/cli/settings.ts +37 -0
- package/cli/state.test.ts +79 -1
- package/cli/state.ts +136 -14
- package/cli/web.ts +163 -10
- package/package.json +1 -1
- package/skills/kaizen/SKILL.md +6 -2
- package/skills/kaizen/spec-build.md +6 -0
- package/skills/kaizen/spec-plan.md +17 -3
- package/skills/kaizen/spec.md +6 -1
- package/web/board.html +133 -50
package/cli/web.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { existsSync, readFileSync, readdirSync, statSync, watch, type FSWatcher
|
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import {
|
|
6
6
|
home, type Item, boardCards, knownProjects, remember, locate, label, tilde,
|
|
7
|
-
readInbox, writeInbox, replaceIdea, abandonRun, launchRun, parseItem, short,
|
|
7
|
+
readInbox, writeInbox, replaceIdea, abandonRun, launchRun, parseItem, short, setArchived, writeNotes,
|
|
8
|
+
appendNote, saveAttachment,
|
|
8
9
|
} from "./state.ts";
|
|
9
10
|
|
|
10
11
|
const PAGE = join(dirname(import.meta.dir), "web", "board.html");
|
|
@@ -47,6 +48,9 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
47
48
|
for (const c of clients) { try { c.enqueue("data: changed\n\n"); } catch { clients.delete(c); } }
|
|
48
49
|
}, 200);
|
|
49
50
|
};
|
|
51
|
+
// Dev: the page reloads itself when board.html is saved. Server code needs
|
|
52
|
+
// `bun --watch cli/install.ts web`; the page reconnects and reloads on hello.
|
|
53
|
+
try { watch(PAGE, () => { for (const c of clients) { try { c.enqueue("data: reload\n\n"); } catch { clients.delete(c); } } }); } catch { /* no dev reload */ }
|
|
50
54
|
const watchers: FSWatcher[] = [];
|
|
51
55
|
const watchAll = () => {
|
|
52
56
|
for (const w of watchers) { try { w.close(); } catch { /* gone */ } }
|
|
@@ -61,7 +65,7 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
61
65
|
const fingerprint = () => {
|
|
62
66
|
let sum = 0;
|
|
63
67
|
for (const dir of states(true)) {
|
|
64
|
-
for (const f of ["inbox.md", "backlog.md"]) { try { sum += statSync(join(dir, f)).mtimeMs; } catch { /* absent */ } }
|
|
68
|
+
for (const f of ["inbox.md", "backlog.md", "archive.md"]) { try { sum += statSync(join(dir, f)).mtimeMs; } catch { /* absent */ } }
|
|
65
69
|
const runs = join(dir, "runs");
|
|
66
70
|
if (!existsSync(runs)) continue;
|
|
67
71
|
for (const id of readdirSync(runs)) { try { sum += statSync(join(runs, id, "state.json")).mtimeMs; } catch { /* absent */ } }
|
|
@@ -69,11 +73,26 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
69
73
|
return sum;
|
|
70
74
|
};
|
|
71
75
|
let last = fingerprint();
|
|
72
|
-
|
|
76
|
+
let seen = states(true).join("\n");
|
|
77
|
+
const poll = setInterval(() => {
|
|
78
|
+
// A project registered since start (a run launched into a fresh dir) gets a watcher too.
|
|
79
|
+
const now = states(true).join("\n"); if (now !== seen) { seen = now; watchAll(); }
|
|
80
|
+
const fp = fingerprint(); if (fp !== last) { last = fp; changed(); }
|
|
81
|
+
}, 2000);
|
|
73
82
|
|
|
74
|
-
const
|
|
83
|
+
const BOOT = Date.now().toString(36);
|
|
84
|
+
const ping = setInterval(() => { for (const c of clients) { try { c.enqueue(": ping\n\n"); } catch { clients.delete(c); } } }, 8000);
|
|
85
|
+
// A launcher clicked while a board is already up should show that board, not die.
|
|
86
|
+
const port = opts.port ?? DEFAULT_PORT;
|
|
87
|
+
const server = (() => { try { return serve(); } catch (e) {
|
|
88
|
+
if ((e as { code?: string }).code !== "EADDRINUSE") throw e;
|
|
89
|
+
return null;
|
|
90
|
+
} })();
|
|
91
|
+
if (!server) { const url = `http://127.0.0.1:${port}/`; console.log(`\n kaizen web already at ${url}\n`); if (opts.open !== false) await openApp(url); return; }
|
|
92
|
+
|
|
93
|
+
function serve() { return Bun.serve({
|
|
75
94
|
hostname: "127.0.0.1",
|
|
76
|
-
port
|
|
95
|
+
port,
|
|
77
96
|
async fetch(req) {
|
|
78
97
|
// Host first: a hostname rebound to 127.0.0.1 by an attacker's DNS still
|
|
79
98
|
// arrives with that hostname in Host, and gets nothing.
|
|
@@ -111,13 +130,43 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
111
130
|
id, short: short(id), state: read("state.json"),
|
|
112
131
|
request: read("00-request.md"), plan: read("01-plan.md"), approval: read("02-approval.md"),
|
|
113
132
|
impl: read("03-impl.md"), review: read("04-review.md"), backlog: read("06-backlog.md"),
|
|
133
|
+
notes: read("notes.md"),
|
|
114
134
|
});
|
|
115
135
|
}
|
|
116
136
|
|
|
137
|
+
if (req.method === "GET" && path === "/settings") {
|
|
138
|
+
const { listSettings } = await import("./settings.ts");
|
|
139
|
+
return json(listSettings(repoDir));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (req.method === "POST" && path === "/settings") {
|
|
143
|
+
if (!local(req)) return bad("cross-origin write refused", 403);
|
|
144
|
+
let body: any;
|
|
145
|
+
try { body = await req.json(); } catch { return bad("json body expected"); }
|
|
146
|
+
const { setSetting } = await import("./settings.ts");
|
|
147
|
+
const r = setSetting(repoDir, String(body.key ?? ""), String(body.value ?? ""));
|
|
148
|
+
return "error" in r ? bad(r.error) : json(r);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Attachments only, never a run's own files: the path must sit under an
|
|
152
|
+
// attachments/ directory of a known state dir, with no way back up.
|
|
153
|
+
if (req.method === "GET" && path === "/file") {
|
|
154
|
+
const dir = url.searchParams.get("dir") ?? state;
|
|
155
|
+
const p = url.searchParams.get("p") ?? "";
|
|
156
|
+
if (!dir || !states(true).includes(dir)) return bad("unknown state dir", 404);
|
|
157
|
+
if (!/^(runs\/[\w.-]+\/)?attachments\/[\w.-]+$/.test(p)) return bad("bad path");
|
|
158
|
+
const f = join(dir, p);
|
|
159
|
+
if (!existsSync(f)) return bad("no such file", 404);
|
|
160
|
+
return new Response(Bun.file(f), { headers: { "cache-control": "private, max-age=3600" } });
|
|
161
|
+
}
|
|
162
|
+
|
|
117
163
|
if (req.method === "GET" && path === "/events") {
|
|
118
164
|
let ctrl: ReadableStreamDefaultController;
|
|
119
165
|
const stream = new ReadableStream({
|
|
120
|
-
|
|
166
|
+
// The boot id lets the page tell a server restart from a dropped
|
|
167
|
+
// connection; only the former means new code. The ping keeps Bun's
|
|
168
|
+
// idle timeout from closing the stream every ten seconds.
|
|
169
|
+
start(c) { ctrl = c; clients.add(c); c.enqueue(`data: hello ${BOOT}\n\n`); },
|
|
121
170
|
cancel() { clients.delete(ctrl); },
|
|
122
171
|
});
|
|
123
172
|
return new Response(stream, {
|
|
@@ -135,13 +184,16 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
135
184
|
|
|
136
185
|
if (path === "/inbox") {
|
|
137
186
|
const text = String(body.text ?? "").trim();
|
|
187
|
+
// Absent means untouched; an empty string clears. replaceIdea keeps the
|
|
188
|
+
// old notes for any op that does not send them.
|
|
189
|
+
const notes = typeof body.notes === "string" ? body.notes.trim() : undefined;
|
|
138
190
|
if (body.op === "add") {
|
|
139
191
|
if (!text) return bad("empty idea");
|
|
140
|
-
writeInbox(dir, [...readInbox(dir), { status: "open", text }]);
|
|
192
|
+
writeInbox(dir, [...readInbox(dir), { status: "open", text, notes }]);
|
|
141
193
|
} else if (body.op === "edit") {
|
|
142
194
|
const next = String(body.next ?? "").trim();
|
|
143
195
|
if (!text || !next) return bad("empty idea");
|
|
144
|
-
replaceIdea(dir, text, { status: "open", text: next });
|
|
196
|
+
replaceIdea(dir, text, notes === undefined ? { status: "open", text: next } : { status: "open", text: next, notes });
|
|
145
197
|
} else if (body.op === "reject") {
|
|
146
198
|
const why = String(body.reason ?? "").trim();
|
|
147
199
|
if (!why) return bad("a rejection needs a reason");
|
|
@@ -158,6 +210,7 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
158
210
|
const kind = body.kind === "full" || body.kind === "lite" ? body.kind : "";
|
|
159
211
|
if (!text) return bad("empty idea");
|
|
160
212
|
const it: Item = parseItem(`${kind} ${text}`.trim());
|
|
213
|
+
it.notes = readInbox(dir).find((l) => l.status === "open" && l.text === text)?.notes;
|
|
161
214
|
const r = launchRun(it, dir);
|
|
162
215
|
if (r.ok) replaceIdea(dir, text, { status: "started", text });
|
|
163
216
|
changed();
|
|
@@ -166,6 +219,54 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
166
219
|
: { ok: false, why: r.why, manual: r.manual, agent: r.agent }, r.ok ? 200 : 500);
|
|
167
220
|
}
|
|
168
221
|
|
|
222
|
+
// Archive is a list beside the runs, never a move: a run keeps its directory
|
|
223
|
+
// and its state, and the board simply stops showing it.
|
|
224
|
+
if (path === "/archive") {
|
|
225
|
+
const archived = body.archived !== false;
|
|
226
|
+
if (typeof body.id === "string") {
|
|
227
|
+
if (!/^[\w.-]+$/.test(body.id)) return bad("bad run id");
|
|
228
|
+
setArchived(dir, body.id, archived);
|
|
229
|
+
} else if (typeof body.text === "string") {
|
|
230
|
+
const text = body.text.trim();
|
|
231
|
+
const lines = readInbox(dir);
|
|
232
|
+
const at = lines.findIndex((l) => l.text === text && (archived ? l.status === "open" : l.status === "archived"));
|
|
233
|
+
if (at < 0) return bad("no such idea", 404);
|
|
234
|
+
lines[at] = { ...lines[at]!, status: archived ? "archived" : "open" };
|
|
235
|
+
writeInbox(dir, lines);
|
|
236
|
+
} else return bad("id or text required");
|
|
237
|
+
changed();
|
|
238
|
+
return json({ ok: true });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// One chat message: text plus files (base64), each file saved and linked
|
|
242
|
+
// from the message so the note carries its own attachments.
|
|
243
|
+
if (path === "/note") {
|
|
244
|
+
const id = typeof body.id === "string" ? body.id : null;
|
|
245
|
+
if (id !== null && !/^[\w.-]+$/.test(id)) return bad("bad run id");
|
|
246
|
+
const text = typeof body.text === "string" ? body.text.trim() : "";
|
|
247
|
+
const files: { name: string; data: string }[] = Array.isArray(body.files) ? body.files : [];
|
|
248
|
+
let total = 0;
|
|
249
|
+
for (const f of files) total += (f.data?.length ?? 0);
|
|
250
|
+
if (total > 20_000_000) return bad("attachments over 15MB");
|
|
251
|
+
const links = files.map((f) => {
|
|
252
|
+
const bytes = Buffer.from(String(f.data).replace(/^data:[^,]*,/, ""), "base64");
|
|
253
|
+
const rel = saveAttachment(dir, id, String(f.name ?? "file"), bytes).replace(/\\/g, "/");
|
|
254
|
+
return (/\.(png|jpe?g|gif|webp|svg)$/i.test(rel) ? "!" : "") + `[${String(f.name ?? "file")}](${rel.replace(/^runs\/[^/]+\//, "")})`;
|
|
255
|
+
});
|
|
256
|
+
const note = [text, ...links].filter(Boolean).join("\n");
|
|
257
|
+
const failed = appendNote(dir, id !== null ? { id } : { text: String(body.for ?? "").trim() }, note);
|
|
258
|
+
changed();
|
|
259
|
+
return failed ? bad(failed, 404) : json({ ok: true });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (path === "/notes") {
|
|
263
|
+
const id = String(body.id ?? "");
|
|
264
|
+
if (!/^[\w.-]+$/.test(id)) return bad("bad run id");
|
|
265
|
+
const failed = writeNotes(dir, id, String(body.text ?? ""));
|
|
266
|
+
changed();
|
|
267
|
+
return failed ? bad(failed, 404) : json({ ok: true });
|
|
268
|
+
}
|
|
269
|
+
|
|
169
270
|
if (path === "/abort") {
|
|
170
271
|
const id = String(body.id ?? "");
|
|
171
272
|
const why = String(body.why ?? "").trim() || "abandoned from the web board";
|
|
@@ -178,7 +279,7 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
178
279
|
|
|
179
280
|
return new Response("not found", { status: 404 });
|
|
180
281
|
},
|
|
181
|
-
});
|
|
282
|
+
}); }
|
|
182
283
|
|
|
183
284
|
const url = `http://127.0.0.1:${server.port}/`;
|
|
184
285
|
console.log(`\n kaizen web ${url}`);
|
|
@@ -187,7 +288,7 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
187
288
|
if (opts.open !== false) await openApp(url);
|
|
188
289
|
|
|
189
290
|
await new Promise<void>((resolve) => {
|
|
190
|
-
const stop = () => { clearInterval(poll); for (const w of watchers) { try { w.close(); } catch { /* gone */ } } server.stop(true); resolve(); };
|
|
291
|
+
const stop = () => { clearInterval(poll); clearInterval(ping); for (const w of watchers) { try { w.close(); } catch { /* gone */ } } server.stop(true); resolve(); };
|
|
191
292
|
process.on("SIGINT", stop);
|
|
192
293
|
process.on("SIGTERM", stop);
|
|
193
294
|
});
|
|
@@ -233,3 +334,55 @@ function winChrome(exe: string) {
|
|
|
233
334
|
for (const r of roots) { const p = join(r, sub[exe] ?? exe); if (existsSync(p)) return p; }
|
|
234
335
|
return null;
|
|
235
336
|
}
|
|
337
|
+
|
|
338
|
+
// `kaizen web --shortcut`: a launcher the OS can find — Spotlight on macOS, the app
|
|
339
|
+
// menu / super-space on Linux, the Desktop on Windows. Each one runs `kaizen web` with
|
|
340
|
+
// the bun and script that ran this command, so it works for a git checkout and a
|
|
341
|
+
// bunx install alike.
|
|
342
|
+
export async function shortcut() {
|
|
343
|
+
const bun = process.execPath;
|
|
344
|
+
const script = Bun.argv[1]!;
|
|
345
|
+
const icon = join(dirname(import.meta.dir), "assets", "kaizen-logo.png");
|
|
346
|
+
const hasIcon = existsSync(icon);
|
|
347
|
+
const { mkdirSync, writeFileSync, chmodSync } = await import("node:fs");
|
|
348
|
+
|
|
349
|
+
if (process.platform === "darwin") {
|
|
350
|
+
const app = join(home, "Applications", "Kaizen.app", "Contents");
|
|
351
|
+
mkdirSync(join(app, "MacOS"), { recursive: true });
|
|
352
|
+
writeFileSync(join(app, "MacOS", "kaizen"), `#!/bin/sh\nexec ${q(bun)} ${q(script)} web --daemon\n`);
|
|
353
|
+
chmodSync(join(app, "MacOS", "kaizen"), 0o755);
|
|
354
|
+
writeFileSync(join(app, "Info.plist"), `<?xml version="1.0" encoding="UTF-8"?>
|
|
355
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
356
|
+
<plist version="1.0"><dict>
|
|
357
|
+
<key>CFBundleName</key><string>Kaizen</string>
|
|
358
|
+
<key>CFBundleIdentifier</key><string>dev.kaizen.web</string>
|
|
359
|
+
<key>CFBundleExecutable</key><string>kaizen</string>
|
|
360
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
361
|
+
<key>LSUIElement</key><true/>
|
|
362
|
+
</dict></plist>
|
|
363
|
+
`);
|
|
364
|
+
return `${tilde(join(home, "Applications", "Kaizen.app"))} — Spotlight finds it once indexed`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (process.platform === "win32") {
|
|
368
|
+
const lnk = join(home, "Desktop", "Kaizen.lnk");
|
|
369
|
+
const ps = `$s=(New-Object -ComObject WScript.Shell).CreateShortcut('${lnk}');$s.TargetPath='${bun}';$s.Arguments='"${script}" web --daemon';$s.WorkingDirectory='${home}';${hasIcon ? `$s.IconLocation='${icon}';` : ""}$s.Save()`;
|
|
370
|
+
await Bun.$`powershell -NoProfile -Command ${ps}`.quiet();
|
|
371
|
+
return lnk;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const dir = join(process.env["XDG_DATA_HOME"] ?? join(home, ".local", "share"), "applications");
|
|
375
|
+
mkdirSync(dir, { recursive: true });
|
|
376
|
+
const file = join(dir, "kaizen.desktop");
|
|
377
|
+
writeFileSync(file, `[Desktop Entry]
|
|
378
|
+
Type=Application
|
|
379
|
+
Name=Kaizen
|
|
380
|
+
Comment=Kaizen board
|
|
381
|
+
Exec=${q(bun)} ${q(script)} web --daemon
|
|
382
|
+
${hasIcon ? `Icon=${icon}\n` : ""}Terminal=false
|
|
383
|
+
Categories=Development;
|
|
384
|
+
`);
|
|
385
|
+
return tilde(file);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const q = (s: string) => (/[\s"']/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s);
|
package/package.json
CHANGED
package/skills/kaizen/SKILL.md
CHANGED
|
@@ -61,7 +61,9 @@ If the user typed `/kaizen` with no argument and a run is in progress, treat it
|
|
|
61
61
|
`~/.kaizen/` keyed by working directory. If neither exists and the user is starting
|
|
62
62
|
work, run `/kaizen init` first: it creates `.kaizen/` with a copy of the `spec*.md`
|
|
63
63
|
files and — where the folder is a git repository — the gitignore entry
|
|
64
|
-
(see [`adapters.md`](adapters.md)).
|
|
64
|
+
(see [`adapters.md`](adapters.md)). Also append the project folder's absolute path
|
|
65
|
+
to `~/.kaizen/projects` if not already listed: the board only retires an idea when
|
|
66
|
+
it can see the run, and it only looks in projects it knows.
|
|
65
67
|
2. Read the config in this order, each file filling in the keys the one before it
|
|
66
68
|
does not set: `.kaizen/config.yml`, then `~/.kaizen/config.yml`, then
|
|
67
69
|
`config.default.yml` in this skill. A project usually has no `config.yml` of its
|
|
@@ -359,10 +361,11 @@ shown automatically.
|
|
|
359
361
|
01-fix.md
|
|
360
362
|
01-recheck.md
|
|
361
363
|
06-backlog.md # what this run deliberately did not do
|
|
364
|
+
notes.md # context attached from the board; the planner reads it
|
|
362
365
|
state.json
|
|
363
366
|
```
|
|
364
367
|
|
|
365
|
-
`state.json` is the resume point, and records the runner the run started with, so a
|
|
368
|
+
`state.json` is the resume point, and records the runner and the agent the run started with, so a
|
|
366
369
|
run picked up later keeps the shape it was reviewed under rather than silently
|
|
367
370
|
changing because a setting moved:
|
|
368
371
|
|
|
@@ -371,6 +374,7 @@ changing because a setting moved:
|
|
|
371
374
|
"id": "2026-09-02-add-oauth",
|
|
372
375
|
"mode": "approve",
|
|
373
376
|
"runner": "full",
|
|
377
|
+
"agent": "claude",
|
|
374
378
|
"stage": "review",
|
|
375
379
|
"awaiting": "approvals.review",
|
|
376
380
|
"iteration": 1,
|
|
@@ -65,6 +65,12 @@ deviations from the plan and why, verification carried out with its actual resul
|
|
|
65
65
|
and anything deliberately left undone. Report failures honestly; a false claim of
|
|
66
66
|
success poisons the review stage, which trusts this report.
|
|
67
67
|
|
|
68
|
+
**A `## Preview` section says how to see the result.** For an app or service: the
|
|
69
|
+
commands to start it locally and the URL to open, copied from the material's own README
|
|
70
|
+
where one exists. For anything else — a document, copy, a runbook, a memo — the path
|
|
71
|
+
to open, or the result itself when it fits in a screen. The board shows this section on
|
|
72
|
+
the run's card, so a person can try the work without reading the rest of the report.
|
|
73
|
+
|
|
68
74
|
**The gate block closes the stage.** `03-impl.md` ends with one line per gate item
|
|
69
75
|
from the plan's Verification section:
|
|
70
76
|
|
|
@@ -6,9 +6,11 @@ planner's and the plan approval's contract. The builder and reviewer do not read
|
|
|
6
6
|
---
|
|
7
7
|
## Stage 1 — Plan
|
|
8
8
|
|
|
9
|
-
**Agent:** planner. **Input:** `00-request.md` (including the track), `
|
|
10
|
-
the
|
|
11
|
-
|
|
9
|
+
**Agent:** planner. **Input:** `00-request.md` (including the track), `notes.md` in
|
|
10
|
+
the run directory when present — links, paths, and background the user attached from
|
|
11
|
+
the board, binding like the request itself — `memory.md`, and the existing material —
|
|
12
|
+
the codebase, the manuscript, the current campaign, whatever this run builds on.
|
|
13
|
+
**Output:** `01-plan.md`.
|
|
12
14
|
|
|
13
15
|
The request is usually one sentence. The planner's first job is to expand it — but by
|
|
14
16
|
proposing, not by interrogating. It restates the problem, brings the nearest good
|
|
@@ -23,6 +25,18 @@ different work behind each, nothing available to break the tie — and even then
|
|
|
23
25
|
its recommendation with the question. Questions the existing material can answer are
|
|
24
26
|
never asked; that is the planner's own work.
|
|
25
27
|
|
|
28
|
+
**Building from nothing is the exception: settle the foundation first.** When the
|
|
29
|
+
track is a new product or app, there is no material to answer the questions that
|
|
30
|
+
decide everything after them, and "wrong" for this track is a wrong foundation — the
|
|
31
|
+
one thing a review cannot fix, because fixing it is a rebuild. Before the steps are
|
|
32
|
+
written, the plan states a decision for each of: platform (web, mobile, PWA, native,
|
|
33
|
+
CLI), who and how many will use it, stack, how it is distributed and reached, access
|
|
34
|
+
(open, invite, accounts), what data is kept and where, and hosting. Each is either
|
|
35
|
+
read from the request, proposed with its reason, or — where two answers lead to
|
|
36
|
+
materially different builds — put to the user as an *Open question* with options,
|
|
37
|
+
all in one round, before anything is built. Three quick questions and a default for
|
|
38
|
+
the rest is how a run ends up writing the second app instead of the first.
|
|
39
|
+
|
|
26
40
|
The goals stay the user's. The planner may propose a better solution to what was
|
|
27
41
|
asked, or argue that the request as stated will not achieve what the user wants, but
|
|
28
42
|
it may not widen the goal on its own. Anything beyond the request goes in *Out of
|
package/skills/kaizen/spec.md
CHANGED
|
@@ -101,9 +101,14 @@ 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`, `runner`, `track`,
|
|
104
|
+
and what wrong means. Record in `state.json`: `id`, `mode`, `runner`, `agent`, `track`,
|
|
105
105
|
`stage: "plan"`, `awaiting: null`.
|
|
106
106
|
|
|
107
|
+
`agent` is the tool this run is happening in — `claude`, `codex`, `agy`, `opencode`,
|
|
108
|
+
`gemini`, or `cursor` — written once at intake and never changed, so a board can say
|
|
109
|
+
which agent to look for when a run is waiting. The tool running this stage knows what
|
|
110
|
+
it is; no detection is needed.
|
|
111
|
+
|
|
107
112
|
`runner` is read from config at intake and written down here, because it decides
|
|
108
113
|
whether the stages that follow are separate agents or this one. A run resumed later
|
|
109
114
|
keeps the runner it started with: a review done in the same session as the work is a
|