Found during merged-main verification of #40 (c046ef8). Recording only.
1. The scripted refinement marker is one-shot. apps/agent/src/project.ts:484 guards the transform with !textNode.text.endsWith(" (refined)"), so once the seeded surface already carries the marker the adapter returns it unchanged. Executed against the real agent: turns 2, 3 and 4 of a refinement thread were byte-identical (592 B, same sha), each still reporting outcome: passed, rendering a canvas, and offering Accept. The slice's own stated invariant is that a scripted refinement returns a deterministic change when the seed is present — that holds for refinement #1 and silently stops thereafter. Real models are unaffected (verified live with gemma4:e4b). Untested because both the DOM spec and the agent test perform exactly one refinement.
2. Refine is offered on failed turns and dead-ends. build.ts:112 records progress.surface from the last attempt regardless of outcome, so canRefine (build-view.tsx:142) and the seed selection (state.tsx:517) both accept a failed turn. Refining one seeds the invalid surface and, in scripted mode, the single-entry refinement script exhausts: the turn renders the raw string ScriptedAdapter: script exhausted after 1 generation(s) with no surface. The natural reaction to a failure — "fix the broken bit" — is the one path that breaks.
3. An intent with no worked example silently borrows another intent's. apps/agent/src/project.ts:517-518 falls back with matching.at(-1) ?? examples.at(-1), crossing intents. The run reports passed and renders a screen built for a different intent; Accept then refuses it forever (correctly — the surface doesn't lint for the selected intent). Note the asymmetry with the adjacent lines, which do fail honestly when no example exists at all.
Directions (not decided): make the marker idempotent-but-progressive or vary it per turn; gate canRefine on outcome === "passed"; make the cross-intent fallback an honest refusal like its neighbour.
🤖 Generated with Claude Code
Found during merged-main verification of #40 (c046ef8). Recording only.
1. The scripted refinement marker is one-shot.
apps/agent/src/project.ts:484guards the transform with!textNode.text.endsWith(" (refined)"), so once the seeded surface already carries the marker the adapter returns it unchanged. Executed against the real agent: turns 2, 3 and 4 of a refinement thread were byte-identical (592 B, same sha), each still reportingoutcome: passed, rendering a canvas, and offering Accept. The slice's own stated invariant is that a scripted refinement returns a deterministic change when the seed is present — that holds for refinement #1 and silently stops thereafter. Real models are unaffected (verified live withgemma4:e4b). Untested because both the DOM spec and the agent test perform exactly one refinement.2. Refine is offered on failed turns and dead-ends.
build.ts:112recordsprogress.surfacefrom the last attempt regardless of outcome, socanRefine(build-view.tsx:142) and the seed selection (state.tsx:517) both accept a failed turn. Refining one seeds the invalid surface and, in scripted mode, the single-entry refinement script exhausts: the turn renders the raw stringScriptedAdapter: script exhausted after 1 generation(s)with no surface. The natural reaction to a failure — "fix the broken bit" — is the one path that breaks.3. An intent with no worked example silently borrows another intent's.
apps/agent/src/project.ts:517-518falls back withmatching.at(-1) ?? examples.at(-1), crossing intents. The run reportspassedand renders a screen built for a different intent; Accept then refuses it forever (correctly — the surface doesn't lint for the selected intent). Note the asymmetry with the adjacent lines, which do fail honestly when no example exists at all.Directions (not decided): make the marker idempotent-but-progressive or vary it per turn; gate
canRefineonoutcome === "passed"; make the cross-intent fallback an honest refusal like its neighbour.🤖 Generated with Claude Code