kaizen-agent 1.7.9 → 1.8.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.
Files changed (47) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +694 -658
  3. package/agents/kaizen-builder.md +82 -77
  4. package/agents/kaizen-planner.md +250 -250
  5. package/agents/kaizen-reviewer.md +127 -127
  6. package/cli/board.test.ts +55 -0
  7. package/cli/dashboard.ts +1188 -1187
  8. package/cli/install.ts +76 -3
  9. package/cli/publish-gh.sh +27 -27
  10. package/cli/release.sh +45 -45
  11. package/cli/settings.ts +7 -3
  12. package/cli/sources.test.ts +494 -0
  13. package/cli/sources.ts +323 -0
  14. package/cli/state.test.ts +427 -197
  15. package/cli/state.ts +1013 -700
  16. package/cli/web.ts +188 -11
  17. package/commands/kaizen-abort.md +5 -5
  18. package/commands/kaizen-approve.md +5 -5
  19. package/commands/kaizen-auto.md +6 -6
  20. package/commands/kaizen-backlog.md +5 -5
  21. package/commands/kaizen-config.md +5 -5
  22. package/commands/kaizen-full.md +5 -5
  23. package/commands/kaizen-gather.md +5 -0
  24. package/commands/kaizen-init.md +5 -5
  25. package/commands/kaizen-install.md +5 -5
  26. package/commands/kaizen-lite.md +5 -5
  27. package/commands/kaizen-plan.md +6 -6
  28. package/commands/kaizen-reject.md +6 -6
  29. package/commands/kaizen-review.md +6 -6
  30. package/commands/kaizen-run.md +5 -5
  31. package/commands/kaizen-status.md +5 -5
  32. package/package.json +3 -1
  33. package/skills/kaizen/README.md +283 -281
  34. package/skills/kaizen/SKILL.md +334 -440
  35. package/skills/kaizen/adapters.md +84 -77
  36. package/skills/kaizen/config.default.yml +181 -167
  37. package/skills/kaizen/gather.md +50 -0
  38. package/skills/kaizen/install.md +42 -0
  39. package/skills/kaizen/settings.md +52 -0
  40. package/skills/kaizen/spec-build.md +131 -119
  41. package/skills/kaizen/spec-main.md +109 -104
  42. package/skills/kaizen/spec-plan.md +201 -201
  43. package/skills/kaizen/spec-review.md +121 -121
  44. package/skills/kaizen/spec.md +154 -153
  45. package/skills/kaizen/status.md +43 -0
  46. package/skills/kaizen-help/SKILL.md +131 -129
  47. package/web/board.html +296 -125
package/cli/web.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  // `kaizen web`: the board in a browser. A loopback HTTP server over state.ts and
2
2
  // one HTML page; the page never sees the filesystem, only JSON.
3
- import { existsSync, readFileSync, readdirSync, statSync, watch, appendFileSync, type FSWatcher } from "node:fs";
3
+ import { existsSync, readFileSync, readdirSync, statSync, watch, appendFileSync, mkdirSync, type FSWatcher } from "node:fs";
4
4
  import { join, dirname } from "node:path";
5
5
  import {
6
- home, type Item, boardCards, knownProjects, remember, locate, label, tilde,
6
+ home, type Item, type Card, boardCards, knownProjects, remember, locate, label, tilde, searchRoots, findProjects,
7
7
  readInbox, writeInbox, replaceIdea, abandonRun, launchRun, parseItem, short, setArchived, writeNotes,
8
- appendNote, saveAttachment, pidOnPort,
8
+ appendNote, saveAttachment, pidOnPort, runGit, cardsGit, readCommit, commitPush, notice, notifier, LOGO,
9
+ reviewFindings, pickFindings, fixPrompt,
9
10
  } from "./state.ts";
11
+ import { readSources, addSource, removeSource, moveSource, gatherSource, gatherAll, gatherDirs, due } from "./sources.ts";
10
12
 
11
13
  const PAGE = join(dirname(import.meta.dir), "web", "board.html");
12
14
  const DEFAULT_PORT = 7420;
@@ -25,9 +27,32 @@ export async function web(repoDir: string, opts: Opts = {}) {
25
27
  if (ev === "uncaughtException") process.exit(1);
26
28
  });
27
29
  }
28
- const state = locate();
30
+ // Not fixed for the life of the board: picking a project in the page moves here.
31
+ let state = locate();
29
32
  if (state && dirname(state) !== home) remember(dirname(state));
30
- const project = state ? label(state) : null;
33
+ let project = state ? label(state) : null;
34
+ // Folders under the "Projects folder" setting that have no .kaizen yet, offered in
35
+ // the project list so one can be chosen without a terminal. A folder that does not
36
+ // look like a project itself (no .git, no package.json) is taken for a group of
37
+ // them, and its own folders are offered too: ~/Projects/<client>/<project>.
38
+ const fresh = async () => {
39
+ const { listSettings } = await import("./settings.ts");
40
+ let root = listSettings(repoDir).rows.find((r) => r.key === "board.projects_dir")?.value ?? "";
41
+ if (root.startsWith("~")) root = home + root.slice(1);
42
+ if (!root) return [];
43
+ const dirs = (d: string) => {
44
+ try {
45
+ return readdirSync(d).filter((n) => !n.startsWith(".") && n !== "node_modules")
46
+ .map((n) => join(d, n)).filter((p) => { try { return statSync(p).isDirectory(); } catch { return false; } });
47
+ } catch { return []; }
48
+ };
49
+ // .kaizen is not a sign of one: a group folder that was chosen once has it, and
50
+ // must keep showing what is inside.
51
+ const isProject = (d: string) => [".git", "package.json"].some((m) => existsSync(join(d, m)));
52
+ // One already set up but never opened joins the known projects instead.
53
+ return dirs(root).flatMap((d) => isProject(d) ? [d] : [d, ...dirs(d)])
54
+ .filter((d) => existsSync(join(d, ".kaizen")) ? (remember(d), false) : true).sort();
55
+ };
31
56
 
32
57
  const states = (all: boolean) => (all || !state
33
58
  ? [...knownProjects().map((d) => join(d, ".kaizen")), join(home, ".kaizen")]
@@ -78,7 +103,7 @@ export async function web(repoDir: string, opts: Opts = {}) {
78
103
  const fingerprint = () => {
79
104
  let sum = 0;
80
105
  for (const dir of states(true)) {
81
- for (const f of ["inbox.md", "backlog.md", "archive.md"]) { try { sum += statSync(join(dir, f)).mtimeMs; } catch { /* absent */ } }
106
+ for (const f of ["inbox.md", "backlog.md", "archive.md", "sources.json"]) { try { sum += statSync(join(dir, f)).mtimeMs; } catch { /* absent */ } }
82
107
  const runs = join(dir, "runs");
83
108
  if (!existsSync(runs)) continue;
84
109
  for (const id of readdirSync(runs)) { try { sum += statSync(join(runs, id, "state.json")).mtimeMs; } catch { /* absent */ } }
@@ -93,6 +118,28 @@ export async function web(repoDir: string, opts: Opts = {}) {
93
118
  const fp = fingerprint(); if (fp !== last) { last = fp; changed(); }
94
119
  }, 2000);
95
120
 
121
+ // The schedule: once a minute, gather every request source that is due. It writes
122
+ // inbox.md and sources.json only, never a state.json, and starts nothing. All of
123
+ // it sits in a try: an uncaught error here would end the board.
124
+ const every = async () => {
125
+ const { listSettings } = await import("./settings.ts");
126
+ return listSettings(repoDir).rows.find((r) => r.key === "sources.every")?.value ?? "off";
127
+ };
128
+ let busy = false;
129
+ const gather = setInterval(async () => {
130
+ if (busy) return;
131
+ busy = true;
132
+ try {
133
+ const ms = (parseInt(await every()) || 0) * 3600_000; // "off" parses to nothing
134
+ if (ms) for (const dir of states(true)) {
135
+ let list;
136
+ try { list = readSources(dir); } catch { continue; } // unreadable: left as it is
137
+ for (const s of list) if (due(s, ms, Date.now()) && (await gatherSource(dir, s.url)).added) changed();
138
+ }
139
+ } catch { /* the next tick tries again */ }
140
+ busy = false;
141
+ }, 60_000);
142
+
96
143
  const BOOT = Date.now().toString(36);
97
144
  const ping = setInterval(() => { for (const c of clients) { try { c.enqueue(": ping\n\n"); } catch { clients.delete(c); } } }, 8000);
98
145
  // A launcher clicked while a board is already up should show that board, not die.
@@ -133,18 +180,34 @@ export async function web(repoDir: string, opts: Opts = {}) {
133
180
 
134
181
  if (req.method === "GET" && path === "/") {
135
182
  if (!existsSync(PAGE)) return new Response("web/board.html missing", { status: 500 });
136
- return new Response(readFileSync(PAGE), {
183
+ // The page gets the notify rule from state.ts rather than keeping its own copy.
184
+ return new Response(readFileSync(PAGE, "utf8").replace("/*notifier*/", () => notifier.toString()), {
137
185
  headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" },
138
186
  });
139
187
  }
140
188
 
189
+ // The icon on a web notification; this one file and nothing beside it.
190
+ if (req.method === "GET" && path === "/logo.png") {
191
+ if (!existsSync(LOGO)) return bad("no logo", 404);
192
+ return new Response(Bun.file(LOGO), { headers: { "content-type": "image/png", "cache-control": "max-age=86400" } });
193
+ }
194
+
141
195
  if (req.method === "GET" && path === "/state") {
142
196
  const all = url.searchParams.get("all") === "1";
197
+ // Before the project list: it registers folders that turn out to be set up.
198
+ const unset = await fresh();
143
199
  const dirs = states(all);
200
+ const cards = boardCards(dirs, Date.now());
201
+ // The git mark, for the cards whose panel offers commit and push: one status per project.
202
+ const marks = new Map<string, Record<string, string>>();
203
+ const markable = (c: Card) => c.kind === "run" && !c.archived && (c.status === "done" || c.awaiting === "approvals.review") && c.state !== join(home, ".kaizen");
204
+ for (const d of new Set(cards.filter(markable).map((c) => c.state)))
205
+ marks.set(d, cardsGit(d, cards.filter((c) => markable(c) && c.state === d).map((c) => c.id!)));
144
206
  return json({
145
- project, all,
207
+ project, all, dir: state,
146
208
  projects: states(true).map((d) => ({ dir: d, label: label(d) })),
147
- cards: boardCards(dirs, Date.now()),
209
+ fresh: unset.map((d) => ({ dir: join(d, ".kaizen"), label: tilde(d) })),
210
+ cards: cards.map((c) => ({ ...c, notice: notice(c), git: markable(c) ? marks.get(c.state)?.[c.id!] : undefined })),
148
211
  now: Date.now(),
149
212
  });
150
213
  }
@@ -161,9 +224,27 @@ export async function web(repoDir: string, opts: Opts = {}) {
161
224
  request: read("00-request.md"), plan: read("01-plan.md"), approval: read("02-approval.md"),
162
225
  impl: read("03-impl.md"), review: read("04-review.md"), backlog: read("06-backlog.md"),
163
226
  notes: read("notes.md"),
227
+ // Nothing to pick in the global state dir: /fix refuses it.
228
+ findings: dir === join(home, ".kaizen") ? [] : reviewFindings(dir, id),
229
+ // The global state dir sits in $HOME, which is not a project to commit.
230
+ commit: readCommit(dir, id), git: dir === join(home, ".kaizen") ? { repo: false } : runGit(dir, id),
164
231
  });
165
232
  }
166
233
 
234
+ if (req.method === "GET" && path === "/sources") {
235
+ // One project's sources, or with no dir every known project's: each entry
236
+ // says which project it belongs to. Nothing is assumed from where the board started.
237
+ const dir = url.searchParams.get("dir");
238
+ if (dir && !states(true).includes(dir)) return bad("unknown state dir", 404);
239
+ const sources = [], failed = [];
240
+ for (const d of dir ? [dir] : states(true)) {
241
+ // The seen list is bookkeeping, not something the page shows.
242
+ try { sources.push(...readSources(d).map(({ seen, ...s }) => ({ ...s, dir: d }))); }
243
+ catch { failed.push(label(d)); }
244
+ }
245
+ return json({ sources, failed, every: await every() });
246
+ }
247
+
167
248
  if (req.method === "GET" && path === "/settings") {
168
249
  const { listSettings } = await import("./settings.ts");
169
250
  return json(listSettings(repoDir));
@@ -208,10 +289,38 @@ export async function web(repoDir: string, opts: Opts = {}) {
208
289
  if (!local(req)) return bad("cross-origin write refused", 403);
209
290
  let body: any;
210
291
  try { body = await req.json(); } catch { return bad("json body expected"); }
292
+
293
+ // The same search as the TUI's "find more": every .kaizen under $HOME, the
294
+ // drives, and where the board was started, added to the registry.
295
+ // ponytail: synchronous walk, the board stalls for its length; move to a worker if a big disk makes that seconds.
296
+ if (path === "/find") {
297
+ const before = knownProjects().length;
298
+ const roots = searchRoots([dirname(state ?? process.cwd()), process.cwd()]);
299
+ for (const d of new Set(roots.flatMap((r) => findProjects(r)))) remember(d);
300
+ changed();
301
+ return json({ ok: true, added: knownProjects().length - before });
302
+ }
303
+
211
304
  // No project here means the global inbox, which is where the TUI puts ideas too.
212
305
  const dir: string = body.dir ?? state ?? join(home, ".kaizen");
306
+ // Choosing a folder from the projects folder is what sets it up: an empty
307
+ // .kaizen, which the first run fills in. Only folders the board offered.
308
+ if (path === "/cd" && dir === join(dirname(dir), ".kaizen") && (await fresh()).includes(dirname(dir))) {
309
+ mkdirSync(dir, { recursive: true });
310
+ remember(dirname(dir));
311
+ }
213
312
  if (!dir || !states(true).includes(dir)) return bad("unknown state dir", 404);
214
313
 
314
+ // The board moves to the chosen project: new ideas and runs default to it,
315
+ // and the process follows so anything resolved from cwd agrees.
316
+ if (path === "/cd") {
317
+ state = dir;
318
+ project = label(dir);
319
+ try { process.chdir(dirname(dir)); } catch { /* the state dir is what matters */ }
320
+ changed();
321
+ return json({ ok: true, project });
322
+ }
323
+
215
324
  if (path === "/inbox") {
216
325
  const text = String(body.text ?? "").trim();
217
326
  // Absent means untouched; an empty string clears. replaceIdea keeps the
@@ -235,6 +344,40 @@ export async function web(repoDir: string, opts: Opts = {}) {
235
344
  return json({ ok: true });
236
345
  }
237
346
 
347
+ // Request sources. Gathering adds ideas and nothing else: no run starts here.
348
+ if (path === "/sources") {
349
+ const link = String(body.url ?? "").trim();
350
+ // A source belongs to the project the request names. The fallback above
351
+ // (where the board started, or the global dir) is a guess, and a guess
352
+ // here puts someone's requests in the wrong project.
353
+ const named = typeof body.dir === "string" && body.dir !== "";
354
+ try {
355
+ let results;
356
+ if (body.op === "gather" && !named && !link) {
357
+ results = await gatherDirs(states(true));
358
+ changed();
359
+ return json({ ok: true, results });
360
+ }
361
+ if (!named) return bad(body.op === "add" ? "choose the project this source belongs to" : "say which project's source this is");
362
+ if (body.op === "add") {
363
+ const why = addSource(dir, link, typeof body.label === "string" ? body.label : undefined);
364
+ if (why) return bad(why);
365
+ } else if (body.op === "remove") {
366
+ const why = removeSource(dir, link);
367
+ if (why) return bad(why, 404);
368
+ } else if (body.op === "move") {
369
+ if (typeof body.to !== "string" || !states(true).includes(body.to)) return bad("unknown state dir", 404);
370
+ const why = moveSource(dir, body.to, link);
371
+ if (why) return bad(why, why === "no such source" ? 404 : 409);
372
+ } else if (body.op === "gather") {
373
+ if (link && !readSources(dir).some((s) => s.url === link)) return bad("no such source", 404);
374
+ results = link ? [await gatherSource(dir, link)] : await gatherAll(dir);
375
+ } else return bad("unknown op");
376
+ changed();
377
+ return json({ ok: true, sources: readSources(dir).map(({ seen, ...s }) => s), results });
378
+ } catch (e) { return bad((e as Error).message, 500); }
379
+ }
380
+
238
381
  if (path === "/run") {
239
382
  const text = String(body.text ?? "").trim();
240
383
  const kind = body.kind === "full" || body.kind === "lite" ? body.kind : "";
@@ -299,6 +442,40 @@ export async function web(repoDir: string, opts: Opts = {}) {
299
442
  return failed ? bad(failed, 404) : json({ ok: true });
300
443
  }
301
444
 
445
+ // Only a finished run, or one waiting at the final approval, and never the
446
+ // global state dir: $HOME may be a repository, and it is not the run's project.
447
+ if (path === "/commit") {
448
+ const id = String(body.id ?? "");
449
+ if (!/^[\w.-]+$/.test(id)) return bad("bad run id");
450
+ if (dir === join(home, ".kaizen")) return bad("no project to commit", 409);
451
+ let run: any;
452
+ try { run = JSON.parse(readFileSync(join(dir, "runs", id, "state.json"), "utf8")); } catch { return bad("no such run", 404); }
453
+ if (run.stage !== "done" && run.awaiting !== "approvals.review") return bad("this run is not finished", 409);
454
+ const r = await commitPush(dir, id, String(body.message ?? ""), Array.isArray(body.files) ? body.files.map(String) : []);
455
+ changed();
456
+ return json(r, r.ok ? 200 : 409);
457
+ }
458
+
459
+ // Fix picked findings, on the same runs /commit takes and for the same reason
460
+ // never in the global state dir: the agent starts in a project, and the run is
461
+ // not there. Only numbers the review itself holds reach the prompt.
462
+ if (path === "/fix") {
463
+ const id = String(body.id ?? "");
464
+ if (!/^[\w.-]+$/.test(id)) return bad("bad run id");
465
+ if (dir === join(home, ".kaizen")) return bad("no project to fix in", 409);
466
+ let run: any;
467
+ try { run = JSON.parse(readFileSync(join(dir, "runs", id, "state.json"), "utf8")); } catch { return bad("no such run", 404); }
468
+ if (run.stage !== "done" && run.awaiting !== "approvals.review" && run.awaiting !== "findings") return bad("this run is still being worked on", 409);
469
+ const nums = pickFindings(dir, id, body.nums);
470
+ if (!nums) return bad("pick open findings of this review");
471
+ const text = fixPrompt(id, nums);
472
+ const r = launchRun({ severity: null, where: null, text, raw: text }, dir);
473
+ changed();
474
+ return json(r.ok
475
+ ? { ok: true, agent: r.agent }
476
+ : { ok: false, why: r.why, manual: r.manual, agent: r.agent }, r.ok ? 200 : 500);
477
+ }
478
+
302
479
  if (path === "/abort") {
303
480
  const id = String(body.id ?? "");
304
481
  const why = String(body.why ?? "").trim() || "abandoned from the web board";
@@ -320,7 +497,7 @@ export async function web(repoDir: string, opts: Opts = {}) {
320
497
  if (opts.open !== false) await openApp(url);
321
498
 
322
499
  await new Promise<void>((resolve) => {
323
- const stop = () => { clearInterval(poll); clearInterval(ping); for (const w of watchers) { try { w.close(); } catch { /* gone */ } } server.stop(true); resolve(); };
500
+ const stop = () => { clearInterval(poll); clearInterval(ping); clearInterval(gather); for (const w of watchers) { try { w.close(); } catch { /* gone */ } } server.stop(true); resolve(); };
324
501
  process.on("SIGINT", stop);
325
502
  process.on("SIGTERM", stop);
326
503
  });
@@ -378,7 +555,7 @@ function winChrome(exe: string) {
378
555
  export async function shortcut() {
379
556
  const bun = process.execPath;
380
557
  const script = Bun.argv[1]!;
381
- const icon = join(dirname(import.meta.dir), "assets", "kaizen-logo.png");
558
+ const icon = LOGO;
382
559
  const hasIcon = existsSync(icon);
383
560
  const { mkdirSync, writeFileSync, chmodSync } = await import("node:fs");
384
561
 
@@ -1,5 +1,5 @@
1
- ---
2
- description: Mark the current kaizen run abandoned
3
- ---
4
-
5
- Invoke the `kaizen` skill with argument `abort`.
1
+ ---
2
+ description: Mark the current kaizen run abandoned
3
+ ---
4
+
5
+ Invoke the `kaizen` skill with argument `abort`.
@@ -1,5 +1,5 @@
1
- ---
2
- description: Approve whatever the current kaizen run is waiting on
3
- ---
4
-
5
- Invoke the `kaizen` skill with argument `approve`.
1
+ ---
2
+ description: Approve whatever the current kaizen run is waiting on
3
+ ---
4
+
5
+ Invoke the `kaizen` skill with argument `approve`.
@@ -1,6 +1,6 @@
1
- ---
2
- description: Start a kaizen run in auto mode — every stage back to back, no approval stops
3
- argument-hint: "<request>"
4
- ---
5
-
6
- Invoke the `kaizen` skill with argument `auto $ARGUMENTS`.
1
+ ---
2
+ description: Start a kaizen run in auto mode — every stage back to back, no approval stops
3
+ argument-hint: "<request>"
4
+ ---
5
+
6
+ Invoke the `kaizen` skill with argument `auto $ARGUMENTS`.
@@ -1,5 +1,5 @@
1
- ---
2
- description: Print open kaizen backlog items across all runs, grouped by source run
3
- ---
4
-
5
- Invoke the `kaizen` skill with argument `backlog`.
1
+ ---
2
+ description: Print open kaizen backlog items across all runs, grouped by source run
3
+ ---
4
+
5
+ Invoke the `kaizen` skill with argument `backlog`.
@@ -1,5 +1,5 @@
1
- ---
2
- description: Show the kaizen settings for this project and change them interactively
3
- ---
4
-
5
- Invoke the `kaizen` skill with argument `config`.
1
+ ---
2
+ description: Show the kaizen settings for this project and change them interactively
3
+ ---
4
+
5
+ Invoke the `kaizen` skill with argument `config`.
@@ -1,5 +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`.
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: Gather new requests from the project's request sources into kaizen ideas
3
+ ---
4
+
5
+ Invoke the `kaizen` skill with argument `gather`.
@@ -1,5 +1,5 @@
1
- ---
2
- description: Set up this project folder for kaizen (spec, config, and a gitignore entry when it is a git repo)
3
- ---
4
-
5
- Invoke the `kaizen` skill with argument `init`.
1
+ ---
2
+ description: Set up this project folder for kaizen (spec, config, and a gitignore entry when it is a git repo)
3
+ ---
4
+
5
+ Invoke the `kaizen` skill with argument `init`.
@@ -1,5 +1,5 @@
1
- ---
2
- description: Install the kaizen workflow itself, globally or per-project
3
- ---
4
-
5
- Invoke the `kaizen` skill with argument `install`.
1
+ ---
2
+ description: Install the kaizen workflow itself, globally or per-project
3
+ ---
4
+
5
+ Invoke the `kaizen` skill with argument `install`.
@@ -1,5 +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`.
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`.
@@ -1,6 +1,6 @@
1
- ---
2
- description: Start a kaizen run and stop after the plan (plan-only)
3
- argument-hint: "<request>"
4
- ---
5
-
6
- Invoke the `kaizen` skill with argument `plan $ARGUMENTS`.
1
+ ---
2
+ description: Start a kaizen run and stop after the plan (plan-only)
3
+ argument-hint: "<request>"
4
+ ---
5
+
6
+ Invoke the `kaizen` skill with argument `plan $ARGUMENTS`.
@@ -1,6 +1,6 @@
1
- ---
2
- description: Reject the kaizen approval the current run is waiting on — reason decides revise (back to planner) vs kill (abandon)
3
- argument-hint: "<reason>"
4
- ---
5
-
6
- Invoke the `kaizen` skill with argument `reject $ARGUMENTS`.
1
+ ---
2
+ description: Reject the kaizen approval the current run is waiting on — reason decides revise (back to planner) vs kill (abandon)
3
+ argument-hint: "<reason>"
4
+ ---
5
+
6
+ Invoke the `kaizen` skill with argument `reject $ARGUMENTS`.
@@ -1,6 +1,6 @@
1
- ---
2
- description: Review-only kaizen run — audit existing code/work, no plan, no implementation
3
- argument-hint: "[target]"
4
- ---
5
-
6
- Invoke the `kaizen` skill with argument `review $ARGUMENTS`.
1
+ ---
2
+ description: Review-only kaizen run — audit existing code/work, no plan, no implementation
3
+ argument-hint: "[target]"
4
+ ---
5
+
6
+ Invoke the `kaizen` skill with argument `review $ARGUMENTS`.
@@ -1,5 +1,5 @@
1
- ---
2
- description: Resume the current kaizen run and execute the approved plan
3
- ---
4
-
5
- Invoke the `kaizen` skill with argument `run`.
1
+ ---
2
+ description: Resume the current kaizen run and execute the approved plan
3
+ ---
4
+
5
+ Invoke the `kaizen` skill with argument `run`.
@@ -1,5 +1,5 @@
1
- ---
2
- description: List every kaizen run grouped waiting-on-you / in flight / done / abandoned, plus open backlog count
3
- ---
4
-
5
- Invoke the `kaizen` skill with argument `status`.
1
+ ---
2
+ description: List every kaizen run grouped waiting-on-you / in flight / done / abandoned, plus open backlog count
3
+ ---
4
+
5
+ Invoke the `kaizen` skill with argument `status`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "1.7.9",
3
+ "version": "1.8.1",
4
4
  "description": "Staged workflow for AI agents — code, writing, research, operations: plan, human approval, build, independent review, bounded fix loop.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,8 @@
21
21
  "scripts": {
22
22
  "install-links": "bun run cli/install.ts",
23
23
  "check": "bun run cli/install.ts --check",
24
+ "test": "bun test",
25
+ "prepublishOnly": "bun test",
24
26
  "postpublish": "sh cli/release.sh",
25
27
  "publish:gh": "sh cli/publish-gh.sh",
26
28
  "release": "bun publish && bun run publish:gh"