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/README.md +65 -572
- package/cli/board.test.ts +67 -0
- package/cli/dashboard.ts +1 -0
- package/cli/install.ts +14 -0
- package/cli/settings.ts +2 -2
- package/cli/state.test.ts +112 -1
- package/cli/state.ts +158 -41
- package/cli/web.ts +65 -3
- package/package.json +1 -1
- package/skills/kaizen/SKILL.md +15 -1
- package/skills/kaizen/spec-build.md +5 -0
- package/skills/kaizen/spec-main.md +5 -2
- package/skills/kaizen/spec.md +16 -2
- package/web/board.html +162 -47
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
package/skills/kaizen/SKILL.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
package/skills/kaizen/spec.md
CHANGED
|
@@ -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.
|
|
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
|