diff --git a/docs/superpowers/plans/2026-08-31-intake-drafting-worker.md b/docs/superpowers/plans/2026-08-31-intake-drafting-worker.md index 747b00d..375253b 100644 --- a/docs/superpowers/plans/2026-08-31-intake-drafting-worker.md +++ b/docs/superpowers/plans/2026-08-31-intake-drafting-worker.md @@ -1,6 +1,6 @@ # Intake Drafting Worker Implementation Plan -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. **Goal:** A submitted item arrives with a drafted name, description, category, tags and suggested price, written from its photos and the sender's note. @@ -62,7 +62,7 @@ A directory rather than six files loose in `src/`: they are one concern, they ch **Interfaces:** - Produces: `@anthropic-ai/sdk` and `zod` available; a boot warning when `ANTHROPIC_API_KEY` is absent. -- [ ] **Step 1: Install** +- [x] **Step 1: Install** ```bash cd backend @@ -80,7 +80,7 @@ Task 4 imports `zodOutputFormat` from `@anthropic-ai/sdk/helpers/zod`. If the in Both are production dependencies — the worker runs in the container. Confirm they landed under `"dependencies"`, not `"devDependencies"`: the final Docker stage installs with `--omit=dev`, so the wrong section produces a container that fails on the first submission and nowhere else. That is exactly how sharp went wrong in #226. -- [ ] **Step 2: Write the failing test** +- [x] **Step 2: Write the failing test** Add to `backend/tests/unit/envValidation.test.ts`: @@ -108,7 +108,7 @@ describe('the intake drafting key', () => { }); ``` -- [ ] **Step 3: Run it to verify it fails** +- [x] **Step 3: Run it to verify it fails** ```bash cd backend && npx jest -c jest.unit.config.js envValidation @@ -116,7 +116,7 @@ cd backend && npx jest -c jest.unit.config.js envValidation Expected: FAIL on the warning assertions. -- [ ] **Step 4: Add the warning** +- [x] **Step 4: Add the warning** In `backend/src/envValidation.ts`, alongside the other warning rules: @@ -136,7 +136,7 @@ function checkDraftingKey(env: NodeJS.ProcessEnv): string[] { Add it to the warning composition the same way the existing rules are composed; do not nest it inside `validateEnv`, which is what keeps that function's cognitive complexity down. -- [ ] **Step 5: Verify** +- [x] **Step 5: Verify** ```bash cd backend && npm run test:unit && npm run lint && npm run build @@ -144,7 +144,7 @@ cd backend && npm run test:unit && npm run lint && npm run build Expected: PASS, lint no new warnings, build clean. -- [ ] **Step 6: Commit** +- [x] **Step 6: Commit** ```bash git add backend/package.json backend/package-lock.json backend/src/envValidation.ts backend/tests/unit/envValidation.test.ts @@ -161,7 +161,7 @@ git commit -m "build(intake): add the Anthropic SDK and warn when its key is abs **Interfaces:** - Produces: `DraftSchema` (Zod), `type DraftResult = z.infer` -- [ ] **Step 1: Write the failing test** +- [x] **Step 1: Write the failing test** Create `backend/tests/unit/draftSchema.test.ts`: @@ -217,7 +217,7 @@ describe('DraftSchema', () => { }); ``` -- [ ] **Step 2: Run it to verify it fails** +- [x] **Step 2: Run it to verify it fails** ```bash cd backend && npx jest -c jest.unit.config.js draftSchema @@ -225,7 +225,7 @@ cd backend && npx jest -c jest.unit.config.js draftSchema Expected: FAIL — module not found. -- [ ] **Step 3: Write the schema** +- [x] **Step 3: Write the schema** Create `backend/src/intake/draftSchema.ts`: @@ -273,7 +273,7 @@ export const DraftSchema = z.object({ export type DraftResult = z.infer; ``` -- [ ] **Step 4: Run it to verify it passes** +- [x] **Step 4: Run it to verify it passes** ```bash cd backend && npx jest -c jest.unit.config.js draftSchema @@ -281,7 +281,7 @@ cd backend && npx jest -c jest.unit.config.js draftSchema Expected: PASS, 6 tests. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add backend/src/intake/draftSchema.ts backend/tests/unit/draftSchema.test.ts @@ -303,7 +303,7 @@ This is the correctness surface. Read "The correctness surface" above before wri - `buildSystemPrompt(categories: string[], tags: string[]): string` - `buildUserContent(photos: {mediaType: string; base64: string}[], note: string | null): unknown[]` -- [ ] **Step 1: Write the failing test** +- [x] **Step 1: Write the failing test** Create `backend/tests/unit/draftPrompt.test.ts`: @@ -372,7 +372,7 @@ describe('buildUserContent', () => { }); ``` -- [ ] **Step 2: Run it to verify it fails** +- [x] **Step 2: Run it to verify it fails** ```bash cd backend && npx jest -c jest.unit.config.js draftPrompt @@ -380,7 +380,7 @@ cd backend && npx jest -c jest.unit.config.js draftPrompt Expected: FAIL — module not found. -- [ ] **Step 3: Write the prompt builder** +- [x] **Step 3: Write the prompt builder** Create `backend/src/intake/draftPrompt.ts`: @@ -455,7 +455,7 @@ export function buildUserContent(photos: Photo[], note: string | null): unknown[ } ``` -- [ ] **Step 4: Run it to verify it passes** +- [x] **Step 4: Run it to verify it passes** ```bash cd backend && npx jest -c jest.unit.config.js draftPrompt @@ -463,7 +463,7 @@ cd backend && npx jest -c jest.unit.config.js draftPrompt Expected: PASS, 8 tests. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add backend/src/intake/draftPrompt.ts backend/tests/unit/draftPrompt.test.ts @@ -487,12 +487,12 @@ Rates confirmed against the pricing page on 2026-08-31, not recalled: Sonnet 5 $ **Interfaces:** - Produces: `DRAFTING_MODELS`, `DEFAULT_DRAFTING_MODEL`, `isDraftingModel()`, `costMicros()`, and a `draftingModel` admin setting. -- [ ] **Step 1: The catalogue, test first.** `costMicros(model, input, output)` in whole micros. An unrecognised model must price above zero — a budget that reads as unspent however much was spent is the one failure a spend guard cannot have. -- [ ] **Step 2: Add a `choice` type to `adminSettings.ts`.** The module's own docstring says adding a setting means adding a row and nothing else; that holds for `hours` and `text`, and a third type is what makes it hold for a constrained one. Row: `{ key: 'drafting_model', name: 'draftingModel', type: 'choice', fallback: DEFAULT_DRAFTING_MODEL, options: [...] }`. -- [ ] **Step 3: Validate membership in the PUT route**, as a third loop beside the hours and text loops. A value outside the set is a 400, not a stored string that breaks drafting later. -- [ ] **Step 4: The dropdown in `Settings.tsx`**, showing each model's price so the person switching can see that Opus costs 2.5x Sonnet before they pick it. -- [ ] **Step 5:** `npm run lint && npm run build && npm run test:unit`, and the frontend's checks. -- [ ] **Step 6: Commit.** +- [x] **Step 1: The catalogue, test first.** `costMicros(model, input, output)` in whole micros. An unrecognised model must price above zero — a budget that reads as unspent however much was spent is the one failure a spend guard cannot have. +- [x] **Step 2: Add a `choice` type to `adminSettings.ts`.** The module's own docstring says adding a setting means adding a row and nothing else; that holds for `hours` and `text`, and a third type is what makes it hold for a constrained one. Row: `{ key: 'drafting_model', name: 'draftingModel', type: 'choice', fallback: DEFAULT_DRAFTING_MODEL, options: [...] }`. +- [x] **Step 3: Validate membership in the PUT route**, as a third loop beside the hours and text loops. A value outside the set is a 400, not a stored string that breaks drafting later. +- [x] **Step 4: The dropdown in `Settings.tsx`**, showing each model's price so the person switching can see that Opus costs 2.5x Sonnet before they pick it. +- [x] **Step 5:** `npm run lint && npm run build && npm run test:unit`, and the frontend's checks. +- [x] **Step 6: Commit.** --- @@ -509,7 +509,7 @@ Rates confirmed against the pricing page on 2026-08-31, not recalled: Sonnet 5 $ - `costMicros(model: string, inputTokens: number, outputTokens: number): number` - `draftListing(client, input): Promise<{ draft: DraftResult; usage: {...} }>` -- [ ] **Step 1: Write the failing cost test** +- [x] **Step 1: Write the failing cost test** Create `backend/tests/unit/draftCost.test.ts`: @@ -542,7 +542,7 @@ describe('costMicros', () => { }); ``` -- [ ] **Step 2: Run it to verify it fails** +- [x] **Step 2: Run it to verify it fails** ```bash cd backend && npx jest -c jest.unit.config.js draftCost @@ -550,7 +550,7 @@ cd backend && npx jest -c jest.unit.config.js draftCost Expected: FAIL — module not found. -- [ ] **Step 3: Write the client factory** +- [x] **Step 3: Write the client factory** Create `backend/src/intake/anthropicClient.ts`: @@ -587,7 +587,7 @@ export function resetAnthropicClient(): void { } ``` -- [ ] **Step 4: Write the drafting call** +- [x] **Step 4: Write the drafting call** Create `backend/src/intake/draftListing.ts`: @@ -681,7 +681,7 @@ export async function draftListing( } ``` -- [ ] **Step 5: Run the tests and build** +- [x] **Step 5: Run the tests and build** ```bash cd backend && npx jest -c jest.unit.config.js draftCost && npm run build && npm run lint @@ -689,7 +689,7 @@ cd backend && npx jest -c jest.unit.config.js draftCost && npm run build && npm Expected: PASS 5 tests, build clean, no new lint warnings. -- [ ] **Step 6: Commit** +- [x] **Step 6: Commit** ```bash git add backend/src/intake/anthropicClient.ts backend/src/intake/draftListing.ts backend/tests/unit/draftCost.test.ts @@ -708,7 +708,7 @@ git commit -m "feat(intake): draft a listing from photos and a note (#223)" - Consumes: `DraftResult` - Produces: `applyDraft(client: PoolClient, itemId: number, outcome: DraftOutcome): Promise` -- [ ] **Step 1: Write the failing test** +- [x] **Step 1: Write the failing test** Create `backend/tests/integration/drafting.integration.test.ts`: @@ -839,7 +839,7 @@ describe('applying a draft', () => { }); ``` -- [ ] **Step 2: Run it to verify it fails** +- [x] **Step 2: Run it to verify it fails** Bring up a database first (see Global Constraints), then: @@ -849,7 +849,7 @@ cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integr Expected: FAIL — module not found. -- [ ] **Step 3: Write it** +- [x] **Step 3: Write it** Create `backend/src/intake/applyDraft.ts`: @@ -932,7 +932,7 @@ export async function applyDraft( } ``` -- [ ] **Step 4: Run it to verify it passes** +- [x] **Step 4: Run it to verify it passes** ```bash cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integration @@ -940,7 +940,7 @@ cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integr Expected: PASS, 5 tests. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add backend/src/intake/applyDraft.ts backend/tests/integration/drafting.integration.test.ts @@ -959,7 +959,7 @@ git commit -m "feat(intake): record a draft without publishing it (#223)" - Consumes: everything above - Produces: `draftQueued(limit?: number): Promise<{ drafted: number; failed: number; skipped: number }>` -- [ ] **Step 1: Write the failing tests** +- [x] **Step 1: Write the failing tests** Append to `backend/tests/integration/drafting.integration.test.ts`: @@ -1006,7 +1006,7 @@ describe('the drafting worker', () => { }); ``` -- [ ] **Step 2: Run it to verify it fails** +- [x] **Step 2: Run it to verify it fails** ```bash cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integration @@ -1014,7 +1014,7 @@ cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integr Expected: FAIL — module not found. -- [ ] **Step 3: Write the worker** +- [x] **Step 3: Write the worker** Create `backend/src/intake/draftingWorker.ts`: @@ -1156,7 +1156,7 @@ export async function draftQueued( } ``` -- [ ] **Step 4: Run it to verify it passes** +- [x] **Step 4: Run it to verify it passes** ```bash cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integration @@ -1164,7 +1164,7 @@ cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integr Expected: PASS, 7 tests. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add backend/src/intake/draftingWorker.ts backend/tests/integration/drafting.integration.test.ts @@ -1178,7 +1178,7 @@ git commit -m "feat(intake): draft queued submissions without ever losing one (# **Files:** - Modify: `backend/src/routes/intake.ts`, `backend/src/server.ts` -- [ ] **Step 1: Kick the worker after a successful submission** +- [x] **Step 1: Kick the worker after a successful submission** In `backend/src/routes/intake.ts`, after `await client.query('COMMIT');` and before the response: @@ -1196,7 +1196,7 @@ Import it at the top: import { draftQueued } from '../intake/draftingWorker'; ``` -- [ ] **Step 2: Add the sweeper** +- [x] **Step 2: Add the sweeper** In `backend/src/server.ts`, beside the existing schedules: @@ -1214,7 +1214,7 @@ Import it beside the other background work: import { draftQueued } from './intake/draftingWorker'; ``` -- [ ] **Step 3: Verify nothing regressed** +- [x] **Step 3: Verify nothing regressed** ```bash cd backend @@ -1226,7 +1226,7 @@ npx jest -c jest.integration.config.js --runInBand Expected: all pass. The intake suite matters most — submissions must still succeed with no key configured, which is the state the test database runs in. -- [ ] **Step 4: Commit** +- [x] **Step 4: Commit** ```bash git add backend/src/routes/intake.ts backend/src/server.ts @@ -1235,6 +1235,9 @@ git commit -m "feat(intake): run the drafting worker after a submission and on a --- + +> **Tasks 1-7 complete** as of 2026-08-31, on `feature/223-drafting-worker`. Task 8 is the only manual step and is blocked on an API key existing. + ### Task 8: One real call Everything above is stubbed. This is the only step that spends money, and it is the only one that proves the prompt produces something worth reading.