kaizen-agent 0.7.4 → 1.1.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/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  <p align="center"><i>Your AI agent shows you the plan before it does the work,<br>and a second agent checks what it did.</i></p>
6
6
 
7
7
  <p align="center">
8
+ <a href="https://github.com/hfadhlullah/kaizen/stargazers"><img alt="stars" src="https://img.shields.io/github/stars/hfadhlullah/kaizen?style=social"></a>
8
9
  <a href="https://www.npmjs.com/package/kaizen-agent"><img alt="npm" src="https://img.shields.io/npm/v/kaizen-agent?color=2d50a5&label=npm"></a>
9
10
  <a href="https://github.com/hfadhlullah/kaizen/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/npm/l/kaizen-agent?color=2d50a5"></a>
10
11
  <img alt="dependencies" src="https://img.shields.io/badge/dependencies-0-2d50a5">
@@ -13,6 +14,16 @@
13
14
 
14
15
  ---
15
16
 
17
+ ```bash
18
+ # 1. Install Bun (prerequisite)
19
+ curl -fsSL https://bun.sh/install | bash
20
+
21
+ # 2. Install Kaizen
22
+ bunx kaizen-agent
23
+ ```
24
+
25
+ ---
26
+
16
27
  ## The problem
17
28
 
18
29
  You ask an AI to do something real — write the launch email, draft the policy,
@@ -217,12 +228,28 @@ kaizen Done, and reviewed. Two things found:
217
228
 
218
229
  ## Install
219
230
 
231
+ ### Prerequisites: Bun
232
+
233
+ Kaizen's installer and interactive dashboard require [Bun](https://bun.sh). If you don't have Bun installed yet:
234
+
235
+ ```bash
236
+ # macOS, Linux, WSL
237
+ curl -fsSL https://bun.sh/install | bash
238
+
239
+ # or via Homebrew
240
+ brew install oven-sh/bun/bun
241
+
242
+ # or via npm
243
+ npm install -g bun
244
+ ```
245
+
246
+ ### Install Kaizen
247
+
220
248
  ```bash
221
249
  bunx kaizen-agent
222
250
  ```
223
251
 
224
- It walks you through where to install, then tells you what to type next. Works with
225
- `npx` too. Restart your editor afterwards.
252
+ It walks you through where to install, prompts for your workflow preset (`medium`, `low`, or `ultra`), then tells you what to type next. Restart your editor afterwards.
226
253
 
227
254
  Run from inside a project it asks whether to install globally or for that project
228
255
  alone; anywhere else — your home directory, a plain folder, a script — it installs
package/cli/dashboard.ts CHANGED
@@ -41,6 +41,80 @@ const WORDMARK = [
41
41
  "█ █ █ █ █ █▄▄ █▄▄ █ █",
42
42
  ];
43
43
 
44
+ const KNOWN_AGENTS = [
45
+ { name: "Claude Code", dir: ".claude", cmd: "claude" },
46
+ { name: "Antigravity", dir: ".agents", cmd: "agy" },
47
+ { name: "Codex", dir: ".codex", cmd: "codex" },
48
+ { name: "OpenCode", dir: ".opencode", cmd: "opencode" },
49
+ { name: "Gemini CLI", dir: ".gemini", cmd: "gemini" },
50
+ { name: "Cursor", dir: ".cursor", cmd: "cursor" },
51
+ ];
52
+
53
+ function detectDefaultAgent(projectDir: string): { name: string; cmd: string } {
54
+ const cfgFile = existsSync(join(projectDir, ".kaizen", "config.yml"))
55
+ ? join(projectDir, ".kaizen", "config.yml")
56
+ : join(home, ".kaizen", "config.yml");
57
+ if (existsSync(cfgFile)) {
58
+ const text = readFileSync(cfgFile, "utf8");
59
+ const m = /^\s*agent:\s*\n\s*default:\s*(\S+)/m.exec(text) || /^\s*agent\.default:\s*(\S+)/m.exec(text);
60
+ if (m && m[1] && m[1] !== "auto") {
61
+ const found = KNOWN_AGENTS.find((a) => a.cmd === m[1] || a.name.toLowerCase() === m[1].toLowerCase());
62
+ if (found && Bun.which(found.cmd)) return found;
63
+ }
64
+ }
65
+
66
+ for (const a of KNOWN_AGENTS) {
67
+ if (existsSync(join(projectDir, a.dir)) && Bun.which(a.cmd)) return a;
68
+ }
69
+ if (Bun.which("claude")) return { name: "Claude Code", cmd: "claude" };
70
+ for (const a of KNOWN_AGENTS) {
71
+ if (Bun.which(a.cmd)) return a;
72
+ }
73
+ return { name: "Claude Code", cmd: "claude" };
74
+ }
75
+
76
+ function findTerminal(cwd: string, fullCmd: string[]): { cmd: string[]; detached: boolean } | null {
77
+ const hasDisplay = Boolean(process.env.WAYLAND_DISPLAY || process.env.DISPLAY);
78
+ const inTmux = Boolean(process.env.TMUX);
79
+
80
+ if (process.env.TERMINAL && Bun.which(process.env.TERMINAL)) {
81
+ const term = process.env.TERMINAL;
82
+ if (term.includes("kitty")) return { cmd: [term, "--directory", cwd, ...fullCmd], detached: true };
83
+ if (term.includes("alacritty")) return { cmd: [term, "--working-directory", cwd, "-e", ...fullCmd], detached: true };
84
+ if (term.includes("ghostty")) return { cmd: [term, `--working-directory=${cwd}`, "-e", ...fullCmd], detached: true };
85
+ if (term.includes("foot")) return { cmd: [term, "-D", cwd, ...fullCmd], detached: true };
86
+ return { cmd: [term, "-e", ...fullCmd], detached: true };
87
+ }
88
+
89
+ if (hasDisplay && Bun.which("xdg-terminal-exec")) {
90
+ return { cmd: ["xdg-terminal-exec", `--dir=${cwd}`, "--", ...fullCmd], detached: true };
91
+ }
92
+
93
+ if (hasDisplay) {
94
+ if (Bun.which("kitty")) return { cmd: ["kitty", "--directory", cwd, ...fullCmd], detached: true };
95
+ if (Bun.which("ghostty")) return { cmd: ["ghostty", `--working-directory=${cwd}`, "-e", ...fullCmd], detached: true };
96
+ if (Bun.which("alacritty")) return { cmd: ["alacritty", "--working-directory", cwd, "-e", ...fullCmd], detached: true };
97
+ if (Bun.which("foot")) return { cmd: ["foot", "-D", cwd, ...fullCmd], detached: true };
98
+ if (Bun.which("wezterm")) return { cmd: ["wezterm", "start", "--cwd", cwd, "--", ...fullCmd], detached: true };
99
+ if (Bun.which("gnome-terminal")) return { cmd: ["gnome-terminal", `--working-directory=${cwd}`, "--", ...fullCmd], detached: true };
100
+ if (Bun.which("xfce4-terminal")) return { cmd: ["xfce4-terminal", `--default-working-directory=${cwd}`, "-x", ...fullCmd], detached: true };
101
+ if (Bun.which("konsole")) return { cmd: ["konsole", "--workdir", cwd, "-e", ...fullCmd], detached: true };
102
+ if (Bun.which("xterm")) return { cmd: ["xterm", "-e", `cd "${cwd}" && ${fullCmd.join(" ")}`], detached: true };
103
+ }
104
+
105
+ if (inTmux && Bun.which("tmux")) {
106
+ const cmdStr = fullCmd.map((a) => (a.includes(" ") ? JSON.stringify(a) : a)).join(" ");
107
+ return { cmd: ["tmux", "new-window", "-c", cwd, cmdStr], detached: false };
108
+ }
109
+
110
+ if (process.platform === "darwin") {
111
+ const cmdStr = fullCmd.map((a) => (a.includes(" ") ? `\\"${a}\\"` : a)).join(" ");
112
+ const script = `tell application "Terminal" to do script "cd \\"${cwd}\\" && ${cmdStr}"\ntell application "Terminal" to activate`;
113
+ return { cmd: ["osascript", "-e", script], detached: true };
114
+ }
115
+
116
+ return null;
117
+ }
44
118
 
45
119
  export async function dashboard(
46
120
  repo: string,
@@ -101,16 +175,19 @@ export async function dashboard(
101
175
 
102
176
  // An action that has something to say says it here, on its own screen, rather
103
177
  // than printing to a terminal the dashboard is about to paint over.
104
- async function report(lines: string[]) {
178
+ async function report(lines: string[], footer = "any key to go back"): Promise<string> {
105
179
  stdout.write("\x1b[?1049h\x1b[?25l\x1b[H\x1b[2J");
106
180
  stdout.write("\n");
107
181
  for (const line of lines) stdout.write(` ${line}\n`);
108
- stdout.write(`\n ${c.dim("any key to go back")}\n`);
182
+ stdout.write(`\n ${c.dim(footer)}\n`);
109
183
  stdin.setRawMode(true);
110
184
  stdin.resume();
185
+ let key = "";
111
186
  await new Promise<void>((resolve) => {
112
187
  const once = (chunk: Buffer) => {
113
- if (chunk.toString().includes("\x03")) process.exit(130);
188
+ const s = chunk.toString();
189
+ if (s.includes("\x03")) process.exit(130);
190
+ key = s;
114
191
  stdin.off("data", once);
115
192
  resolve();
116
193
  };
@@ -119,6 +196,7 @@ export async function dashboard(
119
196
  stdin.setRawMode(false);
120
197
  stdin.pause();
121
198
  stdout.write("\x1b[?25h\x1b[?1049l");
199
+ return key;
122
200
  }
123
201
 
124
202
  // ---------------------------------------------------------------- views
@@ -338,17 +416,84 @@ export async function dashboard(
338
416
  }
339
417
  }
340
418
  for (;;) {
341
- const { index } = await pick(from ? `Backlog — ${label(from)}` : "Backlog — open items",
342
- rows, "enter read · backspace back", (n) => full[n] === null);
419
+ const { index, key } = await pick(
420
+ from ? `Backlog — ${label(from)}` : "Backlog — open items",
421
+ rows,
422
+ "enter read · r run in agent · backspace back",
423
+ (n) => full[n] === null,
424
+ "rR",
425
+ );
343
426
  if (index === null) return;
344
427
  // Rows are cut to the window, so reading one means opening it.
345
428
  const it = full[index];
346
429
  if (!it) continue;
347
- await report([
430
+ if (key === "r" || key === "R") {
431
+ await startBacklogItem(it, from ?? state!);
432
+ continue;
433
+ }
434
+ const k = await report([
348
435
  c.bold(it.where ?? "Backlog item"),
349
436
  it.severity ? c.dim(` ${it.severity}`) : "",
350
437
  "",
351
438
  ...wrap(it.raw.replace(/`/g, ""), 2),
439
+ ], "r run in agent · any other key to go back");
440
+ if (k === "r" || k === "R") {
441
+ await startBacklogItem(it, from ?? state!);
442
+ }
443
+ }
444
+ }
445
+
446
+ async function startBacklogItem(it: Item, from: string) {
447
+ const projectDir = from === join(home, ".kaizen")
448
+ ? process.cwd()
449
+ : (from.endsWith(".kaizen") ? dirname(from) : from);
450
+ const agent = detectDefaultAgent(projectDir);
451
+ const agentBin = Bun.which(agent.cmd) ?? agent.cmd;
452
+ const prompt = it.where ? `/kaizen ${it.text} (${it.where})` : `/kaizen ${it.text}`;
453
+ const fullCmd = [agentBin, prompt];
454
+
455
+ const term = findTerminal(projectDir, fullCmd);
456
+ if (!term) {
457
+ await report([
458
+ c.bold("Could not open terminal"),
459
+ "",
460
+ c.dim("No supported terminal emulator found (xdg-terminal-exec, kitty, ghostty, alacritty, tmux)."),
461
+ "",
462
+ "Run manually:",
463
+ ` ${c.cyan(`cd "${projectDir}" && ${agent.cmd} "${prompt}"`)}`,
464
+ ]);
465
+ return;
466
+ }
467
+
468
+ try {
469
+ const proc = Bun.spawn(term.cmd, {
470
+ cwd: projectDir,
471
+ stdin: "ignore",
472
+ stdout: "ignore",
473
+ stderr: "ignore",
474
+ detached: term.detached,
475
+ });
476
+ if (term.detached) proc.unref();
477
+
478
+ await report([
479
+ c.bold("Started in new terminal"),
480
+ "",
481
+ ` ${c.cyan("Agent")} ${agent.name} (${c.bold(agent.cmd)})`,
482
+ ` ${c.cyan("Project")} ${tilde(projectDir)}`,
483
+ ` ${c.cyan("Task")} ${it.text}`,
484
+ ...(it.where ? [` ${c.cyan("File")} ${it.where}`] : []),
485
+ ...(it.severity ? [` ${c.cyan("Severity")} ${it.severity}`] : []),
486
+ "",
487
+ c.dim(`Opened in new window running: ${agent.cmd} "${prompt}"`),
488
+ ]);
489
+ } catch (err: any) {
490
+ await report([
491
+ c.bold("Failed to spawn terminal"),
492
+ "",
493
+ c.dim(err?.message ?? String(err)),
494
+ "",
495
+ "Run manually:",
496
+ ` ${c.cyan(`cd "${projectDir}" && ${agent.cmd} "${prompt}"`)}`,
352
497
  ]);
353
498
  }
354
499
  }
@@ -482,7 +627,7 @@ export async function dashboard(
482
627
  ? ` ${c.cyan("›")} ${c.bold(a.label.padEnd(20))}${c.dim(a.hint)}\n`
483
628
  : ` ${c.dim(a.label)}\n`);
484
629
  }
485
- stdout.write(`\n ${c.dim("↑↓ move · enter choose · q or backspace quit")}\n`);
630
+ stdout.write(`\n ${c.dim("↑↓ move · enter choose · q quit")} ${c.dim("⭐ github.com/hfadhlullah/kaizen")}\n`);
486
631
  };
487
632
 
488
633
  stdout.write("\x1b[?1049h\x1b[?25l");
package/cli/settings.ts CHANGED
@@ -109,6 +109,7 @@ const SETTINGS: Setting[] = [
109
109
  { key: "state.keep_runs", values: ["5", "10", "20", "50"], help: "Completed runs kept before the oldest is pruned" },
110
110
  { key: "git.auto_commit", values: ["false", "true"], help: "Commit the work when a run finishes" },
111
111
  { key: "git.branch_before_implement", values: ["true", "false"], help: "Branch before building when on the default branch" },
112
+ { key: "agent.default", values: ["auto", "claude", "codex", "agy", "opencode", "gemini"], help: "Default coding agent to launch from dashboard backlog" },
112
113
  ];
113
114
 
114
115
  const c = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "0.7.4",
3
+ "version": "1.1.0",
4
4
  "description": "Staged workflow for AI agents \u2014 code, writing, research, operations: plan, human approval, build, independent review, bounded fix loop.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -94,6 +94,7 @@ whichever the user picks.
94
94
  | `review.write_memory` | `true`, `false` | Let the reviewer append lessons to `memory.md` |
95
95
  | `git.auto_commit` | `true`, `false` | Commit the work when a run finishes |
96
96
  | `git.branch_before_implement` | `true`, `false` | Branch before building when on the default branch |
97
+ | `agent.default` | `auto`, `claude`, `codex`, `agy`, `opencode`, `gemini` | Coding agent to launch from dashboard backlog |
97
98
 
98
99
  Rules for writing the file:
99
100
 
@@ -128,3 +128,9 @@ git:
128
128
  # Create a branch before the implement stage when on the default branch.
129
129
  # Ignored when the project is not a git repository.
130
130
  branch_before_implement: true
131
+
132
+ agent:
133
+ # Default coding agent to launch from dashboard backlog (auto | claude | codex | agy | opencode | gemini).
134
+ # auto picks Claude Code if installed, otherwise the first agent found on PATH.
135
+ default: auto
136
+
@@ -47,6 +47,15 @@ change works" into "nothing else broke". A check already failing before the chan
47
47
  not this run's finding; a check that passed before and fails after is, and it stops
48
48
  the stage.
49
49
 
50
+ **Keep the working transcript small.** Everything the builder reads stays in its
51
+ context and is re-read on every request that follows, so a full test-suite dump at the
52
+ start of the stage is paid for again at every step after it. Pipe long output through
53
+ a filter that keeps the decisive lines — the failure count, the failing test names, the
54
+ first error — and read the full output only when a check actually fails. The same holds
55
+ for reading material: open the manifest's paths at the ranges the plan names, not whole
56
+ files, and never re-read a file this stage already changed. `03-impl.md` records the
57
+ result, not the transcript.
58
+
50
59
  **Read the plan's File manifest instead of searching.** The planner already located
51
60
  every path this run touches. Search the material only for what the manifest does not
52
61
  cover, and note in `03-impl.md` what was missing from it.
@@ -66,11 +66,27 @@ The planner reads code but writes none. Its plan must contain:
66
66
  item; "every new query path has a covering test" is. A tier fixes the severity
67
67
  before the reviewer has an opinion, which is what stops the same class of problem
68
68
  being `high` in one run and `medium` in the next.
69
- 7. **Risks** — what could break, what is irreversible, what needs a migration or a
69
+ 7. **Design direction, for any work with a visual surface** — a website, a landing
70
+ page, a UI, a deck, a printed piece. The direction is settled before the builder
71
+ starts, because it is the one decision that cannot be changed afterwards without
72
+ redoing the work: copy survives a re-skin, markup does not.
73
+
74
+ Look for it in the material first — a `design.md`, a design system, a brand or style
75
+ guide, an existing stylesheet with established tokens. Where one exists, the plan
76
+ names it and follows it, and asks nothing; the answer is already in the repository.
77
+
78
+ Where none exists, the direction is an *Open question* with 3-4 concrete options,
79
+ each named for what it signals rather than for its palette, so the user chooses a
80
+ positioning rather than a color. Bringing one direction and building it is the
81
+ failure this item exists to prevent: the planner has an opinion here as everywhere
82
+ else, but a visual direction chosen silently is a decision taken from the user, not
83
+ a proposal made to them.
84
+
85
+ 8. **Risks** — what could break, what is irreversible, what needs a migration or a
70
86
  backup taken first.
71
- 8. **Open questions** — assumptions that survived the interview. If this list is
87
+ 9. **Open questions** — assumptions that survived the interview. If this list is
72
88
  non-empty, the approval must ask the user about it rather than defaulting.
73
- 9. **Out of scope suggestions** — worthwhile things noticed but not asked for, each
89
+ 10. **Out of scope suggestions** — worthwhile things noticed but not asked for, each
74
90
  its own proposal. Never folded into a step.
75
91
 
76
92
  The plan is a proposal, not a decision. It never edits files, runs migrations, or
@@ -80,7 +96,7 @@ installs packages.
80
96
  fix loop reads it again per iteration, so a line written once is read five or six
81
97
  times. Sections 3 to 6 — the material to reuse, the file manifest, the steps, the
82
98
  verification — earn their length: they are what stop every later stage searching the
83
- material again, and cutting them costs more than it saves. Sections 0, 1, 7 and 9 are
99
+ material again, and cutting them costs more than it saves. Sections 0, 1, 8 and 10 are
84
100
  read once by a human at the approval and should be as short as they can be and still
85
101
  be correct: a paragraph of framing, a paragraph of goal, the risks that are real, the
86
102
  questions that are open. An alternative rejected gets a line saying what and why, not
@@ -48,6 +48,21 @@ operational work: missing prerequisites, absent rollback, steps that cannot be
48
48
  executed as written. The plan's Verification section says what was promised; the
49
49
  reviewer checks that promise was kept.
50
50
 
51
+ **Keep the working transcript small.** Everything the reviewer reads stays in its
52
+ context and is re-read on every request that follows, so the re-run suite dumped in
53
+ full at check 4 is paid for again at every check after it. Pipe long output through a
54
+ filter that keeps the decisive lines — the failure count, the failing test names, the
55
+ first error — and read the full output only where a check actually fails, which is the
56
+ only place the detail becomes a finding. The same holds for the work under review: open
57
+ the manifest's paths at the ranges the plan names, and read the diff rather than the
58
+ whole file wherever the finding would cite a changed line. `04-review.md` records the
59
+ findings, not the transcript.
60
+
61
+ This does not apply to check 3. Blast radius is the check whose whole point is what
62
+ the diff does not contain, so it reads as widely as it needs to — every caller, every
63
+ reader, whole files where that is what it takes. A cheaper blast-radius pass is not a
64
+ cheaper review, it is the check not happening.
65
+
51
66
  Each finding is one line, numbered from 1 within the review:
52
67
  `<n>. <where>: <severity>: <problem>. <fix>. [G-nn]` Where is a `path:line` for code,
53
68
  and a section, page, or step reference otherwise. Severity is one of `critical`,
@@ -94,6 +94,7 @@ Each iteration is written to `runs/<id>/05-iterations/`.
94
94
  | `state.keep_runs` | `20` | Completed runs kept before pruning oldest (rescues open backlog first) |
95
95
  | `git.auto_commit` | `false` | Never commit/push/PR unless the user explicitly asks |
96
96
  | `git.branch_before_implement` | `true` | Branch before implement stage when on the default branch |
97
+ | `agent.default` | `auto` | Coding agent to launch from dashboard backlog |
97
98
 
98
99
  `.kaizen/config.yml` overrides only the keys it sets; everything else falls back to
99
100
  this skill's `config.default.yml`.