kaizen-agent 0.5.1 → 0.7.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
@@ -239,9 +239,14 @@ Agent Skills standard, so the same workflow runs under any of them:
239
239
  | Antigravity | Skills | Parallel agents where available, else sequentially |
240
240
  | OpenCode, Cursor, Gemini CLI | Skills | Sequentially, one session |
241
241
 
242
- Only directories that already exist are touched — installing does not create
243
- `~/.opencode` for someone who does not use OpenCode. Install again after adding an
244
- agent and it picks the new one up.
242
+ An agent counts as present if it has a config directory or a command on your PATH.
243
+ Only what is already there is touched — installing does not create `~/.opencode` for
244
+ someone who does not use OpenCode. Install again after adding an agent and it picks
245
+ the new one up.
246
+
247
+ **No agent yet?** kaizen does nothing on its own — it is a workflow an agent runs.
248
+ If none is found, the installer says so and offers the list above, opening the
249
+ install page for whichever you pick. Install it, run `kaizen` again, and carry on.
245
250
 
246
251
  The stage agents and slash commands are Claude Code's own formats and are not copied
247
252
  elsewhere; under the others you invoke the skill by name. `.kaizen/` is plain files
package/cli/install.ts CHANGED
@@ -27,18 +27,75 @@ const c = {
27
27
  // Skills standard, so every one of these can run the workflow; agents/ and
28
28
  // commands/ are Claude Code's own formats and go nowhere else.
29
29
  const AGENTS = [
30
- { name: "Claude Code", dir: ".claude", full: true },
31
- { name: "Codex", dir: ".codex", full: false },
32
- { name: "Antigravity", dir: ".agents", full: false },
33
- { name: "OpenCode", dir: ".opencode", full: false },
34
- { name: "Cursor", dir: ".cursor", full: false },
35
- { name: "Gemini CLI", dir: ".gemini", full: false },
30
+ { name: "Claude Code", dir: ".claude", cmd: "claude", full: true,
31
+ docs: "https://docs.claude.com/en/docs/claude-code" },
32
+ { name: "Codex", dir: ".codex", cmd: "codex", full: false,
33
+ docs: "https://github.com/openai/codex" },
34
+ { name: "Antigravity", dir: ".agents", cmd: "agy", full: false,
35
+ docs: "https://antigravity.google" },
36
+ { name: "OpenCode", dir: ".opencode", cmd: "opencode", full: false,
37
+ docs: "https://opencode.ai" },
38
+ { name: "Cursor", dir: ".cursor", cmd: "cursor", full: false,
39
+ docs: "https://cursor.com" },
40
+ { name: "Gemini CLI", dir: ".gemini", cmd: "gemini", full: false,
41
+ docs: "https://github.com/google-gemini/gemini-cli" },
36
42
  ];
37
43
 
38
- // Install into an agent that is actually present. Creating ~/.opencode for someone
39
- // who does not use OpenCode leaves litter that looks like configuration. Claude Code
40
- // is always a target: it is the one this is installed from.
41
- const found = (r: string) => AGENTS.filter((a) => a.dir === ".claude" || existsSync(join(r, a.dir)));
44
+ // An agent counts as present if it has a directory or a command. The directory
45
+ // alone misses someone who installed the agent and has not run it yet; the command
46
+ // alone misses one that is not on this shell's PATH.
47
+ const onPath = new Set(
48
+ AGENTS.filter((a) => Bun.which(a.cmd)).map((a) => a.dir),
49
+ );
50
+ const found = (r: string) =>
51
+ AGENTS.filter((a) => existsSync(join(r, a.dir)) || onPath.has(a.dir));
52
+
53
+ // Nothing to install into. kaizen is a workflow an agent runs; on its own it does
54
+ // nothing, so send them to the agent first rather than laying out files no reader
55
+ // will ever load.
56
+ if (!found(home).length && !args.has("--force")) {
57
+ await noAgent();
58
+ process.exit(0);
59
+ }
60
+
61
+ async function noAgent() {
62
+ console.log(` ${c.bold("No AI coding agent found on this machine.")}\n`);
63
+ console.log(` ${c.dim("kaizen is a workflow that an agent runs — it plans, you approve, it builds,")}`);
64
+ console.log(` ${c.dim("and a second agent reviews. So an agent has to be installed first.")}\n`);
65
+
66
+ if (!interactive) {
67
+ for (const a of AGENTS) console.log(` ${a.name.padEnd(14)} ${a.docs}`);
68
+ console.log(`\n ${c.dim("Install one, then run this again.")}`);
69
+ return;
70
+ }
71
+
72
+ const pick = await select("Which one would you like to install?", [
73
+ ...AGENTS.map((a) => ({ label: a.name, hint: a.docs, value: a as (typeof AGENTS)[number] | null })),
74
+ { label: "None of these — just show me the list", hint: "", value: null },
75
+ ]);
76
+
77
+ if (!pick) {
78
+ for (const a of AGENTS) console.log(` ${a.name.padEnd(14)} ${a.docs}`);
79
+ console.log(`\n ${c.dim("Install one, then run this again.")}`);
80
+ return;
81
+ }
82
+
83
+ console.log(` ${c.bold(pick.name)}`);
84
+ console.log(` ${c.cyan(pick.docs)}\n`);
85
+ const opened = await openUrl(pick.docs);
86
+ console.log(opened
87
+ ? ` ${c.dim("Opened in your browser. Install it, then run")} ${c.cyan("kaizen")} ${c.dim("again.")}`
88
+ : ` ${c.dim("Open that page to install it, then run")} ${c.cyan("kaizen")} ${c.dim("again.")}`);
89
+ }
90
+
91
+ // Best effort: a headless box or a locked-down desktop has no opener, and the URL
92
+ // is printed either way.
93
+ async function openUrl(url: string) {
94
+ const opener = process.platform === "darwin" ? "open"
95
+ : process.platform === "win32" ? "start" : "xdg-open";
96
+ if (!Bun.which(opener)) return false;
97
+ try { await Bun.$`${opener} ${url}`.quiet(); return true; } catch { return false; }
98
+ }
42
99
 
43
100
  if (wantSettings) {
44
101
  const { settings } = await import("./settings.ts");
@@ -52,7 +109,7 @@ if (wantSettings) {
52
109
  if (interactive && !upgrade && Bun.argv.length === 2) {
53
110
  const repoDir = await resolveRepoQuietly();
54
111
  const here = found(home);
55
- const linked = here.every((a) =>
112
+ const linked = here.length > 0 && here.every((a) =>
56
113
  ["kaizen", "kaizen-help"].every((n) => existsSync(join(home, a.dir, "skills", n))));
57
114
  if (linked) {
58
115
  const { dashboard } = await import("./dashboard.ts");
@@ -230,10 +287,10 @@ async function chooseRoot() {
230
287
  if (args.has("--project")) return process.cwd();
231
288
  if (args.has("--global") || !interactive) return home;
232
289
  // A project install only means something in a project. Standing in the home
233
- // directory it is the global install under another name, and outside a
234
- // repository there is nothing for it to belong to.
290
+ // directory it is the global install under another name; any other folder is
291
+ // a project worth installing into, git repository or not.
235
292
  const cwd = process.cwd();
236
- if (cwd === home || !existsSync(join(cwd, ".git"))) return home;
293
+ if (cwd === home) return home;
237
294
 
238
295
  const names = (r: string) => found(r).map((a) => a.name).join(", ");
239
296
  return select("Install kaizen for", [
@@ -333,22 +390,43 @@ function installLauncher() {
333
390
  }
334
391
  }
335
392
 
336
- const inGitRepo = existsSync(join(process.cwd(), ".git"));
393
+ const inProject = process.cwd() !== home;
337
394
  const kaizenDir = join(process.cwd(), ".kaizen");
338
395
 
339
- if (inGitRepo && !existsSync(kaizenDir)) {
396
+ // A .kaizen/ in a parent already owns this folder's state, and setting up a child
397
+ // would shadow it, hiding the parent's runs and leaving the child out of the root
398
+ // .gitignore. Deliberately not shared with cli/dashboard.ts locate(): that one
399
+ // resolves which state directory to read and so falls back to a global ~/.kaizen,
400
+ // while this one decides whether a folder is already owned and must stop *before*
401
+ // $HOME — a global ~/.kaizen would otherwise suppress the setup offer for every
402
+ // non-repo folder under the home directory. Do not re-sync the two walks.
403
+ function kaizenAbove() {
404
+ let dir = process.cwd();
405
+ for (;;) {
406
+ if (dir === home) return false;
407
+ if (existsSync(join(dir, ".kaizen"))) return true;
408
+ if (existsSync(join(dir, ".git"))) return false;
409
+ const up = dirname(dir);
410
+ if (up === dir) return false;
411
+ dir = up;
412
+ }
413
+ }
414
+
415
+ if (inProject && !kaizenAbove()) {
340
416
  const now = interactive
341
417
  ? await select(`Set up ${basename(process.cwd())}/ for kaizen now`, [
342
- { label: "Yes", hint: "writes .kaizen/ and a CLAUDE.md line so it runs by default", value: true },
418
+ // "Not now" leads: the offer now appears in any folder outside home, so the
419
+ // safe answer is the one under the cursor.
343
420
  { label: "Not now", hint: "the first /kaizen run will do it", value: false },
421
+ { label: "Yes", hint: "writes .kaizen/ and a CLAUDE.md line so it runs by default", value: true },
344
422
  ])
345
423
  : false;
346
- if (now) initRepo();
424
+ if (now) initProject();
347
425
  } else if (existsSync(kaizenDir)) {
348
426
  step(`${basename(process.cwd())}/.kaizen already set up`);
349
427
  }
350
428
 
351
- function initRepo() {
429
+ function initProject() {
352
430
  mkdirSync(kaizenDir, { recursive: true });
353
431
  for (const f of readdirSync(join(repo, "skills/kaizen")).filter((f) => f.startsWith("spec"))) {
354
432
  copyFileSync(join(repo, "skills/kaizen", f), join(kaizenDir, f));
@@ -356,13 +434,18 @@ function initRepo() {
356
434
  copyFileSync(join(repo, "skills/kaizen/config.default.yml"), join(kaizenDir, "config.yml"));
357
435
 
358
436
  // Run state is local; the workflow itself is shared, so those files stay tracked.
359
- const rules = ".kaizen/*\n!.kaizen/spec*.md\n!.kaizen/config.yml\n";
360
- const gi = join(process.cwd(), ".gitignore");
361
- const current = existsSync(gi) ? readFileSync(gi, "utf8") : "";
362
- if (!current.includes(".kaizen/*")) {
363
- appendFileSync(gi, (current && !current.endsWith("\n") ? "\n" : "") + "\n# kaizen run state\n" + rules);
437
+ // Nothing to ignore where there is no git, so a plain folder gets no .gitignore.
438
+ let ignored = false;
439
+ if (existsSync(join(process.cwd(), ".git"))) {
440
+ const rules = ".kaizen/*\n!.kaizen/spec*.md\n!.kaizen/config.yml\n";
441
+ const gi = join(process.cwd(), ".gitignore");
442
+ const current = existsSync(gi) ? readFileSync(gi, "utf8") : "";
443
+ if (!current.includes(".kaizen/*")) {
444
+ appendFileSync(gi, (current && !current.endsWith("\n") ? "\n" : "") + "\n# kaizen run state\n" + rules);
445
+ ignored = true;
446
+ }
364
447
  }
365
- step("wrote .kaizen/ and the gitignore entry");
448
+ step(ignored ? "wrote .kaizen/ and the gitignore entry" : "wrote .kaizen/");
366
449
  writePointer();
367
450
  }
368
451
 
package/cli/release.sh ADDED
@@ -0,0 +1,34 @@
1
+ #!/bin/sh
2
+ # Runs after `npm publish`: tag the published version and open a GitHub release
3
+ # for it, so a version can never exist on npm with nothing to read on GitHub.
4
+ #
5
+ # Never fails the publish. The package is already public by the time this runs;
6
+ # exiting non-zero here would report a failure that did not happen.
7
+ set -u
8
+ v="${npm_package_version:-$(node -p "require('./package.json').version" 2>/dev/null)}"
9
+ [ -n "$v" ] || { echo "release: no version, skipping"; exit 0; }
10
+ tag="v$v"
11
+
12
+ git rev-parse --git-dir >/dev/null 2>&1 || { echo "release: not a git checkout, skipping"; exit 0; }
13
+
14
+ if git rev-parse "$tag" >/dev/null 2>&1; then
15
+ echo "release: $tag already tagged"
16
+ else
17
+ git tag -a "$tag" -m "kaizen-agent $tag" || { echo "release: could not tag"; exit 0; }
18
+ git push origin "$tag" >/dev/null 2>&1 || echo "release: tag made but not pushed"
19
+ fi
20
+
21
+ command -v gh >/dev/null 2>&1 || { echo "release: gh not installed — tag pushed, write the release by hand"; exit 0; }
22
+ gh auth status >/dev/null 2>&1 || { echo "release: gh not logged in — tag pushed, write the release by hand"; exit 0; }
23
+
24
+ if gh release view "$tag" >/dev/null 2>&1; then
25
+ echo "release: $tag already released"
26
+ exit 0
27
+ fi
28
+
29
+ # --generate-notes lists the commits since the last tag. Good enough to never be
30
+ # empty; rewrite it with what actually changed when the release matters.
31
+ gh release create "$tag" --title "$tag" --generate-notes >/dev/null 2>&1 \
32
+ && echo "release: published $tag https://github.com/hfadhlullah/kaizen/releases/tag/$tag" \
33
+ || echo "release: tag pushed, release not created"
34
+ exit 0
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Install .kaizen/ (spec, config, gitignore entry) into the current repository
2
+ description: Set up this project folder for kaizen (spec, config, and a gitignore entry when it is a git repo)
3
3
  ---
4
4
 
5
5
  Invoke the `kaizen` skill with argument `init`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "0.5.1",
3
+ "version": "0.7.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": {
@@ -17,7 +17,8 @@
17
17
  ],
18
18
  "scripts": {
19
19
  "install-links": "bun run cli/install.ts",
20
- "check": "bun run cli/install.ts --check"
20
+ "check": "bun run cli/install.ts --check",
21
+ "postpublish": "sh cli/release.sh"
21
22
  },
22
23
  "repository": {
23
24
  "type": "git",
@@ -47,7 +47,7 @@ Porting the workflow to Codex or Antigravity is covered in [`adapters.md`](adapt
47
47
  | `/kaizen reject <reason>` | Reject it; the reason is fed back to the stage that produced it |
48
48
  | `/kaizen abort` | Mark the current run abandoned |
49
49
  | `/kaizen config` | Show this project's settings and change them, one picker per setting |
50
- | `/kaizen init` | Install `.kaizen/` with the `spec*.md` files, config, and the gitignore entry into the current repository |
50
+ | `/kaizen init` | Set up this project folder: `.kaizen/` with the `spec*.md` files, config, and — in a git repo — the gitignore entry |
51
51
  | `/kaizen install` | Install the workflow itself globally or per-project (see below) |
52
52
 
53
53
  If the user typed `/kaizen` with no argument and a run is in progress, treat it as
@@ -55,10 +55,11 @@ If the user typed `/kaizen` with no argument and a run is in progress, treat it
55
55
 
56
56
  ## Before doing anything
57
57
 
58
- 1. Locate the state directory: `.kaizen/` in the repository root, else `~/.kaizen/`
59
- keyed by working directory. If neither exists and the user is starting work, run
60
- `/kaizen init` first: it creates `.kaizen/` with a copy of the `spec*.md` files, a config, and
61
- the gitignore entry (see [`adapters.md`](adapters.md)).
58
+ 1. Locate the state directory: `.kaizen/` in the project folder's root, else
59
+ `~/.kaizen/` keyed by working directory. If neither exists and the user is starting
60
+ work, run `/kaizen init` first: it creates `.kaizen/` with a copy of the `spec*.md`
61
+ files, a config, and — where the folder is a git repository — the gitignore entry
62
+ (see [`adapters.md`](adapters.md)).
62
63
  2. Read `.kaizen/config.yml`, falling back to `config.default.yml` in this skill for
63
64
  any key it does not set.
64
65
  3. Read `.kaizen/runs/<current>/state.json` if a run is active. Never assume the
@@ -293,9 +294,11 @@ shown automatically.
293
294
  }
294
295
  ```
295
296
 
296
- `init` adds a gitignore entry that ignores run state but keeps the workflow: it
297
- ignores `.kaizen/*` and un-ignores `.kaizen/spec*.md` and `.kaizen/config.yml`, which
298
- are meant to be committed (see [`adapters.md`](adapters.md)).
297
+ In a git repository, `init` also adds a gitignore entry that ignores run state but
298
+ keeps the workflow: it ignores `.kaizen/*` and un-ignores `.kaizen/spec*.md` and
299
+ `.kaizen/config.yml`, which are meant to be committed (see
300
+ [`adapters.md`](adapters.md)). A project folder that is not a repository gets no
301
+ gitignore.
299
302
 
300
303
  ## Install
301
304
 
@@ -64,8 +64,9 @@ 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, `config.yml`, and the gitignore
68
- entry. `/kaizen install` additionally offers to append the `AGENTS.md` block above.
67
+ `/kaizen init` writes `.kaizen/` with the `spec*.md` files and `config.yml` into the
68
+ project folder, plus the gitignore entry when that folder is a git repository.
69
+ `/kaizen install` additionally offers to append the `AGENTS.md` block above.
69
70
  `.kaizen/spec*.md` and `.kaizen/config.yml` should be committed even though run state
70
71
  is ignored — the workflow is shared, the run data is not:
71
72
 
@@ -55,12 +55,27 @@ review:
55
55
  # Additional checks per track, chosen by what the run actually is. The planner may
56
56
  # name a track not listed here; the reviewer then derives its checks from the
57
57
  # track's "what would make it wrong".
58
+ # Each check below is one question the reviewer asks; the line beside it says which.
58
59
  track_checks:
60
+ # security - no new way in, no secret or user data exposed
61
+ # reuse_simplification - nothing rebuilt that the codebase already had
62
+ # test_coverage - the changed behaviour has a check that fails when it breaks
59
63
  software: [security, reuse_simplification, test_coverage]
60
64
  # blast_radius and regression are universal — see spec-review.md — not listed here.
65
+ # completeness - every part the piece promises is actually present
66
+ # claim_accuracy - every checkable claim traced to a source
67
+ # reader_usability - its intended reader can act on it without asking
61
68
  writing: [completeness, claim_accuracy, reader_usability]
69
+ # audience_fit - written for the people who will actually read it
70
+ # voice_consistency - reads as the same voice as the surrounding material
62
71
  communication: [claim_accuracy, audience_fit, voice_consistency]
72
+ # prerequisites - everything needed before step one is stated
73
+ # rollback - a way back exists for every step that changes something
74
+ # executability - someone else can run it start to finish unaided
63
75
  operations: [prerequisites, rollback, executability]
76
+ # evidence_support - the conclusion follows from evidence shown, not asserted
77
+ # selection_bias - the evidence was not picked to fit the answer
78
+ # alternatives - the readings that were rejected are named and why
64
79
  research: [evidence_support, selection_bias, alternatives]
65
80
  # Let the reviewer append durable lessons to .kaizen/memory.md.
66
81
  write_memory: true
@@ -90,6 +105,8 @@ state:
90
105
 
91
106
  git:
92
107
  # Never commit, push, or open a PR unless the user explicitly asks.
108
+ # Ignored when the project is not a git repository.
93
109
  auto_commit: false
94
110
  # Create a branch before the implement stage when on the default branch.
111
+ # Ignored when the project is not a git repository.
95
112
  branch_before_implement: true
@@ -35,6 +35,9 @@ Rules:
35
35
  abstraction, a new file, a new section — is allowed, and costs one written line
36
36
  saying what it serves. The line is the price of the technique, not a request for
37
37
  permission. No line means the reviewer records a finding.
38
+ - The `git.*` settings apply only where the work sits in a git repository. In a plain
39
+ folder there is no branch to make and no commit to skip, and that absence is not a
40
+ finding.
38
41
 
39
42
  **Baseline first, in any track with an existing suite of checks.** Before changing
40
43
  anything, run whatever already proves the existing material correct — the test suite,
@@ -31,7 +31,7 @@ anything.
31
31
  | `/kaizen reject <reason>` | Reject it; the reason decides revise (back to planner) vs kill (abandon) |
32
32
  | `/kaizen abort` | Mark the current run abandoned |
33
33
  | `/kaizen config` | Show this project's settings and change them, one picker per setting |
34
- | `/kaizen init` | Install `.kaizen/` with `spec.md`, config, and the gitignore entry into the current repository |
34
+ | `/kaizen init` | Set up this project folder: `.kaizen/` with `spec.md`, config, and — in a git repo — the gitignore entry |
35
35
  | `/kaizen install` | Install the workflow itself globally or per-project |
36
36
 
37
37
  `/kaizen` with no argument: `status` if a run is in progress, else ask what to build.