kaizen-agent 1.42.2 → 1.43.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/agents/kaizen-reviewer.md +1 -1
  2. package/cli/backlog.test.ts +133 -0
  3. package/cli/board.test.ts +10 -4
  4. package/cli/browser.test.ts +1 -1
  5. package/cli/browser.ts +12 -7
  6. package/cli/html.test.ts +19 -0
  7. package/cli/notes.ts +2 -6
  8. package/cli/pty.ts +2 -1
  9. package/cli/settings.test.ts +38 -0
  10. package/cli/settings.ts +37 -30
  11. package/cli/state.test.ts +86 -3
  12. package/cli/state.ts +245 -45
  13. package/cli/web.ts +67 -16
  14. package/package.json +1 -1
  15. package/skills/kaizen/README.md +0 -4
  16. package/skills/kaizen/SKILL.md +4 -1
  17. package/skills/kaizen/config.default.yml +1 -50
  18. package/skills/kaizen/docs.md +3 -2
  19. package/skills/kaizen/settings.md +10 -3
  20. package/skills/kaizen/spec-build.md +3 -2
  21. package/skills/kaizen/spec-main.md +40 -5
  22. package/skills/kaizen/spec-review.md +2 -1
  23. package/skills/kaizen-help/SKILL.md +0 -4
  24. package/web/board/agent-term.ts +1 -1
  25. package/web/board/api.ts +2 -0
  26. package/web/board/approvals.ts +3 -3
  27. package/web/board/cards.ts +284 -108
  28. package/web/board/chat.ts +4 -4
  29. package/web/board/dom.ts +17 -5
  30. package/web/board/fixes.ts +6 -8
  31. package/web/board/git.ts +3 -3
  32. package/web/board/icebox.ts +52 -70
  33. package/web/board/keys.ts +9 -6
  34. package/web/board/live.ts +3 -2
  35. package/web/board/main.ts +1 -0
  36. package/web/board/panel.ts +22 -16
  37. package/web/board/projects.ts +11 -16
  38. package/web/board/settings.ts +60 -29
  39. package/web/board/store.ts +2 -0
  40. package/web/board/terms.ts +279 -0
  41. package/web/board/update.ts +7 -7
  42. package/web/board/views.ts +19 -5
  43. package/web/board.html +40 -2
  44. package/web/board.js +25 -24
  45. package/web/notebook.js +1 -1
  46. package/web/notes.html +2 -0
  47. package/web/term.js +1 -1
package/cli/web.ts CHANGED
@@ -4,10 +4,10 @@ import { existsSync, readdirSync, statSync, watch, appendFileSync, mkdirSync, cp
4
4
  import { join, dirname, resolve, sep, basename } from "node:path";
5
5
  import {
6
6
  home, type Item, type Card, boardCards, knownProjects, remember, locate, label, tilde, searchRoots, findProjects,
7
- readInbox, writeInbox, replaceIdea, moveIdea, abandonRun, launchRun, parseItem, agentChoices, projectOf, KNOWN_AGENTS, plainModel, short, setArchived, writeNotes,
7
+ readInbox, writeInbox, replaceIdea, moveIdea, abandonRun, runToIdea, launchRun, parseItem, agentChoices, projectOf, KNOWN_AGENTS, plainModel, short, setArchived, writeNotes,
8
8
  appendNote, saveAttachment, pidOnPort, readText, cmdExe, sysExe, powershellExe, runGit, cardsGit, readCommit, commitPush, notice, notifier, LOGO,
9
- reviewFindings, pickFindings, fixPrompt, approvalPrompt, canApproveAwaiting, nativePath, closeSessions, sweepSessions, issueUrl,
10
- launchMode, readSessions, ownerOf, resumePrompt, runAgent, firstSession, webHosts,
9
+ reviewFindings, pickFindings, backlogCards, backlogItem, startedBacklog, closeBacklog, markBacklogDone, fixPrompt, approvalPrompt, canApproveAwaiting, nativePath, closeSessions, sweepSessions, issueUrl,
10
+ launchMode, readSessions, ownerOf, runIndex, resumePrompt, runAgent, firstSession, webHosts,
11
11
  } from "./state.ts";
12
12
  import { attachPty, writePty, resizePty, ptyState, ptyKeys, adoptLoginEnv } from "./pty.ts";
13
13
  import { readSources, addSource, removeSource, moveSource, setCategory, setWriteback, readIcebox, promote, dismiss, restore, addItem, githubRepo, markDone, markReplied, gatherSource, freshImages, gatherAll, gatherDirs, due, socialUrl, socialOf } from "./sources.ts";
@@ -190,17 +190,27 @@ export async function web(repoDir: string, opts: Opts = {}) {
190
190
  const inCard = (dir: string) => ({ card: launchMode(projectOf(dir)) === "card", onChange: changed });
191
191
  // An idea's session, until its run exists and owns it (ownerOf).
192
192
  const ideaPty = new Map<string, string>();
193
+ // Backlog items this board launched whose run has not written its state.json yet:
194
+ // shown as starting, like a started idea. Forgotten on restart; the card then offers Run again.
195
+ const backlogLaunched = new Set<string>();
193
196
  // The card's newest in-card session, live or recently ended. Sessions are only read
194
197
  // while this board has any, so /state costs nothing more for terminal launches.
195
- const ptyOf = (c: Card) => {
198
+ // Owners are worked out once per /state: per card it re-read every session and every
199
+ // run's state.json, about a second on a board of 400 runs.
200
+ const ptyOf = (cards: Card[]) => {
196
201
  const live = new Set(ptyKeys());
197
- if (!live.size) return undefined;
198
- const key = c.kind === "run"
199
- ? readSessions(c.state).filter((s) => live.has(s.key) && ownerOf(c.state, s) === c.id).pop()?.key
200
- : ideaPty.get(`${c.state}\0${c.text}`);
201
- if (!key || !live.has(key)) return undefined;
202
- const st = ptyState(key)!;
203
- return { key, alive: st.alive, asking: st.asking };
202
+ if (!live.size) return () => undefined;
203
+ const runs = new Map<string, string>();
204
+ for (const d of new Set(cards.filter((c) => c.kind === "run").map((c) => c.state))) {
205
+ const ids = runIndex(d);
206
+ for (const s of readSessions(d)) if (live.has(s.key)) runs.set(`${d}\0${ownerOf(d, s, ids)}`, s.key); // oldest first: the newest wins
207
+ }
208
+ return (c: Card) => {
209
+ const key = c.kind === "run" ? runs.get(`${c.state}\0${c.id}`) : ideaPty.get(`${c.state}\0${c.text}`);
210
+ if (!key || !live.has(key)) return undefined;
211
+ const st = ptyState(key)!;
212
+ return { key, alive: st.alive, asking: st.asking };
213
+ };
204
214
  };
205
215
  // Dev: the page reloads itself when board.html or its bundle is saved. Server code needs
206
216
  // `bun --watch cli/install.ts web`; the page reconnects and reloads on hello.
@@ -271,7 +281,7 @@ export async function web(repoDir: string, opts: Opts = {}) {
271
281
  // Done requests are reported back to their sources here, on the board's own clock: a
272
282
  // page load only reads, so opening the Icebox never closes an issue.
273
283
  const loop = setInterval(async () => {
274
- for (const dir of states(true)) { try { markDone(dir); await writeBacks(dir); } catch { /* the next tick tries again */ } }
284
+ for (const dir of states(true)) { try { markDone(dir); markBacklogDone(dir); await writeBacks(dir); } catch { /* the next tick tries again */ } }
275
285
  }, 60_000);
276
286
 
277
287
  const BOOT = Date.now().toString(36);
@@ -443,7 +453,7 @@ export async function web(repoDir: string, opts: Opts = {}) {
443
453
  return !root || d === state || (fold(p).startsWith(inside)
444
454
  && (!isGroup(p) || filled(d) || fold(dirname(p)) !== fold(root)));
445
455
  });
446
- const cards = boardCards(dirs, Date.now());
456
+ const cards = boardCards(dirs, Date.now()), pty = ptyOf(cards);
447
457
  // The git mark, for the cards whose panel offers commit and push: one status per project.
448
458
  const marks = new Map<string, Record<string, string>>();
449
459
  const markable = (c: Card) => c.kind === "run" && !c.archived && (c.status === "done" || c.awaiting === "approvals.review") && c.state !== join(home, ".kaizen");
@@ -456,7 +466,8 @@ export async function web(repoDir: string, opts: Opts = {}) {
456
466
  project, all, dir: state,
457
467
  projects: picker.map((d) => ({ dir: d, label: label(d), logo: !!projectLogo(d) })),
458
468
  fresh: unset.map((d) => ({ dir: join(d, ".kaizen"), label: tilde(d) })),
459
- cards: cards.map((c) => ({ ...c, notice: notice(c), git: markable(c) ? marks.get(c.state)?.[c.id!] : undefined, open: openFindings(c), pty: ptyOf(c) })),
469
+ cards: cards.map((c) => ({ ...c, notice: notice(c), git: markable(c) ? marks.get(c.state)?.[c.id!] : undefined, open: openFindings(c), pty: pty(c) })),
470
+ backlog: backlogCards(dirs).map((b) => ({ ...b, starting: backlogLaunched.has(`${b.state}\0${b.source}#${b.n}`) || undefined })),
460
471
  now: Date.now(),
461
472
  });
462
473
  }
@@ -667,7 +678,8 @@ export async function web(repoDir: string, opts: Opts = {}) {
667
678
  if (!/^(runs\/[\w.-]+\/)?attachments\/[\w.-]+$/.test(p)) return bad("bad path");
668
679
  const f = join(dir, p);
669
680
  if (!existsSync(f)) return bad("no such file", 404);
670
- return new Response(Bun.file(f), { headers: { "cache-control": "private, max-age=3600" } });
681
+ // no-cache: a revise retakes a screenshot under the same name, and the browser must ask again.
682
+ return new Response(Bun.file(f), { headers: { "cache-control": "private, no-cache" } });
671
683
  }
672
684
 
673
685
  // An icebox image, sent on to a link read fresh from its source: the one kept
@@ -781,6 +793,18 @@ export async function web(repoDir: string, opts: Opts = {}) {
781
793
  return json({ ok: true, project: label(dir) });
782
794
  }
783
795
 
796
+ // "Not doing this" on a backlog card: the line is closed in place with the reason,
797
+ // never removed, so a notebook link to it keeps its number.
798
+ if (path === "/backlog") {
799
+ if (body.op !== "reject") return bad("unknown op");
800
+ const why = String(body.reason ?? "").trim();
801
+ if (!why) return bad("a rejection needs a reason");
802
+ const err = closeBacklog(dir, String(body.source ?? ""), Number(body.n), "rejected", why);
803
+ if (err) return bad(err, err === "that item is already closed" ? 409 : 404);
804
+ changed();
805
+ return json({ ok: true });
806
+ }
807
+
784
808
  if (path === "/inbox") {
785
809
  const text = String(body.text ?? "").trim();
786
810
  // Absent means untouched; an empty string clears. replaceIdea keeps the
@@ -922,11 +946,30 @@ export async function web(repoDir: string, opts: Opts = {}) {
922
946
  if (path === "/run") {
923
947
  const text = String(body.text ?? "").trim();
924
948
  const kind = body.kind === "full" || body.kind === "lite" ? body.kind : "";
925
- if (!text) return bad("empty idea");
949
+ if (!text && !body.backlog) return bad("empty idea");
926
950
  const agent = typeof body.agent === "string" && body.agent ? body.agent : undefined;
927
951
  if (agent && !KNOWN_AGENTS.some((a) => a.cmd === agent)) return bad("unknown agent");
928
952
  const model = typeof body.model === "string" ? body.model.trim() : "";
929
953
  if (model && !plainModel(model)) return bad("A model name is letters, digits and . : / @ _ - only");
954
+ // A backlog item runs as an idea would, its text read here from the file, never
955
+ // from the page. The request names its source, which is how the board finds the
956
+ // line again: to hide the card while the run lives, and to close it when done.
957
+ if (body.backlog) {
958
+ const source = String(body.backlog.source ?? ""), n = Number(body.backlog.n);
959
+ const line = backlogItem(dir, source, n);
960
+ if (!line) return bad("no such backlog item", 404);
961
+ if (line.status !== "open") return bad("that item is already closed", 409);
962
+ if ((startedBacklog(dir).get(`${source}#${n}`) ?? []).some((c) => c.stage !== "abandoned")) return bad("a run was already started from that item", 409);
963
+ const p = parseItem(line.text);
964
+ const it: Item = { ...p, text: kind ? `${kind} ${p.text}` : p.text };
965
+ it.notes = [source !== "orphan" ? `Parent: ${source}` : "", `Backlog: ${source}#${n}`, p.text !== line.text ? `Backlog item: ${line.text}` : ""].filter(Boolean).join("\n");
966
+ const r = launchRun(it, dir, undefined, { agent, model: model || undefined, yolo: body.yolo === true, ...inCard(dir) });
967
+ if (r.ok) backlogLaunched.add(`${dir}\0${source}#${n}`);
968
+ changed();
969
+ return json(r.ok
970
+ ? { ok: true, agent: r.agent, project: tilde(r.projectDir), pty: r.pty }
971
+ : { ok: false, why: r.why, manual: r.manual, agent: r.agent }, r.ok ? 200 : 500);
972
+ }
930
973
  const it: Item = parseItem(`${kind} ${text}`.trim());
931
974
  // A `started` idea may be run again: the terminal the first launch opened
932
975
  // can come up empty, and nothing else tells the board a run never began.
@@ -1097,6 +1140,14 @@ export async function web(repoDir: string, opts: Opts = {}) {
1097
1140
  : { ok: false, why: r.why, manual: r.manual, agent: r.agent }, r.ok ? 200 : 500);
1098
1141
  }
1099
1142
 
1143
+ if (path === "/to-idea") {
1144
+ const id = String(body.id ?? "");
1145
+ if (!/^[\w.-]+$/.test(id)) return bad("bad run id");
1146
+ const failed = runToIdea(dir, id);
1147
+ changed();
1148
+ return failed ? bad(failed, 409) : json({ ok: true });
1149
+ }
1150
+
1100
1151
  if (path === "/abort") {
1101
1152
  const id = String(body.id ?? "");
1102
1153
  const why = String(body.why ?? "").trim() || "abandoned from the web board";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "1.42.2",
3
+ "version": "1.43.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": {
@@ -207,11 +207,7 @@ the defaults.
207
207
  | `auto_fix.max_iterations` | `2` | Fix/recheck rounds before whatever is left is escalated to you |
208
208
  | `review.memory_max_lines` | `100` | The cap on `memory.md`, which every stage of every run reads. Raise only if the lessons are genuinely all load-bearing |
209
209
  | `git.auto_commit` | `false` | `true` commits the files a run changed when it is done, by path. Never pushes |
210
- | `track.default` | `auto` (inferred) | A repo that is always one kind of work, e.g. a docs repo |
211
210
  | `backlog.enabled` | `true` | Set `false` to skip both backlog capture points; the run still executes, just without a `06-backlog.md` |
212
- | `backlog.file` | `06-backlog.md` | The per-run file name. Rarely. |
213
- | `backlog.statuses` | `[open, done, rejected]` | Rarely. Only `open` items are printed |
214
- | `backlog.orphan_file` | `backlog.md` | Where open items go when their run is pruned |
215
211
  | `state.keep_runs` | `20` | You want a longer or shorter run history |
216
212
 
217
213
  Mode never overrides a refusal: destructive or irreversible actions still get confirmed
@@ -276,7 +276,10 @@ After a subagent returns, update `state.json` yourself in the main thread. Subag
276
276
  report; the main thread owns the state machine.
277
277
  Before moving any run from `build` to `review`, under either runner, apply the
278
278
  *Preview gate* in [`spec-main.md`](spec-main.md): no `## Preview` with a screenshot
279
- (or a one-line reason there is none) means the build is not done.
279
+ (or a one-line reason there is none) means the build is not done. Before the final
280
+ report and before `stage: "done"`, under either runner and in every mode, apply the
281
+ *Docs gate* there too: the project's docs vault is brought up to date with what the
282
+ run changed.
280
283
 
281
284
  **Name files, do not narrate them.** The prompt says which paths to read, which
282
285
  decisions bind the stage, and what to report back. It does not summarize those files:
@@ -58,45 +58,7 @@ auto_fix:
58
58
  # knob in the file. A third round rarely closes what two could not.
59
59
  max_iterations: 2
60
60
 
61
- track:
62
- # The kind of work a run is doing, which decides what "deliverable",
63
- # "verification", and "finding" mean for every stage. It is INFERRED from the
64
- # request by the planner and stated back in the plan's Framing for the user to
65
- # correct at the approval — the user is never asked to categorize their own request.
66
- # Set this only to pin a default for a repo that is always one kind of work
67
- # (e.g. a docs repo). auto (or empty) infers per run.
68
- default: auto
69
-
70
61
  review:
71
- # Checks that run in every track.
72
- checks:
73
- - correctness
74
- - plan_conformance
75
- # Additional checks per track, chosen by what the run actually is. The planner may
76
- # name a track not listed here; the reviewer then derives its checks from the
77
- # track's "what would make it wrong".
78
- # Each check below is one question the reviewer asks; the line beside it says which.
79
- track_checks:
80
- # security - no new way in, no secret or user data exposed
81
- # reuse_simplification - nothing rebuilt that the codebase already had
82
- # test_coverage - the changed behaviour has a check that fails when it breaks
83
- software: [security, reuse_simplification, test_coverage]
84
- # blast_radius and regression are universal — see spec-review.md — not listed here.
85
- # completeness - every part the piece promises is actually present
86
- # claim_accuracy - every checkable claim traced to a source
87
- # reader_usability - its intended reader can act on it without asking
88
- writing: [completeness, claim_accuracy, reader_usability]
89
- # audience_fit - written for the people who will actually read it
90
- # voice_consistency - reads as the same voice as the surrounding material
91
- communication: [claim_accuracy, audience_fit, voice_consistency]
92
- # prerequisites - everything needed before step one is stated
93
- # rollback - a way back exists for every step that changes something
94
- # executability - someone else can run it start to finish unaided
95
- operations: [prerequisites, rollback, executability]
96
- # evidence_support - the conclusion follows from evidence shown, not asserted
97
- # selection_bias - the evidence was not picked to fit the answer
98
- # alternatives - the readings that were rejected are named and why
99
- research: [evidence_support, selection_bias, alternatives]
100
62
  # Let the reviewer append durable lessons to .kaizen/memory.md.
101
63
  write_memory: true
102
64
  # Every stage of every future run reads memory.md, so cap it. Over this, the reviewer
@@ -108,20 +70,12 @@ backlog:
108
70
  # not take, findings below the fix threshold, and debts deferred at an approval.
109
71
  # Written by the main thread at the plan approval and again at the final approval.
110
72
  enabled: true
111
- # Per run, beside its plan and review — provenance without a central file to sync.
112
- file: 06-backlog.md
113
- # Items are open | done | rejected. Only open ones are printed by /kaizen backlog;
114
- # closed ones stay in their run's file as record.
115
- statuses: [open, done, rejected]
116
- # Before state.keep_runs prunes a run, its still-open items move here, relative to
117
- # state.dir. Done and rejected items are pruned with the run.
118
- orphan_file: backlog.md
119
73
 
120
74
  state:
121
75
  dir: .kaizen
122
76
  # Keep this many completed runs before pruning the oldest. `kaizen prune` lists
123
77
  # what would go and `kaizen prune --yes` deletes it, after rescuing open backlog
124
- # items into backlog.orphan_file, so it never destroys open work.
78
+ # items into backlog.md beside memory.md, so it never destroys open work.
125
79
  keep_runs: 20
126
80
 
127
81
  git:
@@ -130,9 +84,6 @@ git:
130
84
  # pushes. false: never commit, push, or open a PR unless the user explicitly asks.
131
85
  # Ignored when the project is not a git repository.
132
86
  auto_commit: false
133
- # Create a branch before the implement stage when on the default branch.
134
- # Ignored when the project is not a git repository.
135
- branch_before_implement: true
136
87
  # Build each run on its own branch (kaizen/<run-id>) in its own git worktree under
137
88
  # .kaizen/wt/, so runs and people working at the same time never share a folder. Done
138
89
  # lands the branch by pull request (gh) or a rebase and push. Ignored when the
@@ -1,8 +1,9 @@
1
1
  # Kaizen — docs
2
2
 
3
3
  Read for `/kaizen docs` and `/kaizen docs lint`. Nothing else needs this file, except
4
- the two paragraphs in `spec-build.md` and `spec-review.md` that keep the vault current
5
- during a run.
4
+ the paragraphs in `spec-build.md` and `spec-review.md` that keep the vault current
5
+ during a run, and the *Docs gate* in `spec-main.md`, which reads *Update* step 4 and
6
+ *Lint* before a run is done.
6
7
 
7
8
  `/kaizen docs` writes a project's documentation into the notebook as a small vault an
8
9
  agent reads before it plans or builds: an **Index** listing every note with one line on
@@ -23,17 +23,24 @@ nearest first, so a change lands globally unless this project keeps its own conf
23
23
  | `auto_fix.max_iterations` | `1`–`5` | Fix and recheck rounds before what is left is escalated |
24
24
  | `review.write_memory` | `true`, `false` | Let the reviewer append lessons to `memory.md` |
25
25
  | `git.auto_commit` | `true`, `false` | Commit the work when a run finishes |
26
- | `git.branch_before_implement` | `true`, `false` | Branch before building when on the default branch |
27
26
  | `git.worktree_per_run` | `true`, `false` | Build each run on its own branch in its own worktree and land it by pull request |
28
27
  | `ui.mouse` | `false`, `true` | Click to select, click again to act; while on, the terminal cannot select text |
29
28
  | `subagents.model` | `inherit`, `opus`, `sonnet`, `haiku` | Model the stage subagents run on (Claude Code only) |
30
29
  | `subagents.effort` | `inherit`, `low`, `medium`, `high` | Reasoning effort for the stage subagents (Claude Code only) |
31
30
  | `sources.every` | `off`, `1h`, `6h`, `24h` | How often the web board checks request sources for new requests, into the icebox, while it runs |
32
- | `sources.agent` | `ask`, `chatgpt`, `agent` | Who reads social sources when Gather is pressed: the ChatGPT plan signed in from the Icebox, or your agent on `/kaizen gather` |
31
+ | `sources.agent` | `ask`, `chatgpt`, `agent` | Who picks the posts that fit from social sources when Gather is pressed: the ChatGPT plan signed in from the Icebox, or your agent on `/kaizen gather` |
33
32
  | `sources.model` | text | Model the ChatGPT plan gathers with; empty takes the first it lists |
34
33
  | `sources.effort` | `low`, `medium`, `high` | Reasoning effort of that model while gathering |
35
34
  | `agent.default` | `auto`, `claude`, `codex`, `agy`, `opencode`, `gemini` | Coding agent to launch from dashboard backlog |
36
35
 
36
+ Some settings only have an effect while another stands a certain way: the approvals
37
+ while `mode` is `approve`, `build.executor` and `subagents.*` while `runner` is `full`,
38
+ `auto_fix.min_severity` and `auto_fix.max_iterations` while `auto_fix.enabled` is
39
+ `true`, `sources.model` and `sources.effort` while `sources.agent` is `chatgpt`. When
40
+ printing one of these that has no effect now, say so beside its value. Still offer it:
41
+ a value set now applies once the other setting changes, and `/kaizen full` overrides
42
+ the runner for one run.
43
+
37
44
  Rules for writing the file:
38
45
 
39
46
  - **Change the value in place.** `config.yml` ships with a comment above every key
@@ -46,7 +53,7 @@ Rules for writing the file:
46
53
  - After writing, print the setting and its new value, and nothing else. No summary of
47
54
  the whole file.
48
55
 
49
- Settings not in the table — the review checks, backlog and state paths, and the per-tool
56
+ Settings not in the table — such as `state.dir` and the per-tool
50
57
  `agent.<cmd>.model` / `agent.<cmd>.effort` the board launches a session with — are edited by
51
58
  hand in `config.yml`, which is commented throughout. Say so rather than offering a
52
59
  picker with thirty options.
@@ -86,8 +86,9 @@ the note, correct what the change made untrue or left out — never the `## Note
86
86
  section, which is the user's — and set its `updated` to today. Refresh the note's line
87
87
  in `Index.md` if its `description` changed, and append one line to `Log.md`:
88
88
  `- YYYY-MM-DD run <id> — <notes touched>`. Name the notes touched in `03-impl.md`. A
89
- change no note covers is not written up here: say "suggest `/kaizen docs`" in
90
- `03-impl.md` and leave the vault alone. No Index, nothing to do.
89
+ change no note covers is not written up here: list it in `03-impl.md` as "no note
90
+ covers `<path>`" — the main thread's *Docs gate* in `spec-main.md` documents it before
91
+ the run is done. No Index, nothing to do.
91
92
 
92
93
  **Read the plan's File manifest instead of searching.** The planner already located
93
94
  every path this run touches. Search the material only for what the manifest does not
@@ -97,13 +97,48 @@ tab from staying empty; the reviewer flagging it later is a second net, not the
97
97
 
98
98
  ---
99
99
 
100
+ ## Docs gate
101
+
102
+ Before a run can be `done` — in every runner and every mode, `auto` included — bring
103
+ the project's docs vault up to date with what the run changed. Apply it once the review
104
+ is settled and before the final approval report, so the user approves the notes with
105
+ the work; where the user's answer sends findings back to be fixed, apply it again over
106
+ those fixes before setting `stage: "done"`. In a run with a `worktree`, edit the notes
107
+ in the worktree, where the vault is committed with the code. Where
108
+ `notes/Project/Index.md` does not exist in the state directory, there is no vault and
109
+ nothing to do. The builder already updates the notes it can see during the build; this
110
+ is the step that catches what it could not: fix iterations after it, and changes no
111
+ note covers.
112
+
113
+ 1. **What changed:** every path named in `03-impl.md` and in each
114
+ `05-iterations/*-fix.md`, leaving out `.kaizen/` itself.
115
+ 2. **Covered:** for each note whose `sources` include one of those paths (a folder in
116
+ `sources` covers everything under it), check the note against the changed files.
117
+ Where the change made it untrue or left something out, correct it as *Keep the
118
+ project's docs vault current* in [`spec-build.md`](spec-build.md) says — never the
119
+ `## Notes` section — set `updated` to today, and append the `Log.md` line, unless the
120
+ builder already wrote this run's line, in which case add the notes to it.
121
+ 3. **Not covered:** a changed path no note's `sources` reach. Add it to the `sources`
122
+ of the note whose area it belongs to and describe it there; where it belongs to no
123
+ area, write a note for it as step 4 of *Update* in [`docs.md`](docs.md) says and
124
+ list it in `Index.md`. A path that is not part of the project's material — a lock
125
+ file, a generated bundle — needs no note.
126
+ 4. **Lint** the notes touched, as *Lint* in [`docs.md`](docs.md) lists, and fix what
127
+ this run broke.
128
+
129
+ Name the notes touched in the final report; the commit at done includes them. A run
130
+ whose changes leave every note true touches nothing and says so in one line.
131
+
132
+ ---
133
+
100
134
  ## Final approval
101
135
 
102
136
  Controlled by `approvals.review`, skipped in `auto`.
103
137
 
104
138
  Report: what was built (for a code run, show its Preview proof), verification results, findings fixed, findings still open,
105
139
  every criterion the reviewer answered `CANNOT VERIFY` with the check it says a human
106
- must make, and anything the builder noted as out of scope. Where the run used `runner: lite`, say
140
+ must make, anything the builder noted as out of scope, and the notes the *Docs gate*
141
+ touched. Where the run used `runner: lite`, say
107
142
  so in that report — the reviewer saw the work, and nobody should read a same-session
108
143
  review as an independent one because the output looked the same.
109
144
  Later build requests in the same session follow *Follow-ups are runs too* in [`spec.md`](spec.md): a revise of this run until it is `done`, then a new linked run.
@@ -133,10 +168,10 @@ Set `stage: "done"` in `state.json` once the user accepts.
133
168
 
134
169
  **Commit at done, when `git.auto_commit` is `true`.** Setting `stage: "done"` — the
135
170
  user approving at review, or an `auto` run finishing — is then the ask to commit, under
136
- either runner. Commit only the files this run changed (the paths `03-impl.md` names,
137
- that `git status` shows changed), by path: `git add -A -- <paths>` then
138
- `git commit -m <message> -- <paths>`, never the whole tree, since other sessions edit
139
- the same checkout. The message is `03-impl.md`'s `## Commit`. In a run with a
171
+ either runner. Commit only the files this run changed (the paths `03-impl.md` names
172
+ and the notes the *Docs gate* touched, that `git status` shows changed), by path:
173
+ `git add -A -- <paths>` then `git commit -m <message> -- <paths>`, never the whole
174
+ tree, since other sessions edit the same checkout. The message is `03-impl.md`'s `## Commit`. In a run with a
140
175
  `worktree`, commit there. Then write `runs/<id>/07-commit.md` as `commit: <short sha>`
141
176
  and `pushed: no`, so the board shows it committed and offers the push. Never push. A
142
177
  failed commit is reported as printed; the run is still `done`. With the setting
@@ -125,7 +125,8 @@ and no criterion is `CANNOT VERIFY`; otherwise
125
125
  `; <n> cannot verify` when any criterion was — even when every count is 0, so an open
126
126
  human check never reads as a clean pass.
127
127
 
128
- The reviewer also appends any durable lesson about this material to `memory.md` —
128
+ Where `review.write_memory` is true (the default), the reviewer also appends any
129
+ durable lesson about this material to `memory.md` —
129
130
  a recurring bug pattern, a non-obvious constraint, a convention worth keeping. Not
130
131
  run-specific detail; only what the next run would want to know.
131
132
 
@@ -88,15 +88,11 @@ Each iteration is written to `runs/<id>/05-iterations/`.
88
88
  | `auto_fix.enabled` | `true` | Auto-fix findings vs. escalate everything |
89
89
  | `auto_fix.min_severity` | `high` | Threshold for auto-fix (`critical\|high\|medium\|low`) |
90
90
  | `auto_fix.max_iterations` | `2` | Fix/recheck rounds before escalating remaining findings |
91
- | `track.default` | `auto` (inferred per run) | Pin a track for a repo that is always one kind of work |
92
91
  | `review.write_memory` | `true` | Let the reviewer append lessons to `.kaizen/memory.md` |
93
92
  | `backlog.enabled` | `true` | Capture what a run deliberately did not do |
94
- | `backlog.file` | `06-backlog.md` | Per-run backlog file |
95
- | `backlog.orphan_file` | `backlog.md` | Where a pruned run's open items are rescued to |
96
93
  | `state.dir` | `.kaizen` | State directory |
97
94
  | `state.keep_runs` | `20` | Completed runs kept before pruning oldest (rescues open backlog first) |
98
95
  | `git.auto_commit` | `false` | `true`: commit the run's changed files when it is done; never pushes |
99
- | `git.branch_before_implement` | `true` | Branch before implement stage when on the default branch |
100
96
  | `subagents.model` | `inherit` | Model the stage subagents run on (opus/sonnet/haiku, Claude Code only) |
101
97
  | `subagents.effort` | `inherit` | Reasoning effort for the stage subagents (low/medium/high, Claude Code only) |
102
98
  | `sources.every` | `24h` | How often the web board checks request sources for new ideas (`off\|1h\|6h\|24h`) |
@@ -11,7 +11,7 @@ let termKey = null,
11
11
  termOff = null,
12
12
  termLib = null;
13
13
  export const where = (r) => (r.pty ? "running in its card" : "opened in your terminal");
14
- const termLoad = () =>
14
+ export const termLoad = () =>
15
15
  (termLib ||= new Promise((ok, no) => {
16
16
  const l = document.createElement("link");
17
17
  l.rel = "stylesheet";
package/web/board/api.ts CHANGED
@@ -3,6 +3,7 @@ import { key, render } from "./cards.ts";
3
3
  import { $ } from "./dom.ts";
4
4
  import { close, ideaBody, open, openRun } from "./panel.ts";
5
5
  import { notifyUser, toast } from "./toast.ts";
6
+ import { openTerms, termsShowing } from "./terms.ts";
6
7
 
7
8
  // The server writes notifier() from state.ts over a marker in board.html: one rule for both UIs.
8
9
  declare const due: (key: string, say: string | null, done: boolean, now: number) => boolean;
@@ -36,6 +37,7 @@ export async function load(quiet) {
36
37
  }
37
38
  // A redraw under a form being typed in would reset its fields and its focus.
38
39
  if (!(quiet && store.mode && $("board").querySelector(".inl input"))) render();
40
+ if (termsShowing()) openTerms();
39
41
  // Run on an idea closed the panel: open the card on its agent once the board knows the session.
40
42
  if (store.wantTerm && !store.openKey && !store.mode) {
41
43
  const n = store.S.cards.find((x) => key(x) === store.wantTerm && x.pty);
@@ -2,7 +2,7 @@ import { store } from "./store.ts";
2
2
  import { where } from "./agent-term.ts";
3
3
  import { post } from "./api.ts";
4
4
  import { key, via } from "./cards.ts";
5
- import { $, esc } from "./dom.ts";
5
+ import { $, html } from "./dom.ts";
6
6
  import { openRun } from "./panel.ts";
7
7
  import { askNotify, toast } from "./toast.ts";
8
8
 
@@ -57,7 +57,7 @@ export async function decide(c, b, why, answers) {
57
57
  export function confirmRevise(c) {
58
58
  const foot = $("pfoot"),
59
59
  plan = c.awaiting === "approvals.plan";
60
- foot.innerHTML = `<div class="inl" style="flex:1"><div class="q">${plan ? "What should the plan do differently?" : "What should change in the work?"}</div><div class="q dim">Opens your agent on this run with your comment.${plan ? " It revises the plan and comes back here for approval." : " It changes the work, rechecks it and comes back here."}</div><textarea aria-label="Revise comment"></textarea><div class="row"><button class="btn ghost" data-a="c">Cancel</button><button class="btn pri" data-a="s">Send back</button></div></div>`;
60
+ foot.innerHTML = html`<div class="inl" style="flex:1"><div class="q">${plan ? "What should the plan do differently?" : "What should change in the work?"}</div><div class="q dim">Opens your agent on this run with your comment.${plan ? " It revises the plan and comes back here for approval." : " It changes the work, rechecks it and comes back here."}</div><textarea aria-label="Revise comment"></textarea><div class="row"><button class="btn ghost" data-a="c">Cancel</button><button class="btn pri" data-a="s">Send back</button></div></div>`;
61
61
  const inp = foot.querySelector("textarea");
62
62
  inp.focus();
63
63
  foot.querySelector("[data-a=c]").onclick = () => {
@@ -78,7 +78,7 @@ export function confirmRevise(c) {
78
78
  // a note for what the options miss. Each answer reaches the agent as one line.
79
79
  export function confirmAnswers(c, qs) {
80
80
  const foot = $("pfoot");
81
- foot.innerHTML = `<div class="inl qa" style="flex:1"><div class="q">The plan asks ${qs.length === 1 ? "one question" : qs.length + " questions"}.</div><div class="q dim">Your answers go to your agent with the approval. It records them and builds the plan.</div>${qs.map((x, i) => `<fieldset><legend>${esc(x.q)}</legend>${x.options.map((o, j) => `<label><input type="${x.multi ? "checkbox" : "radio"}" name="q${i}" value="${j}"${o.rec || (!x.multi && !j && !x.options.some((p) => p.rec)) ? " checked" : ""}> ${esc(o.text)}</label>`).join("")}<input type="text" data-n="${i}" placeholder="Note (optional)" aria-label="Note: ${esc(x.q)}"></fieldset>`).join("")}<div class="row"><button class="btn ghost" data-a="c">Cancel</button><button class="btn pri" data-a="s">Approve with these answers</button></div></div>`;
81
+ foot.innerHTML = html`<div class="inl qa" style="flex:1"><div class="q">The plan asks ${qs.length === 1 ? "one question" : qs.length + " questions"}.</div><div class="q dim">Your answers go to your agent with the approval. It records them and builds the plan.</div>${qs.map((x, i) => html`<fieldset><legend>${x.q}</legend>${x.options.map((o, j) => html`<label><input type="${x.multi ? "checkbox" : "radio"}" name="q${i}" value="${j}"${o.rec || (!x.multi && !j && !x.options.some((p) => p.rec)) ? " checked" : ""}> ${o.text}</label>`)}<input type="text" data-n="${i}" placeholder="Note (optional)" aria-label="Note: ${x.q}"></fieldset>`)}<div class="row"><button class="btn ghost" data-a="c">Cancel</button><button class="btn pri" data-a="s">Approve with these answers</button></div></div>`;
82
82
  foot.querySelector("[data-a=c]").onclick = () => {
83
83
  foot.innerHTML = "";
84
84
  openRun(c, true);