Never report success for a site a visitor cannot open - #17
Merged
Conversation
ValidateArtifact is the other end of the run from ValidatePalette. That one stops a vertical granting more capability than its level allows; this one stops it declaring victory over something a visitor would get a 404 from. The websites vertical requires index.html, present and non-empty. Paths must match exactly: a nested pages/index.html does not count, because the serving gateway joins index.html onto the site root and nowhere else. A written-but-empty file does not count either — it is unservable in the only way that matters to a visitor. This is the minimal first form of the validators the component catalog has declared since M3 without any implementation behind it. A profile that requires nothing still accepts anything, so the gate is opt-in per vertical rather than a platform-wide opinion about what a project must contain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiRDTC3E7HGD6Cbq8QeFXM
Commit gains an injected Validate hook beside the existing Quota one, so "a version exists" now means "a site works". Making it a hook rather than a check in the harness matters: Commit is the only way a version is minted, so every present and future caller is covered by construction rather than by remembering to ask first. Validate runs before Quota. Servability is a property of the content and capacity is a property of the account, so a tenant near their storage limit mid-repair hears "there is no home page yet" — which another turn can fix — instead of "you are out of space", which it cannot. There is a test pinning that order, because it is easy to swap and hard to notice. The store executes policy it does not author: the closure is wired in server.go and its error propagates with %w, so callers branch with errors.Is while the store imports no policy package. A nil hook is the old behaviour exactly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiRDTC3E7HGD6Cbq8QeFXM
The harness no longer decides for itself that the model is finished. When the model declares done, the harness calls Commit — the mint gate — and branches on the answer. Asking the store rather than checking separately means the gate and the check are the same code path and cannot disagree. An unservable artifact buys a repair turn: the agent is told what is missing and given the workspace it already built. Two attempts, counted from repair.started events in the log rather than a local variable, so a worker taking over mid-repair inherits the attempts already spent instead of granting a fresh set. The instruction is logged for the same reason — reconstruct() rebuilds the conversation from the log alone, so an unlogged instruction would vanish on takeover and the model would simply re-declare itself finished. repair.started renders nothing: the user asked for a website and is getting one, and a stumble they never saw is not news. If the repair works, repair.completed carries one line acknowledging the time rather than the artifact — naming the home page would invite worry about a problem that no longer exists. Two paths that used to lie now do not. An exhausted budget fails with plain language and commits nothing. The step-limit path also goes through the gate: "the work so far is saved" is only true if the work is a site, and it was previously said either way. commitProgress treats the refusal as "nothing servable to snapshot yet" and skips silently — parking mid-build before a page exists is legitimate, and the workspace keeps the partial work regardless. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiRDTC3E7HGD6Cbq8QeFXM
The mint gate stops new unservable versions being created; this stops the ones that already exist from ever going live. At least one such version exists in real data, so the backstop is not hypothetical. Rollback checks inside its transaction, between resolving the parent and moving the pointer. A handler-side pre-check would be validating a different version than the one about to go live, because the parent is only known once resolved. The API translates the refusal into a sentence and nothing else — the policy stays in the profile, the enforcement in the stores, and the handler does the one job it has. Both routes were already tenant-scoped, so no new hostile case is needed. Covered by a unit test rather than e2e on purpose: post-fix the product can no longer mint an unservable version, so the only way to exercise the backstop is to seed one through a store with no gate wired. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiRDTC3E7HGD6Cbq8QeFXM
Adds no-page (never writes a page, so the repair budget exhausts deterministically), repairs-site (produces the page only once told, the one script that exercises the repair path), and asks-before-page (parks on a question while the site is still unservable, which is what the mid-run commit skip is for). They stay registered permanently. Also changes slow-site: its first page is now index.html rather than page1.html. The gate exposed that fixture as modelling an impossible output — an eight-page website with no home page, which no real build should ever leave standing — and it had passed the whole suite for four milestones because nothing asserted a version was servable. Step count, per-step delay, and the kill-mid-run window are unchanged, so the two AC-1 tests still prove kill → resume → completion; only the filename the existence check looks for moved with the fixture. assertServable is the guardrail that makes this class visible in future: after a run completes, fetch the newest version's preview root and require 200. Wired into the publish choreography and the new scenarios, so any path that mints a version a visitor cannot open fails in CI rather than in someone's browser. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiRDTC3E7HGD6Cbq8QeFXM
components.md §10: validators stop being a declaration with nothing behind them. The ProductProfile contract gains the artifact-side refusal next to the palette-side one — ValidateArtifact is the floor to ValidatePalette's ceiling — and states the invariant plainly: a version exists only if the vertical can serve it, and the live pointer only ever targets one that exists. architecture.md §3.2: repair.* moves from specified to emitted. PRD open question #6 gets a partial answer, not a closure. A build that finishes without a servable site is repaired autonomously and, if that works, acknowledged with one line naming the delay rather than the artifact. That is a deliberate softening of P7's "repairs silently": the repair is silent, the time it cost is not. The plan's RCA claimed every fake script wrote index.html. An audit found seven of nine did, and the exception backed the two AC-1 acceptance tests while passing the whole suite for four milestones — because nothing asserted servability anywhere. Recorded as a correction rather than quietly fixed: it is stronger evidence for the same conclusion, since the fleet held not just kind fixtures but an impossible one. AGENTS.md now asks that fixtures model outputs a real model could produce, with slow-site named as the in-repo example of that drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiRDTC3E7HGD6Cbq8QeFXM
The gate's copy is a user-reachable contract and had only manual verification behind it. A refactor could have dropped the errors.Is branch and degraded it to serverError's generic "something went wrong on our side" with nothing noticing. Covers both refusal paths, because rollback resolves its target inside its transaction and so travels separate code with separate copy. The fixture seeds a three-version lineage with the middle one unservable, which is the only shape that reaches both: publishing that version directly, and rolling back onto it. Seeded through a store with no gate wired, since post-fix the product cannot create that state — which is the point of the backstop. Asserts the properties that matter rather than exact strings: 409, no implementation jargon, and that the sentence names the missing home page so the reader can act on it. Pinning the literal text would make a comma edit a test failure while catching nothing a user would feel. Verified against both regressions — dropping the mapping yields 500, degrading the copy to "invalid artifact" trips the jargon check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiRDTC3E7HGD6Cbq8QeFXM
Records the accepted coverage limit alongside it: the repair mechanics are owned by deterministic fixtures, and the live qwen3.6 run proves only that the gate does not false-positive on healthy output — a real model's early stop could not be forced on demand. Also drops a now-dangling reference. The provenance line named the superseded draft by path; that file has been deleted, so the line describes it without pointing at something a reader would go looking for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiRDTC3E7HGD6Cbq8QeFXM
The acknowledgment was gated on "did this worker repair?", which a takeover answers no. A worker resuming after a crash mid-repair sees the attempt already in the log, finishes the work the instruction asked for, and then said nothing — losing a repair that demonstrably happened and contradicting the decision recorded in PRD open question #6. The right question is "was this run repaired?", and the log is what answers it: any spent budget followed by a successful commit means a repair worked, whoever finished it. Found in review; the test seeds the state a crashed worker leaves behind and fails on the old predicate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiRDTC3E7HGD6Cbq8QeFXM
The guardrail was wired at three call sites while the suite has twenty-three run-completion waits — the gap between what it claimed and what it covered was exactly the drift the AGENTS.md rule it shipped with warns about, starting on day one. Moved into waitCompletedAuthed, which every authenticated completion already funnels through, so a scenario can no longer omit it by forgetting. The session is mapped back to its project through the public API, so call sites need no new plumbing. The two AC-1 tests wait on an inline predicate rather than the helper, so they get the assertion explicitly — they run over slow-site, the fixture that hid this class for four milestones. Verified it bites: recreating the pre-fix world (gate removed, slow-site writing no home page) fails with "the run completed but the site root serves HTTP 404 — a visitor would see an error page". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiRDTC3E7HGD6Cbq8QeFXM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Creo told people their website was live when visitors got a 404. A build that produced no home page was reported as finished, offered for publishing, and celebrated — the platform's most damaging failure mode is not breaking, it is lying.
Nothing between the agent and that celebration ever asked whether the artifact was a site.
Commitaccepted any set of files, success was inferred from the model's stop reason, and publishing checked only that a version existed. The one component that knew better was the serving gateway, which joinsindex.htmlonto every site root and 404s when it is absent.Solution
Make a version impossible to create unless the vertical can serve it, and give the agent a chance to fix its own mistake first. Three layers, outermost first:
The profile owns the policy.
ValidateArtifactis the floor toValidatePalette's ceiling — one stops a vertical granting more capability than its level allows, the other stops it declaring victory over something a visitor cannot open. The websites vertical requiresindex.html, present and non-empty. That is not a convention: it is what the gateway resolves a site root to, so a bundle without one is unservable by construction.The stores enforce policy they do not author. Both gates are injected closures wired in one place, mirroring the existing
Quotahook, soprojectandpublishimport no policy package. InsideCommit,Validateruns beforeQuota— servability is a property of the content, capacity a property of the account, and a tenant near their limit mid-repair should hear "there is no home page yet" (which another turn can fix) rather than "you are out of space" (which it cannot). There is a test pinning that order.The harness asks rather than checks. When the model declares itself finished, the harness calls
Commitand branches on the error. Asking the gate rather than duplicating it means the two cannot disagree, and it needs no new workspace API. An unservable artifact buys up to two repair turns; the instruction is written to the log becausereconstruct()rebuilds the conversation from the log alone, so an unlogged one would vanish on takeover and the model would simply re-declare itself done. The budget is counted from those logged events for the same reason.Two paths that used to lie now do not. An exhausted budget commits nothing and fails in plain language. The step-limit path also goes through the gate — "the work so far is saved" is only true when the work is a site, and it was previously said either way.
Before / After
Same scenario in both: a build that never writes a home page.
404 page not found.And when the agent can fix it, the user simply gets a website:
Verified against the real bug, not only fixtures
The CSS-only version from the original report was still in a local data directory, so the backstop was tested against it rather than against a mock:
A full qwen3.6 build also ran end to end: it produced a real four-file site, published, and served HTTP 200 — the gate does not false-positive on healthy output.
Honest gaps
The repair acknowledgment is currently invisible in the browser — tracked as #18, since web changes are out of scope here.
A real-model early stop could not be forced on demand. The original occurrence was luck. The repair mechanics are covered exhaustively by deterministic fixtures; the live run proves absence of false positives, not presence of a real-world repair.
Other Changes
assertServablefetches the newest version's preview root after a run completes and requires 200. It lives insidewaitCompletedAuthed, which every authenticated completion already funnels through, so a scenario cannot opt out by forgetting; the two AC-1 tests wait on an inline predicate and get it explicitly. Verified it bites by recreating the pre-fix world — gate removed,slow-sitewriting no home page — which fails with "the run completed but the site root serves HTTP 404 — a visitor would see an error page".slow-sitewrotepage1.html…page8.htmland no home page — an eight-page website nobody could open — and passed the entire suite for four milestones, because nothing asserted servability. Its first page is nowindex.html; step count, per-step delay and the kill-mid-run window are unchanged, so both AC-1 tests still prove kill → resume → completion. This corrects a premise in the plan document, which had claimed every script wrote a home page; recorded there as a correction because it is stronger evidence for the same conclusion, not a retraction.no-page,repairs-site) plusasks-before-pagestay registered permanently, andAGENTS.mdnow asks that fixtures model outputs a real model could produce.repair.started/repair.completedmove from specified-but-unemitted to emitted;validatorsin the component catalog stops being a declaration with nothing behind it.web/orinternal/webui/distchanges.Related:
🤖 Generated with Claude Code