From b57de95a7f309e03d9fd9c2551a4118ace7f4046 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 27 Mar 2026 09:52:46 -0600 Subject: [PATCH] fix: write ALL feedback to disk so agent can poll in background mode The agent backgrounds $D serve (Claude Code can't block on a subprocess and do other work simultaneously). With stdout-only feedback delivery, the agent never sees regenerate/remix feedback. Fix: write feedback-pending.json (regenerate/remix) and feedback.json (submit) to disk next to the board HTML. Agent polls the filesystem instead of reading stdout. Both channels (stdout + disk) are always active so foreground mode still works. Co-Authored-By: Claude Opus 4.6 (1M context) --- design/src/serve.ts | 22 ++++++++++++++++------ design/test/serve.test.ts | 16 +++++++++++++--- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/design/src/serve.ts b/design/src/serve.ts index 14075d4f..7d974905 100644 --- a/design/src/serve.ts +++ b/design/src/serve.ts @@ -21,7 +21,14 @@ * │ * └──(timeout)──► exit 1 * - * Stdout: feedback JSON only (one line per feedback event) + * Feedback delivery (two channels, both always active): + * Stdout: feedback JSON (one line per event) — for foreground mode + * Disk: feedback-pending.json (regenerate/remix) or feedback.json (submit) + * written next to the HTML file — for background mode polling + * + * The agent typically backgrounds $D serve and polls for feedback-pending.json. + * When found: read it, delete it, generate new variants, POST /api/reload. + * * Stderr: structured telemetry (SERVE_STARTED, SERVE_FEEDBACK_RECEIVED, etc.) */ @@ -120,14 +127,17 @@ export async function serve(options: ServeOptions): Promise { console.error(`SERVE_FEEDBACK_RECEIVED: type=${action}`); - // Print feedback JSON to stdout (agent reads this) + // Print feedback JSON to stdout (for foreground mode) console.log(JSON.stringify(body)); - if (isSubmit) { - // Write feedback.json next to the HTML file - const feedbackPath = path.join(path.dirname(html), "feedback.json"); - fs.writeFileSync(feedbackPath, JSON.stringify(body, null, 2)); + // ALWAYS write feedback to disk so the agent can poll for it + // (agent typically backgrounds $D serve, can't read stdout) + const feedbackDir = path.dirname(html); + const feedbackFile = isSubmit ? "feedback.json" : "feedback-pending.json"; + const feedbackPath = path.join(feedbackDir, feedbackFile); + fs.writeFileSync(feedbackPath, JSON.stringify(body, null, 2)); + if (isSubmit) { state = "done"; if (timeoutTimer) clearTimeout(timeoutTimer); diff --git a/design/test/serve.test.ts b/design/test/serve.test.ts index 7112918a..439e4ba7 100644 --- a/design/test/serve.test.ts +++ b/design/test/serve.test.ts @@ -84,10 +84,10 @@ describe('Serve HTTP endpoints', () => { try { body = await req.json(); } catch { return Response.json({ error: 'Invalid JSON' }, { status: 400 }); } if (typeof body !== 'object' || body === null) return Response.json({ error: 'Expected JSON object' }, { status: 400 }); const isSubmit = body.regenerated === false; + const feedbackFile = isSubmit ? 'feedback.json' : 'feedback-pending.json'; + fs.writeFileSync(path.join(tmpDir, feedbackFile), JSON.stringify(body, null, 2)); if (isSubmit) { state = 'done'; - const feedbackPath = path.join(tmpDir, 'feedback.json'); - fs.writeFileSync(feedbackPath, JSON.stringify(body, null, 2)); return Response.json({ received: true, action: 'submitted' }); } state = 'regenerating'; @@ -160,8 +160,12 @@ describe('Serve HTTP endpoints', () => { expect(written.ratings.A).toBe(4); }); - test('POST /api/feedback with regenerate sets state to regenerating', async () => { + test('POST /api/feedback with regenerate sets state and writes feedback-pending.json', async () => { state = 'serving'; + // Clean up any prior pending file + const pendingPath = path.join(tmpDir, 'feedback-pending.json'); + if (fs.existsSync(pendingPath)) fs.unlinkSync(pendingPath); + const feedback = { preferred: 'B', ratings: { A: 3, B: 5, C: 2 }, @@ -185,6 +189,12 @@ describe('Serve HTTP endpoints', () => { const progress = await fetch(`${baseUrl}/api/progress`); const pd = await progress.json(); expect(pd.status).toBe('regenerating'); + + // Agent can poll for feedback-pending.json + expect(fs.existsSync(pendingPath)).toBe(true); + const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8')); + expect(pending.regenerated).toBe(true); + expect(pending.regenerateAction).toBe('different'); }); test('POST /api/feedback with remix contains remixSpec', async () => {