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.
- package/.env.example +27 -1
- package/README.es.md +132 -0
- package/README.fr.md +132 -0
- package/README.ja.md +132 -0
- package/README.ko.md +132 -0
- package/README.md +116 -74
- package/README.ru.md +132 -0
- package/README.zh.md +131 -0
- package/app-node/agent_adapters.mjs +37 -5
- package/app-node/agent_adapters.test.mjs +13 -1
- package/app-node/agent_detect.mjs +73 -0
- package/app-node/agent_detect.test.mjs +77 -0
- package/app-node/cli_install.test.mjs +5 -0
- package/app-node/install_config.mjs +5 -0
- package/app-node/main.mjs +339 -4
- package/app-node/notify.mjs +73 -0
- package/app-node/notify.test.mjs +68 -0
- package/app-node/plan_mode.mjs +174 -0
- package/app-node/plan_mode.test.mjs +153 -0
- package/app-node/smart_progress.mjs +94 -0
- package/app-node/smart_progress.test.mjs +66 -0
- package/app-node/stream_sentencer.mjs +61 -0
- package/app-node/stream_sentencer.test.mjs +64 -0
- package/app-node/streaming_tts_queue.mjs +48 -0
- package/app-node/streaming_tts_queue.test.mjs +58 -0
- package/app-node/text_routing.mjs +20 -0
- package/app-node/text_routing.test.mjs +23 -1
- package/docs/CONFIGURATION.md +69 -96
- package/docs/FRESH_INSTALL.md +105 -63
- package/docs/HERMES_VOICE.md +65 -0
- package/docs/MULTI_INSTANCE.md +16 -0
- package/docs/README.md +49 -0
- package/docs/RELEASE.md +42 -19
- package/docs/ROADMAP.md +38 -0
- package/docs/TROUBLESHOOTING.md +126 -0
- package/docs/USAGE.md +72 -40
- package/docs/assets/figures/verbalcoding-flow.svg +1 -1
- package/docs/i18n/CONFIGURATION.es.md +25 -0
- package/docs/i18n/CONFIGURATION.fr.md +25 -0
- package/docs/i18n/CONFIGURATION.ja.md +25 -0
- package/docs/i18n/CONFIGURATION.ko.md +25 -0
- package/docs/i18n/CONFIGURATION.ru.md +25 -0
- package/docs/i18n/CONFIGURATION.zh.md +25 -0
- package/docs/i18n/FRESH_INSTALL.es.md +27 -2
- package/docs/i18n/FRESH_INSTALL.fr.md +27 -2
- package/docs/i18n/FRESH_INSTALL.ja.md +27 -2
- package/docs/i18n/FRESH_INSTALL.ko.md +27 -2
- package/docs/i18n/FRESH_INSTALL.ru.md +27 -2
- package/docs/i18n/FRESH_INSTALL.zh.md +27 -2
- package/docs/i18n/HERMES_VOICE.es.md +46 -0
- package/docs/i18n/HERMES_VOICE.fr.md +46 -0
- package/docs/i18n/HERMES_VOICE.ja.md +46 -0
- package/docs/i18n/HERMES_VOICE.ko.md +65 -0
- package/docs/i18n/HERMES_VOICE.ru.md +46 -0
- package/docs/i18n/HERMES_VOICE.zh.md +46 -0
- package/docs/i18n/MULTI_INSTANCE.es.md +25 -0
- package/docs/i18n/MULTI_INSTANCE.fr.md +25 -0
- package/docs/i18n/MULTI_INSTANCE.ja.md +25 -0
- package/docs/i18n/MULTI_INSTANCE.ko.md +25 -0
- package/docs/i18n/MULTI_INSTANCE.ru.md +25 -0
- package/docs/i18n/MULTI_INSTANCE.zh.md +25 -0
- package/docs/i18n/README.es.md +20 -134
- package/docs/i18n/README.fr.md +20 -134
- package/docs/i18n/README.ja.md +20 -134
- package/docs/i18n/README.ko.md +20 -133
- package/docs/i18n/README.ru.md +20 -134
- package/docs/i18n/README.zh.md +20 -133
- package/docs/i18n/RELEASE.es.md +26 -1
- package/docs/i18n/RELEASE.fr.md +26 -1
- package/docs/i18n/RELEASE.ja.md +26 -1
- package/docs/i18n/RELEASE.ko.md +26 -1
- package/docs/i18n/RELEASE.ru.md +26 -1
- package/docs/i18n/RELEASE.zh.md +26 -1
- package/docs/i18n/TROUBLESHOOTING.es.md +39 -0
- package/docs/i18n/TROUBLESHOOTING.fr.md +39 -0
- package/docs/i18n/TROUBLESHOOTING.ja.md +39 -0
- package/docs/i18n/TROUBLESHOOTING.ko.md +39 -0
- package/docs/i18n/TROUBLESHOOTING.ru.md +39 -0
- package/docs/i18n/TROUBLESHOOTING.zh.md +39 -0
- package/docs/i18n/USAGE.es.md +25 -0
- package/docs/i18n/USAGE.fr.md +25 -0
- package/docs/i18n/USAGE.ja.md +25 -0
- package/docs/i18n/USAGE.ko.md +25 -0
- package/docs/i18n/USAGE.ru.md +25 -0
- package/docs/i18n/USAGE.zh.md +25 -0
- package/docs/superpowers/plans/2026-05-13-phase1-streaming-pipeline.md +122 -0
- package/docs/superpowers/plans/2026-05-13-phase10-push-notifications.md +152 -0
- package/docs/superpowers/plans/2026-05-13-phase2-agent-adapters.md +242 -0
- package/docs/superpowers/plans/2026-05-13-phase6-smart-progress.md +172 -0
- package/docs/superpowers/plans/2026-05-13-phase7-voice-plan-mode.md +108 -0
- package/package.json +2 -1
- package/scripts/cli.mjs +7 -4
- package/scripts/doctor.mjs +11 -0
- 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.
|