kaizen-agent 1.10.0 → 1.11.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.
package/cli/web.ts CHANGED
@@ -4,9 +4,9 @@ import { existsSync, readdirSync, statSync, watch, appendFileSync, mkdirSync, ty
4
4
  import { join, dirname, resolve, sep } 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, abandonRun, launchRun, parseItem, short, setArchived, writeNotes,
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,
9
+ reviewFindings, pickFindings, fixPrompt, approvalPrompt, 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";
@@ -315,6 +315,19 @@ export async function web(repoDir: string, opts: Opts = {}) {
315
315
  return json({ sources, failed, every: await every() });
316
316
  }
317
317
 
318
+ // The Run form's choices for one project: installed tools and their models.
319
+ // A prefilled GitHub bug form. A redirect, so a plain link opens it with no script
320
+ // and no popup blocker in the way.
321
+ if (req.method === "GET" && path === "/issue") {
322
+ return Response.redirect(issueUrl({ title: url.searchParams.get("title") ?? "", what: url.searchParams.get("what") ?? "" }), 302);
323
+ }
324
+
325
+ if (req.method === "GET" && path === "/agents") {
326
+ const dir = url.searchParams.get("dir");
327
+ if (!dir || !states(true).includes(dir)) return bad("unknown state dir", 404);
328
+ return json(agentChoices(projectOf(dir)));
329
+ }
330
+
318
331
  if (req.method === "GET" && path === "/settings") {
319
332
  const { listSettings } = await import("./settings.ts");
320
333
  return json(listSettings(repoDir));
@@ -329,6 +342,28 @@ export async function web(repoDir: string, opts: Opts = {}) {
329
342
  return "error" in r ? bad(r.error) : json(r);
330
343
  }
331
344
 
345
+ // A page cannot learn a real path from a file input, so the board, which runs
346
+ // on the user's machine, opens the system's own folder dialog and answers it.
347
+ if (req.method === "POST" && path === "/pick-folder") {
348
+ if (!local(req)) return bad("cross-origin write refused", 403);
349
+ if (picking) return bad("a folder dialog is already open", 409);
350
+ const cmd = folderDialog();
351
+ if (!cmd) return bad("No folder dialog found: install zenity or kdialog, or type the path");
352
+ picking = true;
353
+ try {
354
+ const p = Bun.spawn(cmd, { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
355
+ const [out, err, code] = await Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text(), p.exited]);
356
+ // osascript ends a folder with "/"; a bare root (/, C:\) keeps its own.
357
+ const picked = /^([A-Za-z]:)?[\\/]$/.test(out.trim()) ? out.trim() : out.trim().replace(/[\\/]+$/, "");
358
+ if (code === 0 && picked) return json({ path: picked });
359
+ // Cancel: exit 1 with nothing said (zenity, kdialog), -128 (osascript), or exit 0 with no path (PowerShell).
360
+ if (code === 0 || !err.trim() || err.includes("-128")) return json({ cancelled: true });
361
+ return bad(err.trim().split("\n")[0]);
362
+ } catch (e) {
363
+ return bad(e instanceof Error ? e.message : String(e));
364
+ } finally { picking = false; }
365
+ }
366
+
332
367
  // Attachments only, never a run's own files: the path must sit under an
333
368
  // attachments/ directory of a known state dir, with no way back up.
334
369
  if (req.method === "GET" && path === "/file") {
@@ -452,11 +487,15 @@ export async function web(repoDir: string, opts: Opts = {}) {
452
487
  const text = String(body.text ?? "").trim();
453
488
  const kind = body.kind === "full" || body.kind === "lite" ? body.kind : "";
454
489
  if (!text) return bad("empty idea");
490
+ const agent = typeof body.agent === "string" && body.agent ? body.agent : undefined;
491
+ if (agent && !KNOWN_AGENTS.some((a) => a.cmd === agent)) return bad("unknown agent");
492
+ const model = typeof body.model === "string" ? body.model.trim() : "";
493
+ if (model && !plainModel(model)) return bad("A model name is letters, digits and . : / @ _ - only");
455
494
  const it: Item = parseItem(`${kind} ${text}`.trim());
456
495
  // A `started` idea may be run again: the terminal the first launch opened
457
496
  // can come up empty, and nothing else tells the board a run never began.
458
497
  it.notes = readInbox(dir).find((l) => (l.status === "open" || l.status === "started") && l.text === text)?.notes;
459
- const r = launchRun(it, dir);
498
+ const r = launchRun(it, dir, undefined, { agent, model: model || undefined, yolo: body.yolo === true });
460
499
  if (r.ok) replaceIdea(dir, text, { status: "started", text });
461
500
  changed();
462
501
  return json(r.ok
@@ -651,6 +690,29 @@ export async function openApp(url: string) {
651
690
  }
652
691
 
653
692
  // Windows keeps browsers out of PATH; the usual install roots are the next best guess.
693
+ // One dialog at a time: a second click while one is open would stack another.
694
+ let picking = false;
695
+
696
+ // The system folder picker for this platform, printing the chosen path on stdout.
697
+ // macOS and Windows always have one; on Linux it is whichever desktop tool exists.
698
+ export function folderDialog(platform: string = process.platform): string[] | null {
699
+ const prompt = "Choose your projects folder";
700
+ // osascript is a background process: without `activate` its dialog can open behind
701
+ // the browser. Plain `activate` targets osascript itself, so no Automation prompt.
702
+ if (platform === "darwin") return ["osascript", "-e", "activate", "-e", `POSIX path of (choose folder with prompt "${prompt}")`];
703
+ if (platform === "win32") {
704
+ // An owner form kept on top, or the dialog opens behind the browser.
705
+ const ps = `[Console]::OutputEncoding=[Text.Encoding]::UTF8;Add-Type -AssemblyName System.Windows.Forms;`
706
+ + `$o=New-Object System.Windows.Forms.Form -Property @{TopMost=$true};$d=New-Object System.Windows.Forms.FolderBrowserDialog;`
707
+ + `$d.Description='${prompt}';if($d.ShowDialog($o) -eq 'OK'){[Console]::Out.Write($d.SelectedPath)}`;
708
+ return [powershellExe(), "-NoProfile", "-STA", "-Command", ps];
709
+ }
710
+ const zenity = Bun.which("zenity", { PATH: process.env.PATH });
711
+ if (zenity) return [zenity, "--file-selection", "--directory", `--title=${prompt}`];
712
+ const kdialog = Bun.which("kdialog", { PATH: process.env.PATH });
713
+ return kdialog ? [kdialog, "--getexistingdirectory", home, "--title", prompt] : null;
714
+ }
715
+
654
716
  function winChrome(exe: string) {
655
717
  const roots = [process.env["ProgramFiles"], process.env["ProgramFiles(x86)"], process.env["LOCALAPPDATA"]].filter(Boolean) as string[];
656
718
  const sub: Record<string, string> = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "1.10.0",
3
+ "version": "1.11.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": {
@@ -62,7 +62,10 @@ If the user typed `/kaizen` with no argument and a run is in progress, treat it
62
62
  ## Before doing anything
63
63
 
64
64
  1. Locate the state directory: `.kaizen/` in the project folder's root, else
65
- `~/.kaizen/` keyed by working directory. If neither exists and the user is starting
65
+ `~/.kaizen/` keyed by working directory. The project folder is the working
66
+ directory this session started in — never the folder this skill was read from, nor
67
+ the repository it lives in, even when that has a `.kaizen/` of its own. A run
68
+ written there is invisible to the board the user launched it from. If neither exists and the user is starting
66
69
  work, run `/kaizen init` first: it creates `.kaizen/` with a copy of the `spec*.md`
67
70
  files and — where the folder is a git repository — the gitignore entry
68
71
  (see [`adapters.md`](adapters.md)). Also append the project folder's absolute path
@@ -105,6 +108,17 @@ worth two more stages to confirm a one-line message now reads correctly. A findi
105
108
  `high` or above pulls the run back onto the full path whatever its size, since that
106
109
  severity is the loop's whole reason for existing.
107
110
 
111
+ **Trivial changes take the direct path.** Where the request is one obvious change with
112
+ nothing to decide — a typo, a label, a one-line copy or config tweak — a plan is longer
113
+ than the work and a review re-reads what the user is about to look at anyway. Write
114
+ `00-request.md`, build straight away, write `03-impl.md`, and stop at
115
+ `stage: "review"`, `awaiting: "approvals.review"`: the user checks the result and
116
+ approves it or says what to change. No `01-plan.md`, no plan approval, no
117
+ `04-review.md` — the user's check is the review. Never the direct path for money, data,
118
+ a migration, security, anything hard to undo, or a request with an open question; in
119
+ `plan-only`, `auto`, or `/kaizen full`; or when in doubt. Work that turns out bigger
120
+ mid-build stops and goes back to a plan.
121
+
108
122
  This is a judgement, not a threshold to enforce: fifty lines that change how money is
109
123
  rounded are not a small change. Say which path you took and why, in one line, when the
110
124
  run starts.
@@ -9,6 +9,11 @@ builder's contract. The planner and reviewer do not read it.
9
9
  **Agent:** builder. **Input:** `01-plan.md`, `02-approval.md`, the track.
10
10
  **Output:** the deliverable itself, plus `03-impl.md`.
11
11
 
12
+ On the direct path (see `spec.md`, Stage 0) there is no plan: the input is
13
+ `00-request.md`, and the request is the scope. If the work turns out to need a
14
+ decision, more than the one obvious change, or anything the direct path excludes,
15
+ stop and send the run back to `stage: "plan"`.
16
+
12
17
  The builder produces whatever the track's deliverable is — code, a chapter, the
13
18
  campaign copy, the runbook — and nothing else.
14
19
 
@@ -84,12 +84,15 @@ subagents at all.
84
84
 
85
85
  ## Final approval
86
86
 
87
- Controlled by `approvals.review`, skipped in `auto`.
87
+ Controlled by `approvals.review`, skipped in `auto`. A direct-path run always stops here,
88
+ whatever `approvals.review` says: the user's check is its only review.
88
89
 
89
90
  Report: what was built, verification results, findings fixed, findings still open,
90
91
  and anything the builder noted as out of scope. Where the run used `runner: lite`, say
91
92
  so in that report — the reviewer saw the work, and nobody should read a same-session
92
- review as an independent one because the output looked the same. Then stop. Committing, pushing, or
93
+ review as an independent one because the output looked the same. Where the run took
94
+ the direct path, say there was no plan and no review: the user's check is the review.
95
+ Then stop. Committing, pushing, or
93
96
  opening a pull request happens only if the user asks.
94
97
 
95
98
  **Open findings are offered by number.** List them with the reviewer's numbering
@@ -5,7 +5,8 @@ follow this document; only the way stages are dispatched differs per tool. If an
5
5
  adapter and this file disagree, this file wins.
6
6
 
7
7
  A stage may only start when the previous stage's artifact exists on disk and the approval
8
- between them, if enabled, has been resolved.
8
+ between them, if enabled, has been resolved. The one exception is the direct path
9
+ (see *Stage 0*), which skips the plan and the review on purpose.
9
10
 
10
11
  ---
11
12
 
@@ -108,7 +109,10 @@ write, refreshed on every later one.
108
109
  `stage` takes exactly one of five words, and boards key their columns on them:
109
110
  `plan`, `build`, `review`, `done`, `abandoned`. Set it when the stage starts —
110
111
  `build` before the builder touches anything, `review` before the reviewer reads — as
111
- its own write, not tacked onto a longer command that may not finish. Any other word
112
+ its own write, not tacked onto a longer command that may not finish. Fixing findings
113
+ is building and a recheck is reviewing, whoever runs them and whether the loop or the
114
+ user picked the findings: `build` with `iteration` raised and `awaiting: null` before
115
+ each iteration's first edit, `review` before its recheck reads. Any other word
112
116
  (`implement`, `fix`, `building`) is unknown to every reader and shows as still planning.
113
117
 
114
118
  `awaiting` is `null` or the config key of the approval being waited on —
@@ -131,6 +135,16 @@ the config said the day someone came back to it.
131
135
  If the request remains ambiguous enough that a plan would be guesswork, ask now. One
132
136
  round of questions here is cheaper than a rejected plan.
133
137
 
138
+ **The direct path.** Where the request is one obvious change with nothing to decide —
139
+ a typo, a label, a one-line copy or config tweak — and being wrong is cheap and
140
+ visible, intake may skip the plan and the review: say so in one line, set
141
+ `stage: "build"`, build against `00-request.md`, write `03-impl.md`, then set
142
+ `stage: "review"`, `awaiting: "approvals.review"` and stop for the user to check the
143
+ result. No `01-plan.md` or `04-review.md` is written, and no `02-approval.md` unless
144
+ the user revises at that stop. Never for money, data, a migration, security, anything
145
+ hard to undo, or a request with an open question; never in `plan-only` or `auto`
146
+ mode or under `runner: full`; and when in doubt, not.
147
+
134
148
  ---
135
149
 
136
150
  ## Stage files