kaizen-agent 1.16.0 → 1.17.0

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/state.test.ts CHANGED
@@ -5,6 +5,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, utimesSync
5
5
  import { join, dirname } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
7
  import { issueUrl, systemInfo, scrubHome, REPO_URL, home as realHome, trustFolder } from "./state.ts";
8
+ import * as stateModule from "./state.ts";
8
9
  import {
9
10
  readRuns, readInbox, writeInbox, replaceIdea, abandonRun, allBacklog, backlog,
10
11
  statusOf, columnOf, parseItem, startedRuns, boardCards, readArchive, setArchived,
@@ -140,6 +141,15 @@ test("approvalPrompt: approve or revise, the comment kept on one line", () => {
140
141
  expect(approvalPrompt("r", "approvals.review", "rename it")).toContain('final approval: "rename it". Record it in 02-approval.md, change the work');
141
142
  });
142
143
 
144
+ test("final approval can explicitly accept a run still awaiting findings", () => {
145
+ expect(stateModule.canApproveAwaiting("approvals.plan")).toBe(true);
146
+ expect(stateModule.canApproveAwaiting("approvals.review")).toBe(true);
147
+ expect(stateModule.canApproveAwaiting("findings")).toBe(true);
148
+ expect(stateModule.canApproveAwaiting("building")).toBe(false);
149
+ expect(approvalPrompt("r", "findings")).toContain("run as-is at the findings gate");
150
+ expect(approvalPrompt("r", "findings")).toContain("leave every remaining finding open");
151
+ });
152
+
143
153
  test("approvalPrompt: a plan's answers numbered on one line", () => {
144
154
  const p = approvalPrompt("r", "approvals.plan", undefined, ["Where? — Footer", "Chip?\n — No; keep it\tsmall"]);
145
155
  expect(p).toBe("run r: the plan is approved from the board, with answers to its open questions: 1) Where? — Footer; 2) Chip? — No; keep it small. Record them in 02-approval.md and build the plan with them.");
package/cli/state.ts CHANGED
@@ -483,6 +483,10 @@ export function fixPrompt(id: string, nums: number[]) {
483
483
  + `Leave every other finding alone, and mark the fixed ones done in its 06-backlog.md.`;
484
484
  }
485
485
 
486
+ export function canApproveAwaiting(awaiting: unknown): awaiting is "approvals.plan" | "approvals.review" | "findings" {
487
+ return awaiting === "approvals.plan" || awaiting === "approvals.review" || awaiting === "findings";
488
+ }
489
+
486
490
  // The board's answer to an approval the run is waiting on. The comment is collapsed to
487
491
  // one line: where the prompt is typed into a shell (herdr), a newline sends it early.
488
492
  // Answers are the plan's open questions answered at the approval, one per question.
@@ -490,6 +494,7 @@ export function approvalPrompt(id: string, awaiting: string, why?: string, answe
490
494
  const plan = awaiting === "approvals.plan";
491
495
  const one = (t: string) => t.replace(/\s+/g, " ").trim();
492
496
  const note = why === undefined ? undefined : one(why);
497
+ if (awaiting === "findings" && !note) return `run ${id}: the user approved the run as-is at the findings gate. Record approval in 02-approval.md, leave every remaining finding open in 06-backlog.md, do not fix or mark findings closed, and finish the run.`;
493
498
  if (plan && !note && answers?.length) return `run ${id}: the plan is approved from the board, with answers to its open questions: `
494
499
  + answers.map((a, i) => `${i + 1}) ${one(a).replace(/[.;]$/, "")}`).join("; ") + `. Record them in 02-approval.md and build the plan with them.`;
495
500
  if (!note) return plan
package/cli/web.ts CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  home, type Item, type Card, boardCards, knownProjects, remember, locate, label, tilde, searchRoots, findProjects,
7
7
  readInbox, writeInbox, replaceIdea, abandonRun, 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, nativePath, closeSessions, sweepSessions, issueUrl,
9
+ reviewFindings, pickFindings, fixPrompt, approvalPrompt, canApproveAwaiting, nativePath, closeSessions, sweepSessions, issueUrl,
10
10
  } from "./state.ts";
11
11
  import { readSources, addSource, removeSource, moveSource, gatherSource, gatherAll, gatherDirs, due } from "./sources.ts";
12
12
  import { listNotes, saveNote, renameNote, deleteNote, boardIndex, linksTo } from "./notes.ts";
@@ -612,7 +612,8 @@ export async function web(repoDir: string, opts: Opts = {}) {
612
612
  if (dir === join(home, ".kaizen")) return bad("no project to run in", 409);
613
613
  let run: any;
614
614
  try { run = JSON.parse(readText(join(dir, "runs", id, "state.json"))); } catch { return bad("no such run", 404); }
615
- if (run.awaiting !== "approvals.plan" && run.awaiting !== "approvals.review" || run.awaiting !== body.awaiting) return bad("this run is not waiting on that approval", 409);
615
+ if (!canApproveAwaiting(run.awaiting) || run.awaiting !== body.awaiting) return bad("this run is not waiting on that approval", 409);
616
+ if (run.awaiting === "findings" && body.why !== undefined) return bad("open findings can only be approved as-is or fixed individually", 409);
616
617
  if (body.why !== undefined && !String(body.why).trim()) return bad("a revise needs a comment");
617
618
  const answers = body.answers;
618
619
  if (answers !== undefined && (run.awaiting !== "approvals.plan" || body.why !== undefined || !Array.isArray(answers)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
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": {
package/web/board.html CHANGED
@@ -735,10 +735,10 @@ async function openRun(c,refresh,again){
735
735
  // One the board cannot read is said, so it is answered in the terminal instead of skipped.
736
736
  const pq=c.status==="waiting"&&c.awaiting==="approvals.plan"?planQuestions(runDoc.plan):[],qs=pq||[];
737
737
  if(c.archived)foot.innerHTML=arch;
738
- else if(c.status==="waiting"&&APPROVAL[c.awaiting]){const sent=decided.has(answer(c)),asks=!pq?" It has open questions the board cannot show: answer them in the terminal running this run, or approve and your agent asks them.":qs.length?` It asks ${qs.length===1?"one question":qs.length+" questions"}: Approve opens ${qs.length===1?"it":"them"}.`:"";
738
+ else if((c.status==="waiting"||c.awaiting==="findings")&&APPROVAL[c.awaiting]){const sent=decided.has(answer(c)),asks=!pq?" It has open questions the board cannot show: answer them in the terminal running this run, or approve and your agent asks them.":qs.length?` It asks ${qs.length===1?"one question":qs.length+" questions"}: Approve opens ${qs.length===1?"it":"them"}.`:"";
739
739
  // gitBtn's buttons restyled as menu rows.
740
740
  const more=gitBtn(c).replace(/class="btn( pri)?"/g,"")+`<button data-a="archive">Archive</button><hr><button data-a="ab" class="danger">Abandon</button>`;
741
- foot.innerHTML=`<span class="note">${sent?"Sent to your agent.":APPROVAL[c.awaiting]+asks+" Answering here opens a new agent session."}</span><details class="more"><summary class="btn ghost">More</summary><div class="menu-pop">${more}</div></details><span class="dec"><button class="btn" data-a="revise"${sent?" disabled":""}>Revise</button><button class="btn pri" data-a="approve"${sent?" disabled":""}>Approve</button></span>`}
741
+ foot.innerHTML=`<span class="note">${sent?"Sent to your agent.":APPROVAL[c.awaiting]+asks+" Answering here opens a new agent session."}</span><details class="more"><summary class="btn ghost">More</summary><div class="menu-pop">${more}</div></details><span class="dec"><button class="btn" data-a="revise"${sent?" disabled":""}>Revise</button><button class="btn pri" data-a="approve"${sent?" disabled":""}>${c.awaiting==="findings"?"Approve as-is":"Approve"}</button></span>`}
742
742
  else if(c.status==="waiting")foot.innerHTML=`<span class="note">Waiting on you: ${esc(need(c.awaiting))}. Answer in the terminal running this run.</span>${arch}<button class="btn ghost" data-a="ab">Abandon</button>`;
743
743
  else if(c.status==="done"||c.status==="abandoned")foot.innerHTML=arch;
744
744
  else foot.innerHTML=`<span class="note">${c.status==="stalled"?"No progress for a while. Resume it with <code>/kaizen run</code> in your agent, or abandon it.":"Running in your agent."}</span>${arch}<button class="btn ghost" data-a="ab">Abandon run</button>`;
@@ -749,7 +749,7 @@ async function openRun(c,refresh,again){
749
749
  // run last moved: the same approval reached again after a revise is a new question.
750
750
  // The footer's More menu is a <details>: close it on a click anywhere else.
751
751
  document.addEventListener("click",e=>{for(const d of document.querySelectorAll(".more[open]"))if(!d.contains(e.target))d.open=false});
752
- const APPROVAL={"approvals.plan":"The plan is ready. Read it in the Plan tab, then approve or send it back.","approvals.review":"The work is ready to check. Approve to finish it, or revise with what to change."},decided=new Set(),answer=c=>key(c)+":"+c.awaiting+":"+c.moved;
752
+ const APPROVAL={"approvals.plan":"The plan is ready. Read it in the Plan tab, then approve or send it back.","approvals.review":"The work is ready to check. Approve to finish it, or revise with what to change.","findings":"The review has open findings. Approve as-is to accept them and finish, or fix selected findings first."},decided=new Set(),answer=c=>key(c)+":"+c.awaiting+":"+c.moved;
753
753
  async function decide(c,b,why,answers){b.disabled=true;askNotify();const r=await post("/decide",{dir:c.state,id:c.id,awaiting:c.awaiting,...(why===undefined?{}:{why}),...(answers?{answers}:{})});
754
754
  // A failed send keeps the footer, and with it a typed comment.
755
755
  if(!r.ok){b.disabled=false;return}