Repository navigation
ISS-13846: Accept real runner evidence in mutation-proof gate - #422
Merged
Merged
Conversation
Real Storybook, Vitest and Playwright failures bind the exact candidate through a private raw receipt; comment-only mutations and authored failure text are refused. A remote-tier Desktop spec is carried as not run only when a live probe of the selected runner agrees; inventory and PR comment share one decision and one candidate base. The verifier documents the remote-tier record; the handoff skill documents a credential-free web E2E proof route. vibe 1.1.68.
…on-proofs # Conflicts: # CHANGELOG.md # plugins/vibe/.claude-plugin/plugin.json # plugins/vibe/.codex-plugin/plugin.json # plugins/vibe/README.md # plugins/vibe/tools/python/test_pr_takeover_skill_contract.py # plugins/vibe/tools/python/test_vibe_ws8_load_contract.py # tools/vibe-bench/src/reply-checker.test.ts
A tier-C probed commit must be a full SHA before it reaches a regex or the remote shell. A direct Storybook frame on another assertion's line no longer proves this candidate. Card-layout candidates keep the context scan; only busy or loading counts are excluded. The verifier documents the raw receipt fields; a misplaced JSDoc is moved back.
…on-proofs # Conflicts: # CHANGELOG.md # plugins/vibe/.claude-plugin/plugin.json # plugins/vibe/.codex-plugin/plugin.json # plugins/vibe/README.md # plugins/vibe/tools/python/test_pr_takeover_skill_contract.py # plugins/vibe/tools/python/test_vibe_ws8_load_contract.py # tools/vibe-bench/src/reply-checker.test.ts
A runner-owned Storybook frame inside another assertion's span no longer proves this candidate; Proxy instrumenter frames keep the receipt-bound mutation check. The runner header must name exactly the candidate's story, not a sibling whose name it prefixes. Card-layout candidates exclude only whole-word busy or loading counts. Tests pin the generic slot report checks for the remote Desktop carry.
On the instrumenter path, a later same-matcher assertion over the same query makes the proof ambiguous and is refused. A busy: false or aria-busy false filter counts ordinary cards and keeps the layout check. A test pins that a restored run is proof only with a numeric zero exit.
The v1.1.68 entry now names the sibling-story and same-query instrumenter refusals.
An instrumented Storybook proof is refused when a later same-matcher assertion could also see the mutation: same variable, a query in any quote or regex form equal to the candidate's or in the inserted text, or a same-matcher helper call. Card counts filtered by a negated busy or loading predicate keep their layout check.
An instrumented Storybook proof must mutate its own story file, and nothing that runs after the mutation (helpers, held queries, composed plays, soft assertions, a helper's calling stories) may share its query. Card counts excluding busy or loading items in any form keep their layout check. The remote-tier receipt test now fails on the receipt it names.
Candidate and later queries are keyed from any testing-library call, a selector or a variable holding one; a later query the gate cannot read is a known limit, not a refusal. Arrow helpers are attributed, and a helper's own definition is no longer counted as a call. A variable named loading or busy no longer exempts a card count from its layout check.
This was referenced Nov 10, 2026
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.
Fixes ISS-13846. The handoff mutation-proof gate refused real runner evidence: genuine Storybook failures, package-relative Vitest coordinates and a truthful remote-tier Desktop carry. That blocked ISS-13699 even after exact-head validation and genuine mutation proofs. This change makes the gate accept that evidence while tightening what counts as proof. vibe 1.1.75.
Vibe gate override: ISS-13900: the installed vibe 1.1.71 handoff gate refuses this PR only on prEvidence (ISS-13900 defect 9, the claude-plugins evidence schema). All four review receipts pass, and every other blocking gate is true (forbiddenPaths, hasChanges, storiesInV2Taxonomy, existingStoryTitlesKept, diffRulesClear, prBody). The 110 evidence problems are 91 candidates read from assertion text inside this PR's own test fixture strings, which main's candidate scan also reports, and 19 symphony-alpha evidence fields this repository cannot produce: e2e by-reference selections, touchFlows, the workflow lane report, the E2E capability probe, and exact-SHA targeted runs for its node test files.
What changed
AC-1 and AC-2: real runner failures bind the exact candidate (
plugins/vibe/skills/handoff/scripts/test-integrity.mjs).stories.tsxprimary assertion now satisfies its candidate. That includes the browser-prefixedFAILheader, the play helper an exported story calls, and the instrumenter's source-map coordinate after a verified adjacent mutation.{/* */}comment, is refused;expect(...)argument (a nested check, awaitForcallback or an imported helper call) is refused, and so is a frame on another assertion that shares a multiline candidate's closing line; the matcher line and theexpectstart still bind;FAIL ... > Story Nameheader must name exactly the candidate's story, not a sibling whose name it prefixes;await expect(idle).not.toBeNull()is no longer an absence candidate, unless that line holds only the assertion's argument or its argument there nests another check or a callback (otherwise main's line fallback is unchanged), and a count whose own assertion names a busy or loading state is no longer card-layout proof (decision PC-2, accepted by the orchestrator); a negated busy predicate (busy: false,aria-busy="false",:not([aria-busy]),!card.loading,card.loading === false,card.status !== "loading",card.dataset.busy === "false") still counts ordinary cards and keeps the layout check. Real absent-element and card-layout assertions still need their proofs, including a count whose card or summary comes only from its test context.AC-3: remote-tier Desktop carry, one decision for inventory and comment (
e2e-tier.mjs,pr-facts.mjs,pr-evidence-comment.mjs)e2e-slot.shroute) finds no supported displayless Electron route (the commit must be a full SHA, so evidence text never reaches the remote shell); the selected lane and spec record match; and the post-mergedesktop-e2e/validatedowner is named. A runner whose native Desktop slot executes keeps the spec required. Fabricated, caller-hashed and stale probes fail.prEvidenceValidationProblems) and one proof-candidate base (proofCorpusBase). The comment previously hard-codedorigin/main.agents/vibe-verify-worker.mddocuments the remote-tier record fields the validator reads.AC-4: credential-free web E2E proof route (
skills/handoff/SKILL.md)AC-5: real ISS-13699 evidence. Run against the real ISS-13699 exact-head checkout (
5afa61fa) and its private evidence, the pull-request inventory returnsok: truewith all seven blocking gates true and zero evidence or body problems. The evidence comment exits 0. The ticket's reproduction of the same inventory on installed 1.1.61 failedprEvidencewith 26 problems.pr-evidence-schema.mjstakes the worktree and checks proofs against the tested source.Main merge (#418, vibe-core). #418 moved
tested-head.mjsandsession-record.mjsintoplugins/vibe-core/liband loads them throughshared/vibe-core.mjs. This branch's own imports of them inpr-facts.mjs,pr-evidence-schema.mjs,pr-evidence-comment.mjsandpr-evidence-comment.test.mjsnow use that loader, with the same names; no file this branch changes was moved.Not in this PR
plugins/vibe-core/hooks/) are unchanged here.Assertion changes
Every cited line keeps its expected value or regex. Each existing call now passes the receipt directory and receipt-bound proofs, because absence proofs require a private raw runner receipt (AC-1, AC-5).
plugins/vibe/skills/handoff/scripts/test-integrity.test.mjs:150: same[]; the proofs are now receipt-bound and the call passes the receipt directory.plugins/vibe/skills/handoff/scripts/test-integrity.test.mjs:152: the mocked-owner negative clones the receipt-bound proofs; itsownerBoundaryregex is unchanged.plugins/vibe/skills/handoff/scripts/test-integrity.test.mjs:207: new assertion. A receipt without recoverable mutation bytes is refused.plugins/vibe/skills/handoff/scripts/test-integrity.test.mjs:227,plugins/vibe/skills/handoff/scripts/test-integrity.test.mjs:231andplugins/vibe/skills/handoff/scripts/test-integrity.test.mjs:235: same regexes (exact assertion failure,primary assertion location); the setup-crash, wrong-assertion and unrelated-stack proofs are receipt-bound.plugins/vibe/skills/handoff/scripts/test-integrity.test.mjs:239: same[]; the UI-text-mentions-TypeError proof is receipt-bound.plugins/vibe/skills/handoff/scripts/test-integrity.test.mjs:482: same[]; the two proofs on one changed line are receipt-bound.plugins/vibe/skills/handoff/scripts/test-integrity.test.mjs:503: sameprimary assertion locationregex; the proof is receipt-bound.plugins/vibe/skills/handoff/scripts/pr-evidence-comment.test.mjs:67: the multiline proof input now goes through the receipt helper. The rendered-text assertion on the next lines is unchanged.plugins/vibe/skills/handoff/scripts/pr-evidence-comment.test.mjs:62andplugins/vibe/skills/handoff/scripts/pr-evidence-comment.test.mjs:74pass the receipt options; their regexes are unchanged.Contract changes
Internal handoff script exports. Each adds only an optional trailing parameter, and every in-repo caller is updated.
specExecutionLimitation(capability, spec, commit, runProbe)andtierEvidenceProblems(capability, e2eSpecs, runProbe): an injectable selected-runner probe; the default probes the runner over SSH.testIntegrityProblems(..., receiptDir, readCandidates)(the changed corpus reader, defaulting to the proof reader),testIntegrityProofShapeProblems(evidence, receiptDir, readSource),prEvidenceShapeProblems(evidence, receiptDir, readSource)andtargetedEvidenceProblems(runsFile, commit, worktree, receiptDir): the private receipt directory and a tested-source reader. An absence proof without a receipt is now refused.renderEvidenceComment(evidence, worktree, options): options carry the receipt directory and candidate corpus to the shared validation.capabilityProbeProblems:probeCapabilitynow defaults to the live probe.proofCorpusBase(worktree, comparisonOperand)inpr-facts.mjs, the one proof-candidate base for the inventory and the comment.Tests
test-integrity.test.mjs(Storybook, Vitest, Playwright and receipt cases, and the comment-only, JSX-comment and authored-text refusals);e2e-tier.test.mjs(selected-runner probe, stale and fabricated probes, supported native route);pr-evidence-comment.test.mjs(inventory and comment parity, the shared corpus base, tampered receipts).test-integrity.test.mjsandpr-evidence-comment.test.mjsnow store runner-shaped raw output and an executable mutation, because AC-1 and AC-5 make authored text and comment-only edits insufficient. Every pre-existing[]expectation, regex and rendered-text assertion is byte-identical. Threepr-facts.test.mjsfixtures now commit their synthetic source and carry source identity and selection inputs; their assertions are unchanged.tools/vibe-bench/src/reply-checker.test.tsare bumped to 1.1.75.Known limits (non-blocking review notes)
Known limits tracked in ISS-13941.
const label = labelFor(grid); await expect(within(canvasElement).queryByText(label)).toBeNull();; and a later assertion that depends on the mutation only semantically, for example a mutation that removesawait userEvent.click(dismiss)before a laterexpect(canvas.queryByRole("dialog")).toBeNull().restoredExitas the number 0, and missing, string, null and non-zero values are refused (pinned by "ISS-13846 final review A-2 a restored run is proof only with a numeric zero exit").functiondeclared on its own line inside the play body, so a later same-matcher assertion after that line is not seen. For example, a play withawait expect(within(grid).queryByText("Loading")).toBeNull();, thenfunction noop() {}on the next line, thenawait expect(within(canvasElement).queryByText("Loading")).toBeNull();.expect(page.getByRole("article", { name: "Loading dock" })).toHaveCount(1).runnerHostandrunnerWorktreethe evidence names, live at the exact head; nothing binds that host to a configured runner, because the plugin has no runner registry to bind against. A host that reaches the probe and lacks a displayless Electron route passes.toBeNull, for exampleexpect(screen.queryByText("a")).not.toBeNull(); expect(screen.queryByText("b")).toBeTruthy();. This over-demands proof and cannot pass wrong evidence; main demands a proof for the same line.expect(screen.queryByText("a")).not.toBeNull(); expect(screen.queryByText("b")).toBeFalsy();. A parsed absence assertion and an unparsed check on one line yield only the parsed candidate, as on main.await expect(canvas.queryByText(/loading/i)).toBeNull();and laterawait expect(within(canvasElement).queryByText("Loading")).toBeNull();.await expectNoLoading(canvas);imported from./helpers.await expect(await waitFor(() => { const el = canvas.queryByText("Loading"); return el; })).toBeNull();after the mutation.await expect(rows).toHaveCount(0, { timeout: 5000 }), and an option-negated matcher, for exampleawait expect(dialog).toBeVisible({ visible: false }).test-integrity.test.mjsis at 1,361 lines andpr-facts.test.mjsat 999. The repo has no size gate; splitting the new review-fix cases into a sibling test file is a follow-up (guardrails advisory).expect.poll) binds any frame on its line within the line's text, so a second assertion's failure on the same line counts, for exampleawait expect.poll(() => panel.count()).toBeNull(); expect(title).toEqual("Docs");. Main refused this with an exact column match.other()framed at line 2 ofawait expect(rows).not.toBeVisible({/timeout: await other(),/});. Main refused this with an exact line match.expect(cards.filter((card) => !card.hasAttribute("aria-busy"))).toHaveLength(1);,=== null,=== undefinedor a ternary.expect(screen.getAllByRole("status", { busy: true })).toHaveLength(1); expect(cards).toHaveLength(1);. Main flags that line. Formatted code puts the two statements on separate lines.const nav = screen.getByRole("navigation")changed toconst nav = document.bodybeforeexpect(within(nav).queryByText("1-10 of 50")).toBeNull(). Main had no receipt binding and accepted authored text at the coordinate.expect(screen.getAllByText("Busy hours")).toHaveLength(1). Main raised the layout candidate.core.quotePath) is not recognized as a test file, so it yields no proof candidate, for example a changednaïve.test.ts. Main has the same diff parsing.guardrails.mdno longer spells out "observe that exact assertion fail, then restore and pass; a green normal run or an unrun mutation is not proof". The worker files keep the fail-then-restore rule (agents/vibe-change-worker.md,agents/vibe-verify-worker.md), and the gate enforces the rest through the raw receipt's mutation and restored exits.diagnosticPathMatchesintest-integrity.mjsknows the package rootspackages/app/,apps/storybook/andapps/desktop/. Making it repository-neutral is a behavior change, left out of this PR.test-integrity.test.mjsare refused through the receipt-binding check, not the identity check. The bound Vitest:1:5negative and the new F2 test cover identity.pr-evidence-schema.mjstakes the worktree only from this branch's script; installed 1.1.71 rejects that option until this release lands.Validation
Head 788a8fb (tree 6727ca61): this branch merged with main 182af94 (#418, vibe-core) in 41274c5, then moved to vibe 1.1.75 (version strings only).
node-tests,test,vibe-eval-t0,lint,typecheckand the version checks), not by a local run.git diff --checkclean; the 1.1.75 changelog check exits 0 (net -2 instruction words against 1.1.71). The real ISS-13699 exact-head (5afa61fa) pull-request inventory, run with this branch's scripts against its unchanged private body copy, hasprEvidencetrue with zero evidence problems, and its evidence comment exits 0.prBodythere is false only because the frozen body copy, written when the proof was taken, does not name four symphony-alpha PRs opened since (#9732, #9735, #9739, #9743).uv run pytest plugins/vibe(668), vibe-bench typecheck,npm testand plugin validation, codex mirror.Docs
Instruction words: about 2 shorter than in 1.1.67 (the
instruction-words.mjschangelog check exits 0). Prose the new gates now enforce was trimmed fromguardrails.md,vibe-change-worker.md,vibe-verify-worker.mdandskills/handoff/SKILL.md.