kaizen-agent 1.7.3 → 1.7.5

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.
Files changed (39) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +658 -657
  3. package/agents/kaizen-builder.md +77 -77
  4. package/agents/kaizen-planner.md +250 -250
  5. package/agents/kaizen-reviewer.md +127 -127
  6. package/cli/dashboard.ts +1187 -1187
  7. package/cli/install.ts +947 -947
  8. package/cli/publish-gh.sh +27 -27
  9. package/cli/release.sh +45 -45
  10. package/cli/settings.ts +416 -415
  11. package/cli/state.test.ts +197 -197
  12. package/cli/state.ts +704 -704
  13. package/cli/web.ts +423 -423
  14. package/commands/kaizen-abort.md +5 -5
  15. package/commands/kaizen-approve.md +5 -5
  16. package/commands/kaizen-auto.md +6 -6
  17. package/commands/kaizen-backlog.md +5 -5
  18. package/commands/kaizen-config.md +5 -5
  19. package/commands/kaizen-full.md +5 -5
  20. package/commands/kaizen-init.md +5 -5
  21. package/commands/kaizen-install.md +5 -5
  22. package/commands/kaizen-lite.md +5 -5
  23. package/commands/kaizen-plan.md +6 -6
  24. package/commands/kaizen-reject.md +6 -6
  25. package/commands/kaizen-review.md +6 -6
  26. package/commands/kaizen-run.md +5 -5
  27. package/commands/kaizen-status.md +5 -5
  28. package/package.json +1 -1
  29. package/skills/kaizen/README.md +281 -281
  30. package/skills/kaizen/SKILL.md +440 -440
  31. package/skills/kaizen/adapters.md +77 -77
  32. package/skills/kaizen/config.default.yml +178 -167
  33. package/skills/kaizen/spec-build.md +119 -119
  34. package/skills/kaizen/spec-main.md +104 -104
  35. package/skills/kaizen/spec-plan.md +201 -201
  36. package/skills/kaizen/spec-review.md +121 -121
  37. package/skills/kaizen/spec.md +153 -153
  38. package/skills/kaizen-help/SKILL.md +129 -129
  39. package/web/board.html +47 -23
@@ -1,129 +1,129 @@
1
- ---
2
- name: kaizen-help
3
- description: >
4
- Quick-reference card for the kaizen workflow: commands, modes, approvals,
5
- fix loop, config knobs, state layout, backlog/status rules. One-shot
6
- display, not a persistent mode. Trigger: /kaizen-help, "kaizen help",
7
- "what kaizen commands", "kaizen cheat sheet".
8
- ---
9
-
10
- # Kaizen Help
11
-
12
- **Print the content of this card into your reply.** The user cannot see this file —
13
- it loads into your context only, so summarizing it or saying "see above" shows them
14
- nothing. Output the tables themselves.
15
-
16
- One-shot: do NOT change kaizen's active mode, write to `state.json`, or persist
17
- anything.
18
-
19
- ## Commands
20
-
21
- | Invocation | Action |
22
- |---|---|
23
- | `/kaizen <request>` | Start a new run in the configured mode (default: `approve`) |
24
- | `/kaizen plan <request>` | Start a run and stop after the plan (`plan-only`) |
25
- | `/kaizen auto <request>` | Start a run in `auto` mode: every stage back to back, no approval stops |
26
- | `/kaizen lite <request>` | Run the stages in this session — cheap, reviewer has seen the work |
27
- | `/kaizen full <request>` | Dispatch each stage as its own cold agent |
28
- | `/kaizen run` | Resume the current run and execute the approved plan |
29
- | `/kaizen review [target]` | Review-only: audit existing code, no plan, no implementation |
30
- | `/kaizen status` | List every run grouped waiting-on-you / in flight / done / abandoned, plus open backlog count |
31
- | `/kaizen backlog` | Print open backlog items across all runs, grouped by source run, rescued items last under `Orphaned` |
32
- | `/kaizen approve` | Approve whatever the current run is waiting on |
33
- | `/kaizen reject <reason>` | Reject it; the reason decides revise (back to planner) vs kill (abandon) |
34
- | `/kaizen abort` | Mark the current run abandoned |
35
- | `/kaizen config` | Show this project's settings and change them, one picker per setting |
36
- | `/kaizen init` | Set up this project folder: `.kaizen/` with `spec.md`, config, and — in a git repo — the gitignore entry |
37
- | `/kaizen install` | Install the workflow itself globally or per-project |
38
-
39
- `/kaizen` with no argument: `status` if a run is in progress, else ask what to build.
40
-
41
- ## Modes
42
-
43
- Set by `mode` in config, overridable per invocation.
44
-
45
- | Mode | What it does |
46
- |---|---|
47
- | `plan-only` | Produce the plan, write it, stop. No code touched. |
48
- | `approve` (default) | Stop at every enabled approval, wait for an explicit decision. |
49
- | `auto` | Run every stage back to back, no approval, report at the end. Fix loop still respects `max_iterations`. |
50
- | `review-only` | Skip plan and implementation, run the reviewer against existing code or a diff. |
51
-
52
- Mode never overrides a refusal: destructive/irreversible actions still confirm even in `auto`.
53
-
54
- ## Approvals
55
-
56
- - `approvals.plan` — after the plan, before any code is written.
57
- - `approvals.review` — after the review passes, before the work is called done.
58
- - `approvals.each_file` — confirm each file edit individually. Off by default; slow.
59
-
60
- A rejection is written to the run directory, and is one of two things — the reason
61
- says which. **Revise** ("do it differently") goes back to the planner, which produces
62
- a new plan and returns to the same approval. **Kill** ("not doing this") means the
63
- request itself was wrong: set `stage: "abandoned"` and stop, nothing to revise toward.
64
-
65
- ## Fix loop
66
-
67
- - `auto_fix.enabled: false` — every finding stops the run, becomes a user decision.
68
- - `auto_fix.enabled: true` — builder fixes findings at or above `auto_fix.min_severity`,
69
- reviewer re-checks, up to `auto_fix.max_iterations` rounds. Findings below threshold,
70
- and anything still open when iterations run out, are reported to the user.
71
-
72
- Each iteration is written to `runs/<id>/05-iterations/`.
73
-
74
- ## Config knobs (`config.default.yml` defaults)
75
-
76
- | Key | Default | Meaning |
77
- |---|---|---|
78
- | `preset` | `medium` | Quick preset (`low`, `medium`, `ultra`, `custom`) |
79
- | `mode` | `approve` | Default run mode |
80
- | `runner` | `full` | `full`: each stage a cold subagent. `lite`: this session runs every stage |
81
- | `approvals.plan` | `true` | Stop after the plan |
82
- | `approvals.review` | `false` | Stop after the review |
83
- | `approvals.each_file` | `false` | Confirm every file edit |
84
- | `build.executor` | `subagent` | Who builds: `subagent`, `inline` (main thread), or `ask` at the approval |
85
- | `auto_fix.enabled` | `true` | Auto-fix findings vs. escalate everything |
86
- | `auto_fix.min_severity` | `high` | Threshold for auto-fix (`critical\|high\|medium\|low`) |
87
- | `auto_fix.max_iterations` | `2` | Fix/recheck rounds before escalating remaining findings |
88
- | `track.default` | `auto` (inferred per run) | Pin a track for a repo that is always one kind of work |
89
- | `review.write_memory` | `true` | Let the reviewer append lessons to `.kaizen/memory.md` |
90
- | `backlog.enabled` | `true` | Capture what a run deliberately did not do |
91
- | `backlog.file` | `06-backlog.md` | Per-run backlog file |
92
- | `backlog.orphan_file` | `backlog.md` | Where a pruned run's open items are rescued to |
93
- | `state.dir` | `.kaizen` | State directory |
94
- | `state.keep_runs` | `20` | Completed runs kept before pruning oldest (rescues open backlog first) |
95
- | `git.auto_commit` | `false` | Never commit/push/PR unless the user explicitly asks |
96
- | `git.branch_before_implement` | `true` | Branch before implement stage when on the default branch |
97
- | `subagents.model` | `inherit` | Model the stage subagents run on (opus/sonnet/haiku, Claude Code only) |
98
- | `subagents.effort` | `inherit` | Reasoning effort for the stage subagents (low/medium/high, Claude Code only) |
99
- | `agent.default` | `auto` | Coding agent to launch from dashboard backlog |
100
- | `agent.<cmd>.model`, `.effort` | — | Model/effort flags for the launched session, per tool (`claude`, `codex`, `agy`, `opencode`, `gemini`); model names are the tool's own |
101
-
102
- `.kaizen/config.yml` overrides only the keys it sets; everything else falls back to
103
- this skill's `config.default.yml`.
104
-
105
- ## State layout
106
-
107
- ```
108
- .kaizen/{spec.md, config.yml, memory.md, backlog.md (orphanage)}
109
- .kaizen/runs/<id>/{00-request,01-plan,02-approval,03-impl,04-review}.md,
110
- 05-iterations/, 06-backlog.md, state.json (id, mode, stage, awaiting, iteration, updated)
111
- ```
112
-
113
- ## Backlog and status
114
-
115
- Backlog items: `- <status>: <item text>`, `<status>` one of `open`, `done`, `rejected`
116
- (`rejected` carries a reason after `|`). `/kaizen backlog`/`status` are **read-only** —
117
- never edit `06-backlog.md` or `state.json`, even to fix a spotted inconsistency; report
118
- it and ask.
119
-
120
- `/kaizen status` groups every run by `state.json` at read time: **Waiting on you**
121
- (awaiting non-null, not abandoned), **In flight** (awaiting null, not done/abandoned),
122
- **Done**, **Abandoned**, plus `Backlog: <n> open` (all runs + orphanage; abandoned
123
- runs' items excluded).
124
-
125
- ## More
126
-
127
- Full walkthrough and rationale: `~/.claude/skills/kaizen/README.md`. Full stage
128
- contract: `~/.claude/skills/kaizen/spec.md`. Codex/Antigravity porting:
129
- `~/.claude/skills/kaizen/adapters.md`.
1
+ ---
2
+ name: kaizen-help
3
+ description: >
4
+ Quick-reference card for the kaizen workflow: commands, modes, approvals,
5
+ fix loop, config knobs, state layout, backlog/status rules. One-shot
6
+ display, not a persistent mode. Trigger: /kaizen-help, "kaizen help",
7
+ "what kaizen commands", "kaizen cheat sheet".
8
+ ---
9
+
10
+ # Kaizen Help
11
+
12
+ **Print the content of this card into your reply.** The user cannot see this file —
13
+ it loads into your context only, so summarizing it or saying "see above" shows them
14
+ nothing. Output the tables themselves.
15
+
16
+ One-shot: do NOT change kaizen's active mode, write to `state.json`, or persist
17
+ anything.
18
+
19
+ ## Commands
20
+
21
+ | Invocation | Action |
22
+ |---|---|
23
+ | `/kaizen <request>` | Start a new run in the configured mode (default: `approve`) |
24
+ | `/kaizen plan <request>` | Start a run and stop after the plan (`plan-only`) |
25
+ | `/kaizen auto <request>` | Start a run in `auto` mode: every stage back to back, no approval stops |
26
+ | `/kaizen lite <request>` | Run the stages in this session — cheap, reviewer has seen the work |
27
+ | `/kaizen full <request>` | Dispatch each stage as its own cold agent |
28
+ | `/kaizen run` | Resume the current run and execute the approved plan |
29
+ | `/kaizen review [target]` | Review-only: audit existing code, no plan, no implementation |
30
+ | `/kaizen status` | List every run grouped waiting-on-you / in flight / done / abandoned, plus open backlog count |
31
+ | `/kaizen backlog` | Print open backlog items across all runs, grouped by source run, rescued items last under `Orphaned` |
32
+ | `/kaizen approve` | Approve whatever the current run is waiting on |
33
+ | `/kaizen reject <reason>` | Reject it; the reason decides revise (back to planner) vs kill (abandon) |
34
+ | `/kaizen abort` | Mark the current run abandoned |
35
+ | `/kaizen config` | Show this project's settings and change them, one picker per setting |
36
+ | `/kaizen init` | Set up this project folder: `.kaizen/` with `spec.md`, config, and — in a git repo — the gitignore entry |
37
+ | `/kaizen install` | Install the workflow itself globally or per-project |
38
+
39
+ `/kaizen` with no argument: `status` if a run is in progress, else ask what to build.
40
+
41
+ ## Modes
42
+
43
+ Set by `mode` in config, overridable per invocation.
44
+
45
+ | Mode | What it does |
46
+ |---|---|
47
+ | `plan-only` | Produce the plan, write it, stop. No code touched. |
48
+ | `approve` (default) | Stop at every enabled approval, wait for an explicit decision. |
49
+ | `auto` | Run every stage back to back, no approval, report at the end. Fix loop still respects `max_iterations`. |
50
+ | `review-only` | Skip plan and implementation, run the reviewer against existing code or a diff. |
51
+
52
+ Mode never overrides a refusal: destructive/irreversible actions still confirm even in `auto`.
53
+
54
+ ## Approvals
55
+
56
+ - `approvals.plan` — after the plan, before any code is written.
57
+ - `approvals.review` — after the review passes, before the work is called done.
58
+ - `approvals.each_file` — confirm each file edit individually. Off by default; slow.
59
+
60
+ A rejection is written to the run directory, and is one of two things — the reason
61
+ says which. **Revise** ("do it differently") goes back to the planner, which produces
62
+ a new plan and returns to the same approval. **Kill** ("not doing this") means the
63
+ request itself was wrong: set `stage: "abandoned"` and stop, nothing to revise toward.
64
+
65
+ ## Fix loop
66
+
67
+ - `auto_fix.enabled: false` — every finding stops the run, becomes a user decision.
68
+ - `auto_fix.enabled: true` — builder fixes findings at or above `auto_fix.min_severity`,
69
+ reviewer re-checks, up to `auto_fix.max_iterations` rounds. Findings below threshold,
70
+ and anything still open when iterations run out, are reported to the user.
71
+
72
+ Each iteration is written to `runs/<id>/05-iterations/`.
73
+
74
+ ## Config knobs (`config.default.yml` defaults)
75
+
76
+ | Key | Default | Meaning |
77
+ |---|---|---|
78
+ | `preset` | `medium` | Quick preset (`low`, `medium`, `ultra`, `custom`) |
79
+ | `mode` | `approve` | Default run mode |
80
+ | `runner` | `full` | `full`: each stage a cold subagent. `lite`: this session runs every stage |
81
+ | `approvals.plan` | `true` | Stop after the plan |
82
+ | `approvals.review` | `false` | Stop after the review |
83
+ | `approvals.each_file` | `false` | Confirm every file edit |
84
+ | `build.executor` | `subagent` | Who builds: `subagent`, `inline` (main thread), or `ask` at the approval |
85
+ | `auto_fix.enabled` | `true` | Auto-fix findings vs. escalate everything |
86
+ | `auto_fix.min_severity` | `high` | Threshold for auto-fix (`critical\|high\|medium\|low`) |
87
+ | `auto_fix.max_iterations` | `2` | Fix/recheck rounds before escalating remaining findings |
88
+ | `track.default` | `auto` (inferred per run) | Pin a track for a repo that is always one kind of work |
89
+ | `review.write_memory` | `true` | Let the reviewer append lessons to `.kaizen/memory.md` |
90
+ | `backlog.enabled` | `true` | Capture what a run deliberately did not do |
91
+ | `backlog.file` | `06-backlog.md` | Per-run backlog file |
92
+ | `backlog.orphan_file` | `backlog.md` | Where a pruned run's open items are rescued to |
93
+ | `state.dir` | `.kaizen` | State directory |
94
+ | `state.keep_runs` | `20` | Completed runs kept before pruning oldest (rescues open backlog first) |
95
+ | `git.auto_commit` | `false` | Never commit/push/PR unless the user explicitly asks |
96
+ | `git.branch_before_implement` | `true` | Branch before implement stage when on the default branch |
97
+ | `subagents.model` | `inherit` | Model the stage subagents run on (opus/sonnet/haiku, Claude Code only) |
98
+ | `subagents.effort` | `inherit` | Reasoning effort for the stage subagents (low/medium/high, Claude Code only) |
99
+ | `agent.default` | `auto` | Coding agent to launch from dashboard backlog |
100
+ | `agent.<cmd>.model`, `.effort` | — | Model/effort flags for the launched session, per tool (`claude`, `codex`, `agy`, `opencode`, `gemini`); model names are the tool's own |
101
+
102
+ `.kaizen/config.yml` overrides only the keys it sets; everything else falls back to
103
+ this skill's `config.default.yml`.
104
+
105
+ ## State layout
106
+
107
+ ```
108
+ .kaizen/{spec.md, config.yml, memory.md, backlog.md (orphanage)}
109
+ .kaizen/runs/<id>/{00-request,01-plan,02-approval,03-impl,04-review}.md,
110
+ 05-iterations/, 06-backlog.md, state.json (id, mode, stage, awaiting, iteration, updated)
111
+ ```
112
+
113
+ ## Backlog and status
114
+
115
+ Backlog items: `- <status>: <item text>`, `<status>` one of `open`, `done`, `rejected`
116
+ (`rejected` carries a reason after `|`). `/kaizen backlog`/`status` are **read-only** —
117
+ never edit `06-backlog.md` or `state.json`, even to fix a spotted inconsistency; report
118
+ it and ask.
119
+
120
+ `/kaizen status` groups every run by `state.json` at read time: **Waiting on you**
121
+ (awaiting non-null, not abandoned), **In flight** (awaiting null, not done/abandoned),
122
+ **Done**, **Abandoned**, plus `Backlog: <n> open` (all runs + orphanage; abandoned
123
+ runs' items excluded).
124
+
125
+ ## More
126
+
127
+ Full walkthrough and rationale: `~/.claude/skills/kaizen/README.md`. Full stage
128
+ contract: `~/.claude/skills/kaizen/spec.md`. Codex/Antigravity porting:
129
+ `~/.claude/skills/kaizen/adapters.md`.
package/web/board.html CHANGED
@@ -28,8 +28,10 @@ pre.mark span:nth-child(1){color:var(--ink-hi)}pre.mark span:nth-child(2){color:
28
28
  .ib.sm{width:26px;height:26px}.ib.sm svg{width:15px;height:15px}
29
29
  /* dictation mic: accent while listening (pulsing) or still transcribing; digits while the model downloads */
30
30
  /* the field being dictated into: accent border and a glow that swells with the voice level (--lvl, 0..1) */
31
- .dictating{border-color:var(--accent)!important;box-shadow:0 0 0 calc(2px + 6px*var(--lvl,0)) color-mix(in srgb,var(--accent) 35%,transparent);transition:box-shadow .08s}
32
- .ib.mic.on{color:var(--accent);animation:pl 1.6s infinite}.ib.mic.busy{color:var(--accent)}.ib.mic i{font-style:normal;font-size:10px;font-variant-numeric:tabular-nums}.row .mic{margin-right:auto;align-self:center}
31
+ .dictating{outline:none;border-color:var(--accent)!important;box-shadow:0 0 calc(6px + 16px*var(--lvl,0)) calc(1px + 3px*var(--lvl,0)) color-mix(in srgb,var(--accent) 45%,transparent);transition:box-shadow .08s}
32
+ /* the mic button turns into a level meter while listening: five bars, each weighted so the set reads as a wave */
33
+ .vu{display:inline-flex;gap:2px;align-items:center;height:100%}.vu i{width:2px;border-radius:1px;background:currentColor;height:calc(3px + 12px*min(1,var(--lvl,0)*var(--w)));transition:height .08s}
34
+ .ib.mic.on{color:var(--accent)}.ib.mic.busy{color:var(--accent)}.ib.mic i{font-style:normal;font-size:10px;font-variant-numeric:tabular-nums}.row .mic{margin-right:auto;align-self:center}
33
35
  .btn{border:1px solid var(--line);border-radius:var(--r);padding:5px 12px;color:var(--ink);background:var(--bg-2);min-height:30px;font-size:13px}
34
36
  .btn:hover{border-color:var(--dim)}
35
37
  .btn.pri{background:var(--accent);border-color:var(--accent);color:#fff}
@@ -147,18 +149,22 @@ aside .foot:empty{display:none}
147
149
  .find input{margin-top:4px}
148
150
  .find .sev{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--warn)}.find .sev.hi{color:var(--danger)}
149
151
  .find .loc{color:var(--dim);font-size:12px;margin-left:8px}
150
- .set{display:grid;grid-template-columns:auto 1fr;gap:10px 20px;align-items:center;font-size:13px}
151
- .set label{color:var(--dim)}
152
- .set .opts{display:flex;flex-wrap:wrap;gap:4px}
153
- .set .opts button{border:1px solid var(--line);border-radius:var(--r);padding:3px 10px;color:var(--dim)}
154
- .set .opts button.on{color:var(--ink);border-color:var(--ink)}
152
+ /* settings: the TUI screen, one row per setting — label, then the value in its own grammar */
153
+ .sg{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--dim);margin:18px 0 6px;font-weight:600}.sg:first-child{margin-top:0}
154
+ .srow{display:grid;grid-template-columns:200px 1fr;gap:0 16px;align-items:center;font-size:13px;padding:4px 8px;margin:0 -8px;border-radius:var(--r)}
155
+ .srow:hover,.srow:focus-within{background:color-mix(in srgb,var(--dim) 8%,transparent)}
156
+ .srow label{color:var(--dim)}
157
+ .srow .hint{display:none;grid-column:1/-1;margin:6px 0 2px;font-size:12px;color:var(--dim);line-height:1.45;max-width:60ch}.srow:hover .hint,.srow:focus-within .hint{display:block}.srow .hint code{background:none;padding:0;color:var(--dim)}
158
+ .srow .sel{padding:2px 6px;font-size:13px;justify-self:start;border-color:transparent}.srow:hover .sel,.srow .sel:focus{border-color:var(--line)}
159
+ .chk{justify-self:start;color:var(--ink);padding:2px 0;font-variant-numeric:tabular-nums}.chk:not(.on){color:var(--dim)}.chk i{font-style:normal;color:var(--ink)}
160
+ .bar{display:inline-flex;align-items:center;gap:2px;justify-self:start}.bar button{width:10px;height:14px;border:1px solid var(--dim);border-radius:2px;background:none}.bar button.on{background:var(--ink);border-color:var(--ink)}.bar button:hover{border-color:var(--accent)}.bar b{font-weight:400;margin-left:6px;font-variant-numeric:tabular-nums}
155
161
  .toast{position:fixed;z-index:12;left:24px;bottom:52px;background:var(--ink);color:var(--bg);border-radius:var(--r);padding:8px 14px;font-size:13px;opacity:0;transition:opacity .2s;pointer-events:none}
156
162
  .toast.on{opacity:1}
157
163
  .help{position:fixed;inset:0;z-index:12;display:none;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bg) 70%,transparent)}
158
164
  .help.on{display:flex}.help div{background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r);padding:20px 24px;line-height:2}
159
165
  .help kbd{display:inline-block;width:7ch;color:var(--accent)}
160
166
  @media(max-width:900px){main{grid-template-columns:1fr;padding-inline:16px;gap:8px}.hdr,.strip,.ftr{padding-inline:16px}section h2{position:sticky;top:0;background:var(--bg);z-index:1}.more{display:grid}.card:hover .age{display:inline}}
161
- @media(prefers-reduced-motion:reduce){aside,.toast,.mascot,.d-run .pulse,.ib.mic.on,.dictating{transition:none;animation:none}}
167
+ @media(prefers-reduced-motion:reduce){aside,.toast,.mascot,.d-run .pulse,.ib.mic.on,.dictating,.vu i{transition:none;animation:none}}
162
168
  </style>
163
169
  </head>
164
170
  <body>
@@ -227,15 +233,17 @@ const ico=n=>`<svg><use href="#i-${n}"/></svg>`;
227
233
 
228
234
  // ---- dictation: Whisper in the page. Audio never leaves the browser; the model is
229
235
  // fetched once from huggingface.co and cached. Library and model are pinned here.
230
- const TFJS="https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0",ASR_MODEL="onnx-community/whisper-base";
236
+ const TFJS="https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0",ASR_MODELS={tiny:"onnx-community/whisper-tiny",base:"onnx-community/whisper-base",small:"onnx-community/whisper-small","large-v3-turbo":"onnx-community/whisper-large-v3-turbo"};
237
+ // The model is a setting (dictation.model); the worker is rebuilt when it changes.
238
+ let asrModel=null;
231
239
  // Inference lives in a module Worker so a slow chunk never freezes the board.
232
240
  const WORKER_SRC=`import{pipeline}from"${TFJS}";let asr=null;
233
241
  onmessage=async e=>{const m=e.data;try{
234
242
  if(m.type==="load"){const prog=p=>{if(p.status==="progress")postMessage({type:"progress",file:p.file,pct:p.progress||0})};
235
243
  // WebGPU first, with the encoder in fp16 only where the adapter has shader-f16; any GPU failure falls back to the CPU.
236
244
  const gpu=m.webgpu?{device:"webgpu",dtype:{encoder_model:m.f16?"fp16":"fp32",decoder_model_merged:"q4"}}:null,cpu={device:"wasm",dtype:"q8"};
237
- try{if(!gpu)throw 0;asr=await pipeline("automatic-speech-recognition","${ASR_MODEL}",{...gpu,progress_callback:prog})}
238
- catch(e){if(gpu)postMessage({type:"fallback",why:String(e&&e.message||e)});asr=await pipeline("automatic-speech-recognition","${ASR_MODEL}",{...cpu,progress_callback:prog})}
245
+ try{if(!gpu)throw 0;asr=await pipeline("automatic-speech-recognition",m.model,{...gpu,progress_callback:prog})}
246
+ catch(e){if(gpu)postMessage({type:"fallback",why:String(e&&e.message||e)});asr=await pipeline("automatic-speech-recognition",m.model,{...cpu,progress_callback:prog})}
239
247
  postMessage({type:"ready"})}
240
248
  if(m.type==="run"){const r=await asr(m.audio,{chunk_length_s:30,return_timestamps:false});postMessage({type:"text",sid:m.sid,seq:m.seq,text:(r.text||"").trim()})}
241
249
  }catch(err){postMessage({type:"error",sid:m.sid,seq:m.seq,error:String(err&&err.message||err)})}}`;
@@ -245,13 +253,18 @@ const HAS_MIC=!!(navigator.mediaDevices&&navigator.mediaDevices.getUserMedia&&wi
245
253
  // `active` is the one session listening; `sessions` also holds stopped ones still waiting on a chunk.
246
254
  let asrWorker=null,asrReady=null,asrProgress=()=>{},active=null,starting=false;const sessions=new Set();
247
255
  // One worker for the page; loading is awaited by whoever pressed first, later presses join.
248
- function loadAsr(){if(asrReady)return asrReady;asrReady=new Promise(async(res,rej)=>{
256
+ // Loads the model named in settings; a model changed since the last load replaces the worker.
257
+ async function loadAsr(){
258
+ let want="small";try{const cfg=await fetch("/settings").then(r=>r.json());want=(cfg.rows.find(r=>r.key==="dictation.model")||{}).value||want}catch(e){}
259
+ if(!ASR_MODELS[want])want="small";
260
+ if(asrReady&&asrModel!==want){try{asrWorker.terminate()}catch(e){}asrWorker=null;asrReady=null;sessions.clear();active=null}
261
+ if(asrReady)return asrReady;asrModel=want;asrReady=new Promise(async(res,rej)=>{
249
262
  asrWorker=new Worker(URL.createObjectURL(new Blob([WORKER_SRC],{type:"text/javascript"})),{type:"module"});
250
263
  const files={};asrWorker.onmessage=e=>{const m=e.data;if(m.type==="progress"){files[m.file]=m.pct;const v=Object.values(files);asrProgress(Math.round(v.reduce((a,b)=>a+b,0)/v.length))}else if(m.type==="ready")res();else if(m.type==="fallback")toast("GPU could not run the model, using the CPU: "+m.why);else if(m.type==="error"&&m.seq==null)rej(new Error(m.error));for(const s of sessions)s.onmsg(m)};
251
264
  asrWorker.onerror=e=>rej(new Error(e.message||"worker failed"));
252
265
  // Ask the adapter what it can do: no adapter means the CPU path; no shader-f16 means an fp32 encoder.
253
266
  let adapter=null;try{adapter=navigator.gpu&&await navigator.gpu.requestAdapter()}catch(e){}
254
- if(!adapter)toast("No WebGPU: dictation will run on the CPU and be slower");asrWorker.postMessage({type:"load",webgpu:!!adapter,f16:!!(adapter&&adapter.features.has("shader-f16"))})});
267
+ if(!adapter)toast("No WebGPU: dictation will run on the CPU and be slower");asrWorker.postMessage({type:"load",model:ASR_MODELS[asrModel],webgpu:!!adapter,f16:!!(adapter&&adapter.features.has("shader-f16"))})});
255
268
  asrReady.catch(()=>{asrReady=null;try{asrWorker.terminate()}catch(e){}asrWorker=null});return asrReady}
256
269
  // After a re-render only a listener whose field is gone is stopped; closing the panel stops any.
257
270
  function stopDictation(all){if(active&&(all||!active.field.isConnected))active.stop()}
@@ -275,20 +288,21 @@ function mic(fields){
275
288
  // Text lands where the caret is when each chunk arrives (or after the last insertion when the
276
289
  // field is not focused), so typing while the mic is on is kept.
277
290
  const f=target;let at=f.selectionStart??f.value.length;
291
+ const vu=document.createElement("span");vu.className="vu";vu.setAttribute("aria-hidden","true");vu.innerHTML=[.5,.9,1.3,.8,.45].map(w=>`<i style="--w:${w}"></i>`).join("");
278
292
  let buf=[],len=0,voiced=0,quiet=0,seq=0,done=0,pending=new Map(),stopping=false;
279
293
  const SR=16000,MAX=6*SR,SIL=0.8*SR,MIN=1*SR;
280
294
  const insert=t=>{if(!t||!f.isConnected)return;const v=f.value;if(document.activeElement===f)at=f.selectionStart;at=Math.min(at,v.length);const head=v.slice(0,at),sep=head&&!/\s$/.test(head)?" ":"";f.value=head+sep+t+v.slice(at);at=head.length+sep.length+t.length;f.selectionStart=f.selectionEnd=at;f.dispatchEvent(new Event("input",{bubbles:true}))};
281
295
  const flush=()=>{if(!len||voiced<MIN){buf=[];len=0;voiced=0;quiet=0;return}const a=new Float32Array(len);let o=0;for(const c of buf){a.set(c,o);o+=c.length}buf=[];len=0;voiced=0;quiet=0;pending.set(seq,null);asrWorker.postMessage({type:"run",sid,seq,audio:a},[a.buffer]);seq++;b.classList.add("busy")};
282
296
  const drain=()=>{while(pending.has(done)&&pending.get(done)!==null){insert(pending.get(done));pending.delete(done);done++}if(!pending.size){b.classList.remove("busy");if(stopping){idle();sessions.delete(sess)}}};
283
297
  tap.port.onmessage=e=>{const c=e.data;buf.push(c);len+=c.length;let s=0;for(let i=0;i<c.length;i++)s+=c[i]*c[i];const rms=Math.sqrt(s/c.length);
284
- if(rms>0.01){voiced+=c.length;quiet=0}else quiet+=c.length;f.style.setProperty("--lvl",Math.min(1,rms*8).toFixed(2));
298
+ if(rms>0.01){voiced+=c.length;quiet=0}else quiet+=c.length;{const l=Math.min(1,rms*8).toFixed(2);f.style.setProperty("--lvl",l);vu.style.setProperty("--lvl",l)}
285
299
  if(len>=MAX||(voiced>=MIN&&quiet>=SIL))flush()};
286
300
  // Chunk seqs are per session, so a result is matched by session id too.
287
301
  const sid=Math.random().toString(36).slice(2);
288
302
  const sess={btn:b,field:f,onmsg:m=>{if(m.sid!==sid)return;if(m.type==="text"){pending.set(m.seq,m.text);drain()}else if(m.type==="error"&&m.seq!=null){pending.set(m.seq,"");toast("Dictation failed: "+m.error);drain()}},
289
- stop:()=>{if(stopping)return;stopping=true;b.classList.remove("on");f.classList.remove("dictating");f.style.removeProperty("--lvl");try{tap.port.onmessage=null;src.disconnect();tap.disconnect();stream.getTracks().forEach(t=>t.stop());ctx.close()}catch(e){}active=null;flush();drain()}};
303
+ stop:()=>{if(stopping)return;stopping=true;b.classList.remove("on");f.classList.remove("dictating");f.style.removeProperty("--lvl");b.innerHTML=ico("mic");try{tap.port.onmessage=null;src.disconnect();tap.disconnect();stream.getTracks().forEach(t=>t.stop());ctx.close()}catch(e){}active=null;flush();drain()}};
290
304
  sessions.add(sess);active=sess;starting=false;
291
- b.classList.remove("busy");b.classList.add("on");f.classList.add("dictating");f.focus()};
305
+ b.classList.remove("busy");b.classList.add("on");f.classList.add("dictating");b.replaceChildren(vu);f.focus()};
292
306
  return b}
293
307
  const m=60e3,h=3600e3,d=86400e3;
294
308
  const age=(t,now)=>{const x=now-t;return x<h?Math.round(x/m)+"m":x<d?Math.round(x/h)+"h":Math.round(x/d)+"d"};
@@ -480,19 +494,29 @@ function md(src){
480
494
  endList();return out.join("")}
481
495
 
482
496
  // ---- settings, toast, notifications, keys
497
+ // Rows in config order, grouped as the TUI groups them.
498
+ const groups=rows=>{const m=new Map();for(const r of rows){if(!m.has(r.group))m.set(r.group,[]);m.get(r.group).push(r)}return [...m]};
483
499
  async function openSettings(){
484
500
  stopDictation(true);
485
501
  openKey=null;runDoc=null;$("panel").classList.add("open");$("scrim").classList.add("on");$("ptitle").textContent="Settings";$("ptabs").innerHTML="";$("pfoot").innerHTML="";
486
502
  const cur=document.documentElement.dataset.theme||"system";
487
- const opts=(id,vals,on)=>`<div class="opts" id="${id}">${vals.map(v=>`<button class="${v===on?"on":""}" data-v="${v}" ${v===""?'title="empty"':""}>${v===""?"—":v}</button>`).join("")}</div>`;
488
503
  const cfg=await fetch("/settings").then(r=>r.json()).catch(()=>null);
489
504
  $("psub").textContent=cfg?`theme is this browser's; the rest is ${cfg.file}`:"theme is this browser's";
490
- $("pbody").innerHTML=`<div class="set"><label>Theme</label>${opts("th",["system","light","dark"],cur)}
491
- <label>Notifications</label><div class="opts" id="nt"><button>${typeof Notification==="undefined"?"unsupported":Notification.permission==="granted"?"on":"ask"}</button></div></div>
492
- ${cfg?`<hr style="border:0;border-top:1px solid var(--line);margin:16px 0"><div class="set">${cfg.rows.map(r=>`<label title="${esc(r.key+" — "+r.help)}">${r.label}</label>${opts("s-"+r.key.replace(/\./g,"-"),r.values,r.value)}`).join("")}</div>`:`<p style="color:var(--dim);margin-top:20px">Settings could not be read. Run <code>kaizen settings</code>.</p>`}`;
493
- $("th").querySelectorAll("button").forEach(b=>b.onclick=()=>{const t=b.dataset.v;if(t==="system")delete document.documentElement.dataset.theme;else document.documentElement.dataset.theme=t;try{t==="system"?localStorage.removeItem("kz-theme"):localStorage.setItem("kz-theme",t)}catch(e){}openSettings()});
494
- $("nt").querySelector("button").onclick=()=>{askNotify();setTimeout(openSettings,300)};
495
- if(cfg)cfg.rows.forEach(r=>$("s-"+r.key.replace(/\./g,"-")).querySelectorAll("button").forEach(b=>b.onclick=async()=>{if(b.classList.contains("on"))return;const j=await post("/settings",{key:r.key,value:b.dataset.v});if(j.ok)toast(`saved ${r.key} = ${b.dataset.v||'""'}`);openSettings()}))}
505
+ // One control per row, in the TUI's grammar: [✓] on / [ ] off for a switch, ▮▮▯ bars for a
506
+ // number, the word itself (a select) for a choice. Help shows for the row under the pointer.
507
+ const ctl=(id,vals,on)=>{
508
+ if(vals.length===2&&vals.includes("true")&&vals.includes("false")){const yes=on==="true";return `<button class="chk${yes?" on":""}" id="${id}" data-v="${yes?"false":"true"}" role="switch" aria-checked="${yes}"><i>[${yes?"✓":" "}]</i> ${yes?"on":"off"}</button>`}
509
+ if(vals.every(v=>/^\d+$/.test(v))){const at=vals.indexOf(on);return `<span class="bar" id="${id}">${vals.map((v,i)=>`<button data-v="${v}" class="${i<=at?"on":""}" title="${v}"></button>`).join("")}<b>${esc(on)}</b></span>`}
510
+ return `<select class="sel" id="${id}">${vals.map(v=>`<option value="${esc(v)}"${v===on?" selected":""}>${v===""?"—":esc(v)}</option>`).join("")}</select>`};
511
+ const row=(label,control,hint)=>`<div class="srow"><label>${esc(label)}</label>${control}<p class="hint">${hint}</p></div>`;
512
+ $("pbody").innerHTML=`<h3 class="sg">Browser</h3>${row("Theme",ctl("th",["system","light","dark"],cur),"Light or dark for this browser only; system follows the OS.")}
513
+ ${row("Notifications",`<button class="chk" id="nt"><i>[${typeof Notification!=="undefined"&&Notification.permission==="granted"?"✓":" "}]</i> ${typeof Notification==="undefined"?"unsupported":Notification.permission==="granted"?"on":"ask"}</button>`,"A desktop notification when a run starts waiting on you.")}
514
+ ${cfg?groups(cfg.rows).map(([g,rows])=>`<h3 class="sg">${esc(g)}</h3>${rows.map(r=>row(r.label,ctl("s-"+r.key.replace(/\./g,"-"),r.values,r.value),`<code>${esc(r.key)}</code> options: ${r.values.map(v=>v||'""').map(esc).join(" / ")}<br>${esc(r.help)}`)).join("")}`).join(""):`<p style="color:var(--dim);margin-top:20px">Settings could not be read. Run <code>kaizen settings</code>.</p>`}`;
515
+ const setTheme=t=>{if(t==="system")delete document.documentElement.dataset.theme;else document.documentElement.dataset.theme=t;try{t==="system"?localStorage.removeItem("kz-theme"):localStorage.setItem("kz-theme",t)}catch(e){}};
516
+ $("th").onchange=e=>setTheme(e.target.value);
517
+ $("nt").onclick=()=>{askNotify();setTimeout(openSettings,300)};
518
+ if(cfg)cfg.rows.forEach(r=>{const el=$("s-"+r.key.replace(/\./g,"-"));const save=async v=>{const j=await post("/settings",{key:r.key,value:v});if(j.ok)toast(`saved ${r.key} = ${v||'""'}`);openSettings()};
519
+ if(el.tagName==="SELECT")el.onchange=()=>save(el.value);else el.querySelectorAll("[data-v]").forEach(b=>b.onclick=()=>save(b.dataset.v));if(el.matches(".chk"))el.onclick=()=>save(el.dataset.v)})}
496
520
  let tt;function toast(s){const t=$("toast");t.textContent=s;t.classList.add("on");clearTimeout(tt);tt=setTimeout(()=>t.classList.remove("on"),4000)}
497
521
  function askNotify(){try{if(typeof Notification!=="undefined"&&Notification.permission==="default")Notification.requestPermission()}catch(e){}}
498
522
  function notifyUser(title,body){try{if(typeof Notification!=="undefined"&&Notification.permission==="granted")new Notification(title,{body})}catch(e){}}