verbalcoding 0.2.10 → 0.2.12

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 (94) hide show
  1. package/.env.example +27 -1
  2. package/README.es.md +132 -0
  3. package/README.fr.md +132 -0
  4. package/README.ja.md +132 -0
  5. package/README.ko.md +132 -0
  6. package/README.md +116 -74
  7. package/README.ru.md +132 -0
  8. package/README.zh.md +131 -0
  9. package/app-node/agent_adapters.mjs +37 -5
  10. package/app-node/agent_adapters.test.mjs +13 -1
  11. package/app-node/agent_detect.mjs +73 -0
  12. package/app-node/agent_detect.test.mjs +77 -0
  13. package/app-node/cli_install.test.mjs +5 -0
  14. package/app-node/install_config.mjs +5 -0
  15. package/app-node/main.mjs +339 -4
  16. package/app-node/notify.mjs +73 -0
  17. package/app-node/notify.test.mjs +68 -0
  18. package/app-node/plan_mode.mjs +174 -0
  19. package/app-node/plan_mode.test.mjs +153 -0
  20. package/app-node/smart_progress.mjs +94 -0
  21. package/app-node/smart_progress.test.mjs +66 -0
  22. package/app-node/stream_sentencer.mjs +61 -0
  23. package/app-node/stream_sentencer.test.mjs +64 -0
  24. package/app-node/streaming_tts_queue.mjs +48 -0
  25. package/app-node/streaming_tts_queue.test.mjs +58 -0
  26. package/app-node/text_routing.mjs +20 -0
  27. package/app-node/text_routing.test.mjs +23 -1
  28. package/docs/CONFIGURATION.md +69 -96
  29. package/docs/FRESH_INSTALL.md +105 -63
  30. package/docs/HERMES_VOICE.md +65 -0
  31. package/docs/MULTI_INSTANCE.md +16 -0
  32. package/docs/README.md +49 -0
  33. package/docs/RELEASE.md +42 -19
  34. package/docs/ROADMAP.md +38 -0
  35. package/docs/TROUBLESHOOTING.md +126 -0
  36. package/docs/USAGE.md +72 -40
  37. package/docs/assets/figures/verbalcoding-flow.svg +1 -1
  38. package/docs/i18n/CONFIGURATION.es.md +25 -0
  39. package/docs/i18n/CONFIGURATION.fr.md +25 -0
  40. package/docs/i18n/CONFIGURATION.ja.md +25 -0
  41. package/docs/i18n/CONFIGURATION.ko.md +25 -0
  42. package/docs/i18n/CONFIGURATION.ru.md +25 -0
  43. package/docs/i18n/CONFIGURATION.zh.md +25 -0
  44. package/docs/i18n/FRESH_INSTALL.es.md +27 -2
  45. package/docs/i18n/FRESH_INSTALL.fr.md +27 -2
  46. package/docs/i18n/FRESH_INSTALL.ja.md +27 -2
  47. package/docs/i18n/FRESH_INSTALL.ko.md +27 -2
  48. package/docs/i18n/FRESH_INSTALL.ru.md +27 -2
  49. package/docs/i18n/FRESH_INSTALL.zh.md +27 -2
  50. package/docs/i18n/HERMES_VOICE.es.md +46 -0
  51. package/docs/i18n/HERMES_VOICE.fr.md +46 -0
  52. package/docs/i18n/HERMES_VOICE.ja.md +46 -0
  53. package/docs/i18n/HERMES_VOICE.ko.md +65 -0
  54. package/docs/i18n/HERMES_VOICE.ru.md +46 -0
  55. package/docs/i18n/HERMES_VOICE.zh.md +46 -0
  56. package/docs/i18n/MULTI_INSTANCE.es.md +25 -0
  57. package/docs/i18n/MULTI_INSTANCE.fr.md +25 -0
  58. package/docs/i18n/MULTI_INSTANCE.ja.md +25 -0
  59. package/docs/i18n/MULTI_INSTANCE.ko.md +25 -0
  60. package/docs/i18n/MULTI_INSTANCE.ru.md +25 -0
  61. package/docs/i18n/MULTI_INSTANCE.zh.md +25 -0
  62. package/docs/i18n/README.es.md +20 -134
  63. package/docs/i18n/README.fr.md +20 -134
  64. package/docs/i18n/README.ja.md +20 -134
  65. package/docs/i18n/README.ko.md +20 -133
  66. package/docs/i18n/README.ru.md +20 -134
  67. package/docs/i18n/README.zh.md +20 -133
  68. package/docs/i18n/RELEASE.es.md +26 -1
  69. package/docs/i18n/RELEASE.fr.md +26 -1
  70. package/docs/i18n/RELEASE.ja.md +26 -1
  71. package/docs/i18n/RELEASE.ko.md +26 -1
  72. package/docs/i18n/RELEASE.ru.md +26 -1
  73. package/docs/i18n/RELEASE.zh.md +26 -1
  74. package/docs/i18n/TROUBLESHOOTING.es.md +39 -0
  75. package/docs/i18n/TROUBLESHOOTING.fr.md +39 -0
  76. package/docs/i18n/TROUBLESHOOTING.ja.md +39 -0
  77. package/docs/i18n/TROUBLESHOOTING.ko.md +39 -0
  78. package/docs/i18n/TROUBLESHOOTING.ru.md +39 -0
  79. package/docs/i18n/TROUBLESHOOTING.zh.md +39 -0
  80. package/docs/i18n/USAGE.es.md +25 -0
  81. package/docs/i18n/USAGE.fr.md +25 -0
  82. package/docs/i18n/USAGE.ja.md +25 -0
  83. package/docs/i18n/USAGE.ko.md +25 -0
  84. package/docs/i18n/USAGE.ru.md +25 -0
  85. package/docs/i18n/USAGE.zh.md +25 -0
  86. package/docs/superpowers/plans/2026-05-13-phase1-streaming-pipeline.md +122 -0
  87. package/docs/superpowers/plans/2026-05-13-phase10-push-notifications.md +152 -0
  88. package/docs/superpowers/plans/2026-05-13-phase2-agent-adapters.md +242 -0
  89. package/docs/superpowers/plans/2026-05-13-phase6-smart-progress.md +172 -0
  90. package/docs/superpowers/plans/2026-05-13-phase7-voice-plan-mode.md +108 -0
  91. package/package.json +2 -1
  92. package/scripts/cli.mjs +7 -4
  93. package/scripts/doctor.mjs +11 -0
  94. package/scripts/install.mjs +44 -1
@@ -0,0 +1,122 @@
1
+ # Phase 1 — Streaming End-to-End Pipeline Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development or superpowers:executing-plans.
4
+
5
+ **Goal:** Stream the agent's stdout into the TTS pipeline so the first sentence plays seconds before the agent finishes the full reply.
6
+
7
+ **Architecture:** Today `agent_adapters.mjs::run` waits for the agent process to exit, then `tts_prefetch.mjs::playChunkedTTSWithPrefetch` consumes the final answer. New path: add `app-node/stream_sentencer.mjs` (a stateful sentence-boundary detector over a stdout stream), plumb it into the existing spawn-with-progress branch in `agent_adapters.mjs`, and feed sentences into a new `streaming_tts_queue.mjs` that synthesises and plays in order with abort support.
8
+
9
+ **Tech Stack:** Node 20 ESM, async stdout chunks, existing `splitForTTS`/`playChunkedTTSWithPrefetch`.
10
+
11
+ ---
12
+
13
+ ## Spec
14
+
15
+ ### Stream sentencer
16
+
17
+ API:
18
+ ```javascript
19
+ const sentencer = createSentencer({ minChars: 40, maxLatencyMs: 800 });
20
+ sentencer.on('sentence', s => ...);
21
+ sentencer.push(chunkText);
22
+ sentencer.flush();
23
+ ```
24
+
25
+ Boundary rules:
26
+ - Terminal punctuation (`.!?。!?…`) followed by whitespace or EOS.
27
+ - Soft boundary on whitespace if buffer ≥ `minChars` and ≥ `maxLatencyMs` since last emit.
28
+ - Strip ANSI and Hermes box glyphs (`╭ ╰ │ ┊`) before emit.
29
+ - Drop `VERBALCODING_PROGRESS:` lines (those belong to the progress channel).
30
+
31
+ ### Adapter changes
32
+
33
+ - `createAgentAdapter(settings, deps)` accepts `onSentence` callback in deps.
34
+ - Existing spawn-streaming branch (the one that already streams stdout for verbose mode) feeds the sentencer; final `flush()` runs on close.
35
+
36
+ ### Bridge changes
37
+
38
+ - `main.mjs` builds a `StreamingTTSQueue` per turn when `STREAMING_TTS=1`.
39
+ - Queue: synth-on-arrival, play-in-order, drops further work when the existing barge-in signal aborts.
40
+ - When flag is off, behavior is unchanged.
41
+
42
+ ---
43
+
44
+ ## File Structure
45
+
46
+ - Create: `app-node/stream_sentencer.mjs`, `app-node/stream_sentencer.test.mjs`.
47
+ - Create: `app-node/streaming_tts_queue.mjs`, `app-node/streaming_tts_queue.test.mjs`.
48
+ - Modify: `app-node/agent_adapters.mjs` — plumb `onSentence` into the spawn-streaming branch.
49
+ - Modify: `app-node/main.mjs` — wire queue.
50
+ - Modify: `.env.example` — `STREAMING_TTS`.
51
+
52
+ ---
53
+
54
+ ## Tasks
55
+
56
+ ### Task 1: Sentencer TDD
57
+
58
+ - [ ] Write `app-node/stream_sentencer.test.mjs` with five cases:
59
+ 1. Emits on terminal punctuation.
60
+ 2. Holds partial sentences until terminator.
61
+ 3. Strips ANSI before emitting.
62
+ 4. Filters `VERBALCODING_PROGRESS:` lines.
63
+ 5. `flush()` emits residual on close.
64
+
65
+ - [ ] Verify FAIL: `node --test app-node/stream_sentencer.test.mjs` → module missing.
66
+
67
+ ### Task 2: Implement sentencer
68
+
69
+ - [ ] Create `app-node/stream_sentencer.mjs`:
70
+ - `EventEmitter`-backed.
71
+ - Internal `buffer` string.
72
+ - On `push`: clean (regex strip ANSI + box chars + drop progress lines), append, scan for terminal punctuation, emit & advance index. If buffer ≥ minChars and elapsed ≥ maxLatencyMs, emit at last whitespace.
73
+ - On `flush`: emit trimmed residual.
74
+
75
+ - [ ] Run tests: PASS.
76
+ - [ ] Commit: `feat(streaming): stream sentencer with ANSI/progress filtering`.
77
+
78
+ ### Task 3: Streaming TTS queue TDD
79
+
80
+ - [ ] Write `app-node/streaming_tts_queue.test.mjs`:
81
+ 1. Synth + play happen in enqueue order.
82
+ 2. Abort signal stops further playback after current.
83
+
84
+ - [ ] Run, expect FAIL.
85
+
86
+ ### Task 4: Implement queue
87
+
88
+ - [ ] Create `app-node/streaming_tts_queue.mjs`:
89
+ - `createStreamingTTSQueue({ synth, play, signal, cleanup })`.
90
+ - Internal FIFO; single async pump promise; honours `signal.aborted` between awaits; runs cleanup when aborted between synth and play.
91
+ - [ ] Run tests: PASS.
92
+ - [ ] Commit: `feat(streaming): TTS queue with abort support`.
93
+
94
+ ### Task 5: Wire adapter
95
+
96
+ - [ ] In `agent_adapters.mjs`, accept `onSentence` from deps; build sentencer once per `run()`; feed both stdout and stderr; call `flush()` on close. Wrap existing `emitVerboseProgress` call site — sentences go to a different callback.
97
+ - [ ] Add adapter test using a fake spawn that emits stdout in three chunks; assert sentences arrive in order before the close handler fires.
98
+ - [ ] Commit.
99
+
100
+ ### Task 6: Wire bridge
101
+
102
+ - [ ] In `main.mjs`, when `STREAMING_TTS=1`:
103
+ - Build queue with the existing synth/play helpers.
104
+ - Pass `onSentence: text => queue.enqueue(text)` to adapter.
105
+ - After adapter returns, await `queue.drain()`; skip the post-run chunked playback path for already-streamed content.
106
+ - [ ] When flag is off, no change.
107
+ - [ ] Commit.
108
+
109
+ ### Task 7: Document
110
+
111
+ - [ ] Add `STREAMING_TTS=1` to `.env.example` with a short comment.
112
+ - [ ] Add a "Streaming pipeline" subsection to `docs/CONFIGURATION.md`.
113
+ - [ ] Commit.
114
+
115
+ ---
116
+
117
+ ## Self-Review
118
+
119
+ - Spec covered.
120
+ - No placeholders.
121
+ - Names consistent: `createSentencer`, `createStreamingTTSQueue`, `onSentence`, `STREAMING_TTS`.
122
+ - Risk: TTS providers vary in latency; queue must not block enqueue on slow synth (single pump avoids this).
@@ -0,0 +1,152 @@
1
+ # Phase 10 — Push Notification Handoff Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans.
4
+
5
+ **Goal:** When a long-running agent task completes while no human is in the voice channel (or after a configurable idle threshold), send a mobile push notification with a 1-line voice summary; tap the notification to rejoin the VC and hear the full reply.
6
+
7
+ **Architecture:** New `app-node/notify.mjs` with a provider interface (`send(title, body, deepLink)`). Default provider: ntfy.sh (zero-setup, works with iOS/Android apps, supports a `Click` action that opens a URL). Optional providers: Pushover, Discord DM. Trigger heuristic: task elapsed > `NOTIFY_MIN_TASK_MS` (default 60s) AND VC has 0 non-bot listeners OR user enabled `!notify on` for the session.
8
+
9
+ **Tech Stack:** Node 20 ESM, `fetch`, optional Discord DM via existing `discord.js` client.
10
+
11
+ ---
12
+
13
+ ## Spec
14
+
15
+ ### Provider API
16
+
17
+ ```javascript
18
+ const notifier = createNotifier({ provider: 'ntfy', topic: env.NTFY_TOPIC });
19
+ await notifier.send({
20
+ title: 'Hermes finished',
21
+ body: 'Refactor done, 3 files changed, all tests green.',
22
+ deepLink: 'discord://discord.com/channels/<guild>/<channel>',
23
+ });
24
+ ```
25
+
26
+ ### Trigger logic
27
+
28
+ - Wrap each `agent.run()`; if elapsed > threshold AND `shouldNotify()` returns true, send.
29
+ - `shouldNotify()`:
30
+ - User toggle `!notify on` → always send.
31
+ - Or: `getVoiceChannelHumanCount() === 0` → send.
32
+
33
+ ### Body composition
34
+
35
+ - Use last sentence of sanitized agent answer (short).
36
+ - Truncate to 200 chars.
37
+ - Strip PII (token/api key patterns) via existing `compactProgressText` helper.
38
+
39
+ ### Discord deep link
40
+
41
+ - Built from guild+channel IDs.
42
+
43
+ ### Privacy
44
+
45
+ - Notification body never contains code, diffs, or session_id.
46
+
47
+ ---
48
+
49
+ ## File Structure
50
+
51
+ - Create: `app-node/notify.mjs`, `app-node/notify.test.mjs`.
52
+ - Modify: `app-node/main.mjs` — wrap agent run completion.
53
+ - Modify: `.env.example` — `NOTIFY_PROVIDER`, `NTFY_TOPIC`, `PUSHOVER_USER`, `PUSHOVER_TOKEN`, `NOTIFY_MIN_TASK_MS`.
54
+
55
+ ---
56
+
57
+ ## Tasks
58
+
59
+ ### Task 1: TDD — ntfy provider sends correct payload
60
+
61
+ - [ ] Step 1: Failing test:
62
+
63
+ ```javascript
64
+ import { test } from 'node:test';
65
+ import assert from 'node:assert/strict';
66
+ import { createNotifier } from './notify.mjs';
67
+
68
+ test('ntfy provider posts to topic URL with title and body', async () => {
69
+ const calls = [];
70
+ const fetchImpl = async (url, opts) => { calls.push({ url, opts }); return { ok: true, status: 200 }; };
71
+ const n = createNotifier({ provider: 'ntfy', topic: 'verbalcoding-test', fetchImpl });
72
+ await n.send({ title: 'Done', body: 'All green.', deepLink: 'discord://x' });
73
+ assert.equal(calls.length, 1);
74
+ assert.match(calls[0].url, /ntfy\.sh\/verbalcoding-test/);
75
+ assert.equal(calls[0].opts.headers.Title, 'Done');
76
+ assert.equal(calls[0].opts.headers.Click, 'discord://x');
77
+ assert.equal(calls[0].opts.body, 'All green.');
78
+ });
79
+
80
+ test('shouldNotify true when zero human listeners', async () => {
81
+ const n = createNotifier({ provider: 'ntfy', topic: 'x', fetchImpl: async () => ({ ok: true }) });
82
+ assert.equal(n.shouldNotify({ humanCount: 0, taskMs: 10_000, minTaskMs: 1000 }), true);
83
+ assert.equal(n.shouldNotify({ humanCount: 1, taskMs: 10_000, minTaskMs: 1000 }), false);
84
+ assert.equal(n.shouldNotify({ humanCount: 0, taskMs: 100, minTaskMs: 1000 }), false);
85
+ assert.equal(n.shouldNotify({ humanCount: 1, taskMs: 10_000, minTaskMs: 1000, userOptIn: true }), true);
86
+ });
87
+
88
+ test('redacts api keys from body', async () => {
89
+ const calls = [];
90
+ const fetchImpl = async (u, o) => { calls.push(o); return { ok: true }; };
91
+ const n = createNotifier({ provider: 'ntfy', topic: 'x', fetchImpl });
92
+ await n.send({ title: 't', body: 'token=sk-abc123 finished', deepLink: '' });
93
+ assert.match(calls[0].body, /\[REDACTED\]/);
94
+ assert.doesNotMatch(calls[0].body, /sk-abc123/);
95
+ });
96
+ ```
97
+
98
+ - [ ] Step 2: Run, expect FAIL.
99
+
100
+ ### Task 2: Implement `notify.mjs`
101
+
102
+ - [ ] Step 1: Create module:
103
+
104
+ ```javascript
105
+ const SECRET_RE = /\b(?:token|api[_-]?key|password|secret|authorization|bearer|sk-[a-zA-Z0-9_-]+)\b[^\s]*/gi;
106
+
107
+ function redact(text) {
108
+ return String(text || '').replace(SECRET_RE, '[REDACTED]');
109
+ }
110
+
111
+ export function createNotifier({ provider = 'ntfy', topic = '', fetchImpl = globalThis.fetch, ntfyBase = 'https://ntfy.sh' } = {}) {
112
+ async function send({ title = 'VerbalCoding', body = '', deepLink = '' } = {}) {
113
+ const safeBody = redact(body).slice(0, 200);
114
+ if (provider === 'ntfy') {
115
+ if (!topic) return { skipped: true, reason: 'no topic' };
116
+ const headers = { Title: title };
117
+ if (deepLink) headers.Click = deepLink;
118
+ const res = await fetchImpl(`${ntfyBase}/${encodeURIComponent(topic)}`, {
119
+ method: 'POST',
120
+ headers,
121
+ body: safeBody,
122
+ });
123
+ return { ok: !!res?.ok, status: res?.status };
124
+ }
125
+ if (provider === 'noop') return { ok: true };
126
+ throw new Error(`unknown notify provider ${provider}`);
127
+ }
128
+
129
+ function shouldNotify({ humanCount = 0, taskMs = 0, minTaskMs = 60_000, userOptIn = false } = {}) {
130
+ if (taskMs < minTaskMs) return false;
131
+ if (userOptIn) return true;
132
+ return humanCount === 0;
133
+ }
134
+
135
+ return { send, shouldNotify };
136
+ }
137
+ ```
138
+
139
+ - [ ] Step 2: Run tests, expect PASS.
140
+ - [ ] Step 3: Commit.
141
+
142
+ ### Task 3: Wire into `main.mjs`
143
+
144
+ - [ ] Step 1: After each agent run, check `shouldNotify`, build deep link, call `send`.
145
+ - [ ] Step 2: Helper `getVoiceChannelHumanCount(channel)` reads voice state.
146
+ - [ ] Step 3: Add `!notify on|off` per-channel toggle.
147
+ - [ ] Step 4: Commit.
148
+
149
+ ### Task 4: Document
150
+
151
+ - [ ] Step 1: `.env.example` + `docs/USAGE.md` notification section.
152
+ - [ ] Step 2: Commit.
@@ -0,0 +1,242 @@
1
+ # Phase 2 — Agent-Agnostic Adapter Completion Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Round out `agent_adapters.mjs` so VerbalCoding works first-class with Aider and Cursor CLI, and auto-detects whichever agent is installed on the host.
6
+
7
+ **Architecture:** Extend the existing `buildAgentSettings()` defaults table in `app-node/agent_adapters.mjs`. Add a new `detectInstalledAgents()` helper in `app-node/agent_detect.mjs` that uses `which`/`command -v` to probe binaries, and wire it into `vc setup` + `vc doctor` + `vc status`. Add adapter quirks for Aider (`--no-pretty`, stdin prompt) and Cursor CLI (`cursor-agent`).
8
+
9
+ **Tech Stack:** Node 20 ESM, `node:child_process`, `node --test` (existing test runner).
10
+
11
+ ---
12
+
13
+ ## Spec
14
+
15
+ ### Adapter additions
16
+
17
+ - **Aider** — `aider --no-pretty --yes-always --message <prompt>`; no built-in session resume via flag, but supports `.aider.chat.history.md` for history. Skip session resume in adapter (treat each turn as fresh w/ project context). Sanitize Aider's "Tokens:" footer.
18
+ - **Cursor CLI** — `cursor-agent --prompt <prompt> --print` (Cursor's CLI flags vary by version; default to `cursor-agent` binary).
19
+
20
+ ### Auto-detection
21
+
22
+ - `detectInstalledAgents(env, { which })` returns ordered list of `{ backend, label, command, present: boolean, version?: string }`.
23
+ - Probe order: `hermes`, `claude`, `codex`, `gemini`, `opencode`, `openclaw`, `aider`, `cursor-agent`.
24
+ - Called by `vc setup` to auto-pick a default when `AGENT_BACKEND` is unset.
25
+ - Called by `vc doctor` to display which agents are reachable.
26
+
27
+ ### CLI surface
28
+
29
+ - `vc setup` — show detected agents and ask user to pick (default = first present).
30
+ - `vc doctor` — section "Agent backends" lists present/missing.
31
+ - `vc status` — show current `AGENT_BACKEND` + whether the binary resolves.
32
+
33
+ ---
34
+
35
+ ## File Structure
36
+
37
+ - Create: `app-node/agent_detect.mjs` — `detectInstalledAgents`, `probeAgentBinary`.
38
+ - Create: `app-node/agent_detect.test.mjs` — unit tests with injected `which`.
39
+ - Modify: `app-node/agent_adapters.mjs` (lines ~208–276) — add `aider` and `cursor` entries to `defaults` in `buildAgentSettings`.
40
+ - Modify: `app-node/agent_contract.mjs` — extend capability flags (`supportsStdinPrompt`, `supportsResume`).
41
+ - Modify: `scripts/install.mjs` (or wherever `vc setup` lives — find via grep) — call `detectInstalledAgents` when prompting for backend.
42
+ - Modify: `scripts/doctor.mjs` — render detection report.
43
+ - Modify: `.env.example` — document `AIDER_COMMAND`, `CURSOR_COMMAND`.
44
+ - Modify: `README.md` — agent list (after Phase 2 lands, full README reframe is its own task).
45
+
46
+ ---
47
+
48
+ ## Tasks
49
+
50
+ ### Task 1: Failing test for `detectInstalledAgents`
51
+
52
+ **Files:** Create `app-node/agent_detect.test.mjs`.
53
+
54
+ - [ ] Step 1: Write failing test
55
+
56
+ ```javascript
57
+ import { test } from 'node:test';
58
+ import assert from 'node:assert/strict';
59
+ import { detectInstalledAgents } from './agent_detect.mjs';
60
+
61
+ test('detectInstalledAgents marks present when which resolves', async () => {
62
+ const fakeWhich = async (bin) => bin === 'hermes' ? '/usr/local/bin/hermes' : null;
63
+ const result = await detectInstalledAgents({}, { which: fakeWhich });
64
+ const hermes = result.find(r => r.backend === 'hermes');
65
+ assert.equal(hermes.present, true);
66
+ assert.equal(hermes.command.includes('hermes'), true);
67
+ const claude = result.find(r => r.backend === 'claude');
68
+ assert.equal(claude.present, false);
69
+ });
70
+
71
+ test('detectInstalledAgents includes aider and cursor', async () => {
72
+ const fakeWhich = async () => null;
73
+ const result = await detectInstalledAgents({}, { which: fakeWhich });
74
+ const backends = result.map(r => r.backend);
75
+ assert.ok(backends.includes('aider'));
76
+ assert.ok(backends.includes('cursor'));
77
+ });
78
+
79
+ test('detectInstalledAgents honors env overrides for command', async () => {
80
+ const fakeWhich = async (bin) => bin === 'aider' ? '/opt/aider' : null;
81
+ const result = await detectInstalledAgents({ AIDER_COMMAND: 'aider --foo' }, { which: fakeWhich });
82
+ const aider = result.find(r => r.backend === 'aider');
83
+ assert.equal(aider.command, 'aider --foo');
84
+ });
85
+ ```
86
+
87
+ - [ ] Step 2: Run test, expect failure
88
+
89
+ ```bash
90
+ node --test app-node/agent_detect.test.mjs
91
+ ```
92
+ Expected: FAIL — `Cannot find module './agent_detect.mjs'`.
93
+
94
+ ### Task 2: Implement `agent_detect.mjs`
95
+
96
+ **Files:** Create `app-node/agent_detect.mjs`.
97
+
98
+ - [ ] Step 1: Implement minimal module
99
+
100
+ ```javascript
101
+ import { execFile } from 'node:child_process';
102
+ import { promisify } from 'node:util';
103
+
104
+ const execFileP = promisify(execFile);
105
+
106
+ const PROBES = [
107
+ { backend: 'hermes', bin: 'hermes', defaultCommand: 'hermes chat -Q -q', envCommand: 'HERMES_COMMAND' },
108
+ { backend: 'claude', bin: 'claude', defaultCommand: 'claude -p', envCommand: 'CLAUDE_COMMAND' },
109
+ { backend: 'codex', bin: 'codex', defaultCommand: 'codex exec', envCommand: 'CODEX_COMMAND' },
110
+ { backend: 'gemini', bin: 'gemini', defaultCommand: 'gemini -p', envCommand: 'GEMINI_COMMAND' },
111
+ { backend: 'opencode', bin: 'opencode', defaultCommand: 'opencode run', envCommand: 'OPENCODE_COMMAND' },
112
+ { backend: 'openclaw', bin: 'openclaw', defaultCommand: 'openclaw run', envCommand: 'OPENCLAW_COMMAND' },
113
+ { backend: 'aider', bin: 'aider', defaultCommand: 'aider --no-pretty --yes-always --message', envCommand: 'AIDER_COMMAND' },
114
+ { backend: 'cursor', bin: 'cursor-agent', defaultCommand: 'cursor-agent --print --prompt', envCommand: 'CURSOR_COMMAND' },
115
+ ];
116
+
117
+ async function defaultWhich(bin) {
118
+ try {
119
+ const { stdout } = await execFileP('which', [bin]);
120
+ const p = stdout.trim();
121
+ return p || null;
122
+ } catch { return null; }
123
+ }
124
+
125
+ export async function detectInstalledAgents(env = process.env, { which = defaultWhich } = {}) {
126
+ const results = await Promise.all(PROBES.map(async (p) => {
127
+ const path = await which(p.bin);
128
+ return {
129
+ backend: p.backend,
130
+ label: backendLabel(p.backend),
131
+ command: env[p.envCommand] || p.defaultCommand,
132
+ bin: p.bin,
133
+ path: path || null,
134
+ present: Boolean(path),
135
+ };
136
+ }));
137
+ return results;
138
+ }
139
+
140
+ function backendLabel(backend) {
141
+ return { hermes: 'Hermes Agent', claude: 'Claude Code', codex: 'Codex', gemini: 'Gemini', opencode: 'OpenCode', openclaw: 'OpenClaw', aider: 'Aider', cursor: 'Cursor CLI' }[backend] || backend;
142
+ }
143
+ ```
144
+
145
+ - [ ] Step 2: Run tests
146
+
147
+ ```bash
148
+ node --test app-node/agent_detect.test.mjs
149
+ ```
150
+ Expected: PASS.
151
+
152
+ - [ ] Step 3: Commit
153
+
154
+ ```bash
155
+ git add app-node/agent_detect.mjs app-node/agent_detect.test.mjs
156
+ git commit -m "feat(adapters): add agent_detect for auto-detection across 8 backends"
157
+ ```
158
+
159
+ ### Task 3: Add Aider + Cursor defaults to `buildAgentSettings`
160
+
161
+ **Files:** Modify `app-node/agent_adapters.mjs` defaults table (~lines 211–260).
162
+
163
+ - [ ] Step 1: Add entries
164
+
165
+ Inside the `defaults` object, insert:
166
+
167
+ ```javascript
168
+ aider: {
169
+ label: 'Aider',
170
+ command: env.AIDER_COMMAND || 'aider --no-pretty --yes-always --message',
171
+ sessionFile: env.AGENT_SESSION_FILE || path.join(root, '.agent-sessions', 'aider'),
172
+ supportsHermesSession: false,
173
+ },
174
+ cursor: {
175
+ label: 'Cursor CLI',
176
+ command: env.CURSOR_COMMAND || 'cursor-agent --print --prompt',
177
+ sessionFile: env.AGENT_SESSION_FILE || path.join(root, '.agent-sessions', 'cursor'),
178
+ supportsHermesSession: false,
179
+ },
180
+ ```
181
+
182
+ - [ ] Step 2: Add a corresponding test to `app-node/agent_adapters.test.mjs`
183
+
184
+ ```javascript
185
+ test('buildAgentSettings supports AGENT_BACKEND=aider', () => {
186
+ const s = buildAgentSettings({ ROOT: '/tmp/r', env: { AGENT_BACKEND: 'aider' } });
187
+ assert.equal(s.backend, 'aider');
188
+ assert.equal(s.label, 'Aider');
189
+ assert.match(s.command, /aider/);
190
+ });
191
+
192
+ test('buildAgentSettings supports AGENT_BACKEND=cursor', () => {
193
+ const s = buildAgentSettings({ ROOT: '/tmp/r', env: { AGENT_BACKEND: 'cursor' } });
194
+ assert.equal(s.backend, 'cursor');
195
+ assert.match(s.command, /cursor-agent/);
196
+ });
197
+ ```
198
+
199
+ - [ ] Step 3: Run
200
+
201
+ ```bash
202
+ node --test app-node/agent_adapters.test.mjs
203
+ ```
204
+ Expected: PASS.
205
+
206
+ - [ ] Step 4: Commit
207
+
208
+ ```bash
209
+ git add app-node/agent_adapters.mjs app-node/agent_adapters.test.mjs
210
+ git commit -m "feat(adapters): first-class Aider and Cursor CLI backends"
211
+ ```
212
+
213
+ ### Task 4: Wire detection into `vc setup`
214
+
215
+ **Files:** Find via `grep -nR "AGENT_BACKEND" scripts/install.mjs scripts/cli.mjs` first, then modify the relevant prompt logic.
216
+
217
+ - [ ] Step 1: Use `detectInstalledAgents` to default the picker, and label entries as `present`/`missing`.
218
+ - [ ] Step 2: Add a test for the picker function (extracted as pure function for testability).
219
+ - [ ] Step 3: Commit:
220
+
221
+ ```bash
222
+ git commit -m "feat(setup): auto-detect installed agents and default the picker"
223
+ ```
224
+
225
+ ### Task 5: Wire detection into `vc doctor`
226
+
227
+ - [ ] Step 1: Render a new "Agent backends" section in doctor output.
228
+ - [ ] Step 2: Commit.
229
+
230
+ ### Task 6: Document in `.env.example` and `docs/USAGE.md`
231
+
232
+ - [ ] Step 1: Add `AIDER_COMMAND` and `CURSOR_COMMAND` to `.env.example` with comments.
233
+ - [ ] Step 2: Update `docs/USAGE.md` agent table.
234
+ - [ ] Step 3: Commit.
235
+
236
+ ---
237
+
238
+ ## Self-Review
239
+
240
+ - Spec coverage: detection ✓, Aider ✓, Cursor ✓, setup wiring ✓, doctor wiring ✓, docs ✓.
241
+ - No placeholders.
242
+ - Types consistent (`backend` strings match across `agent_adapters.mjs` and `agent_detect.mjs`).
@@ -0,0 +1,172 @@
1
+ # Phase 6 — Smart Progress Summarization Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans.
4
+
5
+ **Goal:** Replace pattern-matched progress narration ("editing files routes.ts, server.ts") with semantic summaries ("wiring the new endpoint into the router").
6
+
7
+ **Architecture:** Add a `SmartProgressSummarizer` that buffers raw progress events for `summaryWindowMs` (default 4000ms) and asks a small LLM (Groq llama-3.1-8b or local ollama) to compress them into one human sentence. Fall back to the existing regex labels on summarizer failure/timeout/no-API-key.
8
+
9
+ **Tech Stack:** Node 20 ESM, `fetch` (Groq OpenAI-compatible API by default).
10
+
11
+ ---
12
+
13
+ ## Spec
14
+
15
+ ### API
16
+
17
+ ```javascript
18
+ const summarizer = createSmartProgressSummarizer({
19
+ apiKey: env.SMART_PROGRESS_API_KEY || env.GROQ_API_KEY,
20
+ baseUrl: env.SMART_PROGRESS_BASE_URL || 'https://api.groq.com/openai/v1',
21
+ model: env.SMART_PROGRESS_MODEL || 'llama-3.1-8b-instant',
22
+ windowMs: 4000,
23
+ language: 'en' | 'ko',
24
+ fallback: rawEvent => rawEvent,
25
+ });
26
+ summarizer.ingest(rawEvent);
27
+ summarizer.on('summary', text => ...);
28
+ ```
29
+
30
+ ### Behavior
31
+
32
+ - Buffer events for `windowMs`; when window expires OR buffer hits 8 events, request a summary.
33
+ - Prompt: "Summarize what the coding agent is doing into one short sentence ({language}). Events: ..."
34
+ - 1.5s timeout — fall through to fallback if exceeded.
35
+ - Cache identical event windows for 60s (dedupe back-to-back identical narration).
36
+ - Disabled when no API key — pure fallback to current regex events.
37
+
38
+ ### Integration
39
+
40
+ - `progress_speech.mjs` already handles narration cadence; add `summarizer` upstream.
41
+ - Voice toggleable: `!smart-progress on|off`.
42
+
43
+ ---
44
+
45
+ ## File Structure
46
+
47
+ - Create: `app-node/smart_progress.mjs`, `app-node/smart_progress.test.mjs`.
48
+ - Modify: `app-node/progress_speech.mjs` — inject summarizer.
49
+ - Modify: `app-node/main.mjs` — wire toggle.
50
+ - Modify: `.env.example` — new env keys.
51
+
52
+ ---
53
+
54
+ ## Tasks
55
+
56
+ ### Task 1: TDD — fallback when no API key
57
+
58
+ - [ ] Step 1: Write failing test:
59
+
60
+ ```javascript
61
+ import { test } from 'node:test';
62
+ import assert from 'node:assert/strict';
63
+ import { createSmartProgressSummarizer } from './smart_progress.mjs';
64
+
65
+ test('falls back to raw events when no apiKey', async () => {
66
+ const out = [];
67
+ const s = createSmartProgressSummarizer({ windowMs: 10 });
68
+ s.on('summary', t => out.push(t));
69
+ s.ingest('reading files routes.ts');
70
+ s.ingest('editing files routes.ts');
71
+ await new Promise(r => setTimeout(r, 30));
72
+ assert.ok(out.includes('reading files routes.ts'));
73
+ assert.ok(out.includes('editing files routes.ts'));
74
+ });
75
+ ```
76
+
77
+ - [ ] Step 2: Run, expect FAIL.
78
+
79
+ ### Task 2: Implement minimal summarizer (fallback path only)
80
+
81
+ - [ ] Step 1: Create `app-node/smart_progress.mjs`:
82
+
83
+ ```javascript
84
+ import { EventEmitter } from 'node:events';
85
+
86
+ export function createSmartProgressSummarizer({
87
+ apiKey = '',
88
+ baseUrl = 'https://api.groq.com/openai/v1',
89
+ model = 'llama-3.1-8b-instant',
90
+ windowMs = 4000,
91
+ language = 'en',
92
+ fetchImpl = globalThis.fetch,
93
+ timeoutMs = 1500,
94
+ cacheMs = 60_000,
95
+ } = {}) {
96
+ const ee = new EventEmitter();
97
+ let buffer = [];
98
+ let timer = null;
99
+ const cache = new Map();
100
+
101
+ function flush() {
102
+ timer = null;
103
+ const events = buffer; buffer = [];
104
+ if (!events.length) return;
105
+ if (!apiKey) { for (const e of events) ee.emit('summary', e); return; }
106
+ summarize(events).then(text => ee.emit('summary', text || events[events.length - 1])).catch(() => { for (const e of events) ee.emit('summary', e); });
107
+ }
108
+
109
+ async function summarize(events) {
110
+ const key = events.join('|');
111
+ const cached = cache.get(key);
112
+ if (cached && Date.now() - cached.t < cacheMs) return cached.text;
113
+ const ctl = new AbortController();
114
+ const to = setTimeout(() => ctl.abort(), timeoutMs);
115
+ try {
116
+ const r = await fetchImpl(`${baseUrl}/chat/completions`, {
117
+ method: 'POST',
118
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
119
+ signal: ctl.signal,
120
+ body: JSON.stringify({
121
+ model,
122
+ temperature: 0.2,
123
+ max_tokens: 40,
124
+ messages: [
125
+ { role: 'system', content: `Summarize a coding agent's recent actions in one short ${language === 'ko' ? 'Korean' : 'English'} sentence. No file paths unless essential. No quotes.` },
126
+ { role: 'user', content: events.join('\n') },
127
+ ],
128
+ }),
129
+ });
130
+ const data = await r.json();
131
+ const text = (data?.choices?.[0]?.message?.content || '').trim();
132
+ cache.set(key, { text, t: Date.now() });
133
+ return text;
134
+ } finally { clearTimeout(to); }
135
+ }
136
+
137
+ return {
138
+ on: (e, fn) => ee.on(e, fn),
139
+ ingest(event) {
140
+ if (!event) return;
141
+ buffer.push(String(event));
142
+ if (buffer.length >= 8) { clearTimeout(timer); flush(); return; }
143
+ if (!timer) timer = setTimeout(flush, windowMs);
144
+ },
145
+ };
146
+ }
147
+ ```
148
+
149
+ - [ ] Step 2: Run, expect PASS.
150
+ - [ ] Step 3: Commit.
151
+
152
+ ### Task 3: TDD — calls fetch with apiKey
153
+
154
+ - [ ] Step 1: Write test that injects fake fetch and asserts the request body.
155
+ - [ ] Step 2: Run, expect PASS (implementation already supports this).
156
+ - [ ] Step 3: Commit.
157
+
158
+ ### Task 4: TDD — timeout falls back to last raw event
159
+
160
+ - [ ] Step 1: Inject a fetch that never resolves; assert fallback after `timeoutMs`.
161
+ - [ ] Step 2: Commit.
162
+
163
+ ### Task 5: Wire into `progress_speech.mjs`
164
+
165
+ - [ ] Step 1: Read existing module to find narration entry; pipe summaries through summarizer when enabled.
166
+ - [ ] Step 2: Add `!smart-progress` toggle parsed in `discord_text.mjs`.
167
+ - [ ] Step 3: Commit.
168
+
169
+ ### Task 6: Document
170
+
171
+ - [ ] Step 1: `.env.example` — `SMART_PROGRESS_API_KEY`, `SMART_PROGRESS_MODEL`.
172
+ - [ ] Step 2: Commit.