kaizen-agent 1.7.8 → 1.8.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/LICENSE +21 -21
- package/README.md +694 -658
- package/agents/kaizen-builder.md +82 -77
- package/agents/kaizen-planner.md +250 -250
- package/agents/kaizen-reviewer.md +127 -127
- package/cli/board.test.ts +55 -0
- package/cli/dashboard.ts +1188 -1187
- package/cli/install.ts +76 -3
- package/cli/publish-gh.sh +27 -27
- package/cli/release.sh +45 -45
- package/cli/settings.ts +7 -3
- package/cli/sources.test.ts +494 -0
- package/cli/sources.ts +323 -0
- package/cli/state.test.ts +422 -197
- package/cli/state.ts +989 -700
- package/cli/web.ts +182 -11
- package/commands/kaizen-abort.md +5 -5
- package/commands/kaizen-approve.md +5 -5
- package/commands/kaizen-auto.md +6 -6
- package/commands/kaizen-backlog.md +5 -5
- package/commands/kaizen-config.md +5 -5
- package/commands/kaizen-full.md +5 -5
- package/commands/kaizen-gather.md +5 -0
- package/commands/kaizen-init.md +5 -5
- package/commands/kaizen-install.md +5 -5
- package/commands/kaizen-lite.md +5 -5
- package/commands/kaizen-plan.md +6 -6
- package/commands/kaizen-reject.md +6 -6
- package/commands/kaizen-review.md +6 -6
- package/commands/kaizen-run.md +5 -5
- package/commands/kaizen-status.md +5 -5
- package/package.json +3 -1
- package/skills/kaizen/README.md +283 -281
- package/skills/kaizen/SKILL.md +334 -440
- package/skills/kaizen/adapters.md +84 -77
- package/skills/kaizen/config.default.yml +181 -167
- package/skills/kaizen/gather.md +50 -0
- package/skills/kaizen/install.md +42 -0
- package/skills/kaizen/settings.md +52 -0
- package/skills/kaizen/spec-build.md +131 -119
- package/skills/kaizen/spec-main.md +109 -104
- package/skills/kaizen/spec-plan.md +201 -201
- package/skills/kaizen/spec-review.md +121 -121
- package/skills/kaizen/spec.md +154 -153
- package/skills/kaizen/status.md +43 -0
- package/skills/kaizen-help/SKILL.md +131 -129
- package/web/board.html +295 -126
package/cli/install.ts
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
import {
|
|
3
3
|
symlinkSync, mkdirSync, readdirSync, lstatSync, readlinkSync, unlinkSync,
|
|
4
4
|
existsSync, copyFileSync, readFileSync, appendFileSync, rmSync, writeFileSync, renameSync,
|
|
5
|
-
statSync, cpSync,
|
|
5
|
+
statSync, cpSync, realpathSync,
|
|
6
6
|
} from "node:fs";
|
|
7
7
|
import { join, dirname, basename } from "node:path";
|
|
8
8
|
import { homedir, tmpdir } from "node:os";
|
|
9
9
|
import { applyPreset, detectPreset, PRESETS, type PresetName } from "./settings.ts";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// The real path: process.cwd() is always physical, so a raw $HOME that crosses a
|
|
12
|
+
// symlink (/home -> var/home) would never compare equal to it, and the home
|
|
13
|
+
// directory would be offered set-up as if it were a project.
|
|
14
|
+
const home = (() => { try { return realpathSync(homedir()); } catch { return homedir(); } })();
|
|
12
15
|
const REPO_URL = "https://github.com/hfadhlullah/kaizen.git";
|
|
13
16
|
const args = new Set(Bun.argv.slice(2));
|
|
14
17
|
const upgrade = args.has("upgrade") || args.has("--upgrade");
|
|
@@ -70,6 +73,48 @@ const onPath = new Set(
|
|
|
70
73
|
const found = (r: string) =>
|
|
71
74
|
AGENTS.filter((a) => existsSync(join(r, a.dir)) || onPath.has(a.dir));
|
|
72
75
|
|
|
76
|
+
// Request sources. Dispatched on the first word alone and ahead of every flag
|
|
77
|
+
// check below, so a label or a link that happens to read `web` or `uninstall` is
|
|
78
|
+
// only ever a label.
|
|
79
|
+
if (Bun.argv[2] === "sources" || Bun.argv[2] === "gather") {
|
|
80
|
+
const { locate, label } = await import("./state.ts");
|
|
81
|
+
const src = await import("./sources.ts");
|
|
82
|
+
const state = locate() ?? join(home, ".kaizen");
|
|
83
|
+
// A source belongs to the project the command is run in; every answer names it.
|
|
84
|
+
const where = c.dim(`project ${label(state)}`);
|
|
85
|
+
const [, , cmd, op, url = "", ...rest] = Bun.argv;
|
|
86
|
+
const line = (r: { url: string; ok: boolean; note: string }) => ` ${r.ok ? c.green("ok ") : "fail"} ${r.url} ${c.dim(r.note)}`;
|
|
87
|
+
try {
|
|
88
|
+
if (cmd === "gather" && op === "--stdin") {
|
|
89
|
+
// What an agent read from a link kaizen could not: one request per line, an
|
|
90
|
+
// optional tab, then notes. Only for a link already listed.
|
|
91
|
+
if (!src.readSources(state).some((s) => s.url === url)) { console.log(`
|
|
92
|
+
not a listed source: ${url || "(no link given)"}
|
|
93
|
+
`); process.exit(1); }
|
|
94
|
+
const items = (await Bun.stdin.text()).split("\n").filter((l) => l.trim()).map((l) => {
|
|
95
|
+
const [text = "", ...notes] = l.split("\t");
|
|
96
|
+
return { text, notes: notes.join(" ") };
|
|
97
|
+
});
|
|
98
|
+
console.log(line(src.record(state, url, src.ingest(state, url, items))));
|
|
99
|
+
} else if (cmd === "gather") {
|
|
100
|
+
const results = await src.gatherAll(state);
|
|
101
|
+
console.log(results.length ? results.map(line).join("\n") : `\n no sources yet ${c.dim("— kaizen sources add <link>")}\n`);
|
|
102
|
+
if (results.some((r) => !r.ok)) process.exit(1);
|
|
103
|
+
} else if (op === "add" || op === "rm") {
|
|
104
|
+
const why = op === "add" ? src.addSource(state, url, rest.join(" ")) : src.removeSource(state, url);
|
|
105
|
+
if (why) { console.log(`\n ${why}\n`); process.exit(1); }
|
|
106
|
+
console.log(`\n ${op === "add" ? "added" : "removed"} ${url}\n ${where}\n`);
|
|
107
|
+
} else {
|
|
108
|
+
const list = src.readSources(state);
|
|
109
|
+
console.log(`\n ${where}`);
|
|
110
|
+
console.log(list.length
|
|
111
|
+
? "\n" + list.map((s) => ` ${s.url}${s.label ? ` ${s.label}` : ""}\n ${c.dim(s.last ? `${s.last.at.slice(0, 16).replace("T", " ")} ${s.last.note}` : "not gathered yet")}`).join("\n") + "\n"
|
|
112
|
+
: `\n no sources yet ${c.dim("— kaizen sources add <link>")}\n`);
|
|
113
|
+
}
|
|
114
|
+
} catch (e) { console.log(`\n ${(e as Error).message}\n`); process.exit(1); }
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
73
118
|
if (args.has("--version") || args.has("-v") || args.has("version")) {
|
|
74
119
|
console.log(versionOf(dirname(import.meta.dir)));
|
|
75
120
|
process.exit(0);
|
|
@@ -212,6 +257,22 @@ if (wantSettings) {
|
|
|
212
257
|
process.exit(0);
|
|
213
258
|
}
|
|
214
259
|
|
|
260
|
+
// Deleting runs is a job for code with a test, not for an agent with `rm`. Lists by
|
|
261
|
+
// default; --yes is what deletes.
|
|
262
|
+
if (args.has("prune")) {
|
|
263
|
+
const { locate, pruneRuns } = await import("./state.ts");
|
|
264
|
+
const { listSettings } = await import("./settings.ts");
|
|
265
|
+
const state = locate();
|
|
266
|
+
if (!state) { console.log("\n no .kaizen here\n"); process.exit(1); }
|
|
267
|
+
const keep = Number(listSettings(await resolveRepoQuietly()).rows.find((r) => r.key === "state.keep_runs")?.value) || 20;
|
|
268
|
+
const apply = args.has("--yes");
|
|
269
|
+
const gone = pruneRuns(state, keep, apply);
|
|
270
|
+
console.log(gone.length
|
|
271
|
+
? `\n ${apply ? "pruned" : "would prune"} ${gone.length} finished run(s), keeping the newest ${keep}:\n${gone.map((g) => ` ${g.id}${g.rescued ? c.dim(` ${g.rescued} open item(s) ${apply ? "moved" : "to move"} to backlog.md`) : ""}`).join("\n")}\n${apply ? "" : `\n ${c.dim("nothing deleted — run:")} ${c.cyan("kaizen prune --yes")}\n`}`
|
|
272
|
+
: `\n nothing to prune ${c.dim(`(${keep} finished runs are kept)`)}\n`);
|
|
273
|
+
process.exit(0);
|
|
274
|
+
}
|
|
275
|
+
|
|
215
276
|
if (wantWeb) {
|
|
216
277
|
if (args.has("--shortcut")) { const { shortcut } = await import("./web.ts"); console.log(`\n kaizen web shortcut ${await shortcut()}\n`); process.exit(0); }
|
|
217
278
|
const at = Bun.argv.indexOf("--port");
|
|
@@ -629,7 +690,17 @@ for (const [src, sub] of links) {
|
|
|
629
690
|
if (verbose) console.log(c.dim(` link ${name}`));
|
|
630
691
|
}
|
|
631
692
|
|
|
693
|
+
// A project's spec copies are what a run there reads, and they are copied once.
|
|
694
|
+
function specDrift() {
|
|
695
|
+
const dir = join(process.cwd(), ".kaizen");
|
|
696
|
+
return readdirSync(join(repo, "skills/kaizen"))
|
|
697
|
+
.filter((f) => f.startsWith("spec") && existsSync(join(dir, f)) && !sameFile(join(repo, "skills/kaizen", f), join(dir, f)));
|
|
698
|
+
}
|
|
699
|
+
|
|
632
700
|
if (check) {
|
|
701
|
+
const drift = specDrift();
|
|
702
|
+
for (const f of drift) console.log(`DRIFT .kaizen/${f} (behind the skill's copy)`);
|
|
703
|
+
if (drift.length) { console.log(`\n${drift.length} spec file(s) behind — run: kaizen --yes`); if (!blocked) process.exit(1); }
|
|
633
704
|
console.log(blocked ? `\n${blocked} link(s) missing — run: bunx kaizen-agent` : "\nall linked");
|
|
634
705
|
process.exit(blocked ? 1 : 0);
|
|
635
706
|
}
|
|
@@ -734,7 +805,9 @@ if (inProject && !kaizenAbove()) {
|
|
|
734
805
|
: false;
|
|
735
806
|
if (now) initProject();
|
|
736
807
|
} else if (existsSync(kaizenDir)) {
|
|
737
|
-
|
|
808
|
+
const drift = specDrift();
|
|
809
|
+
for (const f of drift) copyFileSync(join(repo, "skills/kaizen", f), join(kaizenDir, f));
|
|
810
|
+
step(`${basename(process.cwd())}/.kaizen already set up${drift.length ? ` — refreshed ${drift.join(", ")}` : ""}`);
|
|
738
811
|
}
|
|
739
812
|
|
|
740
813
|
function initProject() {
|
package/cli/publish-gh.sh
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# Publish the same tarball to GitHub Packages, so the repo's "Packages" box lists
|
|
3
|
-
# it. GitHub only accepts names scoped to the repo owner, so the package goes there
|
|
4
|
-
# as @hfadhlullah/kaizen-agent while npm keeps kaizen-agent. Run after `bun publish`.
|
|
5
|
-
set -eu
|
|
6
|
-
cd "$(dirname "$0")/.."
|
|
7
|
-
token="${GITHUB_TOKEN:-$(gh auth token 2>/dev/null || true)}"
|
|
8
|
-
[ -n "$token" ] || { echo "publish-gh: no GitHub token (gh auth login, scope write:packages)"; exit 1; }
|
|
9
|
-
|
|
10
|
-
tmp="$(mktemp -d)"
|
|
11
|
-
trap 'rm -rf "$tmp"' EXIT
|
|
12
|
-
cp package.json "$tmp/package.json.orig"
|
|
13
|
-
bun -e '
|
|
14
|
-
const p = JSON.parse(await Bun.file("package.json").text());
|
|
15
|
-
p.name = "@hfadhlullah/kaizen-agent";
|
|
16
|
-
p.publishConfig = { registry: "https://npm.pkg.github.com" };
|
|
17
|
-
await Bun.write("package.json", JSON.stringify(p, null, 2) + "\n");
|
|
18
|
-
'
|
|
19
|
-
# Bun reads the project's own .npmrc; a temporary one carries the token and never
|
|
20
|
-
# lands in git (it is removed on exit, and .gitignore refuses it besides).
|
|
21
|
-
[ -e .npmrc ] && cp .npmrc "$tmp/npmrc.orig"
|
|
22
|
-
printf '//npm.pkg.github.com/:_authToken=%s\n@hfadhlullah:registry=https://npm.pkg.github.com\n' "$token" > .npmrc
|
|
23
|
-
# The scoped copy must not run postpublish: the tag and release already exist.
|
|
24
|
-
bun publish --ignore-scripts "$@" || status=$?
|
|
25
|
-
cp "$tmp/package.json.orig" package.json
|
|
26
|
-
if [ -e "$tmp/npmrc.orig" ]; then cp "$tmp/npmrc.orig" .npmrc; else rm -f .npmrc; fi
|
|
27
|
-
exit "${status:-0}"
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Publish the same tarball to GitHub Packages, so the repo's "Packages" box lists
|
|
3
|
+
# it. GitHub only accepts names scoped to the repo owner, so the package goes there
|
|
4
|
+
# as @hfadhlullah/kaizen-agent while npm keeps kaizen-agent. Run after `bun publish`.
|
|
5
|
+
set -eu
|
|
6
|
+
cd "$(dirname "$0")/.."
|
|
7
|
+
token="${GITHUB_TOKEN:-$(gh auth token 2>/dev/null || true)}"
|
|
8
|
+
[ -n "$token" ] || { echo "publish-gh: no GitHub token (gh auth login, scope write:packages)"; exit 1; }
|
|
9
|
+
|
|
10
|
+
tmp="$(mktemp -d)"
|
|
11
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
12
|
+
cp package.json "$tmp/package.json.orig"
|
|
13
|
+
bun -e '
|
|
14
|
+
const p = JSON.parse(await Bun.file("package.json").text());
|
|
15
|
+
p.name = "@hfadhlullah/kaizen-agent";
|
|
16
|
+
p.publishConfig = { registry: "https://npm.pkg.github.com" };
|
|
17
|
+
await Bun.write("package.json", JSON.stringify(p, null, 2) + "\n");
|
|
18
|
+
'
|
|
19
|
+
# Bun reads the project's own .npmrc; a temporary one carries the token and never
|
|
20
|
+
# lands in git (it is removed on exit, and .gitignore refuses it besides).
|
|
21
|
+
[ -e .npmrc ] && cp .npmrc "$tmp/npmrc.orig"
|
|
22
|
+
printf '//npm.pkg.github.com/:_authToken=%s\n@hfadhlullah:registry=https://npm.pkg.github.com\n' "$token" > .npmrc
|
|
23
|
+
# The scoped copy must not run postpublish: the tag and release already exist.
|
|
24
|
+
bun publish --ignore-scripts "$@" || status=$?
|
|
25
|
+
cp "$tmp/package.json.orig" package.json
|
|
26
|
+
if [ -e "$tmp/npmrc.orig" ]; then cp "$tmp/npmrc.orig" .npmrc; else rm -f .npmrc; fi
|
|
27
|
+
exit "${status:-0}"
|
package/cli/release.sh
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# Runs after `bun 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:-$(bun -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 only lists pull requests, and this repo merges none, so the
|
|
30
|
-
# notes are built here: the title from the release commit's subject, the body from
|
|
31
|
-
# every commit since the previous tag with its message, oldest first.
|
|
32
|
-
prev="$(git describe --tags --abbrev=0 "$tag^" 2>/dev/null || true)"
|
|
33
|
-
subject="$(git log -1 --format=%s "$tag" | sed -n 's/^Release [0-9.]*: //p')"
|
|
34
|
-
title="$tag${subject:+: $subject}"
|
|
35
|
-
notes="$(mktemp)"
|
|
36
|
-
git log --reverse --format='### %s%n%n%b' ${prev:+"$prev.."}"$tag" \
|
|
37
|
-
| grep -vE '^(Co-Authored-By|Claude-Session):' \
|
|
38
|
-
| grep -vE '^### Release [0-9.]+:' \
|
|
39
|
-
| cat -s > "$notes"
|
|
40
|
-
[ -n "$prev" ] && printf '\n**Full Changelog**: https://github.com/hfadhlullah/kaizen/compare/%s...%s\n' "$prev" "$tag" >> "$notes"
|
|
41
|
-
gh release create "$tag" --title "$title" --notes-file "$notes" >/dev/null 2>&1 \
|
|
42
|
-
&& echo "release: published $tag https://github.com/hfadhlullah/kaizen/releases/tag/$tag" \
|
|
43
|
-
|| echo "release: tag pushed, release not created"
|
|
44
|
-
rm -f "$notes"
|
|
45
|
-
exit 0
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Runs after `bun 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:-$(bun -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 only lists pull requests, and this repo merges none, so the
|
|
30
|
+
# notes are built here: the title from the release commit's subject, the body from
|
|
31
|
+
# every commit since the previous tag with its message, oldest first.
|
|
32
|
+
prev="$(git describe --tags --abbrev=0 "$tag^" 2>/dev/null || true)"
|
|
33
|
+
subject="$(git log -1 --format=%s "$tag" | sed -n 's/^Release [0-9.]*: //p')"
|
|
34
|
+
title="$tag${subject:+: $subject}"
|
|
35
|
+
notes="$(mktemp)"
|
|
36
|
+
git log --reverse --format='### %s%n%n%b' ${prev:+"$prev.."}"$tag" \
|
|
37
|
+
| grep -vE '^(Co-Authored-By|Claude-Session):' \
|
|
38
|
+
| grep -vE '^### Release [0-9.]+:' \
|
|
39
|
+
| cat -s > "$notes"
|
|
40
|
+
[ -n "$prev" ] && printf '\n**Full Changelog**: https://github.com/hfadhlullah/kaizen/compare/%s...%s\n' "$prev" "$tag" >> "$notes"
|
|
41
|
+
gh release create "$tag" --title "$title" --notes-file "$notes" >/dev/null 2>&1 \
|
|
42
|
+
&& echo "release: published $tag https://github.com/hfadhlullah/kaizen/releases/tag/$tag" \
|
|
43
|
+
|| echo "release: tag pushed, release not created"
|
|
44
|
+
rm -f "$notes"
|
|
45
|
+
exit 0
|
package/cli/settings.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } from
|
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// `text` is a value typed rather than chosen: a path. It has no values to cycle.
|
|
8
|
+
type Setting = { key: string; label: string; group: string; values: string[]; help: string; text?: boolean };
|
|
8
9
|
|
|
9
10
|
export type PresetName = "low" | "medium" | "ultra";
|
|
10
11
|
|
|
@@ -114,6 +115,8 @@ const SETTINGS: Setting[] = [
|
|
|
114
115
|
{ key: "subagents.effort", label: "Effort for stage agents", group: "Agents", values: ["inherit", "low", "medium", "high"], help: "How hard the stage agents think. low is faster and cheaper; high finds more. Claude Code only" },
|
|
115
116
|
{ key: "agent.default", label: "Tool the board launches", group: "Agents", values: ["auto", "claude", "codex", "agy", "opencode", "gemini"], help: "Coding tool opened when you start a run from the board. auto: Claude Code if installed, else the first one found" },
|
|
116
117
|
{ key: "dictation.model", label: "Dictation model", group: "Board", values: ["tiny", "base", "small", "large-v3-turbo"], help: "Whisper model the web board dictates with, in the browser. tiny ~60 MB rough; base ~165 MB English-leaning; small ~410 MB accurate across languages; large-v3-turbo ~1.5 GB best, needs a capable GPU. Downloaded once on the next mic press" },
|
|
118
|
+
{ key: "sources.every", label: "Gather requests", group: "Board", values: ["off", "1h", "6h", "24h"], help: "How often the board checks your request sources for new ideas while it is running. off: only when you press Gather now" },
|
|
119
|
+
{ key: "board.projects_dir", label: "Projects folder", group: "Board", values: [], text: true, help: "A folder your projects live in, e.g. ~/Projects. Every folder inside it appears in the board's project list, ready to choose; choosing one sets it up. Empty: only projects kaizen already knows" },
|
|
117
120
|
];
|
|
118
121
|
|
|
119
122
|
// The same rows and the same write the TUI uses, for `kaizen web`'s settings panel:
|
|
@@ -127,14 +130,14 @@ export function listSettings(repoRoot: string) {
|
|
|
127
130
|
file: tilde(file),
|
|
128
131
|
rows: SETTINGS.map((s) => ({
|
|
129
132
|
...s,
|
|
130
|
-
value: s.key === "preset" ? preset : (read(text, s.key) || read(defaults, s.key)) ?? s.values[0]
|
|
133
|
+
value: s.key === "preset" ? preset : (read(text, s.key) || read(defaults, s.key)) ?? s.values[0] ?? "",
|
|
131
134
|
})),
|
|
132
135
|
};
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
export function setSetting(repoRoot: string, key: string, value: string) {
|
|
136
139
|
const s = SETTINGS.find((x) => x.key === key);
|
|
137
|
-
if (!s || !s.values.includes(value)) return { error: "unknown setting or value" };
|
|
140
|
+
if (!s || !(s.text || s.values.includes(value))) return { error: "unknown setting or value" };
|
|
138
141
|
const file = locate();
|
|
139
142
|
const defaultsPath = join(repoRoot, "skills/kaizen/config.default.yml");
|
|
140
143
|
if (!existsSync(file)) { mkdirSync(dirname(file), { recursive: true }); copyFileSync(defaultsPath, file); }
|
|
@@ -234,6 +237,7 @@ export async function settings(repoRoot: string, standalone = true) {
|
|
|
234
237
|
function cycle(step: number) {
|
|
235
238
|
const s = SETTINGS[active]!;
|
|
236
239
|
const wasMouse = s.key === "ui.mouse";
|
|
240
|
+
if (s.text) { saved = c.dim(`type ${s.key} in the board's settings, or in ${tilde(file)}`); return; }
|
|
237
241
|
const text = readFileSync(file, "utf8");
|
|
238
242
|
if (s.key === "preset") {
|
|
239
243
|
const current = detectPreset(text, defaults);
|