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 +10 -0
- package/cli/state.ts +5 -0
- package/cli/web.ts +3 -2
- package/package.json +1 -1
- package/web/board.html +3 -3
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
|
|
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
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":""}
|
|
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}
|