kaizen-agent 1.2.2 → 1.3.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
@@ -463,7 +463,6 @@ Everything a run knows lives in files next to the work, not in a chat window:
463
463
 
464
464
  ```
465
465
  .kaizen/
466
- config.yml # how much it asks you, what it fixes on its own
467
466
  memory.md # what past reviews learned about this project
468
467
  runs/2026-09-07-onboarding-email/
469
468
  00-request.md # what you asked, word for word
@@ -480,7 +479,7 @@ in one can be finished in another. See [adapters.md](https://github.com/hfadhlul
480
479
 
481
480
  ## Settings
482
481
 
483
- `.kaizen/config.yml`, written when the project is set up. The three worth knowing:
482
+ `~/.kaizen/config.yml` — one file, every project. The three worth knowing:
484
483
 
485
484
  | Setting | Default | Meaning |
486
485
  |---|---|---|
@@ -499,7 +498,7 @@ opens a full-screen browser over the whole file — arrows to move and change, e
499
498
  change written as you make it, esc to close:
500
499
 
501
500
  ```
502
- kaizen settings ~/Projects/app/.kaizen/config.yml
501
+ kaizen settings ~/.kaizen/config.yml
503
502
 
504
503
  › preset medium
505
504
  mode approve
@@ -517,6 +516,8 @@ change written as you make it, esc to close:
517
516
  ↑↓ move · ←→ change · esc close saved preset = medium
518
517
  ```
519
518
 
519
+ One project can differ: put a `.kaizen/config.yml` in it with just the keys to
520
+ override, and it wins there while everything else follows the global file.
520
521
  It edits each line in place, so the comment above every key survives. `/kaizen-config`
521
522
  does the same thing from inside your agent if you would rather not leave it. Everything is also
522
523
  in [`config.default.yml`](https://github.com/hfadhlullah/kaizen/blob/main/skills/kaizen/config.default.yml),
package/cli/dashboard.ts CHANGED
@@ -208,7 +208,7 @@ export async function dashboard(
208
208
  { key: "backlog", label: "Backlog", hint: "what runs noticed and did not do" },
209
209
  ] : []),
210
210
  { key: "settings", label: "Settings", hint: "modes, approvals, who builds" },
211
- ...(state && !existsSync(join(state, "config.yml"))
211
+ ...(state && !existsSync(join(state, "spec.md"))
212
212
  ? [{ key: "init", label: "Set up this project", hint: "write .kaizen/ here" }] : []),
213
213
  ...(!state ? [{ key: "init", label: "Set up this project", hint: "write .kaizen/ here" }] : []),
214
214
  { key: "upgrade", label: "Upgrade", hint: "pull, relink, clear the installer cache" },
package/cli/install.ts CHANGED
@@ -2,10 +2,11 @@
2
2
  import {
3
3
  symlinkSync, mkdirSync, readdirSync, lstatSync, readlinkSync, unlinkSync,
4
4
  existsSync, copyFileSync, readFileSync, appendFileSync, rmSync, writeFileSync,
5
+ statSync, cpSync,
5
6
  } from "node:fs";
6
7
  import { join, dirname, basename } from "node:path";
7
- import { homedir } from "node:os";
8
- import { applyPreset, detectPreset, PRESETS, type PresetName } from "./settings.ts";
8
+ import { homedir, tmpdir } from "node:os";
9
+ import { applyPreset, PRESETS, type PresetName } from "./settings.ts";
9
10
 
10
11
  const home = homedir();
11
12
  const REPO_URL = "https://github.com/hfadhlullah/kaizen.git";
@@ -319,8 +320,18 @@ async function resolveRepo() {
319
320
  }
320
321
  return dest;
321
322
  }
322
- step(`cloning into ${tilde(dest)}`);
323
- await Bun.$`git clone --quiet ${REPO_URL} ${dest}`;
323
+ if (Bun.which("git")) {
324
+ step(`cloning into ${tilde(dest)}`);
325
+ await Bun.$`git clone --quiet ${REPO_URL} ${dest}`;
326
+ return dest;
327
+ }
328
+ // No git: the bunx download already holds everything a clone would, so copy it
329
+ // in place. Upgrades then come from re-running bunx rather than a pull.
330
+ step(`copying into ${tilde(dest)} ${c.dim("(no git)")}`);
331
+ cpSync(here, dest, {
332
+ recursive: true, dereference: true,
333
+ filter: (src) => basename(src) !== "node_modules",
334
+ });
324
335
  return dest;
325
336
  }
326
337
 
@@ -356,10 +367,30 @@ const links = targets.flatMap((a) => [
356
367
  ] : []),
357
368
  ]);
358
369
 
370
+ const isWindows = process.platform === "win32";
371
+
372
+ // Windows only hands out file symlinks to an admin or a Developer Mode box, so
373
+ // there the link is a directory junction (allowed for everyone) or, for a plain
374
+ // file, a copy that is refreshed whenever the source moves ahead.
375
+ function makeLink(src: string, dest: string) {
376
+ if (!isWindows) { symlinkSync(src, dest); return; }
377
+ if (statSync(src).isDirectory()) { symlinkSync(src, dest, "junction"); return; }
378
+ copyFileSync(src, dest);
379
+ }
380
+
381
+ function sameFile(a: string, b: string) {
382
+ try { return readFileSync(a).equals(readFileSync(b)); } catch { return false; }
383
+ }
384
+
359
385
  function linkState(src: string, dest: string) {
360
386
  try {
361
387
  const st = lstatSync(dest);
362
- if (!st.isSymbolicLink()) return "occupied";
388
+ if (!st.isSymbolicLink()) {
389
+ // A copied link on Windows is a real file; it is ours as long as it still
390
+ // matches the source, and stale once the source changes.
391
+ if (isWindows && st.isFile()) return sameFile(src, dest) ? "linked" : "stale";
392
+ return "occupied";
393
+ }
363
394
  return readlinkSync(dest) === src ? "linked" : "stale";
364
395
  } catch {
365
396
  return "missing";
@@ -385,8 +416,8 @@ for (const [src, sub] of links) {
385
416
  continue;
386
417
  }
387
418
  mkdirSync(dir, { recursive: true });
388
- if (state !== "missing") unlinkSync(dest);
389
- symlinkSync(src, dest);
419
+ if (state !== "missing") rmSync(dest, { recursive: true, force: true });
420
+ makeLink(src, dest);
390
421
  wrote++;
391
422
  if (verbose) console.log(c.dim(` link ${name}`));
392
423
  }
@@ -436,7 +467,9 @@ const kaizenDir = join(process.cwd(), ".kaizen");
436
467
  const globalKaizen = join(home, ".kaizen");
437
468
  const globalConfig = join(globalKaizen, "config.yml");
438
469
 
439
- if (!args.has("--project")) {
470
+ // Always global: a project keeps no config of its own, so ~/.kaizen/config.yml is
471
+ // what every project follows.
472
+ {
440
473
  mkdirSync(globalKaizen, { recursive: true });
441
474
  const defPath = join(repo, "skills/kaizen/config.default.yml");
442
475
  const def = readFileSync(defPath, "utf8");
@@ -476,25 +509,19 @@ if (inProject && !kaizenAbove()) {
476
509
  { label: "Yes", hint: "writes .kaizen/ and a CLAUDE.md line so it runs by default", value: true },
477
510
  ])
478
511
  : false;
479
- if (now) initProject(presetChoice);
512
+ if (now) initProject();
480
513
  } else if (existsSync(kaizenDir)) {
481
514
  step(`${basename(process.cwd())}/.kaizen already set up`);
482
515
  }
483
516
 
484
- function initProject(preset?: PresetName | null) {
517
+ function initProject() {
485
518
  mkdirSync(kaizenDir, { recursive: true });
486
519
  for (const f of readdirSync(join(repo, "skills/kaizen")).filter((f) => f.startsWith("spec"))) {
487
520
  copyFileSync(join(repo, "skills/kaizen", f), join(kaizenDir, f));
488
521
  }
489
- const defPath = join(repo, "skills/kaizen/config.default.yml");
490
- const def = readFileSync(defPath, "utf8");
491
- let chosen: PresetName = preset ?? "medium";
492
- if (!preset && existsSync(globalConfig)) {
493
- const detected = detectPreset(readFileSync(globalConfig, "utf8"), def);
494
- if (detected !== "custom") chosen = detected;
495
- }
496
- const configContent = applyPreset(def, chosen, def);
497
- writeFileSync(join(kaizenDir, "config.yml"), configContent);
522
+ // No config.yml here on purpose: a project without one follows
523
+ // ~/.kaizen/config.yml, so a setting changed once applies everywhere. Write one
524
+ // only to make this project deliberately different.
498
525
 
499
526
  // Run state is local; the workflow itself is shared, so those files stay tracked.
500
527
  // Nothing to ignore where there is no git, so a plain folder gets no .gitignore.
@@ -603,7 +630,7 @@ function clean(out: string) {
603
630
  // without re-resolving, so an upgrade that only pulls the clone still leaves the
604
631
  // next `bunx kaizen-agent` running whatever was cached the first time.
605
632
  async function clearBunxCache() {
606
- const tmp = process.env.TMPDIR ?? "/tmp";
633
+ const tmp = process.env.TMPDIR ?? tmpdir();
607
634
  let removed = 0;
608
635
  try {
609
636
  for (const name of readdirSync(tmp)) {
package/cli/settings.ts CHANGED
@@ -256,11 +256,14 @@ export async function settings(repoRoot: string, standalone = true) {
256
256
  if (standalone) console.log(` ${c.green("+")} settings saved to ${tilde(file)}\n`);
257
257
  }
258
258
 
259
- // The nearest .kaizen wins, exactly as the workflow resolves it.
259
+ // The nearest .kaizen/config.yml wins, exactly as the workflow resolves it. A
260
+ // project that has not written one of its own is edited globally, so one setting
261
+ // changed in ~/.kaizen/config.yml reaches every project.
260
262
  function locate() {
261
263
  let dir = process.cwd();
262
264
  for (;;) {
263
- if (existsSync(join(dir, ".kaizen"))) return join(dir, ".kaizen", "config.yml");
265
+ const conf = join(dir, ".kaizen", "config.yml");
266
+ if (existsSync(conf)) return conf;
264
267
  if (existsSync(join(dir, ".git"))) break;
265
268
  const up = dirname(dir);
266
269
  if (up === dir) break;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "1.2.2",
4
- "description": "Staged workflow for AI agents \u2014 code, writing, research, operations: plan, human approval, build, independent review, bounded fix loop.",
3
+ "version": "1.3.1",
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": {
7
7
  "kaizen-agent": "./cli/install.ts",
@@ -189,9 +189,11 @@ software or writing?"
189
189
 
190
190
  ## Configuration
191
191
 
192
- Defaults live in [`config.default.yml`](config.default.yml). Copy it to
193
- `.kaizen/config.yml` in a repository and override only the keys you care about;
194
- anything absent falls back to the defaults.
192
+ Your settings live in `~/.kaizen/config.yml` and apply to every project — `/kaizen
193
+ config` edits that file. To make one project different, copy
194
+ [`config.default.yml`](config.default.yml) to `.kaizen/config.yml` there and override
195
+ only the keys you care about; anything absent falls back to the global file, then to
196
+ the defaults.
195
197
 
196
198
  | Key | Default | Change it when |
197
199
  |---|---|---|
@@ -60,10 +60,12 @@ If the user typed `/kaizen` with no argument and a run is in progress, treat it
60
60
  1. Locate the state directory: `.kaizen/` in the project folder's root, else
61
61
  `~/.kaizen/` keyed by working directory. If neither exists and the user is starting
62
62
  work, run `/kaizen init` first: it creates `.kaizen/` with a copy of the `spec*.md`
63
- files, a config, and — where the folder is a git repository — the gitignore entry
63
+ files and — where the folder is a git repository — the gitignore entry
64
64
  (see [`adapters.md`](adapters.md)).
65
- 2. Read `.kaizen/config.yml`, falling back to `config.default.yml` in this skill for
66
- any key it does not set. `runner` decides whether this run dispatches subagents at
65
+ 2. Read the config in this order, each file filling in the keys the one before it
66
+ does not set: `.kaizen/config.yml`, then `~/.kaizen/config.yml`, then
67
+ `config.default.yml` in this skill. A project usually has no `config.yml` of its
68
+ own, so the global one is what settings changes reach. `runner` decides whether this run dispatches subagents at
67
69
  all; read it before the first stage, not when you reach one.
68
70
  3. Read `.kaizen/runs/<current>/state.json` if a run is active. Never assume the
69
71
  stage from conversation memory — the run may have been advanced by another tool
@@ -73,11 +75,12 @@ If the user typed `/kaizen` with no argument and a run is in progress, treat it
73
75
 
74
76
  ## Settings
75
77
 
76
- `/kaizen config` reads `.kaizen/config.yml`, falling back to `config.default.yml` for
77
- anything it does not set, and prints the current value of each setting below with a
78
+ `/kaizen config` reads the same chain — `.kaizen/config.yml`, then `~/.kaizen/config.yml`,
79
+ then `config.default.yml` — and prints the current value of each setting below with a
78
80
  one-line meaning. Then it offers the settings as choices — the tool's structured
79
81
  question mechanism, one option per legal value, never free text — and writes back
80
- whichever the user picks.
82
+ whichever the user picks. Write to whichever file in the chain already exists,
83
+ nearest first, so a change lands globally unless this project keeps its own config.
81
84
 
82
85
  | Setting | Values | What it decides |
83
86
  |---|---|---|
@@ -24,7 +24,7 @@ Codex reads `AGENTS.md` from the repository root. Append:
24
24
 
25
25
  Non-trivial work in this repo follows the staged workflow in
26
26
  `.kaizen/spec.md`: plan, human approval, implement, independent review, bounded
27
- fix loop. Read that file before starting, and read `.kaizen/config.yml` for the
27
+ fix loop. Read that file before starting, and read the config chain (`.kaizen/config.yml`, `~/.kaizen/config.yml`, `config.default.yml`) for the
28
28
  mode and approval settings.
29
29
 
30
30
  Run state lives in `.kaizen/runs/<id>/`. Always read `state.json` first and
@@ -38,7 +38,7 @@ from its stage file, and write its artifact before moving on. When reviewing, ju
38
38
  diff on its own terms rather than defending the reasoning you used while writing it.
39
39
  ```
40
40
 
41
- Copy `spec*.md` and `config.yml` into `.kaizen/` so the reference resolves
41
+ Copy `spec*.md` into `.kaizen/` so the reference resolves
42
42
  without this skill installed.
43
43
 
44
44
  ## Antigravity
@@ -64,7 +64,7 @@ completes. A stale `state.json` is the one way this breaks.
64
64
 
65
65
  ## Installing the adapter files
66
66
 
67
- `/kaizen init` writes `.kaizen/` with the `spec*.md` files and `config.yml` into the
67
+ `/kaizen init` writes `.kaizen/` with the `spec*.md` files into the
68
68
  project folder, plus the gitignore entry when that folder is a git repository.
69
69
  `/kaizen install` additionally offers to append the `AGENTS.md` block above.
70
70
  `.kaizen/spec*.md` and `.kaizen/config.yml` should be committed even though run state
@@ -1,5 +1,7 @@
1
- # Kaizen defaults. Copy to .kaizen/config.yml in a repository and override only the
2
- # keys you care about; anything absent falls back to this file.
1
+ # Kaizen defaults. Settings live in ~/.kaizen/config.yml and apply to every project.
2
+ # To make one project different, copy this to .kaizen/config.yml there and override
3
+ # only the keys you care about; anything absent falls back to the global file, then
4
+ # to this one.
3
5
 
4
6
  # Quick preset: low | medium | ultra | custom
5
7
  # Selecting low, medium, or ultra applies its bundled defaults for runner, executor,