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 +1 -1
- package/cli/state.test.ts +14 -0
- package/cli/state.ts +8 -1
- package/package.json +2 -1
- package/skills/kaizen/SKILL.md +3 -2
- package/skills/kaizen/spec-build.md +3 -1
- package/skills/kaizen/spec.md +1 -1
- package/web/board/cards.ts +6 -3
- package/web/board/markdown.ts +8 -5
- package/web/board.js +9 -9
- package/web/notebook.js +1 -1
- package/web/term.js +104 -21
- package/web/term.ts +13 -1
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.
|
|
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
|
}
|
package/skills/kaizen/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
-
``; the board shows them in the Preview tab.
|
|
118
|
+
``; 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>`.
|
package/skills/kaizen/spec.md
CHANGED
|
@@ -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
|
|
package/web/board/cards.ts
CHANGED
|
@@ -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
|
|
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
|
: ""
|
package/web/board/markdown.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|