kaizen-agent 0.7.3 → 1.0.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/dashboard.ts +151 -6
- package/cli/install.ts +27 -28
- package/cli/settings.ts +1 -0
- package/package.json +1 -1
- package/skills/kaizen/SKILL.md +1 -0
- package/skills/kaizen/config.default.yml +6 -0
- package/skills/kaizen-help/SKILL.md +1 -0
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(
|
|
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
|
-
|
|
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(
|
|
342
|
-
|
|
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
|
-
|
|
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
|
}
|
package/cli/install.ts
CHANGED
|
@@ -267,8 +267,32 @@ async function anyKey() {
|
|
|
267
267
|
|
|
268
268
|
const repo = await resolveRepo();
|
|
269
269
|
const root = await chooseRoot();
|
|
270
|
+
const presetChoice = await choosePreset();
|
|
270
271
|
const targets = found(root);
|
|
271
272
|
|
|
273
|
+
async function choosePreset(): Promise<PresetName> {
|
|
274
|
+
if (presetArg) return presetArg;
|
|
275
|
+
if (!interactive) return "medium";
|
|
276
|
+
|
|
277
|
+
return select("Choose workflow preset", [
|
|
278
|
+
{
|
|
279
|
+
label: "Medium (Recommended)",
|
|
280
|
+
hint: "balanced — subagents, plan approval, auto-fix high/critical",
|
|
281
|
+
value: "medium",
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
label: "Low",
|
|
285
|
+
hint: "inline & manual — lite runner, inline builder, manual approvals, no auto-fix",
|
|
286
|
+
value: "low",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
label: "Ultra",
|
|
290
|
+
hint: "autonomous — subagents, auto mode, auto-fix all findings (up to 4 rounds)",
|
|
291
|
+
value: "ultra",
|
|
292
|
+
},
|
|
293
|
+
]);
|
|
294
|
+
}
|
|
295
|
+
|
|
272
296
|
// ---------------------------------------------------------------- repo
|
|
273
297
|
|
|
274
298
|
// Settings only needs the clone's default config, not a pull.
|
|
@@ -411,40 +435,15 @@ const inProject = process.cwd() !== home;
|
|
|
411
435
|
const kaizenDir = join(process.cwd(), ".kaizen");
|
|
412
436
|
const globalKaizen = join(home, ".kaizen");
|
|
413
437
|
const globalConfig = join(globalKaizen, "config.yml");
|
|
414
|
-
const hasConfig = (args.has("onboard") || args.has("--onboard"))
|
|
415
|
-
? false
|
|
416
|
-
: (inProject ? existsSync(localConfig) : existsSync(globalConfig));
|
|
417
|
-
|
|
418
|
-
let presetChoice: PresetName | null = presetArg;
|
|
419
|
-
if (!hasConfig && !presetChoice && interactive) {
|
|
420
|
-
presetChoice = await select("Choose workflow preset", [
|
|
421
|
-
{
|
|
422
|
-
label: "Medium (Recommended)",
|
|
423
|
-
hint: "balanced — subagents, plan approval, auto-fix high/critical",
|
|
424
|
-
value: "medium",
|
|
425
|
-
},
|
|
426
|
-
{
|
|
427
|
-
label: "Low",
|
|
428
|
-
hint: "inline & manual — lite runner, inline builder, manual approvals, no auto-fix",
|
|
429
|
-
value: "low",
|
|
430
|
-
},
|
|
431
|
-
{
|
|
432
|
-
label: "Ultra",
|
|
433
|
-
hint: "autonomous — subagents, auto mode, auto-fix all findings (up to 4 rounds)",
|
|
434
|
-
value: "ultra",
|
|
435
|
-
},
|
|
436
|
-
]);
|
|
437
|
-
}
|
|
438
438
|
|
|
439
|
-
if (
|
|
439
|
+
if (!args.has("--project")) {
|
|
440
440
|
mkdirSync(globalKaizen, { recursive: true });
|
|
441
441
|
const defPath = join(repo, "skills/kaizen/config.default.yml");
|
|
442
442
|
const def = readFileSync(defPath, "utf8");
|
|
443
443
|
const base = existsSync(globalConfig) ? readFileSync(globalConfig, "utf8") : def;
|
|
444
|
-
const
|
|
445
|
-
const conf = applyPreset(base, target, def);
|
|
444
|
+
const conf = applyPreset(base, presetChoice, def);
|
|
446
445
|
writeFileSync(globalConfig, conf);
|
|
447
|
-
step(`configured ${c.bold(
|
|
446
|
+
step(`configured ${c.bold(presetChoice)} preset in ${tilde(globalConfig)}`);
|
|
448
447
|
}
|
|
449
448
|
|
|
450
449
|
// A .kaizen/ in a parent already owns this folder's state, and setting up a child
|
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
package/skills/kaizen/SKILL.md
CHANGED
|
@@ -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
|
+
|
|
@@ -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`.
|