kaizen-agent 1.22.0 → 1.22.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 CHANGED
@@ -49,6 +49,20 @@ Then, in any project:
49
49
 
50
50
  That's it. No init and no config.
51
51
 
52
+ kaizen needs an AI coding agent to run it, such as [Claude Code](https://docs.claude.com/en/docs/claude-code); install one first. Git is needed for the board's commit and push.
53
+
54
+ ## Use it
55
+
56
+ There are three ways in, all on the same runs:
57
+
58
+ | | How | What you do there |
59
+ |---|---|---|
60
+ | **Your agent** | `/kaizen <request>` in Claude Code, Codex, Cursor, … | Start a run and answer it in the chat. All commands are under [Commands](#commands). |
61
+ | **The board** | `kaizen web` opens it in the browser; `kaizen` in a terminal. `kaizen web --shortcut` once adds a launcher: Spotlight on macOS, the app menu on Linux, a Desktop shortcut on Windows | Every project's ideas and runs. Start a run, approve or revise a plan, answer its questions, pick findings to fix, then commit and push. |
62
+ | **Kaizen Bot** | The [desktop app](#download-the-app), or `bun start` in `bot/` | Chat with the board: ask what's waiting on you, hand teammates their asks. The app starts the board itself; from `bot/`, run `kaizen web` first. See [Kaizen Bot](#kaizen-bot). |
63
+
64
+ Settings for a project or for all of them: `kaizen settings`. New version: `kaizen upgrade`.
65
+
52
66
  ## See it
53
67
 
54
68
  ```
@@ -90,7 +90,7 @@ const fromPackage = (make: (k: string) => void, custom = false) => {
90
90
  const root = mkdtempSync(join(tmpdir(), "kz-replace-"));
91
91
  const home = join(root, "home"), pkg = join(root, "pkg");
92
92
  const k = custom ? join(root, "kaizen") : join(home, "kaizen");
93
- mkdirSync(home);
93
+ mkdirSync(join(home, ".claude"), { recursive: true }); // an agent: the installer stops on a box with none
94
94
  const src = dirname(dirname(import.meta.path));
95
95
  for (const d of ["cli", "skills", "agents", "commands", "web", "package.json"]) {
96
96
  cpSync(join(src, d), join(pkg, d), { recursive: true, filter: (p) => !p.includes("node_modules") });
@@ -129,7 +129,7 @@ it.skipIf(!Bun.which("git"))("an npm package never replaces a development checko
129
129
  it("upgrade with npm unreachable says not upgraded, never up to date", () => {
130
130
  const root = mkdtempSync(join(tmpdir(), "kz-offline-"));
131
131
  const home = join(root, "home");
132
- mkdirSync(home);
132
+ mkdirSync(join(home, ".claude"), { recursive: true }); // an agent: the installer stops on a box with none
133
133
  kaizenFiles(join(home, "kaizen"));
134
134
  const env = { PATH: process.env.PATH ?? "", HOME: home, TMPDIR: root, HTTPS_PROXY: "http://127.0.0.1:9", https_proxy: "http://127.0.0.1:9" };
135
135
  const p = Bun.spawnSync([process.execPath, script, "upgrade"], { cwd: home, env });
package/cli/notes.test.ts CHANGED
@@ -71,11 +71,11 @@ test("list: recursive, newest first, dot folders skipped", () => {
71
71
  test("list: only names the notebook can save back; a symlinked folder once, never a loop", () => {
72
72
  const n = join(state, "notes");
73
73
  saveNote(state, "ok", "x", 0);
74
- writeFileSync(join(n, "What?.md"), "x");
75
- writeFileSync(join(n, "a: b.md"), "x");
74
+ // Windows refuses these names outright, so there is nothing to list.
75
+ if (process.platform !== "win32") { writeFileSync(join(n, "What?.md"), "x"); writeFileSync(join(n, "a: b.md"), "x"); }
76
76
  mkdirSync(join(n, "1/2/3/4/5/6"), { recursive: true }); writeFileSync(join(n, "1/2/3/4/5/6/deep.md"), "x");
77
77
  mkdirSync(join(n, "trail ")); writeFileSync(join(n, "trail ", "x.md"), "x");
78
- symlinkSync("..", join(n, "up"));
78
+ symlinkSync(process.platform === "win32" ? dirname(n) : "..", join(n, "up"), "junction"); // a junction needs no admin
79
79
  expect(listNotes(state).map((x) => x.name)).toEqual(["ok"]);
80
80
  });
81
81
 
@@ -498,11 +498,12 @@ test("gather everything: each source's ideas land in its own project and no othe
498
498
  // The real server, in a scratch home on its own port. Nothing is fetched: only add, list and move.
499
499
  test("the board refuses to add a source without a project, and moves one between two", async () => {
500
500
  const homeDir = join(root, "home" + n), a = join(homeDir, "a", ".kaizen"), b = join(homeDir, "b", ".kaizen");
501
- for (const d of [a, b, join(homeDir, ".kaizen")]) mkdirSync(d, { recursive: true });
501
+ // ~/.claude: kaizen will not start on a machine with no agent, and a CI box has none.
502
+ for (const d of [a, b, join(homeDir, ".kaizen"), join(homeDir, ".claude")]) mkdirSync(d, { recursive: true });
502
503
  writeFileSync(join(homeDir, ".kaizen", "projects"), [join(homeDir, "a"), join(homeDir, "b")].join("\n") + "\n");
503
504
  const port = 20000 + Math.floor(Math.random() * 20000);
504
505
  const repo = join(import.meta.dir, "..");
505
- const proc = Bun.spawn([process.execPath, join(repo, "cli", "install.ts"), "web", "--port", String(port), "--no-open"], {
506
+ const proc = Bun.spawn([process.execPath, join(repo, "cli", "install.ts"), "web", "--port", String(port), "--no-open", "--foreground"], {
506
507
  cwd: join(homeDir, "a"), env: { ...process.env, HOME: homeDir, USERPROFILE: homeDir, KAIZEN_HOME: repo }, stdout: "ignore", stderr: "ignore",
507
508
  });
508
509
  try {
@@ -530,4 +531,4 @@ test("the board refuses to add a source without a project, and moves one between
530
531
  expect(await post({ op: "add", dir: b, url: SHEET })).toMatchObject({ status: 200 });
531
532
  expect(await post({ op: "move", dir: b, to: a, url: SHEET })).toMatchObject({ status: 409, error: "that project already has this link" });
532
533
  } finally { proc.kill(); await proc.exited; }
533
- });
534
+ }, 30_000); // a cold board start is slow on Windows
package/cli/state.test.ts CHANGED
@@ -399,6 +399,8 @@ test("commitPush: commits the run's files only, pushes, and records it; the mess
399
399
  expect(readCommit(st, "r1")).toEqual({ sha: r.sha, pushed: true });
400
400
  expect(cardsGit(st, ["r1", "r0", "nope"])).toEqual({ r1: "pushed", r0: "pushed" }); // r0: no record, but its file is in; c.txt still changed is not its business
401
401
 
402
+ // Windows has no file named b*.txt, so the rest is for everywhere else.
403
+ if (process.platform === "win32") return;
402
404
  // A push that fails leaves a record that says so.
403
405
  sh(proj, "remote", "set-url", "origin", join(root, "gone.git"));
404
406
  // A fix round's report names files too, and a name with a star in it is not a pattern.
@@ -415,7 +417,7 @@ test("commitPush: commits the run's files only, pushes, and records it; the mess
415
417
  expect(runGit(st, "r1")).toMatchObject({ files: [], ahead: 1, others: 2 });
416
418
  expect(cardsGit(st, ["r1"])).toEqual({ r1: "pending" }); // committed, not pushed
417
419
  expect(cardsGit(join(root, ".kaizen"), ["r1"])).toEqual({}); // not a repo
418
- });
420
+ }, 60_000); // dozens of git calls, ~250ms each on Windows
419
421
 
420
422
  test.skipIf(process.platform === "win32")("findTerminal: a running herdr wins, then tmux, else neither; the prompt stays one literal argument", () => {
421
423
  const bin = mkdtempSync(join(tmpdir(), "kz-bin-"));
@@ -427,11 +429,11 @@ test.skipIf(process.platform === "win32")("findTerminal: a running herdr wins, t
427
429
  for (const k of keys.slice(1)) delete process.env[k];
428
430
  const prompt = `/kaizen it's "q" $HOME \`id\`\n\nnote`;
429
431
  const full = ["/usr/bin/claude", "--model", "opus", prompt];
430
- expect(findTerminal("/p", full)?.cmd[0]).not.toMatch(/^(sh|tmux)$/); // neither installed
432
+ expect(findTerminal("/p", full)?.cmd[0] ?? "none").not.toMatch(/^(sh|tmux)$/); // neither installed (none: no terminal, as on CI)
431
433
  stub("herdr", 1); stub("tmux", 1);
432
- expect(findTerminal("/p", full)?.cmd[0]).not.toMatch(/^(sh|tmux)$/); // installed, no server
434
+ expect(findTerminal("/p", full)?.cmd[0] ?? "none").not.toMatch(/^(sh|tmux)$/); // installed, no server
433
435
  process.env.TMUX = "/tmp/x,1,0"; process.env.HERDR_ENV = "1";
434
- expect(findTerminal("/p", full)?.cmd[0]).not.toMatch(/^(sh|tmux)$/); // stale env from a server since stopped
436
+ expect(findTerminal("/p", full)?.cmd[0] ?? "none").not.toMatch(/^(sh|tmux)$/); // stale env from a server since stopped
435
437
  delete process.env.TMUX; delete process.env.HERDR_ENV; stub("tmux", 0);
436
438
  const t = findTerminal("/p", full)!; // outside it, server up
437
439
  expect(t.cmd.slice(0, 4)).toEqual(["tmux", "new-window", "-c", "/p"]);
@@ -456,8 +458,9 @@ test("nativePath on Windows reads the spellings Git Bash writes into the project
456
458
  for (const p of ["/c/Users/me/proj", "C:/Users/me/proj", "c:\\Users\\me\\proj\\", "C:\\Users\\me\\proj"])
457
459
  expect(nativePath(p)).toBe("C:\\Users\\me\\proj");
458
460
  expect(nativePath("/d")).toBe("D:\\");
461
+ Object.defineProperty(process, "platform", { value: "linux" });
462
+ expect(nativePath("/c/Users/me")).toBe("/c/Users/me"); // elsewhere a path is left alone
459
463
  } finally { Object.defineProperty(process, "platform", platform); }
460
- expect(nativePath("/c/Users/me")).toBe("/c/Users/me"); // elsewhere a path is left alone
461
464
  });
462
465
 
463
466
  test("findTerminal on Windows names cmd and PowerShell by full path, so a PATH without System32 still launches", () => {
@@ -474,7 +477,9 @@ test("findTerminal on Windows names cmd and PowerShell by full path, so a PATH w
474
477
  expect(t.cmd[6]).toBe("D:\\Win\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
475
478
  process.env.ComSpec = "E:\\cmd.exe";
476
479
  expect(findTerminal("C:\\p", ["claude"])!.cmd[0]).toBe("E:\\cmd.exe");
477
- // A running herdr: a tab, and the run typed into it as one encoded line.
480
+ // A running herdr: a tab, and the run typed into it as one encoded line. The fake
481
+ // is a shell script, which real Windows cannot run with ComSpec pointed elsewhere.
482
+ if (platform.value === "win32") return;
478
483
  writeFileSync(join(process.env.PATH, "herdr"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
479
484
  const h = findTerminal("C:\\p", ["claude", "/kaizen it's\nnote"])!;
480
485
  expect([h.cmd[0], h.cmd[1], h.cmd[2], h.detached]).toEqual(["D:\\Win\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "-NoProfile", "-EncodedCommand", false]);
@@ -815,13 +820,14 @@ test("trustFolder: marks the repo root trusted in a temp config, keeps the rest,
815
820
  mkdirSync(sub, { recursive: true }); mkdirSync(plain);
816
821
  Bun.spawnSync(["git", "init", "-q", repo]);
817
822
  const cfg = join(t, ".claude.json");
818
- writeFileSync(cfg, JSON.stringify({ keep: 1, projects: { [plain]: { x: 2 } } }));
823
+ const key = (p: string) => process.platform === "win32" ? p.replace(/\\/g, "/") : p; // Claude Code's spelling
824
+ writeFileSync(cfg, JSON.stringify({ keep: 1, projects: { [key(plain)]: { x: 2 } } }));
819
825
  trustFolder(sub, cfg);
820
826
  trustFolder(plain, cfg);
821
827
  const d = JSON.parse(readFileSync(cfg, "utf8"));
822
828
  expect(d.keep).toBe(1);
823
- expect(d.projects[realpathSync(repo)]).toEqual({ hasTrustDialogAccepted: true });
824
- expect(d.projects[plain]).toEqual({ x: 2, hasTrustDialogAccepted: true });
829
+ expect(d.projects[key(realpathSync.native(repo))]).toEqual({ hasTrustDialogAccepted: true });
830
+ expect(d.projects[key(plain)]).toEqual({ x: 2, hasTrustDialogAccepted: true });
825
831
  writeFileSync(cfg, "{bad");
826
832
  trustFolder(plain, cfg);
827
833
  expect(readFileSync(cfg, "utf8")).toBe("{bad");
package/cli/state.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // `.kaizen/` directory. No terminal, no HTTP: the TUI (`dashboard.ts`) and the web
3
3
  // board (`web.ts`) both import this and draw it their own way.
4
4
  import { existsSync, readFileSync, readdirSync, appendFileSync, writeFileSync, mkdirSync, statSync, realpathSync, rmSync, renameSync, copyFileSync } from "node:fs";
5
- import { join, dirname, win32 } from "node:path";
5
+ import { join, dirname, win32, posix } from "node:path";
6
6
  import { homedir, arch, cpus, release, totalmem, version as osVersion } from "node:os";
7
7
 
8
8
  // The real path: process.cwd() is always physical, so a raw $HOME that crosses a
@@ -730,11 +730,12 @@ export function appendNote(state: string, target: { id: string } | { text: strin
730
730
  // ponytail: no dedupe, no size accounting beyond the request cap in web.ts.
731
731
  export function saveAttachment(state: string, id: string | null, name: string, bytes: Uint8Array) {
732
732
  const safe = name.replace(/[^\w.-]+/g, "_").replace(/^\.+/, "") || "file";
733
- const rel = id ? join("runs", id, "attachments") : "attachments";
733
+ // posix: the note links it as attachments/..., and a backslash on Windows breaks that.
734
+ const rel = id ? posix.join("runs", id, "attachments") : "attachments";
734
735
  mkdirSync(join(state, rel), { recursive: true });
735
736
  const file = `${Date.now().toString(36)}-${safe}`;
736
737
  writeFileSync(join(state, rel, file), bytes);
737
- return join(rel, file);
738
+ return posix.join(rel, file);
738
739
  }
739
740
 
740
741
  // What a card is worth interrupting someone for, in the words both UIs send: a run
@@ -1133,7 +1134,9 @@ export function sessionLaunch(projectDir: string, cmd: string, base: string[], p
1133
1134
  export function trustFolder(dir: string, cfg = join(process.env.CLAUDE_CONFIG_DIR ?? homedir(), ".claude.json")) {
1134
1135
  try {
1135
1136
  const top = Bun.spawnSync(["git", "-C", dir, "rev-parse", "--show-toplevel"], { stderr: "ignore" });
1136
- const root = top.success ? top.stdout.toString().trim() || dir : dir;
1137
+ // Claude Code keys its projects with forward slashes on Windows, as git prints them.
1138
+ const found = top.success ? top.stdout.toString().trim() || dir : dir;
1139
+ const root = process.platform === "win32" ? found.replace(/\\/g, "/") : found;
1137
1140
  const d = JSON.parse(readFileSync(cfg, "utf8"));
1138
1141
  if (d.projects?.[root]?.hasTrustDialogAccepted) return;
1139
1142
  d.projects ??= {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "1.22.0",
3
+ "version": "1.22.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": {