kaizen-agent 1.40.0 → 1.40.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/board.test.ts CHANGED
@@ -157,7 +157,7 @@ test("card menu items: appropriate actions and distinct icons across phases", ()
157
157
  const menuSnippet = (cards.slice(from, cards.indexOf('el.innerHTML += `<div class="menu-pop"', from)) + "}").replace("const items = [];", "items = [];");
158
158
  const a0 = cards.indexOf("const actionOf =");
159
159
  const actionOf = cards.slice(a0, cards.indexOf("\n\n", a0));
160
- const getItems = new Function("c", "archive", "S", `const ico = ${ico.toString()};\n${actionOf}\nconst store = { S, menuKey: 1 };\nlet k = 1, items = [];\n${menuSnippet}\nreturn items;`);
160
+ const getItems = new Function("c", "archive", "S", `const ico = ${ico.toString()};\nconst decided = new Set(), answer = () => "";\n${actionOf}\nconst store = { S, menuKey: 1 };\nlet k = 1, items = [];\n${menuSnippet}\nreturn items;`);
161
161
 
162
162
  // Phase 0: Ideas (idea card)
163
163
  const idea = { kind: "idea", status: "idea", column: 0 };
package/cli/state.test.ts CHANGED
@@ -145,6 +145,20 @@ test("backlog: open items only, orphanage last, count matches", () => {
145
145
  expect(backlog(state)).toBe(3);
146
146
  });
147
147
 
148
+ test("backlog: an abandoned run's items are neither listed nor counted", () => {
149
+ const dir = join(state, "runs", "2026-09-03-dropped");
150
+ mkdirSync(dir, { recursive: true });
151
+ writeFileSync(join(dir, "state.json"), JSON.stringify({ id: "2026-09-03-dropped", stage: "abandoned" }));
152
+ writeFileSync(join(dir, "06-backlog.md"), "- open: never happening\n");
153
+ expect(allBacklog(state).some((g) => g.run === "2026-09-03-dropped")).toBe(false);
154
+ expect(backlog(state)).toBe(3);
155
+ rmSync(dir, { recursive: true });
156
+ });
157
+
158
+ test("abandonRun refuses a done run", () => {
159
+ expect(abandonRun(state, "2026-09-02-done", "late")).toMatch(/already done/);
160
+ });
161
+
148
162
  test("parseItem: finding line splits into where, severity, text", () => {
149
163
  const it = parseItem("`cli/x.ts:12`: low: first thing. Fix: do it.");
150
164
  expect([it.where, it.severity, it.text]).toEqual(["x.ts:12", "low", "first thing."]);
package/cli/state.ts CHANGED
@@ -435,7 +435,7 @@ export function unrechecked(state: string, id: string) {
435
435
  export function backlog(state: string) {
436
436
  const files = [join(state, "backlog.md")];
437
437
  const runs = join(state, "runs");
438
- if (existsSync(runs)) for (const id of readdirSync(runs)) files.push(join(runs, id, "06-backlog.md"));
438
+ if (existsSync(runs)) for (const id of readdirSync(runs)) if (!abandoned(join(runs, id))) files.push(join(runs, id, "06-backlog.md"));
439
439
  let n = 0;
440
440
  for (const f of files) {
441
441
  if (!existsSync(f)) continue;
@@ -581,11 +581,17 @@ export function readBacklog(file: string) {
581
581
  .map((l) => l.replace(/^-\s*open:\s*/, ""));
582
582
  }
583
583
 
584
+ // An abandoned run's backlog is not open work (spec-main.md).
585
+ function abandoned(run: string) {
586
+ try { return JSON.parse(readText(join(run, "state.json"))).stage === "abandoned"; } catch { return false; }
587
+ }
588
+
584
589
  export function allBacklog(state: string) {
585
590
  const out: { run: string; items: string[] }[] = [];
586
591
  const runs = join(state, "runs");
587
592
  if (existsSync(runs)) {
588
593
  for (const id of readdirSync(runs).sort().reverse()) {
594
+ if (abandoned(join(runs, id))) continue;
589
595
  const items = readBacklog(join(runs, id, "06-backlog.md"));
590
596
  if (items.length) out.push({ run: id, items });
591
597
  }
@@ -880,6 +886,7 @@ export function abandonRun(st: string, id: string, why: string): string | null {
880
886
  try {
881
887
  const run = JSON.parse(readText(file));
882
888
  if (run.stage === "abandoned") return "That run is already abandoned.";
889
+ if (run.stage === "done") return "That run is already done.";
883
890
  run.stage = "abandoned";
884
891
  run.awaiting = null;
885
892
  run.updated = new Date().toISOString();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "1.40.0",
3
+ "version": "1.40.2",
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": {
@@ -55,6 +55,7 @@
55
55
  "@codemirror/view": "6.43.13",
56
56
  "@lezer/highlight": "1.2.5",
57
57
  "@xterm/addon-fit": "0.11.0",
58
+ "@xterm/addon-webgl": "0.19.0",
58
59
  "@xterm/xterm": "6.0.0",
59
60
  "prettier": "3.9.10"
60
61
  }
@@ -108,8 +108,9 @@ rather than dispatching them, then stop after the review: report the findings an
108
108
  the user choose, rather than entering the fix loop. The loop
109
109
  exists because a fix to something intricate can be worse than the defect — it is not
110
110
  worth two more stages to confirm a one-line message now reads correctly. A finding at
111
- `high` or above pulls the run back onto the full path whatever its size, since that
112
- severity is the loop's whole reason for existing.
111
+ `high` or above still enters the fix loop whatever the run's size, since that
112
+ severity is the loop's whole reason for existing — on the same runner, which is fixed
113
+ at intake.
113
114
 
114
115
  **Short work still gets a run.** A one-line change is not skipped past the stages: the card on the board moves through plan, build and review like any other. Run it with `runner: lite` and the short path above — a brief plan, the build, a short review — so the cost stays small and the run stays visible.
115
116
 
@@ -115,7 +115,9 @@ screen; a document gets one of it rendered; a CLI gets one of the terminal runni
115
115
  the real output also pasted in a code block. Only a result with no screen at all — a
116
116
  library, an API — may give command output alone. A report with no screenshot is incomplete. Save each under
117
117
  `runs/<id>/attachments/` (letters, digits, `.`, `-`, `_` only) and embed it as
118
- `![what it shows](attachments/<name>.png)`; the board shows them in the Preview tab.
118
+ `![what it shows](attachments/<name>.png)`; the board shows them in the Preview tab. A link
119
+ the user should open from the board (a video, a captions file) goes there too, as
120
+ `[label](attachments/<name>)`: the board serves nothing else, and shows any other relative link as its path.
119
121
  A headless browser does it: `playwright screenshot <url> runs/<id>/attachments/<name>.png`,
120
122
  or `chromium --headless --screenshot=<abs path>.png --window-size=1280,900 <url>` where
121
123
  playwright has no browser installed. A file (document, diff, output) opens as `file://<path>`.
@@ -143,7 +143,7 @@ the config said the day someone came back to it.
143
143
  If the request remains ambiguous enough that a plan would be guesswork, ask now. One
144
144
  round of questions here is cheaper than a rejected plan.
145
145
 
146
- **Short work is not skipped.** Even a one-line change gets `01-plan.md` and `04-review.md`, so the run moves through every stage. Keep them brief and run under `runner: lite`.
146
+ **Short work is not skipped.** Even a one-line change gets `01-plan.md` and `04-review.md`, so the run moves through every stage. Keep them brief and run under `runner: lite`; a `high`+ finding still enters the fix loop, on that same runner.
147
147
 
148
148
  **Follow-ups are runs too.** Both runners record the same artifacts: `01-plan.md`, `03-impl.md`, `04-review.md`, and a final `state.json` — `lite` and `full` differ only in who executes the stages. A message the user sends later in the same session is judged by what it asks: a question, explanation or chat is answered, no run; a request to change or produce something is building, so it goes through the flow. While the previous run has not reached `done` (including awaiting its final approval), the change is a revise of that run, recorded in `02-approval.md`. After `done` or `abandoned`, open a new run whose `00-request.md` is the user's words verbatim, then a line `Follow-up of: <previous run id>`. Never reopen a finished run. When unsure whether a message asks for a change, ask once.
149
149
 
@@ -9,7 +9,7 @@ import { brief } from "./markdown.ts";
9
9
  import { open, toggleArchive } from "./panel.ts";
10
10
  import { ideaCombo, list, newDir, plogo, short, showList, showable } from "./projects.ts";
11
11
  import { askNotify, toast } from "./toast.ts";
12
- import { resumable, resume } from "./approvals.ts";
12
+ import { answer, decided, resumable, resume } from "./approvals.ts";
13
13
 
14
14
  let filter = null;
15
15
  let query = ""; // the header filter box, matched against a card's title and request
@@ -42,8 +42,11 @@ const nested = (c) =>
42
42
  const kidsOpen = new Set();
43
43
 
44
44
  // What opening a waiting run does, in words: the card's button and its menu say the same.
45
+ // One already answered from this page says so until the agent moves the run on.
45
46
  const actionOf = (c) =>
46
- c.status !== "waiting"
47
+ decided.has(answer(c))
48
+ ? "Sent to agent"
49
+ : c.status !== "waiting"
47
50
  ? "Open"
48
51
  : { "approvals.plan": "Review plan", findings: "See findings", "approvals.review": "Review work" }[c.awaiting] || "Open";
49
52
 
@@ -114,7 +117,7 @@ function card(c) {
114
117
  const act =
115
118
  c.kind === "run" && !archive && !pick
116
119
  ? c.status === "waiting"
117
- ? `<button class="btn sm pri act" data-a="open">${actionOf(c)}</button>`
120
+ ? `<button class="btn sm${decided.has(answer(c)) ? "" : " pri"} act" data-a="open">${actionOf(c)}</button>`
118
121
  : c.status === "stalled" && resumable(c)
119
122
  ? `<button class="btn sm act" data-a="resume">Resume</button>`
120
123
  : ""
@@ -9,12 +9,15 @@ function section(impl, name) {
9
9
  }
10
10
  export const preview = (impl) => section(impl, "Preview");
11
11
  // Links to a card's attachments/ (notes, or screenshots the builder put in Preview) via /file.
12
+ // Any other relative link (../video.mp4, out/x.srt) would resolve against the board's URL
13
+ // and open "not found": the board serves attachments only, so it shows as its path instead.
12
14
  export const attach = (t, c) =>
13
- t.replace(
14
- /\]\((attachments\/[^)]+)\)/g,
15
- (_, p) =>
16
- `](/file?dir=${encodeURIComponent(c.state)}&p=${encodeURIComponent((c.kind === "run" ? `runs/${c.id}/` : "") + p)})`,
17
- );
15
+ t.replace(/(!?)\[([^\]]*)\]\(([^)\s]+)\)/g, (all, img, label, p) => {
16
+ if (/^attachments\/[^)]+$/.test(p))
17
+ return `${img}[${label}](/file?dir=${encodeURIComponent(c.state)}&p=${encodeURIComponent((c.kind === "run" ? `runs/${c.id}/` : "") + p)})`;
18
+ if (/^([a-z][\w+.-]*:|\/|#)/i.test(p)) return all;
19
+ return `${label} (\`${p}\`)`;
20
+ });
18
21
  export const commitMsg = (impl) => (section(impl, "Commit") || "").replace(/^```\w*\n|\n```$/g, "").trim();
19
22
  // A plan's Open questions as a pick list: `Q: question` lines with `- option` lines under
20
23
  // each, "(recommended)" marking the default and "multi-select" in the question allowing