profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM

Commit

Add project spec pack

commit 4b0cad4

5 changed files with +302 and −0

Jump to a changed file
  1. BLOCKED.md +15 −0
  2. spec/PLAN.md +108 −0
  3. spec/SPEC.md +86 −0
  4. spec/TASKS.md +63 −0
  5. spec/VERIFICATION.md +30 −0
added BLOCKED.md +15 −0
@@ -0,0 +1,15 @@
1 +# BLOCKED
2 +
3 +Entries the coding agent could not resolve without a product decision. The agent appends entries; the human answers by editing the entry and adding an `ANSWER:` line, then relaunches per spec/HANDOFF.md.
4 +
5 +Entry format:
6 +
7 +```
8 +## B<n>: <one-line summary>
9 +- Task: T<n>
10 +- Question: <what decision is needed and why the spec does not answer it>
11 +- Options considered: <a>, <b>
12 +- Continued with: <what the agent did instead, or "skipped task">
13 +```
14 +
15 +No entries yet.
added spec/PLAN.md +108 −0
@@ -0,0 +1,108 @@
1 +# PLAN: VoiceTask
2 +
3 +## Stack
4 +
5 +- Node.js >= 20, TypeScript strict, npm.
6 +- Server: Fastify (`fastify`, `@fastify/multipart`, `@fastify/static`, `@fastify/cors`).
7 +- Client: Vite + React + TypeScript. No UI framework, plain CSS in one stylesheet.
8 +- LLM: `@anthropic-ai/sdk`. STT: OpenAI REST API via `fetch` inside the provider only (no OpenAI SDK dependency).
9 +- Validation/schemas: `zod` (shared between API validation and LLM structured output).
10 +- Tests: `vitest`. Typecheck: `tsc --noEmit`.
11 +
12 +No other runtime dependencies without a BLOCKED.md entry.
13 +
14 +## Layout
15 +
16 +```
17 +shared/ types + zod schemas (Session, Segment, Coverage, API payloads)
18 +server/
19 + index.ts Fastify bootstrap, serves client build in prod
20 + routes/ sessions.ts, audio.ts, generate.ts, blockers.ts
21 + store/ sessionStore.ts (JSON files under data/sessions/<id>/session.json)
22 + providers/
23 + types.ts SttProvider, InterviewLlm interfaces
24 + factory.ts env-based selection, MOCK_PROVIDERS=1 forces mocks
25 + sttOpenai.ts sttMock.ts
26 + llmAnthropic.ts llmMock.ts
27 + engine/
28 + coverage.ts categories, state transitions
29 + interview.ts one turn: transcript + coverage in, InterviewTurn out
30 + summary.ts running summary of segments older than the last 40
31 + generator/
32 + prompts.ts per-file generation prompts
33 + generate.ts orchestrates the 5 files + sources.json, backup logic
34 + provenance.ts marker validation (FR-013)
35 + blockers/
36 + parse.ts BLOCKED.md parser
37 +client/
38 + src/App.tsx session list/create, interview view
39 + src/api.ts typed fetch wrappers over shared types
40 + src/audio.ts MediaRecorder push-to-talk
41 + src/tts.ts speechSynthesis wrapper
42 + src/components/ Transcript, CoveragePanel, QuestionCard, GeneratePanel
43 +```
44 +
45 +## Coverage categories (fixed, order matters for tie-breaking)
46 +
47 +1. `goal` (what and why), 2. `users`, 3. `core-flow`, 4. `data`, 5. `integrations`,
48 +6. `edge-cases`, 7. `constraints` (stack, platform, performance), 8. `non-goals`, 9. `verification` (what "done" looks like).
49 +
50 +## Key interfaces (shared/)
51 +
52 +```ts
53 +interface Segment { id: string; ts: string; speaker: "user" | "interviewer"; text: string }
54 +type CoverageLevel = "missing" | "partial" | "clear"
55 +type Coverage = Record<CategoryId, CoverageLevel>
56 +interface Session {
57 + id: string; name: string; targetDir: string; createdAt: string;
58 + segments: Segment[]; coverage: Coverage; summary: string;
59 + status: "interviewing" | "done"; openBlockers: string[]
60 +}
61 +interface InterviewTurn {
62 + coverage: Coverage; nextQuestion: string;
63 + contradiction: { segmentIds: string[]; description: string } | null;
64 + done: boolean; summaryUpdate: string | null
65 +}
66 +```
67 +
68 +`InterviewTurn` is also the zod schema used as the LLM structured output format.
69 +
70 +## Anthropic API usage (llmAnthropic.ts)
71 +
72 +- Model from `ANTHROPIC_MODEL`, default `claude-opus-4-8`. Key from `ANTHROPIC_API_KEY`.
73 +- Interview turn: `client.messages.parse()` with `output_config: { format: zodOutputFormat(InterviewTurnSchema) }`, `thinking: { type: "adaptive" }`, `max_tokens: 4096`. Input: system prompt (interviewer persona + category definitions + rules from SPEC FR-005..FR-008), then summary + last 40 segments + current coverage as the user message. Do not set `temperature` (removed on this model family).
74 +- Spec pack generation: one call per output file (5 calls), plain text output, `client.messages.stream()` with `finalMessage()`, `max_tokens: 32000`. Each prompt receives the full transcript with segment ids and instructs: English output, provenance markers `[S<n>]` on every FR (SPEC.md call only), verification commands per task (TASKS.md call only).
75 +- Schema-mismatch handling per SPEC edge cases: one retry, then error. Use the SDK's typed error classes.
76 +
77 +## STT provider (sttOpenai.ts)
78 +
79 +- `POST https://api.openai.com/v1/audio/transcriptions`, model from `STT_MODEL` (default `gpt-4o-mini-transcribe`), key from `OPENAI_API_KEY`, multipart upload of the webm blob. Returns plain text.
80 +- Reject transcripts that are empty after trimming (maps to the empty-audio edge case).
81 +
82 +## Mocks (sttMock.ts, llmMock.ts)
83 +
84 +- `sttMock`: returns `"mock transcript <n>"` with an incrementing counter, or, if the uploaded "audio" buffer is valid UTF-8 text, echoes it back (lets tests inject specific answers through the audio path).
85 +- `llmMock` interview: deterministic script keyed on turn count; marks one category `clear` per turn in the fixed category order, asks a canned question about the next category, returns `done: true` when all are `clear` or when the last user segment is exactly "done".
86 +- `llmMock` generation: emits minimal valid files; SPEC.md contains two FRs, the first with a valid `[S1]` marker and the second with a bogus `[S999]` marker so the provenance validator (FR-013) is exercised end to end.
87 +
88 +## API routes
89 +
90 +```
91 +POST /api/sessions {name, targetDir} -> Session
92 +GET /api/sessions -> Session[] (id, name, status only)
93 +GET /api/sessions/:id -> Session
94 +POST /api/sessions/:id/audio multipart audio -> {segment, turn: InterviewTurn}
95 +POST /api/sessions/:id/answer {text} -> {segment, turn: InterviewTurn}
96 +POST /api/sessions/:id/generate {overwrite?: boolean} -> {files: string[], warnings: string[]}
97 +POST /api/sessions/:id/blockers {} -> {questions: string[]} (reads <targetDir>/BLOCKED.md)
98 +```
99 +
100 +All request/response bodies validated with the shared zod schemas. Errors: `{error: string}` with appropriate status codes.
101 +
102 +## Decisions already made (do not revisit)
103 +
104 +- Fastify over Express: built-in schema validation hooks and multipart support.
105 +- JSON file storage over SQLite: single user, small data, trivially inspectable.
106 +- One LLM call per generated file instead of one giant call: keeps each output small enough to be reliable and lets provenance validation run per file.
107 +- Browser TTS over API TTS: zero cost and zero latency-sensitive infra for v1.
108 +- The generated spec pack intentionally mirrors the structure of this very spec pack (SPEC/PLAN/TASKS/VERIFICATION/HANDOFF); templates in `generator/prompts.ts` should be derived from these files.
added spec/SPEC.md +86 −0
@@ -0,0 +1,86 @@
1 +# SPEC: VoiceTask
2 +
3 +## Goal
4 +
5 +A local tool for a solo developer who thinks out loud. The user talks about something they want to build. An AI interviewer conducts a spoken Socratic interview: one targeted question at a time, driven by a coverage model of what a good spec needs. When coverage is sufficient (or the user says done), the tool writes a spec pack (SPEC.md, PLAN.md, TASKS.md, VERIFICATION.md, HANDOFF.md) into a target project directory, ready to hand to a sandboxed Claude Code session. Every requirement in the generated spec carries provenance markers pointing to the exact transcript segments it came from, so the user can always tell "I said this" apart from "the AI invented this".
6 +
7 +Differentiators over existing tools (verified 2026-07): ChatPRD interviews but has no voice; WhisperCode takes voice but does not interview; Spec Kit / Kiro clarify in text only; no tool provides requirement-to-utterance provenance or closes the loop from a coding agent's blockers back into a voice interview.
8 +
9 +## Users
10 +
11 +Single user, the developer themselves, on their own machine. No auth, no multi-tenancy.
12 +
13 +## User stories
14 +
15 +P1 (must have):
16 +- US-1: As a developer, I hold a button, talk about my idea, and see my words transcribed into the session.
17 +- US-2: As a developer, I am asked one concrete question at a time, chosen to fill the biggest gap in the spec, and I can answer by voice or by typing.
18 +- US-3: As a developer, I can see which spec areas are covered and which are still open.
19 +- US-4: As a developer, I can generate a spec pack into a target directory and every requirement shows where in my own words it came from.
20 +
21 +P2 (should have):
22 +- US-5: As a developer, I hear the interviewer's question spoken aloud so I can keep my eyes off the screen.
23 +- US-6: As a developer, when I contradict something I said earlier, the interviewer points at both statements and asks which one holds.
24 +- US-7: As a developer, I can close the tool and resume the same session later.
25 +
26 +P3 (nice to have):
27 +- US-8: As a developer, after a sandboxed Claude Code run leaves questions in BLOCKED.md, I can import them and answer them in a new voice round, and the spec pack is regenerated with the answers.
28 +
29 +## Functional requirements
30 +
31 +Interview session:
32 +- FR-001: The user can create a session with a project name and a target directory path. The session gets a unique id.
33 +- FR-002: The client records audio push-to-talk style (press and hold, or click to start/stop) using the browser microphone and sends the recording to the server when released.
34 +- FR-003: The server transcribes audio through the configured STT provider and appends a transcript segment `{id, timestamp, speaker: "user", text}` to the session. Segment ids are sequential (S1, S2, ...).
35 +- FR-004: A text input is always available as a fallback; a typed answer goes through the same pipeline as a transcribed one and also becomes a transcript segment.
36 +- FR-005: After each user answer, the interview engine produces exactly one next question. The question is stored as a transcript segment with `speaker: "interviewer"`.
37 +- FR-006: The engine maintains a coverage state over the nine categories listed in PLAN.md, each rated `missing`, `partial`, or `clear`, updated after every answer. The next question targets the weakest category.
38 +- FR-007: If a new answer conflicts with an earlier statement, the next question must surface the contradiction, quoting or referencing both segments, instead of a coverage question.
39 +- FR-008: The user can end the interview at any time by clicking Done or by saying/typing "done". If categories are still `missing`, generation proceeds but the UI first shows which categories are missing and asks for confirmation.
40 +- FR-009: Each interviewer question is displayed as text and, when the TTS toggle is on, spoken via the browser `speechSynthesis` API.
41 +- FR-010: The coverage state is visible in the UI at all times and updates after every turn.
42 +
43 +Spec pack generation:
44 +- FR-011: On generate, the server writes `spec/SPEC.md`, `spec/PLAN.md`, `spec/TASKS.md`, `spec/VERIFICATION.md`, `spec/HANDOFF.md`, and `spec/sources.json` into the target directory. If any of these files already exist, generation fails with a clear error unless the request sets `overwrite: true`.
45 +- FR-012: Every functional requirement in the generated SPEC.md ends with one or more provenance markers `[S<n>]` referencing transcript segments. `sources.json` maps each segment id to its text and timestamp.
46 +- FR-013: After generation, the server validates all `[S<n>]` markers against the session transcript. Markers pointing at nonexistent segments are removed and the requirement is suffixed with `[unverified]`.
47 +- FR-014: Every task in the generated TASKS.md includes at least one verification command, and the generated HANDOFF.md contains a ready-to-copy command for launching a Claude Code session against the pack.
48 +
49 +Persistence and resume:
50 +- FR-015: Sessions are persisted to disk after every turn. Opening the app lists existing sessions and lets the user resume one, with full transcript and coverage state restored.
51 +
52 +Blocker loop:
53 +- FR-016: The user can import a `BLOCKED.md` file from the target directory. Each blocker entry becomes an open question; the interview resumes asking only those questions, and a subsequent generate regenerates the pack with the new answers (overwrite implied, previous pack backed up to `spec/backup-<timestamp>/`).
54 +
55 +Modes and safety:
56 +- FR-017: With `MOCK_PROVIDERS=1`, both STT and the LLM are replaced by deterministic mocks and the full flow (record or type, interview, generate) works offline with no API keys.
57 +- FR-018: Provider API keys are read from environment variables only and never appear in logs, session files, or generated output.
58 +
59 +## Edge cases
60 +
61 +- Empty or unusable audio (silence, too short): the server returns a distinct error and the UI prompts the user to repeat; no empty segment is stored.
62 +- STT provider failure or timeout: error surfaced in the UI, text input remains usable, session state unchanged.
63 +- LLM returns output that does not match the expected schema: retry once; on second failure, surface the error and keep the session at the previous turn.
64 +- Target directory does not exist or is not writable: generation fails with the OS error shown to the user; the session is unaffected.
65 +- "done" as an ordinary word inside a longer answer must not end the interview; only an answer that is exactly "done" (case-insensitive, trimmed) or the Done button ends it.
66 +- Very long sessions: the engine prompt includes at most the last 40 transcript segments verbatim plus a running summary of earlier ones (summary maintained by the engine).
67 +- BLOCKED.md missing or empty on import: informative message, nothing changes.
68 +
69 +## Out of scope (v1)
70 +
71 +- Multi-user support, auth, or any cloud deployment. Local only.
72 +- Real-time streaming conversation (barge-in, VAD). Push-to-talk only.
73 +- Paid TTS voices. Browser `speechSynthesis` only.
74 +- Running or supervising Claude Code from inside the app. The app only writes files and prints the handoff command.
75 +- Editing the generated spec inside the app.
76 +- Mobile support.
77 +- Any languages other than the one the user speaks in; generated spec pack files are always written in English regardless of interview language.
78 +
79 +## Assumptions (pre-answered, do not ask)
80 +
81 +- Platform: developed and run on Windows 11, but nothing platform-specific outside standard Node APIs; must also work on macOS/Linux.
82 +- Browser target: current Chrome/Edge (MediaRecorder and speechSynthesis available). No polyfills for other browsers.
83 +- One interview session maps to one target project directory. Multiple sessions may exist side by side.
84 +- The interviewer speaks the language the user uses; spec pack output is English.
85 +- Audio format: whatever MediaRecorder produces by default (webm/opus); the STT provider must accept it.
86 +- No streaming transcription needed; per-utterance transcription latency of 1-3 s is acceptable.
added spec/TASKS.md +63 −0
@@ -0,0 +1,63 @@
1 +# TASKS: VoiceTask
2 +
3 +Work strictly in order unless a task's Depends line allows otherwise. One task at a time. Run the Verify commands before checking a task off. All tests run with `MOCK_PROVIDERS=1` (vitest config sets it globally).
4 +
5 +- [ ] T1 Scaffold
6 + - npm package with `server/`, `client/` (Vite React TS template), `shared/`. Scripts: `dev` (server + Vite concurrently), `build`, `typecheck` (tsc --noEmit over all three), `test` (vitest run). Add zod, fastify + plugins, @anthropic-ai/sdk, vitest. One placeholder test that asserts true.
7 + - Depends: nothing
8 + - Verify: `npm run typecheck` and `npm test` and `npm run build` all exit 0.
9 +
10 +- [ ] T2 Shared types and session store
11 + - Implement `shared/` types + zod schemas from PLAN.md. Implement `server/store/sessionStore.ts`: create, get, list, append segment, update coverage/summary/status, persisted to `data/sessions/<id>/session.json` atomically (write temp file, rename).
12 + - Depends: T1
13 + - Verify: `npm test` (store unit tests: create/reload roundtrip, sequential segment ids S1..Sn, list).
14 +
15 +- [ ] T3 Provider interfaces, factory, mocks
16 + - `server/providers/types.ts`, `factory.ts`, `sttMock.ts`, `llmMock.ts` exactly as specified in PLAN.md (including the echo-text behavior of sttMock and the deterministic interview script and bad-marker SPEC.md of llmMock).
17 + - Depends: T2
18 + - Verify: `npm test` (mock behavior tests: echo, turn script reaches done, factory returns mocks under MOCK_PROVIDERS=1).
19 +
20 +- [ ] T4 Session and answer routes
21 + - `POST /api/sessions`, `GET /api/sessions`, `GET /api/sessions/:id`, `POST /api/sessions/:id/answer` wired to store + interview engine stub that calls the InterviewLlm provider. "done" detection per SPEC (exact match, case-insensitive, trimmed).
22 + - Depends: T3
23 + - Verify: `npm test` (route tests via fastify.inject: create, answer produces interviewer segment + coverage update, done ends session).
24 +
25 +- [ ] T5 Interview engine
26 + - `server/engine/`: coverage state handling, prompt construction (summary + last 40 segments), summary maintenance, contradiction passthrough from the LLM response, weakest-category question selection enforced in the prompt. Anthropic implementation `llmAnthropic.ts` per PLAN.md (compiles and is unit-tested for prompt construction only; no network in tests).
27 + - Depends: T4
28 + - Verify: `npm test` (engine tests with llmMock: coverage progresses in category order, >40 segments triggers summary path, contradiction from mock is surfaced).
29 +
30 +- [ ] T6 Audio route and OpenAI STT
31 + - `@fastify/multipart` upload route `POST /api/sessions/:id/audio`, `sttOpenai.ts` per PLAN.md, empty-transcript rejection, provider errors mapped to 502 with `{error}`.
32 + - Depends: T4
33 + - Verify: `npm test` (audio route with sttMock: uploaded text buffer becomes a segment and triggers a turn; empty buffer returns 4xx and stores nothing).
34 +
35 +- [ ] T7 Client: interview UI
36 + - Session list/create screen, interview screen with Transcript, QuestionCard, CoveragePanel, text input, Done button. Typed api.ts wrappers. Vite dev proxy to the server.
37 + - Depends: T4 (T5 makes it meaningful, but the API contract is enough to build against)
38 + - Verify: `npm run typecheck` and `npm run build` exit 0. Manual: `npm run dev` with MOCK_PROVIDERS=1, typing answers advances coverage in the panel.
39 +
40 +- [ ] T8 Client: push-to-talk and TTS
41 + - MediaRecorder hold-to-record button (spacebar and mouse), upload to the audio route, recording state indicator. TTS toggle speaking each new interviewer question via speechSynthesis.
42 + - Depends: T6, T7
43 + - Verify: `npm run typecheck` and `npm run build` exit 0. Manual: recording in Chrome produces a segment (mock mode).
44 +
45 +- [ ] T9 Spec pack generator
46 + - `server/generator/`: five per-file prompts, generation orchestration through the InterviewLlm provider, `sources.json` emission, provenance validation per FR-013, existing-file refusal + `overwrite` flag + backup to `spec/backup-<timestamp>/` on regenerate.
47 + - Depends: T5
48 + - Verify: `npm test` (with llmMock into a temp dir: all six files written; bogus `[S999]` marker removed and `[unverified]` appended; second run without overwrite fails; with overwrite creates backup dir).
49 +
50 +- [ ] T10 Generate route and UI
51 + - `POST /api/sessions/:id/generate`, GeneratePanel with missing-category confirmation dialog per FR-008, result/warnings display.
52 + - Depends: T7, T9
53 + - Verify: `npm test` (route test) and `npm run typecheck`.
54 +
55 +- [ ] T11 Blocker import
56 + - `server/blockers/parse.ts` for the BLOCKED.md format defined in the repo root `BLOCKED.md` template, `POST /api/sessions/:id/blockers`, engine mode that asks only imported questions, UI entry point. Missing/empty file handled per SPEC edge case.
57 + - Depends: T5, T7
58 + - Verify: `npm test` (parser tests incl. empty file; route test: import then next turn asks a blocker question).
59 +
60 +- [ ] T12 End-to-end smoke test
61 + - Single vitest test: boot the server with mocks, create session (temp target dir), answer until done via the answer route, generate, assert all six files exist, all remaining `[S<n>]` markers resolve against sources.json, and HANDOFF.md contains the string `claude`.
62 + - Depends: T9, T10
63 + - Verify: `npm test` runs it green; then `npm run typecheck`, `npm test`, `npm run build` all exit 0 as the final full check.
added spec/VERIFICATION.md +30 −0
@@ -0,0 +1,30 @@
1 +# VERIFICATION: VoiceTask
2 +
3 +## Commands
4 +
5 +All of these must exit 0 for the project to be considered healthy:
6 +
7 +```
8 +npm run typecheck # tsc --noEmit over shared, server, client
9 +npm test # vitest run, MOCK_PROVIDERS=1 enforced by vitest config
10 +npm run build # vite build + server tsc build
11 +```
12 +
13 +Tests must not require network access or API keys. If a test needs a provider, it uses the mocks from `server/providers/`.
14 +
15 +## Definition of done (whole project)
16 +
17 +1. All checkboxes in `spec/TASKS.md` checked.
18 +2. The three commands above exit 0 on a clean `npm install`.
19 +3. The T12 smoke test passes: full session lifecycle through the HTTP API with mocks, generated pack valid per FR-011..FR-014.
20 +4. `BLOCKED.md` contains either no entries or only entries genuinely requiring a product decision.
21 +
22 +## Manual smoke check (human, after the sandboxed run)
23 +
24 +1. `MOCK_PROVIDERS=1 npm run dev`, open the printed URL in Chrome.
25 +2. Create a session with a temp directory as target. Type three answers, watch coverage advance, click Done, generate, confirm files appear in `<target>/spec/`.
26 +3. With real keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY` set, `MOCK_PROVIDERS` unset): hold the record button, say a sentence, confirm it transcribes and a sensible question comes back.
27 +
28 +## Per-task verification
29 +
30 +Each task in `spec/TASKS.md` carries its own Verify line; those are the gate for checking the task off. This file is the gate for finishing the session.