kaizen-agent 1.14.0 → 1.16.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 CHANGED
@@ -1,10 +1,10 @@
1
1
  // State module against a throwaway .kaizen/. Nothing here spawns a terminal: the
2
2
  // launch path is covered only up to the command it would run.
3
3
  import { test, expect, beforeAll, afterAll } from "bun:test";
4
- import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, utimesSync, existsSync, readdirSync } from "node:fs";
4
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, utimesSync, existsSync, readdirSync, realpathSync } from "node:fs";
5
5
  import { join, dirname } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
- import { issueUrl, systemInfo, scrubHome, REPO_URL, home as realHome } from "./state.ts";
7
+ import { issueUrl, systemInfo, scrubHome, REPO_URL, home as realHome, trustFolder } from "./state.ts";
8
8
  import {
9
9
  readRuns, readInbox, writeInbox, replaceIdea, abandonRun, allBacklog, backlog,
10
10
  statusOf, columnOf, parseItem, startedRuns, boardCards, readArchive, setArchived,
@@ -798,3 +798,22 @@ test("scrubHome hides a Windows home however the error spells it", () => {
798
798
  // Case matters off Windows: /home/Ann is not /home/ann.
799
799
  expect(scrubHome("/home/ann/a /home/Ann/b", ["/home/ann"], false)).toBe("~/a /home/Ann/b");
800
800
  });
801
+
802
+ test("trustFolder: marks the repo root trusted in a temp config, keeps the rest, survives a bad file", () => {
803
+ const t = mkdtempSync(join(tmpdir(), "kz-trust-"));
804
+ const repo = join(t, "repo"), sub = join(repo, "sub"), plain = join(t, "plain");
805
+ mkdirSync(sub, { recursive: true }); mkdirSync(plain);
806
+ Bun.spawnSync(["git", "init", "-q", repo]);
807
+ const cfg = join(t, ".claude.json");
808
+ writeFileSync(cfg, JSON.stringify({ keep: 1, projects: { [plain]: { x: 2 } } }));
809
+ trustFolder(sub, cfg);
810
+ trustFolder(plain, cfg);
811
+ const d = JSON.parse(readFileSync(cfg, "utf8"));
812
+ expect(d.keep).toBe(1);
813
+ expect(d.projects[realpathSync(repo)]).toEqual({ hasTrustDialogAccepted: true });
814
+ expect(d.projects[plain]).toEqual({ x: 2, hasTrustDialogAccepted: true });
815
+ writeFileSync(cfg, "{bad");
816
+ trustFolder(plain, cfg);
817
+ expect(readFileSync(cfg, "utf8")).toBe("{bad");
818
+ rmSync(t, { recursive: true, force: true });
819
+ });
package/cli/state.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Everything kaizen knows about a project's state, read from and written to the
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
- import { existsSync, readFileSync, readdirSync, appendFileSync, writeFileSync, mkdirSync, statSync, realpathSync, rmSync } from "node:fs";
4
+ import { existsSync, readFileSync, readdirSync, appendFileSync, writeFileSync, mkdirSync, statSync, realpathSync, rmSync, renameSync } from "node:fs";
5
5
  import { join, dirname, win32 } from "node:path";
6
6
  import { homedir, arch, cpus, release, totalmem, version as osVersion } from "node:os";
7
7
 
@@ -1098,6 +1098,23 @@ export function sessionLaunch(projectDir: string, cmd: string, base: string[], p
1098
1098
  return { fullCmd, pidFile: join(sessionsDir(st), key + ".pid"), record };
1099
1099
  }
1100
1100
 
1101
+ // Claude Code asks "do you trust this folder?" the first time it opens one, and inside
1102
+ // a git repo it looks for the answer only up to the repo root. A launch from the board
1103
+ // is the user opening the project on purpose, so record yes for that root ahead of
1104
+ // time, in Claude's own config. Best effort: any failure just means Claude asks.
1105
+ export function trustFolder(dir: string, cfg = join(process.env.CLAUDE_CONFIG_DIR ?? homedir(), ".claude.json")) {
1106
+ try {
1107
+ const top = Bun.spawnSync(["git", "-C", dir, "rev-parse", "--show-toplevel"], { stderr: "ignore" });
1108
+ const root = top.success ? top.stdout.toString().trim() || dir : dir;
1109
+ const d = JSON.parse(readFileSync(cfg, "utf8"));
1110
+ if (d.projects?.[root]?.hasTrustDialogAccepted) return;
1111
+ d.projects ??= {};
1112
+ d.projects[root] = { ...d.projects[root], hasTrustDialogAccepted: true };
1113
+ writeFileSync(cfg + ".tmp", JSON.stringify(d, null, 2));
1114
+ renameSync(cfg + ".tmp", cfg);
1115
+ } catch { /* Claude asks as before */ }
1116
+ }
1117
+
1101
1118
  // run: the run a decision is for. Without it, an idea starting a new run.
1102
1119
  // pick: the tool, model and yolo chosen for a new run; a decision takes the run's own
1103
1120
  // tool and model, and never yolo.
@@ -1107,6 +1124,7 @@ export function launchRun(it: Item, from: string, run?: string, pick: { agent?:
1107
1124
  remember(projectDir);
1108
1125
  const own = run && !pick.agent ? runAgent(join(projectDir, ".kaizen"), run) : null;
1109
1126
  const agent = detectDefaultAgent(projectDir, pick.agent ?? own?.cmd);
1127
+ if (agent.cmd === "claude") trustFolder(projectDir);
1110
1128
  const agentBin = Bun.which(agent.cmd) ?? agent.cmd;
1111
1129
  const prompt = requestOf(it.where ? `/kaizen ${it.text} (${it.where})` : `/kaizen ${it.text}`, it.notes, from);
1112
1130
  const wanted = pick.model ?? (own?.cmd === agent.cmd ? own.model : null) ?? undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
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": {