agentiqa 1.1.88 → 1.1.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +3 -3
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -48,7 +48,7 @@ Learn more: \x1B[34m${n}\x1B[0m
48
48
  `),n=e.match(/credential\s+['"`]([^'"`]+)['"`]|\bas\s+['"`]([^'"`]+)['"`]|['"`]([^'"`\s]+@[^'"`\s]+)['"`]/i);return n?n[1]??n[2]??n[3]:e.match(/[\w.+-]+@[\w.-]+\.\w+/)?.[0]}function eY(t){let e=t.credentialRole?.trim();return e||ok([t])}function tY(t){if(t.length<=1)return t;let e=t.map(c=>c.credentialRole?.trim()).filter(c=>!!c),n=t.map(c=>eY(c)).filter(c=>!!c);if(new Set(n.map(c=>c.toLowerCase())).size>1)return t;let s=e[0],a=ZK(t),i=s??ok(t),o=i?` as '${i}'`:"";return[{text:a?`Sign in with ${a}${o}`:`Log in${o}`,type:"setup",authRole:"login",...s?{credentialRole:s}:{}}]}function nY(t){let e=[];for(let n=0;n<t.length;){if(t[n].authRole!=="login"){e.push(t[n]),n++;continue}let r=n+1;for(;r<t.length&&t[r].authRole==="login";)r++;e.push(...tY(t.slice(n,r))),n=r}return e}function Ac(t,e){return e?.authModeHint==="under_test"?{steps:t,authMode:"under_test"}:t.some(n=>n.authRole)?{steps:nY(t),authMode:"precondition"}:{steps:t}}var ms="CREDENTIAL_ROLE_NOT_CONFIGURED";function xa(t){if(typeof t!="string")return null;let e=t.trim();return e.length>0?e:null}function _r(t,e){let n=xa(t);if(!n)return{requested:null,canonical:null,state:"not_configured"};let r=n.toLocaleLowerCase(),s=e.map(xa).find(a=>a?.toLocaleLowerCase()===r)??null;return s?{requested:n,canonical:s,state:"configured"}:{requested:n,canonical:null,state:"not_configured"}}function Xs(t){return t.authRole!=="login"?null:xa(t.credentialRole)}function yp(t,e){let n=new Map;for(let r of e){if(!Number.isInteger(r.stepIndex)||r.stepIndex<0||r.stepIndex>=t.steps.length)throw new Error(`Credential role override has invalid step index: ${r.stepIndex}`);if(n.has(r.stepIndex))throw new Error(`Duplicate credential role override for step index ${r.stepIndex}`);if(t.steps[r.stepIndex].authRole!=="login")throw new Error(`Credential role override must target a login step: ${r.stepIndex}`);let a=xa(r.credentialRole);if(!a)throw new Error(`Credential role override must be non-empty: ${r.stepIndex}`);n.set(r.stepIndex,a)}return{...t,steps:t.steps.map((r,s)=>{let a=n.get(s);return a?{...r,credentialRole:a}:r})}}var Hn="CREDENTIAL_POOL_EXHAUSTED";var Po="Project settings \u2192 Project credentials";function bp(t){let e=(t??[]).filter(s=>typeof s=="string"&&s.trim()),n=e.map(s=>`"${s.trim()}"`).join(", ");return`Every shared account for ${e.length===1?`role ${n} is`:`roles ${n} are`} currently in use by another run. Wait for a run to finish, or ask a project admin to add another account to ${e.length===1?"that role":"those roles"} in ${Po}.`}function lk(t,e){if(t!==409)return null;let n;try{n=JSON.parse(e)}catch{return null}if(!n||n.code!==Hn)return null;let r=Array.isArray(n.names)?n.names.filter(s=>typeof s=="string"):[];return{error:typeof n.error=="string"?n.error:bp(r),names:r}}function ck(t,e){let n=new Map;for(let r of e)r?.name&&r.login&&n.set(r.name.trim().toLowerCase(),{name:r.name,secret:r.secret,login:r.login});for(let r of t){if(!r?.name)continue;let s=typeof r.login=="string"?r.login.trim():"";n.set(r.name.trim().toLowerCase(),{name:r.name,secret:r.secret,...s?{login:s}:{}})}return Array.from(n.values())}function Qs(t,e){let n=new Set((e??[]).filter(a=>typeof a=="string").map(a=>a.trim().toLowerCase())),r=[],s=new Set;for(let a of t??[]){if(typeof a!="string")continue;let i=a.trim().toLowerCase();!i||n.has(i)||s.has(i)||(s.add(i),r.push(a))}return r}var rY=new Set(["password","username","email","login","user"]);function ka(t){return rY.has(t.trim().toLowerCase())}var dk="A-Za-z0-9@._+-";function sY(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function aY(t,e="i"){return new RegExp(`(?<![${dk}])${sY(t)}(?![${dk}])`,e)}function Lo(t,e){let n=new Set;if(!t)return n;let r=[...e].filter(a=>typeof a=="string"&&a.trim().length>0).sort((a,i)=>i.trim().length-a.trim().length),s=t;for(let a of r){let i=a.trim(),o=i.toLowerCase();if(n.has(o))continue;let l=aY(i,"gi"),c=!1;s=s.replace(l,d=>(c=!0,"\0".repeat(d.length))),c&&n.add(o)}return n}function kc(t,e){if(!t)return null;let n=new Map;for(let s of e){if(typeof s!="string")continue;let a=s.trim();if(!a||ka(a))continue;let i=a.toLowerCase();n.has(i)||n.set(i,a)}let r=Lo(t,[...n.values()]);return r.size!==1?null:n.get([...r][0])??null}var iY=[{re:/credential\s+['"`]([^'"`]+)['"`]/gi,quoted:!0},{re:/\bas\s+['"`]([^'"`]+)['"`]/gi,quoted:!0},{re:/['"`]([^'"`\s]+@[^'"`\s]+)['"`]/g,quoted:!0},{re:/[\w.+-]+@[\w.-]+\.\w+/g,quoted:!1}];function _p(t){return(t.steps??[]).filter(e=>gp(e))}function Cc(t){let e=[],n=new Set;for(let r of _p(t)){let s=Xs(r);if(s){let i=s.toLowerCase();n.has(i)||(n.add(i),e.push(s));continue}let a=r.text??"";if(a)for(let i of iY){i.re.lastIndex=0;let o;for(;(o=i.re.exec(a))!==null;){let l=(o[1]??o[0]??"").trim();if(!l||!i.quoted&&ka(l))continue;let c=l.toLowerCase();n.has(c)||(n.add(c),e.push(l))}}}return e}var xc=(t,e=[])=>({ok:!0,satisfiedBy:t,referenced:e,missing:[],message:""});function uk(t,e){if(t.authMode==="under_test")return xc("auth_under_test");let n=_p(t);if(n.length===0)return xc("no_signin_step");let r=(e??[]).filter(u=>typeof u=="string"&&u.trim().length>0),s=new Set(r.map(u=>u.trim().toLowerCase())),a=Cc(t),i=a.filter(u=>!s.has(u.trim().toLowerCase())),o=n.map(Xs).filter(u=>u!==null),l=new Set(o.map(u=>u.toLowerCase())),c=i.some(u=>l.has(u.toLowerCase()));if(o.length>0)return i.length===0?xc("name_match",a):{ok:!1,satisfiedBy:null,referenced:a,missing:i,message:vp(i),...c?{code:ms}:{}};if(a.length>0&&i.length<a.length)return xc("name_match",a);let d=n.map(u=>u.text??"").join(`
49
49
  `);return kc(d,r)!==null?xc("step_reference",a):{ok:!1,satisfiedBy:null,referenced:a,missing:i,message:vp(i)}}function vp(t){let e=(t??[]).filter(s=>typeof s=="string"&&s.trim());if(e.length===0)return`This plan declares a sign-in step but does not name the credential it uses, so there is no name to look up against your stored credentials. Add the login this plan needs in ${Po}, and name the credential in the sign-in step so the plan says which one it wants. On a shared project each teammate stores their own login \u2014 nobody else can see it.`;let n=e.map(s=>`"${s.trim()}"`).join(", "),r=e.length===1?"credential":"credentials";return`This plan signs in with the ${r} ${n}, which you have not stored. Add your own in ${Po} using the same ${r==="credential"?"name":"names"} \u2014 the name is only the key the plan looks up, the secret stays yours. On a shared project each teammate stores their own login \u2014 nobody else can see it.`}var qn=t=>t.trim().toLowerCase();function oY(t,e){let n=[],r=new Set;for(let d of e??[]){if(typeof d!="string")continue;let u=qn(d);!u||r.has(u)||(r.add(u),n.push(d))}if(n.length===0)return{required:[],optional:[]};let s=(t??[]).filter(d=>!!d);if(s.length===0)return{required:[],optional:n};let a=n.filter(d=>!ka(d)),i=new Set,o=new Set;for(let d of s){if(d.authMode==="under_test")continue;let u=new Set(Cc(d).map(v=>qn(v))),f=_p(d).filter(v=>Xs(v)===null).map(v=>v.text??"").join(`
50
50
  `),h=(d.steps??[]).filter(v=>!gp(v)&&Uy(v.text)).map(v=>v.text??"").join(`
51
- `),p=Lo(f,a),m=Lo(h,a),g=!1;for(let v of n){let w=qn(v);u.has(w)||p.has(w)?(i.add(w),g=!0):m.has(w)&&(o.add(w),g=!0)}if(!g&&u.size===0)for(let v of n)o.add(qn(v))}let l=n.filter(d=>i.has(qn(d))),c=n.filter(d=>!i.has(qn(d))&&o.has(qn(d)));return{required:l,optional:c}}function pk(t,e,n){let r=Qs([...e,...n],[]),s=oY(t,r);return{required:Qs(s.required,n),optional:Qs(s.optional,n)}}function lY(t){let e=t;return!e||e.code!==Hn?null:Array.isArray(e.names)?e.names.filter(n=>typeof n=="string"):[]}async function hk(t){let e=t.scope?.required??[],n=t.scope?.optional??[],r=t.maxAttempts??3,s=new Set(n.map(l=>qn(l))),a=new Set(e.map(l=>qn(l))),i=l=>{let c=new Set(l.map(d=>qn(d.name)));return n.filter(d=>!c.has(qn(d)))},o=Qs([...e,...n],[]);for(let l=0;l<r;l++){if(o.length===0)return e.length>0?{ok:!1,refused:[...e]}:{ok:!0,granted:[],skipped:[...n]};try{let c=o.filter(u=>s.has(qn(u))&&!a.has(qn(u))),d=await t.lease(o,c);return{ok:!0,granted:d,skipped:i(d)}}catch(c){let d=lY(c);if(d===null)throw c;let u=new Set(d.map(p=>qn(p))),f=e.filter(p=>u.has(qn(p)));if(f.length>0)return{ok:!1,refused:f};let h=o.filter(p=>!u.has(qn(p)));if(h.length===o.length)return e.length>0?{ok:!1,refused:[...e]}:{ok:!0,granted:[],skipped:[...n]};o=h}}return e.length>0?{ok:!1,refused:[...e]}:{ok:!0,granted:[],skipped:[...n]}}var Fy={OBSERVATION_ARCHIVE:{key:"OBSERVATION_ARCHIVE",envVars:["AGENTIQA_OBSERVATION_ARCHIVE","AGENTIQA_EXPERIMENT_OBSERVATION_ARCHIVE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Enables opt-in private observation and provider-attempt custody for trusted rehearsal sessions; capture failure marks the archive incomplete without changing the run verdict.",designDoc:"docs/plans/2026-10-06-rehearsal-observation-reliability-design.md",status:"active",added:"2026-10-06",notes:"Requires separately configured private storage and trusted campaign scope. The flag alone grants no archive access.",graduation:{status:"permanent",gate:"Remain opt-in for approved observation-custody campaigns; no general capture graduation.",evidence:"Private manifest/GET/hash verification and post-engine-exit retrieval on the deployed candidate.",owner:"steering (Alex)",review:"2026-10-08"}},RUNNER_OBSERVATION_LEDGER:{key:"RUNNER_OBSERVATION_LEDGER",envVars:["AGENTIQA_RUNNER_OBSERVATION_LEDGER","AGENTIQA_EXPERIMENT_RUNNER_OBSERVATION_LEDGER"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Retains bounded historical observations in normal and concluding runner grading inputs and enables evidence-bound authored-contract reconciliation under the existing once-per-run block allowance.",designDoc:"docs/plans/2026-10-06-rehearsal-observation-reliability-design.md",status:"active",added:"2026-10-06",graduation:{status:"gated",gate:"Negative-preserving synthetic controls and complete 21 focused plus 25 corpus rehearsal on the final candidate, followed by approved release.",evidence:"Actual normal/concluding provider inputs, authored action history and independent criterion adjudication.",owner:"steering (Alex)",review:"2026-10-08"}},NATIVE_INTERACTION_BROKER:{key:"NATIVE_INTERACTION_BROKER",envVars:["AGENTIQA_NATIVE_INTERACTION_BROKER","AGENTIQA_EXPERIMENT_NATIVE_INTERACTION_BROKER"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Uses acknowledged Chromium native-interception readiness and page-owned late events across guarded click paths, with exact upload targeting and independent dispatch receipts.",designDoc:"docs/plans/2026-10-06-rehearsal-observation-reliability-design.md",status:"active",added:"2026-10-06",notes:"Session-latched; initial enablement is hosted Chromium. No universal write-admission gate or causal attribution from event arrival order.",graduation:{status:"gated",gate:"Real-browser readiness/late-event/target-identity controls and the complete final-candidate hosted rehearsal.",evidence:"Exact dispatch counts, native-event identities, file-count checks and retained scope/actionability regressions.",owner:"steering (Alex)",review:"2026-10-08"}},RUNTIME_AGENT_EVAL:{key:"RUNTIME_AGENT_EVAL",envVars:["AGENTIQA_RUNTIME_AGENT_EVAL","AGENTIQA_EXPERIMENT_RUNTIME_AGENT_EVAL"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"When enabled, activates the autonomous multi-signal agent runtime evaluation harness (git diff + telemetry + customer findings correlation, fault injection, and zero-browser invariant verification).",designDoc:"docs/plans/2026-09-29-runtime-agent-eval-signals-design.md",status:"active",added:"2026-09-29",notes:"Experimental staging rollout for autonomous agent evaluation from git, telemetry, and issue signals.",graduation:{status:"gated",gate:"Graduates to default-on after staging soak validates zero false-FAILs against real customer agent scenarios.",evidence:"packages/engine-core/src/__tests__/agent-eval.test.ts passes cleanly",owner:"steering (Radik)",review:"2026-10-15"}},GROUNDED_EXPECTATIONS:{key:"GROUNDED_EXPECTATIONS",envVars:["AGENTIQA_GROUNDED_EXPECTATIONS","AGENTIQA_EXPERIMENT_GROUNDED_EXPECTATIONS"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"When on, run_complete batch-judges provisionally-passing steps for expectation drift (heal cosmetic, fail-closed critical/judge-unavailable) instead of trusting the model grade verbatim.",designDoc:"docs/plans/2026-07-07-grounded-expectations-phase-b-design.md",status:"active",added:"2026-07-07",notes:"GRADUATED 2026-07-21 (default on): prod ran env-ON (AGENTIQA_EXPERIMENT_GROUNDED_EXPECTATIONS=1 on the orchestrator) since 2026-07-10 with zero grounded-attributable false-FAIL; nightly regressions in the window were all infra/render flake. This flip normalizes code to the already-live prod behavior \u2014 remove the orchestrator env vars (both namespaces) once this reaches each environment."},LOOP_VISION_ESCALATION_CHAT:{key:"LOOP_VISION_ESCALATION_CHAT",envVars:["AGENTIQA_LOOP_VISION_ESCALATION_CHAT","AGENTIQA_EXPERIMENT_LOOP_VISION_ESCALATION_CHAT"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Extends loop-detection vision-supervisor escalation to the Coordinator/chat lane; off disables it for chat only (the master LOOP_VISION_ESCALATION still governs the Runner/Explorer lanes).",designDoc:"docs/plans/2026-07-07-loop-detection-vision-supervisor-design.md",status:"active",added:"2026-07-07",notes:"Staged default-OFF originally, flipped default-ON once baked (AG-6995) \u2014 reads via killSwitchDisabled today."},PIN_PAGE_GROUNDING:{key:"PIN_PAGE_GROUNDING",envVars:["AGENTIQA_PIN_PAGE_GROUNDING","AGENTIQA_EXPERIMENT_PIN_PAGE_GROUNDING"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Runner-lane pin page-grounding: a criterion's pinned expectedValue must appear in the literal full-page a11y text at verify-evidence capture (parroted grade notes no longer suffice). Detection runs in shadow (telemetry) unless VALUE_GROUNDING_ENFORCE is on; '0' disables detection AND the forced full-snapshot capture entirely.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-10"},ISSUE_QUOTE_GROUNDING:{key:"ISSUE_QUOTE_GROUNDING",envVars:["AGENTIQA_ISSUE_QUOTE_GROUNDING","AGENTIQA_EXPERIMENT_ISSUE_QUOTE_GROUNDING"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Chat-lane hallucinated-quote gate on report_issue: a quoted literal asserted as visible must appear in the freshly captured full a11y snapshot. Detection runs in shadow (telemetry) unless VALUE_GROUNDING_ENFORCE is on; '0' disables detection AND the forced full-snapshot capture entirely.",designDoc:"packages/engine-core/src/negativeStateEvidence.ts",status:"active",added:"2026-07-10"},VALUE_GROUNDING_ENFORCE:{key:"VALUE_GROUNDING_ENFORCE",envVars:["AGENTIQA_VALUE_GROUNDING_ENFORCE","AGENTIQA_EXPERIMENT_VALUE_GROUNDING_ENFORCE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Flips both value-grounding gates (PIN_PAGE_GROUNDING, ISSUE_QUOTE_GROUNDING) from shadow telemetry (would_fail / would_bounce diag logs) to enforcement: pin absence records a real per-step oracle failure that fails run_complete; a hallucinated visible quote rejects the report_issue filing.",designDoc:"packages/engine-core/src/killSwitch.ts",status:"active",added:"2026-07-10",notes:"Shadow-first rollout: two adversarial review rounds each surfaced false-fail classes on a hard-fail gate, so enforcement waited on a staging soak. PARKED 2026-07-21 (steering decision, Alex): VERIFY_REOBSERVE_WITHHOLD (graduated default-ON) strictly dominates this hard-fail path \u2014 it closes the same blind-verdict/pin-absence hole with a soft-withhold that never manufactures the false-FAIL classes both review rounds surfaced. Never graduate the enforcement; the oracle_failure recording branch is a deletion candidate. Detection stays: PIN_PAGE_GROUNDING + ISSUE_QUOTE_GROUNDING (default-ON) keep their shadow would_fail/would_bounce telemetry. Remove the staging orchestrator env var (AGENTIQA_EXPERIMENT_VALUE_GROUNDING_ENFORCE) \u2014 the force-ON soak is moot.",graduation:{status:"parked",gate:"Retired in favor of VERIFY_REOBSERVE_WITHHOLD (soft-withhold successor). Do not graduate; delete the enforcement branch once the successor has a clean prod window.",evidence:"F2b eval proof: enforce cannot catch the abstain{snapshot_incremental} class (identical RED both legs); panel + review-round history of false-fail classes on the hard-fail path",owner:"steering (Alex)",review:"2026-08-15"}},RESUME_INPUT_ASK_USER:{key:"RESUME_INPUT_ASK_USER",envVars:["AGENTIQA_RESUME_INPUT_ASK_USER","AGENTIQA_EXPERIMENT_RESUME_INPUT_ASK_USER"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Coordinator turn-continuity: when on, a child Explorer's ask_user that blocks on a MISSING INPUT (a file to upload, a path/value \u2014 not an email/generic async wait) arms a resumable `input_wait` pause that persists the halted child's OBJECTIVE; the next user turn resumes that SAME objective (re-attaching session attachments) instead of falling through to free re-decomposition, and re-asks for the SAME objective when the required file is still absent. Off = byte-identical to today (missing-input ask_user arms no pause; the email-wait / generic-wait paths are unchanged).",designDoc:"docs/plans/2026-07-20-paused-task-resume-design.md",status:"active",added:"2026-07-20",notes:"GRADUATED 2026-07-21 (default on) via the bound-eval arm of its gate: chat/paused-task-resume green 2/2 replicates on origin/staging (pause armed on the missing-file ask_user; resume carried the SAME objective tokens (upload + filename) with an explicit no-re-plan prompt; text-only reply correctly re-asked for the same file) + the earlier recorded GRADUATION-PASS on the lio replay (asess_1784568697674). Organic staging soak was vacuous (organic chat never hits a missing-input upload ask_user \u2014 0 input_wait events in 12 sessions), so the eval arm is the gate per the evidence-count doctrine. Renderer attachment-drop discriminator stays a SEPARATE open item (PostHog coordinator_started.has_attachments). Remove the staging orchestrator env var once this reaches staging."},VERIFY_REOBSERVE_WITHHOLD:{key:"VERIFY_REOBSERVE_WITHHOLD",envVars:["AGENTIQA_VERIFY_REOBSERVE_WITHHOLD","AGENTIQA_EXPERIMENT_VERIFY_REOBSERVE_WITHHOLD"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Runner-lane grade-time re-observe + soft-withhold (batch.verify-never-blind fix). When a pinned STRICT verify criterion cannot be grounded at grade time \u2014 the snapshot is not full (abstain snapshot_incremental/missing/thin) or the value is absent from a full a11y snapshot (would_fail) \u2014 the runtime takes ONE fresh forced-full a11y re-capture and re-grounds before accepting the verdict. Groundable after re-observe (value was present but the grade-time capture was imageless/incremental) \u2192 the model verdict stands; still ungroundable \u2192 the step is SOFT-withheld to a 'warning'+note (NON-confident, never a hard fail, never routed through the VALUE_GROUNDING_ENFORCE oracle_failure path). Closes the blind-verdict hole (F2a canvas + F2b incremental) WITHOUT manufacturing false-FAILs on values present-to-user but absent from the a11y outline (virtualized/scrolled-off rows).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-21",notes:"GRADUATED 2026-07-21 (default on), same-day evidence gate approved by Alex in lieu of a calendar soak: (1) staging replay-soak on the lio fixture (proj_mqzn31900, 3 sequential replicates of the only expectedValue-pinned plan) \u2014 zero false-withholds across 6 present-pin observations, withholds only on genuinely-absent values, surgical parity (grounded and no-pin verdicts untouched, check-only plans fully inert), no failing run laundered to pass, no material latency delta; (2) the grounded-rescue + Regression A/B legs banked by the fix-gate eval run (F2b re-observe\u2192grounded\u2192pass, healthy batch zero-fire, below-fold DOM value grounds). Product semantics approved by Alex: pinned-strict TRUE-MISMATCH fails also downgrade to warning (observed-value note preserved) until typed-match (AG-7753) restores precise typed fails. The two evals (runner-verify-blind-canvas / -incremental) remain the regression gate: fix OFF must reproduce, ON must resolve. Independent of VALUE_GROUNDING_ENFORCE \u2014 when off, that flag behaves unchanged. Remove the staging orchestrator env var once this reaches staging."},BLOCKED_RUN_DRAFT_PLAN:{key:"BLOCKED_RUN_DRAFT_PLAN",envVars:["AGENTIQA_BLOCKED_RUN_DRAFT_PLAN","AGENTIQA_EXPERIMENT_BLOCKED_RUN_DRAFT_PLAN"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'A loop-blocked Explorer child keeps its work: when the loop detector stops a NON-discovery child before it could file assistant_v2_report, the fallback result carries a reconstructed `draftTestCase` built from the natural-language stepText/intent of the interactions it actually performed (observation-only tools excluded; any step text repeated 3+ times \u2014 the stuck target the detector fired on \u2014 dropped whole; capped at 30 steps; suppressed under 2 steps). Without it the Coordinator emits `tested_areas[].draft_steps: []`, FindingsCheckpointCard filters the area out of "What was just tested", and the product offers NO way to save a run that got blocked (issue #2024: the self-agent signup\u2192first-run journey dead-ended at "Save the completed run as a new test case" for two consecutive nights). Off restores the pre-#2024 behaviour \u2014 a blocked child reports no plan and the run is unsaveable. Verdicts are untouched either way: the run stays blocked, the recommendation stays do_not_ship, and the area is surfaced as partial with its not-tested reason by the existing incomplete-child coverage backfill.',designDoc:"packages/engine-core/src/ExplorerRuntime.ts",status:"active",added:"2026-08-02"},VERIFY_GATED_DONE:{key:"VERIFY_GATED_DONE",envVars:["AGENTIQA_VERIFY_GATED_DONE","AGENTIQA_EXPERIMENT_VERIFY_GATED_DONE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Cross-checks a 'done'/'passed' claim against deterministic completion-time oracle failures (wait-style); off restores trusting the model's completion claim verbatim.",designDoc:"docs/plans/2026-07-06-verification-gated-done-design.md",status:"active",added:"2026-07-06"},VERIFY_CONFLICT_RECONCILE:{key:"VERIFY_CONFLICT_RECONCILE",envVars:["AGENTIQA_VERIFY_CONFLICT_RECONCILE","AGENTIQA_EXPERIMENT_VERIFY_CONFLICT_RECONCILE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Downgrades a verification-conflict force-FAIL to a step-level warning (run not failed) when every plan criterion on the conflicted step passed with a substantiated note AND the only unresolved oracle is an agent-invented wait literal absent from the plan; off restores the unconditional bounce-then-fail-closed (false-negative on incident asess_1784155719395_h62evpmp). Since 2026-08-01 this switch is the UMBRELLA kill for the whole agent-invented-literal family: =0 also disables VERIFY_AGENT_LITERAL_ABSTAIN and VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED, so the documented "restores the unconditional force-fail" promise stays true.',designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-16"},VERIFY_AGENT_LITERAL_ABSTAIN:{key:"VERIFY_AGENT_LITERAL_ABSTAIN",envVars:["AGENTIQA_VERIFY_AGENT_LITERAL_ABSTAIN","AGENTIQA_EXPERIMENT_VERIFY_AGENT_LITERAL_ABSTAIN"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Last valve of the verify-conflict cascade (after M1 reconcile + M2 withhold): a step whose ONLY unresolved oracle is a wait literal the AGENT invented (absent from the whole saved plan, substring) is degraded to a step-level WARNING instead of force-FAILing the run \u2014 so the synthesized 'Verification step not satisfied' issue is not filed either. TWO LANES ON SEPARATE SWITCHES since 2026-08-01 round 3: THIS flag enforces lane 'grounded_criteria' (default ON) and is also the parent kill for lane 'action_shaped', which has its OWN switch VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED (shipped default-OFF/shadow 2026-08-01, GRADUATED to default ON 2026-08-02 on its census \u2014 set it to 0 to put that lane back in shadow without touching this one). Shared preconditions of both lanes: an invented literal, no M1 grounded negative, no model-graded negative on the step (neither a 'failed' step nor a 'failed' criterion is ever softened), and no prior downgrade of the step by another valve (step_already_downgraded \u2014 the same screen M2 carries). Lane 'grounded_criteria': the step has >=1 PLAN-AUTHORED criterion, every one came back graded pass, and every one carries FRESH GROUNDING from this run (an engine-written groundingObservation for this run/step, or an observed/expectedValue the engine finds whole-token on the step's OWN retained full frame) \u2014 ungrounded model grades keep the force-fail, because this gate exists to catch fabricated passes and is not weakened for ungrounded grading. Strictly weaker than VERIFY_CONFLICT_RECONCILE, which needs a substantiation note on every criterion and produces a clean PASS; this one can never produce anything better than an honest amber and is one-directional (RED->AMBER only). NOT covered, deliberately: a PLAN-GROUNDED literal the app renders differently (staging run_b501d461's '50 EUR' vs '\u20AC50.00 net') refuses in BOTH lanes \u2014 opening that lane would re-decide four separate M1/M2 refusals and needs a page-side equivalence read; tracked as follow-up. Detection always runs: off logs verify_conflict_reconcile:agent_literal_abstain with enforced:false PLUS a verify_conflict_reconcile:agent_literal_would_abstain census row, and changes nothing; every refusal logs verify_conflict_reconcile:agent_literal_abstain_inapplicable with its reason AND its lane ('none' for the screens that run before lane selection). VERIFY_CONFLICT_RECONCILE=0 is an UMBRELLA kill for this valve too (both address the agent-invented-literal class, and that switch is documented as restoring the unconditional force-fail).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-08-01"},VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED:{key:"VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED",envVars:["AGENTIQA_VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED","AGENTIQA_EXPERIMENT_VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"GRADUATED 2026-08-02 (default ON) \u2014 lane 'action_shaped' of the agent-literal abstain valve, shipped SHADOW on 2026-08-01 (round-3 steering split) and flipped on its own census one release later, the #1893/#1895 shadow-first precedent. The lane covers a conflicted verify step with ZERO plan-authored criteria (an ACTION step mislabeled 'verify' whose saved plan object carries text/type only: prod asess_1785560226558_33t8nuos step 4; staging run_b501d461 step 2), where the timed-out wait literal is the agent's own scaffolding rather than anybody's assertion; ON, such a step degrades to a step-level WARNING exactly as lane 'grounded_criteria' does \u2014 never a pass, and the synthesized 'Verification step not satisfied' issue is not filed either. GRADUATION EVIDENCE (verify_conflict_reconcile:agent_literal_would_abstain census, the shadow lane's whole product): 2026-08-01 evening staging replay \u2014 2 instances, invented literal 'How can I help you today?' on a Lio conversation-UX plan, wouldBecome:'warning' 2/2; 2026-08-02 prod batch asess_1785646213123_5xqheb4g \u2014 invented literal 'Ich kann', currentStatus:'passed', wouldBecome:'warning' on a step the run falsely failed. ZERO censused instances where the abstain would have been harmful (i.e. no row where the withdrawn oracle was the only thing catching a real defect), which is exactly the population question the round-2 review raised \u2014 53% of e2e/evals verify steps are criteria-less and the runner SYNTHESIZES a strict criterion from the verify text, so criteria-less is not unasserted, and the census is what shows the enforced lane is not converting real detections into green runs. The model's own negatives remain hard refusals in this lane (action_shaped_step_graded_failed / action_shaped_criterion_graded_failed), so the synthesized criterion still decides when it is graded failed. KILL-SWITCH SEMANTICS UNCHANGED: an explicit AGENTIQA_VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED=0 (or the AGENTIQA_EXPERIMENT_ spelling) restores the byte-identical pre-graduation SHADOW behavior \u2014 the classifier still reaches the same 'abstain' verdict, logs verify_conflict_reconcile:agent_literal_abstain with enforced:false plus the verify_conflict_reconcile:agent_literal_would_abstain census row, and mutates NO verdict and suppresses NO issue. Parented by VERIFY_AGENT_LITERAL_ABSTAIN (=0 kills both lanes) and by VERIFY_CONFLICT_RECONCILE (=0 is the family umbrella kill and silences the diags entirely). killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND the key listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.agentLiteralAbstain.test.ts (B1/B3 at the DEFAULT \u2192 amber; B1k/B3k/D1 with =0 \u2192 byte-identical red + census).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-08-01"},VERIFY_PLAN_LITERAL_TYPED_EQUIVALENCE:{key:"VERIFY_PLAN_LITERAL_TYPED_EQUIVALENCE",envVars:["AGENTIQA_VERIFY_PLAN_LITERAL_TYPED_EQUIVALENCE","AGENTIQA_EXPERIMENT_VERIFY_PLAN_LITERAL_TYPED_EQUIVALENCE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Lane B1 of the plan-literal equivalence design (docs/plans/2026-08-02-plan-literal-equivalence-design.md; prod Lio 2026-08-02). SHADOW, DEFAULT OFF since the 2026-08-02 round-5 steering de-scope \u2014 the same shadow-first shape as lane B2 below, VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED and #1893/#1895. WHY IT SHIPS OFF RATHER THAN LIVE: THREE CONSECUTIVE adversarial rounds each found a NEW way the page-side scan reads a value that is not on the page (round 2 F2 node-locality/F3 literal ambiguity, round 3 the ten boundary shapes, round 4 D1 contested marker/D2 letter-bearing symbol, round 5 the DE-thousandths and group-separator-truncation holes below). Every round's holes were closed and every round found more; a reader whose defect-discovery rate has not yet flattened does not get to withdraw a grounded negative on a default fleet. WHAT =1 ENABLES: TYPED EQUIVALENCE = PRESENCE at the ONE page read of the verify-conflict cascade (M1 reconcileVerifyConflictsFromSnapshot). When the raw whole-token pinPresentInPage read of a PLAN-AUTHORED wait literal comes back ABSENT, the snapshot is re-read with a deterministic typed scan. SIX PROPERTIES DEFINE THE SCAN, each pinned by acceptance cases through the production wrapper pinEquivalentInPage: (a) NODE-LOCAL \u2014 the unit of scan is a SINGLE a11y node (the raw snapshot's own lines plus flattenAriaSnapshot(...).lines), never the space-joined 'spaced' or separator-free 'stream' haystacks pinPresentInPage uses; amount and currency marker must share one node, because a join turns the newline that separates two nodes into a legal gap char and FABRICATES composites present on neither (measured on five realistic aria shapes, 2026-08-02 round 2 defect F2). (b) UNAMBIGUOUS LITERAL \u2014 the AUTHORED side must carry exactly ONE surviving numeric reading; a both-locales-legal form with a meaningful fraction ('1,500' -> {1500, 1.5}) is refused outright, because under INTERSECT semantics it would equate a 1000x-different page value (round 2 defect F3). (b2) UNAMBIGUOUS PAGE OPERAND (round 5) \u2014 the PAGE atom must ALSO carry exactly one surviving reading after the structural group validation numericCandidates already applies. Under INTERSECT semantics a multi-reading page atom equated a 1000x-different value in the OTHER direction: a DE page rendering 'Rechnungsbetrag 7.890 EUR' (= 7890) reads {7.89, 7890} and satisfied the literal '7.89 EUR'; likewise '2.750'/'2.75 EUR' and '9.990'/'9.99 EUR'. Refusing multi-reading page atoms costs nothing real: 'EUR 50.00 net' is DE-invalid as a thousands group ('.00' is not three digits), so the ordinary renderings stay singletons and still fold. (c) BOUNDARY-GUARDED, MAXIMAL amount runs \u2014 a number-like run may not touch a letter, a digit, a '.'/',' or a '%'/'/'/'-' on either side, so 'EUR 50k', 'EUR 1.5B', 'EUR 50,00abc', 'EUR 50% off', 'EUR 50/Monat' and 'EUR 50-60' all read as NO amount rather than as 50/1.5 (round 3; ten measured false matches, the scaled-magnitude ones off by 1000x-1e9). (c2) NO TRUNCATED GROUP RUN (round 5) \u2014 the boundary class of (c) is a CHARACTER class, and the group separators the world actually renders money with are not in it: the Swiss apostrophe U+2019 and the space family (U+0020/U+00A0/U+202F/U+2009). 'Gesamtsumme: CHF 50\u2019000.00' therefore split into the runs '50' and '000.00', the first of which claimed the adjacent CHF and matched the literal '50 CHF' \u2014 a value 1000x below what the page states. A run adjacent, across group separators only, to another DIGIT is a TRUNCATION of a larger grouped number and yields no atom at all. (d) TYPED IDENTITY \u2014 the literal must parse as an amount+currency composite scalar ('50 EUR' / 'EUR 50' / '\u20AC50' / '50,00 \u20AC' \u2014 recognizeAmountCurrency, which rejects anything carrying another word) or a bare number, and the page must carry a MONEY ATOM with the IDENTICAL ISO-4217 code (symbol<->code via the frozen gazetteer; the ambiguous $ and \xA5 never canonicalize) whose numeric candidate set INTERSECTS the literal's under the existing dual-locale numericCandidates (US 1,234.56 == DE 1.234,56, exact equality, no epsilon). Currency adjacency is horizontal-whitespace-only, so 'EUR 12.00 shipping, 50 items' never reads as EUR 50. FOUND, WITH =1 => the literal counts as PRESENT and takes the EXISTING clean-pass rail (M1 'cleared'), with its OWN decision reason literal_equivalent_in_snapshot and its OWN client note \u2014 the whole-token rail's note ('was present when the run re-checked') is FALSE of this population by construction and is not reused (round 2 defect F4B); no new verdict, no extra browser action. NOT found => today's grounded_negative stands. NO fuzzy matching, NO LLM, NO string similarity: a shape the closed tables cannot canonicalize is not 'probably the same value'. THE CLASS IT WOULD CLOSE: a plan that writes money the way a human writes it while the product renders it the way a locale renders it ('50 EUR' vs 'EUR 50.00 net') produced a force-FAIL plus a HIGH-severity 'Verification step not satisfied' issue in the customer's project while every criterion of the step passed \u2014 that incident still force-fails on a default fleet. DEFAULT OFF = SHADOW, NOT INERT: detection runs in full and the caller censuses verify_conflict_reconcile:literal_typed_equivalence {enforced:false} PLUS a dedicated row verify_conflict_reconcile:literal_typed_equivalence_would_clear {stepIndex, literal, matchType, currency, matched, nodeText, basis, wouldBecome:'passed'}, and mutates NOTHING \u2014 the M1 decision map keeps its grounded_negative, would_fail is logged exactly as with the lane absent, the force-fail and the HIGH-severity issue stand byte-identically, and no capture kind changes. TWO GATES, NOT ONE (round 5 invariance fix): the mutation requires BOTH this switch AND a live VERIFY_CONFLICT_RECONCILE_SNAPSHOT. The decision map is populated even when M1 is in shadow, and its grounded_negative is read UNGATED by two downstream firewalls (M2 and the agent-literal abstain valve) \u2014 so before the fix an opt-in =1 reached THROUGH a disabled M1 and changed verdicts on a build whose operator had switched M1 off. VERIFY_CONFLICT_RECONCILE=0 is the family umbrella kill and silences the census too. Runner lane only (RunnerRuntime run_complete, second escalation).",designDoc:"docs/plans/2026-08-02-plan-literal-equivalence-design.md",status:"active",added:"2026-08-02",graduation:{status:"gated",gate:"NOT graduable as built \u2014 FOUR blockers. THE STANDING ONE: three consecutive adversarial rounds each found a fresh class of page-side false match (round 2 F2/F3, round 3 the ten boundary shapes, round 4 D1/D2, round 5 the two below) and the discovery rate has not flattened, so graduation needs a round that finds nothing new, not merely the closure of the last round's list. (1) PAGE-SIDE SINGLETON-READING SEMANTICS. Round 5 measured the DE-thousandths hole in the direction the round-2 F3 guard does not cover: the AUTHORED side is now required to be unambiguous, but the PAGE side was left multi-reading on the argument that 'the page is not the operand the author controls' \u2014 and under INTERSECT that is exactly what lets a page rendering 7.890 (DE seven-thousand-eight-hundred-ninety) satisfy the literal '7.89 EUR'. The singleton rule shipped in this PR closes the measured shapes; what it does NOT yet have is a systematic argument that the page operand is single-reading everywhere the fold fires, which is what a graduation needs. (2) GROUP-SEPARATOR TRUNCATION. The boundary class of property (c) is a character class and cannot see that a run it accepted is a FRAGMENT of a grouped number: 'CHF 50\u2019000.00' cleared '50 CHF' because U+2019 and the space family are not boundary chars. Closed for the apostrophe and the space family in this PR; the general form (any group separator any locale renders, including the ones no test has enumerated) is not proven closed. (3) SUBJECT BINDING (F1). Node-locality is an AUTHORSHIP guard, not a PLACE one: an equal value on an UNRELATED card clears the presence wait, demonstrated on the lio-mirror/chat-price-format trap where the distractor renders \u20AC7.89 and the anchor \u20AC78.90 \u2014 B1 turns a genuine red into a green there. The census carries nodeText so a clear is auditable post-hoc, but auditability is not a gate; the lane cannot enforce until a subject-binding design exists (bind the matched node to the step's target, or refuse when the page carries more than one atom satisfying the literal). (4) STAGING AND PROD CENSUS. verify_conflict_reconcile:literal_typed_equivalence_would_clear plus the paired literal_typed_equivalence_refused rows, showing the fold fires on genuine locale renderings of the value the step is about and not on pages where the product is actually wrong about the number.",evidence:"an adversarial round that finds NO new page-side false-match class; a systematic (not case-by-case) argument for page-operand single-readingness and for group-separator closure; a subject-binding design with its own acceptance suite that reddens the chat-price-format trap; and a staging+prod literal_typed_equivalence_would_clear / _refused census sized against the population it would clear",owner:"steering (Alex)",review:"2026-08-09"}},VERIFY_PLAN_LITERAL_CRITERIA_DEGRADE:{key:"VERIFY_PLAN_LITERAL_CRITERIA_DEGRADE",envVars:["AGENTIQA_VERIFY_PLAN_LITERAL_CRITERIA_DEGRADE","AGENTIQA_EXPERIMENT_VERIFY_PLAN_LITERAL_CRITERIA_DEGRADE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Lane B2 of the plan-literal equivalence design (docs/plans/2026-08-02-plan-literal-equivalence-design.md; prod Lio 2026-08-02, wait literal 'Munich' on a German page rendering 'M\xFCnchen'). SHADOW, DEFAULT OFF since the 2026-08-02 round-3 steering de-scope \u2014 the same shadow-first shape as VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED and #1893/#1895. WHAT =1 ENABLES: the ONE deliberately scoped-out lane of VERIFY_AGENT_LITERAL_ABSTAIN \u2014 a PLAN-GROUNDED wait literal that B1's typed equivalence could NOT reconcile stops refusing at the literal_plan_grounded provenance screen WHEN the step has >=1 PLAN-AUTHORED criterion, falls through to lane 'grounded_criteria', which applies its FULL, unchanged contract (every authored criterion graded, every one passed, every one seen by the ENGINE on the step's OWN retained frame via criterionValueSeenOnOwnFrame) and degrades the step to a step-level WARNING under the pre-cut reason plan_literal_not_rendered_as_written and the pre-cut client note ('the run waited for this exact wording and did not find it \u2014 the page may present the same value differently'). =1 ALSO arms lane B2's own full-snapshot request at dispatch (verifyWaitCarriesDegradableLiteral), without which the honesty condition has no frame to read and the lane is inert. NEVER a pass: the outcome is the one-directional RED->AMBER the valve already owns, so run.status can go from failed to passed only in the sense that no step is failed, and the synthesized 'Verification step not satisfied' issue is not filed. DEFAULT OFF = SHADOW, NOT INERT: detection runs in full (the provenance screen no longer reads the switch), the classifier reaches the same verdict, the caller logs verify_conflict_reconcile:agent_literal_abstain with enforced:false PLUS a dedicated census row verify_conflict_reconcile:plan_literal_would_degrade {stepIndex, stepText, literal, timeoutMs, reason, provenance, lane, criteriaCount, currentStatus, wouldBecome:'warning', flag}, and mutates NOTHING \u2014 the step keeps its status, the conflict stays in failingConflicts, the force-fail and the HIGH-severity issue stand byte-identically, and NO capture kind changes. The shadow census UNDERCOUNTS BY CONSTRUCTION: with the lane off no full frame is bought, so most of the population refuses one screen earlier and is visible as agent_literal_abstain_inapplicable {reason:'plan_literal_criteria_not_page_grounded', provenance:'plan_grounded', enforced:false} rather than as a would-degrade row; read the two together to size the population. WHAT STAYS RED under =1, unchanged: a criteria-LESS step (lane 'action_shaped' still refuses every plan-grounded literal pre-lane with literal_plan_grounded, lane:'none'), any step the model graded failed or with a failed criterion, any step another valve already marked down, any step whose authored criteria are ungraded or not page-grounded, and any M1 grounded negative (the firewall is untouched \u2014 which is why B1 must clear the equivalence case at M1 rather than here). Parented by VERIFY_AGENT_LITERAL_ABSTAIN (=0 kills this too) and by VERIFY_CONFLICT_RECONCILE (=0 is the family umbrella kill and silences the diags entirely). Runner lane only.",designDoc:"docs/plans/2026-08-02-plan-literal-equivalence-design.md",status:"active",added:"2026-08-02",graduation:{status:"gated",gate:"NOT graduable as built \u2014 three blockers, all found by the round-2/round-3 adversarial verification of this PR and none of them a soak question. (1) HONESTY-NEEDLE REDESIGN. criterionValueSeenOnOwnFrame, the condition that is supposed to pay for the amber, accepts a MODEL-CONTROLLED value: it looks for the criterion's own `observed`/`expectedValue` on the step's frame, and a probe showed ANY on-page token the model chooses to emit buys the amber. 'the engine saw the asserted value' is therefore closer to 'the model quoted something from the page', which is not corroboration of an authored claim; the lane cannot enforce until that predicate is re-designed to read a value the model does not control. (2) F5 BLAST RADIUS. B2 is inert without a full frame, so it requests one at DISPATCH \u2014 before the wait's outcome is known \u2014 which also promotes SUCCEEDING waits to full and widens _domGroundableCandidates (the live predicate-basis candidate set) for steps B2 never touches. Enabling B2 is therefore also a capture-kind change for a wider population and must be re-measured as one. (3) M1-CANARY RE-BASELINE. The late-render canary's failed->warning baseline was recorded with B2 ENFORCING; that decision has to be re-taken against the default-OFF world before the flag can flip. On top of the three: a staging AND prod census of verify_conflict_reconcile:plan_literal_would_degrade plus the plan_literal_criteria_not_page_grounded refusals, showing the lane fires on genuine rendering mismatches (Munich/M\xFCnchen) and not on steps where the product is actually wrong.",evidence:"staging/prod verify_conflict_reconcile:plan_literal_would_degrade census rows + the paired agent_literal_abstain_inapplicable {reason:plan_literal_criteria_not_page_grounded} refusals (population sizing), a re-designed honesty predicate with its own acceptance suite, an F5 capture-kind impact measurement over a full runner-eval pass, and the re-taken m1-canary late-render baseline",owner:"steering (Alex)",review:"2026-08-09"}},VERIFY_LITERAL_PROVENANCE_STEP_SCOPED:{key:"VERIFY_LITERAL_PROVENANCE_STEP_SCOPED",envVars:["AGENTIQA_VERIFY_LITERAL_PROVENANCE_STEP_SCOPED","AGENTIQA_EXPERIMENT_VERIFY_LITERAL_PROVENANCE_STEP_SCOPED"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"GRADUATED 2026-08-03 (default ON) \u2014 STEP-SCOPED LITERAL PROVENANCE, the third provenance class of the agent-literal abstain valve (prod Lio 2026-08-02/03, 5 hits in 24h). Shipped SHADOW on 2026-08-03 and flipped on its own REPLAY-BASED census one release later, the same shadow-first path VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED took on 2026-08-02 (#1893/#1895 precedent). GRADUATION EVIDENCE (2026-08-03, ag-rehearsal prod->staging mirror replay, 18 runs over 6 plans: 16 census runs at =0 plus a 2-run enforcing A/B; VERDICT.md at https://s.assets.agentiqa.com/provenance-graduation-2026-08-03/): (1) COUNT AND SHAPE \u2014 exactly 2 verify_conflict_reconcile:agent_literal_would_rescue rows in the 16 shadow runs, both the known '50 EUR' hit (chat-knowledge-qa tp_0370f61d run14 and asst-knowledge-qa run11), both step 2, both planMatchLocation step_3:criterion_1:check, both currentStatus passed -> wouldBecome warning, on runs the build falsely FAILED. (2) NO OVER-TRIGGER \u2014 zero provenance rows on the other 4 plans across 8 runs, and the 4 plan_grounded refusals were retained as authored, so the population did not creep into the typed-match equivalence track's half. (3) ENFORCING A/B \u2014 run14 (=0): step 2 failed, run failed, would_rescue censused; run18 (=1): step 2 warning, run PASSED, enforced:true and no census row, i.e. the rescue lands as the one-directional RED->AMBER and nothing else moved. (4) PROD RECOMPUTE \u2014 all 5 documented prod hits recomputed under the step-scoped read: 4 remap to agent_invented_plan_coincident at exactly the documented locations; the 5th (m77p3hky) became agent_invented after lio edited that plan at 2026-08-03T09:01:10Z, i.e. it left the class by an authoring change, not by a read change. THE CLASS: the valve's provenance screen asks 'did the AUTHOR write this literal?' against the WHOLE plan document (title + every step's text/criteria, loose substring). When an INVENTED wait literal COINCIDENTALLY appears somewhere else in the plan \u2014 the plan title, or (in all 5 measured hits) a NEIGHBOURING step's criterion \u2014 the screen returns plan_grounded, the criteria-LESS lane 'action_shaped' refuses before lane selection with agent_literal_abstain_inapplicable {reason:'literal_plan_grounded', lane:'none'}, and the action step keeps its force-FAIL while every criterion of its paired verify step passed. Measured: '50 EUR' (asess_1785644983561_n5worrfv step 2, matched step 3's criterion; app renders '\u20AC50 (net)'), 'Munich' (o7hvcun7 step 2, matched step 3's criterion; app renders 'M\xFCnchen'), the German prompt of step 2 (5xqheb4g step 4, matched step 2's TEXT), 'QA Base \u2014 Draft Completion (do not modify)' (m77p3hky step 4, matched step 5's criterion), '50 EUR' again on the 08-03 build (r3ghgy68 step 2). All five failing steps carry ZERO plan-authored criteria, so lane B2's escape hatch (which needs >=1 criterion) cannot reach any of them. WHAT =1 ENABLES: the provenance read is SCOPED TO THE FAILING STEP'S OWN authored text (buildStepPlanHaystack \u2014 its instruction plus each of its criteria's check/expectedValue) with the SAME loose lowercased-substring matcher as before, and splits the old plan_grounded population in two \u2014 literal ON the step's own text stays plan_grounded and every existing refusal is byte-identical; literal absent from the step but present ELSEWHERE in the plan becomes the NEW provenance agent_invented_plan_coincident, which falls through the plan-grounded screen into ordinary lane selection and is then decided by each lane's UNCHANGED contract (lane 'action_shaped': no model-graded negative, no prior downgrade, no M1 grounded negative; lane 'grounded_criteria': every authored criterion graded, passed and engine-grounded this run via criterionGroundedThisRun \u2014 the invented-literal bar, since a literal the author did not write on this step is not an authored claim about it). Literal absent from the plan entirely is agent_invented, unchanged. The two reads bracket the population EXACTLY: plan_grounded + agent_invented_plan_coincident is precisely the old plan_grounded set (the locator falls back to 'plan_document' for the join-boundary residue), so no literal can be reclassified into the default-ON agent_invented path by this change. OUTCOME CONTRACT UNCHANGED: the only mutation is the valve's one-directional RED->AMBER step-level WARNING, never a pass (#2002 doctrine \u2014 a coincidence buys a warning, never green), and the M1 grounded-negative firewall plus both model-negative screens still refuse. KILL-SWITCH SEMANTICS (=0 under either spelling) RESTORE THE BYTE-IDENTICAL PRE-GRADUATION SHADOW, NOT AN INERT PATH: detection always runs, the classifier reaches the same verdict, the caller logs verify_conflict_reconcile:agent_literal_abstain {enforced:false} PLUS a dedicated census row verify_conflict_reconcile:agent_literal_would_rescue {stepIndex, stepText, literal, timeoutMs, reason, provenance:'agent_invented_plan_coincident', planMatchLocation, lane, criteriaCount, currentStatus, wouldBecome:'warning', flag} and mutates NOTHING \u2014 the step keeps its status, the conflict stays in failingConflicts, the force-fail and the synthesized 'Verification step not satisfied' issue stand byte-identically. So an operator who kills the class keeps the measurement. NO capture kind changes in EITHER state (this valve reads no page and calls no model; lane B2's full-frame force is untouched because it is gated on waitLiteralAuthoredOnStep, which this class fails by definition). ONE TELEMETRY DELTA, stated rather than hidden: a coincident literal that reaches a lane reports that lane's reason (e.g. action_shaped_criterion_graded_failed) instead of the pre-2026-08-03 literal_plan_grounded, and its lane field is no longer 'none' \u2014 the census got more specific when the read was scoped, independently of enforcement. The parents are unchanged \u2014 VERIFY_AGENT_LITERAL_ABSTAIN=0 kills both lanes and VERIFY_CONFLICT_RECONCILE=0 is the family umbrella kill that silences the census too. killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND the key listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.agentLiteralAbstain.test.ts \u2014 B2 (the prod shape verbatim), P2, P3d, P9 leg (a) and P10 leg (b) at the DEFAULT -> amber, never a pass; B2k, P3, P5b, P9 leg (b) and P10 leg (a) with =0 -> byte-identical red + census; P1/P1e pin that the own-step (genuinely authored) read never moves; P4 pins the agent_invented path byte-identical; P8 pins that a model-graded negative is still refused; P9 is asserted in BOTH states because the coincident/agent_invented split is what makes this switch reach the population at all (agent_invented is held by VERIFY_AGENT_LITERAL_ABSTAIN, not by this key). Runner lane only.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-08-03"},VERIFY_NOTE_REFUTES_CRITERION:{key:"VERIFY_NOTE_REFUTES_CRITERION",envVars:["AGENTIQA_VERIFY_NOTE_REFUTES_CRITERION","AGENTIQA_EXPERIMENT_VERIFY_NOTE_REFUTES_CRITERION"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"SELF-CONTRADICTION GATE \u2014 ships ENFORCING (default ON) on a measured population. THE DEFECT (false-PASS, the direction that destroys customer trust): a criterion recorded passed:true while its OWN note states the opposite, inside a single verdict record, and the run reported PASSED. TWO measured instances, one on PROD. (1) PROD run_35ffd2b2 / asess_..._eobjjo88, lio's 2026-08-04 04:14Z batch on engine 8741da063, plan 'Lio Assistant - conversation UX' step 3: STRICT criterion 'The answer addresses the question about cleaning-services suppliers (it mentions at least one supplier or a count)' booked passed with note 'The assistant addresses the topic of cleaning-services suppliers, stating it couldn't find specific info but offering to help narrow down the search.' The SAME underlying lio behaviour correctly FAILED a sibling run in the same batch (8vc9g930), so the false pass did not merely mis-grade a criterion \u2014 it ABSORBED a real customer-visible defect our own product had already proven elsewhere. (2) MIRROR run_9bec8ba8 (c13) step 3, plan tp_3d5427b9 'asst-negotiation-email': STRICT criterion 'An email draft is shown with an editable subject and body (an email composer, not just prose in the chat)' booked passed with note 'An email draft was shown in a composer-like UI, but the elements were not programmatically editable (not contenteditable).' Ground truth settled by two independent methods across five sibling replicates: a contenteditable DOM query returned only the chat composer (b13) and an actually-attempted type_text_at into subject and body FAILED (f14, f15). NO existing gate caught either: in c13 step 3 predicate_basis_verify was inconclusive/dom_not_resolvable, typed_atom_floor abstained on no_structured_observed, criteria_containment and ungrounded_pass were silent. WHAT =1 ENABLES: every graded criterion still reported passed after pin substantiation is read by detectCriterionSelfContradiction (packages/engine-core/src/criterionSelfContradiction.ts) and, on a `contradicted` verdict, has `passed` flipped to false \u2014 the SAME single mutation substantiatePinnedCriterion makes for an uncited pin, so the outcome is decided by the ONE existing chokepoint deriveStepStatusFromCriteria: a STRICT criterion FAILS its step, a non-strict one WARNS it. No new status, no new vocabulary, no note rewrite (the criterion's own note already says why, in the grader's own words). ONE-DIRECTIONAL: only ever reached for a grade that is still passed, so it can never rescue a failure and never makes anything green. WHY FAIL RATHER THAN WITHHOLD for a strict criterion: a withhold means 'we could not confirm', and this is not that \u2014 the note is the grader's own AFFIRMATIVE statement that the checked property is ABSENT, i.e. evidence AGAINST the claim rather than missing evidence, and in both measured cases an independent method proved the fail correct. TWO LANES, because the class has two shapes and one mechanism cannot reach both. LANE A: the note negates a term the CHECK ITSELF USES, scoped \u2014 an explicit negator from a closed list, and a >=4-char non-stopword subject token of the check's own main clause inside that negator's 60-char forward window (the mirror c13 shape, negatedTerm 'editable'). LANE B: the note reports the app FAILED TO PRODUCE what the check REQUIRES, IN WORDS THE CHECK NEVER USES \u2014 a closed NOT-FOUND list (couldn't/could not/did not/unable to + find|locate|retrieve|provide|identify|return|surface; 'no results|matches|information|data|details|suppliers|records'; 'not found'; 'found no|nothing|none') against a closed PRESENCE-REQUIREMENT verb list in the check (mentions|shows|displays|contains|includes|lists|states|reports|identifies|names|cites|returns|surfaces|provides|presents|specifies). The PROD case is lane B and lane A provably cannot see it: 'couldn't find specific info' and 'mentions at least one supplier or a count' share NO surface term, and the check's 'or' screens lane A out anyway. NO LLM, NO FUZZY SCORING, NO NETWORK \u2014 two strings the caller already holds, so shadow and enforcement are the same computation and OFF costs nothing. FOUR SCREENS, EACH KILLING A MEASURED FALSE POSITIVE of the naive version (a bare 'does the note contain a negation' scan fires on 7 of the mirror corpus's 526 passed criteria and only ONE is the defect; THREE of the six false positives are on STRICT criteria, so a naive gate shipped enforcing would have turned three HONEST passes into hard failures \u2014 the one direction this gate may never take): check_disjunctive (a09 \u2014 check 'mentions the tracking number QACORE-TRACK-0001 and/or the carrier DHL', note 'Tracking number ... was not mentioned'; DHL WAS mentioned so the disjunction is satisfied and the note refutes nothing; also e05); check_carries_negation, read on the check's MAIN CLAUSE (f01/e01 \u2014 check '\"Final Sign-off\" has not started', note 'Final Sign-off is Not Started', where the note CONFIRMS a check whose own subject is negative; also a11/b11 \u2014 check '... (webshops, private expensing) is not permitted' vs note 'you must not use private expense reimbursement'); GLOSS STRIPPING of quoted spans and parenthetical asides from BOTH texts before anything is read (a08 \u2014 check and note both name the plan title 'QA Base \u2014 Submission Checks (do not modify)' and the negation lives INSIDE THE TARGET NAME, the exact class stripTargetLiteralFromProse exists for; and f13/f16 \u2014 note 'Actual text: \"Payment terms of 60 days end of month instead of 30 days\"', a page quotation rather than the grader's own claim); and CONTRASTIVE EXCLUSION \u2014 'instead of' / 'rather than' / 'but' / 'however' contrast, they do not negate, and 'instead of' was exactly f13/f16's naive hit. CENSUS THAT PAYS FOR ENFORCING: 2 fires over 698 passed criteria, both of them the known defects, ZERO honest passes moved \u2014 1 of 168 in lio's PROD 04:14Z batch (eobjjo88, lane B), where an independent hand audit of the same batch found exactly that one note-contradicted pass so the detector matches the audit, and 1 of 530 in the 46-run prod-parity mirror (c13, lane A). KNOWN BOUNDARIES, stated rather than hidden: a check whose main clause carries any negator abstains wholesale (a genuine self-contradiction there is missed \u2014 f01/e01/a11/b11 are four measured reasons to keep it); a negation the grader writes INSIDE a quotation abstains, because transcription cannot be told from assertion there; morphology is not modelled, so 'mentions' in the check vs 'mentioned' in the note abstains. =0 UNDER EITHER SPELLING IS SHADOW, NOT INERT: detection still runs, the caller logs note_refutes_criterion:would_fail {stepIndex, criterionIndex, lane, reason, negatedTerm, strict, check, note, currentStatus, wouldBecome, decision, enforce:false, mutated:false, flag} and mutates NOTHING, so an operator who kills the class keeps the measurement. A SECOND census row, emitted in BOTH states, sizes the SUPPRESSED population: note_refutes_criterion:suppressed fires when a note DID carry an explicit negator and a screen declined it \u2014 the row a graduation or rollback review reads to see whether the screens are over- or under-refusing. Notes with nothing negative in them are silent, because they carry no signal and would drown it. Acceptance suite: packages/engine-core/src/__tests__/criterionSelfContradiction.test.ts (both prod/mirror defects verbatim, all eight measured honest passes as regression pins, and a mutation guard per screen) + RunnerRuntime.noteContradictsCriterion.test.ts (the cascade: strict -> step failed, non-strict -> step warning, =0 -> byte-identical pass + census). Runner lane only. See docs/VERDICT-GATES.md.",designDoc:"docs/plans/2026-08-04-criterion-self-contradiction-design.md",status:"active",added:"2026-08-04"},VERIFY_STRICT_PASS_SUBSTANTIATION:{key:"VERIFY_STRICT_PASS_SUBSTANTIATION",envVars:["AGENTIQA_VERIFY_STRICT_PASS_SUBSTANTIATION","AGENTIQA_EXPERIMENT_VERIFY_STRICT_PASS_SUBSTANTIATION"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"SHADOW, DEFAULT OFF \u2014 the SECOND shape of the 2026-08-04 false-PASS class: a STRICT criterion booking a pass with NO substantiation at all. Measured on PROD in run_783593d9 (mirror label e13) step 3, where the same plan-13 editability criterion recorded passed:true with note, observed AND groundingObservation ALL absent \u2014 and where all 10 strict passes in that run carried zero criterion-level substantiation. WHY IT IS SHADOW AND NOT ENFORCING, with the number: requiring substantiation to book a strict pass would flip legitimate passes EN MASSE, which is precisely the harm the sibling VERIFY_NOTE_REFUTES_CRITERION gate exists to prevent. 104 of 446 strict passes in the 46-run prod-parity mirror (23.3%) carry no substantiation of any kind, spread over 21 of its 47 runs, and those runs are dominated by HONEST passes (a06, a07, a09, a11, a12, a14, b06, b07, b10, b12, b16 are all honest-pass rows in the rehearsal SCORE.md). On lio's PROD 2026-08-04 04:14Z batch the same count is 79 of 143 strict passes. An independent audit of that batch corroborates the weakness is SYSTEMIC rather than a verdict bug: only 47 of 168 passes carry a distinct page literal at all \u2014 62 are prose self-echo, 59 ungrounded, and 56 already carry ungrounded_pass:would_withhold. Enforcing a substantiation requirement would therefore touch about a third of ALL passes, so the fix belongs at the SOURCE (what the grader is required to transcribe) and is its own track, not a verdict change to make alongside a false-pass fix. IT EXTENDS THE pin_substantiation FAMILY RATHER THAN DUPLICATING IT: substantiatePinnedCriterion already flips a passing criterion whose note does not cite its pin, but returns the grade VERBATIM when planCriterion.expectedValue is empty \u2014 which is every PROSE criterion, including this one. strictPassLacksSubstantiation (packages/engine-core/src/criterionSelfContradiction.ts) is exactly that abstention made countable, on the same 'the grade's evidence is its note' doctrine. WHAT THE DEFAULT-OFF STATE DOES: detection always runs and the caller emits strict_pass_unsubstantiated:would_withhold {stepIndex, criterionIndex, check, currentStatus, wouldBecome:'warning', decision:'abstain', enforce, mutated:false, flag} while mutating NOTHING \u2014 no status write, no note, byte-identical verdicts. WHAT =1 WOULD ENABLE is deliberately NOT WIRED in this PR: the flag currently selects only the census row's `enforce` field, so setting it to 1 changes no verdict. Graduating this class requires the source fix plus its own census, not an env flip. Runner lane only. See docs/VERDICT-GATES.md.",designDoc:"docs/plans/2026-08-04-criterion-self-contradiction-design.md",status:"active",added:"2026-08-04",graduation:{status:"gated",gate:"NOT graduable as built, and deliberately so. Enforcing it would demote 104 of 446 strict passes in the mirror corpus (23.3%, 21 of 47 runs) and 79 of 143 in lio's prod 04:14Z batch, overwhelmingly on runs that are honest passes \u2014 a mass false-fail, i.e. the opposite defect. The blocker is not a soak: the SOURCE of the weakness is that a strict prose criterion is not required to transcribe anything (only 47 of 168 prod passes carry a distinct page literal; 62 are prose self-echo). Graduation requires (1) the grading contract changed so a strict pass must carry a transcribed page value, (2) a re-census showing the unsubstantiated population has collapsed, and (3) a decision on the residual population that legitimately cannot transcribe (visual/layout criteria).",evidence:"a strict_pass_unsubstantiated:would_withhold census on staging AND prod after the grading-contract change, showing the population collapsed rather than merely moved, plus the ungrounded_pass:would_withhold overlap so the two are not double-counted",owner:"steering (Alex)",review:"2026-08-11"}},VERIFY_CONFLICT_NEGATION_BAIL_WITHHOLD:{key:"VERIFY_CONFLICT_NEGATION_BAIL_WITHHOLD",envVars:["AGENTIQA_VERIFY_CONFLICT_NEGATION_BAIL_WITHHOLD","AGENTIQA_EXPERIMENT_VERIFY_CONFLICT_NEGATION_BAIL_WITHHOLD"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"FALSE-FAIL REMOVAL \u2014 ships ENFORCING (default ON) because it only ever runs RED->AMBER and removes a verdict the run's own record contradicted. THE DEFECT: mirror run_86de13b1 (b05) step 3 of plan tp_4ce9dd2d 'chat-item-search' was recorded status:failed \u2014 and the RUN failed with it \u2014 while ALL FOUR of its criteria were passed:true, each carrying a groundingObservation quoting the page ('Inacopia Kopierpapier Elite A4 90g 500Bl' visible, price '7,89 \u20AC', card 'B\xFCrowelt Schmidt GmbH & Co. KG'). The client summary read 'Failed: verification for step 3 ... did not pass \u2014 the expected content never appeared on the page and was not confirmed on a re-check, even after a retry', which the run's own step_results contradict. Rate 1 of 5 replicates; the trigger is the model choosing to scroll back and re-verify, and this plan is seq 4 in lio's nightly batch. MECHANISM: the oracle chain declined in sequence and the LEGACY verify-conflict force-fail owned the verdict BY DEFAULT \u2014 verify_conflict_reconcile:inapplicable reason=negation_marker literal='Inacopia ...', then verify_conflict_withhold:inapplicable reason=negation_marker (M2 is OFF on prod anyway), then verify_conflict_reconcile:agent_literal_abstain_inapplicable lane=none provenance=plan_grounded reason=literal_plan_grounded. M1's negation_marker bail is triggered by lio's own DEFENSIVE criterion wording ('those do not count', 'their presence or absence must not be asserted') and is CORRECT \u2014 a PRESENCE clear is unsound for an assertion that something is GONE, and clearing there would launder a genuine failure into a pass \u2014 so the bail itself is untouched by this flag. The bug is the inference that followed it: 'M1 could not clear this' is an ABSENCE OF PROOF, not proof of failure. WHAT =1 ENABLES: a new rung between the agent-literal abstain valve and the legacy force-fail. For a step in failingConflicts whose M1 decision reason is EXACTLY negation_marker, whose status is still passed, and which carries >=1 graded PLAN-AUTHORED criterion with ALL of them passed, the oracle failure is cleared and the step lands in the WARNING band with an honest client note (clientNegationBailWithheldNote \u2014 'could not be re-checked automatically because this step is worded as a negative or absence condition. Every check the plan wrote for this step passed; the re-check itself is reported as unconfirmed rather than passed or failed'), on exactly the machinery M1's stale-scope rung and M2's withhold already use. Because the conflict leaves failingConflicts, the synthesized 'Verification step not satisfied' issue is not filed for it either, and terminationReason stays 'completed'. NEVER A PASS and never a silent green: the outcome is the one-directional RED->AMBER the valve family already owns. NARROW BY CONSTRUCTION \u2014 every condition is a refusal the class needs: no other M1 bail reason qualifies; authorship is read via the SAME `unauthored` mark deriveStepStatusFromCriteria honours, so a fabricated grade can neither buy nor block the amber; and a step the model itself graded negative, or that another valve already marked down, refuses \u2014 so this can never rescue a model-reported failure. =0 UNDER EITHER SPELLING IS SHADOW, NOT INERT: the eligibility computation still runs and the caller logs verify_conflict_negation_bail:would_withhold {stepIndex, stepText, literal, criteriaCount, currentStatus, wouldBecome:'warning', decision:'withhold', enforce:false, mutated:false, flag} while the force-fail, the failed step status, the client summary and the synthesized issue all stand byte-identically. Reads no page and calls no model, so both states cost the same. Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.negationBailWithhold.test.ts (the b05 shape verbatim -> warning + run not failed at the default; =0 -> byte-identical failed + census; and the refusals: a non-negation_marker bail, a model-graded failed criterion, an already-downgraded step, a step with zero authored criteria, and a step whose only passing grades are `unauthored`). Runner lane only. See docs/VERDICT-GATES.md.",designDoc:"docs/plans/2026-08-04-criterion-self-contradiction-design.md",status:"active",added:"2026-08-04"},VERIFY_CONFLICT_URL_GROUNDED_WITHHOLD:{key:"VERIFY_CONFLICT_URL_GROUNDED_WITHHOLD",envVars:["AGENTIQA_VERIFY_CONFLICT_URL_GROUNDED_WITHHOLD","AGENTIQA_EXPERIMENT_VERIFY_CONFLICT_URL_GROUNDED_WITHHOLD"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"SHADOW-FIRST, DEFAULT OFF \u2014 F4, the URL-GROUNDED WITHHOLD rung of the verify-conflict valve family (#2536; the #1907 RBAC COMPOSITE shape, carved out of #2533 per its steering review). THE DEFECT: a timed-out wait_for_element on a COMPOSITE verify step force-fails a passing run even though the runtime already holds deterministic grounded evidence disproving the failure. Staging run run_ef0687dc\u2026 (session asess_1786637131530_ahwx4prw) step 6 ('Sign-in succeeds: lands on /en/auth/dashboard \u2026 a Log out control is visible \u2026 NO Blog Management button is present'): the wait on 'Log out' timed out \u2014 the control renders as an ICON \u2014 while the wait's own result URL was ALREADY /en/auth/dashboard; the model graded the step passed and EVERY authored strict criterion passed with page-quoted grounding. Every valve then declined for its own honest reason: M1 reconcile inapplicable {negation_marker} (correct \u2014 it cannot clear absence wording by literal presence), M2 withhold inapplicable on the same screen, the agent-literal abstain declined (literal_plan_grounded), and F3 negation-bail \u2014 the rung built for this signature \u2014 NEVER BECAME ELIGIBLE: its whole-step markerIsTheAssertion screen sees the not/no markers of the OTHER conjuncts, and a whole-step marker screen cannot tell whose negation it is (zero verify_conflict_negation_bail:* rows; loosening the screen to clause scope is deliberately NOT done \u2014 it would risk laundering the genuine 'X is missing but the page is still open' false-pass shape). The legacy force-fail wrote the verdict (run_complete:verification_conflict_failed, cause wait_literal_only) 22 ms AFTER the F2c shadow url comparator returned its second would_pass (url_predicate_satisfied) for the same step. WHAT =1 ENABLES: a new rung BETWEEN F3 (negation-bail) and the legacy force-fail, triggering ONLY when ALL hold for a conflicted step: (1) the conflict cause classifies wait_literal_only (classifyVerificationConflictCause \u2014 an unconfirmed wait-style oracle is the SOLE basis of the conflict; a model-graded negative, an M1 fresh grounded negative, a plan-pin contradiction and a partially graded step all refuse via the classifier's louder bands); (2) the step is still model-graded passed and carries >=1 authored criterion with ALL of them graded passed (authorship via the SAME `unauthored` mark deriveStepStatusFromCriteria honours \u2014 a fabricated grade can neither buy nor block the amber); (3) the step carries a url-kind grounded candidate (_groundedStateUrlCandidates \u2014 the F2c comparator's grade-time pinned URL, #2425/#2401, recorded whenever GROUNDED_STATE_UNIFIED or PREDICATE_BASIS_VERIFY is on) whose deterministic compareUrl re-read over the step's own assertion text returns would_pass (url_predicate_satisfied). The re-read is a PURE recompute of exactly what the shadow url branch of runGroundedStateUnified graded on the final attempt (same pinned URL, same assertion construction \u2014 the shadow branch logs its verdict but retains nothing), so the rung consumes machine-checked, TEMPORALLY-ALIGNED page evidence with NO model call and NO text-marker heuristic in the loop. -> The conflict is WITHHELD to the WARNING band on exactly the one-directional RED->AMBER landing pad M1's stale-scope rung, M2's withhold and F3 already use: the oracle-failure ledger entries are cleared, the step lands at 'warning' with an honest client note (clientUrlGroundedWithheldNote \u2014 the page address recorded for the step matches the address the step expects, every check the plan wrote passed, the re-check itself is reported as unconfirmed rather than passed or failed), the conflict leaves failingConflicts so no 'Verification not confirmed' issue is synthesized for it and terminationReason stays 'completed' (billable). NEVER A MANUFACTURED PASS: the model's own passed grade stands; only the conflict force-fail is downgraded. FAIL-CLOSED: a would_fail URL re-read (a genuinely wrong page \u2014 the safety pin), an inconclusive one (no derivable path predicate / unparseable URL) or an ABSENT candidate (url kind never derived, the F2c recording flags off, no URL rode the step's captures) all make the rung inapplicable and the existing force-fail stands byte-identically. DIAG ROWS: verify_conflict_url_grounded_withhold:withheld (enforcing) / verify_conflict_url_grounded_withhold:would_withhold (eligible but flag off \u2014 the graduation soak signal), both carrying {stepIndex, stepText, cause, urlVerdict, spectrumReason, currentUrl, currentPath, predicates, literal, criteriaCount, currentStatus, wouldBecome:'warning', decision:'withhold', enforce, mutated, flag}; ineligible steps are NOT logged (the F3 family convention). =0 / DEFAULT-OFF IS SHADOW, NOT INERT: the full eligibility computation and the URL recompute still run and an eligible step logs would_withhold while the force-fail, the failed step status, the client summary and the synthesized issue all stand byte-identically. Reads no page and calls no model, so both states cost the same. Read site: packages/engine-core/src/RunnerRuntime.ts (verifyConflictUrlGroundedWithholdEnabled -> the F4 rung between F3 and the legacy force-fail in the run_complete conflict chain). Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.urlGroundedWithhold.test.ts (the step-6 composite shape fires in BOTH states \u2014 would_withhold at the default, withheld->warning+run-passed under =1; negative controls: cause != wait_literal_only, a strict criterion graded failed, a would_fail URL re-read \u2014 the genuinely-wrong-page safety pin, and a step with no url candidate; flag-OFF byte-identical force-fail + census; the EXPERIMENT_ spelling honoured). Runner lane only. See docs/VERDICT-GATES.md.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-08-14",graduation:{status:"gated",gate:"Graduates on nightly evidence, not on unit tests alone: (1) the live recurring sample \u2014 the nightly RBAC 'Admin roles: User tier' plan (its nightly label stays for exactly this reason) \u2014 goes green WITHOUT any plan-text edit once the flag enforces; (2) a staging would_withhold census shows the rung firing on exactly the wait_literal_only + all-criteria-passed + url-would_pass shape and NEVER on a step whose URL re-read failed or floored (the false-pass direction must stay untouched); (3) the F2c eval family (runner-verify-redirect, #2403) is extended with the composite REPRODUCED-RED case and bound to batch.verify-never-blind.",evidence:"staging verify_conflict_url_grounded_withhold:would_withhold census rows (shape + count), the nightly RBAC User-tier plan verdicts, and the runner-verify-redirect eval extension",owner:"steering (Alex)",review:"2026-08-21"}},VERIFY_RECONCILE_CLEAN_PASS:{key:"VERIFY_RECONCILE_CLEAN_PASS",envVars:["AGENTIQA_VERIFY_RECONCILE_CLEAN_PASS","AGENTIQA_EXPERIMENT_VERIFY_RECONCILE_CLEAN_PASS"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Presentation of a VERIFY_CONFLICT_RECONCILE-reconciled verify step: on, the reconciled step is a clean PASS and the reconciliation is recorded only in the structured verification_conflict_reconciled diag event (no engine-jargon note on the user-facing step); off restores the legacy step-level WARNING plus the explanatory note. Independent of VERIFY_CONFLICT_RECONCILE, which decides WHETHER a conflict reconciles at all \u2014 this only changes how an already-reconciled step is surfaced.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-22"},REPLAY_STALE_HOST_NORMALIZE:{key:"REPLAY_STALE_HOST_NORMALIZE",envVars:["AGENTIQA_REPLAY_STALE_HOST_NORMALIZE","AGENTIQA_EXPERIMENT_REPLAY_STALE_HOST_NORMALIZE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Post-resolution stale-host criterion normalization (#2308, root of decision #2436). A saved plan authored against a developer machine and replayed in the cloud has its NAVIGATION target resolved to the real product origin at startup (startRun:resolvedInitialUrl), but its URL CRITERIA stayed pinned to the pre-resolution host \u2014 so the deterministic #1685 host floor correctly failed a criterion the run could never satisfy. Five production CLI replays of tp_28e7faec (2026-08-04 \u2026 08-16) logged in, reached https://gotogrow.me/dashboard, verified the account and balance, and were still reported FAILED against http://localhost:3000/dashboard. When on, RunnerRuntime.startRun rewrites \u2014 RUN-LOCALLY, after resolution and never before \u2014 only those criterion literals (check / expectedValue / basis.expected) whose authority equals the host resolution just replaced, and only when that replaced host is one classifyHost calls unreachable-from-here (localhost / loopback / RFC1918 / mDNS-local / link-local / unspecified) and the resolved host is public. Every other host is left untouched, so a TRUE host mismatch \u2014 including a staging\u2192prod environment inconsistency \u2014 still fails closed at the same floor. The authored plan is never mutated (nothing is written back to the customer's saved plan). Set to 0 and startRun is byte-identical to the pre-fix behavior.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-09-02",notes:"Ships default-ON from inception: the normalization removes a confirmed false-FAIL class and cannot manufacture a pass \u2014 it only re-points a pin at the environment the run actually visited, and the #1685 floor still does all the grading afterwards. There is therefore no pre-change OFF behavior worth preserving, and a killSwitch-disabled flag needs no INTENTIONALLY_GRADUATED entry (its no-env value already matches the pre-change hardcoded default). Read site: packages/engine-core/src/RunnerRuntime.ts (replayStaleHostNormalizeEnabled). Pure logic + acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.staleHostNormalize.test.ts. Diags: startRun:staleHostCriteriaNormalized (fired, with the per-criterion rewrites) / startRun:staleHostCriteriaKept (stood down, with the reason). KNOWN RESIDUAL: only CRITERIA are normalized \u2014 a verify step's prose text keeps the stale host, so the model's own grade note can still cite it; the deterministic floor, which is what recorded the downgrade, reads the criterion."},VERIFY_PRESENCE_WAIT_FLOOR:{key:"VERIFY_PRESENCE_WAIT_FLOOR",envVars:["AGENTIQA_VERIFY_PRESENCE_WAIT_FLOOR","AGENTIQA_EXPERIMENT_VERIFY_PRESENCE_WAIT_FLOOR"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Raises a verify-step presence oracle wait (wait_for_element on a `verify` plan step) to a minimum budget (20s) so a slow-rendering but PRESENT element \u2014 e.g. Miro\'s Templates carousel "Blank board" card, which paints several seconds after the dashboard is otherwise interactive \u2014 is not falsely failed by the 5s default wait budget (staging step-5 login/dashboard flake, sessions asess_1784961071757_cqwxon46 / asess_1784960734951_4t0zaxp1, where the next action successfully CLICKED "Blank board"). Floor-only: never lowers a larger model-supplied timeout; off restores the model-supplied / 5s-default budget. Fail-closed preserved \u2014 a genuinely-absent element still times out at the larger budget and records the same oracle failure, so no false-PASS is introduced. Applied in RunnerRuntime.raiseVerifyPresenceWaitBudget (Runner/test-plan lane only; Explorer/Coordinator have no plan steps).',designDoc:"packages/engine-core/src/verifyPresenceWaitBudget.ts",status:"active",added:"2026-07-25"},ABSENCE_AWARE_VERIFY:{key:"ABSENCE_AWARE_VERIFY",envVars:["AGENTIQA_ABSENCE_AWARE_VERIFY","AGENTIQA_EXPERIMENT_ABSENCE_AWARE_VERIFY"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Absence-assertion verify oracle (issue_742c9da8), a BEFORE\u2192AFTER differential. A verify step that asserts a NEGATIVE (the target is GONE) is verified with the presence-only wait_for_element, whose legitimate timeout on the correctly-absent, PLAN-GROUNDED literal is recorded as an oracle failure and force-FAILS the run at run_complete (canReconcileVerificationConflict refuses the plan-grounded literal \u2014 correct for a PRESENCE assertion, wrong for an absence one). Detection ALWAYS runs (shadow): for a conflicted verify step whose step text / any criterion check asserts THIS TARGET's absence (checkTextAssertsAbsence \u2014 the target literal is stripped first, then an EXPLICIT absence lexeme must survive; a negation inside the target NAME or an incidental 'not' cannot route it) and whose unresolved oracle is a wait-style action with a captured target literal, it classifies CONFIRMED vs ABSTAIN and emits an `absence_verify_oracle:shadow` diag. CONFIRMED requires the wait-literal's OWN before\u2192after transition: (a) GENUINELY ABSENT from a FULL, substantive, NON-canvas, non-load-failure AFTER snapshot retained for that step (never the model's note), AND (b) pinPresentInPage-TRUE in an EARLIER full+whole-page+substantive+same-origin BEFORE snapshot (the presence ledger \u2014 reusing the retained full-snapshot maps), AND every graded criterion passed substantiated. The weak container-noun positive anchor is DROPPED as the load-proof (before-presence + whole-page liveness replaces it); a load-failure/retry interstitial AFTER page is REJECTED (snapshotShowsLoadFailure). When this flag is ON it ENFORCES: a CONFIRMED absence reconciles the conflict to a PASS (via the VERIFY_CONFLICT_RECONCILE clean-pass rail), and an ABSTAIN (no full snapshot / canvas / target still present / load-failure after / NO before-presence \u2014 the target was never shown present / unsubstantiated criteria) soft-withholds the step to a WARNING (never a hard fail, never a clean pass). Off leaves every verdict byte-identical (the plan-grounded absence timeout still force-FAILs) with the shadow diag only. Positive (presence) assertions are untouched (checkTextAssertsAbsence false \u2192 inapplicable \u2192 the existing timeout-fails behavior). Runner lane only (RunnerRuntime run_complete).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-25",notes:"GRADUATED 2026-07-26 (default ON) \u2014 the FIRST live trust-verdict graduation. Enforcement (reconcile-to-pass on CONFIRMED / soft-withhold-to-warning on ABSTAIN) is now the no-env default; detection had run in shadow since 2026-07-25 (absence_verify_oracle:shadow diag) \u2014 the PIN_PAGE_GROUNDING / VERIFY_REOBSERVE_WITHHOLD shadow-first precedent. GRADUATION EVIDENCE: the kind-agnostic graduation benchmark (#1857, e2e/benchmark/) returned GATE=GO on the absence corpus (4 false-FAILs fixed \u2192 PASS, 0 regressions, 0 new false-PASS, 0 marginal LLM cost) and is adversarially proven able to say NO-GO; a fresh-build re-confirm held (confirmed-absence\u2192PASS fixes #1816 / unconfirmable\u2192WARNING / still-present\u2192no false-PASS; the full engine-core suite is byte-identical ON vs OFF except the graduated verdicts). HARD CONSTRAINT (unchanged): confirm fires ONLY on the wait-literal's OWN before\u2192after transition (present in an earlier same-origin full+substantive+non-canvas snapshot, absent from the non-load-failure after one) read from real page snapshots \u2014 never the model's note nor a disjoint absence lexeme \u2014 so a hallucinated 'it's gone', a never-loaded presence target (compound presence+absence), and a silently-blank list echoing the container noun all abstain to WARNING rather than confirming; no false-PASS is reintroduced. killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND ABSENCE_AWARE_VERIFY listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire); an explicit AGENTIQA_ABSENCE_AWARE_VERIFY=0 still restores the byte-identical pre-graduation force-FAIL behavior. Read site: packages/engine-core/src/RunnerRuntime.ts (absenceAwareVerifyEnabled). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.absenceVerifyFalseFail.test.ts (end-to-end, now green for the right reason) + RunnerRuntime.absenceVerifyHardening.test.ts. Claim runner.absence-assertion-verify is now bound green."},GROUNDED_STATE_VERIFIER:{key:"GROUNDED_STATE_VERIFIER",envVars:["AGENTIQA_GROUNDED_STATE_VERIFIER","AGENTIQA_EXPERIMENT_GROUNDED_STATE_VERIFIER"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Grounded-state verifier \u2014 S1 escalation gate + shadow instrumentation (Tier-2 vision-extraction cost sizing; design docs/plans/2026-07-25-grounded-state-verifier-design.md). SHADOW-ONLY / MEASUREMENT-ONLY: this slice makes NO model call and NEVER alters a verdict, verdict input, or any other diagnostic. When on, the runner's Tier-1 deterministic pin page-grounding oracle (checkPinPageGrounding), at each GROUNDABILITY abstain on a step carrying a countable expectedValue pin \u2014 canvas_dominant, a non-full/incremental a11y snapshot, or a thin/missing snapshot (NOT the transient/ephemeral-text abstain) \u2014 consults the surface-agnostic capture-groundability signal (captureModeGroundsAbsence, keyed on captureMode, NOT a surface-name check) and emits a structured `verifier_escalated` diag {stepIndex, reason, assertionKind, captureMode, wouldNeedTier2:true} for a capture a vision extractor could ground (the genuine Tier-2 candidate), or `verifier_escalation_abstained` {\u2026, wouldNeedTier2:false, floor:'inconclusive'} when even vision cannot ground it (the fail-closed floor \u2014 abstain, never escalate-and-guess). Off \u21D2 byte-identical to today: the escalation diags are not emitted and nothing else changes. Sizes S2's per-verify vision-extraction cost by measuring how often and WHERE Tier-1 abstains on countable state assertions.",designDoc:"docs/plans/2026-07-25-grounded-state-verifier-design.md",status:"active",added:"2026-07-25",notes:"S1 of the grounded-state verifier (measurement slice). Default OFF; SHADOW-ONLY and verdict-inert even when ON \u2014 this slice only emits verifier_escalated / verifier_escalation_abstained shadow diags at the checkPinPageGrounding abstain points, makes no model call, and changes no verdict. The escalation decision keys on captureModeGroundsAbsence (capture fidelity / checkPinPageGrounding outcome), NOT on canvasDominant/surface identity, so a non-canvas vision-groundable surface escalates through the same path (spec AC-7). Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateVerifierEnabled \u2192 maybeEmitVerifierEscalation, called from checkPinPageGrounding); pure logic in packages/engine-core/src/groundedStateVerifier.ts. Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.groundedStateVerifier.test.ts (wiring, flag-OFF byte-identical) + groundedStateVerifier.test.ts (pure groundability keying). S2 (prompt-only extractor + deterministic comparator) is the next slice and adds the actual Tier-2 call behind this same flag.",graduation:{status:"gated",gate:"S1 is measurement-only (no verdict change), so it graduates by FEEDING S2, not by flipping default-on: a staging shadow soak of verifier_escalated / verifier_escalation_abstained sizes the Tier-1-abstain-on-countable-state rate (per reason + captureMode) that S2 (prompt-only vision extractor + deterministic comparator, same flag) is built against. The flag advances to a real verdict path only under S2+ with its own verdict-parity shadow-soak; S1 alone never flips default-on.",evidence:"staging verifier_escalated / verifier_escalation_abstained diag events (escalation rate + reason/captureMode breakdown) + the engine-core RunnerRuntime.groundedStateVerifier + groundedStateVerifier unit suites",owner:"steering (Alex)",review:"2026-08-08"}},GROUNDED_STATE_EXTRACT:{key:"GROUNDED_STATE_EXTRACT",envVars:["AGENTIQA_GROUNDED_STATE_EXTRACT","AGENTIQA_EXPERIMENT_GROUNDED_STATE_EXTRACT"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Grounded-state verifier \u2014 S2 prompt-only vision EXTRACTOR + deterministic presence COMPARATOR (design docs/plans/2026-07-25-grounded-state-verifier-design.md). The sibling flag to GROUNDED_STATE_VERIFIER (S1's free measurement stays independently runnable). SHADOW-ONLY: when on AND a stateExtractor is wired, each S1 escalation candidate (a Tier-1 checkPinPageGrounding abstain on a vision-groundable capture carrying a PRESENCE assertion, deduped one-call-per-step) gets ONE no-task-stake vision extraction at run_complete that enumerates what is on screen into a FIXED schema (objects/text/labels/counts) \u2014 NEVER a verdict (AC-2: the prompt receives no assertion outcome and no pass/fail framing). A DETERMINISTIC comparator then decides presence of the plan-text-derived target against that extraction, reproducibly from the logged extraction + target without re-calling the model (AC-3), and the runtime LOGS the would-be verdict + its PARITY vs the driver's current grade (grounded_state_extract diag). This slice changes NO live verdict. FAIL-CLOSED (Decision 7): any missing image / extractor abstain / non-answer / error / timeout / ambiguous or thin comparison \u2192 INCONCLUSIVE, never a pass. Off \u21D2 zero extraction calls, no candidate collection, byte-identical behavior (AC-6). Cost: one extraction call per escalated presence step, hard-capped (fork E).",designDoc:"docs/plans/2026-07-25-grounded-state-verifier-design.md",status:"active",added:"2026-07-25",notes:"S2 of the grounded-state verifier (prompt-only extractor + deterministic presence comparator). Default OFF; SHADOW-FIRST \u2014 even when ON it only computes and LOGS the would-be presence verdict and its parity vs the driver grade (grounded_state_extract / _start / _done diags), makes at most ONE extraction model call per escalated step (fork-E hard cap), and changes NO verdict or stepResult. Sibling to GROUNDED_STATE_VERIFIER so S1 measurement runs without paying S2 cost. Requires deps.stateExtractor wired (getStateExtractor in apps/execution-engine/src/buildDeps.ts) \u2014 absent \u21D2 inert. Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateExtractEnabled \u2192 candidate recording in maybeEmitVerifierEscalation, consumed by runGroundedStateExtractions at run_complete); extractor + comparator in packages/engine-core/src/groundedStateExtractor.ts. Acceptance tests: packages/engine-core/src/__tests__/groundedStateExtractor.test.ts (pure prompt/parser/comparator/parity \u2014 AC-2/AC-3) + RunnerRuntime.groundedStateExtract.test.ts (wiring, shadow-no-mutation, fail-closed, one-call cap, seam guard, flag-OFF byte-identical). Binds claim verify.grounded-state-extract-then-compare. S3 (GROUNDED_STATE_DIFFERENTIAL) adds the before\u2192after differential for absence + modification.",graduation:{status:"gated",gate:"S2 is shadow-first (no verdict change). Graduation gates the PRESENCE case only, and only after: (1) a staging verdict-parity shadow soak of grounded_state_extract shows the extract-then-compare would-verdict matching the driver grade on DOM-groundable controls and DISAGREEing (would_fail on a driver-passed step) on the canvas false-pass fixtures (project_canvas_direct_draw_test) that the driver self-grade lets through today; (2) the extractor accuracy on the canvas presence fixtures clears the fork-G bar (else spec the fine-tuned extractor first). Advancing to a LIVE verdict path is a separate step from flipping this flag to shadow-on.",evidence:"staging grounded_state_extract / _start / _done diag events (would-verdict + parity + inconclusive-rate breakdown) + the engine-core groundedStateExtractor + RunnerRuntime.groundedStateExtract unit suites + the claim verify.grounded-state-extract-then-compare",owner:"steering (Alex)",review:"2026-08-15"}},GROUNDED_STATE_DIFFERENTIAL:{key:"GROUNDED_STATE_DIFFERENTIAL",envVars:["AGENTIQA_GROUNDED_STATE_DIFFERENTIAL","AGENTIQA_EXPERIMENT_GROUNDED_STATE_DIFFERENTIAL"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Grounded-state verifier \u2014 S3 BEFORE\u2192AFTER DIFFERENTIAL (absence \u2282 update) for ABSENCE + MODIFICATION (design docs/plans/2026-07-25-grounded-state-verifier-design.md). The sibling flag to GROUNDED_STATE_VERIFIER (S1 measurement) / GROUNDED_STATE_EXTRACT (S2 single-after presence). SHADOW-ONLY: when on AND a stateExtractor is wired, each S1 escalation candidate (a Tier-1 checkPinPageGrounding abstain on a vision-groundable capture carrying an ABSENCE or MODIFICATION assertion, deduped one-call-per-step) runs the S2 no-task-stake vision extraction on BOTH the persisted baseline (before, resolveBaselineMessage) and verify (after, resolveEvidenceMessage) frames \u2014 each with S2's IDENTICAL constant task-blind prompt (AC-2: task-blind on BOTH frames, no assertion / no expected value / no pass/fail framing) \u2014 and a DETERMINISTIC differential comparator decides the verdict from the two extractions + the plan-text-derived target, reproducibly without re-calling the model (AC-3). ABSENCE: target present-before \u2227 absent-after \u2192 would_pass (confirmed_absent); still present-after \u2192 would_fail; before-presence unestablished / after unreadable \u2192 inconclusive. MODIFICATION: a count that changed to the expected value, or a crisp new value that appeared (before-absent + after-present) \u2192 would_pass; unresolvable \u2192 inconclusive. The runtime LOGS the would-be differential verdict + its PARITY vs the driver grade (grounded_state_differential diag); this slice changes NO live verdict. FAIL-CLOSED (Decision 7): any missing-before / unreadable / extractor abstain / error / timeout / ambiguous or unresolvable comparison \u2192 INCONCLUSIVE, never a pass. Canvas is IN scope (the extractor is vision; before/after frames exist via blind-double-read), identified by the plan DESCRIPTOR (fork D1) with an ambiguous match abstaining to inconclusive \u2014 never re-identifying an anonymous object. Off \u21D2 zero candidate collection, zero extraction calls, byte-identical behavior (AC-6). Cost: at most TWO extraction calls per escalated differential step (before + after, fork-E bounded call budget), hard-capped, batched under a deadline.",designDoc:"docs/plans/2026-07-25-grounded-state-verifier-design.md",status:"active",added:"2026-07-25",notes:'S3 of the grounded-state verifier (before\u2192after differential; absence \u2282 update). Default OFF; SHADOW-FIRST \u2014 even when ON it only computes and LOGS the would-be differential verdict and its parity vs the driver grade (grounded_state_differential / _start / _done diags), makes at most TWO extraction model calls per escalated step (before + after; fork-E bounded budget, hard-capped, batched under a deadline \u2192 timeout inconclusive), and changes NO verdict or stepResult. Sibling to GROUNDED_STATE_VERIFIER (S1) / GROUNDED_STATE_EXTRACT (S2). Requires deps.stateExtractor wired (getStateExtractor in apps/execution-engine/src/buildDeps.ts, reused per-frame) \u2014 absent \u21D2 inert. Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateDifferentialEnabled \u2192 differential-candidate recording in maybeEmitVerifierEscalation, consumed by runGroundedStateDifferentials at run_complete); pure differential comparator in packages/engine-core/src/groundedStateDifferential.ts (reuses S2 groundedStateExtractor). Acceptance tests: packages/engine-core/src/__tests__/groundedStateDifferential.test.ts (pure differential \u2014 absence/modification/fail-closed, #1816 RED\u2192GREEN comparator soak hook + green-guard, AC-2/AC-3) + RunnerRuntime.groundedStateDifferential.test.ts (wiring, before+after two-frame extraction, shadow-no-mutation, fail-closed on missing-before/unreadable/timeout, one-differential-per-step cap, canvas descriptor-match, flag-OFF byte-identical). Binds claim verify.grounded-state-extract-then-compare. S4 (GROUNDED_STATE_INCONCLUSIVE) maps verifier-abstain \u2192 the inconclusive floor. Trust-layer Slice 1 (routing-gap fix) ALSO gates a STATIC single-frame COUNT comparator on this same flag: a count-intent assertion ("exactly 3 shapes" \u2014 authored factKind:count or an NL count check) escalates a count candidate and, at run_complete, runGroundedStateCounts runs ONE task-blind extraction + the deterministic compareCount (extracted 4 \u2260 expected 3 \u2192 grounded_state_count would_fail(after_count_mismatch); 3 = 3 \u2192 would_pass(confirmed_count)); shadow-only, fail-closed, one-call-per-step. Acceptance: RunnerRuntime.groundedStateCount.test.ts + the compareCount/checkTextAssertsCount cases in groundedStateDifferential.test.ts; end-to-end firewall proof eval runner-verify-count-canvas.',graduation:{status:"gated",gate:"S3 is shadow-first (no verdict change). Graduation gates the ABSENCE + MODIFICATION differential and only after: (1) a staging verdict-parity shadow soak of grounded_state_differential shows the #1816 reproduced-RED absence scenario would flip RED\u2192GREEN under the differential (confirmed_absent \u2192 would_pass on a driver-failed step) WITHOUT regressing the paired green-guard fixture (target-still-present \u2192 would_fail / driver-pass preserved); (2) new MODIFICATION fixtures (count + value-appearance) shadow-soak clean; (3) the extractor accuracy on the canvas absence/modification fixtures clears the fork-G bar. Advancing to a LIVE verdict path is a separate step from flipping this flag to shadow-on.",evidence:"staging grounded_state_differential / _start / _done diag events (would-verdict + parity + inconclusive-rate breakdown, #1816 RED\u2192GREEN) + the engine-core groundedStateDifferential + RunnerRuntime.groundedStateDifferential unit suites + the claim verify.grounded-state-extract-then-compare",owner:"steering (Alex)",review:"2026-08-22"}},GROUNDED_STATE_INCONCLUSIVE:{key:"GROUNDED_STATE_INCONCLUSIVE",envVars:["AGENTIQA_GROUNDED_STATE_INCONCLUSIVE","AGENTIQA_EXPERIMENT_GROUNDED_STATE_INCONCLUSIVE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Grounded-state verifier \u2014 S4 FAIL-CLOSED INCONCLUSIVE FLOOR wiring (Decision 7 + Fork F2; design docs/plans/2026-07-25-grounded-state-verifier-design.md). The sibling flag to GROUNDED_STATE_VERIFIER (S1) / GROUNDED_STATE_EXTRACT (S2) / GROUNDED_STATE_DIFFERENTIAL (S3). SHADOW-ONLY: when on, each verifier ABSTAIN \u2014 the S1 escalation fail-closed floor (verifier_escalation_abstained, a capture even a vision extractor cannot ground) and every S2 presence / S3 differential `inconclusive` outcome (thin/unreadable capture, extractor refusal/error/timeout, before-presence unestablished, after unreadable, ambiguous match, unresolved count) \u2014 is ALSO mapped to the typed inconclusive floor and LOGGED (grounded_state_inconclusive diag): the DISTINCT `verifier_inconclusive:{reason}` sub-reason + the interim `warning` step status (Fork F2 \u2014 a NEUTRAL 'couldn't tell', NOT an alarm-amber defect per project_warning_display_semantics) + the verifierInconclusive marker + migratesTo:'inconclusive'. This slice mutates NO stepResult and changes NO verdict \u2014 it computes/LOGS the would-be floor mapping only. HONESTY FLOORS: an abstain NEVER maps to `passed` (VERIFIER_INCONCLUSIVE_STEP_STATUS is warning/inconclusive, never passed), and a genuine would_pass/would_fail is never floored (inconclusiveFloorForVerdict returns null on a non-inconclusive verdict; the wiring only fires on the S2/S3 abstain paths + the S1 abstained floor). ONE migration point: flip VERIFIER_INCONCLUSIVE_STEP_STATUS (groundedStateInconclusive.ts) from `warning` to step-level `inconclusive` when Layered-Hybrid Phase-1 lands that status in the step enum. Off \u21D2 zero mapping, zero logs, byte-identical (AC-6).",designDoc:"docs/plans/2026-07-25-grounded-state-verifier-design.md",status:"active",added:"2026-07-25",notes:"S4 of the grounded-state verifier (fail-closed inconclusive floor; interim warning-with-distinct-sub-reason, Fork F2). Default OFF; SHADOW-FIRST \u2014 even when ON it only computes and LOGS the would-be inconclusive floor mapping (grounded_state_inconclusive diag), makes NO model call (pure wiring of S1/S2/S3 abstain outcomes), and changes NO verdict or stepResult. Sibling to GROUNDED_STATE_VERIFIER (S1) / GROUNDED_STATE_EXTRACT (S2) / GROUNDED_STATE_DIFFERENTIAL (S3); the abstains it maps only exist when those slices run, so S4 is additive telemetry on top of them. Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateInconclusiveEnabled \u2192 maybeLogInconclusiveFloor, called at the S1 verifier_escalation_abstained floor in maybeEmitVerifierEscalation + the S2/S3 logInconclusive abstain choke points in runGroundedStateExtractions / runGroundedStateDifferentials); pure mapping in packages/engine-core/src/groundedStateInconclusive.ts (the ONE migration point VERIFIER_INCONCLUSIVE_STEP_STATUS). Acceptance tests: packages/engine-core/src/__tests__/groundedStateInconclusive.test.ts (pure \u2014 each abstain reason \u2192 typed sub-reason \u2192 interim warning-never-passed, distinct-from-product-warning, migration seam, would_pass/would_fail never floored) + RunnerRuntime.groundedStateInconclusive.test.ts (wiring \u2014 flag-OFF byte-identical, shadow-no-mutation, floor logged per S1/S2/S3 abstain, no floor on a would_pass step). Binds claim verify.grounded-state-extract-then-compare. S5 broadens the groundability surface + fine-tune trigger.",graduation:{status:"gated",gate:"S4 is shadow-first (no verdict change) and the INTERIM F2 mapping (warning-with-distinct-sub-reason). Graduation to a LIVE floor is a SEPARATE, later step from flipping this flag shadow-on and requires: (1) a staging shadow soak of grounded_state_inconclusive confirming the abstain\u2192floor breakdown (source / reason / rate) is sane and that no would_pass/would_fail is ever floored (the honesty invariants hold in the field); (2) the S2/S3 verdict-parity soaks having graduated their live-verdict paths (an inconclusive floor is only meaningful once the extract-then-compare verdicts gate); (3) the UI rendering the verifier_inconclusive sub-reason as a NEUTRAL could-not-tell (not alarm-amber). The clean F2\u2192F1 migration (flip VERIFIER_INCONCLUSIVE_STEP_STATUS warning\u2192inconclusive at the ONE point) lands when Layered-Hybrid Phase-1 ships the step-level inconclusive status.",evidence:"staging grounded_state_inconclusive diag events (abstain\u2192floor mapping: source/reason/sub-reason/interim-status breakdown, honesty-invariant field check) + the engine-core groundedStateInconclusive + RunnerRuntime.groundedStateInconclusive unit suites + the claim verify.grounded-state-extract-then-compare",owner:"steering (Alex)",review:"2026-08-29"}},GROUNDED_STATE_FINETUNE_METRIC:{key:"GROUNDED_STATE_FINETUNE_METRIC",envVars:["AGENTIQA_GROUNDED_STATE_FINETUNE_METRIC","AGENTIQA_EXPERIMENT_GROUNDED_STATE_FINETUNE_METRIC"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Grounded-state verifier \u2014 S5 FINE-TUNE TRIGGER METRIC (Fork G: DEFINE the trigger, do NOT fine-tune; design docs/plans/2026-07-25-grounded-state-verifier-design.md). The final sibling flag to GROUNDED_STATE_VERIFIER (S1) / GROUNDED_STATE_EXTRACT (S2) / GROUNDED_STATE_DIFFERENTIAL (S3) / GROUNDED_STATE_INCONCLUSIVE (S4). SHADOW-ONLY: when on AND an S2/S3 shadow batch runs, the runtime ACCUMULATES per-SURFACE outcome counts from the extract-then-compare batches \u2014 extractor-abstain (the prompt-only extractor produced no facts), comparator-inconclusive (facts extracted but no definite verdict), and parity agree/disagree \u2014 bucketed by the escalation reason \u2192 surface class (canvas_dominant\u2192canvas, thin\u2192image_or_svg, incremental/non_full\u2192partial_capture; a DIAGNOSTIC label over the already-recorded reason, NOT a surface-name gate \u2014 the escalation gate stays keyed on captureModeGroundsAbsence, Fork A1). After both batches it LOGS the evaluated fine-tune trigger report against the STATED bar (grounded_state_finetune_metric diag): per surface the extractor-abstain rate, inconclusive rate, disagreement rate, and a would-trigger-fine-tune flag (Fork-G graduation signal, ADVISORY). This makes the prompt-only\u2192dedicated/fine-tuned graduation a DATA read; prompt-only stays v1 (Decision 6) \u2014 this slice invests in NO model, mutates NO stepResult, and changes NO verdict. The metric only has samples to fold when S2 and/or S3 also run. Off \u21D2 zero accumulation, zero logs, byte-identical (AC-6). The STATED bar: per surface, after live graduation, extractor-abstain rate > 0.20 OR inconclusive rate > 0.40 over \u2265 50 escalated extract-then-compares triggers a dedicated/fine-tuned extractor FOR THAT SURFACE (parity-disagreement is reported but NOT a trigger input \u2014 a high disagreement can be the extractor CATCHING driver false-passes, the desired signal).",designDoc:"docs/plans/2026-07-25-grounded-state-verifier-design.md",status:"active",added:"2026-07-25",notes:"S5 of the grounded-state verifier (fine-tune trigger metric; Fork G define-not-fine-tune) and the LAST vision-tier slice. Default OFF; SHADOW-ONLY \u2014 even when ON it only accumulates the per-surface outcome breakdown from the S2/S3 shadow batches and LOGS the evaluated trigger report (grounded_state_finetune_metric diag), makes NO model call (pure metric over existing S2/S3 outcomes), and changes NO verdict or stepResult. Sibling to GROUNDED_STATE_VERIFIER (S1) / GROUNDED_STATE_EXTRACT (S2) / GROUNDED_STATE_DIFFERENTIAL (S3) / GROUNDED_STATE_INCONCLUSIVE (S4); the outcomes it folds only exist when S2/S3 run, so S5 is additive telemetry on top of them. The companion BROADEN half of S5 (SVG/image coverage, spec AC-7 generalized) needed NO gate change \u2014 the S1/S2/S3 path already keys on captureModeGroundsAbsence (Fork A1), so broadening is a fixture/coverage add, proven by the RunnerRuntime.groundedStateBroaden test. Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateFineTuneMetricEnabled \u2192 the run_complete accumulator threaded into runGroundedStateExtractions / runGroundedStateDifferentials, emitted by emitFineTuneTriggerMetric); pure metric + STATED bar in packages/engine-core/src/groundedStateFineTuneTrigger.ts (FINETUNE_TRIGGER_BAR). Acceptance tests: packages/engine-core/src/__tests__/groundedStateFineTuneTrigger.test.ts (pure surface mapping / fold / rates / bar evaluation) + RunnerRuntime.groundedStateFineTuneMetric.test.ts (wiring \u2014 per-surface fold from S2/S3, flag-OFF byte-identical, shadow-no-mutation) + RunnerRuntime.groundedStateBroaden.test.ts (AC-7 generalized: a non-canvas SVG/image surface escalates + extracts + compares through the identical path). Binds claim verify.grounded-state-extract-then-compare. COMPLETES the S1\u2013S5 vision-tier build; remaining work is soak + graduation flips (Alex).",graduation:{status:"gated",gate:"S5 is shadow-first (no verdict change) and DEFINES the Fork-G fine-tune trigger \u2014 it does not fine-tune. The metric graduates by FEEDING the fine-tune decision, not by flipping default-on: a staging shadow soak of grounded_state_finetune_metric measures each surface (canvas / image_or_svg / partial_capture) extractor-abstain + inconclusive rate against the STATED bar (FINETUNE_TRIGGER_BAR: abstain > 0.20 OR inconclusive > 0.40 over \u2265 50 escalated steps). Investing in a dedicated/fine-tuned extractor for a surface is triggered ONLY when that surface clears the bar AFTER the S2/S3 extract-then-compare has graduated live on it (Decision 6: prompt-only ships regardless). This flag alone never flips default-on.",evidence:"staging grounded_state_finetune_metric diag events (per-surface extractor-abstain / inconclusive / disagreement rate + would-trigger evaluation vs FINETUNE_TRIGGER_BAR) + the engine-core groundedStateFineTuneTrigger + RunnerRuntime.groundedStateFineTuneMetric + RunnerRuntime.groundedStateBroaden unit suites + the claim verify.grounded-state-extract-then-compare",owner:"steering (Alex)",review:"2026-09-05"}},GROUNDED_STATE_NL_TRIGGER:{key:"GROUNDED_STATE_NL_TRIGGER",envVars:["AGENTIQA_GROUNDED_STATE_NL_TRIGGER","AGENTIQA_EXPERIMENT_GROUNDED_STATE_NL_TRIGGER"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Groundability-contract Slice 1 \u2014 un-inert the grounded-state verifier for PINLESS criteria (design docs/plans/2026-07-26-groundability-contract-design.md). The Tier-1 pin page-grounding oracle (checkPinPageGrounding) early-returns when a criterion carries no expectedValue pin, so a real/canvas plan's natural-language {check,strict} assertion never reached the S1 escalation path \u2014 the grounded-state verifier was provably INERT on exactly the canvas surfaces (Miro/Figma) it was built for. When on, a PINLESS verify step whose RUNTIME fact-kind (deriveVerifyAssertionKind) is a non-default state-change kind \u2014 `absence` or `modification` \u2014 routes through the SAME capture-groundability abstain\u2192escalate logic the pinned path uses (maybeEmitVerifierEscalation \u2192 buildVerifierEscalation), keyed on the IDENTICAL captureModeGroundsAbsence gate (a capture even a vision extractor cannot ground abstains to the fail-closed inconclusive floor, never escalate-and-guess). F2 ANTI-FLOOD: the DEFAULT `presence` kind is EXCLUDED \u2014 deriveVerifyAssertionKind defaults to presence for everything, so escalating on presence would flood every pinless ungroundable step; only absence/modification (which the classifier returns deliberately) escalate in Slice 1 (authored count/value are Slice 2). ESCALATION-TRIGGER ONLY: matchType/comparison/expectedValue-as-comparator are untouched, this is shadow/verdict-inert like S1\u2013S5, and it emits nothing on its own \u2014 the escalation diag / candidate still requires GROUNDED_STATE_VERIFIER / _EXTRACT / _DIFFERENTIAL / _INCONCLUSIVE. Off \u21D2 byte-identical: the pinless early-return stands, zero new telemetry, zero behavior change (AC-6).",designDoc:"docs/plans/2026-07-26-groundability-contract-design.md",status:"active",added:"2026-07-25",notes:"Groundability-contract Slice 1 (PR-A). Default OFF; SHADOW-ONLY and verdict-inert even when ON \u2014 it only relocates the S1 escalation TRIGGER for pinless criteria off the expectedValue pin onto the existing runtime fact-kind classifier (deriveVerifyAssertionKind). NO schema change (migration-safe). The escalate-vs-abstain disposition stays keyed on the exact captureModeGroundsAbsence gate reused from the pinned path (GUARDRAIL 1: no pinless bypass). Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateNlTriggerEnabled \u2192 maybeEmitPinlessNlEscalation, called at the checkPinPageGrounding pinless early-return; routes through the shared maybeEmitVerifierEscalation). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.groundedStateNlTrigger.test.ts (pinless absence/modification escalates on ungroundable canvas/thin/non_full; groundable-mode capture abstains to the inconclusive floor NOT escalate; presence pinless never escalates \u2014 F2 anti-flood; flag-OFF byte-identical; pinned path unchanged). OVERLAP NOTE: ABSENCE_AWARE_VERIFY (#1818, classifyAbsenceVerifyConflict / wait-oracle literal) is a DISTINCT pin-independent absence route \u2014 both are shadow/verdict-inert, so absence carries a DOUBLE shadow signal; do not double-count in soak evidence. Slice 2 adds authored count/value kinds.",graduation:{status:"gated",gate:"Slice 1 is measurement-only (no verdict change): it graduates by FEEDING the same S2/S3 extract-then-compare shadow soak \u2014 a staging shadow soak of verifier_escalated / verifier_escalation_abstained on PINLESS absence/modification steps (canvas/real plans) sizes the escalation rate the vision tier is built against, previously unmeasurable because pinless steps never escalated. The flag advances to a real verdict path only under S2+ with its own verdict-parity shadow-soak; Slice 1 alone never flips default-on. Slice 2 (authored count/value kinds) is a separate gated slice.",evidence:"staging verifier_escalated / verifier_escalation_abstained diag events on pinless absence/modification steps (escalation rate + reason/captureMode breakdown, de-duplicated against the ABSENCE_AWARE_VERIFY absence shadow signal) + the engine-core RunnerRuntime.groundedStateNlTrigger unit suite",owner:"steering (Alex)",review:"2026-08-15"}},GROUNDED_STATE_FACTKIND_AUTHORED:{key:"GROUNDED_STATE_FACTKIND_AUTHORED",envVars:["AGENTIQA_GROUNDED_STATE_FACTKIND_AUTHORED","AGENTIQA_EXPERIMENT_GROUNDED_STATE_FACTKIND_AUTHORED"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Groundability-contract Slice 2 \u2014 consume the MODEL-authored `factKind` as source-of-truth for the grounded-state escalation trigger's assertion shape (design docs/plans/2026-07-26-groundability-contract-design.md). Builds on Slice 1 (GROUNDED_STATE_NL_TRIGGER, pinless escalation on runtime absence/modification). The new OPTIONAL `factKind` enum (value|count|presence|absence|modification|relation) on TestPlanV2Criterion is authored by the generation model (all three producer schemas) as a deliberate typed choice (spawn_agent authMode/authSurfaceKind precedent). When on, a criterion carrying an AUTHORED factKind (a) OVERRIDES the runtime deriveVerifyAssertionKind inference (mapped 6\u21923 for the escalation branch, Fork F1: absence\u2192absence, modification\u2192modification, value/count/presence/relation\u2192presence) AND (b) LIFTS Slice 1's F2 anti-flood filter for that step so ANY authored kind escalates \u2014 authoring IS the deliberate groundable-fact signal \u2014 still gated by the IDENTICAL captureModeGroundsAbsence floor (a capture even a vision extractor cannot ground abstains to the fail-closed inconclusive floor, NEVER escalate-and-guess), routed per collapsed kind (absence/modification \u2192 S3 differential candidate; value/count/presence/relation \u2192 S2 presence-family extraction candidate). This fixes the count/canvas case (authored count \u2192 escalates to S2 extraction) WITHOUT the runtime-presence flood. UNAUTHORED criteria keep Slice 1's runtime path EXACTLY (only absence/modification escalate; the runtime `presence` default does NOT). matchType/comparison/expectedValue-as-comparator UNTOUCHED; SHADOW-ONLY / verdict-inert like S1\u2013S5; it emits nothing on its own \u2014 the escalation diag / candidate still requires GROUNDED_STATE_VERIFIER / _EXTRACT / _DIFFERENTIAL / _INCONCLUSIVE. Off (or factKind absent) \u21D2 byte-identical: the field is unread, Slice 1's runtime behavior stands (AC-6). The field may be authored + persisted with this flag OFF (harmless, unread).",designDoc:"docs/plans/2026-07-26-groundability-contract-design.md",status:"active",added:"2026-07-25",notes:"Groundability-contract Slice 2 (PR-B). Default OFF; SHADOW-ONLY and verdict-inert even when ON. Read sites: packages/engine-core/src/RunnerRuntime.ts (groundedStateFactKindAuthoredEnabled \u2192 authoredFactKind, consumed by deriveVerifyAssertionKind override + the pinless F2-filter lift in maybeEmitPinlessNlEscalation). Field: packages/shared-types/src/index.ts (FactKind + TestPlanV2Criterion.factKind); producer schemas: coordinatorToolDefs.ts test_plan_criteria_schema, agentToolDefs.ts criteria_schema, runnerToolDefs.ts step_with_criteria_schema (thin inline); generation steer: planStepGuidance.ts buildCriteriaFactKindGuidance; 6\u21923 mapping + type-guard: groundedStateVerifier.ts factKindToAssertionKind / isAuthoredFactKind. DROP-SITES registered (else a plain step-text edit silently wipes the authored field \u2014 the matchType bug): renderer finalizeCriterion (apps/desktop-next/.../testPlanStepsSerde.ts) + server carryCriterionPins (apps/web-next/lib/testPlanSaveNormalize.ts) \u2014 both preserve factKind on an UNCHANGED check, drop it on an EDITED check (\u2192 runtime fallback, safe). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.groundedStateFactKindAuthored.test.ts (authored count/presence on ungroundable canvas \u2192 S2 extraction candidate; authored absence \u2192 S3 differential candidate; authored + degraded capture \u2192 inconclusive floor, no escalate-and-guess; flag OFF \u2192 byte-identical field-unread Slice-1 behavior; field absent \u2192 Slice-1 behavior) + drop-site round-trip tests in testPlanStepsSerde.test.ts / testPlanSaveNormalize.test.ts. The MANDATORY real-runtime generation eval + the qa-box claim verify.groundability-contract-authored are Slice-2b (separate follow-on), NOT this PR. OVERLAP NOTE: composes with GROUNDED_STATE_NL_TRIGGER (Slice 1) via the shared maybeEmitPinlessNlEscalation helper.",graduation:{status:"gated",gate:"Slice 2 is measurement-only (no verdict change): it graduates by FEEDING the same S2/S3 extract-then-compare shadow soak, now widened to authored count/value/presence/relation kinds (the count/canvas case Slice 1 could not reach because runtime presence does not escalate). It advances to a real verdict path only under S2+ with its own verdict-parity shadow-soak; Slice 2 alone never flips default-on. Requires the Slice-2b generation eval (real ExplorerRuntime/CoordinatorRuntime authoring the correct factKind) to gate the authoring quality before any graduation.",evidence:"staging verifier_escalated / verifier_escalation_abstained diag events on PINLESS authored-factKind steps (escalation rate + factKind/reason/captureMode breakdown) + the engine-core RunnerRuntime.groundedStateFactKindAuthored unit suite; graduation additionally blocked on the Slice-2b real-runtime generation eval (authoring-distribution baseline)",owner:"steering (Alex)",review:"2026-08-15"}},GROUNDED_STATE_FACTKIND_TYPING_PASS:{key:"GROUNDED_STATE_FACTKIND_TYPING_PASS",envVars:["AGENTIQA_GROUNDED_STATE_FACTKIND_TYPING_PASS","AGENTIQA_EXPERIMENT_GROUNDED_STATE_FACTKIND_TYPING_PASS"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Groundability-contract Slice 2b \u2014 MODEL-authored focused second-pass factKind typing that FIXES the realistic-authoring gap (design docs/plans/2026-07-26-groundability-contract-design.md). Slice 2 (GROUNDED_STATE_FACTKIND_AUTHORED) consumes an authored `factKind`, but the live ExplorerRuntime authors it on ~0% of criteria produced by a realistic multimodal explore (screenshots + long trace + coordinator\u2192explorer spawn = the load that suppresses a deeply-nested OPTIONAL enum) \u2014 MEASURED 0/8 on e2e/evals/plans/groundability-factkind-explored.ts, and the three natural schema fixes (imperative guidance, required field, field reorder) ALL stayed 0%. But a FOCUSED low-load typing call over JUST the drafted check-texts types factKind at 100% (probe 78/78, real gemini-3-flash-preview). When on, AFTER the Explorer's draftTestCase is finalized (assistant_v2_report accept seam, past every re-prompt gate), a SEPARATE model-authored pass (packages/engine-core/src/factKindTypingPass.ts runFactKindTypingPass) collects the verify criteria that LACK an authored factKind, makes ONE batched Gemini call (the session's own model + the REAL buildCriteriaFactKindGuidance the producer schema ships; generateText + Output.object, thinkingBudget:0) that returns {index,factKind}[], and writes the kind back onto each criterion IN PLACE. It is MODEL-authored (a real Gemini call), NOT the rejected deterministic runtime factKind inference. NO OVERWRITE: only a MISSING factKind is filled (an already-authored kind is skipped at collect + double-guarded at write-back). FAIL-SAFE: any error/empty/timeout/invalid-kind leaves factKind UNSET (falls through to runtime inference) \u2014 never crashes authoring, never writes a garbage kind. SHADOW-SAFE: the written factKind is read ONLY by the separately flag-gated GROUNDED_STATE_FACTKIND_AUTHORED verifier (default-OFF), so verdicts are byte-identical whether or not this pass ran. Off \u21D2 the caller never invokes the module: ZERO new model calls, byte-identical authoring (AC-6).",designDoc:"docs/plans/2026-07-26-groundability-contract-design.md",status:"active",added:"2026-07-26",notes:"Groundability-contract Slice 2b (the realistic-authoring FIX; a flag-gated prototype \u2014 adding a model call to the authoring flow is Alex's architecture/graduation call). Default OFF; when ON adds ONE auxiliary Gemini call per COMPLETED explore that produced untyped verify criteria (batched, text-only, cost-isolated via emitAuxiliaryLlmUsage \u2014 not a billable step). Read site: packages/engine-core/src/ExplorerRuntime.ts assistant_v2_report accept seam (killSwitchEnabled('GROUNDED_STATE_FACTKIND_TYPING_PASS') \u2192 runFactKindTypingPass over draftTestCase.steps, mutating criteria in place BEFORE the report message is persisted/emitted, so the authored factKind rides both the saved plan and the diag). Pure module core (collectUntypedVerifyCriteria / buildFactKindTypingPrompt / mapTypesByIndex / applyFactKindTypes) + the single generateText seam. Unit suite: packages/engine-core/src/__tests__/factKindTypingPass.test.ts (ON fills missing factKind from a mocked typing response; OFF = no call / byte-identical; already-authored factKind never overwritten; error/empty response \u2192 factKind stays unset, fail-safe). RE-MEASURE: e2e/evals/plans/groundability-factkind-explored.ts COMMITTED_BASELINE carries the with-typing-pass authored-rate alongside the without (0/8).",graduation:{status:"gated",gate:"The realistic-authoring re-measure (groundability-factkind-explored with GROUNDED_STATE_FACTKIND_TYPING_PASS ON) shows the authored-factKind rate on the real explore path jump from ~0% to high (target near the probe 100%, \u2265~85%) with CORRECT kinds, AND the engine-core factKindTypingPass unit suite green (fill / no-overwrite / fail-safe / off-byte-identical). Graduation to any verdict path additionally requires GROUNDED_STATE_FACTKIND_AUTHORED (the consumer) to graduate under its own shadow-soak \u2014 this pass only PRODUCES the field.",evidence:"e2e/evals/plans/groundability-factkind-explored.ts with-typing-pass measuredAt entry (authored-rate + kind-correctness) + the engine-core factKindTypingPass unit suite + factkind_typing_pass diag events",owner:"steering (Alex)",review:"2026-08-15"}},GROUNDED_STATE_UNIFIED:{key:"GROUNDED_STATE_UNIFIED",envVars:["AGENTIQA_GROUNDED_STATE_UNIFIED","AGENTIQA_EXPERIMENT_GROUNDED_STATE_UNIFIED"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Trust-layer Slice 2 \u2014 PER-SNAPSHOT UNIFICATION (design docs/plans/2026-07-26-trust-layer-slice-2-per-snapshot-unification-design.md; north star docs/plans/2026-07-26-trust-layer-verification-architecture.md). Collapses the three Slice-1 per-kind run_complete batches (runGroundedStateCounts / runGroundedStateExtractions / runGroundedStateDifferentials \u2014 each re-extracting once PER step/kind) into ONE task-blind extraction PER SNAPSHOT feeding ONE typed-comparator dispatch, and introduces the verdict SPECTRUM (Verified / Assessed / Inconclusive) + semantic matchType. SHADOW-ONLY: when on AND a stateExtractor is wired, it consumes the SAME escalation candidates the per-kind batches do (its flag is OR-ed into the maybeEmitVerifierEscalation candidate-recording seam), groups them by shared captured AFTER-frame (groupCandidatesBySnapshot \u2014 verify-steps sharing a frame with no intervening state-mutating action share ONE extraction; differential candidates additionally share ONE run-start baseline extraction), runs ONE extraction per unique frame, and for each candidate runs its typed comparator (compareCount / comparePresence / compareAbsenceDifferential / compareModificationDifferential) \u2014 or, for a matchType:'semantic' criterion, the task-blind concept classifier (deps.conceptClassifier) \u2014 against that shared extraction. It LOGS grounded_state_unified (band + shadowVerdict + parity vs the driver grade) + grounded_state_unified_start/_done (the extraction-vs-comparison counts that PROVE 1-extraction-per-snapshot); it mutates NO stepResult and changes NO verdict. Part B is a REFACTOR that must be verdict-PARITY with the three per-kind paths: the unified would-verdict equals what the retired per-kind batch logged (same evidence resolution + same target derivation + same deterministic comparator on the same extraction). The verdict spectrum: a deterministic comparator would_pass/would_fail \u2192 Verified (the no-false-positive guarantee); a low-confidence semantic result \u2192 Assessed (the SLOT only \u2014 the independent reasoned judge is a LATER slice); no confident extraction/classification / missing image / extractor abstain / error / timeout / unresolvable comparison / unavailable semantic classifier \u2192 the fail-closed Inconclusive floor, NEVER a pass. Task-blindness preserved: the extraction prompt (buildExtractionQuestion) carries observation targets only and the semantic classifier carries a concept + a neutral observation rendering \u2014 NEVER the expected values or pass/fail framing. It runs ALONGSIDE the per-kind batches (its own flag) so the shadow soak can prove parity BEFORE the per-kind methods are physically retired. Off \u21D2 zero candidate consumption here, zero extraction calls, byte-identical (the per-kind paths, if their flags are on, are untouched) (AC-6). Cost: #snapshots (+1 shared before, if any differential) extraction calls per run \u2014 strictly \u2264 the sum of the three per-kind batches, and 1 for N verify-steps on one screen.",designDoc:"docs/plans/2026-07-26-trust-layer-slice-2-per-snapshot-unification-design.md",status:"active",added:"2026-07-26",notes:"Trust-layer Slice 2 (per-snapshot unification). Default OFF; SHADOW-FIRST \u2014 even when ON it only computes + LOGS the would-be verdict-spectrum result and its parity vs the driver grade (grounded_state_unified / _start / _done diags), makes #snapshots (+1 shared before) extraction calls plus one concept-classifier call per semantic candidate (both cost-isolated Flash-model seams via emitAuxiliaryLlmUsage, hard-capped, batched under the same deadline as the per-kind batches), and changes NO verdict or stepResult. Sibling to GROUNDED_STATE_EXTRACT (S2) / GROUNDED_STATE_DIFFERENTIAL (S3) \u2014 it consumes the SAME candidate maps and reuses the SAME comparators, so its would-verdicts are byte-parity with the three per-kind batches (the Slice-2 acceptance gate). Requires deps.stateExtractor wired; a matchType:'semantic' criterion additionally requires deps.conceptClassifier (absent \u21D2 that candidate floors to Inconclusive). Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateUnifiedEnabled \u2192 the OR-ed candidate recording in maybeEmitVerifierEscalation + collectUnifiedCandidates + runGroundedStateUnified at run_complete); pure core (snapshot grouping / verdict spectrum / semantic mapping) in packages/engine-core/src/groundedStateUnified.ts. Acceptance tests: packages/engine-core/src/__tests__/groundedStateUnified.test.ts (pure \u2014 grouping, spectrum mapping, semantic mapping) + RunnerRuntime.groundedStateUnified.test.ts (wiring \u2014 verdict-PARITY vs the three per-kind batches, ONE extraction for N\u22653 verify-steps on one snapshot, semantic success\u2192Verified / ambiguous\u2192Assessed / error\u2192Verified-FAIL, shadow-no-mutation, flag-OFF byte-identical). Evals: e2e/evals/plans/trust-unified-parity.ts (runner-trust-unified-parity), trust-unified-batching.ts (runner-trust-unified-batching), trust-unified-semantic.ts (runner-trust-unified-semantic). The three per-kind batch methods are RETAINED in this slice as the parity oracle; their physical removal is the graduation follow-up. Binds claim verify.grounded-state-extract-then-compare.",graduation:{status:"gated",gate:"Slice 2 is shadow-first (no verdict change). Graduation gates on: (1) a staging verdict-PARITY shadow soak of grounded_state_unified vs the per-kind grounded_state_count / _extract / _differential diags showing ZERO would-verdict drift across the count / presence / absence / modification fixtures; (2) grounded_state_unified_done confirming extractions == #snapshots (+ shared before), NOT #comparisons (the batching win) in the field; (3) the semantic-matchType Assessed slot behaving (confident \u2192 Verified, ambiguous \u2192 Assessed, contradiction \u2192 Verified-FAIL) at an acceptable concept-classifier accuracy. ONLY after parity is proven do the three per-kind batch methods get physically retired (a separate refactor PR) and does advancing to a LIVE verdict path get considered \u2014 both separate steps from flipping this flag shadow-on.",evidence:"staging grounded_state_unified / _start / _done diag events (spectrum verdict + parity + extraction-vs-comparison counts) cross-checked against the per-kind diags for parity + the engine-core groundedStateUnified + RunnerRuntime.groundedStateUnified unit suites + the trust-unified-parity / -batching / -semantic evals + the claim verify.grounded-state-extract-then-compare",owner:"steering (Alex)",review:"2026-09-05"}},INTERACTION_CLEARS_PRESENCE_ORACLE:{key:"INTERACTION_CLEARS_PRESENCE_ORACLE",envVars:["AGENTIQA_INTERACTION_CLEARS_PRESENCE_ORACLE","AGENTIQA_EXPERIMENT_INTERACTION_CLEARS_PRESENCE_ORACLE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Interaction-clears-presence-oracle (rank-2 of the step-5 slow-render flake wave, sibling to VERIFY_PRESENCE_WAIT_FLOOR's rank-1 budget raise). A verify-step PRESENCE wait oracle (wait_for_element) that STILL times out after the raised budget records an unresolved oracle failure that verify-gated-done only lets a fresh successful wait_for_element/run_js clear \u2014 a screenshot or a successful click is refused and the plan-grounded literal blocks canReconcileVerificationConflict \u2014 so a genuinely-present-but-slow element force-FAILs (staging sessions asess_1784961071757_cqwxon46 / asess_1784960734951_4t0zaxp1, where the NEXT step's click_at('Blank board') SUCCEEDED and navigated). When on, a subsequent SUCCESSFUL deterministic click_at (or a type_text_at/set_focused_input_value write) on the current verify step or the one immediately preceding it (lookback 1) whose ENGINE-RESOLVED element identity (clickTarget.accessibleName/textContent for a ref/coordinate click, clickedElement.textContent for a label click, or typedIntoField \u2014 the resolved field's accessible name \u2014 for a type write; never the model's narration or the typed VALUE) whole-token-substantiates (citationSubstantiates \u2014 the SAME contiguous-token machinery pinPresentInPage uses, NOT substring) the failed wait's captured target literal CLEARS that step's wait-oracle failure \u2014 engine-observed, un-hallucinable proof of presence, strictly stronger than the screenshot the gate already refuses. HARD CONSTRAINTS (no false-PASS): whole-token resolved-name match only, gated by a SIGNIFICANCE floor (the wait literal must carry a \u22654-char token OR \u22652 tokens \u2014 a bare common single short token like \"ok\"/\"3\" whole-token-matches an unrelated control name too easily, so it can never clear); the #1476 real-hit guard (a coordinate no-op-success \u2014 noObservedEffect side channel \u2014 and a non-interactive pixel landing whose accessibleName merely mirrors a container's textContent are BOTH rejected, so a click that reports success but hit nothing cannot launder a miss); a type only ever clears when it genuinely resolved+focused a named field (typedIntoField populated \u2014 a blind write carries no identity); scope = wait-style presence oracle only (isVerificationOracleAction, NEVER the plan-derived pin-page-grounding oracle) on a Runner verify step; an ABSENCE-intent verify step (checkTextAssertsAbsence over the step text + criteria) is NEVER cleared \u2014 a successful interaction DISPROVES an absence assertion, so clearing would manufacture a pass on a real absence-violation bug; fail-closed (a genuinely-absent element cannot be successfully interacted with, and an ambiguous / non-matching / insignificant-literal / stale-beyond-lookback / absence-intent identity does NOT clear \u2014 the force-fail stands). SHADOW-FIRST: off (default) leaves every verdict identical (the force-fail stands) and only emits an `interaction_clears_presence_oracle:would_clear` diag of what it WOULD have cleared, so a staging soak can confirm it fires only on genuine presence before the flip; on makes the clear live (`\u2026:cleared`).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-25",notes:"Default OFF in code; detection runs in shadow always (interaction_clears_presence_oracle:would_clear diag), the actual clear is gated \u2014 the ABSENCE_AWARE_VERIFY / PIN_PAGE_GROUNDING shadow-first precedent. killSwitch resolves the no-env value from this defaultState (#1729), so GRADUATING = flip defaultState to 'on' AND add INTERACTION_CLEARS_PRESENCE_ORACLE to INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Read site: packages/engine-core/src/RunnerRuntime.ts (interactionClearsPresenceOracleEnabled \u2192 maybeInteractionClearsPresenceOracle, called from the browser-action dispatch after recordOffPlanResolvedClick). Resolved-identity sources: label click \u2192 response.clickedElement.text (clickByLabel only populates it after resolving exactly one clickable control by name AND clicking without error \u2014 un-hallucinable); ref/coordinate click \u2192 response.clickTarget.accessibleName/text, admitted ONLY when isInteractiveClickTarget(clickTarget) so a pixel that landed on a text container cannot launder via the accessibleName\u2192textContent fallback; type write (type_text_at / set_focused_input_value) \u2192 response.typedIntoField (getFocusedFieldName \u2014 the accessible name of the field the write actually resolved+focused; the typed VALUE is never an identity). SIGNIFICANCE floor (waitLiteralHasSignificantTokens over the same tokenizeCitation basis the match uses): a bare common single short token (a status word, a lone digit) whole-token-matches an unrelated resolved name too easily, so the wait literal must carry a \u22654-char token OR \u22652 tokens or it never clears. Lookback is intentionally tight (1 step) to keep a stale REAL miss from an earlier step from being laundered by a same-named element that appears much later; a soak may widen it. Absence-intent verify steps are excluded via checkTextAssertsAbsence (a successful interaction disproves an absence assertion \u2192 would be a false-PASS); a suppressed match emits interaction_clears_presence_oracle:absence_skip. Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.interactionClearsPresenceOracle.test.ts (flag-OFF shadow-only + byte-identical for both click and type, flag-ON label + ref + type clear, type resolved-different-field reject, type-with-no-resolved-field reject, significance-floor bare-one-token reject, #1476 no-op reject, non-interactive container-text reject, non-matching-element reject, whole-token-not-substring, genuinely-absent still-fails, absence-intent step NEVER cleared, pin-page-grounding scope guard, cross-step lookback bound).",graduation:{status:"gated",gate:"Staging shadow soak clean (interaction_clears_presence_oracle:would_clear fires ONLY where a successful click genuinely resolved+acted on an element whose engine-resolved identity whole-token-matches the failed wait literal \u2014 never on a #1476 coordinate no-op, a non-interactive container-text mirror, an absence-intent verify step, or a stale failure beyond the 1-step lookback) AND the engine-core interaction-clears-presence-oracle detection-inversion passes (flag OFF \u2192 the timed-out presence step still force-FAILs; flag ON \u2192 a matching successful click clears it to a PASS, a no-op/non-matching/absent/absence-intent element still FAILs)",evidence:"staging interaction_clears_presence_oracle:would_clear / :cleared diag events (clear rate + resolvedVia/literal breakdown) + the RunnerRuntime.interactionClearsPresenceOracle unit suite + the step-5 slow-render rehearsal replay (asess_1784961071757_cqwxon46 / asess_1784960734951_4t0zaxp1)",owner:"steering (Alex)",review:"2026-08-01"}},CANVAS_STRATEGY:{key:"CANVAS_STRATEGY",envVars:["AGENTIQA_CANVAS_STRATEGY","AGENTIQA_EXPERIMENT_CANVAS_STRATEGY"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Enables the canvas-app (Miro/Figma/spreadsheet) per-turn strategy prompt injection and the coordinate-action tool-result caveat; off drops both so a false canvas classification cannot alter targeting guidance.",designDoc:"docs/plans/2026-07-06-canvas-capability-design.md",status:"active",added:"2026-07-06"},SCREENSHOT_DIRECT_UPLOAD:{key:"SCREENSHOT_DIRECT_UPLOAD",envVars:["AGENTIQA_SCREENSHOT_DIRECT_UPLOAD","AGENTIQA_EXPERIMENT_SCREENSHOT_DIRECT_UPLOAD"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Analytics-sink screenshot upload path: on, the sink fetches a presigned R2 PUT URL from /api/analytics/presign-screenshot and PUTs the PNG straight to R2 (bytes never transit web-next); "0" forces the legacy base64 /api/analytics/upload-image route. On any presign/PUT failure the sink falls back to the legacy route per-screenshot regardless of this switch.',designDoc:"packages/engine-core/src/sinks/RemoteAnalyticsSink.ts",status:"active",added:"2026-07-19"},SCOPE_SAME_ORIGIN_LINKS:{key:"SCOPE_SAME_ORIGIN_LINKS",envVars:["AGENTIQA_SCOPE_SAME_ORIGIN_LINKS","AGENTIQA_EXPERIMENT_SCOPE_SAME_ORIGIN_LINKS"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:`Scope-gate valve for page-authored links: a clicked href on a scoped ORIGIN whose path falls outside every scope prefix is allowed (reason same_origin_link, with a scope-drift diag) instead of hard-blocked as an "external link". Cross-origin targets and model-typed navigate URLs keep the hard block. Prevents an LLM-guessed scope prefix from blocking the app's own navigation (assertqa /why-assertqa false 404, 2026-08-05); "0" restores the unconditional prefix block.`,designDoc:"docs/plans/2026-08-05-scope-observed-url-grounding.md",status:"active",added:"2026-08-05"},SCOPE_SPAWN_GROUNDING:{key:"SCOPE_SPAWN_GROUNDING",envVars:["AGENTIQA_SCOPE_SPAWN_GROUNDING","AGENTIQA_EXPERIMENT_SCOPE_SPAWN_GROUNDING"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Deterministic scope-authoring grounding at the spawn boundary: a same-origin spawn scope entry that no child ever visited (observed-URL ledger, runtime-observed only) and the user never supplied is bounced back to the Coordinator ONCE with the observed-URL list to re-author from; an insisted repeat proceeds fail-open. Also stops checkpoint surfaces with unobserved same-origin URLs from being persisted into the durable AppMap. Pure set-membership on parsed URLs \u2014 no label matching or slug heuristics; "0" disables both the bounce and the AppMap filter.',designDoc:"docs/plans/2026-08-05-scope-observed-url-grounding.md",status:"active",added:"2026-08-05"},SAME_GOAL_ABORT:{key:"SAME_GOAL_ABORT",envVars:["AGENTIQA_SAME_GOAL_ABORT","AGENTIQA_EXPERIMENT_SAME_GOAL_ABORT"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Escalates consecutive milestone-free supervisor REDIRECT verdicts into an abort-then-block, bounding a stuck same-goal step; off leaves the supervisor redirecting until the iteration budget runs out.",designDoc:"docs/plans/2026-07-06-canvas-capability-design.md",status:"active",added:"2026-07-06"},SUPERVISOR_VERDICT_MEMORY:{key:"SUPERVISOR_VERDICT_MEMORY",envVars:["AGENTIQA_SUPERVISOR_VERDICT_MEMORY","AGENTIQA_EXPERIMENT_SUPERVISOR_VERDICT_MEMORY"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Phase 3 Layer 1 of the Lio verdict-integrity track. Feeds the last 6 APPLIED supervisor verdicts (action + iteration + instruction truncated to 120 chars) back into the PERIODIC supervisor prompt as a trailing context segment, with a no-self-reversal instruction: do not issue a REDIRECT that reverses a correction already given unless the page state materially changed, and when your own corrections already conflict decide TERMINALLY (WRAP_UP / BLOCK) rather than adding a third direction. THE DEFECT: SupervisorService.evaluate() is stateless per call and its prompt shows only the recent ACTION table, so the judge re-derives an opinion every consult with no idea it already gave the opposite one \u2014 prod 2026-08-06 tp_d1360115 drew 97 verdicts, 69 REDIRECT, ~1/3 reversing the previous instruction ('complete steps 2-6 first' alternating with 'proceed with Step 7'), and the agent oscillated to the 300-iteration cap. SHIPS DEFAULT-ON FROM INCEPTION rather than shadow-first because it writes NO status and takes NO deterministic decision: it only adds text to a judge prompt, and the deterministic bound on the same class is REDIRECT_FLIPFLOP_GUARD (shadow-first, Layer 2), which is where the graduation evidence lives. LOOP-BREAKER PATH UNTOUCHED \u2014 evaluateLoopBreaker keeps its exact prompt (single-shot false-block question, no history to contradict). =0 under either spelling restores the byte-identical pre-change prompt: the ring still fills (it costs nothing) but nothing is rendered and contextHints collapse back to the caller's static hints. Acceptance suite: packages/engine-core/src/__tests__/supervisorVerdictMemory.test.ts (render pin through the exported buildSupervisorPrompt) + BaseRuntime.supervisorVerdictMemory.test.ts (buffer fill/trim, post-escalation recording, run scoping, flag off).",designDoc:"docs/plans/2026-08-06-lio-terminal-verdicts-and-zero-grade-bounce-design.md",status:"active",added:"2026-08-06"},REDIRECT_FLIPFLOP_GUARD:{key:"REDIRECT_FLIPFLOP_GUARD",envVars:["AGENTIQA_REDIRECT_FLIPFLOP_GUARD","AGENTIQA_EXPERIMENT_REDIRECT_FLIPFLOP_GUARD"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Phase 3 Layer 2 of the Lio verdict-integrity track \u2014 the DETERMINISTIC bound under SUPERVISOR_VERDICT_MEMORY's prompt-side nudge. SHADOW-FIRST. THE DEFECT: prod 2026-08-06 tp_d1360115 drew 69 REDIRECT verdicts, ~1/3 of them reversing the previous instruction ('complete steps 2-6 first' \u2194 'proceed with Step 7/8/9'), and rode the 300-iteration cap to a verdict-less terminal. SameGoalAbortTracker exists for this class and was disarmed THREE WAYS at once: 28 of 97 verdicts were non-REDIRECT (each resets its consecutive count), the entity-minting click loop minted 252 progress milestones (each resets BOTH its counters), and the step boundaries reset it again. WHAT =1 ENABLES: RedirectFlipFlopTracker \u2014 run-scoped, with NO RESET LANES AT ALL (not on milestones, not on non-redirect verdicts, not on step boundaries; that scoping IS the fix and is pinned by the unit test 'redirects across step boundaries still count'). TWO TRIGGERS: (1) STEP-TARGET CYCLE \u2014 each 'steps \u2026' MENTION in a redirect message is parsed whole (a number, a range, or an enumeration over , & / and or: 'Step 7', 'steps 2-6', 'Step 7/8/9', 'steps 2, 4 and 6'), mentions that overlap each other cluster into one direction, consecutive redirects naming overlapping steps merge into the standing direction, and a completed A\u2192B\u2192A over pairwise-disjoint step sets is one cycle counted NON-OVERLAPPING; 2 completed cycles trip a strike. A message naming TWO disjoint clusters ('Complete steps 2-6 first, then proceed to Step 7') states a PLAN, not a direction: it counts toward the budget only and never touches the standing direction. BOTH READINGS WERE FORCED BY MEASUREMENT (adversarial review 2026-08-06): the original regex read only the first number of an enumeration, so a supervisor repeating ONE consistent instruction produced alternating phantom-disjoint directions and fired a wrap_up on a run that never reversed (poisoning the very census this gate grades); and flattening a two-direction sentence into one union set let it be merged into the standing direction, after which every later pure message overlapped, no second direction was ever pushed, and the trigger was dead for the rest of the run. (2) TOTAL REDIRECT BUDGET \u2014 24 redirects/run, non-resettable, re-tripping at each further multiple; this is the mechanism-free bound that catches flip-flops whose messages carry no step numbers, and it is also what bounds a runner-lane wander that REPEATED_SIDE_EFFECT_GUARD cannot (that layer's credits refill per declared step in the runner lane). STRIKE LADDER (SameGoalAbortTracker's shape): strike 1 synthesizes wrap_up, strike 2 synthesizes block, routed through the existing supervisor hard-stop path (blockKind 'supervisor', endMechanism supervisor_halted) so no LLM cooperation is needed. THE TWO SYNTHESIZED STRINGS ARE TRUTHFUL PER TRIGGER: only the cycle trigger says the corrections contradict each other and names both directions; the budget trigger fires on COUNT alone (completedCycles can be zero \u2014 24 corrections advancing monotonically through distinct steps never form a cycle) and therefore says only that, because the block reason is CLIENT-VISIBLE (it becomes blockedReason and the run's stop reason) and the wrap_up steers the agent's final report. PRECEDENCE: the guard runs BEFORE SameGoalAbortTracker in applySupervisorVerdict, so a rewritten verdict is already wrap_up/block when the same-goal tracker observes it \u2014 the tracker resets its consecutive count and can never double-synthesize over one verdict (pinned). DEFAULT OFF = SHADOW, NOT INERT: the tracker runs in full and the caller emits agent_lifecycle redirect_flipflop_would_fire {trigger, strike, escalation, redirectCount, directions} plus a warn log, and mutates NOTHING \u2014 the redirect applies byte-identically and SameGoalAbortTracker sees the original 'redirect'. An explicit =0 under either spelling switches the DETECTION off too (killSwitchForcedOff idiom), reclaiming the measurement cost. Chat and runner lanes both. Acceptance suite: packages/engine-core/src/__tests__/redirectFlipFlop.test.ts (pure: cycles, strikes, budget, NO resets, parser) + BaseRuntime.redirectFlipFlop.test.ts (wiring: shadow = would-fire with no synthesis, enforce = wrap_up then block, precedence over sameGoalAbort, no double synthesis, run scoping).",designDoc:"docs/plans/2026-08-06-lio-terminal-verdicts-and-zero-grade-bounce-design.md",status:"active",added:"2026-08-06",graduation:{status:"gated",gate:`A nightly/staging census of redirect_flipflop_would_fire showing (a) the guard fires on runs that actually oscillate \u2014 the tp_d1360115 shape and the entity-minting eval \u2014 and (b) ZERO would-fire rows on runs that completed normally, i.e. no healthy run is one strike away from a forced wrap_up. The step-target parser remains the specific risk, and the 2026-08-06 review is the reason to treat it as an empirical question rather than a design one: it shipped with TWO parser defects that each moved a real run's outcome \u2014 enumerations collapsing to their first number (phantom cycles on a consistent supervisor) and two-direction sentences absorbing the standing direction (the trigger silently dead). Both are closed and pinned, but the class is "prose read as a steering target", so the census must show the DIRECTIONS ROW BY ROW: that each parsed direction was a real steer and not incidental prose ("you are still on step 3"), and that runs whose supervisor genuinely oscillated produced cycle fires rather than only budget fires \u2014 a run that reaches the count bound without a single cycle fire is evidence the parser is still blind, not that the guard worked.`,evidence:"agent_lifecycle redirect_flipflop_would_fire rows on the nightly qa-exhaustive lane and on a Lio prod\u2192staging mirror replay, sized against the runs that completed normally in the same batch",owner:"steering (Alex)",review:"2026-08-13"}},TARGET_CONTAINMENT:{key:"TARGET_CONTAINMENT",envVars:["AGENTIQA_TARGET_CONTAINMENT","AGENTIQA_EXPERIMENT_TARGET_CONTAINMENT"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Keeps the agent within the app-under-test origin at every navigation chokepoint; off makes every boundary check return "allow" (pre-containment behavior).',designDoc:"packages/engine-core/src/BasePlaywrightService.ts",status:"active",added:"2026-07-05"},INCIDENTAL_EXTERNAL_LOGIN_RECOVERY:{key:"INCIDENTAL_EXTERNAL_LOGIN_RECOVERY",envVars:["AGENTIQA_INCIDENTAL_EXTERNAL_LOGIN_RECOVERY","AGENTIQA_EXPERIMENT_INCIDENTAL_EXTERNAL_LOGIN_RECOVERY"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Necessity-gated split of the third-party-IdP boundary, in BOTH lanes: with NO login credentials configured, an external login reached on a DIFFERENT registrable domain than the app under test AND judged task-INCIDENTAL is RECOVERED \u2014 the service returns the page to the app (popup close \u2192 history back \u2192 pre-navigation app URL \u2192 app origin, every leg bounded at 5s), injects an agent-visible note that names the host, forbids re-following it AND warns that the return trip reset the page's in-page state, and the run continues. Unattended (RunnerRuntime.startRun) that replaces terminating the run as exploration_blocked (staging asess_1785402865447_25cimyx8: a \"check all the buttons\" run followed a social footer link to instagram.com/accounts/login and lost 49 recorded actions); in the interactive CHAT lane (Coordinator/Explorer, and the runner's own sendMessage) it replaces the ask-user credentials pause (staging asess_1785434018769_usq42s45: the self-agent's inner \"Direct task\" Coordinator run blocked on the same host with reason:'interactive_session'). TWO LAYERS: the registrable-domain rule is only the fail-closed floor; before any recovery the service consults the NECESSITY judge (externalLoginNecessityJudge.ts, Layer 2 of the uncertain-boundary class, engine-owned Flash model, at most ONE call per external host per run) with the active plan step \u2014 or, in the chat lane, the turn's task/objective text (BaseRuntime.necessityContextText). FAIL-SAFE onto the pre-existing terminal for `required`, `uncertain`, confidence < 0.8, a judge error, and NO judge wired at all (desktop / no engine key \u21D2 the whole feature is inert) \u2014 so a human who genuinely needs to hand over SSO credentials still gets the pause, and only logins the task does not need stop interrupting them. Bounded at 3 recoveries per run via ONE counter shared by both lanes; a 4th hit, a recovery that lands back on an external login, or a SAME-registrable-domain login (the app's own SSO) keeps today's terminal block verbatim. GRADUATED default-ON 2026-07-30 (gates G1-G4): with NO env both lanes now recover a confidently-`incidental` login and the judge is consulted in the would-recover branch. An explicit `=0` under either spelling restores the pre-graduation terminal verbatim in BOTH lanes and spends NO judge call, while the decision is still computed and emitted as `incidental_external_login` diagnostics (with `wouldRecover`, the domain-signal-only upper bound across BOTH lanes \u2014 pair it with `unattended`; plus the necessity verdict/confidence/reason whenever a judge ran). Recovery reasons are lane-labeled (`incidental` unattended / `chat_incidental` interactive); the pre-2026-07-30 `interactive_session` terminal reason is retired. Never fires when credentials are configured (the pre-existing suppress) and never sees a same-product cross-environment escape (targetContainment runs first).",designDoc:"docs/plans/2026-07-06-uncertain-boundary-escalation-design.md",status:"active",added:"2026-07-30",notes:"GRADUATED 2026-07-30 (default ON) \u2014 graduation gate v2 (PR #1969), four attestations; the first graduation whose effect is to REMOVE a whole-run terminal rather than add a gate. killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND INCIDENTAL_EXTERNAL_LOGIN_RECOVERY listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire); an explicit AGENTIQA_INCIDENTAL_EXTERNAL_LOGIN_RECOVERY=0 (or the AGENTIQA_EXPERIMENT_ spelling) still restores the byte-identical pre-graduation terminal \u2014 `exploration_blocked` unattended, the ask-user credentials pause in chat \u2014 with ZERO judge calls. GRADUATION EVIDENCE. G1 (necessity-discrimination eval PAIR, deterministic + local): e2e/evals/plans/incidental-external-login-recovery.ts \u2014 6 legs, both necessity directions, 2/2 green with the REAL gemini-3-flash judge (incidental @0.90 / required @1.00 on the SAME external host) and 1/1 green with the deterministic stub judge. G2 (suites): engine-core incidentalExternalLogin (L1 decision matrix incl. the necessity fold-in AND the chat lane: judge-incidental recovers as `chat_incidental`, required/uncertain/unjudged keep the pause, shared cap), BasePlaywrightService.incidentalExternalLogin (L2 wiring over a real headless browser and synthetic hosts, stubbed judge through the production DI seam, both directions x both lanes + per-host cache + no-judge fail-safe + one shared counter + the forced-OFF parity cases, which are now built by forcing `=0` and never by deleting the key), RunnerRuntime.unattendedRunLifecycle (the necessity-context channel: plan step in a run, task text in chat, undefined when blank), externalLoginNecessityJudge (judge fail-safe). G3 (bounded live staging validation, flag force-ON via the staging orchestrator env): self-agent.yml GH run 30572972048 (2026-07-30, the SECOND G3 attempt) \u2014 3/3 PASSED including ci-first-plan end-to-end (signup -> inner directed run completed -> Save -> re-run of the saved plan). The FIRST attempt failed informatively: the inner run is an assistant_v2 Coordinator session, so the recovery was lane-gated OFF and the run blocked exactly as before the flag existed \u2014 that is what motivated the chat-lane extension and forced the re-run. G4 (telemetry): a LIVE unattended-lane recovery on the saved-plan re-run \u2014 `incidental_external_login {route:'popup_closed', reason:'incidental', idpHost:'www.instagram.com', outcome:'recovered', returnedTo:'https://agentiqa.com/en/pricing', unattended:true, recoveryCount:1, pageStateReset:false, necessityReason:\u2026}` in asess_1785438775291_qqz9uae4 \u2014 a real judge, a real third-party login wall, the popup route; 0 recovery loops, 0 nav-timeout fallbacks, 0 misfires. HONEST RESIDUAL \u2014 the CHAT lane is live-UNEXERCISED: in that run the external site showed the inner run no login wall at all (it redirected to X instead \u2014 a nondeterministic third party), so no live `chat_incidental` recovery was observed. Chat-lane behavior is pinned by HERMETIC evidence only (the G1 eval's chat leg plus the L1/L2 chat-lane cases), which is the one non-live leg of this graduation. SCENARIO-19 CONSEQUENCE: agent-core/oauth-idp-redirect-no-creds-pause used to pass for TWO independent reasons (the flag was staged OFF, and its prompt IS the task-REQUIRED SSO shape); post-graduation only the second holds, so that nightly scenario is now a live assertion that the necessity judge answers `required` on it (G1 measured confidence 1.00 on exactly that shape). The staging orchestrator force-ON override (AGENTIQA_EXPERIMENT_INCIDENTAL_EXTERNAL_LOGIN_RECOVERY=1, hand-set for G3) is redundant once this ships and is scheduled for removal \u2014 see docs/FLAGS-STATE.md. STAGING-FIRST: merging to staging makes this default-ON on the STAGING engine only; PROD stays OFF until a later staging->main release carries it (a natural staged soak). Read site: packages/engine-core/src/BasePlaywrightService.ts (recoverIncidentalExternalLoginIfNeeded). Binds claim agent.incidental-external-login-recovery."},PIN_SUBSTANTIATION:{key:"PIN_SUBSTANTIATION",envVars:["AGENTIQA_PIN_SUBSTANTIATION","AGENTIQA_EXPERIMENT_PIN_SUBSTANTIATION"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Flips a criterion the model graded "passed" to failed when the pinned expected value is not substantiated; off returns the model grade verbatim (pins still render, enforcement is off).',designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-07"},PIN_CROSS_STEP_SUBSTANTIATION:{key:"PIN_CROSS_STEP_SUBSTANTIATION",envVars:["AGENTIQA_PIN_CROSS_STEP_SUBSTANTIATION","AGENTIQA_EXPERIMENT_PIN_CROSS_STEP_SUBSTANTIATION"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:`FIX 3 (extends FIX 2 to the cross-STEP misattribution class). Before Amendment 5a synthesizes a strict pinned criterion as "never graded \u2192 unconfirmed \u2192 fail", it searches EARLIER step entries for an ORPHAN grade of the IDENTICAL check \u2014 graded passed=true, bound to NONE of its own step's plan criteria, and whose observed value substantiates THIS pin's expectedValue (the SAME substantiatePinnedCriterion gate). A match means the grading LLM misfiled the pin under a PRECEDING/action step (prod asess_1785211658727: step 2's 3 strict pins graded inside step 1, an action step), so the pin was really evaluated \u2014 the substantiated pass is relocated to its own slot instead of false-failing the step. Off restores FIX 2 behavior verbatim (no cross-step search). The rescue is SCOPED to misattributed grades, not a blanket 'a passing grade exists elsewhere': (1) only orphan grades qualify \u2014 a step's OWN bound verdict is never borrowed, so a still-passing earlier verify cannot mask a later regression; (2) a contradicting FAILING grade for the same check anywhere blocks the rescue; (3) only a STRICTLY EARLIER step's grade qualifies, so a later step's page state is never laundered backward onto an earlier assertion (a backward misattribution stays fail-closed); (4) the caller only rescues a pin unique among plan verify criteria. Outside that envelope the pre-existing fail-closed synthesis stands. EVIDENCE-EPOCH DISPOSITION (steering 2026-07-28): 'strictly earlier' does not mean 'same page state', so a rescue is only a silent PASS when NO state-changing plan step (type action/setup) sits STRICTLY BETWEEN the rescuing entry and the pin's own step \u2014 the confirmed prod shape (verify step k's grades misfiled under the immediately preceding action step k-1) has nothing in between, so it keeps its full PASS rescue. A STALE-FORWARD rescue (an intervening action/setup broke the epoch, so the observation may predate a regression) is instead SOFT-WITHHELD to a step-level 'warning' \u2014 the criterion carries the substantiated pass noted 'substantiated by an earlier-step grade; not re-verified at this step' and the step is demoted passed->warning (never a hard fail, never a clean pass; run status is untouched since only 'failed' steps downgrade a run). This is the ABSENCE_AWARE_VERIFY-ABSTAIN / GROUNDING_EPOCH_FIDELITY withhold rail: the criterion stays passed:true on purpose, because flipping it would route deriveStepStatusFromCriteria to a hard 'failed' on a strict criterion (warning-cap-only). Every accepted rescue emits RunnerRuntime log pin_cross_step_substantiated {stepIndex, sourceStep, expected, orphanCandidates, corpusSize, reason} for prod frequency/provenance, where reason is 'same-epoch-rescue' (PASS) or 'stale-forward-warning' (withheld). SHARED BINDER (round 4): the orphan test (guard 1) READS the already-computed per-entry bindings from bindGradesToPlanSlots \u2014 the ONE authoritative two-pass binder that also drives FIX 2's cross-entry union and the reported criteria results \u2014 and never re-derives them. An earlier revision ran its own SEQUENTIAL bindGradeToPlanIdx loop, which diverges from the two-pass binder wherever a rephrased grade's positional fallback would steal a slot a later grade matches by exact text: the authoritative binder books that grade onto its own step (non-orphan) while the sequential mirror leaves it unbound (orphan) and thus eligible to rescue another step's pin \u2014 an EMERGENT false-PASS reachable only with PIN_CROSS_STEP_SUBSTANTIATION and CRITERION_BIND_RESIDUAL both on, which each flag's own tests miss. Pinned by a 4-cell flag-matrix test. PER-CRITERION FLOOR (round 4): the rescue pushes a synthetic passed result, which switched OFF the whole-step zero-grade 'unsubstantiated verify' floor (that floor tests criteriaResults.length === 0) for the step's OTHER criteria \u2014 and Amendment 5a itself only covers PINNED strict criteria, so a plain strict criterion beside a rescued pin was adjudicated by nothing and rode through on the reported 'passed'. On steps where a rescue fired, each plan criterion that is in neither the FIX 2 union nor Amendment 5a's coverage AND has no matching grade anywhere in the cross-step corpus now synthesizes its own failure (strict) or warning (strict:false), emitting pin_cross_step_sibling_unsubstantiated {stepIndex, check, strict, rescuedPinsOnStep}. Scoped to rescue-touched steps so no untouched verdict moves, and gated on 'ungraded ANYWHERE' so the whole-step misattribution the rescue tolerates is not re-punished.`,designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-28"},CHECK_TEXT_ATOM_PIN:{key:"CHECK_TEXT_ATOM_PIN",envVars:["AGENTIQA_CHECK_TEXT_ATOM_PIN","AGENTIQA_EXPERIMENT_CHECK_TEXT_ATOM_PIN"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"run_complete grade-time atom pinning: a strict verify criterion whose check text pins exactly one URL literal (http(s):// or bare localhost) and carries no expectedValue has that literal promoted to an effective expectedValue so the pin-substantiation / grounding machinery fires on it, plus a deterministic URL-host floor that fails the criterion closed (naming both hosts) when the evidence observes a different-host URL and preserves the pass on a scheme-only / trailing-slash difference \u2014 regardless of what the substantiation/drift judge decided. Off leaves a bare {check, strict} URL criterion ungraded past the model self-grade (pre-feature behavior).",designDoc:"docs/plans/2026-07-19-ag7727-run-fidelity-fixes-design.md",status:"active",added:"2026-07-19"},TYPED_MATCH:{key:"TYPED_MATCH",envVars:["AGENTIQA_TYPED_MATCH","AGENTIQA_EXPERIMENT_TYPED_MATCH"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Routes pin-substantiation's observed-vs-expected comparison through the typed match-comparator registry keyed on a criterion's `matchType` (country ISO-3166 fold DE\u2261Germany, locale numeric equality); a typed match keeps a pass the brittle literal compare would have flipped, a typed mismatch (wrong country) flips closed. Off (or an absent/`literal` matchType) restores the pure literal `citationSubstantiates` behavior verbatim \u2014 inert until a criterion carries a matchType, so this only removes a false-fail class, never changes an untyped verdict (AG-7753).",designDoc:"docs/plans/2026-07-20-typed-match-comparator-design.md",status:"active",added:"2026-07-20"},COUNTRY_EQUIV_RESCUE:{key:"COUNTRY_EQUIV_RESCUE",envVars:["AGENTIQA_COUNTRY_EQUIV_RESCUE","AGENTIQA_EXPERIMENT_COUNTRY_EQUIV_RESCUE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'C2 country-equivalence rescue in run_complete pin substantiation: when an already model-PASSED strict criterion\'s literal citation fails, the citation is retried with every recognized country surface form on BOTH sides folded to its ISO-3166 alpha-3, so an address that differs ONLY by country NAME vs CODE ("\u2026H\xF6henkirchen Germany" pin vs "\u2026H\xF6henkirchen DE" observed, prod Lio tp_5a2a7f4c step 9) substantiates instead of brittle-failing. Narrowly scoped: the rescue runs only for a country-SHAPED pin \u2014 `matchType: \'country\'`, or (untyped/literal/text-normalized/presence) a pin whose TERMINAL token is a full country NAME or alpha-3; a bare terminal alpha-2 pin ("Dover, DE") and every number/currency/url/semantic matchType are excluded. The fold itself only rewrites a country NAME anywhere, and a country CODE only as an uppercase token in the terminal country slot (never before a US zip), with subdivision/unit collisions (CA/NL/GB/CH/IE/PL/PT/SE/IN/CAN/NOR/\u2026) restricted to a sole-token read. That extra ambiguity is FOLD-SCOPED (`FOLD_AMBIGUOUS_ALPHA2`/`FOLD_AMBIGUOUS_ALPHA3`): the shared gazetteer read by citedCountry/compareTyped \u2014 i.e. the typed-atom floor and the predicate-basis typed comparator, neither of which this switch gates \u2014 is untouched BY THIS FLAG, verified by a differential sweep of the whole gazetteer. Off restores the pure literal `citationSubstantiates` verdict, so this flag only ever removes a false-FAIL class and nothing this flag contributes survives turning it off. NOTE (AG-8212, 2026-07-29/30) \u2014 that is a statement about THIS flag, not about the comparator stack as a whole: BOTH free-text country reads have since gained their own ungated guard (the same eight collision-prone alpha-2 codes are now gated in `citedCountry` \u2014 the observed scan, direction 1 \u2014 and in `authoredCountryRead` \u2014 the typed-atom floor\'s check-text read, direction 2 \u2014 so a bare code buried in prose resolves a country only when a full NAME corroborates it or when it is the sole token), which this switch does not gate and cannot revert. The fold path and both fold-ambiguity sets remain pre-C2 byte-identical. Split out of TYPED_MATCH (which stays inert on untyped criteria) because this rescue fires on criteria that carry NO matchType.',designDoc:"docs/plans/2026-07-20-typed-match-comparator-design.md",status:"active",added:"2026-07-28"},TYPED_ATOM_FLOOR:{key:"TYPED_ATOM_FLOOR",envVars:["AGENTIQA_TYPED_ATOM_FLOOR","AGENTIQA_EXPERIMENT_TYPED_ATOM_FLOOR"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"AG-7753 Phase 2 strict-unpinned typed-atom floor. run_complete detection ALWAYS runs (shadow): for a strict verify criterion the model graded passed that carries NO expectedValue and whose check text names exactly one recognized typed atom (country ISO-3166 / currency ISO-4217 / number \u2014 the generalization of #1685's URL host floor), the page-read observed value (a11y snapshot, else the structured `observed` grade field; disagreement \u2192 abstain) is compared to the recognized expectation via compareTyped. On a same-type MISMATCH (France where Germany expected) it emits a shadow `typed_atom_floor:would_fail` diag; when this flag is ON it ENFORCES \u2014 flipping the criterion passed\u2192false (only-fails, never originates a pass) and emitting `typed_atom_floor:fire`. Off leaves every verdict byte-identical (shadow diag only). URL stays owned by CHECK_TEXT_ATOM_PIN \u2014 a URL-bearing check is not recognized here.",designDoc:"docs/plans/2026-07-20-typed-match-comparator-design.md",status:"active",added:"2026-07-20",notes:"GRADUATED 2026-07-21 (default on) via the evidence-count doctrine: detection-inversion PROVEN twice \u2014 deterministically in RunnerRuntime.typedAtomFloor.test.ts (33 assertions) and live in the runner/typed-atom-country-floor L3 eval (qa-exhaustive 29861680933: seeded wrong-country step correctly failed) after the self-testing fixture deploy was unblocked (productionBranch was pinned to main). Shadow soak was clean but thin (organic staging plans carry no recognizable unpinned atoms \u2014 vacuous-soak class). Enforcement only-fails a same-type mismatch, never originates a pass. Distinct from TYPED_MATCH (Phase 1, default ON, pinned-criterion fold) and CHECK_TEXT_ATOM_PIN (#1685, URL host floor). Runner lane only (RunnerRuntime run_complete)."},COMPLETION_EVIDENCE_FLOOR:{key:"COMPLETION_EVIDENCE_FLOOR",envVars:["AGENTIQA_COMPLETION_EVIDENCE_FLOOR","AGENTIQA_EXPERIMENT_COMPLETION_EVIDENCE_FLOOR"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Completion-evidence floor for the CHAT/CLI objective lane (the ungrounded-ship seam VERIFY_GATED_DONE misses: it fires only on a failed wait-oracle in the last 5 actions, so a fabricated value with no failing wait ships silently). Detection ALWAYS runs (shadow) at ExplorerRuntime.handleReport on a terminal `completed` report: for an evidence-bearing objective (find/copy/extract a named value) it grounds the agent's TYPED CLAIM (the optional `extractedValues` field on the assistant_v2_report payload; free-text is never parsed): a claimed value found nowhere in the runtime a11y-snapshot corpus nor in runtime_observed revealed facts (whole-token `citationSubstantiates` \u2014 free-form; `compareTyped`'s closed grammar does NOT apply) emits a shadow `completion_grounding:would_demote` diag; a completed extraction objective with NO typed claim is shadow-only signal (reason no_typed_claim) and is NEVER enforced. Label-presence in the corpus never substantiates a value (v1.1). When this flag is ON it ENFORCES the typed-claim-fabrication path only \u2014 attaching an Explorer `VerificationConflict` with the NEW source `completion_ungrounded` (own card copy; NO terminal blockKind) that the existing verification-conflict rail (`applyVerificationConflictFindings` / `focusedTaskVerdictRecommendation`) demotes to do_not_ship in BOTH Coordinator producers. ABSTAINS (never demotes) when the objective is not evidence-bearing, the target is undecidable, or the observed corpus is thin/absent (canvas/off-DOM/dynamic value not in the snapshot) \u2014 demote-on-absence must abstain on any evidence-availability gap (blind-double-read precedent). Off leaves every verdict byte-identical (shadow diag only). Distinct from TYPED_ATOM_FLOOR (runner lane, closed-grammar value mismatch); this is the chat-lane grounding net (absence of any observed evidence, not value-correctness).",designDoc:"docs/plans/2026-07-21-completion-evidence-floor-design.md",status:"active",added:"2026-07-21",notes:"Default OFF in code; detection runs in shadow always (would_demote diag), enforcement is gated \u2014 the PIN_PAGE_GROUNDING / TYPED_ATOM_FLOOR shadow-first precedent. Single Explorer-level hook (handleReport) so both Coordinator verdict producers surface it (the cross-producer parity bug class, PR #643). Chat/Explorer lane only (assistant_v2_report). v1.1: enforcement requires a typed extractedValues claim proven absent from corpus+facts (provable fabrication); strict-extraction recognizer with common-UI-word stoplist (generic read/find objectives abstain); no-typed-claim path stays shadow-only so the soak measures claim-population rate. Grounds fabrication of a claimed value, NOT mis-selection of a real-but-wrong on-page value.",graduation:{status:"gated",gate:"Staging shadow soak clean (would_demote fires on the wandering-maze fabricate-and-ship replicates, zero would_demote on legitimately-shipping value-extraction controls) AND the engine-core completion-evidence-floor detection-inversion passes (floor OFF \u2192 fabricate-and-ship rides through as ship, ON \u2192 do_not_ship on the identical input in BOTH producers)",evidence:"staging completion_grounding:would_demote diag events + the engine-core completion-evidence-floor unit suite + the loop-detection/wandering-self-stop flag-ON graduation run",owner:"steering (Alex)",review:"2026-07-23"}},SETUP_NOTE_GROUNDING:{key:"SETUP_NOTE_GROUNDING",envVars:["AGENTIQA_SETUP_NOTE_GROUNDING","AGENTIQA_EXPERIMENT_SETUP_NOTE_GROUNDING"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Setup/action step-note grounding \u2014 surface S-A of the substantiation-fidelity (\"honest words\") track. Detection ALWAYS runs (shadow) at RunnerRuntime run_complete, after substantiation + the typed-atom floor: for each setup/action step (verify steps out of scope) graded passed/warning whose NOTE names a completed UI action (a closed past-tense/gerund verb lexicon \u2014 entered/typed/filled/submitted/clicked/selected/uploaded/dragged/\u2026) it checks the step's own planStepIndex-stamped action-tool window; a note asserting an action with ZERO grounding action tool calls in-window (the census shape asess_1784714866561_jhc34ht9 \u2014 pre-authed login steps graded green with 'Entered the email'/'Submitted the login form' notes and no type_*/click ever fired) emits a shadow `narration_fidelity:would_flag{surface:'setup_note', stepIndex, claim, missing_action, toolCallsInWindow}`. ABSTAINS (never flags) on a verify/read-only step, a note with no action verb, a non-terminal grade, or a window carrying ANY grounding action tool \u2014 ambiguity always abstains (closed allowlist). When this flag is ON it ENFORCES honest note substitution \u2014 the fabricated action clause is replaced with an honest precondition note; the step's verdict/status is UNTOUCHED (never a hard fail \u2014 the precondition was met, just not by the asserted action). Off leaves every stepResults note byte-identical (shadow diag only). Distinct from TYPED_ATOM_FLOOR (verify-criterion value mismatch) and COMPLETION_EVIDENCE_FLOOR (chat-lane claimed-value fabrication); this grounds SETUP/ACTION step NARRATION against the action log.",designDoc:"docs/plans/2026-07-22-substantiation-fidelity-design.md",status:"active",added:"2026-07-22",notes:"Default OFF in code; detection runs in shadow always (would_flag diag), enforcement (honest note substitution) is gated \u2014 the COMPLETION_EVIDENCE_FLOOR / TYPED_ATOM_FLOOR shadow-first precedent. Single RunnerRuntime run_complete pass (mirrors the typed-atom floor shadow). Deterministic \u2014 ~zero marginal LLM cost (reads the already-collected stepResults notes + the planStepIndex-stamped action-message log). P0 of the substantiation-fidelity track (S-A); S-B/S-C/S-D are separate per-phase flags.",graduation:{status:"gated",gate:"Staging shadow soak clean (narration_fidelity:would_flag{surface:'setup_note'} fires on the census-shaped reproduced-RED fixture, zero would_flag on legitimately-honest action notes whose tool call fired) AND the engine-core setupNoteGrounding detection-inversion passes (rip the pass out \u2192 the fixture stops flagging; enforce-ON rewrites the note, verdict untouched, on the identical input)",evidence:"staging narration_fidelity:would_flag diag events + the engine-core setupNoteGrounding + RunnerRuntime.setupNoteGrounding unit suites + the runner/narration-ghost-setup reproduced-RED eval",owner:"steering (Alex)",review:"2026-07-29"}},CAUSE_GROUNDING:{key:"CAUSE_GROUNDING",envVars:["AGENTIQA_CAUSE_GROUNDING","AGENTIQA_EXPERIMENT_CAUSE_GROUNDING"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Cause-grounding gate (#3091 milestone 2) \u2014 anchors a RED verdict's CLAIMED CAUSE to observed evidence. THE DEFECT: a red run's cause is entirely model prose. `run_complete`'s `summary` and `stepResults[].note` are copied verbatim and every existing gate only PREFIXES/APPENDS around them, so nothing ever inspects the model's own diagnosis. On the milestone-1 mutant fixture (`runner/duplicate-field-breaks-submit`) a duplicated `name=\"password\"` input breaks the POST and the page shows a GENERIC failure with no credentials wording at all \u2014 and the model writes \"Login failed \u2014 the credentials appear to be wrong.\" The verdict is right, the diagnosis is invented, and the customer rotates a password that was never the problem. DETECTION ALWAYS RUNS (shadow) at RunnerRuntime run_complete cascade stage 19c, immediately after `applyStepReplayDisclosure` (19b) and before the `run.status`/`run.summary` write: a deterministic regex lexicon (packages/engine-core/src/causeGrounding.ts \u2014 NOT an LLM) classifies the model-authored remainder of each `failed` step's note and of the run summary into one of nine mechanisms {credentials, permissions, network, validation, missing_data, element_absent, unresponsive_control, rate_limit, app_error}, then asks whether that step's bounded OBSERVED-anchor ledger (`_stepObservedAnchors`) carries an anchor that can ground it. ANCHORS ARE RUNTIME OBSERVATIONS ONLY \u2014 tool errors (both the `status:'error'` and the informative `error`/`valueMismatch` shapes, the same capture sites as `_stepActionErrors`), the per-action EventDigest (failedRequests, recentWrites status codes, consoleErrors, pageErrors), the Layer-B terminal-error quote, and a `detectCredentialRejection` hit on the step's RETAINED full page snapshot. MODEL-AUTHORED FIELDS ARE NEVER ANCHORS (a criterion's `observed` / `groundingObservation` / note cannot ground the same model's claim \u2014 that is the laundering this gate exists to stop; pinned by RunnerRuntime.causeGrounding.test.ts). MODEL-INFLUENCED CHANNELS ARE TAGGED, NOT EXCLUDED: `EventDigest.consoleErrors` (the page console is writable by `run_js`, a runner tool the model calls) and a `run_js` tool error carry `source:'model_influenced'`. They remain anchors because their direction is SILENCE \u2014 an extra anchor costs a missed rewrite, never a deleted sentence \u2014 but a rewrite whose ledger is made ENTIRELY of them is REFUSED under `=1` and abstains with `model_influenced_ledger`; shadow still emits `would_rewrite` carrying `anchorSource`, so the two flag states disagree on this class BY DESIGN and a census must read both. One runtime anchor alongside restores normal judgement. ABSTAINS (stays silent): a note naming no mechanism (\"submit did not work\" is honest by construction), a mechanism the plan step's own text or a PLAN-AUTHORED criterion LICENSES (\"verify an 'invalid credentials' error is shown\" \u2014 the note is quoting the plan; licensing reads all three AUTHOR-WRITTEN criterion surfaces, `check` prose + the pinned `expectedValue` + the compiled `basis.expected`, from `testPlan.steps[].criteria` ONLY, never `stepResults[].criteriaResults[].*`, which is the model's own echo and would let the model silence the gate with a sentence it wrote itself; `expectedValue` is the CANONICAL home of a pinned literal, so a plan with mechanism-free `check` prose and the literal in the pin used to license nothing and had its honest note rewritten), a phrase governed by a negator (\"no validation error was shown\"), any `environment_block` wording (captcha/WAF/bot-protection \u2014 #2606 already authors a runtime message there and a second rewrite would double-author it), a step the model did not grade `failed`, and gate-authored text (the pass reads ONLY the model-authored remainder \u2014 the segment after the last registered gate prefix and before the first registered gate suffix; the registries GATE_SUMMARY_PREFIX_PATTERNS / GATE_NOTE_PREFIX_PATTERNS / GATE_NOTE_SUFFIX_PATTERNS are the single extension point a new prefix/suffix-writing gate must join). WHAT =1 ENABLES: the SILENT rewrite \u2014 the first model-authored sentence carrying an unanchored claim becomes HONEST_CAUSE_FALLBACK (\"The action failed; the cause could not be established from what was observed (no error message, validation text or failed request was captured for this step).\") and any later unanchored sentence is dropped. NO STATUS AND NO CRITERION IS WRITTEN \u2014 the run stays `failed`, the step stays `failed`, only the cause sentence changes (the stage-19b `step_replay_disclosure` precedent). THREE OUTCOMES, NOT TWO \u2014 grounded (silent), unanchored (rewritten under =1), and ABSTAINED. `cause_grounding:abstain {stepIndex, surface, claimed, phrase, reason, anchorsSearched}` is emitted in BOTH flag states and NEVER accompanies a text change: `no_ledger` = the step carries no anchor, no per-action EventDigest receipt and no retained snapshot, so nothing was ever SEARCHED (an empty ledger there means 'we never looked', not 'the app reported nothing' \u2014 an EMPTY digest, by contrast, is a real observation and the gate judges normally); `model_influenced_ledger` = every anchor that would justify the rewrite came from a model-influenced channel (enforcement-only; the same case is a tagged `would_rewrite` in shadow); `quoted_unverifiable` = the sentence's cause is carried by a QUOTED page literal ('the page showed \"Invalid email or password\"') that no retained snapshot can confirm or contradict. A quotation FOUND in any retained snapshot is treated as grounded and left alone silently. COVERAGE RESIDUAL #3072 \u2014 THE TERMINAL 19c NEVER REACHES: a run that ends via `exploration_blocked` (terminationReason `blocked_by_issue`) never calls `run_complete`, so stage 19c does not run AT ALL. `handleBlocked` writes `run.summary` straight from the model's own `obstacle` prose, which is then the only cause text the customer reads on that run. Measured 2026-09-09: 4 of 8 live mutant replicates terminated this way. That surface belongs to m3 / issueEvidenceGate.ts and is NOT fixed here, but it is machine-countable: the blocked path emits ONE `cause_grounding:not_reached {terminal:'blocked_by_issue', surface:'run_summary', reason:'exploration_blocked'}` row, in both flag states, with no text change. A `would_rewrite` census MUST subtract these runs from its denominator instead of scoring them as silence. Off = shadow: byte-identical text, `cause_grounding:would_rewrite {stepIndex, surface, claimed, phrase, anchorsSearched}` diag only; an explicit `=0` under either spelling switches the detection (and the anchor ledger) off entirely. Acceptance suites: packages/engine-core/src/__tests__/causeGrounding.test.ts + RunnerRuntime.causeGrounding.test.ts; evals runner/duplicate-field-breaks-submit (unanchored) and runner/credential-rejection-cause (the negative control \u2014 a real 'Invalid email or password' on the page must NOT be rewritten).",designDoc:"packages/engine-core/src/causeGrounding.ts",status:"active",added:"2026-09-09",notes:'SURFACES OUT OF SCOPE: report_issue free text is NOT covered (it has its own evidence-bundle gate, issueEvidenceGate.ts) \u2014 the 2026-09-09 mutant lane filed an issue titled "Sign in fails with server error" on a purely client-side form bug, which is the same defect class on a surface this gate deliberately does not touch. Default OFF in code; detection runs in shadow always (would_rewrite diag), the rewrite is gated \u2014 the SETUP_NOTE_GROUNDING / PIN_PAGE_GROUNDING shadow-first precedent. Deterministic, ~zero marginal cost (regex over already-collected notes + an anchor ledger fed at existing per-step capture sites; the terminal-error quote is the one model read and it is already paid for by TERMINAL_ERROR_FLOOR). Generalises #2606 from ENVIRONMENT causes (cloudflareDetector / credentialRejectionDetector produce a runtime-authored message) to APP-SIDE causes. CROSS-LANE BOUNDARY (milestone 3, PR #3094 \u2014 merges FIRST): the label-drift disclosure appended at cascade stage 19b\u2032 is REGISTERED in GATE_NOTE_SUFFIX_PATTERNS ("Plan drift: this step\u2019s wording no longer matches the app \u2014 \u2026", quoting two runtime-read control literals, so it can carry lexicon wording); registering it also forced 19b\u2019s own replay-note pattern off its `$` anchor, since 19b\u2032 appends after it. m3\u2019s other two runtime strings are deliberately NOT registered and are pinned as an audit: unnamedControlClickNote lands on a TOOL-RESULT note the model reads (registering it would let the model buy an exemption by echoing it) and unnamedControlRuntimeNote / affordanceMismatchRelabel land on an Issue.description, which this pass never reads. A RED RUN WITH NO RED STEP is covered too: `runFailed` is threaded from the caller so the summary pass also runs on the blocked path (browserUnobservableConcluding), whose summary becomes the customer-visible blockedReason behind "Retained grading summary:".',graduation:{status:"gated",gate:"A shadow census of cause_grounding:would_rewrite on the nightly runner lanes, computed over a denominator that EXCLUDES the runs the gate never saw \u2014 a run carrying a `cause_grounding:not_reached` row terminated before stage 19c (residual #3072) and is `not_reached`, neither a hit nor a silence \u2014 showing (a) it fires on the runs that DO invent a mechanism \u2014 read the rows one by one and confirm each names a mechanism the page never evidenced \u2014 and (b) ZERO would_rewrite rows on runner/credential-rejection-cause, runner/seeded-broken and runner/happy-form, i.e. no honest, anchored or cause-free note is having its diagnosis withdrawn. NOTE ON YIELD (measured 2026-09-09, 3 shadow replicates + 1 enforce + 2 controls on gemini-3-flash-preview): the incumbent model was HONEST on runner/duplicate-field-breaks-submit every time \u2014 it quoted the page's generic banner and one replicate even found the duplicate field with run_js \u2014 so the fixture produced zero would_rewrite rows and the lane green is a NO-REGRESSION result, not a detection one. Detection is proven by the unit suites; the census must therefore run wide (nightly qa-exhaustive, not this fixture alone) before the flip. The false-positive class to answer explicitly: a note whose cause IS grounded by an observation the ledger does not carry (a mechanism visible only in a screenshot, or on a step whose snapshot was never retained). Two shapes of that class are now ABSTENTIONS rather than rewrites (no_ledger, quoted_unverifiable) \u2014 the census must read `cause_grounding:abstain` alongside `would_rewrite`, because a rising abstain rate is the honest signal that the ledger, not the model, is the thing that needs work.",evidence:"cause_grounding:would_rewrite diag rows on the qa-exhaustive runner lane + the two bound eval cases (runner/duplicate-field-breaks-submit, runner/credential-rejection-cause) in both flag states",owner:"steering (Alex)",review:"2026-09-23"}},GROUNDING_EPOCH_FIDELITY:{key:"GROUNDING_EPOCH_FIDELITY",envVars:["AGENTIQA_GROUNDING_EPOCH_FIDELITY","AGENTIQA_EXPERIMENT_GROUNDING_EPOCH_FIDELITY"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Criterion grounding-epoch fidelity \u2014 surface S-B of the substantiation-fidelity (\"honest words\") track. Detection ALWAYS runs (shadow) at RunnerRuntime run_complete, after substantiation + grounding + the typed-atom floor: for each verify-step criterion substantiated as PASSED it compares the criterion's grounding epoch (the monotonic capture/generation index at which its pinned value was actually groundable in a full page snapshot) against the step's OWN verify-time epoch. A value groundable ONLY in a STRICTLY-LATER capture \u2014 a subsequent step's / a different entity's snapshot \u2014 is cross-entity-deferred grounding (the census asess_1784714866561_jhc34ht9 step 6: base-draft pins pin_page_grounding:would_fail at the step's own epoch, then matched off the duplicate-draft (e674/e731) and submitted-request (e1337) snapshots, all past the base-draft epoch) and emits a shadow `narration_fidelity:would_flag{surface:'grounding_epoch', stepIndex, criterion, groundingEpoch, stepEpoch, epochDriftRefs}`. ABSTAINS (never flags) on a value grounded at/before the step's own epoch (honest same-epoch grounding, or a legitimately-earlier carried observation), an explicitly carried-forward observation with recorded provenance, a criterion that did not pass, or an UNDETERMINED epoch (a missing epoch never manufactures a flag). When this flag is ON it ENFORCES via the existing VERIFY_REOBSERVE_WITHHOLD soft-withhold rail: the cross-entity-grounded criterion is treated as UNCONFIRMED and routed to a warning (never a red fail \u2014 the values may be correct; the fix removes the false EVIDENCE ATTRIBUTION, not the pass). Off leaves every verdict byte-identical (shadow diag only). Distinct from PIN_PAGE_GROUNDING (value ABSENCE at grade time) and SETUP_NOTE_GROUNDING (setup/action step NARRATION); this grounds a verify criterion's EVIDENCE-EPOCH provenance.",designDoc:"docs/plans/2026-07-22-substantiation-fidelity-design.md",status:"active",added:"2026-07-23",notes:"Default OFF in code; detection runs in shadow always (would_flag diag), enforcement (soft-withhold to warning via VERIFY_REOBSERVE_WITHHOLD) is gated \u2014 the COMPLETION_EVIDENCE_FLOOR / SETUP_NOTE_GROUNDING shadow-first precedent. Single RunnerRuntime run_complete pass (mirrors the setup-note-grounding shadow). Deterministic \u2014 ~zero marginal LLM cost (reconstructs each step's own capture epoch + each pinned value's grounding epoch from the retained per-generation full snapshots collected during the run). P1 of the substantiation-fidelity track (S-B); S-A (SETUP_NOTE_GROUNDING) shipped P0, S-C/S-D are separate flags.",graduation:{status:"gated",gate:"Staging shadow soak clean (narration_fidelity:would_flag{surface:'grounding_epoch'} fires on the census-shaped A\u2192duplicate-B reproduced-RED fixture, zero would_flag on same-epoch honest grounding controls) AND the engine-core groundingEpochFidelity detection-inversion passes (rip the drift out \u2192 the fixture stops flagging; enforce-ON soft-withholds the criterion to warning, never red, on the identical input)",evidence:"staging narration_fidelity:would_flag diag events + the engine-core groundingEpochFidelity + RunnerRuntime.groundingEpoch unit suites + the runner/narration-grounding-epoch reproduced-RED eval",owner:"steering (Alex)",review:"2026-07-30"}},NOTE_CONTRADICTION_FLOOR:{key:"NOTE_CONTRADICTION_FLOOR",envVars:["AGENTIQA_NOTE_CONTRADICTION_FLOOR","AGENTIQA_EXPERIMENT_NOTE_CONTRADICTION_FLOOR"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Terminal-error verdict-honesty Layer A \u2014 the deterministic note-contradiction floor (run_27825c90). Detection ALWAYS runs (pure module) at RunnerRuntime run_complete, LAST \u2014 after every fail gate: for a step the model graded `passed` carrying a passed criterion whose OWN note/groundingObservation text contains a conservative error marker (error/failed/failure/timeout/timed out/aborted/exception/went wrong, word-boundary) that the criterion's own check/expectedValue (or its step text) does NOT license, it caps that step to `warning` with an explanatory note. A marker GOVERNED BY A NEGATOR in the same clause (no/not/never/without/none/zero/didn't/no longer/free of/\u2026 \u2014 see NEGATOR_RE) is BENIGN and never caps ('No error appeared', 'verified no timeout occurred'), so a note that negates every marker is skipped; a note that re-asserts an error after negating one ('no error at first, then Error: timeout appeared') still caps. ONE-DIRECTIONAL \u2014 only caps a passed step (never rescues/escalates a model-failed grade; the criterion result stays passed, the STEP status is soft-withheld like VERIFY_REOBSERVE_WITHHOLD). Check-text licensing is dumb-string (accepts the documented `verify NO error` negation blind spot on the CHECK \u2014 Layer B handles it semantically). This flag ALSO gates the deterministic report_issue verdict-bearing fold (decision 6): a high/medium-severity `logical` issue filed during an otherwise-clean `passed` run caps the run at `warning`. When ON it ENFORCES; off \u21D2 `note_contradiction:would_cap` / `report_issue_contradiction:would_cap` diags and every verdict byte-identical. Distinct from Layer B (TERMINAL_ERROR_FLOOR, a model terminal-screen read). Ships default-ON (the trap eval's warning-cap signature only greens with both floors live).",designDoc:"docs/plans/2026-07-23-terminal-error-verdict-honesty-design.md",status:"active",added:"2026-07-23",notes:"Ships default-ON from inception (not graduated from an off default) \u2014 registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED. Deterministic (~zero marginal LLM cost \u2014 scans the already-collected stepResults notes + grounding observations). Pure logic in packages/engine-core/src/noteContradictionFloor.ts; applied at RunnerRuntime run_complete AFTER all fail gates so it only ever touches a still-passed step. Runner lane only. Layer A of the terminal-error wave; TERMINAL_ERROR_FLOOR is Layer B."},TERMINAL_ERROR_FLOOR:{key:"TERMINAL_ERROR_FLOOR",envVars:["AGENTIQA_TERMINAL_ERROR_FLOOR","AGENTIQA_EXPERIMENT_TERMINAL_ERROR_FLOOR"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Terminal-error verdict-honesty Layer B \u2014 one unconditional terminal-screen model read per plan run (run_27825c90). At RunnerRuntime run_complete (LAST, after every fail gate) the terminal capture plus the plan's step texts are sent through the existing `deps.blindReader` seam, asked whether an app error/failure state is visible that no plan step asserts. The terminal frame is selected DELIBERATELY from the in-memory `_screenshots` ledger (NOT R2 \u2014 works in the anonymous eval lane where imageStorage is null): the LAST full-frame capture (crops and post-upload shots are skipped; each push site is kind-tagged), abstaining when none exists or when it predates the last browser action (stale). Licensing is SEMANTIC (the reader sees the step texts, so it handles negation like `verify NO error is shown`). An unlicensed visible error \u21D2 cap the run at `warning` by demoting the highest-index PASSED step (via capRunToWarning \u2014 never a skipped/failed step; when a sibling floor already capped the terminal step, the quote is appended, not double-capped) and quote the error text in the step note + run summary. Reader unavailable / no full-frame / stale frame / non-answer / licensed error \u21D2 abstain no-op (never a new false-fail). ONE-DIRECTIONAL (only caps a still-passed step; the binary run status never flips \u2014 `warning` is the step-level/derived aggregate). When ON it ENFORCES; off \u21D2 `terminal_error:would_cap` shadow, no cap. This is the ONLY layer that fires when the model never transcribes the banner into any note (the incident shape). Distinct from Layer A (NOTE_CONTRADICTION_FLOOR, deterministic note scan). Ships default-ON.",designDoc:"docs/plans/2026-07-23-terminal-error-verdict-honesty-design.md",status:"active",added:"2026-07-23",notes:"Ships default-ON from inception \u2014 registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED. Cost \u2248 one cheap Flash vision call per plan run (the cost-isolated blindReader model, same as BLIND_DOUBLE_READ). Detection (the read) runs whenever a reader + final capture are available, EXCEPT at an explicit `=0`: since #3114 the switch gates the call itself (`terminal_error:detection_off`), so `=0` genuinely reclaims the vision call \u2014 it used to read and shadow anyway. Consequence: `enforce` is false under exactly that condition, so the `terminal_error:would_cap` shadow row is unreachable from production and survives only as the method parameter contract (still unit-pinned). Pure prompt/interpretation in packages/engine-core/src/terminalErrorFloor.ts. Runner lane only. Layer B of the terminal-error wave; NOTE_CONTRADICTION_FLOOR is Layer A + the report_issue fold."},TERMINAL_ERROR_FLOOR_NARROW:{key:"TERMINAL_ERROR_FLOOR_NARROW",envVars:["AGENTIQA_TERMINAL_ERROR_FLOOR_NARROW","AGENTIQA_EXPERIMENT_TERMINAL_ERROR_FLOOR_NARROW"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"NARROWS Layer B's cap rule (#3114). Two screens over the SAME terminal read TERMINAL_ERROR_FLOOR already pays for \u2014 no extra LLM call. (1) CLASSIFICATION of the reader's quoted APP text: `progress` (an in-flight/pending notice \u2014 'Some search agents have not finished in time\u2026') \u21D2 NEVER cap, NO summary prefix, NO `terminal_error_quote` cause-grounding anchor; `error` (an un-negated error marker or an HTTP 4xx/5xx form survives Layer B's OWN narrow negator \u2014 \u22642 tokens before the marker, clause ends at a comma) \u21D2 eligible; `ambiguous` (neither lexicon hits \u2014 both are English-only, so every non-English banner lands here) \u21D2 TODAY'S BEHAVIOUR UNCHANGED, deliberately, so the narrowing is a progress narrowing and not an English-language one. (2) ATTRIBUTION for the `error` class: the EARLIEST step whose retained full page observation (`_fullSnapshotByStep`) contains the quote, else the terminal capture's own `_currentStepIndex` stamp clamped above by `_maxStepEverExecuted`; NO attribution \u21D2 DISCLOSE (summary prefix + anchor) and demote NOTHING. The `argmax(stepIndex)` cap target and `capRunToWarning`'s highest-passed fallback are BOTH removed from this path (the fallback re-arms the defect: an attributed step that is not `passed` would otherwise fall through to the terminal step again). Requirement (c) \u2014 never demote a strict-substantiated step unless the banner is attributed to its own action \u2014 holds by construction. UNSET (default) is SHADOW: classification + attribution run beside the live rule and emit `terminal_error:banner_class`, `:attribution`, `:would_cap_narrowed` / `:would_not_cap` and `:narrow_divergence`, while every verdict byte is the live rule's. `=1` ENFORCES the narrowed rule. `=0` switches the narrowed screens off entirely (the live rule alone, no new rows). The PARENT `AGENTIQA_TERMINAL_ERROR_FLOOR=0` now disables DETECTION itself \u2014 the blind read no longer runs at all \u2014 so `=0` on the parent really does reclaim the call (it used to read and shadow anyway).",designDoc:"docs/plans/2026-07-23-terminal-error-verdict-honesty-design.md",status:"active",added:"2026-09-10",notes:"Shadow-first per feedback_runtime_guards_shadow_first: a runtime guard over model/app behaviour ships with would_* markers and a census on real plans, never enforce-by-default. Zero marginal LLM cost \u2014 regex over strings already in memory. defaultState off \u21D2 no killSwitchDefaultState INTENTIONALLY_GRADUATED entry (that allowlist is for 1-enables flags left ON). Pure logic in packages/engine-core/src/terminalErrorFloor.ts (classifyBannerSemantics / attributeBannerToStep / decideNarrowedTerminalError); wired in RunnerRuntime.applyTerminalErrorFloor. Runner lane only. KNOWN STRUCTURAL BIAS, measured not hidden: `_fullSnapshotByStep` is written only from the auto-verify-evidence path, so attribution source `snapshot_text` can only ever name a VERIFY step \u2014 the same population #3103 VERIFY_REOBSERVE_WITHHOLD already demotes. The `attribution` census row carries `attributedStepIsVerify` so that interaction is visible BEFORE any flip.",graduation:{status:"gated",gate:"GRADUATE to defaultState:'on' only when ALL hold on the prod nightly census over >=5 consecutive nights and >=60 runner sessions that emitted a `terminal_error:banner_class` row: (1) NO SUPPRESSED TRUE POSITIVE \u2014 zero `terminal_error:narrow_divergence` rows whose run ALSO carries an independent error signal for the same quote (a `note_contradiction:cap`/`would_cap` row, a high/medium `logical` report_issue, or a model-graded failed/blocked step); this is the only direction the narrowing can hurt. (2) THE FALSE POSITIVE IS MEASURABLY GONE \u2014 >=1 `narrow_divergence{reason:'progress_notice'|'different_step'|'strict_substantiated'}` on a run whose steps were otherwise all passed (the #3114 shape reproduced, with the narrowed rule standing down). (3) ATTRIBUTION IS REAL, NOT DEGENERATE \u2014 `terminal_error:attribution{source:'none'}` < 50% of error-classified reads; a majority-`none` census means the rule collapsed into 'never cap' and must be redesigned, not graduated. (4) THE AMBIGUOUS RATE IS BOUNDED \u2014 publish the full `banner_class` distribution and require `ambiguous` < 25% of classified reads; a high ambiguous rate means the English-only lexicons are carrying a non-English population on the legacy path and the classifier needs the language work BEFORE the flip, not after. (5) BOUND EVALS GREEN \u2014 plan-run/terminal-error-surfaced and \u2026-loud at their declared minPassRate 0.67 with AGENTIQA_EXPERIMENT_TERMINAL_ERROR_FLOOR_NARROW=1 forced in the case env, plus plan-run/terminal-progress-notice-control and \u2026-de-control green at N>=3. The de-control's judge REQUIRES a `terminal_error:banner_class` row (a replicate whose reader stayed silent FAILS) and requires the legacy path (no `terminal_error:no_cap`/`:disclose_no_cap` row, any cap booked under `ambiguous`), so its green cannot be earned by a run that never exercised the mechanism; a replicate whose reader TRANSLATED the German toast is reported not-applicable with the quote, never red. The en-control deliberately still passes on reader silence \u2014 a `NONE` answer there means Layer B never fired, the best outcome the narrowing can produce. The deterministic half of the non-English claim (an `ambiguous` banner takes the legacy path byte for byte) is pinned with a mocked reader in RunnerRuntime.terminalErrorFloor.test.ts, so gate (5) reads the live cases as a census, not as the proof. (6) RECORD \u2014 docs/FLAGS-STATE.md records the live state and docs/VERDICT-GATES.md the flipped mechanism in the SAME PR as the defaultState flip.",evidence:"prod nightly `terminal_error:*` census rows (banner_class / attribution / would_cap_narrowed / would_not_cap / narrow_divergence) + the four bound runner eval cases",owner:"steering (Alex)",review:"2026-09-24"}},BLIND_DOUBLE_READ:{key:"BLIND_DOUBLE_READ",envVars:["AGENTIQA_BLIND_DOUBLE_READ","AGENTIQA_EXPERIMENT_BLIND_DOUBLE_READ"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"When on, run_complete independently re-reads the persisted evidence screenshot for each pinned criterion that survived substantiation as passed (a blind LLM read that never sees the expected value) and flips the pass to failed when the observed value does not match the pin; abstains (no-op) on missing image / R2 read failure / reader non-answer / batch timeout. Off makes zero extra LLM calls and leaves every verdict unchanged.",designDoc:"docs/plans/2026-07-15-blind-double-read-design.md",status:"active",added:"2026-07-15",notes:"Default OFF in code; forced ON in staging via the orchestrator env (AGENTIQA_EXPERIMENT_BLIND_DOUBLE_READ=1) \u2014 the exact GROUNDED_EXPECTATIONS precedent. Prod enablement is a later explicit flip after nightly baselines. Runner lane only (RunnerRuntime run_complete).",graduation:{status:"gated",gate:"HARDENED 2026-07-21: BDR must actually FIRE (reads>0) and FLIP on runner/blind-read-conflation-trap \u2014 not merely avoid errors. BLOCKED on the evidence-resolution gap: on the conflation trap BDR abstained missing_image (resolveEvidenceMessage found no hasScreenshot+planStepIndex message \u2014 the model graded from the inline tool-result snapshot, which is never persisted as a screenshot message) and the seeded false-pass shipped (qa-exhaustive 29861680933). Fix = guarantee a planStepIndex-stamped verify screenshot (STEP_MARKER_FOLD stamping path is the natural vehicle \u2014 same root as the run-detail evidence-fidelity gap) or broaden resolveEvidenceMessage fallback. Plus: no new false-FAIL class on nightly runner baselines (MET as of 07-21; abstains acceptable). tp_00b325d5 confirm-path re-verified 07-21 (2 reads/2 confirms/0 flips).",evidence:"runner/blind-read-{parroting,conflation}-trap eval verdicts (conflation must flip) + blind_double_read diag events + nightly runner baselines",owner:"steering (Alex)",review:"2026-07-28"}},NEVER_GRADED_RETRY:{key:"NEVER_GRADED_RETRY",envVars:["AGENTIQA_NEVER_GRADED_RETRY","AGENTIQA_EXPERIMENT_NEVER_GRADED_RETRY"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:'When on (and a criterionRegrader is wired at engine boot), run_complete re-asks the grader ONCE for a STRICT pinned required criterion that NO stepResults entry graded (genuine model-omission \u2014 absent from the FIX 2 cross-entry union) before Amendment 5a synthesizes its "never graded \u2192 unconfirmed \u2192 fail". The returned grade is fed through the SAME substantiatePinnedCriterion gate, so only a re-grade that independently CITES the pinned value rescues the pin to passed; a returned FAIL, an unsubstantiated pass, or an abstain (no image / read failure / non-answer / error / batch timeout) leaves the "never graded" fail byte-identical. Cap ONE retry per pin, no loops. Off makes zero extra LLM calls and leaves every verdict unchanged.',designDoc:"docs/plans/2026-07-27-never-graded-retry-design.md",status:"active",added:"2026-07-27",notes:"Default OFF in code; force ON in staging via the orchestrator env (AGENTIQA_EXPERIMENT_NEVER_GRADED_RETRY=1) \u2014 the BLIND_DOUBLE_READ / GROUNDED_EXPECTATIONS shadow-first precedent. Prod enablement is a later explicit flip after a benchmark + staging soak. Runner lane only (RunnerRuntime run_complete, buildRunnerDeps.getCriterionRegrader \u2014 the SAME cost-isolated Flash model as blindReader; child Runners on the web-coordinator lane do not receive it yet, mirroring blindReader's own coordinator-forward gap). COMPOSES with (does not regress) PR #1884 FIX 2: the retry fires ONLY on pins genuinely absent from the cross-entry union FIX 2 computes, i.e. exactly the omission FIX 2 deliberately left as a fail. killSwitch resolves the no-env value from this defaultState (#1729), so GRADUATING = flip defaultState to 'on' AND add NEVER_GRADED_RETRY to INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Read site: packages/engine-core/src/RunnerRuntime.ts (neverGradedRetryEnabled \u2192 runNeverGradedRetries). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.neverGradedRetry.test.ts.",graduation:{status:"gated",gate:"FIRST SLICE = build + unit (this PR): shadow behind the default-OFF flag, byte-identical when off, fail-closed-safe (retry never manufactures a pass; a genuinely-ungraded-after-retry pin still fails). GRADUATION (later, separate slices) needs: (1) a kind-agnostic graduation benchmark (e2e/benchmark/, the ABSENCE_AWARE_VERIFY #1857 precedent) showing the Lio-s2 omission false-FAIL class is rescued to PASS with 0 new false-PASS and 0 regressions on the runner corpus, adversarially proven able to say NO-GO; (2) a staging shadow/force-ON soak measuring re-grade fire rate + rescue vs abstain vs still-fail; (3) parity when BLIND_DOUBLE_READ is also on; (4) gate-review sign-off (Alex).",evidence:"never_graded_retry:{start,rescue,abstain,still_fail} diag events + the graduation benchmark verdict + a staging soak on re-grade outcomes",owner:"steering (Alex)",review:"2026-08-03"}},EVIDENCE_FIDELITY:{key:"EVIDENCE_FIDELITY",envVars:["AGENTIQA_EVIDENCE_FIDELITY","AGENTIQA_EXPERIMENT_EVIDENCE_FIDELITY"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"P0 of the evidence-fidelity ('honest pictures') design: makes full_page_screenshot honest. When on, BasePlaywrightService.fullPageScreenshot climbs a capture ladder (document_full \u2192 cdp_full \u2192 scroll_stitch \u2192 viewport_degraded) so an inner-overflow layout (asklio line-items, where the document is viewport-height but the scrollable content lives in an inner overflow:auto container) is captured at its true extent instead of a silent viewport crop; the ONLY non-full path stamps EnvState.captureMode 'viewport_degraded' and emits the full_page_capture:degraded marker \u2014 never a silent lie. Off is byte-identical to today EXCEPT the geometry measurement + a shadow degradation marker still run (detect-only) so staging can measure the lie's live frequency before the capture behavior flips. EnvState.captureMode is set in BOTH states.",designDoc:"docs/plans/2026-07-22-verify-evidence-fidelity-design.md",status:"active",added:"2026-07-22",notes:"Default OFF in code; force ON in staging via the orchestrator env (AGENTIQA_EXPERIMENT_EVIDENCE_FIDELITY=1) \u2014 the GROUNDED_EXPECTATIONS / BLIND_DOUBLE_READ precedent. Prod flip is a later explicit step after nightly baselines + a shadow soak on the degradation marker. killSwitch resolves the no-env value from this defaultState (#1729), so GRADUATING = flip defaultState to 'on' AND add EVIDENCE_FIDELITY to INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Read sites: (P0) packages/engine-core/src/BasePlaywrightService.ts fullPageScreenshot \u2192 the honest capture ladder; (P1) packages/engine-core/src/RunnerRuntime.ts \u2192 per settled static-verify batch, capture ONE canonical honest full_page_screenshot, derive planStepIndex-stamped per-criterion crops (region resolved off the graded node; uncropped fallback), and persist a StepEvidenceRef on each TestPlanV2StepResult/CriterionResult at run_complete. Flag OFF is byte-identical to today EXCEPT the P0 shadow degradation marker: no canonical capture, no refs. P1 supersedes the run-detail findStepEvidenceIndex heuristic on runs that carry refs (legacy no-ref runs fall back). P2 (flip BLIND_DOUBLE_READ on the planStepIndex-stamped evidence P1 now produces) is the remaining follow-up gated on this.",graduation:{status:"gated",gate:"P0/P1 reproduced-RED evals green with the flag ON (runner-fullpage-honesty capture-mode/height/marker facts; degraded-abstain unit; P1 wrong-region + stamping-integrity) AND a staging shadow soak on the full_page_capture:degraded marker showing the expected live frequency with NO capture regression on batch.verify-never-blind",evidence:"full_page_capture:degraded diag events (staging shadow soak) + the engine-core capture-honesty integration test + the captureFidelity unit suite + the batch.verify-never-blind regression",owner:"steering (Alex)",review:"2026-07-29"}},PER_ACTION_BILLED_STEPS:{key:"PER_ACTION_BILLED_STEPS",envVars:["AGENTIQA_PER_ACTION_BILLED_STEPS","AGENTIQA_EXPERIMENT_PER_ACTION_BILLED_STEPS"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Decouples the billed step count from the LLM-iteration count (W1 of the batched-actions design, Decision 6). When on, the main-loop agent_step llm_usage event carries an additive billedUnits integer = executed browser actions + verify captures this turn (a marker-only signal_step turn earns 0); ingest sums it into run_billing.step_count. Off omits the field entirely, so ingest bills one step per agent_step event \u2014 byte-identical behavior AND billing to pre-change. The only intended billing delta when on (today, one-action-per-turn) is that marker-only iterations bill 0 instead of 1; a batched turn executing k actions will bill k (W3).",designDoc:"docs/plans/2026-07-18-batched-actions-design.md",status:"active",added:"2026-07-18",notes:"GRADUATED 2026-07-21 (default on): staging run_billing parity check over the force-ON window (since 07-18) held exactly \u2014 116/116 flag-ON runs with step_count == executed actions + verify captures, 99/99 marker-only iterations billed 0; billing.batched-steps-parity unit lane green. Billing substrate for the batched-actions family (W1). Read site: packages/engine-core/src/billedUnits.ts (perActionBilledStepsEnabled). Remove the staging orchestrator env var once this reaches staging."},STEP_MARKER_FOLD:{key:"STEP_MARKER_FOLD",envVars:["AGENTIQA_STEP_MARKER_FOLD","AGENTIQA_EXPERIMENT_STEP_MARKER_FOLD"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Phase 1 of the batched-actions design (Decision 3): when on, the RunnerRuntime run-mode prompt instructs the model to emit the signal_step boundary marker TOGETHER with the signaled step's FIRST action in the SAME turn \u2014 but ONLY for setup/action steps; VERIFY steps keep the separate marker turn so evidence is never captured blind. This collapses the ~18.7% of runner iterations that are marker-only today. HARD COUPLING: inert unless PER_ACTION_BILLED_STEPS is ALSO enabled \u2014 a folded turn under iteration-billing would bill 1 where marker+action should bill 2, breaking billing parity. The code guard (stepMarkerFold.ts::stepMarkerFoldEnabled = STEP_MARKER_FOLD && PER_ACTION_BILLED_STEPS) makes the fold prompt byte-identical to pre-change whenever PER_ACTION_BILLED_STEPS is off, so this flag alone changes nothing. No engine dispatch change: the multi-call loop already runs signal_step before the folded action in order (both in one generation the model saw the same screen), and billedUnits (W1) already counts marker+action as 1 billed unit.",designDoc:"docs/plans/2026-07-18-batched-actions-design.md",status:"active",added:"2026-07-18",notes:"Default OFF in code. Prompt-only change (no engine dispatch or verify-flow change). Read site: packages/engine-core/src/stepMarkerFold.ts (stepMarkerFoldEnabled), consumed in RunnerRuntime.buildRunnerPrompt run-mode signal_step cadence directive. The AND-coupling with PER_ACTION_BILLED_STEPS lives in code, not just here: flipping STEP_MARKER_FOLD=1 while PER_ACTION_BILLED_STEPS stays off is a no-op.",graduation:{status:"gated",gate:"Default-off soak on staging, then a staging flip (with PER_ACTION_BILLED_STEPS on) proves per-step verdict counts identical to OFF and marker-only iterations drop from ~18.7% to <7% (batch.step-attribution-preserved)",evidence:"batch.step-attribution-preserved unit lane + staging marker-only-iteration % soak measurement",owner:"steering (Alex)",review:"2026-07-25"}},WARNING_CRITERION_BIND:{key:"WARNING_CRITERION_BIND",envVars:["AGENTIQA_WARNING_CRITERION_BIND","AGENTIQA_EXPERIMENT_WARNING_CRITERION_BIND"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'RETIRED 2026-08-04 by the typed criterion reference. It gated the render tiers of the TEXT binder \u2014 strip the platform-appended " (warning only)" suffix, resolve {{unique}}/{{timestamp}} \u2014 so a failing warning-only grade could bind by text and keep its authored strict:false instead of being forced strict:true by the ordinal fallback, plus the C1 structural gate that decided when an ORDINAL-bound failing grade could keep an explicit strict:false. A grade now names its criterion by index (`criteriaResults[].criterionIndex`), so there is no text tier to gate and no ordinal fallback to fail closed against: a bound reference simply takes the criterion authored flag, an unbound one takes strict:true. Setting this env does nothing. Entry kept for provenance only; the switch has no read site.',designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"deprecated",added:"2026-07-16"},CRITERION_BIND_RESIDUAL:{key:"CRITERION_BIND_RESIDUAL",envVars:["AGENTIQA_CRITERION_BIND_RESIDUAL","AGENTIQA_EXPERIMENT_CRITERION_BIND_RESIDUAL"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"RETIRED 2026-08-04 by the typed criterion reference. It chose between the two-pass binder (text bindings claim their slot first, then a positional residual pass) and the sequential one whose ordinal fallback could steal a slot a later grade matched by exact text \u2014 a grade-ORDER lottery over identical observations. Both passes are gone: a grade states which criterion it graded, so no slot is ever assigned by position and the order the model lists its grades in decides nothing. Setting this env does nothing. Entry kept for provenance only; the switch has no read site.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"deprecated",added:"2026-07-28"},CRITERION_TYPED_REFERENCE:{key:"CRITERION_TYPED_REFERENCE",envVars:["AGENTIQA_CRITERION_TYPED_REFERENCE","AGENTIQA_EXPERIMENT_CRITERION_TYPED_REFERENCE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"THE ESCAPE HATCH #2238 SHIPPED WITHOUT (issue #2252). The typed criterion reference made `bindGradesByCriterionIndex` the single, unconditional grade -> criterion binder and RETIRED both of its predecessors' switches (WARNING_CRITERION_BIND, CRITERION_BIND_RESIDUAL are `deprecated` and inert), so from 2026-08-04 there was NO way to turn the class off. That matters because the binder decides more than a binding: a grade whose `criterionIndex` is missing / non-integer / out of range / step-mismatched binds to -1, which (a) leaves the pin's plan slot OUT of FIX 2's `gradedPlanSlotsByStep` union, so Amendment 5a synthesizes `{strict:true, passed:false, note:'Pinned expectation \"X\" was never graded - treated as unconfirmed.'}` and FAILS the step, (b) forces `strict:true` on the grade regardless of the criterion's authored flag, and (c) makes the whole `run_complete` bounce once. PIN_SUBSTANTIATION is default-ON in prod, so at low reference compliance that is a manufactured false FAIL for every customer running pinned verify plans. Nothing in the stack can force compliance - zod does not gate this path (ai-sdk swallows InvalidToolInputError) and Gemini treats `required` as advisory - and the 99.15% figure on record (1055/1064) is 97.3% three CI plans replayed in one window with zero customer plans, i.e. replicates, not a real-traffic measurement. ON (the default, and byte-identical to every build since 2026-08-04): the typed reference is the ONLY binder; an unusable reference binds to nothing and the bounce asks for a correction. OFF (explicit `=0` under either spelling): unbound grades get ONE recovery pass inside the same binder - each still-unbound grade's echoed `check` text is matched against the step's UNCLAIMED authored criteria via `renderedCheckMatchesCriterion` (the four render tiers: raw exact, ' (warning only)' suffix-stripped, {{unique}}/{{timestamp}}-resolved, and both), first match in grade order claims the slot. Because it is inside the ONE binder, every consumer moves together and none can drift: the FIX 2 coverage union stops synthesizing a never-graded failure for a pin that WAS graded, the grade takes its criterion's authored `strict` flag instead of the fail-closed `true`, containment no longer sees it as unauthored, and the bounce no longer fires on it. The recovery is deliberately weaker than the retired text binder - no positional/ordinal residual, and no cross-step re-homing: a grade that DECLARED a step other than the entry's is refused by the recovery pass exactly as the typed branch refuses it, so it can only rebind a grade that ECHOES a criterion the plan really printed FOR THAT STEP. OFF IS NOT MERELY 'REMOVES A SYNTHESIZED FAILURE' (corrected in round-2 review, 2026-09-02). Recovery ENLARGES the entry's bound-slot set, which is the input to `evaluateCriteriaContainmentEligibility`; an entry that ON refuses containment for (`authored-slot-ungraded`) can therefore flip to `applies` under OFF, and every OTHER still-unbound grade of that entry - including one the model itself stated `passed:false` - is then EXCLUDED from the step verdict rather than failing it. The direction is still un-fail-ward in both mechanisms, but the second one drops a MODEL-STATED failure, not a synthesized one. By the same flip, a fabrication is NOT 'contained in both states': under ON with an authored slot left ungraded containment REFUSES and the fabrication is verdict-bearing; under OFF, once recovery fills that slot, containment applies and the fabrication is contained. It stays UNBOUND in both states - recovery never binds text the plan did not print - but whether it reaches the verdict differs. VERDICT-CHANGING IN THE OFF DIRECTION ONLY (docs/VERDICT-GATES.md).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-09-02",notes:"Read site: packages/engine-core/src/RunnerRuntime.ts (`bindGradesByCriterionIndex`, consulted only when the caller supplies the optional `textRecovery` argument - the run_complete pre-pass does; the exported helper called without it is unchanged for every other caller, including the L3 eval e2e/evals/plans/verdict-bounce-unauthored-grades.ts). Default ON, so this PR is a NO-OP on every verdict; the switch exists so an operator can answer a low-compliance incident with an env flip instead of a revert. Pinned in BOTH states by packages/engine-core/src/__tests__/RunnerRuntime.criterionTypedReferenceKillSwitch.test.ts."},LOGIN_IDENTITY_DERIVATION_GUARD:{key:"LOGIN_IDENTITY_DERIVATION_GUARD",envVars:["AGENTIQA_LOGIN_IDENTITY_DERIVATION_GUARD","AGENTIQA_EXPERIMENT_LOGIN_IDENTITY_DERIVATION_GUARD"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"The two corrections #2245 needed before its behaviour could be trusted in a release (issue #2252), behind one switch so an operator can restore the exact 2026-08-04 derivation without a redeploy. (1) SECRETS HANDLING - `pushIdentity` probed `looksLikeSecretValue(normalizeEmail(value))`, i.e. a TRIMMED AND LOWERCASED string, which makes the high-entropy tier (it requires an upper-case character) UNREACHABLE and the `AKIA[0-9A-Z]{16}` and `-----BEGIN ... PRIVATE KEY-----` patterns unmatchable. Plaintext passwords, AWS access-key ids and PEM private keys therefore entered `loginCredentialIdentities`, which is compared against typed field values and can reach the profile ATOM. ON probes the RAW trimmed value; `isEmailLike` still reads the normalized form, because address comparison is case-insensitive by design and only the secret shape is case-bearing. (2) IDENTITY-LABEL NARROWING - a credential that already yielded its real `login` ALSO pushed its SECRET as an identity whenever its `name` read as an identity-field label (`username`, `login`, `email`), which is exactly the pooled-row shape (account in `login`, role-ish name beside it). ON gates that push on `!hasLogin`; the row keeps contributing its `login`, so no project loses an identity. The back-compat email-like-secret clause is gated on the EFFECTIVE branch, not on the raw `isIdentityLabel`: keying it to the raw test (the first cut, caught in review) made ON DROP an email-like secret outright on an `isIdentityLabel && hasLogin` row, since the branch refused it and the clause skipped it as already handled. That clause only ever pushes email-like values and `pushIdentity` exempts those from the secret-shape denylist by construction, so routing them through it re-opens no leak. Both directions are NARROWING (fewer values in the identity set), which is why the switch matters: `credentialsProvideLoginIdentity` is this list being non-empty, and it decides whether the testing-email canonicalizer overwrites a typed login, whether the third-party-IdP terminal fires, and whether the credential-fabrication guard rejects a `report_issue`. Neither correction can empty a set that was non-empty for a legitimate reason - (1) only drops values that are secret-SHAPED and not email-like, (2) only drops a value from a row that supplied a `login` which is itself pushed - but the switch exists so that claim is falsifiable in production rather than merely argued. OFF restores the byte-identical 2026-08-04 derivation.",designDoc:"packages/engine-core/src/runner/credentialUtils.ts",status:"active",added:"2026-09-02",notes:"Read site: packages/engine-core/src/runner/credentialUtils.ts (`loginIdentityDerivationGuardEnabled`, consulted by `pushIdentity` and by `deriveCredentialIdentityRecords`). Pinned in BOTH states by packages/engine-core/src/__tests__/credentialUtils.loginIdentityDerivationGuard.test.ts; the pre-existing credentialUtils.loginIdentityRule suite pins that no legitimate identity shape moved."},CRITERIA_CONTAINMENT:{key:"CRITERIA_CONTAINMENT",envVars:["AGENTIQA_CRITERIA_CONTAINMENT","AGENTIQA_EXPERIMENT_CRITERIA_CONTAINMENT"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"GRADUATED 2026-08-03 (default ON) \u2014 flipped OFF\u2192ON because the class it contains is LIVE ON PROD and is what the verdict-replay gate is red on. Closed-world containment on run_complete: a step's verdict is decided ONLY by criteria the PLAN authored. A graded criterion result that binds to NO authored slot of its step (the grading model invented it) is still persisted \u2014 marked `unauthored: true` for forensics \u2014 but `deriveStepStatusFromCriteria` skips it, so a fabricated strict failure can no longer decide a step. Live prod-shaped defect (canary tp_vrgate_offer_upload_retry step 13, ~2/9 replicates): the step authors exactly 2 criteria (Description \"Montage und Einweisung\", Unit \"St\xFCck\"), both graded PASS with correct grounding, and the model additionally graded a THIRD criterion \u2014 `Order line 2 Quantity is \"St\xFCck\"`, strict:true, observed \"1\" \u2014 that exists in neither the plan nor the DB; that invented failure hard-failed the run. Detection ALWAYS runs, in BOTH states: at the default the engine excludes and logs `criteria_containment:excluded` {stepIndex, criterionText, strict, observed, wouldFlipStep, authoredCriteria, gradedCriteria}; with an explicit `=0` under either spelling every verdict is byte-identical to pre-graduation staging and the engine logs `criteria_containment:would_exclude` with the SAME payload \u2014 so an operator who kills the class keeps the measurement. `wouldFlipStep` was the graduation evidence and remains the incidence metric \u2014 it is true only where the exclusion actually moves the step status, so a fabricated PASSING grade stays pure telemetry in either state. THREE DELIBERATE NARROWINGS keep this rescue-polarity gate from opening a false-PASS channel: (1) it fires only on a step that AUTHORED \u22651 criterion \u2014 grades filed under a criteria-less action/setup step are the cross-step MISFILE class, where today's fail-closed treatment of the unbound grade is the only adjudication that failure gets; and (2) only when EVERY authored criterion of that entry received a grade, so an unbound grade can never be excluded while an authored slot went unadjudicated (it would then plausibly BE that slot's grade under a heavy rephrase). NARROWING (2b) \u2014 TEXT-JUSTIFIED COVERAGE, added 2026-08-03 after adversarial verification of PR #2153 found a LAUNDERING vector: (2) counted a slot as graded no matter HOW it was covered, and `bindGradesToPlanSlots` PASS 2 assigns leftover slots in GRADE ORDER, so a FABRICATED grade emitted BEFORE a rephrased real one takes that real criterion's slot, coverage reads complete, and the grade left unbound (and excluded) is the REAL one. Reproduced by execution on head 2dc7691f4: criteria [Description strict, Unit strict] + grades [fabricated \"Order line 2 Quantity looks plausible\" PASS, Description exact PASS, Unit REPHRASED FAIL observed \"kg\"] booked the run `passed` at the graduated default versus `failed` at `=0`, on 7 of 8 realistic rephrase forms (reworded, units added, whitespace, case, smart quotes, DE/EN, truncated; only the platform's own \" (warning only)\" suffix still bound by text), with the diag itself reporting wouldFlipStep:true. The replay gate could NOT see it \u2014 the exclusion consumes exactly one unit of surplus, so (C1) 2<=2, (C2) overreach 0 and (B) were all green and buildReport returned ok:true on a laundered run. `evaluateCriteriaContainmentEligibility` therefore refuses exclusion whenever ANY authored slot was covered by PASS 2's positional residual rather than a PASS 1 `matchedByText` binding: a positional binding is ordinal-only evidence (the C1 strict-honoring gate already refuses to trust it for the strict flag, and containment is strictly stronger since it DELETES the grade). The refusal is entry-wide rather than per-slot \u2014 an honest reorder is indistinguishable from slot theft without reading grade text \u2014 so both keep the fail-closed treatment, and the engine logs `criteria_containment:refused` {stepIndex, reason:'positional-only-coverage', criterionText, strict, observed, wouldFlipStep, positionalSlots, authoredCriteria, gradedCriteria} in BOTH flag states, where wouldFlipStep:true means laundering was PREVENTED. ACCEPTED COST, pinned by test: a genuine fabrication sitting beside an honest positional binding is no longer contained \u2014 a false FAIL, never a false PASS. Both frozen incident runs bind BOTH authored slots by exact text, so the graduation class is untouched (asserted end-to-end through handleToolCall and through the replay gate's own buildReport: `=0` -> ok:false with plan_verdict_regression x2 + criteria_over_grading x2; default -> ok:true, engineContainedResults 2, containmentOverreachSteps 0). Identification reuses the ONE authoritative binder \u2014 `bindGradesToPlanSlots` per-entry output (text tiers incl. the platform's \" (warning only)\" rewrite, then residual positional) \u2014 never a new text comparison: measured over 76 persisted corpus runs, 58 of 60 text-unbound results were exactly that legal rewrite, so a text-equality containment rule would be ~97% false positives. A THIRD narrowing closes the DUPLICATE-READ false-PASS channel (review round 2026-07-29): the binder claims slots EXCLUSIVELY, so a SECOND grade of the SAME authored criterion binds to nothing and unguarded containment would discard it \u2014 and when the two reads contradict ([passed:true, then passed:false observed \"\u20AC35.00\"] for one authored total) the discarded one is the FAILING one, passing the step on a record whose \"unauthored\" criterionText is byte-identical to the authored check. Before excluding, `findDuplicateReadCriterionIdx` re-runs the binder's own text tiers over ALL authored criteria with the claimed set IGNORED; a match means duplicate READ, containment REFUSES to exclude, the grade keeps its flag-OFF effect (the step can still fail \u2014 the correct polarity for a contradicting observation), and the engine logs `criteria_containment:duplicate_read` {stepIndex, criterionIndex, criterionText, passed, wouldHaveExcluded} in BOTH flag states. RESIDUAL: identification is a TEXT relation, so a REPHRASED contradicting second read (\"Der Gesamtbetrag lautet \u2026\") matches no tier and is still excluded under the flag \u2014 the shadow soak's would_exclude population must be reviewed for that shape before graduation. That residual is the DUPLICATE shape (one authored criterion graded TWICE, the paraphrased second read dropped) and is DISTINCT from the positional-slot-theft class narrowing (2b) closes, where the authored criterion is graded exactly ONCE and that single grade is discarded. GATE HALF (detector, no verdict effect): `criteria_over_grading` in e2e/scripts/verdict-replay-gate.mjs, PR #1934, split in two at this graduation because the gate reads a PERSISTED array in which the excluded result is still present. (C1) counts only the results that still DECIDE \u2014 `criteriaResults.filter((cr) => cr.unauthored !== true).length <= step.criteria.length` \u2014 so a contained fabrication is no longer a violation while an UNMARKED surplus is the pre-graduation violation verbatim (identical on every legacy run and under `=0`, where nothing is marked). (C2) `criteria_containment_overreach` polices the OPPOSITE direction: the engine may exclude at most `persisted - authored`, so marking more than the surplus \u2014 containment removing a criterion the plan really authored \u2014 is a hard gate failure. The gate deliberately CONSUMES this mark rather than re-deriving a binding of its own: a second, independently-drifting binder in the gate is exactly how a runtime fix and its gate come to disagree, and it is (C2) that makes trusting the mark safe, since the mark is only ever believed up to a cardinality the gate verifies for itself. Equivalence is pinned by e2e/scripts/criteria-containment-gate-equivalence.test.ts (the gate's counted set === the set `deriveStepStatusFromCriteria` reduces over, asserted on the two frozen prod-shaped runs). NO RESCUE \u2014 THE ENGINE BOUNCES INSTEAD (2026-08-04). The filter alone is STRUCTURALLY INERT on the production shape: `deriveStepStatusIgnoringAuthorship` returns a reported `failed` VERBATIM before it looks at any criterion, so when the model stamps the step `failed` BECAUSE of the criterion it invented, `derive(failed, all) === derive(failed, contained) === failed` and the exclusion moves nothing. Live proof \u2014 staging replicate run_d5b6f130-734f-4f57-8067-d43aa617c7ae, plan tp_vrgate_offer_upload_retry step 13: 2 authored criteria both PASS with grounding, plus a third graded `Order line 2 Quantity is 4.2` (strict, observed 1, NO groundingObservation) which is step 12's line-1 quantity copied across lines; the target is a STATIC self-testing fixture (apps/self-testing/app/lio-mirror/offer-upload-retry/page.tsx hardcodes Quantity 1) and the sibling replicate run_eccf41c3-9e09-4493-9e25-d6ae44b93c60 graded all 14 steps identically and PASSED, so there is zero app non-determinism. THREE SUCCESSIVE DESIGNS TRIED TO RESCUE THAT VERDICT and adversarial verification defeated each: (1) co-occurrence read as attribution \u2014 a failing unauthored grade merely CO-EXISTING with the step's failure rescued it, laundering 8/8 genuine non-criterion failures (timeout, error banner, HTTP 500, thrown exception, blocked navigation, disabled control, credential rejection, unobservable browser); (2) an ALLOWLIST of recognized genuine-failure signals \u2014 everything unenumerated was invisible by construction; (3) a positive execution attestation PLUS an attribution requirement \u2014 which fell too, because the attribution absorbed the excluded grade's own note/observed while BOTH that note and the step note are written by the same model in the same run_complete call, so one coherent narration made its own cause 'accounted for' (proven end-to-end: a genuine HTTP 500 with a fabricated grade whose note echoed the 500 booked passed/passed, and one coherent narration made all 8 laundering fixtures attributable). The lesson is structural: EVIDENCE THE MODEL AUTHORED CANNOT VALIDATE THE MODEL'S OWN REPORT, and every rescue reduced to trusting some model-written field. So the engine no longer rewrites a verdict at all. Instead `run_complete:unauthored_criterion_grades_rejected` BOUNCES the malformed call ONCE \u2014 naming the offending step indexes and the exact fabricated criterion texts, and instructing the model to resubmit grading ONLY the criteria the plan authored \u2014 mirroring the shipped `run_complete:incomplete_step_results_rejected` bounce (#2207). Bounded to one rejection per run (`_unauthoredGradeRunCompleteRejections`, reset in startRun beside its sibling counter) and placed immediately after the grade map, BEFORE every cascade adjudicator, so a bounce discards no gate's work. On the resubmission \u2014 or if the model repeats the fabrication and is not bounced again (`run_complete:unauthored_criterion_grades_repeated`) \u2014 the unauthored grades stay excluded from the derivation and marked, and WHATEVER THE MODEL BOOKS STANDS: a `failed` is never turned into a `passed`, at the step level or the run level. The bounce fires on ANY unauthored-and-unrelated grade regardless of its `passed` value, deliberately: whether a fabrication 'matters' is itself a model-written field, and reading one to decide is the exact error the three rescues each made in a different disguise. FORGERY GUARD (kept from the rework, now used to avoid a WRONG bounce): `excludedGradeRelatesToAuthoredCriterion` relates an excluded grade to an authored criterion over four deterministic tiers \u2014 the binder's own tiers (`findDuplicateReadCriterionIdx`), COSMETIC equality (`canonicalizeCheckTextForRelatedness`: NFKC, NBSP/space family, curly/angle/prime quotes, dash family, case, terminal punctuation, currency marks, locale number separators so 1.234,50 equals 1234.50), TRUNCATION (prefix either direction, floored at 12 canonical chars), and SUBJECT-TERM identity (`significantSubjectTokens`, shared with the note-contradiction lane). A related grade is a REPHRASED second read of a criterion the plan really does define \u2014 `findDuplicateReadCriterionIdx`'s documented residual \u2014 so the model is NOT asked to drop it, and it keeps its flag-OFF effect of failing the step. Reported on the containment diag as `relatesToAuthoredCriterion`. DIAGNOSTIC SEAM (verdict-neutral, kept on its own merits): `BaseRuntime.executeAction` calls `onBrowserActionError`, whose base implementation is an empty method RunnerRuntime never overrode, so an errored click_at / type_text_at / navigate left NO step-attributable trace anywhere in the codebase. `_stepActionErrors` now records both error shapes \u2014 the generic `status:'error'` via the overridden hook and the informative `status:'ok'` carrying `error`/`valueMismatch:true` at the dispatch seam \u2014 bounded per step, emitted as `step_action_error` and surfaced as a per-step count on the containment diag. NOTHING READS IT TO DECIDE A STATUS. GATE HALF (C3) `criteria_containment_inert`: C1 stops counting a surplus result the moment it is marked, so a MARKED-but-still-deciding fabrication went silent \u2014 gate run 30889770637 printed 'steps that graded more criteria than authored: 0' beside engineContainedResults: 1 on a run that booked failed on a criterion nobody wrote, and that false zero was read as proof the class was fixed. C3 reds when some excluded result FAILED, every counted result PASSED, and the step still books `failed`; `strictInvariant.containmentInertSteps` is printed on the same summary line as the over-graded count so the two can never be read apart again. C3 is now the whole visibility mechanism rather than a backstop to a rescue, and it fires on the incident shape by design.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-29",notes:"2026-10-01 (#3292): the same switch now gates exact-authored-criteria correction and final invalid-report classification. One correction at most; a known counted/action failure is finalized without a replacement report. Repeated malformed reports become non-billable verification errors unless independent failures or an existing concluding blocked terminal take precedence. No failed step is promoted to passed. See docs/VERDICT-GATES.md, run_complete:invalid_criterion_report. Historical graduation record follows. GRADUATED 2026-08-03 (default ON), ahead of the 2026-08-12 review date, because the containment class is NOT a hypothetical: it is red on release PR #2149 AND on the `main` nightly (GH run 30781168843, build 0555cc09a = the build prod is serving), so the pre-graduation default is what is booking false `failed` verdicts on prod TODAY. killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND CRITERIA_CONTAINMENT listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire); an explicit AGENTIQA_CRITERIA_CONTAINMENT=0 (or the AGENTIQA_EXPERIMENT_ spelling) restores the byte-identical pre-graduation SHADOW \u2014 detection still runs, `criteria_containment:would_exclude` still fires, and NO verdict moves. STAGING-FIRST: merging to staging makes this default-ON on the STAGING engine only; PROD stays OFF until a later staging->main release carries it (a natural staged soak). Rides on the binder flags: with WARNING_CRITERION_BIND=0 or CRITERION_BIND_RESIDUAL=0 the binder is the sequential one, which leaves grades unbound in shapes that are NOT surplus \u2014 narrowing (2) is what keeps containment inert there rather than excluding a legitimately-graded criterion. Runner lane only (RunnerRuntime run_complete). Scope is step-status derivation: the rescue-eligibility gates that read `criteriaResults.every(passed)` (absence oracle, VERIFY_REOBSERVE_WITHHOLD, verify-conflict reconcile) still see the full persisted array, so a fabricated failing result can still BLOCK a rescue \u2014 the fail-closed direction, left deliberately. OPEN RESIDUAL CARRIED THROUGH THE FLIP, stated rather than hidden: the REPHRASED-DUPLICATE shape (a paraphrased second read of an authored criterion) matches no text tier, is therefore indistinguishable from a fabrication, and IS excluded at the default \u2014 a false-PASS channel the duplicate-read guard does not close and which the new (C2) gate check does NOT catch either (a rephrased duplicate consumes exactly one unit of surplus, so the exclusion is within budget). The pre-flip gate asked for a grade-by-grade shadow-soak review of that population; no such soak was run before this flip, which is the one piece of the original graduation bar that is being WAIVED rather than met, and it is why this flip is Alex's call and not a mechanical one.",graduation:{status:"gated",gate:'GRADUATED 2026-08-03 (default ON) with ONE part of this bar explicitly waived \u2014 kept as `gated` rather than deleted because the waived part is a real open false-PASS channel that still needs closing. MET: (1) the fabrication class reproduces on the canary at the documented location \u2014 tp_vrgate_offer_upload_retry step 13 authors exactly 2 criteria and the model graded 3, frozen from TWO real staging runs (run_11eeceb1-3bfc-4810-a17f-e7c23170c12e, run_81ef70ae-a3f9-4673-ba52-95378ca477d3, both 2026-08-03, both booking `failed` against a `passed` baseline on the invented `Order line 2 Quantity is "2"`, strict:true, passed:false, observed "1") into packages/engine-core/src/__tests__/fixtures/criteriaContainment.overGradedRuns.json; (2) the #1934 `criteria_over_grading` detector agrees on exactly those steps \u2014 it is what turned the gate red \u2014 and its 76-run corpus measurement recorded 2 true hits / 0 false positives, both this same defect; (3) the detector now also polices the exclusion in the opposite direction (`criteria_containment_overreach`), so over-exclusion beyond the surplus is a permanent CI check rather than a one-off review. WAIVED: the grade-by-grade shadow-soak review of the `would_exclude` population for the REPHRASED-DUPLICATE shape (a paraphrased second read of an authored criterion, which the text tiers cannot distinguish from a fabrication and which the duplicate-read guard does NOT catch). No soak was run; the flip was driven by the class being live on prod and blocking a release. That residual is the remaining work behind this entry.',evidence:"engine `criteria_containment:excluded` / `:would_exclude` / `:duplicate_read` diag events + the RunnerRuntime.criteriaContainment unit suite incl. the two frozen prod-shaped runs (claim verify.containment.authored-criteria-only) + the replay-gate `criteria_over_grading` / `criteria_containment_overreach` checks + e2e/scripts/criteria-containment-gate-equivalence.test.ts (runtime filter === gate filter)",owner:"steering (Alex)",review:"2026-08-12"}},PLAN_OBEDIENCE:{key:"PLAN_OBEDIENCE",envVars:["AGENTIQA_PLAN_OBEDIENCE","AGENTIQA_EXPERIMENT_PLAN_OBEDIENCE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Enforces the active plan step: rejects off-plan label clicks, records deviations, and lets run_complete fail on them; off makes the pre-check, deviation recording, and verdict gate all inert (pre-#1294 behavior).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-07"},LOOP_VISION_ESCALATION:{key:"LOOP_VISION_ESCALATION",envVars:["AGENTIQA_LOOP_VISION_ESCALATION","AGENTIQA_EXPERIMENT_LOOP_VISION_ESCALATION"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Master gate (Runner/Explorer lanes) for consulting the vision supervisor once before an ambiguous screenshot-blind force-block terminates the run; off keeps the deterministic hard-block.",designDoc:"docs/plans/2026-07-07-loop-detection-vision-supervisor-design.md",status:"active",added:"2026-07-07"},LOOP_VISION_DIFFERENTIAL:{key:"LOOP_VISION_DIFFERENTIAL",envVars:["AGENTIQA_LOOP_VISION_DIFFERENTIAL","AGENTIQA_EXPERIMENT_LOOP_VISION_DIFFERENTIAL"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Phase-2 differential-grant refinement of loop-vision escalation: grants 2..K need a concrete task-unit delta and the per-step ceiling rises to 12; off reverts to Phase-1 (absolute judgment, 4-grant ceiling).",designDoc:"docs/plans/2026-07-09-loop-vision-differential-extension-design.md",status:"active",added:"2026-07-09"},CANVAS_PIXEL_PROGRESS:{key:"CANVAS_PIXEL_PROGRESS",envVars:["AGENTIQA_CANVAS_PIXEL_PROGRESS","AGENTIQA_EXPERIMENT_CANVAS_PIXEL_PROGRESS"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Canvas-aware loop progress: on a canvas-dominant screen (a <canvas> covers >=40% of the viewport) the LoopDetector novel_screen milestone is driven by a coarse screenshot pixel-diff hash (own NOVEL_SCREEN_BUDGET) instead of the static DOM/a11y hash, and the loop-vision differential delta gate accepts a qualitative delta_evidence string when countable task units are unavailable. Off drops both so a canvas build behaves exactly as before (structural breaker climbs on the static DOM hash, delta gate stays countable-only).",designDoc:"docs/plans/2026-07-14-canvas-chat-failure-class-design.md",status:"active",added:"2026-07-14"},RUNNER_SAME_VALUE_WRITE_BREAKER:{key:"RUNNER_SAME_VALUE_WRITE_BREAKER",envVars:["AGENTIQA_RUNNER_SAME_VALUE_WRITE_BREAKER","AGENTIQA_EXPERIMENT_RUNNER_SAME_VALUE_WRITE_BREAKER"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Runner-lane same-value write breaker (#2612 / #2986 / #3006). THE DEFECT: on a plan step carrying two values the model keeps re-sending STALE args while `intent` narrates the value it meant to write \u2014 in one staging run all 11 `type_text_at` calls returned success, 10 targeted the same label, and every one carried the EMAIL as `args.text` while `intent` said it was setting the contact name. VERBATIM_INPUT_PIN is inert on customer-authored plans (no `verbatimInput` slot), and three supervisor REDIRECTs naming the error did not break the loop. WHAT =0 DISABLES: the pre-dispatch bounce only. On the 3rd CONSECUTIVE identical (normalized target, written value) write inside one declared step, with no observed field-value change in between, the write is refused as a meta result (never dispatched to the browser, and deliberately NOT a resetLoopDetector site) with an error naming the field, the repeat count, and the alternative entry paths (focus + insertText, select-all-then-type, native fill). It bounces exactly ONCE per (step, target, value) triple so a stubborn re-emitter cannot wedge the run in a bounce loop. EXEMPTIONS, so a legitimate form fill is never touched: two different fields alternating are different targets; the same field with a changed value is a different key; and an OBSERVED field-value change between two identical writes (a clear that worked, an append) resets the counter \u2014 `clearBeforeTyping` is deliberately NOT that signal, since it defaults to true on every type_text_at and reading it as a reset would make the breaker inert on exactly the traffic it exists for. ADDRESSING IS NOT IDENTITY: `set_focused_input_value` has no targeting arg at all (it writes into whatever is focused) and a coordinate-only `type_text_at` is bucketed to 50 units, so several real fields collapse onto one normalized target \u2014 three selects each focused by a click and set to the same option value in one step is ordinary. Such an AMBIGUOUS target therefore never bounces until the engine\'s own `typedIntoField` (the accessible name of the field that received the input) has confirmed the repeats reached one and the same field, and any change in that name resets the counter on every target, explicit or not. UNCONDITIONAL COMPANIONS, not gated by this flag: (a) LoopDetector.buildKey now keys write-class actions on normalized target + a short hash of the written value, so a same-field/same-value repeat is a real repeat and `set_focused_input_value` stops collapsing every focused-field write onto one key \u2014 it changes loop-block timing in every lane whatever this flag is set to, and carries its own docs/VERDICT-GATES.md entry; (b) a SHADOW-ONLY diagnostic `type_text_at:intent_literal_mismatch {stepIndex, quoted, textHash}` when `args.intent` quotes a literal that the written value contradicts \u2014 diag only, no bounce, pending a false-positive census (the `Set the "Name" field to John` shape, where the quoted token is the FIELD, is a known false positive). Diagnostic for the breaker itself: `type_text_at:same_value_repeat {target, count, stepIndex}`. Acceptance suite: packages/engine-core/src/__tests__/sameValueWrite.test.ts, RunnerRuntime.sameValueWrite.test.ts, LoopDetector.test.ts.',designDoc:"packages/engine-core/src/sameValueWrite.ts",status:"active",added:"2026-09-08"},RUNNER_RUNJS_HUNT_BREAKER:{key:"RUNNER_RUNJS_HUNT_BREAKER",envVars:["AGENTIQA_RUNNER_RUNJS_HUNT_BREAKER","AGENTIQA_EXPERIMENT_RUNNER_RUNJS_HUNT_BREAKER"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Runner-lane read-only scripting hunt breaker (#2377). THE DEFECT: on a step whose payload the runtime supplies (a `verbatimInput` pin the prompt never mentioned) the model reads "enter the requested prompt" as "the text is somewhere I have not looked" and scripts the DOM to find it \u2014 two measured blocked runs of runner/verbatim-payload show five consecutive run_js calls, ZERO screenshots, ZERO writes, then a generic loop_block with no verdict. In one run every script THREW (selector miss \u2192 TypeError); in the other all five returned `status: success` because the model\'s own `|| "No alert text found"` fallbacks masked the misses, so a status-only predicate would have caught neither reliably \u2014 what is invariant is the STREAK. None of the existing nets fire: the steering veto fingerprints name+args and a scripting tool\'s arg space is unbounded (five scripts = five fingerprints), the same-value write breaker is gated on write-class tools and these runs contain no writes, and the LoopDetector fires late and generically after awarding novel-screen milestones to the first calls. WHAT =0 DISABLES: the pre-dispatch bounce only. On the 3rd CONSECUTIVE run_js inside one declared step the call is refused before dispatch with `{status: "error", runJsHuntRefused: true, consecutiveRunJs}` and an instruction to grade the criterion from what is VISIBLE \u2014 take a screenshot or snapshot and grade honestly, including an honest fail. ACCELERATED to the 2nd call when every preceding run_js in the streak returned NO SIGNAL (top-level `status: "error"`, a `runJsResultStatus` of error/empty/undefined_return, or a serialized `runJsResult.value` of ""/null/undefined/[]/{}) \u2014 nothing is being learned, so a third identical outcome only spends iterations. RESETS on any non-run_js browser action (a screenshot, a snapshot, a click, a write \u2014 the agent looked at or acted on the page) and on a step change. Bounces exactly ONCE per declared step so a stubborn hunter is let through rather than wedged in a bounce loop. DELIBERATELY NOT AN AMNESTY: the refusal carries no `resetLoopDetector` and is NOT marked `isMetaTool`, so BaseRuntime still runs the post-dispatch LoopDetector check and still records the fingerprint for the steering veto \u2014 a breaker that re-armed the machinery it fired inside would be self-defeating, and this must not become a fifth loop-safety amnesty sibling. Diagnostic: `run_js:hunt_breaker {stepIndex, count, accelerated}`. UNCONDITIONAL COMPANIONS, not gated by this flag: (a) the step prompt now renders the EXISTENCE of a `verbatimInput` pin ("the exact text to enter is supplied by the runtime and will be typed verbatim \u2014 do NOT search the page for it") and never its bytes, removing the reason for the hunt in the whole pinned class (gated by VERBATIM_INPUT_PIN, whose contract it belongs to); (b) `steeringVeto.projectFingerprintArgs` drops the script body for SCRIPT tools (run_js) so the veto fingerprint is name-only and a supervisor REDIRECT naming the tool refuses the next, differently-worded script; (c) a SHADOW-ONLY diagnostic `verify_evidence:runjs_empty_would_withhold {stepIndex}` when a SUCCESSFUL run_js with an empty/trivial value is credited as verify evidence \u2014 diag only, the evidence is still credited and still clears a prior wait-oracle failure exactly as before. Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.runJsHuntBreaker.test.ts, steeringVeto.test.ts, RunnerRuntime.verbatimInput.test.ts.',designDoc:"packages/engine-core/src/runJsHunt.ts",status:"active",added:"2026-09-09"},LOOP_URL_NOVELTY_REARM:{key:"LOOP_URL_NOVELTY_REARM",envVars:["AGENTIQA_LOOP_URL_NOVELTY_REARM","AGENTIQA_EXPERIMENT_LOOP_URL_NOVELTY_REARM"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Re-arms the structural loop breaker in the chat/explorer (assistant_v2) lane. URL-ONLY (narrow-safe): only the novel-URL seen-set, its NOVEL_URL_BUDGET=40/turn budget, and actionsSinceProgress become TURN-scoped (resetForNewStep no longer wipes them), so a recycled URL revisited across many declared steps stops re-counting as novel and actionsSinceProgress climbs to the 30-action structural threshold. The novel-REF and novel-SCREEN budgets stay PER-DECLARED-STEP in both states \u2014 turn-scoping them was reverted because it starved a legit long single-stable-URL SPA turn producing genuinely-new screen content each action (force-block ~action 53). A genuinely-new URL each step still resets (multi-page wizards unaffected); a wander that also mints novel screens escapes this deterministic re-arm and the run backstop is the net. Off restores the per-step reset (recycled URLs re-count as novel forever, unbudgeted novel_url) \u2014 today's prod behavior. Runner (test_run) lane never opts in.",designDoc:"docs/plans/2026-07-20-loop-safety-rearm-and-backstop-design.md",status:"active",added:"2026-07-20"},RUNNER_SIGNAL_STEP_FORWARD_RESET:{key:"RUNNER_SIGNAL_STEP_FORWARD_RESET",envVars:["AGENTIQA_RUNNER_SIGNAL_STEP_FORWARD_RESET","AGENTIQA_EXPERIMENT_RUNNER_SIGNAL_STEP_FORWARD_RESET"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Runner-lane (test_run) loop safety: `RunnerRuntime.handleSignalStep` returns `resetLoopDetector: true` ONLY on a genuine FORWARD activation \u2014 the activated index names a real plan step (it was NOT clamped into `[1, steps.length]`) AND is STRICTLY GREATER than the monotone high-water mark read at call time. THE DEFECT: the flag was returned unconditionally, and BaseRuntime's dispatch loop treats it as a full amnesty (LoopDetector.resetForNewStep() \u2014 every consecutive / no-progress / cumulative counter AND the seenRefs / seenScreenContentHashes / seenPixelHashes / seenUrls novelty sets \u2014 plus tapRetryTracker, steeringVeto, sameGoalAbort and the loop-vision extension budget). `signal_step` is a filler meta-tool the MODEL emits freely, including a re-signal of the step it is already on and a BACKWARD re-bind to an already-graded step, both prod-observed and both permitted by design; so a runner grinding one form re-armed every structural counter from zero on every generation and survived to iteration 136 / 139 actions (#3019), 67 actions (#2485) and 22 actions (#2577) with no deterministic breaker ever arming. The two lane-agnostic floors that would otherwise have caught it (RUN_PROGRESS_BACKSTOP, LOOP_URL_NOVELTY_REARM) are hard-gated to session.kind === 'assistant_v2', so the runner lane ran with a purely step-scoped, model-defeatable net. WHAT ON CHANGES: a same-step re-signal, a backward re-bind and an out-of-range index return the byte-identical result WITHOUT the reset flag, and emit a structured `signal_step:reset_withheld {stepIdx, maxStepEverActivated, maxInRangeStepActivated, direction, path}` diag for the nightly census. THE GATE READS A SECOND MARK, `_maxInRangeStepEverActivated`, not `_maxStepEverActivated`: the handler clamps a prod-observed out-of-range index to the LAST plan step and then activates it, which must keep raising the temporal-snapshot mark but would otherwise let ONE malformed index pin the gate at the plan length and withhold the reset at every remaining step boundary of the run. BOTH of the handler's returns are gated \u2014 the ordinary activation and the unverified-verify DEFLECTION (findUnverifiedVerifyStepBefore \u2192 buildVerifyEvidenceRequiredResult), which carries the same unconditional amnesty while binding BACKWARD to an already-activated verify step; a deflection to a verify step the run never reached is forward and keeps the flag. Every other effect of signal_step is untouched \u2014 `_currentStepIndex` re-binding (backward included), verify-pin scoping (resetVerbatimPinTracking / noteStepActivated), which step the deflection bounces to and its rejection payload, and the step-marker-fold latch all behave exactly as before, so no step, criterion or run status is re-graded by this flag. It only changes WHETHER an existing deterministic breaker can arm. OFF (`=0` under either spelling) restores today's unconditional reset with no other code path touched. NON-GOALS, deliberately out of scope: un-gating the lane-agnostic floors for the runner lane, a durable supervisor-redirect channel, a step anti-regression / bounded-replay guard, and splitting BaseRuntime's composite reset so sameGoalAbort / steeringVeto survive independently. NAMED RESIDUAL: four SIBLING RunnerRuntime returns carried an unconditional resetLoopDetector \u2014 maybeBlockActionUntilVerifyEvidence, the retained-verify-snapshot reuse, the run_complete verification_conflict bounce and buildMissingEmailVerificationRejection; the first two fire once per model action, so a run parked on an unverified verify step still re-arms every breaker per action. THREE REMAIN: maybeBlockActionUntilVerifyEvidence dropped the flag on 2026-09-09 under VERIFY_EVIDENCE_GATE_STANDOFF (#2599), which also bounds its refusal at 2 per verify step. Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.signalStepForwardReset.test.ts (unit decisions in both flag states, plus the composite that drives a real LoopDetector through BaseRuntime's dispatch seam on the measured step-bouncing shape and pins wizard safety in both states).",designDoc:"docs/plans/2026-09-08-runner-signal-step-forward-only-reset-design.md",status:"active",added:"2026-09-08"},RUNNER_STEP_ANTI_REGRESSION:{key:"RUNNER_STEP_ANTI_REGRESSION",envVars:["AGENTIQA_RUNNER_STEP_ANTI_REGRESSION","AGENTIQA_EXPERIMENT_RUNNER_STEP_ANTI_REGRESSION"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Runner-lane (test_run) STEP ANTI-REGRESSION: a bounded licence on returning to a page where an already-completed plan step ended. **LIVE BY DEFAULT since 2026-09-10 (GRADUATED \u2014 see DEFAULT below): with no env the guard ENFORCES.** `=0` (either spelling) is the kill switch and stops the detection itself; `=1` is still accepted and now merely restates the default. It shipped SHADOW-FIRST in round 7 and graduated on the evidence recorded under DEFAULT. THE DEFECT (#2577, staging replay of a 25-step plan): having completed the step that mints a duplicate record, the runner navigated back to the BASE page where the earlier steps had ended, to \"finish\" verifications it believed were owed there. That page does not carry the duplicate's title, so the next wait_for_element failed, the model re-signalled backward, minted ANOTHER duplicate and navigated back again \u2014 3 navigations to the same URL, 3 duplicate records left in the tenant, 3 supervisor REDIRECTs ignored, terminal supervisor_halted. Nothing in the runtime had a concept of \"this URL is where a completed step ended\", so nothing could tell that navigate apart from ordinary forward movement. THE RULE (structural, round 4; a third term in round 6): a page is a step regression when it is the END URL of a COMPLETED step AND it is NOT in the CURRENT step's own VISITED set AND it is NOT the run's WORKING PAGE. `_stepEndUrls[k]` is the page the browser was on when the run ADVANCED PAST step k, written EXACTLY ONCE at that forward activation (round 3 wrote it on every hop, which made a multi-page step rewrite its own entry on the way out); `_stepVisitedUrls[k]` is every page observed while step k was current, INCLUDING the page each action dispatched FROM, so a step that opens a detail page and returns (A\u2192B\u2192A) is doing ordinary work and a VERIFY step that starts on the base page is exempt by construction. ROUND 7 MADE THAT SENTENCE TRUE AGAIN: rounds 4-6 excluded a FLAGGED page from every step's visited set (and evicted it from the ones it was already in), which silently deleted the exemption for the rest of the run \u2014 the reviewer measured `signal_step` onto a step STANDING on a flagged page whose visited set then came back EMPTY, so the \"exempt by construction\" mitigation could not fire on the shape it was written for. The visited set now records the current step's pages unconditionally; the flagged set survives for LICENCE ACCOUNTING only (it is what stops an excursion walking from one completed step's page to the next from promoting its own last target into the working-page exemption). NAMED CONSEQUENCE: a SECOND return to the same page inside ONE step is now exempt rather than contained \u2014 which is exactly the legitimate plan shape review measured being refused (\"compare the totals on the summary page and the cart page, then go back to the summary\") \u2014 so within a single step this guard bounds the FIRST unlicensed return only; a run that oscillates inside one step is owned by the LoopDetector and the supervisor, and a run that re-signals BACKWARD (the measured #2577 loop) is unaffected, because the visited-set exemption is gated off entirely once `_maxInRangeStepEverActivated > currentStep`. THE LICENCE IS SPENT ON ARRIVAL, not on the call: the first landing on a completed step's page outside the current step's visited set consumes it \u2014 `step_regression:licensed {via, fromStep, toStep, furthestStep, url}` \u2014 for every navigation the AGENT issued, whether or not the pre-dispatch layer could read its target (`navigate`, `go_back`, an anchored `run_js` including a computed `location.href` the classifier reads as navigational-but-unreadable). APP-AUTHORED ARRIVALS ARE NOT REGRESSIONS (round 7): an arrival produced by a click, a form submit or a server redirect emits `step_regression:arrived_via_app {tool, url, fromStep, toStep}`, joins the current step's visited set, and does NOT spend the licence, does NOT count as a bypass and can never end the run. Rounds 3-6 counted them, on the argument that the licence must be channel-independent; the reviewer measured what that costs on a HUB-AND-SPOKE app (dashboard \u2192 task \u2192 the server 302s back to the hub after each save, and the hub is an earlier step's end URL): `refusalsIssued: 0`, two bypasses, terminal `blockKind:'loop'` \u21D2 supervisor_halted with every remaining step synthesized skipped \u2014 a run ended for the APPLICATION's navigation, under a customer-visible reason claiming the agent had routed around a refusal that was never issued. A server-side redirect is not the agent's regression. THE COST, named: a run that walks back through a back-LINK is no longer contained by this guard at all (the pre-dispatch refusal never saw it either \u2014 it is not a nav-class call), so the click channel is now owned by the LoopDetector, the supervisor and the veto. Round 3 spent it only for a nav-class call with a READABLE target, so every other channel regressed unboundedly while the licence stayed unspent and the containment (which opens on the licence) never woke up. THE PRE-DISPATCH REFUSAL IS SUBORDINATE: it fires only once the licence is already spent and only for a readable target \u2014 navigate (args.url), go_back and a scripted history.back()/history.go(-1) (the LoopDetector's previous distinct URL), and a scripted location.href/assign/replace with a quoted literal. An UNREADABLE target is never refused (round 3 refused those fail-closed, which was measured refusing a plain history.go(0) page RELOAD and a `d.location = someVar` that is not a navigation at all); anything this layer misses is caught on arrival. Script classification is ANCHORED on `location.` / `history.` \u2014 round 3's unanchored patterns matched `xhr.open('GET', \u2026)` and `payload.location = x`. ROUND 8 (2026-09-10, #3118) \u2014 A STEP IS NEVER A REGRESSION AGAINST ITSELF. \"A step behind the run\" was `k < _maxInRangeStepEverActivated` (the FURTHEST step ever reached); it is now `k < currentStepIndex` \u2014 STRICTLY EARLIER THAN THE STEP THE RUN IS CURRENTLY DECLARING. The two differ exactly when the model re-signals BACKWARD, which is when this guard is most active, and the FORCED-ON graduation rehearsal (2026-09-10, 17 staging-mirror plans, engine `45e821072`, the flag hand-set to `=1` for one hour) measured the cost on its very first enforcement delta. Chain, verbatim from the session events: `signal_step:reset_withheld {path:\"activation\", stepIdx:3, direction:\"backward\", maxStepEverActivated:4}`, then the same at `stepIdx:2` \u2014 an ALREADY COMPLETED step is current again \u2014 then `navigate` to the base draft step 2 had ENDED on, booked `step_regression:licensed {fromStep:2, toStep:2, furthestStep:4}`; 72 s later the same `navigate` was answered `step_regression:refused {fromStep:2, toStep:2}`, whose dispatch details read, verbatim, \"step 2 must not return to step 2's page\". Cut off from the page it was grading, the model captured the WRONG record 4 s later, the supervisor issued `block` 4.7 s after that, and the run ended `supervisor_halted` with 5 grades of 29 \u2014 against `passed 29/29` for the same plan in shadow the day before. A step's own end URL is the page that step is DEFINED to work on; a LATER step's end URL is a FORWARD re-walk after a backward signal, which is progress. ROUND 8b (2026-09-10, review round 2 of the same fix) \u2014 THE TERM IS KEYED ON THE URL, NOT ON THE LEDGER INDEX. `k < currentStepIndex` excludes the current step's KEY, not its PAGE: on the ORDINARY SINGLE-PAGE FLOW (\"open the draft\" ends on the draft, \"fill the title\" ends on the draft) `_stepEndUrls[k-1] === _stepEndUrls[k]`, the scan matches `k-1`, and the guard books the identical harm one index over \u2014 measured on the rehearsal's geometry as `licensed {fromStep:1, toStep:2, url: DRAFT}` then `refused {fromStep:1, toStep:2}`, details \"step 2 must not return to step 1's page\", where DRAFT is `_stepEndUrls[2]`. Both halves of the harm survive (the refusal on the re-declared step's own page, and the licence burned on it); only the prose stops being self-contradictory, which also makes the row invisible to a `fromStep >= toStep` census. So the FIRST term is: a target that is the END URL of ANY step AT OR ABOVE the declared one is not a regression target, whichever earlier index also ended there \u2014 \"at or above\" because a later step's page is work the run has not undone. ITS COST, named: the #2577 containment is given up on a page SHARED between an early step and a current-or-later one while the run works backward below both. The ledger invariant (`k < _maxInRangeStepEverActivated`, written once at the forward activation that leaves step k behind) is kept beside it as the assertion it always was \u2014 it can only narrow the target set further, never widen it. THE FURTHEST-STEP MARK STILL KEYS THE LICENCE UNIT AND THE VISITED-SET EXEMPTION (`_maxInRangeStepEverActivated <= currentStep`), so a BACKWARD `signal_step` still cannot make a frozen step \"current\" and switch the visited-set exemption on, and the measured #2577 loop \u2014 forward to step 7, then steps 2-6 re-signalled backward with a return to the BASE page each time \u2014 still gets exactly ONE licensed return and a refusal on every trip after it, because the base page belongs to step 1, which is below every step that loop re-declares. NAMED COST OF ROUND 8: a run that re-signals step k backward and then oscillates on step k's OWN page is not contained by this guard at all \u2014 the same trade round 7 made for the click channel, and owned by the LoopDetector, the supervisor and the veto. WHY THE SHADOW CENSUS COULD NOT HAVE FOUND THIS: 44 shadow runs (27 rehearsal 09-09 + 17 prod 09-10) emitted ZERO `would_refuse`; the shape needs a backward `signal_step` FOLLOWED BY a return to the re-signalled step's own page, and silence in shadow is equally consistent with \"the rule is right\" and \"the entry condition is never reached\" \u2014 which is why the forced-ON rehearsal is now part of the graduation gate below. THE LICENCE UNIT IS THE FURTHEST-REACHED STEP, NOT THE RUN (round 5): the allowance is keyed on `_maxInRangeStepEverActivated`, so a genuine FORWARD activation raises the mark and brings a fresh licence, while a BACKWARD `signal_step` does not \u2014 the measured production loop (forward to step 7, then steps 2-6 re-signalled BACKWARD with a return each time, the furthest step pinned at 7) still gets exactly ONE licensed return and containment on every one after it, and within one furthest step the second return is still refused. A RUN-scoped licence (round 4) refused a LATER step that genuinely needed a completed step's page \u2014 \"go back to the list and verify the copy appears there\" \u2014 and forced the model to grade that step from the wrong page; a PER-STEP one re-grants at every boundary, including the one the loop never crosses. There is still no signal-based re-grant WITHOUT forward progress: that would need an UNFORCED-navigation signal this runtime does not have. THE WORKING PAGE IS NEVER A REGRESSION TARGET (round 6): the licence pays for the EXCURSION, so the way HOME was judged on its own terms \u2014 and the page the plan's forward progress stands on is by construction the END URL of several completed steps. MEASURED LIVE 2026-09-09: one forced-lane run took SEVEN refusals getting back to the submitted copy after its step-9 licence had gone on the base record, recovering only through a contained bypass. Neither round-4 exemption covered it: the working page had been FLAGGED by an earlier arrival (and until round 7 a flagged page could never re-enter any visited set), or a NEW step begins on the excursion page and the working page is in no visited set at all \u2014 the second half is still true, which is why this exemption survives round 7's visited-set fix rather than being subsumed by it. `_stepRegressionWorkingUrl` is the page the run DEPARTED FROM when it left for an excursion, written on every regression arrival \u2014 the departure point rather than a live \"last page seen at the high-water step\", because the departure is the last moment forward progress and the browser agreed on one page and it survives a step advance, an intermediate hop and a supervisor correction, while a live reading would be overwritten by the new step's first ordinary hop on the excursion page. It is NOT a hole in the containment: it exempts exactly ONE page in ONE direction, the OUTBOUND leg of every excursion is still licensed/refused/contained, an A\u2192B oscillation is still caught on every B, the two-bypass ladder still ends the run, and a page the guard has already FLAGGED is never promoted into the exemption (an excursion walking from one completed step's page to the next cannot make its own last target exempt). In the measured production loop it changes exactly one row \u2014 the return leg to the duplicated draft, which round 5 counted as a bypass \u2014 and none of the departures, which are the loop. RESIDUAL OF THAT: the bypass ladder now counts DEPARTURES only, so an oscillating run needs two unlicensed DEPARTURES to reach the loop terminal where it previously needed two arrivals of either kind \u2014 measured 2026-09-09, one forced-lane run oscillated inside the double-return step and was ended by the SUPERVISOR loop breaker (2 redirects, then block) rather than by this containment, which had booked one bypass; the run still ends, with the same supervisor_halted terminal. NAMED RESIDUAL: the mark rises on a FORWARD `signal_step`, which the MODEL emits, so a model that answers a refusal by declaring the next plan step earns a fresh licence (measured live 2026-09-09) \u2014 bounded and monotonic (at most one return per plan step, never re-granted for a step already passed) and paid for with a forward step declaration the run cannot take back, while the measured BACKWARD-re-signalling loop earns nothing. SEAM ORDER: the pre-dispatch layer runs AFTER `steeringVeto.maybeVeto` and the veto wins; on the observation side the ARRIVAL is judged BEFORE it joins the visited set, or every arrival would exempt itself. NORMALIZATION: a plain anchor fragment (`#top`) dropped but a ROUTE-LIKE one (`#/\u2026`, `#!\u2026`) KEPT \u2014 round 7, after the reviewer measured a hash-routed SPA collapsing every route onto one key, which made the sibling SUPERVISOR_REDIRECT_DURABLE veto refuse `#/checkout` and `#/orders/12` because a redirect had named `#/cart`, and made THIS guard wholly inert (every step's end URL identical, 0 licensed / 0 refused / 0 contained on three distinct routes). Trailing slash is significant and deliberately not canonicalized (fail-open in both consumers). An SPA that routes without changing the URL AT ALL is still invisible here, and is owned by the LoopDetector's novel-ref/novel-screen signals. Also: a small denylist of volatile query params stripped (`action_loading_message` \u2014 the measured transient marker \u2014 plus the `_`/`t`/`ts`/`cache`/`nonce`/`rand` cache-buster family), remaining query sorted, compared as scheme+host+path+query; a DENYLIST on purpose, because `?id=` is exactly what distinguishes the base record from a duplicate and an allowlist would refuse legitimate forward navigation. ARRIVAL CONTAINMENT and its escalation count AGENT-AUTHORED ARRIVALS, so they still cover every channel the AGENT can navigate through (including the ones the pre-dispatch layer cannot read) and no channel the APPLICATION drives: the first arrival is the licence, the SECOND emits `step_regression:bypassed {tool, url, fromStep, toStep, bypasses}` and arms a ONE-SHOT standing constraint (the SUPERVISOR_REDIRECT_DURABLE channel) plus a steering-veto redirect, the THIRD ends the run through the LOOP path (`blockKind:'loop'` \u21D2 supervisor_halted), never `blocked_by_issue`. THE RUN-ENDING REASON IS CUSTOMER-VISIBLE (it becomes the `exploration_blocked` obstacle) and its \"routing around the refusal\" clause is now CONDITIONAL on `_stepRegressionRefusals > 0`: a bypass can be booked for a target the refusal channel never read, and the reviewer measured that text asserting a refusal that never happened. The flagged set is RUN-scoped and no longer touches any visited set (round 7) \u2014 it exists so the working-page exemption cannot be handed to a page the guard has already sent the run back from. THE ISSUE GATE is structural and FAIL-OPEN (round 4), and its WINDOW is keyed structurally too (round 6): a `report_issue` / `exploration_blocked` is bounced ONLY when the refusal is still RECENT \u2014 it happened ON THE STEP THE RUN IS CURRENTLY DECLARING, or within the last 6 model turns, whichever is wider \u2014 AND the report's own text carries a token only this runtime writes (`runtime guard`, `step_regression`, or the refusal's own first sentence verbatim) \u2014 `step_regression:issue_rejected {surface, stepIndex, rejections, refusals, refusedAtIteration, refusedAtStep, armedBy, iteration}`, where `armedBy` is `declared_step` or `turn_window` so the census can tell whether the constant still earns its place. Round 4's bare 2-turn constant was outrun by ROUND 5'S OWN LICENCE: measured live 2026-09-09 on 1 of 10 flag-ON forced-lane runs, the model was refused, answered by declaring the next plan step, took the LICENSED retry that declaration earned, and filed an `exploration_blocked` quoting the guard VERBATIM three turns later \u2014 `rejections=0`, terminal blocked/blocked_by_issue at step 9 of 13 with every other step row synthesized skipped. A turn count cannot be the primary key of a window the licence itself widens; the DECLARED STEP can, and the turn arm survives as the OR-arm for a report filed one step later. The TOKEN half is untouched, which is what protects a genuine report \u2014 widening WHEN the gate is armed cannot reject anything that does not quote this runtime's own strings. The exploration_blocked bounce lands BEFORE the terminal write, which is what keeps a completable run off `blocked_by_issue`. RESIDUAL: `_currentStepIndex` is model-controlled, so a backward `signal_step` back onto the refused step re-arms the declared-step arm later in the run; bounded by the token half, and re-declaring a passed step is itself what this guard watches. Round 3's LOOSE ordinary-word tier and its EVIDENCE-BUFFER arm are deleted: they were measured rejecting genuine customer defects (an honest 'Pagination regression: navigating to page 2 still shows the page 1 rows', and a report about a wrong invoice total rejected because an unrelated buffered action's `intent` mentioned navigating back). A defect the customer never receives is caught by nothing; a false `blocked_by_issue` is caught by the arrival containment and visible in the census. NO step, criterion or run status is re-graded \u2014 the refusal is a tool-call rejection, exactly like the steering veto. DEFAULT = ENFORCE (GRADUATED 2026-09-10). With no env set every seam below is live: the pre-dispatch refusal, the licence, the arrival containment and its two-bypass loop terminal, the one-shot standing constraint, the steering-veto redirect and the issue gate. `=0` (either spelling) is the kill switch and is now the ONLY non-enforcing state \u2014 it switches the DETECTION off too, so nothing is recorded, refused, contained or bounced and no row of any name is emitted. `=1` is still accepted and resolves identically to no env. THE SHADOW STATE IS GONE: `would_license` / `would_refuse` / `would_contain` are unreachable, because the only state that turned enforcement off also turns the measurement off. Post-flip watchdog (the graduation process's step 4) is therefore a census of the LIVE names \u2014 `step_regression:refused` / `:bypassed`, and any row with `fromStep >= toStep` \u2014 on nightly runs, and a regression is answered with `AGENTIQA_EXPERIMENT_RUNNER_STEP_ANTI_REGRESSION=0` on the orchestrator FIRST and a fix after. HOW IT GRADUATED, in two measurements, because neither alone was sufficient. (1) SHADOW CENSUS, 44 runs (27 staging-mirror rehearsal 2026-09-09 + 17 prod 2026-09-10): ZERO `would_refuse`, ZERO `would_contain`. That census was NOT the graduation, and reading it as one is the trap this entry exists to document \u2014 `licensed` and `arrived_via_app` take the byte-identical `return null` path in both flag states, so the ONLY evidence shadow can produce about the correctness of a refusal is a `would_refuse`, and silence in it is equally consistent with \"the rule is right\" and \"the entry condition is never reached\". (2) FORCED-ON REHEARSAL, the second gate, two passes of the same 17 fixed customer plans on the staging mirror with the flag hand-set `=1` on the orchestrator for the duration. PASS 1 (2026-09-10 09:31\u201310:29Z, engine `45e821072`, FLAGS-STATE PR #3115) was a **NO-GO** on its first enforcement delta: `licensed` 2, **`refused` 1**, `bypassed` 0, `arrived_via_app` 2 \u2014 and that single refusal was FALSE, the `fromStep == toStep` own-page class described under ROUND 8 above (#3118), which turned a `passed` 29/29 shadow run into `supervisor_halted` with 5 grades of 29. PASS 2 (2026-09-10 12:23\u201313:42Z, after the ROUND 8 / 8b fix, FLAGS-STATE PR #3125) is the **GO**: `licensed` **1**, `refused` **0**, `bypassed` **0**, `arrived_via_app` **2**, `issue_rejected` **0**, `would_*` **0** (enforcement confirmed) and **0** rows with `fromStep >= toStep`. Three facts make that zero a measurement rather than a silence: the pass-1 blocker plan recovered to `passed` 29/29 \u2014 its shadow baseline exactly \u2014 with no guard row and no supervisor redirect or block, where pass 1 had 2 redirects and a block all on that one plan; the shape that produced the false refusal was exercised MORE, not less (`signal_step:reset_withheld[backward]` 9 vs 5), and produced nothing; and the guard is not inert, because the one `licensed` row is a genuine far return `{fromStep: 4, toStep: 27, furthestStep: 27}` on a run that passed 28/28. Two runs got worse than pass 1 and NEITHER carries a `refused` or `bypassed` row \u2014 a chronic criteria-derivation downgrade (#2213) and a Lio \"Loading\u2026\" hang that had already blocked in two of that plan's five prior baselines \u2014 so neither is attributable to this flag. THE RULE THE TWO PASSES ESTABLISH, for any shadow-first guard: graduation goes through an enforcing window, never through a longer soak; and a repeat window only proves a fix if the shape that broke the previous one is shown to have RECURRED \u2014 a nulled entry-condition counter makes a clean census mean nothing. WHY IT WAS SHADOW-FIRST AT ALL: three consecutive review rounds each found a NEW false-refusal class on this enforcing guard \u2014 app-authored hub-and-spoke returns, hash-routed SPAs, and a step that legitimately needs an earlier step's page twice \u2014 and round 8 added a fourth found by the first enforcing pass; all four are fixed above. Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.stepAntiRegression.test.ts, stepRegressionGuard.test.ts, navUrl.test.ts.",designDoc:"docs/plans/2026-09-08-runner-signal-step-forward-only-reset-design.md",status:"active",added:"2026-09-09"},SUPERVISOR_REDIRECT_DURABLE:{key:"SUPERVISOR_REDIRECT_DURABLE",envVars:["AGENTIQA_SUPERVISOR_REDIRECT_DURABLE","AGENTIQA_EXPERIMENT_SUPERVISOR_REDIRECT_DURABLE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Supervisor corrections OUTLIVE the turn they were issued on. Two behaviors under one switch. THE DEFECT (#2577): `applySupervisorVerdict` returns `injectedFunctionResponseError`, BaseRuntime writes it into the TRAILING function response's `metadata.error`, and one turn later it is just another line of an ever-growing trace. On the reconstructed session three REDIRECTs all said the same thing \u2014 stop navigating back, do the work on the page you are on \u2014 and the behavior each forbade returned within two turns; the run ended supervisor_halted anyway. SEPARATELY, the veto that WOULD have refused the third navigate had its armed forbidden set wiped one action earlier by a meta `resetLoopDetector` return (the retained-verify-snapshot reuse, `browser_action:short_circuited {reason:\"auto_verify_evidence\"}`) \u2014 budget 1/3 used, TTL 2/8 spent, and the set was empty. (1) STANDING CONSTRAINT: a `redirect` or `abort` verdict now also returns `standingConstraint {text, turnsLeft: 3}`; BaseRuntime re-injects the correction on the trailing function response of each of the next 3 model turns through the SAME channel (no prompt-shape change), decrementing once per turn, cleared EARLY on the next `continue` \u2014 the judge's own signal that the corrected behavior stopped. The arming turn is skipped (it already carries the verdict's prose), an empty turn is a no-op that does not spend budget, and an error the turn already carries is preserved rather than overwritten. (2) COMPOSITE-RESET SPLIT: `toolResult.resetLoopDetector` still resets the LoopDetector counters and the tapRetryTracker for EVERY producer, but the STEERING state (steeringVeto's armed block, sameGoalAbort's streak) is now cleared only by a result declaring `resetScope: 'full'` \u2014 stamped in `RunnerRuntime.gateSignalStepReset`, i.e. exactly the `signal_step` step activation, which IS a genuinely new goal. A meta bounce instead calls the new `steeringVeto.resetForNewStep()`, which drops the recent-window evidence (so the next step cannot inherit this one's loop set) but leaves an ARMED block standing until its own TTL/budget retires it. This closes the named residual of RUNNER_SIGNAL_STEP_FORWARD_RESET without touching any of the four sibling returns. COMPANION, under this same switch: `steeringVeto.actionFingerprint` now projects nav-class args through `projectFingerprintArgs`, normalizing the URL, and `recordRedirect` snapshots the recent window's normalized nav URLs into a `forbiddenNavUrls` set so a navigate carrying different companion args still matches on the PAGE it targets. Both widen a REFUSAL path, so `=0` restores the pre-#2577 byte-equality matching exactly. NO step, criterion or run status is re-graded; this changes only what the model is told and WHEN an existing deterministic breaker can arm. RUN SCOPE: the constraint is cleared at runLoop start by `resetSteeringStateForNewRun()`, beside `steeringVeto.reset()` \u2014 it is armed and cleared by verdicts only, so a runtime instance ending a run with `turnsLeft > 0` otherwise re-injected a stale correction into the NEXT run's first turns. SECOND CONSUMER (2026-09-09, round 3): RUNNER_STEP_ANTI_REGRESSION's post-dispatch bypass containment arms a ONE-SHOT standing constraint on this same channel, so with `=0` a detected bypass still emits `step_regression:bypassed` and still records the steering-veto redirect, but the model is not re-told on the following turn. OFF (`=0` under either spelling): one-shot prose, the indivisible amnesty and the pre-#2577 veto matching, byte-identical to before. Acceptance suite: packages/engine-core/src/__tests__/supervisorRedirectDurable.test.ts, BaseRuntime.compositeReset.test.ts, steeringVeto.test.ts.",designDoc:"docs/plans/2026-09-08-runner-signal-step-forward-only-reset-design.md",status:"active",added:"2026-09-09"},STEERING_VETO_OBSERVATION_EXEMPT:{key:"STEERING_VETO_OBSERVATION_EXEMPT",envVars:["AGENTIQA_STEERING_VETO_OBSERVATION_EXEMPT","AGENTIQA_EXPERIMENT_STEERING_VETO_OBSERVATION_EXEMPT"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"The steering veto refuses only calls that can CHANGE PAGE STATE. THE DEFECT (#3112, prod asess_1789009823056_btkyv0ns, 2026-09-10): on a page that genuinely had no Approve/Reject control for the acting role, the supervisor asserted a locator it had never observed and the veto then refused the five calls that could have proved it wrong \u2014 `snapshot` x1, `run_js` x2 (the `[]`-buttons absence probes), `click_at` x2 \u2014 275 s and 28 calls burned on one step, `report_issue` x0, terminal supervisor_halted. TWO FINGERPRINT PROPERTIES CAUSED IT: `snapshot` takes no arguments, so its fingerprint is the CONSTANT `snapshot:{}` and one earlier snapshot inside the 6-call window forbids every later one; and #2377's script-body strip in `projectFingerprintArgs` collapses EVERY `run_js` to `run_js:{}`, so the differently-worded absence probe is byte-identical, to the veto, to whatever `run_js` preceded the redirect. The refusals also STARVED the mechanism that owns read-only scripting: `run_js_hunt` was 0 for the whole run, because a call refused pre-dispatch never enters `RunJsHuntBreaker`'s streak. THE RULE: a call whose side-effect class is `observation` is never refused by the veto; its repetition is bounded by `RunJsHuntBreaker` (once per declared step) and the LoopDetector's novel-screen / structural force-block. CLASSIFICATION IS STRUCTURAL (`toolSideEffectClass.ts`), built from the registries that already exist \u2014 `negativeStateEvidence.STABILIZING_NEGATIVE_STATE_ACTIONS` for observation, `BaseRuntime.OBSERVATION_STATE_CHANGING_WEB_ACTIONS` / `...MOBILE_ACTIONS` / `CLICK_CLASS_ACTIONS` and `navUrl.NAV_CLASS_ACTIONS` for mutation \u2014 and made TOTAL by an audit test over every live tool set, so a new tool cannot silently fall through. `run_js` is resolved by SOURCE (`classifyRunJsSideEffect`) BEFORE any set membership, because it is a member of the observation registry AND can carry a write; the scan is fail-closed (an unreadable, over-long, obfuscated or dynamic source is `mutation`), so #2377's widening stays live for MUTATING scripts and only the read-only remainder is handed back to the hunt breaker. AN EXEMPTED CALL IS INVISIBLE TO THE VETO IN BOTH DIRECTIONS: it does not enter `recentExecuted` (or the next REDIRECT would forbid `snapshot:{}` again) and it does NOT spend `executedSinceArmed` \u2014 without that half the exemption would open a bigger hole than it closes, since eight exempted snapshots would legally retire an armed forbidden set with no LoopDetector signal. It does not spend the 3-refusal budget either: an exemption is not a refusal. The scroll/swipe amnesty (`VETO_EXEMPT_ACTIONS`) is consulted FIRST at both seams, so its pre-#3112 behaviour \u2014 exempt from refusal, still spending one TTL slot \u2014 is byte-identical and pinned. SHIPS LIVE (default on), which inverts this repo's shadow-first convention for a new runtime guard, deliberately: the polarity is inverted too. This NARROWS an existing refusal, so it can only ever REMOVE a refusal and never add one, and every uncertainty in the classifier resolves to `mutation` (= today's behaviour). A shadow rollout would have measured a hole production was already measuring. Diag: `steering_veto:observation_exempt {tool, sideEffectClass, runJsVerdict?, runJsReason?, instructionHead}` (kind `log`, source `SteeringVeto`) on every exempted call, and NO `agent_lifecycle{event:'steering_veto'}` row for it. `=0` (either spelling) restores `45e821072` exactly: no classification runs at either seam, an observation call is refused as before and counts toward the window and the TTL as before. RESIDUAL, named: the pre-existing scroll amnesty still spends TTL, so 8 scrolls still retire an armed set \u2014 unchanged by this flag and out of its scope. Acceptance suite: packages/engine-core/src/__tests__/steeringVeto.observationExempt.test.ts, toolSideEffectClass.test.ts, toolSideEffectClass.audit.test.ts.",designDoc:"packages/engine-core/src/toolSideEffectClass.ts",status:"active",added:"2026-09-10"},SUPERVISOR_REDIRECT_GROUNDING:{key:"SUPERVISOR_REDIRECT_GROUNDING",envVars:["AGENTIQA_SUPERVISOR_REDIRECT_GROUNDING","AGENTIQA_EXPERIMENT_SUPERVISOR_REDIRECT_GROUNDING"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"LIVE BY DEFAULT IN THE RUNNER LANE since 2026-09-11 (GRADUATED \u2014 see DEFAULT below): with no env the gate ENFORCES in the runner lane and books SHADOW rows in chat/explorer, because that is the only lane the graduating census covered (#3162 \u2014 `BaseRuntime.supervisorGroundingEnforcementLane()`, `false` by default, overridden `true` by `RunnerRuntime`); `=0` (either spelling) is the kill switch in EVERY lane and stops the DETECTION itself; `=1` is still accepted and ENFORCES IN EVERY LANE, so it is the deliberate opt-in a chat/explorer census needs rather than a bare restatement of the default. The two spellings must AGREE for that `=1` to mean anything: a contradictory pair (`AGENTIQA_SUPERVISOR_REDIRECT_GROUNDING=1` with `AGENTIQA_EXPERIMENT_SUPERVISOR_REDIRECT_GROUNDING=0`) resolves to DETECTION OFF, because `groundSupervisorRedirect` consults `killSwitchForcedOff` \u2014 any `=0` wins \u2014 before it reads anything else. Safe direction, and pre-existing. It shipped SHADOW-FIRST on 2026-09-10 and graduated on the evidence recorded under DEFAULT. A supervisor REDIRECT that asserts WHERE a control is may arm the steering veto and the K-turn standing constraint only when every affordance literal in that locator clause was observed in a FULL a11y snapshot this run captured, on the page it is standing on. THE DEFECT (#3112): `SupervisorService.buildPrompt` renders the judge's whole input from the agent's OWN narration \u2014 the Intent and Screen columns are `call.args.intent` / `call.args.screen` \u2014 plus one screenshot. The judge has no runtime observation channel and nothing downstream ever asked whether its answer matched the page, so an invented locator (\"Reject is in the three-dot menu on the Procurement Approval node\", on a node with no overflow control at all) armed the veto AND was re-injected as a standing constraint 7 times: the channel whose whole purpose is to make a correction survive contradiction, amplifying a hallucination. THREE-WAY VERDICT, abstain-dominant: `ungrounded` means we searched a page-scoped FULL corpus and did not find the operand; every uncertainty is an abstain \u2014 `no_locator_claim` (an ordinary correction names no control: \"stop navigating back to the draft\", \"finish step 7\"), `no_snapshot` (nothing at full fidelity yet \u2014 a redirect about a page the run JUST navigated to is UNOBSERVED, not ungrounded), `stale_corpus` (the retained captures are all of other pages), `operand_unextractable`, `plan_licensed` (the plan's own step text names every operand, so the judge is echoing the plan), `negated_or_expected_claim` (delegated to the hallucinated-quote gate's own NEGATIVE_CLAIM_RE / EXPECTED_MARKER_RE), and `collapsed_affordance` \u2014 the container the claim names IS on the page and the control is not, which in an a11y outline is INDISTINGUISHABLE from a menu nobody has opened yet. That last one was found by MEASUREMENT, not by reading: a live `_snapshotForAI` read of the runner-role-gated-approval fixture (2026-09-10) showed the gate calling a real, reachable control ungrounded the moment the model had not yet clicked the overflow trigger. PRESENCE IS NODE-SCOPED (round 2): round 1 evaluated that abstain page-wide, which made the gate silent on the issue's OWN case (the sibling Finance row carries a U+22EE, so a claim about the Procurement row abstained even though that row has no overflow control at all) and left the shadow census able to graduate only the residual 'a container this page never rendered' class. When the clause names a node and that node resolves to exactly ONE record in every retained outline, the haystack narrows to that record's a11y subtree \u2014 climbing from the naming line to its closest record-role ancestor, because a row's controls are the naming line's SIBLINGS rather than its descendants. Every operand but the anchor is judged inside the subtree; the anchor stays page-wide, so a node the page lacks entirely is `ungrounded`, not abstained. FAIL-OPEN: no anchor, a non-unique anchor, or no record ancestor falls back to page scope, because a wrong subtree produces a wrong `ungrounded`. Measured live against the real renderer: reviewer-role page -> `ungrounded [three-dot menu, Reject]`; the same page's Finance-row claim -> `abstain collapsed_affordance`; admin-role page -> `abstain collapsed_affordance`. The locator lexicon is NARROW by construction: a clause counts only when it carries BOTH a locator shape (is/sits/lives in|inside|under|behind, open/use the ... menu, next to|beside|to the right of) AND a container noun \u2014 `page` is deliberately excluded, so \"go back to the checkout page\" abstains. Affordance descriptors carry an explicit variant set (three-dot / kebab / ellipsis / overflow / U+22EE / \"more actions\"), because \"three-dot menu\" is never the literal text of an a11y node. DEFAULT = ENFORCE IN THE RUNNER LANE (GRADUATED 2026-09-11, lane-scoped by #3162). With no env the full classification runs in every lane, and in the RUNNER lane an `ungrounded` verdict DOWNGRADES the redirect: no `steeringVeto.recordRedirect`, no `armSupervisorStandingConstraint`, and the injected `metadata.error` becomes a runtime-authored observe-first instruction, emitting `supervisor_redirect:downgraded {iteration, reason, unobserved, operands, snapshotsSearched, claimHead}`. IN THE CHAT/EXPLORER LANES the same classification runs and publishes `supervisor_redirect:would_downgrade {\u2026}` while the redirect applies untouched \u2014 the shadow behaviour, kept there on purpose: all 236 messages of the graduating census are runner rows, and the one lane-specific rescue is runner-only too, since `supervisorGroundingPlanText()` is a `RunnerRuntime` override and `plan_licensed` therefore cannot fire in chat. That abstain exists because a supervisor echoing the user's OWN declared operand is restating rather than inventing; in chat that operand lives in the USER'S MESSAGE and there is no `user_licensed` abstain, so the chat lane is BOTH unmeasured and structurally missing the rescue. Enforcing there on a corpus with zero rows from it is precisely what this gate's own graduation rule forbids \u2014 so chat/explorer GRADUATION NEEDS ITS OWN CENSUS, which now accrues under the live default from the `lane: 'other'` rows rather than needing a fresh forcing window. EVERY ROW CARRIES `lane` (`'runner'` where the default enforces, `'other'` where it books shadow rows), single-sourced from the guard the enforcement decision itself reads. `=1` enforces in BOTH lanes and is kept accepted. `=0` is the kill switch and is the only state that stops the detection: no snapshot ledger is written, no classification runs, no row of any name is emitted, and the redirect applies BYTE-IDENTICALLY as it did before this gate existed \u2014 veto armed, constraint armed, the judge's own prose injected. THE SHADOW STATE IS GONE IN THE RUNNER LANE, and that is deliberate: `supervisor_redirect:would_downgrade` is unreachable there, because the only state that stops runner enforcement also stops the measurement (the RUNNER_STEP_ANTI_REGRESSION/#3128 shape \u2014 a kill switch on a gate whose detection costs something). It is REACHABLE, and is the live default, in chat/explorer \u2014 that is the whole point of the lane split. The POST-FLIP WATCHDOG therefore reads two things: the LIVE names on `lane: 'runner'` rows \u2014 `supervisor_redirect:downgraded` / `:grounded` / `:grounding_abstain`, row by row, never as a rate \u2014 and, on `lane: 'other'` rows, the accruing chat/explorer census (`:would_downgrade` + the abstain split) that a chat graduation will have to be argued from. and a regression is answered by setting `AGENTIQA_EXPERIMENT_SUPERVISOR_REDIRECT_GROUNDING=0` on the orchestrator FIRST and fixing after. HOW IT GRADUATED, in two passes, because the mirror could not supply the denominator. (1) MIRROR PASS 1 (2026-09-11 08:13:41-09:29:58Z, staging orchestrator hand-set `=1`, 17 fixed Lio plans, engine `7094117cb`): 14 passed / 3 failed / 0 blocked \u2014 the best of that plan set's four baselines \u2014 and exactly TWO classified redirect messages, both `grounding_abstain {no_locator_claim}`, `downgraded` 0 and `would_downgrade` 0 (enforcement confirmed). Both abstains are CORRECT (neither correction names a control or a container), and no run regressed; but a `downgraded` never fired, so the GO condition was vacuous and the pass was booked NO-GO BY INSUFFICIENT n. At ~2 messages per 17-plan pass the >=100 floor needs ~50 such passes: the mirror's plans are direct scenarios on which the supervisor answers `continue` (101 of 103 verdicts), so pass 2 had to ORDER the form rather than hope to meet it. (2) FIXTURE PASS 2 (2026-09-11, local, tree `462e48a80` = `origin/staging` incl. #3119; 8 cases x 10 replicates x 2 arms = 160 runs on `/flows/runner-role-gated-approval`, `gemini-3-flash-preview`, `LLM_CACHE=0`): 236 CLASSIFIED REDIRECT MESSAGES \u2014 the floor is met on count. Against a deterministic PAGE-TRUTH preflight (the real `_snapshotForAI` of the fixture in four states, judged by the shipped `groundRedirect`): a fact the page does NOT have \u2014 100 messages, 100 DOWNGRADED (100%), 0 grounded, 0 abstained; a control behind THAT node's own collapsed menu \u2014 48 messages, 0 downgraded, 21 grounded, 27 `collapsed_affordance`; a control literally in the outline \u2014 87 messages, 0 downgraded, 87 grounded; plus 1 real judge correction with no locator clause, `no_locator_claim`. So: ZERO false downgrades on 135 true messages, ZERO false `grounded` on 100 false ones, and zero absent facts left un-downgraded. All three distinct `unobserved` sets were read individually and checked against the preflight. Abstain split (gate clause 3): `collapsed_affordance` 27, `no_locator_claim` 1, `no_snapshot` 0, `stale_corpus` 0. THE BEHAVIOURAL HALF, which no prior window had: on the 12 runs per arm whose every row was a downgrade, `standing_constraint:reinjected` is 0 under `=1` against 23 in shadow \u2014 the production `x7` re-injection of `btkyv0ns` reproduced in shadow and zeroed by enforcement \u2014 while the SAME arm still re-injects 30 times on runs carrying `grounded` / `collapsed_affordance` rows, which is the control against a blanket muzzle. The three completable controls are 10/10 `passed` in BOTH arms (60 runs, zero downgrades), and on the population where the flag can act at all no run is worse than its shadow twin (enforce 32/32 lane-judge pass vs shadow 33/33; the six pairwise cells where enforce failed and shadow passed all have `supervisorConsults = 0`, i.e. both arms ran a byte-identical path). READ THE DENOMINATOR HONESTLY: 235 of the 236 messages were ORDERED by the test-only `e2e/evals/runner/supervisorRedirectInjection.ts` seam plus `AGENTIQA_TEST_SUPERVISOR_EAGER=1`, neither of which exists in production. The pass measures the CLASSIFIER AND ITS CONSEQUENCES, not how often a real judge invents a locator \u2014 that frequency is known only from prod/mirror (redirects on 1 session in 17). (3) MIRROR PASS 2, the COMBINED window (2026-09-11 12:29:09-13:56:51Z, staging orchestrator `=1` alongside `BLOCKED_SYNTHESIS_PRESERVE_GRADES` \u2014 disjoint marker namespaces, so every row attributes to exactly one flag; engine `e89f22e7f`, a DIFFERENT SHA from pass 1's `7094117cb`): 19 runs, 14 passed / 3 failed / 2 blocked, TWO classified redirect messages, both correct `grounding_abstain {no_locator_claim}`, `downgraded` 0, `grounded` 0, and zero regressions attributable to this flag. Like pass 1 it is a NO-GO on its own terms (the `ungrounded` branch was never reached, so its zero is VACUOUS) and it is cited on exactly those terms: the mirror's job here is the other half of the proof \u2014 the fixture lane shows the gate fires and fires correctly, the two mirror windows show it does not fire where it must not, on two different engine SHAs. WHAT THE THREE PASSES DID NOT CLOSE is recorded in `graduation` below rather than papered over. EXACTLY ONE ROW PER CLASSIFIED REDIRECT (round 2): a grounded classification emits `supervisor_redirect:grounded`, which the first cut omitted to keep the census small \u2014 wrongly on both counts, since redirects are rare (one Lio session in 17) and without that row the DENOMINATOR this flag graduates on (redirect-MESSAGE counts, floor >=100) cannot be read at all; it also made the eval gate unsound, since 'at least one row' could not tell a grounded classification from a gate that never ran (measured on 2 of 3 live runs). Abstains are published in both live states as `supervisor_redirect:grounding_abstain {reason}` (the `cause_grounding:abstain` precedent): a rising `no_snapshot` rate is the honest signal that the LEDGER, not the judge, is what needs work, and an abstain-heavy census is not evidence the gate works. A DOWNGRADED REDIRECT IS STILL A REDIRECT FOR ESCALATION: `redirectFlipFlop.observeRedirect` and `sameGoalAbort.observeVerdict` both read `verdict.action` BEFORE this gate runs, so the abort/block escalation arithmetic is unchanged \u2014 pinned by test, because a refactor that moved the grounding call earlier would silently change abort timing. NOT A TEXT GATE OVER MODEL PROSE: it reads the SUPERVISOR's runtime-injected instruction, writes no status and no criterion, and DOWNGRADES rather than refuses (the cause-grounding stage-19c precedent). THE LEDGER is the only new state: the last 3 FULL `groundingSnapshot`s (12 kB each), written by one guarded, exception-safe read at the dispatch seam \u2014 never inside a `page.evaluate`, never between an addressing marker and its action \u2014 cleared by `resetSteeringStateForNewRun` beside the other steering state, and not written at all under `=0`. Acceptance suite: packages/engine-core/src/__tests__/supervisorRedirectGrounding.test.ts, BaseRuntime.supervisorRedirectGrounding.test.ts.",designDoc:"packages/engine-core/src/supervisorRedirectGrounding.ts",status:"active",added:"2026-09-10",graduation:{status:"gated",gate:"GRADUATED 2026-09-11 (default ON in the RUNNER lane; chat/explorer stay shadow \u2014 #3162) with TWO parts of this bar met by a SUBSTITUTE, one met only by a UNIT PIN, and TWO still OPEN \u2014 kept as `gated` rather than deleted, the CRITERIA_CONTAINMENT precedent, because the open parts are a real unmeasured false-fire channel and a whole unmeasured lane pair. THE ORIGINAL BAR, verbatim: census in REDIRECT-MESSAGE counts, not run counts, with a floor of >=100 redirect messages across the runner + chat + explorer lanes; (1) it fires on a replay of asess_1789009823056_btkyv0ns and every fired row names an operand the retained page-scoped snapshots genuinely do not contain \u2014 read every row, not a rate; (2) ZERO `would_downgrade` rows on the redirect population of runs that completed normally (runner/happy-form, runner/seeded-broken, the Lio mirror's passing plans); (3) the `grounding_abstain` split published alongside with `no_snapshot` below the review's stated bar; (4) the two named false-fire classes answered explicitly \u2014 an operand visible only in a SCREENSHOT (canvas text, an image-rendered menu) and a step whose capture was incremental must land as `abstain`, never `downgrade`. MET: the FLOOR \u2014 236 classified redirect messages (fixture pass 2) + 2 (mirror pass 1). (3) MET IN FULL \u2014 `collapsed_affordance` 27, `no_locator_claim` 1, `no_snapshot` 0, `stale_corpus` 0 of 236; the ledger was healthy on the measured page (`snapshotsSearched` 3 on 181 rows, 2 on 41, 1 on 13, 0 on the single early-return row), which is a statement about the ledger ON A HEALTHY PAGE, not about the ledger in general. (4b) MET BY UNIT TEST; NOT OBSERVED IN THE SURVIVING CENSUS. The incremental-capture class lands in `abstain` (`no_snapshot` / `stale_corpus`) and is pinned by `BaseRuntime.supervisorRedirectGrounding.test.ts`'s page-scoping section \u2014 a real pin, and the only one this clause has. The 5 live observations previously cited here come from census pass 2a, which was ERASED from a session scratchpad and cannot be re-read; in the surviving 236-message census `no_snapshot` is 0 and `stale_corpus` is 0, i.e. the class fired ZERO times. A unit pin is what the other clauses were explicitly not allowed to be, so this clause is downgraded rather than argued: nothing else in the gate depends on it, and reproducing it in the lane is cheap whenever someone wants it closed. MET BY SUBSTITUTE, stated as such: (1) NOT a session replay of `asess_1789009823056_btkyv0ns` \u2014 it is a REDUCED DETERMINISTIC FIXTURE (`/flows/runner-role-gated-approval`) carrying that session's correction VERBATIM, judged against a preflight page-truth table taken from the real `_snapshotForAI`; the 'read every row' half IS satisfied \u2014 all three distinct `unobserved` sets were read and each is true of the page. (2) NOT the named population \u2014 `runner/happy-form`, `runner/seeded-broken` and the mirror's passing plans were not driven with a scripted redirect; the substitute is 135 messages about facts the page genuinely HAS (0 downgrades), 60 runs of three COMPLETABLE controls 10/10 `passed` in both arms with 0 downgrades, and the mirror pass's 14 passing plans carrying 0 downgrades. NOT MET, and the reason this block stays: (0) LANE COVERAGE \u2014 all 236 messages are RUNNER. Chat and explorer produced no rows at all, so the `plan_licensed` branch (which only `RunnerRuntime` overrides) is unmeasured in the two lanes where it is structurally dead. ENFORCEMENT IS THEREFORE SCOPED TO THE LANE THE EVIDENCE COVERS (#3162): the graduated default downgrades in the runner lane only, chat/explorer keep the shadow behaviour and book `lane: 'other'` rows, and CHAT/EXPLORER GRADUATION NEEDS ITS OWN CENSUS \u2014 argued from those rows, or from a deliberate `=1` window, and landed as its own flip. That is not a residual to be waived: it is the open half of this entry, and the `graduation` block stays `gated` until it closes. (4a) THE SCREENSHOT-ONLY OPERAND \u2014 a control visible only in a screenshot (canvas text, an image-rendered menu) is not reproducible on this fixture and remains an unmeasured false-DOWNGRADE channel. Also on the record: 235 of the 236 messages were ORDERED by a test seam, so nothing here measures how often a real judge invents a locator. WHAT CLOSES THE REMAINDER, cheaply and without another forcing window: (a) one chat case with a scripted redirect \u2014 lane coverage plus the first live exercise of `plan_licensed`; (b) a fixture whose menu is rendered in canvas \u2014 (4a); (c) the same scripted redirects on `runner/happy-form` / `runner/seeded-broken` \u2014 the letter of (2). Until then the post-flip watchdog in `gates` above is the standing measurement, and `AGENTIQA_EXPERIMENT_SUPERVISOR_REDIRECT_GROUNDING=0` is the lever.",evidence:"Post-flip watchdog: the diag families `supervisor_redirect:downgraded` / `:grounded` / `:grounding_abstain` / `:would_downgrade` on the nightly qa-exhaustive runner + chat lanes (exported by e2e/evals/runner/groundingDiag.ts), read row by row and SPLIT BY THE `lane` FIELD \u2014 `runner` rows are the live gate, `other` rows are the chat/explorer census this flag has not yet graduated on. REPRODUCIBLE CORPUS: the 236-message census harness is committed at e2e/evals/runner/census/ (six census-only cases resolvable by --ids, the page-truth preflight, the analyzer and the recipe in its README); it was uncommitted at flip time, which is why the flip PR landed it. Graduation evidence, three passes: rehearsal-2026-09-11-redirect-p1.md (mirror pass 1, forced-ON, 17 plans, NO-GO by insufficient n), rehearsal-2026-09-11-combined.md (mirror pass 2, the combined forced-ON window, 19 runs, 2 abstain rows, zero regressions, engine e89f22e7f) and rehearsal-2026-09-11-redirect-fixtures.md (160 runs / 236 messages, the GO), all steering-local under ~/agentiqa-agents/lio-harvest/reports/.",owner:"steering (Alex)",review:"2026-10-10"}},UNNAMED_CONTROL_A11Y_FINDING:{key:"UNNAMED_CONTROL_A11Y_FINDING",envVars:["AGENTIQA_UNNAMED_CONTROL_A11Y_FINDING","AGENTIQA_EXPERIMENT_UNNAMED_CONTROL_A11Y_FINDING"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Structural accessibility findings on unnamed controls (#3091 milestone 3). THE GAP: a working flow whose primary action is an icon-only button is a real accessibility defect (a screen reader announces nothing) that no model reading a screenshot ever catches \u2014 the icon looks fine \u2014 while the BROWSER can answer it exactly. WHOSE READING (review round 2, 2026-09-09): the first cut read collectClickTargetStateFromElement's `accessibleName` \u2014 the first non-blank of [aria-label || title, aria-labelledby text, associated <label> text, own text]. That is an element-LOCAL formula, not the accname algorithm: it never descends, so `<button><img alt=\"Save\">` and `<button><svg role=img aria-label=\"Save\">` read as UNNAMED there while a screen reader announces them correctly. Harmless while the value only matched and described elements; as the sole substantiation of a customer-facing DEFECT it was a false-positive machine on the two commonest icon-button idioms. The engine now asks the browser: when the element-local formula came back empty AND the target is an interactive, enabled, non-decorative control, `stampComputedAccessibleName` resolves the element over CDP and reads `Accessibility.getPartialAXTree`'s `name.value` into `clickTarget.computedAccessibleName`; a node the tree reports as IGNORED (aria-hidden, role=presentation, hidden subtree) has no name to be missing and ABSTAINS; and when the probe cannot answer, the capture's `descendantName` fallback (a labelled img/svg/[title] inside the control, skipping aria-hidden subtrees) stands in and the notes say which reading was used. The flag OFF fires no probe at all, so the click hot path is unchanged. WHAT ON ENABLES, in one slice: (a) `accessibility` joins the `report_issue` category enum on all four declaration surfaces (agentToolDefs / agentTools / runnerToolDefs / runnerTools, via tools/reportIssueCategories.ts) and the field description names the one admissible use; (b) one bullet each in buildRunnerPrompt's QUALITY OBSERVATION block (both the snapshot-only and screenshot branches) and in ExplorerRuntime's issue-reporting rules, prescribing category='accessibility', severity='low', and that a working flow still PASSES; (c) the runner appends a verdict-free SOFT NOTE to the click result naming the engine's observation; (d) the runner keeps a per-step ledger of unnamed activated controls, keyed on `_currentStepIndex` exactly like `_stepActionErrors`, capped at 5/step; (e) handleReportIssue requires POSITIVE substantiation for the new category \u2014 at least one ledger observation FROM THE STEP BEING REPORTED ON \u2014 and rejects otherwise, and stamps a runtime marker naming the controls observed on that step onto the filed issue. THE SUBSTANTIATION IS THE POINT, not an add-on: `shouldRejectEmptyEvidenceIssueReport` short-circuits on any category that is not logical/visual, so a new category would BYPASS the empty-evidence gate and be filable on zero evidence; (e) replaces that bypass with a positive engine-fact requirement, so the model can no more invent an accessibility finding than it can invent a page error. VERDICT-NEUTRAL BY CONSTRUCTION: `reportedIssueCapsPassedRun` admits only (high|medium) AND (logical|environment), so an accessibility/low finding cannot cap a passed run to warning at any severity \u2014 the flow still passes and the finding is a curation item. EXCLUSIONS, each because its absence would assert something the evidence does not support: non-activation tools; no clickTarget (no engine observation, and the label-addressed click path resolved a NAME by definition); a named control; a DISABLED control (never activated); a RETARGETED click (which element the captured name belongs to is ambiguous); an author-declared role=presentation/none and any node the accessibility tree reports as IGNORED; a determinately-decorative target (isDeterminatelyDecorativeTarget \u2014 a styled span has no accessible name by design, the #2060 false-positive class); and a role-less element with neither an interactive tag/role nor click-handler evidence. SHADOW-FIRST (review round 4, 2026-09-09 \u2014 the RUNNER_STEP_ANTI_REGRESSION/#3088 convention). With NO env set the DETECTION runs \u2014 the CDP name probe, the DOM fallback capture, the classifier and the per-step ledger \u2014 and emits one `a11y:would_file {stepIndex, role, nameSource, selector}` row per distinct control, and NOTHING customer- or model-facing changes: no click note, no prompt bullet, no `accessibility` in the schema, and the ledger is READ AS EMPTY by the report_issue gate so a non-conforming filing is rejected exactly as before. `=1` ENFORCES (a)-(e) below. `=0` is the only state that stops the measurement itself, including the CDP round trip. The census is the point: a flag whose detection only ran where it was already on could never be graduated on evidence. MED (round 4): a VALUELESS `input[type=submit|reset]` is NOT unnamed \u2014 HTML-AAM 4.1.6 gives it the user agent's own localized default label and every screen reader announces \"Submit\"/\"Reset\" \u2014 so the DOM fallback now returns that default and the control is never flagged; `type=button`, which has no UA default, correctly stays reportable. OFF (`=0`) \u21D2 byte-identical tool declarations and prompts, no ledger, no note, no probe, and `accessibility` unreachable from the schemas. The ledger counts DISTINCT controls, keyed on `targetSignature` (tag|role|accessibleName|text): an unnamed control's `describeClickTarget` collapses to a bare \"button\", so without the key five clicks on one icon button would make the runtime marker state a control count the engine never measured. Diag: `unnamed_control_observed {stepIndex, tag, role, tool, nameSource}` (nameSource = computed | dom-fallback, so the census can weigh the browser's reading and the DOM fallback apart), `unnamed_control_finding:filed {issueId, observations}`, `unnamed_control_finding:rejected {reason}`. Acceptance suite: packages/engine-core/src/__tests__/unnamedControlSignal.test.ts (incl. the round-2 probes: <button><img alt> and <button><svg aria-label> => NO observation, <button><svg></svg></button> => observation, title => named, role=presentation and an a11y-tree-IGNORED node => excluded), BasePlaywrightService.computedAccessibleName.test.ts (the CDP probe, and when it is NOT fired), clickTargetAccessibleName.test.ts (the DOM fallback and its absence with the flag off), unnamedControlPromptParity.test.ts (flag-OFF byte parity on all four declaration surfaces and both prompt builders), RunnerRuntime.structuralFindings.test.ts. Eval fixtures: runner/textless-icon-button, plus runner/labelled-controls-fp-control as the false-positive control (the same contact form with every control labelled and MULTI-CONTROL steps, run with BOTH structural flags ON and judged `structural-control`: zero findings, zero drifts). runner/happy-form cannot serve as that control \u2014 it carries no `env`, so both flags are OFF while it runs.",designDoc:"packages/engine-core/src/unnamedControlSignal.ts",status:"active",added:"2026-09-09",graduation:{status:"gated",gate:"Graduates to defaultState:'on' by CENSUS, not by reading (steering ruling, review round 4). THE GATE: a nightly shadow census over at least 20 DISTINCT runner runs spanning the qa-exhaustive lane and the two clean #2577 lanes, with ZERO `a11y:would_file` rows against a control that HAS an accessible name. One false row blocks graduation until its CLASS is named and either excluded or accepted on the record \u2014 the count alone never graduates it. Read the rows split by `nameSource`: a `dom-fallback` row is the weaker evidence (the CDP probe could not answer) and a census that cannot tell the two apart would graduate on the wrong population. Additionally: zero `unnamed_control_finding:rejected` rows attributable to a correct finding the substantiation gate wrongly refused, and runner/labelled-controls-fp-control \u2014 the flags-ON false-positive control whose `structural-control` contract fails the lane on any filing \u2014 green across the same window. Until then it is opt-in per run/eval case; the shadow rows accrue with no env at all.",evidence:"nightly shadow census of a11y:would_file (>=20 distinct runner runs across qa-exhaustive + the two clean #2577 lanes, split by nameSource) plus unnamed_control_finding:{filed,rejected} and the runner lane false-positive controls",owner:"steering (Alex)",review:"2026-10-01"}},LABEL_DRIFT_DISCLOSURE:{key:"LABEL_DRIFT_DISCLOSURE",envVars:["AGENTIQA_LABEL_DRIFT_DISCLOSURE","AGENTIQA_EXPERIMENT_LABEL_DRIFT_DISCLOSURE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:'Runner-lane plan/app label-drift disclosure (#3091 milestone 3). THE SHAPE: a saved plan says `Fill the "Email address" field with \u2026`, the app has since renamed that field to "Login e-mail". The step does NOT fail \u2014 the resolver misses, returns its addressable-labels soft note, the agent re-addresses by ref or by the new name, and the step passes. That is the right outcome; what is missing is the RECORD, so the customer cannot tell that their plan text has gone stale. WHAT ON ENABLES: on a successful label-addressed type or click, the RESOLVER ITSELF answers, per control name the plan step STATES, whether this page addresses by it \u2014 same in-page matcher (`typeByLabel`\'s rank ladder incl. its CONTAINS tier / `resolveClickableByLabel`\'s `matchRank`), same candidate set (`collectLabels` per editable; `accessibleName` per clickable) \u2014 and returns `{planLiteral, resolves, rank?, matchedLabel?}` on `ActionMetadata.planLabelResolutions`. That verdict rides the ToolCallResult side channel (never LLM-facing) and is the ONLY evidence the disclosure accepts for "the page does not answer to the plan\'s name". WHY IT IS THE RESOLVER AND NOT THE RUNNER (review round 3, 2026-09-09): round 2 read `typeByLabel`\'s addressable-label enumeration and judged membership with a LOCAL ladder, and both were narrower than the resolver whose miss they claimed to report \u2014 no CONTAINS tier, and one label per field instead of `collectLabels` \u2014 so `Message` ~ "Your message here", `Name` ~ "What is your name?", `email` ~ "E-mail address", `Prenom` ~ "Pr\xE9nom" and a field whose `<label>` is still rendered beside its `aria-label` were all booked as renames on apps nothing had renamed. Both the local ladder and the enumeration census are DELETED. WHAT CAN DRIFT: only a control name the step QUOTED. A disclosure is a claim that a NAME went stale, so it is made only about a plan that states one; `into the chat input` describes a control rather than naming it, and a composer whose only name is "How can I help you today?" genuinely does not answer to "chat" \u2014 no resolver-based rule could rescue that shape. Cost, accepted: a plan that names its controls without quoting them discloses nothing. THE THREE REFUSALS, in order: MATCH-ANY (`sameControlName` \u2014 a case/punctuation/DIACRITIC-folded containment fold, deliberately more generous than any resolver and a SUPPRESSOR ONLY: it can silence a disclosure and can never create one), KIND + UNIQUENESS (a type compares against the step\'s FIELDS, a click against its buttons and links; two candidates of the acting kind abstain), and the RESOLVER\'S MISS (`resolves === false`; an absent verdict abstains, so a coordinate type, a ref click, an option pick or a dialog dismissal can never disclose). The engine side of the comparison is the label the RESOLVER matched, read from the engine-internal ToolCallResult side channel \u2014 `clickMatchedName` for a click, `writeMatchedLabel` for a write. Round 5 (MED-1) moved the click half there: it used to be read from the MODEL-FACING `response.matchedName`, which is gated on this flag, so under the shadow default `usedLabel` was empty and the census was structurally blind to the ENTIRE CLICK PATH \u2014 the population round 3\'s banner-then-button defect lived in, and a census silent on a population is not evidence that the population is clean. `typedIntoField` is deliberately NOT used: it comes from `getFocusedFieldName`, a third formula that can answer with an internal `name` attribute which is not a user-visible label at all. DRIFT IS A STEP-END JUDGEMENT (review round 4, 2026-09-09). A drift is booked from ONE action but claims something about the whole STEP, and round 3 retracted only on a later LABEL-ADDRESSED action \u2014 so a step that reached the plan\'s control by REF, by coordinates or by keyboard could neither re-probe nor retract, and the review\'s banner-then-button-by-ref repro shipped a "your plan text has gone stale" note about a page rendering exactly the named button. A row is disclosed ONLY when the literal missed on EVERY mid-step probe AND on a final probe at STEP END \u2014 the same function, same census, run once more on the page the step left behind (`IComputerUseService.probePlanLabelResolutions`, both resolvers in `probeOnly` mode, called at the `signal_step` transition and once more at run_complete for the step the run ended on) \u2014 AND at least one label-addressed action in the step succeeded via a DIFFERENT matchedLabel. Any resolving probe at any point retracts (`label_drift_retracted`), and an UNANSWERABLE step-end probe leaves the step unsettled, which discloses nothing: an unmeasured page is not an empty one. Diag `label_drift:step_end_probe {stepIndex, literals, resolved, withdrawn, surviving}`. THE PROBE\'S CENSUS IS THE PAGE, NOT THE RESOLVER\'S REACH (review round 4, HIGH-1 root). The resolvers address only this document\'s light DOM, so a plan field rendered inside an OPEN SHADOW ROOT or a SAME-ORIGIN IFRAME made the probe answer "nothing on this page answers to that name" about a page rendering exactly that field. The probe now walks every root the page renders controls in (this document, open shadow roots transitively, same-origin frame documents; bounded at 40 roots) with the SAME ladder and the SAME per-field `collectLabels` census, and the `instanceof` tag tests are realm-safe so a cross-frame node is classified correctly. The ADDRESS is untouched. Widening is one-directional and therefore safe: a bigger census can only turn `resolves:false` into `true`, i.e. only ever SILENCE. THE PROBE CANNOT FAIL THE ACTION (review round 5, HIGH-1). Round 4 ran the probe INSIDE the address\'s `page.evaluate` and read `host.shadowRoot` unwrapped, so a page defining a throwing `shadowRoot` getter rejected that evaluate and KILLED THE TOOL CALL \u2014 a failed step the `=0` state does not have, in the state this ships in by default. The probe now runs in its OWN `probeOnly` evaluate behind one Node-side seam (`runPlanLiteralProbe` \u2014 the single implementation used by the click resolver, the editable resolver and the step-end re-probe) which wraps the whole evaluate, and every in-page read is wrapped per root and per candidate. A failure at either layer ABSTAINS \u2014 no verdict, which is silence at both seams \u2014 and emits one `label_drift:probe_failed {reason, kinds, literals}` row; never a `resolves:false`, because unanswerable is not absent, and one poisoned read poisons the WHOLE verdict rather than quietly narrowing the census. AND IT CANNOT COST THE ACTION BY TIMING EITHER (round 6, HIGH-1r): round 5 ran the probe BETWEEN the address\'s `setAttribute(marker)` and the `locator.boundingBox()` that consumes the marker, so a page that MUTATES from a getter read \u2014 `location.replace()`, a getter that replaces the marked input, or a plain SPA container re-render, all three deterministic in real Chromium \u2014 stranded the write with `locator.boundingBox: Timeout 3000ms exceeded` while the probe reported success (`probe_failed = 0`). The probe now runs BEFORE the address (`probeThenAddress`), so the marker does not exist while the page is read; a probe read that navigates the page costs ONE re-issue of the address (`label_drift:probe_navigated`), reachable only when a probe ran and therefore never at `=0`. Stated skew: the census is of the page one evaluate before the address rather than one after it. FRAME DESCENT IS REALM-SAFE TOO (round 5, HIGH-2): the walk descended with `host instanceof HTMLIFrameElement`, which is false for an `<iframe>` that lives in a CHILD document, so the census stopped ONE frame deep (measured `{rootCount:2}` on a page rendering three roots) and booked a false miss on a field the page renders at depth 2. It descends by tag name now, and the two remaining cross-realm `instanceof`s reachable through `collectLabels` are gone. A CROSS-ORIGIN frame answers `contentDocument === null` without throwing and is skipped SILENTLY \u2014 a narrower census, which can only silence. A drift is recorded into a per-step ledger keyed like `_stepActionErrors`, capped at 4/step and de-duplicated per (plan, used) pair, and `applyLabelDriftDisclosure()` appends one sentence naming BOTH literals to each such step\'s `note` at cascade stage 19b\u2032, immediately after applyStepReplayDisclosure. WRITES NO VERDICT \u2014 not a status, not a criterion, not a polarity \u2014 exactly the step_replay_disclosed contract; and it is NOT a propose_update: the runtime discloses, the customer curates. SHADOW-FIRST (review round 4 \u2014 the RUNNER_STEP_ANTI_REGRESSION/#3088 convention). With NO env set the DETECTION runs \u2014 literals injected, the ACTING resolver probed on every label-addressed action (click as well as type, since round 5 MED-1), BOTH resolvers probed at step end, the ledger kept \u2014 and one `label_drift:would_disclose {stepIndex, planLiteral, matchedLabel, evidence}` row is emitted per surviving pair at 19b\u2032, while NOTHING customer- or model-facing changes: no step note, and `matchedName` is still NOT forwarded to the model. `=1` writes the note. `=0` \u21D2 the plan literals are never injected into the per-action config, so both resolvers skip the probe entirely (no extra DOM work, no metadata), there is no ledger, no step-end probe and no note, and the click response is byte-identical to the pre-change shape. SUPPRESSOR COST, measured at review (round 4, MED). `sameControlName` folds case, whitespace, punctuation, separators AND diacritics, then accepts containment IN EITHER DIRECTION \u2014 so it silences `Email`\u2192`Login e-mail`, `Name`\u2192`Surname`, `Name`\u2192`Username`, `Password`\u2192`Confirm password`, `Send`\u2192`Send message`, `Email`\u2192`E-Mail`/`Emails`, and a handful of accidental cross-word collisions (`To`/`Total`, `ID`/`Video`, `Card`/`Discard`). The disclosure therefore fires only on a rename that REPLACES the word entirely; renames that ELABORATE a name \u2014 the common kind \u2014 are silent. A second narrowing on the type path: `collectLabels` feeds the probe a field\'s `id` and `name`, and the type ladder\'s CONTAINS tier accepts either direction, so any 1-2 character `id` on any editable resolves almost any literal. Neither is a defect \u2014 both are the silence-biased direction \u2014 but the census must be read as a measurement of THAT population, never as "renames are rare". RESIDUALS, recorded (round 4): (1) a QUOTED DESCRIPTOR still fires \u2014 `Type "Hello" into the "chat" input` on a composer whose only name is "How can I help you today?" discloses, because quoting is a heuristic about how humans write, not a fact about what the app calls the control; the suppressor rescues most descriptors because they are folded substrings of the real label, and the composer prompt is the shape where it does not. (2) FIXED in round 4: the quote families now include German `\u201E\u2026\u201C`, guillemets `\xAB\u2026\xBB`/`\u2039\u2026\u203A` and CJK corner brackets, so a non-English-authored plan is no longer structurally invisible. Diag: `label_drift_detected {stepIndex, planLabel, usedLabel, requestedLabel, evidence, tool}` (evidence = resolver-miss, now the only class \u2014 a request BY the plan literal that succeeded means the literal resolves) at the moment of detection, `label_drift_retracted {stepIndex, planLabel, usedLabel, withdrawn, tool}` on a withdrawal, and `label_drift:disclosed {stepIndexes, notedSteps}` at 19b\u2032. Acceptance suite: packages/engine-core/src/__tests__/labelDriftDisclosure.test.ts (incl. the four round-2 false-positive shapes \u2014 dropdown option, cookie banner, dialog confirmation, neighbouring-field coordinate type \u2014 and the five round-3 never-renamed shapes, each pinned at ZERO disclosures), RunnerRuntime.structuralFindings.test.ts (detection, retraction, and the flag-OFF injection). Eval fixture: runner/label-renamed, plus runner/labelled-controls-fp-control as the no-drift control \u2014 a MULTI-CONTROL step whose STATED names the app renders under CONTAINS-tier labels ("What is your name?", "E-mail address", "Your message here"), run with both structural flags ON and judged `structural-control`. runner/happy-form cannot serve as that control \u2014 it carries no `env`, so the flag is OFF while it runs.',designDoc:"packages/engine-core/src/runner/labelDriftDisclosure.ts",status:"active",added:"2026-09-09",graduation:{status:"gated",gate:"Graduates to defaultState:'on' (the STEP_REPLAY_DISCLOSURE shape \u2014 disclosure-only behavior earns default-on) BY CENSUS, not by reading (steering ruling, review round 4). THE GATE: a nightly shadow census over at least 20 DISTINCT runner runs spanning the qa-exhaustive lane and the two clean #2577 lanes, with ZERO `label_drift:would_disclose` rows on a plan whose literal was NOT renamed. One false row blocks graduation until its CLASS is named and either suppressed or accepted on the record \u2014 the count alone never graduates it. The rows carry {planLiteral, matchedLabel, evidence, stepIndex} precisely so a false one can be read back to its class without opening the run. Read the census against the suppressor cost recorded in `gates` above: the surviving population is word-REPLACING renames only, so a low row count is expected and is not by itself evidence that the signal is quiet. runner/labelled-controls-fp-control \u2014 the flags-ON no-drift control whose `structural-control` contract fails the lane on any recorded drift \u2014 must stay green across the same window.",evidence:"nightly shadow census of label_drift:would_disclose (>=20 distinct runner runs across qa-exhaustive + the two clean #2577 lanes) with a manual read of every row, plus label_drift:step_end_probe and the runner lane no-drift controls",owner:"steering (Alex)",review:"2026-10-01"}},VERIFY_EVIDENCE_GATE_STANDOFF:{key:"VERIFY_EVIDENCE_GATE_STANDOFF",envVars:["AGENTIQA_VERIFY_EVIDENCE_GATE_STANDOFF","AGENTIQA_EXPERIMENT_VERIFY_EVIDENCE_GATE_STANDOFF"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Runner-lane (test_run) standoff-breaker for the DISPATCH-SEAM verify-evidence gate (`RunnerRuntime.maybeBlockActionUntilVerifyEvidence`). THE DEFECT (#2599): that gate refuses every non-evidence browser action on a `type=verify` step until the step has recorded explicit evidence, and it had NO counter, NO cap and NO kill switch \u2014 so a model that keeps trying to act (a `click_at` on a login wall it must clear first, a `hover_at` at step 10) was refused forever. The refusal is `isMetaTool`, and BaseRuntime skips the post-dispatch loop-detector CHECK for a meta result, so the stall safety net never armed either: measured as runs terminating with 0/6 and 0/10 executed steps and no verdict. WHAT ON CHANGES: consecutive refusals are counted PER VERIFY STEP in the dedicated `_verifyEvidenceGateBlockCounts` map, and on the SECOND the runtime captures the step's evidence itself via `autoCaptureVerifyEvidence` \u2014 the same auto-capture the sibling `maybeIsolateVerifyMarker` has used since AG-7661: a real `screenshot` down the normal `executeAction` -> `executeBrowserAction` path, so it is stamped `planStepIndex = currentPlanStepIndex()` (the ACTIVE verify step) and criterion 3 of claim batch.verify-never-blind holds by construction. The step then enters `_verifyEvidenceStepIndexes` permanently, so the THIRD attempt dispatches and `noteStepExecuted` finally raises `_maxStepEverExecuted`. Bounded at 2 refusals per verify step; the capture fires at most once per step. The capture carries a GATE-SPECIFIC model-facing note (the sibling's note talks about a co-located marker the model never emitted here) and is a NORMAL non-meta result, so the loop detector sees it like any other screenshot. THE COUNTER IS DEDICATED, NOT THE SHARED `_consecutiveBrowserShortCircuits` STREAK, for two reasons: that streak is fed by other short-circuit sources (auto-evidence reuse, the off-plan label-click rejection, the same-value write breaker), so the gate would capture on a standoff it did not cause; and \u2014 load-bearing \u2014 ANY real dispatch resets it, INCLUDING the plain `wait` that #2952 just exempted, and the measured composite shape is `wait, click, wait, click ...`, on which a shared-streak breaker would never reach 2. SECOND HALF, SAME SWITCH: the remaining bounces stop carrying `resetLoopDetector: true` \u2014 this is the first of the four NAMED RESIDUAL sites listed under RUNNER_SIGNAL_STEP_FORWARD_RESET. A meta block is already excluded from the post-dispatch loop check, so the flag only DISARMED the stall net on every refusal (`buildVerifyMarkerIsolationBlock` documents the same policy verbatim); with the bounce now bounded at 2 per step the behavioral delta is small and bounded, which is why it rides this change rather than shipping alone. NOT TOUCHED: the RUN-CLOSE half (`buildVerifyEvidenceRequiredResult`) stays unbounded, unflagged by this switch and diag-silent; the gate's exempt sets (`VERIFY_EVIDENCE_ACTIONS`, `isPassiveSettleAction`) are unchanged; and no step, criterion or run status is re-graded by this flag \u2014 it changes only WHETHER an action reaches the browser and WHEN the step's own evidence exists. Diagnostics, both new (the gate previously emitted nothing of its own \u2014 the only trace was `handleToolCall`'s streak row): `verify_evidence_gate:blocked {stepIndex, action, count}` on each refusal and `verify_evidence_gate:standoff_capture {stepIndex, action, priorBlocks}` on the capture. OFF (`=0` under either spelling) restores BOTH halves byte-identically: unbounded refusal AND `resetLoopDetector: true`. Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.verifyEvidenceGateWait.test.ts (sections 7-12: bounce -> bounce+capture -> dispatch, capture-once-per-step, per-step isolation, the interleaved-`wait` trap, the planStepIndex stamp, and both flag states incl. the await pin).",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-09-09"},REPEATED_SIDE_EFFECT_GUARD:{key:"REPEATED_SIDE_EFFECT_GUARD",envVars:["AGENTIQA_REPEATED_SIDE_EFFECT_GUARD","AGENTIQA_EXPERIMENT_REPEATED_SIDE_EFFECT_GUARD"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Phase 3 Layer 3 of the Lio verdict-integrity track. SHADOW-FIRST. THE DEFECT: `novel_url` is an UNLIMITED structural-progress reset in the runner lane \u2014 NOVEL_URL_BUDGET=40 is consulted only when rearmUrlNovelty is on, and BaseRuntime sets that for the assistant_v2 lane only \u2014 while resetForNewStep additionally wipes seenUrls every declared step. On prod tp_d1360115 every 'Duplicate Request' click minted a NEW draft id and therefore a new URL, so ONE control produced 252 credited progress milestones, the 30-action structural breaker never armed once in 300 iterations, and the same false signal ALSO reset sameGoalAbort and the supervisor verdict freshness (the triple defeat). The run left 9 junk draft duplicates in the customer's tenant. WHAT =1 ENABLES: per-TARGET novel-URL credits inside LoopDetector.updateUrl, keyed by the engine-resolved click-target signature (targetSignature: tag|role|accessibleName|text, threaded from response.clickTarget through ToolCallResult.loopDetectorUpdate.clickTargetSignature) and falling back to the detector's own buildKey for click-class actions the engine did not resolve. The first K=3 novel URLs from one target (\xD72 in exploration mode) are credited exactly as today; past the budget the actionsSinceProgress reset is SUPPRESSED and a distinct `repeated_side_effect` signal is emitted instead \u2014 which BaseRuntime routes AWAY from sameGoalAbort.recordMilestone() and supervisorVerdictFreshness.recordProgress(), so a suppressed signal cannot disarm the supervisor guards either. BOTH PROGRESS CHANNELS, NOT ONE (adversarial review 2026-08-06 \u2014 the first build was a placebo): an over-budget verdict carries from updateUrl to the SAME action's updateScreenContent, because BaseRuntime sets trackNovelScreenContent:true on every surface and a page that mints an entity necessarily renders new content (its own id), so each suppressed click produced a novel snapshot hash one line later, credited `novel_screen`, and re-opened all three defeats. The snapshot hash is still recorded (a genuine later revisit is never re-credited) but the reset is withheld and the novel-screen budget is NOT spent. Under the production configuration the structural breaker now arms on click 32 \u2014 the same bound the URL-only tests assert. Credits follow the URL subsystem's scoping exactly (per declared step in the runner lane, turn-scoped when rearmUrlNovelty is on), which is what keeps a legitimate wizard safe: a fresh step gets a fresh budget, a stable-URL SPA funnel never enters the guarded branch at all and keeps its novel_screen progress in both flag states. WIZARD SAFETY is pinned under the PRODUCTION config (linear-wizard + SPA-funnel) plus the loop-eval replay corpus. KNOWN LIMIT, pinned as a test rather than hidden: in the RUNNER lane resetForNewStep refills the per-target credits along with seenUrls and actionsSinceProgress (the pre-existing LOOP_URL_NOVELTY_REARM scoping the runner lane never opted into), so a run that re-declares a step per click refills 3 credits per click and is NOT bounded by this layer \u2014 the measured incident had that shape, and REDIRECT_FLIPFLOP_GUARD's run-scoped 24-redirect budget is what bounds it. COMPANION FIX, unconditional (not gated by this flag): LoopDetector.buildKey now reads args.label for click_at / double_click_at / right_click_at / hover_at / type_text_at. `label` is a first-class targeting modality PREFERRED over ref/x/y, so every label-addressed click previously collapsed onto the coordinate default 'click_at:0,0' \u2014 clicking 'Submit' and clicking 'Cancel' were the same key. BLOCK WORDING, ENFORCE-ONLY: when one target dominates the suppressed credits (>=2), the structural force_block message says the control WORKED and each click committed another record, rather than the generic 'the task is not converging' \u2014 the pattern is the defect, not the control, and that wording reaches the filed issue. It is gated on enforce because the suppression COUNTER fills in both states: ungated, the default state rewrote the model-facing block reason (which also feeds the loop-breaker judge's contextHints) with text that was FALSE there \u2014 in shadow those repeats did credit progress \u2014 and attributed the block to the minting control when something else caused it. DEFAULT OFF = SHADOW, NOT INERT AND NOW GENUINELY DIAG-ONLY: the per-target accounting runs in full and emits agent_lifecycle repeated_side_effect_would_suppress {target, channel:'novel_url'|'novel_screen', url, creditsGranted, creditBudget, suppressionCount, actionsSinceProgress}, while both credits are granted byte-identically to today and no model-facing string changes. An explicit =0 under either spelling switches the accounting itself off. Acceptance suite: packages/engine-core/src/__tests__/LoopDetector.repeatedSideEffect.test.ts (incl. the 'PRODUCTION configuration' and 'shadow purity of the block wording' describes) + LoopDetector.test.ts (buildKey label) + BaseRuntime.repeatedSideEffect.test.ts (the signal does not reach the milestone resets).",designDoc:"docs/plans/2026-08-06-lio-terminal-verdicts-and-zero-grade-bounce-design.md",status:"active",added:"2026-08-06",graduation:{status:"gated",gate:"A shadow census of repeated_side_effect_would_suppress on the nightly lanes showing (a) it fires on the entity-minting eval and the tp_d1360115 replay shape, and (b) ZERO would-suppress rows on the wizard/funnel fixtures and on runs that completed normally \u2014 i.e. no legitimate multi-page flow is having its progress signal withdrawn. TWO risks the census must answer, one per channel now that both are guarded: a genuinely-progressing flow whose per-page 'Next' button shares one accessible name spends its K=3 within a single declared step (novel_url rows), and a stable-target flow that renders genuinely-new content per action loses that content credit too (novel_screen rows \u2014 read them separately via the row's `channel` field). SEPARATE, AND REQUIRED BEFORE THE RUNNER LANE GRADUATES: decide the runner-lane credit scoping. Today resetForNewStep refills the credits every declared step there, so a step-bouncing run \u2014 the measured incident's own shape \u2014 is not bounded by this layer at all; either turn-scope the credits in the runner lane (with its own wizard census, since that removes the per-step refill this design leans on) or graduate the chat lane only and state that REDIRECT_FLIPFLOP_GUARD is the runner-lane bound.",evidence:"agent_lifecycle repeated_side_effect_would_suppress rows (both channels) on qa-exhaustive and the loop-detection eval lane, plus the eval loop-detection/entity-minting-wander flipping from wall-clock-backstop termination to a structural block inside its 60-iteration budget. The eval is a CHAT-lane case, where credits are turn-scoped, so it certifies the chat lane only \u2014 and its fixture must render DISTINCT content per minted draft, otherwise the run's screens hash identically, the novel_screen channel is inert for reasons unrelated to this guard, and the eval measures a shape production never produces.",owner:"steering (Alex)",review:"2026-08-13"}},RUN_PROGRESS_BACKSTOP:{key:"RUN_PROGRESS_BACKSTOP",envVars:["AGENTIQA_RUN_PROGRESS_BACKSTOP","AGENTIQA_EXPERIMENT_RUN_PROGRESS_BACKSTOP"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Judge-gated run backstop in the chat/explorer (assistant_v2) runLoop: a wall-clock (12 min, primary), iteration (200), and cumulative prompt+completion billed-token (6M) ceiling \u2014 all well above p99 legit chat turns and below the 300-iteration explorer child cap. At a ceiling breach the loop-vision progress judge (the same fail-closed SupervisorService judge the loop breaker uses, temp-0 / thinkingBudget-0) is consulted ONCE with a goal-anchored progress question, rather than blind-terminating: "progressing" grants a BOUNDED extension (each ceiling raised by one window) up to a hard cap of RUN_BACKSTOP_MAX_EXTENSIONS=2 (worst case ~36 min), after which the run terminates regardless of the judge; "wandering" terminates with a judge-confirmed "not getting closer to the objective" message; any no-signal case (no judge wired in this lane, judge error / timeout / unparseable) fails CLOSED to a terminate with a neutral "hit the safety limit" message. Ends with blockKind=backstop / endKind=run_backstop, NOT loop_block, so it is not counted toward the session structural-loop cap. No interactive ask and no cross-turn state (the escalate\u2192ask_user ladder was removed; the judge consult is synchronous and re-derived per breach). Off removes all three ceilings (only bound remains iteration<=maxIterations=300, ~2.5h). Never a crash \u2014 the emit path fails open while the judge fails closed. Runner (test_run) lane never opts in.',designDoc:"docs/plans/2026-07-20-loop-safety-rearm-and-backstop-design.md",status:"active",added:"2026-07-20"},CLICK_AT_INTERACTIVE_DESCEND:{key:"CLICK_AT_INTERACTIVE_DESCEND",envVars:["AGENTIQA_CLICK_AT_INTERACTIVE_DESCEND","AGENTIQA_EXPERIMENT_CLICK_AT_INTERACTIVE_DESCEND"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"A coordinate click_at/double_click_at whose point resolves to a NON-interactive element descends to an interactive descendant within 16px and clicks it via locator, plus a no-effect advisory on a zero-mutation same-URL click; off restores the raw mouse.click(x,y) with no advisory.",designDoc:"docs/plans/2026-07-11-click-at-interactive-descend-design.md",status:"active",added:"2026-07-11"},LABEL_CLICK_TWIN_COLLAPSE:{key:"LABEL_CLICK_TWIN_COLLAPSE",envVars:["AGENTIQA_LABEL_CLICK_TWIN_COLLAPSE","AGENTIQA_EXPERIMENT_LABEL_CLICK_TWIN_COLLAPSE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'BOTH twin-collapse kinds in `resolveClickableByLabel`, plus the actionable-click stage they feed (`pickActionableTwin` / `probeClickTopmost`). (1) Same-destination: a click_at/double_click_at label ties at the best rank across <= 3 elements that all resolve to ONE absolute http(s) href which is not the current page, opened in the same browsing context (equal `target`), each inside a landmark, whose OUTERMOST landmarks span >= 2 distinct region kinds including a header or a footer \u2014 the header-nav / footer-nav twin, e.g. "Security" on the pricing page. (2) Containment: the tied candidates nest (`<a href><button>Log in</button></a>`), so the innermost one is clicked. Either way the collapsed control is clicked only after it is proven topmost where it will be clicked; if nothing can be, nothing is clicked. Off restores the pre-fix behavior \u2014 any rank tie returns `ambiguous` and nothing is clicked. It does NOT gate the diagnostics on the no-click paths: the enriched ambiguity note (landmark / visibility / href hints) and the already-on-this-page note for twins that link to the current URL.',designDoc:"packages/engine-core/src/BasePlaywrightService.ts",status:"active",added:"2026-08-02",notes:'Root-cause fix for the #2024 blocked-loop class: the ambiguous no-op pushed the agent onto snapshot refs, which `_snapshotForAI` (refs: "interactable") never mints for an auto-hidden `fixed` header \u2014 so both recovery paths were dead and LoopDetector force-blocked on screen_cycling.'},LOOP_BLOCK_ATTRIBUTION:{key:"LOOP_BLOCK_ATTRIBUTION",envVars:["AGENTIQA_LOOP_BLOCK_ATTRIBUTION","AGENTIQA_EXPERIMENT_LOOP_BLOCK_ATTRIBUTION"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:`Evidence-gated loop-block finding attribution: when a force-block's dominant repeated click target is a non-interactive element or a no-op self-anchor, suppress the false "Page appears stuck" auto-issue instead of filing it. Off restores the old unconditional filing (subject only to the AG-6107/AG-6490 gates).`,designDoc:"docs/plans/2026-07-11-loop-block-finding-attribution-design.md",status:"active",added:"2026-07-11"},CLICK_AFFORDANCE_CAPTURE:{key:"CLICK_AFFORDANCE_CAPTURE",envVars:["AGENTIQA_CLICK_AFFORDANCE_CAPTURE","AGENTIQA_EXPERIMENT_CLICK_AFFORDANCE_CAPTURE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Phase-2 of loop-block attribution: captures click-handler affordance evidence onto clickTarget (page-context inline/React/Vue signals + a CDP addEventListener probe on the rare non-interactive branch) so a dominant non-interactive target with a real-but-dead handler files a 'Custom control appears unresponsive' issue instead of being suppressed agent-side. Off \u21D2 no affordance field emitted \u21D2 the custom-control classifier branch can never fire (falls back to the #1477 status quo); it does NOT re-enable filing on a bare non-interactive target.",designDoc:"docs/plans/2026-07-11-custom-control-unresponsive-detection-design.md",status:"active",added:"2026-07-11"},DECORATIVE_NON_CONTROL_ROUTING:{key:"DECORATIVE_NON_CONTROL_ROUTING",envVars:["AGENTIQA_DECORATIVE_NON_CONTROL_ROUTING","AGENTIQA_EXPERIMENT_DECORATIVE_NON_CONTROL_ROUTING"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:`Affordance-mismatch RE-ROUTING of an "unresponsive control" report (#2060). A report_issue whose title/description claims a dead/unresponsive control and names a recently-clicked element the click evidence determinately shows carries NO handler of any kind (inlineHandler false, frameworkHandler null, directListener false AND activationListener false \u2014 no element-own listener for click/pointerdown/mousedown/pointerup/mouseup/keydown/keyup/touchstart), no interactive tag/role, no author-declared control affordance (tabindex >= 0, contenteditable, aria-expanded/haspopup/pressed/controls) and no hard-failure evidence on that element's own clicks, is ACCEPTED and RE-LABELLED rather than refused: title becomes 'Looks clickable but is not wired: <label>', severity is capped at low, a runtime note naming the negative probes is appended, and evidence.claimRelabel records the model's original title/severity. When the SAME report also names a handler-bearing control the claim is bounced ONCE (twice at most) asking for separate filings, then fails open and files as-is, so the genuine finding is never lost. Also gates the probe widening that produces activationListener (the click-only directListener is unchanged, so the loop-block custom-control classifier is untouched). Off \u21D2 no re-label, no bounce, no activationListener capture \u21D2 byte-identical to the post-#2957 behavior. It is a DEDICATED switch: CLICK_AFFORDANCE_CAPTURE=0 would also kill the POSITIVE dead-control detection, so it is not a rollback for this feature.`,designDoc:"docs/plans/2026-07-11-custom-control-unresponsive-detection-design.md",status:"active",added:"2026-09-02",notes:"Refuse-vs-re-route was the design fork. A failed-hydration / never-wired button has EXACTLY this probe signature (role-less, unfocusable, no handler of any kind), so a hard refusal drops a real bug class with no honest alternative claim. The re-label keeps the finding, changes the claim, and caps the grade; the hard-failure escape (a console/page error captured on that element's own click) keeps a broken control out of the re-label entirely."},CLICK_EFFECT_SIGNAL:{key:"CLICK_EFFECT_SIGNAL",envVars:["AGENTIQA_CLICK_EFFECT_SIGNAL","AGENTIQA_EXPERIMENT_CLICK_EFFECT_SIGNAL"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:`Post-click effect signal on EVERY click path (coordinate, retargeted, ref and label \u2014 click_at and double_click_at), SHADOW-FIRST. After the click the engine waits up to a 500ms settle window for the DomObserver to record any DOM/text/attribute mutation (returning the instant one lands), then compares the URL and the auto-accepted-dialog tally. Any movement \u21D2 an effect was observed. No movement \u21D2 the click STILL reports SUCCESS (a click that legitimately changes nothing must never become an error) and the feature's only output is agent STEER. THREE STATES, not two: no env (default) = SHADOW \u2014 detection runs and emits the click_effect:would_signal diag, but no advisory, no effectObserved metadata and no prompt-byte change reach the model; =1 = ENFORCE \u2014 the hedged re-observe advisory rides the successful tool result, effectObserved lands on the ToolCallResult side channel, the wait_for_element "Do NOT retry" coaching softens to its effect-aware wording and the runner prompt gains the re-observe-after-corrective-action line, with a click_effect:signaled diag; =0 = FULLY OFF \u2014 not even the probe runs, so there is zero added latency and click results plus every prompt byte are identical to pre-change behavior. Targets the 2026-07-29 vrgate false-FAIL (run_b72f5099-cee2-4063-b109-1961c55c9898), where a click_at on a rendered React button ~1.3s after a Next.js client navigation reported success while the onClick never fired (painted but not yet hydrated), the agent trusted the success, and the plan FAILED for a defect that did not exist. EXECUTOR-SIDE ONLY: it changes no verdict path, writes no step/criterion/run status, and adds no gate \u2014 it changes what the AGENT is told, and the agent still grades. Fail-safe by construction: an in-page probe that cannot run (CSP, cross-origin frame, document destroyed mid-navigation) yields NO verdict rather than a false "no effect"; navigation/dialog evidence is evaluated BEFORE the mutation counters so the one case where the probe reliably dies is the case the URL alone already proves; canvas-dominant surfaces and native property-only controls (checkbox/radio/select/text input \u2014 where a spurious "re-perform once" would TOGGLE the control back) are advisory-suppressed; and the pre-existing #1476 dead-coordinate peek keeps its exact timing and semantics. Canvas surfaces additionally skip the PROBE (not just the advisory) once a capture has classified the page as canvas-dominant, so a whiteboard/design flow never pays the settle window per click for a verdict that is suppressed on arrival; those clicks are counted as click_effect:probe_skipped. A SECOND probe skip covers the unobservable case: in SHADOW, where the diag is the feature's only output, a platform with no BasePlaywrightService.diagLog wired skips the probe entirely rather than pay the settle window for a measurement nothing can read (ENFORCE always probes \u2014 its output is agent-visible behavior, not telemetry).`,designDoc:"packages/engine-core/src/clickEffectSignal.ts",status:"active",added:"2026-07-29",notes:"Default OFF (shadow). Detection runs in shadow always unless explicitly =0 \u2014 the PIN_PAGE_GROUNDING / INTERACTION_CLEARS_PRESENCE_ORACLE shadow-first precedent \u2014 because a shadow that skipped the settle wait would measure a DIFFERENT detector than the one enforcement ships, and its numbers would not predict the ON behavior. The one deliberate non-identity in shadow is therefore TIMING, not tool-result bytes: a click that has mutated nothing yet pays up to 500ms of settle before the state capture that follows it (an effective click returns on the first in-page read and pays one evaluate round-trip). Stated plainly because it is a real, if small, behavior delta in the DEFAULT state \u2014 the post-click screenshot/snapshot of a no-effect click is taken up to 500ms later than before, which is more settled, not less faithful. AGENTIQA_CLICK_EFFECT_SIGNAL=0 removes even that and restores exact timing parity. killSwitch resolves the no-env value from this defaultState (#1729), so GRADUATING = flip defaultState to 'on' AND add CLICK_EFFECT_SIGNAL to INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Read sites: packages/engine-core/src/BasePlaywrightService.ts (probeClickEffect / resolveClickEffect on clickAt, clickByRef, clickByLabel), packages/engine-core/src/waitToolCoaching.ts (waitNotFoundError picks the coaching variant), packages/engine-core/src/tools/browserTools.ts (getFailureHandlingPrompt), packages/engine-core/src/RunnerRuntime.ts (buildRunnerPrompt re-observe line); pure decision logic in packages/engine-core/src/clickEffectSignal.ts. Acceptance tests: clickEffectSignal.test.ts (pure verdict matrix incl. every suppression and the unknown-probe degrade), BasePlaywrightService.clickEffectSignal.test.ts (per-path wiring + shadow/enforce/off + the canvas-memo skip + degraded-note preservation on the ref path), clickEffectPromptParity.test.ts (flag-OFF prompt BYTE-parity across all four prompt surfaces). The 500ms settle cost is bounded on canvas-dominant surfaces by the SessionState.lastCaptureCanvasDominant memo: captureState records canvasDominant on every capture and probeClickEffect returns null (emitting click_effect:probe_skipped, reason canvas-memo) while it is set, because the canvas verdict was suppressed anyway. It is bounded a second way by the OBSERVER check: in shadow, where the diag is the only output, probeClickEffect returns null before the settle window when this.diagLog is unwired \u2014 an unobservable measurement is pure latency. Enforce is exempt (its verdict is agent-visible behavior). Acceptance: the shadow/enforce no-observer pair in BasePlaywrightService.clickEffectSignal.test.ts.",graduation:{status:"gated",gate:'SOAK SOURCE \u2014 read this first. click_effect:* rides BasePlaywrightService.diagLog, which was wired ONLY in DesktopPlaywrightService until the CloudPlaywrightService sink resolver landed (2026-07-30), so before that commit desktop runs were the ONLY source and a census of 0 from a cloud/staging replay lane is NON-EVIDENCE (the events were structurally unemittable there, not absent). Any soak reading must therefore be taken from engine builds that carry the cloud diagLog wiring; older cloud replays cannot be counted, and in shadow an unwired lane now skips the probe outright so it contributes no fires by construction. Staging shadow soak on click_effect:would_signal establishes (a) the no-effect rate per click path, (b) that the fires are dominated by genuinely ineffective clicks rather than by the known effect-free-but-legitimate classes, and (c) that no page class fires it continuously. FOUR NAMED REQUIREMENTS, all of which must be answered before the flag moves past shadow. (1) BLOCKER \u2014 THE NON-IDEMPOTENT SUBMIT. `<button>` and `<a>` are deliberately NOT in the property-only suppression set, so a form submit whose only feedback is a server round-trip (no spinner, no optimistic DOM write) reads as no-effect and, under enforce, receives the "re-perform the action ONCE" advisory. That directly contradicts the failure-handling prompt already shipped in browserTools.getFailureHandlingPrompt, which names "status: ok with url unchanged and no visible DOM change" as a SIGN OF AN IN-FLIGHT WRITE and instructs "do NOT re-click ... Re-clicking the same button while a write is in flight is a no-op for the user and burns retry budget" \u2014 and on a non-idempotent endpoint a second submit is not merely wasted, it can double-charge, double-book or double-post. The soak must show ZERO enforce-mode advisories on non-idempotent submits (classify fires by targetTag/targetRole plus the accessible name against a submit-shaped vocabulary, and cross-check each against pendingRequests at the moment of the fire), OR enforce must first gain a submit-shaped suppression (e.g. withhold the advisory whenever a same-origin request was in flight at probe time, or whenever the target is a submit-shaped control) \u2014 either outcome, and NEITHER may be waived. (2) IFRAME BLIND SPOT. Every counter read (peek / waitForMutation / flush) goes through page.evaluate, which runs in the MAIN frame only, and a top-document MutationObserver does not cross an iframe boundary \u2014 so a click whose whole effect renders inside an embedded frame (payment element, third-party booking/chat widget, embedded editor preview) reads as no-effect however well it worked. There is no target shape to suppress on, so iframe-hosted effects are a KNOWN-LEGITIMATE fire class the soak must be able to account for and subtract, exactly like property-only controls, downloads, clipboard and focus-only clicks; it is enumerated in the clickEffectSignal.ts header for the same reason. (3) SHADOW IS NOT INERT \u2014 READ THE SOAK ACCORDINGLY. The probe delays the post-click captureState by up to the full 500ms settle window on any click that has mutated nothing yet, INCLUDING in the default shadow state. Captured screenshots/snapshots on those clicks are therefore of a MORE SETTLED page than pre-change, so a shadow-vs-baseline comparison that shows different captured content on no-effect clicks is expected and is not evidence of a detector defect; only the =0 state is timing-identical to pre-change. (4) HYDRATION EMPIRICAL CRITERION. The soak must answer "what would run_b72f5099-cee2-4063-b109-1961c55c9898 have produced?" \u2014 i.e. for clicks landing inside a post-navigation hydration window, what fraction show mutationCount > 0 or attrCount > 0 (some other script mutated the page, so the detector stays SILENT and would not have rescued the incident) versus both counters at 0 (the detector fires and the advisory would have reached the agent). Both counters are already in the click_effect payload, so the query is: filter click_effect:would_signal to fires within ~2s of a navigation, then bucket on (mutationCount > 0 || attrCount > 0). A silent-dominated result means this feature does not fix its own motivating incident and enforcement is not justified on that basis. Enforcement additionally needs an inversion showing the advisory does not induce a harmful second click on a toggle.',evidence:"staging click_effect:would_signal diag events FROM AN ENGINE BUILD THAT CARRIES THE CLOUD diagLog WIRING (rate + clickPath/suppressReason/targetTag/targetRole breakdown, plus the mutationCount/attrCount split inside the post-navigation hydration window and the submit-shaped-target cross-check against in-flight same-origin requests) + click_effect:probe_skipped counts for the canvas-memo skips + the engine-core clickEffectSignal / BasePlaywrightService.clickEffectSignal / clickEffectPromptParity unit suites + apps/execution-engine/__tests__/CloudPlaywrightService.diagLog.test.ts for the cloud emit path itself",owner:"steering (Alex)",review:"2026-08-08"}},HALT_CONCLUDING_TURN:{key:"HALT_CONCLUDING_TURN",envVars:["AGENTIQA_HALT_CONCLUDING_TURN","AGENTIQA_EXPERIMENT_HALT_CONCLUDING_TURN"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"ONE final, tool-restricted turn offered to the runner when the MACHINERY (loop detector / vision supervisor / run backstop) halts a saved-plan run that still has status=running AND the deterministic unresponsive-control window holds \u2014 i.e. exactly the runs that end today with terminationReason=supervisor_halted and NO run_complete, hence no verdict at all, narrowed to the halt shape the change was measured against. SCOPE GATE: the turn is offered only when hasUnresponsiveControlEvidence() is true (a dominant repeated click target, noObservedEffect on EVERY one of those clicks, a non-canvas surface, no committed 2xx/3xx write \u2014 a model-free predicate that consults no LLM output). Every OTHER machinery halt keeps its current behavior: no extra turn, no extra LLM call, status=blocked + supervisor_halted. The gate is checked BEFORE the single-use latch, so a halt without that evidence does not consume the run's one attempt, and a `concluding_turn:not_offered_no_evidence` diag records each suppression so the gate's live fire rate is measurable. The turn is offered run_complete ONLY (the _editOnly idiom, RUNNER_TOOL_SET_CONCLUDE_ONLY), so it cannot act on the browser, cannot navigate, and cannot extend the loop: it is a single generateText call with no iteration, and it is offered at most once per run (_concludingTurnUsed). Its directive is NEUTRAL \u2014 it names no finding and no failure shape, asks for each step to be graded only on observed evidence, and tells the model to mark an unsubstantiable step 'skipped' rather than assert a verdict; an earlier revision spelled the dead-control conclusion out, which asked the model to ratify what the gate already believed. The halted path also performs NO cross-run writes: maybeCaptureProjectProfile and maybeMigratePlanFromCapture are skipped while _concludingTurnActive is true, because both end at isRunAuthenticated \u2014 routinely TRUE on a dead-CTA halt, where the login worked and the CTA did not \u2014 and a halted run must not persist its browser state or rewrite the customer's saved plan. Motivation, measured 2026-08-04 \u2014 READ THE PROVENANCE BEFORE QUOTING THE NUMBER (corrected 2026-08-06). It is run r1 of an UNCOMMITTED model-reliability harness (e2e/benchmark/model-reliability/, never merged to staging): its own execution-engine, fixtures on the deployed self-testing app, engine = staging @ fb89690cf, all 46 verification gates forced off as AGENTIQA_EXPERIMENT_<FLAG>=0 (PLAN_OBEDIENCE and CLICK_EFFECT_SIGNAL among them) and LLM_CACHE=0 \u2014 NOT the runner eval lane, which was not run that day. In it the two DEAD-CTA fixtures (a button that is present, whose click reports success, and which does nothing) produced 12/12 runs ending supervisor_halted/loop_block with ZERO run_complete, ZERO report_issue and ZERO agent-declared blocks. Those 12 are google:gemini-3-flash-preview \xD75 per fixture plus google:gemini-3.5-flash \xD71 per fixture: the second arm is n=1 per fixture and there was no Anthropic arm. THE CLASS IS MODEL-DEPENDENT, so 12/12 is not a property of the defect \u2014 a rerun the same day on the same code and fixtures (r2, 2026-08-04T20:11Z) had gemini-3.6-flash booking `failed` on 9 of 10 dead-CTA runs; only r1 reached this text. The halt is not the agent refusing to grade; it is the machinery taking the turn away before a verdict can be booked. MEASUREMENT STATUS: the post-merge L3 on the real lane (runner/seeded-broken, 2026-08-06 @ bdbfa8e30, 12 branch + 6 control, incumbent model) fired this gate 0/12 \u2014 GRADUATE-NO. Every eligible halt was suppressed by the scope gate (concluding_turn:not_offered_no_evidence) because the fixture's semantic `button` classifies as app-defect / non-interactive-target rather than custom-control-unresponsive, and its click has a real DOM effect so no noObservedEffect is ever sampled. The mechanism is unreachable on the very corpus that motivated it; see docs/VERDICT-GATES.md and PR #2246. GUARD RAILS, all pinned by tests: (a) a status=passed booked from this turn is REJECTED and the run falls back to today's blocked terminal unchanged \u2014 the agent must never be able to declare success out of a halt, and the halt itself is evidence against the claim; (b) the turn is NOT offered when blockKind==='agent' (the agent already concluded via exploration_blocked \u2014 nothing to second-guess); (c) exactly one turn, one LLM call, no tool loop, so a runaway loop is extended by that bounded turn and nothing more; (d) anything unusable (no run_complete, a throw, a stop mid-flight) falls back to the unchanged blocked terminal; (e) the scope gate above \u2014 a machinery halt with no unresponsive-control evidence gets no turn at all, in any blockKind. WHAT OFF (=0) DOES AND DOES NOT COVER \u2014 read this before using it as a kill switch. OFF removes the TURN: no extra turn, no extra LLM call, no extra token spend, and no run can be moved from blocked to failed by this mechanism. OFF does NOT change the terminal VOCABULARY: the `control_unresponsive` reason written by the blocked-terminal branch in startRun is UNCONDITIONAL and not gated by this flag, because it comes from `hasUnresponsiveControlEvidence` \u2014 a deterministic, model-free predicate that consults no LLM output \u2014 and it exists to stop blaming the agent for a dead control in the customer's own app, which is a separate improvement from giving a halted run a verdict. So with the flag OFF a halted run still terminates `control_unresponsive` rather than `supervisor_halted` whenever that evidence holds; to suppress the vocabulary too, turn off its input (`AGENTIQA_CLICK_EFFECT_SIGNAL=0` captures no `noObservedEffect`, so the predicate returns false and the terminal falls back to `supervisor_halted`). docs/VERDICT-GATES.md states the same split.",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-08-04",notes:"Default ON. Polarity is the reason: this gate can only move a run from blocked (NO verdict) to failed (a verdict the model booked), never toward a pass \u2014 the passed arm is refused in code, so there is no path by which it manufactures a false pass, which is the asymmetry docs/VERDICT-GATES.md guards hardest. Its worst case is a false FAIL on a run that today produces no verdict at all, and 'blocked' was never a pass either. Billing is NEUTRAL, and the ORDER is what makes it so: `handleRunComplete` reads `_concludingTurnActive` and stamps the non-billable reason (control_unresponsive when the deterministic unresponsive-control window holds, else supervisor_halted) BEFORE the first persist \u2014 because that upsert IS the billing trigger. It PUTs the entity-sync route, which fires commitRunUsage on any terminal + billable row, and `committed_at` is written in exactly one place in apps/web-next and only ever to NOW(); there is no un-commit path, so a later discardRunUsage no-ops with `already_committed`. An earlier revision corrected the reason with a second upsert AFTER handleRunComplete had already persisted 'completed'; that shipped the money before the correction and was replaced by this single-mechanism ordering (PR #2246 review finding 1). The same ordering is what makes a throwing upsert safe: the surviving in-memory reason is already non-billable, so startRun's finally cannot finalize a charge (finding 2). Read site: packages/engine-core/src/RunnerRuntime.ts#attemptConcludingRunComplete (called from startRun, immediately before the blocked terminal write) plus the `_concludingTurnActive` branch in #handleRunComplete. The turn's own LLM call is metered as auxiliary spend via emitAuxiliaryLlmUsage on every exit path (booked, declined, passed-refused) \u2014 it is a full-trace prompt plus up to 8192 output tokens, so leaving it unmetered hid the feature's entire cost (findings 3+4). Scope, directive neutrality and blast radius were narrowed in review: the turn fires only on the deterministic unresponsive-control halt shape (so every other machinery halt is untouched), its prompt names no finding, and the halted path skips the two cross-run writers handleRunComplete otherwise performs. Acceptance tests: RunnerRuntime.concludingTurn.test.ts (offer conditions incl. the scope gate across every blockKind, the neutral directive, the no-cross-run-writes pins, the passed refusal, the single-turn bound, the restricted tool set, flag-off parity), RunnerRuntime.controlUnresponsiveTerminal.test.ts (the terminal vocabulary + the browser_unobservable-style regression pins), RunnerRuntime.concludingTurnBilling.test.ts (the first-persist reason and the throwing-upsert path), RunnerRuntime.concludingTurnMetering.test.ts (the aux emit on every exit) and RunStatusBanner.controlUnresponsive.test.ts (the renderer copy + the booked-verdict line). MEASUREMENT STATUS 2026-08-06: post-merge L3 on runner/seeded-broken @ bdbfa8e30 (12 branch + 6 control) gave rescue 0/12 \u2014 GRADUATE-NO, with every eligible halt suppressed by the scope gate; the concluding_turn:not_offered_no_evidence diag remains the source of the live fire rate and is the number to read before revisiting this default."},CONCLUDING_TURN_ORACLE_FAIL_SCOPE:{key:"CONCLUDING_TURN_ORACLE_FAIL_SCOPE",envVars:["AGENTIQA_CONCLUDING_TURN_ORACLE_FAIL_SCOPE","AGENTIQA_EXPERIMENT_CONCLUDING_TURN_ORACLE_FAIL_SCOPE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"SECOND scope arm on the post-halt concluding turn (a CHILD of HALT_CONCLUDING_TURN \u2014 see that entry first). The turn is offered when hasControlUnresponsiveEvidence() OR hasUnresolvedVerifyOracleFailure(); this flag gates ONLY the second disjunct. The new arm matches when the run holds a verify step whose latest WAIT oracle (wait_for_element) failed and was never superseded by a successful re-verification \u2014 the wait-oracle subset of the Decision-6b ledger (accessor unresolvedWaitOracleFailureStepsForConcludingTurn(), over the provenance-blind base unresolvedOracleFailureStepsAnyProvenance()), discriminated per step through the existing conflictOracleMix() so it cannot drift from that classifier. PIN-GROUNDING entries do NOT count: the second writer of that ledger (checkPinPageGrounding / pin_page_grounding) is a plan-derived value check evaluated against whatever snapshot was captured, not an observation the run itself made and failed, and counting it would widen the arm past what was measured. Model-free in a stronger sense than the sibling arm: every ledger entry is written by the runtime from a dispatched tool RESULT and is keyed by VERIFY STEP INDEX, so the predicate reads no _currentStepIndex (that key is model-supplied via signal_step and non-monotonic) and nothing the model writes can put an entry there. WHY IT EXISTS: the parent's only arm was measured GRADUATE-NO on 2026-08-06 (L3 lane, runner/seeded-broken @ bdbfa8e30, 12 branch + 6 control) \u2014 0/12 rescues, every eligible halt suppressed by the scope gate, because a semantic <button> classifies app-defect / non-interactive-target rather than custom-control-unresponsive and noObservedEffect is sampled only on a dead-point click while that fixture's click has a real DOM effect. The class that actually reaches the halt seam verdict-less on that corpus is this one: step-4's wait oracle has already failed and been recorded, then the supervisor emits BLOCK (a free keyword choice \u2014 19/19 outcome correlation with BLOCK vs WRAP_UP on otherwise identical trajectories) and the run terminates blocked/supervisor_halted with no verdict at all. POLARITY, inherited and unchanged: the arm can only move a run from blocked (NO verdict) to failed (a verdict the model booked) \u2014 a `passed` from the turn is refused in code and NOT rewritten to failed, so no configuration of this flag can invent a pass. TERMINAL VOCABULARY IS UNTOUCHED: a turn opened by this arm leaves hasControlUnresponsiveEvidence() false, so handleRunComplete stamps the pre-existing non-billable `supervisor_halted` \u2014 never `control_unresponsive`, whose user-visible copy would then claim a dead control nobody measured. No TERMINATION_REASONS change, no new banner copy. LADDER: AGENTIQA_HALT_CONCLUDING_TURN=0 kills the whole turn regardless of this flag; this flag =0 restores the parent's control-unresponsive-only scope \u2014 same offer decisions, same terminals, no rollback record written (so the ledger is pre-change too), and the concluding_turn:not_offered_no_evidence payload drops back to its pre-change keys (oracleFailLedgerSize is emitted only while the arm is enabled). HARD DEPENDENCY ON VERIFY_GATED_DONE (default-on everywhere, and NOT gated by this flag): the oracle-failure ledger is written only under that switch (the recording branch is `gateOn`-guarded) and the base accessor unresolvedOracleFailureStepsAnyProvenance() hard-returns [] when it is off, so with AGENTIQA_VERIFY_GATED_DONE=0 this arm is SILENTLY INERT \u2014 it can neither fire nor be measured. Any census that pinned VERIFY_GATED_DONE to 0 says nothing about this mechanism (the 2026-08-04 model-reliability harness did exactly that with all 46 gates, which is one reason its 12/12 was not evidence about this seam). SHIPPED IN THE SAME COMMIT AND GATED BY THIS FLAG TOO (amended 2026-08-07): the rollback-safe recording fix. An auto-advanced verify wait (maybeAdvanceToVerifyForWait) that TIMES OUT rolls _currentStepIndex back before recordVerifyEvidenceFromToolResult runs, whose isVerifyStep(_currentStepIndex) pre-gate then dropped the record \u2014 measured on the L3 halt corpus as only 3 of 6 halted runs recording their own failed verification. The failure is now written at the rollback site, keyed to the verify step being rolled back FROM, into the same ledger with the same clearing semantics (diag verify_gate:oracle_failure_recorded carries via:'dispatch'|'rollback'), honoring every abstain the dispatch path honors (retained-frame abstain, canvas-dominant capture basis) and never clobbering an existing ledger entry \u2014 except an existing ARM-ONLY entry, which a later pin-grounding failure UPGRADES so the pin miss stays verdict-visible. THAT RECORD IS ARM-ONLY EVIDENCE: stamped via:'rollback' and read by THIS ARM ONLY. Every live-verdict consumer of the ledger filters it out \u2014 the run_complete verification-conflict cascade (verifyStepsWithUnresolvedOracleFailureForLiveVerdict()), applyRetainedFrameAbstain's supersede check (hasLiveVerdictOracleFailure()) and the VERIFY_REOBSERVE_WITHHOLD pass (liveVerdictOracleFailureDetail()). WHY: fed to the live cascade the record created a NEW FALSE-FAIL class, reproduced A/B on the real seam \u2014 a wait the runtime provisionally bound to a verify step the model had NOT signalled times out (slow render, or a matcher miss while the literal was on the page), the model then honestly grades that step passed from its own later observation, and run_complete bounces once then force-fails the resubmit: passed/completed on the base tree vs failed/blocked_by_issue with the record fed in, plus a synthesized 'Verification step not satisfied' issue. No rescue valve catches that class: canReconcileVerificationConflict (and the agent-literal abstain rungs) refuse a PLAN-GROUNDED wait literal, and an auto-advanced wait's literal is plan-quoted BY CONSTRUCTION because verifyWaitTargetMatchesOracle is what licensed the bind; the M1 snapshot reconcile VERIFY_CONFLICT_RECONCILE_SNAPSHOT is code-default OFF (hand-set =1 only on the GKE orchestrators) and is not a mitigation. Because the record has exactly ONE reader, it lives under THIS switch rather than being an independently-shipped half: =0 writes nothing at all, so the flag reverts BOTH halves of the change (offer decision AND ledger). VERIFY_GATED_DONE stays a hard dependency on top of it. OBSERVABILITY: concluding_turn:offered {scope: 'control_unresponsive'|'oracle_fail', oracleFailSteps} on every offer (control_unresponsive wins the label when both arms hold \u2014 it is the arm that also picks the terminal), and the suppression diag concluding_turn:not_offered_no_evidence gains oracleFailLedgerSize (the RAW ledger size, which separates 'nothing failed' from 'only the pin oracle failed' from 'VERIFY_GATED_DONE is off').",designDoc:"docs/plans/2026-08-06-concluding-turn-oracle-fail-scope-design.md",status:"active",added:"2026-08-07",notes:"Default ON, for the parent's reason: the reachable verdict set of this mechanism is {failed} \u2014 the passed arm is refused in code \u2014 so its worst case is a false FAIL on a run that today produces NO verdict at all, and `blocked` was never a pass either. It adds ONE bounded generateText call to a halt that already ended without a verdict, metered as auxiliary spend on every exit path (emitAuxiliaryLlmUsage), and the run stays non-billable. Read site: packages/engine-core/src/RunnerRuntime.ts#attemptConcludingRunComplete (the scope gate, before the single-use latch \u2014 a gated-out halt still does not burn the run's one attempt); predicate: #hasUnresolvedVerifyOracleFailure \u2192 #unresolvedWaitOracleFailureStepsForConcludingTurn \u2192 #unresolvedOracleFailureStepsAnyProvenance + #conflictOracleMix. The rollback recording gated with it lives at #rollbackVerifyWaitAdvanceIfFailed \u2192 #recordRolledBackVerifyWaitOracleFailure \u2192 #writeVerifyOracleFailure (the single writer both seams share, which persists the `via` provenance). The live-verdict side of the accessor split \u2014 the sites this arm's evidence must NOT reach \u2014 is #verifyStepsWithUnresolvedOracleFailureForLiveVerdict / #hasLiveVerdictOracleFailure / #liveVerdictOracleFailureDetail, all three over #isArmOnlyOracleFailure. Acceptance tests: RunnerRuntime.concludingTurnOracleFailScope.test.ts (the arm across every machinery blockKind in both directions, blockKind='agent' never offered, latch-not-burned, child=0 scope parity incl. the diag payload, parent=0 dominance, pin-grounding-only ledger does not match, VERIFY_GATED_DONE=0 inertness, the booked-verdict terminal/billing/cross-run-write/passed-refusal properties on the new arm), RunnerRuntime.verifyWaitRollbackOracleRecord.test.ts (the rollback record and its key, the accessor split, the clearing semantics, no double-record when the model signal_step'ed the verify step itself, the abstain carve-outs incl. this flag =0, non-clobber) and RunnerRuntime.oracleFailArmOnlyEvidence.test.ts (the A/B-reproduced false-FAIL class through the real handleToolCall/run_complete seams, the dispatch-record parity pin that proves the filter weakened nothing, the halted run that still gets the turn on arm-only evidence, the retained-frame warning cap, and child=0 full parity). GRADUATION IS NOT UNIT-TESTABLE: the parent shipped green units and measured 0/12 in prod-shaped replay, which is the whole reason this child exists \u2014 the gate is the L3 lane replay in docs/plans/2026-08-06-concluding-turn-oracle-fail-scope-design.md criterion 8 (12 branch replicates vs 6 with this flag =0), and `concluding_turn:offered {scope:'oracle_fail'}` plus the halted-run verdict split are the numbers to read.",graduation:{status:"gated",gate:'L3 lane replay on runner/seeded-broken (e2e/evals/runner/run-lane.ts, 12 replicates branch vs 6 replicates with AGENTIQA_CONCLUDING_TURN_ORACLE_FAIL_SCOPE=0 as control) against a STAGING build carrying this arm. Targets: every supervisor-halted run books a verdict through the concluding turn (the recording fix is expected to close the measured 3/6 ledger gap), ZERO booked `passed`, the control arm reproduces the current halt-vs-booked split, `concluding_turn:offered {scope:"oracle_fail"}` present on every rescue, and the stage-15 completeness-guard bounce rate on the turn reported rather than assumed. A green unit suite is explicitly NOT the gate: the parent had one and fired 0/12 in the lane.',evidence:"staging concluding_turn:offered / concluding_turn:not_offered_no_evidence (with oracleFailLedgerSize) / concluding_turn:verdict_booked|verdict_not_booked diag events from the lane replay, plus verify_gate:oracle_failure_recorded {via} counts for the rollback-recording share, and the engine-core concludingTurn / concludingTurnOracleFailScope / verifyWaitRollbackOracleRecord / concludingTurnBilling / concludingTurnMetering suites",owner:"steering (Alex)",review:"2026-08-14"}},RUNNER_UNOBSERVABLE_CONCLUDING_GRADE:{key:"RUNNER_UNOBSERVABLE_CONCLUDING_GRADE",envVars:["AGENTIQA_RUNNER_UNOBSERVABLE_CONCLUDING_GRADE","AGENTIQA_EXPERIMENT_RUNNER_UNOBSERVABLE_CONCLUDING_GRADE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"THIRD scope arm on the post-halt concluding turn (a CHILD of HALT_CONCLUDING_TURN \u2014 read that entry first): the `browser_unobservable` terminal in RunnerRuntime.startRun's catch routes through ONE bounded conclude-only grading turn BEFORE the synthetic all-skipped fallback. THE CLASS IT CLOSES (issue #2605; prod lio runner suites, sessions asess_1786679659847_bpczs2mx and asess_1786850942202_sm2dus65, 2026-08-14/16): a busy customer page starved page.screenshot 3 consecutive times, BrowserUnobservableError killed a run in which 21+ steps had demonstrably executed with passing verify evidence, and synthesizeBlockedVerdictIfMissing then wrote EVERY step {status:'skipped', synthesized:true} \u2014 grading only happens in handleRunComplete, so at terminal time there were no per-step grades to preserve and the synthesis destroyed the run's own proof. With this arm ON the terminal offers the model the same neutral, run_complete-only turn the supervisor-halt arms use (same single-use latch, same conclude-only tool set, same auxiliary metering, same passed-refusal) so executed steps get graded from the retained trace/evidence \u2014 the catch trims the dangling tool call the mid-dispatch throw leaves in the trace first, without which the provider rejects the turn's messages and every live run silently falls back to the synthesized shape. A booked verdict is then FORCED (the `browserUnobservableConcluding` clamp in handleRunComplete, keyed on `_concludingTerminalReason`) to run status 'blocked' + terminationReason 'browser_unobservable' at the terminal write \u2014 the model grades STEPS, it does not choose the run-level outcome of a run OUR capture pipeline killed. FIREWALL ASYMMETRY, stronger than either sibling arm: the reachable run-level status set of this arm is exactly {blocked} (the sibling arms can at least book 'failed'), so no configuration of it can manufacture a pass OR a fail at run level \u2014 it can only move step rows from synthesized-skipped to graded-with-evidence. AND AT STEP LEVEL since the #2626 review: applyUnobservableStopPointFloor runs LAST in handleRunComplete's cascade and clamps a 'failed' grade on the ONE step the capture died on down to 'skipped' (the only target that survives deriveStepStatusIgnoringAuthorship's short-circuit \u2014 a 'warning' row carrying the model's own strict-failed criterion is re-derived straight back to 'failed' by every reader), while a 'passed' grade on that step STANDS. The asymmetry is the point: retained pre-death frames are legitimate positive evidence that a step did what it said, and are not legitimate evidence of a failure to complete. Earlier steps, graded from frames that actually existed, are never touched. NOT GATED BY THIS FLAG (deterministic, model-free, additive \u2014 the ticket's items 2 and 3): the blockedReason naming the capture failure + the page URL on the run_complete card (built from buildBlockedSummary over the catch's URL-carrying `detail`), and the auto-filed unobservable/page-hang issue + report_issue card (synthesizeBrowserUnobservableIssue); those land on BOTH the booked and the fallback paths in every flag state. =0 restores the pre-change terminal byte-for-byte on the TURN (no extra LLM call, all-skipped synthesis \u2014 now with the unconditional blockedReason/detail/issue additions above). LADDER: AGENTIQA_HALT_CONCLUDING_TURN=0 kills the whole concluding-turn mechanism including this arm; this flag =0 kills only this arm (checked at the unobservable catch, so a =0 run never consults the other arms' predicates there and never burns the single-use latch).",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-08-19",notes:"Default ON with kill switch, per the family's precedent and for a strictly stronger reason than HALT_CONCLUDING_TURN's: that entry justifies default-ON because its reachable verdict set is {failed} (the passed arm is refused in code, so the worst case is a false FAIL on a run that had NO verdict). This arm's reachable RUN-LEVEL set is {blocked} \u2014 the terminal write in handleRunComplete stamps status='blocked' + terminationReason='browser_unobservable' whenever the turn was opened by this arm, regardless of what the model booked \u2014 so it cannot produce a false FAIL either; only step rows move, and only from synthesized-skipped toward model-graded statuses backed by the retained trace. The #2626 review made that claim true at STEP level too, where it previously was not: the concluding model CAN grade the in-flight step 'failed' (the eval lane's own live baseline recorded #4 {status:failed} on the busy-finale fixture) \u2014 a red row for a step nothing could observe \u2014 so applyUnobservableStopPointFloor now clamps exactly that one row to 'skipped' and composes a stop-point note explaining the withheld verdict onto whatever the model wrote. (Shadow-first default-OFF is the family's shape for RED\u2192AMBER verdict-SOFTENING valves like the VERIFY_CONFLICT_* lanes, which can convert a detection into a green step; nothing here softens a red \u2014 a blocked evidence-destroyed run was never a pass or a fail.) Billing: the run stays non-billable and the ORDER is inherited from the parent \u2014 handleRunComplete stamps 'browser_unobservable' (absent from BILLABLE_TERMINATION_REASONS) BEFORE its first upsert, which is the billing trigger, so the money can never move on this terminal; the turn's own LLM call is metered as auxiliary spend via emitAuxiliaryLlmUsage on every exit path. Cross-run writes (maybeCaptureProjectProfile / maybeMigratePlanFromCapture) stay skipped under _concludingTurnActive exactly as on the sibling arms \u2014 a run whose browser could not be captured must not persist its browser profile. Read site: packages/engine-core/src/RunnerRuntime.ts \u2014 the unobservable catch in startRun (gates the trim + the attemptConcludingRunComplete call whose arm is keyed on blockKind='browser_unobservable'); the run-level clamp lives in handleRunComplete (`browserUnobservableConcluding`) next to the sibling arms' reason stamp, and the step-level one in applyUnobservableStopPointFloor immediately before it. Acceptance: RunnerRuntime.blockedSynthesisPreservesSteps.test.ts (the #2605 contract: real statuses preserved, blockedReason naming capture+URL, issue auto-filed, run stays blocked/browser_unobservable; plus the #2626 cases (e)-(n): stop-point note + reached markers on the graded record, the asymmetric failed\u2192skipped clamp, passed standing, the summary count read after the clamp, the open-issue fingerprint dedup, and the no-usable-identity fail-open) and the certifying eval e2e/evals/plans/runner-blocked-synthesis.ts (judgement 'correct' against the deterministic busy-finale fixture \u2014 re-measured live 2026-08-21 WITH the clamp: the model graded the stop-point step failed and the floor withheld it to 'skipped', so the step-level claim above is confirmed against a real model rather than only against mocks)."},BLOCKED_SYNTHESIS_PRESERVE_GRADES:{key:"BLOCKED_SYNTHESIS_PRESERVE_GRADES",envVars:["AGENTIQA_BLOCKED_SYNTHESIS_PRESERVE_GRADES","AGENTIQA_EXPERIMENT_BLOCKED_SYNTHESIS_PRESERVE_GRADES"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"FOURTH scope arm on the post-halt concluding turn (a CHILD of HALT_CONCLUDING_TURN \u2014 read that entry first). ENFORCING BY DEFAULT (GRADUATED 2026-09-11) \u2014 this whole entry describes the LIVE path. With NO env the arm is armed: a halted run that executed at least one step and holds no verdict is offered its one bounded conclude-only turn whatever halted it, the `blockKind === 'agent'` early return is lifted, and the run-level clamp forces the terminal the halt would otherwise have written. An explicit `=1` is still accepted and resolves identically. `=0` (either spelling) IS THE KILL SWITCH and is now the ONLY non-enforcing state: it restores the pre-change offer decisions and terminals AND switches the detection off, so no marker row of any name is emitted. THE SHADOW STATE IS GONE, DELIBERATELY, AND ASYMMETRICALLY \u2014 state it precisely rather than as a slogan. MARKER 1 (`blocked_synthesis:would_preserve`) SURVIVES the flip: it is emitted from startRun's `finally` over the FINAL record and reads the terminal, not the offer, so its `kept` / `wiped` / `concludingTurnScope` fields are exactly the post-flip watchdog and its `enforcing` field still separates a `=0` control from a live run. Its `would_` name is now a misnomer and is KEPT on purpose so archived shadow-era artifacts and the live stream stay one queryable population. MARKER 2 (`concluding_turn:would_offer_executed_steps`) is what CLOSES: its `agent_block` and `no_scope_arm` buckets are structurally UNREACHABLE (use that word, not 'near-unreachable' \u2014 a future census author must not read the hedge as 'go look for residual rows'). `agent_block` suppression is lifted by this flag and `no_scope_arm` now requires `_maxStepEverExecuted === 0`, which the emitter itself filters out, so the only reachable `suppressedBy` left is `latch_spent` \u2014 which the cost metric excludes by design. CONSEQUENCE, AND THE REASON THE COST HAD TO BE PRICED BEFORE THE FLIP: the cost census bottoms out at zero by construction once this is on, so it can never be re-run after the fact. POST-FLIP WATCHDOG \u2014 census the LIVE names, not the `would_*` ones: `concluding_turn:offered {scope:'executed_steps'}` (the arm firing), `blocked:graded_by_concluding_turn` (the agent-block branch booking through it), `concluding_turn:passed_refused` (the firewall being exercised \u2014 a non-zero count is not a regression, a `passed` that gets THROUGH is), `blocked_synthesis:would_preserve {kept, wiped}` (a `kept < maxStepExecuted \u2212 1` under enforcement is the regression shape), and any `failed` step row BELOW the stop point. A regression is answered by setting `AGENTIQA_EXPERIMENT_BLOCKED_SYNTHESIS_PRESERVE_GRADES=0` on the orchestrator FIRST and fixing after, updating docs/FLAGS-STATE.md in the same session per its Rule 1. THE DEFECT (#3072, three independent terminals): grades are CREATED only in handleRunComplete \u2014 every `stepResults.push` in RunnerRuntime is inside that method's gate cascade, and both synthesizers bail on a non-empty record \u2014 so on a halt with no verdict there is nothing for a synthesizer to preserve. Whether a halted run keeps its grades is therefore decided ENTIRELY by whether the bounded conclude-only turn was OFFERED, and the offer was gated on WHICH defect the run happened to hit: `hasControlUnresponsiveEvidence()` OR an unresolved verify-oracle failure OR `blockKind === 'browser_unobservable'`, with `blockKind === 'agent'` returning silently before any of them. Measured consequences on one plan across two nights: a supervisor BLOCK carrying an oracle failure kept 7 of 25; the SAME plan's supervisor BLOCK with an empty ledger (staging run_1566360e, 82 iterations) kept 0 of 24; a prod `loop_block` (asess_1788926139197_wdukkeb2, iteration 88) kept 0 of 16; the same plan's `browser_unobservable` terminal the same night kept 24 of 25. The terminal is not the variable \u2014 the offer predicate is. THE ARM: a halted run that executed at least one step (`_maxStepEverExecuted > 0`) and holds no verdict is offered its one bounded turn regardless of halt reason or ledger emptiness, and the `blockKind === 'agent'` early return becomes conditional on this same flag. THAT RETURN IS NOT THE ONLY GATE ON THE AGENT-BLOCK TERMINAL, and the first revision of this flag shipped believing it was (review round 1, finding F0, measured live 4/4): `exploration_blocked` is terminated by handleBlocked() INSIDE the tool handler, which writes run.status='blocked' + blocked_by_issue and emits run:completed during the loop, so by the time startRun calls attemptConcludingRunComplete its `run.status !== 'running'` invariant has already fired. `=1` and unset were byte-identical there and marker 2's `agent_block` bucket was structurally EMPTY, which made the graduation cost metric uncomputable on the terminal #3072 reports most often. handleBlocked now calls the same seam while the run is still 'running', passing its own composed summary as blockedReason so the clamp's base string and the fallback write's come from ONE expression; when the turn books, handleRunComplete owns endedAt/the upsert/run:completed and the fallback block is skipped, so the event fires exactly once on either arm. That path also closes this terminal's stage-19c coverage gap \u2014 `cause_grounding:not_reached` is now emitted only when the run really did end without run_complete. `_maxStepEverExecuted` is the monotonic, model-free EXECUTION mark raised only where a page-affecting action is dispatched \u2014 the SAME licence `synthesizePartialStepResults` already uses to write `reached: true` \u2014 so the arm claims nothing the floor does not already claim. RUN-LEVEL FIREWALL, the reason this is shippable at all: every turn opened by THIS arm sets `_concludingTerminalReason` to the terminal the run would otherwise have got (`blocked_by_issue` for an agent block, `supervisor_halted` otherwise) and the generalized `concludingTerminalClamp` in handleRunComplete then forces `run.status = 'blocked'` plus that reason, so the reachable RUN-LEVEL status set of this arm is exactly {blocked} \u2014 the #2605 firewall asymmetry, inherited verbatim. No configuration of this flag can invent a pass OR a fail at run level; only step rows move, from synthesized-`skipped` toward model-graded-with-evidence. The clamp is keyed on `_concludingTerminalReason !== null`, which is null on the control-unresponsive and oracle-fail arms and can only be `browser_unobservable` under this flag's `=0` kill switch, so all FIVE clamped writes (the stop-point floor, `run.status`, `run.summary`, `run.terminationReason` and the `run_complete` card's own `blockedReason`) and the 19c cause-grounding input are byte-identical to pre-change BY CONSTRUCTION rather than by a flag read. WHAT THE ARM CAN STILL GET WRONG, stated because the run-level firewall does not cover it: a model grading from a truncated trace can book `failed` on a step BELOW the stop point, where today's record says `skipped`. `applyConcludingStopPointFloor` clamps a `failed` only on the ONE stop-point row (#2626 contract, unchanged), so the arm CAN mint red rows on a run that has none today. THE MIRROR RESIDUAL, decided in review round 1 (F2): that same clamp WITHDRAWS a model `failed` on the stop-point row with no evidence test, and on the newly-clamped terminals the browser was healthy and the finding may be true \u2014 4/4 live `=1` runs graded that step failed with a correct reason and the floor withheld it. It stays withheld deliberately: the stop-point step never finished, so a red row there cannot separate 'the app is broken' from 'we stopped it mid-step', and minting it is the false-fail family this wave closes elsewhere. Nothing regresses (that row was skipped+synthesized before) and the finding survives in the summary, the card's blockedReason and the agent's filed issue. GRADUATION MUST NOT READ `kept` AS 'the finding is now a red row': `kept` counts the steps BELOW the stop point only. That is the false-fail family #1907 / #3103 / #3114 are closing elsewhere, and it is why the graduation gate below bounds NEW `failed` rows against a paired `=0` control rather than only bounding `kept`. TERMINAL VOCABULARY AND FRAMING ARE UNTOUCHED: an agent block still terminates `blocked_by_issue` (its `exploration_blocked` card and auto-filed issue survive), a machinery halt still terminates `supervisor_halted`, and the base summary is the exact string the fallback blocked write would have produced \u2014 the model's own summary is appended behind `Retained grading summary:`, the same composition the `browser_unobservable` arm has shipped since #2605. `control_unresponsive` is never chosen by this arm (its own predicate is false whenever this arm is the one that fired, since the arms are ORed and checked first). No TERMINATION_REASONS change, no new banner copy. NOT IN SCOPE \u2014 `max_iterations` and `slow_site` keep wiping: they finalize through `synthesizeBlockedVerdictIfMissing` from `onLoopExhausted` / the startRun catch, never reach `attemptConcludingRunComplete`, and carry no stop point at all (`stopKind = null`). Marker 1 is emitted from `startRun`'s `finally` and therefore COVERS those runs in the census even though the arm cannot rescue them. LADDER: `AGENTIQA_HALT_CONCLUDING_TURN=0` kills the whole concluding-turn mechanism including this arm and every marker that depends on reaching its seam (marker 1 still emits \u2014 it reads the terminal record, not the offer). This flag `=0` restores the pre-change offer decisions and terminals AND silences both markers. The single-use latch is respected exactly as by the sibling arms: the widened arm now burns it on the FIRST halt, so a run with two halts gets a grading turn only on the first \u2014 pinned by test, not left emergent. OBSERVABILITY: `concluding_turn:offered {scope:'executed_steps'}` on a turn this arm opened; `blocked:graded_by_concluding_turn {runId, status, terminationReason, gradedSteps}` when the agent-block terminal books through it; `concluding_turn:would_offer_executed_steps {runId, blockKind, suppressedBy: 'agent_block'|'latch_spent'|'no_scope_arm', maxStepExecuted, currentStepIndex, controlUnresponsive, oracleFailLedgerSize, enforcing}` at the ONE suppression seam, emitted only AFTER every invariant of attemptConcludingRunComplete holds (a user-stopped or reaped run produces no row, so the cost census is not inflated by runs that could never be offered a turn); `blocked_synthesis:would_preserve {runId, terminal, terminationReason, blockKind, kept, wiped, stopStepIndex, maxStepExecuted, totalSteps, concludingTurnUsed, concludingTurnScope, enforcing}` once per ABNORMAL terminal (`blocked` / `error` only \u2014 `cancelled` is excluded because a user who pressed Stop lost no grades) from startRun's finally, over the FINAL record so it covers the graded, floored and AG-7981 catch-arm shapes alike.",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-09-10",graduation:{status:"gated",gate:"GRADUATED 2026-09-11 (default ON). KEPT AS `gated` RATHER THAN DELETED \u2014 per the CRITERIA_CONTAINMENT precedent, a graduated flag keeps this block when a named residual is still owed, and one is (below). WHAT THE FIVE-PART BAR ACTUALLY RETURNED, item by item, including the two items it could not satisfy as written. (1) COST CENSUS \u2014 NOT MET AS WRITTEN, AND NOW UNSATISFIABLE. The bar asked for >= 5 consecutive nights of the nightly qa-exhaustive runner lane plus one ag-rehearsal replay, counting `concluding_turn:would_offer_executed_steps` de-duplicated per runId over `suppressedBy IN {'agent_block','no_scope_arm'}`. The rehearsal half ran (2026-09-11 mirror pass 1, 17 fixed customer plans, flag hand-set `=1` on the staging orchestrator) and returned ZERO rows of that marker and ZERO of marker 1 \u2014 no run on that corpus reached a `blocked`/`error` terminal at all \u2014 so the flip's cost ON THE PILOT CORPUS is measured at zero extra bounded generateText calls. The 5-night nightly half was NOT run, and after this flip it cannot be: marker 2 is reachable only as `latch_spent` post-flip (see the entry's gates text), so the cost census bottoms out at zero by construction. The bar is therefore REWRITTEN to the price the deterministic lane could establish, WITH THE DENOMINATOR STATED AS MEASURED (the offer rate is per HALT, not per run \u2014 30/8 is runs-per-offer and prices the flip ~44% too cheap): on the fixture lane the arm opened `concluding_turn:offered {scope:'executed_steps'}` on 13 of the 25 forced-`=1` HALTS of the RE-RUN WITH INTACT RAW \u2014 a MEAN of ONE extra bounded generateText call PER 1.9 BLOCKED RUNS on fixtures BUILT to halt \u2014 and 0 times without the flag. Read the denominator as HALTS: per RUN it is 13/30, which is not the price of a halt. On the concluding-turn mechanism as a whole the flag roughly DOUBLES the turns taken on blocked runs (1.00 offers per halt under `=1`, 25 over 25 halts, against 0.54 in shadow, 13 over 24) and buys nothing on runs that do not halt \u2014 zero offers there. THIS SUPERSEDES the 2.6-halts figure computed from the earlier two-batch pass, which is retained only as corroboration. On 17 real customer plans the cost was zero because no run reached the terminal at all; on the 19-run combined pass the same day the corpus DID reach two `blocked` terminals and the arm still opened 0 times, so the measured mirror cost stays zero on a non-empty population. (2) LOSS CENSUS \u2014 NOT MET ON THE PILOT CORPUS AS WRITTEN, but NO LONGER AN EMPTY MEASUREMENT, and this is the freshest evidence in the entry: mirror pass 1 (06:57->08:01Z, 17 plans) returned 0 `blocked_synthesis:would_preserve` rows because no run reached a `blocked`/`error` terminal, but the COMBINED forced-ON window the SAME DAY on the same mirror and the same engine (12:29:09Z->13:56:51Z, 19 runs) reached 2 `blocked` terminals and booked 2 marker-1 rows, both `enforcing: true`, both `kept = 24 / wiped = 0` with 24 of 24 real grades below the stop point and `synthesized` 0 \u2014 AND a paired `=0` control on that same engine returned `kept = 24 / wiped = 0` on the same plan and terminal. SO THE LOSS CENSUS ON REAL CUSTOMER TRAFFIC IS NON-EMPTY AND ZERO-LOSS: the delta attributable to #3072 on the pilot corpus is not 'unmeasured', it is MEASURED AT EXACTLY ZERO, because the pre-existing `browser_unobservable` arm had already bought both terminals. The bar as written asked for `wiped > 0` on that corpus and did not get it; that is the corpus being healthy, not the instrument being blind. REWRITTEN to the deterministic lane, where it is met with room \u2014 numbers from the RE-RUN WITH INTACT RAW that item (3) rests on: 12 of the 24 shadow halts carry `wiped > 0`, INCLUDING 11 OF 11 on the target fixture (10 x `kept=0 wiped=4` plus 1 x `kept=0 wiped=3`) and one more on the sibling, across BOTH fixtures and BOTH blocked seams (`supervisor_halted` and `blocked_by_issue`), against 25 forced-`=1` halts of which 24 are `kept=3 wiped=0` (the 25th is the declined-turn miss named in item (3), on a pre-flag arm). Every shadow `{kept > 0}` row is attributable via `concludingTurnScope` to the pre-existing `browser_unobservable` arm (12 of the 24 shadow halts), so no kept grade is being credited to this arm that a sibling already bought. (3) FORCED `=1` LANE \u2014 MET on a RE-RUN WITH INTACT RAW, with three amendments on the record. N: 25 blocked runs under `=1` (bar >= 20) out of 30, 11 on runner/blocked-synthesis-supervisor-block and 14 on runner/hang-busy-finale-static, against 24 halts in the paired shadow arm. THE GRADUATION RESTS ON THIS RE-RUN, and the earlier two-batch pass is retained as CORROBORATION ONLY. The re-run was executed 2026-09-11 at `origin/staging` a79a61d5f (`dist` rebuilt and verified by inspection to carry BOTH #3072 and #3148), 60 runs in ONE batch with both arms running in parallel so each got the same load floor, and every artifact written straight to ~/agentiqa-agents/lio-harvest/work/gate3-3072-rerun/ OUTSIDE the managed scratchpad: 78 MB of raw diag, 60 ndjson + 60 run.json, so EVERY row below is re-derivable from raw events. WHY IT EXISTS: the earlier pass's 21 halts spanned TWO batches and batch 1's raw diag was DESTROYED MID-RUN by an unrelated parallel session wiping the managed scratchpad, which left 13 of those 21 rows rehydrated from a verbatim 40/40 census extract that can no longer be re-derived (the extract survives next to the report, and batch 2's intact raw reproduced every conclusion at N = 8). Nothing in this graduation now depends on a rehydrated row. WHAT THE RE-RUN SHOWS: 24 of the 25 `=1` halts are `kept=3 wiped=0`; the populations are EQUAL on the target fixture at 11/15 in BOTH arms, where the earlier pass was lopsided at 8/15 vs 5/15; the paired shadow arm wiped on 12 of its 24 halts INCLUDING 11 OF 11 on the target fixture, on both blocked seams; this flag's own arm fired 13 times (`concluding_turn:offered {scope:'executed_steps'}`) and on BOTH fixtures this time, against 0 in shadow, and the agent-block F0 branch (`blocked:graded_by_concluding_turn`) booked 4 times on two fixtures against 0; the arming leaked in neither direction (25/25 `enforcing: true` against 25/25 `false` on 60 runs); and every safety counter is ZERO across all 60 runs \u2014 no NEW `failed` row below the stop point, no run-level `passed` booked by the turn, no `concluding_turn:passed_refused`, no `concluding_turn:failed`. On every run this flag's arm touched, the terminal kind is `blocked` and nothing else. THE ONE MISS, named rather than averaged away: runner/hang-busy-finale-static r15 under `=1` is `kept=0 wiped=4`, and it is NOT this arm \u2014 the turn was opened with `scope: 'browser_unobservable'`, the #2605 arm this flag does not gate at all, and THE MODEL DECLINED THE TURN, returning 1378 characters of prose instead of calling run_complete, so no grades were created and synthesis filled all four rows. `kept`/`wiped` MEASURES the final record rather than driving it, so a declined turn loses the grades in every flag state; on this flag's OWN arm the record is 13 of 13 with no exception. THAT DECLINE IS A NAMED, NON-BLOCKING RESIDUAL, recorded because no prior census had produced one: a concluding turn CAN be declined and on a decline the grades are lost entirely \u2014 pre-flag behaviour on a pre-flag arm, measured at 1 per 25 halts, not counted against this flip and not covered by this gate's safety counters. It is owed its own tracking item rather than a footnote here. AMENDMENT A: the bar named THREE cases, one of which \u2014 runner/blocked-synthesis-loop-block \u2014 DOES NOT EXIST. It was deleted in this feature's own review round 1 (F3) after `loop_block` occurred 0 times in 9 halted runs, i.e. it sampled the same population for +83% lane budget. The bar is two cases, and the third was never a measurable thing to ask for. AMENDMENT B: 'ZERO runs whose run-level status is `passed`' is met in the sense its own parenthetical defines \u2014 zero escapes past the passed-refusal and the clamp: on the re-run `concluding_turn:verdict_booked` is 24 x `blocked` and 0 x `passed` (the 25th halt is the declined turn, which booked nothing), `concluding_turn:passed_refused` is 0 and `concluding_turn:failed` (F0b) is 0 \u2014 but NOT as a literal run-level count: runner/hang-busy-finale-static produced 1/15 run-level `passed` under `=1` against 0/15 in shadow, and that run carries NO `concluding_turn:*` row at all, i.e. the flag provably did not execute on it. It is the documented false-pass class of that fixture (the model claims the finale banner on a permanently hung renderer), which the earlier two-batch pass observed SYMMETRICALLY at 2/15 in BOTH arms; on the re-run the residue is one run in the `=1` direction and is reported as such rather than rounded away. It is why that case gates NO_FALSE_PASS_CLAIM at 1.0, and it is not attributable to this flag. AMENDMENT C: 'a `terminationReason` histogram IDENTICAL to the paired `=0` control' is UNSATISFIABLE BY CONSTRUCTION on a stochastic lane and should never have been written that way. This flag is read only at a seam where the halt is ALREADY decided (`attemptConcludingRunComplete` runs after `result.blocked`), so it can neither cause nor prevent a halt; halt rates differ by model variance alone (8/15 vs 5/15 on the target fixture). REWRITTEN to the property the item was reaching for \u2014 and ON THE RE-RUN THE NAIVE FORM OF THAT PROPERTY FAILS, which is stated here rather than quietly replaced by the form that passes. NAIVE FORM, on RUN-LEVEL STATUS (`blocked` / `failed` / `passed`): the set of terminal KINDS under `=1` is NOT a subset of the control's on runner/hang-busy-finale-static, because `passed` appears under `=1` (1/15) and not in shadow. That is the amendment-B false-pass run, and it carries no `concluding_turn:*` row, i.e. the flag did not execute on it. THE MEANINGFUL FORM \u2014 restricted to the population the turn was actually OFFERED \u2014 holds on both fixtures: there the terminal kind is `blocked` and ONLY `blocked` in both arms (supervisor-block 11 under `=1` against 0 in shadow, where this arm does not exist; hang-busy 14 against 13), so the flag created NO new kind on a single run it touched, and the `passed`/`failed` spread lives exclusively where the flag provably did not execute. THE FINER AXIS HOLDS TOO: no `terminationReason` appears under `=1` that is absent from the paired control (supervisor-block `{supervisor_halted, blocked_by_issue, completed}` against the same three; hang-busy `{browser_unobservable, blocked_by_issue, completed}` against the control's same three plus one harness-INVALID row). What is NOT claimed on either axis is the bar's literal word, an IDENTICAL histogram \u2014 the COUNTS differ (hang-busy halts 14/15 vs 13/15; on the target fixture the re-run's populations are in fact EQUAL at 11/15 vs 11/15, which the earlier pass's 8/15 vs 5/15 was not) and cannot be guaranteed to match on a stochastic lane. (4) NO NEW RED, AND NO LOST GRADE \u2014 MET on the re-run, with one named exception that is not this arm's. `kept > 0` on 24/25 `=1` halts = 96% (bar: >= 80%), each of those 24 exactly `kept=3` of `executed=4`, i.e. `executed - 1`; on THIS FLAG'S OWN arm (`scope:'executed_steps'`) it is 13 of 13 with no exception. The single exception is runner/hang-busy-finale-static r15, `kept=0 wiped=4`, where the pre-flag `browser_unobservable` arm opened the turn and the MODEL DECLINED it \u2014 detailed in item (3). It also costs the bar's second clause its literal form: 'NO run where `kept` is lower under `=1` than in the paired control' holds at 24 of the 25 replicate indices and FAILS at exactly r15, whose shadow twin kept `3/0`. Say it that way rather than claim the clause outright \u2014 the divergence is a declined turn on an arm this flag does not gate, not a grade this flag lost. NEW `failed` step rows BELOW the stop point: 0 across all 60 runs on BOTH arms. (5) TRACE PARITY \u2014 MET, RE-KEYED, because the flip makes the original wording false: 'with the flag UNSET ... byte-identical to the pre-change tree' cannot hold once unset IS the enforcing state. The pin is re-keyed in RunnerRuntime.blockedSynthesisCensus.test.ts to the post-flip triad \u2014 `=0` is byte-identical to the pre-change tree (zero marker rows, zero generateText, the pre-change floor rows on every blockKind), no-env is identical to `=1` seam for seam, and no env spelling can reach a suppression row on an offered run. THE RESIDUAL THIS BLOCK IS KEPT FOR \u2014 UNCHANGED, but the REASON for it is re-keyed to the combined pass, because the old reason ('items (1) and (2) returned structurally empty') is now stale: THE ARM HAS NEVER BEEN OBSERVED TO FIRE ON A REAL CUSTOMER PLAN. The population is no longer absent \u2014 the combined pass produced it twice \u2014 the ARM is: every positive measurement of the arm firing comes from two deterministic fixtures built to halt. The arm's false-positive rate on real customer traffic is therefore UNMEASURED, not measured-zero. AND THE STATED RETIREMENT CONDITION MAY BE UNSATISFIABLE ON THIS CORPUS BY CONSTRUCTION \u2014 say so rather than let a future reader wait on it: this arm sits LAST in the ORed scope-arm chain by design ('the fallback that fires only when no proven arm did'), and on the pilot mirror a proven arm always wins first (`oracle_fail` in pass 1, `browser_unobservable` twice in the combined window). THREE consecutive mirror passes \u2014 19 + 17 + 19 runs \u2014 and the arm has fired ZERO times. That is a structural property of the corpus (the Lio mirror does not produce halts with no proven arm), not an n problem, so closing this residual needs EITHER a second corpus whose halts lack a proven arm OR an explicit acknowledgement that the pilot mirror cannot produce the `scope:'executed_steps'` population at all. See the combined report's section 3.3. The instrument that closes it is the post-flip watchdog in the gates text above \u2014 `concluding_turn:offered {scope:'executed_steps'}`, `blocked:graded_by_concluding_turn` and `blocked_synthesis:would_preserve {kept, wiped}` on the nightly qa-exhaustive runner lane and on the next mirror rehearsal. Second, smaller residual, already named in the entry: a model grading from a truncated trace CAN mint a `failed` row BELOW the stop point (the stop-point floor clamps only the one in-flight row) \u2014 measured 0 across 60 runs, structurally still possible. RETIRE THIS BLOCK when the watchdog has read a non-zero `scope:'executed_steps'` population on real customer runs with no `kept` regression and no below-stop-point red \u2014 SUBJECT TO THE CORPUS CAVEAT ABOVE: if no corpus that produces halts without a proven arm becomes available by the review date, retire it instead on an explicit steering acknowledgement that the pilot mirror structurally cannot produce that population, rather than leaving it open forever on a condition that cannot be met.",evidence:"PRIMARY (the graduation): ~/agentiqa-agents/lio-harvest/reports/rehearsal-2026-09-11-preserve-fixtures.md, section `Registry gate (3) - re-run with intact raw (2026-09-11)` \u2014 THE RUN THIS GRADUATION RESTS ON. 60 runs in ONE batch (2 cases x 15 replicates x 2 flag states, arms in parallel) at `origin/staging` a79a61d5f with `dist` rebuilt and verified to carry both #3072 and #3148: 25 forced-`=1` halts against a bar of >= 20, 24 of them `kept=3 wiped=0` and the 25th a DECLINED turn on the pre-flag `browser_unobservable` arm (see gate item (3)); 13 firings of this flag's own arm across BOTH fixtures against 0 in shadow; 12 paired shadow halts with `wiped > 0`, including 11 of 11 on the target fixture; zeros on every safety counter over all 60 runs; no arming leak (25/25 `enforcing: true` against 25/25 `false`). THE RAW IS INTACT AND OUTSIDE THE MANAGED SCRATCHPAD \u2014 78 MB, 60 ndjson + 60 run.json, at ~/agentiqa-agents/lio-harvest/work/gate3-3072-rerun/ \u2014 so every number here is re-derivable from raw events; cost from that same raw is one extra bounded generateText call per 1.9 blocked runs. CORROBORATION, retained but NOT relied on: the same report's earlier 24-run 4-case lane and its 60-run `Registry gate (3) - N>=20` pass (21 forced-`=1` halts all `kept=3 wiped=0`, 6 paired shadow losses all `kept=0 wiped=4`), which carry the provenance caveat the report discloses (section 12, anomaly 5) \u2014 batch 1's raw diag was destroyed mid-run by an unrelated parallel session, its verbatim 40/40 census extract survives next to the report, and batch 2's intact raw reproduced every conclusion at N = 8. Nothing in this graduation now depends on a rehydrated row. SECONDARY, THE FRESHEST MEASUREMENT ON REAL CUSTOMER TRAFFIC: ~/agentiqa-agents/lio-harvest/reports/rehearsal-2026-09-11-combined.md sections 3.2 / 3.3 / 7 \u2014 the 12:29->13:56Z combined forced-ON window, 19 runs, 14 passed / 3 failed / 2 blocked, the FIRST pass on which this flag's population reproduced: 2 `blocked` terminals, 2 marker-1 rows both `enforcing: true` and both `kept=24 wiped=0`, against a paired `=0` control on the same engine and plan also `kept=24 wiped=0` \u2014 delta EXACTLY ZERO, measured rather than absent, and the arm still did not open because `browser_unobservable` took both halts first. Its own verdict is 'no regressions; GO does not follow from the mirror', and it is cited on exactly those terms. ALSO SECONDARY, and an EMPTY measurement rather than a silent one: ~/agentiqa-agents/lio-harvest/reports/rehearsal-2026-09-11-preserve-p1.md \u2014 the staging-mirror forced-ON pass over 17 fixed customer plans, 13 passed / 4 failed / 0 blocked, in which this arm fired ZERO times (the single concluding turn in the batch was opened by the pre-existing `oracle_fail` arm and books the identical verdict with the flag off), 0 regressions against baseline. That pass is a NO-GO on its own terms and is cited as such: it is what establishes the residual above, not what graduates the flag. LIVE STREAM: `blocked_synthesis:would_preserve` (survives the flip) and `concluding_turn:offered {scope:'executed_steps'}` from the nightly qa-exhaustive runner lane. UNIT ACCEPTANCE: packages/engine-core/src/__tests__/RunnerRuntime.blockedSynthesisCensus.test.ts (the re-keyed post-flip parity triad) and RunnerRuntime.blockedSynthesisEnforce.test.ts (the arm, the run-level clamp on every blockKind, the passed refusal, the latch, the mark-0 refusal and the stop-point floor); packages/engine-core/src/__tests__/killSwitchDefaultState.test.ts carries the graduation in INTENTIONALLY_GRADUATED.",owner:"steering (Alex)",review:"2026-09-25"}},REVISION_CRITERIA_CARRYOVER:{key:"REVISION_CRITERIA_CARRYOVER",envVars:["AGENTIQA_REVISION_CRITERIA_CARRYOVER","AGENTIQA_EXPERIMENT_REVISION_CRITERIA_CARRYOVER"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Backstop that carries prior-draft criteria forward when a plan revision strips ALL criteria across ALL steps; off lets a total criteria-strip through.",designDoc:"packages/engine-core/src/revisionCriteriaCarryOver.ts",status:"active",added:"2026-07-07"},CREDENTIAL_GUARD:{key:"CREDENTIAL_GUARD",envVars:["AGENTIQA_CREDENTIAL_GUARD","AGENTIQA_EXPERIMENT_CREDENTIAL_GUARD"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Credential-fabrication guard: injects the login-prohibition prompt wording and fires the false-premise report_issue gate; off omits both (pre-feature behavior).",designDoc:"packages/engine-core/src/testingEmailPolicy.ts",status:"active",added:"2026-07-06"},CREDENTIAL_BINDING:{key:"CREDENTIAL_BINDING",envVars:["AGENTIQA_CREDENTIAL_BINDING","AGENTIQA_EXPERIMENT_CREDENTIAL_BINDING"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Runner deterministic credential-step binding (v1.1): when a plan step unambiguously references exactly one non-generic stored credential AND the model's own type_project_credential_at pick was a generic field word, the mismatched credentialName is overridden to the referenced one (credential_binding_override diag) \u2014 a non-generic pick is never rewritten; and when a credential fill is followed within a 3-action adjacency window by a 401/403 while the step-referenced credential is untried and was not the last one filled, ONE report_issue/exploration_blocked per run is deflected toward that specific credential. A genuine 401 of the step's own credential, an unrelated stray 401, or a step naming no credential all proceed to the report. Off restores the model's free credential pick and no auth-failure nudge (pre-feature behavior).",designDoc:"docs/plans/2026-07-19-ag7727-run-fidelity-fixes-design.md",status:"active",added:"2026-07-19"},EMAIL_CODE_PROVENANCE:{key:"EMAIL_CODE_PROVENANCE",envVars:["AGENTIQA_EMAIL_CODE_PROVENANCE","AGENTIQA_EXPERIMENT_EMAIL_CODE_PROVENANCE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Verification-code write provenance gate: a write the model TYPED as fieldPurpose='verification_code' is refused unless its value equals a whole code token in an email actually fetched via check_email this run (subject + text + html-as-text, token-equality not raw substring). Off passes the write through ungrounded (pre-feature behavior), so a guessed or fabricated code can mutate the page into a false success state.",designDoc:"packages/engine-core/src/emailVerificationGate.ts",status:"active",added:"2026-07-12"},EMAIL_REPLY_COMPLETION:{key:"EMAIL_REPLY_COMPLETION",envVars:["AGENTIQA_EMAIL_REPLY_COMPLETION","AGENTIQA_EXPERIMENT_EMAIL_REPLY_COMPLETION"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Required-email-reply completion gate on the Explorer's assistant_v2_report: when the Coordinator typed the task with the `email_send` capability AND this Explorer wrote the canonical testing email into the app, an ok/done report is refused (status email_reply_incomplete) until the runtime holds a server-confirmed outbound reply. The correction branches three ways \u2014 reply still owed (check_email/reply_to_email/verify), flow already completed WITHOUT an outbound reply (report complete, restate the in-product evidence), or a required reply that could not be delivered (blocked) \u2014 so a defensively-stamped email_send never downgrades a genuinely complete area. Bounded at 2 rejections per turn, then fails open: the report is accepted with a hedged undelivered-reply disclosure appended to the model's summary. Sits BELOW the email-wait conversion, so a run whose inbox is still empty reaches the user-decidable Stop/Wait pause instead, and exempts structured child-protocol summaries (login_ok:/signup_failed_*:) from both the rejection and the disclosure. Off restores pre-feature behavior: a request-submitted-only run reports clean with no rejection and no disclosure.",designDoc:"packages/engine-core/src/emailReplyHonesty.ts",status:"active",added:"2026-08-20"},VERBATIM_INPUT_PIN:{key:"VERBATIM_INPUT_PIN",envVars:["AGENTIQA_VERBATIM_INPUT_PIN","AGENTIQA_EXPERIMENT_VERBATIM_INPUT_PIN"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Verbatim user-input payload pinning: committed text writes carry a source-action id, field identity, and model-authored purpose role; an exact match to a configured login-email identity is forced to credential_identifier from typed project state. On a source-bound run, a text-entry step's verbatimInput is kept ONLY when its exact source action resolves uniquely and has inputRole=user_supplied_payload; duplicate, credential/generated/other, missing, conflicting, and ambiguous bindings are omitted. The value must also provenance-match user chat/attachment text. Legacy source-less producers retain the corpus-only gate. RunnerRuntime types the accepted pin verbatim (slot wins over prose). Off restores the prior write-tool contract (inputRole optional, legacy descriptions) and disables capture/attach plus runner consumption.",designDoc:"docs/plans/2026-07-13-verbatim-input-payload-design.md",status:"active",added:"2026-07-13"},VERIFY_ORACLE_FIDELITY:{key:"VERIFY_ORACLE_FIDELITY",envVars:["AGENTIQA_VERIFY_ORACLE_FIDELITY","AGENTIQA_EXPERIMENT_VERIFY_ORACLE_FIDELITY"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Verify-oracle fidelity (verbatim-input Phase 2): a deterministic post-generation pass at both producer gates (CoordinatorRuntime.handleSaveTestPlan + the Explorer draft path), after pinVerbatimInputSteps, that restores user-authored acceptance criteria the LLM summarized away. It captures verification check-bullets under a high-precision verification-heading line (r2: heading ends with ':' and its core matches a verification PHRASE whole, not merely contains a strong token; each bullet captured unless action-imperative-shaped) plus a bounded single-paragraph anti-false-pass lookback, tests each against the persisted verify-side content (verify-step texts + criteria checks + expectedValue pins) by literal-atom containment (quoted spans + numerals/ranges with EN/RU 0\u201320 spelled-form equivalence; atom-free \u2192 normalized-token containment \u2265 0.6), and APPENDS a verify step for every uncovered item carrying the user's bullet verbatim as both the step text AND a compiled criterion ({check, strict:true}, r2/F7 \u2014 so the appended step passes validateDraftPlanSteps and never hits the runner's criteria-less synthesis path). Append-only (never edits/deletes an existing step), fail-open (any error \u2192 save unchanged). Off restores pure LLM-compliance generation (summarized oracles persist as-is). Emits an oracle_fidelity diag {bullets, covered, appended, lookbackCaptured, looseBullets, disabled} regardless of flag state (looseBullets = diag-only recall telemetry, never appends).",designDoc:"docs/plans/2026-07-15-verify-oracle-fidelity-design.md",status:"active",added:"2026-07-15"},TYPE_NEWLINE_SAFE:{key:"TYPE_NEWLINE_SAFE",envVars:["AGENTIQA_TYPE_NEWLINE_SAFE","AGENTIQA_EXPERIMENT_TYPE_NEWLINE_SAFE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Multi-line text typing: embedded newlines are entered as Shift+Enter soft line breaks (never a raw Enter keypress) so a multi-line prompt does not premature-submit an Enter-to-send composer (e.g. Miro/Slack chat sidekicks). Off restores the raw keyboard.type(text) behavior where each "\\n" fires an Enter keypress. Applies to all four text-typing paths (typeTextAt / typeByRef / typeByLabel / setFocusedInputValue fallback); pressEnter still appends one trailing Enter regardless.',designDoc:"packages/engine-core/src/typeMultiline.ts",status:"active",added:"2026-07-13"},CANVAS_TYPE_GROUNDING:{key:"CANVAS_TYPE_GROUNDING",envVars:["AGENTIQA_CANVAS_TYPE_GROUNDING","AGENTIQA_EXPERIMENT_CANVAS_TYPE_GROUNDING"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Canvas typed-text grounding: after a coordinate type_text_at on a canvas-dominant screen, verify direct canvas writes and press-Enter commits from transient DOM editors. Both start with an a11y/DOM positive check on the reused post-action snapshot, else a pixel route gated by an ambient-animation pre-check (two reads a settle apart); a self-animating board yields uncertain_animated \u2014 byte-identical to flag-off, recorded via a canvas_type_verify diag event. DIRECT mode (focus not a DOM editor, no Enter) diffs the full viewport against the 2nd pre-capture: any change = landed (off-clip renders included), byte-identical = confident negative \u2192 explicit failure (metadata.error + canvasTypeVerification='text_not_found') with a switch-strategy hint. COMMIT mode (#2615) arms on a coordinate double-click whose focused editor sits DIRECTLY over a canvas, holds both elements as live handles (instance identity, not a DOM fingerprint), and takes its pre-edit baseline from that canvas's BACKING STORE (toDataURL digest) \u2014 which excludes every DOM overlay by construction, so nothing on the page is mutated and no element is hidden to take the reading. The next press-Enter type on the same editor instance re-reads the same canvas: changed = landed; identical = canvasTypeVerification='commit_unconfirmed', a hedged metadata.note (never metadata.error) telling the model the value may not have persisted OR may equal what the cell already showed, and to read the board before treating the edit as done or filing a bug. Abstains (fail-open) on a tainted canvas, an empty drawing buffer (WebGL without preserveDrawingBuffer), a detached canvas, a lost arm, or a read that exceeds its 2s runtime-side budget. Each backing-store read downscales the board onto a detached scratch canvas capped at 262144 pixels before encoding, so the SUT main-thread cost is independent of board size (a dpr-2 full-viewport board would otherwise PNG-encode multiple MB three times per gesture); the trade-off is that a sub-scale-factor change can be averaged away, which in commit mode reads as the hedged note and never as a failure. \u22643 verification reads per qualifying action (screenshots in direct mode, canvas reads in commit mode). Off restores the pre-feature bare-success canvas type: no arming probe, no verification reads, no a11y check.",designDoc:"docs/plans/2026-07-14-canvas-chat-failure-class-design.md",status:"active",added:"2026-07-14"},RESULT_FIRST_DISCOVERY:{key:"RESULT_FIRST_DISCOVERY",envVars:["AGENTIQA_EXPERIMENT_RESULT_FIRST_DISCOVERY"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"env-const-disabled",gates:'Result-first discovery (#373): auto-approve discovered scope and run high-risk areas immediately; off restores the "question-first" scope-approval checkpoint that waits.',designDoc:"packages/engine-core/src/resultFirstDiscovery.ts",status:"active",added:"2026-06-01",notes:"Single spelling: reads ONLY AGENTIQA_EXPERIMENT_RESULT_FIRST_DISCOVERY (no bare AGENTIQA_ spelling). NOT migrated to killSwitchDisabled \u2014 that would add the bare spelling and change behavior. Also has a per-session config opt-out."},SCOPE_PROVENANCE:{key:"SCOPE_PROVENANCE",envVars:["AGENTIQA_EXPERIMENT_SCOPE_PROVENANCE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"env-const-disabled",gates:"Scope-provenance-aware plan selection: user-enumerated medium/low areas still run in the first pass; off falls back to the legacy risk-only split.",designDoc:"packages/engine-core/src/resultFirstDiscovery.ts",status:"active",added:"2026-07-01",notes:"Single spelling: reads ONLY AGENTIQA_EXPERIMENT_SCOPE_PROVENANCE. NOT migrated to killSwitch (would add a bare spelling)."},MEMORY_WRITEBACK:{key:"MEMORY_WRITEBACK",envVars:["AGENTIQA_EXPERIMENT_MEMORY_WRITEBACK"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"env-const-enabled",gates:"Verified-memory writeback: offers the save_verified_memory tool and activates the durable write path; off leaves writeback inert.",designDoc:"docs/plans/2026-07-04-verified-memory-writeback-design.md",status:"active",added:"2026-07-04",notes:"Single spelling: reads ONLY AGENTIQA_EXPERIMENT_MEMORY_WRITEBACK. NOT migrated to killSwitchEnabled (would add a bare spelling). Also has a per-session config force-on for evals.",graduation:{status:"gated",gate:"Model-elicitation quality gate from the 2026-07-04 verified-memory-writeback design doc closes",evidence:"design-doc gate + eval force-on runs",owner:"steering (Alex)",review:"2026-08-01"}},FAST_START_PROMPT:{key:"FAST_START_PROMPT",envVars:["AGENTIQA_EXPERIMENT_FAST_START_PROMPT"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"is-experiment-enabled",gates:"Explorer prompt variant: emits the fast-start initial prompt instead of the standard one.",designDoc:"packages/engine-core/src/ExplorerRuntime.ts",status:"active",added:"2026-06-01",notes:'Single spelling via isExperimentEnabled (exact "1"). NOT a killSwitch flag \u2014 no bare AGENTIQA_ spelling honored.',graduation:{status:"parked",gate:"No rollout intent; delete registry entry + code path if still unused by 2026-09-01"}},MINIMAL_INITIAL_CONTEXT:{key:"MINIMAL_INITIAL_CONTEXT",envVars:["AGENTIQA_EXPERIMENT_MINIMAL_INITIAL_CONTEXT"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"is-experiment-enabled",gates:"Explorer context variant: sends a minimal initial context to the model instead of the full one.",designDoc:"packages/engine-core/src/ExplorerRuntime.ts",status:"active",added:"2026-06-01",notes:'Single spelling via isExperimentEnabled (exact "1"). NOT a killSwitch flag \u2014 no bare AGENTIQA_ spelling honored.',graduation:{status:"parked",gate:"No rollout intent; delete registry entry + code path if still unused by 2026-09-01"}},STEERING_VETO:{key:"STEERING_VETO",envVars:["AGENTIQA_STEERING_VETO"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!1,read:"env-direct",gates:"Supervisor steering veto: at a redirect, refuses the recent loop-set of action fingerprints for a bounded TTL/budget; off passes those calls through.",designDoc:"packages/engine-core/src/steeringVeto.ts",status:"active",added:"2026-06-01",notes:'Reads ONLY the bare AGENTIQA_STEERING_VETO (via direct process.env access, "!== 0"). It has NO AGENTIQA_EXPERIMENT_ spelling, so the orchestrator passthrough cannot flip it in a cloud pod (cloud-unreachable \u2014 known limitation). NOT migrated to killSwitchDisabled: that would add the EXPERIMENT_ spelling and change behavior/reachability.'},AUTH_DISABLE_EMAIL_VERIFICATION:{key:"AUTH_DISABLE_EMAIL_VERIFICATION",envVars:["AUTH_DISABLE_EMAIL_VERIFICATION"],polarity:"1-enables",defaultState:"off",surfaces:["web-next-node"],cloudForwarded:!1,read:"env-direct",gates:"On-prem escape hatch: disables signup email verification; IGNORED (fail-safe) on a hosted Vercel deploy where VERCEL/VERCEL_ENV is present.",designDoc:"apps/web-next/lib/auth-flags.ts",status:"active",added:"2026-06-01",notes:"Not AGENTIQA_-prefixed. Static process.env member access (edge-safe). Hosted-platform sentinel forces it off on Vercel.",graduation:{status:"permanent",gate:"On-prem operational escape hatch, not an experiment \u2014 never graduates; forced off on hosted Vercel"}},RUN_DISPATCH_MUTEX:{key:"RUN_DISPATCH_MUTEX",envVars:["RUN_DISPATCH_MUTEX"],polarity:"1-enables",defaultState:"off",surfaces:["web-next-edge"],cloudForwarded:!1,read:"env-direct",gates:"Per-plan run-dispatch mutual exclusion (AG-8221): when on, a new run of a plan that already has a non-terminal run either supersedes a ZOMBIE predecessor (no activity for RUN_DISPATCH_ZOMBIE_MS, default 600s \u2014 a measured floor, see packages/shared-types/src/runDispatch.ts) or is rejected 409 plan_run_active behind a LIVE one. Off = SHADOW: the same scan and decision run and log run_dispatch_mutex:would_supersede/would_block, but every caller is told to proceed and nothing is written.",designDoc:"packages/shared-types/src/runDispatch.ts",status:"active",added:"2026-07-30",notes:"Not AGENTIQA_-prefixed. Static process.env member access via runDispatchMutexEnforced() in apps/web-next/lib/run-dispatch-mutex.ts (single read point, edge-safe). The engine only relays the decision \u2014 it reads no flag \u2014 so this one env var is the whole gate. Companion tuning knob RUN_DISPATCH_ZOMBIE_MS overrides the zombie threshold; it is a threshold, not a behavior gate, so it is deliberately not a registry entry.",graduation:{status:"gated",gate:"Shadow diags from the Lio daily bursts show would_block/would_supersede firing only on genuine overlaps, with zero would_supersede against a run that later produced a verdict (a superseded-live event is the dangerous direction and blocks graduation).",evidence:"run_dispatch_mutex:would_* lines from web-next logs, cross-checked against the superseded runs' final status in app.test_plan_runs.",owner:"Alex",review:"2026-08-13"}},ORG_ENTITLEMENT_ENABLED:{key:"ORG_ENTITLEMENT_ENABLED",envVars:["ORG_ENTITLEMENT_ENABLED"],polarity:"0-disables",defaultState:"on",surfaces:["web-next-node"],cloudForwarded:!1,read:"env-direct",gates:'Company-plan org entitlement (O0+): when on, an active OrgMembership resolves the billing subject to the org and its plan wins over the personal plan in all entitlement gates; "0" forces the personal subject everywhere (org rows become inert).',designDoc:"docs/plans/2026-07-11-company-plan-access-control-design.md",status:"active",added:"2026-07-11",notes:"Not AGENTIQA_-prefixed. Static process.env member access via isOrgEntitlementEnabled() in apps/web-next/lib/billing-subject.ts (single read point). Dark by data until an Organization row exists \u2014 with zero orgs the flag has no observable effect."},ORG_PROJECT_SHARING:{key:"ORG_PROJECT_SHARING",envVars:["ORG_PROJECT_SHARING"],polarity:"0-disables",defaultState:"on",surfaces:["web-next-edge","web-next-node"],cloudForwarded:!1,read:"env-direct",gates:"Org project sharing (Company plan): when on, `projectScopedGrantFor` lets a member of the OWNER's active Company-tier org read AND write the shared child entities of a project whose owner set `app.projects.org_shared = true` (test-plans, test-plan-runs, batch-runs, issues, labels, app-map), and widens the owner's own child queries from user-scoped to project-scoped so the owner sees what teammates ran and edited. \"0\" collapses every project grant to owner-only \u2014 reads re-narrow to the caller's own rows and writes re-gate to the owner \u2014 with no redeploy and no data change. NEVER gates credentials (plaintext secrets are excluded from the share by design), memory, journal, or Project Files.",designDoc:"docs/plans/2026-08-03-org-project-sharing-design.md",status:"active",added:"2026-08-03",notes:"Not AGENTIQA_-prefixed, so NOT cloud-forwarded: the orchestrator forwards only AGENTIQA_EXPERIMENT_* into engine session pods, and this flag is read exclusively in web-next (Vercel env), never in the engine \u2014 same shape as ORG_ENTITLEMENT_ENABLED. Static process.env member access via isOrgProjectSharingEnabled() in apps/web-next/lib/org-read-grant.ts (single read point; the sync-entity routes are edge functions, so the read must stay a static literal member access). Dark by data twice over: org_shared defaults false, and a true toggle still grants nobody unless the owner is an active member of an enterprise-tier org \u2014 so with zero shared projects the flag has no observable effect. ORG_ENTITLEMENT_ENABLED=0 also collapses this grant, because the Company-org half resolves through resolveBillingSubject."},EMAIL_REPLY_ENABLED:{key:"EMAIL_REPLY_ENABLED",envVars:["EMAIL_REPLY_ENABLED"],polarity:"0-disables",defaultState:"on",surfaces:["web-next-edge","web-next-node"],cloudForwarded:!1,read:"env-direct",gates:'Whether an agent may ANSWER a message delivered to its testing alias (`reply_to_email` \u2192 POST /api/email/reply). Sending is the only step of the email path that leaves our infrastructure, so it has a switch that needs no deploy: "0" turns every reply attempt into a clean 503 and makes the readiness probe report `canSend: false`, so sessions created afterwards do not even offer the tool \u2014 while reading the inbox (check_email) keeps working. Delivery itself is SMTP from the same mailbox the read path uses, under the same GMAIL_USER / GMAIL_APP_PASSWORD app password, `From` = the session alias. The tool can only answer a message that was actually delivered to that alias (the recipient is resolved server-side from the stored message, never from client input), so the switch is a blast-radius control, not the authorization boundary.',designDoc:"docs/qa-transparency/OPERATING-LOOP.md",status:"active",added:"2026-08-13",notes:'Not AGENTIQA_-prefixed, so NOT cloud-forwarded: read only in web-next (Vercel env), like ORG_ENTITLEMENT_ENABLED / EMAIL_ALIAS_STRICT. Read points: replyEnabled() in apps/web-next/lib/emailReply.ts (node) and the two readiness routes, one of which is EDGE \u2014 so the read must stay a static literal member access. Default ON because the surrounding gates already bound it: server-minted alias owned by the caller, a message that must actually exist in that alias inbox, a per-owner hourly attempt cap, and an email_send_log row per attempt. Claim: email.reply-flow. DEPLOYMENT REQUIREMENT (measured 2026-08-13): delivery must go through the Workspace SMTP RELAY, not plain smtp.gmail.com. Gmail rewrites `From` to the account address unless the sender is a confirmed "Send mail as" alias, and per-run aliases cannot be confirmed in advance \u2014 the reply then arrives from a-agent@ with the alias surviving only in Reply-To, with NO SMTP error to notice. The relay (Admin Console -> Apps -> Google Workspace -> Gmail -> Routing -> SMTP relay service; Allowed senders = Only addresses in my domains; Require SMTP Authentication; Require TLS) accepts any address in our domains and preserves the alias, verified end to end on staging. Selected by GMAIL_SMTP_HOST=smtp-relay.gmail.com / GMAIL_SMTP_PORT=587 / GMAIL_SMTP_SECURE=false, set in the custom staging env 2026-08-13; PRODUCTION STILL NEEDS THEM before this ships there, or prod silently reverts to the rewritten From. There is no API for the relay setting \u2014 GAM has no gmailrouting command and the Policy API exposes only 8 Gmail settings, none of them routing.'},EMAIL_ALIAS_STRICT:{key:"EMAIL_ALIAS_STRICT",envVars:["EMAIL_ALIAS_STRICT"],polarity:"1-enables",defaultState:"off",surfaces:["web-next-edge","web-next-node"],cloudForwarded:!1,read:"env-direct",gates:'Whether an UNSIGNED testing-inbox alias may still be read. One physical mailbox (GMAIL_USER) serves every tenant through plus-aliases, and until 2026-08-13 both fetch routes authorized on the alias SHAPE alone while the suffix was a base36 Date.now() \u2014 i.e. another tenant\'s signup codes and password-reset links were readable by anyone who guessed a millisecond. Server-minted aliases now carry an HMAC tag over their owner and are verified per read; a tag that verifies for somebody else, or a minted alias with no identity to check against, is refused REGARDLESS of this flag. This flag governs only the legacy shape (`<inbox-local>+<suffix>` with no tag), which packaged Desktop builds in the field still mint for themselves: default OFF serves it with a warning, "1" refuses it. Flipping it closes the enumeration hole for good and breaks every client that has not been updated \u2014 flip once the published Desktop manifest no longer self-mints.',designDoc:"docs/qa-transparency/OPERATING-LOOP.md",status:"active",added:"2026-08-13",notes:"Not AGENTIQA_-prefixed, so NOT cloud-forwarded: the orchestrator forwards only AGENTIQA_EXPERIMENT_* into engine session pods, and this is read exclusively in web-next (Vercel env) \u2014 same shape as ORG_ENTITLEMENT_ENABLED / PROJECT_RUN_LOCK. Single read point aliasStrictMode() in apps/web-next/lib/emailAlias.ts; the module is imported by an EDGE route (/api/email/readiness), so every env read must stay a static literal member access. Companion vars, both non-gating: EMAIL_ALIAS_SECRET (mints new tags; falls back to AUTH_SECRET / NEXTAUTH_SECRET, so no env rollout is needed) and EMAIL_ALIAS_SECRET_PREVIOUS (verification-only, so a secret rotation does not invalidate aliases already in flight). Claim: email.inbox-tenancy.",graduation:{status:"gated",gate:'Flip to "1" once no client in the field still self-mints an alias. Cloud sessions and the current Desktop/CLI build take the server-minted alias as of this change, so the remaining population is packaged Desktop builds released before it, plus any long-lived session started before the deploy. Concretely: the published manifest-latest.json version is at or above the first release carrying the server-minted alias, AND a week of readiness/fetch logs shows no `testing-inbox read on an unsigned alias` line. Until it flips, the enumeration hole stays open for exactly that legacy shape \u2014 this is a rollout window, not a permanent posture.',evidence:"desktop manifest-latest.json version \u2265 the first build that reads `alias` from /api/email/readiness; a 7-day window of web-next logs with zero unsigned-alias warnings; the two-user staging scenario (user B requests user A alias \u2192 403) that lights this claim at L2",owner:"steering (Alex)",review:"2026-09-01"}},PROJECT_RUN_LOCK:{key:"PROJECT_RUN_LOCK",envVars:["PROJECT_RUN_LOCK"],polarity:"1-enables",defaultState:"off",surfaces:["web-next-edge"],cloudForwarded:!1,read:"env-direct",gates:"Per-PROJECT run concurrency lock (P1.6): at most ONE live run OR batch per project at a time, across ALL identities \u2014 the owner, any member of the owner's Company org on a shared project, and ServiceKey CLI runs alike. Enforced at the two server seams that create the rows: the new-row path of `PUT /api/sync/entities/test-plan-runs/[id]` and of `PUT /api/sync/entities/batch-runs/[id]`. A refused start gets 409 `{code:'PROJECT_RUN_IN_PROGRESS', runId, kind, startedAt, startedByLabel}` naming who holds the project and since when \u2014 never 403 (a 403 on an entity write trips the SPA signOut trap, HC1) and never a 200 no-op. Liveness is GREATEST(run.updated_at, run_billing.last_step_at) for a single run and the freshest of the batch row and ALL its legs for a batch, with a 90-minute silence TTL; a holder past the TTL stops blocking and is opportunistically reaped to status='cancelled' (+ termination_reason='stale_lock_reap' on runs, which bills zero). NEVER blocks an update to an EXISTING row \u2014 only the creation of a new one \u2014 nor a leg of an already-admitted batch. The exemption is EXISTENCE, not status: until 2026-08-03 any PUT whose `status` was already terminal skipped the lock outright, which let a refused caller INSERT itself as a brand-new terminal row simply by finishing (measured web bypass) \u2014 so a first PUT that is already terminal is now admitted whenever nothing holds the project (the desktop late-sync case) and refused only while another run/batch of that project is live. \"0\" removes the lock entirely: both seams revert to an unguarded upsert with no reap and no 409. RETIRED 2026-08-04 \u2014 now default OFF, \"1\" restores it. Its rationale opened with \"the thing two concurrent runs collide on is the TARGET APPLICATION and the credentials used to reach it, and in this product both are properties of the PROJECT\": the credential half was never true (app.project_credentials has carried user_id since its first migration and the read path has always been WHERE user_id = caller), and the data half was declined by the customer it was built for (\"two accounts will operate on different data and test cases in 95-99% of cases \u2026 we're fine not handling that case\"). The credential POOL contends on the ACCOUNT \u2014 the resource that is actually shared \u2014 and hands a distinct one to each caller instead of refusing the second run. What made keeping it untenable rather than merely redundant: the lock is active ONLY on org-shared projects, exactly where the pool lives, so it gated the one scenario the feature exists for \u2014 and did so AFTER the caller had already leased an account, leaving that slot held until teardown. Nothing is deleted: the transaction, the reap and the 409 contract are the shape a per-account guard needs, and lib/credential-pool.ts already reuses them.",designDoc:"docs/plans/2026-08-03-org-project-sharing-design.md",status:"active",added:"2026-08-03",notes:"Not AGENTIQA_-prefixed, so NOT cloud-forwarded: the orchestrator forwards only AGENTIQA_EXPERIMENT_* into engine session pods, and this flag is read exclusively in web-next (Vercel env), never in the engine \u2014 same shape as ORG_PROJECT_SHARING. Static process.env member access via projectRunLockEnabled() in apps/web-next/lib/project-run-lock.ts (single read point; both seams are edge functions, so the read must stay a static literal member access). Scope is deliberately the PROJECT, not the plan and not the org: credentials and default_url are project-scoped, so two runs of DIFFERENT plans in one project drive the same account against the same environment \u2014 which the pre-existing per-plan mutex RUN_DISPATCH_MUTEX (AG-8221) does not cover \u2014 while an org-wide lock would serialize unrelated products that merely share a payer. The two locks are complementary and use deliberately different staleness thresholds because their safe directions are opposite (AG-8221 may KILL a predecessor, so it wants the smallest value above the observed silence distribution: 600s; this lock only REFUSES a newcomer, so it wants a value above the longest legitimate LIFETIME: the 90-minute sandbox ceiling, overridable via PROJECT_RUN_LOCK_TTL_MS). RETIRED 2026-08-04 (default OFF): with the parent off, PROJECT_RUN_LOCK_UNSHARED cannot revive it either \u2014 pinned by projectRunLockApplies returning false for BOTH shapes, so a stray child flag left in some environment is inert. Fail-open on any error \u2014 a guard that refuses customers' runs because its own bookkeeping broke is worse than the overlap it prevents.",graduation:{status:"permanent",gate:"RETIRED 2026-08-04, not parked: this does not graduate back to default-ON. The credential pool contends on the ACCOUNT \u2014 the resource two concurrent runs actually share \u2014 while this lock refused the whole PROJECT, was active only where the pool lives (org-shared projects), and consumed a leased account before refusing. Kept as `=1` for the one case it would still answer: whole-project serialisation for a plan that signs in with nothing, on a project whose data two runs would genuinely corrupt. Nobody has asked for that; the customer it was built for declined it explicitly.",evidence:"a project that needs whole-project serialisation despite per-account leasing \u2014 none known",owner:"steering (Alex)",review:"2026-11-04"}},PROJECT_RUN_LOCK_UNSHARED:{key:"PROJECT_RUN_LOCK_UNSHARED",envVars:["PROJECT_RUN_LOCK_UNSHARED"],polarity:"1-enables",defaultState:"off",surfaces:["web-next-edge"],cloudForwarded:!1,read:"env-direct",gates:"Extends the PROJECT_RUN_LOCK enforcement (above) from org-SHARED projects to NON-shared, single-owner projects. OFF (default): a project with no live org share is never locked and both seams behave exactly as they do today \u2014 no probe, no reap, no 409. ON: the same one-live-run-or-batch-per-project rule applies to every project regardless of sharing. Parented by PROJECT_RUN_LOCK (=0 disables both halves).",designDoc:"docs/plans/2026-08-03-org-project-sharing-design.md",status:"active",added:"2026-08-03",graduation:{status:"gated",gate:"NOT graduable until concurrent solo runs are either migrated or accepted as breakage. MEASURED ON PROD 2026-08-03: over the trailing 90 days, 50 pairs of SAME-USER runs in ONE project genuinely overlapped in time \u2014 8 distinct users, 8 distinct projects, every pair with BOTH rows terminal (so real concurrency, not a zombie artefact), 35 pairs overlapping >60s and 10 >5 minutes, and every single pair origin='desktop'. Flipping this ON today would start answering 409 to a workflow eight people use. Graduation therefore needs, in order: (1) a decision on whether concurrent desktop runs in one project are legitimate (the desktop app drives a LOCAL browser, so the collision domain argument is weaker there than for cloud runs) \u2014 if they are, the flag needs a per-origin carve-out rather than a flip; (2) if they are not, a client-side surface that explains the refusal before the user starts the second run, since the CLI/SPA today learn about it only from the 409; (3) a shadow census from the shared-project half showing the refusal rate and that no refusal was a false positive. Note the lock cannot be graduated by soak alone: with the shared half dark by data (no org_shared projects on prod), the shared half produces no evidence about the solo population.",evidence:"a product decision on concurrent same-project desktop runs (the measured 50-pair / 8-user population); client-side pre-start surfacing so a refusal is not first learned from a 409; and a shared-project refusal census with zero false positives",owner:"steering (Alex)",review:"2026-08-17"},notes:"Same read discipline as its parent: static process.env member access via projectRunLockUnsharedEnabled() in apps/web-next/lib/project-run-lock.ts, web-next-edge only, never cloud-forwarded. Exists as a separate key rather than a mode on PROJECT_RUN_LOCK because the two populations have opposite evidence: the shared-project case has ZERO current population and ships enforcing, while the solo case has a measured live population and must ship dark. One flag cannot carry two defaults."},PROJECT_CREDENTIAL_POOL:{key:"PROJECT_CREDENTIAL_POOL",envVars:["PROJECT_CREDENTIAL_POOL"],polarity:"0-disables",defaultState:"on",surfaces:["web-next-node"],cloudForwarded:!1,read:"env-direct",gates:"Project credential pool (2026-08-04): a project may hold SEVERAL ACCOUNTS \u2014 each a LOGIN/PASSWORD PAIR \u2014 under one role NAME (`scope='project'` rows of `app.project_credentials`, keyed `(project_id, name, slot)`), provisioned by the project owner or an `OrgMembership.role='admin'` of the owner's org. A caller holding no PERSONAL credential of that name LEASES one for the life of an engine session and releases it at teardown; the lease returns the account's real LOGIN alongside its secret and the seed carries both, because `credential.name` is otherwise the login the agent types into the email field \u2014 a role with no login would be typed AS the login (corrected 2026-08-04 after the first merge shipped role/password, which was one login with N passwords); a lease older than the staleness cutoff is reaped so a lost release cannot wedge a slot (default 5400000 ms = 90 minutes, the sandbox wall-clock ceiling and deliberately the same number as PROJECT_RUN_LOCK_DEFAULT_TTL_MS; overridable via `CREDENTIAL_LEASE_TTL_MS`, whose invalid or non-positive values fall back to the default rather than disabling reaping \u2014 a pool that never reaps would wedge permanently after one lost release). Personal rows always win, so a caller with their own `admin` consumes no shared account and behaves exactly as before. `GET /api/sync/entities/credentials` gains pool NAMES \u2014 never secrets, because it is called on every pre-flight and every seed build; the ONLY route that returns a pool secret is `POST /api/projects/{id}/credential-pool/lease`, and only for the slots it just granted to that lease. An exhausted role answers 409 `{code:'CREDENTIAL_POOL_EXHAUSTED', names}`, which both clients re-tag as the existing `CREDENTIAL_PREFLIGHT_REFUSED` so the refusal lands before a run row or engine session exists. FAIL-CLOSED, unlike the sibling PROJECT_RUN_LOCK: a session seeded without its credential burns a full run to fail at the login step. \"0\" hides the pool everywhere \u2014 list returns personal rows only, leasing 404s, the settings section renders empty \u2014 with no redeploy and no data change. Since 2026-08-06 the pool list route also reports the flag itself as `poolEnabled` (false on the disabled early return, before the grant check, since the flag is a property of the deployment and not of the caller): the settings page infers \"off\" from an empty pool it may not administer, but the NEW-PROJECT form has no project to inspect and so kept offering a Whole-team tab whose write could only 404 \u2014 clients read `poolEnabled` and fail OPEN when it is absent. Dark by data: a project with no `scope='project'` row behaves exactly as today. Since 2026-08-06 (#2338) release is no longer purely client-driven: the seed carries `{projectId, credentialLeaseId}` into the engine session, and the ENGINE \u2014 which holds the secrets \u2014 releases at every teardown the browser cannot see (session DELETE, idle reap, browser-crash sweep, SIGTERM), so a closed tab or a dead pod no longer strands the slots for the whole TTL; a terminal run row may also carry the lease id and free it in the run-completion cascade, but ONLY when the client declared that run owns the whole lease (`credentialLeaseRunId`), because a batch leg and a Coordinator-spawned child both write terminal rows while their session's lease is still in use. The engine additionally REFRESHES `leased_at` (PATCH on the lease route) for a lease whose session is still active, throttled to once per `CREDENTIAL_LEASE_REFRESH_MS` (default 900000 ms = TTL/6) and gated on activity since the last refresh: the 90-minute TTL assumed no holding unit outlives it, which stopped being true when the chat turn became one. Slots also record `leased_kind` ('chat' | 'run', narrowed server-side) so project settings names what holds an account instead of asserting \"a running test\" over a chat, and a pool admin can force-release a stuck slot without retiring the account.",designDoc:"docs/plans/2026-08-04-project-credential-pool-design.md",status:"active",added:"2026-08-04",notes:"Not AGENTIQA_-prefixed, so NOT cloud-forwarded: read exclusively in web-next (Vercel env), never in the engine \u2014 same shape as ORG_PROJECT_SHARING and PROJECT_RUN_LOCK. Single read point: credentialPoolEnabled() in apps/web-next/lib/credential-pool.ts, as a static process.env member access. NODE surface, not edge: provisioning encrypts and leasing decrypts with node:crypto, which the edge runtime does not provide \u2014 the routes originally declared runtime='edge', which compiled, type-checked and passed all 20 DB-evaluated tests (they import the handlers directly under Node) and then answered 500 on the first live request. RELATIONSHIP TO PROJECT_RUN_LOCK: that lock models the PROJECT as the contended resource and refuses a second run outright; the pool models the ACCOUNT, which is what two concurrent runs actually collide on, and hands out a distinct one per caller. The run lock is the degenerate case of a pool of size one, and on a project WITH a pool it is the coarser instrument \u2014 it will refuse a run for which a free account exists. Note the run lock's own rationale cites 'credentials are project-scoped', which was never true: app.project_credentials has carried user_id since its first migration and the read path has always been WHERE user_id = caller. Recorded scope cut (design P6): v1 returns the leased secret to the CALLER'S OWN CLIENT, exactly as their own credential already travels, so a member can read a shared secret out of their client; closing that needs server-side seeding (P1.5), which touches the engine image (Pipeline B). There is no lease heartbeat: every normal exit releases explicitly, so the TTL covers only a release that never arrived (closed tab, killed CI job, crashed pod), and it errs long because the failure directions are asymmetric \u2014 a slot wrongly held for an hour costs one refusal, while a slot wrongly freed hands the same account to two live agents, the exact failure the pool exists to prevent. The narrowed HC3 invariant the suite pins: a caller may receive a secret only when it is their own row, OR it is a pool row of a project they may access AND they hold a live lease on that exact slot \u2014 no caller ever receives another MEMBER'S personal row, under any grant, on any route."},ORG_GRANT_DEBIT_AT_FINALIZE:{key:"ORG_GRANT_DEBIT_AT_FINALIZE",envVars:["ORG_GRANT_DEBIT_AT_FINALIZE"],polarity:"1-enables",defaultState:"off",surfaces:["web-next-edge"],cloudForwarded:!1,read:"env-direct",gates:"Moves the org run-grant debit from analytics-ingest to finalize-run (2026-07-30). At ingest neither termination_reason nor the unit's final length is known, so the grant counter over-debited every non-billable termination, could never apply the per-unit min_billable_steps floor, could not refund, and double-debited AG-8220 duplicate pairs \u2014 measured on lio as +1041 / -437 / net +615 units (+24.6 credits) of customer-visible over-consumption against the invoice-authoritative getOrgUsage. When ON, ingest debits nothing and /api/billing/finalize-run/[id] recomputes the whole billing UNIT against getOrgUsage's own math (same unit keying incl. the chat/explore fan-out collapse, same per-(unit,byok) floor, same billable-termination filter) and applies only the DELTA against run_billing.grant_debited_units, claimed atomically per billing unit (one non-interactive Neon transaction behind a pg_advisory_xact_lock on (org, user, unit_key)) \u2014 which makes the debit idempotent (a double-finalize settles 0, and so does a concurrent one), refundable (a non-billable unit recomputes to 0 and the units are credited back), and safe across the flip. ON also activates the in-flight HOLD in the org cap gate (getOrgInFlightHeldUnits): unfinalized runs' not-yet-debited steps are subtracted from remainingUnits, so the orchestrator's per-dispatch admission check keeps decrementing continuously instead of standing still for the whole duration of a run. OFF = SHADOW: the ingest debit continues with byte-identical org_run_grant writes; finalize additionally computes the would-be total, stamps run_billing.grant_shadow_units, and logs grant_debit:finalize_shadow \u2014 the soak corpus that gates graduation.",designDoc:"docs/plans/2026-07-23-ag7872-org-run-grants-design.md",status:"active",added:"2026-07-30",notes:"Not AGENTIQA_-prefixed. Static process.env member access via isGrantDebitAtFinalizeEnabled() in apps/web-next/lib/org-grant-debit.ts (single read point, edge-safe \u2014 read from two Edge routes and the cap gate). Dark by data for every org without run-grant rows: debitOrgGrants/creditOrgGrants match no active grant and issue zero UPDATEs, and personal subjects short-circuit before any query. TRANSITION PROTOCOL (why the flip cannot double-debit): the debit is derived from run_billing.grant_debited_units, so that ledger has to be accurate for HISTORY as well as for new rows. Two things make it so, and both are load-bearing: (a) the 20260730120000 migration BACKFILLS every org-stamped pre-migration row to its step_count \u2014 the amount the ingest boundary actually debited, since /api/analytics/ingest passed the same billedStepIncrement to debitOrgGrants that recordRunStep added to step_count \u2014 and without that backfill a chat session still alive at the flip would recompute alreadyDebited=0 and re-debit its whole history; (b) under the flag OFF the ingest path records what it debits into the same column. A run ingested pre-flip and finalized post-flip therefore settles only trueUnits - alreadyDebited, covered by the flag-flip and post-sweep no-op tests in apps/web-next/lib/org-grant-debit.db.test.ts. EXACTLY-ONCE: the recompute-and-claim is a single non-interactive Neon transaction guarded by a pg_advisory_xact_lock keyed on (org, user, unit_key), because the ledger IS the idempotence guard and a read-modify-write across HTTP round trips let N concurrent finalizes of one unit each claim the whole total (measured 5x on an explore fan-out, 2x on a double finalize; regression suite apps/web-next/lib/org-grant-debit.race.db.test.ts). Historical drift accrued BEFORE the flip is NOT self-correcting (those units never finalize again) and is returned separately by scripts/true-up-org-grant.ts, which is operator-run and never automatic.",graduation:{status:"gated",gate:"Graduate only when the shadow corpus shows the finalize-time recomputation agreeing with getOrgUsage. The corpus is a plain SQL query over run_billing \u2014 compare grant_shadow_units against the unit's summed grant_debited_units, grouped by termination_reason \u2014 NOT scraped logs. Graduation criteria: (a) for billable terminations the shadow total matches the unit's floored getOrgUsage total exactly; (b) every non-billable termination shows a NEGATIVE delta of exactly what ingest debited (the refund the current boundary cannot make); (c) no unit shows a positive delta unexplained by the floor. Then, in this ORDER: (1) run scripts/true-up-org-grant.ts --apply per grant-mode org to return the historical drift (lio: -615 units / -24.6 credits as of 2026-07-30); (2) only then flip to 1 in the web-next Vercel env. The two commute \u2014 the migration's backfill keeps the ledger accurate for history either way \u2014 but sweeping FIRST makes the flip a provable no-op on everything already settled: the sweep rewrites each unit's ledger to its true total, so that unit's next settlement computes a delta of exactly 0 and the flip cannot move a historical unit at all. Flip-first is a correct fallback if the sweep has to wait, not the default. The sweep is operator-run and never automatic.",evidence:"run_billing.grant_shadow_units vs summed grant_debited_units per unit (durable, queryable soak corpus) + the grant_debit:finalize_shadow finalize logs; the unit + live-DB suites in apps/web-next/lib/org-grant-debit.test.ts, org-grant-debit.db.test.ts (per-termination-class debit, chat/explore folding, double-finalize idempotence, AG-8220 duplicate pair, flag-flip no-double-debit, in-flight hold) and org-grant-debit.race.db.test.ts (exactly-once under a 5-way concurrent fan-out over 20 iterations, concurrent double refund, no-active-grant reconcile); scripts/true-up-org-grant.ts dry run per org",owner:"steering (Alex)",review:"2026-08-13"}},EXTENSION_PROFILE_PERSISTENCE:{key:"EXTENSION_PROFILE_PERSISTENCE",envVars:[],polarity:"1-enables",defaultState:"on",surfaces:["desktop-main","desktop-renderer"],cloudForwarded:!1,read:"compile-const",gates:"Saves/restores a per-project Chrome profile (cookies/storage) across sessions so login can be skipped on replay; a false value skips profile persistence.",designDoc:"apps/desktop-next/src/renderer/featureFlags.ts",status:"active",added:"2026-06-01",notes:"Hardcoded TypeScript boolean (`= true`), no env var \u2014 a compile-time gate flipped by editing the const. Defined twice: apps/desktop-next/src/renderer/featureFlags.ts and apps/desktop-next/src/main/computerUse/DesktopPlaywrightService.ts (the main-process copy is the one actually consumed)."},TRANSIENT_ENV_RETRY:{key:"TRANSIENT_ENV_RETRY",envVars:["AGENTIQA_TRANSIENT_ENV_RETRY","AGENTIQA_EXPERIMENT_TRANSIENT_ENV_RETRY"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:`Transient-environment retry-once policy (RunnerRuntime / test-plan runs only, v1 \u2014 never ExplorerRuntime/chat). Motivating incident: staging run run_9fff560a \u2014 the app's POST to its own API failed transiently (console: CORS block + AxiosError "Network Error"), the agent waited 2\xD730s, then filed report_issue and blocked after ONE attempt; later probing showed the API healthy. When on, a report_issue (or the ensuing exploration_blocked) whose failure is a DETERMINISTIC transient-environment stall \u2014 BOTH a stall/timeout symptom (a wait/wait_for_element in the evidence window, or explicit stall/network phrasing; never a content/assertion mismatch) AND an environment signature in the captured EventDigest (any failedRequests, a console/page error matching the network class \u2014 CORS / "Network Error" / AxiosError / net::ERR_ / "fetch failed" \u2014 or an explicit 5xx write) \u2014 is BOUNCED once with a structured instruction to repeat the triggering action and report only if it reproduces, tracked per step index. The second matching failure at the SAME step passes through and the runtime (not the model) stamps the issue evidence JSON with attempts:2, both attempt timestamps, both EventDigest snapshots, sets category='environment', and appends 'Reproduced on retry \u2014 2 attempts.' to the description. Bounds: exactly 1 retry per step, max 2 retried steps per run; a further environment stall after the budget is spent passes through with attempts:1 and the note 'Environment degraded \u2014 not retried (budget exhausted).'. A run-mode prompt nudge asks the agent to retry proactively; the deterministic gate is the backstop (never trust LLM compliance). Off \u21D2 every environment failure files immediately on the first attempt, byte-identical to pre-change behavior. Non-environment failures are never retried regardless of this flag.`,designDoc:"docs/plans/2026-07-23-transient-env-retry-once-design.md",status:"active",added:"2026-07-23",notes:"Ships default-ON from inception (not graduated from an off default) \u2014 the policy is a strict reduction of a confirmed false-block class (one transient blip \u2192 a blocked run + a filed non-defect issue), so there is no pre-change OFF behavior to preserve. Registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED (the tripwire acknowledging the default-ON state deliberately). Deterministic classifier + state machine in packages/engine-core/src/transientEnvRetry.ts; the kill-switch read + wiring live at packages/engine-core/src/RunnerRuntime.ts handleReportIssue / handleBlocked. Runner lane only."},VERIFY_STEP_BLOCK_DEFLECT:{key:"VERIFY_STEP_BLOCK_DEFLECT",envVars:["AGENTIQA_VERIFY_STEP_BLOCK_DEFLECT","AGENTIQA_EXPERIMENT_VERIFY_STEP_BLOCK_DEFLECT"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Verify-step block deflection (RunnerRuntime / test-plan runs only \u2014 never ExplorerRuntime/chat). Motivating incident: prod asess_1786420838044_uwx2kmxq (Lio CI, 2026-08-11) \u2014 the Guided Buying assistant answered \"I could not find any information about purchase request 18\", the agent correctly filed report_issue and then called exploration_blocked asking whether to proceed; nothing answers a question in a non-interactive CI run, so the run ended blocked_by_issue with NO run_complete: the verify step's criteria were never graded and the two later steps (unrelated to request 18) were synthesized skipped. When on, ONE exploration_blocked per run is bounced with an instruction to grade the step instead, but only when ALL of: (a) the blocked step \u2014 model-declared args.stepIndex first, runtime pointer as fallback \u2014 is a `verify` step carrying >= 1 authored criterion; (b) the obstacle text POSITIVELY describes the application's own observable response (replied/answered/showed/returned/\u2026); (c) NO environmental-obstacle signature is present (login/credential, captcha, mailbox, network/timeout/5xx, our capture pipeline, stuck/frozen/never-loaded, paywall/quota, run memory); (d) the recent-action evidence buffer holds at least one non-errored action with a page url, i.e. positive proof the agent could observe the app. A repeated block passes straight through. The gate NEVER writes a verdict \u2014 it cannot know whether the criteria are met, only that they are gradable \u2014 so it can manufacture neither a pass nor a fail. Off \u21D2 every exploration_blocked terminates on the first call, byte-identical to pre-change behavior.",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-08-11",notes:"Ships default-ON from inception (not graduated from an off default) \u2014 a strict reduction of a confirmed no-verdict class (a gradable verification silently converted into a terminal block), so there is no pre-change OFF behavior to preserve. Registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED. Deterministic predicate in packages/engine-core/src/runner/verifyStepBlockDeflection.ts; the kill-switch read + wiring live at packages/engine-core/src/RunnerRuntime.ts (the exploration_blocked arm of dispatchToolCall). Runner lane only. Claim runner.verify-step-block-gradable."},CAPTURE_STARVATION_FALLBACK:{key:"CAPTURE_STARVATION_FALLBACK",envVars:["AGENTIQA_CAPTURE_STARVATION_FALLBACK","AGENTIQA_EXPERIMENT_CAPTURE_STARVATION_FALLBACK"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Capture-starvation fallback (#2430): a PRECONDITION on the browser_unobservable terminal. On a page whose renderer main thread is hogged (the lio customer's duplicate_request_loading state), every page.screenshot starves past its full retry budget and three in a row terminate the run 'blocked / browser_unobservable' \u2014 the same 25-step plan ended that way on eight consecutive recorded nights (prod asess_1786679659847_bpczs2mx, asess_1786850942202_sm2dus65, asess_1786247013077_24q166cv, asess_1788232725045_0hlvaoio with 24/25 steps already passed). The page is ALIVE throughout: the CDP screencast that feeds the run video kept delivering frames (357 in the 08-14 incident) because they come from the compositor frame sink and never touch the renderer main thread. When ON, a capture whose Playwright budgets are BOTH spent tries, in order: (1) the newest recorder frame when it is at most 3000ms old, transcoded JPEG->PNG (jpeg-js + pngjs, already engine-core deps) so it is byte-shaped like a page.screenshot for every image/png consumer, and labelled with a soft note saying it came from the recorder and may be soft; (2) a raw CDP Page.captureScreenshot, reached only when NO fresh frame exists (a non-compositing page may have a quiescent-but-fine renderer), yielding a CURRENT picture with no staleness to discount \u2014 since 2026-09-09 that rung is the keepalive-shaped {format:'jpeg', quality:40} at 3000ms, transcoded to PNG and labelled 'current but re-compressed', with CAPTURE_FALLBACK_JPEG_RUNG=0 restoring the original full-fidelity png/fromSurface call at 2500ms (see that flag for the measurement); and (1b) since 2026-09-09 round 2, when NOTHING FRESH EXISTS BY ANY ROUTE \u2014 rung 1 found no frame inside its 3000ms window and rung 2 could not get the renderer to produce a current capture, which is the production signature \u2014 the LAST recorder frame however old, up to 120s, served as capture_degraded {payload:'stale', frameAgeMs} under its own kill switch CAPTURE_FALLBACK_STALE_FRAME and labelled 'STALE image \u2026 N s ago \u2026 do not claim any change that could have happened after this frame' (see that flag). TAB SAFETY: no rung ever serves a frame of a page that is not the active tab, but the two get there differently \u2014 rung 1 is REFUSED unless the recorder's CDP session is bound to the page being captured, while rung 2 always captures the ACTIVE page (it reuses the recorder's session only when that session is bound to it, and otherwise attaches its own), so for rung 2 a tab mismatch is resolved rather than refused. A rung that yields a frame is a SUCCESSFUL observation: it resets the capture streak instead of incrementing it, and emits capture_degraded {path, frameAgeMs, payload, consecutiveDegraded, bound, playwrightError} for the nightly census. RESCUES ARE BOUNDED: consecutive degraded captures are counted per session and capped at 8 (CAPTURE_FALLBACK_MAX_CONSECUTIVE_DEGRADED, roughly 2-3 minutes of a blind-but-painting page), reset by ANY healthy page.screenshot. Without that cap the terminal would be UNREACHABLE on a page whose compositor keeps painting while its main thread never returns, since every rescue resets the streak. Past the cap the ladder logs capture_degraded_exhausted {consecutive, bound}, declines, and the pre-existing miss accounting and streak-3 terminal proceed with classification 'degraded_exhausted' (distinct from page_busy / unobservable) so the census can tell an exhausted rescue from a page that never produced a frame. A rescue also makes the step GRADABLE IN BOTH DIRECTIONS (it can now book a real failed, not only a real passed), so the degraded provenance is carried past the turn: EnvState.degradedCapture {path, frameAgeMs, payload} rides the invoke boundary, is persisted on the tool message, and is rendered into the blind-double-read question and the criterion-regrade prompt so a grader re-reading the persisted frame is told it is a recorder frame of a stated age at reduced quality. When a fallback frame is used, the three page-touching reads that follow the screenshot in captureState (_snapshotForAI, DomObserver.flush, the canvas-dominance evaluate) are bounded at 3000ms each \u2014 unbounded they would burn Playwright's 30s default on a starved renderer, i.e. up to 90s per rescued capture \u2014 and since 2026-09-09 (round 3) they are also ISSUED TOGETHER on that path, so a rescued capture pays that bound ONCE instead of three times (measured 8.07s/5.99s sequential vs 3.00s/0.004s concurrent on the transient busy fixture). That changes no verdict content: on a starved renderer all three already degrade to fixed neutral values, and the HEALTHY path keeps its original sequence, unbounded, so DomObserver.flush still drains strictly after the snapshot that describes those mutations. WHAT A RESCUE COSTS (measured 2026-09-09, headless Chromium): 5s steady page.screenshot + 10s evidence retry + 3s rung 2 + ~3s tail, about 21-24s of engine time per rescued capture; the ~100s observed between two capture_degraded rows in a live run is that plus the model's own turn. ORDER IS EVIDENCE-LED, NOT FIDELITY-LED: measured 2026-09-01 on headless Chromium 1.58.2 against a chained-20s-busy-block page, Page.captureScreenshot TIMED OUT at 2.5s, 8s and 25s with fromSurface true AND false and in both png and jpeg, while the screencast delivered 955 frames with the newest 0-54ms old \u2014 so CDP is not main-thread-independent and trying it first would cost 2.5s on every starved capture to attempt a rung measured never to land in this class. OFF restores the pre-change behaviour exactly: an exhausted capture counts as a miss and the streak-3 browser_unobservable terminal fires as it does today. The terminal itself, its bound, its wording and its auto-filed issue are UNCHANGED on both settings; this flag only decides whether a frame that demonstrably exists is used before declaring the browser blind. Since 2026-09-09 (round 4) a refusal by ANY rung is also guaranteed to reach that terminal rather than the target-app-slowness one: a counted capture miss tags its rethrow and startRun resolves resolveUnobservableTerminal before isTargetAppSlownessError, so a miss that escapes at one of the few capture sites outside BrowserActionExecutor (the advisory re-reads in the issue/block handlers) still books the grade-preserving browser_unobservable terminal. Since 2026-09-09 (round 5) rung 1b additionally spends a LATCHED per-run budget of CAPTURE_FALLBACK_MAX_STALE_PER_RUN = 2 stale rescues that no later observation refunds. Round 5 measured that bounding rung 1b alone was not enough: rung 2 sat outside the budget and a raw-CDP capture still landed occasionally right after the cap refusal (elapsedMs 120-2937), resetting the capture-miss streak and buying the model the 50-100s it needed to quit via exploration_blocked first \u2014 a blocked_by_issue terminal that books no per-step grades, 2/3 runs. Since 2026-09-09 (round 6) spending that budget therefore LATCHES THE WHOLE LADDER: rungs 1b AND 2 are both closed for the rest of the run, announced once as capture_fallback_ladder_latched {reason:'stale_run_cap', used, cap} and then silent, so past the cap only a steady page.screenshot or a FRESH rung-1 recorder frame (at most 3000ms old \u2014 real current evidence, which is why rung 1 is deliberately NOT gated and the animated fixture is byte-identical) can produce an observation and every other attempt is a plain miss; the misses that follow then fire the honest terminal 20.0-46.8s after the cap (measured, 4 runs) with the pre-hog grades preserved \u2014 at streak 2 rather than 3 whenever round 4's tagged below-bound miss routes an escaped one straight there. Round 6 also keys the budget and the latch on the RUN rather than on the SessionState: invoke's browser_recovery:session_closed path deletes that object and recreates it under the same session id, which REFUNDED the budget (measured round 5 \u2014 a lane stream bound tore the session down at 420.0s and the bring-up at 438.4s handed the wedged run a second ladder). See CAPTURE_FALLBACK_STALE_FRAME for the measurement.",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-09-01",notes:"Ships default-ON from inception (not graduated from an off default) \u2014 it strictly reduces a confirmed false-block class (a run killed with no verdict while a real frame of the page was sitting in the recorder) and can only ADD an observation, never remove or downgrade one, so there is no pre-change OFF behaviour worth preserving as the default. Registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED. Read site: packages/engine-core/src/BasePlaywrightService.ts (captureStarvationFallbackFrame, called from captureState's evidence and wait/verify-oracle timeout arms). Pure module: packages/engine-core/src/captureStarvationFallback.ts (freshness predicate + JPEG->PNG transcode + the consecutive-degraded ceiling + the degraded-capture label + the measurement that sets the rung order). Unit suite: packages/engine-core/src/__tests__/BasePlaywrightService.captureStarvationFallback.test.ts."},CAPTURE_FALLBACK_JPEG_RUNG:{key:"CAPTURE_FALLBACK_JPEG_RUNG",envVars:["AGENTIQA_CAPTURE_FALLBACK_JPEG_RUNG","AGENTIQA_EXPERIMENT_CAPTURE_FALLBACK_JPEG_RUNG"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Rung-2 payload of the capture-starvation ladder (#2430, second incident wave). THE DEFECT: rung 1 (a fresh recorder frame) has NEVER fired in production \u2014 on the pilot's hogged loading state every one of the six recorded capture misses carried screencastAlive:false with recorder-frame ages of 22.5-60.3s, because that overlay is STATIC and the compositor stops the moment the hog starts. The animated self-testing fixture hid this: its transform-only spinner guarantees frames, so the unit and L3 layers were green on a rung prod has never reached. That leaves rung 2 as the only rescue, and rung 2 asked for the wrong thing: {format:'png', fromSurface:true, captureBeyondViewport:false} bounded at 2500ms timed out at exactly 2501ms 6/6 across two sessions, while the SCREENCAST KEEPALIVE's {format:'jpeg', quality:40} at 3000ms landed 14/14 on the same pages in the same seconds \u2014 one of them 194ms AFTER a rung-2 miss on the identical page, with zero screencast_keepalive_failed. WHAT =0 DISABLES: the payload swap only. ON, rung 2 sends the keepalive's exact call at the keepalive's 3000ms bound (CAPTURE_FALLBACK_CDP_TIMEOUT_MS, which also bounds attaching the rung's own CDP session) and transcodes the JPEG to PNG through the same screencastFrameToPng rung 1 uses, so every image/png consumer downstream is unchanged; the degraded marker carries payload:'jpeg' and the grader-facing label says the frame is CURRENT but re-compressed rather than 'full fidelity'. OFF restores the png/fromSurface call at CAPTURE_FALLBACK_CDP_LEGACY_TIMEOUT_MS=2500 with the frame used raw and no payload on the marker \u2014 byte-identical to the pre-change rung, including its full-fidelity label wording. UNCONDITIONAL COMPANION, not gated by this flag: rung 1 now emits capture_fallback_frame_absent {reason:'no_frame'|'tab_mismatch', frameAgeMs} when it cannot be attempted at all. All three of rung 1's existing markers sit inside its guard, so the two ways of never entering it were indistinguishable in the nightly census \u2014 and every production miss so far has been one of them. It is diagnostic only and changes no verdict. NOT CHANGED BY EITHER SETTING: the rung ORDER, the 3000ms recorder-frame freshness bound, the consecutive-degraded ceiling, the streak accounting, and the browser_unobservable terminal, its bound, its wording and its auto-filed issue. LOCAL EVIDENCE IS NULL, DELIBERATELY STATED: probed 2026-09-09 against both busy-finale fixtures (headless Chromium, screencast running, payloads alternated on one CDP session, 8 rounds each), png@2500 scored 0/8 and jpeg-q40@3000 scored 0/8 on BOTH, and on the static fixture jpeg also missed at 8000ms and 25000ms \u2014 the fixtures' hog is one 24s synchronous block that yields a single macrotask turn, so nothing is capturable there and the fixture cannot discriminate the payloads. The production 14/14-vs-0/6 counter-example is the only evidence separating them, which is exactly why this rung ships behind its own switch. A SECOND, INDEPENDENT REASON THE RUNG EARNS ITS 3000ms, measured 2026-09-09 (round 3) on the transient busy fixture: its ABANDONED Page.captureScreenshot provokes a screencast frame at SEND time even though the capture itself never lands. With the rung, recorder-frame arrivals during a 48s hog were t=0.00/0.07/16.04/39.12s; with it skipped, only t=0.00/0.07s. So the rung leaves rung 1b a ~3s frame to serve instead of a ~39s one, and skipping it to save 3s per rescue would age the served evidence by an order of magnitude. Acceptance suite: packages/engine-core/src/__tests__/BasePlaywrightService.captureStarvationFallback.test.ts.",designDoc:"packages/engine-core/src/captureStarvationFallback.ts",status:"active",added:"2026-09-09",notes:"Ships default-ON as an ordinary 0-disables kill switch (not a graduation, and therefore NOT in killSwitchDefaultState.test.ts's INTENTIONALLY_GRADUATED allowlist, which covers 1-enables flags left on) \u2014 it can only change WHICH payload an already-failing capture asks for, never whether a capture is attempted or how a miss is accounted, so there is no pre-change behaviour worth preserving as the default. Read site: packages/engine-core/src/BasePlaywrightService.ts (captureStarvationFallbackFrame, rung 2). Pure module: packages/engine-core/src/captureStarvationFallback.ts (buildCaptureFallbackCdpRequest + describeAbsentScreencastRung). Claim runner.browser-unobservable-terminal; L3 fixtures runner/hang-busy-finale-static and runner/hang-busy-finale-transient."},CAPTURE_FALLBACK_STALE_FRAME:{key:"CAPTURE_FALLBACK_STALE_FRAME",envVars:["AGENTIQA_CAPTURE_FALLBACK_STALE_FRAME","AGENTIQA_EXPERIMENT_CAPTURE_FALLBACK_STALE_FRAME"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Rung 1b of the capture-starvation ladder (#2430, round 2): serve the LAST recorder frame, labelled with its age, when no fresh picture can be obtained by any route. THE DEFECT THIS CLOSES: the jpeg hypothesis behind CAPTURE_FALLBACK_JPEG_RUNG was refuted locally \u2014 on the static busy-finale fixture (and the animated one) jpeg-q40@3000ms misses exactly as often as png@2500ms (0/8 each), and jpeg also misses at 8000ms and 25000ms \u2014 and the static fixture reproduces the production class end to end (the run ends blocked / browser_unobservable). The honest reading is that on a hogged renderer main thread NO FRESH CAPTURE EXISTS BY ANY ROUTE: rung 1 wants a frame <=3000ms old and the compositor went quiet 22.5-60.3s ago, rung 2 wants a current capture and the renderer cannot service one. The only picture of the page that exists is the last frame the compositor emitted, and the ladder was throwing it away \u2014 so a run died with no verdict on a step it could have graded. WHEN ON (default), after rung 1 found nothing FRESH and rung 2 failed, the ladder accepts the NEWEST recorder frame if its age is within CAPTURE_FALLBACK_STALE_FRAME_MAX_AGE_MS = 120000 (chosen against the measured production ages 22.5/23.4/23.7/36.3/42.8/60.3s, with headroom), transcodes it JPEG->PNG through the same screencastFrameToPng rung 1 uses, and marks it capture_degraded {path:'screencast', payload:'stale', frameAgeMs} \u2014 a SUCCESSFUL observation that resets the capture-miss streak. The frame is labelled everywhere it can be read: buildDegradedCaptureLabel produces 'STALE image: the last frame the browser produced, N s ago. The page has stopped painting since (busy / blocked main thread). Treat everything as possibly outdated; do not claim any change that could have happened after this frame; say so rather than guessing.', which is the turn-time EnvState.metadata note AND (via EnvState.degradedCapture -> the persisted tool message -> EvidenceMessage.degradedCapture) the line the blind-double-read question and the criterion-regrade prompt carry. TAB SAFETY IS NOT RELAXED: a frame whose recorder session is bound to another page is still REFUSED (staleness never makes a wrong page acceptable). BOUNDED THREE TIMES, AND THE BOUND THAT HOLDS IS THE RUN CAP. A stale rescue is a capture_degraded, so the shared consecutive-degraded ceiling of 8 (CAPTURE_FALLBACK_MAX_CONSECUTIVE_DEGRADED) still applies; rung 1b ALSO carries its own CAPTURE_FALLBACK_MAX_CONSECUTIVE_STALE = 2, counted on its own session counter and reset by ANY non-stale observation (a healthy page.screenshot, a fresh rung-1 frame, or a CDP rescue); and since 2026-09-09 (round 5) it carries CAPTURE_FALLBACK_MAX_STALE_PER_RUN = 2, a LATCHED per-run budget that NOTHING refunds. WHY: measured on runner/hang-busy-finale-static (2026-09-09, 3/3 runs), under the shared ceiling alone the rung kept a PERMANENTLY wedged page alive for 352-476s and never reached 8, because the model gave up first and called exploration_blocked \u2014 a blocked_by_issue terminal that books NO per-step grades (only run_complete and the browser_unobservable concluding turn do). The run then recorded [1:skipped 2:skipped 3:skipped 4:warning] where the un-rescued run had recorded [1:passed 2:passed 3:passed 4:skipped]: the rescue had converted a grade-PRESERVING terminal into a grade-DESTROYING one, which is the exact Bug B this ladder exists to prevent. ATTRIBUTED HONESTLY: the same run shape was then measured on the ANIMATED fixture with rung 1b irrelevant \u2014 rung 1 (fresh frames, ages 1-34ms) rescued 4 captures, the model exited via exploration_blocked at 386.1s and the run recorded the same all-skipped rows. So grade loss on a permanently wedged page is a property of ANY successful rescue, not of the stale rung; the real fix is preserving grades on the agent-block terminal, which is tracked separately. This ceiling bounds the NEW exposure rather than pretending to close the gap. Two stale frames in a row are already the strongest available signal that this is not a busy INTERVAL but a page that is gone, so the third capture misses and the honest browser_unobservable terminal fires with the earlier grades kept. The production class this rung is for is the opposite shape \u2014 a loading overlay whose hog is seconds, well inside two captures. WHY THE CONSECUTIVE CEILING WAS NOT ENOUGH, AND WHAT THE RUN CAP DOES (round 5, measured 2026-09-09 across rounds 2 and 4, 4 runs on runner/hang-busy-finale-static): a consecutive counter is reopened by ANY non-stale observation, and on a hogged page the raw-CDP rung lands every so often \u2014 one run's ceiling refusal was followed immediately by capture_degraded {path:'cdp', payload:'jpeg', elapsedMs:2889}, and another spent FOUR stale rescues (consecutiveStale 1,2 \u2026 1,2) over 430.6s without ever booking a capture miss. CAPTURE_FALLBACK_MAX_STALE_PER_RUN latches instead: two stale rescues per run, ever, and neither a fresh rung-1 frame nor a CDP rescue refunds one. The consecutive ceiling is kept as the inner, page-scoped bound (it is what the census and the L3 judge read, and what would still bound an unbroken burst if the run budget were widened), but at the shipped values 2 and 2 the run cap subsumes it and is what is REPORTED \u2014 used >= consecutiveStale always. MEASURED 3/3 on runner/hang-busy-finale-static (2026-09-09): exactly two stale rescues per run, then capture_fallback_stale_frame_refused {refusal:'stale_run_cap', used:2, cap:2} at 246.8/242.1/217.3s, zero cap breaches, zero startRun:target_app_slowness_blocked. IT WAS NOT SUFFICIENT ON ITS OWN, AND SPENDING IT NOW LATCHES THE WHOLE LADDER (round 6): rung 2 (raw CDP) used to sit outside this cap \u2014 bounded only by the shared degraded ceiling of 8 \u2014 and on that fixture it still landed occasionally right after the cap refusal (capture_degraded {path:'cdp', payload:'jpeg', elapsedMs:2937/934/120+1862}), which reset the capture-miss streak and bought the model the ~50-100s it needed to reach exploration_blocked first (295.2s and 361.5s). Since 2026-09-09 round 6, the capture on which this rung would be refused for stale_run_cap does not even ASK rung 2: the ladder emits capture_fallback_ladder_latched {reason:'stale_run_cap', used, cap} once and then declines every capture that a steady page.screenshot and a FRESH rung-1 frame both failed to answer. Rung 1 stays open by design (a frame inside the 3000ms bound is current evidence, not a rescue), which is why the ANIMATED fixture is byte-identical. The budget and the latch are keyed on the RUN, not on the SessionState, because invoke's browser_recovery:session_closed path recreates that object under the same session id and refunded the budget (measured round 5: session torn down 420.0s, session_bringup 438.4s, ladder reopened). MEASURED round 6 on runner/hang-busy-finale-static, 5 runs: zero post-cap CDP rescues 5/5, the latch announced exactly once in each of the 4 runs that reached the cap, and the honest terminal 20.0s / 46.8s after the latch with [1:passed 2:passed 3:passed 4:skipped] preserved; 4/5 judged pass (round 5: 0/3). The remaining failure is the model's own give-up clock, not the ladder: a PRE-cap rung-2 landing at 187.3s delayed the second rescue to 234.9s and exploration_blocked arrived at 261.8s before the next capture. Effect on the production class: none, it needs one rescue (transient fixture round 5: PASS, all four steps, 88.9s, one stale rescue). THE COST, STATED: a long plan that meets the overlay a third time falls through to the honest browser_unobservable terminal \u2014 which is the pre-#2430 behaviour with grades kept \u2014 so the cap bounds this ladder's BENEFIT on such a run and can never make one worse than the baseline it replaced. browser_unobservable:capture_missed therefore fires when there is NO recorder frame within 120s at all, when the tab-safety refusal applies, once two consecutive stale rescues are spent, or once the run's two-rescue budget is spent. Every refusal is attributable in the census: capture_fallback_stale_frame_refused {refusal:'disabled'|'stale_run_cap'|'stale_ceiling'|'no_frame'|'tab_mismatch'|'outside_window', frameAgeMs, consecutiveStale, staleBound, used, cap} \u2014 used/cap are reported on EVERY refusal and on every stale capture_degraded, not only when the cap is the refusal. WHEN OFF (=0), the ladder is byte-identical to round 1: rung 1 (fresh) -> rung 2 (CDP) -> miss, no rung 1b, no 'stale' marker, no label. NOT CHANGED BY EITHER SETTING: rung 1's 3000ms freshness bound, the rung order, rung 2's payload/bound (that is CAPTURE_FALLBACK_JPEG_RUNG's business), the degraded ceiling, the streak accounting, and the browser_unobservable terminal's bound, wording and auto-filed issue. VERDICT-WIDENING, DELIBERATELY: a stale frame is real evidence of a real PAST state, and a grader that ignores the label can read it as the present \u2014 a new false-passed/false-failed path that did not exist before. It is mitigated by labelling and by blind-double-read, never by suppression, and it is why this rung is switchable independently of the rest of the ladder. Acceptance suite: packages/engine-core/src/__tests__/BasePlaywrightService.captureStarvationFallback.test.ts. L3: TWO cases, because a permanently wedged page can only certify the honest terminal. runner/hang-busy-finale-static gates the CONTRACT (an unobservable terminal that keeps the pre-hog grades, or a rescue \u2014 never a false pass, never destroyed evidence) and reports which rungs fired. runner/hang-busy-finale-transient (added 2026-09-09, round 3) GATES THE RESCUE: the same flow and the same quiet compositor, but the hog ENDS after ~48s, which is the production shape \u2014 the pilot's runs survived 2-3 capture failures and went on to grade later steps. On a page that comes back the two behaviours separate cleanly: without this rung the three starved captures trip the streak-3 terminal BEFORE the page returns and the last step is never graded, with it the run stays observable across the hog and grades that step off a FRESH capture. The case therefore requires at least one capture_degraded {payload:'stale'}, the ABSENCE of browser_unobservable:capture_missed {terminal:true}, and a real 'passed' grade on the busy step. ROUND 4 (2026-09-09) \u2014 WHERE A REFUSAL LANDS. Every refusal of this rung, the stale ceiling and refusal:'disabled' included, is a capture MISS: it counts on the capture_missed streak and the run continues to the streak-3 browser_unobservable terminal, which is the grade-PRESERVING one. It must never land on the target-app-slowness arm (blocked_by_issue / slow_site), which tells the customer their app was too slow and synthesizes skipped rows over grades the run already earned. Round 3 measured it doing exactly that on runner/hang-busy-finale-static: the ladder was correct throughout (consecutiveStale 1 -> 2 -> refusal:'stale_ceiling' -> capture_missed {streak:1}) but the rethrown raw Playwright TimeoutError escaped the run loop and isTargetAppSlownessError answered TRUE for it, so the case red twice (211.8s all rows skipped; 420.1s stream-bound error) where the same page with this flag OFF ends browser_unobservable keeping [1:passed 2:passed 3:passed 4:skipped]. The cause was neither the rung nor the streak: almost every capture sits behind BrowserActionExecutor, which turns the rethrow into an error tool result and lets the streak keep counting, but the advisory re-reads inside the issue/block handlers call computerUseService.invoke directly, so a below-bound miss there escapes the loop \u2014 and which site the model's next tool call lands on is a coin flip. Round 4 tags a counted miss (markCaptureObservabilityMiss: url, streak, limit, screencastAlive) and startRun resolves resolveUnobservableTerminal \u2014 the streak-bound error OR a tagged below-bound miss \u2014 BEFORE the slowness arm, so an escaped miss books the honest terminal with its concluding turn. Nothing else moves: only that one seam sets the tag, so the cold-start SlowSiteError and every screenshot timeout that never reached the miss accounting keep the terminal they have today. Confirmed on that fixture: 2/2 round-4 runs on the shipped default logged ZERO startRun:target_app_slowness_blocked (round 3: 1 of 2). ROUND 5 (2026-09-09) \u2014 THE PIN IS GONE. Round 4 left one blocker: a rescue costs ~70-100s, so on a page that never comes back the MODEL reached exploration_blocked \u2014 a blocked_by_issue terminal that books no per-step grades \u2014 long before the capture streak could reach 3 (331.9s and 430.6s, both [1:skipped 2:skipped 3:skipped 4:warning]), where the un-rescued run reaches the honest terminal in 60-90s. CAPTURE_FALLBACK_MAX_STALE_PER_RUN makes the STALE budget unrefundable, which removes this rung as the cause; runner/hang-busy-finale-static runs the SHIPPED DEFAULT (no env pin, so the measurement stays visible instead of hidden behind it) and gates the run cap directly: at most CAPTURE_FALLBACK_MAX_STALE_PER_RUN stale rescues, and if the budget was spent and a terminal capture miss followed, a capture_fallback_stale_frame_refused {refusal:'stale_run_cap'} must be in the census. Round 5 measured that case still RED 3/3 for a reason the cap does not cover \u2014 rung 2 (raw CDP) sat outside the budget and still landed after the refusal (elapsedMs 120-2937), resetting the miss streak \u2014 and the pin was deliberately not restored, because re-pinning would have hidden the residual. Round 6 closes it: spending this rung's run budget now LATCHES THE WHOLE LADDER (see CAPTURE_STARVATION_FALLBACK), so rung 2 is closed too and the case's judge additionally reds on any rescue booked after the cap and on a latch that was not announced exactly once. The budget and the latch are keyed on the RUN, so a mid-run bring-up cannot refund either. Grade preservation across the agent-block terminal (#3072) remains a real gap, but it is no longer this ladder's problem to route around \u2014 the latch keeps the run away from that terminal. What round 4 added to that case still holds: its judge reds on the slowness terminal and on a stale streak past the consecutive ceiling, and the routing itself is pinned deterministically by unit tests rather than by a stochastic L3.",designDoc:"packages/engine-core/src/captureStarvationFallback.ts",status:"active",added:"2026-09-09",notes:"Ships default-ON as an ordinary 0-disables kill switch (not a graduation, and therefore NOT in killSwitchDefaultState.test.ts's INTENTIONALLY_GRADUATED allowlist, which covers 1-enables flags left on). Read site: packages/engine-core/src/BasePlaywrightService.ts (captureStarvationFallbackFrame, rung 1b, after rung 2 returns nothing). Pure module: packages/engine-core/src/captureStarvationFallback.ts (evaluateStaleFrameRung + the payload:'stale' branch of buildDegradedCaptureLabel). Claim runner.browser-unobservable-terminal. Rollback is one env var and restores the round-1 ladder exactly. TWO L3 cases, deliberately split by fixture shape. runner/hang-busy-finale-static does NOT gate the rescue: its hog is PERMANENT by construction, so the correct outcome there is the honest terminal on both settings and a case demanding a rescue would manufacture a red \u2014 it gates the contract and REPORTS which rungs fired. runner/hang-busy-finale-transient (round 3) DOES gate it, because its hog ends and the page comes back, which is the production shape and the only one on which a rescue is a strict improvement."},CAPTURE_FALLBACK_TAB_REBIND:{key:"CAPTURE_FALLBACK_TAB_REBIND",envVars:["AGENTIQA_CAPTURE_FALLBACK_TAB_REBIND","AGENTIQA_EXPERIMENT_CAPTURE_FALLBACK_TAB_REBIND"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Re-bind the screencast RECORDER to the page the executor is driving when the capture ladder reports a tab mismatch (#3104). SHADOW-FIRST: with no env (and with an explicit =0, which is indistinguishable and pinned to be) the full predicate runs and emits capture_fallback:would_rebind, and NOTHING attaches, detaches or moves; =1 performs the rebind. THE DEFECT. #3089 gave rung 1 an attribution row for a NON-attempt, and the first production census it produced named a reason nobody had budgeted for: capture_fallback_frame_absent {reason:'tab_mismatch'} \u2014 a decodable, FRESH frame existed and tab safety withheld it \u2014 followed by capture_fallback_stale_frame_refused {refusal:'tab_mismatch', used:0, cap:2} and the browser_unobservable terminal. used:0 is the signature: the run reached the terminal without spending a single unit of the rescue budget it had (staging asess_1788993885491_don3yg17 / run_952d0f20 and prod roawoucv, both 2026-09-09, step 25 of the pilot's 25-step draft-completion plan; reproduced 4+ times across 09-08/09-09). WHY IT IS PERMANENT, NOT TRANSIENT. startScreencast picked its page ONCE as `tab1 ?? page`, and restartScreencast \u2014 the only re-binding path that existed \u2014 re-read the same expression, so every routine restart (screencast_frame_ack_failed, the keepalive watchdog) re-bound to tab1 too. session.page meanwhile moves at eight seams (a popup becoming the active surface, that popup closing, containment restoring the app tab, extension/web navigate routing, switch_tab either way, close_tab) and none of them touched the recorder. So the moment a window.open makes the executor's page tab2, rungs 1 and 1b are STRUCTURALLY DEAD for the rest of the run and only rung 2 (which attaches its own session against session.page) can rescue \u2014 and on a hogged active tab rung 2 does not land, so every rung refuses. THE RULE: a screencast frame is evidence of the page its CDP session was attached to WHEN THE FRAME ARRIVED, and the recorder's attachment must FOLLOW the executor \u2014 so a tab_mismatch is repaired by RE-BINDING the recorder, NEVER by relaxing which page a frame may be served as. TAB SAFETY IS NOT RELAXED ANYWHERE: the second half of that rule is what makes the first half safe. Frames now carry ownerPage, stamped at remember time, and BOTH rung predicates read the frame's owner rather than the recorder's current binding \u2014 so a frame remembered while the recorder was on tab1 is still refused for tab2 after the rebind. Without that stamp the rebind would open a false-evidence hole: the same pixels, re-labelled as a different page. That attribution change is UNFLAGGED and behaviour-identical today (on a build where the recorder never moves, the owner and the current binding are the same value on every reachable state, including the permissive undefined branch), and it is pinned by a parity test. DURABILITY. The binding now has ONE source of truth, session.screencastTargetPage, which startScreencast and therefore every restartScreencast read. Without it a rebind would be a one-shot that the next ack failure or keepalive restart silently revoked, and the run cap would be spent re-fixing the same mismatch. Undefined until the first attach and reverted to a LIVE page when the target closes, so a session that never re-binds resolves `tab1 ?? page` exactly as before. WHERE IT DECIDES. Two sites, one pure predicate (evaluateTabRebind): at:'invoke', the EAGER repair at the invoke boundary \u2014 a swap that persisted to the next action is fixed OFF the starved path, so the ladder normally finds the recorder already correct and pays nothing; and at:'ladder', between the ladder latch and rung 2, on a capture that has already burned 5s + 10s of Playwright budget. REFUSALS, in order, each with a reason in the census: no_mismatch (first, so no row can ever describe a capture whose recorder already owned the active page \u2014 including every desktop lane, where the screencast never starts and the owner is undefined); extension_session (the whole mechanism is scoped OUT of the wallet lane, where tab1 IS the extension page and the executor deliberately drives the dapp \u2014 a mismatch there is the lane's design, so following it would re-point the run video at chrome-extension:// pages and refusing it per invoke would storm the census; ONE capture_fallback:rebind_declined row per RUN instead); latched (the run has spent its stale budget, so rungs 1b and 2 are closed and a rebind would buy no evidence at all); rebind_run_cap (CAPTURE_FALLBACK_MAX_TAB_REBINDS_PER_RUN = 2, latched and never refunded); then the TARGET guards \u2014 extension_page, pending_escape (a cross-environment popup the invoke tail is about to CLOSE), blank_target (about:blank / data: / an unstatable URL \u2014 the window.open('') + location=\u2026 pattern is about:blank for a beat, and a recorder re-pointed at it makes the run video blank AND lets the ladder serve blank frames as ownerTab === activeTab evidence); restart_in_flight (the keepalive watchdog's own restartScreencast holds the same re-entrancy guard); and shadow LAST, which is what makes the census exact \u2014 reason:'shadow' means every other condition said yes and only the flag said no, so a would_rebind row is one-for-one with a rebind the enforcing build would have performed. THE MARKER IS AN EDGE, not a level: one row per mismatch TRANSITION (identity-compared page pair), re-armed the moment the recorder owns the active page again. In shadow nothing repairs the mismatch, so a level-triggered row would fire on EVERY invoke for the rest of a multi-tab run \u2014 the pilot's 25-step plan is exactly that shape. THE CAP IS COUNTED IN BOTH STATES: in shadow it is a VIRTUAL counter over the rebinds the enforcing build would have performed, so the shadow row count predicts the enforced cost and the gate's 'at most two per run' is testable BEFORE the flip. RUN-KEYED, like the stale budget: it lives on captureLadderRunBudget(sessionId), which invoke's browser_recovery:session_closed bring-up cannot refund and which cleanupOtherSessions' peer sweep is forbidden to drop (#3089 round 7). WHAT ELSE MOVES WHEN IT IS ON. (1) Rung 2's transport is PINNED to its own private CDP session for the rest of a run that has re-bound: getCaptureFallbackCdpSession prefers the recorder's session when cdpSessionPage === page, which on the tab-mismatch path was never true before and becomes true after a rebind \u2014 that would move rung 2's Page.captureScreenshot onto the session the keepalive is also sending on, against a hogged renderer, and #3089 round 1 measured that the session/payload choice on this exact surface decides whether the rung lands. (2) The run video and the live view follow the agent between tabs (both ride screencastFrameCallbacks); screencastStartTime is preserved so frame timestamps stay monotonic, and the 2-per-run cap plus the invoke-boundary placement bound the strobing a popup-heavy run could otherwise produce. (3) noteCaptureObservability reads session.lastScreencastFrameAt, which after a rebind is stamped by the NEW tab \u2014 so screencastAlive becomes truthful for the first time on a mismatched run and classification can flip from page_busy to unobservable on the same physical failure. That is a census-string change the L3 judge reads, and it is EXPECTED rather than incidental: today the row claims page_busy on the strength of frames of a page nobody is driving. CENSUS, unconditional and on every setting: capture_fallback_frame_absent, capture_fallback_stale_frame_refused and every capture_degraded now carry {recorderTab, ownerTab, activeTab, recorderUrl, activeUrl} \u2014 the issue's question verbatim, which tab the executor targets versus which tab the recorder streams. WHEN OFF (unset or =0) the ladder is byte-identical to #3089's: same rung order, same refusals, same used/cap accounting, same latch, same terminal. NOT CHANGED BY EITHER SETTING: rung 1's 3000ms freshness bound, the stale rung's 120s window and its two budgets, the whole-ladder latch, the consecutive-degraded ceiling, the capture-miss streak, and the browser_unobservable terminal's bound, wording and auto-filed issue. VERDICT-WIDENING, DELIBERATELY: a capture that used to be withheld on tab_mismatch can now produce a gradable frame \u2014 the same widening rungs 1/1b already carry, recorded on the browser_unobservable two-sided entry in docs/VERDICT-GATES.md, and with the tab-safety invariant explicitly NOT relaxed. Acceptance suite: packages/engine-core/src/__tests__/BasePlaywrightService.captureStarvationFallback.test.ts. L3: runner/tab-switch-busy-finale (a same-origin window.open + focus switch before a transient busy finale), pinned =1 in its case env \u2014 enforcement before graduation exists ONLY there, and nothing in the nightly default lane runs the rebind.",designDoc:"packages/engine-core/src/captureStarvationFallback.ts",status:"active",added:"2026-09-10",graduation:{status:"gated",gate:"Flip defaultState to 'on' when ALL SIX hold across >=5 consecutive nightly qa-exhaustive runner lanes AND >=2 ci:qa-targeted dispatches of the pilot's 25-step draft-completion plan on staging. (1) NO SPURIOUS FIRE: every capture_fallback:would_rebind row is preceded, in the same capture, by capture_fallback_frame_absent {reason:'tab_mismatch'} (ladder site) or by a session.page reassignment since the previous invoke (invoke site); ZERO rows on a decision whose recorder already owned the active page. (2) NO SINGLE-TAB FIRE: zero would_rebind rows on any run whose activeTab is 'tab1' throughout \u2014 a row there means the attribution predicate is wrong, not the binding. (3) TARGET PARITY: of the rows where a FOLLOWING tool_call recorded a post-state URL, 100% name the page that call acts on, denominator published (the gate cannot be stated over all rows \u2014 on a wedged renderer the next tool call may never produce a post-state URL). (4) FORCED-ON EVIDENCE on runner/tab-switch-busy-finale over >=8 runs: a landed capture_fallback:rebind {ok:true} on EVERY run, ZERO tab_mismatch rows of either kind, ZERO capture_degraded whose ownerTab !== activeTab, AND the browser_unobservable:capture_missed.classification distribution published beside the =0 control \u2014 the flip is a CUSTOMER-FACING string change (see below) and must be a number before the graduation, not a footnote. (5) SHADOW-DEFAULT TRACE PARITY: runner/hang-busy-finale-static, runner/hang-busy-finale-transient and runner-blocked-synthesis book ZERO would_rebind rows and a ladder census identical to the pre-change one. (6) COST: the would_rebind count per run stays at or below CAPTURE_FALLBACK_MAX_TAB_REBINDS_PER_RUN \u2014 which is exactly what the virtual counter makes measurable in shadow. CLIENT-VISIBLE WORDING, MEASURED (#3121 review MED-2; unit-pinned by 'MEASURES the page_busy -> unobservable classification flip under =1'). noteCaptureObservability derives screencastAlive from session.lastScreencastFrameAt, which only the RECORDER's own CDP session stamps. On a mismatched multi-tab run that is the HEALTHY launcher tab, so TODAY the row claims page_busy / screencastAlive:true off frames of a page nobody is driving; under =1 the recorder follows the executor onto the hogged tab, the stamps stop, and the SAME physical failure classifies unobservable / screencastAlive:false. That boolean is threaded into BrowserUnobservableError and decides TWO customer-facing strings in RunnerRuntime's unobservable catch, and in BOTH the =1 run LOSES a clause: (a) the blocked verdict's detail, embedded verbatim by buildBlockedSummary into run.summary \u2014 '... after N consecutive screenshot timeouts while screencast frames kept arriving - the page was alive but busy (possible page hang or main-thread busy loop).' becomes '... after N consecutive screenshot timeouts.'; (b) the auto-filed environment issue's description, which loses 'Screencast frames kept arriving while captures starved, so the page was alive but busy - a possible page hang or main-thread busy loop (e.g. a synchronous loading state).' The direction is TRUTHFUL \u2014 the busy claim was made off another tab's frames \u2014 but it is a wording change on a record the customer reads, which is why gate (4) requires the distribution rather than this note alone. It is =1-ONLY: the shipped shadow default moves no recorder and therefore changes neither string. RETIRE INSTEAD if the census shows the mechanism fires on runs whose recorder already owned the active page, or if the run video's tab-following turns out to be unacceptable to customers: the attribution half (frame ownerPage + the census fields) is unflagged and stays either way.",evidence:"capture_fallback:would_rebind / :rebind / :rebind_declined rows plus the {recorderTab, ownerTab, activeTab, recorderUrl, activeUrl} census on capture_fallback_frame_absent, capture_fallback_stale_frame_refused and capture_degraded, in the nightly qa-exhaustive runner lane and in ci:qa-targeted staging dispatches; the forced-ON leg is the runner/tab-switch-busy-finale eval case. Baseline measured 2026-09-10 (local, 3 runs each leg): pinned =1 3/3 pass with zero tab_mismatch rows and one rebind per run at the invoke boundary in 15-25 ms; shadow default 0/3 with capture_fallback_frame_absent {reason:'tab_mismatch'} on every run.",owner:"steering (Alex)",review:"2026-10-01"},notes:"Ships SHADOW (1-enables, defaultState off), so it is NOT in killSwitchDefaultState.test.ts's INTENTIONALLY_GRADUATED allowlist \u2014 that list covers 1-enables flags left ON. Read site: packages/engine-core/src/BasePlaywrightService.ts (maybeRebindScreencastToActivePage, called from invokeAction's boundary and from captureStarvationFallbackFrame between the latch and rung 2). Pure module: packages/engine-core/src/captureStarvationFallback.ts (evaluateTabRebind, describeTabBinding, classifyRebindTarget/isBlankRebindTarget, CAPTURE_FALLBACK_MAX_TAB_REBINDS_PER_RUN, CAPTURE_FALLBACK_REBIND_TIMEOUT_MS, CAPTURE_FALLBACK_TARGET_PROBE_TIMEOUT_MS). Claim runner.browser-unobservable-terminal. ROUND 3 (2026-09-11), wave-4 preflight, prod asess_1789096730801_ibhs2ezp (engine d4e21f2d1, the pilot's 25-step draft-completion plan): the ONLY rebind decision the whole run took was capture_fallback:rebind_declined {at:'invoke', reason:'blank_target', used:0, cap:2, ownerTab:'tab1', activeTab:'tab2', activeUrl:':'}, then frame_absent {tab_mismatch} -> stale_frame_refused {tab_mismatch, used:0} -> capture_missed 1/2/3 -> browser_unobservable, with ZERO would_rebind rows. blank_target sat at guard 7 of 9 and the enabled switch is guard 9, so =1 was a guaranteed NO-OP on precisely the tab_mismatch shape this flag exists for, and the shadow census measured that population (the app's action_loading_message=duplicate_request_loading transition, whose page.url() degenerates to ':') at zero. FIXED by deciding blank_target on the target's DOCUMENT rather than on the text of its URL (classifyRebindTarget): a known-blank URL (about:blank and its query/fragment forms, bare about:, data:, empty) is still refused synchronously with no read; a statable URL is still eligible with no read; an UNSTATABLE url (':', about:srcdoc, anything new URL() rejects) buys a CAPTURE_FALLBACK_TARGET_PROBE_TIMEOUT_MS=500ms page.evaluate of {readyState, bodyChildCount, title}, only when every guard before blank_target already said yes, AND AT MOST ONCE PER MISMATCH EDGE (memoised on captureLadderRunBudget.lastTabMismatch.probe, keyed on the url the read was taken for) \u2014 the third gate being what keeps the shadow default's 'changes NOTHING' claim true, since issued before the edge dedup the read was re-bought on every invoke of a standing mismatch for the life of the run while booking one row (#3148 review MED-1, measured: 25 invoke decisions => 25 reads / 12539 ms / 1 would_rebind; the run cap cannot bound it because the cap is only spent when a row IS booked). Memoised: a standing mismatch costs ONE read per run, a navigating target re-arms exactly one, a latched/capped/extension/pending-escape decision pays nothing, a single-tab run reaches no classifier at all \u2014 content => eligible (document_live), a settled contentless document => still refused (document_empty), NO ANSWER => ELIGIBLE (document_unreadable), because a read that cannot come back is a busy renderer while a blank page's renderer is idle and answers at once. Real-Chromium measurement (packages/engine-core/__tests__/BasePlaywrightService.rebindTargetProbe.integration.test.ts): a genuine window.open('') popup reports about:blank with {readyState:'complete', bodyChildCount:0, title:''} and is refused by BOTH routes; a hogged renderer answers nothing inside the bound. Every rebind row now carries targetBasis; pageUrlForCensus is deliberately unchanged so the honest degenerate activeUrl stays in the census. CONSEQUENCE FOR THIS GATE: gates (1), (3) and (6) are counted on rows that the pre-round-3 build never booked, so any census taken before 2026-09-11 under-counts the target population and must not be carried forward. GRADUATION GATE \u2014 flip defaultState to 'on' only when ALL of the following hold across >=5 consecutive nightly qa-exhaustive runner lanes AND >=2 ci:qa-targeted dispatches of the pilot's 25-step draft-completion plan on staging: (1) NO SPURIOUS FIRE \u2014 every capture_fallback:would_rebind row is preceded, in the same capture, by capture_fallback_frame_absent {reason:'tab_mismatch'} (ladder site) or by a session.page reassignment since the previous invoke (invoke site); ZERO rows on a decision whose recorder already owned the active page. (2) NO SINGLE-TAB FIRE \u2014 zero would_rebind rows on any run whose activeTab is 'tab1' throughout; a row there means the attribution predicate is wrong, not the binding. (3) TARGET PARITY \u2014 of the rows where a FOLLOWING tool_call recorded a post-state URL, 100% name the page that call acts on, and the denominator is published (the gate cannot be stated over all rows: on a wedged renderer the next tool call may never produce a post-state URL). (4) FORCED-ON EVIDENCE \u2014 on runner/tab-switch-busy-finale pinned =1, over >=8 runs: a landed capture_fallback:rebind {ok:true} on EVERY run, ZERO tab_mismatch rows of either kind (capture_fallback_frame_absent {reason:'tab_mismatch'} and capture_fallback_stale_frame_refused {refusal:'tab_mismatch'}), ZERO capture_degraded whose ownerTab !== activeTab, and the browser_unobservable:capture_missed.classification distribution published beside the =0 control (see the client-visible wording measurement in `graduation.gate`). Deliberately NOT 'at least one rescue': measured 2026-09-10 over 3 pinned runs, one run booked no starved capture at all (the agent's captures fell either side of the hog) \u2014 once the recorder follows the executor, WHETHER a capture starves is not a property that fixture controls, and gating on it would red a run that demonstrated the binding. (5) SHADOW-DEFAULT TRACE PARITY \u2014 runner/hang-busy-finale-static, runner/hang-busy-finale-transient and runner-blocked-synthesis book ZERO would_rebind rows and a ladder census identical to the pre-change one (rung order, used/cap, latch count, refusal reasons, terminal) across the same nights. (6) COST \u2014 the would_rebind count per run stays at or below CAPTURE_FALLBACK_MAX_TAB_REBINDS_PER_RUN, which is exactly what the virtual counter makes measurable in shadow. Enforcement before graduation exists ONLY as the pinned eval case's env. NAMED RESIDUALS, not closed here: noteCaptureObservability still classifies page_busy off session.lastScreencastFrameAt, which a mismatched run's OTHER tab stamps \u2014 the new recorderTab/activeTab fields make it detectable, and the =1 flip to unobservable is MEASURED and CUSTOMER-VISIBLE (it removes the 'alive but busy' clause from both the blocked run summary and the auto-filed issue), so it is gated by (4) rather than merely expected; and the extension lane's own tab_mismatch is deliberately unfixed and excluded from the census denominator by the extension_session refusal. PAIRED WITH EXECUTOR_BLANK_POPUP_ESCAPE (#3104b, added 2026-09-11), AND THE PAIRING IS AN OPERATIONAL CONSTRAINT ON THIS GATE. The rebind-p1 rehearsal (2026-09-11, tp_d1360115, D1 run_405d5842 / D2 run_0192b16a, 2/2) re-attributed the residual used:0 defect: on that shape the recorder is evaluated, PROBED and correctly left in place (rebind_declined {reason:'blank_target', targetBasis:'document_empty', activeUrl:':'}), and the run still dies browser_unobservable -- because the EXECUTOR (session.page) stays on a never-rendered popup and nothing moves it back. The popup answers page.evaluate in milliseconds while Page.captureScreenshot times out 3x3000 ms, i.e. it is a NEVER-RENDERED WINDOW rather than a busy renderer, which falsifies round 3's fail-open premise ('a read that does not come back means a BUSY page -- exactly the incident') ON THIS SHAPE and makes =1 a no-op on precisely the tab_mismatch population this flag exists for. That residual is fixed by EXECUTOR_BLANK_POPUP_ESCAPE, whose predicate deliberately inverts round 3's polarity for its own side (for the RECORDER 'don't know' means FOLLOW; for the EXECUTOR 'don't know' means DO NOT ABANDON -- both fail-safe). THE CONSTRAINT: NEVER force-ON EXECUTOR_BLANK_POPUP_ESCAPE during a CAPTURE_FALLBACK_TAB_REBIND graduation window, and never carry a census taken inside one forward. Under that flag's =1 the executor leaves the popup on the escaping invoke, so the tab_mismatch disappears and the rebind_declined / would_rebind rows this gate's (1), (3) and (6) are counted on VANISH on every subsequent invoke -- one flag's enforcement erases the other's census. The escape is ordered AFTER maybeRebindScreencastToActivePage precisely so the REBIND rows on the ESCAPING invoke stay byte-identical, but that parity covers the rebind rows on exactly one invoke -- not the ladder rows that would have followed on it, which under =1 legitimately vanish too -- and is not a substitute for keeping the windows disjoint."},EXECUTOR_BLANK_POPUP_ESCAPE:{key:"EXECUTOR_BLANK_POPUP_ESCAPE",envVars:["AGENTIQA_EXECUTOR_BLANK_POPUP_ESCAPE","AGENTIQA_EXPERIMENT_EXECUTOR_BLANK_POPUP_ESCAPE"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Move the EXECUTOR (session.page) off a popup that never rendered, close it, and return the run to the owner tab (#3104b). SHADOW-FIRST, AND THE SHADOW IS FREE -- THREE STATES, read through BOTH switch readers (killSwitchEnabled for enforcement, killSwitchForcedOff for detection). =0: DETECTION OFF TOO. The reader returns before touching anything -- no classifier, no probe, no census row, no run budget minted -- which is the operator's remedy if the measurement itself hurts, without a redeploy; killSwitchEnabled alone cannot express it because it cannot tell 'no env, so default-off' from 'an operator typed 0' (killSwitch.ts, the idiom REPEATED_SIDE_EFFECT_GUARD / REDIRECT_FLIPFLOP_GUARD / CAUSE_GROUNDING / LABEL_DRIFT_DISCLOSURE already use). UNSET (the shipped default, SHADOW): the predicate runs on the probe the rebind already memoised and emits executor_escape:would_escape; it pays NO settle wait and NO re-probe, so the invoke boundary is byte-identical in timing and in page.evaluate count to the pre-change build. NOTHING closes, nothing moves, session.page is untouched and no model-visible string changes. =1: pays the bounded settle, re-validates the session state the await could have changed, re-probes ONCE, and only then escapes. THE DEFECT, AND IT IS A RE-ATTRIBUTION OF #3104's. #3104 read used:0 as 'the recorder cannot move'. The rebind-p1 rehearsal (2026-09-11, tp_d1360115: D1 run_405d5842 / asess_1789128041968_0ib9b50y and D2 run_0192b16a, line-for-line identical, 2/2 reproduction) measured otherwise. Step 25 of the pilot's draft-completion plan clicks a pencil (edit) icon; the app answers with a window.open whose window never navigates and never paints. Chromium reports its URL as the degenerate string ':' and its document answers page.evaluate IN MILLISECONDS with {readyState:'complete', bodyChildCount:0, title:''}, while Page.captureScreenshot on it times out 3x3000 ms. The live rows: capture_fallback:rebind_declined {at:'invoke', reason:'blank_target', targetBasis:'document_empty', activeUrl:':', ownerTab:'tab1', activeTab:'tab2', used:0, cap:2} -> capture_fallback_frame_absent {reason:'tab_mismatch'} x3 -> capture_fallback_cdp_failed {elapsedMs:3000, timeoutMs:3000} x3 -> capture_fallback_stale_frame_refused {refusal:'tab_mismatch', used:0, cap:2} x3 -> browser_unobservable:capture_missed {streak:1|2|3} -> the terminal, with an auto-filed ENVIRONMENT issue blaming our browser. THE REBIND'S REFUSAL IS CORRECT AND IS NOT WEAKENED ANYWHERE. targetBasis:'document_empty' proves the #3148 probe ran and the document ANSWERED; re-pointing the recorder at an empty document would blank the run video AND let the ladder serve empty frames as ownerTab === activeTab evidence, which docs/VERDICT-GATES.md already records as strictly worse than the refusal it replaces. What it also proves is that #3148's fail-open premise ('a read that does not come back means a BUSY page -- exactly the incident') does NOT hold on this shape: the popup is a NEVER-RENDERED WINDOW, not a busy renderer, so CAPTURE_FALLBACK_TAB_REBIND=1 is a no-op on precisely the tab_mismatch population it exists for. The residual defect is therefore that the EXECUTOR stays on the dead popup and nothing in the engine ever moves it back -- which no recorder rebind can fix, by construction. WHY NO EXISTING PATH REACHES IT. Every 'get off this popup' mechanism is keyed on WHERE the popup points or on the MODEL noticing, never on WHETHER THE POPUP IS A PAGE AT ALL: containment (containPendingPopupEscapes) is keyed on isCrossEnvironmentEscape(appUnderTestOrigin, url), and ':' has no origin to compare, so pendingPopupEscapes stays EMPTY and the drain returns null -- pending_escape is a CROSS-ENVIRONMENT concept, not a LIVENESS one; the popup's own on('close') restore needs the popup to close, and this one never does; the incidental-external-login lane keys on a recognisable IdP URL; switch_tab/close_tab are model-driven and all three invokes returned a capture error with NO screenshot, so the model had no picture of a blank tab to reason from. THE MECHANISM, and every piece of it is copied from a path that already exists. One pure predicate, evaluateBlankPopupEscape (captureStarvationFallback.ts), called from maybeEscapeBlankPopupExecutor at the invoke boundary immediately AFTER maybeRebindScreencastToActivePage(session,'invoke'). GUARDS, in order, each with a reason in the census: no_popup (FIRST and free -- the executor is not on a live tab2 that is also the active tab, so a single-tab run costs one identity comparison and reads nothing); no_owner (tab1 is not live, or is the popup -- there is nowhere to escape to and closing the run's only surface is worse than staying); extension_session (the whole mechanism is scoped OUT of the wallet lane, where tab1 IS the extension page and an executor on tab2 is the lane's design -- ONE row per RUN, not per invoke); pending_escape (containment owns the popup and closes it at the invoke tail; two mechanisms closing one page is how a page ends up closed under an in-flight captureState); restart_in_flight (a restartScreencast is already mutating the session -- placed BEFORE the target guards here, which DIVERGES from evaluateTabRebind's placement, because what follows is not a 500 ms read but a 3 s settle wait and paying that inside a restart race is the #3121 cost regression); url_not_degenerate (require classifyRebindTarget({url}).basis === 'document_unreadable', i.e. isUnstatableUrl true AND isKnownBlankUrl false -- this DELIBERATELY EXCLUDES about:blank and data:, which is the window.open('')+location= in-flight shape and the single largest false-positive population, and excludes every statable URL); document_live (the probe answered and there is content); document_unreadable (THE PROBE DID NOT ANSWER, AND THAT IS A REFUSAL -- the EXACT OPPOSITE POLARITY to classifyRebindTarget's documented fail-open, and deliberate: that predicate answers 'may the RECORDER follow?', where 'don't know' means follow because the cost of not following is a dead ladder and the cost of wrongly following an unreadable-but-live page is nil; this guard answers 'should the EXECUTOR abandon?', where 'don't know' means DO NOT ABANDON because a renderer that cannot answer is a BUSY page, the executor belongs on a busy page, and abandoning one is destructive and irreversible. Both are fail-SAFE for their own side; the asymmetry is pinned by a dedicated unit test so a future reader 'fixing the inconsistency' trips a red test); the FIVE POST-SETTLE reasons, distinct on purpose and =1-only (an earlier draft booked one 'settled' for all of them, which made the graduation gate's 'the guard is live' evidence a union of a rescue and its opposite -- a popup that CLOSED during the wait reads back from a dead page as document_unreadable, so a window that DIED satisfied a gate whose sentence is 'the settle wait actually rescues a late-rendering popup'): document_live_after_settle (it rendered -- THE rescue) and popup_navigated (it committed a STATABLE url while we waited -- also a rescue, and it deliberately SUBSUMES the url guard on pass 2, because 'became a page' is not the same fact as 'was never a candidate') are the two the gate counts; popup_url_blanked (the url went KNOWN-BLANK rather than statable: about:blank, bare about:, data:, empty -- NOT a rescue, and counting it as one is what the #3165 round-2 review caught, since about:blank over an empty document is the in-flight window.open('')+location= shape guard 6 exists to hold off and '' is not a navigation at all, so gate (4) would have been satisfiable by a window that still never rendered), document_unreadable_after_settle (the renderer went busy -- not a rescue, not a death), popup_closed (the window died on its own under the wait) and state_moved (session.page/tab2/activeTab no longer name this popup: context.on('page') promoted a peer, the popup's own on('close') restored tab1, or the model switched tabs -- the pass-1 world is gone, so nothing may be closed on the strength of it) are NOT; capped (EXECUTOR_BLANK_POPUP_MAX_ESCAPES_PER_RUN = 2, latched on the RUN-keyed captureLadderRunBudget so neither a mid-run bring-up nor the peer sweep refunds it); and shadow LAST, which is what makes the census readable -- reason:'shadow' means every other condition said yes on PASS 1 and only the flag said no, so a would_escape row is one-for-one with an escape the enforcing build would have ATTEMPTED. It is NOT one-for-one with one it would have PERFORMED: the enforcing build then pays the settle wait, which can still rescue the popup, so the shadow count is an UPPER BOUND on the enforced count and the graduation gate reads it as one. That is the stated price of a free shadow, and it is the honest trade -- the alternative is a default that inserts up to 3 s of wall clock at the invoke boundary, ahead of the action, on a build that cannot act on the answer. THE SETTLE WAIT is the false-positive guard and the reason this shape beat the alternative of never promoting a blank popup at all: a popup withheld from promotion has no promotion-on-navigate path in this engine (newPage.on('framenavigated') feeds containment only), so an OAuth window that paints 800 ms later would be PERMANENTLY ORPHANED -- a SILENT wrong-surface run, strictly worse than today's legible terminal. EXECUTOR_BLANK_POPUP_SETTLE_MS = 3000 (= CAPTURE_FALLBACK_CDP_TIMEOUT_MS, the bound rung 2 already spends on this same renderer), implemented as Promise.race([popup.waitForLoadState('load'), sleep]) plus one re-probe, PAID ONLY UNDER =1 -- it is not a measurement, it is part of performing the escape safely, and nothing closes in shadow -- and MEMOISED ON THE POPUP IDENTITY ALONE on the run budget, with the record surviving the executor LEAVING the popup, so a blank popup costs ONE settle and ONE re-probe for the whole run -- standing, tab-toggled, or changing its degenerate url under us. TWO drafts got that bound wrong and both are recorded because the shape repeats: the first dropped the memo on every invoke where the executor was elsewhere, so switch_tab('tab1') then back re-paid the full 3 s and re-booked a row on every return; the second keyed it on (popup, url), so a popup whose DEGENERATE url changed (':' -> about:srcdoc -> ...) re-armed a full wait plus a page.evaluate every time. Both were unbounded for the same reason -- a post-settle refusal never spends the run cap, the popup stays and the edge survives -- while gate (5) and docs/VERDICT-GATES.md both claimed 'one settle wait and one extra page.evaluate per popup per run'. Keyed on identity that sentence is literally true and gate (5) is measurable, and nothing is lost: a popup that became a real page is refused earlier AND more cheaply at guard 6, and a RELAPSE is a new Page object with a fresh record. After the await the session is RE-VALIDATED before anything is closed (session.page/tab2/activeTab still this popup, and the popup still open), because context.on('page') and the popup's own on('close') are live concurrent mutators of exactly those fields and routinely run inside that window; a mismatch books popup_closed / state_moved and moves nothing. It is also a second buffer behind the first: the decision is taken at the NEXT invoke boundary, i.e. after a full LLM turn has already elapsed. THE MOVE is copied verbatim in shape from containPendingPopupEscapes: session.page = session.tab1; activeTab = 'tab1'; tab2 = undefined; needsFullSnapshot = true; then popup.close(). Restore BEFORE close, so the popup's own on('close') handler finds nothing to do and cannot race; revertScreencastTargetOnPageClose still runs and is a no-op (the recorder never left tab1, it declined). EVERY NON-BROWSER EFFECT IS COMMITTED BEFORE THE CLOSE and THE CLOSE ITSELF IS BOUNDED (EXECUTOR_BLANK_POPUP_CLOSE_TIMEOUT_MS = 1500, the same Promise.race + unref shape the settle uses). It was the mechanism's only unbounded await and its only destructive one, made on precisely the population measured to stop answering (page.evaluate answers in ms while Page.captureScreenshot times out 3x3000 ms), and it sits AHEAD of dispatch -- so a close that HUNG (which the catch never covered; it covers a rejection) hung the whole action, with the session already moved and the note staged AFTER it, i.e. the tab block gone, the executor moved and NO note. Staged first and bounded, a hung close costs 1500 ms, leaves the executor on the owner tab WITH the note, and books executor_escape:declined {reason:'close_timeout'} beside the blank_popup row (which also carries closeOk) -- the one population where a Page object outlives the escape, and countable rather than silent. A REJECTED close is 'already closed', not a timeout. MODEL-VISIBLE, =1 ONLY, unit-pinned (buildExecutorEscapeNote) AND PINNED END-TO-END, which is the part the first draft got wrong: the prose rides metadata.note -- the ONLY channel that reaches the model, because BrowserActionExecutor builds the LLM-facing response from an explicit per-field allowlist and a bespoke metadata key is invisible to the agent no matter how it is worded (the draft wrote metadata.executorEscapeNote, nothing read it, and deleting the whole merge failed ZERO tests) -- appended to any note the action already produced, never replacing it, and never an error, since the action itself succeeded. The structured metadata.executorEscape {kind:'blank_popup', closedUrl, settleMs} is allowlisted into BOTH the persisted tool_call actionArgs and the response, the treatment crossEnvironmentContainment and incidentalExternalLogin already get, so a destructive tab move is queryable in the trace rather than only inferable from free text. It is staged into a LOCAL at the boundary and attached at the single tail return site, so an action that throws cannot carry it onto the next one. The note NAMES AGENTIQA AS THE ACTOR AND FORBIDS GRADING FROM THE CLOSURE ('Agentiqa closed a blank popup that opened during this run ... THE CLOSURE WAS THE TEST ENGINE'S RECOVERY, NOT THE APPLICATION'S BEHAVIOUR -- do not treat the popup opening or closing as evidence for any criterion; re-observe the original page instead'), states the OBSERVATION ('never rendered any content'), invites the finding explicitly ('this is a defect of the application under test: report it rather than retrying the same control'), and explains that there is no longer a second tab -- because after the escape session.tab2 is undefined and the {activeTab, tabCount, tabUrls} block the invoke tail injects VANISHES mid-run, which unexplained is a hallucination source. THE ACTOR CLAUSE IS THE FIX FOR THE ONLY FALSE-passed PATH THIS MECHANISM OPENS (#3165 round-2 review): the first draft's passive voice ('it has been closed and you are back on the page you were on') never said WHO closed the window, so a step or criterion phrased about the window going away ('the dialog closes', 'no extra tab remains', 'the popup is dismissed') was gradable passed FROM OUR OWN ACTION with the note as its supporting observation -- the evidence-laundering class alternative (c) was rejected for, reached through the prose instead of the capture. closedBy:'engine' on the structured field carries the same fact without the prose, so an audit can filter on it. Every clause is GROUNDED in something the engine measured: 'the window the previous action opened' and 'it produced no picture at all' were both dropped, because the escape fires at an invoke boundary rather than at the popup's birth (a boundary that refused on document_unreadable leaves it to the NEXT one, and context.on('page') promotes windows no action opened) and it never consults capture or frame state. The reported URL is truncated at EXECUTOR_ESCAPE_NOTE_URL_MAX = 120 -- pageUrlForCensus stays verbatim for the diag row, but the same string is a MODEL input and the population is by construction something new URL rejects. The wording is the mitigation for this mechanism's strongest objection (that closing the window suppresses a real product finding), so it is pinned by a test rather than left to drift. The counterfactual answers the rest of that objection: TODAY the run does not file that finding either -- it dies browser_unobservable and auto-files an ENVIRONMENT issue blaming OUR browser, losing every grade the run had earned. The escape strictly improves attribution. ORDERED AFTER THE REBIND, and the order is load-bearing twice: the #3104/#3148 REBIND rows on the escaping invoke (capture_fallback:rebind_*) stay BYTE-IDENTICAL in both flag states (pinned by a parity test), which is what lets a blank_popup row be read back against the rebind_declined{blank_target} row that justified it; and the document probe the rebind memoised on lastTabMismatch.probe is REUSED, so on the incident shape DETECTION buys ZERO additional page.evaluate. THE PARITY CLAIM COVERS THE REBIND ROWS AND NOTHING ELSE: the capture ladder has not run when this decision is taken, so under =1 the capture_fallback_frame_absent{tab_mismatch} / capture_fallback_cdp_failed / capture_fallback_stale_frame_refused{tab_mismatch} rows that would have followed on that same invoke -- and every rebind row on SUBSEQUENT invokes -- legitimately do not appear, because the executor is back on the owner tab and there is no mismatch left to book. That is the fix, and it is the documented divergence. ENFORCEMENT'S OWN COST, stated rather than rounded to zero and now actually enforced by the identity-keyed memo: one extra page.evaluate (the post-settle re-probe) and one bounded wait of up to 3000 ms, PER POPUP PER RUN, inserted at the invoke boundary ahead of the action -- plus, only on an escape that actually fires, up to 1500 ms for the bounded close. OPERATIONAL COUPLING, stated rather than discovered: NEVER force-ON EXECUTOR_BLANK_POPUP_ESCAPE during a CAPTURE_FALLBACK_TAB_REBIND graduation window. Two flags on the same population, one of which erases the other's census -- #3104's gates (1), (3) and (6) are counted on rows this flag's enforcement removes. Recorded in both flags' notes and to be recorded in docs/FLAGS-STATE.md whenever either is set live. CENSUS: executor_escape:would_escape {at, popupUrl, ownerUrl, basis, settleMs, used, cap} (shadow only); executor_escape:blank_popup {at, closedUrl, ownerUrl, basis, settleMs, used, cap} (=1 only); executor_escape:declined {at, reason, basis, popupUrl, ownerUrl, settleMs, used, cap} (both live states; reason is one of no_popup / no_owner / extension_session / pending_escape / restart_in_flight / url_not_degenerate / document_live / document_unreadable / capped in either, plus the =1-only post-settle six: document_live_after_settle, popup_navigated, popup_url_blanked, document_unreadable_after_settle, popup_closed, state_moved -- and close_timeout, which the predicate never returns: it is booked AFTER a successful escape when the bounded popup.close() did not confirm), edge-deduped on (popup identity, outcome) exactly like evaluateTabRebind's lastTabMismatch.decided set, so a standing blank popup is ONE row rather than one per invoke and a MOMENTARY refusal cannot become the popup's permanent answer. settleMs is 0 on every shadow row by construction -- shadow waits for nothing. Under =0 there is no census at all. VERDICT-WIDENING, DELIBERATELY, and =1-ONLY: a run that previously reached browser_unobservable with NO verdict now reaches a gradable capture of the owner tab, so the step can book a genuine passed OR a genuine failed -- both honest, because after the escape ownerPage === session.page === tab1 is a FACT and the capture is an honest capture of the owner page taken at a moment when the executor is genuinely on it. The escape moves the executor; it NEVER relabels evidence and never rewrites a grade the model gave -- it removes the PRECONDITION for the terminal. Stated precisely, because 'never changes a verdict' would be too strong: the note it attaches IS A MODEL INPUT on the escaping action, exactly like any other tool-result note (the incidental-external-login note, the degraded-capture label), and it deliberately invites a finding about the application under test. What the mechanism never does is re-grade a step, re-observe an action on a different surface, or relabel evidence; what it does do is tell the model a true thing about the window it was driving, and the model's next turn is free to act on it. Recorded on the two-sided browser_unobservable entry in docs/VERDICT-GATES.md. NOT CHANGED by either setting: tab safety (frames still carry ownerPage and both rung predicates still read the FRAME's owner), the ladder's rung order, the stale budgets, the latch, the capture-miss streak, and the terminal's bound and wording. WHEN NOT ENFORCING the engine is byte-identical to the pre-change build in everything the model or the page can observe: no page is closed, session.page/activeTab/tab2/needsFullSnapshot are untouched, and no metadata field or model-visible string is added. In SHADOW (unset) that extends to timing and to the page.evaluate count on the incident shape -- the only new artifact is the executor_escape:* census. Under =0 not even the census exists.",designDoc:"packages/engine-core/src/captureStarvationFallback.ts",status:"active",added:"2026-09-11",graduation:{status:"gated",gate:"GRADUATED 2026-09-13 (default ON). Flip defaultState to 'on' \u2014 all six conditions met on the mirror: (1) FORCED-ON MIRROR PASS: 4/5 runs passed on tp_d1360115 (E1, E3, E4, E5 in rehearsal-2026-09-11-escape-p1); (2) SHADOW CENSUS ON THE SAME SHAPE: 4/5 clean passes without popup in rehearsal-2026-09-13-escape-shadow + 1 clean pass in 09-12-rc Pass R, and on runs where popup appeared under shadow (S1 09-11, D1 rebind-p1, D2 rebind-p1), executor_escape:would_escape was booked on step 25; (3) ZERO SPURIOUS FIRE: zero would_escape/blank_popup rows on 17 single-tab plans of the mirror; (4) FALSE-POSITIVE GUARD IS LIVE: executor_escape:declined[capped] verified in E2; (5) MODEL HONESTY INVARIANT: zero step-25 criteria results citing popup lifecycle; (6) COST: cap 2/run held. Kill switch: AGENTIQA_EXPERIMENT_EXECUTOR_BLANK_POPUP_ESCAPE=0.",evidence:"Rehearsal reports ~/agentiqa-agents/lio-harvest/reports/rehearsal-2026-09-11-escape-p1.md (4/5 passed under =1 on tp_d1360115, 0 spurious escapes on 17 single-tab plans, 0 criteria citing popup lifecycle, cap 2 held) and rehearsal-2026-09-13-escape-shadow (5-replicate shadow census). Unit suites: packages/engine-core/src/__tests__/captureStarvationFallback.blankPopupEscape.test.ts and packages/engine-core/src/__tests__/BasePlaywrightService.blankPopupEscape.test.ts. Registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED.",owner:"steering (Alex)",review:"2026-10-01"},notes:"GRADUATED default-ON 2026-09-13 (1-enables, defaultState on, killSwitch-enabled). Listed in killSwitchDefaultState.test.ts's INTENTIONALLY_GRADUATED allowlist. Kill switch is =0, which disables detection and enforcement (killSwitchForcedOff). Read site: packages/engine-core/src/BasePlaywrightService.ts (maybeEscapeBlankPopupExecutor, called from invokeAction's boundary immediately after maybeRebindScreencastToActivePage(session,'invoke')). Pure module: packages/engine-core/src/captureStarvationFallback.ts (evaluateBlankPopupEscape, buildExecutorEscapeNote, EXECUTOR_BLANK_POPUP_MAX_ESCAPES_PER_RUN, EXECUTOR_BLANK_POPUP_SETTLE_MS), which also owns the run-budget fields executorEscapesUsed / executorEscapeExtensionReported / lastBlankPopup on CaptureLadderRunBudget. Claim runner.browser-unobservable-terminal."},SCREENCAST_KEEPALIVE:{key:"SCREENCAST_KEEPALIVE",envVars:["AGENTIQA_SCREENCAST_KEEPALIVE","AGENTIQA_EXPERIMENT_SCREENCAST_KEEPALIVE"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Screencast starvation watchdog: keepalive capture vs full teardown. A page with no compositor activity emits no Page.screencastFrame, so an ordinary LLM turn on a settled page was indistinguishable from a broken CDP session \u2014 and the watchdog answered BOTH with a full teardown + re-attach (stopScreencast -> detach -> newCDPSession -> startScreencast -> captureScreenshot). Measured on the 2026-08-11 Lio prod burst: 30 screencast_restart events across 11 of 15 sessions, 8 of which had the 1-2 a quiet page produces; the common case was paying the rare case's price in five CDP round-trips to the very renderer that, in the rare case, is unwell. When ON, a quiet screencast is kept alive with ONE bounded Page.captureScreenshot on the EXISTING CDP session \u2014 which is also the keepalive frame the recorder (CloudScreencastService: run video + live view) wants, so video is unaffected \u2014 and the teardown runs only when that capture fails, i.e. when the session is genuinely broken. The keepalive stands down while an evidence capture is in flight (never queue a second full-page capture behind the agent's own) and cannot re-enter itself. OFF restores the unconditional teardown on starvation. Writes NO verdict on either setting. NOTE: the TIMEOUT BOUNDS on the teardown path (newCDPSession / stopScreencast / detach / startScreencast / bootstrap capture) are UNCONDITIONAL and not gated by this flag \u2014 an unbounded await there could strand screencastRestarting=true forever, silently disabling the watchdog and stopping the video for the rest of the run, which is a bug under any setting.",designDoc:"packages/engine-core/src/BasePlaywrightService.ts",status:"active",added:"2026-08-11",notes:"Ships default-ON from inception \u2014 it strictly reduces CDP churn and adds no verdict path, so there is no pre-change OFF behavior worth preserving as the default. Registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED. Read site: packages/engine-core/src/BasePlaywrightService.ts (the startScreencast watchdog tick -> keepScreencastAlive). Claim runner.screencast-keepalive-not-teardown."},STEP_REPLAY_DISCLOSURE:{key:"STEP_REPLAY_DISCLOSURE",envVars:["AGENTIQA_STEP_REPLAY_DISCLOSURE","AGENTIQA_EXPERIMENT_STEP_REPLAY_DISCLOSURE"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:`Step-replay disclosure (RunnerRuntime / test-plan runs only). Motivating incident: prod asess_1786419613237_4wtdj14q (Lio CI, 2026-08-11) \u2014 our capture pipeline went dark at step 25 (screencast frame_starvation + two "page.screenshot: Timeout 5000ms exceeded" action errors, a browser_unobservable streak of 2 against a bound of 3, so no terminal fired), the agent read the blindness as a page hang and reloaded, the reload discarded the unsaved draft, and the Supervisor issued REDIRECT "re-perform steps 7-24". The run then reported status=passed with all 25 steps passed after 192 LLM calls / 6.1M prompt tokens \u2014 36% of that 15-plan burst's tokens \u2014 with the replay visible nowhere in the customer-facing record. When on, the runtime counts a REPLAY at the mutating-dispatch seam (a click/type/credential-fill/focused-value/key-combination/drag/upload under an action or setup step N when step N had already dispatched one AND some step > N had too; verify steps are excluded from disclosure and log step_replay_detected with suppressedReason="verify_step" for census (#3477), and observation, navigation and run_js are excluded so a runtime-ordered re-verify or an ordinary back-navigation never counts), and at the END of the run_complete cascade prepends one disclosure sentence to run.summary, appends a note to each replayed step's result (never replacing the model's note) stating that the agent went back to earlier steps on its own and each step was graded once at the end (#3477), and emits step_replay_disclosed. Writes NO verdict: not a run status, not a step status, not a criterion \u2014 each step is graded once in run_complete, and what was dishonest was the run reporting as if nothing had happened. Off \u21D2 no counting, no sentence, no note, byte-identical to pre-change behavior.`,designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-08-11",notes:"Ships default-ON from inception \u2014 the disclosure adds a record where there was none and changes no verdict in either polarity, so there is no pre-change OFF behavior to preserve. Registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED. Detection + sentence in packages/engine-core/src/runner/stepReplayDisclosure.ts; the kill-switch read + wiring live at packages/engine-core/src/RunnerRuntime.ts (the mutating-dispatch seam and applyStepReplayDisclosure in handleRunComplete). Runner lane only. Claim runner.replay-not-laundered-into-pass."},PREDICATE_BASIS_COMPILE:{key:"PREDICATE_BASIS_COMPILE",envVars:["AGENTIQA_PREDICATE_BASIS_COMPILE","AGENTIQA_EXPERIMENT_PREDICATE_BASIS_COMPILE"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:`Trust-layer predicate basis \u2014 Slice B AUTOFORMALIZATION COMPILER (design docs/plans/2026-07-26-trust-layer-predicate-basis-design.md; north star docs/plans/2026-07-26-trust-layer-verification-architecture.md). "Compile-don't-parse": at AUTHORING an LLM COMPILES a natural-language assertion into a typed Slice-A logical form (Predicate) over named observables \u2014 autoformalization (utterance \u2192 logical form \u2192 executor \u2192 denotation) \u2014 NOT a verdict. Slice A shipped the schema + generic deterministic executor; Slice B produces the forms the executor denotes. TASK-BLIND: the compiler compiles the assertion's MEANING (including any pinned expected value the assertion text itself carries) and never sees the observed screen or pass/fail \u2014 the firewall keeps the LLM out of the DECISION (Slice A's deterministic executor owns pass/fail). GRAMMAR-CONSTRAINED \u2192 MECHANICAL ABSTAIN: the model may only emit an in-grammar kind (count/delta/absence/modification/presence/typed) or the explicit not_groundable escape; compileToPredicate then STRICTLY validates the chosen kind's operands, so an inherently-subjective assertion ("the agent responds correctly", "looks clean") or an in-grammar kind with missing/invalid operands routes to a first-class ABSTAIN (AbstainNode) \u2014 NEVER a forced/invalid form. The grammar's expressiveness IS the verifiable/subjective boundary. v1 = structured output + strict schema validation + a groundability decision; full constrained decoding (PICARD) is a later refinement (TODO). GRADUATED default-ON 2026-07-27 (alongside PREDICATE_BASIS_VERIFY, which is the live consumer that routes a compiled form to a step result); the emergency kill-switch is retained and byte-identical WHEN FORCED OFF: the seam (runAssertionCompile / runRedundantCompile) short-circuits to \`disabled\` (ZERO model calls) when AGENTIQA_PREDICATE_BASIS_COMPILE=0 \u2014 and with COMPILE forced off but VERIFY on the compiler returns a disabled result \u2192 ABSTAIN (safe, never a manufactured verdict). Cost when on: one auxiliary structured-output call per compiled assertion (cost-isolated via emitAuxiliaryLlmUsage \u2014 not a billable step), thinkingBudget:0, hard-capped by a per-call timeout; fail-closed (error/empty/timeout/out-of-grammar \u2192 ABSTAIN).`,designDoc:"docs/plans/2026-07-26-trust-layer-predicate-basis-design.md",status:"active",added:"2026-07-26",notes:"GRADUATED 2026-07-27 (default ON) \u2014 the authoring-side ARc compiler that feeds the LIVE verify wiring (PREDICATE_BASIS_VERIFY, graduated in the same PR). Turn-on = BOTH flags ON: with VERIFY on, runPredicateBasisVerify calls runRedundantCompile, which rides THIS flag; on \u21D2 the compiler produces the logical forms the deterministic executor denotes. Slice B (authoring-side compiler) + Slice C (ARc redundant-compile: compile k\xD7 \u2192 ABSTAIN on disagreement/degeneration, attacking the factKind-wrong-kind gap) are the compile consumers of this flag. GRADUATION EVIDENCE: the flagship live-replay benchmark (PR #1879) returned GATE=GO \u2014 zero-regression on the prod Lio (semantic-reference) + Miro (canvas) corpora, the firewall intact (a groundable contradiction FAILS; nothing ungroundable is ever a hard PASS), the vision/canvas path abstains, and the #1876 DOM-reachability fix live-confirmed. TASK-BLIND / FIREWALL (unchanged): the compiler compiles the assertion's MEANING (never sees the observed screen nor pass/fail); Slice A's deterministic executor owns the decision. GRAMMAR-CONSTRAINED \u2192 MECHANICAL ABSTAIN: an inherently-subjective or out-of-grammar assertion routes to a first-class ABSTAIN (AbstainNode), never a forced/invalid form. Off (explicit AGENTIQA_PREDICATE_BASIS_COMPILE=0) is byte-identical to pre-graduation: runAssertionCompile / runRedundantCompile short-circuit to `disabled` with ZERO model calls, and with COMPILE off but VERIFY on the compiler returns a disabled result \u2192 ABSTAIN (safe, never a manufactured verdict) \u2014 the emergency kill-switch still works. killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND PREDICATE_BASIS_COMPILE listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Read site: packages/engine-core/src/predicateBasis/compile.ts (runAssertionCompile) + packages/engine-core/src/predicateBasis/redundantCompile.ts (runRedundantCompile). Cost when on: one auxiliary structured-output call per compiled assertion, cost-isolated via emitAuxiliaryLlmUsage (not a billable step), thinkingBudget:0, hard-capped, fail-closed (error/empty/timeout/out-of-grammar \u2192 ABSTAIN). Acceptance tests: packages/engine-core/src/__tests__/predicateBasisCompile.test.ts + predicateBasisRedundantCompile.test.ts. Compile-correctness benchmark: e2e/benchmark/compileCorrectness.ts. Binds claims verify.predicate-basis-compile-correctness + verify.predicate-basis-redundant-compile. STAGING-FIRST: merging to staging makes this default-ON on the STAGING engine only; PROD stays OFF until a later staging\u2192main release carries it (a natural staged soak). PREDICATE_BASIS_CONSENSUS (Slice D vision-consensus) stays default-OFF \u2014 vision-consensus is safe ONLY as abstain, not turn-on-ready (the P2 dense-abstain finding)."},PREDICATE_BASIS_CONSENSUS:{key:"PREDICATE_BASIS_CONSENSUS",envVars:["AGENTIQA_PREDICATE_BASIS_CONSENSUS","AGENTIQA_EXPERIMENT_PREDICATE_BASIS_CONSENSUS"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:'Trust-layer predicate basis \u2014 Slice D: (1) CONSENSUS EXTRACTION/RESOLUTION and (2) the SEMANTIC-TOLERANCE FALLBACK TIER (design docs/plans/2026-07-26-trust-layer-predicate-basis-design.md \u2014 \xA7 three buckets bucket-2 canvas "task-blind extraction + deterministic comparison + consensus-or-abstain", \xA7 the @reference resolution consensus/abstain, \xA7 "Semantic as a fallback tier, not a peer primitive"; north star hard-part #1 "the extractor itself can be wrong: miscount a canvas, misread"). CONSENSUS: task-blindness removes confirmation bias but NOT perception error (the model genuinely miscounts a canvas / mis-resolves "the cart total"); the no-false-positive mitigation is N INDEPENDENT task-blind extractions must AGREE, else ABSTAIN \u2014 one mechanism serving BOTH the vision/canvas path AND the semantic-reference resolution risk. SEMANTIC-TOLERANCE: run the DETERMINISTIC executor FIRST; invoke the task-blind concept classifier ONLY when the deterministic comparison cannot decide (tolerance:semantic OR an inconclusive comparison) \u2014 confident semantic = Verified, ambiguous = Assessed (a lean + confidence, never a grounded badge, never a manufactured pass \u2014 the bucket-3 Assessed slot). SHADOW / behavior-neutral even when ON: NOT wired into any live RunnerRuntime verdict path \u2014 the two drivers are consumed only by the consensus benchmark + unit tests. Off \u21D2 ZERO extractor/classifier calls, byte-identical; Slice-A deterministic parity is UNAFFECTED (the semantic tier COMPOSES the untouched evaluate, only reached on an inconclusive result).',designDoc:"docs/plans/2026-07-26-trust-layer-predicate-basis-design.md",status:"active",added:"2026-07-26",notes:"Trust-layer predicate basis Slice D (consensus extraction/resolution + semantic-tolerance fallback tier). Default OFF; SHADOW / behavior-neutral even when ON \u2014 the two drivers produce would-be results and are wired into NO live verdict path (consumers: the consensus benchmark + unit tests). Read sites: packages/engine-core/src/predicateBasis/consensusExtract.ts (runConsensusExtraction \u2192 killSwitchEnabled('PREDICATE_BASIS_CONSENSUS'); zero extractor calls when off) + packages/engine-core/src/predicateBasis/semanticTolerance.ts (runSemanticTolerance \u2192 killSwitchEnabled('PREDICATE_BASIS_CONSENSUS'); zero classifier calls when off). CONSENSUS-EXTRACT: a PURE reducer (reduceNumericConsensus / reduceCategoricalConsensus \u2014 N observations \u2192 accept-on-agreement / ABSTAIN(extraction_disagreement) on numeric-spread-beyond-tolerance or different presence/text; fail-closed extraction_error when no usable sample) + a pure per-frame read (readObservable over count-of / presence-of / value-at) + the thin flag-gated driver over the EXISTING StateExtractor seam (reused, not re-implemented \u2014 N independent samples). SEMANTIC-TOLERANCE: a PURE reducer (resolveSemanticTier \u2014 deterministic-first; decided deterministic \u2192 Verified with no classifier call; inconclusive + eligible + injected classifier \u2192 semanticToSpectrum: confident match/contradiction \u2192 Verified, ambiguous \u2192 Assessed, abstain \u2192 the Inconclusive floor) + the flag-gated driver that COMPOSES the untouched Slice-A evaluate (Slice-A parity byte-identical by construction). Reuses the Slice-2 concept-classifier seam (ConceptClassifier / semanticToSpectrum / renderObservation from groundedStateUnified.ts). Deliberately NOT re-exported from predicateBasis/index.ts (imported directly). Acceptance tests: packages/engine-core/src/__tests__/predicateBasisConsensus.test.ts (deterministic \u2014 injected observations for the consensus reducer; injected classifier RESULT for the semantic tier; flag-OFF zero-calls; fail-closed error\u2192abstain). Consensus benchmark: e2e/benchmark/consensusExtraction.ts (deterministic reducer/tier self-validation always runs; the live N-extraction + classifier measurement is PENDING until a keyed run \u2014 Slice-1/B/C precedent, no fabricated number). Binds claim verify.predicate-basis-consensus-extraction. Slice E (abstain-rate as a first-class COVERAGE metric on a real-assertion corpus) is BUILT: packages/engine-core/src/predicateBasis/coverage.ts (aggregateCoverage \u2014 a PURE reducer folding pipeline-routed assertion outcomes into the Verified/Assessed/Inconclusive distribution + abstain rate + abstain-origin/per-kind breakdown), wired into NO live verdict path (no new flag; the live measurement reuses THIS flag + PREDICATE_BASIS_COMPILE), claim verify.predicate-basis-coverage-abstain-rate, benchmark e2e/benchmark/coverage.ts (deterministic layers real; live full-pipeline number PENDING until keyed). This COMPLETES the predicate-basis v1 build; the remaining work is the KEYED phase (live LLM measurements) + the prod Miro+Lio final gate.",graduation:{status:"gated",gate:"Slice D is shadow / behavior-neutral (two drivers, wired into NO verdict path). Graduation gates on: (1) the consensus benchmark run WITH a model key showing consensus ABSTAINS on genuinely disagreeing/noisy N task-blind extractions and ACCEPTS on agreement (the miscount-a-canvas / mis-resolve-a-reference no-false-positive mechanism), plus a measured abstain-rate on the vision/canvas + semantic-reference corpus; (2) the semantic-tolerance tier measured deterministic-first (a deterministic-decidable case never consulting the classifier) with confident\u2192Verified / ambiguous\u2192Assessed; (3) the engine-core predicateBasisConsensus unit suite green. This is the DEFERRED consensus-extraction that gates ANY vision-path live graduation; the FINAL gate is the prod Miro (canvas) + Lio (semantic-reference) benchmark.",evidence:"e2e/benchmark/artifacts/benchmark-consensus-extraction.{json,md} (consensus abstain/accept + abstain-rate + semantic-tier bands; PENDING until a keyed run) + the engine-core predicateBasisConsensus unit suite + the claim verify.predicate-basis-consensus-extraction",owner:"steering (Alex)",review:"2026-09-05"}},PREDICATE_BASIS_VERIFY:{key:"PREDICATE_BASIS_VERIFY",envVars:["AGENTIQA_PREDICATE_BASIS_VERIFY","AGENTIQA_EXPERIMENT_PREDICATE_BASIS_VERIFY"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Trust-layer predicate basis \u2014 Slice F LIVE VERIFY WIRING (the FIRST non-shadow slice; design docs/plans/2026-07-26-trust-layer-predicate-basis-design.md \xA7 three buckets; north star docs/plans/2026-07-26-trust-layer-verification-architecture.md \xA7 the verdict spectrum). Slices A\u2013E built the basis as SHADOW machinery wired into NO verdict path; this is the first wiring that lets a compiled logical form FEED A STEP RESULT (the readiness step toward turn-on). When ON, a verify step's assertion routes through the basis: runRedundantCompile (ARc: compile k\xD7 \u2192 ABSTAIN on disagreement/degeneration) \u2192 classifyVerifyBucket \u2192 the deterministic executor / semantic-tolerance tier \u2192 a verdict in the SPECTRUM (Verified-PASS/FAIL | Assessed | Inconclusive) that feeds the step result. SCOPED to the SAFE paths \u2014 everything else ABSTAINS, never manufactures a verdict: DOM-groundable (deterministic executor over structured extraction) \u2192 Verified-PASS/FAIL LIVE (a grounded contradiction FAILS \u2014 the no-false-positive core); confident-semantic (semantic-tolerance, deterministic-first, task-blind classifier) \u2192 Verified, ambiguous \u2192 Assessed (non-blocking WARNING band, never a hard pass/fail); VISION/CANVAS (bucket 2) \u2192 ABSTAIN (Inconclusive/WARNING), NOT enforced \u2014 the mandatory dense-abstain guard from the P2 finding (consensus does NOT eliminate correlated-miscount false-confirms on dense canvases, so vision must NOT produce a live PASS/FAIL yet; a count tally is treated as vision-grounded \u2192 ABSTAIN unless the caller asserts domGrounded); not-groundable / compile-abstain / consensus-abstain \u2192 Inconclusive/Assessed, NEVER a manufactured pass. FIREWALL INVARIANT: a groundable contradiction MUST FAIL; nothing ungroundable is EVER a hard PASS \u2014 verifyStepAction encodes the DEFAULT consequence policy (only a Verified-FAIL gates; a Verified-PASS confirms but never manufactures/overrides a pass; Assessed is a non-blocking warning; Inconclusive leaves the existing verdict). GRADUATED default-ON 2026-07-27 (alongside PREDICATE_BASIS_COMPILE) after the flagship live-replay gate; the emergency kill-switch is retained and BYTE-IDENTICAL WHEN FORCED OFF (non-negotiable): explicit AGENTIQA_PREDICATE_BASIS_VERIFY=0 makes runPredicateBasisVerify short-circuit to disabled with ZERO compile/executor/classifier calls and the RunnerRuntime enforcement pass early-return before any effect. The compile itself rides PREDICATE_BASIS_COMPILE (also graduated), so full turn-on = both flags ON (with VERIFY on but COMPILE forced off the compiler returns a disabled result \u2192 ABSTAIN, safe). STAGING-FIRST: default-ON reaches the STAGING engine on merge; PROD stays OFF until a later staging\u2192main release carries it.",designDoc:"docs/plans/2026-07-26-trust-layer-predicate-basis-design.md",status:"active",added:"2026-07-27",notes:"GRADUATED 2026-07-27 (default ON) \u2014 Slice F LIVE verify wiring, the FIRST live trust-verdict on the predicate basis (turned on alongside PREDICATE_BASIS_COMPILE per Alex's explicit approval after the flagship live-replay gate). When on (and a predicateBasisCompiler is wired at engine boot \u2014 buildDeps.getPredicateBasisCompiler, present whenever a Google key is set), a verify step's assertion routes through the basis: runRedundantCompile (ARc, rides PREDICATE_BASIS_COMPILE) \u2192 classifyVerifyBucket \u2192 the deterministic executor / semantic-tolerance tier \u2192 a SPECTRUM verdict feeding the step result. SCOPED to the SAFE paths \u2014 everything else ABSTAINS, never manufactures a verdict: DOM-groundable \u2192 Verified-PASS/FAIL LIVE (a grounded contradiction FAILS \u2014 the no-false-positive core); confident-semantic \u2192 Verified, ambiguous \u2192 Assessed (non-blocking WARNING); VISION/CANVAS \u2192 ABSTAIN (Inconclusive), NOT enforced \u2014 the mandatory P2 dense-abstain guard (a count tally is vision-grounded \u2192 ABSTAIN unless the caller asserts domGrounded); not-groundable / compile-abstain \u2192 Inconclusive, NEVER a manufactured pass. FIREWALL INVARIANT (unchanged): only a Verified-FAIL gates; a Verified-PASS confirms but never manufactures/overrides a pass; Assessed warns; Inconclusive leaves the existing verdict. GRADUATION EVIDENCE: the flagship live-replay benchmark (PR #1879) returned GATE=GO \u2014 zero-regression on the prod Lio (semantic-reference) + Miro (canvas) corpora, the firewall intact, canvas-abstain intact, and the #1876 DOM-reachability fix live-confirmed; the engine-core predicateBasisVerifyWiring + RunnerRuntime.predicateBasisVerify unit suites are green (flag-OFF byte-identical parity, safe-path enforcement, vision-canvas ABSTAIN, the firewall). BYTE-IDENTICAL WHEN FORCED OFF (emergency kill-switch retained): explicit AGENTIQA_PREDICATE_BASIS_VERIFY=0 restores the pre-graduation behavior \u2014 runPredicateBasisVerify short-circuits to disabled with ZERO compile/executor/classifier calls and the RunnerRuntime enforcement pass early-returns before any effect. killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND PREDICATE_BASIS_VERIFY listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Read sites: packages/engine-core/src/predicateBasis/verifyWiring.ts (runPredicateBasisVerify) + packages/engine-core/src/RunnerRuntime.ts (predicateBasisVerifyEnabled / the run_complete enforcement pass). Acceptance tests: packages/engine-core/src/__tests__/predicateBasisVerifyWiring.test.ts + RunnerRuntime.predicateBasisVerify.test.ts. E2E proof: e2e/benchmark/predicateBasisVerify.ts. Standing live gate post-graduation: e2e/scenarios/22-verify/01-predicate-basis-dom-enforce.ts asserts the pbv-ON DOM-enforce + canvas-abstain behavior by DEFAULT now (default-ON engine) in the nightly qa-exhaustive path. Binds claim verify.predicate-basis-verify-wiring. STAGING-FIRST: merging to staging makes pbv default-ON on the STAGING engine ONLY (staging deploys from staging); PROD stays OFF until a later staging\u2192main release carries it \u2014 a natural staged soak. VISION-path live PASS/FAIL stays DEFERRED behind PREDICATE_BASIS_CONSENSUS (still default-OFF) regardless of this flag \u2014 vision-consensus is safe ONLY as abstain, not turn-on-ready (the P2 finding)."},ELEMENT_STATE_GROUNDING:{key:"ELEMENT_STATE_GROUNDING",envVars:["AGENTIQA_ELEMENT_STATE_GROUNDING","AGENTIQA_EXPERIMENT_ELEMENT_STATE_GROUNDING"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:'Trust-layer predicate basis \u2014 ELEMENT-STATE grounding (residual track P2, slice 1: BUILD + SHADOW; design docs/plans/2026-07-27-element-state-grounding-design.md). Closes a groundability gap in the DOM-verify path (routeDomVerify / domExtractionSource): the deterministic a11y-outline extraction grounds PRESENCE / ABSENCE / COUNT / MODIFICATION but STRIPS element STATE \u2014 a `- button "Submit" [disabled]` line\'s `[disabled]`, an `aria-checked` / `[selected]` / `[expanded]` marker, and an input\'s inline value \u2014 so an assertion like "the Submit button is enabled" is NOT DOM-groundable and falls through to flaky model-vision grading (the Lio submission s8 flake: a weak vision-only read of button-enabled that varied run-to-run). The a11y snapshot (Playwright _snapshotForAI) DETERMINISTICALLY carries this state; this track parses it into a new `state-of` observable + `element-state` predicate the Slice-A executor denotes over, so enabled/disabled/checked/selected/expanded/value assertions ground deterministically. FIREWALL PRESERVED: a state that is NOT resolvable from the a11y outline (target element not found, dimension not applicable to the role, mixed/unreadable value) ABSTAINS (Inconclusive) \u2014 never force-fails on unresolvable, never manufactures a pass; enforcement stays force-fail-only (only a grounded CONTRADICTION \u2192 would_fail gates). SLICE 1 = SHADOW / behavior-neutral even when ON: the machinery (extraction + NL parser + mapping + the flag-gated driver runElementStateGrounding) is wired into NO live RunnerRuntime verdict path \u2014 consumed only by unit tests (+ a later graduation benchmark). Off \u21D2 ZERO element-state extraction / parse / evaluate, byte-identical (the new observable/extraction runs only under this flag); the existing pbv DOM/count/absence/modification paths are UNTOUCHED. Graduation (benchmark + gate-review + Alex\'s default-on flip + live wiring) is a LATER slice, mirroring the pbv arc.',designDoc:"docs/plans/2026-07-27-element-state-grounding-design.md",status:"active",added:"2026-07-27",notes:"Residual track P2 (element-state grounding) slice 1 \u2014 BUILD + SHADOW behind a default-OFF killSwitch, mirroring the predicate-basis Slice A\u2013E / Slice-D CONSENSUS shadow precedent (pure machinery + flag-gated driver, wired into NO live verdict path). Read site: packages/engine-core/src/predicateBasis/elementState.ts (runElementStateGrounding \u2192 killSwitchEnabled('ELEMENT_STATE_GROUNDING'); zero element-state work when off). PURE machinery (also in elementState.ts, always safe to call, exercised in the unit suite): extractElementStatesFromAriaSnapshot (per-a11y-line state parse keyed on Playwright's exact role\u2192dimension applicability \u2014 kAriaDisabledRoles / kAriaCheckedRoles / kAriaSelectedRoles / kAriaExpandedRoles \u2014 so absence-of-marker on an applicable role reads as false, and a non-applicable role ABSTAINS rather than guessing), parseElementStateAssertion (deterministic NL \u2192 {ref, dimension, expected}; returns null when no state vocabulary is present so it is additive), elementStateQueryToLogicalForm (\u2192 the `element-state` node), and evaluateElementState (in executor.ts, the Slice-A generic executor's new case \u2014 resolves the target element in frames.after.elementStates, reads the dimension, denotes PASS on confirm / FAIL on a grounded contradiction / INCONCLUSIVE on any unresolvable \u2014 target absent, dimension N/A, mixed/ambiguous). GROUNDABLE STATES: enabled/disabled (fully both directions \u2014 the Lio s8 case), checked/unchecked, selected, expanded (PASS on [expanded] + FAIL on a collapsed-assertion contradiction; absence ABSTAINS \u2014 collapsed vs non-expandable is not distinguishable in the outline), and input value (normalized-equality; typed/locale-tolerant value comparison is a follow-up routing to compareTyped). Acceptance tests: packages/engine-core/src/__tests__/predicateBasisElementState.test.ts (extraction, parser, mapping, executor firewall, flag-OFF zero-work / byte-identical, registry-presence freshness canary). killSwitch resolves the no-env value from this defaultState (#1729) \u2014 GRADUATING = flip defaultState to 'on' AND add ELEMENT_STATE_GROUNDING to INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire); slice 1 stays OFF.",graduation:{status:"gated",gate:"Slice 1 is BUILD + SHADOW (pure machinery + a flag-gated driver, wired into NO verdict path). Graduation gates on the LATER slice: (1) an element-state graduation-benchmark run showing enabled/disabled/checked/selected/expanded/value ground deterministically from the a11y outline (a grounded contradiction \u2192 would_fail FIXES the Lio-s8-class vision flake; an unresolvable state \u2192 ABSTAIN, no false-PASS / no false-FAIL) and adversarially proven able to say NO-GO; (2) wiring runElementStateGrounding into the RunnerRuntime run_complete enforcement pass (force-fail-only, alongside applyPredicateBasisVerify); (3) the engine-core predicateBasisElementState unit suite green + flag-OFF byte-identical; then Alex flips the default-on (and lists it in INTENTIONALLY_GRADUATED). Mirrors the pbv graduation arc (shadow \u2192 benchmark \u2192 gate-review \u2192 default-on).",evidence:"e2e/benchmark element-state corpus (grounded PASS/FAIL vs ABSTAIN; Lio-s8 vision flake fixed; PENDING until the graduation slice) + the engine-core predicateBasisElementState unit suite + the (later) claim verify.element-state-grounding",owner:"steering (Alex)",review:"2026-08-15"}},VERIFY_CONFLICT_RECONCILE_SNAPSHOT:{key:"VERIFY_CONFLICT_RECONCILE_SNAPSHOT",envVars:["AGENTIQA_VERIFY_CONFLICT_RECONCILE_SNAPSHOT","AGENTIQA_EXPERIMENT_VERIFY_CONFLICT_RECONCILE_SNAPSHOT"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Verify-conflict DETERMINISTIC snapshot reconcile \u2014 M1 of the verify-oracle reconcile design (docs/plans/2026-07-28-verify-oracle-reconcile-design.md; GH #1907, root cause of #1813). WHAT IT DECIDES: at the SECOND verification-conflict escalation ONLY (the run_complete pass that would otherwise force-FAIL; the first bounce is untouched, and the bounce alone already resolves ~73% of oracle failures), the runtime takes ONE forced-full a11y snapshot for the whole run and re-checks each conflicted step's recorded wait literal (`_verifyOracleFailureDetails[step].literal`) against it with the SAME whole-token `pinPresentInPage` matcher, under the `waitLiteralHasSignificantTokens` significance floor. ZERO LLM calls \u2014 the model is not in the loop, so a lie about a genuinely broken wait fails constructively. THREE OUTCOMES: (a) literal significant AND present in the fresh snapshot \u2192 CLEARED: the oracle failure is removed from the ledger, the step's own already-passed criteria grades stand, the step passes, and the step is TAINTED via noteStepReopenedForReverify (the runtime consumed a re-observation it demanded, so predicate-basis-verify must not enforce over a basis it cannot attribute \u2014 the same invariant the bounce path carries); (b) literal significant and ABSENT from the fresh snapshot \u2192 GROUNDED NEGATIVE: the hard fail stands and the step is permanently INELIGIBLE for the VERIFY_CONFLICT_WITHHOLD warning band; (c) INAPPLICABLE \u2014 which is the only input VERIFY_CONFLICT_WITHHOLD (M2) ever sees \u2014 for no literal / insignificant literal / capture failed / budget already spent / the plan-derived `pin_page_grounding` oracle (excluded WHOLESALE \u2014 a plan-pin failure is a deterministic contradiction, not an absence of confirmation) / **any negation-or-absence marker on the step text or a criterion check** (`stepCarriesNegationOrAbsenceMarker`, reason `negation_marker`: a presence-clear is INVERTED for an assertion that the target is gone, and the absence lane's routing lexicon is narrow BY DESIGN, so this deliberately broader screen \u2014 not/no/none/without/missing/gone/empty/removed/deleted/closes/hidden/disappear*/vanish*/clear*/away/left-the-list/moves-to-trash/ceases-to/count-drops/zero-rows/fewer/struck-through/back-to-default/\u2026 plus a minimal DE/FR set, read over prose with only the target literal's OCCURRENCES removed (round-3: the earlier strip also deleted every \u22654-char TOKEN of the literal from the whole check, so a destination name like 'Deleted Items' silently disarmed the screen) \u2014 keeps every such step ineligible. HONEST LIMIT: this is a best-effort keyword screen over open-ended NL and completeness is unreachable; a phrasing it misses stays eligible and can be presence-cleared. The guarantee for absence assertions is the ABSENCE_AWARE_VERIFY lane, which decides with evidence rather than vocabulary) / **a literal that is not PLAN-GROUNDED whole-token with \u22652 tokens** (`waitLiteralGroundsSnapshotClear`, reason `literal_not_plan_grounded`: a CLEAR is only sound when the literal is the AUTHOR's expectation, read with the same `pinPresentInPage` matcher against the step text + criteria check/expectedValue \u2014 a model-invented literal that whole-token-matches unrelated chrome on another screen proves nothing, and a one-token literal is never distinctive enough even when the plan does contain it. KNOWN BOUNDARY, documented not fixed (round-3): this gate reads AUTHORSHIP, never PLACE \u2014 an AUTHORED literal that appears in unrelated chrome (a nav item, a help-sidebar link, the header of an Error 500 page) CLEARS exactly as it would in the region the step names; page-state and container binding are the next slice, and until then such clears are only visible in the `verify_conflict_reconcile:would_clear` census the graduation soak reads). Every inapplicable decision is logged per step as `verify_conflict_reconcile:inapplicable` with its reason. DETECTION ALWAYS RUNS (shadow): with the flag OFF the same decision is computed against the RETAINED full-snapshot corpus (`_fullSnapshotByStep`) \u2014 NO fresh capture, so OFF stays byte-identical including ZERO extra browser actions \u2014 and logged as `verify_conflict_reconcile:would_clear` / `:would_fail` with `basis:'retained_snapshot'` and `enforced:false`; ON logs the same events with `basis:'fresh_snapshot'` and `enforced:true`. Exactly ONE reconcile snapshot per run (guarded); a second escalation after the budget is spent decides INAPPLICABLE rather than re-enforcing off a stale page. NOT COVERED BY THIS FLAG (deferred slices, see the design \xA7 4): dropping `wait_for_element` from VERIFY_EVIDENCE_ACTIONS (own flag `VERIFY_EVIDENCE_REQUIRE_CAPTURE`, needs a shadow taint-rate measurement first), and still-running-vs-terminal recognition (a nested run still in flight at run end reads as a grounded negative and correctly stays red in this slice \u2014 the honest known gap). Runner lane only (RunnerRuntime run_complete).",designDoc:"docs/plans/2026-07-28-verify-oracle-reconcile-design.md",status:"active",added:"2026-07-28",notes:"M1 of the verify-oracle reconcile track (design docs/plans/2026-07-28-verify-oracle-reconcile-design.md), default OFF. Fixes the residual verify-conflict FALSE-FAIL that `canReconcileVerificationConflict` deliberately refuses: a PLAN-GROUNDED wait literal (repro run_5a6f0681 step 2, plan tp_vmlio_d1360115, literal 'QA Base \u2014 Draft Completion (do not modify)'; asess_1785247451027 step 1 is the same shape). That refusal is CORRECT as a static rule \u2014 a plan-grounded literal is a real must-pass presence oracle \u2014 so the fix is not to relax the rule but to consult the PAGE one more time: the element rendered late, and a fresh forced-full snapshot proves it. THREE FACTUAL CORRECTIONS to #1907 this design records (staging code, not the issue's reading): (a) evidence is NOT recorded on a timed-out oracle \u2014 the `_verifyEvidenceStepIndexes` add sits in the SUCCESS branch, so a timeout does not satisfy the evidence gate; (b) 'criteria are advisory' is too broad \u2014 `deriveStepStatusFromCriteria` does derive from grades; the real root is that the verify-conflict force-fail OVERRIDES already-passed criteria because `oracleConflictReason` never sees stepResults; (c) an existing valve (`canReconcileVerificationConflict`) already reconciles the non-plan-grounded case to a full PASS \u2014 the residual false-fail is exactly its plan-grounded refusal branch, not the whole mechanism. Read site: packages/engine-core/src/RunnerRuntime.ts (verifyConflictReconcileSnapshotEnabled \u2192 reconcileVerifyConflictsFromSnapshot, called from the second verification-conflict escalation in handleRunComplete). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.verifyConflictReconcile.test.ts (B-R1 repro form + B-C1..B-C9 counter-probes + the flag-OFF parity / one-snapshot-per-run invariants). killSwitch resolves the no-env value from this defaultState (#1729) \u2014 graduating means flipping defaultState to 'on' AND listing the key in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts.",graduation:{status:"gated",gate:"Graduation gates on the LIVE harness, not on units: Chin's ci-first-plan `tp_c0dcd706` run \xD710 against staging with the flag ON \u2014 baseline is 0/10, the target is \u22658/10 on the step-9 mode (post-login redirect), with the cleared-vs-would_fail split reported from the `verify_conflict_reconcile:*` diags. Steps 17\u201318 (split 2026-09-02: 17 = action, wait for the nested run to reach a terminal state by polling; 18 = verify, terminal state is Passed/Failed/Blocked) are NOT an expected-red slice: the pre-split step 17 was an async-terminal-state false-fail BY DESIGN, so with the plan fixed step 18 going green is the BENIGN expected outcome \u2014 it shows the plan now waits for the state it asserts, and it is NOT evidence that the significance threshold leaks (nor, on its own, evidence to graduate on). The leak-detector role that premise used to carry moves to the F4/M1 withhold census \u2014 the `verify_conflict_reconcile:would_clear` / `:would_fail` soak already required below, read alongside F4's `verify_conflict_url_grounded_withhold:would_withhold` \u2014 which sizes clears against genuinely-absent targets directly instead of inferring a leak from one plan step's colour. Plus: a staging shadow soak of `verify_conflict_reconcile:would_clear` / `:would_fail` sizing the cleared rate and confirming no would_clear fires on a genuinely-absent target, and the engine-core verifyConflictReconcile unit suite green with flag-OFF parity over the full runner corpus.",evidence:"staging `verify_conflict_reconcile:would_clear` / `:would_fail` diag events (cleared rate, basis, per-step literals) + the tp_c0dcd706 \xD710 harness pass rate per failure mode + packages/engine-core/src/__tests__/RunnerRuntime.verifyConflictReconcile.test.ts",owner:"steering (Alex)",review:"2026-08-15"}},VERIFY_CONFLICT_WITHHOLD:{key:"VERIFY_CONFLICT_WITHHOLD",envVars:["AGENTIQA_VERIFY_CONFLICT_WITHHOLD","AGENTIQA_EXPERIMENT_VERIFY_CONFLICT_WITHHOLD"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Verify-conflict RESIDUAL WARNING policy \u2014 M2 of the verify-oracle reconcile design (docs/plans/2026-07-28-verify-oracle-reconcile-design.md). Strictly downstream of VERIFY_CONFLICT_RECONCILE_SNAPSHOT (M1): it sees ONLY the conflicts M1 could not decide (no captured literal, an insignificant literal, a failed capture, or the reconcile budget already spent) and NEVER a step M1 read as a GROUNDED NEGATIVE \u2014 a target the runtime looked for and did not find always stays a hard FAIL. When ON, such a residual conflict is soft-withheld to a step-level WARNING ('could not be independently confirmed on this run') instead of force-FAILing the run. ALL SEVEN conditions must hold or the existing force-fail stands: (1) the unresolved oracle is a wait-style action, never the plan-derived `pin_page_grounding` oracle; (2) every graded criterion on the step passed with a non-empty substantiation note (the same bar `canReconcileVerificationConflict` uses) AND the step result is still 'passed' with no other deterministic negative on it (no reobserve-withhold, no upstream floor having already flipped it) ; (3) \u22651 STRICT criterion carries an engine-corroborated concrete value \u2014 its `observed` (or, absent that, its pinned `expectedValue`) is whole-token present in the retained page corpus via `pinPresentInPage`; (4) the wait literal's significant tokens are corroborated by the `observed`/note corpus of some passed criterion (this is what links 'what we waited for' to 'what was confirmed', and it is why a short/frequent literal like 'ok' or '3' can never withhold \u2014 the significance floor rejects it); (5) the step is NOT absence-intent (`checkTextAssertsAbsence` \u2014 that shape belongs to ABSENCE_AWARE_VERIFY, which owns its own confirm/abstain ladder) and, since that lexicon is narrow by design, ALSO carries no negation-or-absence marker at all under the broader M1 screen (`stepCarriesNegationOrAbsenceMarker`, reason `negation_marker`) \u2014 a warning there would withdraw a force-fail the engine has no evidence to withdraw; (6) \u2014 folded into (2) \u2014 no other deterministic negative on the step; (7) the step was OBSERVED AGAIN after the conflict bounce (a runtime capture-sequence watermark taken at the bounce; a verbatim resubmit of pre-bounce grades never withholds). DETECTION ALWAYS RUNS (shadow): `verify_conflict_withhold:would_withhold` (and a reasoned `:inapplicable`) is logged with `enforced:` reflecting the flag, so OFF is byte-identical with telemetry only. KNOWN, DELIBERATE SIDE EFFECTS when enforcing (asserted in CI, not accidents): withdrawing the forced conflict fail leaves `terminationReason='completed'`, which makes the run BILLABLE; and no 'Potential Issue Detected' card is synthesized for a withheld step \u2014 one fewer false issue. Runner lane only (RunnerRuntime run_complete).",designDoc:"docs/plans/2026-07-28-verify-oracle-reconcile-design.md",status:"active",added:"2026-07-28",notes:"M2 of the verify-oracle reconcile track, default OFF and inert unless M1's decision for the step is INAPPLICABLE. The honesty floor is the point: a withheld step is a WARNING \u2014 never a pass, never a fail \u2014 so this flag can only ever move a run out of a false RED into an honest AMBER, and it can never manufacture a green. The grounded-negative exclusion is the hard invariant (design \xA7 3): M1 having actually read the page and not found the target is exactly the evidence that the step is genuinely broken, so those never enter this path. Measured coverage of conditions (2)-(4) on the design's corpus is 32/33 steps; the one exception (staging ck-cli_test 2026-07-28 step 17, a genuinely failed criterion) correctly stays red. Read site: packages/engine-core/src/RunnerRuntime.ts (verifyConflictWithholdEnabled \u2192 classifyVerifyConflictWithhold, applied at the second verification-conflict escalation after the M1 pass). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.verifyConflictReconcile.test.ts (B-C2/B-C3/B-C5/B-C6 keep the reds red; B-C8 asserts the billing invariant deliberately). killSwitch resolves the no-env value from this defaultState (#1729) \u2014 graduating means flipping defaultState to 'on' AND listing the key in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts.",graduation:{status:"gated",gate:"Graduation gates on M1 graduating FIRST (M2 is only meaningful over M1's inapplicable residue), then on a staging shadow soak of `verify_conflict_withhold:would_withhold` showing (a) no would_withhold on a step any deterministic oracle reads as a negative, (b) the withheld population is dominated by genuinely unconfirmable captures rather than real failures \u2014 sampled and hand-adjudicated \u2014 and (c) an explicit product decision on the billing consequence (withdrawing the forced fail makes the run terminate 'completed' and therefore BILLABLE) plus the warning-display semantics review (a withheld step must read as a neutral 'could not confirm', not as an alarm-amber defect).",evidence:"staging `verify_conflict_withhold:would_withhold` / `:inapplicable` diag events (rate + hand-adjudicated sample of the withheld population) + the tp_c0dcd706 harness residue after M1 + packages/engine-core/src/__tests__/RunnerRuntime.verifyConflictReconcile.test.ts",owner:"steering (Alex)",review:"2026-08-15"}},RUN_COMPLETENESS_ALL_STATUSES:{key:"RUN_COMPLETENESS_ALL_STATUSES",envVars:["AGENTIQA_RUN_COMPLETENESS_ALL_STATUSES","AGENTIQA_EXPERIMENT_RUN_COMPLETENESS_ALL_STATUSES"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Run-completeness guard for NON-passed claims. The guard (cascade stage 15: bounce-once, then fail-closed with placeholder rows and a summary prefix) was gated on `status === 'passed'`, so a `run_complete{status:'failed'}` bypassed it ENTIRELY \u2014 the model could declare a run failed part-way and every unexecuted step was silently absent from the verdict. When ON the guard evaluates on EVERY claimed terminal status, and the fail-closed branch additionally discloses the FULL unexecuted-step set (every plan step with no reported result, not just the `required` verify subset the bounce keys on) as `skipped`+`synthesized` rows plus a coverage clause in the run summary. It cannot launder a failure into a pass (the only status write in this branch is \u2192 `failed`) and it cannot invent a fail on a complete run (it is unreachable when every required step is reported). OFF restores the byte-identical pre-change behavior: guard on `passed` only, no coverage clause, no unexecuted rows.",designDoc:"docs/plans/2026-08-04-run-completeness-guard-all-statuses-design.md",status:"active",added:"2026-08-04",notes:"Ships default-ON from inception \u2014 there is no pre-change OFF behavior worth preserving: the gated-on-`passed` condition is a pure false-signal + coverage-loss defect measured at prod parity on the lio mirror (2026-08-03, 46 runs, https://s.assets.agentiqa.com/lio-prod-parity-2026-08-03/SCORE.md finding 1). Repro run `run_04a6292b-f968-47e5-a1ee-4013f26afce1` (plan tp_2d051c76 `workflow-approval`, session asess_1785788803738_bf4wmgv2) booked `failed` with 10 of 29 steps in step_results, 7/7 criteria passed / 0 failed / 0 never-graded, and the client-visible summary was the model's own mid-run narration \"Test run in progress. Step 10 verified.\" The same gate is live on the prod build, so lio's nightly CLI batch can hit it. Direction is strictly information-adding: the run status can only move `passed`\u2192`failed` (unchanged from the pre-change branch) and the new rows are `skipped` \u2014 never a pass, never a fail, the same trust-layer rule as `synthesizePartialStepResults`. Read site: packages/engine-core/src/RunnerRuntime.ts (`runCompletenessAllStatusesEnabled` \u2192 the stage-15 guard in handleRunComplete; `getUnexecutedStepIndexes` computes the disclosed set). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.runCompletenessAllStatuses.test.ts (fixture built from the measured run). killSwitch resolves the no-env value from this defaultState (#1729) \u2014 this entry is listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts so the no-delta proof acknowledges the default-ON state deliberately; an explicit AGENTIQA_RUN_COMPLETENESS_ALL_STATUSES=0 is the emergency kill switch that restores the bypass."},ZERO_GRADE_VERIFY_BOUNCE:{key:"ZERO_GRADE_VERIFY_BOUNCE",envVars:["AGENTIQA_ZERO_GRADE_VERIFY_BOUNCE","AGENTIQA_EXPERIMENT_ZERO_GRADE_VERIFY_BOUNCE"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Zero-grade verify bounce (cascade stage 12c). The whole-step zero-grade floor (`run_complete:verify_step_unsubstantiated`) refuses a verify step reported `passed` with NONE of its plan-printed criteria graded \u2014 correctly \u2014 but until this flag it did so SILENTLY, deriving `failed` (strict) / `warning` (non-strict) with no chance to correct, the only sibling gate without the one-bounce self-correction contract. When ON the first offending `run_complete` is rejected once (`run_complete:verify_step_unsubstantiated_rejected`, response status `ungraded_verify_criteria`) naming each offending step and its full printed criteria range 1..N; a repeat offense takes the floor's derivation exactly as before, with a de-jargonized client note recording that a resubmission was already asked for. The trigger keys on the POST-synthetics `unsubstantiatedVerifySteps` collection, so a step the FIX 3 cross-step rescue or Amendment 5a fail-closed synthesis already adjudicated never bounces. The bounce writes no step or run status and mutates nothing. OFF restores the byte-identical pre-change behavior: immediate silent derivation with the original note/summary text.",designDoc:"docs/plans/2026-08-06-lio-terminal-verdicts-and-zero-grade-bounce-design.md",status:"active",added:"2026-08-06",notes:"Ships default-ON from inception (precedent: RUN_COMPLETENESS_ALL_STATUSES \u2014 the bounce writes no status, so there is no verdict-polarity change to soak; `=0` restores today's silent derivation byte-identically). Measured trigger: Lio's first post-#2238 prod nightly (2026-08-06, 15 runs, proj_b6424373, gemini-3-flash-preview) \u2014 3/15 runs whose substance passed were reported FAILED because the model submitted step 1 `passed` with `criteriaResults: []` and the floor derived `failed` from the strict criterion, vs 0/30 on the 08-04/08-05 pre-#2238 baseline; the #2238 exclusion counters (unauthored/step-mismatch/out-of-range) were all zero, i.e. the typed reference did not drop the grades \u2014 the model stopped emitting them. Bounce doctrine (one bounce per class per run, then fail-closed, NEVER a rescue/rewrite) is the `_unauthoredGradeRunCompleteRejections` doctrine; worst-case chain incomplete \u2192 zero-grade \u2192 over-grading \u2192 conflict is ~4 extra run_complete turns, the same accepted profile as the pre-existing bounces. Read site: packages/engine-core/src/RunnerRuntime.ts (`zeroGradeVerifyBounceEnabled` \u2192 the stage-12c bounce in handleRunComplete; the counter is `_zeroGradeVerifyRejections`). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.zeroGradeVerifyBounce.test.ts. killSwitch resolves the no-env value from this defaultState (#1729) \u2014 this entry is listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts so the no-delta proof acknowledges the default-ON state deliberately; an explicit AGENTIQA_ZERO_GRADE_VERIFY_BOUNCE=0 is the emergency kill switch. Known residual carried, not fixed (AG-8153): the floor and this bounce cover `type === 'verify'` steps only \u2014 zero-graded strict criteria on action/setup steps remain a false-PASS class."},PERSIST_PROFILE_SERVER_RESOLVE:{key:"PERSIST_PROFILE_SERVER_RESOLVE",envVars:["AGENTIQA_PERSIST_PROFILE_SERVER_RESOLVE","AGENTIQA_EXPERIMENT_PERSIST_PROFILE_SERVER_RESOLVE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Server-side resolution of the per-project browser-profile gate at engine session create: when the request body OMITS `persistBrowserProfile` and the session names a projectId, the engine reads `persist_browser_profile` off the project (web-next `/api/sync/entities/projects`, admin service key) instead of leaving the session's gate unset. An explicit body value always wins and is never re-read. '0' restores the pre-2026-09-02 behavior: an omitted field means the profile is neither seeded nor captured, whatever the project says.",designDoc:"apps/execution-engine/src/services/projectPersistProfile.ts",status:"active",added:"2026-09-02",notes:"Ships default-ON from inception: the OFF state is not a safer prior behavior, it is the defect. The project column is NOT NULL DEFAULT true and the product treats the gate as per-project state, but the flag only ever reached the engine when a CLIENT chose to send it \u2014 the SPA does, the CLI did not \u2014 so a project with persist_browser_profile=true ran its CLI cloud runs with persistent profiles silently off (Lio nightly login-gate, 2026-09-02). Direction is one-way and bounded: it can only fill in a field the request LEFT ABSENT, never override a client's explicit true/false, and a failed/timed-out lookup leaves the field absent exactly as today (fail-open, never fatal to session create). Diagnostic: `persist_profile:resolved_from_project {projectId, value}` on a fill-in, `persist_profile:project_resolve_failed` on a miss. Read site: apps/execution-engine/src/services/projectPersistProfile.ts (`persistProfileServerResolveEnabled`), consumed by the POST /api/engine/session handler in apps/execution-engine/src/server.ts. Acceptance tests: apps/execution-engine/__tests__/projectPersistProfile.test.ts."},OFF_STEP_BLOCK_DEFLECT:{key:"OFF_STEP_BLOCK_DEFLECT",envVars:["AGENTIQA_OFF_STEP_BLOCK_DEFLECT","AGENTIQA_EXPERIMENT_OFF_STEP_BLOCK_DEFLECT"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Off-step block deflection (#3481, RunnerRuntime / test-plan runs only \u2014 never ExplorerRuntime/chat). Motivating incident: design-partner customer report (2026-10-13, fingerprint 7e17784a5645) \u2014 the run was on a later step whose own exit condition was already met, the model re-anchored on a button that an earlier, already-passed step had used (hidden by design once that step's action happened), filed a HIGH report_issue, and called exploration_blocked declaring the earlier step's index while the runtime pointer was on the current step, booking the run blocked / blocked_by_issue with later steps synthesized skipped. When on, an exploration_blocked whose declared positive integer args.stepIndex is strictly below _currentStepIndex (Number(args.stepIndex) < _currentStepIndex) and names an already-past, completed step (<= _maxInRangeStepEverActivated and completed) is bounced once per step with an instruction quoting the current step's own instruction and goal and explaining that a control used by a completed earlier step may be hidden by app state, so the agent should continue with the current step or call run_complete if its exit condition is met. A second block on the same step passes straight through (bounded once per step, bounding the edge case where the pointer ran ahead of the model). Writes NO verdict of its own. '0' disables the deflection and restores immediate termination on the first call.",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-10-14",notes:"Ships default-ON as a 0-disables kill switch (following the VERIFY_STEP_BLOCK_DEFLECT precedent) \u2014 a strict reduction of a confirmed false-block class where the model blocks on an already-completed earlier step's hidden control, and the gate writes no verdict of its own. Diagnostic: `off_step_block_deflected {stepIndex, currentStepIndex, declaredStepIndex, maxInRangeStepEverActivated, deflectionsIssued}`. Read site: packages/engine-core/src/RunnerRuntime.ts (`maybeDeflectOffStepBlock`, called from the `exploration_blocked` arm of `dispatchToolCall`). Pure module: packages/engine-core/src/runner/offStepBlockDeflection.ts. Unit suite: packages/engine-core/src/__tests__/RunnerRuntime.offStepBlockDeflection.test.ts."}},YRe=Object.values(Fy);var cY=["run","run the test","run the test plan","run test plan"];function fk(t){let e=t.toLowerCase().trim();return cY.includes(e)}function mk(t){return t.replace(/\s+/g," ").trim()}var Nc="plan_run_active";var Ca="PROJECT_RUN_IN_PROGRESS";function jy(t){return`This project already has a ${t.kind==="batch"?"batch":"run"} in progress, started by ${t.startedByLabel} at ${t.startedAt}. Only one run or batch can be active per project at a time \u2014 wait for it to finish, or stop it first.`}function vi(t,e){if(t!==409)return null;let n;try{n=JSON.parse(e)}catch{return null}return!n||n.code!==Ca?null:{error:typeof n.error=="string"?n.error:"",code:Ca,runId:typeof n.runId=="string"?n.runId:"",kind:n.kind==="batch"?"batch":"run",startedAt:typeof n.startedAt=="string"?n.startedAt:"",startedByLabel:typeof n.startedByLabel=="string"?n.startedByLabel:""}}var Zs=class extends Error{reason=Ca;holderRunId;holderKind;startedAt;startedByLabel;body;constructor(e){super(e.error.trim()||jy({kind:e.kind,startedByLabel:e.startedByLabel,startedAt:e.startedAt})),this.name="ProjectRunInProgressError",this.holderRunId=e.runId,this.holderKind=e.kind,this.startedAt=e.startedAt,this.startedByLabel=e.startedByLabel,this.body=e}};function wp(t){if(t instanceof Zs)return t;if(!t||typeof t!="object")return null;let e=t;if(e.reason===Ca){let n=e.body;if(n&&n.code===Ca)return new Zs(n)}return e.code!==Ca?null:new Zs({error:typeof e.message=="string"?e.message:"",code:Ca,runId:"",kind:"run",startedAt:"",startedByLabel:""})}function Na(t){let e=t.indexOf(":");return e===-1?{provider:"google",modelName:t}:{provider:t.slice(0,e),modelName:t.slice(e+1)}}var Mr="google:gemini-3-flash-preview",gk="google:gemini-3-flash-preview",ea=["credential_identifier","user_supplied_payload","generated_test_value","other"];var Ep=[{name:"Green",hex:"#4ade80"},{name:"Blue",hex:"#60a5fa"},{name:"Purple",hex:"#a78bfa"},{name:"Amber",hex:"#fbbf24"},{name:"Red",hex:"#f87171"},{name:"Cyan",hex:"#22d3ee"},{name:"Pink",hex:"#f472b6"},{name:"Slate",hex:"#94a3b8"}];function fe(t){return`${t}_${crypto.randomUUID()}`}function Do(t,e){return typeof process>"u"||!process.env?!1:process.env[`AGENTIQA_${t}`]===e||process.env[`AGENTIQA_EXPERIMENT_${t}`]===e}function yk(t){return Fy[t]?.defaultState}function ve(t){return Do(t,"0")?!0:Do(t,"1")?!1:yk(t)==="off"}function Re(t){return Do(t,"1")?!0:Do(t,"0")?!1:yk(t)==="on"}function Vt(t){return Do(t,"0")}function $y(t){return Do(t,"1")}function Oc(){return Re("VALUE_GROUNDING_ENFORCE")}function By(t){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`}function bk(t){switch(t){case"message":return"msg";case"tool_call":return"tool";case"llm_usage":return"llm";case"supervisor_verdict":return"sv";case"agent_lifecycle":return"lc";case"user_action":return"ua";case"session_start":case"session_end":case"turn_start":case"turn_end":return"sl";case"log":case"pageSnapshot.structuralStrip":return"diag";default:return"evt"}}var Mo=class{apiUrl;fetchFn;sessions=new Map;queues=new Map;timer=null;isUploading=!1;inFlight=new Set;eventIds=new WeakMap;directUploadFailedSessions=new Set;BATCH_SIZE=25;FLUSH_INTERVAL=6e4;MAX_PAYLOAD_BYTES=35e5;auth;constructor(e,n,r=globalThis.fetch.bind(globalThis)){this.apiUrl=e,this.fetchFn=r,this.auth=typeof n=="string"?{kind:"bearer",token:n}:n,this.timer=setInterval(()=>this.flushAll(),this.FLUSH_INTERVAL)}buildAuthHeaders(e,n=!1){let r={"Content-Type":"application/json"};if(this.auth.kind==="bearer")return r.Authorization=`Bearer ${this.auth.token}`,r;if(n&&this.auth.bearerFallback)return r.Authorization=`Bearer ${this.auth.bearerFallback}`,r;r["x-admin-service-key"]=this.auth.serviceKey;let s=e?.userId??this.auth.fallbackUserId;return s&&(r["x-user-id"]=s),r}hasBearerFallback(){return this.auth.kind==="service"&&!!this.auth.bearerFallback}emit(e){let n=e.sessionId;if(this.eventIds.has(e)||this.eventIds.set(e,By(bk(e.kind))),e.kind==="session_start"&&e.sessionMeta&&this.sessions.set(n,{...e.sessionMeta,desktopSessionId:n,status:"active",startedAt:new Date(e.ts).toISOString()}),e.kind==="session_end"){let s=this.sessions.get(n);s&&(s.status=e.status??"completed",s.endedAt=new Date(e.ts).toISOString())}if(this.isProviderLocationUnsupportedEvent(e)){let s=this.sessions.get(n);s&&(s.terminalErrorClass="provider_location_unsupported")}!n&&!this.sessions.has("")&&this.sessions.set("",{desktopSessionId:"global",projectId:"_global",status:"active",startedAt:new Date(e.ts).toISOString()});let r=this.queues.get(n);r||(r=[],this.queues.set(n,r)),r.push(e),r.length>=this.BATCH_SIZE&&this.flushSession(n),e.kind==="session_end"&&this.flushSession(n)}async flush(){await this.flushAll()}destroy(){this.timer&&(clearInterval(this.timer),this.timer=null),this.flushAll()}async flushAll(){let e=[];for(let r of this.queues.keys()){let s=this.flushSession(r);s&&e.push(s)}let n=Array.from(this.inFlight);await Promise.allSettled([...e,...n])}flushSession(e){let n=this.sessions.get(e),r=this.queues.get(e);if(!n||!r||r.length===0)return null;let s=r.splice(0),a=this.uploadWithRetry(n,s).catch(i=>{let o=s.filter(l=>l.kind==="session_end");if(o.length>0){let l=this.queues.get(n.desktopSessionId);l&&l.unshift(...o)}console.error(`[RemoteAnalyticsSink] Failed to upload ${s.length} events:`,i.message)});return this.inFlight.add(a),a.finally(()=>{this.inFlight.delete(a)}),a}async uploadWithRetry(e,n,r=3){let s;for(let a=1;a<=r;a++)try{await this.upload(e,n);return}catch(i){s=i;let o=i?.status;if(o!==void 0&&o>=400&&o<500)throw i;if(a<r){let l=Math.min(1e3*Math.pow(3,a-1),9e3);await new Promise(c=>setTimeout(c,l))}}throw s}async upload(e,n){let r=await this.mapEvents(e.desktopSessionId,n);await this.postIngest(e,r)}async postIngest(e,n,r=!1){if(n.length===0)return;let s=JSON.stringify({session:{...e},events:n});if(s.length>this.MAX_PAYLOAD_BYTES&&n.length>1){let l=Math.floor(n.length/2);await this.postIngest(e,n.slice(0,l),r),await this.postIngest(e,n.slice(l),r);return}let a;try{a=await this.fetchFn(`${this.apiUrl}/api/analytics/ingest`,{method:"POST",headers:this.buildAuthHeaders(e,r),body:s})}catch(l){throw new Error(`analytics upload network error: ${l?.message??String(l)}`)}if(a.ok)return;if((a.status===401||a.status===403)&&!r&&this.hasBearerFallback()){console.warn(`[RemoteAnalyticsSink] service-key auth got ${a.status} for session ${e.desktopSessionId} \u2014 retrying with bearer fallback (AG-169)`),await this.postIngest(e,n,!0);return}if(a.status===413){if(n.length>1){let l=Math.floor(n.length/2);await this.postIngest(e,n.slice(0,l),r),await this.postIngest(e,n.slice(l),r);return}console.warn(`[RemoteAnalyticsSink] Dropping single oversized event (${Math.round(s.length/1024)} KB)`);return}let i=await a.text().catch(()=>`HTTP ${a.status}`);if(i.includes("FUNCTION_PAYLOAD_TOO_LARGE")||i.includes("Request Entity Too Large")){if(n.length>1){let l=Math.floor(n.length/2);await this.postIngest(e,n.slice(0,l),r),await this.postIngest(e,n.slice(l),r);return}console.warn(`[RemoteAnalyticsSink] Dropping single oversized event (${Math.round(s.length/1024)} KB)`);return}(a.status===401||a.status===403)&&console.error(`[RemoteAnalyticsSink] auth_failed status=${a.status} authKind=${this.auth.kind} sessionId=${e.desktopSessionId} userId=${e.userId??"(unset)"} body=${i.slice(0,200)}`);let o=new Error(i);throw o.status=a.status,o}async mapEvents(e,n){let r=[];for(let s of n){let a={timestamp:new Date(s.ts).toISOString(),childId:No(s.childId)},i=this.eventIds.get(s)??By(bk(s.kind));switch(s.kind){case"message":r.push({...a,id:i,eventType:"message",role:s.role,messageText:s.text,toolName:s.actionName,toolArgs:s.actionArgs,url:s.url});break;case"tool_call":{let o;s.screenshotBase64&&(o=await this.uploadScreenshot(e,s.screenshotBase64)),r.push({...a,id:i,eventType:"tool_call",toolName:s.toolName,toolArgs:s.args,toolResult:s.result,screenshotUrl:o,url:s.url,stepIndex:s.stepIndex,actionMetadata:{durationMs:s.durationMs,tokenCount:s.tokenCount}});break}case"llm_usage":r.push({...a,id:i,eventType:"llm_usage",toolName:s.model,promptTokens:s.promptTokens,completionTokens:s.completionTokens,totalTokens:s.totalTokens,runId:s.runId,callKind:s.callKind,keySource:s.keySource,llmProvider:s.llmProvider,billedUnits:s.billedUnits,actionMetadata:{durationMs:s.durationMs,finishReason:s.finishReason,tokenCount:s.tokenCount,messageCount:s.messageCount,systemPromptHash:s.systemPromptHash,lastToolResults:s.lastToolResults,chosenActions:s.chosenActions,textResponse:s.textResponse,cachedInputTokens:s.cachedInputTokens,planStepIndex:s.planStepIndex,planStepType:s.planStepType}});break;case"supervisor_verdict":r.push({...a,id:i,eventType:"supervisor_verdict",actionType:s.verdict,actionMetadata:{verdict:s.verdict,message:s.message,iteration:s.iteration,actionLogSize:s.actionLogSize,stepText:s.stepText,progress:s.progress??null,differential:s.differential??null}});break;case"agent_lifecycle":r.push({...a,id:i,eventType:"agent_lifecycle",actionType:s.event,actionMetadata:{event:s.event,iteration:s.iteration,details:s.details}});break;case"user_action":r.push({...a,id:i,eventType:"user_action",actionType:s.action,actionTargetId:s.targetId,actionMetadata:s.metadata});break;case"session_start":case"session_end":case"turn_start":case"turn_end":r.push({...a,id:i,eventType:"user_action",actionType:s.kind,actionTargetId:s.sessionId,actionMetadata:s.sessionMeta?{...s.sessionMeta}:{status:s.status,...s.kind==="session_end"&&s.endKind?{endKind:s.endKind}:{}}});break;case"log":r.push({...a,id:i,eventType:"diagnostic",actionType:s.level,actionMetadata:{source:s.source,msg:s.msg,...s.data}});break;case"pageSnapshot.structuralStrip":r.push({...a,id:i,eventType:"diagnostic",actionType:s.kind,actionMetadata:{originalLen:s.originalLen,finalLen:s.finalLen,droppedNodeCount:s.droppedNodeCount,foldedRunCount:s.foldedRunCount,capHit:s.capHit}});break;default:{console.warn(`[RemoteAnalyticsSink] dropping unmapped DiagnosticEvent kind=${s.kind}`);break}}}return r}isProviderLocationUnsupportedEvent(e){return e.kind!=="log"?!1:fs([e.msg,e.source,typeof e.data=="object"&&e.data?JSON.stringify(e.data):""].join(`
51
+ `),p=Lo(f,a),m=Lo(h,a),g=!1;for(let v of n){let w=qn(v);u.has(w)||p.has(w)?(i.add(w),g=!0):m.has(w)&&(o.add(w),g=!0)}if(!g&&u.size===0)for(let v of n)o.add(qn(v))}let l=n.filter(d=>i.has(qn(d))),c=n.filter(d=>!i.has(qn(d))&&o.has(qn(d)));return{required:l,optional:c}}function pk(t,e,n){let r=Qs([...e,...n],[]),s=oY(t,r);return{required:Qs(s.required,n),optional:Qs(s.optional,n)}}function lY(t){let e=t;return!e||e.code!==Hn?null:Array.isArray(e.names)?e.names.filter(n=>typeof n=="string"):[]}async function hk(t){let e=t.scope?.required??[],n=t.scope?.optional??[],r=t.maxAttempts??3,s=new Set(n.map(l=>qn(l))),a=new Set(e.map(l=>qn(l))),i=l=>{let c=new Set(l.map(d=>qn(d.name)));return n.filter(d=>!c.has(qn(d)))},o=Qs([...e,...n],[]);for(let l=0;l<r;l++){if(o.length===0)return e.length>0?{ok:!1,refused:[...e]}:{ok:!0,granted:[],skipped:[...n]};try{let c=o.filter(u=>s.has(qn(u))&&!a.has(qn(u))),d=await t.lease(o,c);return{ok:!0,granted:d,skipped:i(d)}}catch(c){let d=lY(c);if(d===null)throw c;let u=new Set(d.map(p=>qn(p))),f=e.filter(p=>u.has(qn(p)));if(f.length>0)return{ok:!1,refused:f};let h=o.filter(p=>!u.has(qn(p)));if(h.length===o.length)return e.length>0?{ok:!1,refused:[...e]}:{ok:!0,granted:[],skipped:[...n]};o=h}}return e.length>0?{ok:!1,refused:[...e]}:{ok:!0,granted:[],skipped:[...n]}}var Fy={OBSERVATION_ARCHIVE:{key:"OBSERVATION_ARCHIVE",envVars:["AGENTIQA_OBSERVATION_ARCHIVE","AGENTIQA_EXPERIMENT_OBSERVATION_ARCHIVE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Enables opt-in private observation and provider-attempt custody for trusted rehearsal sessions; capture failure marks the archive incomplete without changing the run verdict.",designDoc:"docs/plans/2026-10-06-rehearsal-observation-reliability-design.md",status:"active",added:"2026-10-06",notes:"Requires separately configured private storage and trusted campaign scope. The flag alone grants no archive access.",graduation:{status:"permanent",gate:"Remain opt-in for approved observation-custody campaigns; no general capture graduation.",evidence:"Private manifest/GET/hash verification and post-engine-exit retrieval on the deployed candidate.",owner:"steering (Alex)",review:"2026-10-08"}},RUNNER_OBSERVATION_LEDGER:{key:"RUNNER_OBSERVATION_LEDGER",envVars:["AGENTIQA_RUNNER_OBSERVATION_LEDGER","AGENTIQA_EXPERIMENT_RUNNER_OBSERVATION_LEDGER"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Retains bounded historical observations in normal and concluding runner grading inputs and enables evidence-bound authored-contract reconciliation under the existing once-per-run block allowance.",designDoc:"docs/plans/2026-10-06-rehearsal-observation-reliability-design.md",status:"active",added:"2026-10-06",graduation:{status:"gated",gate:"Negative-preserving synthetic controls and complete 21 focused plus 25 corpus rehearsal on the final candidate, followed by approved release.",evidence:"Actual normal/concluding provider inputs, authored action history and independent criterion adjudication.",owner:"steering (Alex)",review:"2026-10-08"}},NATIVE_INTERACTION_BROKER:{key:"NATIVE_INTERACTION_BROKER",envVars:["AGENTIQA_NATIVE_INTERACTION_BROKER","AGENTIQA_EXPERIMENT_NATIVE_INTERACTION_BROKER"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Uses acknowledged Chromium native-interception readiness and page-owned late events across guarded click paths, with exact upload targeting and independent dispatch receipts.",designDoc:"docs/plans/2026-10-06-rehearsal-observation-reliability-design.md",status:"active",added:"2026-10-06",notes:"Session-latched; initial enablement is hosted Chromium. No universal write-admission gate or causal attribution from event arrival order.",graduation:{status:"gated",gate:"Real-browser readiness/late-event/target-identity controls and the complete final-candidate hosted rehearsal.",evidence:"Exact dispatch counts, native-event identities, file-count checks and retained scope/actionability regressions.",owner:"steering (Alex)",review:"2026-10-08"}},RUNTIME_AGENT_EVAL:{key:"RUNTIME_AGENT_EVAL",envVars:["AGENTIQA_RUNTIME_AGENT_EVAL","AGENTIQA_EXPERIMENT_RUNTIME_AGENT_EVAL"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"When enabled, activates the autonomous multi-signal agent runtime evaluation harness (git diff + telemetry + customer findings correlation, fault injection, and zero-browser invariant verification).",designDoc:"docs/plans/2026-09-29-runtime-agent-eval-signals-design.md",status:"active",added:"2026-09-29",notes:"Experimental staging rollout for autonomous agent evaluation from git, telemetry, and issue signals.",graduation:{status:"gated",gate:"Graduates to default-on after staging soak validates zero false-FAILs against real customer agent scenarios.",evidence:"packages/engine-core/src/__tests__/agent-eval.test.ts passes cleanly",owner:"steering (Radik)",review:"2026-10-15"}},GROUNDED_EXPECTATIONS:{key:"GROUNDED_EXPECTATIONS",envVars:["AGENTIQA_GROUNDED_EXPECTATIONS","AGENTIQA_EXPERIMENT_GROUNDED_EXPECTATIONS"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"When on, run_complete batch-judges provisionally-passing steps for expectation drift (heal cosmetic, fail-closed critical/judge-unavailable) instead of trusting the model grade verbatim.",designDoc:"docs/plans/2026-07-07-grounded-expectations-phase-b-design.md",status:"active",added:"2026-07-07",notes:"GRADUATED 2026-07-21 (default on): prod ran env-ON (AGENTIQA_EXPERIMENT_GROUNDED_EXPECTATIONS=1 on the orchestrator) since 2026-07-10 with zero grounded-attributable false-FAIL; nightly regressions in the window were all infra/render flake. This flip normalizes code to the already-live prod behavior \u2014 remove the orchestrator env vars (both namespaces) once this reaches each environment."},LOOP_VISION_ESCALATION_CHAT:{key:"LOOP_VISION_ESCALATION_CHAT",envVars:["AGENTIQA_LOOP_VISION_ESCALATION_CHAT","AGENTIQA_EXPERIMENT_LOOP_VISION_ESCALATION_CHAT"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Extends loop-detection vision-supervisor escalation to the Coordinator/chat lane; off disables it for chat only (the master LOOP_VISION_ESCALATION still governs the Runner/Explorer lanes).",designDoc:"docs/plans/2026-07-07-loop-detection-vision-supervisor-design.md",status:"active",added:"2026-07-07",notes:"Staged default-OFF originally, flipped default-ON once baked (AG-6995) \u2014 reads via killSwitchDisabled today."},PIN_PAGE_GROUNDING:{key:"PIN_PAGE_GROUNDING",envVars:["AGENTIQA_PIN_PAGE_GROUNDING","AGENTIQA_EXPERIMENT_PIN_PAGE_GROUNDING"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Runner-lane pin page-grounding: a criterion's pinned expectedValue must appear in the literal full-page a11y text at verify-evidence capture (parroted grade notes no longer suffice). Detection runs in shadow (telemetry) unless VALUE_GROUNDING_ENFORCE is on; '0' disables detection AND the forced full-snapshot capture entirely.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-10"},ISSUE_QUOTE_GROUNDING:{key:"ISSUE_QUOTE_GROUNDING",envVars:["AGENTIQA_ISSUE_QUOTE_GROUNDING","AGENTIQA_EXPERIMENT_ISSUE_QUOTE_GROUNDING"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Chat-lane hallucinated-quote gate on report_issue: a quoted literal asserted as visible must appear in the freshly captured full a11y snapshot. Detection runs in shadow (telemetry) unless VALUE_GROUNDING_ENFORCE is on; '0' disables detection AND the forced full-snapshot capture entirely.",designDoc:"packages/engine-core/src/negativeStateEvidence.ts",status:"active",added:"2026-07-10"},VALUE_GROUNDING_ENFORCE:{key:"VALUE_GROUNDING_ENFORCE",envVars:["AGENTIQA_VALUE_GROUNDING_ENFORCE","AGENTIQA_EXPERIMENT_VALUE_GROUNDING_ENFORCE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Flips both value-grounding gates (PIN_PAGE_GROUNDING, ISSUE_QUOTE_GROUNDING) from shadow telemetry (would_fail / would_bounce diag logs) to enforcement: pin absence records a real per-step oracle failure that fails run_complete; a hallucinated visible quote rejects the report_issue filing.",designDoc:"packages/engine-core/src/killSwitch.ts",status:"active",added:"2026-07-10",notes:"Shadow-first rollout: two adversarial review rounds each surfaced false-fail classes on a hard-fail gate, so enforcement waited on a staging soak. PARKED 2026-07-21 (steering decision, Alex): VERIFY_REOBSERVE_WITHHOLD (graduated default-ON) strictly dominates this hard-fail path \u2014 it closes the same blind-verdict/pin-absence hole with a soft-withhold that never manufactures the false-FAIL classes both review rounds surfaced. Never graduate the enforcement; the oracle_failure recording branch is a deletion candidate. Detection stays: PIN_PAGE_GROUNDING + ISSUE_QUOTE_GROUNDING (default-ON) keep their shadow would_fail/would_bounce telemetry. Remove the staging orchestrator env var (AGENTIQA_EXPERIMENT_VALUE_GROUNDING_ENFORCE) \u2014 the force-ON soak is moot.",graduation:{status:"parked",gate:"Retired in favor of VERIFY_REOBSERVE_WITHHOLD (soft-withhold successor). Do not graduate; delete the enforcement branch once the successor has a clean prod window.",evidence:"F2b eval proof: enforce cannot catch the abstain{snapshot_incremental} class (identical RED both legs); panel + review-round history of false-fail classes on the hard-fail path",owner:"steering (Alex)",review:"2026-08-15"}},RESUME_INPUT_ASK_USER:{key:"RESUME_INPUT_ASK_USER",envVars:["AGENTIQA_RESUME_INPUT_ASK_USER","AGENTIQA_EXPERIMENT_RESUME_INPUT_ASK_USER"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Coordinator turn-continuity: when on, a child Explorer's ask_user that blocks on a MISSING INPUT (a file to upload, a path/value \u2014 not an email/generic async wait) arms a resumable `input_wait` pause that persists the halted child's OBJECTIVE; the next user turn resumes that SAME objective (re-attaching session attachments) instead of falling through to free re-decomposition, and re-asks for the SAME objective when the required file is still absent. Off = byte-identical to today (missing-input ask_user arms no pause; the email-wait / generic-wait paths are unchanged).",designDoc:"docs/plans/2026-07-20-paused-task-resume-design.md",status:"active",added:"2026-07-20",notes:"GRADUATED 2026-07-21 (default on) via the bound-eval arm of its gate: chat/paused-task-resume green 2/2 replicates on origin/staging (pause armed on the missing-file ask_user; resume carried the SAME objective tokens (upload + filename) with an explicit no-re-plan prompt; text-only reply correctly re-asked for the same file) + the earlier recorded GRADUATION-PASS on the lio replay (asess_1784568697674). Organic staging soak was vacuous (organic chat never hits a missing-input upload ask_user \u2014 0 input_wait events in 12 sessions), so the eval arm is the gate per the evidence-count doctrine. Renderer attachment-drop discriminator stays a SEPARATE open item (PostHog coordinator_started.has_attachments). Remove the staging orchestrator env var once this reaches staging."},VERIFY_REOBSERVE_WITHHOLD:{key:"VERIFY_REOBSERVE_WITHHOLD",envVars:["AGENTIQA_VERIFY_REOBSERVE_WITHHOLD","AGENTIQA_EXPERIMENT_VERIFY_REOBSERVE_WITHHOLD"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Runner-lane grade-time re-observe + soft-withhold (batch.verify-never-blind fix). When a pinned STRICT verify criterion cannot be grounded at grade time \u2014 the snapshot is not full (abstain snapshot_incremental/missing/thin) or the value is absent from a full a11y snapshot (would_fail) \u2014 the runtime takes ONE fresh forced-full a11y re-capture and re-grounds before accepting the verdict. Groundable after re-observe (value was present but the grade-time capture was imageless/incremental) \u2192 the model verdict stands; still ungroundable \u2192 the step is SOFT-withheld to a 'warning'+note (NON-confident, never a hard fail, never routed through the VALUE_GROUNDING_ENFORCE oracle_failure path). Closes the blind-verdict hole (F2a canvas + F2b incremental) WITHOUT manufacturing false-FAILs on values present-to-user but absent from the a11y outline (virtualized/scrolled-off rows).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-21",notes:"GRADUATED 2026-07-21 (default on), same-day evidence gate approved by Alex in lieu of a calendar soak: (1) staging replay-soak on the lio fixture (proj_mqzn31900, 3 sequential replicates of the only expectedValue-pinned plan) \u2014 zero false-withholds across 6 present-pin observations, withholds only on genuinely-absent values, surgical parity (grounded and no-pin verdicts untouched, check-only plans fully inert), no failing run laundered to pass, no material latency delta; (2) the grounded-rescue + Regression A/B legs banked by the fix-gate eval run (F2b re-observe\u2192grounded\u2192pass, healthy batch zero-fire, below-fold DOM value grounds). Product semantics approved by Alex: pinned-strict TRUE-MISMATCH fails also downgrade to warning (observed-value note preserved) until typed-match (AG-7753) restores precise typed fails. The two evals (runner-verify-blind-canvas / -incremental) remain the regression gate: fix OFF must reproduce, ON must resolve. Independent of VALUE_GROUNDING_ENFORCE \u2014 when off, that flag behaves unchanged. Remove the staging orchestrator env var once this reaches staging."},BLOCKED_RUN_DRAFT_PLAN:{key:"BLOCKED_RUN_DRAFT_PLAN",envVars:["AGENTIQA_BLOCKED_RUN_DRAFT_PLAN","AGENTIQA_EXPERIMENT_BLOCKED_RUN_DRAFT_PLAN"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'A loop-blocked Explorer child keeps its work: when the loop detector stops a NON-discovery child before it could file assistant_v2_report, the fallback result carries a reconstructed `draftTestCase` built from the natural-language stepText/intent of the interactions it actually performed (observation-only tools excluded; any step text repeated 3+ times \u2014 the stuck target the detector fired on \u2014 dropped whole; capped at 30 steps; suppressed under 2 steps). Without it the Coordinator emits `tested_areas[].draft_steps: []`, FindingsCheckpointCard filters the area out of "What was just tested", and the product offers NO way to save a run that got blocked (issue #2024: the self-agent signup\u2192first-run journey dead-ended at "Save the completed run as a new test case" for two consecutive nights). Off restores the pre-#2024 behaviour \u2014 a blocked child reports no plan and the run is unsaveable. Verdicts are untouched either way: the run stays blocked, the recommendation stays do_not_ship, and the area is surfaced as partial with its not-tested reason by the existing incomplete-child coverage backfill.',designDoc:"packages/engine-core/src/ExplorerRuntime.ts",status:"active",added:"2026-08-02"},VERIFY_GATED_DONE:{key:"VERIFY_GATED_DONE",envVars:["AGENTIQA_VERIFY_GATED_DONE","AGENTIQA_EXPERIMENT_VERIFY_GATED_DONE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Cross-checks a 'done'/'passed' claim against deterministic completion-time oracle failures (wait-style); off restores trusting the model's completion claim verbatim.",designDoc:"docs/plans/2026-07-06-verification-gated-done-design.md",status:"active",added:"2026-07-06"},VERIFY_CONFLICT_RECONCILE:{key:"VERIFY_CONFLICT_RECONCILE",envVars:["AGENTIQA_VERIFY_CONFLICT_RECONCILE","AGENTIQA_EXPERIMENT_VERIFY_CONFLICT_RECONCILE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Downgrades a verification-conflict force-FAIL to a step-level warning (run not failed) when every plan criterion on the conflicted step passed with a substantiated note AND the only unresolved oracle is an agent-invented wait literal absent from the plan; off restores the unconditional bounce-then-fail-closed (false-negative on incident asess_1784155719395_h62evpmp). Since 2026-08-01 this switch is the UMBRELLA kill for the whole agent-invented-literal family: =0 also disables VERIFY_AGENT_LITERAL_ABSTAIN and VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED, so the documented "restores the unconditional force-fail" promise stays true.',designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-16"},VERIFY_AGENT_LITERAL_ABSTAIN:{key:"VERIFY_AGENT_LITERAL_ABSTAIN",envVars:["AGENTIQA_VERIFY_AGENT_LITERAL_ABSTAIN","AGENTIQA_EXPERIMENT_VERIFY_AGENT_LITERAL_ABSTAIN"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Last valve of the verify-conflict cascade (after M1 reconcile + M2 withhold): a step whose ONLY unresolved oracle is a wait literal the AGENT invented (absent from the whole saved plan, substring) is degraded to a step-level WARNING instead of force-FAILing the run \u2014 so the synthesized 'Verification step not satisfied' issue is not filed either. TWO LANES ON SEPARATE SWITCHES since 2026-08-01 round 3: THIS flag enforces lane 'grounded_criteria' (default ON) and is also the parent kill for lane 'action_shaped', which has its OWN switch VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED (shipped default-OFF/shadow 2026-08-01, GRADUATED to default ON 2026-08-02 on its census \u2014 set it to 0 to put that lane back in shadow without touching this one). Shared preconditions of both lanes: an invented literal, no M1 grounded negative, no model-graded negative on the step (neither a 'failed' step nor a 'failed' criterion is ever softened), and no prior downgrade of the step by another valve (step_already_downgraded \u2014 the same screen M2 carries). Lane 'grounded_criteria': the step has >=1 PLAN-AUTHORED criterion, every one came back graded pass, and every one carries FRESH GROUNDING from this run (an engine-written groundingObservation for this run/step, or an observed/expectedValue the engine finds whole-token on the step's OWN retained full frame) \u2014 ungrounded model grades keep the force-fail, because this gate exists to catch fabricated passes and is not weakened for ungrounded grading. Strictly weaker than VERIFY_CONFLICT_RECONCILE, which needs a substantiation note on every criterion and produces a clean PASS; this one can never produce anything better than an honest amber and is one-directional (RED->AMBER only). NOT covered, deliberately: a PLAN-GROUNDED literal the app renders differently (staging run_b501d461's '50 EUR' vs '\u20AC50.00 net') refuses in BOTH lanes \u2014 opening that lane would re-decide four separate M1/M2 refusals and needs a page-side equivalence read; tracked as follow-up. Detection always runs: off logs verify_conflict_reconcile:agent_literal_abstain with enforced:false PLUS a verify_conflict_reconcile:agent_literal_would_abstain census row, and changes nothing; every refusal logs verify_conflict_reconcile:agent_literal_abstain_inapplicable with its reason AND its lane ('none' for the screens that run before lane selection). VERIFY_CONFLICT_RECONCILE=0 is an UMBRELLA kill for this valve too (both address the agent-invented-literal class, and that switch is documented as restoring the unconditional force-fail).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-08-01"},VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED:{key:"VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED",envVars:["AGENTIQA_VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED","AGENTIQA_EXPERIMENT_VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"GRADUATED 2026-08-02 (default ON) \u2014 lane 'action_shaped' of the agent-literal abstain valve, shipped SHADOW on 2026-08-01 (round-3 steering split) and flipped on its own census one release later, the #1893/#1895 shadow-first precedent. The lane covers a conflicted verify step with ZERO plan-authored criteria (an ACTION step mislabeled 'verify' whose saved plan object carries text/type only: prod asess_1785560226558_33t8nuos step 4; staging run_b501d461 step 2), where the timed-out wait literal is the agent's own scaffolding rather than anybody's assertion; ON, such a step degrades to a step-level WARNING exactly as lane 'grounded_criteria' does \u2014 never a pass, and the synthesized 'Verification step not satisfied' issue is not filed either. GRADUATION EVIDENCE (verify_conflict_reconcile:agent_literal_would_abstain census, the shadow lane's whole product): 2026-08-01 evening staging replay \u2014 2 instances, invented literal 'How can I help you today?' on a Lio conversation-UX plan, wouldBecome:'warning' 2/2; 2026-08-02 prod batch asess_1785646213123_5xqheb4g \u2014 invented literal 'Ich kann', currentStatus:'passed', wouldBecome:'warning' on a step the run falsely failed. ZERO censused instances where the abstain would have been harmful (i.e. no row where the withdrawn oracle was the only thing catching a real defect), which is exactly the population question the round-2 review raised \u2014 53% of e2e/evals verify steps are criteria-less and the runner SYNTHESIZES a strict criterion from the verify text, so criteria-less is not unasserted, and the census is what shows the enforced lane is not converting real detections into green runs. The model's own negatives remain hard refusals in this lane (action_shaped_step_graded_failed / action_shaped_criterion_graded_failed), so the synthesized criterion still decides when it is graded failed. KILL-SWITCH SEMANTICS UNCHANGED: an explicit AGENTIQA_VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED=0 (or the AGENTIQA_EXPERIMENT_ spelling) restores the byte-identical pre-graduation SHADOW behavior \u2014 the classifier still reaches the same 'abstain' verdict, logs verify_conflict_reconcile:agent_literal_abstain with enforced:false plus the verify_conflict_reconcile:agent_literal_would_abstain census row, and mutates NO verdict and suppresses NO issue. Parented by VERIFY_AGENT_LITERAL_ABSTAIN (=0 kills both lanes) and by VERIFY_CONFLICT_RECONCILE (=0 is the family umbrella kill and silences the diags entirely). killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND the key listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.agentLiteralAbstain.test.ts (B1/B3 at the DEFAULT \u2192 amber; B1k/B3k/D1 with =0 \u2192 byte-identical red + census).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-08-01"},VERIFY_PLAN_LITERAL_TYPED_EQUIVALENCE:{key:"VERIFY_PLAN_LITERAL_TYPED_EQUIVALENCE",envVars:["AGENTIQA_VERIFY_PLAN_LITERAL_TYPED_EQUIVALENCE","AGENTIQA_EXPERIMENT_VERIFY_PLAN_LITERAL_TYPED_EQUIVALENCE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Lane B1 of the plan-literal equivalence design (docs/plans/2026-08-02-plan-literal-equivalence-design.md; prod Lio 2026-08-02). SHADOW, DEFAULT OFF since the 2026-08-02 round-5 steering de-scope \u2014 the same shadow-first shape as lane B2 below, VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED and #1893/#1895. WHY IT SHIPS OFF RATHER THAN LIVE: THREE CONSECUTIVE adversarial rounds each found a NEW way the page-side scan reads a value that is not on the page (round 2 F2 node-locality/F3 literal ambiguity, round 3 the ten boundary shapes, round 4 D1 contested marker/D2 letter-bearing symbol, round 5 the DE-thousandths and group-separator-truncation holes below). Every round's holes were closed and every round found more; a reader whose defect-discovery rate has not yet flattened does not get to withdraw a grounded negative on a default fleet. WHAT =1 ENABLES: TYPED EQUIVALENCE = PRESENCE at the ONE page read of the verify-conflict cascade (M1 reconcileVerifyConflictsFromSnapshot). When the raw whole-token pinPresentInPage read of a PLAN-AUTHORED wait literal comes back ABSENT, the snapshot is re-read with a deterministic typed scan. SIX PROPERTIES DEFINE THE SCAN, each pinned by acceptance cases through the production wrapper pinEquivalentInPage: (a) NODE-LOCAL \u2014 the unit of scan is a SINGLE a11y node (the raw snapshot's own lines plus flattenAriaSnapshot(...).lines), never the space-joined 'spaced' or separator-free 'stream' haystacks pinPresentInPage uses; amount and currency marker must share one node, because a join turns the newline that separates two nodes into a legal gap char and FABRICATES composites present on neither (measured on five realistic aria shapes, 2026-08-02 round 2 defect F2). (b) UNAMBIGUOUS LITERAL \u2014 the AUTHORED side must carry exactly ONE surviving numeric reading; a both-locales-legal form with a meaningful fraction ('1,500' -> {1500, 1.5}) is refused outright, because under INTERSECT semantics it would equate a 1000x-different page value (round 2 defect F3). (b2) UNAMBIGUOUS PAGE OPERAND (round 5) \u2014 the PAGE atom must ALSO carry exactly one surviving reading after the structural group validation numericCandidates already applies. Under INTERSECT semantics a multi-reading page atom equated a 1000x-different value in the OTHER direction: a DE page rendering 'Rechnungsbetrag 7.890 EUR' (= 7890) reads {7.89, 7890} and satisfied the literal '7.89 EUR'; likewise '2.750'/'2.75 EUR' and '9.990'/'9.99 EUR'. Refusing multi-reading page atoms costs nothing real: 'EUR 50.00 net' is DE-invalid as a thousands group ('.00' is not three digits), so the ordinary renderings stay singletons and still fold. (c) BOUNDARY-GUARDED, MAXIMAL amount runs \u2014 a number-like run may not touch a letter, a digit, a '.'/',' or a '%'/'/'/'-' on either side, so 'EUR 50k', 'EUR 1.5B', 'EUR 50,00abc', 'EUR 50% off', 'EUR 50/Monat' and 'EUR 50-60' all read as NO amount rather than as 50/1.5 (round 3; ten measured false matches, the scaled-magnitude ones off by 1000x-1e9). (c2) NO TRUNCATED GROUP RUN (round 5) \u2014 the boundary class of (c) is a CHARACTER class, and the group separators the world actually renders money with are not in it: the Swiss apostrophe U+2019 and the space family (U+0020/U+00A0/U+202F/U+2009). 'Gesamtsumme: CHF 50\u2019000.00' therefore split into the runs '50' and '000.00', the first of which claimed the adjacent CHF and matched the literal '50 CHF' \u2014 a value 1000x below what the page states. A run adjacent, across group separators only, to another DIGIT is a TRUNCATION of a larger grouped number and yields no atom at all. (d) TYPED IDENTITY \u2014 the literal must parse as an amount+currency composite scalar ('50 EUR' / 'EUR 50' / '\u20AC50' / '50,00 \u20AC' \u2014 recognizeAmountCurrency, which rejects anything carrying another word) or a bare number, and the page must carry a MONEY ATOM with the IDENTICAL ISO-4217 code (symbol<->code via the frozen gazetteer; the ambiguous $ and \xA5 never canonicalize) whose numeric candidate set INTERSECTS the literal's under the existing dual-locale numericCandidates (US 1,234.56 == DE 1.234,56, exact equality, no epsilon). Currency adjacency is horizontal-whitespace-only, so 'EUR 12.00 shipping, 50 items' never reads as EUR 50. FOUND, WITH =1 => the literal counts as PRESENT and takes the EXISTING clean-pass rail (M1 'cleared'), with its OWN decision reason literal_equivalent_in_snapshot and its OWN client note \u2014 the whole-token rail's note ('was present when the run re-checked') is FALSE of this population by construction and is not reused (round 2 defect F4B); no new verdict, no extra browser action. NOT found => today's grounded_negative stands. NO fuzzy matching, NO LLM, NO string similarity: a shape the closed tables cannot canonicalize is not 'probably the same value'. THE CLASS IT WOULD CLOSE: a plan that writes money the way a human writes it while the product renders it the way a locale renders it ('50 EUR' vs 'EUR 50.00 net') produced a force-FAIL plus a HIGH-severity 'Verification step not satisfied' issue in the customer's project while every criterion of the step passed \u2014 that incident still force-fails on a default fleet. DEFAULT OFF = SHADOW, NOT INERT: detection runs in full and the caller censuses verify_conflict_reconcile:literal_typed_equivalence {enforced:false} PLUS a dedicated row verify_conflict_reconcile:literal_typed_equivalence_would_clear {stepIndex, literal, matchType, currency, matched, nodeText, basis, wouldBecome:'passed'}, and mutates NOTHING \u2014 the M1 decision map keeps its grounded_negative, would_fail is logged exactly as with the lane absent, the force-fail and the HIGH-severity issue stand byte-identically, and no capture kind changes. TWO GATES, NOT ONE (round 5 invariance fix): the mutation requires BOTH this switch AND a live VERIFY_CONFLICT_RECONCILE_SNAPSHOT. The decision map is populated even when M1 is in shadow, and its grounded_negative is read UNGATED by two downstream firewalls (M2 and the agent-literal abstain valve) \u2014 so before the fix an opt-in =1 reached THROUGH a disabled M1 and changed verdicts on a build whose operator had switched M1 off. VERIFY_CONFLICT_RECONCILE=0 is the family umbrella kill and silences the census too. Runner lane only (RunnerRuntime run_complete, second escalation).",designDoc:"docs/plans/2026-08-02-plan-literal-equivalence-design.md",status:"active",added:"2026-08-02",graduation:{status:"gated",gate:"NOT graduable as built \u2014 FOUR blockers. THE STANDING ONE: three consecutive adversarial rounds each found a fresh class of page-side false match (round 2 F2/F3, round 3 the ten boundary shapes, round 4 D1/D2, round 5 the two below) and the discovery rate has not flattened, so graduation needs a round that finds nothing new, not merely the closure of the last round's list. (1) PAGE-SIDE SINGLETON-READING SEMANTICS. Round 5 measured the DE-thousandths hole in the direction the round-2 F3 guard does not cover: the AUTHORED side is now required to be unambiguous, but the PAGE side was left multi-reading on the argument that 'the page is not the operand the author controls' \u2014 and under INTERSECT that is exactly what lets a page rendering 7.890 (DE seven-thousand-eight-hundred-ninety) satisfy the literal '7.89 EUR'. The singleton rule shipped in this PR closes the measured shapes; what it does NOT yet have is a systematic argument that the page operand is single-reading everywhere the fold fires, which is what a graduation needs. (2) GROUP-SEPARATOR TRUNCATION. The boundary class of property (c) is a character class and cannot see that a run it accepted is a FRAGMENT of a grouped number: 'CHF 50\u2019000.00' cleared '50 CHF' because U+2019 and the space family are not boundary chars. Closed for the apostrophe and the space family in this PR; the general form (any group separator any locale renders, including the ones no test has enumerated) is not proven closed. (3) SUBJECT BINDING (F1). Node-locality is an AUTHORSHIP guard, not a PLACE one: an equal value on an UNRELATED card clears the presence wait, demonstrated on the lio-mirror/chat-price-format trap where the distractor renders \u20AC7.89 and the anchor \u20AC78.90 \u2014 B1 turns a genuine red into a green there. The census carries nodeText so a clear is auditable post-hoc, but auditability is not a gate; the lane cannot enforce until a subject-binding design exists (bind the matched node to the step's target, or refuse when the page carries more than one atom satisfying the literal). (4) STAGING AND PROD CENSUS. verify_conflict_reconcile:literal_typed_equivalence_would_clear plus the paired literal_typed_equivalence_refused rows, showing the fold fires on genuine locale renderings of the value the step is about and not on pages where the product is actually wrong about the number.",evidence:"an adversarial round that finds NO new page-side false-match class; a systematic (not case-by-case) argument for page-operand single-readingness and for group-separator closure; a subject-binding design with its own acceptance suite that reddens the chat-price-format trap; and a staging+prod literal_typed_equivalence_would_clear / _refused census sized against the population it would clear",owner:"steering (Alex)",review:"2026-08-09"}},VERIFY_PLAN_LITERAL_CRITERIA_DEGRADE:{key:"VERIFY_PLAN_LITERAL_CRITERIA_DEGRADE",envVars:["AGENTIQA_VERIFY_PLAN_LITERAL_CRITERIA_DEGRADE","AGENTIQA_EXPERIMENT_VERIFY_PLAN_LITERAL_CRITERIA_DEGRADE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Lane B2 of the plan-literal equivalence design (docs/plans/2026-08-02-plan-literal-equivalence-design.md; prod Lio 2026-08-02, wait literal 'Munich' on a German page rendering 'M\xFCnchen'). SHADOW, DEFAULT OFF since the 2026-08-02 round-3 steering de-scope \u2014 the same shadow-first shape as VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED and #1893/#1895. WHAT =1 ENABLES: the ONE deliberately scoped-out lane of VERIFY_AGENT_LITERAL_ABSTAIN \u2014 a PLAN-GROUNDED wait literal that B1's typed equivalence could NOT reconcile stops refusing at the literal_plan_grounded provenance screen WHEN the step has >=1 PLAN-AUTHORED criterion, falls through to lane 'grounded_criteria', which applies its FULL, unchanged contract (every authored criterion graded, every one passed, every one seen by the ENGINE on the step's OWN retained frame via criterionValueSeenOnOwnFrame) and degrades the step to a step-level WARNING under the pre-cut reason plan_literal_not_rendered_as_written and the pre-cut client note ('the run waited for this exact wording and did not find it \u2014 the page may present the same value differently'). =1 ALSO arms lane B2's own full-snapshot request at dispatch (verifyWaitCarriesDegradableLiteral), without which the honesty condition has no frame to read and the lane is inert. NEVER a pass: the outcome is the one-directional RED->AMBER the valve already owns, so run.status can go from failed to passed only in the sense that no step is failed, and the synthesized 'Verification step not satisfied' issue is not filed. DEFAULT OFF = SHADOW, NOT INERT: detection runs in full (the provenance screen no longer reads the switch), the classifier reaches the same verdict, the caller logs verify_conflict_reconcile:agent_literal_abstain with enforced:false PLUS a dedicated census row verify_conflict_reconcile:plan_literal_would_degrade {stepIndex, stepText, literal, timeoutMs, reason, provenance, lane, criteriaCount, currentStatus, wouldBecome:'warning', flag}, and mutates NOTHING \u2014 the step keeps its status, the conflict stays in failingConflicts, the force-fail and the HIGH-severity issue stand byte-identically, and NO capture kind changes. The shadow census UNDERCOUNTS BY CONSTRUCTION: with the lane off no full frame is bought, so most of the population refuses one screen earlier and is visible as agent_literal_abstain_inapplicable {reason:'plan_literal_criteria_not_page_grounded', provenance:'plan_grounded', enforced:false} rather than as a would-degrade row; read the two together to size the population. WHAT STAYS RED under =1, unchanged: a criteria-LESS step (lane 'action_shaped' still refuses every plan-grounded literal pre-lane with literal_plan_grounded, lane:'none'), any step the model graded failed or with a failed criterion, any step another valve already marked down, any step whose authored criteria are ungraded or not page-grounded, and any M1 grounded negative (the firewall is untouched \u2014 which is why B1 must clear the equivalence case at M1 rather than here). Parented by VERIFY_AGENT_LITERAL_ABSTAIN (=0 kills this too) and by VERIFY_CONFLICT_RECONCILE (=0 is the family umbrella kill and silences the diags entirely). Runner lane only.",designDoc:"docs/plans/2026-08-02-plan-literal-equivalence-design.md",status:"active",added:"2026-08-02",graduation:{status:"gated",gate:"NOT graduable as built \u2014 three blockers, all found by the round-2/round-3 adversarial verification of this PR and none of them a soak question. (1) HONESTY-NEEDLE REDESIGN. criterionValueSeenOnOwnFrame, the condition that is supposed to pay for the amber, accepts a MODEL-CONTROLLED value: it looks for the criterion's own `observed`/`expectedValue` on the step's frame, and a probe showed ANY on-page token the model chooses to emit buys the amber. 'the engine saw the asserted value' is therefore closer to 'the model quoted something from the page', which is not corroboration of an authored claim; the lane cannot enforce until that predicate is re-designed to read a value the model does not control. (2) F5 BLAST RADIUS. B2 is inert without a full frame, so it requests one at DISPATCH \u2014 before the wait's outcome is known \u2014 which also promotes SUCCEEDING waits to full and widens _domGroundableCandidates (the live predicate-basis candidate set) for steps B2 never touches. Enabling B2 is therefore also a capture-kind change for a wider population and must be re-measured as one. (3) M1-CANARY RE-BASELINE. The late-render canary's failed->warning baseline was recorded with B2 ENFORCING; that decision has to be re-taken against the default-OFF world before the flag can flip. On top of the three: a staging AND prod census of verify_conflict_reconcile:plan_literal_would_degrade plus the plan_literal_criteria_not_page_grounded refusals, showing the lane fires on genuine rendering mismatches (Munich/M\xFCnchen) and not on steps where the product is actually wrong.",evidence:"staging/prod verify_conflict_reconcile:plan_literal_would_degrade census rows + the paired agent_literal_abstain_inapplicable {reason:plan_literal_criteria_not_page_grounded} refusals (population sizing), a re-designed honesty predicate with its own acceptance suite, an F5 capture-kind impact measurement over a full runner-eval pass, and the re-taken m1-canary late-render baseline",owner:"steering (Alex)",review:"2026-08-09"}},VERIFY_LITERAL_PROVENANCE_STEP_SCOPED:{key:"VERIFY_LITERAL_PROVENANCE_STEP_SCOPED",envVars:["AGENTIQA_VERIFY_LITERAL_PROVENANCE_STEP_SCOPED","AGENTIQA_EXPERIMENT_VERIFY_LITERAL_PROVENANCE_STEP_SCOPED"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"GRADUATED 2026-08-03 (default ON) \u2014 STEP-SCOPED LITERAL PROVENANCE, the third provenance class of the agent-literal abstain valve (prod Lio 2026-08-02/03, 5 hits in 24h). Shipped SHADOW on 2026-08-03 and flipped on its own REPLAY-BASED census one release later, the same shadow-first path VERIFY_AGENT_LITERAL_ABSTAIN_ACTION_SHAPED took on 2026-08-02 (#1893/#1895 precedent). GRADUATION EVIDENCE (2026-08-03, ag-rehearsal prod->staging mirror replay, 18 runs over 6 plans: 16 census runs at =0 plus a 2-run enforcing A/B; VERDICT.md at https://s.assets.agentiqa.com/provenance-graduation-2026-08-03/): (1) COUNT AND SHAPE \u2014 exactly 2 verify_conflict_reconcile:agent_literal_would_rescue rows in the 16 shadow runs, both the known '50 EUR' hit (chat-knowledge-qa tp_0370f61d run14 and asst-knowledge-qa run11), both step 2, both planMatchLocation step_3:criterion_1:check, both currentStatus passed -> wouldBecome warning, on runs the build falsely FAILED. (2) NO OVER-TRIGGER \u2014 zero provenance rows on the other 4 plans across 8 runs, and the 4 plan_grounded refusals were retained as authored, so the population did not creep into the typed-match equivalence track's half. (3) ENFORCING A/B \u2014 run14 (=0): step 2 failed, run failed, would_rescue censused; run18 (=1): step 2 warning, run PASSED, enforced:true and no census row, i.e. the rescue lands as the one-directional RED->AMBER and nothing else moved. (4) PROD RECOMPUTE \u2014 all 5 documented prod hits recomputed under the step-scoped read: 4 remap to agent_invented_plan_coincident at exactly the documented locations; the 5th (m77p3hky) became agent_invented after lio edited that plan at 2026-08-03T09:01:10Z, i.e. it left the class by an authoring change, not by a read change. THE CLASS: the valve's provenance screen asks 'did the AUTHOR write this literal?' against the WHOLE plan document (title + every step's text/criteria, loose substring). When an INVENTED wait literal COINCIDENTALLY appears somewhere else in the plan \u2014 the plan title, or (in all 5 measured hits) a NEIGHBOURING step's criterion \u2014 the screen returns plan_grounded, the criteria-LESS lane 'action_shaped' refuses before lane selection with agent_literal_abstain_inapplicable {reason:'literal_plan_grounded', lane:'none'}, and the action step keeps its force-FAIL while every criterion of its paired verify step passed. Measured: '50 EUR' (asess_1785644983561_n5worrfv step 2, matched step 3's criterion; app renders '\u20AC50 (net)'), 'Munich' (o7hvcun7 step 2, matched step 3's criterion; app renders 'M\xFCnchen'), the German prompt of step 2 (5xqheb4g step 4, matched step 2's TEXT), 'QA Base \u2014 Draft Completion (do not modify)' (m77p3hky step 4, matched step 5's criterion), '50 EUR' again on the 08-03 build (r3ghgy68 step 2). All five failing steps carry ZERO plan-authored criteria, so lane B2's escape hatch (which needs >=1 criterion) cannot reach any of them. WHAT =1 ENABLES: the provenance read is SCOPED TO THE FAILING STEP'S OWN authored text (buildStepPlanHaystack \u2014 its instruction plus each of its criteria's check/expectedValue) with the SAME loose lowercased-substring matcher as before, and splits the old plan_grounded population in two \u2014 literal ON the step's own text stays plan_grounded and every existing refusal is byte-identical; literal absent from the step but present ELSEWHERE in the plan becomes the NEW provenance agent_invented_plan_coincident, which falls through the plan-grounded screen into ordinary lane selection and is then decided by each lane's UNCHANGED contract (lane 'action_shaped': no model-graded negative, no prior downgrade, no M1 grounded negative; lane 'grounded_criteria': every authored criterion graded, passed and engine-grounded this run via criterionGroundedThisRun \u2014 the invented-literal bar, since a literal the author did not write on this step is not an authored claim about it). Literal absent from the plan entirely is agent_invented, unchanged. The two reads bracket the population EXACTLY: plan_grounded + agent_invented_plan_coincident is precisely the old plan_grounded set (the locator falls back to 'plan_document' for the join-boundary residue), so no literal can be reclassified into the default-ON agent_invented path by this change. OUTCOME CONTRACT UNCHANGED: the only mutation is the valve's one-directional RED->AMBER step-level WARNING, never a pass (#2002 doctrine \u2014 a coincidence buys a warning, never green), and the M1 grounded-negative firewall plus both model-negative screens still refuse. KILL-SWITCH SEMANTICS (=0 under either spelling) RESTORE THE BYTE-IDENTICAL PRE-GRADUATION SHADOW, NOT AN INERT PATH: detection always runs, the classifier reaches the same verdict, the caller logs verify_conflict_reconcile:agent_literal_abstain {enforced:false} PLUS a dedicated census row verify_conflict_reconcile:agent_literal_would_rescue {stepIndex, stepText, literal, timeoutMs, reason, provenance:'agent_invented_plan_coincident', planMatchLocation, lane, criteriaCount, currentStatus, wouldBecome:'warning', flag} and mutates NOTHING \u2014 the step keeps its status, the conflict stays in failingConflicts, the force-fail and the synthesized 'Verification step not satisfied' issue stand byte-identically. So an operator who kills the class keeps the measurement. NO capture kind changes in EITHER state (this valve reads no page and calls no model; lane B2's full-frame force is untouched because it is gated on waitLiteralAuthoredOnStep, which this class fails by definition). ONE TELEMETRY DELTA, stated rather than hidden: a coincident literal that reaches a lane reports that lane's reason (e.g. action_shaped_criterion_graded_failed) instead of the pre-2026-08-03 literal_plan_grounded, and its lane field is no longer 'none' \u2014 the census got more specific when the read was scoped, independently of enforcement. The parents are unchanged \u2014 VERIFY_AGENT_LITERAL_ABSTAIN=0 kills both lanes and VERIFY_CONFLICT_RECONCILE=0 is the family umbrella kill that silences the census too. killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND the key listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.agentLiteralAbstain.test.ts \u2014 B2 (the prod shape verbatim), P2, P3d, P9 leg (a) and P10 leg (b) at the DEFAULT -> amber, never a pass; B2k, P3, P5b, P9 leg (b) and P10 leg (a) with =0 -> byte-identical red + census; P1/P1e pin that the own-step (genuinely authored) read never moves; P4 pins the agent_invented path byte-identical; P8 pins that a model-graded negative is still refused; P9 is asserted in BOTH states because the coincident/agent_invented split is what makes this switch reach the population at all (agent_invented is held by VERIFY_AGENT_LITERAL_ABSTAIN, not by this key). Runner lane only.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-08-03"},VERIFY_NOTE_REFUTES_CRITERION:{key:"VERIFY_NOTE_REFUTES_CRITERION",envVars:["AGENTIQA_VERIFY_NOTE_REFUTES_CRITERION","AGENTIQA_EXPERIMENT_VERIFY_NOTE_REFUTES_CRITERION"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"SELF-CONTRADICTION GATE \u2014 ships ENFORCING (default ON) on a measured population. THE DEFECT (false-PASS, the direction that destroys customer trust): a criterion recorded passed:true while its OWN note states the opposite, inside a single verdict record, and the run reported PASSED. TWO measured instances, one on PROD. (1) PROD run_35ffd2b2 / asess_..._eobjjo88, lio's 2026-08-04 04:14Z batch on engine 8741da063, plan 'Lio Assistant - conversation UX' step 3: STRICT criterion 'The answer addresses the question about cleaning-services suppliers (it mentions at least one supplier or a count)' booked passed with note 'The assistant addresses the topic of cleaning-services suppliers, stating it couldn't find specific info but offering to help narrow down the search.' The SAME underlying lio behaviour correctly FAILED a sibling run in the same batch (8vc9g930), so the false pass did not merely mis-grade a criterion \u2014 it ABSORBED a real customer-visible defect our own product had already proven elsewhere. (2) MIRROR run_9bec8ba8 (c13) step 3, plan tp_3d5427b9 'asst-negotiation-email': STRICT criterion 'An email draft is shown with an editable subject and body (an email composer, not just prose in the chat)' booked passed with note 'An email draft was shown in a composer-like UI, but the elements were not programmatically editable (not contenteditable).' Ground truth settled by two independent methods across five sibling replicates: a contenteditable DOM query returned only the chat composer (b13) and an actually-attempted type_text_at into subject and body FAILED (f14, f15). NO existing gate caught either: in c13 step 3 predicate_basis_verify was inconclusive/dom_not_resolvable, typed_atom_floor abstained on no_structured_observed, criteria_containment and ungrounded_pass were silent. WHAT =1 ENABLES: every graded criterion still reported passed after pin substantiation is read by detectCriterionSelfContradiction (packages/engine-core/src/criterionSelfContradiction.ts) and, on a `contradicted` verdict, has `passed` flipped to false \u2014 the SAME single mutation substantiatePinnedCriterion makes for an uncited pin, so the outcome is decided by the ONE existing chokepoint deriveStepStatusFromCriteria: a STRICT criterion FAILS its step, a non-strict one WARNS it. No new status, no new vocabulary, no note rewrite (the criterion's own note already says why, in the grader's own words). ONE-DIRECTIONAL: only ever reached for a grade that is still passed, so it can never rescue a failure and never makes anything green. WHY FAIL RATHER THAN WITHHOLD for a strict criterion: a withhold means 'we could not confirm', and this is not that \u2014 the note is the grader's own AFFIRMATIVE statement that the checked property is ABSENT, i.e. evidence AGAINST the claim rather than missing evidence, and in both measured cases an independent method proved the fail correct. TWO LANES, because the class has two shapes and one mechanism cannot reach both. LANE A: the note negates a term the CHECK ITSELF USES, scoped \u2014 an explicit negator from a closed list, and a >=4-char non-stopword subject token of the check's own main clause inside that negator's 60-char forward window (the mirror c13 shape, negatedTerm 'editable'). LANE B: the note reports the app FAILED TO PRODUCE what the check REQUIRES, IN WORDS THE CHECK NEVER USES \u2014 a closed NOT-FOUND list (couldn't/could not/did not/unable to + find|locate|retrieve|provide|identify|return|surface; 'no results|matches|information|data|details|suppliers|records'; 'not found'; 'found no|nothing|none') against a closed PRESENCE-REQUIREMENT verb list in the check (mentions|shows|displays|contains|includes|lists|states|reports|identifies|names|cites|returns|surfaces|provides|presents|specifies). The PROD case is lane B and lane A provably cannot see it: 'couldn't find specific info' and 'mentions at least one supplier or a count' share NO surface term, and the check's 'or' screens lane A out anyway. NO LLM, NO FUZZY SCORING, NO NETWORK \u2014 two strings the caller already holds, so shadow and enforcement are the same computation and OFF costs nothing. FOUR SCREENS, EACH KILLING A MEASURED FALSE POSITIVE of the naive version (a bare 'does the note contain a negation' scan fires on 7 of the mirror corpus's 526 passed criteria and only ONE is the defect; THREE of the six false positives are on STRICT criteria, so a naive gate shipped enforcing would have turned three HONEST passes into hard failures \u2014 the one direction this gate may never take): check_disjunctive (a09 \u2014 check 'mentions the tracking number QACORE-TRACK-0001 and/or the carrier DHL', note 'Tracking number ... was not mentioned'; DHL WAS mentioned so the disjunction is satisfied and the note refutes nothing; also e05); check_carries_negation, read on the check's MAIN CLAUSE (f01/e01 \u2014 check '\"Final Sign-off\" has not started', note 'Final Sign-off is Not Started', where the note CONFIRMS a check whose own subject is negative; also a11/b11 \u2014 check '... (webshops, private expensing) is not permitted' vs note 'you must not use private expense reimbursement'); GLOSS STRIPPING of quoted spans and parenthetical asides from BOTH texts before anything is read (a08 \u2014 check and note both name the plan title 'QA Base \u2014 Submission Checks (do not modify)' and the negation lives INSIDE THE TARGET NAME, the exact class stripTargetLiteralFromProse exists for; and f13/f16 \u2014 note 'Actual text: \"Payment terms of 60 days end of month instead of 30 days\"', a page quotation rather than the grader's own claim); and CONTRASTIVE EXCLUSION \u2014 'instead of' / 'rather than' / 'but' / 'however' contrast, they do not negate, and 'instead of' was exactly f13/f16's naive hit. CENSUS THAT PAYS FOR ENFORCING: 2 fires over 698 passed criteria, both of them the known defects, ZERO honest passes moved \u2014 1 of 168 in lio's PROD 04:14Z batch (eobjjo88, lane B), where an independent hand audit of the same batch found exactly that one note-contradicted pass so the detector matches the audit, and 1 of 530 in the 46-run prod-parity mirror (c13, lane A). KNOWN BOUNDARIES, stated rather than hidden: a check whose main clause carries any negator abstains wholesale (a genuine self-contradiction there is missed \u2014 f01/e01/a11/b11 are four measured reasons to keep it); a negation the grader writes INSIDE a quotation abstains, because transcription cannot be told from assertion there; morphology is not modelled, so 'mentions' in the check vs 'mentioned' in the note abstains. =0 UNDER EITHER SPELLING IS SHADOW, NOT INERT: detection still runs, the caller logs note_refutes_criterion:would_fail {stepIndex, criterionIndex, lane, reason, negatedTerm, strict, check, note, currentStatus, wouldBecome, decision, enforce:false, mutated:false, flag} and mutates NOTHING, so an operator who kills the class keeps the measurement. A SECOND census row, emitted in BOTH states, sizes the SUPPRESSED population: note_refutes_criterion:suppressed fires when a note DID carry an explicit negator and a screen declined it \u2014 the row a graduation or rollback review reads to see whether the screens are over- or under-refusing. Notes with nothing negative in them are silent, because they carry no signal and would drown it. Acceptance suite: packages/engine-core/src/__tests__/criterionSelfContradiction.test.ts (both prod/mirror defects verbatim, all eight measured honest passes as regression pins, and a mutation guard per screen) + RunnerRuntime.noteContradictsCriterion.test.ts (the cascade: strict -> step failed, non-strict -> step warning, =0 -> byte-identical pass + census). Runner lane only. See docs/VERDICT-GATES.md.",designDoc:"docs/plans/2026-08-04-criterion-self-contradiction-design.md",status:"active",added:"2026-08-04"},VERIFY_STRICT_PASS_SUBSTANTIATION:{key:"VERIFY_STRICT_PASS_SUBSTANTIATION",envVars:["AGENTIQA_VERIFY_STRICT_PASS_SUBSTANTIATION","AGENTIQA_EXPERIMENT_VERIFY_STRICT_PASS_SUBSTANTIATION"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"SHADOW, DEFAULT OFF \u2014 the SECOND shape of the 2026-08-04 false-PASS class: a STRICT criterion booking a pass with NO substantiation at all. Measured on PROD in run_783593d9 (mirror label e13) step 3, where the same plan-13 editability criterion recorded passed:true with note, observed AND groundingObservation ALL absent \u2014 and where all 10 strict passes in that run carried zero criterion-level substantiation. WHY IT IS SHADOW AND NOT ENFORCING, with the number: requiring substantiation to book a strict pass would flip legitimate passes EN MASSE, which is precisely the harm the sibling VERIFY_NOTE_REFUTES_CRITERION gate exists to prevent. 104 of 446 strict passes in the 46-run prod-parity mirror (23.3%) carry no substantiation of any kind, spread over 21 of its 47 runs, and those runs are dominated by HONEST passes (a06, a07, a09, a11, a12, a14, b06, b07, b10, b12, b16 are all honest-pass rows in the rehearsal SCORE.md). On lio's PROD 2026-08-04 04:14Z batch the same count is 79 of 143 strict passes. An independent audit of that batch corroborates the weakness is SYSTEMIC rather than a verdict bug: only 47 of 168 passes carry a distinct page literal at all \u2014 62 are prose self-echo, 59 ungrounded, and 56 already carry ungrounded_pass:would_withhold. Enforcing a substantiation requirement would therefore touch about a third of ALL passes, so the fix belongs at the SOURCE (what the grader is required to transcribe) and is its own track, not a verdict change to make alongside a false-pass fix. IT EXTENDS THE pin_substantiation FAMILY RATHER THAN DUPLICATING IT: substantiatePinnedCriterion already flips a passing criterion whose note does not cite its pin, but returns the grade VERBATIM when planCriterion.expectedValue is empty \u2014 which is every PROSE criterion, including this one. strictPassLacksSubstantiation (packages/engine-core/src/criterionSelfContradiction.ts) is exactly that abstention made countable, on the same 'the grade's evidence is its note' doctrine. WHAT THE DEFAULT-OFF STATE DOES: detection always runs and the caller emits strict_pass_unsubstantiated:would_withhold {stepIndex, criterionIndex, check, currentStatus, wouldBecome:'warning', decision:'abstain', enforce, mutated:false, flag} while mutating NOTHING \u2014 no status write, no note, byte-identical verdicts. WHAT =1 WOULD ENABLE is deliberately NOT WIRED in this PR: the flag currently selects only the census row's `enforce` field, so setting it to 1 changes no verdict. Graduating this class requires the source fix plus its own census, not an env flip. Runner lane only. See docs/VERDICT-GATES.md.",designDoc:"docs/plans/2026-08-04-criterion-self-contradiction-design.md",status:"active",added:"2026-08-04",graduation:{status:"gated",gate:"NOT graduable as built, and deliberately so. Enforcing it would demote 104 of 446 strict passes in the mirror corpus (23.3%, 21 of 47 runs) and 79 of 143 in lio's prod 04:14Z batch, overwhelmingly on runs that are honest passes \u2014 a mass false-fail, i.e. the opposite defect. The blocker is not a soak: the SOURCE of the weakness is that a strict prose criterion is not required to transcribe anything (only 47 of 168 prod passes carry a distinct page literal; 62 are prose self-echo). Graduation requires (1) the grading contract changed so a strict pass must carry a transcribed page value, (2) a re-census showing the unsubstantiated population has collapsed, and (3) a decision on the residual population that legitimately cannot transcribe (visual/layout criteria).",evidence:"a strict_pass_unsubstantiated:would_withhold census on staging AND prod after the grading-contract change, showing the population collapsed rather than merely moved, plus the ungrounded_pass:would_withhold overlap so the two are not double-counted",owner:"steering (Alex)",review:"2026-08-11"}},VERIFY_CONFLICT_NEGATION_BAIL_WITHHOLD:{key:"VERIFY_CONFLICT_NEGATION_BAIL_WITHHOLD",envVars:["AGENTIQA_VERIFY_CONFLICT_NEGATION_BAIL_WITHHOLD","AGENTIQA_EXPERIMENT_VERIFY_CONFLICT_NEGATION_BAIL_WITHHOLD"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"FALSE-FAIL REMOVAL \u2014 ships ENFORCING (default ON) because it only ever runs RED->AMBER and removes a verdict the run's own record contradicted. THE DEFECT: mirror run_86de13b1 (b05) step 3 of plan tp_4ce9dd2d 'chat-item-search' was recorded status:failed \u2014 and the RUN failed with it \u2014 while ALL FOUR of its criteria were passed:true, each carrying a groundingObservation quoting the page ('Inacopia Kopierpapier Elite A4 90g 500Bl' visible, price '7,89 \u20AC', card 'B\xFCrowelt Schmidt GmbH & Co. KG'). The client summary read 'Failed: verification for step 3 ... did not pass \u2014 the expected content never appeared on the page and was not confirmed on a re-check, even after a retry', which the run's own step_results contradict. Rate 1 of 5 replicates; the trigger is the model choosing to scroll back and re-verify, and this plan is seq 4 in lio's nightly batch. MECHANISM: the oracle chain declined in sequence and the LEGACY verify-conflict force-fail owned the verdict BY DEFAULT \u2014 verify_conflict_reconcile:inapplicable reason=negation_marker literal='Inacopia ...', then verify_conflict_withhold:inapplicable reason=negation_marker (M2 is OFF on prod anyway), then verify_conflict_reconcile:agent_literal_abstain_inapplicable lane=none provenance=plan_grounded reason=literal_plan_grounded. M1's negation_marker bail is triggered by lio's own DEFENSIVE criterion wording ('those do not count', 'their presence or absence must not be asserted') and is CORRECT \u2014 a PRESENCE clear is unsound for an assertion that something is GONE, and clearing there would launder a genuine failure into a pass \u2014 so the bail itself is untouched by this flag. The bug is the inference that followed it: 'M1 could not clear this' is an ABSENCE OF PROOF, not proof of failure. WHAT =1 ENABLES: a new rung between the agent-literal abstain valve and the legacy force-fail. For a step in failingConflicts whose M1 decision reason is EXACTLY negation_marker, whose status is still passed, and which carries >=1 graded PLAN-AUTHORED criterion with ALL of them passed, the oracle failure is cleared and the step lands in the WARNING band with an honest client note (clientNegationBailWithheldNote \u2014 'could not be re-checked automatically because this step is worded as a negative or absence condition. Every check the plan wrote for this step passed; the re-check itself is reported as unconfirmed rather than passed or failed'), on exactly the machinery M1's stale-scope rung and M2's withhold already use. Because the conflict leaves failingConflicts, the synthesized 'Verification step not satisfied' issue is not filed for it either, and terminationReason stays 'completed'. NEVER A PASS and never a silent green: the outcome is the one-directional RED->AMBER the valve family already owns. NARROW BY CONSTRUCTION \u2014 every condition is a refusal the class needs: no other M1 bail reason qualifies; authorship is read via the SAME `unauthored` mark deriveStepStatusFromCriteria honours, so a fabricated grade can neither buy nor block the amber; and a step the model itself graded negative, or that another valve already marked down, refuses \u2014 so this can never rescue a model-reported failure. =0 UNDER EITHER SPELLING IS SHADOW, NOT INERT: the eligibility computation still runs and the caller logs verify_conflict_negation_bail:would_withhold {stepIndex, stepText, literal, criteriaCount, currentStatus, wouldBecome:'warning', decision:'withhold', enforce:false, mutated:false, flag} while the force-fail, the failed step status, the client summary and the synthesized issue all stand byte-identically. Reads no page and calls no model, so both states cost the same. Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.negationBailWithhold.test.ts (the b05 shape verbatim -> warning + run not failed at the default; =0 -> byte-identical failed + census; and the refusals: a non-negation_marker bail, a model-graded failed criterion, an already-downgraded step, a step with zero authored criteria, and a step whose only passing grades are `unauthored`). Runner lane only. See docs/VERDICT-GATES.md.",designDoc:"docs/plans/2026-08-04-criterion-self-contradiction-design.md",status:"active",added:"2026-08-04"},VERIFY_CONFLICT_URL_GROUNDED_WITHHOLD:{key:"VERIFY_CONFLICT_URL_GROUNDED_WITHHOLD",envVars:["AGENTIQA_VERIFY_CONFLICT_URL_GROUNDED_WITHHOLD","AGENTIQA_EXPERIMENT_VERIFY_CONFLICT_URL_GROUNDED_WITHHOLD"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"SHADOW-FIRST, DEFAULT OFF \u2014 F4, the URL-GROUNDED WITHHOLD rung of the verify-conflict valve family (#2536; the #1907 RBAC COMPOSITE shape, carved out of #2533 per its steering review). THE DEFECT: a timed-out wait_for_element on a COMPOSITE verify step force-fails a passing run even though the runtime already holds deterministic grounded evidence disproving the failure. Staging run run_ef0687dc\u2026 (session asess_1786637131530_ahwx4prw) step 6 ('Sign-in succeeds: lands on /en/auth/dashboard \u2026 a Log out control is visible \u2026 NO Blog Management button is present'): the wait on 'Log out' timed out \u2014 the control renders as an ICON \u2014 while the wait's own result URL was ALREADY /en/auth/dashboard; the model graded the step passed and EVERY authored strict criterion passed with page-quoted grounding. Every valve then declined for its own honest reason: M1 reconcile inapplicable {negation_marker} (correct \u2014 it cannot clear absence wording by literal presence), M2 withhold inapplicable on the same screen, the agent-literal abstain declined (literal_plan_grounded), and F3 negation-bail \u2014 the rung built for this signature \u2014 NEVER BECAME ELIGIBLE: its whole-step markerIsTheAssertion screen sees the not/no markers of the OTHER conjuncts, and a whole-step marker screen cannot tell whose negation it is (zero verify_conflict_negation_bail:* rows; loosening the screen to clause scope is deliberately NOT done \u2014 it would risk laundering the genuine 'X is missing but the page is still open' false-pass shape). The legacy force-fail wrote the verdict (run_complete:verification_conflict_failed, cause wait_literal_only) 22 ms AFTER the F2c shadow url comparator returned its second would_pass (url_predicate_satisfied) for the same step. WHAT =1 ENABLES: a new rung BETWEEN F3 (negation-bail) and the legacy force-fail, triggering ONLY when ALL hold for a conflicted step: (1) the conflict cause classifies wait_literal_only (classifyVerificationConflictCause \u2014 an unconfirmed wait-style oracle is the SOLE basis of the conflict; a model-graded negative, an M1 fresh grounded negative, a plan-pin contradiction and a partially graded step all refuse via the classifier's louder bands); (2) the step is still model-graded passed and carries >=1 authored criterion with ALL of them graded passed (authorship via the SAME `unauthored` mark deriveStepStatusFromCriteria honours \u2014 a fabricated grade can neither buy nor block the amber); (3) the step carries a url-kind grounded candidate (_groundedStateUrlCandidates \u2014 the F2c comparator's grade-time pinned URL, #2425/#2401, recorded whenever GROUNDED_STATE_UNIFIED or PREDICATE_BASIS_VERIFY is on) whose deterministic compareUrl re-read over the step's own assertion text returns would_pass (url_predicate_satisfied). The re-read is a PURE recompute of exactly what the shadow url branch of runGroundedStateUnified graded on the final attempt (same pinned URL, same assertion construction \u2014 the shadow branch logs its verdict but retains nothing), so the rung consumes machine-checked, TEMPORALLY-ALIGNED page evidence with NO model call and NO text-marker heuristic in the loop. -> The conflict is WITHHELD to the WARNING band on exactly the one-directional RED->AMBER landing pad M1's stale-scope rung, M2's withhold and F3 already use: the oracle-failure ledger entries are cleared, the step lands at 'warning' with an honest client note (clientUrlGroundedWithheldNote \u2014 the page address recorded for the step matches the address the step expects, every check the plan wrote passed, the re-check itself is reported as unconfirmed rather than passed or failed), the conflict leaves failingConflicts so no 'Verification not confirmed' issue is synthesized for it and terminationReason stays 'completed' (billable). NEVER A MANUFACTURED PASS: the model's own passed grade stands; only the conflict force-fail is downgraded. FAIL-CLOSED: a would_fail URL re-read (a genuinely wrong page \u2014 the safety pin), an inconclusive one (no derivable path predicate / unparseable URL) or an ABSENT candidate (url kind never derived, the F2c recording flags off, no URL rode the step's captures) all make the rung inapplicable and the existing force-fail stands byte-identically. DIAG ROWS: verify_conflict_url_grounded_withhold:withheld (enforcing) / verify_conflict_url_grounded_withhold:would_withhold (eligible but flag off \u2014 the graduation soak signal), both carrying {stepIndex, stepText, cause, urlVerdict, spectrumReason, currentUrl, currentPath, predicates, literal, criteriaCount, currentStatus, wouldBecome:'warning', decision:'withhold', enforce, mutated, flag}; ineligible steps are NOT logged (the F3 family convention). =0 / DEFAULT-OFF IS SHADOW, NOT INERT: the full eligibility computation and the URL recompute still run and an eligible step logs would_withhold while the force-fail, the failed step status, the client summary and the synthesized issue all stand byte-identically. Reads no page and calls no model, so both states cost the same. Read site: packages/engine-core/src/RunnerRuntime.ts (verifyConflictUrlGroundedWithholdEnabled -> the F4 rung between F3 and the legacy force-fail in the run_complete conflict chain). Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.urlGroundedWithhold.test.ts (the step-6 composite shape fires in BOTH states \u2014 would_withhold at the default, withheld->warning+run-passed under =1; negative controls: cause != wait_literal_only, a strict criterion graded failed, a would_fail URL re-read \u2014 the genuinely-wrong-page safety pin, and a step with no url candidate; flag-OFF byte-identical force-fail + census; the EXPERIMENT_ spelling honoured). Runner lane only. See docs/VERDICT-GATES.md.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-08-14",graduation:{status:"gated",gate:"Graduates on nightly evidence, not on unit tests alone: (1) the live recurring sample \u2014 the nightly RBAC 'Admin roles: User tier' plan (its nightly label stays for exactly this reason) \u2014 goes green WITHOUT any plan-text edit once the flag enforces; (2) a staging would_withhold census shows the rung firing on exactly the wait_literal_only + all-criteria-passed + url-would_pass shape and NEVER on a step whose URL re-read failed or floored (the false-pass direction must stay untouched); (3) the F2c eval family (runner-verify-redirect, #2403) is extended with the composite REPRODUCED-RED case and bound to batch.verify-never-blind.",evidence:"staging verify_conflict_url_grounded_withhold:would_withhold census rows (shape + count), the nightly RBAC User-tier plan verdicts, and the runner-verify-redirect eval extension",owner:"steering (Alex)",review:"2026-08-21"}},VERIFY_RECONCILE_CLEAN_PASS:{key:"VERIFY_RECONCILE_CLEAN_PASS",envVars:["AGENTIQA_VERIFY_RECONCILE_CLEAN_PASS","AGENTIQA_EXPERIMENT_VERIFY_RECONCILE_CLEAN_PASS"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Presentation of a VERIFY_CONFLICT_RECONCILE-reconciled verify step: on, the reconciled step is a clean PASS and the reconciliation is recorded only in the structured verification_conflict_reconciled diag event (no engine-jargon note on the user-facing step); off restores the legacy step-level WARNING plus the explanatory note. Independent of VERIFY_CONFLICT_RECONCILE, which decides WHETHER a conflict reconciles at all \u2014 this only changes how an already-reconciled step is surfaced.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-22"},REPLAY_STALE_HOST_NORMALIZE:{key:"REPLAY_STALE_HOST_NORMALIZE",envVars:["AGENTIQA_REPLAY_STALE_HOST_NORMALIZE","AGENTIQA_EXPERIMENT_REPLAY_STALE_HOST_NORMALIZE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Post-resolution stale-host criterion normalization (#2308, root of decision #2436). A saved plan authored against a developer machine and replayed in the cloud has its NAVIGATION target resolved to the real product origin at startup (startRun:resolvedInitialUrl), but its URL CRITERIA stayed pinned to the pre-resolution host \u2014 so the deterministic #1685 host floor correctly failed a criterion the run could never satisfy. Five production CLI replays of tp_28e7faec (2026-08-04 \u2026 08-16) logged in, reached https://gotogrow.me/dashboard, verified the account and balance, and were still reported FAILED against http://localhost:3000/dashboard. When on, RunnerRuntime.startRun rewrites \u2014 RUN-LOCALLY, after resolution and never before \u2014 only those criterion literals (check / expectedValue / basis.expected) whose authority equals the host resolution just replaced, and only when that replaced host is one classifyHost calls unreachable-from-here (localhost / loopback / RFC1918 / mDNS-local / link-local / unspecified) and the resolved host is public. Every other host is left untouched, so a TRUE host mismatch \u2014 including a staging\u2192prod environment inconsistency \u2014 still fails closed at the same floor. The authored plan is never mutated (nothing is written back to the customer's saved plan). Set to 0 and startRun is byte-identical to the pre-fix behavior.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-09-02",notes:"Ships default-ON from inception: the normalization removes a confirmed false-FAIL class and cannot manufacture a pass \u2014 it only re-points a pin at the environment the run actually visited, and the #1685 floor still does all the grading afterwards. There is therefore no pre-change OFF behavior worth preserving, and a killSwitch-disabled flag needs no INTENTIONALLY_GRADUATED entry (its no-env value already matches the pre-change hardcoded default). Read site: packages/engine-core/src/RunnerRuntime.ts (replayStaleHostNormalizeEnabled). Pure logic + acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.staleHostNormalize.test.ts. Diags: startRun:staleHostCriteriaNormalized (fired, with the per-criterion rewrites) / startRun:staleHostCriteriaKept (stood down, with the reason). KNOWN RESIDUAL: only CRITERIA are normalized \u2014 a verify step's prose text keeps the stale host, so the model's own grade note can still cite it; the deterministic floor, which is what recorded the downgrade, reads the criterion."},VERIFY_PRESENCE_WAIT_FLOOR:{key:"VERIFY_PRESENCE_WAIT_FLOOR",envVars:["AGENTIQA_VERIFY_PRESENCE_WAIT_FLOOR","AGENTIQA_EXPERIMENT_VERIFY_PRESENCE_WAIT_FLOOR"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Raises a verify-step presence oracle wait (wait_for_element on a `verify` plan step) to a minimum budget (20s) so a slow-rendering but PRESENT element \u2014 e.g. Miro\'s Templates carousel "Blank board" card, which paints several seconds after the dashboard is otherwise interactive \u2014 is not falsely failed by the 5s default wait budget (staging step-5 login/dashboard flake, sessions asess_1784961071757_cqwxon46 / asess_1784960734951_4t0zaxp1, where the next action successfully CLICKED "Blank board"). Floor-only: never lowers a larger model-supplied timeout; off restores the model-supplied / 5s-default budget. Fail-closed preserved \u2014 a genuinely-absent element still times out at the larger budget and records the same oracle failure, so no false-PASS is introduced. Applied in RunnerRuntime.raiseVerifyPresenceWaitBudget (Runner/test-plan lane only; Explorer/Coordinator have no plan steps).',designDoc:"packages/engine-core/src/verifyPresenceWaitBudget.ts",status:"active",added:"2026-07-25"},ABSENCE_AWARE_VERIFY:{key:"ABSENCE_AWARE_VERIFY",envVars:["AGENTIQA_ABSENCE_AWARE_VERIFY","AGENTIQA_EXPERIMENT_ABSENCE_AWARE_VERIFY"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Absence-assertion verify oracle (issue_742c9da8), a BEFORE\u2192AFTER differential. A verify step that asserts a NEGATIVE (the target is GONE) is verified with the presence-only wait_for_element, whose legitimate timeout on the correctly-absent, PLAN-GROUNDED literal is recorded as an oracle failure and force-FAILS the run at run_complete (canReconcileVerificationConflict refuses the plan-grounded literal \u2014 correct for a PRESENCE assertion, wrong for an absence one). Detection ALWAYS runs (shadow): for a conflicted verify step whose step text / any criterion check asserts THIS TARGET's absence (checkTextAssertsAbsence \u2014 the target literal is stripped first, then an EXPLICIT absence lexeme must survive; a negation inside the target NAME or an incidental 'not' cannot route it) and whose unresolved oracle is a wait-style action with a captured target literal, it classifies CONFIRMED vs ABSTAIN and emits an `absence_verify_oracle:shadow` diag. CONFIRMED requires the wait-literal's OWN before\u2192after transition: (a) GENUINELY ABSENT from a FULL, substantive, NON-canvas, non-load-failure AFTER snapshot retained for that step (never the model's note), AND (b) pinPresentInPage-TRUE in an EARLIER full+whole-page+substantive+same-origin BEFORE snapshot (the presence ledger \u2014 reusing the retained full-snapshot maps), AND every graded criterion passed substantiated. The weak container-noun positive anchor is DROPPED as the load-proof (before-presence + whole-page liveness replaces it); a load-failure/retry interstitial AFTER page is REJECTED (snapshotShowsLoadFailure). When this flag is ON it ENFORCES: a CONFIRMED absence reconciles the conflict to a PASS (via the VERIFY_CONFLICT_RECONCILE clean-pass rail), and an ABSTAIN (no full snapshot / canvas / target still present / load-failure after / NO before-presence \u2014 the target was never shown present / unsubstantiated criteria) soft-withholds the step to a WARNING (never a hard fail, never a clean pass). Off leaves every verdict byte-identical (the plan-grounded absence timeout still force-FAILs) with the shadow diag only. Positive (presence) assertions are untouched (checkTextAssertsAbsence false \u2192 inapplicable \u2192 the existing timeout-fails behavior). Runner lane only (RunnerRuntime run_complete).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-25",notes:"GRADUATED 2026-07-26 (default ON) \u2014 the FIRST live trust-verdict graduation. Enforcement (reconcile-to-pass on CONFIRMED / soft-withhold-to-warning on ABSTAIN) is now the no-env default; detection had run in shadow since 2026-07-25 (absence_verify_oracle:shadow diag) \u2014 the PIN_PAGE_GROUNDING / VERIFY_REOBSERVE_WITHHOLD shadow-first precedent. GRADUATION EVIDENCE: the kind-agnostic graduation benchmark (#1857, e2e/benchmark/) returned GATE=GO on the absence corpus (4 false-FAILs fixed \u2192 PASS, 0 regressions, 0 new false-PASS, 0 marginal LLM cost) and is adversarially proven able to say NO-GO; a fresh-build re-confirm held (confirmed-absence\u2192PASS fixes #1816 / unconfirmable\u2192WARNING / still-present\u2192no false-PASS; the full engine-core suite is byte-identical ON vs OFF except the graduated verdicts). HARD CONSTRAINT (unchanged): confirm fires ONLY on the wait-literal's OWN before\u2192after transition (present in an earlier same-origin full+substantive+non-canvas snapshot, absent from the non-load-failure after one) read from real page snapshots \u2014 never the model's note nor a disjoint absence lexeme \u2014 so a hallucinated 'it's gone', a never-loaded presence target (compound presence+absence), and a silently-blank list echoing the container noun all abstain to WARNING rather than confirming; no false-PASS is reintroduced. killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND ABSENCE_AWARE_VERIFY listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire); an explicit AGENTIQA_ABSENCE_AWARE_VERIFY=0 still restores the byte-identical pre-graduation force-FAIL behavior. Read site: packages/engine-core/src/RunnerRuntime.ts (absenceAwareVerifyEnabled). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.absenceVerifyFalseFail.test.ts (end-to-end, now green for the right reason) + RunnerRuntime.absenceVerifyHardening.test.ts. Claim runner.absence-assertion-verify is now bound green."},GROUNDED_STATE_VERIFIER:{key:"GROUNDED_STATE_VERIFIER",envVars:["AGENTIQA_GROUNDED_STATE_VERIFIER","AGENTIQA_EXPERIMENT_GROUNDED_STATE_VERIFIER"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Grounded-state verifier \u2014 S1 escalation gate + shadow instrumentation (Tier-2 vision-extraction cost sizing; design docs/plans/2026-07-25-grounded-state-verifier-design.md). SHADOW-ONLY / MEASUREMENT-ONLY: this slice makes NO model call and NEVER alters a verdict, verdict input, or any other diagnostic. When on, the runner's Tier-1 deterministic pin page-grounding oracle (checkPinPageGrounding), at each GROUNDABILITY abstain on a step carrying a countable expectedValue pin \u2014 canvas_dominant, a non-full/incremental a11y snapshot, or a thin/missing snapshot (NOT the transient/ephemeral-text abstain) \u2014 consults the surface-agnostic capture-groundability signal (captureModeGroundsAbsence, keyed on captureMode, NOT a surface-name check) and emits a structured `verifier_escalated` diag {stepIndex, reason, assertionKind, captureMode, wouldNeedTier2:true} for a capture a vision extractor could ground (the genuine Tier-2 candidate), or `verifier_escalation_abstained` {\u2026, wouldNeedTier2:false, floor:'inconclusive'} when even vision cannot ground it (the fail-closed floor \u2014 abstain, never escalate-and-guess). Off \u21D2 byte-identical to today: the escalation diags are not emitted and nothing else changes. Sizes S2's per-verify vision-extraction cost by measuring how often and WHERE Tier-1 abstains on countable state assertions.",designDoc:"docs/plans/2026-07-25-grounded-state-verifier-design.md",status:"active",added:"2026-07-25",notes:"S1 of the grounded-state verifier (measurement slice). Default OFF; SHADOW-ONLY and verdict-inert even when ON \u2014 this slice only emits verifier_escalated / verifier_escalation_abstained shadow diags at the checkPinPageGrounding abstain points, makes no model call, and changes no verdict. The escalation decision keys on captureModeGroundsAbsence (capture fidelity / checkPinPageGrounding outcome), NOT on canvasDominant/surface identity, so a non-canvas vision-groundable surface escalates through the same path (spec AC-7). Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateVerifierEnabled \u2192 maybeEmitVerifierEscalation, called from checkPinPageGrounding); pure logic in packages/engine-core/src/groundedStateVerifier.ts. Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.groundedStateVerifier.test.ts (wiring, flag-OFF byte-identical) + groundedStateVerifier.test.ts (pure groundability keying). S2 (prompt-only extractor + deterministic comparator) is the next slice and adds the actual Tier-2 call behind this same flag.",graduation:{status:"gated",gate:"S1 is measurement-only (no verdict change), so it graduates by FEEDING S2, not by flipping default-on: a staging shadow soak of verifier_escalated / verifier_escalation_abstained sizes the Tier-1-abstain-on-countable-state rate (per reason + captureMode) that S2 (prompt-only vision extractor + deterministic comparator, same flag) is built against. The flag advances to a real verdict path only under S2+ with its own verdict-parity shadow-soak; S1 alone never flips default-on.",evidence:"staging verifier_escalated / verifier_escalation_abstained diag events (escalation rate + reason/captureMode breakdown) + the engine-core RunnerRuntime.groundedStateVerifier + groundedStateVerifier unit suites",owner:"steering (Alex)",review:"2026-08-08"}},GROUNDED_STATE_EXTRACT:{key:"GROUNDED_STATE_EXTRACT",envVars:["AGENTIQA_GROUNDED_STATE_EXTRACT","AGENTIQA_EXPERIMENT_GROUNDED_STATE_EXTRACT"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Grounded-state verifier \u2014 S2 prompt-only vision EXTRACTOR + deterministic presence COMPARATOR (design docs/plans/2026-07-25-grounded-state-verifier-design.md). The sibling flag to GROUNDED_STATE_VERIFIER (S1's free measurement stays independently runnable). SHADOW-ONLY: when on AND a stateExtractor is wired, each S1 escalation candidate (a Tier-1 checkPinPageGrounding abstain on a vision-groundable capture carrying a PRESENCE assertion, deduped one-call-per-step) gets ONE no-task-stake vision extraction at run_complete that enumerates what is on screen into a FIXED schema (objects/text/labels/counts) \u2014 NEVER a verdict (AC-2: the prompt receives no assertion outcome and no pass/fail framing). A DETERMINISTIC comparator then decides presence of the plan-text-derived target against that extraction, reproducibly from the logged extraction + target without re-calling the model (AC-3), and the runtime LOGS the would-be verdict + its PARITY vs the driver's current grade (grounded_state_extract diag). This slice changes NO live verdict. FAIL-CLOSED (Decision 7): any missing image / extractor abstain / non-answer / error / timeout / ambiguous or thin comparison \u2192 INCONCLUSIVE, never a pass. Off \u21D2 zero extraction calls, no candidate collection, byte-identical behavior (AC-6). Cost: one extraction call per escalated presence step, hard-capped (fork E).",designDoc:"docs/plans/2026-07-25-grounded-state-verifier-design.md",status:"active",added:"2026-07-25",notes:"S2 of the grounded-state verifier (prompt-only extractor + deterministic presence comparator). Default OFF; SHADOW-FIRST \u2014 even when ON it only computes and LOGS the would-be presence verdict and its parity vs the driver grade (grounded_state_extract / _start / _done diags), makes at most ONE extraction model call per escalated step (fork-E hard cap), and changes NO verdict or stepResult. Sibling to GROUNDED_STATE_VERIFIER so S1 measurement runs without paying S2 cost. Requires deps.stateExtractor wired (getStateExtractor in apps/execution-engine/src/buildDeps.ts) \u2014 absent \u21D2 inert. Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateExtractEnabled \u2192 candidate recording in maybeEmitVerifierEscalation, consumed by runGroundedStateExtractions at run_complete); extractor + comparator in packages/engine-core/src/groundedStateExtractor.ts. Acceptance tests: packages/engine-core/src/__tests__/groundedStateExtractor.test.ts (pure prompt/parser/comparator/parity \u2014 AC-2/AC-3) + RunnerRuntime.groundedStateExtract.test.ts (wiring, shadow-no-mutation, fail-closed, one-call cap, seam guard, flag-OFF byte-identical). Binds claim verify.grounded-state-extract-then-compare. S3 (GROUNDED_STATE_DIFFERENTIAL) adds the before\u2192after differential for absence + modification.",graduation:{status:"gated",gate:"S2 is shadow-first (no verdict change). Graduation gates the PRESENCE case only, and only after: (1) a staging verdict-parity shadow soak of grounded_state_extract shows the extract-then-compare would-verdict matching the driver grade on DOM-groundable controls and DISAGREEing (would_fail on a driver-passed step) on the canvas false-pass fixtures (project_canvas_direct_draw_test) that the driver self-grade lets through today; (2) the extractor accuracy on the canvas presence fixtures clears the fork-G bar (else spec the fine-tuned extractor first). Advancing to a LIVE verdict path is a separate step from flipping this flag to shadow-on.",evidence:"staging grounded_state_extract / _start / _done diag events (would-verdict + parity + inconclusive-rate breakdown) + the engine-core groundedStateExtractor + RunnerRuntime.groundedStateExtract unit suites + the claim verify.grounded-state-extract-then-compare",owner:"steering (Alex)",review:"2026-08-15"}},GROUNDED_STATE_DIFFERENTIAL:{key:"GROUNDED_STATE_DIFFERENTIAL",envVars:["AGENTIQA_GROUNDED_STATE_DIFFERENTIAL","AGENTIQA_EXPERIMENT_GROUNDED_STATE_DIFFERENTIAL"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Grounded-state verifier \u2014 S3 BEFORE\u2192AFTER DIFFERENTIAL (absence \u2282 update) for ABSENCE + MODIFICATION (design docs/plans/2026-07-25-grounded-state-verifier-design.md). The sibling flag to GROUNDED_STATE_VERIFIER (S1 measurement) / GROUNDED_STATE_EXTRACT (S2 single-after presence). SHADOW-ONLY: when on AND a stateExtractor is wired, each S1 escalation candidate (a Tier-1 checkPinPageGrounding abstain on a vision-groundable capture carrying an ABSENCE or MODIFICATION assertion, deduped one-call-per-step) runs the S2 no-task-stake vision extraction on BOTH the persisted baseline (before, resolveBaselineMessage) and verify (after, resolveEvidenceMessage) frames \u2014 each with S2's IDENTICAL constant task-blind prompt (AC-2: task-blind on BOTH frames, no assertion / no expected value / no pass/fail framing) \u2014 and a DETERMINISTIC differential comparator decides the verdict from the two extractions + the plan-text-derived target, reproducibly without re-calling the model (AC-3). ABSENCE: target present-before \u2227 absent-after \u2192 would_pass (confirmed_absent); still present-after \u2192 would_fail; before-presence unestablished / after unreadable \u2192 inconclusive. MODIFICATION: a count that changed to the expected value, or a crisp new value that appeared (before-absent + after-present) \u2192 would_pass; unresolvable \u2192 inconclusive. The runtime LOGS the would-be differential verdict + its PARITY vs the driver grade (grounded_state_differential diag); this slice changes NO live verdict. FAIL-CLOSED (Decision 7): any missing-before / unreadable / extractor abstain / error / timeout / ambiguous or unresolvable comparison \u2192 INCONCLUSIVE, never a pass. Canvas is IN scope (the extractor is vision; before/after frames exist via blind-double-read), identified by the plan DESCRIPTOR (fork D1) with an ambiguous match abstaining to inconclusive \u2014 never re-identifying an anonymous object. Off \u21D2 zero candidate collection, zero extraction calls, byte-identical behavior (AC-6). Cost: at most TWO extraction calls per escalated differential step (before + after, fork-E bounded call budget), hard-capped, batched under a deadline.",designDoc:"docs/plans/2026-07-25-grounded-state-verifier-design.md",status:"active",added:"2026-07-25",notes:'S3 of the grounded-state verifier (before\u2192after differential; absence \u2282 update). Default OFF; SHADOW-FIRST \u2014 even when ON it only computes and LOGS the would-be differential verdict and its parity vs the driver grade (grounded_state_differential / _start / _done diags), makes at most TWO extraction model calls per escalated step (before + after; fork-E bounded budget, hard-capped, batched under a deadline \u2192 timeout inconclusive), and changes NO verdict or stepResult. Sibling to GROUNDED_STATE_VERIFIER (S1) / GROUNDED_STATE_EXTRACT (S2). Requires deps.stateExtractor wired (getStateExtractor in apps/execution-engine/src/buildDeps.ts, reused per-frame) \u2014 absent \u21D2 inert. Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateDifferentialEnabled \u2192 differential-candidate recording in maybeEmitVerifierEscalation, consumed by runGroundedStateDifferentials at run_complete); pure differential comparator in packages/engine-core/src/groundedStateDifferential.ts (reuses S2 groundedStateExtractor). Acceptance tests: packages/engine-core/src/__tests__/groundedStateDifferential.test.ts (pure differential \u2014 absence/modification/fail-closed, #1816 RED\u2192GREEN comparator soak hook + green-guard, AC-2/AC-3) + RunnerRuntime.groundedStateDifferential.test.ts (wiring, before+after two-frame extraction, shadow-no-mutation, fail-closed on missing-before/unreadable/timeout, one-differential-per-step cap, canvas descriptor-match, flag-OFF byte-identical). Binds claim verify.grounded-state-extract-then-compare. S4 (GROUNDED_STATE_INCONCLUSIVE) maps verifier-abstain \u2192 the inconclusive floor. Trust-layer Slice 1 (routing-gap fix) ALSO gates a STATIC single-frame COUNT comparator on this same flag: a count-intent assertion ("exactly 3 shapes" \u2014 authored factKind:count or an NL count check) escalates a count candidate and, at run_complete, runGroundedStateCounts runs ONE task-blind extraction + the deterministic compareCount (extracted 4 \u2260 expected 3 \u2192 grounded_state_count would_fail(after_count_mismatch); 3 = 3 \u2192 would_pass(confirmed_count)); shadow-only, fail-closed, one-call-per-step. Acceptance: RunnerRuntime.groundedStateCount.test.ts + the compareCount/checkTextAssertsCount cases in groundedStateDifferential.test.ts; end-to-end firewall proof eval runner-verify-count-canvas.',graduation:{status:"gated",gate:"S3 is shadow-first (no verdict change). Graduation gates the ABSENCE + MODIFICATION differential and only after: (1) a staging verdict-parity shadow soak of grounded_state_differential shows the #1816 reproduced-RED absence scenario would flip RED\u2192GREEN under the differential (confirmed_absent \u2192 would_pass on a driver-failed step) WITHOUT regressing the paired green-guard fixture (target-still-present \u2192 would_fail / driver-pass preserved); (2) new MODIFICATION fixtures (count + value-appearance) shadow-soak clean; (3) the extractor accuracy on the canvas absence/modification fixtures clears the fork-G bar. Advancing to a LIVE verdict path is a separate step from flipping this flag to shadow-on.",evidence:"staging grounded_state_differential / _start / _done diag events (would-verdict + parity + inconclusive-rate breakdown, #1816 RED\u2192GREEN) + the engine-core groundedStateDifferential + RunnerRuntime.groundedStateDifferential unit suites + the claim verify.grounded-state-extract-then-compare",owner:"steering (Alex)",review:"2026-08-22"}},GROUNDED_STATE_INCONCLUSIVE:{key:"GROUNDED_STATE_INCONCLUSIVE",envVars:["AGENTIQA_GROUNDED_STATE_INCONCLUSIVE","AGENTIQA_EXPERIMENT_GROUNDED_STATE_INCONCLUSIVE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Grounded-state verifier \u2014 S4 FAIL-CLOSED INCONCLUSIVE FLOOR wiring (Decision 7 + Fork F2; design docs/plans/2026-07-25-grounded-state-verifier-design.md). The sibling flag to GROUNDED_STATE_VERIFIER (S1) / GROUNDED_STATE_EXTRACT (S2) / GROUNDED_STATE_DIFFERENTIAL (S3). SHADOW-ONLY: when on, each verifier ABSTAIN \u2014 the S1 escalation fail-closed floor (verifier_escalation_abstained, a capture even a vision extractor cannot ground) and every S2 presence / S3 differential `inconclusive` outcome (thin/unreadable capture, extractor refusal/error/timeout, before-presence unestablished, after unreadable, ambiguous match, unresolved count) \u2014 is ALSO mapped to the typed inconclusive floor and LOGGED (grounded_state_inconclusive diag): the DISTINCT `verifier_inconclusive:{reason}` sub-reason + the interim `warning` step status (Fork F2 \u2014 a NEUTRAL 'couldn't tell', NOT an alarm-amber defect per project_warning_display_semantics) + the verifierInconclusive marker + migratesTo:'inconclusive'. This slice mutates NO stepResult and changes NO verdict \u2014 it computes/LOGS the would-be floor mapping only. HONESTY FLOORS: an abstain NEVER maps to `passed` (VERIFIER_INCONCLUSIVE_STEP_STATUS is warning/inconclusive, never passed), and a genuine would_pass/would_fail is never floored (inconclusiveFloorForVerdict returns null on a non-inconclusive verdict; the wiring only fires on the S2/S3 abstain paths + the S1 abstained floor). ONE migration point: flip VERIFIER_INCONCLUSIVE_STEP_STATUS (groundedStateInconclusive.ts) from `warning` to step-level `inconclusive` when Layered-Hybrid Phase-1 lands that status in the step enum. Off \u21D2 zero mapping, zero logs, byte-identical (AC-6).",designDoc:"docs/plans/2026-07-25-grounded-state-verifier-design.md",status:"active",added:"2026-07-25",notes:"S4 of the grounded-state verifier (fail-closed inconclusive floor; interim warning-with-distinct-sub-reason, Fork F2). Default OFF; SHADOW-FIRST \u2014 even when ON it only computes and LOGS the would-be inconclusive floor mapping (grounded_state_inconclusive diag), makes NO model call (pure wiring of S1/S2/S3 abstain outcomes), and changes NO verdict or stepResult. Sibling to GROUNDED_STATE_VERIFIER (S1) / GROUNDED_STATE_EXTRACT (S2) / GROUNDED_STATE_DIFFERENTIAL (S3); the abstains it maps only exist when those slices run, so S4 is additive telemetry on top of them. Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateInconclusiveEnabled \u2192 maybeLogInconclusiveFloor, called at the S1 verifier_escalation_abstained floor in maybeEmitVerifierEscalation + the S2/S3 logInconclusive abstain choke points in runGroundedStateExtractions / runGroundedStateDifferentials); pure mapping in packages/engine-core/src/groundedStateInconclusive.ts (the ONE migration point VERIFIER_INCONCLUSIVE_STEP_STATUS). Acceptance tests: packages/engine-core/src/__tests__/groundedStateInconclusive.test.ts (pure \u2014 each abstain reason \u2192 typed sub-reason \u2192 interim warning-never-passed, distinct-from-product-warning, migration seam, would_pass/would_fail never floored) + RunnerRuntime.groundedStateInconclusive.test.ts (wiring \u2014 flag-OFF byte-identical, shadow-no-mutation, floor logged per S1/S2/S3 abstain, no floor on a would_pass step). Binds claim verify.grounded-state-extract-then-compare. S5 broadens the groundability surface + fine-tune trigger.",graduation:{status:"gated",gate:"S4 is shadow-first (no verdict change) and the INTERIM F2 mapping (warning-with-distinct-sub-reason). Graduation to a LIVE floor is a SEPARATE, later step from flipping this flag shadow-on and requires: (1) a staging shadow soak of grounded_state_inconclusive confirming the abstain\u2192floor breakdown (source / reason / rate) is sane and that no would_pass/would_fail is ever floored (the honesty invariants hold in the field); (2) the S2/S3 verdict-parity soaks having graduated their live-verdict paths (an inconclusive floor is only meaningful once the extract-then-compare verdicts gate); (3) the UI rendering the verifier_inconclusive sub-reason as a NEUTRAL could-not-tell (not alarm-amber). The clean F2\u2192F1 migration (flip VERIFIER_INCONCLUSIVE_STEP_STATUS warning\u2192inconclusive at the ONE point) lands when Layered-Hybrid Phase-1 ships the step-level inconclusive status.",evidence:"staging grounded_state_inconclusive diag events (abstain\u2192floor mapping: source/reason/sub-reason/interim-status breakdown, honesty-invariant field check) + the engine-core groundedStateInconclusive + RunnerRuntime.groundedStateInconclusive unit suites + the claim verify.grounded-state-extract-then-compare",owner:"steering (Alex)",review:"2026-08-29"}},GROUNDED_STATE_FINETUNE_METRIC:{key:"GROUNDED_STATE_FINETUNE_METRIC",envVars:["AGENTIQA_GROUNDED_STATE_FINETUNE_METRIC","AGENTIQA_EXPERIMENT_GROUNDED_STATE_FINETUNE_METRIC"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Grounded-state verifier \u2014 S5 FINE-TUNE TRIGGER METRIC (Fork G: DEFINE the trigger, do NOT fine-tune; design docs/plans/2026-07-25-grounded-state-verifier-design.md). The final sibling flag to GROUNDED_STATE_VERIFIER (S1) / GROUNDED_STATE_EXTRACT (S2) / GROUNDED_STATE_DIFFERENTIAL (S3) / GROUNDED_STATE_INCONCLUSIVE (S4). SHADOW-ONLY: when on AND an S2/S3 shadow batch runs, the runtime ACCUMULATES per-SURFACE outcome counts from the extract-then-compare batches \u2014 extractor-abstain (the prompt-only extractor produced no facts), comparator-inconclusive (facts extracted but no definite verdict), and parity agree/disagree \u2014 bucketed by the escalation reason \u2192 surface class (canvas_dominant\u2192canvas, thin\u2192image_or_svg, incremental/non_full\u2192partial_capture; a DIAGNOSTIC label over the already-recorded reason, NOT a surface-name gate \u2014 the escalation gate stays keyed on captureModeGroundsAbsence, Fork A1). After both batches it LOGS the evaluated fine-tune trigger report against the STATED bar (grounded_state_finetune_metric diag): per surface the extractor-abstain rate, inconclusive rate, disagreement rate, and a would-trigger-fine-tune flag (Fork-G graduation signal, ADVISORY). This makes the prompt-only\u2192dedicated/fine-tuned graduation a DATA read; prompt-only stays v1 (Decision 6) \u2014 this slice invests in NO model, mutates NO stepResult, and changes NO verdict. The metric only has samples to fold when S2 and/or S3 also run. Off \u21D2 zero accumulation, zero logs, byte-identical (AC-6). The STATED bar: per surface, after live graduation, extractor-abstain rate > 0.20 OR inconclusive rate > 0.40 over \u2265 50 escalated extract-then-compares triggers a dedicated/fine-tuned extractor FOR THAT SURFACE (parity-disagreement is reported but NOT a trigger input \u2014 a high disagreement can be the extractor CATCHING driver false-passes, the desired signal).",designDoc:"docs/plans/2026-07-25-grounded-state-verifier-design.md",status:"active",added:"2026-07-25",notes:"S5 of the grounded-state verifier (fine-tune trigger metric; Fork G define-not-fine-tune) and the LAST vision-tier slice. Default OFF; SHADOW-ONLY \u2014 even when ON it only accumulates the per-surface outcome breakdown from the S2/S3 shadow batches and LOGS the evaluated trigger report (grounded_state_finetune_metric diag), makes NO model call (pure metric over existing S2/S3 outcomes), and changes NO verdict or stepResult. Sibling to GROUNDED_STATE_VERIFIER (S1) / GROUNDED_STATE_EXTRACT (S2) / GROUNDED_STATE_DIFFERENTIAL (S3) / GROUNDED_STATE_INCONCLUSIVE (S4); the outcomes it folds only exist when S2/S3 run, so S5 is additive telemetry on top of them. The companion BROADEN half of S5 (SVG/image coverage, spec AC-7 generalized) needed NO gate change \u2014 the S1/S2/S3 path already keys on captureModeGroundsAbsence (Fork A1), so broadening is a fixture/coverage add, proven by the RunnerRuntime.groundedStateBroaden test. Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateFineTuneMetricEnabled \u2192 the run_complete accumulator threaded into runGroundedStateExtractions / runGroundedStateDifferentials, emitted by emitFineTuneTriggerMetric); pure metric + STATED bar in packages/engine-core/src/groundedStateFineTuneTrigger.ts (FINETUNE_TRIGGER_BAR). Acceptance tests: packages/engine-core/src/__tests__/groundedStateFineTuneTrigger.test.ts (pure surface mapping / fold / rates / bar evaluation) + RunnerRuntime.groundedStateFineTuneMetric.test.ts (wiring \u2014 per-surface fold from S2/S3, flag-OFF byte-identical, shadow-no-mutation) + RunnerRuntime.groundedStateBroaden.test.ts (AC-7 generalized: a non-canvas SVG/image surface escalates + extracts + compares through the identical path). Binds claim verify.grounded-state-extract-then-compare. COMPLETES the S1\u2013S5 vision-tier build; remaining work is soak + graduation flips (Alex).",graduation:{status:"gated",gate:"S5 is shadow-first (no verdict change) and DEFINES the Fork-G fine-tune trigger \u2014 it does not fine-tune. The metric graduates by FEEDING the fine-tune decision, not by flipping default-on: a staging shadow soak of grounded_state_finetune_metric measures each surface (canvas / image_or_svg / partial_capture) extractor-abstain + inconclusive rate against the STATED bar (FINETUNE_TRIGGER_BAR: abstain > 0.20 OR inconclusive > 0.40 over \u2265 50 escalated steps). Investing in a dedicated/fine-tuned extractor for a surface is triggered ONLY when that surface clears the bar AFTER the S2/S3 extract-then-compare has graduated live on it (Decision 6: prompt-only ships regardless). This flag alone never flips default-on.",evidence:"staging grounded_state_finetune_metric diag events (per-surface extractor-abstain / inconclusive / disagreement rate + would-trigger evaluation vs FINETUNE_TRIGGER_BAR) + the engine-core groundedStateFineTuneTrigger + RunnerRuntime.groundedStateFineTuneMetric + RunnerRuntime.groundedStateBroaden unit suites + the claim verify.grounded-state-extract-then-compare",owner:"steering (Alex)",review:"2026-09-05"}},GROUNDED_STATE_NL_TRIGGER:{key:"GROUNDED_STATE_NL_TRIGGER",envVars:["AGENTIQA_GROUNDED_STATE_NL_TRIGGER","AGENTIQA_EXPERIMENT_GROUNDED_STATE_NL_TRIGGER"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Groundability-contract Slice 1 \u2014 un-inert the grounded-state verifier for PINLESS criteria (design docs/plans/2026-07-26-groundability-contract-design.md). The Tier-1 pin page-grounding oracle (checkPinPageGrounding) early-returns when a criterion carries no expectedValue pin, so a real/canvas plan's natural-language {check,strict} assertion never reached the S1 escalation path \u2014 the grounded-state verifier was provably INERT on exactly the canvas surfaces (Miro/Figma) it was built for. When on, a PINLESS verify step whose RUNTIME fact-kind (deriveVerifyAssertionKind) is a non-default state-change kind \u2014 `absence` or `modification` \u2014 routes through the SAME capture-groundability abstain\u2192escalate logic the pinned path uses (maybeEmitVerifierEscalation \u2192 buildVerifierEscalation), keyed on the IDENTICAL captureModeGroundsAbsence gate (a capture even a vision extractor cannot ground abstains to the fail-closed inconclusive floor, never escalate-and-guess). F2 ANTI-FLOOD: the DEFAULT `presence` kind is EXCLUDED \u2014 deriveVerifyAssertionKind defaults to presence for everything, so escalating on presence would flood every pinless ungroundable step; only absence/modification (which the classifier returns deliberately) escalate in Slice 1 (authored count/value are Slice 2). ESCALATION-TRIGGER ONLY: matchType/comparison/expectedValue-as-comparator are untouched, this is shadow/verdict-inert like S1\u2013S5, and it emits nothing on its own \u2014 the escalation diag / candidate still requires GROUNDED_STATE_VERIFIER / _EXTRACT / _DIFFERENTIAL / _INCONCLUSIVE. Off \u21D2 byte-identical: the pinless early-return stands, zero new telemetry, zero behavior change (AC-6).",designDoc:"docs/plans/2026-07-26-groundability-contract-design.md",status:"active",added:"2026-07-25",notes:"Groundability-contract Slice 1 (PR-A). Default OFF; SHADOW-ONLY and verdict-inert even when ON \u2014 it only relocates the S1 escalation TRIGGER for pinless criteria off the expectedValue pin onto the existing runtime fact-kind classifier (deriveVerifyAssertionKind). NO schema change (migration-safe). The escalate-vs-abstain disposition stays keyed on the exact captureModeGroundsAbsence gate reused from the pinned path (GUARDRAIL 1: no pinless bypass). Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateNlTriggerEnabled \u2192 maybeEmitPinlessNlEscalation, called at the checkPinPageGrounding pinless early-return; routes through the shared maybeEmitVerifierEscalation). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.groundedStateNlTrigger.test.ts (pinless absence/modification escalates on ungroundable canvas/thin/non_full; groundable-mode capture abstains to the inconclusive floor NOT escalate; presence pinless never escalates \u2014 F2 anti-flood; flag-OFF byte-identical; pinned path unchanged). OVERLAP NOTE: ABSENCE_AWARE_VERIFY (#1818, classifyAbsenceVerifyConflict / wait-oracle literal) is a DISTINCT pin-independent absence route \u2014 both are shadow/verdict-inert, so absence carries a DOUBLE shadow signal; do not double-count in soak evidence. Slice 2 adds authored count/value kinds.",graduation:{status:"gated",gate:"Slice 1 is measurement-only (no verdict change): it graduates by FEEDING the same S2/S3 extract-then-compare shadow soak \u2014 a staging shadow soak of verifier_escalated / verifier_escalation_abstained on PINLESS absence/modification steps (canvas/real plans) sizes the escalation rate the vision tier is built against, previously unmeasurable because pinless steps never escalated. The flag advances to a real verdict path only under S2+ with its own verdict-parity shadow-soak; Slice 1 alone never flips default-on. Slice 2 (authored count/value kinds) is a separate gated slice.",evidence:"staging verifier_escalated / verifier_escalation_abstained diag events on pinless absence/modification steps (escalation rate + reason/captureMode breakdown, de-duplicated against the ABSENCE_AWARE_VERIFY absence shadow signal) + the engine-core RunnerRuntime.groundedStateNlTrigger unit suite",owner:"steering (Alex)",review:"2026-08-15"}},GROUNDED_STATE_FACTKIND_AUTHORED:{key:"GROUNDED_STATE_FACTKIND_AUTHORED",envVars:["AGENTIQA_GROUNDED_STATE_FACTKIND_AUTHORED","AGENTIQA_EXPERIMENT_GROUNDED_STATE_FACTKIND_AUTHORED"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Groundability-contract Slice 2 \u2014 consume the MODEL-authored `factKind` as source-of-truth for the grounded-state escalation trigger's assertion shape (design docs/plans/2026-07-26-groundability-contract-design.md). Builds on Slice 1 (GROUNDED_STATE_NL_TRIGGER, pinless escalation on runtime absence/modification). The new OPTIONAL `factKind` enum (value|count|presence|absence|modification|relation) on TestPlanV2Criterion is authored by the generation model (all three producer schemas) as a deliberate typed choice (spawn_agent authMode/authSurfaceKind precedent). When on, a criterion carrying an AUTHORED factKind (a) OVERRIDES the runtime deriveVerifyAssertionKind inference (mapped 6\u21923 for the escalation branch, Fork F1: absence\u2192absence, modification\u2192modification, value/count/presence/relation\u2192presence) AND (b) LIFTS Slice 1's F2 anti-flood filter for that step so ANY authored kind escalates \u2014 authoring IS the deliberate groundable-fact signal \u2014 still gated by the IDENTICAL captureModeGroundsAbsence floor (a capture even a vision extractor cannot ground abstains to the fail-closed inconclusive floor, NEVER escalate-and-guess), routed per collapsed kind (absence/modification \u2192 S3 differential candidate; value/count/presence/relation \u2192 S2 presence-family extraction candidate). This fixes the count/canvas case (authored count \u2192 escalates to S2 extraction) WITHOUT the runtime-presence flood. UNAUTHORED criteria keep Slice 1's runtime path EXACTLY (only absence/modification escalate; the runtime `presence` default does NOT). matchType/comparison/expectedValue-as-comparator UNTOUCHED; SHADOW-ONLY / verdict-inert like S1\u2013S5; it emits nothing on its own \u2014 the escalation diag / candidate still requires GROUNDED_STATE_VERIFIER / _EXTRACT / _DIFFERENTIAL / _INCONCLUSIVE. Off (or factKind absent) \u21D2 byte-identical: the field is unread, Slice 1's runtime behavior stands (AC-6). The field may be authored + persisted with this flag OFF (harmless, unread).",designDoc:"docs/plans/2026-07-26-groundability-contract-design.md",status:"active",added:"2026-07-25",notes:"Groundability-contract Slice 2 (PR-B). Default OFF; SHADOW-ONLY and verdict-inert even when ON. Read sites: packages/engine-core/src/RunnerRuntime.ts (groundedStateFactKindAuthoredEnabled \u2192 authoredFactKind, consumed by deriveVerifyAssertionKind override + the pinless F2-filter lift in maybeEmitPinlessNlEscalation). Field: packages/shared-types/src/index.ts (FactKind + TestPlanV2Criterion.factKind); producer schemas: coordinatorToolDefs.ts test_plan_criteria_schema, agentToolDefs.ts criteria_schema, runnerToolDefs.ts step_with_criteria_schema (thin inline); generation steer: planStepGuidance.ts buildCriteriaFactKindGuidance; 6\u21923 mapping + type-guard: groundedStateVerifier.ts factKindToAssertionKind / isAuthoredFactKind. DROP-SITES registered (else a plain step-text edit silently wipes the authored field \u2014 the matchType bug): renderer finalizeCriterion (apps/desktop-next/.../testPlanStepsSerde.ts) + server carryCriterionPins (apps/web-next/lib/testPlanSaveNormalize.ts) \u2014 both preserve factKind on an UNCHANGED check, drop it on an EDITED check (\u2192 runtime fallback, safe). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.groundedStateFactKindAuthored.test.ts (authored count/presence on ungroundable canvas \u2192 S2 extraction candidate; authored absence \u2192 S3 differential candidate; authored + degraded capture \u2192 inconclusive floor, no escalate-and-guess; flag OFF \u2192 byte-identical field-unread Slice-1 behavior; field absent \u2192 Slice-1 behavior) + drop-site round-trip tests in testPlanStepsSerde.test.ts / testPlanSaveNormalize.test.ts. The MANDATORY real-runtime generation eval + the qa-box claim verify.groundability-contract-authored are Slice-2b (separate follow-on), NOT this PR. OVERLAP NOTE: composes with GROUNDED_STATE_NL_TRIGGER (Slice 1) via the shared maybeEmitPinlessNlEscalation helper.",graduation:{status:"gated",gate:"Slice 2 is measurement-only (no verdict change): it graduates by FEEDING the same S2/S3 extract-then-compare shadow soak, now widened to authored count/value/presence/relation kinds (the count/canvas case Slice 1 could not reach because runtime presence does not escalate). It advances to a real verdict path only under S2+ with its own verdict-parity shadow-soak; Slice 2 alone never flips default-on. Requires the Slice-2b generation eval (real ExplorerRuntime/CoordinatorRuntime authoring the correct factKind) to gate the authoring quality before any graduation.",evidence:"staging verifier_escalated / verifier_escalation_abstained diag events on PINLESS authored-factKind steps (escalation rate + factKind/reason/captureMode breakdown) + the engine-core RunnerRuntime.groundedStateFactKindAuthored unit suite; graduation additionally blocked on the Slice-2b real-runtime generation eval (authoring-distribution baseline)",owner:"steering (Alex)",review:"2026-08-15"}},GROUNDED_STATE_FACTKIND_TYPING_PASS:{key:"GROUNDED_STATE_FACTKIND_TYPING_PASS",envVars:["AGENTIQA_GROUNDED_STATE_FACTKIND_TYPING_PASS","AGENTIQA_EXPERIMENT_GROUNDED_STATE_FACTKIND_TYPING_PASS"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Groundability-contract Slice 2b \u2014 MODEL-authored focused second-pass factKind typing that FIXES the realistic-authoring gap (design docs/plans/2026-07-26-groundability-contract-design.md). Slice 2 (GROUNDED_STATE_FACTKIND_AUTHORED) consumes an authored `factKind`, but the live ExplorerRuntime authors it on ~0% of criteria produced by a realistic multimodal explore (screenshots + long trace + coordinator\u2192explorer spawn = the load that suppresses a deeply-nested OPTIONAL enum) \u2014 MEASURED 0/8 on e2e/evals/plans/groundability-factkind-explored.ts, and the three natural schema fixes (imperative guidance, required field, field reorder) ALL stayed 0%. But a FOCUSED low-load typing call over JUST the drafted check-texts types factKind at 100% (probe 78/78, real gemini-3-flash-preview). When on, AFTER the Explorer's draftTestCase is finalized (assistant_v2_report accept seam, past every re-prompt gate), a SEPARATE model-authored pass (packages/engine-core/src/factKindTypingPass.ts runFactKindTypingPass) collects the verify criteria that LACK an authored factKind, makes ONE batched Gemini call (the session's own model + the REAL buildCriteriaFactKindGuidance the producer schema ships; generateText + Output.object, thinkingBudget:0) that returns {index,factKind}[], and writes the kind back onto each criterion IN PLACE. It is MODEL-authored (a real Gemini call), NOT the rejected deterministic runtime factKind inference. NO OVERWRITE: only a MISSING factKind is filled (an already-authored kind is skipped at collect + double-guarded at write-back). FAIL-SAFE: any error/empty/timeout/invalid-kind leaves factKind UNSET (falls through to runtime inference) \u2014 never crashes authoring, never writes a garbage kind. SHADOW-SAFE: the written factKind is read ONLY by the separately flag-gated GROUNDED_STATE_FACTKIND_AUTHORED verifier (default-OFF), so verdicts are byte-identical whether or not this pass ran. Off \u21D2 the caller never invokes the module: ZERO new model calls, byte-identical authoring (AC-6).",designDoc:"docs/plans/2026-07-26-groundability-contract-design.md",status:"active",added:"2026-07-26",notes:"Groundability-contract Slice 2b (the realistic-authoring FIX; a flag-gated prototype \u2014 adding a model call to the authoring flow is Alex's architecture/graduation call). Default OFF; when ON adds ONE auxiliary Gemini call per COMPLETED explore that produced untyped verify criteria (batched, text-only, cost-isolated via emitAuxiliaryLlmUsage \u2014 not a billable step). Read site: packages/engine-core/src/ExplorerRuntime.ts assistant_v2_report accept seam (killSwitchEnabled('GROUNDED_STATE_FACTKIND_TYPING_PASS') \u2192 runFactKindTypingPass over draftTestCase.steps, mutating criteria in place BEFORE the report message is persisted/emitted, so the authored factKind rides both the saved plan and the diag). Pure module core (collectUntypedVerifyCriteria / buildFactKindTypingPrompt / mapTypesByIndex / applyFactKindTypes) + the single generateText seam. Unit suite: packages/engine-core/src/__tests__/factKindTypingPass.test.ts (ON fills missing factKind from a mocked typing response; OFF = no call / byte-identical; already-authored factKind never overwritten; error/empty response \u2192 factKind stays unset, fail-safe). RE-MEASURE: e2e/evals/plans/groundability-factkind-explored.ts COMMITTED_BASELINE carries the with-typing-pass authored-rate alongside the without (0/8).",graduation:{status:"gated",gate:"The realistic-authoring re-measure (groundability-factkind-explored with GROUNDED_STATE_FACTKIND_TYPING_PASS ON) shows the authored-factKind rate on the real explore path jump from ~0% to high (target near the probe 100%, \u2265~85%) with CORRECT kinds, AND the engine-core factKindTypingPass unit suite green (fill / no-overwrite / fail-safe / off-byte-identical). Graduation to any verdict path additionally requires GROUNDED_STATE_FACTKIND_AUTHORED (the consumer) to graduate under its own shadow-soak \u2014 this pass only PRODUCES the field.",evidence:"e2e/evals/plans/groundability-factkind-explored.ts with-typing-pass measuredAt entry (authored-rate + kind-correctness) + the engine-core factKindTypingPass unit suite + factkind_typing_pass diag events",owner:"steering (Alex)",review:"2026-08-15"}},GROUNDED_STATE_UNIFIED:{key:"GROUNDED_STATE_UNIFIED",envVars:["AGENTIQA_GROUNDED_STATE_UNIFIED","AGENTIQA_EXPERIMENT_GROUNDED_STATE_UNIFIED"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Trust-layer Slice 2 \u2014 PER-SNAPSHOT UNIFICATION (design docs/plans/2026-07-26-trust-layer-slice-2-per-snapshot-unification-design.md; north star docs/plans/2026-07-26-trust-layer-verification-architecture.md). Collapses the three Slice-1 per-kind run_complete batches (runGroundedStateCounts / runGroundedStateExtractions / runGroundedStateDifferentials \u2014 each re-extracting once PER step/kind) into ONE task-blind extraction PER SNAPSHOT feeding ONE typed-comparator dispatch, and introduces the verdict SPECTRUM (Verified / Assessed / Inconclusive) + semantic matchType. SHADOW-ONLY: when on AND a stateExtractor is wired, it consumes the SAME escalation candidates the per-kind batches do (its flag is OR-ed into the maybeEmitVerifierEscalation candidate-recording seam), groups them by shared captured AFTER-frame (groupCandidatesBySnapshot \u2014 verify-steps sharing a frame with no intervening state-mutating action share ONE extraction; differential candidates additionally share ONE run-start baseline extraction), runs ONE extraction per unique frame, and for each candidate runs its typed comparator (compareCount / comparePresence / compareAbsenceDifferential / compareModificationDifferential) \u2014 or, for a matchType:'semantic' criterion, the task-blind concept classifier (deps.conceptClassifier) \u2014 against that shared extraction. It LOGS grounded_state_unified (band + shadowVerdict + parity vs the driver grade) + grounded_state_unified_start/_done (the extraction-vs-comparison counts that PROVE 1-extraction-per-snapshot); it mutates NO stepResult and changes NO verdict. Part B is a REFACTOR that must be verdict-PARITY with the three per-kind paths: the unified would-verdict equals what the retired per-kind batch logged (same evidence resolution + same target derivation + same deterministic comparator on the same extraction). The verdict spectrum: a deterministic comparator would_pass/would_fail \u2192 Verified (the no-false-positive guarantee); a low-confidence semantic result \u2192 Assessed (the SLOT only \u2014 the independent reasoned judge is a LATER slice); no confident extraction/classification / missing image / extractor abstain / error / timeout / unresolvable comparison / unavailable semantic classifier \u2192 the fail-closed Inconclusive floor, NEVER a pass. Task-blindness preserved: the extraction prompt (buildExtractionQuestion) carries observation targets only and the semantic classifier carries a concept + a neutral observation rendering \u2014 NEVER the expected values or pass/fail framing. It runs ALONGSIDE the per-kind batches (its own flag) so the shadow soak can prove parity BEFORE the per-kind methods are physically retired. Off \u21D2 zero candidate consumption here, zero extraction calls, byte-identical (the per-kind paths, if their flags are on, are untouched) (AC-6). Cost: #snapshots (+1 shared before, if any differential) extraction calls per run \u2014 strictly \u2264 the sum of the three per-kind batches, and 1 for N verify-steps on one screen.",designDoc:"docs/plans/2026-07-26-trust-layer-slice-2-per-snapshot-unification-design.md",status:"active",added:"2026-07-26",notes:"Trust-layer Slice 2 (per-snapshot unification). Default OFF; SHADOW-FIRST \u2014 even when ON it only computes + LOGS the would-be verdict-spectrum result and its parity vs the driver grade (grounded_state_unified / _start / _done diags), makes #snapshots (+1 shared before) extraction calls plus one concept-classifier call per semantic candidate (both cost-isolated Flash-model seams via emitAuxiliaryLlmUsage, hard-capped, batched under the same deadline as the per-kind batches), and changes NO verdict or stepResult. Sibling to GROUNDED_STATE_EXTRACT (S2) / GROUNDED_STATE_DIFFERENTIAL (S3) \u2014 it consumes the SAME candidate maps and reuses the SAME comparators, so its would-verdicts are byte-parity with the three per-kind batches (the Slice-2 acceptance gate). Requires deps.stateExtractor wired; a matchType:'semantic' criterion additionally requires deps.conceptClassifier (absent \u21D2 that candidate floors to Inconclusive). Read site: packages/engine-core/src/RunnerRuntime.ts (groundedStateUnifiedEnabled \u2192 the OR-ed candidate recording in maybeEmitVerifierEscalation + collectUnifiedCandidates + runGroundedStateUnified at run_complete); pure core (snapshot grouping / verdict spectrum / semantic mapping) in packages/engine-core/src/groundedStateUnified.ts. Acceptance tests: packages/engine-core/src/__tests__/groundedStateUnified.test.ts (pure \u2014 grouping, spectrum mapping, semantic mapping) + RunnerRuntime.groundedStateUnified.test.ts (wiring \u2014 verdict-PARITY vs the three per-kind batches, ONE extraction for N\u22653 verify-steps on one snapshot, semantic success\u2192Verified / ambiguous\u2192Assessed / error\u2192Verified-FAIL, shadow-no-mutation, flag-OFF byte-identical). Evals: e2e/evals/plans/trust-unified-parity.ts (runner-trust-unified-parity), trust-unified-batching.ts (runner-trust-unified-batching), trust-unified-semantic.ts (runner-trust-unified-semantic). The three per-kind batch methods are RETAINED in this slice as the parity oracle; their physical removal is the graduation follow-up. Binds claim verify.grounded-state-extract-then-compare.",graduation:{status:"gated",gate:"Slice 2 is shadow-first (no verdict change). Graduation gates on: (1) a staging verdict-PARITY shadow soak of grounded_state_unified vs the per-kind grounded_state_count / _extract / _differential diags showing ZERO would-verdict drift across the count / presence / absence / modification fixtures; (2) grounded_state_unified_done confirming extractions == #snapshots (+ shared before), NOT #comparisons (the batching win) in the field; (3) the semantic-matchType Assessed slot behaving (confident \u2192 Verified, ambiguous \u2192 Assessed, contradiction \u2192 Verified-FAIL) at an acceptable concept-classifier accuracy. ONLY after parity is proven do the three per-kind batch methods get physically retired (a separate refactor PR) and does advancing to a LIVE verdict path get considered \u2014 both separate steps from flipping this flag shadow-on.",evidence:"staging grounded_state_unified / _start / _done diag events (spectrum verdict + parity + extraction-vs-comparison counts) cross-checked against the per-kind diags for parity + the engine-core groundedStateUnified + RunnerRuntime.groundedStateUnified unit suites + the trust-unified-parity / -batching / -semantic evals + the claim verify.grounded-state-extract-then-compare",owner:"steering (Alex)",review:"2026-09-05"}},INTERACTION_CLEARS_PRESENCE_ORACLE:{key:"INTERACTION_CLEARS_PRESENCE_ORACLE",envVars:["AGENTIQA_INTERACTION_CLEARS_PRESENCE_ORACLE","AGENTIQA_EXPERIMENT_INTERACTION_CLEARS_PRESENCE_ORACLE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Interaction-clears-presence-oracle (rank-2 of the step-5 slow-render flake wave, sibling to VERIFY_PRESENCE_WAIT_FLOOR's rank-1 budget raise). A verify-step PRESENCE wait oracle (wait_for_element) that STILL times out after the raised budget records an unresolved oracle failure that verify-gated-done only lets a fresh successful wait_for_element/run_js clear \u2014 a screenshot or a successful click is refused and the plan-grounded literal blocks canReconcileVerificationConflict \u2014 so a genuinely-present-but-slow element force-FAILs (staging sessions asess_1784961071757_cqwxon46 / asess_1784960734951_4t0zaxp1, where the NEXT step's click_at('Blank board') SUCCEEDED and navigated). When on, a subsequent SUCCESSFUL deterministic click_at (or a type_text_at/set_focused_input_value write) on the current verify step or the one immediately preceding it (lookback 1) whose ENGINE-RESOLVED element identity (clickTarget.accessibleName/textContent for a ref/coordinate click, clickedElement.textContent for a label click, or typedIntoField \u2014 the resolved field's accessible name \u2014 for a type write; never the model's narration or the typed VALUE) whole-token-substantiates (citationSubstantiates \u2014 the SAME contiguous-token machinery pinPresentInPage uses, NOT substring) the failed wait's captured target literal CLEARS that step's wait-oracle failure \u2014 engine-observed, un-hallucinable proof of presence, strictly stronger than the screenshot the gate already refuses. HARD CONSTRAINTS (no false-PASS): whole-token resolved-name match only, gated by a SIGNIFICANCE floor (the wait literal must carry a \u22654-char token OR \u22652 tokens \u2014 a bare common single short token like \"ok\"/\"3\" whole-token-matches an unrelated control name too easily, so it can never clear); the #1476 real-hit guard (a coordinate no-op-success \u2014 noObservedEffect side channel \u2014 and a non-interactive pixel landing whose accessibleName merely mirrors a container's textContent are BOTH rejected, so a click that reports success but hit nothing cannot launder a miss); a type only ever clears when it genuinely resolved+focused a named field (typedIntoField populated \u2014 a blind write carries no identity); scope = wait-style presence oracle only (isVerificationOracleAction, NEVER the plan-derived pin-page-grounding oracle) on a Runner verify step; an ABSENCE-intent verify step (checkTextAssertsAbsence over the step text + criteria) is NEVER cleared \u2014 a successful interaction DISPROVES an absence assertion, so clearing would manufacture a pass on a real absence-violation bug; fail-closed (a genuinely-absent element cannot be successfully interacted with, and an ambiguous / non-matching / insignificant-literal / stale-beyond-lookback / absence-intent identity does NOT clear \u2014 the force-fail stands). SHADOW-FIRST: off (default) leaves every verdict identical (the force-fail stands) and only emits an `interaction_clears_presence_oracle:would_clear` diag of what it WOULD have cleared, so a staging soak can confirm it fires only on genuine presence before the flip; on makes the clear live (`\u2026:cleared`).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-25",notes:"Default OFF in code; detection runs in shadow always (interaction_clears_presence_oracle:would_clear diag), the actual clear is gated \u2014 the ABSENCE_AWARE_VERIFY / PIN_PAGE_GROUNDING shadow-first precedent. killSwitch resolves the no-env value from this defaultState (#1729), so GRADUATING = flip defaultState to 'on' AND add INTERACTION_CLEARS_PRESENCE_ORACLE to INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Read site: packages/engine-core/src/RunnerRuntime.ts (interactionClearsPresenceOracleEnabled \u2192 maybeInteractionClearsPresenceOracle, called from the browser-action dispatch after recordOffPlanResolvedClick). Resolved-identity sources: label click \u2192 response.clickedElement.text (clickByLabel only populates it after resolving exactly one clickable control by name AND clicking without error \u2014 un-hallucinable); ref/coordinate click \u2192 response.clickTarget.accessibleName/text, admitted ONLY when isInteractiveClickTarget(clickTarget) so a pixel that landed on a text container cannot launder via the accessibleName\u2192textContent fallback; type write (type_text_at / set_focused_input_value) \u2192 response.typedIntoField (getFocusedFieldName \u2014 the accessible name of the field the write actually resolved+focused; the typed VALUE is never an identity). SIGNIFICANCE floor (waitLiteralHasSignificantTokens over the same tokenizeCitation basis the match uses): a bare common single short token (a status word, a lone digit) whole-token-matches an unrelated resolved name too easily, so the wait literal must carry a \u22654-char token OR \u22652 tokens or it never clears. Lookback is intentionally tight (1 step) to keep a stale REAL miss from an earlier step from being laundered by a same-named element that appears much later; a soak may widen it. Absence-intent verify steps are excluded via checkTextAssertsAbsence (a successful interaction disproves an absence assertion \u2192 would be a false-PASS); a suppressed match emits interaction_clears_presence_oracle:absence_skip. Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.interactionClearsPresenceOracle.test.ts (flag-OFF shadow-only + byte-identical for both click and type, flag-ON label + ref + type clear, type resolved-different-field reject, type-with-no-resolved-field reject, significance-floor bare-one-token reject, #1476 no-op reject, non-interactive container-text reject, non-matching-element reject, whole-token-not-substring, genuinely-absent still-fails, absence-intent step NEVER cleared, pin-page-grounding scope guard, cross-step lookback bound).",graduation:{status:"gated",gate:"Staging shadow soak clean (interaction_clears_presence_oracle:would_clear fires ONLY where a successful click genuinely resolved+acted on an element whose engine-resolved identity whole-token-matches the failed wait literal \u2014 never on a #1476 coordinate no-op, a non-interactive container-text mirror, an absence-intent verify step, or a stale failure beyond the 1-step lookback) AND the engine-core interaction-clears-presence-oracle detection-inversion passes (flag OFF \u2192 the timed-out presence step still force-FAILs; flag ON \u2192 a matching successful click clears it to a PASS, a no-op/non-matching/absent/absence-intent element still FAILs)",evidence:"staging interaction_clears_presence_oracle:would_clear / :cleared diag events (clear rate + resolvedVia/literal breakdown) + the RunnerRuntime.interactionClearsPresenceOracle unit suite + the step-5 slow-render rehearsal replay (asess_1784961071757_cqwxon46 / asess_1784960734951_4t0zaxp1)",owner:"steering (Alex)",review:"2026-08-01"}},CANVAS_STRATEGY:{key:"CANVAS_STRATEGY",envVars:["AGENTIQA_CANVAS_STRATEGY","AGENTIQA_EXPERIMENT_CANVAS_STRATEGY"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Enables the canvas-app (Miro/Figma/spreadsheet) per-turn strategy prompt injection and the coordinate-action tool-result caveat; off drops both so a false canvas classification cannot alter targeting guidance.",designDoc:"docs/plans/2026-07-06-canvas-capability-design.md",status:"active",added:"2026-07-06"},SCREENSHOT_DIRECT_UPLOAD:{key:"SCREENSHOT_DIRECT_UPLOAD",envVars:["AGENTIQA_SCREENSHOT_DIRECT_UPLOAD","AGENTIQA_EXPERIMENT_SCREENSHOT_DIRECT_UPLOAD"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Analytics-sink screenshot upload path: on, the sink fetches a presigned R2 PUT URL from /api/analytics/presign-screenshot and PUTs the PNG straight to R2 (bytes never transit web-next); "0" forces the legacy base64 /api/analytics/upload-image route. On any presign/PUT failure the sink falls back to the legacy route per-screenshot regardless of this switch.',designDoc:"packages/engine-core/src/sinks/RemoteAnalyticsSink.ts",status:"active",added:"2026-07-19"},SCOPE_SAME_ORIGIN_LINKS:{key:"SCOPE_SAME_ORIGIN_LINKS",envVars:["AGENTIQA_SCOPE_SAME_ORIGIN_LINKS","AGENTIQA_EXPERIMENT_SCOPE_SAME_ORIGIN_LINKS"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:`Scope-gate valve for page-authored links: a clicked href on a scoped ORIGIN whose path falls outside every scope prefix is allowed (reason same_origin_link, with a scope-drift diag) instead of hard-blocked as an "external link". Cross-origin targets and model-typed navigate URLs keep the hard block. Prevents an LLM-guessed scope prefix from blocking the app's own navigation (assertqa /why-assertqa false 404, 2026-08-05); "0" restores the unconditional prefix block.`,designDoc:"docs/plans/2026-08-05-scope-observed-url-grounding.md",status:"active",added:"2026-08-05"},SCOPE_SPAWN_GROUNDING:{key:"SCOPE_SPAWN_GROUNDING",envVars:["AGENTIQA_SCOPE_SPAWN_GROUNDING","AGENTIQA_EXPERIMENT_SCOPE_SPAWN_GROUNDING"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Deterministic scope-authoring grounding at the spawn boundary: a same-origin spawn scope entry that no child ever visited (observed-URL ledger, runtime-observed only) and the user never supplied is bounced back to the Coordinator ONCE with the observed-URL list to re-author from; an insisted repeat proceeds fail-open. Also stops checkpoint surfaces with unobserved same-origin URLs from being persisted into the durable AppMap. Pure set-membership on parsed URLs \u2014 no label matching or slug heuristics; "0" disables both the bounce and the AppMap filter.',designDoc:"docs/plans/2026-08-05-scope-observed-url-grounding.md",status:"active",added:"2026-08-05"},SAME_GOAL_ABORT:{key:"SAME_GOAL_ABORT",envVars:["AGENTIQA_SAME_GOAL_ABORT","AGENTIQA_EXPERIMENT_SAME_GOAL_ABORT"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Escalates consecutive milestone-free supervisor REDIRECT verdicts into an abort-then-block, bounding a stuck same-goal step; off leaves the supervisor redirecting until the iteration budget runs out.",designDoc:"docs/plans/2026-07-06-canvas-capability-design.md",status:"active",added:"2026-07-06"},SUPERVISOR_VERDICT_MEMORY:{key:"SUPERVISOR_VERDICT_MEMORY",envVars:["AGENTIQA_SUPERVISOR_VERDICT_MEMORY","AGENTIQA_EXPERIMENT_SUPERVISOR_VERDICT_MEMORY"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Phase 3 Layer 1 of the Lio verdict-integrity track. Feeds the last 6 APPLIED supervisor verdicts (action + iteration + instruction truncated to 120 chars) back into the PERIODIC supervisor prompt as a trailing context segment, with a no-self-reversal instruction: do not issue a REDIRECT that reverses a correction already given unless the page state materially changed, and when your own corrections already conflict decide TERMINALLY (WRAP_UP / BLOCK) rather than adding a third direction. THE DEFECT: SupervisorService.evaluate() is stateless per call and its prompt shows only the recent ACTION table, so the judge re-derives an opinion every consult with no idea it already gave the opposite one \u2014 prod 2026-08-06 tp_d1360115 drew 97 verdicts, 69 REDIRECT, ~1/3 reversing the previous instruction ('complete steps 2-6 first' alternating with 'proceed with Step 7'), and the agent oscillated to the 300-iteration cap. SHIPS DEFAULT-ON FROM INCEPTION rather than shadow-first because it writes NO status and takes NO deterministic decision: it only adds text to a judge prompt, and the deterministic bound on the same class is REDIRECT_FLIPFLOP_GUARD (shadow-first, Layer 2), which is where the graduation evidence lives. LOOP-BREAKER PATH UNTOUCHED \u2014 evaluateLoopBreaker keeps its exact prompt (single-shot false-block question, no history to contradict). =0 under either spelling restores the byte-identical pre-change prompt: the ring still fills (it costs nothing) but nothing is rendered and contextHints collapse back to the caller's static hints. Acceptance suite: packages/engine-core/src/__tests__/supervisorVerdictMemory.test.ts (render pin through the exported buildSupervisorPrompt) + BaseRuntime.supervisorVerdictMemory.test.ts (buffer fill/trim, post-escalation recording, run scoping, flag off).",designDoc:"docs/plans/2026-08-06-lio-terminal-verdicts-and-zero-grade-bounce-design.md",status:"active",added:"2026-08-06"},REDIRECT_FLIPFLOP_GUARD:{key:"REDIRECT_FLIPFLOP_GUARD",envVars:["AGENTIQA_REDIRECT_FLIPFLOP_GUARD","AGENTIQA_EXPERIMENT_REDIRECT_FLIPFLOP_GUARD"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Phase 3 Layer 2 of the Lio verdict-integrity track \u2014 the DETERMINISTIC bound under SUPERVISOR_VERDICT_MEMORY's prompt-side nudge. SHADOW-FIRST. THE DEFECT: prod 2026-08-06 tp_d1360115 drew 69 REDIRECT verdicts, ~1/3 of them reversing the previous instruction ('complete steps 2-6 first' \u2194 'proceed with Step 7/8/9'), and rode the 300-iteration cap to a verdict-less terminal. SameGoalAbortTracker exists for this class and was disarmed THREE WAYS at once: 28 of 97 verdicts were non-REDIRECT (each resets its consecutive count), the entity-minting click loop minted 252 progress milestones (each resets BOTH its counters), and the step boundaries reset it again. WHAT =1 ENABLES: RedirectFlipFlopTracker \u2014 run-scoped, with NO RESET LANES AT ALL (not on milestones, not on non-redirect verdicts, not on step boundaries; that scoping IS the fix and is pinned by the unit test 'redirects across step boundaries still count'). TWO TRIGGERS: (1) STEP-TARGET CYCLE \u2014 each 'steps \u2026' MENTION in a redirect message is parsed whole (a number, a range, or an enumeration over , & / and or: 'Step 7', 'steps 2-6', 'Step 7/8/9', 'steps 2, 4 and 6'), mentions that overlap each other cluster into one direction, consecutive redirects naming overlapping steps merge into the standing direction, and a completed A\u2192B\u2192A over pairwise-disjoint step sets is one cycle counted NON-OVERLAPPING; 2 completed cycles trip a strike. A message naming TWO disjoint clusters ('Complete steps 2-6 first, then proceed to Step 7') states a PLAN, not a direction: it counts toward the budget only and never touches the standing direction. BOTH READINGS WERE FORCED BY MEASUREMENT (adversarial review 2026-08-06): the original regex read only the first number of an enumeration, so a supervisor repeating ONE consistent instruction produced alternating phantom-disjoint directions and fired a wrap_up on a run that never reversed (poisoning the very census this gate grades); and flattening a two-direction sentence into one union set let it be merged into the standing direction, after which every later pure message overlapped, no second direction was ever pushed, and the trigger was dead for the rest of the run. (2) TOTAL REDIRECT BUDGET \u2014 24 redirects/run, non-resettable, re-tripping at each further multiple; this is the mechanism-free bound that catches flip-flops whose messages carry no step numbers, and it is also what bounds a runner-lane wander that REPEATED_SIDE_EFFECT_GUARD cannot (that layer's credits refill per declared step in the runner lane). STRIKE LADDER (SameGoalAbortTracker's shape): strike 1 synthesizes wrap_up, strike 2 synthesizes block, routed through the existing supervisor hard-stop path (blockKind 'supervisor', endMechanism supervisor_halted) so no LLM cooperation is needed. THE TWO SYNTHESIZED STRINGS ARE TRUTHFUL PER TRIGGER: only the cycle trigger says the corrections contradict each other and names both directions; the budget trigger fires on COUNT alone (completedCycles can be zero \u2014 24 corrections advancing monotonically through distinct steps never form a cycle) and therefore says only that, because the block reason is CLIENT-VISIBLE (it becomes blockedReason and the run's stop reason) and the wrap_up steers the agent's final report. PRECEDENCE: the guard runs BEFORE SameGoalAbortTracker in applySupervisorVerdict, so a rewritten verdict is already wrap_up/block when the same-goal tracker observes it \u2014 the tracker resets its consecutive count and can never double-synthesize over one verdict (pinned). DEFAULT OFF = SHADOW, NOT INERT: the tracker runs in full and the caller emits agent_lifecycle redirect_flipflop_would_fire {trigger, strike, escalation, redirectCount, directions} plus a warn log, and mutates NOTHING \u2014 the redirect applies byte-identically and SameGoalAbortTracker sees the original 'redirect'. An explicit =0 under either spelling switches the DETECTION off too (killSwitchForcedOff idiom), reclaiming the measurement cost. Chat and runner lanes both. Acceptance suite: packages/engine-core/src/__tests__/redirectFlipFlop.test.ts (pure: cycles, strikes, budget, NO resets, parser) + BaseRuntime.redirectFlipFlop.test.ts (wiring: shadow = would-fire with no synthesis, enforce = wrap_up then block, precedence over sameGoalAbort, no double synthesis, run scoping).",designDoc:"docs/plans/2026-08-06-lio-terminal-verdicts-and-zero-grade-bounce-design.md",status:"active",added:"2026-08-06",graduation:{status:"gated",gate:`A nightly/staging census of redirect_flipflop_would_fire showing (a) the guard fires on runs that actually oscillate \u2014 the tp_d1360115 shape and the entity-minting eval \u2014 and (b) ZERO would-fire rows on runs that completed normally, i.e. no healthy run is one strike away from a forced wrap_up. The step-target parser remains the specific risk, and the 2026-08-06 review is the reason to treat it as an empirical question rather than a design one: it shipped with TWO parser defects that each moved a real run's outcome \u2014 enumerations collapsing to their first number (phantom cycles on a consistent supervisor) and two-direction sentences absorbing the standing direction (the trigger silently dead). Both are closed and pinned, but the class is "prose read as a steering target", so the census must show the DIRECTIONS ROW BY ROW: that each parsed direction was a real steer and not incidental prose ("you are still on step 3"), and that runs whose supervisor genuinely oscillated produced cycle fires rather than only budget fires \u2014 a run that reaches the count bound without a single cycle fire is evidence the parser is still blind, not that the guard worked.`,evidence:"agent_lifecycle redirect_flipflop_would_fire rows on the nightly qa-exhaustive lane and on a Lio prod\u2192staging mirror replay, sized against the runs that completed normally in the same batch",owner:"steering (Alex)",review:"2026-08-13"}},TARGET_CONTAINMENT:{key:"TARGET_CONTAINMENT",envVars:["AGENTIQA_TARGET_CONTAINMENT","AGENTIQA_EXPERIMENT_TARGET_CONTAINMENT"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Keeps the agent within the app-under-test origin at every navigation chokepoint; off makes every boundary check return "allow" (pre-containment behavior).',designDoc:"packages/engine-core/src/BasePlaywrightService.ts",status:"active",added:"2026-07-05"},INCIDENTAL_EXTERNAL_LOGIN_RECOVERY:{key:"INCIDENTAL_EXTERNAL_LOGIN_RECOVERY",envVars:["AGENTIQA_INCIDENTAL_EXTERNAL_LOGIN_RECOVERY","AGENTIQA_EXPERIMENT_INCIDENTAL_EXTERNAL_LOGIN_RECOVERY"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Necessity-gated split of the third-party-IdP boundary, in BOTH lanes: with NO login credentials configured, an external login reached on a DIFFERENT registrable domain than the app under test AND judged task-INCIDENTAL is RECOVERED \u2014 the service returns the page to the app (popup close \u2192 history back \u2192 pre-navigation app URL \u2192 app origin, every leg bounded at 5s), injects an agent-visible note that names the host, forbids re-following it AND warns that the return trip reset the page's in-page state, and the run continues. Unattended (RunnerRuntime.startRun) that replaces terminating the run as exploration_blocked (staging asess_1785402865447_25cimyx8: a \"check all the buttons\" run followed a social footer link to instagram.com/accounts/login and lost 49 recorded actions); in the interactive CHAT lane (Coordinator/Explorer, and the runner's own sendMessage) it replaces the ask-user credentials pause (staging asess_1785434018769_usq42s45: the self-agent's inner \"Direct task\" Coordinator run blocked on the same host with reason:'interactive_session'). TWO LAYERS: the registrable-domain rule is only the fail-closed floor; before any recovery the service consults the NECESSITY judge (externalLoginNecessityJudge.ts, Layer 2 of the uncertain-boundary class, engine-owned Flash model, at most ONE call per external host per run) with the active plan step \u2014 or, in the chat lane, the turn's task/objective text (BaseRuntime.necessityContextText). FAIL-SAFE onto the pre-existing terminal for `required`, `uncertain`, confidence < 0.8, a judge error, and NO judge wired at all (desktop / no engine key \u21D2 the whole feature is inert) \u2014 so a human who genuinely needs to hand over SSO credentials still gets the pause, and only logins the task does not need stop interrupting them. Bounded at 3 recoveries per run via ONE counter shared by both lanes; a 4th hit, a recovery that lands back on an external login, or a SAME-registrable-domain login (the app's own SSO) keeps today's terminal block verbatim. GRADUATED default-ON 2026-07-30 (gates G1-G4): with NO env both lanes now recover a confidently-`incidental` login and the judge is consulted in the would-recover branch. An explicit `=0` under either spelling restores the pre-graduation terminal verbatim in BOTH lanes and spends NO judge call, while the decision is still computed and emitted as `incidental_external_login` diagnostics (with `wouldRecover`, the domain-signal-only upper bound across BOTH lanes \u2014 pair it with `unattended`; plus the necessity verdict/confidence/reason whenever a judge ran). Recovery reasons are lane-labeled (`incidental` unattended / `chat_incidental` interactive); the pre-2026-07-30 `interactive_session` terminal reason is retired. Never fires when credentials are configured (the pre-existing suppress) and never sees a same-product cross-environment escape (targetContainment runs first).",designDoc:"docs/plans/2026-07-06-uncertain-boundary-escalation-design.md",status:"active",added:"2026-07-30",notes:"GRADUATED 2026-07-30 (default ON) \u2014 graduation gate v2 (PR #1969), four attestations; the first graduation whose effect is to REMOVE a whole-run terminal rather than add a gate. killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND INCIDENTAL_EXTERNAL_LOGIN_RECOVERY listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire); an explicit AGENTIQA_INCIDENTAL_EXTERNAL_LOGIN_RECOVERY=0 (or the AGENTIQA_EXPERIMENT_ spelling) still restores the byte-identical pre-graduation terminal \u2014 `exploration_blocked` unattended, the ask-user credentials pause in chat \u2014 with ZERO judge calls. GRADUATION EVIDENCE. G1 (necessity-discrimination eval PAIR, deterministic + local): e2e/evals/plans/incidental-external-login-recovery.ts \u2014 6 legs, both necessity directions, 2/2 green with the REAL gemini-3-flash judge (incidental @0.90 / required @1.00 on the SAME external host) and 1/1 green with the deterministic stub judge. G2 (suites): engine-core incidentalExternalLogin (L1 decision matrix incl. the necessity fold-in AND the chat lane: judge-incidental recovers as `chat_incidental`, required/uncertain/unjudged keep the pause, shared cap), BasePlaywrightService.incidentalExternalLogin (L2 wiring over a real headless browser and synthetic hosts, stubbed judge through the production DI seam, both directions x both lanes + per-host cache + no-judge fail-safe + one shared counter + the forced-OFF parity cases, which are now built by forcing `=0` and never by deleting the key), RunnerRuntime.unattendedRunLifecycle (the necessity-context channel: plan step in a run, task text in chat, undefined when blank), externalLoginNecessityJudge (judge fail-safe). G3 (bounded live staging validation, flag force-ON via the staging orchestrator env): self-agent.yml GH run 30572972048 (2026-07-30, the SECOND G3 attempt) \u2014 3/3 PASSED including ci-first-plan end-to-end (signup -> inner directed run completed -> Save -> re-run of the saved plan). The FIRST attempt failed informatively: the inner run is an assistant_v2 Coordinator session, so the recovery was lane-gated OFF and the run blocked exactly as before the flag existed \u2014 that is what motivated the chat-lane extension and forced the re-run. G4 (telemetry): a LIVE unattended-lane recovery on the saved-plan re-run \u2014 `incidental_external_login {route:'popup_closed', reason:'incidental', idpHost:'www.instagram.com', outcome:'recovered', returnedTo:'https://agentiqa.com/en/pricing', unattended:true, recoveryCount:1, pageStateReset:false, necessityReason:\u2026}` in asess_1785438775291_qqz9uae4 \u2014 a real judge, a real third-party login wall, the popup route; 0 recovery loops, 0 nav-timeout fallbacks, 0 misfires. HONEST RESIDUAL \u2014 the CHAT lane is live-UNEXERCISED: in that run the external site showed the inner run no login wall at all (it redirected to X instead \u2014 a nondeterministic third party), so no live `chat_incidental` recovery was observed. Chat-lane behavior is pinned by HERMETIC evidence only (the G1 eval's chat leg plus the L1/L2 chat-lane cases), which is the one non-live leg of this graduation. SCENARIO-19 CONSEQUENCE: agent-core/oauth-idp-redirect-no-creds-pause used to pass for TWO independent reasons (the flag was staged OFF, and its prompt IS the task-REQUIRED SSO shape); post-graduation only the second holds, so that nightly scenario is now a live assertion that the necessity judge answers `required` on it (G1 measured confidence 1.00 on exactly that shape). The staging orchestrator force-ON override (AGENTIQA_EXPERIMENT_INCIDENTAL_EXTERNAL_LOGIN_RECOVERY=1, hand-set for G3) is redundant once this ships and is scheduled for removal \u2014 see docs/FLAGS-STATE.md. STAGING-FIRST: merging to staging makes this default-ON on the STAGING engine only; PROD stays OFF until a later staging->main release carries it (a natural staged soak). Read site: packages/engine-core/src/BasePlaywrightService.ts (recoverIncidentalExternalLoginIfNeeded). Binds claim agent.incidental-external-login-recovery."},PIN_SUBSTANTIATION:{key:"PIN_SUBSTANTIATION",envVars:["AGENTIQA_PIN_SUBSTANTIATION","AGENTIQA_EXPERIMENT_PIN_SUBSTANTIATION"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Flips a criterion the model graded "passed" to failed when the pinned expected value is not substantiated; off returns the model grade verbatim (pins still render, enforcement is off).',designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-07"},PIN_CROSS_STEP_SUBSTANTIATION:{key:"PIN_CROSS_STEP_SUBSTANTIATION",envVars:["AGENTIQA_PIN_CROSS_STEP_SUBSTANTIATION","AGENTIQA_EXPERIMENT_PIN_CROSS_STEP_SUBSTANTIATION"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:`FIX 3 (extends FIX 2 to the cross-STEP misattribution class). Before Amendment 5a synthesizes a strict pinned criterion as "never graded \u2192 unconfirmed \u2192 fail", it searches EARLIER step entries for an ORPHAN grade of the IDENTICAL check \u2014 graded passed=true, bound to NONE of its own step's plan criteria, and whose observed value substantiates THIS pin's expectedValue (the SAME substantiatePinnedCriterion gate). A match means the grading LLM misfiled the pin under a PRECEDING/action step (prod asess_1785211658727: step 2's 3 strict pins graded inside step 1, an action step), so the pin was really evaluated \u2014 the substantiated pass is relocated to its own slot instead of false-failing the step. Off restores FIX 2 behavior verbatim (no cross-step search). The rescue is SCOPED to misattributed grades, not a blanket 'a passing grade exists elsewhere': (1) only orphan grades qualify \u2014 a step's OWN bound verdict is never borrowed, so a still-passing earlier verify cannot mask a later regression; (2) a contradicting FAILING grade for the same check anywhere blocks the rescue; (3) only a STRICTLY EARLIER step's grade qualifies, so a later step's page state is never laundered backward onto an earlier assertion (a backward misattribution stays fail-closed); (4) the caller only rescues a pin unique among plan verify criteria. Outside that envelope the pre-existing fail-closed synthesis stands. EVIDENCE-EPOCH DISPOSITION (steering 2026-07-28): 'strictly earlier' does not mean 'same page state', so a rescue is only a silent PASS when NO state-changing plan step (type action/setup) sits STRICTLY BETWEEN the rescuing entry and the pin's own step \u2014 the confirmed prod shape (verify step k's grades misfiled under the immediately preceding action step k-1) has nothing in between, so it keeps its full PASS rescue. A STALE-FORWARD rescue (an intervening action/setup broke the epoch, so the observation may predate a regression) is instead SOFT-WITHHELD to a step-level 'warning' \u2014 the criterion carries the substantiated pass noted 'substantiated by an earlier-step grade; not re-verified at this step' and the step is demoted passed->warning (never a hard fail, never a clean pass; run status is untouched since only 'failed' steps downgrade a run). This is the ABSENCE_AWARE_VERIFY-ABSTAIN / GROUNDING_EPOCH_FIDELITY withhold rail: the criterion stays passed:true on purpose, because flipping it would route deriveStepStatusFromCriteria to a hard 'failed' on a strict criterion (warning-cap-only). Every accepted rescue emits RunnerRuntime log pin_cross_step_substantiated {stepIndex, sourceStep, expected, orphanCandidates, corpusSize, reason} for prod frequency/provenance, where reason is 'same-epoch-rescue' (PASS) or 'stale-forward-warning' (withheld). SHARED BINDER (round 4): the orphan test (guard 1) READS the already-computed per-entry bindings from bindGradesToPlanSlots \u2014 the ONE authoritative two-pass binder that also drives FIX 2's cross-entry union and the reported criteria results \u2014 and never re-derives them. An earlier revision ran its own SEQUENTIAL bindGradeToPlanIdx loop, which diverges from the two-pass binder wherever a rephrased grade's positional fallback would steal a slot a later grade matches by exact text: the authoritative binder books that grade onto its own step (non-orphan) while the sequential mirror leaves it unbound (orphan) and thus eligible to rescue another step's pin \u2014 an EMERGENT false-PASS reachable only with PIN_CROSS_STEP_SUBSTANTIATION and CRITERION_BIND_RESIDUAL both on, which each flag's own tests miss. Pinned by a 4-cell flag-matrix test. PER-CRITERION FLOOR (round 4): the rescue pushes a synthetic passed result, which switched OFF the whole-step zero-grade 'unsubstantiated verify' floor (that floor tests criteriaResults.length === 0) for the step's OTHER criteria \u2014 and Amendment 5a itself only covers PINNED strict criteria, so a plain strict criterion beside a rescued pin was adjudicated by nothing and rode through on the reported 'passed'. On steps where a rescue fired, each plan criterion that is in neither the FIX 2 union nor Amendment 5a's coverage AND has no matching grade anywhere in the cross-step corpus now synthesizes its own failure (strict) or warning (strict:false), emitting pin_cross_step_sibling_unsubstantiated {stepIndex, check, strict, rescuedPinsOnStep}. Scoped to rescue-touched steps so no untouched verdict moves, and gated on 'ungraded ANYWHERE' so the whole-step misattribution the rescue tolerates is not re-punished.`,designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-28"},CHECK_TEXT_ATOM_PIN:{key:"CHECK_TEXT_ATOM_PIN",envVars:["AGENTIQA_CHECK_TEXT_ATOM_PIN","AGENTIQA_EXPERIMENT_CHECK_TEXT_ATOM_PIN"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"run_complete grade-time atom pinning: a strict verify criterion whose check text pins exactly one URL literal (http(s):// or bare localhost) and carries no expectedValue has that literal promoted to an effective expectedValue so the pin-substantiation / grounding machinery fires on it, plus a deterministic URL-host floor that fails the criterion closed (naming both hosts) when the evidence observes a different-host URL and preserves the pass on a scheme-only / trailing-slash difference \u2014 regardless of what the substantiation/drift judge decided. Off leaves a bare {check, strict} URL criterion ungraded past the model self-grade (pre-feature behavior).",designDoc:"docs/plans/2026-07-19-ag7727-run-fidelity-fixes-design.md",status:"active",added:"2026-07-19"},TYPED_MATCH:{key:"TYPED_MATCH",envVars:["AGENTIQA_TYPED_MATCH","AGENTIQA_EXPERIMENT_TYPED_MATCH"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Routes pin-substantiation's observed-vs-expected comparison through the typed match-comparator registry keyed on a criterion's `matchType` (country ISO-3166 fold DE\u2261Germany, locale numeric equality); a typed match keeps a pass the brittle literal compare would have flipped, a typed mismatch (wrong country) flips closed. Off (or an absent/`literal` matchType) restores the pure literal `citationSubstantiates` behavior verbatim \u2014 inert until a criterion carries a matchType, so this only removes a false-fail class, never changes an untyped verdict (AG-7753).",designDoc:"docs/plans/2026-07-20-typed-match-comparator-design.md",status:"active",added:"2026-07-20"},COUNTRY_EQUIV_RESCUE:{key:"COUNTRY_EQUIV_RESCUE",envVars:["AGENTIQA_COUNTRY_EQUIV_RESCUE","AGENTIQA_EXPERIMENT_COUNTRY_EQUIV_RESCUE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'C2 country-equivalence rescue in run_complete pin substantiation: when an already model-PASSED strict criterion\'s literal citation fails, the citation is retried with every recognized country surface form on BOTH sides folded to its ISO-3166 alpha-3, so an address that differs ONLY by country NAME vs CODE ("\u2026H\xF6henkirchen Germany" pin vs "\u2026H\xF6henkirchen DE" observed, prod Lio tp_5a2a7f4c step 9) substantiates instead of brittle-failing. Narrowly scoped: the rescue runs only for a country-SHAPED pin \u2014 `matchType: \'country\'`, or (untyped/literal/text-normalized/presence) a pin whose TERMINAL token is a full country NAME or alpha-3; a bare terminal alpha-2 pin ("Dover, DE") and every number/currency/url/semantic matchType are excluded. The fold itself only rewrites a country NAME anywhere, and a country CODE only as an uppercase token in the terminal country slot (never before a US zip), with subdivision/unit collisions (CA/NL/GB/CH/IE/PL/PT/SE/IN/CAN/NOR/\u2026) restricted to a sole-token read. That extra ambiguity is FOLD-SCOPED (`FOLD_AMBIGUOUS_ALPHA2`/`FOLD_AMBIGUOUS_ALPHA3`): the shared gazetteer read by citedCountry/compareTyped \u2014 i.e. the typed-atom floor and the predicate-basis typed comparator, neither of which this switch gates \u2014 is untouched BY THIS FLAG, verified by a differential sweep of the whole gazetteer. Off restores the pure literal `citationSubstantiates` verdict, so this flag only ever removes a false-FAIL class and nothing this flag contributes survives turning it off. NOTE (AG-8212, 2026-07-29/30) \u2014 that is a statement about THIS flag, not about the comparator stack as a whole: BOTH free-text country reads have since gained their own ungated guard (the same eight collision-prone alpha-2 codes are now gated in `citedCountry` \u2014 the observed scan, direction 1 \u2014 and in `authoredCountryRead` \u2014 the typed-atom floor\'s check-text read, direction 2 \u2014 so a bare code buried in prose resolves a country only when a full NAME corroborates it or when it is the sole token), which this switch does not gate and cannot revert. The fold path and both fold-ambiguity sets remain pre-C2 byte-identical. Split out of TYPED_MATCH (which stays inert on untyped criteria) because this rescue fires on criteria that carry NO matchType.',designDoc:"docs/plans/2026-07-20-typed-match-comparator-design.md",status:"active",added:"2026-07-28"},TYPED_ATOM_FLOOR:{key:"TYPED_ATOM_FLOOR",envVars:["AGENTIQA_TYPED_ATOM_FLOOR","AGENTIQA_EXPERIMENT_TYPED_ATOM_FLOOR"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"AG-7753 Phase 2 strict-unpinned typed-atom floor. run_complete detection ALWAYS runs (shadow): for a strict verify criterion the model graded passed that carries NO expectedValue and whose check text names exactly one recognized typed atom (country ISO-3166 / currency ISO-4217 / number \u2014 the generalization of #1685's URL host floor), the page-read observed value (a11y snapshot, else the structured `observed` grade field; disagreement \u2192 abstain) is compared to the recognized expectation via compareTyped. On a same-type MISMATCH (France where Germany expected) it emits a shadow `typed_atom_floor:would_fail` diag; when this flag is ON it ENFORCES \u2014 flipping the criterion passed\u2192false (only-fails, never originates a pass) and emitting `typed_atom_floor:fire`. Off leaves every verdict byte-identical (shadow diag only). URL stays owned by CHECK_TEXT_ATOM_PIN \u2014 a URL-bearing check is not recognized here.",designDoc:"docs/plans/2026-07-20-typed-match-comparator-design.md",status:"active",added:"2026-07-20",notes:"GRADUATED 2026-07-21 (default on) via the evidence-count doctrine: detection-inversion PROVEN twice \u2014 deterministically in RunnerRuntime.typedAtomFloor.test.ts (33 assertions) and live in the runner/typed-atom-country-floor L3 eval (qa-exhaustive 29861680933: seeded wrong-country step correctly failed) after the self-testing fixture deploy was unblocked (productionBranch was pinned to main). Shadow soak was clean but thin (organic staging plans carry no recognizable unpinned atoms \u2014 vacuous-soak class). Enforcement only-fails a same-type mismatch, never originates a pass. Distinct from TYPED_MATCH (Phase 1, default ON, pinned-criterion fold) and CHECK_TEXT_ATOM_PIN (#1685, URL host floor). Runner lane only (RunnerRuntime run_complete)."},COMPLETION_EVIDENCE_FLOOR:{key:"COMPLETION_EVIDENCE_FLOOR",envVars:["AGENTIQA_COMPLETION_EVIDENCE_FLOOR","AGENTIQA_EXPERIMENT_COMPLETION_EVIDENCE_FLOOR"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Completion-evidence floor for the CHAT/CLI objective lane (the ungrounded-ship seam VERIFY_GATED_DONE misses: it fires only on a failed wait-oracle in the last 5 actions, so a fabricated value with no failing wait ships silently). Detection ALWAYS runs (shadow) at ExplorerRuntime.handleReport on a terminal `completed` report: for an evidence-bearing objective (find/copy/extract a named value) it grounds the agent's TYPED CLAIM (the optional `extractedValues` field on the assistant_v2_report payload; free-text is never parsed): a claimed value found nowhere in the runtime a11y-snapshot corpus nor in runtime_observed revealed facts (whole-token `citationSubstantiates` \u2014 free-form; `compareTyped`'s closed grammar does NOT apply) emits a shadow `completion_grounding:would_demote` diag; a completed extraction objective with NO typed claim is shadow-only signal (reason no_typed_claim) and is NEVER enforced. Label-presence in the corpus never substantiates a value (v1.1). When this flag is ON it ENFORCES the typed-claim-fabrication path only \u2014 attaching an Explorer `VerificationConflict` with the NEW source `completion_ungrounded` (own card copy; NO terminal blockKind) that the existing verification-conflict rail (`applyVerificationConflictFindings` / `focusedTaskVerdictRecommendation`) demotes to do_not_ship in BOTH Coordinator producers. ABSTAINS (never demotes) when the objective is not evidence-bearing, the target is undecidable, or the observed corpus is thin/absent (canvas/off-DOM/dynamic value not in the snapshot) \u2014 demote-on-absence must abstain on any evidence-availability gap (blind-double-read precedent). Off leaves every verdict byte-identical (shadow diag only). Distinct from TYPED_ATOM_FLOOR (runner lane, closed-grammar value mismatch); this is the chat-lane grounding net (absence of any observed evidence, not value-correctness).",designDoc:"docs/plans/2026-07-21-completion-evidence-floor-design.md",status:"active",added:"2026-07-21",notes:"Default OFF in code; detection runs in shadow always (would_demote diag), enforcement is gated \u2014 the PIN_PAGE_GROUNDING / TYPED_ATOM_FLOOR shadow-first precedent. Single Explorer-level hook (handleReport) so both Coordinator verdict producers surface it (the cross-producer parity bug class, PR #643). Chat/Explorer lane only (assistant_v2_report). v1.1: enforcement requires a typed extractedValues claim proven absent from corpus+facts (provable fabrication); strict-extraction recognizer with common-UI-word stoplist (generic read/find objectives abstain); no-typed-claim path stays shadow-only so the soak measures claim-population rate. Grounds fabrication of a claimed value, NOT mis-selection of a real-but-wrong on-page value.",graduation:{status:"gated",gate:"Staging shadow soak clean (would_demote fires on the wandering-maze fabricate-and-ship replicates, zero would_demote on legitimately-shipping value-extraction controls) AND the engine-core completion-evidence-floor detection-inversion passes (floor OFF \u2192 fabricate-and-ship rides through as ship, ON \u2192 do_not_ship on the identical input in BOTH producers)",evidence:"staging completion_grounding:would_demote diag events + the engine-core completion-evidence-floor unit suite + the loop-detection/wandering-self-stop flag-ON graduation run",owner:"steering (Alex)",review:"2026-07-23"}},SETUP_NOTE_GROUNDING:{key:"SETUP_NOTE_GROUNDING",envVars:["AGENTIQA_SETUP_NOTE_GROUNDING","AGENTIQA_EXPERIMENT_SETUP_NOTE_GROUNDING"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Setup/action step-note grounding \u2014 surface S-A of the substantiation-fidelity (\"honest words\") track. Detection ALWAYS runs (shadow) at RunnerRuntime run_complete, after substantiation + the typed-atom floor: for each setup/action step (verify steps out of scope) graded passed/warning whose NOTE names a completed UI action (a closed past-tense/gerund verb lexicon \u2014 entered/typed/filled/submitted/clicked/selected/uploaded/dragged/\u2026) it checks the step's own planStepIndex-stamped action-tool window; a note asserting an action with ZERO grounding action tool calls in-window (the census shape asess_1784714866561_jhc34ht9 \u2014 pre-authed login steps graded green with 'Entered the email'/'Submitted the login form' notes and no type_*/click ever fired) emits a shadow `narration_fidelity:would_flag{surface:'setup_note', stepIndex, claim, missing_action, toolCallsInWindow}`. ABSTAINS (never flags) on a verify/read-only step, a note with no action verb, a non-terminal grade, or a window carrying ANY grounding action tool \u2014 ambiguity always abstains (closed allowlist). When this flag is ON it ENFORCES honest note substitution \u2014 the fabricated action clause is replaced with an honest precondition note; the step's verdict/status is UNTOUCHED (never a hard fail \u2014 the precondition was met, just not by the asserted action). Off leaves every stepResults note byte-identical (shadow diag only). Distinct from TYPED_ATOM_FLOOR (verify-criterion value mismatch) and COMPLETION_EVIDENCE_FLOOR (chat-lane claimed-value fabrication); this grounds SETUP/ACTION step NARRATION against the action log.",designDoc:"docs/plans/2026-07-22-substantiation-fidelity-design.md",status:"active",added:"2026-07-22",notes:"Default OFF in code; detection runs in shadow always (would_flag diag), enforcement (honest note substitution) is gated \u2014 the COMPLETION_EVIDENCE_FLOOR / TYPED_ATOM_FLOOR shadow-first precedent. Single RunnerRuntime run_complete pass (mirrors the typed-atom floor shadow). Deterministic \u2014 ~zero marginal LLM cost (reads the already-collected stepResults notes + the planStepIndex-stamped action-message log). P0 of the substantiation-fidelity track (S-A); S-B/S-C/S-D are separate per-phase flags.",graduation:{status:"gated",gate:"Staging shadow soak clean (narration_fidelity:would_flag{surface:'setup_note'} fires on the census-shaped reproduced-RED fixture, zero would_flag on legitimately-honest action notes whose tool call fired) AND the engine-core setupNoteGrounding detection-inversion passes (rip the pass out \u2192 the fixture stops flagging; enforce-ON rewrites the note, verdict untouched, on the identical input)",evidence:"staging narration_fidelity:would_flag diag events + the engine-core setupNoteGrounding + RunnerRuntime.setupNoteGrounding unit suites + the runner/narration-ghost-setup reproduced-RED eval",owner:"steering (Alex)",review:"2026-07-29"}},CAUSE_GROUNDING:{key:"CAUSE_GROUNDING",envVars:["AGENTIQA_CAUSE_GROUNDING","AGENTIQA_EXPERIMENT_CAUSE_GROUNDING"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Cause-grounding gate (#3091 milestone 2) \u2014 anchors a RED verdict's CLAIMED CAUSE to observed evidence. THE DEFECT: a red run's cause is entirely model prose. `run_complete`'s `summary` and `stepResults[].note` are copied verbatim and every existing gate only PREFIXES/APPENDS around them, so nothing ever inspects the model's own diagnosis. On the milestone-1 mutant fixture (`runner/duplicate-field-breaks-submit`) a duplicated `name=\"password\"` input breaks the POST and the page shows a GENERIC failure with no credentials wording at all \u2014 and the model writes \"Login failed \u2014 the credentials appear to be wrong.\" The verdict is right, the diagnosis is invented, and the customer rotates a password that was never the problem. DETECTION ALWAYS RUNS (shadow) at RunnerRuntime run_complete cascade stage 19c, immediately after `applyStepReplayDisclosure` (19b) and before the `run.status`/`run.summary` write: a deterministic regex lexicon (packages/engine-core/src/causeGrounding.ts \u2014 NOT an LLM) classifies the model-authored remainder of each `failed` step's note and of the run summary into one of nine mechanisms {credentials, permissions, network, validation, missing_data, element_absent, unresponsive_control, rate_limit, app_error}, then asks whether that step's bounded OBSERVED-anchor ledger (`_stepObservedAnchors`) carries an anchor that can ground it. ANCHORS ARE RUNTIME OBSERVATIONS ONLY \u2014 tool errors (both the `status:'error'` and the informative `error`/`valueMismatch` shapes, the same capture sites as `_stepActionErrors`), the per-action EventDigest (failedRequests, recentWrites status codes, consoleErrors, pageErrors), the Layer-B terminal-error quote, and a `detectCredentialRejection` hit on the step's RETAINED full page snapshot. MODEL-AUTHORED FIELDS ARE NEVER ANCHORS (a criterion's `observed` / `groundingObservation` / note cannot ground the same model's claim \u2014 that is the laundering this gate exists to stop; pinned by RunnerRuntime.causeGrounding.test.ts). MODEL-INFLUENCED CHANNELS ARE TAGGED, NOT EXCLUDED: `EventDigest.consoleErrors` (the page console is writable by `run_js`, a runner tool the model calls) and a `run_js` tool error carry `source:'model_influenced'`. They remain anchors because their direction is SILENCE \u2014 an extra anchor costs a missed rewrite, never a deleted sentence \u2014 but a rewrite whose ledger is made ENTIRELY of them is REFUSED under `=1` and abstains with `model_influenced_ledger`; shadow still emits `would_rewrite` carrying `anchorSource`, so the two flag states disagree on this class BY DESIGN and a census must read both. One runtime anchor alongside restores normal judgement. ABSTAINS (stays silent): a note naming no mechanism (\"submit did not work\" is honest by construction), a mechanism the plan step's own text or a PLAN-AUTHORED criterion LICENSES (\"verify an 'invalid credentials' error is shown\" \u2014 the note is quoting the plan; licensing reads all three AUTHOR-WRITTEN criterion surfaces, `check` prose + the pinned `expectedValue` + the compiled `basis.expected`, from `testPlan.steps[].criteria` ONLY, never `stepResults[].criteriaResults[].*`, which is the model's own echo and would let the model silence the gate with a sentence it wrote itself; `expectedValue` is the CANONICAL home of a pinned literal, so a plan with mechanism-free `check` prose and the literal in the pin used to license nothing and had its honest note rewritten), a phrase governed by a negator (\"no validation error was shown\"), any `environment_block` wording (captcha/WAF/bot-protection \u2014 #2606 already authors a runtime message there and a second rewrite would double-author it), a step the model did not grade `failed`, and gate-authored text (the pass reads ONLY the model-authored remainder \u2014 the segment after the last registered gate prefix and before the first registered gate suffix; the registries GATE_SUMMARY_PREFIX_PATTERNS / GATE_NOTE_PREFIX_PATTERNS / GATE_NOTE_SUFFIX_PATTERNS are the single extension point a new prefix/suffix-writing gate must join). WHAT =1 ENABLES: the SILENT rewrite \u2014 the first model-authored sentence carrying an unanchored claim becomes HONEST_CAUSE_FALLBACK (\"The action failed; the cause could not be established from what was observed (no error message, validation text or failed request was captured for this step).\") and any later unanchored sentence is dropped. NO STATUS AND NO CRITERION IS WRITTEN \u2014 the run stays `failed`, the step stays `failed`, only the cause sentence changes (the stage-19b `step_replay_disclosure` precedent). THREE OUTCOMES, NOT TWO \u2014 grounded (silent), unanchored (rewritten under =1), and ABSTAINED. `cause_grounding:abstain {stepIndex, surface, claimed, phrase, reason, anchorsSearched}` is emitted in BOTH flag states and NEVER accompanies a text change: `no_ledger` = the step carries no anchor, no per-action EventDigest receipt and no retained snapshot, so nothing was ever SEARCHED (an empty ledger there means 'we never looked', not 'the app reported nothing' \u2014 an EMPTY digest, by contrast, is a real observation and the gate judges normally); `model_influenced_ledger` = every anchor that would justify the rewrite came from a model-influenced channel (enforcement-only; the same case is a tagged `would_rewrite` in shadow); `quoted_unverifiable` = the sentence's cause is carried by a QUOTED page literal ('the page showed \"Invalid email or password\"') that no retained snapshot can confirm or contradict. A quotation FOUND in any retained snapshot is treated as grounded and left alone silently. COVERAGE RESIDUAL #3072 \u2014 THE TERMINAL 19c NEVER REACHES: a run that ends via `exploration_blocked` (terminationReason `blocked_by_issue`) never calls `run_complete`, so stage 19c does not run AT ALL. `handleBlocked` writes `run.summary` straight from the model's own `obstacle` prose, which is then the only cause text the customer reads on that run. Measured 2026-09-09: 4 of 8 live mutant replicates terminated this way. That surface belongs to m3 / issueEvidenceGate.ts and is NOT fixed here, but it is machine-countable: the blocked path emits ONE `cause_grounding:not_reached {terminal:'blocked_by_issue', surface:'run_summary', reason:'exploration_blocked'}` row, in both flag states, with no text change. A `would_rewrite` census MUST subtract these runs from its denominator instead of scoring them as silence. Off = shadow: byte-identical text, `cause_grounding:would_rewrite {stepIndex, surface, claimed, phrase, anchorsSearched}` diag only; an explicit `=0` under either spelling switches the detection (and the anchor ledger) off entirely. Acceptance suites: packages/engine-core/src/__tests__/causeGrounding.test.ts + RunnerRuntime.causeGrounding.test.ts; evals runner/duplicate-field-breaks-submit (unanchored) and runner/credential-rejection-cause (the negative control \u2014 a real 'Invalid email or password' on the page must NOT be rewritten).",designDoc:"packages/engine-core/src/causeGrounding.ts",status:"active",added:"2026-09-09",notes:'SURFACES OUT OF SCOPE: report_issue free text is NOT covered (it has its own evidence-bundle gate, issueEvidenceGate.ts) \u2014 the 2026-09-09 mutant lane filed an issue titled "Sign in fails with server error" on a purely client-side form bug, which is the same defect class on a surface this gate deliberately does not touch. Default OFF in code; detection runs in shadow always (would_rewrite diag), the rewrite is gated \u2014 the SETUP_NOTE_GROUNDING / PIN_PAGE_GROUNDING shadow-first precedent. Deterministic, ~zero marginal cost (regex over already-collected notes + an anchor ledger fed at existing per-step capture sites; the terminal-error quote is the one model read and it is already paid for by TERMINAL_ERROR_FLOOR). Generalises #2606 from ENVIRONMENT causes (cloudflareDetector / credentialRejectionDetector produce a runtime-authored message) to APP-SIDE causes. CROSS-LANE BOUNDARY (milestone 3, PR #3094 \u2014 merges FIRST): the label-drift disclosure appended at cascade stage 19b\u2032 is REGISTERED in GATE_NOTE_SUFFIX_PATTERNS ("Plan drift: this step\u2019s wording no longer matches the app \u2014 \u2026", quoting two runtime-read control literals, so it can carry lexicon wording); registering it also forced 19b\u2019s own replay-note pattern off its `$` anchor, since 19b\u2032 appends after it. m3\u2019s other two runtime strings are deliberately NOT registered and are pinned as an audit: unnamedControlClickNote lands on a TOOL-RESULT note the model reads (registering it would let the model buy an exemption by echoing it) and unnamedControlRuntimeNote / affordanceMismatchRelabel land on an Issue.description, which this pass never reads. A RED RUN WITH NO RED STEP is covered too: `runFailed` is threaded from the caller so the summary pass also runs on the blocked path (browserUnobservableConcluding), whose summary becomes the customer-visible blockedReason behind "Retained grading summary:".',graduation:{status:"gated",gate:"A shadow census of cause_grounding:would_rewrite on the nightly runner lanes, computed over a denominator that EXCLUDES the runs the gate never saw \u2014 a run carrying a `cause_grounding:not_reached` row terminated before stage 19c (residual #3072) and is `not_reached`, neither a hit nor a silence \u2014 showing (a) it fires on the runs that DO invent a mechanism \u2014 read the rows one by one and confirm each names a mechanism the page never evidenced \u2014 and (b) ZERO would_rewrite rows on runner/credential-rejection-cause, runner/seeded-broken and runner/happy-form, i.e. no honest, anchored or cause-free note is having its diagnosis withdrawn. NOTE ON YIELD (measured 2026-09-09, 3 shadow replicates + 1 enforce + 2 controls on gemini-3-flash-preview): the incumbent model was HONEST on runner/duplicate-field-breaks-submit every time \u2014 it quoted the page's generic banner and one replicate even found the duplicate field with run_js \u2014 so the fixture produced zero would_rewrite rows and the lane green is a NO-REGRESSION result, not a detection one. Detection is proven by the unit suites; the census must therefore run wide (nightly qa-exhaustive, not this fixture alone) before the flip. The false-positive class to answer explicitly: a note whose cause IS grounded by an observation the ledger does not carry (a mechanism visible only in a screenshot, or on a step whose snapshot was never retained). Two shapes of that class are now ABSTENTIONS rather than rewrites (no_ledger, quoted_unverifiable) \u2014 the census must read `cause_grounding:abstain` alongside `would_rewrite`, because a rising abstain rate is the honest signal that the ledger, not the model, is the thing that needs work.",evidence:"cause_grounding:would_rewrite diag rows on the qa-exhaustive runner lane + the two bound eval cases (runner/duplicate-field-breaks-submit, runner/credential-rejection-cause) in both flag states",owner:"steering (Alex)",review:"2026-09-23"}},GROUNDING_EPOCH_FIDELITY:{key:"GROUNDING_EPOCH_FIDELITY",envVars:["AGENTIQA_GROUNDING_EPOCH_FIDELITY","AGENTIQA_EXPERIMENT_GROUNDING_EPOCH_FIDELITY"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Criterion grounding-epoch fidelity \u2014 surface S-B of the substantiation-fidelity (\"honest words\") track. Detection ALWAYS runs (shadow) at RunnerRuntime run_complete, after substantiation + grounding + the typed-atom floor: for each verify-step criterion substantiated as PASSED it compares the criterion's grounding epoch (the monotonic capture/generation index at which its pinned value was actually groundable in a full page snapshot) against the step's OWN verify-time epoch. A value groundable ONLY in a STRICTLY-LATER capture \u2014 a subsequent step's / a different entity's snapshot \u2014 is cross-entity-deferred grounding (the census asess_1784714866561_jhc34ht9 step 6: base-draft pins pin_page_grounding:would_fail at the step's own epoch, then matched off the duplicate-draft (e674/e731) and submitted-request (e1337) snapshots, all past the base-draft epoch) and emits a shadow `narration_fidelity:would_flag{surface:'grounding_epoch', stepIndex, criterion, groundingEpoch, stepEpoch, epochDriftRefs}`. ABSTAINS (never flags) on a value grounded at/before the step's own epoch (honest same-epoch grounding, or a legitimately-earlier carried observation), an explicitly carried-forward observation with recorded provenance, a criterion that did not pass, or an UNDETERMINED epoch (a missing epoch never manufactures a flag). When this flag is ON it ENFORCES via the existing VERIFY_REOBSERVE_WITHHOLD soft-withhold rail: the cross-entity-grounded criterion is treated as UNCONFIRMED and routed to a warning (never a red fail \u2014 the values may be correct; the fix removes the false EVIDENCE ATTRIBUTION, not the pass). Off leaves every verdict byte-identical (shadow diag only). Distinct from PIN_PAGE_GROUNDING (value ABSENCE at grade time) and SETUP_NOTE_GROUNDING (setup/action step NARRATION); this grounds a verify criterion's EVIDENCE-EPOCH provenance.",designDoc:"docs/plans/2026-07-22-substantiation-fidelity-design.md",status:"active",added:"2026-07-23",notes:"Default OFF in code; detection runs in shadow always (would_flag diag), enforcement (soft-withhold to warning via VERIFY_REOBSERVE_WITHHOLD) is gated \u2014 the COMPLETION_EVIDENCE_FLOOR / SETUP_NOTE_GROUNDING shadow-first precedent. Single RunnerRuntime run_complete pass (mirrors the setup-note-grounding shadow). Deterministic \u2014 ~zero marginal LLM cost (reconstructs each step's own capture epoch + each pinned value's grounding epoch from the retained per-generation full snapshots collected during the run). P1 of the substantiation-fidelity track (S-B); S-A (SETUP_NOTE_GROUNDING) shipped P0, S-C/S-D are separate flags.",graduation:{status:"gated",gate:"Staging shadow soak clean (narration_fidelity:would_flag{surface:'grounding_epoch'} fires on the census-shaped A\u2192duplicate-B reproduced-RED fixture, zero would_flag on same-epoch honest grounding controls) AND the engine-core groundingEpochFidelity detection-inversion passes (rip the drift out \u2192 the fixture stops flagging; enforce-ON soft-withholds the criterion to warning, never red, on the identical input)",evidence:"staging narration_fidelity:would_flag diag events + the engine-core groundingEpochFidelity + RunnerRuntime.groundingEpoch unit suites + the runner/narration-grounding-epoch reproduced-RED eval",owner:"steering (Alex)",review:"2026-07-30"}},NOTE_CONTRADICTION_FLOOR:{key:"NOTE_CONTRADICTION_FLOOR",envVars:["AGENTIQA_NOTE_CONTRADICTION_FLOOR","AGENTIQA_EXPERIMENT_NOTE_CONTRADICTION_FLOOR"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Terminal-error verdict-honesty Layer A \u2014 the deterministic note-contradiction floor (run_27825c90). Detection ALWAYS runs (pure module) at RunnerRuntime run_complete, LAST \u2014 after every fail gate: for a step the model graded `passed` carrying a passed criterion whose OWN note/groundingObservation text contains a conservative error marker (error/failed/failure/timeout/timed out/aborted/exception/went wrong, word-boundary) that the criterion's own check/expectedValue (or its step text) does NOT license, it caps that step to `warning` with an explanatory note. A marker GOVERNED BY A NEGATOR in the same clause (no/not/never/without/none/zero/didn't/no longer/free of/\u2026 \u2014 see NEGATOR_RE) is BENIGN and never caps ('No error appeared', 'verified no timeout occurred'), so a note that negates every marker is skipped; a note that re-asserts an error after negating one ('no error at first, then Error: timeout appeared') still caps. ONE-DIRECTIONAL \u2014 only caps a passed step (never rescues/escalates a model-failed grade; the criterion result stays passed, the STEP status is soft-withheld like VERIFY_REOBSERVE_WITHHOLD). Check-text licensing is dumb-string (accepts the documented `verify NO error` negation blind spot on the CHECK \u2014 Layer B handles it semantically). This flag ALSO gates the deterministic report_issue verdict-bearing fold (decision 6): a high/medium-severity `logical` issue filed during an otherwise-clean `passed` run caps the run at `warning`. When ON it ENFORCES; off \u21D2 `note_contradiction:would_cap` / `report_issue_contradiction:would_cap` diags and every verdict byte-identical. Distinct from Layer B (TERMINAL_ERROR_FLOOR, a model terminal-screen read). Ships default-ON (the trap eval's warning-cap signature only greens with both floors live).",designDoc:"docs/plans/2026-07-23-terminal-error-verdict-honesty-design.md",status:"active",added:"2026-07-23",notes:"Ships default-ON from inception (not graduated from an off default) \u2014 registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED. Deterministic (~zero marginal LLM cost \u2014 scans the already-collected stepResults notes + grounding observations). Pure logic in packages/engine-core/src/noteContradictionFloor.ts; applied at RunnerRuntime run_complete AFTER all fail gates so it only ever touches a still-passed step. Runner lane only. Layer A of the terminal-error wave; TERMINAL_ERROR_FLOOR is Layer B."},TERMINAL_ERROR_FLOOR:{key:"TERMINAL_ERROR_FLOOR",envVars:["AGENTIQA_TERMINAL_ERROR_FLOOR","AGENTIQA_EXPERIMENT_TERMINAL_ERROR_FLOOR"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Terminal-error verdict-honesty Layer B \u2014 one unconditional terminal-screen model read per plan run (run_27825c90). At RunnerRuntime run_complete (LAST, after every fail gate) the terminal capture plus the plan's step texts are sent through the existing `deps.blindReader` seam, asked whether an app error/failure state is visible that no plan step asserts. The terminal frame is selected DELIBERATELY from the in-memory `_screenshots` ledger (NOT R2 \u2014 works in the anonymous eval lane where imageStorage is null): the LAST full-frame capture (crops and post-upload shots are skipped; each push site is kind-tagged), abstaining when none exists or when it predates the last browser action (stale). Licensing is SEMANTIC (the reader sees the step texts, so it handles negation like `verify NO error is shown`). An unlicensed visible error \u21D2 cap the run at `warning` by demoting the highest-index PASSED step (via capRunToWarning \u2014 never a skipped/failed step; when a sibling floor already capped the terminal step, the quote is appended, not double-capped) and quote the error text in the step note + run summary. Reader unavailable / no full-frame / stale frame / non-answer / licensed error \u21D2 abstain no-op (never a new false-fail). ONE-DIRECTIONAL (only caps a still-passed step; the binary run status never flips \u2014 `warning` is the step-level/derived aggregate). When ON it ENFORCES; off \u21D2 `terminal_error:would_cap` shadow, no cap. This is the ONLY layer that fires when the model never transcribes the banner into any note (the incident shape). Distinct from Layer A (NOTE_CONTRADICTION_FLOOR, deterministic note scan). Ships default-ON.",designDoc:"docs/plans/2026-07-23-terminal-error-verdict-honesty-design.md",status:"active",added:"2026-07-23",notes:"Ships default-ON from inception \u2014 registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED. Cost \u2248 one cheap Flash vision call per plan run (the cost-isolated blindReader model, same as BLIND_DOUBLE_READ). Detection (the read) runs whenever a reader + final capture are available, EXCEPT at an explicit `=0`: since #3114 the switch gates the call itself (`terminal_error:detection_off`), so `=0` genuinely reclaims the vision call \u2014 it used to read and shadow anyway. Consequence: `enforce` is false under exactly that condition, so the `terminal_error:would_cap` shadow row is unreachable from production and survives only as the method parameter contract (still unit-pinned). Pure prompt/interpretation in packages/engine-core/src/terminalErrorFloor.ts. Runner lane only. Layer B of the terminal-error wave; NOTE_CONTRADICTION_FLOOR is Layer A + the report_issue fold."},TERMINAL_ERROR_FLOOR_NARROW:{key:"TERMINAL_ERROR_FLOOR_NARROW",envVars:["AGENTIQA_TERMINAL_ERROR_FLOOR_NARROW","AGENTIQA_EXPERIMENT_TERMINAL_ERROR_FLOOR_NARROW"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"NARROWS Layer B's cap rule (#3114). Two screens over the SAME terminal read TERMINAL_ERROR_FLOOR already pays for \u2014 no extra LLM call. (1) CLASSIFICATION of the reader's quoted APP text: `progress` (an in-flight/pending notice \u2014 'Some search agents have not finished in time\u2026') \u21D2 NEVER cap, NO summary prefix, NO `terminal_error_quote` cause-grounding anchor; `error` (an un-negated error marker or an HTTP 4xx/5xx form survives Layer B's OWN narrow negator \u2014 \u22642 tokens before the marker, clause ends at a comma) \u21D2 eligible; `ambiguous` (neither lexicon hits \u2014 both are English-only, so every non-English banner lands here) \u21D2 TODAY'S BEHAVIOUR UNCHANGED, deliberately, so the narrowing is a progress narrowing and not an English-language one. (2) ATTRIBUTION for the `error` class: the EARLIEST step whose retained full page observation (`_fullSnapshotByStep`) contains the quote, else the terminal capture's own `_currentStepIndex` stamp clamped above by `_maxStepEverExecuted`; NO attribution \u21D2 DISCLOSE (summary prefix + anchor) and demote NOTHING. The `argmax(stepIndex)` cap target and `capRunToWarning`'s highest-passed fallback are BOTH removed from this path (the fallback re-arms the defect: an attributed step that is not `passed` would otherwise fall through to the terminal step again). Requirement (c) \u2014 never demote a strict-substantiated step unless the banner is attributed to its own action \u2014 holds by construction. UNSET (default) is SHADOW: classification + attribution run beside the live rule and emit `terminal_error:banner_class`, `:attribution`, `:would_cap_narrowed` / `:would_not_cap` and `:narrow_divergence`, while every verdict byte is the live rule's. `=1` ENFORCES the narrowed rule. `=0` switches the narrowed screens off entirely (the live rule alone, no new rows). The PARENT `AGENTIQA_TERMINAL_ERROR_FLOOR=0` now disables DETECTION itself \u2014 the blind read no longer runs at all \u2014 so `=0` on the parent really does reclaim the call (it used to read and shadow anyway).",designDoc:"docs/plans/2026-07-23-terminal-error-verdict-honesty-design.md",status:"active",added:"2026-09-10",notes:"Shadow-first per feedback_runtime_guards_shadow_first: a runtime guard over model/app behaviour ships with would_* markers and a census on real plans, never enforce-by-default. Zero marginal LLM cost \u2014 regex over strings already in memory. defaultState off \u21D2 no killSwitchDefaultState INTENTIONALLY_GRADUATED entry (that allowlist is for 1-enables flags left ON). Pure logic in packages/engine-core/src/terminalErrorFloor.ts (classifyBannerSemantics / attributeBannerToStep / decideNarrowedTerminalError); wired in RunnerRuntime.applyTerminalErrorFloor. Runner lane only. KNOWN STRUCTURAL BIAS, measured not hidden: `_fullSnapshotByStep` is written only from the auto-verify-evidence path, so attribution source `snapshot_text` can only ever name a VERIFY step \u2014 the same population #3103 VERIFY_REOBSERVE_WITHHOLD already demotes. The `attribution` census row carries `attributedStepIsVerify` so that interaction is visible BEFORE any flip.",graduation:{status:"gated",gate:"GRADUATE to defaultState:'on' only when ALL hold on the prod nightly census over >=5 consecutive nights and >=60 runner sessions that emitted a `terminal_error:banner_class` row: (1) NO SUPPRESSED TRUE POSITIVE \u2014 zero `terminal_error:narrow_divergence` rows whose run ALSO carries an independent error signal for the same quote (a `note_contradiction:cap`/`would_cap` row, a high/medium `logical` report_issue, or a model-graded failed/blocked step); this is the only direction the narrowing can hurt. (2) THE FALSE POSITIVE IS MEASURABLY GONE \u2014 >=1 `narrow_divergence{reason:'progress_notice'|'different_step'|'strict_substantiated'}` on a run whose steps were otherwise all passed (the #3114 shape reproduced, with the narrowed rule standing down). (3) ATTRIBUTION IS REAL, NOT DEGENERATE \u2014 `terminal_error:attribution{source:'none'}` < 50% of error-classified reads; a majority-`none` census means the rule collapsed into 'never cap' and must be redesigned, not graduated. (4) THE AMBIGUOUS RATE IS BOUNDED \u2014 publish the full `banner_class` distribution and require `ambiguous` < 25% of classified reads; a high ambiguous rate means the English-only lexicons are carrying a non-English population on the legacy path and the classifier needs the language work BEFORE the flip, not after. (5) BOUND EVALS GREEN \u2014 plan-run/terminal-error-surfaced and \u2026-loud at their declared minPassRate 0.67 with AGENTIQA_EXPERIMENT_TERMINAL_ERROR_FLOOR_NARROW=1 forced in the case env, plus plan-run/terminal-progress-notice-control and \u2026-de-control green at N>=3. The de-control's judge REQUIRES a `terminal_error:banner_class` row (a replicate whose reader stayed silent FAILS) and requires the legacy path (no `terminal_error:no_cap`/`:disclose_no_cap` row, any cap booked under `ambiguous`), so its green cannot be earned by a run that never exercised the mechanism; a replicate whose reader TRANSLATED the German toast is reported not-applicable with the quote, never red. The en-control deliberately still passes on reader silence \u2014 a `NONE` answer there means Layer B never fired, the best outcome the narrowing can produce. The deterministic half of the non-English claim (an `ambiguous` banner takes the legacy path byte for byte) is pinned with a mocked reader in RunnerRuntime.terminalErrorFloor.test.ts, so gate (5) reads the live cases as a census, not as the proof. (6) RECORD \u2014 docs/FLAGS-STATE.md records the live state and docs/VERDICT-GATES.md the flipped mechanism in the SAME PR as the defaultState flip.",evidence:"prod nightly `terminal_error:*` census rows (banner_class / attribution / would_cap_narrowed / would_not_cap / narrow_divergence) + the four bound runner eval cases",owner:"steering (Alex)",review:"2026-09-24"}},BLIND_DOUBLE_READ:{key:"BLIND_DOUBLE_READ",envVars:["AGENTIQA_BLIND_DOUBLE_READ","AGENTIQA_EXPERIMENT_BLIND_DOUBLE_READ"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"When on, run_complete independently re-reads the persisted evidence screenshot for each pinned criterion that survived substantiation as passed (a blind LLM read that never sees the expected value) and flips the pass to failed when the observed value does not match the pin; abstains (no-op) on missing image / R2 read failure / reader non-answer / batch timeout. Off makes zero extra LLM calls and leaves every verdict unchanged.",designDoc:"docs/plans/2026-07-15-blind-double-read-design.md",status:"active",added:"2026-07-15",notes:"Default OFF in code; forced ON in staging via the orchestrator env (AGENTIQA_EXPERIMENT_BLIND_DOUBLE_READ=1) \u2014 the exact GROUNDED_EXPECTATIONS precedent. Prod enablement is a later explicit flip after nightly baselines. Runner lane only (RunnerRuntime run_complete).",graduation:{status:"gated",gate:"HARDENED 2026-07-21: BDR must actually FIRE (reads>0) and FLIP on runner/blind-read-conflation-trap \u2014 not merely avoid errors. BLOCKED on the evidence-resolution gap: on the conflation trap BDR abstained missing_image (resolveEvidenceMessage found no hasScreenshot+planStepIndex message \u2014 the model graded from the inline tool-result snapshot, which is never persisted as a screenshot message) and the seeded false-pass shipped (qa-exhaustive 29861680933). Fix = guarantee a planStepIndex-stamped verify screenshot (STEP_MARKER_FOLD stamping path is the natural vehicle \u2014 same root as the run-detail evidence-fidelity gap) or broaden resolveEvidenceMessage fallback. Plus: no new false-FAIL class on nightly runner baselines (MET as of 07-21; abstains acceptable). tp_00b325d5 confirm-path re-verified 07-21 (2 reads/2 confirms/0 flips).",evidence:"runner/blind-read-{parroting,conflation}-trap eval verdicts (conflation must flip) + blind_double_read diag events + nightly runner baselines",owner:"steering (Alex)",review:"2026-07-28"}},NEVER_GRADED_RETRY:{key:"NEVER_GRADED_RETRY",envVars:["AGENTIQA_NEVER_GRADED_RETRY","AGENTIQA_EXPERIMENT_NEVER_GRADED_RETRY"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:'When on (and a criterionRegrader is wired at engine boot), run_complete re-asks the grader ONCE for a STRICT pinned required criterion that NO stepResults entry graded (genuine model-omission \u2014 absent from the FIX 2 cross-entry union) before Amendment 5a synthesizes its "never graded \u2192 unconfirmed \u2192 fail". The returned grade is fed through the SAME substantiatePinnedCriterion gate, so only a re-grade that independently CITES the pinned value rescues the pin to passed; a returned FAIL, an unsubstantiated pass, or an abstain (no image / read failure / non-answer / error / batch timeout) leaves the "never graded" fail byte-identical. Cap ONE retry per pin, no loops. Off makes zero extra LLM calls and leaves every verdict unchanged.',designDoc:"docs/plans/2026-07-27-never-graded-retry-design.md",status:"active",added:"2026-07-27",notes:"Default OFF in code; force ON in staging via the orchestrator env (AGENTIQA_EXPERIMENT_NEVER_GRADED_RETRY=1) \u2014 the BLIND_DOUBLE_READ / GROUNDED_EXPECTATIONS shadow-first precedent. Prod enablement is a later explicit flip after a benchmark + staging soak. Runner lane only (RunnerRuntime run_complete, buildRunnerDeps.getCriterionRegrader \u2014 the SAME cost-isolated Flash model as blindReader; child Runners on the web-coordinator lane do not receive it yet, mirroring blindReader's own coordinator-forward gap). COMPOSES with (does not regress) PR #1884 FIX 2: the retry fires ONLY on pins genuinely absent from the cross-entry union FIX 2 computes, i.e. exactly the omission FIX 2 deliberately left as a fail. killSwitch resolves the no-env value from this defaultState (#1729), so GRADUATING = flip defaultState to 'on' AND add NEVER_GRADED_RETRY to INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Read site: packages/engine-core/src/RunnerRuntime.ts (neverGradedRetryEnabled \u2192 runNeverGradedRetries). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.neverGradedRetry.test.ts.",graduation:{status:"gated",gate:"FIRST SLICE = build + unit (this PR): shadow behind the default-OFF flag, byte-identical when off, fail-closed-safe (retry never manufactures a pass; a genuinely-ungraded-after-retry pin still fails). GRADUATION (later, separate slices) needs: (1) a kind-agnostic graduation benchmark (e2e/benchmark/, the ABSENCE_AWARE_VERIFY #1857 precedent) showing the Lio-s2 omission false-FAIL class is rescued to PASS with 0 new false-PASS and 0 regressions on the runner corpus, adversarially proven able to say NO-GO; (2) a staging shadow/force-ON soak measuring re-grade fire rate + rescue vs abstain vs still-fail; (3) parity when BLIND_DOUBLE_READ is also on; (4) gate-review sign-off (Alex).",evidence:"never_graded_retry:{start,rescue,abstain,still_fail} diag events + the graduation benchmark verdict + a staging soak on re-grade outcomes",owner:"steering (Alex)",review:"2026-08-03"}},EVIDENCE_FIDELITY:{key:"EVIDENCE_FIDELITY",envVars:["AGENTIQA_EVIDENCE_FIDELITY","AGENTIQA_EXPERIMENT_EVIDENCE_FIDELITY"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"P0 of the evidence-fidelity ('honest pictures') design: makes full_page_screenshot honest. When on, BasePlaywrightService.fullPageScreenshot climbs a capture ladder (document_full \u2192 cdp_full \u2192 scroll_stitch \u2192 viewport_degraded) so an inner-overflow layout (asklio line-items, where the document is viewport-height but the scrollable content lives in an inner overflow:auto container) is captured at its true extent instead of a silent viewport crop; the ONLY non-full path stamps EnvState.captureMode 'viewport_degraded' and emits the full_page_capture:degraded marker \u2014 never a silent lie. Off is byte-identical to today EXCEPT the geometry measurement + a shadow degradation marker still run (detect-only) so staging can measure the lie's live frequency before the capture behavior flips. EnvState.captureMode is set in BOTH states.",designDoc:"docs/plans/2026-07-22-verify-evidence-fidelity-design.md",status:"active",added:"2026-07-22",notes:"Default OFF in code; force ON in staging via the orchestrator env (AGENTIQA_EXPERIMENT_EVIDENCE_FIDELITY=1) \u2014 the GROUNDED_EXPECTATIONS / BLIND_DOUBLE_READ precedent. Prod flip is a later explicit step after nightly baselines + a shadow soak on the degradation marker. killSwitch resolves the no-env value from this defaultState (#1729), so GRADUATING = flip defaultState to 'on' AND add EVIDENCE_FIDELITY to INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Read sites: (P0) packages/engine-core/src/BasePlaywrightService.ts fullPageScreenshot \u2192 the honest capture ladder; (P1) packages/engine-core/src/RunnerRuntime.ts \u2192 per settled static-verify batch, capture ONE canonical honest full_page_screenshot, derive planStepIndex-stamped per-criterion crops (region resolved off the graded node; uncropped fallback), and persist a StepEvidenceRef on each TestPlanV2StepResult/CriterionResult at run_complete. Flag OFF is byte-identical to today EXCEPT the P0 shadow degradation marker: no canonical capture, no refs. P1 supersedes the run-detail findStepEvidenceIndex heuristic on runs that carry refs (legacy no-ref runs fall back). P2 (flip BLIND_DOUBLE_READ on the planStepIndex-stamped evidence P1 now produces) is the remaining follow-up gated on this.",graduation:{status:"gated",gate:"P0/P1 reproduced-RED evals green with the flag ON (runner-fullpage-honesty capture-mode/height/marker facts; degraded-abstain unit; P1 wrong-region + stamping-integrity) AND a staging shadow soak on the full_page_capture:degraded marker showing the expected live frequency with NO capture regression on batch.verify-never-blind",evidence:"full_page_capture:degraded diag events (staging shadow soak) + the engine-core capture-honesty integration test + the captureFidelity unit suite + the batch.verify-never-blind regression",owner:"steering (Alex)",review:"2026-07-29"}},PER_ACTION_BILLED_STEPS:{key:"PER_ACTION_BILLED_STEPS",envVars:["AGENTIQA_PER_ACTION_BILLED_STEPS","AGENTIQA_EXPERIMENT_PER_ACTION_BILLED_STEPS"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Decouples the billed step count from the LLM-iteration count (W1 of the batched-actions design, Decision 6). When on, the main-loop agent_step llm_usage event carries an additive billedUnits integer = executed browser actions + verify captures this turn (a marker-only signal_step turn earns 0); ingest sums it into run_billing.step_count. Off omits the field entirely, so ingest bills one step per agent_step event \u2014 byte-identical behavior AND billing to pre-change. The only intended billing delta when on (today, one-action-per-turn) is that marker-only iterations bill 0 instead of 1; a batched turn executing k actions will bill k (W3).",designDoc:"docs/plans/2026-07-18-batched-actions-design.md",status:"active",added:"2026-07-18",notes:"GRADUATED 2026-07-21 (default on): staging run_billing parity check over the force-ON window (since 07-18) held exactly \u2014 116/116 flag-ON runs with step_count == executed actions + verify captures, 99/99 marker-only iterations billed 0; billing.batched-steps-parity unit lane green. Billing substrate for the batched-actions family (W1). Read site: packages/engine-core/src/billedUnits.ts (perActionBilledStepsEnabled). Remove the staging orchestrator env var once this reaches staging."},STEP_MARKER_FOLD:{key:"STEP_MARKER_FOLD",envVars:["AGENTIQA_STEP_MARKER_FOLD","AGENTIQA_EXPERIMENT_STEP_MARKER_FOLD"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Phase 1 of the batched-actions design (Decision 3): when on, the RunnerRuntime run-mode prompt instructs the model to emit the signal_step boundary marker TOGETHER with the signaled step's FIRST action in the SAME turn \u2014 but ONLY for setup/action steps; VERIFY steps keep the separate marker turn so evidence is never captured blind. This collapses the ~18.7% of runner iterations that are marker-only today. HARD COUPLING: inert unless PER_ACTION_BILLED_STEPS is ALSO enabled \u2014 a folded turn under iteration-billing would bill 1 where marker+action should bill 2, breaking billing parity. The code guard (stepMarkerFold.ts::stepMarkerFoldEnabled = STEP_MARKER_FOLD && PER_ACTION_BILLED_STEPS) makes the fold prompt byte-identical to pre-change whenever PER_ACTION_BILLED_STEPS is off, so this flag alone changes nothing. No engine dispatch change: the multi-call loop already runs signal_step before the folded action in order (both in one generation the model saw the same screen), and billedUnits (W1) already counts marker+action as 1 billed unit.",designDoc:"docs/plans/2026-07-18-batched-actions-design.md",status:"active",added:"2026-07-18",notes:"Default OFF in code. Prompt-only change (no engine dispatch or verify-flow change). Read site: packages/engine-core/src/stepMarkerFold.ts (stepMarkerFoldEnabled), consumed in RunnerRuntime.buildRunnerPrompt run-mode signal_step cadence directive. The AND-coupling with PER_ACTION_BILLED_STEPS lives in code, not just here: flipping STEP_MARKER_FOLD=1 while PER_ACTION_BILLED_STEPS stays off is a no-op.",graduation:{status:"gated",gate:"Default-off soak on staging, then a staging flip (with PER_ACTION_BILLED_STEPS on) proves per-step verdict counts identical to OFF and marker-only iterations drop from ~18.7% to <7% (batch.step-attribution-preserved)",evidence:"batch.step-attribution-preserved unit lane + staging marker-only-iteration % soak measurement",owner:"steering (Alex)",review:"2026-07-25"}},WARNING_CRITERION_BIND:{key:"WARNING_CRITERION_BIND",envVars:["AGENTIQA_WARNING_CRITERION_BIND","AGENTIQA_EXPERIMENT_WARNING_CRITERION_BIND"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'RETIRED 2026-08-04 by the typed criterion reference. It gated the render tiers of the TEXT binder \u2014 strip the platform-appended " (warning only)" suffix, resolve {{unique}}/{{timestamp}} \u2014 so a failing warning-only grade could bind by text and keep its authored strict:false instead of being forced strict:true by the ordinal fallback, plus the C1 structural gate that decided when an ORDINAL-bound failing grade could keep an explicit strict:false. A grade now names its criterion by index (`criteriaResults[].criterionIndex`), so there is no text tier to gate and no ordinal fallback to fail closed against: a bound reference simply takes the criterion authored flag, an unbound one takes strict:true. Setting this env does nothing. Entry kept for provenance only; the switch has no read site.',designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"deprecated",added:"2026-07-16"},CRITERION_BIND_RESIDUAL:{key:"CRITERION_BIND_RESIDUAL",envVars:["AGENTIQA_CRITERION_BIND_RESIDUAL","AGENTIQA_EXPERIMENT_CRITERION_BIND_RESIDUAL"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"RETIRED 2026-08-04 by the typed criterion reference. It chose between the two-pass binder (text bindings claim their slot first, then a positional residual pass) and the sequential one whose ordinal fallback could steal a slot a later grade matched by exact text \u2014 a grade-ORDER lottery over identical observations. Both passes are gone: a grade states which criterion it graded, so no slot is ever assigned by position and the order the model lists its grades in decides nothing. Setting this env does nothing. Entry kept for provenance only; the switch has no read site.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"deprecated",added:"2026-07-28"},CRITERION_TYPED_REFERENCE:{key:"CRITERION_TYPED_REFERENCE",envVars:["AGENTIQA_CRITERION_TYPED_REFERENCE","AGENTIQA_EXPERIMENT_CRITERION_TYPED_REFERENCE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"THE ESCAPE HATCH #2238 SHIPPED WITHOUT (issue #2252). The typed criterion reference made `bindGradesByCriterionIndex` the single, unconditional grade -> criterion binder and RETIRED both of its predecessors' switches (WARNING_CRITERION_BIND, CRITERION_BIND_RESIDUAL are `deprecated` and inert), so from 2026-08-04 there was NO way to turn the class off. That matters because the binder decides more than a binding: a grade whose `criterionIndex` is missing / non-integer / out of range / step-mismatched binds to -1, which (a) leaves the pin's plan slot OUT of FIX 2's `gradedPlanSlotsByStep` union, so Amendment 5a synthesizes `{strict:true, passed:false, note:'Pinned expectation \"X\" was never graded - treated as unconfirmed.'}` and FAILS the step, (b) forces `strict:true` on the grade regardless of the criterion's authored flag, and (c) makes the whole `run_complete` bounce once. PIN_SUBSTANTIATION is default-ON in prod, so at low reference compliance that is a manufactured false FAIL for every customer running pinned verify plans. Nothing in the stack can force compliance - zod does not gate this path (ai-sdk swallows InvalidToolInputError) and Gemini treats `required` as advisory - and the 99.15% figure on record (1055/1064) is 97.3% three CI plans replayed in one window with zero customer plans, i.e. replicates, not a real-traffic measurement. ON (the default, and byte-identical to every build since 2026-08-04): the typed reference is the ONLY binder; an unusable reference binds to nothing and the bounce asks for a correction. OFF (explicit `=0` under either spelling): unbound grades get ONE recovery pass inside the same binder - each still-unbound grade's echoed `check` text is matched against the step's UNCLAIMED authored criteria via `renderedCheckMatchesCriterion` (the four render tiers: raw exact, ' (warning only)' suffix-stripped, {{unique}}/{{timestamp}}-resolved, and both), first match in grade order claims the slot. Because it is inside the ONE binder, every consumer moves together and none can drift: the FIX 2 coverage union stops synthesizing a never-graded failure for a pin that WAS graded, the grade takes its criterion's authored `strict` flag instead of the fail-closed `true`, containment no longer sees it as unauthored, and the bounce no longer fires on it. The recovery is deliberately weaker than the retired text binder - no positional/ordinal residual, and no cross-step re-homing: a grade that DECLARED a step other than the entry's is refused by the recovery pass exactly as the typed branch refuses it, so it can only rebind a grade that ECHOES a criterion the plan really printed FOR THAT STEP. OFF IS NOT MERELY 'REMOVES A SYNTHESIZED FAILURE' (corrected in round-2 review, 2026-09-02). Recovery ENLARGES the entry's bound-slot set, which is the input to `evaluateCriteriaContainmentEligibility`; an entry that ON refuses containment for (`authored-slot-ungraded`) can therefore flip to `applies` under OFF, and every OTHER still-unbound grade of that entry - including one the model itself stated `passed:false` - is then EXCLUDED from the step verdict rather than failing it. The direction is still un-fail-ward in both mechanisms, but the second one drops a MODEL-STATED failure, not a synthesized one. By the same flip, a fabrication is NOT 'contained in both states': under ON with an authored slot left ungraded containment REFUSES and the fabrication is verdict-bearing; under OFF, once recovery fills that slot, containment applies and the fabrication is contained. It stays UNBOUND in both states - recovery never binds text the plan did not print - but whether it reaches the verdict differs. VERDICT-CHANGING IN THE OFF DIRECTION ONLY (docs/VERDICT-GATES.md).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-09-02",notes:"Read site: packages/engine-core/src/RunnerRuntime.ts (`bindGradesByCriterionIndex`, consulted only when the caller supplies the optional `textRecovery` argument - the run_complete pre-pass does; the exported helper called without it is unchanged for every other caller, including the L3 eval e2e/evals/plans/verdict-bounce-unauthored-grades.ts). Default ON, so this PR is a NO-OP on every verdict; the switch exists so an operator can answer a low-compliance incident with an env flip instead of a revert. Pinned in BOTH states by packages/engine-core/src/__tests__/RunnerRuntime.criterionTypedReferenceKillSwitch.test.ts."},LOGIN_IDENTITY_DERIVATION_GUARD:{key:"LOGIN_IDENTITY_DERIVATION_GUARD",envVars:["AGENTIQA_LOGIN_IDENTITY_DERIVATION_GUARD","AGENTIQA_EXPERIMENT_LOGIN_IDENTITY_DERIVATION_GUARD"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"The two corrections #2245 needed before its behaviour could be trusted in a release (issue #2252), behind one switch so an operator can restore the exact 2026-08-04 derivation without a redeploy. (1) SECRETS HANDLING - `pushIdentity` probed `looksLikeSecretValue(normalizeEmail(value))`, i.e. a TRIMMED AND LOWERCASED string, which makes the high-entropy tier (it requires an upper-case character) UNREACHABLE and the `AKIA[0-9A-Z]{16}` and `-----BEGIN ... PRIVATE KEY-----` patterns unmatchable. Plaintext passwords, AWS access-key ids and PEM private keys therefore entered `loginCredentialIdentities`, which is compared against typed field values and can reach the profile ATOM. ON probes the RAW trimmed value; `isEmailLike` still reads the normalized form, because address comparison is case-insensitive by design and only the secret shape is case-bearing. (2) IDENTITY-LABEL NARROWING - a credential that already yielded its real `login` ALSO pushed its SECRET as an identity whenever its `name` read as an identity-field label (`username`, `login`, `email`), which is exactly the pooled-row shape (account in `login`, role-ish name beside it). ON gates that push on `!hasLogin`; the row keeps contributing its `login`, so no project loses an identity. The back-compat email-like-secret clause is gated on the EFFECTIVE branch, not on the raw `isIdentityLabel`: keying it to the raw test (the first cut, caught in review) made ON DROP an email-like secret outright on an `isIdentityLabel && hasLogin` row, since the branch refused it and the clause skipped it as already handled. That clause only ever pushes email-like values and `pushIdentity` exempts those from the secret-shape denylist by construction, so routing them through it re-opens no leak. Both directions are NARROWING (fewer values in the identity set), which is why the switch matters: `credentialsProvideLoginIdentity` is this list being non-empty, and it decides whether the testing-email canonicalizer overwrites a typed login, whether the third-party-IdP terminal fires, and whether the credential-fabrication guard rejects a `report_issue`. Neither correction can empty a set that was non-empty for a legitimate reason - (1) only drops values that are secret-SHAPED and not email-like, (2) only drops a value from a row that supplied a `login` which is itself pushed - but the switch exists so that claim is falsifiable in production rather than merely argued. OFF restores the byte-identical 2026-08-04 derivation.",designDoc:"packages/engine-core/src/runner/credentialUtils.ts",status:"active",added:"2026-09-02",notes:"Read site: packages/engine-core/src/runner/credentialUtils.ts (`loginIdentityDerivationGuardEnabled`, consulted by `pushIdentity` and by `deriveCredentialIdentityRecords`). Pinned in BOTH states by packages/engine-core/src/__tests__/credentialUtils.loginIdentityDerivationGuard.test.ts; the pre-existing credentialUtils.loginIdentityRule suite pins that no legitimate identity shape moved."},CRITERIA_CONTAINMENT:{key:"CRITERIA_CONTAINMENT",envVars:["AGENTIQA_CRITERIA_CONTAINMENT","AGENTIQA_EXPERIMENT_CRITERIA_CONTAINMENT"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"GRADUATED 2026-08-03 (default ON) \u2014 flipped OFF\u2192ON because the class it contains is LIVE ON PROD and is what the verdict-replay gate is red on. Closed-world containment on run_complete: a step's verdict is decided ONLY by criteria the PLAN authored. A graded criterion result that binds to NO authored slot of its step (the grading model invented it) is still persisted \u2014 marked `unauthored: true` for forensics \u2014 but `deriveStepStatusFromCriteria` skips it, so a fabricated strict failure can no longer decide a step. Live prod-shaped defect (canary tp_vrgate_offer_upload_retry step 13, ~2/9 replicates): the step authors exactly 2 criteria (Description \"Montage und Einweisung\", Unit \"St\xFCck\"), both graded PASS with correct grounding, and the model additionally graded a THIRD criterion \u2014 `Order line 2 Quantity is \"St\xFCck\"`, strict:true, observed \"1\" \u2014 that exists in neither the plan nor the DB; that invented failure hard-failed the run. Detection ALWAYS runs, in BOTH states: at the default the engine excludes and logs `criteria_containment:excluded` {stepIndex, criterionText, strict, observed, wouldFlipStep, authoredCriteria, gradedCriteria}; with an explicit `=0` under either spelling every verdict is byte-identical to pre-graduation staging and the engine logs `criteria_containment:would_exclude` with the SAME payload \u2014 so an operator who kills the class keeps the measurement. `wouldFlipStep` was the graduation evidence and remains the incidence metric \u2014 it is true only where the exclusion actually moves the step status, so a fabricated PASSING grade stays pure telemetry in either state. THREE DELIBERATE NARROWINGS keep this rescue-polarity gate from opening a false-PASS channel: (1) it fires only on a step that AUTHORED \u22651 criterion \u2014 grades filed under a criteria-less action/setup step are the cross-step MISFILE class, where today's fail-closed treatment of the unbound grade is the only adjudication that failure gets; and (2) only when EVERY authored criterion of that entry received a grade, so an unbound grade can never be excluded while an authored slot went unadjudicated (it would then plausibly BE that slot's grade under a heavy rephrase). NARROWING (2b) \u2014 TEXT-JUSTIFIED COVERAGE, added 2026-08-03 after adversarial verification of PR #2153 found a LAUNDERING vector: (2) counted a slot as graded no matter HOW it was covered, and `bindGradesToPlanSlots` PASS 2 assigns leftover slots in GRADE ORDER, so a FABRICATED grade emitted BEFORE a rephrased real one takes that real criterion's slot, coverage reads complete, and the grade left unbound (and excluded) is the REAL one. Reproduced by execution on head 2dc7691f4: criteria [Description strict, Unit strict] + grades [fabricated \"Order line 2 Quantity looks plausible\" PASS, Description exact PASS, Unit REPHRASED FAIL observed \"kg\"] booked the run `passed` at the graduated default versus `failed` at `=0`, on 7 of 8 realistic rephrase forms (reworded, units added, whitespace, case, smart quotes, DE/EN, truncated; only the platform's own \" (warning only)\" suffix still bound by text), with the diag itself reporting wouldFlipStep:true. The replay gate could NOT see it \u2014 the exclusion consumes exactly one unit of surplus, so (C1) 2<=2, (C2) overreach 0 and (B) were all green and buildReport returned ok:true on a laundered run. `evaluateCriteriaContainmentEligibility` therefore refuses exclusion whenever ANY authored slot was covered by PASS 2's positional residual rather than a PASS 1 `matchedByText` binding: a positional binding is ordinal-only evidence (the C1 strict-honoring gate already refuses to trust it for the strict flag, and containment is strictly stronger since it DELETES the grade). The refusal is entry-wide rather than per-slot \u2014 an honest reorder is indistinguishable from slot theft without reading grade text \u2014 so both keep the fail-closed treatment, and the engine logs `criteria_containment:refused` {stepIndex, reason:'positional-only-coverage', criterionText, strict, observed, wouldFlipStep, positionalSlots, authoredCriteria, gradedCriteria} in BOTH flag states, where wouldFlipStep:true means laundering was PREVENTED. ACCEPTED COST, pinned by test: a genuine fabrication sitting beside an honest positional binding is no longer contained \u2014 a false FAIL, never a false PASS. Both frozen incident runs bind BOTH authored slots by exact text, so the graduation class is untouched (asserted end-to-end through handleToolCall and through the replay gate's own buildReport: `=0` -> ok:false with plan_verdict_regression x2 + criteria_over_grading x2; default -> ok:true, engineContainedResults 2, containmentOverreachSteps 0). Identification reuses the ONE authoritative binder \u2014 `bindGradesToPlanSlots` per-entry output (text tiers incl. the platform's \" (warning only)\" rewrite, then residual positional) \u2014 never a new text comparison: measured over 76 persisted corpus runs, 58 of 60 text-unbound results were exactly that legal rewrite, so a text-equality containment rule would be ~97% false positives. A THIRD narrowing closes the DUPLICATE-READ false-PASS channel (review round 2026-07-29): the binder claims slots EXCLUSIVELY, so a SECOND grade of the SAME authored criterion binds to nothing and unguarded containment would discard it \u2014 and when the two reads contradict ([passed:true, then passed:false observed \"\u20AC35.00\"] for one authored total) the discarded one is the FAILING one, passing the step on a record whose \"unauthored\" criterionText is byte-identical to the authored check. Before excluding, `findDuplicateReadCriterionIdx` re-runs the binder's own text tiers over ALL authored criteria with the claimed set IGNORED; a match means duplicate READ, containment REFUSES to exclude, the grade keeps its flag-OFF effect (the step can still fail \u2014 the correct polarity for a contradicting observation), and the engine logs `criteria_containment:duplicate_read` {stepIndex, criterionIndex, criterionText, passed, wouldHaveExcluded} in BOTH flag states. RESIDUAL: identification is a TEXT relation, so a REPHRASED contradicting second read (\"Der Gesamtbetrag lautet \u2026\") matches no tier and is still excluded under the flag \u2014 the shadow soak's would_exclude population must be reviewed for that shape before graduation. That residual is the DUPLICATE shape (one authored criterion graded TWICE, the paraphrased second read dropped) and is DISTINCT from the positional-slot-theft class narrowing (2b) closes, where the authored criterion is graded exactly ONCE and that single grade is discarded. GATE HALF (detector, no verdict effect): `criteria_over_grading` in e2e/scripts/verdict-replay-gate.mjs, PR #1934, split in two at this graduation because the gate reads a PERSISTED array in which the excluded result is still present. (C1) counts only the results that still DECIDE \u2014 `criteriaResults.filter((cr) => cr.unauthored !== true).length <= step.criteria.length` \u2014 so a contained fabrication is no longer a violation while an UNMARKED surplus is the pre-graduation violation verbatim (identical on every legacy run and under `=0`, where nothing is marked). (C2) `criteria_containment_overreach` polices the OPPOSITE direction: the engine may exclude at most `persisted - authored`, so marking more than the surplus \u2014 containment removing a criterion the plan really authored \u2014 is a hard gate failure. The gate deliberately CONSUMES this mark rather than re-deriving a binding of its own: a second, independently-drifting binder in the gate is exactly how a runtime fix and its gate come to disagree, and it is (C2) that makes trusting the mark safe, since the mark is only ever believed up to a cardinality the gate verifies for itself. Equivalence is pinned by e2e/scripts/criteria-containment-gate-equivalence.test.ts (the gate's counted set === the set `deriveStepStatusFromCriteria` reduces over, asserted on the two frozen prod-shaped runs). NO RESCUE \u2014 THE ENGINE BOUNCES INSTEAD (2026-08-04). The filter alone is STRUCTURALLY INERT on the production shape: `deriveStepStatusIgnoringAuthorship` returns a reported `failed` VERBATIM before it looks at any criterion, so when the model stamps the step `failed` BECAUSE of the criterion it invented, `derive(failed, all) === derive(failed, contained) === failed` and the exclusion moves nothing. Live proof \u2014 staging replicate run_d5b6f130-734f-4f57-8067-d43aa617c7ae, plan tp_vrgate_offer_upload_retry step 13: 2 authored criteria both PASS with grounding, plus a third graded `Order line 2 Quantity is 4.2` (strict, observed 1, NO groundingObservation) which is step 12's line-1 quantity copied across lines; the target is a STATIC self-testing fixture (apps/self-testing/app/lio-mirror/offer-upload-retry/page.tsx hardcodes Quantity 1) and the sibling replicate run_eccf41c3-9e09-4493-9e25-d6ae44b93c60 graded all 14 steps identically and PASSED, so there is zero app non-determinism. THREE SUCCESSIVE DESIGNS TRIED TO RESCUE THAT VERDICT and adversarial verification defeated each: (1) co-occurrence read as attribution \u2014 a failing unauthored grade merely CO-EXISTING with the step's failure rescued it, laundering 8/8 genuine non-criterion failures (timeout, error banner, HTTP 500, thrown exception, blocked navigation, disabled control, credential rejection, unobservable browser); (2) an ALLOWLIST of recognized genuine-failure signals \u2014 everything unenumerated was invisible by construction; (3) a positive execution attestation PLUS an attribution requirement \u2014 which fell too, because the attribution absorbed the excluded grade's own note/observed while BOTH that note and the step note are written by the same model in the same run_complete call, so one coherent narration made its own cause 'accounted for' (proven end-to-end: a genuine HTTP 500 with a fabricated grade whose note echoed the 500 booked passed/passed, and one coherent narration made all 8 laundering fixtures attributable). The lesson is structural: EVIDENCE THE MODEL AUTHORED CANNOT VALIDATE THE MODEL'S OWN REPORT, and every rescue reduced to trusting some model-written field. So the engine no longer rewrites a verdict at all. Instead `run_complete:unauthored_criterion_grades_rejected` BOUNCES the malformed call ONCE \u2014 naming the offending step indexes and the exact fabricated criterion texts, and instructing the model to resubmit grading ONLY the criteria the plan authored \u2014 mirroring the shipped `run_complete:incomplete_step_results_rejected` bounce (#2207). Bounded to one rejection per run (`_unauthoredGradeRunCompleteRejections`, reset in startRun beside its sibling counter) and placed immediately after the grade map, BEFORE every cascade adjudicator, so a bounce discards no gate's work. On the resubmission \u2014 or if the model repeats the fabrication and is not bounced again (`run_complete:unauthored_criterion_grades_repeated`) \u2014 the unauthored grades stay excluded from the derivation and marked, and WHATEVER THE MODEL BOOKS STANDS: a `failed` is never turned into a `passed`, at the step level or the run level. The bounce fires on ANY unauthored-and-unrelated grade regardless of its `passed` value, deliberately: whether a fabrication 'matters' is itself a model-written field, and reading one to decide is the exact error the three rescues each made in a different disguise. FORGERY GUARD (kept from the rework, now used to avoid a WRONG bounce): `excludedGradeRelatesToAuthoredCriterion` relates an excluded grade to an authored criterion over four deterministic tiers \u2014 the binder's own tiers (`findDuplicateReadCriterionIdx`), COSMETIC equality (`canonicalizeCheckTextForRelatedness`: NFKC, NBSP/space family, curly/angle/prime quotes, dash family, case, terminal punctuation, currency marks, locale number separators so 1.234,50 equals 1234.50), TRUNCATION (prefix either direction, floored at 12 canonical chars), and SUBJECT-TERM identity (`significantSubjectTokens`, shared with the note-contradiction lane). A related grade is a REPHRASED second read of a criterion the plan really does define \u2014 `findDuplicateReadCriterionIdx`'s documented residual \u2014 so the model is NOT asked to drop it, and it keeps its flag-OFF effect of failing the step. Reported on the containment diag as `relatesToAuthoredCriterion`. DIAGNOSTIC SEAM (verdict-neutral, kept on its own merits): `BaseRuntime.executeAction` calls `onBrowserActionError`, whose base implementation is an empty method RunnerRuntime never overrode, so an errored click_at / type_text_at / navigate left NO step-attributable trace anywhere in the codebase. `_stepActionErrors` now records both error shapes \u2014 the generic `status:'error'` via the overridden hook and the informative `status:'ok'` carrying `error`/`valueMismatch:true` at the dispatch seam \u2014 bounded per step, emitted as `step_action_error` and surfaced as a per-step count on the containment diag. NOTHING READS IT TO DECIDE A STATUS. GATE HALF (C3) `criteria_containment_inert`: C1 stops counting a surplus result the moment it is marked, so a MARKED-but-still-deciding fabrication went silent \u2014 gate run 30889770637 printed 'steps that graded more criteria than authored: 0' beside engineContainedResults: 1 on a run that booked failed on a criterion nobody wrote, and that false zero was read as proof the class was fixed. C3 reds when some excluded result FAILED, every counted result PASSED, and the step still books `failed`; `strictInvariant.containmentInertSteps` is printed on the same summary line as the over-graded count so the two can never be read apart again. C3 is now the whole visibility mechanism rather than a backstop to a rescue, and it fires on the incident shape by design.",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-29",notes:"2026-10-01 (#3292): the same switch now gates exact-authored-criteria correction and final invalid-report classification. One correction at most; a known counted/action failure is finalized without a replacement report. Repeated malformed reports become non-billable verification errors unless independent failures or an existing concluding blocked terminal take precedence. No failed step is promoted to passed. See docs/VERDICT-GATES.md, run_complete:invalid_criterion_report. Historical graduation record follows. GRADUATED 2026-08-03 (default ON), ahead of the 2026-08-12 review date, because the containment class is NOT a hypothetical: it is red on release PR #2149 AND on the `main` nightly (GH run 30781168843, build 0555cc09a = the build prod is serving), so the pre-graduation default is what is booking false `failed` verdicts on prod TODAY. killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND CRITERIA_CONTAINMENT listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire); an explicit AGENTIQA_CRITERIA_CONTAINMENT=0 (or the AGENTIQA_EXPERIMENT_ spelling) restores the byte-identical pre-graduation SHADOW \u2014 detection still runs, `criteria_containment:would_exclude` still fires, and NO verdict moves. STAGING-FIRST: merging to staging makes this default-ON on the STAGING engine only; PROD stays OFF until a later staging->main release carries it (a natural staged soak). Rides on the binder flags: with WARNING_CRITERION_BIND=0 or CRITERION_BIND_RESIDUAL=0 the binder is the sequential one, which leaves grades unbound in shapes that are NOT surplus \u2014 narrowing (2) is what keeps containment inert there rather than excluding a legitimately-graded criterion. Runner lane only (RunnerRuntime run_complete). Scope is step-status derivation: the rescue-eligibility gates that read `criteriaResults.every(passed)` (absence oracle, VERIFY_REOBSERVE_WITHHOLD, verify-conflict reconcile) still see the full persisted array, so a fabricated failing result can still BLOCK a rescue \u2014 the fail-closed direction, left deliberately. OPEN RESIDUAL CARRIED THROUGH THE FLIP, stated rather than hidden: the REPHRASED-DUPLICATE shape (a paraphrased second read of an authored criterion) matches no text tier, is therefore indistinguishable from a fabrication, and IS excluded at the default \u2014 a false-PASS channel the duplicate-read guard does not close and which the new (C2) gate check does NOT catch either (a rephrased duplicate consumes exactly one unit of surplus, so the exclusion is within budget). The pre-flip gate asked for a grade-by-grade shadow-soak review of that population; no such soak was run before this flip, which is the one piece of the original graduation bar that is being WAIVED rather than met, and it is why this flip is Alex's call and not a mechanical one.",graduation:{status:"gated",gate:'GRADUATED 2026-08-03 (default ON) with ONE part of this bar explicitly waived \u2014 kept as `gated` rather than deleted because the waived part is a real open false-PASS channel that still needs closing. MET: (1) the fabrication class reproduces on the canary at the documented location \u2014 tp_vrgate_offer_upload_retry step 13 authors exactly 2 criteria and the model graded 3, frozen from TWO real staging runs (run_11eeceb1-3bfc-4810-a17f-e7c23170c12e, run_81ef70ae-a3f9-4673-ba52-95378ca477d3, both 2026-08-03, both booking `failed` against a `passed` baseline on the invented `Order line 2 Quantity is "2"`, strict:true, passed:false, observed "1") into packages/engine-core/src/__tests__/fixtures/criteriaContainment.overGradedRuns.json; (2) the #1934 `criteria_over_grading` detector agrees on exactly those steps \u2014 it is what turned the gate red \u2014 and its 76-run corpus measurement recorded 2 true hits / 0 false positives, both this same defect; (3) the detector now also polices the exclusion in the opposite direction (`criteria_containment_overreach`), so over-exclusion beyond the surplus is a permanent CI check rather than a one-off review. WAIVED: the grade-by-grade shadow-soak review of the `would_exclude` population for the REPHRASED-DUPLICATE shape (a paraphrased second read of an authored criterion, which the text tiers cannot distinguish from a fabrication and which the duplicate-read guard does NOT catch). No soak was run; the flip was driven by the class being live on prod and blocking a release. That residual is the remaining work behind this entry.',evidence:"engine `criteria_containment:excluded` / `:would_exclude` / `:duplicate_read` diag events + the RunnerRuntime.criteriaContainment unit suite incl. the two frozen prod-shaped runs (claim verify.containment.authored-criteria-only) + the replay-gate `criteria_over_grading` / `criteria_containment_overreach` checks + e2e/scripts/criteria-containment-gate-equivalence.test.ts (runtime filter === gate filter)",owner:"steering (Alex)",review:"2026-08-12"}},PLAN_OBEDIENCE:{key:"PLAN_OBEDIENCE",envVars:["AGENTIQA_PLAN_OBEDIENCE","AGENTIQA_EXPERIMENT_PLAN_OBEDIENCE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Enforces the active plan step: rejects off-plan label clicks, records deviations, and lets run_complete fail on them; off makes the pre-check, deviation recording, and verdict gate all inert (pre-#1294 behavior).",designDoc:"packages/engine-core/src/RunnerRuntime.ts",status:"active",added:"2026-07-07"},LOOP_VISION_ESCALATION:{key:"LOOP_VISION_ESCALATION",envVars:["AGENTIQA_LOOP_VISION_ESCALATION","AGENTIQA_EXPERIMENT_LOOP_VISION_ESCALATION"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Master gate (Runner/Explorer lanes) for consulting the vision supervisor once before an ambiguous screenshot-blind force-block terminates the run; off keeps the deterministic hard-block.",designDoc:"docs/plans/2026-07-07-loop-detection-vision-supervisor-design.md",status:"active",added:"2026-07-07"},LOOP_VISION_DIFFERENTIAL:{key:"LOOP_VISION_DIFFERENTIAL",envVars:["AGENTIQA_LOOP_VISION_DIFFERENTIAL","AGENTIQA_EXPERIMENT_LOOP_VISION_DIFFERENTIAL"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Phase-2 differential-grant refinement of loop-vision escalation: grants 2..K need a concrete task-unit delta and the per-step ceiling rises to 12; off reverts to Phase-1 (absolute judgment, 4-grant ceiling).",designDoc:"docs/plans/2026-07-09-loop-vision-differential-extension-design.md",status:"active",added:"2026-07-09"},RUNNER_LOOP_VISION_DELTA_FROM_GRANT_2:{key:"RUNNER_LOOP_VISION_DELTA_FROM_GRANT_2",envVars:["AGENTIQA_RUNNER_LOOP_VISION_DELTA_FROM_GRANT_2","AGENTIQA_EXPERIMENT_RUNNER_LOOP_VISION_DELTA_FROM_GRANT_2"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Runner-lane (test-plan run) override of LOOP_VISION_DIFFERENTIAL's hybrid floor: the loop-vision judge's CONTINUE on grants 2..K is honored only with a measured task-unit delta (`hasTaskUnitDelta`), instead of from grant 5. Grant 1 stays absolute judgment; the chat/explorer lanes keep the 4-grant floor. THE DEFECT: on a polling step the judge said CONTINUE on grant 2 with no extractable progress (`done: null`, `deltaPresent: false`), the detector's later trips were re-armed by novelty resets, and the run looped 12.7 min / 80+ actions until a periodic supervisor BLOCK (design-partner report, 2026-10-14). Census (prod, all tenants, 60 days to 2026-10-15): a grant-2+ CONTINUE without a delta occurred in 3 sessions \u2014 that run and two assistant_v2 chat sessions, which this flag does not touch \u2014 so no other test-plan run changes. Polarity: an earlier run-level `blocked` (loop) on the same halt path; no step or criterion is re-graded and no pass can be created. Requires LOOP_VISION_DIFFERENTIAL on (the delta is only measured on a differential consult); OFF (`=0` under either spelling) restores the 4-grant floor in the runner lane.",designDoc:"docs/plans/2026-07-09-loop-vision-differential-extension-design.md",status:"active",added:"2026-10-15"},CANVAS_PIXEL_PROGRESS:{key:"CANVAS_PIXEL_PROGRESS",envVars:["AGENTIQA_CANVAS_PIXEL_PROGRESS","AGENTIQA_EXPERIMENT_CANVAS_PIXEL_PROGRESS"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Canvas-aware loop progress: on a canvas-dominant screen (a <canvas> covers >=40% of the viewport) the LoopDetector novel_screen milestone is driven by a coarse screenshot pixel-diff hash (own NOVEL_SCREEN_BUDGET) instead of the static DOM/a11y hash, and the loop-vision differential delta gate accepts a qualitative delta_evidence string when countable task units are unavailable. Off drops both so a canvas build behaves exactly as before (structural breaker climbs on the static DOM hash, delta gate stays countable-only).",designDoc:"docs/plans/2026-07-14-canvas-chat-failure-class-design.md",status:"active",added:"2026-07-14"},RUNNER_SAME_VALUE_WRITE_BREAKER:{key:"RUNNER_SAME_VALUE_WRITE_BREAKER",envVars:["AGENTIQA_RUNNER_SAME_VALUE_WRITE_BREAKER","AGENTIQA_EXPERIMENT_RUNNER_SAME_VALUE_WRITE_BREAKER"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Runner-lane same-value write breaker (#2612 / #2986 / #3006). THE DEFECT: on a plan step carrying two values the model keeps re-sending STALE args while `intent` narrates the value it meant to write \u2014 in one staging run all 11 `type_text_at` calls returned success, 10 targeted the same label, and every one carried the EMAIL as `args.text` while `intent` said it was setting the contact name. VERBATIM_INPUT_PIN is inert on customer-authored plans (no `verbatimInput` slot), and three supervisor REDIRECTs naming the error did not break the loop. WHAT =0 DISABLES: the pre-dispatch bounce only. On the 3rd CONSECUTIVE identical (normalized target, written value) write inside one declared step, with no observed field-value change in between, the write is refused as a meta result (never dispatched to the browser, and deliberately NOT a resetLoopDetector site) with an error naming the field, the repeat count, and the alternative entry paths (focus + insertText, select-all-then-type, native fill). It bounces exactly ONCE per (step, target, value) triple so a stubborn re-emitter cannot wedge the run in a bounce loop. EXEMPTIONS, so a legitimate form fill is never touched: two different fields alternating are different targets; the same field with a changed value is a different key; and an OBSERVED field-value change between two identical writes (a clear that worked, an append) resets the counter \u2014 `clearBeforeTyping` is deliberately NOT that signal, since it defaults to true on every type_text_at and reading it as a reset would make the breaker inert on exactly the traffic it exists for. ADDRESSING IS NOT IDENTITY: `set_focused_input_value` has no targeting arg at all (it writes into whatever is focused) and a coordinate-only `type_text_at` is bucketed to 50 units, so several real fields collapse onto one normalized target \u2014 three selects each focused by a click and set to the same option value in one step is ordinary. Such an AMBIGUOUS target therefore never bounces until the engine\'s own `typedIntoField` (the accessible name of the field that received the input) has confirmed the repeats reached one and the same field, and any change in that name resets the counter on every target, explicit or not. UNCONDITIONAL COMPANIONS, not gated by this flag: (a) LoopDetector.buildKey now keys write-class actions on normalized target + a short hash of the written value, so a same-field/same-value repeat is a real repeat and `set_focused_input_value` stops collapsing every focused-field write onto one key \u2014 it changes loop-block timing in every lane whatever this flag is set to, and carries its own docs/VERDICT-GATES.md entry; (b) a SHADOW-ONLY diagnostic `type_text_at:intent_literal_mismatch {stepIndex, quoted, textHash}` when `args.intent` quotes a literal that the written value contradicts \u2014 diag only, no bounce, pending a false-positive census (the `Set the "Name" field to John` shape, where the quoted token is the FIELD, is a known false positive). Diagnostic for the breaker itself: `type_text_at:same_value_repeat {target, count, stepIndex}`. Acceptance suite: packages/engine-core/src/__tests__/sameValueWrite.test.ts, RunnerRuntime.sameValueWrite.test.ts, LoopDetector.test.ts.',designDoc:"packages/engine-core/src/sameValueWrite.ts",status:"active",added:"2026-09-08"},RUNNER_RUNJS_HUNT_BREAKER:{key:"RUNNER_RUNJS_HUNT_BREAKER",envVars:["AGENTIQA_RUNNER_RUNJS_HUNT_BREAKER","AGENTIQA_EXPERIMENT_RUNNER_RUNJS_HUNT_BREAKER"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Runner-lane read-only scripting hunt breaker (#2377). THE DEFECT: on a step whose payload the runtime supplies (a `verbatimInput` pin the prompt never mentioned) the model reads "enter the requested prompt" as "the text is somewhere I have not looked" and scripts the DOM to find it \u2014 two measured blocked runs of runner/verbatim-payload show five consecutive run_js calls, ZERO screenshots, ZERO writes, then a generic loop_block with no verdict. In one run every script THREW (selector miss \u2192 TypeError); in the other all five returned `status: success` because the model\'s own `|| "No alert text found"` fallbacks masked the misses, so a status-only predicate would have caught neither reliably \u2014 what is invariant is the STREAK. None of the existing nets fire: the steering veto fingerprints name+args and a scripting tool\'s arg space is unbounded (five scripts = five fingerprints), the same-value write breaker is gated on write-class tools and these runs contain no writes, and the LoopDetector fires late and generically after awarding novel-screen milestones to the first calls. WHAT =0 DISABLES: the pre-dispatch bounce only. On the 3rd CONSECUTIVE run_js inside one declared step the call is refused before dispatch with `{status: "error", runJsHuntRefused: true, consecutiveRunJs}` and an instruction to grade the criterion from what is VISIBLE \u2014 take a screenshot or snapshot and grade honestly, including an honest fail. ACCELERATED to the 2nd call when every preceding run_js in the streak returned NO SIGNAL (top-level `status: "error"`, a `runJsResultStatus` of error/empty/undefined_return, or a serialized `runJsResult.value` of ""/null/undefined/[]/{}) \u2014 nothing is being learned, so a third identical outcome only spends iterations. RESETS on any non-run_js browser action (a screenshot, a snapshot, a click, a write \u2014 the agent looked at or acted on the page) and on a step change. Bounces exactly ONCE per declared step so a stubborn hunter is let through rather than wedged in a bounce loop. DELIBERATELY NOT AN AMNESTY: the refusal carries no `resetLoopDetector` and is NOT marked `isMetaTool`, so BaseRuntime still runs the post-dispatch LoopDetector check and still records the fingerprint for the steering veto \u2014 a breaker that re-armed the machinery it fired inside would be self-defeating, and this must not become a fifth loop-safety amnesty sibling. Diagnostic: `run_js:hunt_breaker {stepIndex, count, accelerated}`. UNCONDITIONAL COMPANIONS, not gated by this flag: (a) the step prompt now renders the EXISTENCE of a `verbatimInput` pin ("the exact text to enter is supplied by the runtime and will be typed verbatim \u2014 do NOT search the page for it") and never its bytes, removing the reason for the hunt in the whole pinned class (gated by VERBATIM_INPUT_PIN, whose contract it belongs to); (b) `steeringVeto.projectFingerprintArgs` drops the script body for SCRIPT tools (run_js) so the veto fingerprint is name-only and a supervisor REDIRECT naming the tool refuses the next, differently-worded script; (c) a SHADOW-ONLY diagnostic `verify_evidence:runjs_empty_would_withhold {stepIndex}` when a SUCCESSFUL run_js with an empty/trivial value is credited as verify evidence \u2014 diag only, the evidence is still credited and still clears a prior wait-oracle failure exactly as before. Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.runJsHuntBreaker.test.ts, steeringVeto.test.ts, RunnerRuntime.verbatimInput.test.ts.',designDoc:"packages/engine-core/src/runJsHunt.ts",status:"active",added:"2026-09-09"},LOOP_URL_NOVELTY_REARM:{key:"LOOP_URL_NOVELTY_REARM",envVars:["AGENTIQA_LOOP_URL_NOVELTY_REARM","AGENTIQA_EXPERIMENT_LOOP_URL_NOVELTY_REARM"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Re-arms the structural loop breaker in the chat/explorer (assistant_v2) lane. URL-ONLY (narrow-safe): only the novel-URL seen-set, its NOVEL_URL_BUDGET=40/turn budget, and actionsSinceProgress become TURN-scoped (resetForNewStep no longer wipes them), so a recycled URL revisited across many declared steps stops re-counting as novel and actionsSinceProgress climbs to the 30-action structural threshold. The novel-REF and novel-SCREEN budgets stay PER-DECLARED-STEP in both states \u2014 turn-scoping them was reverted because it starved a legit long single-stable-URL SPA turn producing genuinely-new screen content each action (force-block ~action 53). A genuinely-new URL each step still resets (multi-page wizards unaffected); a wander that also mints novel screens escapes this deterministic re-arm and the run backstop is the net. Off restores the per-step reset (recycled URLs re-count as novel forever, unbudgeted novel_url) \u2014 today's prod behavior. Runner (test_run) lane never opts in.",designDoc:"docs/plans/2026-07-20-loop-safety-rearm-and-backstop-design.md",status:"active",added:"2026-07-20"},RUNNER_SIGNAL_STEP_FORWARD_RESET:{key:"RUNNER_SIGNAL_STEP_FORWARD_RESET",envVars:["AGENTIQA_RUNNER_SIGNAL_STEP_FORWARD_RESET","AGENTIQA_EXPERIMENT_RUNNER_SIGNAL_STEP_FORWARD_RESET"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Runner-lane (test_run) loop safety: `RunnerRuntime.handleSignalStep` returns `resetLoopDetector: true` ONLY on a genuine FORWARD activation \u2014 the activated index names a real plan step (it was NOT clamped into `[1, steps.length]`) AND is STRICTLY GREATER than the monotone high-water mark read at call time. THE DEFECT: the flag was returned unconditionally, and BaseRuntime's dispatch loop treats it as a full amnesty (LoopDetector.resetForNewStep() \u2014 every consecutive / no-progress / cumulative counter AND the seenRefs / seenScreenContentHashes / seenPixelHashes / seenUrls novelty sets \u2014 plus tapRetryTracker, steeringVeto, sameGoalAbort and the loop-vision extension budget). `signal_step` is a filler meta-tool the MODEL emits freely, including a re-signal of the step it is already on and a BACKWARD re-bind to an already-graded step, both prod-observed and both permitted by design; so a runner grinding one form re-armed every structural counter from zero on every generation and survived to iteration 136 / 139 actions (#3019), 67 actions (#2485) and 22 actions (#2577) with no deterministic breaker ever arming. The two lane-agnostic floors that would otherwise have caught it (RUN_PROGRESS_BACKSTOP, LOOP_URL_NOVELTY_REARM) are hard-gated to session.kind === 'assistant_v2', so the runner lane ran with a purely step-scoped, model-defeatable net. WHAT ON CHANGES: a same-step re-signal, a backward re-bind and an out-of-range index return the byte-identical result WITHOUT the reset flag, and emit a structured `signal_step:reset_withheld {stepIdx, maxStepEverActivated, maxInRangeStepActivated, direction, path}` diag for the nightly census. THE GATE READS A SECOND MARK, `_maxInRangeStepEverActivated`, not `_maxStepEverActivated`: the handler clamps a prod-observed out-of-range index to the LAST plan step and then activates it, which must keep raising the temporal-snapshot mark but would otherwise let ONE malformed index pin the gate at the plan length and withhold the reset at every remaining step boundary of the run. BOTH of the handler's returns are gated \u2014 the ordinary activation and the unverified-verify DEFLECTION (findUnverifiedVerifyStepBefore \u2192 buildVerifyEvidenceRequiredResult), which carries the same unconditional amnesty while binding BACKWARD to an already-activated verify step; a deflection to a verify step the run never reached is forward and keeps the flag. Every other effect of signal_step is untouched \u2014 `_currentStepIndex` re-binding (backward included), verify-pin scoping (resetVerbatimPinTracking / noteStepActivated), which step the deflection bounces to and its rejection payload, and the step-marker-fold latch all behave exactly as before, so no step, criterion or run status is re-graded by this flag. It only changes WHETHER an existing deterministic breaker can arm. OFF (`=0` under either spelling) restores today's unconditional reset with no other code path touched. NON-GOALS, deliberately out of scope: un-gating the lane-agnostic floors for the runner lane, a durable supervisor-redirect channel, a step anti-regression / bounded-replay guard, and splitting BaseRuntime's composite reset so sameGoalAbort / steeringVeto survive independently. NAMED RESIDUAL: four SIBLING RunnerRuntime returns carried an unconditional resetLoopDetector \u2014 maybeBlockActionUntilVerifyEvidence, the retained-verify-snapshot reuse, the run_complete verification_conflict bounce and buildMissingEmailVerificationRejection; the first two fire once per model action, so a run parked on an unverified verify step still re-arms every breaker per action. THREE REMAIN: maybeBlockActionUntilVerifyEvidence dropped the flag on 2026-09-09 under VERIFY_EVIDENCE_GATE_STANDOFF (#2599), which also bounds its refusal at 2 per verify step. Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.signalStepForwardReset.test.ts (unit decisions in both flag states, plus the composite that drives a real LoopDetector through BaseRuntime's dispatch seam on the measured step-bouncing shape and pins wizard safety in both states).",designDoc:"docs/plans/2026-09-08-runner-signal-step-forward-only-reset-design.md",status:"active",added:"2026-09-08"},RUNNER_STEP_ANTI_REGRESSION:{key:"RUNNER_STEP_ANTI_REGRESSION",envVars:["AGENTIQA_RUNNER_STEP_ANTI_REGRESSION","AGENTIQA_EXPERIMENT_RUNNER_STEP_ANTI_REGRESSION"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Runner-lane (test_run) STEP ANTI-REGRESSION: a bounded licence on returning to a page where an already-completed plan step ended. **LIVE BY DEFAULT since 2026-09-10 (GRADUATED \u2014 see DEFAULT below): with no env the guard ENFORCES.** `=0` (either spelling) is the kill switch and stops the detection itself; `=1` is still accepted and now merely restates the default. It shipped SHADOW-FIRST in round 7 and graduated on the evidence recorded under DEFAULT. THE DEFECT (#2577, staging replay of a 25-step plan): having completed the step that mints a duplicate record, the runner navigated back to the BASE page where the earlier steps had ended, to \"finish\" verifications it believed were owed there. That page does not carry the duplicate's title, so the next wait_for_element failed, the model re-signalled backward, minted ANOTHER duplicate and navigated back again \u2014 3 navigations to the same URL, 3 duplicate records left in the tenant, 3 supervisor REDIRECTs ignored, terminal supervisor_halted. Nothing in the runtime had a concept of \"this URL is where a completed step ended\", so nothing could tell that navigate apart from ordinary forward movement. THE RULE (structural, round 4; a third term in round 6): a page is a step regression when it is the END URL of a COMPLETED step AND it is NOT in the CURRENT step's own VISITED set AND it is NOT the run's WORKING PAGE. `_stepEndUrls[k]` is the page the browser was on when the run ADVANCED PAST step k, written EXACTLY ONCE at that forward activation (round 3 wrote it on every hop, which made a multi-page step rewrite its own entry on the way out); `_stepVisitedUrls[k]` is every page observed while step k was current, INCLUDING the page each action dispatched FROM, so a step that opens a detail page and returns (A\u2192B\u2192A) is doing ordinary work and a VERIFY step that starts on the base page is exempt by construction. ROUND 7 MADE THAT SENTENCE TRUE AGAIN: rounds 4-6 excluded a FLAGGED page from every step's visited set (and evicted it from the ones it was already in), which silently deleted the exemption for the rest of the run \u2014 the reviewer measured `signal_step` onto a step STANDING on a flagged page whose visited set then came back EMPTY, so the \"exempt by construction\" mitigation could not fire on the shape it was written for. The visited set now records the current step's pages unconditionally; the flagged set survives for LICENCE ACCOUNTING only (it is what stops an excursion walking from one completed step's page to the next from promoting its own last target into the working-page exemption). NAMED CONSEQUENCE: a SECOND return to the same page inside ONE step is now exempt rather than contained \u2014 which is exactly the legitimate plan shape review measured being refused (\"compare the totals on the summary page and the cart page, then go back to the summary\") \u2014 so within a single step this guard bounds the FIRST unlicensed return only; a run that oscillates inside one step is owned by the LoopDetector and the supervisor, and a run that re-signals BACKWARD (the measured #2577 loop) is unaffected, because the visited-set exemption is gated off entirely once `_maxInRangeStepEverActivated > currentStep`. THE LICENCE IS SPENT ON ARRIVAL, not on the call: the first landing on a completed step's page outside the current step's visited set consumes it \u2014 `step_regression:licensed {via, fromStep, toStep, furthestStep, url}` \u2014 for every navigation the AGENT issued, whether or not the pre-dispatch layer could read its target (`navigate`, `go_back`, an anchored `run_js` including a computed `location.href` the classifier reads as navigational-but-unreadable). APP-AUTHORED ARRIVALS ARE NOT REGRESSIONS (round 7): an arrival produced by a click, a form submit or a server redirect emits `step_regression:arrived_via_app {tool, url, fromStep, toStep}`, joins the current step's visited set, and does NOT spend the licence, does NOT count as a bypass and can never end the run. Rounds 3-6 counted them, on the argument that the licence must be channel-independent; the reviewer measured what that costs on a HUB-AND-SPOKE app (dashboard \u2192 task \u2192 the server 302s back to the hub after each save, and the hub is an earlier step's end URL): `refusalsIssued: 0`, two bypasses, terminal `blockKind:'loop'` \u21D2 supervisor_halted with every remaining step synthesized skipped \u2014 a run ended for the APPLICATION's navigation, under a customer-visible reason claiming the agent had routed around a refusal that was never issued. A server-side redirect is not the agent's regression. THE COST, named: a run that walks back through a back-LINK is no longer contained by this guard at all (the pre-dispatch refusal never saw it either \u2014 it is not a nav-class call), so the click channel is now owned by the LoopDetector, the supervisor and the veto. Round 3 spent it only for a nav-class call with a READABLE target, so every other channel regressed unboundedly while the licence stayed unspent and the containment (which opens on the licence) never woke up. THE PRE-DISPATCH REFUSAL IS SUBORDINATE: it fires only once the licence is already spent and only for a readable target \u2014 navigate (args.url), go_back and a scripted history.back()/history.go(-1) (the LoopDetector's previous distinct URL), and a scripted location.href/assign/replace with a quoted literal. An UNREADABLE target is never refused (round 3 refused those fail-closed, which was measured refusing a plain history.go(0) page RELOAD and a `d.location = someVar` that is not a navigation at all); anything this layer misses is caught on arrival. Script classification is ANCHORED on `location.` / `history.` \u2014 round 3's unanchored patterns matched `xhr.open('GET', \u2026)` and `payload.location = x`. ROUND 8 (2026-09-10, #3118) \u2014 A STEP IS NEVER A REGRESSION AGAINST ITSELF. \"A step behind the run\" was `k < _maxInRangeStepEverActivated` (the FURTHEST step ever reached); it is now `k < currentStepIndex` \u2014 STRICTLY EARLIER THAN THE STEP THE RUN IS CURRENTLY DECLARING. The two differ exactly when the model re-signals BACKWARD, which is when this guard is most active, and the FORCED-ON graduation rehearsal (2026-09-10, 17 staging-mirror plans, engine `45e821072`, the flag hand-set to `=1` for one hour) measured the cost on its very first enforcement delta. Chain, verbatim from the session events: `signal_step:reset_withheld {path:\"activation\", stepIdx:3, direction:\"backward\", maxStepEverActivated:4}`, then the same at `stepIdx:2` \u2014 an ALREADY COMPLETED step is current again \u2014 then `navigate` to the base draft step 2 had ENDED on, booked `step_regression:licensed {fromStep:2, toStep:2, furthestStep:4}`; 72 s later the same `navigate` was answered `step_regression:refused {fromStep:2, toStep:2}`, whose dispatch details read, verbatim, \"step 2 must not return to step 2's page\". Cut off from the page it was grading, the model captured the WRONG record 4 s later, the supervisor issued `block` 4.7 s after that, and the run ended `supervisor_halted` with 5 grades of 29 \u2014 against `passed 29/29` for the same plan in shadow the day before. A step's own end URL is the page that step is DEFINED to work on; a LATER step's end URL is a FORWARD re-walk after a backward signal, which is progress. ROUND 8b (2026-09-10, review round 2 of the same fix) \u2014 THE TERM IS KEYED ON THE URL, NOT ON THE LEDGER INDEX. `k < currentStepIndex` excludes the current step's KEY, not its PAGE: on the ORDINARY SINGLE-PAGE FLOW (\"open the draft\" ends on the draft, \"fill the title\" ends on the draft) `_stepEndUrls[k-1] === _stepEndUrls[k]`, the scan matches `k-1`, and the guard books the identical harm one index over \u2014 measured on the rehearsal's geometry as `licensed {fromStep:1, toStep:2, url: DRAFT}` then `refused {fromStep:1, toStep:2}`, details \"step 2 must not return to step 1's page\", where DRAFT is `_stepEndUrls[2]`. Both halves of the harm survive (the refusal on the re-declared step's own page, and the licence burned on it); only the prose stops being self-contradictory, which also makes the row invisible to a `fromStep >= toStep` census. So the FIRST term is: a target that is the END URL of ANY step AT OR ABOVE the declared one is not a regression target, whichever earlier index also ended there \u2014 \"at or above\" because a later step's page is work the run has not undone. ITS COST, named: the #2577 containment is given up on a page SHARED between an early step and a current-or-later one while the run works backward below both. The ledger invariant (`k < _maxInRangeStepEverActivated`, written once at the forward activation that leaves step k behind) is kept beside it as the assertion it always was \u2014 it can only narrow the target set further, never widen it. THE FURTHEST-STEP MARK STILL KEYS THE LICENCE UNIT AND THE VISITED-SET EXEMPTION (`_maxInRangeStepEverActivated <= currentStep`), so a BACKWARD `signal_step` still cannot make a frozen step \"current\" and switch the visited-set exemption on, and the measured #2577 loop \u2014 forward to step 7, then steps 2-6 re-signalled backward with a return to the BASE page each time \u2014 still gets exactly ONE licensed return and a refusal on every trip after it, because the base page belongs to step 1, which is below every step that loop re-declares. NAMED COST OF ROUND 8: a run that re-signals step k backward and then oscillates on step k's OWN page is not contained by this guard at all \u2014 the same trade round 7 made for the click channel, and owned by the LoopDetector, the supervisor and the veto. WHY THE SHADOW CENSUS COULD NOT HAVE FOUND THIS: 44 shadow runs (27 rehearsal 09-09 + 17 prod 09-10) emitted ZERO `would_refuse`; the shape needs a backward `signal_step` FOLLOWED BY a return to the re-signalled step's own page, and silence in shadow is equally consistent with \"the rule is right\" and \"the entry condition is never reached\" \u2014 which is why the forced-ON rehearsal is now part of the graduation gate below. THE LICENCE UNIT IS THE FURTHEST-REACHED STEP, NOT THE RUN (round 5): the allowance is keyed on `_maxInRangeStepEverActivated`, so a genuine FORWARD activation raises the mark and brings a fresh licence, while a BACKWARD `signal_step` does not \u2014 the measured production loop (forward to step 7, then steps 2-6 re-signalled BACKWARD with a return each time, the furthest step pinned at 7) still gets exactly ONE licensed return and containment on every one after it, and within one furthest step the second return is still refused. A RUN-scoped licence (round 4) refused a LATER step that genuinely needed a completed step's page \u2014 \"go back to the list and verify the copy appears there\" \u2014 and forced the model to grade that step from the wrong page; a PER-STEP one re-grants at every boundary, including the one the loop never crosses. There is still no signal-based re-grant WITHOUT forward progress: that would need an UNFORCED-navigation signal this runtime does not have. THE WORKING PAGE IS NEVER A REGRESSION TARGET (round 6): the licence pays for the EXCURSION, so the way HOME was judged on its own terms \u2014 and the page the plan's forward progress stands on is by construction the END URL of several completed steps. MEASURED LIVE 2026-09-09: one forced-lane run took SEVEN refusals getting back to the submitted copy after its step-9 licence had gone on the base record, recovering only through a contained bypass. Neither round-4 exemption covered it: the working page had been FLAGGED by an earlier arrival (and until round 7 a flagged page could never re-enter any visited set), or a NEW step begins on the excursion page and the working page is in no visited set at all \u2014 the second half is still true, which is why this exemption survives round 7's visited-set fix rather than being subsumed by it. `_stepRegressionWorkingUrl` is the page the run DEPARTED FROM when it left for an excursion, written on every regression arrival \u2014 the departure point rather than a live \"last page seen at the high-water step\", because the departure is the last moment forward progress and the browser agreed on one page and it survives a step advance, an intermediate hop and a supervisor correction, while a live reading would be overwritten by the new step's first ordinary hop on the excursion page. It is NOT a hole in the containment: it exempts exactly ONE page in ONE direction, the OUTBOUND leg of every excursion is still licensed/refused/contained, an A\u2192B oscillation is still caught on every B, the two-bypass ladder still ends the run, and a page the guard has already FLAGGED is never promoted into the exemption (an excursion walking from one completed step's page to the next cannot make its own last target exempt). In the measured production loop it changes exactly one row \u2014 the return leg to the duplicated draft, which round 5 counted as a bypass \u2014 and none of the departures, which are the loop. RESIDUAL OF THAT: the bypass ladder now counts DEPARTURES only, so an oscillating run needs two unlicensed DEPARTURES to reach the loop terminal where it previously needed two arrivals of either kind \u2014 measured 2026-09-09, one forced-lane run oscillated inside the double-return step and was ended by the SUPERVISOR loop breaker (2 redirects, then block) rather than by this containment, which had booked one bypass; the run still ends, with the same supervisor_halted terminal. NAMED RESIDUAL: the mark rises on a FORWARD `signal_step`, which the MODEL emits, so a model that answers a refusal by declaring the next plan step earns a fresh licence (measured live 2026-09-09) \u2014 bounded and monotonic (at most one return per plan step, never re-granted for a step already passed) and paid for with a forward step declaration the run cannot take back, while the measured BACKWARD-re-signalling loop earns nothing. SEAM ORDER: the pre-dispatch layer runs AFTER `steeringVeto.maybeVeto` and the veto wins; on the observation side the ARRIVAL is judged BEFORE it joins the visited set, or every arrival would exempt itself. NORMALIZATION: a plain anchor fragment (`#top`) dropped but a ROUTE-LIKE one (`#/\u2026`, `#!\u2026`) KEPT \u2014 round 7, after the reviewer measured a hash-routed SPA collapsing every route onto one key, which made the sibling SUPERVISOR_REDIRECT_DURABLE veto refuse `#/checkout` and `#/orders/12` because a redirect had named `#/cart`, and made THIS guard wholly inert (every step's end URL identical, 0 licensed / 0 refused / 0 contained on three distinct routes). Trailing slash is significant and deliberately not canonicalized (fail-open in both consumers). An SPA that routes without changing the URL AT ALL is still invisible here, and is owned by the LoopDetector's novel-ref/novel-screen signals. Also: a small denylist of volatile query params stripped (`action_loading_message` \u2014 the measured transient marker \u2014 plus the `_`/`t`/`ts`/`cache`/`nonce`/`rand` cache-buster family), remaining query sorted, compared as scheme+host+path+query; a DENYLIST on purpose, because `?id=` is exactly what distinguishes the base record from a duplicate and an allowlist would refuse legitimate forward navigation. ARRIVAL CONTAINMENT and its escalation count AGENT-AUTHORED ARRIVALS, so they still cover every channel the AGENT can navigate through (including the ones the pre-dispatch layer cannot read) and no channel the APPLICATION drives: the first arrival is the licence, the SECOND emits `step_regression:bypassed {tool, url, fromStep, toStep, bypasses}` and arms a ONE-SHOT standing constraint (the SUPERVISOR_REDIRECT_DURABLE channel) plus a steering-veto redirect, the THIRD ends the run through the LOOP path (`blockKind:'loop'` \u21D2 supervisor_halted), never `blocked_by_issue`. THE RUN-ENDING REASON IS CUSTOMER-VISIBLE (it becomes the `exploration_blocked` obstacle) and its \"routing around the refusal\" clause is now CONDITIONAL on `_stepRegressionRefusals > 0`: a bypass can be booked for a target the refusal channel never read, and the reviewer measured that text asserting a refusal that never happened. The flagged set is RUN-scoped and no longer touches any visited set (round 7) \u2014 it exists so the working-page exemption cannot be handed to a page the guard has already sent the run back from. THE ISSUE GATE is structural and FAIL-OPEN (round 4), and its WINDOW is keyed structurally too (round 6): a `report_issue` / `exploration_blocked` is bounced ONLY when the refusal is still RECENT \u2014 it happened ON THE STEP THE RUN IS CURRENTLY DECLARING, or within the last 6 model turns, whichever is wider \u2014 AND the report's own text carries a token only this runtime writes (`runtime guard`, `step_regression`, or the refusal's own first sentence verbatim) \u2014 `step_regression:issue_rejected {surface, stepIndex, rejections, refusals, refusedAtIteration, refusedAtStep, armedBy, iteration}`, where `armedBy` is `declared_step` or `turn_window` so the census can tell whether the constant still earns its place. Round 4's bare 2-turn constant was outrun by ROUND 5'S OWN LICENCE: measured live 2026-09-09 on 1 of 10 flag-ON forced-lane runs, the model was refused, answered by declaring the next plan step, took the LICENSED retry that declaration earned, and filed an `exploration_blocked` quoting the guard VERBATIM three turns later \u2014 `rejections=0`, terminal blocked/blocked_by_issue at step 9 of 13 with every other step row synthesized skipped. A turn count cannot be the primary key of a window the licence itself widens; the DECLARED STEP can, and the turn arm survives as the OR-arm for a report filed one step later. The TOKEN half is untouched, which is what protects a genuine report \u2014 widening WHEN the gate is armed cannot reject anything that does not quote this runtime's own strings. The exploration_blocked bounce lands BEFORE the terminal write, which is what keeps a completable run off `blocked_by_issue`. RESIDUAL: `_currentStepIndex` is model-controlled, so a backward `signal_step` back onto the refused step re-arms the declared-step arm later in the run; bounded by the token half, and re-declaring a passed step is itself what this guard watches. Round 3's LOOSE ordinary-word tier and its EVIDENCE-BUFFER arm are deleted: they were measured rejecting genuine customer defects (an honest 'Pagination regression: navigating to page 2 still shows the page 1 rows', and a report about a wrong invoice total rejected because an unrelated buffered action's `intent` mentioned navigating back). A defect the customer never receives is caught by nothing; a false `blocked_by_issue` is caught by the arrival containment and visible in the census. NO step, criterion or run status is re-graded \u2014 the refusal is a tool-call rejection, exactly like the steering veto. DEFAULT = ENFORCE (GRADUATED 2026-09-10). With no env set every seam below is live: the pre-dispatch refusal, the licence, the arrival containment and its two-bypass loop terminal, the one-shot standing constraint, the steering-veto redirect and the issue gate. `=0` (either spelling) is the kill switch and is now the ONLY non-enforcing state \u2014 it switches the DETECTION off too, so nothing is recorded, refused, contained or bounced and no row of any name is emitted. `=1` is still accepted and resolves identically to no env. THE SHADOW STATE IS GONE: `would_license` / `would_refuse` / `would_contain` are unreachable, because the only state that turned enforcement off also turns the measurement off. Post-flip watchdog (the graduation process's step 4) is therefore a census of the LIVE names \u2014 `step_regression:refused` / `:bypassed`, and any row with `fromStep >= toStep` \u2014 on nightly runs, and a regression is answered with `AGENTIQA_EXPERIMENT_RUNNER_STEP_ANTI_REGRESSION=0` on the orchestrator FIRST and a fix after. HOW IT GRADUATED, in two measurements, because neither alone was sufficient. (1) SHADOW CENSUS, 44 runs (27 staging-mirror rehearsal 2026-09-09 + 17 prod 2026-09-10): ZERO `would_refuse`, ZERO `would_contain`. That census was NOT the graduation, and reading it as one is the trap this entry exists to document \u2014 `licensed` and `arrived_via_app` take the byte-identical `return null` path in both flag states, so the ONLY evidence shadow can produce about the correctness of a refusal is a `would_refuse`, and silence in it is equally consistent with \"the rule is right\" and \"the entry condition is never reached\". (2) FORCED-ON REHEARSAL, the second gate, two passes of the same 17 fixed customer plans on the staging mirror with the flag hand-set `=1` on the orchestrator for the duration. PASS 1 (2026-09-10 09:31\u201310:29Z, engine `45e821072`, FLAGS-STATE PR #3115) was a **NO-GO** on its first enforcement delta: `licensed` 2, **`refused` 1**, `bypassed` 0, `arrived_via_app` 2 \u2014 and that single refusal was FALSE, the `fromStep == toStep` own-page class described under ROUND 8 above (#3118), which turned a `passed` 29/29 shadow run into `supervisor_halted` with 5 grades of 29. PASS 2 (2026-09-10 12:23\u201313:42Z, after the ROUND 8 / 8b fix, FLAGS-STATE PR #3125) is the **GO**: `licensed` **1**, `refused` **0**, `bypassed` **0**, `arrived_via_app` **2**, `issue_rejected` **0**, `would_*` **0** (enforcement confirmed) and **0** rows with `fromStep >= toStep`. Three facts make that zero a measurement rather than a silence: the pass-1 blocker plan recovered to `passed` 29/29 \u2014 its shadow baseline exactly \u2014 with no guard row and no supervisor redirect or block, where pass 1 had 2 redirects and a block all on that one plan; the shape that produced the false refusal was exercised MORE, not less (`signal_step:reset_withheld[backward]` 9 vs 5), and produced nothing; and the guard is not inert, because the one `licensed` row is a genuine far return `{fromStep: 4, toStep: 27, furthestStep: 27}` on a run that passed 28/28. Two runs got worse than pass 1 and NEITHER carries a `refused` or `bypassed` row \u2014 a chronic criteria-derivation downgrade (#2213) and a Lio \"Loading\u2026\" hang that had already blocked in two of that plan's five prior baselines \u2014 so neither is attributable to this flag. THE RULE THE TWO PASSES ESTABLISH, for any shadow-first guard: graduation goes through an enforcing window, never through a longer soak; and a repeat window only proves a fix if the shape that broke the previous one is shown to have RECURRED \u2014 a nulled entry-condition counter makes a clean census mean nothing. WHY IT WAS SHADOW-FIRST AT ALL: three consecutive review rounds each found a NEW false-refusal class on this enforcing guard \u2014 app-authored hub-and-spoke returns, hash-routed SPAs, and a step that legitimately needs an earlier step's page twice \u2014 and round 8 added a fourth found by the first enforcing pass; all four are fixed above. Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.stepAntiRegression.test.ts, stepRegressionGuard.test.ts, navUrl.test.ts.",designDoc:"docs/plans/2026-09-08-runner-signal-step-forward-only-reset-design.md",status:"active",added:"2026-09-09"},SUPERVISOR_REDIRECT_DURABLE:{key:"SUPERVISOR_REDIRECT_DURABLE",envVars:["AGENTIQA_SUPERVISOR_REDIRECT_DURABLE","AGENTIQA_EXPERIMENT_SUPERVISOR_REDIRECT_DURABLE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Supervisor corrections OUTLIVE the turn they were issued on. Two behaviors under one switch. THE DEFECT (#2577): `applySupervisorVerdict` returns `injectedFunctionResponseError`, BaseRuntime writes it into the TRAILING function response's `metadata.error`, and one turn later it is just another line of an ever-growing trace. On the reconstructed session three REDIRECTs all said the same thing \u2014 stop navigating back, do the work on the page you are on \u2014 and the behavior each forbade returned within two turns; the run ended supervisor_halted anyway. SEPARATELY, the veto that WOULD have refused the third navigate had its armed forbidden set wiped one action earlier by a meta `resetLoopDetector` return (the retained-verify-snapshot reuse, `browser_action:short_circuited {reason:\"auto_verify_evidence\"}`) \u2014 budget 1/3 used, TTL 2/8 spent, and the set was empty. (1) STANDING CONSTRAINT: a `redirect` or `abort` verdict now also returns `standingConstraint {text, turnsLeft: 3}`; BaseRuntime re-injects the correction on the trailing function response of each of the next 3 model turns through the SAME channel (no prompt-shape change), decrementing once per turn, cleared EARLY on the next `continue` \u2014 the judge's own signal that the corrected behavior stopped. The arming turn is skipped (it already carries the verdict's prose), an empty turn is a no-op that does not spend budget, and an error the turn already carries is preserved rather than overwritten. (2) COMPOSITE-RESET SPLIT: `toolResult.resetLoopDetector` still resets the LoopDetector counters and the tapRetryTracker for EVERY producer, but the STEERING state (steeringVeto's armed block, sameGoalAbort's streak) is now cleared only by a result declaring `resetScope: 'full'` \u2014 stamped in `RunnerRuntime.gateSignalStepReset`, i.e. exactly the `signal_step` step activation, which IS a genuinely new goal. A meta bounce instead calls the new `steeringVeto.resetForNewStep()`, which drops the recent-window evidence (so the next step cannot inherit this one's loop set) but leaves an ARMED block standing until its own TTL/budget retires it. This closes the named residual of RUNNER_SIGNAL_STEP_FORWARD_RESET without touching any of the four sibling returns. COMPANION, under this same switch: `steeringVeto.actionFingerprint` now projects nav-class args through `projectFingerprintArgs`, normalizing the URL, and `recordRedirect` snapshots the recent window's normalized nav URLs into a `forbiddenNavUrls` set so a navigate carrying different companion args still matches on the PAGE it targets. Both widen a REFUSAL path, so `=0` restores the pre-#2577 byte-equality matching exactly. NO step, criterion or run status is re-graded; this changes only what the model is told and WHEN an existing deterministic breaker can arm. RUN SCOPE: the constraint is cleared at runLoop start by `resetSteeringStateForNewRun()`, beside `steeringVeto.reset()` \u2014 it is armed and cleared by verdicts only, so a runtime instance ending a run with `turnsLeft > 0` otherwise re-injected a stale correction into the NEXT run's first turns. SECOND CONSUMER (2026-09-09, round 3): RUNNER_STEP_ANTI_REGRESSION's post-dispatch bypass containment arms a ONE-SHOT standing constraint on this same channel, so with `=0` a detected bypass still emits `step_regression:bypassed` and still records the steering-veto redirect, but the model is not re-told on the following turn. OFF (`=0` under either spelling): one-shot prose, the indivisible amnesty and the pre-#2577 veto matching, byte-identical to before. Acceptance suite: packages/engine-core/src/__tests__/supervisorRedirectDurable.test.ts, BaseRuntime.compositeReset.test.ts, steeringVeto.test.ts.",designDoc:"docs/plans/2026-09-08-runner-signal-step-forward-only-reset-design.md",status:"active",added:"2026-09-09"},STEERING_VETO_OBSERVATION_EXEMPT:{key:"STEERING_VETO_OBSERVATION_EXEMPT",envVars:["AGENTIQA_STEERING_VETO_OBSERVATION_EXEMPT","AGENTIQA_EXPERIMENT_STEERING_VETO_OBSERVATION_EXEMPT"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"The steering veto refuses only calls that can CHANGE PAGE STATE. THE DEFECT (#3112, prod asess_1789009823056_btkyv0ns, 2026-09-10): on a page that genuinely had no Approve/Reject control for the acting role, the supervisor asserted a locator it had never observed and the veto then refused the five calls that could have proved it wrong \u2014 `snapshot` x1, `run_js` x2 (the `[]`-buttons absence probes), `click_at` x2 \u2014 275 s and 28 calls burned on one step, `report_issue` x0, terminal supervisor_halted. TWO FINGERPRINT PROPERTIES CAUSED IT: `snapshot` takes no arguments, so its fingerprint is the CONSTANT `snapshot:{}` and one earlier snapshot inside the 6-call window forbids every later one; and #2377's script-body strip in `projectFingerprintArgs` collapses EVERY `run_js` to `run_js:{}`, so the differently-worded absence probe is byte-identical, to the veto, to whatever `run_js` preceded the redirect. The refusals also STARVED the mechanism that owns read-only scripting: `run_js_hunt` was 0 for the whole run, because a call refused pre-dispatch never enters `RunJsHuntBreaker`'s streak. THE RULE: a call whose side-effect class is `observation` is never refused by the veto; its repetition is bounded by `RunJsHuntBreaker` (once per declared step) and the LoopDetector's novel-screen / structural force-block. CLASSIFICATION IS STRUCTURAL (`toolSideEffectClass.ts`), built from the registries that already exist \u2014 `negativeStateEvidence.STABILIZING_NEGATIVE_STATE_ACTIONS` for observation, `BaseRuntime.OBSERVATION_STATE_CHANGING_WEB_ACTIONS` / `...MOBILE_ACTIONS` / `CLICK_CLASS_ACTIONS` and `navUrl.NAV_CLASS_ACTIONS` for mutation \u2014 and made TOTAL by an audit test over every live tool set, so a new tool cannot silently fall through. `run_js` is resolved by SOURCE (`classifyRunJsSideEffect`) BEFORE any set membership, because it is a member of the observation registry AND can carry a write; the scan is fail-closed (an unreadable, over-long, obfuscated or dynamic source is `mutation`), so #2377's widening stays live for MUTATING scripts and only the read-only remainder is handed back to the hunt breaker. AN EXEMPTED CALL IS INVISIBLE TO THE VETO IN BOTH DIRECTIONS: it does not enter `recentExecuted` (or the next REDIRECT would forbid `snapshot:{}` again) and it does NOT spend `executedSinceArmed` \u2014 without that half the exemption would open a bigger hole than it closes, since eight exempted snapshots would legally retire an armed forbidden set with no LoopDetector signal. It does not spend the 3-refusal budget either: an exemption is not a refusal. The scroll/swipe amnesty (`VETO_EXEMPT_ACTIONS`) is consulted FIRST at both seams, so its pre-#3112 behaviour \u2014 exempt from refusal, still spending one TTL slot \u2014 is byte-identical and pinned. SHIPS LIVE (default on), which inverts this repo's shadow-first convention for a new runtime guard, deliberately: the polarity is inverted too. This NARROWS an existing refusal, so it can only ever REMOVE a refusal and never add one, and every uncertainty in the classifier resolves to `mutation` (= today's behaviour). A shadow rollout would have measured a hole production was already measuring. Diag: `steering_veto:observation_exempt {tool, sideEffectClass, runJsVerdict?, runJsReason?, instructionHead}` (kind `log`, source `SteeringVeto`) on every exempted call, and NO `agent_lifecycle{event:'steering_veto'}` row for it. `=0` (either spelling) restores `45e821072` exactly: no classification runs at either seam, an observation call is refused as before and counts toward the window and the TTL as before. RESIDUAL, named: the pre-existing scroll amnesty still spends TTL, so 8 scrolls still retire an armed set \u2014 unchanged by this flag and out of its scope. Acceptance suite: packages/engine-core/src/__tests__/steeringVeto.observationExempt.test.ts, toolSideEffectClass.test.ts, toolSideEffectClass.audit.test.ts.",designDoc:"packages/engine-core/src/toolSideEffectClass.ts",status:"active",added:"2026-09-10"},SUPERVISOR_REDIRECT_GROUNDING:{key:"SUPERVISOR_REDIRECT_GROUNDING",envVars:["AGENTIQA_SUPERVISOR_REDIRECT_GROUNDING","AGENTIQA_EXPERIMENT_SUPERVISOR_REDIRECT_GROUNDING"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"LIVE BY DEFAULT IN THE RUNNER LANE since 2026-09-11 (GRADUATED \u2014 see DEFAULT below): with no env the gate ENFORCES in the runner lane and books SHADOW rows in chat/explorer, because that is the only lane the graduating census covered (#3162 \u2014 `BaseRuntime.supervisorGroundingEnforcementLane()`, `false` by default, overridden `true` by `RunnerRuntime`); `=0` (either spelling) is the kill switch in EVERY lane and stops the DETECTION itself; `=1` is still accepted and ENFORCES IN EVERY LANE, so it is the deliberate opt-in a chat/explorer census needs rather than a bare restatement of the default. The two spellings must AGREE for that `=1` to mean anything: a contradictory pair (`AGENTIQA_SUPERVISOR_REDIRECT_GROUNDING=1` with `AGENTIQA_EXPERIMENT_SUPERVISOR_REDIRECT_GROUNDING=0`) resolves to DETECTION OFF, because `groundSupervisorRedirect` consults `killSwitchForcedOff` \u2014 any `=0` wins \u2014 before it reads anything else. Safe direction, and pre-existing. It shipped SHADOW-FIRST on 2026-09-10 and graduated on the evidence recorded under DEFAULT. A supervisor REDIRECT that asserts WHERE a control is may arm the steering veto and the K-turn standing constraint only when every affordance literal in that locator clause was observed in a FULL a11y snapshot this run captured, on the page it is standing on. THE DEFECT (#3112): `SupervisorService.buildPrompt` renders the judge's whole input from the agent's OWN narration \u2014 the Intent and Screen columns are `call.args.intent` / `call.args.screen` \u2014 plus one screenshot. The judge has no runtime observation channel and nothing downstream ever asked whether its answer matched the page, so an invented locator (\"Reject is in the three-dot menu on the Procurement Approval node\", on a node with no overflow control at all) armed the veto AND was re-injected as a standing constraint 7 times: the channel whose whole purpose is to make a correction survive contradiction, amplifying a hallucination. THREE-WAY VERDICT, abstain-dominant: `ungrounded` means we searched a page-scoped FULL corpus and did not find the operand; every uncertainty is an abstain \u2014 `no_locator_claim` (an ordinary correction names no control: \"stop navigating back to the draft\", \"finish step 7\"), `no_snapshot` (nothing at full fidelity yet \u2014 a redirect about a page the run JUST navigated to is UNOBSERVED, not ungrounded), `stale_corpus` (the retained captures are all of other pages), `operand_unextractable`, `plan_licensed` (the plan's own step text names every operand, so the judge is echoing the plan), `negated_or_expected_claim` (delegated to the hallucinated-quote gate's own NEGATIVE_CLAIM_RE / EXPECTED_MARKER_RE), and `collapsed_affordance` \u2014 the container the claim names IS on the page and the control is not, which in an a11y outline is INDISTINGUISHABLE from a menu nobody has opened yet. That last one was found by MEASUREMENT, not by reading: a live `_snapshotForAI` read of the runner-role-gated-approval fixture (2026-09-10) showed the gate calling a real, reachable control ungrounded the moment the model had not yet clicked the overflow trigger. PRESENCE IS NODE-SCOPED (round 2): round 1 evaluated that abstain page-wide, which made the gate silent on the issue's OWN case (the sibling Finance row carries a U+22EE, so a claim about the Procurement row abstained even though that row has no overflow control at all) and left the shadow census able to graduate only the residual 'a container this page never rendered' class. When the clause names a node and that node resolves to exactly ONE record in every retained outline, the haystack narrows to that record's a11y subtree \u2014 climbing from the naming line to its closest record-role ancestor, because a row's controls are the naming line's SIBLINGS rather than its descendants. Every operand but the anchor is judged inside the subtree; the anchor stays page-wide, so a node the page lacks entirely is `ungrounded`, not abstained. FAIL-OPEN: no anchor, a non-unique anchor, or no record ancestor falls back to page scope, because a wrong subtree produces a wrong `ungrounded`. Measured live against the real renderer: reviewer-role page -> `ungrounded [three-dot menu, Reject]`; the same page's Finance-row claim -> `abstain collapsed_affordance`; admin-role page -> `abstain collapsed_affordance`. The locator lexicon is NARROW by construction: a clause counts only when it carries BOTH a locator shape (is/sits/lives in|inside|under|behind, open/use the ... menu, next to|beside|to the right of) AND a container noun \u2014 `page` is deliberately excluded, so \"go back to the checkout page\" abstains. Affordance descriptors carry an explicit variant set (three-dot / kebab / ellipsis / overflow / U+22EE / \"more actions\"), because \"three-dot menu\" is never the literal text of an a11y node. DEFAULT = ENFORCE IN THE RUNNER LANE (GRADUATED 2026-09-11, lane-scoped by #3162). With no env the full classification runs in every lane, and in the RUNNER lane an `ungrounded` verdict DOWNGRADES the redirect: no `steeringVeto.recordRedirect`, no `armSupervisorStandingConstraint`, and the injected `metadata.error` becomes a runtime-authored observe-first instruction, emitting `supervisor_redirect:downgraded {iteration, reason, unobserved, operands, snapshotsSearched, claimHead}`. IN THE CHAT/EXPLORER LANES the same classification runs and publishes `supervisor_redirect:would_downgrade {\u2026}` while the redirect applies untouched \u2014 the shadow behaviour, kept there on purpose: all 236 messages of the graduating census are runner rows, and the one lane-specific rescue is runner-only too, since `supervisorGroundingPlanText()` is a `RunnerRuntime` override and `plan_licensed` therefore cannot fire in chat. That abstain exists because a supervisor echoing the user's OWN declared operand is restating rather than inventing; in chat that operand lives in the USER'S MESSAGE and there is no `user_licensed` abstain, so the chat lane is BOTH unmeasured and structurally missing the rescue. Enforcing there on a corpus with zero rows from it is precisely what this gate's own graduation rule forbids \u2014 so chat/explorer GRADUATION NEEDS ITS OWN CENSUS, which now accrues under the live default from the `lane: 'other'` rows rather than needing a fresh forcing window. EVERY ROW CARRIES `lane` (`'runner'` where the default enforces, `'other'` where it books shadow rows), single-sourced from the guard the enforcement decision itself reads. `=1` enforces in BOTH lanes and is kept accepted. `=0` is the kill switch and is the only state that stops the detection: no snapshot ledger is written, no classification runs, no row of any name is emitted, and the redirect applies BYTE-IDENTICALLY as it did before this gate existed \u2014 veto armed, constraint armed, the judge's own prose injected. THE SHADOW STATE IS GONE IN THE RUNNER LANE, and that is deliberate: `supervisor_redirect:would_downgrade` is unreachable there, because the only state that stops runner enforcement also stops the measurement (the RUNNER_STEP_ANTI_REGRESSION/#3128 shape \u2014 a kill switch on a gate whose detection costs something). It is REACHABLE, and is the live default, in chat/explorer \u2014 that is the whole point of the lane split. The POST-FLIP WATCHDOG therefore reads two things: the LIVE names on `lane: 'runner'` rows \u2014 `supervisor_redirect:downgraded` / `:grounded` / `:grounding_abstain`, row by row, never as a rate \u2014 and, on `lane: 'other'` rows, the accruing chat/explorer census (`:would_downgrade` + the abstain split) that a chat graduation will have to be argued from. and a regression is answered by setting `AGENTIQA_EXPERIMENT_SUPERVISOR_REDIRECT_GROUNDING=0` on the orchestrator FIRST and fixing after. HOW IT GRADUATED, in two passes, because the mirror could not supply the denominator. (1) MIRROR PASS 1 (2026-09-11 08:13:41-09:29:58Z, staging orchestrator hand-set `=1`, 17 fixed Lio plans, engine `7094117cb`): 14 passed / 3 failed / 0 blocked \u2014 the best of that plan set's four baselines \u2014 and exactly TWO classified redirect messages, both `grounding_abstain {no_locator_claim}`, `downgraded` 0 and `would_downgrade` 0 (enforcement confirmed). Both abstains are CORRECT (neither correction names a control or a container), and no run regressed; but a `downgraded` never fired, so the GO condition was vacuous and the pass was booked NO-GO BY INSUFFICIENT n. At ~2 messages per 17-plan pass the >=100 floor needs ~50 such passes: the mirror's plans are direct scenarios on which the supervisor answers `continue` (101 of 103 verdicts), so pass 2 had to ORDER the form rather than hope to meet it. (2) FIXTURE PASS 2 (2026-09-11, local, tree `462e48a80` = `origin/staging` incl. #3119; 8 cases x 10 replicates x 2 arms = 160 runs on `/flows/runner-role-gated-approval`, `gemini-3-flash-preview`, `LLM_CACHE=0`): 236 CLASSIFIED REDIRECT MESSAGES \u2014 the floor is met on count. Against a deterministic PAGE-TRUTH preflight (the real `_snapshotForAI` of the fixture in four states, judged by the shipped `groundRedirect`): a fact the page does NOT have \u2014 100 messages, 100 DOWNGRADED (100%), 0 grounded, 0 abstained; a control behind THAT node's own collapsed menu \u2014 48 messages, 0 downgraded, 21 grounded, 27 `collapsed_affordance`; a control literally in the outline \u2014 87 messages, 0 downgraded, 87 grounded; plus 1 real judge correction with no locator clause, `no_locator_claim`. So: ZERO false downgrades on 135 true messages, ZERO false `grounded` on 100 false ones, and zero absent facts left un-downgraded. All three distinct `unobserved` sets were read individually and checked against the preflight. Abstain split (gate clause 3): `collapsed_affordance` 27, `no_locator_claim` 1, `no_snapshot` 0, `stale_corpus` 0. THE BEHAVIOURAL HALF, which no prior window had: on the 12 runs per arm whose every row was a downgrade, `standing_constraint:reinjected` is 0 under `=1` against 23 in shadow \u2014 the production `x7` re-injection of `btkyv0ns` reproduced in shadow and zeroed by enforcement \u2014 while the SAME arm still re-injects 30 times on runs carrying `grounded` / `collapsed_affordance` rows, which is the control against a blanket muzzle. The three completable controls are 10/10 `passed` in BOTH arms (60 runs, zero downgrades), and on the population where the flag can act at all no run is worse than its shadow twin (enforce 32/32 lane-judge pass vs shadow 33/33; the six pairwise cells where enforce failed and shadow passed all have `supervisorConsults = 0`, i.e. both arms ran a byte-identical path). READ THE DENOMINATOR HONESTLY: 235 of the 236 messages were ORDERED by the test-only `e2e/evals/runner/supervisorRedirectInjection.ts` seam plus `AGENTIQA_TEST_SUPERVISOR_EAGER=1`, neither of which exists in production. The pass measures the CLASSIFIER AND ITS CONSEQUENCES, not how often a real judge invents a locator \u2014 that frequency is known only from prod/mirror (redirects on 1 session in 17). (3) MIRROR PASS 2, the COMBINED window (2026-09-11 12:29:09-13:56:51Z, staging orchestrator `=1` alongside `BLOCKED_SYNTHESIS_PRESERVE_GRADES` \u2014 disjoint marker namespaces, so every row attributes to exactly one flag; engine `e89f22e7f`, a DIFFERENT SHA from pass 1's `7094117cb`): 19 runs, 14 passed / 3 failed / 2 blocked, TWO classified redirect messages, both correct `grounding_abstain {no_locator_claim}`, `downgraded` 0, `grounded` 0, and zero regressions attributable to this flag. Like pass 1 it is a NO-GO on its own terms (the `ungrounded` branch was never reached, so its zero is VACUOUS) and it is cited on exactly those terms: the mirror's job here is the other half of the proof \u2014 the fixture lane shows the gate fires and fires correctly, the two mirror windows show it does not fire where it must not, on two different engine SHAs. WHAT THE THREE PASSES DID NOT CLOSE is recorded in `graduation` below rather than papered over. EXACTLY ONE ROW PER CLASSIFIED REDIRECT (round 2): a grounded classification emits `supervisor_redirect:grounded`, which the first cut omitted to keep the census small \u2014 wrongly on both counts, since redirects are rare (one Lio session in 17) and without that row the DENOMINATOR this flag graduates on (redirect-MESSAGE counts, floor >=100) cannot be read at all; it also made the eval gate unsound, since 'at least one row' could not tell a grounded classification from a gate that never ran (measured on 2 of 3 live runs). Abstains are published in both live states as `supervisor_redirect:grounding_abstain {reason}` (the `cause_grounding:abstain` precedent): a rising `no_snapshot` rate is the honest signal that the LEDGER, not the judge, is what needs work, and an abstain-heavy census is not evidence the gate works. A DOWNGRADED REDIRECT IS STILL A REDIRECT FOR ESCALATION: `redirectFlipFlop.observeRedirect` and `sameGoalAbort.observeVerdict` both read `verdict.action` BEFORE this gate runs, so the abort/block escalation arithmetic is unchanged \u2014 pinned by test, because a refactor that moved the grounding call earlier would silently change abort timing. NOT A TEXT GATE OVER MODEL PROSE: it reads the SUPERVISOR's runtime-injected instruction, writes no status and no criterion, and DOWNGRADES rather than refuses (the cause-grounding stage-19c precedent). THE LEDGER is the only new state: the last 3 FULL `groundingSnapshot`s (12 kB each), written by one guarded, exception-safe read at the dispatch seam \u2014 never inside a `page.evaluate`, never between an addressing marker and its action \u2014 cleared by `resetSteeringStateForNewRun` beside the other steering state, and not written at all under `=0`. Acceptance suite: packages/engine-core/src/__tests__/supervisorRedirectGrounding.test.ts, BaseRuntime.supervisorRedirectGrounding.test.ts.",designDoc:"packages/engine-core/src/supervisorRedirectGrounding.ts",status:"active",added:"2026-09-10",graduation:{status:"gated",gate:"GRADUATED 2026-09-11 (default ON in the RUNNER lane; chat/explorer stay shadow \u2014 #3162) with TWO parts of this bar met by a SUBSTITUTE, one met only by a UNIT PIN, and TWO still OPEN \u2014 kept as `gated` rather than deleted, the CRITERIA_CONTAINMENT precedent, because the open parts are a real unmeasured false-fire channel and a whole unmeasured lane pair. THE ORIGINAL BAR, verbatim: census in REDIRECT-MESSAGE counts, not run counts, with a floor of >=100 redirect messages across the runner + chat + explorer lanes; (1) it fires on a replay of asess_1789009823056_btkyv0ns and every fired row names an operand the retained page-scoped snapshots genuinely do not contain \u2014 read every row, not a rate; (2) ZERO `would_downgrade` rows on the redirect population of runs that completed normally (runner/happy-form, runner/seeded-broken, the Lio mirror's passing plans); (3) the `grounding_abstain` split published alongside with `no_snapshot` below the review's stated bar; (4) the two named false-fire classes answered explicitly \u2014 an operand visible only in a SCREENSHOT (canvas text, an image-rendered menu) and a step whose capture was incremental must land as `abstain`, never `downgrade`. MET: the FLOOR \u2014 236 classified redirect messages (fixture pass 2) + 2 (mirror pass 1). (3) MET IN FULL \u2014 `collapsed_affordance` 27, `no_locator_claim` 1, `no_snapshot` 0, `stale_corpus` 0 of 236; the ledger was healthy on the measured page (`snapshotsSearched` 3 on 181 rows, 2 on 41, 1 on 13, 0 on the single early-return row), which is a statement about the ledger ON A HEALTHY PAGE, not about the ledger in general. (4b) MET BY UNIT TEST; NOT OBSERVED IN THE SURVIVING CENSUS. The incremental-capture class lands in `abstain` (`no_snapshot` / `stale_corpus`) and is pinned by `BaseRuntime.supervisorRedirectGrounding.test.ts`'s page-scoping section \u2014 a real pin, and the only one this clause has. The 5 live observations previously cited here come from census pass 2a, which was ERASED from a session scratchpad and cannot be re-read; in the surviving 236-message census `no_snapshot` is 0 and `stale_corpus` is 0, i.e. the class fired ZERO times. A unit pin is what the other clauses were explicitly not allowed to be, so this clause is downgraded rather than argued: nothing else in the gate depends on it, and reproducing it in the lane is cheap whenever someone wants it closed. MET BY SUBSTITUTE, stated as such: (1) NOT a session replay of `asess_1789009823056_btkyv0ns` \u2014 it is a REDUCED DETERMINISTIC FIXTURE (`/flows/runner-role-gated-approval`) carrying that session's correction VERBATIM, judged against a preflight page-truth table taken from the real `_snapshotForAI`; the 'read every row' half IS satisfied \u2014 all three distinct `unobserved` sets were read and each is true of the page. (2) NOT the named population \u2014 `runner/happy-form`, `runner/seeded-broken` and the mirror's passing plans were not driven with a scripted redirect; the substitute is 135 messages about facts the page genuinely HAS (0 downgrades), 60 runs of three COMPLETABLE controls 10/10 `passed` in both arms with 0 downgrades, and the mirror pass's 14 passing plans carrying 0 downgrades. NOT MET, and the reason this block stays: (0) LANE COVERAGE \u2014 all 236 messages are RUNNER. Chat and explorer produced no rows at all, so the `plan_licensed` branch (which only `RunnerRuntime` overrides) is unmeasured in the two lanes where it is structurally dead. ENFORCEMENT IS THEREFORE SCOPED TO THE LANE THE EVIDENCE COVERS (#3162): the graduated default downgrades in the runner lane only, chat/explorer keep the shadow behaviour and book `lane: 'other'` rows, and CHAT/EXPLORER GRADUATION NEEDS ITS OWN CENSUS \u2014 argued from those rows, or from a deliberate `=1` window, and landed as its own flip. That is not a residual to be waived: it is the open half of this entry, and the `graduation` block stays `gated` until it closes. (4a) THE SCREENSHOT-ONLY OPERAND \u2014 a control visible only in a screenshot (canvas text, an image-rendered menu) is not reproducible on this fixture and remains an unmeasured false-DOWNGRADE channel. Also on the record: 235 of the 236 messages were ORDERED by a test seam, so nothing here measures how often a real judge invents a locator. WHAT CLOSES THE REMAINDER, cheaply and without another forcing window: (a) one chat case with a scripted redirect \u2014 lane coverage plus the first live exercise of `plan_licensed`; (b) a fixture whose menu is rendered in canvas \u2014 (4a); (c) the same scripted redirects on `runner/happy-form` / `runner/seeded-broken` \u2014 the letter of (2). Until then the post-flip watchdog in `gates` above is the standing measurement, and `AGENTIQA_EXPERIMENT_SUPERVISOR_REDIRECT_GROUNDING=0` is the lever.",evidence:"Post-flip watchdog: the diag families `supervisor_redirect:downgraded` / `:grounded` / `:grounding_abstain` / `:would_downgrade` on the nightly qa-exhaustive runner + chat lanes (exported by e2e/evals/runner/groundingDiag.ts), read row by row and SPLIT BY THE `lane` FIELD \u2014 `runner` rows are the live gate, `other` rows are the chat/explorer census this flag has not yet graduated on. REPRODUCIBLE CORPUS: the 236-message census harness is committed at e2e/evals/runner/census/ (six census-only cases resolvable by --ids, the page-truth preflight, the analyzer and the recipe in its README); it was uncommitted at flip time, which is why the flip PR landed it. Graduation evidence, three passes: rehearsal-2026-09-11-redirect-p1.md (mirror pass 1, forced-ON, 17 plans, NO-GO by insufficient n), rehearsal-2026-09-11-combined.md (mirror pass 2, the combined forced-ON window, 19 runs, 2 abstain rows, zero regressions, engine e89f22e7f) and rehearsal-2026-09-11-redirect-fixtures.md (160 runs / 236 messages, the GO), all steering-local under ~/agentiqa-agents/lio-harvest/reports/.",owner:"steering (Alex)",review:"2026-10-10"}},UNNAMED_CONTROL_A11Y_FINDING:{key:"UNNAMED_CONTROL_A11Y_FINDING",envVars:["AGENTIQA_UNNAMED_CONTROL_A11Y_FINDING","AGENTIQA_EXPERIMENT_UNNAMED_CONTROL_A11Y_FINDING"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Structural accessibility findings on unnamed controls (#3091 milestone 3). THE GAP: a working flow whose primary action is an icon-only button is a real accessibility defect (a screen reader announces nothing) that no model reading a screenshot ever catches \u2014 the icon looks fine \u2014 while the BROWSER can answer it exactly. WHOSE READING (review round 2, 2026-09-09): the first cut read collectClickTargetStateFromElement's `accessibleName` \u2014 the first non-blank of [aria-label || title, aria-labelledby text, associated <label> text, own text]. That is an element-LOCAL formula, not the accname algorithm: it never descends, so `<button><img alt=\"Save\">` and `<button><svg role=img aria-label=\"Save\">` read as UNNAMED there while a screen reader announces them correctly. Harmless while the value only matched and described elements; as the sole substantiation of a customer-facing DEFECT it was a false-positive machine on the two commonest icon-button idioms. The engine now asks the browser: when the element-local formula came back empty AND the target is an interactive, enabled, non-decorative control, `stampComputedAccessibleName` resolves the element over CDP and reads `Accessibility.getPartialAXTree`'s `name.value` into `clickTarget.computedAccessibleName`; a node the tree reports as IGNORED (aria-hidden, role=presentation, hidden subtree) has no name to be missing and ABSTAINS; and when the probe cannot answer, the capture's `descendantName` fallback (a labelled img/svg/[title] inside the control, skipping aria-hidden subtrees) stands in and the notes say which reading was used. The flag OFF fires no probe at all, so the click hot path is unchanged. WHAT ON ENABLES, in one slice: (a) `accessibility` joins the `report_issue` category enum on all four declaration surfaces (agentToolDefs / agentTools / runnerToolDefs / runnerTools, via tools/reportIssueCategories.ts) and the field description names the one admissible use; (b) one bullet each in buildRunnerPrompt's QUALITY OBSERVATION block (both the snapshot-only and screenshot branches) and in ExplorerRuntime's issue-reporting rules, prescribing category='accessibility', severity='low', and that a working flow still PASSES; (c) the runner appends a verdict-free SOFT NOTE to the click result naming the engine's observation; (d) the runner keeps a per-step ledger of unnamed activated controls, keyed on `_currentStepIndex` exactly like `_stepActionErrors`, capped at 5/step; (e) handleReportIssue requires POSITIVE substantiation for the new category \u2014 at least one ledger observation FROM THE STEP BEING REPORTED ON \u2014 and rejects otherwise, and stamps a runtime marker naming the controls observed on that step onto the filed issue. THE SUBSTANTIATION IS THE POINT, not an add-on: `shouldRejectEmptyEvidenceIssueReport` short-circuits on any category that is not logical/visual, so a new category would BYPASS the empty-evidence gate and be filable on zero evidence; (e) replaces that bypass with a positive engine-fact requirement, so the model can no more invent an accessibility finding than it can invent a page error. VERDICT-NEUTRAL BY CONSTRUCTION: `reportedIssueCapsPassedRun` admits only (high|medium) AND (logical|environment), so an accessibility/low finding cannot cap a passed run to warning at any severity \u2014 the flow still passes and the finding is a curation item. EXCLUSIONS, each because its absence would assert something the evidence does not support: non-activation tools; no clickTarget (no engine observation, and the label-addressed click path resolved a NAME by definition); a named control; a DISABLED control (never activated); a RETARGETED click (which element the captured name belongs to is ambiguous); an author-declared role=presentation/none and any node the accessibility tree reports as IGNORED; a determinately-decorative target (isDeterminatelyDecorativeTarget \u2014 a styled span has no accessible name by design, the #2060 false-positive class); and a role-less element with neither an interactive tag/role nor click-handler evidence. SHADOW-FIRST (review round 4, 2026-09-09 \u2014 the RUNNER_STEP_ANTI_REGRESSION/#3088 convention). With NO env set the DETECTION runs \u2014 the CDP name probe, the DOM fallback capture, the classifier and the per-step ledger \u2014 and emits one `a11y:would_file {stepIndex, role, nameSource, selector}` row per distinct control, and NOTHING customer- or model-facing changes: no click note, no prompt bullet, no `accessibility` in the schema, and the ledger is READ AS EMPTY by the report_issue gate so a non-conforming filing is rejected exactly as before. `=1` ENFORCES (a)-(e) below. `=0` is the only state that stops the measurement itself, including the CDP round trip. The census is the point: a flag whose detection only ran where it was already on could never be graduated on evidence. MED (round 4): a VALUELESS `input[type=submit|reset]` is NOT unnamed \u2014 HTML-AAM 4.1.6 gives it the user agent's own localized default label and every screen reader announces \"Submit\"/\"Reset\" \u2014 so the DOM fallback now returns that default and the control is never flagged; `type=button`, which has no UA default, correctly stays reportable. OFF (`=0`) \u21D2 byte-identical tool declarations and prompts, no ledger, no note, no probe, and `accessibility` unreachable from the schemas. The ledger counts DISTINCT controls, keyed on `targetSignature` (tag|role|accessibleName|text): an unnamed control's `describeClickTarget` collapses to a bare \"button\", so without the key five clicks on one icon button would make the runtime marker state a control count the engine never measured. Diag: `unnamed_control_observed {stepIndex, tag, role, tool, nameSource}` (nameSource = computed | dom-fallback, so the census can weigh the browser's reading and the DOM fallback apart), `unnamed_control_finding:filed {issueId, observations}`, `unnamed_control_finding:rejected {reason}`. Acceptance suite: packages/engine-core/src/__tests__/unnamedControlSignal.test.ts (incl. the round-2 probes: <button><img alt> and <button><svg aria-label> => NO observation, <button><svg></svg></button> => observation, title => named, role=presentation and an a11y-tree-IGNORED node => excluded), BasePlaywrightService.computedAccessibleName.test.ts (the CDP probe, and when it is NOT fired), clickTargetAccessibleName.test.ts (the DOM fallback and its absence with the flag off), unnamedControlPromptParity.test.ts (flag-OFF byte parity on all four declaration surfaces and both prompt builders), RunnerRuntime.structuralFindings.test.ts. Eval fixtures: runner/textless-icon-button, plus runner/labelled-controls-fp-control as the false-positive control (the same contact form with every control labelled and MULTI-CONTROL steps, run with BOTH structural flags ON and judged `structural-control`: zero findings, zero drifts). runner/happy-form cannot serve as that control \u2014 it carries no `env`, so both flags are OFF while it runs.",designDoc:"packages/engine-core/src/unnamedControlSignal.ts",status:"active",added:"2026-09-09",graduation:{status:"gated",gate:"Graduates to defaultState:'on' by CENSUS, not by reading (steering ruling, review round 4). THE GATE: a nightly shadow census over at least 20 DISTINCT runner runs spanning the qa-exhaustive lane and the two clean #2577 lanes, with ZERO `a11y:would_file` rows against a control that HAS an accessible name. One false row blocks graduation until its CLASS is named and either excluded or accepted on the record \u2014 the count alone never graduates it. Read the rows split by `nameSource`: a `dom-fallback` row is the weaker evidence (the CDP probe could not answer) and a census that cannot tell the two apart would graduate on the wrong population. Additionally: zero `unnamed_control_finding:rejected` rows attributable to a correct finding the substantiation gate wrongly refused, and runner/labelled-controls-fp-control \u2014 the flags-ON false-positive control whose `structural-control` contract fails the lane on any filing \u2014 green across the same window. Until then it is opt-in per run/eval case; the shadow rows accrue with no env at all.",evidence:"nightly shadow census of a11y:would_file (>=20 distinct runner runs across qa-exhaustive + the two clean #2577 lanes, split by nameSource) plus unnamed_control_finding:{filed,rejected} and the runner lane false-positive controls",owner:"steering (Alex)",review:"2026-10-01"}},LABEL_DRIFT_DISCLOSURE:{key:"LABEL_DRIFT_DISCLOSURE",envVars:["AGENTIQA_LABEL_DRIFT_DISCLOSURE","AGENTIQA_EXPERIMENT_LABEL_DRIFT_DISCLOSURE"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:'Runner-lane plan/app label-drift disclosure (#3091 milestone 3). THE SHAPE: a saved plan says `Fill the "Email address" field with \u2026`, the app has since renamed that field to "Login e-mail". The step does NOT fail \u2014 the resolver misses, returns its addressable-labels soft note, the agent re-addresses by ref or by the new name, and the step passes. That is the right outcome; what is missing is the RECORD, so the customer cannot tell that their plan text has gone stale. WHAT ON ENABLES: on a successful label-addressed type or click, the RESOLVER ITSELF answers, per control name the plan step STATES, whether this page addresses by it \u2014 same in-page matcher (`typeByLabel`\'s rank ladder incl. its CONTAINS tier / `resolveClickableByLabel`\'s `matchRank`), same candidate set (`collectLabels` per editable; `accessibleName` per clickable) \u2014 and returns `{planLiteral, resolves, rank?, matchedLabel?}` on `ActionMetadata.planLabelResolutions`. That verdict rides the ToolCallResult side channel (never LLM-facing) and is the ONLY evidence the disclosure accepts for "the page does not answer to the plan\'s name". WHY IT IS THE RESOLVER AND NOT THE RUNNER (review round 3, 2026-09-09): round 2 read `typeByLabel`\'s addressable-label enumeration and judged membership with a LOCAL ladder, and both were narrower than the resolver whose miss they claimed to report \u2014 no CONTAINS tier, and one label per field instead of `collectLabels` \u2014 so `Message` ~ "Your message here", `Name` ~ "What is your name?", `email` ~ "E-mail address", `Prenom` ~ "Pr\xE9nom" and a field whose `<label>` is still rendered beside its `aria-label` were all booked as renames on apps nothing had renamed. Both the local ladder and the enumeration census are DELETED. WHAT CAN DRIFT: only a control name the step QUOTED. A disclosure is a claim that a NAME went stale, so it is made only about a plan that states one; `into the chat input` describes a control rather than naming it, and a composer whose only name is "How can I help you today?" genuinely does not answer to "chat" \u2014 no resolver-based rule could rescue that shape. Cost, accepted: a plan that names its controls without quoting them discloses nothing. THE THREE REFUSALS, in order: MATCH-ANY (`sameControlName` \u2014 a case/punctuation/DIACRITIC-folded containment fold, deliberately more generous than any resolver and a SUPPRESSOR ONLY: it can silence a disclosure and can never create one), KIND + UNIQUENESS (a type compares against the step\'s FIELDS, a click against its buttons and links; two candidates of the acting kind abstain), and the RESOLVER\'S MISS (`resolves === false`; an absent verdict abstains, so a coordinate type, a ref click, an option pick or a dialog dismissal can never disclose). The engine side of the comparison is the label the RESOLVER matched, read from the engine-internal ToolCallResult side channel \u2014 `clickMatchedName` for a click, `writeMatchedLabel` for a write. Round 5 (MED-1) moved the click half there: it used to be read from the MODEL-FACING `response.matchedName`, which is gated on this flag, so under the shadow default `usedLabel` was empty and the census was structurally blind to the ENTIRE CLICK PATH \u2014 the population round 3\'s banner-then-button defect lived in, and a census silent on a population is not evidence that the population is clean. `typedIntoField` is deliberately NOT used: it comes from `getFocusedFieldName`, a third formula that can answer with an internal `name` attribute which is not a user-visible label at all. DRIFT IS A STEP-END JUDGEMENT (review round 4, 2026-09-09). A drift is booked from ONE action but claims something about the whole STEP, and round 3 retracted only on a later LABEL-ADDRESSED action \u2014 so a step that reached the plan\'s control by REF, by coordinates or by keyboard could neither re-probe nor retract, and the review\'s banner-then-button-by-ref repro shipped a "your plan text has gone stale" note about a page rendering exactly the named button. A row is disclosed ONLY when the literal missed on EVERY mid-step probe AND on a final probe at STEP END \u2014 the same function, same census, run once more on the page the step left behind (`IComputerUseService.probePlanLabelResolutions`, both resolvers in `probeOnly` mode, called at the `signal_step` transition and once more at run_complete for the step the run ended on) \u2014 AND at least one label-addressed action in the step succeeded via a DIFFERENT matchedLabel. Any resolving probe at any point retracts (`label_drift_retracted`), and an UNANSWERABLE step-end probe leaves the step unsettled, which discloses nothing: an unmeasured page is not an empty one. Diag `label_drift:step_end_probe {stepIndex, literals, resolved, withdrawn, surviving}`. THE PROBE\'S CENSUS IS THE PAGE, NOT THE RESOLVER\'S REACH (review round 4, HIGH-1 root). The resolvers address only this document\'s light DOM, so a plan field rendered inside an OPEN SHADOW ROOT or a SAME-ORIGIN IFRAME made the probe answer "nothing on this page answers to that name" about a page rendering exactly that field. The probe now walks every root the page renders controls in (this document, open shadow roots transitively, same-origin frame documents; bounded at 40 roots) with the SAME ladder and the SAME per-field `collectLabels` census, and the `instanceof` tag tests are realm-safe so a cross-frame node is classified correctly. The ADDRESS is untouched. Widening is one-directional and therefore safe: a bigger census can only turn `resolves:false` into `true`, i.e. only ever SILENCE. THE PROBE CANNOT FAIL THE ACTION (review round 5, HIGH-1). Round 4 ran the probe INSIDE the address\'s `page.evaluate` and read `host.shadowRoot` unwrapped, so a page defining a throwing `shadowRoot` getter rejected that evaluate and KILLED THE TOOL CALL \u2014 a failed step the `=0` state does not have, in the state this ships in by default. The probe now runs in its OWN `probeOnly` evaluate behind one Node-side seam (`runPlanLiteralProbe` \u2014 the single implementation used by the click resolver, the editable resolver and the step-end re-probe) which wraps the whole evaluate, and every in-page read is wrapped per root and per candidate. A failure at either layer ABSTAINS \u2014 no verdict, which is silence at both seams \u2014 and emits one `label_drift:probe_failed {reason, kinds, literals}` row; never a `resolves:false`, because unanswerable is not absent, and one poisoned read poisons the WHOLE verdict rather than quietly narrowing the census. AND IT CANNOT COST THE ACTION BY TIMING EITHER (round 6, HIGH-1r): round 5 ran the probe BETWEEN the address\'s `setAttribute(marker)` and the `locator.boundingBox()` that consumes the marker, so a page that MUTATES from a getter read \u2014 `location.replace()`, a getter that replaces the marked input, or a plain SPA container re-render, all three deterministic in real Chromium \u2014 stranded the write with `locator.boundingBox: Timeout 3000ms exceeded` while the probe reported success (`probe_failed = 0`). The probe now runs BEFORE the address (`probeThenAddress`), so the marker does not exist while the page is read; a probe read that navigates the page costs ONE re-issue of the address (`label_drift:probe_navigated`), reachable only when a probe ran and therefore never at `=0`. Stated skew: the census is of the page one evaluate before the address rather than one after it. FRAME DESCENT IS REALM-SAFE TOO (round 5, HIGH-2): the walk descended with `host instanceof HTMLIFrameElement`, which is false for an `<iframe>` that lives in a CHILD document, so the census stopped ONE frame deep (measured `{rootCount:2}` on a page rendering three roots) and booked a false miss on a field the page renders at depth 2. It descends by tag name now, and the two remaining cross-realm `instanceof`s reachable through `collectLabels` are gone. A CROSS-ORIGIN frame answers `contentDocument === null` without throwing and is skipped SILENTLY \u2014 a narrower census, which can only silence. A drift is recorded into a per-step ledger keyed like `_stepActionErrors`, capped at 4/step and de-duplicated per (plan, used) pair, and `applyLabelDriftDisclosure()` appends one sentence naming BOTH literals to each such step\'s `note` at cascade stage 19b\u2032, immediately after applyStepReplayDisclosure. WRITES NO VERDICT \u2014 not a status, not a criterion, not a polarity \u2014 exactly the step_replay_disclosed contract; and it is NOT a propose_update: the runtime discloses, the customer curates. SHADOW-FIRST (review round 4 \u2014 the RUNNER_STEP_ANTI_REGRESSION/#3088 convention). With NO env set the DETECTION runs \u2014 literals injected, the ACTING resolver probed on every label-addressed action (click as well as type, since round 5 MED-1), BOTH resolvers probed at step end, the ledger kept \u2014 and one `label_drift:would_disclose {stepIndex, planLiteral, matchedLabel, evidence}` row is emitted per surviving pair at 19b\u2032, while NOTHING customer- or model-facing changes: no step note, and `matchedName` is still NOT forwarded to the model. `=1` writes the note. `=0` \u21D2 the plan literals are never injected into the per-action config, so both resolvers skip the probe entirely (no extra DOM work, no metadata), there is no ledger, no step-end probe and no note, and the click response is byte-identical to the pre-change shape. SUPPRESSOR COST, measured at review (round 4, MED). `sameControlName` folds case, whitespace, punctuation, separators AND diacritics, then accepts containment IN EITHER DIRECTION \u2014 so it silences `Email`\u2192`Login e-mail`, `Name`\u2192`Surname`, `Name`\u2192`Username`, `Password`\u2192`Confirm password`, `Send`\u2192`Send message`, `Email`\u2192`E-Mail`/`Emails`, and a handful of accidental cross-word collisions (`To`/`Total`, `ID`/`Video`, `Card`/`Discard`). The disclosure therefore fires only on a rename that REPLACES the word entirely; renames that ELABORATE a name \u2014 the common kind \u2014 are silent. A second narrowing on the type path: `collectLabels` feeds the probe a field\'s `id` and `name`, and the type ladder\'s CONTAINS tier accepts either direction, so any 1-2 character `id` on any editable resolves almost any literal. Neither is a defect \u2014 both are the silence-biased direction \u2014 but the census must be read as a measurement of THAT population, never as "renames are rare". RESIDUALS, recorded (round 4): (1) a QUOTED DESCRIPTOR still fires \u2014 `Type "Hello" into the "chat" input` on a composer whose only name is "How can I help you today?" discloses, because quoting is a heuristic about how humans write, not a fact about what the app calls the control; the suppressor rescues most descriptors because they are folded substrings of the real label, and the composer prompt is the shape where it does not. (2) FIXED in round 4: the quote families now include German `\u201E\u2026\u201C`, guillemets `\xAB\u2026\xBB`/`\u2039\u2026\u203A` and CJK corner brackets, so a non-English-authored plan is no longer structurally invisible. Diag: `label_drift_detected {stepIndex, planLabel, usedLabel, requestedLabel, evidence, tool}` (evidence = resolver-miss, now the only class \u2014 a request BY the plan literal that succeeded means the literal resolves) at the moment of detection, `label_drift_retracted {stepIndex, planLabel, usedLabel, withdrawn, tool}` on a withdrawal, and `label_drift:disclosed {stepIndexes, notedSteps}` at 19b\u2032. Acceptance suite: packages/engine-core/src/__tests__/labelDriftDisclosure.test.ts (incl. the four round-2 false-positive shapes \u2014 dropdown option, cookie banner, dialog confirmation, neighbouring-field coordinate type \u2014 and the five round-3 never-renamed shapes, each pinned at ZERO disclosures), RunnerRuntime.structuralFindings.test.ts (detection, retraction, and the flag-OFF injection). Eval fixture: runner/label-renamed, plus runner/labelled-controls-fp-control as the no-drift control \u2014 a MULTI-CONTROL step whose STATED names the app renders under CONTAINS-tier labels ("What is your name?", "E-mail address", "Your message here"), run with both structural flags ON and judged `structural-control`. runner/happy-form cannot serve as that control \u2014 it carries no `env`, so the flag is OFF while it runs.',designDoc:"packages/engine-core/src/runner/labelDriftDisclosure.ts",status:"active",added:"2026-09-09",graduation:{status:"gated",gate:"Graduates to defaultState:'on' (the STEP_REPLAY_DISCLOSURE shape \u2014 disclosure-only behavior earns default-on) BY CENSUS, not by reading (steering ruling, review round 4). THE GATE: a nightly shadow census over at least 20 DISTINCT runner runs spanning the qa-exhaustive lane and the two clean #2577 lanes, with ZERO `label_drift:would_disclose` rows on a plan whose literal was NOT renamed. One false row blocks graduation until its CLASS is named and either suppressed or accepted on the record \u2014 the count alone never graduates it. The rows carry {planLiteral, matchedLabel, evidence, stepIndex} precisely so a false one can be read back to its class without opening the run. Read the census against the suppressor cost recorded in `gates` above: the surviving population is word-REPLACING renames only, so a low row count is expected and is not by itself evidence that the signal is quiet. runner/labelled-controls-fp-control \u2014 the flags-ON no-drift control whose `structural-control` contract fails the lane on any recorded drift \u2014 must stay green across the same window.",evidence:"nightly shadow census of label_drift:would_disclose (>=20 distinct runner runs across qa-exhaustive + the two clean #2577 lanes) with a manual read of every row, plus label_drift:step_end_probe and the runner lane no-drift controls",owner:"steering (Alex)",review:"2026-10-01"}},VERIFY_EVIDENCE_GATE_STANDOFF:{key:"VERIFY_EVIDENCE_GATE_STANDOFF",envVars:["AGENTIQA_VERIFY_EVIDENCE_GATE_STANDOFF","AGENTIQA_EXPERIMENT_VERIFY_EVIDENCE_GATE_STANDOFF"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Runner-lane (test_run) standoff-breaker for the DISPATCH-SEAM verify-evidence gate (`RunnerRuntime.maybeBlockActionUntilVerifyEvidence`). THE DEFECT (#2599): that gate refuses every non-evidence browser action on a `type=verify` step until the step has recorded explicit evidence, and it had NO counter, NO cap and NO kill switch \u2014 so a model that keeps trying to act (a `click_at` on a login wall it must clear first, a `hover_at` at step 10) was refused forever. The refusal is `isMetaTool`, and BaseRuntime skips the post-dispatch loop-detector CHECK for a meta result, so the stall safety net never armed either: measured as runs terminating with 0/6 and 0/10 executed steps and no verdict. WHAT ON CHANGES: consecutive refusals are counted PER VERIFY STEP in the dedicated `_verifyEvidenceGateBlockCounts` map, and on the SECOND the runtime captures the step's evidence itself via `autoCaptureVerifyEvidence` \u2014 the same auto-capture the sibling `maybeIsolateVerifyMarker` has used since AG-7661: a real `screenshot` down the normal `executeAction` -> `executeBrowserAction` path, so it is stamped `planStepIndex = currentPlanStepIndex()` (the ACTIVE verify step) and criterion 3 of claim batch.verify-never-blind holds by construction. The step then enters `_verifyEvidenceStepIndexes` permanently, so the THIRD attempt dispatches and `noteStepExecuted` finally raises `_maxStepEverExecuted`. Bounded at 2 refusals per verify step; the capture fires at most once per step. The capture carries a GATE-SPECIFIC model-facing note (the sibling's note talks about a co-located marker the model never emitted here) and is a NORMAL non-meta result, so the loop detector sees it like any other screenshot. THE COUNTER IS DEDICATED, NOT THE SHARED `_consecutiveBrowserShortCircuits` STREAK, for two reasons: that streak is fed by other short-circuit sources (auto-evidence reuse, the off-plan label-click rejection, the same-value write breaker), so the gate would capture on a standoff it did not cause; and \u2014 load-bearing \u2014 ANY real dispatch resets it, INCLUDING the plain `wait` that #2952 just exempted, and the measured composite shape is `wait, click, wait, click ...`, on which a shared-streak breaker would never reach 2. SECOND HALF, SAME SWITCH: the remaining bounces stop carrying `resetLoopDetector: true` \u2014 this is the first of the four NAMED RESIDUAL sites listed under RUNNER_SIGNAL_STEP_FORWARD_RESET. A meta block is already excluded from the post-dispatch loop check, so the flag only DISARMED the stall net on every refusal (`buildVerifyMarkerIsolationBlock` documents the same policy verbatim); with the bounce now bounded at 2 per step the behavioral delta is small and bounded, which is why it rides this change rather than shipping alone. NOT TOUCHED: the RUN-CLOSE half (`buildVerifyEvidenceRequiredResult`) stays unbounded, unflagged by this switch and diag-silent; the gate's exempt sets (`VERIFY_EVIDENCE_ACTIONS`, `isPassiveSettleAction`) are unchanged; and no step, criterion or run status is re-graded by this flag \u2014 it changes only WHETHER an action reaches the browser and WHEN the step's own evidence exists. Diagnostics, both new (the gate previously emitted nothing of its own \u2014 the only trace was `handleToolCall`'s streak row): `verify_evidence_gate:blocked {stepIndex, action, count}` on each refusal and `verify_evidence_gate:standoff_capture {stepIndex, action, priorBlocks}` on the capture. OFF (`=0` under either spelling) restores BOTH halves byte-identically: unbounded refusal AND `resetLoopDetector: true`. Acceptance suite: packages/engine-core/src/__tests__/RunnerRuntime.verifyEvidenceGateWait.test.ts (sections 7-12: bounce -> bounce+capture -> dispatch, capture-once-per-step, per-step isolation, the interleaved-`wait` trap, the planStepIndex stamp, and both flag states incl. the await pin).",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-09-09"},REPEATED_SIDE_EFFECT_GUARD:{key:"REPEATED_SIDE_EFFECT_GUARD",envVars:["AGENTIQA_REPEATED_SIDE_EFFECT_GUARD","AGENTIQA_EXPERIMENT_REPEATED_SIDE_EFFECT_GUARD"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Phase 3 Layer 3 of the Lio verdict-integrity track. SHADOW-FIRST. THE DEFECT: `novel_url` is an UNLIMITED structural-progress reset in the runner lane \u2014 NOVEL_URL_BUDGET=40 is consulted only when rearmUrlNovelty is on, and BaseRuntime sets that for the assistant_v2 lane only \u2014 while resetForNewStep additionally wipes seenUrls every declared step. On prod tp_d1360115 every 'Duplicate Request' click minted a NEW draft id and therefore a new URL, so ONE control produced 252 credited progress milestones, the 30-action structural breaker never armed once in 300 iterations, and the same false signal ALSO reset sameGoalAbort and the supervisor verdict freshness (the triple defeat). The run left 9 junk draft duplicates in the customer's tenant. WHAT =1 ENABLES: per-TARGET novel-URL credits inside LoopDetector.updateUrl, keyed by the engine-resolved click-target signature (targetSignature: tag|role|accessibleName|text, threaded from response.clickTarget through ToolCallResult.loopDetectorUpdate.clickTargetSignature) and falling back to the detector's own buildKey for click-class actions the engine did not resolve. The first K=3 novel URLs from one target (\xD72 in exploration mode) are credited exactly as today; past the budget the actionsSinceProgress reset is SUPPRESSED and a distinct `repeated_side_effect` signal is emitted instead \u2014 which BaseRuntime routes AWAY from sameGoalAbort.recordMilestone() and supervisorVerdictFreshness.recordProgress(), so a suppressed signal cannot disarm the supervisor guards either. BOTH PROGRESS CHANNELS, NOT ONE (adversarial review 2026-08-06 \u2014 the first build was a placebo): an over-budget verdict carries from updateUrl to the SAME action's updateScreenContent, because BaseRuntime sets trackNovelScreenContent:true on every surface and a page that mints an entity necessarily renders new content (its own id), so each suppressed click produced a novel snapshot hash one line later, credited `novel_screen`, and re-opened all three defeats. The snapshot hash is still recorded (a genuine later revisit is never re-credited) but the reset is withheld and the novel-screen budget is NOT spent. Under the production configuration the structural breaker now arms on click 32 \u2014 the same bound the URL-only tests assert. Credits follow the URL subsystem's scoping exactly (per declared step in the runner lane, turn-scoped when rearmUrlNovelty is on), which is what keeps a legitimate wizard safe: a fresh step gets a fresh budget, a stable-URL SPA funnel never enters the guarded branch at all and keeps its novel_screen progress in both flag states. WIZARD SAFETY is pinned under the PRODUCTION config (linear-wizard + SPA-funnel) plus the loop-eval replay corpus. KNOWN LIMIT, pinned as a test rather than hidden: in the RUNNER lane resetForNewStep refills the per-target credits along with seenUrls and actionsSinceProgress (the pre-existing LOOP_URL_NOVELTY_REARM scoping the runner lane never opted into), so a run that re-declares a step per click refills 3 credits per click and is NOT bounded by this layer \u2014 the measured incident had that shape, and REDIRECT_FLIPFLOP_GUARD's run-scoped 24-redirect budget is what bounds it. COMPANION FIX, unconditional (not gated by this flag): LoopDetector.buildKey now reads args.label for click_at / double_click_at / right_click_at / hover_at / type_text_at. `label` is a first-class targeting modality PREFERRED over ref/x/y, so every label-addressed click previously collapsed onto the coordinate default 'click_at:0,0' \u2014 clicking 'Submit' and clicking 'Cancel' were the same key. BLOCK WORDING, ENFORCE-ONLY: when one target dominates the suppressed credits (>=2), the structural force_block message says the control WORKED and each click committed another record, rather than the generic 'the task is not converging' \u2014 the pattern is the defect, not the control, and that wording reaches the filed issue. It is gated on enforce because the suppression COUNTER fills in both states: ungated, the default state rewrote the model-facing block reason (which also feeds the loop-breaker judge's contextHints) with text that was FALSE there \u2014 in shadow those repeats did credit progress \u2014 and attributed the block to the minting control when something else caused it. DEFAULT OFF = SHADOW, NOT INERT AND NOW GENUINELY DIAG-ONLY: the per-target accounting runs in full and emits agent_lifecycle repeated_side_effect_would_suppress {target, channel:'novel_url'|'novel_screen', url, creditsGranted, creditBudget, suppressionCount, actionsSinceProgress}, while both credits are granted byte-identically to today and no model-facing string changes. An explicit =0 under either spelling switches the accounting itself off. Acceptance suite: packages/engine-core/src/__tests__/LoopDetector.repeatedSideEffect.test.ts (incl. the 'PRODUCTION configuration' and 'shadow purity of the block wording' describes) + LoopDetector.test.ts (buildKey label) + BaseRuntime.repeatedSideEffect.test.ts (the signal does not reach the milestone resets).",designDoc:"docs/plans/2026-08-06-lio-terminal-verdicts-and-zero-grade-bounce-design.md",status:"active",added:"2026-08-06",graduation:{status:"gated",gate:"A shadow census of repeated_side_effect_would_suppress on the nightly lanes showing (a) it fires on the entity-minting eval and the tp_d1360115 replay shape, and (b) ZERO would-suppress rows on the wizard/funnel fixtures and on runs that completed normally \u2014 i.e. no legitimate multi-page flow is having its progress signal withdrawn. TWO risks the census must answer, one per channel now that both are guarded: a genuinely-progressing flow whose per-page 'Next' button shares one accessible name spends its K=3 within a single declared step (novel_url rows), and a stable-target flow that renders genuinely-new content per action loses that content credit too (novel_screen rows \u2014 read them separately via the row's `channel` field). SEPARATE, AND REQUIRED BEFORE THE RUNNER LANE GRADUATES: decide the runner-lane credit scoping. Today resetForNewStep refills the credits every declared step there, so a step-bouncing run \u2014 the measured incident's own shape \u2014 is not bounded by this layer at all; either turn-scope the credits in the runner lane (with its own wizard census, since that removes the per-step refill this design leans on) or graduate the chat lane only and state that REDIRECT_FLIPFLOP_GUARD is the runner-lane bound.",evidence:"agent_lifecycle repeated_side_effect_would_suppress rows (both channels) on qa-exhaustive and the loop-detection eval lane, plus the eval loop-detection/entity-minting-wander flipping from wall-clock-backstop termination to a structural block inside its 60-iteration budget. The eval is a CHAT-lane case, where credits are turn-scoped, so it certifies the chat lane only \u2014 and its fixture must render DISTINCT content per minted draft, otherwise the run's screens hash identically, the novel_screen channel is inert for reasons unrelated to this guard, and the eval measures a shape production never produces.",owner:"steering (Alex)",review:"2026-08-13"}},RUN_PROGRESS_BACKSTOP:{key:"RUN_PROGRESS_BACKSTOP",envVars:["AGENTIQA_RUN_PROGRESS_BACKSTOP","AGENTIQA_EXPERIMENT_RUN_PROGRESS_BACKSTOP"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Judge-gated run backstop in the chat/explorer (assistant_v2) runLoop: a wall-clock (12 min, primary), iteration (200), and cumulative prompt+completion billed-token (6M) ceiling \u2014 all well above p99 legit chat turns and below the 300-iteration explorer child cap. At a ceiling breach the loop-vision progress judge (the same fail-closed SupervisorService judge the loop breaker uses, temp-0 / thinkingBudget-0) is consulted ONCE with a goal-anchored progress question, rather than blind-terminating: "progressing" grants a BOUNDED extension (each ceiling raised by one window) up to a hard cap of RUN_BACKSTOP_MAX_EXTENSIONS=2 (worst case ~36 min), after which the run terminates regardless of the judge; "wandering" terminates with a judge-confirmed "not getting closer to the objective" message; any no-signal case (no judge wired in this lane, judge error / timeout / unparseable) fails CLOSED to a terminate with a neutral "hit the safety limit" message. Ends with blockKind=backstop / endKind=run_backstop, NOT loop_block, so it is not counted toward the session structural-loop cap. No interactive ask and no cross-turn state (the escalate\u2192ask_user ladder was removed; the judge consult is synchronous and re-derived per breach). Off removes all three ceilings (only bound remains iteration<=maxIterations=300, ~2.5h). Never a crash \u2014 the emit path fails open while the judge fails closed. Runner (test_run) lane never opts in.',designDoc:"docs/plans/2026-07-20-loop-safety-rearm-and-backstop-design.md",status:"active",added:"2026-07-20"},CLICK_AT_INTERACTIVE_DESCEND:{key:"CLICK_AT_INTERACTIVE_DESCEND",envVars:["AGENTIQA_CLICK_AT_INTERACTIVE_DESCEND","AGENTIQA_EXPERIMENT_CLICK_AT_INTERACTIVE_DESCEND"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"A coordinate click_at/double_click_at whose point resolves to a NON-interactive element descends to an interactive descendant within 16px and clicks it via locator, plus a no-effect advisory on a zero-mutation same-URL click; off restores the raw mouse.click(x,y) with no advisory.",designDoc:"docs/plans/2026-07-11-click-at-interactive-descend-design.md",status:"active",added:"2026-07-11"},LABEL_CLICK_TWIN_COLLAPSE:{key:"LABEL_CLICK_TWIN_COLLAPSE",envVars:["AGENTIQA_LABEL_CLICK_TWIN_COLLAPSE","AGENTIQA_EXPERIMENT_LABEL_CLICK_TWIN_COLLAPSE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'BOTH twin-collapse kinds in `resolveClickableByLabel`, plus the actionable-click stage they feed (`pickActionableTwin` / `probeClickTopmost`). (1) Same-destination: a click_at/double_click_at label ties at the best rank across <= 3 elements that all resolve to ONE absolute http(s) href which is not the current page, opened in the same browsing context (equal `target`), each inside a landmark, whose OUTERMOST landmarks span >= 2 distinct region kinds including a header or a footer \u2014 the header-nav / footer-nav twin, e.g. "Security" on the pricing page. (2) Containment: the tied candidates nest (`<a href><button>Log in</button></a>`), so the innermost one is clicked. Either way the collapsed control is clicked only after it is proven topmost where it will be clicked; if nothing can be, nothing is clicked. Off restores the pre-fix behavior \u2014 any rank tie returns `ambiguous` and nothing is clicked. It does NOT gate the diagnostics on the no-click paths: the enriched ambiguity note (landmark / visibility / href hints) and the already-on-this-page note for twins that link to the current URL.',designDoc:"packages/engine-core/src/BasePlaywrightService.ts",status:"active",added:"2026-08-02",notes:'Root-cause fix for the #2024 blocked-loop class: the ambiguous no-op pushed the agent onto snapshot refs, which `_snapshotForAI` (refs: "interactable") never mints for an auto-hidden `fixed` header \u2014 so both recovery paths were dead and LoopDetector force-blocked on screen_cycling.'},LOOP_BLOCK_ATTRIBUTION:{key:"LOOP_BLOCK_ATTRIBUTION",envVars:["AGENTIQA_LOOP_BLOCK_ATTRIBUTION","AGENTIQA_EXPERIMENT_LOOP_BLOCK_ATTRIBUTION"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:`Evidence-gated loop-block finding attribution: when a force-block's dominant repeated click target is a non-interactive element or a no-op self-anchor, suppress the false "Page appears stuck" auto-issue instead of filing it. Off restores the old unconditional filing (subject only to the AG-6107/AG-6490 gates).`,designDoc:"docs/plans/2026-07-11-loop-block-finding-attribution-design.md",status:"active",added:"2026-07-11"},CLICK_AFFORDANCE_CAPTURE:{key:"CLICK_AFFORDANCE_CAPTURE",envVars:["AGENTIQA_CLICK_AFFORDANCE_CAPTURE","AGENTIQA_EXPERIMENT_CLICK_AFFORDANCE_CAPTURE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Phase-2 of loop-block attribution: captures click-handler affordance evidence onto clickTarget (page-context inline/React/Vue signals + a CDP addEventListener probe on the rare non-interactive branch) so a dominant non-interactive target with a real-but-dead handler files a 'Custom control appears unresponsive' issue instead of being suppressed agent-side. Off \u21D2 no affordance field emitted \u21D2 the custom-control classifier branch can never fire (falls back to the #1477 status quo); it does NOT re-enable filing on a bare non-interactive target.",designDoc:"docs/plans/2026-07-11-custom-control-unresponsive-detection-design.md",status:"active",added:"2026-07-11"},DECORATIVE_NON_CONTROL_ROUTING:{key:"DECORATIVE_NON_CONTROL_ROUTING",envVars:["AGENTIQA_DECORATIVE_NON_CONTROL_ROUTING","AGENTIQA_EXPERIMENT_DECORATIVE_NON_CONTROL_ROUTING"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:`Affordance-mismatch RE-ROUTING of an "unresponsive control" report (#2060). A report_issue whose title/description claims a dead/unresponsive control and names a recently-clicked element the click evidence determinately shows carries NO handler of any kind (inlineHandler false, frameworkHandler null, directListener false AND activationListener false \u2014 no element-own listener for click/pointerdown/mousedown/pointerup/mouseup/keydown/keyup/touchstart), no interactive tag/role, no author-declared control affordance (tabindex >= 0, contenteditable, aria-expanded/haspopup/pressed/controls) and no hard-failure evidence on that element's own clicks, is ACCEPTED and RE-LABELLED rather than refused: title becomes 'Looks clickable but is not wired: <label>', severity is capped at low, a runtime note naming the negative probes is appended, and evidence.claimRelabel records the model's original title/severity. When the SAME report also names a handler-bearing control the claim is bounced ONCE (twice at most) asking for separate filings, then fails open and files as-is, so the genuine finding is never lost. Also gates the probe widening that produces activationListener (the click-only directListener is unchanged, so the loop-block custom-control classifier is untouched). Off \u21D2 no re-label, no bounce, no activationListener capture \u21D2 byte-identical to the post-#2957 behavior. It is a DEDICATED switch: CLICK_AFFORDANCE_CAPTURE=0 would also kill the POSITIVE dead-control detection, so it is not a rollback for this feature.`,designDoc:"docs/plans/2026-07-11-custom-control-unresponsive-detection-design.md",status:"active",added:"2026-09-02",notes:"Refuse-vs-re-route was the design fork. A failed-hydration / never-wired button has EXACTLY this probe signature (role-less, unfocusable, no handler of any kind), so a hard refusal drops a real bug class with no honest alternative claim. The re-label keeps the finding, changes the claim, and caps the grade; the hard-failure escape (a console/page error captured on that element's own click) keeps a broken control out of the re-label entirely."},CLICK_EFFECT_SIGNAL:{key:"CLICK_EFFECT_SIGNAL",envVars:["AGENTIQA_CLICK_EFFECT_SIGNAL","AGENTIQA_EXPERIMENT_CLICK_EFFECT_SIGNAL"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:`Post-click effect signal on EVERY click path (coordinate, retargeted, ref and label \u2014 click_at and double_click_at), SHADOW-FIRST. After the click the engine waits up to a 500ms settle window for the DomObserver to record any DOM/text/attribute mutation (returning the instant one lands), then compares the URL and the auto-accepted-dialog tally. Any movement \u21D2 an effect was observed. No movement \u21D2 the click STILL reports SUCCESS (a click that legitimately changes nothing must never become an error) and the feature's only output is agent STEER. THREE STATES, not two: no env (default) = SHADOW \u2014 detection runs and emits the click_effect:would_signal diag, but no advisory, no effectObserved metadata and no prompt-byte change reach the model; =1 = ENFORCE \u2014 the hedged re-observe advisory rides the successful tool result, effectObserved lands on the ToolCallResult side channel, the wait_for_element "Do NOT retry" coaching softens to its effect-aware wording and the runner prompt gains the re-observe-after-corrective-action line, with a click_effect:signaled diag; =0 = FULLY OFF \u2014 not even the probe runs, so there is zero added latency and click results plus every prompt byte are identical to pre-change behavior. Targets the 2026-07-29 vrgate false-FAIL (run_b72f5099-cee2-4063-b109-1961c55c9898), where a click_at on a rendered React button ~1.3s after a Next.js client navigation reported success while the onClick never fired (painted but not yet hydrated), the agent trusted the success, and the plan FAILED for a defect that did not exist. EXECUTOR-SIDE ONLY: it changes no verdict path, writes no step/criterion/run status, and adds no gate \u2014 it changes what the AGENT is told, and the agent still grades. Fail-safe by construction: an in-page probe that cannot run (CSP, cross-origin frame, document destroyed mid-navigation) yields NO verdict rather than a false "no effect"; navigation/dialog evidence is evaluated BEFORE the mutation counters so the one case where the probe reliably dies is the case the URL alone already proves; canvas-dominant surfaces and native property-only controls (checkbox/radio/select/text input \u2014 where a spurious "re-perform once" would TOGGLE the control back) are advisory-suppressed; and the pre-existing #1476 dead-coordinate peek keeps its exact timing and semantics. Canvas surfaces additionally skip the PROBE (not just the advisory) once a capture has classified the page as canvas-dominant, so a whiteboard/design flow never pays the settle window per click for a verdict that is suppressed on arrival; those clicks are counted as click_effect:probe_skipped. A SECOND probe skip covers the unobservable case: in SHADOW, where the diag is the feature's only output, a platform with no BasePlaywrightService.diagLog wired skips the probe entirely rather than pay the settle window for a measurement nothing can read (ENFORCE always probes \u2014 its output is agent-visible behavior, not telemetry).`,designDoc:"packages/engine-core/src/clickEffectSignal.ts",status:"active",added:"2026-07-29",notes:"Default OFF (shadow). Detection runs in shadow always unless explicitly =0 \u2014 the PIN_PAGE_GROUNDING / INTERACTION_CLEARS_PRESENCE_ORACLE shadow-first precedent \u2014 because a shadow that skipped the settle wait would measure a DIFFERENT detector than the one enforcement ships, and its numbers would not predict the ON behavior. The one deliberate non-identity in shadow is therefore TIMING, not tool-result bytes: a click that has mutated nothing yet pays up to 500ms of settle before the state capture that follows it (an effective click returns on the first in-page read and pays one evaluate round-trip). Stated plainly because it is a real, if small, behavior delta in the DEFAULT state \u2014 the post-click screenshot/snapshot of a no-effect click is taken up to 500ms later than before, which is more settled, not less faithful. AGENTIQA_CLICK_EFFECT_SIGNAL=0 removes even that and restores exact timing parity. killSwitch resolves the no-env value from this defaultState (#1729), so GRADUATING = flip defaultState to 'on' AND add CLICK_EFFECT_SIGNAL to INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Read sites: packages/engine-core/src/BasePlaywrightService.ts (probeClickEffect / resolveClickEffect on clickAt, clickByRef, clickByLabel), packages/engine-core/src/waitToolCoaching.ts (waitNotFoundError picks the coaching variant), packages/engine-core/src/tools/browserTools.ts (getFailureHandlingPrompt), packages/engine-core/src/RunnerRuntime.ts (buildRunnerPrompt re-observe line); pure decision logic in packages/engine-core/src/clickEffectSignal.ts. Acceptance tests: clickEffectSignal.test.ts (pure verdict matrix incl. every suppression and the unknown-probe degrade), BasePlaywrightService.clickEffectSignal.test.ts (per-path wiring + shadow/enforce/off + the canvas-memo skip + degraded-note preservation on the ref path), clickEffectPromptParity.test.ts (flag-OFF prompt BYTE-parity across all four prompt surfaces). The 500ms settle cost is bounded on canvas-dominant surfaces by the SessionState.lastCaptureCanvasDominant memo: captureState records canvasDominant on every capture and probeClickEffect returns null (emitting click_effect:probe_skipped, reason canvas-memo) while it is set, because the canvas verdict was suppressed anyway. It is bounded a second way by the OBSERVER check: in shadow, where the diag is the only output, probeClickEffect returns null before the settle window when this.diagLog is unwired \u2014 an unobservable measurement is pure latency. Enforce is exempt (its verdict is agent-visible behavior). Acceptance: the shadow/enforce no-observer pair in BasePlaywrightService.clickEffectSignal.test.ts.",graduation:{status:"gated",gate:'SOAK SOURCE \u2014 read this first. click_effect:* rides BasePlaywrightService.diagLog, which was wired ONLY in DesktopPlaywrightService until the CloudPlaywrightService sink resolver landed (2026-07-30), so before that commit desktop runs were the ONLY source and a census of 0 from a cloud/staging replay lane is NON-EVIDENCE (the events were structurally unemittable there, not absent). Any soak reading must therefore be taken from engine builds that carry the cloud diagLog wiring; older cloud replays cannot be counted, and in shadow an unwired lane now skips the probe outright so it contributes no fires by construction. Staging shadow soak on click_effect:would_signal establishes (a) the no-effect rate per click path, (b) that the fires are dominated by genuinely ineffective clicks rather than by the known effect-free-but-legitimate classes, and (c) that no page class fires it continuously. FOUR NAMED REQUIREMENTS, all of which must be answered before the flag moves past shadow. (1) BLOCKER \u2014 THE NON-IDEMPOTENT SUBMIT. `<button>` and `<a>` are deliberately NOT in the property-only suppression set, so a form submit whose only feedback is a server round-trip (no spinner, no optimistic DOM write) reads as no-effect and, under enforce, receives the "re-perform the action ONCE" advisory. That directly contradicts the failure-handling prompt already shipped in browserTools.getFailureHandlingPrompt, which names "status: ok with url unchanged and no visible DOM change" as a SIGN OF AN IN-FLIGHT WRITE and instructs "do NOT re-click ... Re-clicking the same button while a write is in flight is a no-op for the user and burns retry budget" \u2014 and on a non-idempotent endpoint a second submit is not merely wasted, it can double-charge, double-book or double-post. The soak must show ZERO enforce-mode advisories on non-idempotent submits (classify fires by targetTag/targetRole plus the accessible name against a submit-shaped vocabulary, and cross-check each against pendingRequests at the moment of the fire), OR enforce must first gain a submit-shaped suppression (e.g. withhold the advisory whenever a same-origin request was in flight at probe time, or whenever the target is a submit-shaped control) \u2014 either outcome, and NEITHER may be waived. (2) IFRAME BLIND SPOT. Every counter read (peek / waitForMutation / flush) goes through page.evaluate, which runs in the MAIN frame only, and a top-document MutationObserver does not cross an iframe boundary \u2014 so a click whose whole effect renders inside an embedded frame (payment element, third-party booking/chat widget, embedded editor preview) reads as no-effect however well it worked. There is no target shape to suppress on, so iframe-hosted effects are a KNOWN-LEGITIMATE fire class the soak must be able to account for and subtract, exactly like property-only controls, downloads, clipboard and focus-only clicks; it is enumerated in the clickEffectSignal.ts header for the same reason. (3) SHADOW IS NOT INERT \u2014 READ THE SOAK ACCORDINGLY. The probe delays the post-click captureState by up to the full 500ms settle window on any click that has mutated nothing yet, INCLUDING in the default shadow state. Captured screenshots/snapshots on those clicks are therefore of a MORE SETTLED page than pre-change, so a shadow-vs-baseline comparison that shows different captured content on no-effect clicks is expected and is not evidence of a detector defect; only the =0 state is timing-identical to pre-change. (4) HYDRATION EMPIRICAL CRITERION. The soak must answer "what would run_b72f5099-cee2-4063-b109-1961c55c9898 have produced?" \u2014 i.e. for clicks landing inside a post-navigation hydration window, what fraction show mutationCount > 0 or attrCount > 0 (some other script mutated the page, so the detector stays SILENT and would not have rescued the incident) versus both counters at 0 (the detector fires and the advisory would have reached the agent). Both counters are already in the click_effect payload, so the query is: filter click_effect:would_signal to fires within ~2s of a navigation, then bucket on (mutationCount > 0 || attrCount > 0). A silent-dominated result means this feature does not fix its own motivating incident and enforcement is not justified on that basis. Enforcement additionally needs an inversion showing the advisory does not induce a harmful second click on a toggle.',evidence:"staging click_effect:would_signal diag events FROM AN ENGINE BUILD THAT CARRIES THE CLOUD diagLog WIRING (rate + clickPath/suppressReason/targetTag/targetRole breakdown, plus the mutationCount/attrCount split inside the post-navigation hydration window and the submit-shaped-target cross-check against in-flight same-origin requests) + click_effect:probe_skipped counts for the canvas-memo skips + the engine-core clickEffectSignal / BasePlaywrightService.clickEffectSignal / clickEffectPromptParity unit suites + apps/execution-engine/__tests__/CloudPlaywrightService.diagLog.test.ts for the cloud emit path itself",owner:"steering (Alex)",review:"2026-08-08"}},HALT_CONCLUDING_TURN:{key:"HALT_CONCLUDING_TURN",envVars:["AGENTIQA_HALT_CONCLUDING_TURN","AGENTIQA_EXPERIMENT_HALT_CONCLUDING_TURN"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"ONE final, tool-restricted turn offered to the runner when the MACHINERY (loop detector / vision supervisor / run backstop) halts a saved-plan run that still has status=running AND the deterministic unresponsive-control window holds \u2014 i.e. exactly the runs that end today with terminationReason=supervisor_halted and NO run_complete, hence no verdict at all, narrowed to the halt shape the change was measured against. SCOPE GATE: the turn is offered only when hasUnresponsiveControlEvidence() is true (a dominant repeated click target, noObservedEffect on EVERY one of those clicks, a non-canvas surface, no committed 2xx/3xx write \u2014 a model-free predicate that consults no LLM output). Every OTHER machinery halt keeps its current behavior: no extra turn, no extra LLM call, status=blocked + supervisor_halted. The gate is checked BEFORE the single-use latch, so a halt without that evidence does not consume the run's one attempt, and a `concluding_turn:not_offered_no_evidence` diag records each suppression so the gate's live fire rate is measurable. The turn is offered run_complete ONLY (the _editOnly idiom, RUNNER_TOOL_SET_CONCLUDE_ONLY), so it cannot act on the browser, cannot navigate, and cannot extend the loop: it is a single generateText call with no iteration, and it is offered at most once per run (_concludingTurnUsed). Its directive is NEUTRAL \u2014 it names no finding and no failure shape, asks for each step to be graded only on observed evidence, and tells the model to mark an unsubstantiable step 'skipped' rather than assert a verdict; an earlier revision spelled the dead-control conclusion out, which asked the model to ratify what the gate already believed. The halted path also performs NO cross-run writes: maybeCaptureProjectProfile and maybeMigratePlanFromCapture are skipped while _concludingTurnActive is true, because both end at isRunAuthenticated \u2014 routinely TRUE on a dead-CTA halt, where the login worked and the CTA did not \u2014 and a halted run must not persist its browser state or rewrite the customer's saved plan. Motivation, measured 2026-08-04 \u2014 READ THE PROVENANCE BEFORE QUOTING THE NUMBER (corrected 2026-08-06). It is run r1 of an UNCOMMITTED model-reliability harness (e2e/benchmark/model-reliability/, never merged to staging): its own execution-engine, fixtures on the deployed self-testing app, engine = staging @ fb89690cf, all 46 verification gates forced off as AGENTIQA_EXPERIMENT_<FLAG>=0 (PLAN_OBEDIENCE and CLICK_EFFECT_SIGNAL among them) and LLM_CACHE=0 \u2014 NOT the runner eval lane, which was not run that day. In it the two DEAD-CTA fixtures (a button that is present, whose click reports success, and which does nothing) produced 12/12 runs ending supervisor_halted/loop_block with ZERO run_complete, ZERO report_issue and ZERO agent-declared blocks. Those 12 are google:gemini-3-flash-preview \xD75 per fixture plus google:gemini-3.5-flash \xD71 per fixture: the second arm is n=1 per fixture and there was no Anthropic arm. THE CLASS IS MODEL-DEPENDENT, so 12/12 is not a property of the defect \u2014 a rerun the same day on the same code and fixtures (r2, 2026-08-04T20:11Z) had gemini-3.6-flash booking `failed` on 9 of 10 dead-CTA runs; only r1 reached this text. The halt is not the agent refusing to grade; it is the machinery taking the turn away before a verdict can be booked. MEASUREMENT STATUS: the post-merge L3 on the real lane (runner/seeded-broken, 2026-08-06 @ bdbfa8e30, 12 branch + 6 control, incumbent model) fired this gate 0/12 \u2014 GRADUATE-NO. Every eligible halt was suppressed by the scope gate (concluding_turn:not_offered_no_evidence) because the fixture's semantic `button` classifies as app-defect / non-interactive-target rather than custom-control-unresponsive, and its click has a real DOM effect so no noObservedEffect is ever sampled. The mechanism is unreachable on the very corpus that motivated it; see docs/VERDICT-GATES.md and PR #2246. GUARD RAILS, all pinned by tests: (a) a status=passed booked from this turn is REJECTED and the run falls back to today's blocked terminal unchanged \u2014 the agent must never be able to declare success out of a halt, and the halt itself is evidence against the claim; (b) the turn is NOT offered when blockKind==='agent' (the agent already concluded via exploration_blocked \u2014 nothing to second-guess); (c) exactly one turn, one LLM call, no tool loop, so a runaway loop is extended by that bounded turn and nothing more; (d) anything unusable (no run_complete, a throw, a stop mid-flight) falls back to the unchanged blocked terminal; (e) the scope gate above \u2014 a machinery halt with no unresponsive-control evidence gets no turn at all, in any blockKind. WHAT OFF (=0) DOES AND DOES NOT COVER \u2014 read this before using it as a kill switch. OFF removes the TURN: no extra turn, no extra LLM call, no extra token spend, and no run can be moved from blocked to failed by this mechanism. OFF does NOT change the terminal VOCABULARY: the `control_unresponsive` reason written by the blocked-terminal branch in startRun is UNCONDITIONAL and not gated by this flag, because it comes from `hasUnresponsiveControlEvidence` \u2014 a deterministic, model-free predicate that consults no LLM output \u2014 and it exists to stop blaming the agent for a dead control in the customer's own app, which is a separate improvement from giving a halted run a verdict. So with the flag OFF a halted run still terminates `control_unresponsive` rather than `supervisor_halted` whenever that evidence holds; to suppress the vocabulary too, turn off its input (`AGENTIQA_CLICK_EFFECT_SIGNAL=0` captures no `noObservedEffect`, so the predicate returns false and the terminal falls back to `supervisor_halted`). docs/VERDICT-GATES.md states the same split.",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-08-04",notes:"Default ON. Polarity is the reason: this gate can only move a run from blocked (NO verdict) to failed (a verdict the model booked), never toward a pass \u2014 the passed arm is refused in code, so there is no path by which it manufactures a false pass, which is the asymmetry docs/VERDICT-GATES.md guards hardest. Its worst case is a false FAIL on a run that today produces no verdict at all, and 'blocked' was never a pass either. Billing is NEUTRAL, and the ORDER is what makes it so: `handleRunComplete` reads `_concludingTurnActive` and stamps the non-billable reason (control_unresponsive when the deterministic unresponsive-control window holds, else supervisor_halted) BEFORE the first persist \u2014 because that upsert IS the billing trigger. It PUTs the entity-sync route, which fires commitRunUsage on any terminal + billable row, and `committed_at` is written in exactly one place in apps/web-next and only ever to NOW(); there is no un-commit path, so a later discardRunUsage no-ops with `already_committed`. An earlier revision corrected the reason with a second upsert AFTER handleRunComplete had already persisted 'completed'; that shipped the money before the correction and was replaced by this single-mechanism ordering (PR #2246 review finding 1). The same ordering is what makes a throwing upsert safe: the surviving in-memory reason is already non-billable, so startRun's finally cannot finalize a charge (finding 2). Read site: packages/engine-core/src/RunnerRuntime.ts#attemptConcludingRunComplete (called from startRun, immediately before the blocked terminal write) plus the `_concludingTurnActive` branch in #handleRunComplete. The turn's own LLM call is metered as auxiliary spend via emitAuxiliaryLlmUsage on every exit path (booked, declined, passed-refused) \u2014 it is a full-trace prompt plus up to 8192 output tokens, so leaving it unmetered hid the feature's entire cost (findings 3+4). Scope, directive neutrality and blast radius were narrowed in review: the turn fires only on the deterministic unresponsive-control halt shape (so every other machinery halt is untouched), its prompt names no finding, and the halted path skips the two cross-run writers handleRunComplete otherwise performs. Acceptance tests: RunnerRuntime.concludingTurn.test.ts (offer conditions incl. the scope gate across every blockKind, the neutral directive, the no-cross-run-writes pins, the passed refusal, the single-turn bound, the restricted tool set, flag-off parity), RunnerRuntime.controlUnresponsiveTerminal.test.ts (the terminal vocabulary + the browser_unobservable-style regression pins), RunnerRuntime.concludingTurnBilling.test.ts (the first-persist reason and the throwing-upsert path), RunnerRuntime.concludingTurnMetering.test.ts (the aux emit on every exit) and RunStatusBanner.controlUnresponsive.test.ts (the renderer copy + the booked-verdict line). MEASUREMENT STATUS 2026-08-06: post-merge L3 on runner/seeded-broken @ bdbfa8e30 (12 branch + 6 control) gave rescue 0/12 \u2014 GRADUATE-NO, with every eligible halt suppressed by the scope gate; the concluding_turn:not_offered_no_evidence diag remains the source of the live fire rate and is the number to read before revisiting this default."},CONCLUDING_TURN_ORACLE_FAIL_SCOPE:{key:"CONCLUDING_TURN_ORACLE_FAIL_SCOPE",envVars:["AGENTIQA_CONCLUDING_TURN_ORACLE_FAIL_SCOPE","AGENTIQA_EXPERIMENT_CONCLUDING_TURN_ORACLE_FAIL_SCOPE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"SECOND scope arm on the post-halt concluding turn (a CHILD of HALT_CONCLUDING_TURN \u2014 see that entry first). The turn is offered when hasControlUnresponsiveEvidence() OR hasUnresolvedVerifyOracleFailure(); this flag gates ONLY the second disjunct. The new arm matches when the run holds a verify step whose latest WAIT oracle (wait_for_element) failed and was never superseded by a successful re-verification \u2014 the wait-oracle subset of the Decision-6b ledger (accessor unresolvedWaitOracleFailureStepsForConcludingTurn(), over the provenance-blind base unresolvedOracleFailureStepsAnyProvenance()), discriminated per step through the existing conflictOracleMix() so it cannot drift from that classifier. PIN-GROUNDING entries do NOT count: the second writer of that ledger (checkPinPageGrounding / pin_page_grounding) is a plan-derived value check evaluated against whatever snapshot was captured, not an observation the run itself made and failed, and counting it would widen the arm past what was measured. Model-free in a stronger sense than the sibling arm: every ledger entry is written by the runtime from a dispatched tool RESULT and is keyed by VERIFY STEP INDEX, so the predicate reads no _currentStepIndex (that key is model-supplied via signal_step and non-monotonic) and nothing the model writes can put an entry there. WHY IT EXISTS: the parent's only arm was measured GRADUATE-NO on 2026-08-06 (L3 lane, runner/seeded-broken @ bdbfa8e30, 12 branch + 6 control) \u2014 0/12 rescues, every eligible halt suppressed by the scope gate, because a semantic <button> classifies app-defect / non-interactive-target rather than custom-control-unresponsive and noObservedEffect is sampled only on a dead-point click while that fixture's click has a real DOM effect. The class that actually reaches the halt seam verdict-less on that corpus is this one: step-4's wait oracle has already failed and been recorded, then the supervisor emits BLOCK (a free keyword choice \u2014 19/19 outcome correlation with BLOCK vs WRAP_UP on otherwise identical trajectories) and the run terminates blocked/supervisor_halted with no verdict at all. POLARITY, inherited and unchanged: the arm can only move a run from blocked (NO verdict) to failed (a verdict the model booked) \u2014 a `passed` from the turn is refused in code and NOT rewritten to failed, so no configuration of this flag can invent a pass. TERMINAL VOCABULARY IS UNTOUCHED: a turn opened by this arm leaves hasControlUnresponsiveEvidence() false, so handleRunComplete stamps the pre-existing non-billable `supervisor_halted` \u2014 never `control_unresponsive`, whose user-visible copy would then claim a dead control nobody measured. No TERMINATION_REASONS change, no new banner copy. LADDER: AGENTIQA_HALT_CONCLUDING_TURN=0 kills the whole turn regardless of this flag; this flag =0 restores the parent's control-unresponsive-only scope \u2014 same offer decisions, same terminals, no rollback record written (so the ledger is pre-change too), and the concluding_turn:not_offered_no_evidence payload drops back to its pre-change keys (oracleFailLedgerSize is emitted only while the arm is enabled). HARD DEPENDENCY ON VERIFY_GATED_DONE (default-on everywhere, and NOT gated by this flag): the oracle-failure ledger is written only under that switch (the recording branch is `gateOn`-guarded) and the base accessor unresolvedOracleFailureStepsAnyProvenance() hard-returns [] when it is off, so with AGENTIQA_VERIFY_GATED_DONE=0 this arm is SILENTLY INERT \u2014 it can neither fire nor be measured. Any census that pinned VERIFY_GATED_DONE to 0 says nothing about this mechanism (the 2026-08-04 model-reliability harness did exactly that with all 46 gates, which is one reason its 12/12 was not evidence about this seam). SHIPPED IN THE SAME COMMIT AND GATED BY THIS FLAG TOO (amended 2026-08-07): the rollback-safe recording fix. An auto-advanced verify wait (maybeAdvanceToVerifyForWait) that TIMES OUT rolls _currentStepIndex back before recordVerifyEvidenceFromToolResult runs, whose isVerifyStep(_currentStepIndex) pre-gate then dropped the record \u2014 measured on the L3 halt corpus as only 3 of 6 halted runs recording their own failed verification. The failure is now written at the rollback site, keyed to the verify step being rolled back FROM, into the same ledger with the same clearing semantics (diag verify_gate:oracle_failure_recorded carries via:'dispatch'|'rollback'), honoring every abstain the dispatch path honors (retained-frame abstain, canvas-dominant capture basis) and never clobbering an existing ledger entry \u2014 except an existing ARM-ONLY entry, which a later pin-grounding failure UPGRADES so the pin miss stays verdict-visible. THAT RECORD IS ARM-ONLY EVIDENCE: stamped via:'rollback' and read by THIS ARM ONLY. Every live-verdict consumer of the ledger filters it out \u2014 the run_complete verification-conflict cascade (verifyStepsWithUnresolvedOracleFailureForLiveVerdict()), applyRetainedFrameAbstain's supersede check (hasLiveVerdictOracleFailure()) and the VERIFY_REOBSERVE_WITHHOLD pass (liveVerdictOracleFailureDetail()). WHY: fed to the live cascade the record created a NEW FALSE-FAIL class, reproduced A/B on the real seam \u2014 a wait the runtime provisionally bound to a verify step the model had NOT signalled times out (slow render, or a matcher miss while the literal was on the page), the model then honestly grades that step passed from its own later observation, and run_complete bounces once then force-fails the resubmit: passed/completed on the base tree vs failed/blocked_by_issue with the record fed in, plus a synthesized 'Verification step not satisfied' issue. No rescue valve catches that class: canReconcileVerificationConflict (and the agent-literal abstain rungs) refuse a PLAN-GROUNDED wait literal, and an auto-advanced wait's literal is plan-quoted BY CONSTRUCTION because verifyWaitTargetMatchesOracle is what licensed the bind; the M1 snapshot reconcile VERIFY_CONFLICT_RECONCILE_SNAPSHOT is code-default OFF (hand-set =1 only on the GKE orchestrators) and is not a mitigation. Because the record has exactly ONE reader, it lives under THIS switch rather than being an independently-shipped half: =0 writes nothing at all, so the flag reverts BOTH halves of the change (offer decision AND ledger). VERIFY_GATED_DONE stays a hard dependency on top of it. OBSERVABILITY: concluding_turn:offered {scope: 'control_unresponsive'|'oracle_fail', oracleFailSteps} on every offer (control_unresponsive wins the label when both arms hold \u2014 it is the arm that also picks the terminal), and the suppression diag concluding_turn:not_offered_no_evidence gains oracleFailLedgerSize (the RAW ledger size, which separates 'nothing failed' from 'only the pin oracle failed' from 'VERIFY_GATED_DONE is off').",designDoc:"docs/plans/2026-08-06-concluding-turn-oracle-fail-scope-design.md",status:"active",added:"2026-08-07",notes:"Default ON, for the parent's reason: the reachable verdict set of this mechanism is {failed} \u2014 the passed arm is refused in code \u2014 so its worst case is a false FAIL on a run that today produces NO verdict at all, and `blocked` was never a pass either. It adds ONE bounded generateText call to a halt that already ended without a verdict, metered as auxiliary spend on every exit path (emitAuxiliaryLlmUsage), and the run stays non-billable. Read site: packages/engine-core/src/RunnerRuntime.ts#attemptConcludingRunComplete (the scope gate, before the single-use latch \u2014 a gated-out halt still does not burn the run's one attempt); predicate: #hasUnresolvedVerifyOracleFailure \u2192 #unresolvedWaitOracleFailureStepsForConcludingTurn \u2192 #unresolvedOracleFailureStepsAnyProvenance + #conflictOracleMix. The rollback recording gated with it lives at #rollbackVerifyWaitAdvanceIfFailed \u2192 #recordRolledBackVerifyWaitOracleFailure \u2192 #writeVerifyOracleFailure (the single writer both seams share, which persists the `via` provenance). The live-verdict side of the accessor split \u2014 the sites this arm's evidence must NOT reach \u2014 is #verifyStepsWithUnresolvedOracleFailureForLiveVerdict / #hasLiveVerdictOracleFailure / #liveVerdictOracleFailureDetail, all three over #isArmOnlyOracleFailure. Acceptance tests: RunnerRuntime.concludingTurnOracleFailScope.test.ts (the arm across every machinery blockKind in both directions, blockKind='agent' never offered, latch-not-burned, child=0 scope parity incl. the diag payload, parent=0 dominance, pin-grounding-only ledger does not match, VERIFY_GATED_DONE=0 inertness, the booked-verdict terminal/billing/cross-run-write/passed-refusal properties on the new arm), RunnerRuntime.verifyWaitRollbackOracleRecord.test.ts (the rollback record and its key, the accessor split, the clearing semantics, no double-record when the model signal_step'ed the verify step itself, the abstain carve-outs incl. this flag =0, non-clobber) and RunnerRuntime.oracleFailArmOnlyEvidence.test.ts (the A/B-reproduced false-FAIL class through the real handleToolCall/run_complete seams, the dispatch-record parity pin that proves the filter weakened nothing, the halted run that still gets the turn on arm-only evidence, the retained-frame warning cap, and child=0 full parity). GRADUATION IS NOT UNIT-TESTABLE: the parent shipped green units and measured 0/12 in prod-shaped replay, which is the whole reason this child exists \u2014 the gate is the L3 lane replay in docs/plans/2026-08-06-concluding-turn-oracle-fail-scope-design.md criterion 8 (12 branch replicates vs 6 with this flag =0), and `concluding_turn:offered {scope:'oracle_fail'}` plus the halted-run verdict split are the numbers to read.",graduation:{status:"gated",gate:'L3 lane replay on runner/seeded-broken (e2e/evals/runner/run-lane.ts, 12 replicates branch vs 6 replicates with AGENTIQA_CONCLUDING_TURN_ORACLE_FAIL_SCOPE=0 as control) against a STAGING build carrying this arm. Targets: every supervisor-halted run books a verdict through the concluding turn (the recording fix is expected to close the measured 3/6 ledger gap), ZERO booked `passed`, the control arm reproduces the current halt-vs-booked split, `concluding_turn:offered {scope:"oracle_fail"}` present on every rescue, and the stage-15 completeness-guard bounce rate on the turn reported rather than assumed. A green unit suite is explicitly NOT the gate: the parent had one and fired 0/12 in the lane.',evidence:"staging concluding_turn:offered / concluding_turn:not_offered_no_evidence (with oracleFailLedgerSize) / concluding_turn:verdict_booked|verdict_not_booked diag events from the lane replay, plus verify_gate:oracle_failure_recorded {via} counts for the rollback-recording share, and the engine-core concludingTurn / concludingTurnOracleFailScope / verifyWaitRollbackOracleRecord / concludingTurnBilling / concludingTurnMetering suites",owner:"steering (Alex)",review:"2026-08-14"}},RUNNER_UNOBSERVABLE_CONCLUDING_GRADE:{key:"RUNNER_UNOBSERVABLE_CONCLUDING_GRADE",envVars:["AGENTIQA_RUNNER_UNOBSERVABLE_CONCLUDING_GRADE","AGENTIQA_EXPERIMENT_RUNNER_UNOBSERVABLE_CONCLUDING_GRADE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"THIRD scope arm on the post-halt concluding turn (a CHILD of HALT_CONCLUDING_TURN \u2014 read that entry first): the `browser_unobservable` terminal in RunnerRuntime.startRun's catch routes through ONE bounded conclude-only grading turn BEFORE the synthetic all-skipped fallback. THE CLASS IT CLOSES (issue #2605; prod lio runner suites, sessions asess_1786679659847_bpczs2mx and asess_1786850942202_sm2dus65, 2026-08-14/16): a busy customer page starved page.screenshot 3 consecutive times, BrowserUnobservableError killed a run in which 21+ steps had demonstrably executed with passing verify evidence, and synthesizeBlockedVerdictIfMissing then wrote EVERY step {status:'skipped', synthesized:true} \u2014 grading only happens in handleRunComplete, so at terminal time there were no per-step grades to preserve and the synthesis destroyed the run's own proof. With this arm ON the terminal offers the model the same neutral, run_complete-only turn the supervisor-halt arms use (same single-use latch, same conclude-only tool set, same auxiliary metering, same passed-refusal) so executed steps get graded from the retained trace/evidence \u2014 the catch trims the dangling tool call the mid-dispatch throw leaves in the trace first, without which the provider rejects the turn's messages and every live run silently falls back to the synthesized shape. A booked verdict is then FORCED (the `browserUnobservableConcluding` clamp in handleRunComplete, keyed on `_concludingTerminalReason`) to run status 'blocked' + terminationReason 'browser_unobservable' at the terminal write \u2014 the model grades STEPS, it does not choose the run-level outcome of a run OUR capture pipeline killed. FIREWALL ASYMMETRY, stronger than either sibling arm: the reachable run-level status set of this arm is exactly {blocked} (the sibling arms can at least book 'failed'), so no configuration of it can manufacture a pass OR a fail at run level \u2014 it can only move step rows from synthesized-skipped to graded-with-evidence. AND AT STEP LEVEL since the #2626 review: applyUnobservableStopPointFloor runs LAST in handleRunComplete's cascade and clamps a 'failed' grade on the ONE step the capture died on down to 'skipped' (the only target that survives deriveStepStatusIgnoringAuthorship's short-circuit \u2014 a 'warning' row carrying the model's own strict-failed criterion is re-derived straight back to 'failed' by every reader), while a 'passed' grade on that step STANDS. The asymmetry is the point: retained pre-death frames are legitimate positive evidence that a step did what it said, and are not legitimate evidence of a failure to complete. Earlier steps, graded from frames that actually existed, are never touched. NOT GATED BY THIS FLAG (deterministic, model-free, additive \u2014 the ticket's items 2 and 3): the blockedReason naming the capture failure + the page URL on the run_complete card (built from buildBlockedSummary over the catch's URL-carrying `detail`), and the auto-filed unobservable/page-hang issue + report_issue card (synthesizeBrowserUnobservableIssue); those land on BOTH the booked and the fallback paths in every flag state. =0 restores the pre-change terminal byte-for-byte on the TURN (no extra LLM call, all-skipped synthesis \u2014 now with the unconditional blockedReason/detail/issue additions above). LADDER: AGENTIQA_HALT_CONCLUDING_TURN=0 kills the whole concluding-turn mechanism including this arm; this flag =0 kills only this arm (checked at the unobservable catch, so a =0 run never consults the other arms' predicates there and never burns the single-use latch).",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-08-19",notes:"Default ON with kill switch, per the family's precedent and for a strictly stronger reason than HALT_CONCLUDING_TURN's: that entry justifies default-ON because its reachable verdict set is {failed} (the passed arm is refused in code, so the worst case is a false FAIL on a run that had NO verdict). This arm's reachable RUN-LEVEL set is {blocked} \u2014 the terminal write in handleRunComplete stamps status='blocked' + terminationReason='browser_unobservable' whenever the turn was opened by this arm, regardless of what the model booked \u2014 so it cannot produce a false FAIL either; only step rows move, and only from synthesized-skipped toward model-graded statuses backed by the retained trace. The #2626 review made that claim true at STEP level too, where it previously was not: the concluding model CAN grade the in-flight step 'failed' (the eval lane's own live baseline recorded #4 {status:failed} on the busy-finale fixture) \u2014 a red row for a step nothing could observe \u2014 so applyUnobservableStopPointFloor now clamps exactly that one row to 'skipped' and composes a stop-point note explaining the withheld verdict onto whatever the model wrote. (Shadow-first default-OFF is the family's shape for RED\u2192AMBER verdict-SOFTENING valves like the VERIFY_CONFLICT_* lanes, which can convert a detection into a green step; nothing here softens a red \u2014 a blocked evidence-destroyed run was never a pass or a fail.) Billing: the run stays non-billable and the ORDER is inherited from the parent \u2014 handleRunComplete stamps 'browser_unobservable' (absent from BILLABLE_TERMINATION_REASONS) BEFORE its first upsert, which is the billing trigger, so the money can never move on this terminal; the turn's own LLM call is metered as auxiliary spend via emitAuxiliaryLlmUsage on every exit path. Cross-run writes (maybeCaptureProjectProfile / maybeMigratePlanFromCapture) stay skipped under _concludingTurnActive exactly as on the sibling arms \u2014 a run whose browser could not be captured must not persist its browser profile. Read site: packages/engine-core/src/RunnerRuntime.ts \u2014 the unobservable catch in startRun (gates the trim + the attemptConcludingRunComplete call whose arm is keyed on blockKind='browser_unobservable'); the run-level clamp lives in handleRunComplete (`browserUnobservableConcluding`) next to the sibling arms' reason stamp, and the step-level one in applyUnobservableStopPointFloor immediately before it. Acceptance: RunnerRuntime.blockedSynthesisPreservesSteps.test.ts (the #2605 contract: real statuses preserved, blockedReason naming capture+URL, issue auto-filed, run stays blocked/browser_unobservable; plus the #2626 cases (e)-(n): stop-point note + reached markers on the graded record, the asymmetric failed\u2192skipped clamp, passed standing, the summary count read after the clamp, the open-issue fingerprint dedup, and the no-usable-identity fail-open) and the certifying eval e2e/evals/plans/runner-blocked-synthesis.ts (judgement 'correct' against the deterministic busy-finale fixture \u2014 re-measured live 2026-08-21 WITH the clamp: the model graded the stop-point step failed and the floor withheld it to 'skipped', so the step-level claim above is confirmed against a real model rather than only against mocks)."},BLOCKED_SYNTHESIS_PRESERVE_GRADES:{key:"BLOCKED_SYNTHESIS_PRESERVE_GRADES",envVars:["AGENTIQA_BLOCKED_SYNTHESIS_PRESERVE_GRADES","AGENTIQA_EXPERIMENT_BLOCKED_SYNTHESIS_PRESERVE_GRADES"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"FOURTH scope arm on the post-halt concluding turn (a CHILD of HALT_CONCLUDING_TURN \u2014 read that entry first). ENFORCING BY DEFAULT (GRADUATED 2026-09-11) \u2014 this whole entry describes the LIVE path. With NO env the arm is armed: a halted run that executed at least one step and holds no verdict is offered its one bounded conclude-only turn whatever halted it, the `blockKind === 'agent'` early return is lifted, and the run-level clamp forces the terminal the halt would otherwise have written. An explicit `=1` is still accepted and resolves identically. `=0` (either spelling) IS THE KILL SWITCH and is now the ONLY non-enforcing state: it restores the pre-change offer decisions and terminals AND switches the detection off, so no marker row of any name is emitted. THE SHADOW STATE IS GONE, DELIBERATELY, AND ASYMMETRICALLY \u2014 state it precisely rather than as a slogan. MARKER 1 (`blocked_synthesis:would_preserve`) SURVIVES the flip: it is emitted from startRun's `finally` over the FINAL record and reads the terminal, not the offer, so its `kept` / `wiped` / `concludingTurnScope` fields are exactly the post-flip watchdog and its `enforcing` field still separates a `=0` control from a live run. Its `would_` name is now a misnomer and is KEPT on purpose so archived shadow-era artifacts and the live stream stay one queryable population. MARKER 2 (`concluding_turn:would_offer_executed_steps`) is what CLOSES: its `agent_block` and `no_scope_arm` buckets are structurally UNREACHABLE (use that word, not 'near-unreachable' \u2014 a future census author must not read the hedge as 'go look for residual rows'). `agent_block` suppression is lifted by this flag and `no_scope_arm` now requires `_maxStepEverExecuted === 0`, which the emitter itself filters out, so the only reachable `suppressedBy` left is `latch_spent` \u2014 which the cost metric excludes by design. CONSEQUENCE, AND THE REASON THE COST HAD TO BE PRICED BEFORE THE FLIP: the cost census bottoms out at zero by construction once this is on, so it can never be re-run after the fact. POST-FLIP WATCHDOG \u2014 census the LIVE names, not the `would_*` ones: `concluding_turn:offered {scope:'executed_steps'}` (the arm firing), `blocked:graded_by_concluding_turn` (the agent-block branch booking through it), `concluding_turn:passed_refused` (the firewall being exercised \u2014 a non-zero count is not a regression, a `passed` that gets THROUGH is), `blocked_synthesis:would_preserve {kept, wiped}` (a `kept < maxStepExecuted \u2212 1` under enforcement is the regression shape), and any `failed` step row BELOW the stop point. A regression is answered by setting `AGENTIQA_EXPERIMENT_BLOCKED_SYNTHESIS_PRESERVE_GRADES=0` on the orchestrator FIRST and fixing after, updating docs/FLAGS-STATE.md in the same session per its Rule 1. THE DEFECT (#3072, three independent terminals): grades are CREATED only in handleRunComplete \u2014 every `stepResults.push` in RunnerRuntime is inside that method's gate cascade, and both synthesizers bail on a non-empty record \u2014 so on a halt with no verdict there is nothing for a synthesizer to preserve. Whether a halted run keeps its grades is therefore decided ENTIRELY by whether the bounded conclude-only turn was OFFERED, and the offer was gated on WHICH defect the run happened to hit: `hasControlUnresponsiveEvidence()` OR an unresolved verify-oracle failure OR `blockKind === 'browser_unobservable'`, with `blockKind === 'agent'` returning silently before any of them. Measured consequences on one plan across two nights: a supervisor BLOCK carrying an oracle failure kept 7 of 25; the SAME plan's supervisor BLOCK with an empty ledger (staging run_1566360e, 82 iterations) kept 0 of 24; a prod `loop_block` (asess_1788926139197_wdukkeb2, iteration 88) kept 0 of 16; the same plan's `browser_unobservable` terminal the same night kept 24 of 25. The terminal is not the variable \u2014 the offer predicate is. THE ARM: a halted run that executed at least one step (`_maxStepEverExecuted > 0`) and holds no verdict is offered its one bounded turn regardless of halt reason or ledger emptiness, and the `blockKind === 'agent'` early return becomes conditional on this same flag. THAT RETURN IS NOT THE ONLY GATE ON THE AGENT-BLOCK TERMINAL, and the first revision of this flag shipped believing it was (review round 1, finding F0, measured live 4/4): `exploration_blocked` is terminated by handleBlocked() INSIDE the tool handler, which writes run.status='blocked' + blocked_by_issue and emits run:completed during the loop, so by the time startRun calls attemptConcludingRunComplete its `run.status !== 'running'` invariant has already fired. `=1` and unset were byte-identical there and marker 2's `agent_block` bucket was structurally EMPTY, which made the graduation cost metric uncomputable on the terminal #3072 reports most often. handleBlocked now calls the same seam while the run is still 'running', passing its own composed summary as blockedReason so the clamp's base string and the fallback write's come from ONE expression; when the turn books, handleRunComplete owns endedAt/the upsert/run:completed and the fallback block is skipped, so the event fires exactly once on either arm. That path also closes this terminal's stage-19c coverage gap \u2014 `cause_grounding:not_reached` is now emitted only when the run really did end without run_complete. `_maxStepEverExecuted` is the monotonic, model-free EXECUTION mark raised only where a page-affecting action is dispatched \u2014 the SAME licence `synthesizePartialStepResults` already uses to write `reached: true` \u2014 so the arm claims nothing the floor does not already claim. RUN-LEVEL FIREWALL, the reason this is shippable at all: every turn opened by THIS arm sets `_concludingTerminalReason` to the terminal the run would otherwise have got (`blocked_by_issue` for an agent block, `supervisor_halted` otherwise) and the generalized `concludingTerminalClamp` in handleRunComplete then forces `run.status = 'blocked'` plus that reason, so the reachable RUN-LEVEL status set of this arm is exactly {blocked} \u2014 the #2605 firewall asymmetry, inherited verbatim. No configuration of this flag can invent a pass OR a fail at run level; only step rows move, from synthesized-`skipped` toward model-graded-with-evidence. The clamp is keyed on `_concludingTerminalReason !== null`, which is null on the control-unresponsive and oracle-fail arms and can only be `browser_unobservable` under this flag's `=0` kill switch, so all FIVE clamped writes (the stop-point floor, `run.status`, `run.summary`, `run.terminationReason` and the `run_complete` card's own `blockedReason`) and the 19c cause-grounding input are byte-identical to pre-change BY CONSTRUCTION rather than by a flag read. WHAT THE ARM CAN STILL GET WRONG, stated because the run-level firewall does not cover it: a model grading from a truncated trace can book `failed` on a step BELOW the stop point, where today's record says `skipped`. `applyConcludingStopPointFloor` clamps a `failed` only on the ONE stop-point row (#2626 contract, unchanged), so the arm CAN mint red rows on a run that has none today. THE MIRROR RESIDUAL, decided in review round 1 (F2): that same clamp WITHDRAWS a model `failed` on the stop-point row with no evidence test, and on the newly-clamped terminals the browser was healthy and the finding may be true \u2014 4/4 live `=1` runs graded that step failed with a correct reason and the floor withheld it. It stays withheld deliberately: the stop-point step never finished, so a red row there cannot separate 'the app is broken' from 'we stopped it mid-step', and minting it is the false-fail family this wave closes elsewhere. Nothing regresses (that row was skipped+synthesized before) and the finding survives in the summary, the card's blockedReason and the agent's filed issue. GRADUATION MUST NOT READ `kept` AS 'the finding is now a red row': `kept` counts the steps BELOW the stop point only. That is the false-fail family #1907 / #3103 / #3114 are closing elsewhere, and it is why the graduation gate below bounds NEW `failed` rows against a paired `=0` control rather than only bounding `kept`. TERMINAL VOCABULARY AND FRAMING ARE UNTOUCHED: an agent block still terminates `blocked_by_issue` (its `exploration_blocked` card and auto-filed issue survive), a machinery halt still terminates `supervisor_halted`, and the base summary is the exact string the fallback blocked write would have produced \u2014 the model's own summary is appended behind `Retained grading summary:`, the same composition the `browser_unobservable` arm has shipped since #2605. `control_unresponsive` is never chosen by this arm (its own predicate is false whenever this arm is the one that fired, since the arms are ORed and checked first). No TERMINATION_REASONS change, no new banner copy. NOT IN SCOPE \u2014 `max_iterations` and `slow_site` keep wiping: they finalize through `synthesizeBlockedVerdictIfMissing` from `onLoopExhausted` / the startRun catch, never reach `attemptConcludingRunComplete`, and carry no stop point at all (`stopKind = null`). Marker 1 is emitted from `startRun`'s `finally` and therefore COVERS those runs in the census even though the arm cannot rescue them. LADDER: `AGENTIQA_HALT_CONCLUDING_TURN=0` kills the whole concluding-turn mechanism including this arm and every marker that depends on reaching its seam (marker 1 still emits \u2014 it reads the terminal record, not the offer). This flag `=0` restores the pre-change offer decisions and terminals AND silences both markers. The single-use latch is respected exactly as by the sibling arms: the widened arm now burns it on the FIRST halt, so a run with two halts gets a grading turn only on the first \u2014 pinned by test, not left emergent. OBSERVABILITY: `concluding_turn:offered {scope:'executed_steps'}` on a turn this arm opened; `blocked:graded_by_concluding_turn {runId, status, terminationReason, gradedSteps}` when the agent-block terminal books through it; `concluding_turn:would_offer_executed_steps {runId, blockKind, suppressedBy: 'agent_block'|'latch_spent'|'no_scope_arm', maxStepExecuted, currentStepIndex, controlUnresponsive, oracleFailLedgerSize, enforcing}` at the ONE suppression seam, emitted only AFTER every invariant of attemptConcludingRunComplete holds (a user-stopped or reaped run produces no row, so the cost census is not inflated by runs that could never be offered a turn); `blocked_synthesis:would_preserve {runId, terminal, terminationReason, blockKind, kept, wiped, stopStepIndex, maxStepExecuted, totalSteps, concludingTurnUsed, concludingTurnScope, enforcing}` once per ABNORMAL terminal (`blocked` / `error` only \u2014 `cancelled` is excluded because a user who pressed Stop lost no grades) from startRun's finally, over the FINAL record so it covers the graded, floored and AG-7981 catch-arm shapes alike.",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-09-10",graduation:{status:"gated",gate:"GRADUATED 2026-09-11 (default ON). KEPT AS `gated` RATHER THAN DELETED \u2014 per the CRITERIA_CONTAINMENT precedent, a graduated flag keeps this block when a named residual is still owed, and one is (below). WHAT THE FIVE-PART BAR ACTUALLY RETURNED, item by item, including the two items it could not satisfy as written. (1) COST CENSUS \u2014 NOT MET AS WRITTEN, AND NOW UNSATISFIABLE. The bar asked for >= 5 consecutive nights of the nightly qa-exhaustive runner lane plus one ag-rehearsal replay, counting `concluding_turn:would_offer_executed_steps` de-duplicated per runId over `suppressedBy IN {'agent_block','no_scope_arm'}`. The rehearsal half ran (2026-09-11 mirror pass 1, 17 fixed customer plans, flag hand-set `=1` on the staging orchestrator) and returned ZERO rows of that marker and ZERO of marker 1 \u2014 no run on that corpus reached a `blocked`/`error` terminal at all \u2014 so the flip's cost ON THE PILOT CORPUS is measured at zero extra bounded generateText calls. The 5-night nightly half was NOT run, and after this flip it cannot be: marker 2 is reachable only as `latch_spent` post-flip (see the entry's gates text), so the cost census bottoms out at zero by construction. The bar is therefore REWRITTEN to the price the deterministic lane could establish, WITH THE DENOMINATOR STATED AS MEASURED (the offer rate is per HALT, not per run \u2014 30/8 is runs-per-offer and prices the flip ~44% too cheap): on the fixture lane the arm opened `concluding_turn:offered {scope:'executed_steps'}` on 13 of the 25 forced-`=1` HALTS of the RE-RUN WITH INTACT RAW \u2014 a MEAN of ONE extra bounded generateText call PER 1.9 BLOCKED RUNS on fixtures BUILT to halt \u2014 and 0 times without the flag. Read the denominator as HALTS: per RUN it is 13/30, which is not the price of a halt. On the concluding-turn mechanism as a whole the flag roughly DOUBLES the turns taken on blocked runs (1.00 offers per halt under `=1`, 25 over 25 halts, against 0.54 in shadow, 13 over 24) and buys nothing on runs that do not halt \u2014 zero offers there. THIS SUPERSEDES the 2.6-halts figure computed from the earlier two-batch pass, which is retained only as corroboration. On 17 real customer plans the cost was zero because no run reached the terminal at all; on the 19-run combined pass the same day the corpus DID reach two `blocked` terminals and the arm still opened 0 times, so the measured mirror cost stays zero on a non-empty population. (2) LOSS CENSUS \u2014 NOT MET ON THE PILOT CORPUS AS WRITTEN, but NO LONGER AN EMPTY MEASUREMENT, and this is the freshest evidence in the entry: mirror pass 1 (06:57->08:01Z, 17 plans) returned 0 `blocked_synthesis:would_preserve` rows because no run reached a `blocked`/`error` terminal, but the COMBINED forced-ON window the SAME DAY on the same mirror and the same engine (12:29:09Z->13:56:51Z, 19 runs) reached 2 `blocked` terminals and booked 2 marker-1 rows, both `enforcing: true`, both `kept = 24 / wiped = 0` with 24 of 24 real grades below the stop point and `synthesized` 0 \u2014 AND a paired `=0` control on that same engine returned `kept = 24 / wiped = 0` on the same plan and terminal. SO THE LOSS CENSUS ON REAL CUSTOMER TRAFFIC IS NON-EMPTY AND ZERO-LOSS: the delta attributable to #3072 on the pilot corpus is not 'unmeasured', it is MEASURED AT EXACTLY ZERO, because the pre-existing `browser_unobservable` arm had already bought both terminals. The bar as written asked for `wiped > 0` on that corpus and did not get it; that is the corpus being healthy, not the instrument being blind. REWRITTEN to the deterministic lane, where it is met with room \u2014 numbers from the RE-RUN WITH INTACT RAW that item (3) rests on: 12 of the 24 shadow halts carry `wiped > 0`, INCLUDING 11 OF 11 on the target fixture (10 x `kept=0 wiped=4` plus 1 x `kept=0 wiped=3`) and one more on the sibling, across BOTH fixtures and BOTH blocked seams (`supervisor_halted` and `blocked_by_issue`), against 25 forced-`=1` halts of which 24 are `kept=3 wiped=0` (the 25th is the declined-turn miss named in item (3), on a pre-flag arm). Every shadow `{kept > 0}` row is attributable via `concludingTurnScope` to the pre-existing `browser_unobservable` arm (12 of the 24 shadow halts), so no kept grade is being credited to this arm that a sibling already bought. (3) FORCED `=1` LANE \u2014 MET on a RE-RUN WITH INTACT RAW, with three amendments on the record. N: 25 blocked runs under `=1` (bar >= 20) out of 30, 11 on runner/blocked-synthesis-supervisor-block and 14 on runner/hang-busy-finale-static, against 24 halts in the paired shadow arm. THE GRADUATION RESTS ON THIS RE-RUN, and the earlier two-batch pass is retained as CORROBORATION ONLY. The re-run was executed 2026-09-11 at `origin/staging` a79a61d5f (`dist` rebuilt and verified by inspection to carry BOTH #3072 and #3148), 60 runs in ONE batch with both arms running in parallel so each got the same load floor, and every artifact written straight to ~/agentiqa-agents/lio-harvest/work/gate3-3072-rerun/ OUTSIDE the managed scratchpad: 78 MB of raw diag, 60 ndjson + 60 run.json, so EVERY row below is re-derivable from raw events. WHY IT EXISTS: the earlier pass's 21 halts spanned TWO batches and batch 1's raw diag was DESTROYED MID-RUN by an unrelated parallel session wiping the managed scratchpad, which left 13 of those 21 rows rehydrated from a verbatim 40/40 census extract that can no longer be re-derived (the extract survives next to the report, and batch 2's intact raw reproduced every conclusion at N = 8). Nothing in this graduation now depends on a rehydrated row. WHAT THE RE-RUN SHOWS: 24 of the 25 `=1` halts are `kept=3 wiped=0`; the populations are EQUAL on the target fixture at 11/15 in BOTH arms, where the earlier pass was lopsided at 8/15 vs 5/15; the paired shadow arm wiped on 12 of its 24 halts INCLUDING 11 OF 11 on the target fixture, on both blocked seams; this flag's own arm fired 13 times (`concluding_turn:offered {scope:'executed_steps'}`) and on BOTH fixtures this time, against 0 in shadow, and the agent-block F0 branch (`blocked:graded_by_concluding_turn`) booked 4 times on two fixtures against 0; the arming leaked in neither direction (25/25 `enforcing: true` against 25/25 `false` on 60 runs); and every safety counter is ZERO across all 60 runs \u2014 no NEW `failed` row below the stop point, no run-level `passed` booked by the turn, no `concluding_turn:passed_refused`, no `concluding_turn:failed`. On every run this flag's arm touched, the terminal kind is `blocked` and nothing else. THE ONE MISS, named rather than averaged away: runner/hang-busy-finale-static r15 under `=1` is `kept=0 wiped=4`, and it is NOT this arm \u2014 the turn was opened with `scope: 'browser_unobservable'`, the #2605 arm this flag does not gate at all, and THE MODEL DECLINED THE TURN, returning 1378 characters of prose instead of calling run_complete, so no grades were created and synthesis filled all four rows. `kept`/`wiped` MEASURES the final record rather than driving it, so a declined turn loses the grades in every flag state; on this flag's OWN arm the record is 13 of 13 with no exception. THAT DECLINE IS A NAMED, NON-BLOCKING RESIDUAL, recorded because no prior census had produced one: a concluding turn CAN be declined and on a decline the grades are lost entirely \u2014 pre-flag behaviour on a pre-flag arm, measured at 1 per 25 halts, not counted against this flip and not covered by this gate's safety counters. It is owed its own tracking item rather than a footnote here. AMENDMENT A: the bar named THREE cases, one of which \u2014 runner/blocked-synthesis-loop-block \u2014 DOES NOT EXIST. It was deleted in this feature's own review round 1 (F3) after `loop_block` occurred 0 times in 9 halted runs, i.e. it sampled the same population for +83% lane budget. The bar is two cases, and the third was never a measurable thing to ask for. AMENDMENT B: 'ZERO runs whose run-level status is `passed`' is met in the sense its own parenthetical defines \u2014 zero escapes past the passed-refusal and the clamp: on the re-run `concluding_turn:verdict_booked` is 24 x `blocked` and 0 x `passed` (the 25th halt is the declined turn, which booked nothing), `concluding_turn:passed_refused` is 0 and `concluding_turn:failed` (F0b) is 0 \u2014 but NOT as a literal run-level count: runner/hang-busy-finale-static produced 1/15 run-level `passed` under `=1` against 0/15 in shadow, and that run carries NO `concluding_turn:*` row at all, i.e. the flag provably did not execute on it. It is the documented false-pass class of that fixture (the model claims the finale banner on a permanently hung renderer), which the earlier two-batch pass observed SYMMETRICALLY at 2/15 in BOTH arms; on the re-run the residue is one run in the `=1` direction and is reported as such rather than rounded away. It is why that case gates NO_FALSE_PASS_CLAIM at 1.0, and it is not attributable to this flag. AMENDMENT C: 'a `terminationReason` histogram IDENTICAL to the paired `=0` control' is UNSATISFIABLE BY CONSTRUCTION on a stochastic lane and should never have been written that way. This flag is read only at a seam where the halt is ALREADY decided (`attemptConcludingRunComplete` runs after `result.blocked`), so it can neither cause nor prevent a halt; halt rates differ by model variance alone (8/15 vs 5/15 on the target fixture). REWRITTEN to the property the item was reaching for \u2014 and ON THE RE-RUN THE NAIVE FORM OF THAT PROPERTY FAILS, which is stated here rather than quietly replaced by the form that passes. NAIVE FORM, on RUN-LEVEL STATUS (`blocked` / `failed` / `passed`): the set of terminal KINDS under `=1` is NOT a subset of the control's on runner/hang-busy-finale-static, because `passed` appears under `=1` (1/15) and not in shadow. That is the amendment-B false-pass run, and it carries no `concluding_turn:*` row, i.e. the flag did not execute on it. THE MEANINGFUL FORM \u2014 restricted to the population the turn was actually OFFERED \u2014 holds on both fixtures: there the terminal kind is `blocked` and ONLY `blocked` in both arms (supervisor-block 11 under `=1` against 0 in shadow, where this arm does not exist; hang-busy 14 against 13), so the flag created NO new kind on a single run it touched, and the `passed`/`failed` spread lives exclusively where the flag provably did not execute. THE FINER AXIS HOLDS TOO: no `terminationReason` appears under `=1` that is absent from the paired control (supervisor-block `{supervisor_halted, blocked_by_issue, completed}` against the same three; hang-busy `{browser_unobservable, blocked_by_issue, completed}` against the control's same three plus one harness-INVALID row). What is NOT claimed on either axis is the bar's literal word, an IDENTICAL histogram \u2014 the COUNTS differ (hang-busy halts 14/15 vs 13/15; on the target fixture the re-run's populations are in fact EQUAL at 11/15 vs 11/15, which the earlier pass's 8/15 vs 5/15 was not) and cannot be guaranteed to match on a stochastic lane. (4) NO NEW RED, AND NO LOST GRADE \u2014 MET on the re-run, with one named exception that is not this arm's. `kept > 0` on 24/25 `=1` halts = 96% (bar: >= 80%), each of those 24 exactly `kept=3` of `executed=4`, i.e. `executed - 1`; on THIS FLAG'S OWN arm (`scope:'executed_steps'`) it is 13 of 13 with no exception. The single exception is runner/hang-busy-finale-static r15, `kept=0 wiped=4`, where the pre-flag `browser_unobservable` arm opened the turn and the MODEL DECLINED it \u2014 detailed in item (3). It also costs the bar's second clause its literal form: 'NO run where `kept` is lower under `=1` than in the paired control' holds at 24 of the 25 replicate indices and FAILS at exactly r15, whose shadow twin kept `3/0`. Say it that way rather than claim the clause outright \u2014 the divergence is a declined turn on an arm this flag does not gate, not a grade this flag lost. NEW `failed` step rows BELOW the stop point: 0 across all 60 runs on BOTH arms. (5) TRACE PARITY \u2014 MET, RE-KEYED, because the flip makes the original wording false: 'with the flag UNSET ... byte-identical to the pre-change tree' cannot hold once unset IS the enforcing state. The pin is re-keyed in RunnerRuntime.blockedSynthesisCensus.test.ts to the post-flip triad \u2014 `=0` is byte-identical to the pre-change tree (zero marker rows, zero generateText, the pre-change floor rows on every blockKind), no-env is identical to `=1` seam for seam, and no env spelling can reach a suppression row on an offered run. THE RESIDUAL THIS BLOCK IS KEPT FOR \u2014 UNCHANGED, but the REASON for it is re-keyed to the combined pass, because the old reason ('items (1) and (2) returned structurally empty') is now stale: THE ARM HAS NEVER BEEN OBSERVED TO FIRE ON A REAL CUSTOMER PLAN. The population is no longer absent \u2014 the combined pass produced it twice \u2014 the ARM is: every positive measurement of the arm firing comes from two deterministic fixtures built to halt. The arm's false-positive rate on real customer traffic is therefore UNMEASURED, not measured-zero. AND THE STATED RETIREMENT CONDITION MAY BE UNSATISFIABLE ON THIS CORPUS BY CONSTRUCTION \u2014 say so rather than let a future reader wait on it: this arm sits LAST in the ORed scope-arm chain by design ('the fallback that fires only when no proven arm did'), and on the pilot mirror a proven arm always wins first (`oracle_fail` in pass 1, `browser_unobservable` twice in the combined window). THREE consecutive mirror passes \u2014 19 + 17 + 19 runs \u2014 and the arm has fired ZERO times. That is a structural property of the corpus (the Lio mirror does not produce halts with no proven arm), not an n problem, so closing this residual needs EITHER a second corpus whose halts lack a proven arm OR an explicit acknowledgement that the pilot mirror cannot produce the `scope:'executed_steps'` population at all. See the combined report's section 3.3. The instrument that closes it is the post-flip watchdog in the gates text above \u2014 `concluding_turn:offered {scope:'executed_steps'}`, `blocked:graded_by_concluding_turn` and `blocked_synthesis:would_preserve {kept, wiped}` on the nightly qa-exhaustive runner lane and on the next mirror rehearsal. Second, smaller residual, already named in the entry: a model grading from a truncated trace CAN mint a `failed` row BELOW the stop point (the stop-point floor clamps only the one in-flight row) \u2014 measured 0 across 60 runs, structurally still possible. RETIRE THIS BLOCK when the watchdog has read a non-zero `scope:'executed_steps'` population on real customer runs with no `kept` regression and no below-stop-point red \u2014 SUBJECT TO THE CORPUS CAVEAT ABOVE: if no corpus that produces halts without a proven arm becomes available by the review date, retire it instead on an explicit steering acknowledgement that the pilot mirror structurally cannot produce that population, rather than leaving it open forever on a condition that cannot be met.",evidence:"PRIMARY (the graduation): ~/agentiqa-agents/lio-harvest/reports/rehearsal-2026-09-11-preserve-fixtures.md, section `Registry gate (3) - re-run with intact raw (2026-09-11)` \u2014 THE RUN THIS GRADUATION RESTS ON. 60 runs in ONE batch (2 cases x 15 replicates x 2 flag states, arms in parallel) at `origin/staging` a79a61d5f with `dist` rebuilt and verified to carry both #3072 and #3148: 25 forced-`=1` halts against a bar of >= 20, 24 of them `kept=3 wiped=0` and the 25th a DECLINED turn on the pre-flag `browser_unobservable` arm (see gate item (3)); 13 firings of this flag's own arm across BOTH fixtures against 0 in shadow; 12 paired shadow halts with `wiped > 0`, including 11 of 11 on the target fixture; zeros on every safety counter over all 60 runs; no arming leak (25/25 `enforcing: true` against 25/25 `false`). THE RAW IS INTACT AND OUTSIDE THE MANAGED SCRATCHPAD \u2014 78 MB, 60 ndjson + 60 run.json, at ~/agentiqa-agents/lio-harvest/work/gate3-3072-rerun/ \u2014 so every number here is re-derivable from raw events; cost from that same raw is one extra bounded generateText call per 1.9 blocked runs. CORROBORATION, retained but NOT relied on: the same report's earlier 24-run 4-case lane and its 60-run `Registry gate (3) - N>=20` pass (21 forced-`=1` halts all `kept=3 wiped=0`, 6 paired shadow losses all `kept=0 wiped=4`), which carry the provenance caveat the report discloses (section 12, anomaly 5) \u2014 batch 1's raw diag was destroyed mid-run by an unrelated parallel session, its verbatim 40/40 census extract survives next to the report, and batch 2's intact raw reproduced every conclusion at N = 8. Nothing in this graduation now depends on a rehydrated row. SECONDARY, THE FRESHEST MEASUREMENT ON REAL CUSTOMER TRAFFIC: ~/agentiqa-agents/lio-harvest/reports/rehearsal-2026-09-11-combined.md sections 3.2 / 3.3 / 7 \u2014 the 12:29->13:56Z combined forced-ON window, 19 runs, 14 passed / 3 failed / 2 blocked, the FIRST pass on which this flag's population reproduced: 2 `blocked` terminals, 2 marker-1 rows both `enforcing: true` and both `kept=24 wiped=0`, against a paired `=0` control on the same engine and plan also `kept=24 wiped=0` \u2014 delta EXACTLY ZERO, measured rather than absent, and the arm still did not open because `browser_unobservable` took both halts first. Its own verdict is 'no regressions; GO does not follow from the mirror', and it is cited on exactly those terms. ALSO SECONDARY, and an EMPTY measurement rather than a silent one: ~/agentiqa-agents/lio-harvest/reports/rehearsal-2026-09-11-preserve-p1.md \u2014 the staging-mirror forced-ON pass over 17 fixed customer plans, 13 passed / 4 failed / 0 blocked, in which this arm fired ZERO times (the single concluding turn in the batch was opened by the pre-existing `oracle_fail` arm and books the identical verdict with the flag off), 0 regressions against baseline. That pass is a NO-GO on its own terms and is cited as such: it is what establishes the residual above, not what graduates the flag. LIVE STREAM: `blocked_synthesis:would_preserve` (survives the flip) and `concluding_turn:offered {scope:'executed_steps'}` from the nightly qa-exhaustive runner lane. UNIT ACCEPTANCE: packages/engine-core/src/__tests__/RunnerRuntime.blockedSynthesisCensus.test.ts (the re-keyed post-flip parity triad) and RunnerRuntime.blockedSynthesisEnforce.test.ts (the arm, the run-level clamp on every blockKind, the passed refusal, the latch, the mark-0 refusal and the stop-point floor); packages/engine-core/src/__tests__/killSwitchDefaultState.test.ts carries the graduation in INTENTIONALLY_GRADUATED.",owner:"steering (Alex)",review:"2026-09-25"}},REVISION_CRITERIA_CARRYOVER:{key:"REVISION_CRITERIA_CARRYOVER",envVars:["AGENTIQA_REVISION_CRITERIA_CARRYOVER","AGENTIQA_EXPERIMENT_REVISION_CRITERIA_CARRYOVER"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Backstop that carries prior-draft criteria forward when a plan revision strips ALL criteria across ALL steps; off lets a total criteria-strip through.",designDoc:"packages/engine-core/src/revisionCriteriaCarryOver.ts",status:"active",added:"2026-07-07"},CREDENTIAL_GUARD:{key:"CREDENTIAL_GUARD",envVars:["AGENTIQA_CREDENTIAL_GUARD","AGENTIQA_EXPERIMENT_CREDENTIAL_GUARD"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Credential-fabrication guard: injects the login-prohibition prompt wording and fires the false-premise report_issue gate; off omits both (pre-feature behavior).",designDoc:"packages/engine-core/src/testingEmailPolicy.ts",status:"active",added:"2026-07-06"},CREDENTIAL_BINDING:{key:"CREDENTIAL_BINDING",envVars:["AGENTIQA_CREDENTIAL_BINDING","AGENTIQA_EXPERIMENT_CREDENTIAL_BINDING"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Runner deterministic credential-step binding (v1.1): when a plan step unambiguously references exactly one non-generic stored credential AND the model's own type_project_credential_at pick was a generic field word, the mismatched credentialName is overridden to the referenced one (credential_binding_override diag) \u2014 a non-generic pick is never rewritten; and when a credential fill is followed within a 3-action adjacency window by a 401/403 while the step-referenced credential is untried and was not the last one filled, ONE report_issue/exploration_blocked per run is deflected toward that specific credential. A genuine 401 of the step's own credential, an unrelated stray 401, or a step naming no credential all proceed to the report. Off restores the model's free credential pick and no auth-failure nudge (pre-feature behavior).",designDoc:"docs/plans/2026-07-19-ag7727-run-fidelity-fixes-design.md",status:"active",added:"2026-07-19"},EMAIL_CODE_PROVENANCE:{key:"EMAIL_CODE_PROVENANCE",envVars:["AGENTIQA_EMAIL_CODE_PROVENANCE","AGENTIQA_EXPERIMENT_EMAIL_CODE_PROVENANCE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Verification-code write provenance gate: a write the model TYPED as fieldPurpose='verification_code' is refused unless its value equals a whole code token in an email actually fetched via check_email this run (subject + text + html-as-text, token-equality not raw substring). Off passes the write through ungrounded (pre-feature behavior), so a guessed or fabricated code can mutate the page into a false success state.",designDoc:"packages/engine-core/src/emailVerificationGate.ts",status:"active",added:"2026-07-12"},EMAIL_REPLY_COMPLETION:{key:"EMAIL_REPLY_COMPLETION",envVars:["AGENTIQA_EMAIL_REPLY_COMPLETION","AGENTIQA_EXPERIMENT_EMAIL_REPLY_COMPLETION"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Required-email-reply completion gate on the Explorer's assistant_v2_report: when the Coordinator typed the task with the `email_send` capability AND this Explorer wrote the canonical testing email into the app, an ok/done report is refused (status email_reply_incomplete) until the runtime holds a server-confirmed outbound reply. The correction branches three ways \u2014 reply still owed (check_email/reply_to_email/verify), flow already completed WITHOUT an outbound reply (report complete, restate the in-product evidence), or a required reply that could not be delivered (blocked) \u2014 so a defensively-stamped email_send never downgrades a genuinely complete area. Bounded at 2 rejections per turn, then fails open: the report is accepted with a hedged undelivered-reply disclosure appended to the model's summary. Sits BELOW the email-wait conversion, so a run whose inbox is still empty reaches the user-decidable Stop/Wait pause instead, and exempts structured child-protocol summaries (login_ok:/signup_failed_*:) from both the rejection and the disclosure. Off restores pre-feature behavior: a request-submitted-only run reports clean with no rejection and no disclosure.",designDoc:"packages/engine-core/src/emailReplyHonesty.ts",status:"active",added:"2026-08-20"},VERBATIM_INPUT_PIN:{key:"VERBATIM_INPUT_PIN",envVars:["AGENTIQA_VERBATIM_INPUT_PIN","AGENTIQA_EXPERIMENT_VERBATIM_INPUT_PIN"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Verbatim user-input payload pinning: committed text writes carry a source-action id, field identity, and model-authored purpose role; an exact match to a configured login-email identity is forced to credential_identifier from typed project state. On a source-bound run, a text-entry step's verbatimInput is kept ONLY when its exact source action resolves uniquely and has inputRole=user_supplied_payload; duplicate, credential/generated/other, missing, conflicting, and ambiguous bindings are omitted. The value must also provenance-match user chat/attachment text. Legacy source-less producers retain the corpus-only gate. RunnerRuntime types the accepted pin verbatim (slot wins over prose). Off restores the prior write-tool contract (inputRole optional, legacy descriptions) and disables capture/attach plus runner consumption.",designDoc:"docs/plans/2026-07-13-verbatim-input-payload-design.md",status:"active",added:"2026-07-13"},VERIFY_ORACLE_FIDELITY:{key:"VERIFY_ORACLE_FIDELITY",envVars:["AGENTIQA_VERIFY_ORACLE_FIDELITY","AGENTIQA_EXPERIMENT_VERIFY_ORACLE_FIDELITY"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Verify-oracle fidelity (verbatim-input Phase 2): a deterministic post-generation pass at both producer gates (CoordinatorRuntime.handleSaveTestPlan + the Explorer draft path), after pinVerbatimInputSteps, that restores user-authored acceptance criteria the LLM summarized away. It captures verification check-bullets under a high-precision verification-heading line (r2: heading ends with ':' and its core matches a verification PHRASE whole, not merely contains a strong token; each bullet captured unless action-imperative-shaped) plus a bounded single-paragraph anti-false-pass lookback, tests each against the persisted verify-side content (verify-step texts + criteria checks + expectedValue pins) by literal-atom containment (quoted spans + numerals/ranges with EN/RU 0\u201320 spelled-form equivalence; atom-free \u2192 normalized-token containment \u2265 0.6), and APPENDS a verify step for every uncovered item carrying the user's bullet verbatim as both the step text AND a compiled criterion ({check, strict:true}, r2/F7 \u2014 so the appended step passes validateDraftPlanSteps and never hits the runner's criteria-less synthesis path). Append-only (never edits/deletes an existing step), fail-open (any error \u2192 save unchanged). Off restores pure LLM-compliance generation (summarized oracles persist as-is). Emits an oracle_fidelity diag {bullets, covered, appended, lookbackCaptured, looseBullets, disabled} regardless of flag state (looseBullets = diag-only recall telemetry, never appends).",designDoc:"docs/plans/2026-07-15-verify-oracle-fidelity-design.md",status:"active",added:"2026-07-15"},TYPE_NEWLINE_SAFE:{key:"TYPE_NEWLINE_SAFE",envVars:["AGENTIQA_TYPE_NEWLINE_SAFE","AGENTIQA_EXPERIMENT_TYPE_NEWLINE_SAFE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:'Multi-line text typing: embedded newlines are entered as Shift+Enter soft line breaks (never a raw Enter keypress) so a multi-line prompt does not premature-submit an Enter-to-send composer (e.g. Miro/Slack chat sidekicks). Off restores the raw keyboard.type(text) behavior where each "\\n" fires an Enter keypress. Applies to all four text-typing paths (typeTextAt / typeByRef / typeByLabel / setFocusedInputValue fallback); pressEnter still appends one trailing Enter regardless.',designDoc:"packages/engine-core/src/typeMultiline.ts",status:"active",added:"2026-07-13"},CANVAS_TYPE_GROUNDING:{key:"CANVAS_TYPE_GROUNDING",envVars:["AGENTIQA_CANVAS_TYPE_GROUNDING","AGENTIQA_EXPERIMENT_CANVAS_TYPE_GROUNDING"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Canvas typed-text grounding: after a coordinate type_text_at on a canvas-dominant screen, verify direct canvas writes and press-Enter commits from transient DOM editors. Both start with an a11y/DOM positive check on the reused post-action snapshot, else a pixel route gated by an ambient-animation pre-check (two reads a settle apart); a self-animating board yields uncertain_animated \u2014 byte-identical to flag-off, recorded via a canvas_type_verify diag event. DIRECT mode (focus not a DOM editor, no Enter) diffs the full viewport against the 2nd pre-capture: any change = landed (off-clip renders included), byte-identical = confident negative \u2192 explicit failure (metadata.error + canvasTypeVerification='text_not_found') with a switch-strategy hint. COMMIT mode (#2615) arms on a coordinate double-click whose focused editor sits DIRECTLY over a canvas, holds both elements as live handles (instance identity, not a DOM fingerprint), and takes its pre-edit baseline from that canvas's BACKING STORE (toDataURL digest) \u2014 which excludes every DOM overlay by construction, so nothing on the page is mutated and no element is hidden to take the reading. The next press-Enter type on the same editor instance re-reads the same canvas: changed = landed; identical = canvasTypeVerification='commit_unconfirmed', a hedged metadata.note (never metadata.error) telling the model the value may not have persisted OR may equal what the cell already showed, and to read the board before treating the edit as done or filing a bug. Abstains (fail-open) on a tainted canvas, an empty drawing buffer (WebGL without preserveDrawingBuffer), a detached canvas, a lost arm, or a read that exceeds its 2s runtime-side budget. Each backing-store read downscales the board onto a detached scratch canvas capped at 262144 pixels before encoding, so the SUT main-thread cost is independent of board size (a dpr-2 full-viewport board would otherwise PNG-encode multiple MB three times per gesture); the trade-off is that a sub-scale-factor change can be averaged away, which in commit mode reads as the hedged note and never as a failure. \u22643 verification reads per qualifying action (screenshots in direct mode, canvas reads in commit mode). Off restores the pre-feature bare-success canvas type: no arming probe, no verification reads, no a11y check.",designDoc:"docs/plans/2026-07-14-canvas-chat-failure-class-design.md",status:"active",added:"2026-07-14"},RESULT_FIRST_DISCOVERY:{key:"RESULT_FIRST_DISCOVERY",envVars:["AGENTIQA_EXPERIMENT_RESULT_FIRST_DISCOVERY"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"env-const-disabled",gates:'Result-first discovery (#373): auto-approve discovered scope and run high-risk areas immediately; off restores the "question-first" scope-approval checkpoint that waits.',designDoc:"packages/engine-core/src/resultFirstDiscovery.ts",status:"active",added:"2026-06-01",notes:"Single spelling: reads ONLY AGENTIQA_EXPERIMENT_RESULT_FIRST_DISCOVERY (no bare AGENTIQA_ spelling). NOT migrated to killSwitchDisabled \u2014 that would add the bare spelling and change behavior. Also has a per-session config opt-out."},SCOPE_PROVENANCE:{key:"SCOPE_PROVENANCE",envVars:["AGENTIQA_EXPERIMENT_SCOPE_PROVENANCE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"env-const-disabled",gates:"Scope-provenance-aware plan selection: user-enumerated medium/low areas still run in the first pass; off falls back to the legacy risk-only split.",designDoc:"packages/engine-core/src/resultFirstDiscovery.ts",status:"active",added:"2026-07-01",notes:"Single spelling: reads ONLY AGENTIQA_EXPERIMENT_SCOPE_PROVENANCE. NOT migrated to killSwitch (would add a bare spelling)."},MEMORY_WRITEBACK:{key:"MEMORY_WRITEBACK",envVars:["AGENTIQA_EXPERIMENT_MEMORY_WRITEBACK"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"env-const-enabled",gates:"Verified-memory writeback: offers the save_verified_memory tool and activates the durable write path; off leaves writeback inert.",designDoc:"docs/plans/2026-07-04-verified-memory-writeback-design.md",status:"active",added:"2026-07-04",notes:"Single spelling: reads ONLY AGENTIQA_EXPERIMENT_MEMORY_WRITEBACK. NOT migrated to killSwitchEnabled (would add a bare spelling). Also has a per-session config force-on for evals.",graduation:{status:"gated",gate:"Model-elicitation quality gate from the 2026-07-04 verified-memory-writeback design doc closes",evidence:"design-doc gate + eval force-on runs",owner:"steering (Alex)",review:"2026-08-01"}},FAST_START_PROMPT:{key:"FAST_START_PROMPT",envVars:["AGENTIQA_EXPERIMENT_FAST_START_PROMPT"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"is-experiment-enabled",gates:"Explorer prompt variant: emits the fast-start initial prompt instead of the standard one.",designDoc:"packages/engine-core/src/ExplorerRuntime.ts",status:"active",added:"2026-06-01",notes:'Single spelling via isExperimentEnabled (exact "1"). NOT a killSwitch flag \u2014 no bare AGENTIQA_ spelling honored.',graduation:{status:"parked",gate:"No rollout intent; delete registry entry + code path if still unused by 2026-09-01"}},MINIMAL_INITIAL_CONTEXT:{key:"MINIMAL_INITIAL_CONTEXT",envVars:["AGENTIQA_EXPERIMENT_MINIMAL_INITIAL_CONTEXT"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"is-experiment-enabled",gates:"Explorer context variant: sends a minimal initial context to the model instead of the full one.",designDoc:"packages/engine-core/src/ExplorerRuntime.ts",status:"active",added:"2026-06-01",notes:'Single spelling via isExperimentEnabled (exact "1"). NOT a killSwitch flag \u2014 no bare AGENTIQA_ spelling honored.',graduation:{status:"parked",gate:"No rollout intent; delete registry entry + code path if still unused by 2026-09-01"}},STEERING_VETO:{key:"STEERING_VETO",envVars:["AGENTIQA_STEERING_VETO"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!1,read:"env-direct",gates:"Supervisor steering veto: at a redirect, refuses the recent loop-set of action fingerprints for a bounded TTL/budget; off passes those calls through.",designDoc:"packages/engine-core/src/steeringVeto.ts",status:"active",added:"2026-06-01",notes:'Reads ONLY the bare AGENTIQA_STEERING_VETO (via direct process.env access, "!== 0"). It has NO AGENTIQA_EXPERIMENT_ spelling, so the orchestrator passthrough cannot flip it in a cloud pod (cloud-unreachable \u2014 known limitation). NOT migrated to killSwitchDisabled: that would add the EXPERIMENT_ spelling and change behavior/reachability.'},AUTH_DISABLE_EMAIL_VERIFICATION:{key:"AUTH_DISABLE_EMAIL_VERIFICATION",envVars:["AUTH_DISABLE_EMAIL_VERIFICATION"],polarity:"1-enables",defaultState:"off",surfaces:["web-next-node"],cloudForwarded:!1,read:"env-direct",gates:"On-prem escape hatch: disables signup email verification; IGNORED (fail-safe) on a hosted Vercel deploy where VERCEL/VERCEL_ENV is present.",designDoc:"apps/web-next/lib/auth-flags.ts",status:"active",added:"2026-06-01",notes:"Not AGENTIQA_-prefixed. Static process.env member access (edge-safe). Hosted-platform sentinel forces it off on Vercel.",graduation:{status:"permanent",gate:"On-prem operational escape hatch, not an experiment \u2014 never graduates; forced off on hosted Vercel"}},RUN_DISPATCH_MUTEX:{key:"RUN_DISPATCH_MUTEX",envVars:["RUN_DISPATCH_MUTEX"],polarity:"1-enables",defaultState:"off",surfaces:["web-next-edge"],cloudForwarded:!1,read:"env-direct",gates:"Per-plan run-dispatch mutual exclusion (AG-8221): when on, a new run of a plan that already has a non-terminal run either supersedes a ZOMBIE predecessor (no activity for RUN_DISPATCH_ZOMBIE_MS, default 600s \u2014 a measured floor, see packages/shared-types/src/runDispatch.ts) or is rejected 409 plan_run_active behind a LIVE one. Off = SHADOW: the same scan and decision run and log run_dispatch_mutex:would_supersede/would_block, but every caller is told to proceed and nothing is written.",designDoc:"packages/shared-types/src/runDispatch.ts",status:"active",added:"2026-07-30",notes:"Not AGENTIQA_-prefixed. Static process.env member access via runDispatchMutexEnforced() in apps/web-next/lib/run-dispatch-mutex.ts (single read point, edge-safe). The engine only relays the decision \u2014 it reads no flag \u2014 so this one env var is the whole gate. Companion tuning knob RUN_DISPATCH_ZOMBIE_MS overrides the zombie threshold; it is a threshold, not a behavior gate, so it is deliberately not a registry entry.",graduation:{status:"gated",gate:"Shadow diags from the Lio daily bursts show would_block/would_supersede firing only on genuine overlaps, with zero would_supersede against a run that later produced a verdict (a superseded-live event is the dangerous direction and blocks graduation).",evidence:"run_dispatch_mutex:would_* lines from web-next logs, cross-checked against the superseded runs' final status in app.test_plan_runs.",owner:"Alex",review:"2026-08-13"}},ORG_ENTITLEMENT_ENABLED:{key:"ORG_ENTITLEMENT_ENABLED",envVars:["ORG_ENTITLEMENT_ENABLED"],polarity:"0-disables",defaultState:"on",surfaces:["web-next-node"],cloudForwarded:!1,read:"env-direct",gates:'Company-plan org entitlement (O0+): when on, an active OrgMembership resolves the billing subject to the org and its plan wins over the personal plan in all entitlement gates; "0" forces the personal subject everywhere (org rows become inert).',designDoc:"docs/plans/2026-07-11-company-plan-access-control-design.md",status:"active",added:"2026-07-11",notes:"Not AGENTIQA_-prefixed. Static process.env member access via isOrgEntitlementEnabled() in apps/web-next/lib/billing-subject.ts (single read point). Dark by data until an Organization row exists \u2014 with zero orgs the flag has no observable effect."},ORG_PROJECT_SHARING:{key:"ORG_PROJECT_SHARING",envVars:["ORG_PROJECT_SHARING"],polarity:"0-disables",defaultState:"on",surfaces:["web-next-edge","web-next-node"],cloudForwarded:!1,read:"env-direct",gates:"Org project sharing (Company plan): when on, `projectScopedGrantFor` lets a member of the OWNER's active Company-tier org read AND write the shared child entities of a project whose owner set `app.projects.org_shared = true` (test-plans, test-plan-runs, batch-runs, issues, labels, app-map), and widens the owner's own child queries from user-scoped to project-scoped so the owner sees what teammates ran and edited. \"0\" collapses every project grant to owner-only \u2014 reads re-narrow to the caller's own rows and writes re-gate to the owner \u2014 with no redeploy and no data change. NEVER gates credentials (plaintext secrets are excluded from the share by design), memory, journal, or Project Files.",designDoc:"docs/plans/2026-08-03-org-project-sharing-design.md",status:"active",added:"2026-08-03",notes:"Not AGENTIQA_-prefixed, so NOT cloud-forwarded: the orchestrator forwards only AGENTIQA_EXPERIMENT_* into engine session pods, and this flag is read exclusively in web-next (Vercel env), never in the engine \u2014 same shape as ORG_ENTITLEMENT_ENABLED. Static process.env member access via isOrgProjectSharingEnabled() in apps/web-next/lib/org-read-grant.ts (single read point; the sync-entity routes are edge functions, so the read must stay a static literal member access). Dark by data twice over: org_shared defaults false, and a true toggle still grants nobody unless the owner is an active member of an enterprise-tier org \u2014 so with zero shared projects the flag has no observable effect. ORG_ENTITLEMENT_ENABLED=0 also collapses this grant, because the Company-org half resolves through resolveBillingSubject."},EMAIL_REPLY_ENABLED:{key:"EMAIL_REPLY_ENABLED",envVars:["EMAIL_REPLY_ENABLED"],polarity:"0-disables",defaultState:"on",surfaces:["web-next-edge","web-next-node"],cloudForwarded:!1,read:"env-direct",gates:'Whether an agent may ANSWER a message delivered to its testing alias (`reply_to_email` \u2192 POST /api/email/reply). Sending is the only step of the email path that leaves our infrastructure, so it has a switch that needs no deploy: "0" turns every reply attempt into a clean 503 and makes the readiness probe report `canSend: false`, so sessions created afterwards do not even offer the tool \u2014 while reading the inbox (check_email) keeps working. Delivery itself is SMTP from the same mailbox the read path uses, under the same GMAIL_USER / GMAIL_APP_PASSWORD app password, `From` = the session alias. The tool can only answer a message that was actually delivered to that alias (the recipient is resolved server-side from the stored message, never from client input), so the switch is a blast-radius control, not the authorization boundary.',designDoc:"docs/qa-transparency/OPERATING-LOOP.md",status:"active",added:"2026-08-13",notes:'Not AGENTIQA_-prefixed, so NOT cloud-forwarded: read only in web-next (Vercel env), like ORG_ENTITLEMENT_ENABLED / EMAIL_ALIAS_STRICT. Read points: replyEnabled() in apps/web-next/lib/emailReply.ts (node) and the two readiness routes, one of which is EDGE \u2014 so the read must stay a static literal member access. Default ON because the surrounding gates already bound it: server-minted alias owned by the caller, a message that must actually exist in that alias inbox, a per-owner hourly attempt cap, and an email_send_log row per attempt. Claim: email.reply-flow. DEPLOYMENT REQUIREMENT (measured 2026-08-13): delivery must go through the Workspace SMTP RELAY, not plain smtp.gmail.com. Gmail rewrites `From` to the account address unless the sender is a confirmed "Send mail as" alias, and per-run aliases cannot be confirmed in advance \u2014 the reply then arrives from a-agent@ with the alias surviving only in Reply-To, with NO SMTP error to notice. The relay (Admin Console -> Apps -> Google Workspace -> Gmail -> Routing -> SMTP relay service; Allowed senders = Only addresses in my domains; Require SMTP Authentication; Require TLS) accepts any address in our domains and preserves the alias, verified end to end on staging. Selected by GMAIL_SMTP_HOST=smtp-relay.gmail.com / GMAIL_SMTP_PORT=587 / GMAIL_SMTP_SECURE=false, set in the custom staging env 2026-08-13; PRODUCTION STILL NEEDS THEM before this ships there, or prod silently reverts to the rewritten From. There is no API for the relay setting \u2014 GAM has no gmailrouting command and the Policy API exposes only 8 Gmail settings, none of them routing.'},EMAIL_ALIAS_STRICT:{key:"EMAIL_ALIAS_STRICT",envVars:["EMAIL_ALIAS_STRICT"],polarity:"1-enables",defaultState:"off",surfaces:["web-next-edge","web-next-node"],cloudForwarded:!1,read:"env-direct",gates:'Whether an UNSIGNED testing-inbox alias may still be read. One physical mailbox (GMAIL_USER) serves every tenant through plus-aliases, and until 2026-08-13 both fetch routes authorized on the alias SHAPE alone while the suffix was a base36 Date.now() \u2014 i.e. another tenant\'s signup codes and password-reset links were readable by anyone who guessed a millisecond. Server-minted aliases now carry an HMAC tag over their owner and are verified per read; a tag that verifies for somebody else, or a minted alias with no identity to check against, is refused REGARDLESS of this flag. This flag governs only the legacy shape (`<inbox-local>+<suffix>` with no tag), which packaged Desktop builds in the field still mint for themselves: default OFF serves it with a warning, "1" refuses it. Flipping it closes the enumeration hole for good and breaks every client that has not been updated \u2014 flip once the published Desktop manifest no longer self-mints.',designDoc:"docs/qa-transparency/OPERATING-LOOP.md",status:"active",added:"2026-08-13",notes:"Not AGENTIQA_-prefixed, so NOT cloud-forwarded: the orchestrator forwards only AGENTIQA_EXPERIMENT_* into engine session pods, and this is read exclusively in web-next (Vercel env) \u2014 same shape as ORG_ENTITLEMENT_ENABLED / PROJECT_RUN_LOCK. Single read point aliasStrictMode() in apps/web-next/lib/emailAlias.ts; the module is imported by an EDGE route (/api/email/readiness), so every env read must stay a static literal member access. Companion vars, both non-gating: EMAIL_ALIAS_SECRET (mints new tags; falls back to AUTH_SECRET / NEXTAUTH_SECRET, so no env rollout is needed) and EMAIL_ALIAS_SECRET_PREVIOUS (verification-only, so a secret rotation does not invalidate aliases already in flight). Claim: email.inbox-tenancy.",graduation:{status:"gated",gate:'Flip to "1" once no client in the field still self-mints an alias. Cloud sessions and the current Desktop/CLI build take the server-minted alias as of this change, so the remaining population is packaged Desktop builds released before it, plus any long-lived session started before the deploy. Concretely: the published manifest-latest.json version is at or above the first release carrying the server-minted alias, AND a week of readiness/fetch logs shows no `testing-inbox read on an unsigned alias` line. Until it flips, the enumeration hole stays open for exactly that legacy shape \u2014 this is a rollout window, not a permanent posture.',evidence:"desktop manifest-latest.json version \u2265 the first build that reads `alias` from /api/email/readiness; a 7-day window of web-next logs with zero unsigned-alias warnings; the two-user staging scenario (user B requests user A alias \u2192 403) that lights this claim at L2",owner:"steering (Alex)",review:"2026-09-01"}},PROJECT_RUN_LOCK:{key:"PROJECT_RUN_LOCK",envVars:["PROJECT_RUN_LOCK"],polarity:"1-enables",defaultState:"off",surfaces:["web-next-edge"],cloudForwarded:!1,read:"env-direct",gates:"Per-PROJECT run concurrency lock (P1.6): at most ONE live run OR batch per project at a time, across ALL identities \u2014 the owner, any member of the owner's Company org on a shared project, and ServiceKey CLI runs alike. Enforced at the two server seams that create the rows: the new-row path of `PUT /api/sync/entities/test-plan-runs/[id]` and of `PUT /api/sync/entities/batch-runs/[id]`. A refused start gets 409 `{code:'PROJECT_RUN_IN_PROGRESS', runId, kind, startedAt, startedByLabel}` naming who holds the project and since when \u2014 never 403 (a 403 on an entity write trips the SPA signOut trap, HC1) and never a 200 no-op. Liveness is GREATEST(run.updated_at, run_billing.last_step_at) for a single run and the freshest of the batch row and ALL its legs for a batch, with a 90-minute silence TTL; a holder past the TTL stops blocking and is opportunistically reaped to status='cancelled' (+ termination_reason='stale_lock_reap' on runs, which bills zero). NEVER blocks an update to an EXISTING row \u2014 only the creation of a new one \u2014 nor a leg of an already-admitted batch. The exemption is EXISTENCE, not status: until 2026-08-03 any PUT whose `status` was already terminal skipped the lock outright, which let a refused caller INSERT itself as a brand-new terminal row simply by finishing (measured web bypass) \u2014 so a first PUT that is already terminal is now admitted whenever nothing holds the project (the desktop late-sync case) and refused only while another run/batch of that project is live. \"0\" removes the lock entirely: both seams revert to an unguarded upsert with no reap and no 409. RETIRED 2026-08-04 \u2014 now default OFF, \"1\" restores it. Its rationale opened with \"the thing two concurrent runs collide on is the TARGET APPLICATION and the credentials used to reach it, and in this product both are properties of the PROJECT\": the credential half was never true (app.project_credentials has carried user_id since its first migration and the read path has always been WHERE user_id = caller), and the data half was declined by the customer it was built for (\"two accounts will operate on different data and test cases in 95-99% of cases \u2026 we're fine not handling that case\"). The credential POOL contends on the ACCOUNT \u2014 the resource that is actually shared \u2014 and hands a distinct one to each caller instead of refusing the second run. What made keeping it untenable rather than merely redundant: the lock is active ONLY on org-shared projects, exactly where the pool lives, so it gated the one scenario the feature exists for \u2014 and did so AFTER the caller had already leased an account, leaving that slot held until teardown. Nothing is deleted: the transaction, the reap and the 409 contract are the shape a per-account guard needs, and lib/credential-pool.ts already reuses them.",designDoc:"docs/plans/2026-08-03-org-project-sharing-design.md",status:"active",added:"2026-08-03",notes:"Not AGENTIQA_-prefixed, so NOT cloud-forwarded: the orchestrator forwards only AGENTIQA_EXPERIMENT_* into engine session pods, and this flag is read exclusively in web-next (Vercel env), never in the engine \u2014 same shape as ORG_PROJECT_SHARING. Static process.env member access via projectRunLockEnabled() in apps/web-next/lib/project-run-lock.ts (single read point; both seams are edge functions, so the read must stay a static literal member access). Scope is deliberately the PROJECT, not the plan and not the org: credentials and default_url are project-scoped, so two runs of DIFFERENT plans in one project drive the same account against the same environment \u2014 which the pre-existing per-plan mutex RUN_DISPATCH_MUTEX (AG-8221) does not cover \u2014 while an org-wide lock would serialize unrelated products that merely share a payer. The two locks are complementary and use deliberately different staleness thresholds because their safe directions are opposite (AG-8221 may KILL a predecessor, so it wants the smallest value above the observed silence distribution: 600s; this lock only REFUSES a newcomer, so it wants a value above the longest legitimate LIFETIME: the 90-minute sandbox ceiling, overridable via PROJECT_RUN_LOCK_TTL_MS). RETIRED 2026-08-04 (default OFF): with the parent off, PROJECT_RUN_LOCK_UNSHARED cannot revive it either \u2014 pinned by projectRunLockApplies returning false for BOTH shapes, so a stray child flag left in some environment is inert. Fail-open on any error \u2014 a guard that refuses customers' runs because its own bookkeeping broke is worse than the overlap it prevents.",graduation:{status:"permanent",gate:"RETIRED 2026-08-04, not parked: this does not graduate back to default-ON. The credential pool contends on the ACCOUNT \u2014 the resource two concurrent runs actually share \u2014 while this lock refused the whole PROJECT, was active only where the pool lives (org-shared projects), and consumed a leased account before refusing. Kept as `=1` for the one case it would still answer: whole-project serialisation for a plan that signs in with nothing, on a project whose data two runs would genuinely corrupt. Nobody has asked for that; the customer it was built for declined it explicitly.",evidence:"a project that needs whole-project serialisation despite per-account leasing \u2014 none known",owner:"steering (Alex)",review:"2026-11-04"}},PROJECT_RUN_LOCK_UNSHARED:{key:"PROJECT_RUN_LOCK_UNSHARED",envVars:["PROJECT_RUN_LOCK_UNSHARED"],polarity:"1-enables",defaultState:"off",surfaces:["web-next-edge"],cloudForwarded:!1,read:"env-direct",gates:"Extends the PROJECT_RUN_LOCK enforcement (above) from org-SHARED projects to NON-shared, single-owner projects. OFF (default): a project with no live org share is never locked and both seams behave exactly as they do today \u2014 no probe, no reap, no 409. ON: the same one-live-run-or-batch-per-project rule applies to every project regardless of sharing. Parented by PROJECT_RUN_LOCK (=0 disables both halves).",designDoc:"docs/plans/2026-08-03-org-project-sharing-design.md",status:"active",added:"2026-08-03",graduation:{status:"gated",gate:"NOT graduable until concurrent solo runs are either migrated or accepted as breakage. MEASURED ON PROD 2026-08-03: over the trailing 90 days, 50 pairs of SAME-USER runs in ONE project genuinely overlapped in time \u2014 8 distinct users, 8 distinct projects, every pair with BOTH rows terminal (so real concurrency, not a zombie artefact), 35 pairs overlapping >60s and 10 >5 minutes, and every single pair origin='desktop'. Flipping this ON today would start answering 409 to a workflow eight people use. Graduation therefore needs, in order: (1) a decision on whether concurrent desktop runs in one project are legitimate (the desktop app drives a LOCAL browser, so the collision domain argument is weaker there than for cloud runs) \u2014 if they are, the flag needs a per-origin carve-out rather than a flip; (2) if they are not, a client-side surface that explains the refusal before the user starts the second run, since the CLI/SPA today learn about it only from the 409; (3) a shadow census from the shared-project half showing the refusal rate and that no refusal was a false positive. Note the lock cannot be graduated by soak alone: with the shared half dark by data (no org_shared projects on prod), the shared half produces no evidence about the solo population.",evidence:"a product decision on concurrent same-project desktop runs (the measured 50-pair / 8-user population); client-side pre-start surfacing so a refusal is not first learned from a 409; and a shared-project refusal census with zero false positives",owner:"steering (Alex)",review:"2026-08-17"},notes:"Same read discipline as its parent: static process.env member access via projectRunLockUnsharedEnabled() in apps/web-next/lib/project-run-lock.ts, web-next-edge only, never cloud-forwarded. Exists as a separate key rather than a mode on PROJECT_RUN_LOCK because the two populations have opposite evidence: the shared-project case has ZERO current population and ships enforcing, while the solo case has a measured live population and must ship dark. One flag cannot carry two defaults."},PROJECT_CREDENTIAL_POOL:{key:"PROJECT_CREDENTIAL_POOL",envVars:["PROJECT_CREDENTIAL_POOL"],polarity:"0-disables",defaultState:"on",surfaces:["web-next-node"],cloudForwarded:!1,read:"env-direct",gates:"Project credential pool (2026-08-04): a project may hold SEVERAL ACCOUNTS \u2014 each a LOGIN/PASSWORD PAIR \u2014 under one role NAME (`scope='project'` rows of `app.project_credentials`, keyed `(project_id, name, slot)`), provisioned by the project owner or an `OrgMembership.role='admin'` of the owner's org. A caller holding no PERSONAL credential of that name LEASES one for the life of an engine session and releases it at teardown; the lease returns the account's real LOGIN alongside its secret and the seed carries both, because `credential.name` is otherwise the login the agent types into the email field \u2014 a role with no login would be typed AS the login (corrected 2026-08-04 after the first merge shipped role/password, which was one login with N passwords); a lease older than the staleness cutoff is reaped so a lost release cannot wedge a slot (default 5400000 ms = 90 minutes, the sandbox wall-clock ceiling and deliberately the same number as PROJECT_RUN_LOCK_DEFAULT_TTL_MS; overridable via `CREDENTIAL_LEASE_TTL_MS`, whose invalid or non-positive values fall back to the default rather than disabling reaping \u2014 a pool that never reaps would wedge permanently after one lost release). Personal rows always win, so a caller with their own `admin` consumes no shared account and behaves exactly as before. `GET /api/sync/entities/credentials` gains pool NAMES \u2014 never secrets, because it is called on every pre-flight and every seed build; the ONLY route that returns a pool secret is `POST /api/projects/{id}/credential-pool/lease`, and only for the slots it just granted to that lease. An exhausted role answers 409 `{code:'CREDENTIAL_POOL_EXHAUSTED', names}`, which both clients re-tag as the existing `CREDENTIAL_PREFLIGHT_REFUSED` so the refusal lands before a run row or engine session exists. FAIL-CLOSED, unlike the sibling PROJECT_RUN_LOCK: a session seeded without its credential burns a full run to fail at the login step. \"0\" hides the pool everywhere \u2014 list returns personal rows only, leasing 404s, the settings section renders empty \u2014 with no redeploy and no data change. Since 2026-08-06 the pool list route also reports the flag itself as `poolEnabled` (false on the disabled early return, before the grant check, since the flag is a property of the deployment and not of the caller): the settings page infers \"off\" from an empty pool it may not administer, but the NEW-PROJECT form has no project to inspect and so kept offering a Whole-team tab whose write could only 404 \u2014 clients read `poolEnabled` and fail OPEN when it is absent. Dark by data: a project with no `scope='project'` row behaves exactly as today. Since 2026-08-06 (#2338) release is no longer purely client-driven: the seed carries `{projectId, credentialLeaseId}` into the engine session, and the ENGINE \u2014 which holds the secrets \u2014 releases at every teardown the browser cannot see (session DELETE, idle reap, browser-crash sweep, SIGTERM), so a closed tab or a dead pod no longer strands the slots for the whole TTL; a terminal run row may also carry the lease id and free it in the run-completion cascade, but ONLY when the client declared that run owns the whole lease (`credentialLeaseRunId`), because a batch leg and a Coordinator-spawned child both write terminal rows while their session's lease is still in use. The engine additionally REFRESHES `leased_at` (PATCH on the lease route) for a lease whose session is still active, throttled to once per `CREDENTIAL_LEASE_REFRESH_MS` (default 900000 ms = TTL/6) and gated on activity since the last refresh: the 90-minute TTL assumed no holding unit outlives it, which stopped being true when the chat turn became one. Slots also record `leased_kind` ('chat' | 'run', narrowed server-side) so project settings names what holds an account instead of asserting \"a running test\" over a chat, and a pool admin can force-release a stuck slot without retiring the account.",designDoc:"docs/plans/2026-08-04-project-credential-pool-design.md",status:"active",added:"2026-08-04",notes:"Not AGENTIQA_-prefixed, so NOT cloud-forwarded: read exclusively in web-next (Vercel env), never in the engine \u2014 same shape as ORG_PROJECT_SHARING and PROJECT_RUN_LOCK. Single read point: credentialPoolEnabled() in apps/web-next/lib/credential-pool.ts, as a static process.env member access. NODE surface, not edge: provisioning encrypts and leasing decrypts with node:crypto, which the edge runtime does not provide \u2014 the routes originally declared runtime='edge', which compiled, type-checked and passed all 20 DB-evaluated tests (they import the handlers directly under Node) and then answered 500 on the first live request. RELATIONSHIP TO PROJECT_RUN_LOCK: that lock models the PROJECT as the contended resource and refuses a second run outright; the pool models the ACCOUNT, which is what two concurrent runs actually collide on, and hands out a distinct one per caller. The run lock is the degenerate case of a pool of size one, and on a project WITH a pool it is the coarser instrument \u2014 it will refuse a run for which a free account exists. Note the run lock's own rationale cites 'credentials are project-scoped', which was never true: app.project_credentials has carried user_id since its first migration and the read path has always been WHERE user_id = caller. Recorded scope cut (design P6): v1 returns the leased secret to the CALLER'S OWN CLIENT, exactly as their own credential already travels, so a member can read a shared secret out of their client; closing that needs server-side seeding (P1.5), which touches the engine image (Pipeline B). There is no lease heartbeat: every normal exit releases explicitly, so the TTL covers only a release that never arrived (closed tab, killed CI job, crashed pod), and it errs long because the failure directions are asymmetric \u2014 a slot wrongly held for an hour costs one refusal, while a slot wrongly freed hands the same account to two live agents, the exact failure the pool exists to prevent. The narrowed HC3 invariant the suite pins: a caller may receive a secret only when it is their own row, OR it is a pool row of a project they may access AND they hold a live lease on that exact slot \u2014 no caller ever receives another MEMBER'S personal row, under any grant, on any route."},ORG_GRANT_DEBIT_AT_FINALIZE:{key:"ORG_GRANT_DEBIT_AT_FINALIZE",envVars:["ORG_GRANT_DEBIT_AT_FINALIZE"],polarity:"1-enables",defaultState:"off",surfaces:["web-next-edge"],cloudForwarded:!1,read:"env-direct",gates:"Moves the org run-grant debit from analytics-ingest to finalize-run (2026-07-30). At ingest neither termination_reason nor the unit's final length is known, so the grant counter over-debited every non-billable termination, could never apply the per-unit min_billable_steps floor, could not refund, and double-debited AG-8220 duplicate pairs \u2014 measured on lio as +1041 / -437 / net +615 units (+24.6 credits) of customer-visible over-consumption against the invoice-authoritative getOrgUsage. When ON, ingest debits nothing and /api/billing/finalize-run/[id] recomputes the whole billing UNIT against getOrgUsage's own math (same unit keying incl. the chat/explore fan-out collapse, same per-(unit,byok) floor, same billable-termination filter) and applies only the DELTA against run_billing.grant_debited_units, claimed atomically per billing unit (one non-interactive Neon transaction behind a pg_advisory_xact_lock on (org, user, unit_key)) \u2014 which makes the debit idempotent (a double-finalize settles 0, and so does a concurrent one), refundable (a non-billable unit recomputes to 0 and the units are credited back), and safe across the flip. ON also activates the in-flight HOLD in the org cap gate (getOrgInFlightHeldUnits): unfinalized runs' not-yet-debited steps are subtracted from remainingUnits, so the orchestrator's per-dispatch admission check keeps decrementing continuously instead of standing still for the whole duration of a run. OFF = SHADOW: the ingest debit continues with byte-identical org_run_grant writes; finalize additionally computes the would-be total, stamps run_billing.grant_shadow_units, and logs grant_debit:finalize_shadow \u2014 the soak corpus that gates graduation.",designDoc:"docs/plans/2026-07-23-ag7872-org-run-grants-design.md",status:"active",added:"2026-07-30",notes:"Not AGENTIQA_-prefixed. Static process.env member access via isGrantDebitAtFinalizeEnabled() in apps/web-next/lib/org-grant-debit.ts (single read point, edge-safe \u2014 read from two Edge routes and the cap gate). Dark by data for every org without run-grant rows: debitOrgGrants/creditOrgGrants match no active grant and issue zero UPDATEs, and personal subjects short-circuit before any query. TRANSITION PROTOCOL (why the flip cannot double-debit): the debit is derived from run_billing.grant_debited_units, so that ledger has to be accurate for HISTORY as well as for new rows. Two things make it so, and both are load-bearing: (a) the 20260730120000 migration BACKFILLS every org-stamped pre-migration row to its step_count \u2014 the amount the ingest boundary actually debited, since /api/analytics/ingest passed the same billedStepIncrement to debitOrgGrants that recordRunStep added to step_count \u2014 and without that backfill a chat session still alive at the flip would recompute alreadyDebited=0 and re-debit its whole history; (b) under the flag OFF the ingest path records what it debits into the same column. A run ingested pre-flip and finalized post-flip therefore settles only trueUnits - alreadyDebited, covered by the flag-flip and post-sweep no-op tests in apps/web-next/lib/org-grant-debit.db.test.ts. EXACTLY-ONCE: the recompute-and-claim is a single non-interactive Neon transaction guarded by a pg_advisory_xact_lock keyed on (org, user, unit_key), because the ledger IS the idempotence guard and a read-modify-write across HTTP round trips let N concurrent finalizes of one unit each claim the whole total (measured 5x on an explore fan-out, 2x on a double finalize; regression suite apps/web-next/lib/org-grant-debit.race.db.test.ts). Historical drift accrued BEFORE the flip is NOT self-correcting (those units never finalize again) and is returned separately by scripts/true-up-org-grant.ts, which is operator-run and never automatic.",graduation:{status:"gated",gate:"Graduate only when the shadow corpus shows the finalize-time recomputation agreeing with getOrgUsage. The corpus is a plain SQL query over run_billing \u2014 compare grant_shadow_units against the unit's summed grant_debited_units, grouped by termination_reason \u2014 NOT scraped logs. Graduation criteria: (a) for billable terminations the shadow total matches the unit's floored getOrgUsage total exactly; (b) every non-billable termination shows a NEGATIVE delta of exactly what ingest debited (the refund the current boundary cannot make); (c) no unit shows a positive delta unexplained by the floor. Then, in this ORDER: (1) run scripts/true-up-org-grant.ts --apply per grant-mode org to return the historical drift (lio: -615 units / -24.6 credits as of 2026-07-30); (2) only then flip to 1 in the web-next Vercel env. The two commute \u2014 the migration's backfill keeps the ledger accurate for history either way \u2014 but sweeping FIRST makes the flip a provable no-op on everything already settled: the sweep rewrites each unit's ledger to its true total, so that unit's next settlement computes a delta of exactly 0 and the flip cannot move a historical unit at all. Flip-first is a correct fallback if the sweep has to wait, not the default. The sweep is operator-run and never automatic.",evidence:"run_billing.grant_shadow_units vs summed grant_debited_units per unit (durable, queryable soak corpus) + the grant_debit:finalize_shadow finalize logs; the unit + live-DB suites in apps/web-next/lib/org-grant-debit.test.ts, org-grant-debit.db.test.ts (per-termination-class debit, chat/explore folding, double-finalize idempotence, AG-8220 duplicate pair, flag-flip no-double-debit, in-flight hold) and org-grant-debit.race.db.test.ts (exactly-once under a 5-way concurrent fan-out over 20 iterations, concurrent double refund, no-active-grant reconcile); scripts/true-up-org-grant.ts dry run per org",owner:"steering (Alex)",review:"2026-08-13"}},EXTENSION_PROFILE_PERSISTENCE:{key:"EXTENSION_PROFILE_PERSISTENCE",envVars:[],polarity:"1-enables",defaultState:"on",surfaces:["desktop-main","desktop-renderer"],cloudForwarded:!1,read:"compile-const",gates:"Saves/restores a per-project Chrome profile (cookies/storage) across sessions so login can be skipped on replay; a false value skips profile persistence.",designDoc:"apps/desktop-next/src/renderer/featureFlags.ts",status:"active",added:"2026-06-01",notes:"Hardcoded TypeScript boolean (`= true`), no env var \u2014 a compile-time gate flipped by editing the const. Defined twice: apps/desktop-next/src/renderer/featureFlags.ts and apps/desktop-next/src/main/computerUse/DesktopPlaywrightService.ts (the main-process copy is the one actually consumed)."},TRANSIENT_ENV_RETRY:{key:"TRANSIENT_ENV_RETRY",envVars:["AGENTIQA_TRANSIENT_ENV_RETRY","AGENTIQA_EXPERIMENT_TRANSIENT_ENV_RETRY"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:`Transient-environment retry-once policy (RunnerRuntime / test-plan runs only, v1 \u2014 never ExplorerRuntime/chat). Motivating incident: staging run run_9fff560a \u2014 the app's POST to its own API failed transiently (console: CORS block + AxiosError "Network Error"), the agent waited 2\xD730s, then filed report_issue and blocked after ONE attempt; later probing showed the API healthy. When on, a report_issue (or the ensuing exploration_blocked) whose failure is a DETERMINISTIC transient-environment stall \u2014 BOTH a stall/timeout symptom (a wait/wait_for_element in the evidence window, or explicit stall/network phrasing; never a content/assertion mismatch) AND an environment signature in the captured EventDigest (any failedRequests, a console/page error matching the network class \u2014 CORS / "Network Error" / AxiosError / net::ERR_ / "fetch failed" \u2014 or an explicit 5xx write) \u2014 is BOUNCED once with a structured instruction to repeat the triggering action and report only if it reproduces, tracked per step index. The second matching failure at the SAME step passes through and the runtime (not the model) stamps the issue evidence JSON with attempts:2, both attempt timestamps, both EventDigest snapshots, sets category='environment', and appends 'Reproduced on retry \u2014 2 attempts.' to the description. Bounds: exactly 1 retry per step, max 2 retried steps per run; a further environment stall after the budget is spent passes through with attempts:1 and the note 'Environment degraded \u2014 not retried (budget exhausted).'. A run-mode prompt nudge asks the agent to retry proactively; the deterministic gate is the backstop (never trust LLM compliance). Off \u21D2 every environment failure files immediately on the first attempt, byte-identical to pre-change behavior. Non-environment failures are never retried regardless of this flag.`,designDoc:"docs/plans/2026-07-23-transient-env-retry-once-design.md",status:"active",added:"2026-07-23",notes:"Ships default-ON from inception (not graduated from an off default) \u2014 the policy is a strict reduction of a confirmed false-block class (one transient blip \u2192 a blocked run + a filed non-defect issue), so there is no pre-change OFF behavior to preserve. Registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED (the tripwire acknowledging the default-ON state deliberately). Deterministic classifier + state machine in packages/engine-core/src/transientEnvRetry.ts; the kill-switch read + wiring live at packages/engine-core/src/RunnerRuntime.ts handleReportIssue / handleBlocked. Runner lane only."},VERIFY_STEP_BLOCK_DEFLECT:{key:"VERIFY_STEP_BLOCK_DEFLECT",envVars:["AGENTIQA_VERIFY_STEP_BLOCK_DEFLECT","AGENTIQA_EXPERIMENT_VERIFY_STEP_BLOCK_DEFLECT"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Verify-step block deflection (RunnerRuntime / test-plan runs only \u2014 never ExplorerRuntime/chat). Motivating incident: prod asess_1786420838044_uwx2kmxq (Lio CI, 2026-08-11) \u2014 the Guided Buying assistant answered \"I could not find any information about purchase request 18\", the agent correctly filed report_issue and then called exploration_blocked asking whether to proceed; nothing answers a question in a non-interactive CI run, so the run ended blocked_by_issue with NO run_complete: the verify step's criteria were never graded and the two later steps (unrelated to request 18) were synthesized skipped. When on, ONE exploration_blocked per run is bounced with an instruction to grade the step instead, but only when ALL of: (a) the blocked step \u2014 model-declared args.stepIndex first, runtime pointer as fallback \u2014 is a `verify` step carrying >= 1 authored criterion; (b) the obstacle text POSITIVELY describes the application's own observable response (replied/answered/showed/returned/\u2026); (c) NO environmental-obstacle signature is present (login/credential, captcha, mailbox, network/timeout/5xx, our capture pipeline, stuck/frozen/never-loaded, paywall/quota, run memory); (d) the recent-action evidence buffer holds at least one non-errored action with a page url, i.e. positive proof the agent could observe the app. A repeated block passes straight through. The gate NEVER writes a verdict \u2014 it cannot know whether the criteria are met, only that they are gradable \u2014 so it can manufacture neither a pass nor a fail. Off \u21D2 every exploration_blocked terminates on the first call, byte-identical to pre-change behavior.",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-08-11",notes:"Ships default-ON from inception (not graduated from an off default) \u2014 a strict reduction of a confirmed no-verdict class (a gradable verification silently converted into a terminal block), so there is no pre-change OFF behavior to preserve. Registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED. Deterministic predicate in packages/engine-core/src/runner/verifyStepBlockDeflection.ts; the kill-switch read + wiring live at packages/engine-core/src/RunnerRuntime.ts (the exploration_blocked arm of dispatchToolCall). Runner lane only. Claim runner.verify-step-block-gradable."},CAPTURE_STARVATION_FALLBACK:{key:"CAPTURE_STARVATION_FALLBACK",envVars:["AGENTIQA_CAPTURE_STARVATION_FALLBACK","AGENTIQA_EXPERIMENT_CAPTURE_STARVATION_FALLBACK"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Capture-starvation fallback (#2430): a PRECONDITION on the browser_unobservable terminal. On a page whose renderer main thread is hogged (the lio customer's duplicate_request_loading state), every page.screenshot starves past its full retry budget and three in a row terminate the run 'blocked / browser_unobservable' \u2014 the same 25-step plan ended that way on eight consecutive recorded nights (prod asess_1786679659847_bpczs2mx, asess_1786850942202_sm2dus65, asess_1786247013077_24q166cv, asess_1788232725045_0hlvaoio with 24/25 steps already passed). The page is ALIVE throughout: the CDP screencast that feeds the run video kept delivering frames (357 in the 08-14 incident) because they come from the compositor frame sink and never touch the renderer main thread. When ON, a capture whose Playwright budgets are BOTH spent tries, in order: (1) the newest recorder frame when it is at most 3000ms old, transcoded JPEG->PNG (jpeg-js + pngjs, already engine-core deps) so it is byte-shaped like a page.screenshot for every image/png consumer, and labelled with a soft note saying it came from the recorder and may be soft; (2) a raw CDP Page.captureScreenshot, reached only when NO fresh frame exists (a non-compositing page may have a quiescent-but-fine renderer), yielding a CURRENT picture with no staleness to discount \u2014 since 2026-09-09 that rung is the keepalive-shaped {format:'jpeg', quality:40} at 3000ms, transcoded to PNG and labelled 'current but re-compressed', with CAPTURE_FALLBACK_JPEG_RUNG=0 restoring the original full-fidelity png/fromSurface call at 2500ms (see that flag for the measurement); and (1b) since 2026-09-09 round 2, when NOTHING FRESH EXISTS BY ANY ROUTE \u2014 rung 1 found no frame inside its 3000ms window and rung 2 could not get the renderer to produce a current capture, which is the production signature \u2014 the LAST recorder frame however old, up to 120s, served as capture_degraded {payload:'stale', frameAgeMs} under its own kill switch CAPTURE_FALLBACK_STALE_FRAME and labelled 'STALE image \u2026 N s ago \u2026 do not claim any change that could have happened after this frame' (see that flag). TAB SAFETY: no rung ever serves a frame of a page that is not the active tab, but the two get there differently \u2014 rung 1 is REFUSED unless the recorder's CDP session is bound to the page being captured, while rung 2 always captures the ACTIVE page (it reuses the recorder's session only when that session is bound to it, and otherwise attaches its own), so for rung 2 a tab mismatch is resolved rather than refused. A rung that yields a frame is a SUCCESSFUL observation: it resets the capture streak instead of incrementing it, and emits capture_degraded {path, frameAgeMs, payload, consecutiveDegraded, bound, playwrightError} for the nightly census. RESCUES ARE BOUNDED: consecutive degraded captures are counted per session and capped at 8 (CAPTURE_FALLBACK_MAX_CONSECUTIVE_DEGRADED, roughly 2-3 minutes of a blind-but-painting page), reset by ANY healthy page.screenshot. Without that cap the terminal would be UNREACHABLE on a page whose compositor keeps painting while its main thread never returns, since every rescue resets the streak. Past the cap the ladder logs capture_degraded_exhausted {consecutive, bound}, declines, and the pre-existing miss accounting and streak-3 terminal proceed with classification 'degraded_exhausted' (distinct from page_busy / unobservable) so the census can tell an exhausted rescue from a page that never produced a frame. A rescue also makes the step GRADABLE IN BOTH DIRECTIONS (it can now book a real failed, not only a real passed), so the degraded provenance is carried past the turn: EnvState.degradedCapture {path, frameAgeMs, payload} rides the invoke boundary, is persisted on the tool message, and is rendered into the blind-double-read question and the criterion-regrade prompt so a grader re-reading the persisted frame is told it is a recorder frame of a stated age at reduced quality. When a fallback frame is used, the three page-touching reads that follow the screenshot in captureState (_snapshotForAI, DomObserver.flush, the canvas-dominance evaluate) are bounded at 3000ms each \u2014 unbounded they would burn Playwright's 30s default on a starved renderer, i.e. up to 90s per rescued capture \u2014 and since 2026-09-09 (round 3) they are also ISSUED TOGETHER on that path, so a rescued capture pays that bound ONCE instead of three times (measured 8.07s/5.99s sequential vs 3.00s/0.004s concurrent on the transient busy fixture). That changes no verdict content: on a starved renderer all three already degrade to fixed neutral values, and the HEALTHY path keeps its original sequence, unbounded, so DomObserver.flush still drains strictly after the snapshot that describes those mutations. WHAT A RESCUE COSTS (measured 2026-09-09, headless Chromium): 5s steady page.screenshot + 10s evidence retry + 3s rung 2 + ~3s tail, about 21-24s of engine time per rescued capture; the ~100s observed between two capture_degraded rows in a live run is that plus the model's own turn. ORDER IS EVIDENCE-LED, NOT FIDELITY-LED: measured 2026-09-01 on headless Chromium 1.58.2 against a chained-20s-busy-block page, Page.captureScreenshot TIMED OUT at 2.5s, 8s and 25s with fromSurface true AND false and in both png and jpeg, while the screencast delivered 955 frames with the newest 0-54ms old \u2014 so CDP is not main-thread-independent and trying it first would cost 2.5s on every starved capture to attempt a rung measured never to land in this class. OFF restores the pre-change behaviour exactly: an exhausted capture counts as a miss and the streak-3 browser_unobservable terminal fires as it does today. The terminal itself, its bound, its wording and its auto-filed issue are UNCHANGED on both settings; this flag only decides whether a frame that demonstrably exists is used before declaring the browser blind. Since 2026-09-09 (round 4) a refusal by ANY rung is also guaranteed to reach that terminal rather than the target-app-slowness one: a counted capture miss tags its rethrow and startRun resolves resolveUnobservableTerminal before isTargetAppSlownessError, so a miss that escapes at one of the few capture sites outside BrowserActionExecutor (the advisory re-reads in the issue/block handlers) still books the grade-preserving browser_unobservable terminal. Since 2026-09-09 (round 5) rung 1b additionally spends a LATCHED per-run budget of CAPTURE_FALLBACK_MAX_STALE_PER_RUN = 2 stale rescues that no later observation refunds. Round 5 measured that bounding rung 1b alone was not enough: rung 2 sat outside the budget and a raw-CDP capture still landed occasionally right after the cap refusal (elapsedMs 120-2937), resetting the capture-miss streak and buying the model the 50-100s it needed to quit via exploration_blocked first \u2014 a blocked_by_issue terminal that books no per-step grades, 2/3 runs. Since 2026-09-09 (round 6) spending that budget therefore LATCHES THE WHOLE LADDER: rungs 1b AND 2 are both closed for the rest of the run, announced once as capture_fallback_ladder_latched {reason:'stale_run_cap', used, cap} and then silent, so past the cap only a steady page.screenshot or a FRESH rung-1 recorder frame (at most 3000ms old \u2014 real current evidence, which is why rung 1 is deliberately NOT gated and the animated fixture is byte-identical) can produce an observation and every other attempt is a plain miss; the misses that follow then fire the honest terminal 20.0-46.8s after the cap (measured, 4 runs) with the pre-hog grades preserved \u2014 at streak 2 rather than 3 whenever round 4's tagged below-bound miss routes an escaped one straight there. Round 6 also keys the budget and the latch on the RUN rather than on the SessionState: invoke's browser_recovery:session_closed path deletes that object and recreates it under the same session id, which REFUNDED the budget (measured round 5 \u2014 a lane stream bound tore the session down at 420.0s and the bring-up at 438.4s handed the wedged run a second ladder). See CAPTURE_FALLBACK_STALE_FRAME for the measurement.",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-09-01",notes:"Ships default-ON from inception (not graduated from an off default) \u2014 it strictly reduces a confirmed false-block class (a run killed with no verdict while a real frame of the page was sitting in the recorder) and can only ADD an observation, never remove or downgrade one, so there is no pre-change OFF behaviour worth preserving as the default. Registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED. Read site: packages/engine-core/src/BasePlaywrightService.ts (captureStarvationFallbackFrame, called from captureState's evidence and wait/verify-oracle timeout arms). Pure module: packages/engine-core/src/captureStarvationFallback.ts (freshness predicate + JPEG->PNG transcode + the consecutive-degraded ceiling + the degraded-capture label + the measurement that sets the rung order). Unit suite: packages/engine-core/src/__tests__/BasePlaywrightService.captureStarvationFallback.test.ts."},CAPTURE_FALLBACK_JPEG_RUNG:{key:"CAPTURE_FALLBACK_JPEG_RUNG",envVars:["AGENTIQA_CAPTURE_FALLBACK_JPEG_RUNG","AGENTIQA_EXPERIMENT_CAPTURE_FALLBACK_JPEG_RUNG"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Rung-2 payload of the capture-starvation ladder (#2430, second incident wave). THE DEFECT: rung 1 (a fresh recorder frame) has NEVER fired in production \u2014 on the pilot's hogged loading state every one of the six recorded capture misses carried screencastAlive:false with recorder-frame ages of 22.5-60.3s, because that overlay is STATIC and the compositor stops the moment the hog starts. The animated self-testing fixture hid this: its transform-only spinner guarantees frames, so the unit and L3 layers were green on a rung prod has never reached. That leaves rung 2 as the only rescue, and rung 2 asked for the wrong thing: {format:'png', fromSurface:true, captureBeyondViewport:false} bounded at 2500ms timed out at exactly 2501ms 6/6 across two sessions, while the SCREENCAST KEEPALIVE's {format:'jpeg', quality:40} at 3000ms landed 14/14 on the same pages in the same seconds \u2014 one of them 194ms AFTER a rung-2 miss on the identical page, with zero screencast_keepalive_failed. WHAT =0 DISABLES: the payload swap only. ON, rung 2 sends the keepalive's exact call at the keepalive's 3000ms bound (CAPTURE_FALLBACK_CDP_TIMEOUT_MS, which also bounds attaching the rung's own CDP session) and transcodes the JPEG to PNG through the same screencastFrameToPng rung 1 uses, so every image/png consumer downstream is unchanged; the degraded marker carries payload:'jpeg' and the grader-facing label says the frame is CURRENT but re-compressed rather than 'full fidelity'. OFF restores the png/fromSurface call at CAPTURE_FALLBACK_CDP_LEGACY_TIMEOUT_MS=2500 with the frame used raw and no payload on the marker \u2014 byte-identical to the pre-change rung, including its full-fidelity label wording. UNCONDITIONAL COMPANION, not gated by this flag: rung 1 now emits capture_fallback_frame_absent {reason:'no_frame'|'tab_mismatch', frameAgeMs} when it cannot be attempted at all. All three of rung 1's existing markers sit inside its guard, so the two ways of never entering it were indistinguishable in the nightly census \u2014 and every production miss so far has been one of them. It is diagnostic only and changes no verdict. NOT CHANGED BY EITHER SETTING: the rung ORDER, the 3000ms recorder-frame freshness bound, the consecutive-degraded ceiling, the streak accounting, and the browser_unobservable terminal, its bound, its wording and its auto-filed issue. LOCAL EVIDENCE IS NULL, DELIBERATELY STATED: probed 2026-09-09 against both busy-finale fixtures (headless Chromium, screencast running, payloads alternated on one CDP session, 8 rounds each), png@2500 scored 0/8 and jpeg-q40@3000 scored 0/8 on BOTH, and on the static fixture jpeg also missed at 8000ms and 25000ms \u2014 the fixtures' hog is one 24s synchronous block that yields a single macrotask turn, so nothing is capturable there and the fixture cannot discriminate the payloads. The production 14/14-vs-0/6 counter-example is the only evidence separating them, which is exactly why this rung ships behind its own switch. A SECOND, INDEPENDENT REASON THE RUNG EARNS ITS 3000ms, measured 2026-09-09 (round 3) on the transient busy fixture: its ABANDONED Page.captureScreenshot provokes a screencast frame at SEND time even though the capture itself never lands. With the rung, recorder-frame arrivals during a 48s hog were t=0.00/0.07/16.04/39.12s; with it skipped, only t=0.00/0.07s. So the rung leaves rung 1b a ~3s frame to serve instead of a ~39s one, and skipping it to save 3s per rescue would age the served evidence by an order of magnitude. Acceptance suite: packages/engine-core/src/__tests__/BasePlaywrightService.captureStarvationFallback.test.ts.",designDoc:"packages/engine-core/src/captureStarvationFallback.ts",status:"active",added:"2026-09-09",notes:"Ships default-ON as an ordinary 0-disables kill switch (not a graduation, and therefore NOT in killSwitchDefaultState.test.ts's INTENTIONALLY_GRADUATED allowlist, which covers 1-enables flags left on) \u2014 it can only change WHICH payload an already-failing capture asks for, never whether a capture is attempted or how a miss is accounted, so there is no pre-change behaviour worth preserving as the default. Read site: packages/engine-core/src/BasePlaywrightService.ts (captureStarvationFallbackFrame, rung 2). Pure module: packages/engine-core/src/captureStarvationFallback.ts (buildCaptureFallbackCdpRequest + describeAbsentScreencastRung). Claim runner.browser-unobservable-terminal; L3 fixtures runner/hang-busy-finale-static and runner/hang-busy-finale-transient."},CAPTURE_FALLBACK_STALE_FRAME:{key:"CAPTURE_FALLBACK_STALE_FRAME",envVars:["AGENTIQA_CAPTURE_FALLBACK_STALE_FRAME","AGENTIQA_EXPERIMENT_CAPTURE_FALLBACK_STALE_FRAME"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Rung 1b of the capture-starvation ladder (#2430, round 2): serve the LAST recorder frame, labelled with its age, when no fresh picture can be obtained by any route. THE DEFECT THIS CLOSES: the jpeg hypothesis behind CAPTURE_FALLBACK_JPEG_RUNG was refuted locally \u2014 on the static busy-finale fixture (and the animated one) jpeg-q40@3000ms misses exactly as often as png@2500ms (0/8 each), and jpeg also misses at 8000ms and 25000ms \u2014 and the static fixture reproduces the production class end to end (the run ends blocked / browser_unobservable). The honest reading is that on a hogged renderer main thread NO FRESH CAPTURE EXISTS BY ANY ROUTE: rung 1 wants a frame <=3000ms old and the compositor went quiet 22.5-60.3s ago, rung 2 wants a current capture and the renderer cannot service one. The only picture of the page that exists is the last frame the compositor emitted, and the ladder was throwing it away \u2014 so a run died with no verdict on a step it could have graded. WHEN ON (default), after rung 1 found nothing FRESH and rung 2 failed, the ladder accepts the NEWEST recorder frame if its age is within CAPTURE_FALLBACK_STALE_FRAME_MAX_AGE_MS = 120000 (chosen against the measured production ages 22.5/23.4/23.7/36.3/42.8/60.3s, with headroom), transcodes it JPEG->PNG through the same screencastFrameToPng rung 1 uses, and marks it capture_degraded {path:'screencast', payload:'stale', frameAgeMs} \u2014 a SUCCESSFUL observation that resets the capture-miss streak. The frame is labelled everywhere it can be read: buildDegradedCaptureLabel produces 'STALE image: the last frame the browser produced, N s ago. The page has stopped painting since (busy / blocked main thread). Treat everything as possibly outdated; do not claim any change that could have happened after this frame; say so rather than guessing.', which is the turn-time EnvState.metadata note AND (via EnvState.degradedCapture -> the persisted tool message -> EvidenceMessage.degradedCapture) the line the blind-double-read question and the criterion-regrade prompt carry. TAB SAFETY IS NOT RELAXED: a frame whose recorder session is bound to another page is still REFUSED (staleness never makes a wrong page acceptable). BOUNDED THREE TIMES, AND THE BOUND THAT HOLDS IS THE RUN CAP. A stale rescue is a capture_degraded, so the shared consecutive-degraded ceiling of 8 (CAPTURE_FALLBACK_MAX_CONSECUTIVE_DEGRADED) still applies; rung 1b ALSO carries its own CAPTURE_FALLBACK_MAX_CONSECUTIVE_STALE = 2, counted on its own session counter and reset by ANY non-stale observation (a healthy page.screenshot, a fresh rung-1 frame, or a CDP rescue); and since 2026-09-09 (round 5) it carries CAPTURE_FALLBACK_MAX_STALE_PER_RUN = 2, a LATCHED per-run budget that NOTHING refunds. WHY: measured on runner/hang-busy-finale-static (2026-09-09, 3/3 runs), under the shared ceiling alone the rung kept a PERMANENTLY wedged page alive for 352-476s and never reached 8, because the model gave up first and called exploration_blocked \u2014 a blocked_by_issue terminal that books NO per-step grades (only run_complete and the browser_unobservable concluding turn do). The run then recorded [1:skipped 2:skipped 3:skipped 4:warning] where the un-rescued run had recorded [1:passed 2:passed 3:passed 4:skipped]: the rescue had converted a grade-PRESERVING terminal into a grade-DESTROYING one, which is the exact Bug B this ladder exists to prevent. ATTRIBUTED HONESTLY: the same run shape was then measured on the ANIMATED fixture with rung 1b irrelevant \u2014 rung 1 (fresh frames, ages 1-34ms) rescued 4 captures, the model exited via exploration_blocked at 386.1s and the run recorded the same all-skipped rows. So grade loss on a permanently wedged page is a property of ANY successful rescue, not of the stale rung; the real fix is preserving grades on the agent-block terminal, which is tracked separately. This ceiling bounds the NEW exposure rather than pretending to close the gap. Two stale frames in a row are already the strongest available signal that this is not a busy INTERVAL but a page that is gone, so the third capture misses and the honest browser_unobservable terminal fires with the earlier grades kept. The production class this rung is for is the opposite shape \u2014 a loading overlay whose hog is seconds, well inside two captures. WHY THE CONSECUTIVE CEILING WAS NOT ENOUGH, AND WHAT THE RUN CAP DOES (round 5, measured 2026-09-09 across rounds 2 and 4, 4 runs on runner/hang-busy-finale-static): a consecutive counter is reopened by ANY non-stale observation, and on a hogged page the raw-CDP rung lands every so often \u2014 one run's ceiling refusal was followed immediately by capture_degraded {path:'cdp', payload:'jpeg', elapsedMs:2889}, and another spent FOUR stale rescues (consecutiveStale 1,2 \u2026 1,2) over 430.6s without ever booking a capture miss. CAPTURE_FALLBACK_MAX_STALE_PER_RUN latches instead: two stale rescues per run, ever, and neither a fresh rung-1 frame nor a CDP rescue refunds one. The consecutive ceiling is kept as the inner, page-scoped bound (it is what the census and the L3 judge read, and what would still bound an unbroken burst if the run budget were widened), but at the shipped values 2 and 2 the run cap subsumes it and is what is REPORTED \u2014 used >= consecutiveStale always. MEASURED 3/3 on runner/hang-busy-finale-static (2026-09-09): exactly two stale rescues per run, then capture_fallback_stale_frame_refused {refusal:'stale_run_cap', used:2, cap:2} at 246.8/242.1/217.3s, zero cap breaches, zero startRun:target_app_slowness_blocked. IT WAS NOT SUFFICIENT ON ITS OWN, AND SPENDING IT NOW LATCHES THE WHOLE LADDER (round 6): rung 2 (raw CDP) used to sit outside this cap \u2014 bounded only by the shared degraded ceiling of 8 \u2014 and on that fixture it still landed occasionally right after the cap refusal (capture_degraded {path:'cdp', payload:'jpeg', elapsedMs:2937/934/120+1862}), which reset the capture-miss streak and bought the model the ~50-100s it needed to reach exploration_blocked first (295.2s and 361.5s). Since 2026-09-09 round 6, the capture on which this rung would be refused for stale_run_cap does not even ASK rung 2: the ladder emits capture_fallback_ladder_latched {reason:'stale_run_cap', used, cap} once and then declines every capture that a steady page.screenshot and a FRESH rung-1 frame both failed to answer. Rung 1 stays open by design (a frame inside the 3000ms bound is current evidence, not a rescue), which is why the ANIMATED fixture is byte-identical. The budget and the latch are keyed on the RUN, not on the SessionState, because invoke's browser_recovery:session_closed path recreates that object under the same session id and refunded the budget (measured round 5: session torn down 420.0s, session_bringup 438.4s, ladder reopened). MEASURED round 6 on runner/hang-busy-finale-static, 5 runs: zero post-cap CDP rescues 5/5, the latch announced exactly once in each of the 4 runs that reached the cap, and the honest terminal 20.0s / 46.8s after the latch with [1:passed 2:passed 3:passed 4:skipped] preserved; 4/5 judged pass (round 5: 0/3). The remaining failure is the model's own give-up clock, not the ladder: a PRE-cap rung-2 landing at 187.3s delayed the second rescue to 234.9s and exploration_blocked arrived at 261.8s before the next capture. Effect on the production class: none, it needs one rescue (transient fixture round 5: PASS, all four steps, 88.9s, one stale rescue). THE COST, STATED: a long plan that meets the overlay a third time falls through to the honest browser_unobservable terminal \u2014 which is the pre-#2430 behaviour with grades kept \u2014 so the cap bounds this ladder's BENEFIT on such a run and can never make one worse than the baseline it replaced. browser_unobservable:capture_missed therefore fires when there is NO recorder frame within 120s at all, when the tab-safety refusal applies, once two consecutive stale rescues are spent, or once the run's two-rescue budget is spent. Every refusal is attributable in the census: capture_fallback_stale_frame_refused {refusal:'disabled'|'stale_run_cap'|'stale_ceiling'|'no_frame'|'tab_mismatch'|'outside_window', frameAgeMs, consecutiveStale, staleBound, used, cap} \u2014 used/cap are reported on EVERY refusal and on every stale capture_degraded, not only when the cap is the refusal. WHEN OFF (=0), the ladder is byte-identical to round 1: rung 1 (fresh) -> rung 2 (CDP) -> miss, no rung 1b, no 'stale' marker, no label. NOT CHANGED BY EITHER SETTING: rung 1's 3000ms freshness bound, the rung order, rung 2's payload/bound (that is CAPTURE_FALLBACK_JPEG_RUNG's business), the degraded ceiling, the streak accounting, and the browser_unobservable terminal's bound, wording and auto-filed issue. VERDICT-WIDENING, DELIBERATELY: a stale frame is real evidence of a real PAST state, and a grader that ignores the label can read it as the present \u2014 a new false-passed/false-failed path that did not exist before. It is mitigated by labelling and by blind-double-read, never by suppression, and it is why this rung is switchable independently of the rest of the ladder. Acceptance suite: packages/engine-core/src/__tests__/BasePlaywrightService.captureStarvationFallback.test.ts. L3: TWO cases, because a permanently wedged page can only certify the honest terminal. runner/hang-busy-finale-static gates the CONTRACT (an unobservable terminal that keeps the pre-hog grades, or a rescue \u2014 never a false pass, never destroyed evidence) and reports which rungs fired. runner/hang-busy-finale-transient (added 2026-09-09, round 3) GATES THE RESCUE: the same flow and the same quiet compositor, but the hog ENDS after ~48s, which is the production shape \u2014 the pilot's runs survived 2-3 capture failures and went on to grade later steps. On a page that comes back the two behaviours separate cleanly: without this rung the three starved captures trip the streak-3 terminal BEFORE the page returns and the last step is never graded, with it the run stays observable across the hog and grades that step off a FRESH capture. The case therefore requires at least one capture_degraded {payload:'stale'}, the ABSENCE of browser_unobservable:capture_missed {terminal:true}, and a real 'passed' grade on the busy step. ROUND 4 (2026-09-09) \u2014 WHERE A REFUSAL LANDS. Every refusal of this rung, the stale ceiling and refusal:'disabled' included, is a capture MISS: it counts on the capture_missed streak and the run continues to the streak-3 browser_unobservable terminal, which is the grade-PRESERVING one. It must never land on the target-app-slowness arm (blocked_by_issue / slow_site), which tells the customer their app was too slow and synthesizes skipped rows over grades the run already earned. Round 3 measured it doing exactly that on runner/hang-busy-finale-static: the ladder was correct throughout (consecutiveStale 1 -> 2 -> refusal:'stale_ceiling' -> capture_missed {streak:1}) but the rethrown raw Playwright TimeoutError escaped the run loop and isTargetAppSlownessError answered TRUE for it, so the case red twice (211.8s all rows skipped; 420.1s stream-bound error) where the same page with this flag OFF ends browser_unobservable keeping [1:passed 2:passed 3:passed 4:skipped]. The cause was neither the rung nor the streak: almost every capture sits behind BrowserActionExecutor, which turns the rethrow into an error tool result and lets the streak keep counting, but the advisory re-reads inside the issue/block handlers call computerUseService.invoke directly, so a below-bound miss there escapes the loop \u2014 and which site the model's next tool call lands on is a coin flip. Round 4 tags a counted miss (markCaptureObservabilityMiss: url, streak, limit, screencastAlive) and startRun resolves resolveUnobservableTerminal \u2014 the streak-bound error OR a tagged below-bound miss \u2014 BEFORE the slowness arm, so an escaped miss books the honest terminal with its concluding turn. Nothing else moves: only that one seam sets the tag, so the cold-start SlowSiteError and every screenshot timeout that never reached the miss accounting keep the terminal they have today. Confirmed on that fixture: 2/2 round-4 runs on the shipped default logged ZERO startRun:target_app_slowness_blocked (round 3: 1 of 2). ROUND 5 (2026-09-09) \u2014 THE PIN IS GONE. Round 4 left one blocker: a rescue costs ~70-100s, so on a page that never comes back the MODEL reached exploration_blocked \u2014 a blocked_by_issue terminal that books no per-step grades \u2014 long before the capture streak could reach 3 (331.9s and 430.6s, both [1:skipped 2:skipped 3:skipped 4:warning]), where the un-rescued run reaches the honest terminal in 60-90s. CAPTURE_FALLBACK_MAX_STALE_PER_RUN makes the STALE budget unrefundable, which removes this rung as the cause; runner/hang-busy-finale-static runs the SHIPPED DEFAULT (no env pin, so the measurement stays visible instead of hidden behind it) and gates the run cap directly: at most CAPTURE_FALLBACK_MAX_STALE_PER_RUN stale rescues, and if the budget was spent and a terminal capture miss followed, a capture_fallback_stale_frame_refused {refusal:'stale_run_cap'} must be in the census. Round 5 measured that case still RED 3/3 for a reason the cap does not cover \u2014 rung 2 (raw CDP) sat outside the budget and still landed after the refusal (elapsedMs 120-2937), resetting the miss streak \u2014 and the pin was deliberately not restored, because re-pinning would have hidden the residual. Round 6 closes it: spending this rung's run budget now LATCHES THE WHOLE LADDER (see CAPTURE_STARVATION_FALLBACK), so rung 2 is closed too and the case's judge additionally reds on any rescue booked after the cap and on a latch that was not announced exactly once. The budget and the latch are keyed on the RUN, so a mid-run bring-up cannot refund either. Grade preservation across the agent-block terminal (#3072) remains a real gap, but it is no longer this ladder's problem to route around \u2014 the latch keeps the run away from that terminal. What round 4 added to that case still holds: its judge reds on the slowness terminal and on a stale streak past the consecutive ceiling, and the routing itself is pinned deterministically by unit tests rather than by a stochastic L3.",designDoc:"packages/engine-core/src/captureStarvationFallback.ts",status:"active",added:"2026-09-09",notes:"Ships default-ON as an ordinary 0-disables kill switch (not a graduation, and therefore NOT in killSwitchDefaultState.test.ts's INTENTIONALLY_GRADUATED allowlist, which covers 1-enables flags left on). Read site: packages/engine-core/src/BasePlaywrightService.ts (captureStarvationFallbackFrame, rung 1b, after rung 2 returns nothing). Pure module: packages/engine-core/src/captureStarvationFallback.ts (evaluateStaleFrameRung + the payload:'stale' branch of buildDegradedCaptureLabel). Claim runner.browser-unobservable-terminal. Rollback is one env var and restores the round-1 ladder exactly. TWO L3 cases, deliberately split by fixture shape. runner/hang-busy-finale-static does NOT gate the rescue: its hog is PERMANENT by construction, so the correct outcome there is the honest terminal on both settings and a case demanding a rescue would manufacture a red \u2014 it gates the contract and REPORTS which rungs fired. runner/hang-busy-finale-transient (round 3) DOES gate it, because its hog ends and the page comes back, which is the production shape and the only one on which a rescue is a strict improvement."},CAPTURE_FALLBACK_TAB_REBIND:{key:"CAPTURE_FALLBACK_TAB_REBIND",envVars:["AGENTIQA_CAPTURE_FALLBACK_TAB_REBIND","AGENTIQA_EXPERIMENT_CAPTURE_FALLBACK_TAB_REBIND"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Re-bind the screencast RECORDER to the page the executor is driving when the capture ladder reports a tab mismatch (#3104). SHADOW-FIRST: with no env (and with an explicit =0, which is indistinguishable and pinned to be) the full predicate runs and emits capture_fallback:would_rebind, and NOTHING attaches, detaches or moves; =1 performs the rebind. THE DEFECT. #3089 gave rung 1 an attribution row for a NON-attempt, and the first production census it produced named a reason nobody had budgeted for: capture_fallback_frame_absent {reason:'tab_mismatch'} \u2014 a decodable, FRESH frame existed and tab safety withheld it \u2014 followed by capture_fallback_stale_frame_refused {refusal:'tab_mismatch', used:0, cap:2} and the browser_unobservable terminal. used:0 is the signature: the run reached the terminal without spending a single unit of the rescue budget it had (staging asess_1788993885491_don3yg17 / run_952d0f20 and prod roawoucv, both 2026-09-09, step 25 of the pilot's 25-step draft-completion plan; reproduced 4+ times across 09-08/09-09). WHY IT IS PERMANENT, NOT TRANSIENT. startScreencast picked its page ONCE as `tab1 ?? page`, and restartScreencast \u2014 the only re-binding path that existed \u2014 re-read the same expression, so every routine restart (screencast_frame_ack_failed, the keepalive watchdog) re-bound to tab1 too. session.page meanwhile moves at eight seams (a popup becoming the active surface, that popup closing, containment restoring the app tab, extension/web navigate routing, switch_tab either way, close_tab) and none of them touched the recorder. So the moment a window.open makes the executor's page tab2, rungs 1 and 1b are STRUCTURALLY DEAD for the rest of the run and only rung 2 (which attaches its own session against session.page) can rescue \u2014 and on a hogged active tab rung 2 does not land, so every rung refuses. THE RULE: a screencast frame is evidence of the page its CDP session was attached to WHEN THE FRAME ARRIVED, and the recorder's attachment must FOLLOW the executor \u2014 so a tab_mismatch is repaired by RE-BINDING the recorder, NEVER by relaxing which page a frame may be served as. TAB SAFETY IS NOT RELAXED ANYWHERE: the second half of that rule is what makes the first half safe. Frames now carry ownerPage, stamped at remember time, and BOTH rung predicates read the frame's owner rather than the recorder's current binding \u2014 so a frame remembered while the recorder was on tab1 is still refused for tab2 after the rebind. Without that stamp the rebind would open a false-evidence hole: the same pixels, re-labelled as a different page. That attribution change is UNFLAGGED and behaviour-identical today (on a build where the recorder never moves, the owner and the current binding are the same value on every reachable state, including the permissive undefined branch), and it is pinned by a parity test. DURABILITY. The binding now has ONE source of truth, session.screencastTargetPage, which startScreencast and therefore every restartScreencast read. Without it a rebind would be a one-shot that the next ack failure or keepalive restart silently revoked, and the run cap would be spent re-fixing the same mismatch. Undefined until the first attach and reverted to a LIVE page when the target closes, so a session that never re-binds resolves `tab1 ?? page` exactly as before. WHERE IT DECIDES. Two sites, one pure predicate (evaluateTabRebind): at:'invoke', the EAGER repair at the invoke boundary \u2014 a swap that persisted to the next action is fixed OFF the starved path, so the ladder normally finds the recorder already correct and pays nothing; and at:'ladder', between the ladder latch and rung 2, on a capture that has already burned 5s + 10s of Playwright budget. REFUSALS, in order, each with a reason in the census: no_mismatch (first, so no row can ever describe a capture whose recorder already owned the active page \u2014 including every desktop lane, where the screencast never starts and the owner is undefined); extension_session (the whole mechanism is scoped OUT of the wallet lane, where tab1 IS the extension page and the executor deliberately drives the dapp \u2014 a mismatch there is the lane's design, so following it would re-point the run video at chrome-extension:// pages and refusing it per invoke would storm the census; ONE capture_fallback:rebind_declined row per RUN instead); latched (the run has spent its stale budget, so rungs 1b and 2 are closed and a rebind would buy no evidence at all); rebind_run_cap (CAPTURE_FALLBACK_MAX_TAB_REBINDS_PER_RUN = 2, latched and never refunded); then the TARGET guards \u2014 extension_page, pending_escape (a cross-environment popup the invoke tail is about to CLOSE), blank_target (about:blank / data: / an unstatable URL \u2014 the window.open('') + location=\u2026 pattern is about:blank for a beat, and a recorder re-pointed at it makes the run video blank AND lets the ladder serve blank frames as ownerTab === activeTab evidence); restart_in_flight (the keepalive watchdog's own restartScreencast holds the same re-entrancy guard); and shadow LAST, which is what makes the census exact \u2014 reason:'shadow' means every other condition said yes and only the flag said no, so a would_rebind row is one-for-one with a rebind the enforcing build would have performed. THE MARKER IS AN EDGE, not a level: one row per mismatch TRANSITION (identity-compared page pair), re-armed the moment the recorder owns the active page again. In shadow nothing repairs the mismatch, so a level-triggered row would fire on EVERY invoke for the rest of a multi-tab run \u2014 the pilot's 25-step plan is exactly that shape. THE CAP IS COUNTED IN BOTH STATES: in shadow it is a VIRTUAL counter over the rebinds the enforcing build would have performed, so the shadow row count predicts the enforced cost and the gate's 'at most two per run' is testable BEFORE the flip. RUN-KEYED, like the stale budget: it lives on captureLadderRunBudget(sessionId), which invoke's browser_recovery:session_closed bring-up cannot refund and which cleanupOtherSessions' peer sweep is forbidden to drop (#3089 round 7). WHAT ELSE MOVES WHEN IT IS ON. (1) Rung 2's transport is PINNED to its own private CDP session for the rest of a run that has re-bound: getCaptureFallbackCdpSession prefers the recorder's session when cdpSessionPage === page, which on the tab-mismatch path was never true before and becomes true after a rebind \u2014 that would move rung 2's Page.captureScreenshot onto the session the keepalive is also sending on, against a hogged renderer, and #3089 round 1 measured that the session/payload choice on this exact surface decides whether the rung lands. (2) The run video and the live view follow the agent between tabs (both ride screencastFrameCallbacks); screencastStartTime is preserved so frame timestamps stay monotonic, and the 2-per-run cap plus the invoke-boundary placement bound the strobing a popup-heavy run could otherwise produce. (3) noteCaptureObservability reads session.lastScreencastFrameAt, which after a rebind is stamped by the NEW tab \u2014 so screencastAlive becomes truthful for the first time on a mismatched run and classification can flip from page_busy to unobservable on the same physical failure. That is a census-string change the L3 judge reads, and it is EXPECTED rather than incidental: today the row claims page_busy on the strength of frames of a page nobody is driving. CENSUS, unconditional and on every setting: capture_fallback_frame_absent, capture_fallback_stale_frame_refused and every capture_degraded now carry {recorderTab, ownerTab, activeTab, recorderUrl, activeUrl} \u2014 the issue's question verbatim, which tab the executor targets versus which tab the recorder streams. WHEN OFF (unset or =0) the ladder is byte-identical to #3089's: same rung order, same refusals, same used/cap accounting, same latch, same terminal. NOT CHANGED BY EITHER SETTING: rung 1's 3000ms freshness bound, the stale rung's 120s window and its two budgets, the whole-ladder latch, the consecutive-degraded ceiling, the capture-miss streak, and the browser_unobservable terminal's bound, wording and auto-filed issue. VERDICT-WIDENING, DELIBERATELY: a capture that used to be withheld on tab_mismatch can now produce a gradable frame \u2014 the same widening rungs 1/1b already carry, recorded on the browser_unobservable two-sided entry in docs/VERDICT-GATES.md, and with the tab-safety invariant explicitly NOT relaxed. Acceptance suite: packages/engine-core/src/__tests__/BasePlaywrightService.captureStarvationFallback.test.ts. L3: runner/tab-switch-busy-finale (a same-origin window.open + focus switch before a transient busy finale), pinned =1 in its case env \u2014 enforcement before graduation exists ONLY there, and nothing in the nightly default lane runs the rebind.",designDoc:"packages/engine-core/src/captureStarvationFallback.ts",status:"active",added:"2026-09-10",graduation:{status:"gated",gate:"Flip defaultState to 'on' when ALL SIX hold across >=5 consecutive nightly qa-exhaustive runner lanes AND >=2 ci:qa-targeted dispatches of the pilot's 25-step draft-completion plan on staging. (1) NO SPURIOUS FIRE: every capture_fallback:would_rebind row is preceded, in the same capture, by capture_fallback_frame_absent {reason:'tab_mismatch'} (ladder site) or by a session.page reassignment since the previous invoke (invoke site); ZERO rows on a decision whose recorder already owned the active page. (2) NO SINGLE-TAB FIRE: zero would_rebind rows on any run whose activeTab is 'tab1' throughout \u2014 a row there means the attribution predicate is wrong, not the binding. (3) TARGET PARITY: of the rows where a FOLLOWING tool_call recorded a post-state URL, 100% name the page that call acts on, denominator published (the gate cannot be stated over all rows \u2014 on a wedged renderer the next tool call may never produce a post-state URL). (4) FORCED-ON EVIDENCE on runner/tab-switch-busy-finale over >=8 runs: a landed capture_fallback:rebind {ok:true} on EVERY run, ZERO tab_mismatch rows of either kind, ZERO capture_degraded whose ownerTab !== activeTab, AND the browser_unobservable:capture_missed.classification distribution published beside the =0 control \u2014 the flip is a CUSTOMER-FACING string change (see below) and must be a number before the graduation, not a footnote. (5) SHADOW-DEFAULT TRACE PARITY: runner/hang-busy-finale-static, runner/hang-busy-finale-transient and runner-blocked-synthesis book ZERO would_rebind rows and a ladder census identical to the pre-change one. (6) COST: the would_rebind count per run stays at or below CAPTURE_FALLBACK_MAX_TAB_REBINDS_PER_RUN \u2014 which is exactly what the virtual counter makes measurable in shadow. CLIENT-VISIBLE WORDING, MEASURED (#3121 review MED-2; unit-pinned by 'MEASURES the page_busy -> unobservable classification flip under =1'). noteCaptureObservability derives screencastAlive from session.lastScreencastFrameAt, which only the RECORDER's own CDP session stamps. On a mismatched multi-tab run that is the HEALTHY launcher tab, so TODAY the row claims page_busy / screencastAlive:true off frames of a page nobody is driving; under =1 the recorder follows the executor onto the hogged tab, the stamps stop, and the SAME physical failure classifies unobservable / screencastAlive:false. That boolean is threaded into BrowserUnobservableError and decides TWO customer-facing strings in RunnerRuntime's unobservable catch, and in BOTH the =1 run LOSES a clause: (a) the blocked verdict's detail, embedded verbatim by buildBlockedSummary into run.summary \u2014 '... after N consecutive screenshot timeouts while screencast frames kept arriving - the page was alive but busy (possible page hang or main-thread busy loop).' becomes '... after N consecutive screenshot timeouts.'; (b) the auto-filed environment issue's description, which loses 'Screencast frames kept arriving while captures starved, so the page was alive but busy - a possible page hang or main-thread busy loop (e.g. a synchronous loading state).' The direction is TRUTHFUL \u2014 the busy claim was made off another tab's frames \u2014 but it is a wording change on a record the customer reads, which is why gate (4) requires the distribution rather than this note alone. It is =1-ONLY: the shipped shadow default moves no recorder and therefore changes neither string. RETIRE INSTEAD if the census shows the mechanism fires on runs whose recorder already owned the active page, or if the run video's tab-following turns out to be unacceptable to customers: the attribution half (frame ownerPage + the census fields) is unflagged and stays either way.",evidence:"capture_fallback:would_rebind / :rebind / :rebind_declined rows plus the {recorderTab, ownerTab, activeTab, recorderUrl, activeUrl} census on capture_fallback_frame_absent, capture_fallback_stale_frame_refused and capture_degraded, in the nightly qa-exhaustive runner lane and in ci:qa-targeted staging dispatches; the forced-ON leg is the runner/tab-switch-busy-finale eval case. Baseline measured 2026-09-10 (local, 3 runs each leg): pinned =1 3/3 pass with zero tab_mismatch rows and one rebind per run at the invoke boundary in 15-25 ms; shadow default 0/3 with capture_fallback_frame_absent {reason:'tab_mismatch'} on every run.",owner:"steering (Alex)",review:"2026-10-01"},notes:"Ships SHADOW (1-enables, defaultState off), so it is NOT in killSwitchDefaultState.test.ts's INTENTIONALLY_GRADUATED allowlist \u2014 that list covers 1-enables flags left ON. Read site: packages/engine-core/src/BasePlaywrightService.ts (maybeRebindScreencastToActivePage, called from invokeAction's boundary and from captureStarvationFallbackFrame between the latch and rung 2). Pure module: packages/engine-core/src/captureStarvationFallback.ts (evaluateTabRebind, describeTabBinding, classifyRebindTarget/isBlankRebindTarget, CAPTURE_FALLBACK_MAX_TAB_REBINDS_PER_RUN, CAPTURE_FALLBACK_REBIND_TIMEOUT_MS, CAPTURE_FALLBACK_TARGET_PROBE_TIMEOUT_MS). Claim runner.browser-unobservable-terminal. ROUND 3 (2026-09-11), wave-4 preflight, prod asess_1789096730801_ibhs2ezp (engine d4e21f2d1, the pilot's 25-step draft-completion plan): the ONLY rebind decision the whole run took was capture_fallback:rebind_declined {at:'invoke', reason:'blank_target', used:0, cap:2, ownerTab:'tab1', activeTab:'tab2', activeUrl:':'}, then frame_absent {tab_mismatch} -> stale_frame_refused {tab_mismatch, used:0} -> capture_missed 1/2/3 -> browser_unobservable, with ZERO would_rebind rows. blank_target sat at guard 7 of 9 and the enabled switch is guard 9, so =1 was a guaranteed NO-OP on precisely the tab_mismatch shape this flag exists for, and the shadow census measured that population (the app's action_loading_message=duplicate_request_loading transition, whose page.url() degenerates to ':') at zero. FIXED by deciding blank_target on the target's DOCUMENT rather than on the text of its URL (classifyRebindTarget): a known-blank URL (about:blank and its query/fragment forms, bare about:, data:, empty) is still refused synchronously with no read; a statable URL is still eligible with no read; an UNSTATABLE url (':', about:srcdoc, anything new URL() rejects) buys a CAPTURE_FALLBACK_TARGET_PROBE_TIMEOUT_MS=500ms page.evaluate of {readyState, bodyChildCount, title}, only when every guard before blank_target already said yes, AND AT MOST ONCE PER MISMATCH EDGE (memoised on captureLadderRunBudget.lastTabMismatch.probe, keyed on the url the read was taken for) \u2014 the third gate being what keeps the shadow default's 'changes NOTHING' claim true, since issued before the edge dedup the read was re-bought on every invoke of a standing mismatch for the life of the run while booking one row (#3148 review MED-1, measured: 25 invoke decisions => 25 reads / 12539 ms / 1 would_rebind; the run cap cannot bound it because the cap is only spent when a row IS booked). Memoised: a standing mismatch costs ONE read per run, a navigating target re-arms exactly one, a latched/capped/extension/pending-escape decision pays nothing, a single-tab run reaches no classifier at all \u2014 content => eligible (document_live), a settled contentless document => still refused (document_empty), NO ANSWER => ELIGIBLE (document_unreadable), because a read that cannot come back is a busy renderer while a blank page's renderer is idle and answers at once. Real-Chromium measurement (packages/engine-core/__tests__/BasePlaywrightService.rebindTargetProbe.integration.test.ts): a genuine window.open('') popup reports about:blank with {readyState:'complete', bodyChildCount:0, title:''} and is refused by BOTH routes; a hogged renderer answers nothing inside the bound. Every rebind row now carries targetBasis; pageUrlForCensus is deliberately unchanged so the honest degenerate activeUrl stays in the census. CONSEQUENCE FOR THIS GATE: gates (1), (3) and (6) are counted on rows that the pre-round-3 build never booked, so any census taken before 2026-09-11 under-counts the target population and must not be carried forward. GRADUATION GATE \u2014 flip defaultState to 'on' only when ALL of the following hold across >=5 consecutive nightly qa-exhaustive runner lanes AND >=2 ci:qa-targeted dispatches of the pilot's 25-step draft-completion plan on staging: (1) NO SPURIOUS FIRE \u2014 every capture_fallback:would_rebind row is preceded, in the same capture, by capture_fallback_frame_absent {reason:'tab_mismatch'} (ladder site) or by a session.page reassignment since the previous invoke (invoke site); ZERO rows on a decision whose recorder already owned the active page. (2) NO SINGLE-TAB FIRE \u2014 zero would_rebind rows on any run whose activeTab is 'tab1' throughout; a row there means the attribution predicate is wrong, not the binding. (3) TARGET PARITY \u2014 of the rows where a FOLLOWING tool_call recorded a post-state URL, 100% name the page that call acts on, and the denominator is published (the gate cannot be stated over all rows: on a wedged renderer the next tool call may never produce a post-state URL). (4) FORCED-ON EVIDENCE \u2014 on runner/tab-switch-busy-finale pinned =1, over >=8 runs: a landed capture_fallback:rebind {ok:true} on EVERY run, ZERO tab_mismatch rows of either kind (capture_fallback_frame_absent {reason:'tab_mismatch'} and capture_fallback_stale_frame_refused {refusal:'tab_mismatch'}), ZERO capture_degraded whose ownerTab !== activeTab, and the browser_unobservable:capture_missed.classification distribution published beside the =0 control (see the client-visible wording measurement in `graduation.gate`). Deliberately NOT 'at least one rescue': measured 2026-09-10 over 3 pinned runs, one run booked no starved capture at all (the agent's captures fell either side of the hog) \u2014 once the recorder follows the executor, WHETHER a capture starves is not a property that fixture controls, and gating on it would red a run that demonstrated the binding. (5) SHADOW-DEFAULT TRACE PARITY \u2014 runner/hang-busy-finale-static, runner/hang-busy-finale-transient and runner-blocked-synthesis book ZERO would_rebind rows and a ladder census identical to the pre-change one (rung order, used/cap, latch count, refusal reasons, terminal) across the same nights. (6) COST \u2014 the would_rebind count per run stays at or below CAPTURE_FALLBACK_MAX_TAB_REBINDS_PER_RUN, which is exactly what the virtual counter makes measurable in shadow. Enforcement before graduation exists ONLY as the pinned eval case's env. NAMED RESIDUALS, not closed here: noteCaptureObservability still classifies page_busy off session.lastScreencastFrameAt, which a mismatched run's OTHER tab stamps \u2014 the new recorderTab/activeTab fields make it detectable, and the =1 flip to unobservable is MEASURED and CUSTOMER-VISIBLE (it removes the 'alive but busy' clause from both the blocked run summary and the auto-filed issue), so it is gated by (4) rather than merely expected; and the extension lane's own tab_mismatch is deliberately unfixed and excluded from the census denominator by the extension_session refusal. PAIRED WITH EXECUTOR_BLANK_POPUP_ESCAPE (#3104b, added 2026-09-11), AND THE PAIRING IS AN OPERATIONAL CONSTRAINT ON THIS GATE. The rebind-p1 rehearsal (2026-09-11, tp_d1360115, D1 run_405d5842 / D2 run_0192b16a, 2/2) re-attributed the residual used:0 defect: on that shape the recorder is evaluated, PROBED and correctly left in place (rebind_declined {reason:'blank_target', targetBasis:'document_empty', activeUrl:':'}), and the run still dies browser_unobservable -- because the EXECUTOR (session.page) stays on a never-rendered popup and nothing moves it back. The popup answers page.evaluate in milliseconds while Page.captureScreenshot times out 3x3000 ms, i.e. it is a NEVER-RENDERED WINDOW rather than a busy renderer, which falsifies round 3's fail-open premise ('a read that does not come back means a BUSY page -- exactly the incident') ON THIS SHAPE and makes =1 a no-op on precisely the tab_mismatch population this flag exists for. That residual is fixed by EXECUTOR_BLANK_POPUP_ESCAPE, whose predicate deliberately inverts round 3's polarity for its own side (for the RECORDER 'don't know' means FOLLOW; for the EXECUTOR 'don't know' means DO NOT ABANDON -- both fail-safe). THE CONSTRAINT: NEVER force-ON EXECUTOR_BLANK_POPUP_ESCAPE during a CAPTURE_FALLBACK_TAB_REBIND graduation window, and never carry a census taken inside one forward. Under that flag's =1 the executor leaves the popup on the escaping invoke, so the tab_mismatch disappears and the rebind_declined / would_rebind rows this gate's (1), (3) and (6) are counted on VANISH on every subsequent invoke -- one flag's enforcement erases the other's census. The escape is ordered AFTER maybeRebindScreencastToActivePage precisely so the REBIND rows on the ESCAPING invoke stay byte-identical, but that parity covers the rebind rows on exactly one invoke -- not the ladder rows that would have followed on it, which under =1 legitimately vanish too -- and is not a substitute for keeping the windows disjoint."},EXECUTOR_BLANK_POPUP_ESCAPE:{key:"EXECUTOR_BLANK_POPUP_ESCAPE",envVars:["AGENTIQA_EXECUTOR_BLANK_POPUP_ESCAPE","AGENTIQA_EXPERIMENT_EXECUTOR_BLANK_POPUP_ESCAPE"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Move the EXECUTOR (session.page) off a popup that never rendered, close it, and return the run to the owner tab (#3104b). SHADOW-FIRST, AND THE SHADOW IS FREE -- THREE STATES, read through BOTH switch readers (killSwitchEnabled for enforcement, killSwitchForcedOff for detection). =0: DETECTION OFF TOO. The reader returns before touching anything -- no classifier, no probe, no census row, no run budget minted -- which is the operator's remedy if the measurement itself hurts, without a redeploy; killSwitchEnabled alone cannot express it because it cannot tell 'no env, so default-off' from 'an operator typed 0' (killSwitch.ts, the idiom REPEATED_SIDE_EFFECT_GUARD / REDIRECT_FLIPFLOP_GUARD / CAUSE_GROUNDING / LABEL_DRIFT_DISCLOSURE already use). UNSET (the shipped default, SHADOW): the predicate runs on the probe the rebind already memoised and emits executor_escape:would_escape; it pays NO settle wait and NO re-probe, so the invoke boundary is byte-identical in timing and in page.evaluate count to the pre-change build. NOTHING closes, nothing moves, session.page is untouched and no model-visible string changes. =1: pays the bounded settle, re-validates the session state the await could have changed, re-probes ONCE, and only then escapes. THE DEFECT, AND IT IS A RE-ATTRIBUTION OF #3104's. #3104 read used:0 as 'the recorder cannot move'. The rebind-p1 rehearsal (2026-09-11, tp_d1360115: D1 run_405d5842 / asess_1789128041968_0ib9b50y and D2 run_0192b16a, line-for-line identical, 2/2 reproduction) measured otherwise. Step 25 of the pilot's draft-completion plan clicks a pencil (edit) icon; the app answers with a window.open whose window never navigates and never paints. Chromium reports its URL as the degenerate string ':' and its document answers page.evaluate IN MILLISECONDS with {readyState:'complete', bodyChildCount:0, title:''}, while Page.captureScreenshot on it times out 3x3000 ms. The live rows: capture_fallback:rebind_declined {at:'invoke', reason:'blank_target', targetBasis:'document_empty', activeUrl:':', ownerTab:'tab1', activeTab:'tab2', used:0, cap:2} -> capture_fallback_frame_absent {reason:'tab_mismatch'} x3 -> capture_fallback_cdp_failed {elapsedMs:3000, timeoutMs:3000} x3 -> capture_fallback_stale_frame_refused {refusal:'tab_mismatch', used:0, cap:2} x3 -> browser_unobservable:capture_missed {streak:1|2|3} -> the terminal, with an auto-filed ENVIRONMENT issue blaming our browser. THE REBIND'S REFUSAL IS CORRECT AND IS NOT WEAKENED ANYWHERE. targetBasis:'document_empty' proves the #3148 probe ran and the document ANSWERED; re-pointing the recorder at an empty document would blank the run video AND let the ladder serve empty frames as ownerTab === activeTab evidence, which docs/VERDICT-GATES.md already records as strictly worse than the refusal it replaces. What it also proves is that #3148's fail-open premise ('a read that does not come back means a BUSY page -- exactly the incident') does NOT hold on this shape: the popup is a NEVER-RENDERED WINDOW, not a busy renderer, so CAPTURE_FALLBACK_TAB_REBIND=1 is a no-op on precisely the tab_mismatch population it exists for. The residual defect is therefore that the EXECUTOR stays on the dead popup and nothing in the engine ever moves it back -- which no recorder rebind can fix, by construction. WHY NO EXISTING PATH REACHES IT. Every 'get off this popup' mechanism is keyed on WHERE the popup points or on the MODEL noticing, never on WHETHER THE POPUP IS A PAGE AT ALL: containment (containPendingPopupEscapes) is keyed on isCrossEnvironmentEscape(appUnderTestOrigin, url), and ':' has no origin to compare, so pendingPopupEscapes stays EMPTY and the drain returns null -- pending_escape is a CROSS-ENVIRONMENT concept, not a LIVENESS one; the popup's own on('close') restore needs the popup to close, and this one never does; the incidental-external-login lane keys on a recognisable IdP URL; switch_tab/close_tab are model-driven and all three invokes returned a capture error with NO screenshot, so the model had no picture of a blank tab to reason from. THE MECHANISM, and every piece of it is copied from a path that already exists. One pure predicate, evaluateBlankPopupEscape (captureStarvationFallback.ts), called from maybeEscapeBlankPopupExecutor at the invoke boundary immediately AFTER maybeRebindScreencastToActivePage(session,'invoke'). GUARDS, in order, each with a reason in the census: no_popup (FIRST and free -- the executor is not on a live tab2 that is also the active tab, so a single-tab run costs one identity comparison and reads nothing); no_owner (tab1 is not live, or is the popup -- there is nowhere to escape to and closing the run's only surface is worse than staying); extension_session (the whole mechanism is scoped OUT of the wallet lane, where tab1 IS the extension page and an executor on tab2 is the lane's design -- ONE row per RUN, not per invoke); pending_escape (containment owns the popup and closes it at the invoke tail; two mechanisms closing one page is how a page ends up closed under an in-flight captureState); restart_in_flight (a restartScreencast is already mutating the session -- placed BEFORE the target guards here, which DIVERGES from evaluateTabRebind's placement, because what follows is not a 500 ms read but a 3 s settle wait and paying that inside a restart race is the #3121 cost regression); url_not_degenerate (require classifyRebindTarget({url}).basis === 'document_unreadable', i.e. isUnstatableUrl true AND isKnownBlankUrl false -- this DELIBERATELY EXCLUDES about:blank and data:, which is the window.open('')+location= in-flight shape and the single largest false-positive population, and excludes every statable URL); document_live (the probe answered and there is content); document_unreadable (THE PROBE DID NOT ANSWER, AND THAT IS A REFUSAL -- the EXACT OPPOSITE POLARITY to classifyRebindTarget's documented fail-open, and deliberate: that predicate answers 'may the RECORDER follow?', where 'don't know' means follow because the cost of not following is a dead ladder and the cost of wrongly following an unreadable-but-live page is nil; this guard answers 'should the EXECUTOR abandon?', where 'don't know' means DO NOT ABANDON because a renderer that cannot answer is a BUSY page, the executor belongs on a busy page, and abandoning one is destructive and irreversible. Both are fail-SAFE for their own side; the asymmetry is pinned by a dedicated unit test so a future reader 'fixing the inconsistency' trips a red test); the FIVE POST-SETTLE reasons, distinct on purpose and =1-only (an earlier draft booked one 'settled' for all of them, which made the graduation gate's 'the guard is live' evidence a union of a rescue and its opposite -- a popup that CLOSED during the wait reads back from a dead page as document_unreadable, so a window that DIED satisfied a gate whose sentence is 'the settle wait actually rescues a late-rendering popup'): document_live_after_settle (it rendered -- THE rescue) and popup_navigated (it committed a STATABLE url while we waited -- also a rescue, and it deliberately SUBSUMES the url guard on pass 2, because 'became a page' is not the same fact as 'was never a candidate') are the two the gate counts; popup_url_blanked (the url went KNOWN-BLANK rather than statable: about:blank, bare about:, data:, empty -- NOT a rescue, and counting it as one is what the #3165 round-2 review caught, since about:blank over an empty document is the in-flight window.open('')+location= shape guard 6 exists to hold off and '' is not a navigation at all, so gate (4) would have been satisfiable by a window that still never rendered), document_unreadable_after_settle (the renderer went busy -- not a rescue, not a death), popup_closed (the window died on its own under the wait) and state_moved (session.page/tab2/activeTab no longer name this popup: context.on('page') promoted a peer, the popup's own on('close') restored tab1, or the model switched tabs -- the pass-1 world is gone, so nothing may be closed on the strength of it) are NOT; capped (EXECUTOR_BLANK_POPUP_MAX_ESCAPES_PER_RUN = 2, latched on the RUN-keyed captureLadderRunBudget so neither a mid-run bring-up nor the peer sweep refunds it); and shadow LAST, which is what makes the census readable -- reason:'shadow' means every other condition said yes on PASS 1 and only the flag said no, so a would_escape row is one-for-one with an escape the enforcing build would have ATTEMPTED. It is NOT one-for-one with one it would have PERFORMED: the enforcing build then pays the settle wait, which can still rescue the popup, so the shadow count is an UPPER BOUND on the enforced count and the graduation gate reads it as one. That is the stated price of a free shadow, and it is the honest trade -- the alternative is a default that inserts up to 3 s of wall clock at the invoke boundary, ahead of the action, on a build that cannot act on the answer. THE SETTLE WAIT is the false-positive guard and the reason this shape beat the alternative of never promoting a blank popup at all: a popup withheld from promotion has no promotion-on-navigate path in this engine (newPage.on('framenavigated') feeds containment only), so an OAuth window that paints 800 ms later would be PERMANENTLY ORPHANED -- a SILENT wrong-surface run, strictly worse than today's legible terminal. EXECUTOR_BLANK_POPUP_SETTLE_MS = 3000 (= CAPTURE_FALLBACK_CDP_TIMEOUT_MS, the bound rung 2 already spends on this same renderer), implemented as Promise.race([popup.waitForLoadState('load'), sleep]) plus one re-probe, PAID ONLY UNDER =1 -- it is not a measurement, it is part of performing the escape safely, and nothing closes in shadow -- and MEMOISED ON THE POPUP IDENTITY ALONE on the run budget, with the record surviving the executor LEAVING the popup, so a blank popup costs ONE settle and ONE re-probe for the whole run -- standing, tab-toggled, or changing its degenerate url under us. TWO drafts got that bound wrong and both are recorded because the shape repeats: the first dropped the memo on every invoke where the executor was elsewhere, so switch_tab('tab1') then back re-paid the full 3 s and re-booked a row on every return; the second keyed it on (popup, url), so a popup whose DEGENERATE url changed (':' -> about:srcdoc -> ...) re-armed a full wait plus a page.evaluate every time. Both were unbounded for the same reason -- a post-settle refusal never spends the run cap, the popup stays and the edge survives -- while gate (5) and docs/VERDICT-GATES.md both claimed 'one settle wait and one extra page.evaluate per popup per run'. Keyed on identity that sentence is literally true and gate (5) is measurable, and nothing is lost: a popup that became a real page is refused earlier AND more cheaply at guard 6, and a RELAPSE is a new Page object with a fresh record. After the await the session is RE-VALIDATED before anything is closed (session.page/tab2/activeTab still this popup, and the popup still open), because context.on('page') and the popup's own on('close') are live concurrent mutators of exactly those fields and routinely run inside that window; a mismatch books popup_closed / state_moved and moves nothing. It is also a second buffer behind the first: the decision is taken at the NEXT invoke boundary, i.e. after a full LLM turn has already elapsed. THE MOVE is copied verbatim in shape from containPendingPopupEscapes: session.page = session.tab1; activeTab = 'tab1'; tab2 = undefined; needsFullSnapshot = true; then popup.close(). Restore BEFORE close, so the popup's own on('close') handler finds nothing to do and cannot race; revertScreencastTargetOnPageClose still runs and is a no-op (the recorder never left tab1, it declined). EVERY NON-BROWSER EFFECT IS COMMITTED BEFORE THE CLOSE and THE CLOSE ITSELF IS BOUNDED (EXECUTOR_BLANK_POPUP_CLOSE_TIMEOUT_MS = 1500, the same Promise.race + unref shape the settle uses). It was the mechanism's only unbounded await and its only destructive one, made on precisely the population measured to stop answering (page.evaluate answers in ms while Page.captureScreenshot times out 3x3000 ms), and it sits AHEAD of dispatch -- so a close that HUNG (which the catch never covered; it covers a rejection) hung the whole action, with the session already moved and the note staged AFTER it, i.e. the tab block gone, the executor moved and NO note. Staged first and bounded, a hung close costs 1500 ms, leaves the executor on the owner tab WITH the note, and books executor_escape:declined {reason:'close_timeout'} beside the blank_popup row (which also carries closeOk) -- the one population where a Page object outlives the escape, and countable rather than silent. A REJECTED close is 'already closed', not a timeout. MODEL-VISIBLE, =1 ONLY, unit-pinned (buildExecutorEscapeNote) AND PINNED END-TO-END, which is the part the first draft got wrong: the prose rides metadata.note -- the ONLY channel that reaches the model, because BrowserActionExecutor builds the LLM-facing response from an explicit per-field allowlist and a bespoke metadata key is invisible to the agent no matter how it is worded (the draft wrote metadata.executorEscapeNote, nothing read it, and deleting the whole merge failed ZERO tests) -- appended to any note the action already produced, never replacing it, and never an error, since the action itself succeeded. The structured metadata.executorEscape {kind:'blank_popup', closedUrl, settleMs} is allowlisted into BOTH the persisted tool_call actionArgs and the response, the treatment crossEnvironmentContainment and incidentalExternalLogin already get, so a destructive tab move is queryable in the trace rather than only inferable from free text. It is staged into a LOCAL at the boundary and attached at the single tail return site, so an action that throws cannot carry it onto the next one. The note NAMES AGENTIQA AS THE ACTOR AND FORBIDS GRADING FROM THE CLOSURE ('Agentiqa closed a blank popup that opened during this run ... THE CLOSURE WAS THE TEST ENGINE'S RECOVERY, NOT THE APPLICATION'S BEHAVIOUR -- do not treat the popup opening or closing as evidence for any criterion; re-observe the original page instead'), states the OBSERVATION ('never rendered any content'), invites the finding explicitly ('this is a defect of the application under test: report it rather than retrying the same control'), and explains that there is no longer a second tab -- because after the escape session.tab2 is undefined and the {activeTab, tabCount, tabUrls} block the invoke tail injects VANISHES mid-run, which unexplained is a hallucination source. THE ACTOR CLAUSE IS THE FIX FOR THE ONLY FALSE-passed PATH THIS MECHANISM OPENS (#3165 round-2 review): the first draft's passive voice ('it has been closed and you are back on the page you were on') never said WHO closed the window, so a step or criterion phrased about the window going away ('the dialog closes', 'no extra tab remains', 'the popup is dismissed') was gradable passed FROM OUR OWN ACTION with the note as its supporting observation -- the evidence-laundering class alternative (c) was rejected for, reached through the prose instead of the capture. closedBy:'engine' on the structured field carries the same fact without the prose, so an audit can filter on it. Every clause is GROUNDED in something the engine measured: 'the window the previous action opened' and 'it produced no picture at all' were both dropped, because the escape fires at an invoke boundary rather than at the popup's birth (a boundary that refused on document_unreadable leaves it to the NEXT one, and context.on('page') promotes windows no action opened) and it never consults capture or frame state. The reported URL is truncated at EXECUTOR_ESCAPE_NOTE_URL_MAX = 120 -- pageUrlForCensus stays verbatim for the diag row, but the same string is a MODEL input and the population is by construction something new URL rejects. The wording is the mitigation for this mechanism's strongest objection (that closing the window suppresses a real product finding), so it is pinned by a test rather than left to drift. The counterfactual answers the rest of that objection: TODAY the run does not file that finding either -- it dies browser_unobservable and auto-files an ENVIRONMENT issue blaming OUR browser, losing every grade the run had earned. The escape strictly improves attribution. ORDERED AFTER THE REBIND, and the order is load-bearing twice: the #3104/#3148 REBIND rows on the escaping invoke (capture_fallback:rebind_*) stay BYTE-IDENTICAL in both flag states (pinned by a parity test), which is what lets a blank_popup row be read back against the rebind_declined{blank_target} row that justified it; and the document probe the rebind memoised on lastTabMismatch.probe is REUSED, so on the incident shape DETECTION buys ZERO additional page.evaluate. THE PARITY CLAIM COVERS THE REBIND ROWS AND NOTHING ELSE: the capture ladder has not run when this decision is taken, so under =1 the capture_fallback_frame_absent{tab_mismatch} / capture_fallback_cdp_failed / capture_fallback_stale_frame_refused{tab_mismatch} rows that would have followed on that same invoke -- and every rebind row on SUBSEQUENT invokes -- legitimately do not appear, because the executor is back on the owner tab and there is no mismatch left to book. That is the fix, and it is the documented divergence. ENFORCEMENT'S OWN COST, stated rather than rounded to zero and now actually enforced by the identity-keyed memo: one extra page.evaluate (the post-settle re-probe) and one bounded wait of up to 3000 ms, PER POPUP PER RUN, inserted at the invoke boundary ahead of the action -- plus, only on an escape that actually fires, up to 1500 ms for the bounded close. OPERATIONAL COUPLING, stated rather than discovered: NEVER force-ON EXECUTOR_BLANK_POPUP_ESCAPE during a CAPTURE_FALLBACK_TAB_REBIND graduation window. Two flags on the same population, one of which erases the other's census -- #3104's gates (1), (3) and (6) are counted on rows this flag's enforcement removes. Recorded in both flags' notes and to be recorded in docs/FLAGS-STATE.md whenever either is set live. CENSUS: executor_escape:would_escape {at, popupUrl, ownerUrl, basis, settleMs, used, cap} (shadow only); executor_escape:blank_popup {at, closedUrl, ownerUrl, basis, settleMs, used, cap} (=1 only); executor_escape:declined {at, reason, basis, popupUrl, ownerUrl, settleMs, used, cap} (both live states; reason is one of no_popup / no_owner / extension_session / pending_escape / restart_in_flight / url_not_degenerate / document_live / document_unreadable / capped in either, plus the =1-only post-settle six: document_live_after_settle, popup_navigated, popup_url_blanked, document_unreadable_after_settle, popup_closed, state_moved -- and close_timeout, which the predicate never returns: it is booked AFTER a successful escape when the bounded popup.close() did not confirm), edge-deduped on (popup identity, outcome) exactly like evaluateTabRebind's lastTabMismatch.decided set, so a standing blank popup is ONE row rather than one per invoke and a MOMENTARY refusal cannot become the popup's permanent answer. settleMs is 0 on every shadow row by construction -- shadow waits for nothing. Under =0 there is no census at all. VERDICT-WIDENING, DELIBERATELY, and =1-ONLY: a run that previously reached browser_unobservable with NO verdict now reaches a gradable capture of the owner tab, so the step can book a genuine passed OR a genuine failed -- both honest, because after the escape ownerPage === session.page === tab1 is a FACT and the capture is an honest capture of the owner page taken at a moment when the executor is genuinely on it. The escape moves the executor; it NEVER relabels evidence and never rewrites a grade the model gave -- it removes the PRECONDITION for the terminal. Stated precisely, because 'never changes a verdict' would be too strong: the note it attaches IS A MODEL INPUT on the escaping action, exactly like any other tool-result note (the incidental-external-login note, the degraded-capture label), and it deliberately invites a finding about the application under test. What the mechanism never does is re-grade a step, re-observe an action on a different surface, or relabel evidence; what it does do is tell the model a true thing about the window it was driving, and the model's next turn is free to act on it. Recorded on the two-sided browser_unobservable entry in docs/VERDICT-GATES.md. NOT CHANGED by either setting: tab safety (frames still carry ownerPage and both rung predicates still read the FRAME's owner), the ladder's rung order, the stale budgets, the latch, the capture-miss streak, and the terminal's bound and wording. WHEN NOT ENFORCING the engine is byte-identical to the pre-change build in everything the model or the page can observe: no page is closed, session.page/activeTab/tab2/needsFullSnapshot are untouched, and no metadata field or model-visible string is added. In SHADOW (unset) that extends to timing and to the page.evaluate count on the incident shape -- the only new artifact is the executor_escape:* census. Under =0 not even the census exists.",designDoc:"packages/engine-core/src/captureStarvationFallback.ts",status:"active",added:"2026-09-11",graduation:{status:"gated",gate:"GRADUATED 2026-09-13 (default ON). Flip defaultState to 'on' \u2014 all six conditions met on the mirror: (1) FORCED-ON MIRROR PASS: 4/5 runs passed on tp_d1360115 (E1, E3, E4, E5 in rehearsal-2026-09-11-escape-p1); (2) SHADOW CENSUS ON THE SAME SHAPE: 4/5 clean passes without popup in rehearsal-2026-09-13-escape-shadow + 1 clean pass in 09-12-rc Pass R, and on runs where popup appeared under shadow (S1 09-11, D1 rebind-p1, D2 rebind-p1), executor_escape:would_escape was booked on step 25; (3) ZERO SPURIOUS FIRE: zero would_escape/blank_popup rows on 17 single-tab plans of the mirror; (4) FALSE-POSITIVE GUARD IS LIVE: executor_escape:declined[capped] verified in E2; (5) MODEL HONESTY INVARIANT: zero step-25 criteria results citing popup lifecycle; (6) COST: cap 2/run held. Kill switch: AGENTIQA_EXPERIMENT_EXECUTOR_BLANK_POPUP_ESCAPE=0.",evidence:"Rehearsal reports ~/agentiqa-agents/lio-harvest/reports/rehearsal-2026-09-11-escape-p1.md (4/5 passed under =1 on tp_d1360115, 0 spurious escapes on 17 single-tab plans, 0 criteria citing popup lifecycle, cap 2 held) and rehearsal-2026-09-13-escape-shadow (5-replicate shadow census). Unit suites: packages/engine-core/src/__tests__/captureStarvationFallback.blankPopupEscape.test.ts and packages/engine-core/src/__tests__/BasePlaywrightService.blankPopupEscape.test.ts. Registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED.",owner:"steering (Alex)",review:"2026-10-01"},notes:"GRADUATED default-ON 2026-09-13 (1-enables, defaultState on, killSwitch-enabled). Listed in killSwitchDefaultState.test.ts's INTENTIONALLY_GRADUATED allowlist. Kill switch is =0, which disables detection and enforcement (killSwitchForcedOff). Read site: packages/engine-core/src/BasePlaywrightService.ts (maybeEscapeBlankPopupExecutor, called from invokeAction's boundary immediately after maybeRebindScreencastToActivePage(session,'invoke')). Pure module: packages/engine-core/src/captureStarvationFallback.ts (evaluateBlankPopupEscape, buildExecutorEscapeNote, EXECUTOR_BLANK_POPUP_MAX_ESCAPES_PER_RUN, EXECUTOR_BLANK_POPUP_SETTLE_MS), which also owns the run-budget fields executorEscapesUsed / executorEscapeExtensionReported / lastBlankPopup on CaptureLadderRunBudget. Claim runner.browser-unobservable-terminal."},SCREENCAST_KEEPALIVE:{key:"SCREENCAST_KEEPALIVE",envVars:["AGENTIQA_SCREENCAST_KEEPALIVE","AGENTIQA_EXPERIMENT_SCREENCAST_KEEPALIVE"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Screencast starvation watchdog: keepalive capture vs full teardown. A page with no compositor activity emits no Page.screencastFrame, so an ordinary LLM turn on a settled page was indistinguishable from a broken CDP session \u2014 and the watchdog answered BOTH with a full teardown + re-attach (stopScreencast -> detach -> newCDPSession -> startScreencast -> captureScreenshot). Measured on the 2026-08-11 Lio prod burst: 30 screencast_restart events across 11 of 15 sessions, 8 of which had the 1-2 a quiet page produces; the common case was paying the rare case's price in five CDP round-trips to the very renderer that, in the rare case, is unwell. When ON, a quiet screencast is kept alive with ONE bounded Page.captureScreenshot on the EXISTING CDP session \u2014 which is also the keepalive frame the recorder (CloudScreencastService: run video + live view) wants, so video is unaffected \u2014 and the teardown runs only when that capture fails, i.e. when the session is genuinely broken. The keepalive stands down while an evidence capture is in flight (never queue a second full-page capture behind the agent's own) and cannot re-enter itself. OFF restores the unconditional teardown on starvation. Writes NO verdict on either setting. NOTE: the TIMEOUT BOUNDS on the teardown path (newCDPSession / stopScreencast / detach / startScreencast / bootstrap capture) are UNCONDITIONAL and not gated by this flag \u2014 an unbounded await there could strand screencastRestarting=true forever, silently disabling the watchdog and stopping the video for the rest of the run, which is a bug under any setting.",designDoc:"packages/engine-core/src/BasePlaywrightService.ts",status:"active",added:"2026-08-11",notes:"Ships default-ON from inception \u2014 it strictly reduces CDP churn and adds no verdict path, so there is no pre-change OFF behavior worth preserving as the default. Registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED. Read site: packages/engine-core/src/BasePlaywrightService.ts (the startScreencast watchdog tick -> keepScreencastAlive). Claim runner.screencast-keepalive-not-teardown."},STEP_REPLAY_DISCLOSURE:{key:"STEP_REPLAY_DISCLOSURE",envVars:["AGENTIQA_STEP_REPLAY_DISCLOSURE","AGENTIQA_EXPERIMENT_STEP_REPLAY_DISCLOSURE"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:`Step-replay disclosure (RunnerRuntime / test-plan runs only). Motivating incident: prod asess_1786419613237_4wtdj14q (Lio CI, 2026-08-11) \u2014 our capture pipeline went dark at step 25 (screencast frame_starvation + two "page.screenshot: Timeout 5000ms exceeded" action errors, a browser_unobservable streak of 2 against a bound of 3, so no terminal fired), the agent read the blindness as a page hang and reloaded, the reload discarded the unsaved draft, and the Supervisor issued REDIRECT "re-perform steps 7-24". The run then reported status=passed with all 25 steps passed after 192 LLM calls / 6.1M prompt tokens \u2014 36% of that 15-plan burst's tokens \u2014 with the replay visible nowhere in the customer-facing record. When on, the runtime counts a REPLAY at the mutating-dispatch seam (a click/type/credential-fill/focused-value/key-combination/drag/upload under an action or setup step N when step N had already dispatched one AND some step > N had too; verify steps are excluded from disclosure and log step_replay_detected with suppressedReason="verify_step" for census (#3477), and observation, navigation and run_js are excluded so a runtime-ordered re-verify or an ordinary back-navigation never counts), and at the END of the run_complete cascade prepends one disclosure sentence to run.summary, appends a note to each replayed step's result (never replacing the model's note) stating that the agent went back to earlier steps on its own and each step was graded once at the end (#3477), and emits step_replay_disclosed. Writes NO verdict: not a run status, not a step status, not a criterion \u2014 each step is graded once in run_complete, and what was dishonest was the run reporting as if nothing had happened. Off \u21D2 no counting, no sentence, no note, byte-identical to pre-change behavior.`,designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-08-11",notes:"Ships default-ON from inception \u2014 the disclosure adds a record where there was none and changes no verdict in either polarity, so there is no pre-change OFF behavior to preserve. Registered in killSwitchDefaultState.test.ts INTENTIONALLY_GRADUATED. Detection + sentence in packages/engine-core/src/runner/stepReplayDisclosure.ts; the kill-switch read + wiring live at packages/engine-core/src/RunnerRuntime.ts (the mutating-dispatch seam and applyStepReplayDisclosure in handleRunComplete). Runner lane only. Claim runner.replay-not-laundered-into-pass."},PREDICATE_BASIS_COMPILE:{key:"PREDICATE_BASIS_COMPILE",envVars:["AGENTIQA_PREDICATE_BASIS_COMPILE","AGENTIQA_EXPERIMENT_PREDICATE_BASIS_COMPILE"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:`Trust-layer predicate basis \u2014 Slice B AUTOFORMALIZATION COMPILER (design docs/plans/2026-07-26-trust-layer-predicate-basis-design.md; north star docs/plans/2026-07-26-trust-layer-verification-architecture.md). "Compile-don't-parse": at AUTHORING an LLM COMPILES a natural-language assertion into a typed Slice-A logical form (Predicate) over named observables \u2014 autoformalization (utterance \u2192 logical form \u2192 executor \u2192 denotation) \u2014 NOT a verdict. Slice A shipped the schema + generic deterministic executor; Slice B produces the forms the executor denotes. TASK-BLIND: the compiler compiles the assertion's MEANING (including any pinned expected value the assertion text itself carries) and never sees the observed screen or pass/fail \u2014 the firewall keeps the LLM out of the DECISION (Slice A's deterministic executor owns pass/fail). GRAMMAR-CONSTRAINED \u2192 MECHANICAL ABSTAIN: the model may only emit an in-grammar kind (count/delta/absence/modification/presence/typed) or the explicit not_groundable escape; compileToPredicate then STRICTLY validates the chosen kind's operands, so an inherently-subjective assertion ("the agent responds correctly", "looks clean") or an in-grammar kind with missing/invalid operands routes to a first-class ABSTAIN (AbstainNode) \u2014 NEVER a forced/invalid form. The grammar's expressiveness IS the verifiable/subjective boundary. v1 = structured output + strict schema validation + a groundability decision; full constrained decoding (PICARD) is a later refinement (TODO). GRADUATED default-ON 2026-07-27 (alongside PREDICATE_BASIS_VERIFY, which is the live consumer that routes a compiled form to a step result); the emergency kill-switch is retained and byte-identical WHEN FORCED OFF: the seam (runAssertionCompile / runRedundantCompile) short-circuits to \`disabled\` (ZERO model calls) when AGENTIQA_PREDICATE_BASIS_COMPILE=0 \u2014 and with COMPILE forced off but VERIFY on the compiler returns a disabled result \u2192 ABSTAIN (safe, never a manufactured verdict). Cost when on: one auxiliary structured-output call per compiled assertion (cost-isolated via emitAuxiliaryLlmUsage \u2014 not a billable step), thinkingBudget:0, hard-capped by a per-call timeout; fail-closed (error/empty/timeout/out-of-grammar \u2192 ABSTAIN).`,designDoc:"docs/plans/2026-07-26-trust-layer-predicate-basis-design.md",status:"active",added:"2026-07-26",notes:"GRADUATED 2026-07-27 (default ON) \u2014 the authoring-side ARc compiler that feeds the LIVE verify wiring (PREDICATE_BASIS_VERIFY, graduated in the same PR). Turn-on = BOTH flags ON: with VERIFY on, runPredicateBasisVerify calls runRedundantCompile, which rides THIS flag; on \u21D2 the compiler produces the logical forms the deterministic executor denotes. Slice B (authoring-side compiler) + Slice C (ARc redundant-compile: compile k\xD7 \u2192 ABSTAIN on disagreement/degeneration, attacking the factKind-wrong-kind gap) are the compile consumers of this flag. GRADUATION EVIDENCE: the flagship live-replay benchmark (PR #1879) returned GATE=GO \u2014 zero-regression on the prod Lio (semantic-reference) + Miro (canvas) corpora, the firewall intact (a groundable contradiction FAILS; nothing ungroundable is ever a hard PASS), the vision/canvas path abstains, and the #1876 DOM-reachability fix live-confirmed. TASK-BLIND / FIREWALL (unchanged): the compiler compiles the assertion's MEANING (never sees the observed screen nor pass/fail); Slice A's deterministic executor owns the decision. GRAMMAR-CONSTRAINED \u2192 MECHANICAL ABSTAIN: an inherently-subjective or out-of-grammar assertion routes to a first-class ABSTAIN (AbstainNode), never a forced/invalid form. Off (explicit AGENTIQA_PREDICATE_BASIS_COMPILE=0) is byte-identical to pre-graduation: runAssertionCompile / runRedundantCompile short-circuit to `disabled` with ZERO model calls, and with COMPILE off but VERIFY on the compiler returns a disabled result \u2192 ABSTAIN (safe, never a manufactured verdict) \u2014 the emergency kill-switch still works. killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND PREDICATE_BASIS_COMPILE listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Read site: packages/engine-core/src/predicateBasis/compile.ts (runAssertionCompile) + packages/engine-core/src/predicateBasis/redundantCompile.ts (runRedundantCompile). Cost when on: one auxiliary structured-output call per compiled assertion, cost-isolated via emitAuxiliaryLlmUsage (not a billable step), thinkingBudget:0, hard-capped, fail-closed (error/empty/timeout/out-of-grammar \u2192 ABSTAIN). Acceptance tests: packages/engine-core/src/__tests__/predicateBasisCompile.test.ts + predicateBasisRedundantCompile.test.ts. Compile-correctness benchmark: e2e/benchmark/compileCorrectness.ts. Binds claims verify.predicate-basis-compile-correctness + verify.predicate-basis-redundant-compile. STAGING-FIRST: merging to staging makes this default-ON on the STAGING engine only; PROD stays OFF until a later staging\u2192main release carries it (a natural staged soak). PREDICATE_BASIS_CONSENSUS (Slice D vision-consensus) stays default-OFF \u2014 vision-consensus is safe ONLY as abstain, not turn-on-ready (the P2 dense-abstain finding)."},PREDICATE_BASIS_CONSENSUS:{key:"PREDICATE_BASIS_CONSENSUS",envVars:["AGENTIQA_PREDICATE_BASIS_CONSENSUS","AGENTIQA_EXPERIMENT_PREDICATE_BASIS_CONSENSUS"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:'Trust-layer predicate basis \u2014 Slice D: (1) CONSENSUS EXTRACTION/RESOLUTION and (2) the SEMANTIC-TOLERANCE FALLBACK TIER (design docs/plans/2026-07-26-trust-layer-predicate-basis-design.md \u2014 \xA7 three buckets bucket-2 canvas "task-blind extraction + deterministic comparison + consensus-or-abstain", \xA7 the @reference resolution consensus/abstain, \xA7 "Semantic as a fallback tier, not a peer primitive"; north star hard-part #1 "the extractor itself can be wrong: miscount a canvas, misread"). CONSENSUS: task-blindness removes confirmation bias but NOT perception error (the model genuinely miscounts a canvas / mis-resolves "the cart total"); the no-false-positive mitigation is N INDEPENDENT task-blind extractions must AGREE, else ABSTAIN \u2014 one mechanism serving BOTH the vision/canvas path AND the semantic-reference resolution risk. SEMANTIC-TOLERANCE: run the DETERMINISTIC executor FIRST; invoke the task-blind concept classifier ONLY when the deterministic comparison cannot decide (tolerance:semantic OR an inconclusive comparison) \u2014 confident semantic = Verified, ambiguous = Assessed (a lean + confidence, never a grounded badge, never a manufactured pass \u2014 the bucket-3 Assessed slot). SHADOW / behavior-neutral even when ON: NOT wired into any live RunnerRuntime verdict path \u2014 the two drivers are consumed only by the consensus benchmark + unit tests. Off \u21D2 ZERO extractor/classifier calls, byte-identical; Slice-A deterministic parity is UNAFFECTED (the semantic tier COMPOSES the untouched evaluate, only reached on an inconclusive result).',designDoc:"docs/plans/2026-07-26-trust-layer-predicate-basis-design.md",status:"active",added:"2026-07-26",notes:"Trust-layer predicate basis Slice D (consensus extraction/resolution + semantic-tolerance fallback tier). Default OFF; SHADOW / behavior-neutral even when ON \u2014 the two drivers produce would-be results and are wired into NO live verdict path (consumers: the consensus benchmark + unit tests). Read sites: packages/engine-core/src/predicateBasis/consensusExtract.ts (runConsensusExtraction \u2192 killSwitchEnabled('PREDICATE_BASIS_CONSENSUS'); zero extractor calls when off) + packages/engine-core/src/predicateBasis/semanticTolerance.ts (runSemanticTolerance \u2192 killSwitchEnabled('PREDICATE_BASIS_CONSENSUS'); zero classifier calls when off). CONSENSUS-EXTRACT: a PURE reducer (reduceNumericConsensus / reduceCategoricalConsensus \u2014 N observations \u2192 accept-on-agreement / ABSTAIN(extraction_disagreement) on numeric-spread-beyond-tolerance or different presence/text; fail-closed extraction_error when no usable sample) + a pure per-frame read (readObservable over count-of / presence-of / value-at) + the thin flag-gated driver over the EXISTING StateExtractor seam (reused, not re-implemented \u2014 N independent samples). SEMANTIC-TOLERANCE: a PURE reducer (resolveSemanticTier \u2014 deterministic-first; decided deterministic \u2192 Verified with no classifier call; inconclusive + eligible + injected classifier \u2192 semanticToSpectrum: confident match/contradiction \u2192 Verified, ambiguous \u2192 Assessed, abstain \u2192 the Inconclusive floor) + the flag-gated driver that COMPOSES the untouched Slice-A evaluate (Slice-A parity byte-identical by construction). Reuses the Slice-2 concept-classifier seam (ConceptClassifier / semanticToSpectrum / renderObservation from groundedStateUnified.ts). Deliberately NOT re-exported from predicateBasis/index.ts (imported directly). Acceptance tests: packages/engine-core/src/__tests__/predicateBasisConsensus.test.ts (deterministic \u2014 injected observations for the consensus reducer; injected classifier RESULT for the semantic tier; flag-OFF zero-calls; fail-closed error\u2192abstain). Consensus benchmark: e2e/benchmark/consensusExtraction.ts (deterministic reducer/tier self-validation always runs; the live N-extraction + classifier measurement is PENDING until a keyed run \u2014 Slice-1/B/C precedent, no fabricated number). Binds claim verify.predicate-basis-consensus-extraction. Slice E (abstain-rate as a first-class COVERAGE metric on a real-assertion corpus) is BUILT: packages/engine-core/src/predicateBasis/coverage.ts (aggregateCoverage \u2014 a PURE reducer folding pipeline-routed assertion outcomes into the Verified/Assessed/Inconclusive distribution + abstain rate + abstain-origin/per-kind breakdown), wired into NO live verdict path (no new flag; the live measurement reuses THIS flag + PREDICATE_BASIS_COMPILE), claim verify.predicate-basis-coverage-abstain-rate, benchmark e2e/benchmark/coverage.ts (deterministic layers real; live full-pipeline number PENDING until keyed). This COMPLETES the predicate-basis v1 build; the remaining work is the KEYED phase (live LLM measurements) + the prod Miro+Lio final gate.",graduation:{status:"gated",gate:"Slice D is shadow / behavior-neutral (two drivers, wired into NO verdict path). Graduation gates on: (1) the consensus benchmark run WITH a model key showing consensus ABSTAINS on genuinely disagreeing/noisy N task-blind extractions and ACCEPTS on agreement (the miscount-a-canvas / mis-resolve-a-reference no-false-positive mechanism), plus a measured abstain-rate on the vision/canvas + semantic-reference corpus; (2) the semantic-tolerance tier measured deterministic-first (a deterministic-decidable case never consulting the classifier) with confident\u2192Verified / ambiguous\u2192Assessed; (3) the engine-core predicateBasisConsensus unit suite green. This is the DEFERRED consensus-extraction that gates ANY vision-path live graduation; the FINAL gate is the prod Miro (canvas) + Lio (semantic-reference) benchmark.",evidence:"e2e/benchmark/artifacts/benchmark-consensus-extraction.{json,md} (consensus abstain/accept + abstain-rate + semantic-tier bands; PENDING until a keyed run) + the engine-core predicateBasisConsensus unit suite + the claim verify.predicate-basis-consensus-extraction",owner:"steering (Alex)",review:"2026-09-05"}},PREDICATE_BASIS_VERIFY:{key:"PREDICATE_BASIS_VERIFY",envVars:["AGENTIQA_PREDICATE_BASIS_VERIFY","AGENTIQA_EXPERIMENT_PREDICATE_BASIS_VERIFY"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Trust-layer predicate basis \u2014 Slice F LIVE VERIFY WIRING (the FIRST non-shadow slice; design docs/plans/2026-07-26-trust-layer-predicate-basis-design.md \xA7 three buckets; north star docs/plans/2026-07-26-trust-layer-verification-architecture.md \xA7 the verdict spectrum). Slices A\u2013E built the basis as SHADOW machinery wired into NO verdict path; this is the first wiring that lets a compiled logical form FEED A STEP RESULT (the readiness step toward turn-on). When ON, a verify step's assertion routes through the basis: runRedundantCompile (ARc: compile k\xD7 \u2192 ABSTAIN on disagreement/degeneration) \u2192 classifyVerifyBucket \u2192 the deterministic executor / semantic-tolerance tier \u2192 a verdict in the SPECTRUM (Verified-PASS/FAIL | Assessed | Inconclusive) that feeds the step result. SCOPED to the SAFE paths \u2014 everything else ABSTAINS, never manufactures a verdict: DOM-groundable (deterministic executor over structured extraction) \u2192 Verified-PASS/FAIL LIVE (a grounded contradiction FAILS \u2014 the no-false-positive core); confident-semantic (semantic-tolerance, deterministic-first, task-blind classifier) \u2192 Verified, ambiguous \u2192 Assessed (non-blocking WARNING band, never a hard pass/fail); VISION/CANVAS (bucket 2) \u2192 ABSTAIN (Inconclusive/WARNING), NOT enforced \u2014 the mandatory dense-abstain guard from the P2 finding (consensus does NOT eliminate correlated-miscount false-confirms on dense canvases, so vision must NOT produce a live PASS/FAIL yet; a count tally is treated as vision-grounded \u2192 ABSTAIN unless the caller asserts domGrounded); not-groundable / compile-abstain / consensus-abstain \u2192 Inconclusive/Assessed, NEVER a manufactured pass. FIREWALL INVARIANT: a groundable contradiction MUST FAIL; nothing ungroundable is EVER a hard PASS \u2014 verifyStepAction encodes the DEFAULT consequence policy (only a Verified-FAIL gates; a Verified-PASS confirms but never manufactures/overrides a pass; Assessed is a non-blocking warning; Inconclusive leaves the existing verdict). GRADUATED default-ON 2026-07-27 (alongside PREDICATE_BASIS_COMPILE) after the flagship live-replay gate; the emergency kill-switch is retained and BYTE-IDENTICAL WHEN FORCED OFF (non-negotiable): explicit AGENTIQA_PREDICATE_BASIS_VERIFY=0 makes runPredicateBasisVerify short-circuit to disabled with ZERO compile/executor/classifier calls and the RunnerRuntime enforcement pass early-return before any effect. The compile itself rides PREDICATE_BASIS_COMPILE (also graduated), so full turn-on = both flags ON (with VERIFY on but COMPILE forced off the compiler returns a disabled result \u2192 ABSTAIN, safe). STAGING-FIRST: default-ON reaches the STAGING engine on merge; PROD stays OFF until a later staging\u2192main release carries it.",designDoc:"docs/plans/2026-07-26-trust-layer-predicate-basis-design.md",status:"active",added:"2026-07-27",notes:"GRADUATED 2026-07-27 (default ON) \u2014 Slice F LIVE verify wiring, the FIRST live trust-verdict on the predicate basis (turned on alongside PREDICATE_BASIS_COMPILE per Alex's explicit approval after the flagship live-replay gate). When on (and a predicateBasisCompiler is wired at engine boot \u2014 buildDeps.getPredicateBasisCompiler, present whenever a Google key is set), a verify step's assertion routes through the basis: runRedundantCompile (ARc, rides PREDICATE_BASIS_COMPILE) \u2192 classifyVerifyBucket \u2192 the deterministic executor / semantic-tolerance tier \u2192 a SPECTRUM verdict feeding the step result. SCOPED to the SAFE paths \u2014 everything else ABSTAINS, never manufactures a verdict: DOM-groundable \u2192 Verified-PASS/FAIL LIVE (a grounded contradiction FAILS \u2014 the no-false-positive core); confident-semantic \u2192 Verified, ambiguous \u2192 Assessed (non-blocking WARNING); VISION/CANVAS \u2192 ABSTAIN (Inconclusive), NOT enforced \u2014 the mandatory P2 dense-abstain guard (a count tally is vision-grounded \u2192 ABSTAIN unless the caller asserts domGrounded); not-groundable / compile-abstain \u2192 Inconclusive, NEVER a manufactured pass. FIREWALL INVARIANT (unchanged): only a Verified-FAIL gates; a Verified-PASS confirms but never manufactures/overrides a pass; Assessed warns; Inconclusive leaves the existing verdict. GRADUATION EVIDENCE: the flagship live-replay benchmark (PR #1879) returned GATE=GO \u2014 zero-regression on the prod Lio (semantic-reference) + Miro (canvas) corpora, the firewall intact, canvas-abstain intact, and the #1876 DOM-reachability fix live-confirmed; the engine-core predicateBasisVerifyWiring + RunnerRuntime.predicateBasisVerify unit suites are green (flag-OFF byte-identical parity, safe-path enforcement, vision-canvas ABSTAIN, the firewall). BYTE-IDENTICAL WHEN FORCED OFF (emergency kill-switch retained): explicit AGENTIQA_PREDICATE_BASIS_VERIFY=0 restores the pre-graduation behavior \u2014 runPredicateBasisVerify short-circuits to disabled with ZERO compile/executor/classifier calls and the RunnerRuntime enforcement pass early-returns before any effect. killSwitch resolves the no-env value from this defaultState (#1729), so graduation = defaultState 'on' AND PREDICATE_BASIS_VERIFY listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire). Read sites: packages/engine-core/src/predicateBasis/verifyWiring.ts (runPredicateBasisVerify) + packages/engine-core/src/RunnerRuntime.ts (predicateBasisVerifyEnabled / the run_complete enforcement pass). Acceptance tests: packages/engine-core/src/__tests__/predicateBasisVerifyWiring.test.ts + RunnerRuntime.predicateBasisVerify.test.ts. E2E proof: e2e/benchmark/predicateBasisVerify.ts. Standing live gate post-graduation: e2e/scenarios/22-verify/01-predicate-basis-dom-enforce.ts asserts the pbv-ON DOM-enforce + canvas-abstain behavior by DEFAULT now (default-ON engine) in the nightly qa-exhaustive path. Binds claim verify.predicate-basis-verify-wiring. STAGING-FIRST: merging to staging makes pbv default-ON on the STAGING engine ONLY (staging deploys from staging); PROD stays OFF until a later staging\u2192main release carries it \u2014 a natural staged soak. VISION-path live PASS/FAIL stays DEFERRED behind PREDICATE_BASIS_CONSENSUS (still default-OFF) regardless of this flag \u2014 vision-consensus is safe ONLY as abstain, not turn-on-ready (the P2 finding)."},ELEMENT_STATE_GROUNDING:{key:"ELEMENT_STATE_GROUNDING",envVars:["AGENTIQA_ELEMENT_STATE_GROUNDING","AGENTIQA_EXPERIMENT_ELEMENT_STATE_GROUNDING"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:'Trust-layer predicate basis \u2014 ELEMENT-STATE grounding (residual track P2, slice 1: BUILD + SHADOW; design docs/plans/2026-07-27-element-state-grounding-design.md). Closes a groundability gap in the DOM-verify path (routeDomVerify / domExtractionSource): the deterministic a11y-outline extraction grounds PRESENCE / ABSENCE / COUNT / MODIFICATION but STRIPS element STATE \u2014 a `- button "Submit" [disabled]` line\'s `[disabled]`, an `aria-checked` / `[selected]` / `[expanded]` marker, and an input\'s inline value \u2014 so an assertion like "the Submit button is enabled" is NOT DOM-groundable and falls through to flaky model-vision grading (the Lio submission s8 flake: a weak vision-only read of button-enabled that varied run-to-run). The a11y snapshot (Playwright _snapshotForAI) DETERMINISTICALLY carries this state; this track parses it into a new `state-of` observable + `element-state` predicate the Slice-A executor denotes over, so enabled/disabled/checked/selected/expanded/value assertions ground deterministically. FIREWALL PRESERVED: a state that is NOT resolvable from the a11y outline (target element not found, dimension not applicable to the role, mixed/unreadable value) ABSTAINS (Inconclusive) \u2014 never force-fails on unresolvable, never manufactures a pass; enforcement stays force-fail-only (only a grounded CONTRADICTION \u2192 would_fail gates). SLICE 1 = SHADOW / behavior-neutral even when ON: the machinery (extraction + NL parser + mapping + the flag-gated driver runElementStateGrounding) is wired into NO live RunnerRuntime verdict path \u2014 consumed only by unit tests (+ a later graduation benchmark). Off \u21D2 ZERO element-state extraction / parse / evaluate, byte-identical (the new observable/extraction runs only under this flag); the existing pbv DOM/count/absence/modification paths are UNTOUCHED. Graduation (benchmark + gate-review + Alex\'s default-on flip + live wiring) is a LATER slice, mirroring the pbv arc.',designDoc:"docs/plans/2026-07-27-element-state-grounding-design.md",status:"active",added:"2026-07-27",notes:"Residual track P2 (element-state grounding) slice 1 \u2014 BUILD + SHADOW behind a default-OFF killSwitch, mirroring the predicate-basis Slice A\u2013E / Slice-D CONSENSUS shadow precedent (pure machinery + flag-gated driver, wired into NO live verdict path). Read site: packages/engine-core/src/predicateBasis/elementState.ts (runElementStateGrounding \u2192 killSwitchEnabled('ELEMENT_STATE_GROUNDING'); zero element-state work when off). PURE machinery (also in elementState.ts, always safe to call, exercised in the unit suite): extractElementStatesFromAriaSnapshot (per-a11y-line state parse keyed on Playwright's exact role\u2192dimension applicability \u2014 kAriaDisabledRoles / kAriaCheckedRoles / kAriaSelectedRoles / kAriaExpandedRoles \u2014 so absence-of-marker on an applicable role reads as false, and a non-applicable role ABSTAINS rather than guessing), parseElementStateAssertion (deterministic NL \u2192 {ref, dimension, expected}; returns null when no state vocabulary is present so it is additive), elementStateQueryToLogicalForm (\u2192 the `element-state` node), and evaluateElementState (in executor.ts, the Slice-A generic executor's new case \u2014 resolves the target element in frames.after.elementStates, reads the dimension, denotes PASS on confirm / FAIL on a grounded contradiction / INCONCLUSIVE on any unresolvable \u2014 target absent, dimension N/A, mixed/ambiguous). GROUNDABLE STATES: enabled/disabled (fully both directions \u2014 the Lio s8 case), checked/unchecked, selected, expanded (PASS on [expanded] + FAIL on a collapsed-assertion contradiction; absence ABSTAINS \u2014 collapsed vs non-expandable is not distinguishable in the outline), and input value (normalized-equality; typed/locale-tolerant value comparison is a follow-up routing to compareTyped). Acceptance tests: packages/engine-core/src/__tests__/predicateBasisElementState.test.ts (extraction, parser, mapping, executor firewall, flag-OFF zero-work / byte-identical, registry-presence freshness canary). killSwitch resolves the no-env value from this defaultState (#1729) \u2014 GRADUATING = flip defaultState to 'on' AND add ELEMENT_STATE_GROUNDING to INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts (the tripwire); slice 1 stays OFF.",graduation:{status:"gated",gate:"Slice 1 is BUILD + SHADOW (pure machinery + a flag-gated driver, wired into NO verdict path). Graduation gates on the LATER slice: (1) an element-state graduation-benchmark run showing enabled/disabled/checked/selected/expanded/value ground deterministically from the a11y outline (a grounded contradiction \u2192 would_fail FIXES the Lio-s8-class vision flake; an unresolvable state \u2192 ABSTAIN, no false-PASS / no false-FAIL) and adversarially proven able to say NO-GO; (2) wiring runElementStateGrounding into the RunnerRuntime run_complete enforcement pass (force-fail-only, alongside applyPredicateBasisVerify); (3) the engine-core predicateBasisElementState unit suite green + flag-OFF byte-identical; then Alex flips the default-on (and lists it in INTENTIONALLY_GRADUATED). Mirrors the pbv graduation arc (shadow \u2192 benchmark \u2192 gate-review \u2192 default-on).",evidence:"e2e/benchmark element-state corpus (grounded PASS/FAIL vs ABSTAIN; Lio-s8 vision flake fixed; PENDING until the graduation slice) + the engine-core predicateBasisElementState unit suite + the (later) claim verify.element-state-grounding",owner:"steering (Alex)",review:"2026-08-15"}},VERIFY_CONFLICT_RECONCILE_SNAPSHOT:{key:"VERIFY_CONFLICT_RECONCILE_SNAPSHOT",envVars:["AGENTIQA_VERIFY_CONFLICT_RECONCILE_SNAPSHOT","AGENTIQA_EXPERIMENT_VERIFY_CONFLICT_RECONCILE_SNAPSHOT"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Verify-conflict DETERMINISTIC snapshot reconcile \u2014 M1 of the verify-oracle reconcile design (docs/plans/2026-07-28-verify-oracle-reconcile-design.md; GH #1907, root cause of #1813). WHAT IT DECIDES: at the SECOND verification-conflict escalation ONLY (the run_complete pass that would otherwise force-FAIL; the first bounce is untouched, and the bounce alone already resolves ~73% of oracle failures), the runtime takes ONE forced-full a11y snapshot for the whole run and re-checks each conflicted step's recorded wait literal (`_verifyOracleFailureDetails[step].literal`) against it with the SAME whole-token `pinPresentInPage` matcher, under the `waitLiteralHasSignificantTokens` significance floor. ZERO LLM calls \u2014 the model is not in the loop, so a lie about a genuinely broken wait fails constructively. THREE OUTCOMES: (a) literal significant AND present in the fresh snapshot \u2192 CLEARED: the oracle failure is removed from the ledger, the step's own already-passed criteria grades stand, the step passes, and the step is TAINTED via noteStepReopenedForReverify (the runtime consumed a re-observation it demanded, so predicate-basis-verify must not enforce over a basis it cannot attribute \u2014 the same invariant the bounce path carries); (b) literal significant and ABSENT from the fresh snapshot \u2192 GROUNDED NEGATIVE: the hard fail stands and the step is permanently INELIGIBLE for the VERIFY_CONFLICT_WITHHOLD warning band; (c) INAPPLICABLE \u2014 which is the only input VERIFY_CONFLICT_WITHHOLD (M2) ever sees \u2014 for no literal / insignificant literal / capture failed / budget already spent / the plan-derived `pin_page_grounding` oracle (excluded WHOLESALE \u2014 a plan-pin failure is a deterministic contradiction, not an absence of confirmation) / **any negation-or-absence marker on the step text or a criterion check** (`stepCarriesNegationOrAbsenceMarker`, reason `negation_marker`: a presence-clear is INVERTED for an assertion that the target is gone, and the absence lane's routing lexicon is narrow BY DESIGN, so this deliberately broader screen \u2014 not/no/none/without/missing/gone/empty/removed/deleted/closes/hidden/disappear*/vanish*/clear*/away/left-the-list/moves-to-trash/ceases-to/count-drops/zero-rows/fewer/struck-through/back-to-default/\u2026 plus a minimal DE/FR set, read over prose with only the target literal's OCCURRENCES removed (round-3: the earlier strip also deleted every \u22654-char TOKEN of the literal from the whole check, so a destination name like 'Deleted Items' silently disarmed the screen) \u2014 keeps every such step ineligible. HONEST LIMIT: this is a best-effort keyword screen over open-ended NL and completeness is unreachable; a phrasing it misses stays eligible and can be presence-cleared. The guarantee for absence assertions is the ABSENCE_AWARE_VERIFY lane, which decides with evidence rather than vocabulary) / **a literal that is not PLAN-GROUNDED whole-token with \u22652 tokens** (`waitLiteralGroundsSnapshotClear`, reason `literal_not_plan_grounded`: a CLEAR is only sound when the literal is the AUTHOR's expectation, read with the same `pinPresentInPage` matcher against the step text + criteria check/expectedValue \u2014 a model-invented literal that whole-token-matches unrelated chrome on another screen proves nothing, and a one-token literal is never distinctive enough even when the plan does contain it. KNOWN BOUNDARY, documented not fixed (round-3): this gate reads AUTHORSHIP, never PLACE \u2014 an AUTHORED literal that appears in unrelated chrome (a nav item, a help-sidebar link, the header of an Error 500 page) CLEARS exactly as it would in the region the step names; page-state and container binding are the next slice, and until then such clears are only visible in the `verify_conflict_reconcile:would_clear` census the graduation soak reads). Every inapplicable decision is logged per step as `verify_conflict_reconcile:inapplicable` with its reason. DETECTION ALWAYS RUNS (shadow): with the flag OFF the same decision is computed against the RETAINED full-snapshot corpus (`_fullSnapshotByStep`) \u2014 NO fresh capture, so OFF stays byte-identical including ZERO extra browser actions \u2014 and logged as `verify_conflict_reconcile:would_clear` / `:would_fail` with `basis:'retained_snapshot'` and `enforced:false`; ON logs the same events with `basis:'fresh_snapshot'` and `enforced:true`. Exactly ONE reconcile snapshot per run (guarded); a second escalation after the budget is spent decides INAPPLICABLE rather than re-enforcing off a stale page. NOT COVERED BY THIS FLAG (deferred slices, see the design \xA7 4): dropping `wait_for_element` from VERIFY_EVIDENCE_ACTIONS (own flag `VERIFY_EVIDENCE_REQUIRE_CAPTURE`, needs a shadow taint-rate measurement first), and still-running-vs-terminal recognition (a nested run still in flight at run end reads as a grounded negative and correctly stays red in this slice \u2014 the honest known gap). Runner lane only (RunnerRuntime run_complete).",designDoc:"docs/plans/2026-07-28-verify-oracle-reconcile-design.md",status:"active",added:"2026-07-28",notes:"M1 of the verify-oracle reconcile track (design docs/plans/2026-07-28-verify-oracle-reconcile-design.md), default OFF. Fixes the residual verify-conflict FALSE-FAIL that `canReconcileVerificationConflict` deliberately refuses: a PLAN-GROUNDED wait literal (repro run_5a6f0681 step 2, plan tp_vmlio_d1360115, literal 'QA Base \u2014 Draft Completion (do not modify)'; asess_1785247451027 step 1 is the same shape). That refusal is CORRECT as a static rule \u2014 a plan-grounded literal is a real must-pass presence oracle \u2014 so the fix is not to relax the rule but to consult the PAGE one more time: the element rendered late, and a fresh forced-full snapshot proves it. THREE FACTUAL CORRECTIONS to #1907 this design records (staging code, not the issue's reading): (a) evidence is NOT recorded on a timed-out oracle \u2014 the `_verifyEvidenceStepIndexes` add sits in the SUCCESS branch, so a timeout does not satisfy the evidence gate; (b) 'criteria are advisory' is too broad \u2014 `deriveStepStatusFromCriteria` does derive from grades; the real root is that the verify-conflict force-fail OVERRIDES already-passed criteria because `oracleConflictReason` never sees stepResults; (c) an existing valve (`canReconcileVerificationConflict`) already reconciles the non-plan-grounded case to a full PASS \u2014 the residual false-fail is exactly its plan-grounded refusal branch, not the whole mechanism. Read site: packages/engine-core/src/RunnerRuntime.ts (verifyConflictReconcileSnapshotEnabled \u2192 reconcileVerifyConflictsFromSnapshot, called from the second verification-conflict escalation in handleRunComplete). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.verifyConflictReconcile.test.ts (B-R1 repro form + B-C1..B-C9 counter-probes + the flag-OFF parity / one-snapshot-per-run invariants). killSwitch resolves the no-env value from this defaultState (#1729) \u2014 graduating means flipping defaultState to 'on' AND listing the key in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts.",graduation:{status:"gated",gate:"Graduation gates on the LIVE harness, not on units: Chin's ci-first-plan `tp_c0dcd706` run \xD710 against staging with the flag ON \u2014 baseline is 0/10, the target is \u22658/10 on the step-9 mode (post-login redirect), with the cleared-vs-would_fail split reported from the `verify_conflict_reconcile:*` diags. Steps 17\u201318 (split 2026-09-02: 17 = action, wait for the nested run to reach a terminal state by polling; 18 = verify, terminal state is Passed/Failed/Blocked) are NOT an expected-red slice: the pre-split step 17 was an async-terminal-state false-fail BY DESIGN, so with the plan fixed step 18 going green is the BENIGN expected outcome \u2014 it shows the plan now waits for the state it asserts, and it is NOT evidence that the significance threshold leaks (nor, on its own, evidence to graduate on). The leak-detector role that premise used to carry moves to the F4/M1 withhold census \u2014 the `verify_conflict_reconcile:would_clear` / `:would_fail` soak already required below, read alongside F4's `verify_conflict_url_grounded_withhold:would_withhold` \u2014 which sizes clears against genuinely-absent targets directly instead of inferring a leak from one plan step's colour. Plus: a staging shadow soak of `verify_conflict_reconcile:would_clear` / `:would_fail` sizing the cleared rate and confirming no would_clear fires on a genuinely-absent target, and the engine-core verifyConflictReconcile unit suite green with flag-OFF parity over the full runner corpus.",evidence:"staging `verify_conflict_reconcile:would_clear` / `:would_fail` diag events (cleared rate, basis, per-step literals) + the tp_c0dcd706 \xD710 harness pass rate per failure mode + packages/engine-core/src/__tests__/RunnerRuntime.verifyConflictReconcile.test.ts",owner:"steering (Alex)",review:"2026-08-15"}},VERIFY_CONFLICT_WITHHOLD:{key:"VERIFY_CONFLICT_WITHHOLD",envVars:["AGENTIQA_VERIFY_CONFLICT_WITHHOLD","AGENTIQA_EXPERIMENT_VERIFY_CONFLICT_WITHHOLD"],polarity:"1-enables",defaultState:"off",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Verify-conflict RESIDUAL WARNING policy \u2014 M2 of the verify-oracle reconcile design (docs/plans/2026-07-28-verify-oracle-reconcile-design.md). Strictly downstream of VERIFY_CONFLICT_RECONCILE_SNAPSHOT (M1): it sees ONLY the conflicts M1 could not decide (no captured literal, an insignificant literal, a failed capture, or the reconcile budget already spent) and NEVER a step M1 read as a GROUNDED NEGATIVE \u2014 a target the runtime looked for and did not find always stays a hard FAIL. When ON, such a residual conflict is soft-withheld to a step-level WARNING ('could not be independently confirmed on this run') instead of force-FAILing the run. ALL SEVEN conditions must hold or the existing force-fail stands: (1) the unresolved oracle is a wait-style action, never the plan-derived `pin_page_grounding` oracle; (2) every graded criterion on the step passed with a non-empty substantiation note (the same bar `canReconcileVerificationConflict` uses) AND the step result is still 'passed' with no other deterministic negative on it (no reobserve-withhold, no upstream floor having already flipped it) ; (3) \u22651 STRICT criterion carries an engine-corroborated concrete value \u2014 its `observed` (or, absent that, its pinned `expectedValue`) is whole-token present in the retained page corpus via `pinPresentInPage`; (4) the wait literal's significant tokens are corroborated by the `observed`/note corpus of some passed criterion (this is what links 'what we waited for' to 'what was confirmed', and it is why a short/frequent literal like 'ok' or '3' can never withhold \u2014 the significance floor rejects it); (5) the step is NOT absence-intent (`checkTextAssertsAbsence` \u2014 that shape belongs to ABSENCE_AWARE_VERIFY, which owns its own confirm/abstain ladder) and, since that lexicon is narrow by design, ALSO carries no negation-or-absence marker at all under the broader M1 screen (`stepCarriesNegationOrAbsenceMarker`, reason `negation_marker`) \u2014 a warning there would withdraw a force-fail the engine has no evidence to withdraw; (6) \u2014 folded into (2) \u2014 no other deterministic negative on the step; (7) the step was OBSERVED AGAIN after the conflict bounce (a runtime capture-sequence watermark taken at the bounce; a verbatim resubmit of pre-bounce grades never withholds). DETECTION ALWAYS RUNS (shadow): `verify_conflict_withhold:would_withhold` (and a reasoned `:inapplicable`) is logged with `enforced:` reflecting the flag, so OFF is byte-identical with telemetry only. KNOWN, DELIBERATE SIDE EFFECTS when enforcing (asserted in CI, not accidents): withdrawing the forced conflict fail leaves `terminationReason='completed'`, which makes the run BILLABLE; and no 'Potential Issue Detected' card is synthesized for a withheld step \u2014 one fewer false issue. Runner lane only (RunnerRuntime run_complete).",designDoc:"docs/plans/2026-07-28-verify-oracle-reconcile-design.md",status:"active",added:"2026-07-28",notes:"M2 of the verify-oracle reconcile track, default OFF and inert unless M1's decision for the step is INAPPLICABLE. The honesty floor is the point: a withheld step is a WARNING \u2014 never a pass, never a fail \u2014 so this flag can only ever move a run out of a false RED into an honest AMBER, and it can never manufacture a green. The grounded-negative exclusion is the hard invariant (design \xA7 3): M1 having actually read the page and not found the target is exactly the evidence that the step is genuinely broken, so those never enter this path. Measured coverage of conditions (2)-(4) on the design's corpus is 32/33 steps; the one exception (staging ck-cli_test 2026-07-28 step 17, a genuinely failed criterion) correctly stays red. Read site: packages/engine-core/src/RunnerRuntime.ts (verifyConflictWithholdEnabled \u2192 classifyVerifyConflictWithhold, applied at the second verification-conflict escalation after the M1 pass). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.verifyConflictReconcile.test.ts (B-C2/B-C3/B-C5/B-C6 keep the reds red; B-C8 asserts the billing invariant deliberately). killSwitch resolves the no-env value from this defaultState (#1729) \u2014 graduating means flipping defaultState to 'on' AND listing the key in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts.",graduation:{status:"gated",gate:"Graduation gates on M1 graduating FIRST (M2 is only meaningful over M1's inapplicable residue), then on a staging shadow soak of `verify_conflict_withhold:would_withhold` showing (a) no would_withhold on a step any deterministic oracle reads as a negative, (b) the withheld population is dominated by genuinely unconfirmable captures rather than real failures \u2014 sampled and hand-adjudicated \u2014 and (c) an explicit product decision on the billing consequence (withdrawing the forced fail makes the run terminate 'completed' and therefore BILLABLE) plus the warning-display semantics review (a withheld step must read as a neutral 'could not confirm', not as an alarm-amber defect).",evidence:"staging `verify_conflict_withhold:would_withhold` / `:inapplicable` diag events (rate + hand-adjudicated sample of the withheld population) + the tp_c0dcd706 harness residue after M1 + packages/engine-core/src/__tests__/RunnerRuntime.verifyConflictReconcile.test.ts",owner:"steering (Alex)",review:"2026-08-15"}},RUN_COMPLETENESS_ALL_STATUSES:{key:"RUN_COMPLETENESS_ALL_STATUSES",envVars:["AGENTIQA_RUN_COMPLETENESS_ALL_STATUSES","AGENTIQA_EXPERIMENT_RUN_COMPLETENESS_ALL_STATUSES"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Run-completeness guard for NON-passed claims. The guard (cascade stage 15: bounce-once, then fail-closed with placeholder rows and a summary prefix) was gated on `status === 'passed'`, so a `run_complete{status:'failed'}` bypassed it ENTIRELY \u2014 the model could declare a run failed part-way and every unexecuted step was silently absent from the verdict. When ON the guard evaluates on EVERY claimed terminal status, and the fail-closed branch additionally discloses the FULL unexecuted-step set (every plan step with no reported result, not just the `required` verify subset the bounce keys on) as `skipped`+`synthesized` rows plus a coverage clause in the run summary. It cannot launder a failure into a pass (the only status write in this branch is \u2192 `failed`) and it cannot invent a fail on a complete run (it is unreachable when every required step is reported). OFF restores the byte-identical pre-change behavior: guard on `passed` only, no coverage clause, no unexecuted rows.",designDoc:"docs/plans/2026-08-04-run-completeness-guard-all-statuses-design.md",status:"active",added:"2026-08-04",notes:"Ships default-ON from inception \u2014 there is no pre-change OFF behavior worth preserving: the gated-on-`passed` condition is a pure false-signal + coverage-loss defect measured at prod parity on the lio mirror (2026-08-03, 46 runs, https://s.assets.agentiqa.com/lio-prod-parity-2026-08-03/SCORE.md finding 1). Repro run `run_04a6292b-f968-47e5-a1ee-4013f26afce1` (plan tp_2d051c76 `workflow-approval`, session asess_1785788803738_bf4wmgv2) booked `failed` with 10 of 29 steps in step_results, 7/7 criteria passed / 0 failed / 0 never-graded, and the client-visible summary was the model's own mid-run narration \"Test run in progress. Step 10 verified.\" The same gate is live on the prod build, so lio's nightly CLI batch can hit it. Direction is strictly information-adding: the run status can only move `passed`\u2192`failed` (unchanged from the pre-change branch) and the new rows are `skipped` \u2014 never a pass, never a fail, the same trust-layer rule as `synthesizePartialStepResults`. Read site: packages/engine-core/src/RunnerRuntime.ts (`runCompletenessAllStatusesEnabled` \u2192 the stage-15 guard in handleRunComplete; `getUnexecutedStepIndexes` computes the disclosed set). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.runCompletenessAllStatuses.test.ts (fixture built from the measured run). killSwitch resolves the no-env value from this defaultState (#1729) \u2014 this entry is listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts so the no-delta proof acknowledges the default-ON state deliberately; an explicit AGENTIQA_RUN_COMPLETENESS_ALL_STATUSES=0 is the emergency kill switch that restores the bypass."},ZERO_GRADE_VERIFY_BOUNCE:{key:"ZERO_GRADE_VERIFY_BOUNCE",envVars:["AGENTIQA_ZERO_GRADE_VERIFY_BOUNCE","AGENTIQA_EXPERIMENT_ZERO_GRADE_VERIFY_BOUNCE"],polarity:"1-enables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-enabled",gates:"Zero-grade verify bounce (cascade stage 12c). The whole-step zero-grade floor (`run_complete:verify_step_unsubstantiated`) refuses a verify step reported `passed` with NONE of its plan-printed criteria graded \u2014 correctly \u2014 but until this flag it did so SILENTLY, deriving `failed` (strict) / `warning` (non-strict) with no chance to correct, the only sibling gate without the one-bounce self-correction contract. When ON the first offending `run_complete` is rejected once (`run_complete:verify_step_unsubstantiated_rejected`, response status `ungraded_verify_criteria`) naming each offending step and its full printed criteria range 1..N; a repeat offense takes the floor's derivation exactly as before, with a de-jargonized client note recording that a resubmission was already asked for. The trigger keys on the POST-synthetics `unsubstantiatedVerifySteps` collection, so a step the FIX 3 cross-step rescue or Amendment 5a fail-closed synthesis already adjudicated never bounces. The bounce writes no step or run status and mutates nothing. OFF restores the byte-identical pre-change behavior: immediate silent derivation with the original note/summary text.",designDoc:"docs/plans/2026-08-06-lio-terminal-verdicts-and-zero-grade-bounce-design.md",status:"active",added:"2026-08-06",notes:"Ships default-ON from inception (precedent: RUN_COMPLETENESS_ALL_STATUSES \u2014 the bounce writes no status, so there is no verdict-polarity change to soak; `=0` restores today's silent derivation byte-identically). Measured trigger: Lio's first post-#2238 prod nightly (2026-08-06, 15 runs, proj_b6424373, gemini-3-flash-preview) \u2014 3/15 runs whose substance passed were reported FAILED because the model submitted step 1 `passed` with `criteriaResults: []` and the floor derived `failed` from the strict criterion, vs 0/30 on the 08-04/08-05 pre-#2238 baseline; the #2238 exclusion counters (unauthored/step-mismatch/out-of-range) were all zero, i.e. the typed reference did not drop the grades \u2014 the model stopped emitting them. Bounce doctrine (one bounce per class per run, then fail-closed, NEVER a rescue/rewrite) is the `_unauthoredGradeRunCompleteRejections` doctrine; worst-case chain incomplete \u2192 zero-grade \u2192 over-grading \u2192 conflict is ~4 extra run_complete turns, the same accepted profile as the pre-existing bounces. Read site: packages/engine-core/src/RunnerRuntime.ts (`zeroGradeVerifyBounceEnabled` \u2192 the stage-12c bounce in handleRunComplete; the counter is `_zeroGradeVerifyRejections`). Acceptance tests: packages/engine-core/src/__tests__/RunnerRuntime.zeroGradeVerifyBounce.test.ts. killSwitch resolves the no-env value from this defaultState (#1729) \u2014 this entry is listed in INTENTIONALLY_GRADUATED in killSwitchDefaultState.test.ts so the no-delta proof acknowledges the default-ON state deliberately; an explicit AGENTIQA_ZERO_GRADE_VERIFY_BOUNCE=0 is the emergency kill switch. Known residual carried, not fixed (AG-8153): the floor and this bounce cover `type === 'verify'` steps only \u2014 zero-graded strict criteria on action/setup steps remain a false-PASS class."},PERSIST_PROFILE_SERVER_RESOLVE:{key:"PERSIST_PROFILE_SERVER_RESOLVE",envVars:["AGENTIQA_PERSIST_PROFILE_SERVER_RESOLVE","AGENTIQA_EXPERIMENT_PERSIST_PROFILE_SERVER_RESOLVE"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Server-side resolution of the per-project browser-profile gate at engine session create: when the request body OMITS `persistBrowserProfile` and the session names a projectId, the engine reads `persist_browser_profile` off the project (web-next `/api/sync/entities/projects`, admin service key) instead of leaving the session's gate unset. An explicit body value always wins and is never re-read. '0' restores the pre-2026-09-02 behavior: an omitted field means the profile is neither seeded nor captured, whatever the project says.",designDoc:"apps/execution-engine/src/services/projectPersistProfile.ts",status:"active",added:"2026-09-02",notes:"Ships default-ON from inception: the OFF state is not a safer prior behavior, it is the defect. The project column is NOT NULL DEFAULT true and the product treats the gate as per-project state, but the flag only ever reached the engine when a CLIENT chose to send it \u2014 the SPA does, the CLI did not \u2014 so a project with persist_browser_profile=true ran its CLI cloud runs with persistent profiles silently off (Lio nightly login-gate, 2026-09-02). Direction is one-way and bounded: it can only fill in a field the request LEFT ABSENT, never override a client's explicit true/false, and a failed/timed-out lookup leaves the field absent exactly as today (fail-open, never fatal to session create). Diagnostic: `persist_profile:resolved_from_project {projectId, value}` on a fill-in, `persist_profile:project_resolve_failed` on a miss. Read site: apps/execution-engine/src/services/projectPersistProfile.ts (`persistProfileServerResolveEnabled`), consumed by the POST /api/engine/session handler in apps/execution-engine/src/server.ts. Acceptance tests: apps/execution-engine/__tests__/projectPersistProfile.test.ts."},OFF_STEP_BLOCK_DEFLECT:{key:"OFF_STEP_BLOCK_DEFLECT",envVars:["AGENTIQA_OFF_STEP_BLOCK_DEFLECT","AGENTIQA_EXPERIMENT_OFF_STEP_BLOCK_DEFLECT"],polarity:"0-disables",defaultState:"on",surfaces:["engine"],cloudForwarded:!0,read:"killSwitch-disabled",gates:"Off-step block deflection (#3481, RunnerRuntime / test-plan runs only \u2014 never ExplorerRuntime/chat). Motivating incident: design-partner customer report (2026-10-13, fingerprint 7e17784a5645) \u2014 the run was on a later step whose own exit condition was already met, the model re-anchored on a button that an earlier, already-passed step had used (hidden by design once that step's action happened), filed a HIGH report_issue, and called exploration_blocked declaring the earlier step's index while the runtime pointer was on the current step, booking the run blocked / blocked_by_issue with later steps synthesized skipped. When on, an exploration_blocked whose declared positive integer args.stepIndex is strictly below _currentStepIndex (Number(args.stepIndex) < _currentStepIndex) and names an already-past, completed step (<= _maxInRangeStepEverActivated and completed) is bounced once per step with an instruction quoting the current step's own instruction and goal and explaining that a control used by a completed earlier step may be hidden by app state, so the agent should continue with the current step or call run_complete if its exit condition is met. A second block on the same step passes straight through (bounded once per step, bounding the edge case where the pointer ran ahead of the model). Writes NO verdict of its own. '0' disables the deflection and restores immediate termination on the first call.",designDoc:"docs/VERDICT-GATES.md",status:"active",added:"2026-10-14",notes:"Ships default-ON as a 0-disables kill switch (following the VERIFY_STEP_BLOCK_DEFLECT precedent) \u2014 a strict reduction of a confirmed false-block class where the model blocks on an already-completed earlier step's hidden control, and the gate writes no verdict of its own. Diagnostic: `off_step_block_deflected {stepIndex, currentStepIndex, declaredStepIndex, maxInRangeStepEverActivated, deflectionsIssued}`. Read site: packages/engine-core/src/RunnerRuntime.ts (`maybeDeflectOffStepBlock`, called from the `exploration_blocked` arm of `dispatchToolCall`). Pure module: packages/engine-core/src/runner/offStepBlockDeflection.ts. Unit suite: packages/engine-core/src/__tests__/RunnerRuntime.offStepBlockDeflection.test.ts."}},YRe=Object.values(Fy);var cY=["run","run the test","run the test plan","run test plan"];function fk(t){let e=t.toLowerCase().trim();return cY.includes(e)}function mk(t){return t.replace(/\s+/g," ").trim()}var Nc="plan_run_active";var Ca="PROJECT_RUN_IN_PROGRESS";function jy(t){return`This project already has a ${t.kind==="batch"?"batch":"run"} in progress, started by ${t.startedByLabel} at ${t.startedAt}. Only one run or batch can be active per project at a time \u2014 wait for it to finish, or stop it first.`}function vi(t,e){if(t!==409)return null;let n;try{n=JSON.parse(e)}catch{return null}return!n||n.code!==Ca?null:{error:typeof n.error=="string"?n.error:"",code:Ca,runId:typeof n.runId=="string"?n.runId:"",kind:n.kind==="batch"?"batch":"run",startedAt:typeof n.startedAt=="string"?n.startedAt:"",startedByLabel:typeof n.startedByLabel=="string"?n.startedByLabel:""}}var Zs=class extends Error{reason=Ca;holderRunId;holderKind;startedAt;startedByLabel;body;constructor(e){super(e.error.trim()||jy({kind:e.kind,startedByLabel:e.startedByLabel,startedAt:e.startedAt})),this.name="ProjectRunInProgressError",this.holderRunId=e.runId,this.holderKind=e.kind,this.startedAt=e.startedAt,this.startedByLabel=e.startedByLabel,this.body=e}};function wp(t){if(t instanceof Zs)return t;if(!t||typeof t!="object")return null;let e=t;if(e.reason===Ca){let n=e.body;if(n&&n.code===Ca)return new Zs(n)}return e.code!==Ca?null:new Zs({error:typeof e.message=="string"?e.message:"",code:Ca,runId:"",kind:"run",startedAt:"",startedByLabel:""})}function Na(t){let e=t.indexOf(":");return e===-1?{provider:"google",modelName:t}:{provider:t.slice(0,e),modelName:t.slice(e+1)}}var Mr="google:gemini-3-flash-preview",gk="google:gemini-3-flash-preview",ea=["credential_identifier","user_supplied_payload","generated_test_value","other"];var Ep=[{name:"Green",hex:"#4ade80"},{name:"Blue",hex:"#60a5fa"},{name:"Purple",hex:"#a78bfa"},{name:"Amber",hex:"#fbbf24"},{name:"Red",hex:"#f87171"},{name:"Cyan",hex:"#22d3ee"},{name:"Pink",hex:"#f472b6"},{name:"Slate",hex:"#94a3b8"}];function fe(t){return`${t}_${crypto.randomUUID()}`}function Do(t,e){return typeof process>"u"||!process.env?!1:process.env[`AGENTIQA_${t}`]===e||process.env[`AGENTIQA_EXPERIMENT_${t}`]===e}function yk(t){return Fy[t]?.defaultState}function ve(t){return Do(t,"0")?!0:Do(t,"1")?!1:yk(t)==="off"}function Re(t){return Do(t,"1")?!0:Do(t,"0")?!1:yk(t)==="on"}function Vt(t){return Do(t,"0")}function $y(t){return Do(t,"1")}function Oc(){return Re("VALUE_GROUNDING_ENFORCE")}function By(t){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`}function bk(t){switch(t){case"message":return"msg";case"tool_call":return"tool";case"llm_usage":return"llm";case"supervisor_verdict":return"sv";case"agent_lifecycle":return"lc";case"user_action":return"ua";case"session_start":case"session_end":case"turn_start":case"turn_end":return"sl";case"log":case"pageSnapshot.structuralStrip":return"diag";default:return"evt"}}var Mo=class{apiUrl;fetchFn;sessions=new Map;queues=new Map;timer=null;isUploading=!1;inFlight=new Set;eventIds=new WeakMap;directUploadFailedSessions=new Set;BATCH_SIZE=25;FLUSH_INTERVAL=6e4;MAX_PAYLOAD_BYTES=35e5;auth;constructor(e,n,r=globalThis.fetch.bind(globalThis)){this.apiUrl=e,this.fetchFn=r,this.auth=typeof n=="string"?{kind:"bearer",token:n}:n,this.timer=setInterval(()=>this.flushAll(),this.FLUSH_INTERVAL)}buildAuthHeaders(e,n=!1){let r={"Content-Type":"application/json"};if(this.auth.kind==="bearer")return r.Authorization=`Bearer ${this.auth.token}`,r;if(n&&this.auth.bearerFallback)return r.Authorization=`Bearer ${this.auth.bearerFallback}`,r;r["x-admin-service-key"]=this.auth.serviceKey;let s=e?.userId??this.auth.fallbackUserId;return s&&(r["x-user-id"]=s),r}hasBearerFallback(){return this.auth.kind==="service"&&!!this.auth.bearerFallback}emit(e){let n=e.sessionId;if(this.eventIds.has(e)||this.eventIds.set(e,By(bk(e.kind))),e.kind==="session_start"&&e.sessionMeta&&this.sessions.set(n,{...e.sessionMeta,desktopSessionId:n,status:"active",startedAt:new Date(e.ts).toISOString()}),e.kind==="session_end"){let s=this.sessions.get(n);s&&(s.status=e.status??"completed",s.endedAt=new Date(e.ts).toISOString())}if(this.isProviderLocationUnsupportedEvent(e)){let s=this.sessions.get(n);s&&(s.terminalErrorClass="provider_location_unsupported")}!n&&!this.sessions.has("")&&this.sessions.set("",{desktopSessionId:"global",projectId:"_global",status:"active",startedAt:new Date(e.ts).toISOString()});let r=this.queues.get(n);r||(r=[],this.queues.set(n,r)),r.push(e),r.length>=this.BATCH_SIZE&&this.flushSession(n),e.kind==="session_end"&&this.flushSession(n)}async flush(){await this.flushAll()}destroy(){this.timer&&(clearInterval(this.timer),this.timer=null),this.flushAll()}async flushAll(){let e=[];for(let r of this.queues.keys()){let s=this.flushSession(r);s&&e.push(s)}let n=Array.from(this.inFlight);await Promise.allSettled([...e,...n])}flushSession(e){let n=this.sessions.get(e),r=this.queues.get(e);if(!n||!r||r.length===0)return null;let s=r.splice(0),a=this.uploadWithRetry(n,s).catch(i=>{let o=s.filter(l=>l.kind==="session_end");if(o.length>0){let l=this.queues.get(n.desktopSessionId);l&&l.unshift(...o)}console.error(`[RemoteAnalyticsSink] Failed to upload ${s.length} events:`,i.message)});return this.inFlight.add(a),a.finally(()=>{this.inFlight.delete(a)}),a}async uploadWithRetry(e,n,r=3){let s;for(let a=1;a<=r;a++)try{await this.upload(e,n);return}catch(i){s=i;let o=i?.status;if(o!==void 0&&o>=400&&o<500)throw i;if(a<r){let l=Math.min(1e3*Math.pow(3,a-1),9e3);await new Promise(c=>setTimeout(c,l))}}throw s}async upload(e,n){let r=await this.mapEvents(e.desktopSessionId,n);await this.postIngest(e,r)}async postIngest(e,n,r=!1){if(n.length===0)return;let s=JSON.stringify({session:{...e},events:n});if(s.length>this.MAX_PAYLOAD_BYTES&&n.length>1){let l=Math.floor(n.length/2);await this.postIngest(e,n.slice(0,l),r),await this.postIngest(e,n.slice(l),r);return}let a;try{a=await this.fetchFn(`${this.apiUrl}/api/analytics/ingest`,{method:"POST",headers:this.buildAuthHeaders(e,r),body:s})}catch(l){throw new Error(`analytics upload network error: ${l?.message??String(l)}`)}if(a.ok)return;if((a.status===401||a.status===403)&&!r&&this.hasBearerFallback()){console.warn(`[RemoteAnalyticsSink] service-key auth got ${a.status} for session ${e.desktopSessionId} \u2014 retrying with bearer fallback (AG-169)`),await this.postIngest(e,n,!0);return}if(a.status===413){if(n.length>1){let l=Math.floor(n.length/2);await this.postIngest(e,n.slice(0,l),r),await this.postIngest(e,n.slice(l),r);return}console.warn(`[RemoteAnalyticsSink] Dropping single oversized event (${Math.round(s.length/1024)} KB)`);return}let i=await a.text().catch(()=>`HTTP ${a.status}`);if(i.includes("FUNCTION_PAYLOAD_TOO_LARGE")||i.includes("Request Entity Too Large")){if(n.length>1){let l=Math.floor(n.length/2);await this.postIngest(e,n.slice(0,l),r),await this.postIngest(e,n.slice(l),r);return}console.warn(`[RemoteAnalyticsSink] Dropping single oversized event (${Math.round(s.length/1024)} KB)`);return}(a.status===401||a.status===403)&&console.error(`[RemoteAnalyticsSink] auth_failed status=${a.status} authKind=${this.auth.kind} sessionId=${e.desktopSessionId} userId=${e.userId??"(unset)"} body=${i.slice(0,200)}`);let o=new Error(i);throw o.status=a.status,o}async mapEvents(e,n){let r=[];for(let s of n){let a={timestamp:new Date(s.ts).toISOString(),childId:No(s.childId)},i=this.eventIds.get(s)??By(bk(s.kind));switch(s.kind){case"message":r.push({...a,id:i,eventType:"message",role:s.role,messageText:s.text,toolName:s.actionName,toolArgs:s.actionArgs,url:s.url});break;case"tool_call":{let o;s.screenshotBase64&&(o=await this.uploadScreenshot(e,s.screenshotBase64)),r.push({...a,id:i,eventType:"tool_call",toolName:s.toolName,toolArgs:s.args,toolResult:s.result,screenshotUrl:o,url:s.url,stepIndex:s.stepIndex,actionMetadata:{durationMs:s.durationMs,tokenCount:s.tokenCount}});break}case"llm_usage":r.push({...a,id:i,eventType:"llm_usage",toolName:s.model,promptTokens:s.promptTokens,completionTokens:s.completionTokens,totalTokens:s.totalTokens,runId:s.runId,callKind:s.callKind,keySource:s.keySource,llmProvider:s.llmProvider,billedUnits:s.billedUnits,actionMetadata:{durationMs:s.durationMs,finishReason:s.finishReason,tokenCount:s.tokenCount,messageCount:s.messageCount,systemPromptHash:s.systemPromptHash,lastToolResults:s.lastToolResults,chosenActions:s.chosenActions,textResponse:s.textResponse,cachedInputTokens:s.cachedInputTokens,planStepIndex:s.planStepIndex,planStepType:s.planStepType}});break;case"supervisor_verdict":r.push({...a,id:i,eventType:"supervisor_verdict",actionType:s.verdict,actionMetadata:{verdict:s.verdict,message:s.message,iteration:s.iteration,actionLogSize:s.actionLogSize,stepText:s.stepText,progress:s.progress??null,differential:s.differential??null}});break;case"agent_lifecycle":r.push({...a,id:i,eventType:"agent_lifecycle",actionType:s.event,actionMetadata:{event:s.event,iteration:s.iteration,details:s.details}});break;case"user_action":r.push({...a,id:i,eventType:"user_action",actionType:s.action,actionTargetId:s.targetId,actionMetadata:s.metadata});break;case"session_start":case"session_end":case"turn_start":case"turn_end":r.push({...a,id:i,eventType:"user_action",actionType:s.kind,actionTargetId:s.sessionId,actionMetadata:s.sessionMeta?{...s.sessionMeta}:{status:s.status,...s.kind==="session_end"&&s.endKind?{endKind:s.endKind}:{}}});break;case"log":r.push({...a,id:i,eventType:"diagnostic",actionType:s.level,actionMetadata:{source:s.source,msg:s.msg,...s.data}});break;case"pageSnapshot.structuralStrip":r.push({...a,id:i,eventType:"diagnostic",actionType:s.kind,actionMetadata:{originalLen:s.originalLen,finalLen:s.finalLen,droppedNodeCount:s.droppedNodeCount,foldedRunCount:s.foldedRunCount,capHit:s.capHit}});break;default:{console.warn(`[RemoteAnalyticsSink] dropping unmapped DiagnosticEvent kind=${s.kind}`);break}}}return r}isProviderLocationUnsupportedEvent(e){return e.kind!=="log"?!1:fs([e.msg,e.source,typeof e.data=="object"&&e.data?JSON.stringify(e.data):""].join(`
52
52
  `))}async uploadScreenshot(e,n){let r=By("img"),s=this.sessions.get(e)??null;if(!ve("SCREENSHOT_DIRECT_UPLOAD")&&!this.directUploadFailedSessions.has(e)){let a=await this.uploadScreenshotDirect(e,s,r,n);if(a)return a;this.directUploadFailedSessions.add(e),console.warn(`[RemoteAnalyticsSink] direct screenshot upload failed for session ${e} \u2014 falling back to legacy base64 upload for this session`)}return this.uploadScreenshotLegacy(e,s,r,n)}async uploadScreenshotDirect(e,n,r,s){let a;try{let u=s.includes(",")?s.split(",")[1]:s;if(u=u.replace(/\s/g,""),u.length===0)return;a=Uint8Array.from(atob(u),f=>f.charCodeAt(0))}catch{return}let i=JSON.stringify({sessionId:e,eventId:r,sizeBytes:a.byteLength}),o=u=>this.fetchFn(`${this.apiUrl}/api/analytics/presign-screenshot`,{method:"POST",headers:this.buildAuthHeaders(n,u),body:i}),l;try{l=await o(!1),(l.status===401||l.status===403)&&this.hasBearerFallback()&&(l=await o(!0))}catch{return}if(!l.ok)return;let c=await l.json().catch(()=>null);if(!c||!c.putUrl||!c.publicUrl)return;let d;try{d=await this.fetchFn(c.putUrl,{method:"PUT",headers:{"content-type":"image/png"},body:a})}catch{return}if(d.ok)return c.publicUrl}async uploadScreenshotLegacy(e,n,r,s){let a=JSON.stringify({sessionId:e,eventId:r,imageBase64:s}),i=o=>this.fetchFn(`${this.apiUrl}/api/analytics/upload-image`,{method:"POST",headers:this.buildAuthHeaders(n,o),body:a});try{let o=await i(!1);if((o.status===401||o.status===403)&&this.hasBearerFallback()&&(console.warn(`[RemoteAnalyticsSink] upload-image service-key auth got ${o.status} for session ${e} \u2014 retrying with bearer fallback (AG-388)`),o=await i(!0)),!o.ok){let c=await o.text().catch(()=>`HTTP ${o.status}`);console.error(`[RemoteAnalyticsSink] upload-image failed status=${o.status} authKind=${this.auth.kind} sessionId=${e} userId=${n?.userId??"(unset)"} body=${c.slice(0,200)}`);return}let l=await o.json().catch(()=>null);if(!l||!l.url){console.warn(`[RemoteAnalyticsSink] upload-image returned no url (success=${l?.success??"(none)"}) for session ${e} \u2014 screenshot_url will be null`);return}return l.url}catch(o){console.error(`[RemoteAnalyticsSink] upload-image network error for session ${e}:`,o.message);return}}};function mt(t,e){return t.replace(/\{\{timestamp\}\}/g,String(e)).replace(/\{\{unique\}\}/g,dY(e))}function dY(t){let e="abcdefghijklmnopqrstuvwxyz",n="",r=t;for(;r>0;)n=e[r%26]+n,r=Math.floor(r/26);return n||"a"}var Sp=500;function Tp(){return Vt("CLICK_EFFECT_SIGNAL")?"off":Re("CLICK_EFFECT_SIGNAL")?"enforce":"shadow"}function Uo(){return Tp()==="enforce"}var uY=new Set(["input","select","textarea","option"]),pY=new Set(["checkbox","radio","switch","textbox","searchbox","combobox","listbox","option","spinbutton","slider","menuitemcheckbox","menuitemradio"]);function hY(t){if(!t)return!1;let e=(t.tag??"").toLowerCase(),n=(t.role??"").toLowerCase();return uY.has(e)||pY.has(n)}var fY=`The click was dispatched but no page change was observed within ${Sp}ms (no DOM, text or attribute change, no navigation, no dialog). The target may not have been interactive yet \u2014 for example a freshly navigated page that is still hydrating, where a control is already painted but its handler is not attached. Re-check the element state in the latest screenshot/snapshot and re-perform the action ONCE if it did not take effect; do not assume it succeeded just because it was dispatched.`;function vk(t,e={}){return t.urlBefore!==t.urlAfter?{effectObserved:!0}:t.dialogsAfter>t.dialogsBefore?{effectObserved:!0}:t.mutationCount==null||t.attrCount==null?{}:t.mutationCount>0||t.attrCount>0?{effectObserved:!0}:hY(e.target)?{effectObserved:!1,suppressedBy:"property-only-control"}:e.canvasDominant?{effectObserved:!1,suppressedBy:"canvas"}:e.suppressAdvisory?{effectObserved:!1,suppressedBy:"caller-note"}:{effectObserved:!1,note:fY}}function Pc(){return" When type='verify', the criteria array MUST contain at least one concrete outcome check. Never leave criteria empty or omit it on verify steps; the runner has nothing to assert against and may skip the check."}function Rp(){return"Concrete check describing the expected OUTCOME. Focus on data created or changed during the test. When action steps use {{unique}} or {{timestamp}}, criteria must refer to the same token. Do not quote transient UI text from memory; describe the expected outcome and let the runner read the live screen. Always also set this criterion\u2019s factKind \u2014 the KIND of fact it asserts (count, presence, absence, modification, value, or relation)."}function Ip(){return"REQUIRED for verify steps. Include one or more concrete checks the runner can perform against the live screen. Omit for setup/action steps."+Gy()}function Gy(){return" Prefer exactly ONE criterion per verify step: when a step would assert several distinct outcomes, split them into separate sequential verify steps, each with its single check, rather than stacking multiple criteria on one step. A verify step's text and its single criterion's check must not repeat each other verbatim \u2014 the text states the intent, the criterion states the concrete outcome to confirm."}function Ap(){return'Optional. The exact literal this check pins \u2014 set it ONLY when the assertion checks for a specific value: a quoted string, an exact number or count, or an exact URL (e.g. check "Order ID is \\"TC-009\\"" \u2192 expectedValue "TC-009"). This is the machine-comparable expectation; the runner requires the observed value to match it verbatim. Omit it for vague/qualitative checks (e.g. "a success message is visible"). When revising an existing plan, keep every existing expectedValue pin in its criterion; never move a pinned value into step prose.'}function Oa(){return`Author this on EVERY verify criterion whenever the kind is clear \u2014 committing the right kind is what lets the grounded-state verifier confirm the RIGHT kind of state (especially on canvas/visual surfaces where a count or presence fact has no DOM literal to pin). It is a deliberate typed choice you commit to, NOT a re-description of the check text (like the auth enums, prefer authoring this over relying on the check wording). Pick the kind the check asserts:
53
53
  - "count" \u2014 a quantity or "N of X" fact (e.g. "exactly 3 shapes on the board", "5 rows in the table"), especially countable canvas/visual facts with no exact string to pin.
54
54
  - "absence" \u2014 something was REMOVED / is gone / no longer shown (e.g. "the banner is no longer visible", "the sticky note was deleted").
@@ -558,7 +558,7 @@ Create a structured summary that preserves:
558
558
  Be concise but preserve critical details like URLs, credentials used, and test data.
559
559
  Output ONLY the structured summary, no preamble.`;try{let i=Date.now(),o=await yt({model:this.model,messages:[{role:"user",content:a}],temperature:.1,maxOutputTokens:2048,maxRetries:2});return At(this.sessionAuxMeter(),this.model,o,{durationMs:Date.now()-i}),(o.text??"").trim()}catch(i){return this.log("error","BaseRuntime","Summarization failed",{error:i?.message}),s}}async maybeSummarizeContext(e,n){let r=n.tokenCount??e.lastTokenCount??this.tokenCount;if(r<=2e5||this.conversationTrace.length===0)return!1;let s=Math.floor(2e5*.25);if(this.lastContextSummaryMessageId&&this.lastContextSummaryTokenCount>0&&r<this.lastContextSummaryTokenCount+s)return!1;let a=await this.baseDeps.chatRepo.listMessages(e.id);if(this.countUserMessages(this.conversationTrace)<=tv)return!1;let o=a.slice(0,Math.max(0,a.length-tv*3));if(o.length===0)return!1;this.log("info",n.source,"Token count exceeds threshold",{tokenCount:r,iteration:n.iteration}),this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:e.id,event:"context_summarized",iteration:n.iteration,details:`tokenCount=${r}`});let l=await this.summarizeContext(e,o),c=o[o.length-1]?.id;e.contextSummary=l,e.summarizedUpToMessageId=c,await this.baseDeps.chatRepo.updateSessionFields(e.id,{contextSummary:l,summarizedUpToMessageId:c});let d=this.conversationTrace,u=!!this.systemPromptText&&d[0]?.role==="user"&&d[0]?.parts?.[0]?.text===this.systemPromptText,f=u?d[0]:void 0,p=(u?d.slice(1):d.slice()).slice(-tv*2),m={role:"user",parts:[{text:`[CONTEXT SUMMARY from earlier in conversation]
560
560
  ${l}
561
- [END SUMMARY]`}]},g=f?[f,m,...p]:[m,...p];d.length=0,d.push(...g),this.conversationTrace=d,await this.persistConversationTrace(e,d);let v={sessionId:e.id,id:fe("msg"),role:"system",actionName:"context_summarized",text:"Chat context summarized",timestamp:Date.now()};return await this.persistMessage(v),this.emit("message:added",{sessionId:e.id,message:v}),this.lastContextSummaryTokenCount=r,this.lastContextSummaryMessageId=c,!0}async persistConversationTrace(e,n){await this.baseDeps.chatRepo.updateSessionFields(e.id,{conversationTrace:n})}getObservationArchiveContext(){return{sessionId:this.sessionId,runtimeId:this.sessionId,runId:this.getCurrentRunId(),stepIndex:this.currentPlanStepIndex()}}async withObservationCall(e,n){let r=this.baseDeps.observationScope;if(!r)return n();let s;try{s={...r.getContext(),...this.getObservationArchiveContext(),logicalCallId:fe("model_call"),logicalCallContext:"bound",callPurpose:e}}catch{return En(this.baseDeps.observationArchive,"lifecycle",{sessionId:this.sessionId},()=>({state:"incomplete",reasonCode:"observer_context_failed"})),n()}let a;try{return await r.runWithContext(s,()=>{try{a=n()}catch(i){a=Promise.reject(i)}return a})}catch{return a||(En(r.archive,"lifecycle",s,()=>({state:"incomplete",reasonCode:"observer_context_failed"})),n())}}recordObservationToolResponse(e,n,r,s){En(this.baseDeps.observationArchive,"tool_response",e,()=>zr({functionResponse:n,screenshotParts:r,occurrence:s,stage:"assembled_before_pruning"}))}currentPlanStepIndex(){}onBrowserActionDispatched(e){}currentPlanStepType(){}currentPlanStepText(){}planControlLabelsForAction(){}necessityContextText(){let e=this.currentPlanStepText()?.trim();if(e)return e;let n=this._currentTaskDescription?.trim();return n||void 0}planStepAttribution(){let e=this.currentPlanStepType();return e===void 0?{}:{planStepIndex:this.currentPlanStepIndex(),planStepType:e}}hasTypedCredentialThisRun(){return this._credentialTypedThisRun}typedCredentialNamesThisRun(){return[...this._typedCredentialNames]}resetCredentialTypingObservations(){this._credentialTypedThisRun=!1,this._typedCredentialNames=new Set}async executeBrowserAction(e,n){if(!this.browserActionExecutor)throw new Error("[BaseRuntime] Browser actions not available \u2014 no computerUseService");let r=e.args??{},s=typeof r.intent=="string"?r.intent.trim():void 0;if(Lk({config:n.session.config,actionName:e.name,text:r.text,value:r.value,inputType:r.inputType,label:r.label,matchedLabel:r.matchedLabel,intent:r.intent,screen:r.screen,stepText:r.stepText}))return this.log("info","BaseRuntime","executeBrowserAction: refusing email write in discovery run (AG-3588)",{sessionId:n.session.id,action:e.name,label:r.label}),{response:{status:"error",error:"Refused: discovery runs must not submit credentials, sign up, or fill email/login fields. This is expected \u2014 signup / authenticated flows belong in the post-scope Explorer, not in discovery. Treat this signup surface as a discovered area: call log_observation to record the form fields, validation rules, and what the surface offers (including requires_auth=true), then continue mapping other public/unauthenticated pages. When discovery is complete, finish with assistant_v2_report including the signup gate in discoveredAreas. Do NOT call exploration_blocked \u2014 a signup/auth gate is normal discovery output, not a failure. The Coordinator presents the scope checkpoint from your partial discovery (AG-3588)."},isMetaTool:!0};this.recordStartupMilestone("first_browser_tool_dispatched",{toolName:e.name,isMobile:!1,iteration:n.iteration}),this.onBrowserActionDispatched(e.name),n.session.config.extensionPath&&this.log("info","BaseRuntime","executeBrowserAction with extension",{sessionId:n.session.id,projectId:n.session.projectId,extensionPath:n.session.config.extensionPath,action:e.name});let a=this.drainAbortControllers.get(n.session.id);(!a||a.signal.aborted)&&(a=new AbortController,this.drainAbortControllers.set(n.session.id,a));let i=await this.browserActionExecutor.execute(n.session.id,e.name,r,n.session.projectId,{...n.session.config,unattendedRun:this.unattendedRun,planStepText:this.necessityContextText(),planControlLabels:this.planControlLabelsForAction()},{intent:s,stepIndex:n.stepIndex,planStepIndex:this.currentPlanStepIndex(),turnTimestamp:n.turnTimestamp,signal:a.signal,...this.baseDeps.observationArchive?{observationContext:{...this.getObservationArchiveContext(),toolCallId:e.toolCallId,iteration:n.iteration}}:{}});this.recordStartupMilestone("first_browser_tool_completed",{toolName:e.name,isMobile:!1,iteration:n.iteration});let{result:o,response:l,message:c}=i;this.captureVerbatimTypedValue(e,l,n.session.config);let d=l&&typeof l=="object"?l.verbatimInputSource:void 0;if(d&&c&&(c.actionArgs={...c.actionArgs??{},verbatimInputSource:d}),l&&typeof l=="object"&&l.status==="error"&&typeof l.error=="string"&&this.onBrowserActionError(e.name,l.error),Dc(e.name)){let g=l??{};if(!(g.status==="error"||g.error!=null||g.valueMismatch===!0)){this._credentialTypedThisRun=!0;let w=e.args?.credentialName;typeof w=="string"&&w.trim()&&this._typedCredentialNames.add(w.trim())}}i.drainResult&&i.drainResult.pendingAtStart>0&&this.log("info","BrowserActionExecutor","drain",{sessionId:n.session.id,actionName:e.name,iteration:n.iteration,pendingAtStart:i.drainResult.pendingAtStart,pendingAtEnd:i.drainResult.pendingAtEnd,waitedMs:i.drainResult.waitedMs,timedOut:i.drainResult.timedOut,aborted:i.drainResult.aborted,oldestAgeMs:i.drainResult.oldestAgeMs,projectId:n.session.projectId});let u=o.metadata?.clickReceipt,f=u?.addressing==="label"||!u&&r.label?`label=${u?.requestedTarget.label??r.label}`:r.ref?`ref=${r.ref}`:r.x!=null?`(${r.x},${r.y})`:void 0,h={action:e.name,intent:s,screen:typeof r.screen=="string"?r.screen:void 0,target:f,activeTab:o?.metadata?.activeTab,drainMs:i.drainResult?.waitedMs,clickReceipt:u,observedUrl:o.url},p=!n.snapshotOnly&&o.screenshot?[{inlineData:{mimeType:"image/png",data:o.screenshot}}]:void 0;o.metadata?.pageSettled!==void 0&&(this._lastNavPageSettled=o.metadata.pageSettled),this._lastCanvasDominant=o.canvasDominant===!0;let m=o.metadata?.navigateMeta;if(m?.originChanged===!0&&l){let g=eh(m);if(g){let v=g.redirectChain.length>0?g.redirectChain.join(" -> "):"(direct)";l.navigateNote=`navigate: requested ${g.requestedUrl}, redirected cross-origin to ${g.finalUrl} via ${v}. This destination looks like an authentication bridge (path matches ${g.matchedPattern}). The requested URL likely requires credentials Agentiqa does not have. Do NOT retry navigate to ${g.requestedUrl} in this turn \u2014 instead, surface this to the user (ask whether they have credentials for the preview, or want to provide a different URL).`,this.baseDeps.sink.emit({kind:"log",ts:Date.now(),sessionId:n.session.id,level:"info",source:"PreviewAuthDetector",msg:"preview_requires_auth",data:g})}else l.navigateNote=Sb(m)}else if(m&&l){let g=Eb(m);g&&(l.navigateNote=g,this.baseDeps.sink.emit({kind:"log",ts:Date.now(),sessionId:n.session.id,level:"info",source:"NavigateQueryDrop",msg:"navigate_dropped_query",data:{previousUrl:m.previousUrl,requestedUrl:m.requestedUrl,finalUrl:m.finalUrl}}))}return{response:l,parts:p,message:c,...i.observationCaptureId?{observationCaptureId:i.observationCaptureId}:{},...o.metadata?.observation?{observation:o.metadata.observation}:{},supervisorEntry:h,loopDetectorUpdate:{url:o.url,screenContent:l?.pageSnapshot,screenshotSize:n.skipScreenshotSet.has(n.callIndex)?void 0:o.screenshot?.length,canvasDominant:o.canvasDominant===!0,drainTimedOut:i.drainResult?.timedOut??!1,...l?.clickTarget?{clickTargetSignature:Fr(l.clickTarget)}:{}},screenshotBase64:o.screenshot,...o.captureMode&&{captureMode:o.captureMode},...typeof o.aiSnapshot=="string"&&{groundingSnapshot:o.aiSnapshot,groundingCanvasDominant:o.canvasDominant===!0},...o.snapshotKind&&{groundingSnapshotKind:o.snapshotKind},supervisorObservationUnavailable:typeof o.aiSnapshot!="string"&&!o.screenshot,...o.metadata?.noObservedEffect&&{noObservedEffect:!0},...o.metadata?.effectObserved!==void 0&&{effectObserved:o.metadata.effectObserved},...o.metadata?.elementFingerprint&&{writeElementFingerprint:o.metadata.elementFingerprint},...o.metadata?.verbatimGuardMismatch&&{writeGuardMismatch:!0},...o.metadata?.planLabelResolutions?.length&&{planLabelResolutions:o.metadata.planLabelResolutions},...o.metadata?.matchedLabel&&{writeMatchedLabel:o.metadata.matchedLabel},...o.metadata?.matchedName&&{clickMatchedName:o.metadata.matchedName}}}onBrowserActionError(e,n){}async executeMobileAction(e,n){if(!this.mobileActionExecutor)return{response:{status:"error",error:"Mobile executor not available"}};let r=e.args??{},s=typeof r.intent=="string"?r.intent.trim():void 0;this.recordStartupMilestone("first_browser_tool_dispatched",{toolName:e.name,isMobile:!0,iteration:n.iteration});let a=await this.mobileActionExecutor.execute(n.session.id,e.name,r,n.session.projectId,n.session.config,{intent:s,stepIndex:n.stepIndex,skipScreenshot:n.skipScreenshotSet.has(n.callIndex),turnTimestamp:n.turnTimestamp});this.recordStartupMilestone("first_browser_tool_completed",{toolName:e.name,isMobile:!0,iteration:n.iteration});let{result:i,response:o,message:l}=a,c=r.ref?`ref=${r.ref}`:r.x!=null?`(${r.x},${r.y})`:void 0,d={action:e.name,intent:s,screen:typeof r.screen=="string"?r.screen:void 0,target:c,activeTab:i?.metadata?.activeTab},u=!n.snapshotOnly&&i.screenshot?[{inlineData:{mimeType:"image/png",data:i.screenshot}}]:void 0;return{response:o,parts:u,message:l,supervisorEntry:d,loopDetectorUpdate:{url:i.url,screenContent:o?.pageSnapshot,screenshotSize:n.skipScreenshotSet.has(n.callIndex)?void 0:i.screenshot?.length},screenshotBase64:i.screenshot}}captureVerbatimTypedValue(e,n,r){if(!Es()||e.name!=="type_text_at"&&e.name!=="set_focused_input_value"||e.args?.fieldPurpose==="verification_code")return;let s=n;if(s?.status==="error"||s?.error!=null||s?.valueMismatch===!0||typeof s?.inputType=="string"&&s.inputType.toLowerCase()==="password")return;let a=e.name==="type_text_at"?e.args?.text:e.args?.value;if(typeof a!="string")return;let i=a.slice(0,4e3);if(i.trim().length===0||xi(i))return;let o=e.args?.inputRole,l=Jy(i,r)?"credential_identifier":o==="credential_identifier"||o==="user_supplied_payload"||o==="generated_test_value"||o==="other"?o:"other",c=typeof s?.typedIntoField=="string"&&s.typedIntoField.trim()||typeof e.args?.label=="string"&&e.args.label.trim()||typeof e.args?.ref=="string"&&`ref:${e.args.ref}`||"",d={sourceActionId:typeof e.toolCallId=="string"&&e.toolCallId.length>0?e.toolCallId:fe("write"),inputRole:l,...c?{fieldIdentity:c.slice(0,200)}:{}};this._capturedTypedInputs.push({value:i,source:d}),this._capturedTypedInputs.length>50&&this._capturedTypedInputs.shift(),s&&typeof s=="object"&&(s.verbatimInputSource=d),!this._capturedTypedValues.includes(i)&&(this._capturedTypedValues.push(i),this._capturedTypedValues.length>50&&this._capturedTypedValues.shift())}getCapturedTypedValues(){return this._capturedTypedValues}getCapturedTypedInputs(){return this._capturedTypedInputs}async executeAction(e,n){if((e.name==="type_text_at"||e.name==="set_focused_input_value")&&Wo({config:n.session?.config,text:e.name==="type_text_at"?e.args?.text:e.args?.value,inputType:typeof e.args?.inputType=="string"?e.args.inputType:void 0,label:e.args?.label,intent:e.args?.intent,screen:e.args?.screen,stepText:e.args?.stepText})&&(this._canonicalEmailWritten=!0,!this._emailVerificationExpected&&kk(e.args?.intent,e.args?.screen,e.args?.stepText)&&(this._emailVerificationExpected=!0),!this._canonicalEmailUsedForSignup&&Ck(e.args?.intent,e.args?.screen,e.args?.stepText)&&(this._canonicalEmailUsedForSignup=!0)),!ve("EMAIL_CODE_PROVENANCE")&&this._emailVerificationExpected&&e.args?.fieldPurpose==="verification_code"&&(e.name==="type_text_at"||e.name==="set_focused_input_value")){let o=String(e.name==="type_text_at"?e.args?.text??"":e.args?.value??""),l=U0({value:o,fetchedEmailTexts:this._fetchedEmailTexts});if(!l.allowed)return this.log("warn","BaseRuntime","email_verification_gate: ungrounded code write refused",{toolName:e.name,grounded:this._fetchedEmailTexts.length>0}),{response:{status:"error",error:l.reason},isMetaTool:!0}}let r=this.maybeBlockForObservationCheckpoint(e,n);if(r)return r;let s=n.isMobile&&Ma(e.name)?await this.executeMobileAction(e,n):await this.executeBrowserAction(e,n);this._canonicalEmailWritten&&Nk(s.loopDetectorUpdate?.screenContent)&&(this._emailVerificationExpected||(this._emailVerificationExpected=!0),this.recordRuntimeRevealedFact({kind:"capability_required",provenance:"runtime_observed",confidence:"high",source:"testingEmailPolicy.screenIndicatesVerificationPending",message:"The product screen indicates email verification is pending.",data:{capability:"email_verification",...typeof s.loopDetectorUpdate?.url=="string"?{url:s.loopDetectorUpdate.url}:{}}}));let i=q0({scope:n.session?.config?.browserNavigationScope,currentUrl:s.loopDetectorUpdate?.url,screenContent:s.loopDetectorUpdate?.screenContent});return i&&this.recordRuntimeRevealedFact(i),this.updateObservationScreenState(s.loopDetectorUpdate?.url,s.loopDetectorUpdate?.screenContent),s}isObservationCheckpointEnabled(e){return!1}updateObservationScreenState(e,n){let r=typeof e=="string"?e.trim():"",s=typeof n=="string"?n.trim():"";if(!r&&!s)return;let a=s?s.slice(0,2e3):"",i=`${r}|${this.quickHash(a)}`;i!==this.currentObservationScreenKey&&(this.currentObservationScreenKey=i)}markObservationCheckpoint(){this.currentObservationScreenKey&&this.checkpointedObservationScreenKeys.add(this.currentObservationScreenKey)}maybeBlockForObservationCheckpoint(e,n){return!this.isObservationCheckpointEnabled(n)||!this.currentObservationScreenKey||!this.isObservationStateChangingAction(e,n)||this.checkpointedObservationScreenKeys.has(this.currentObservationScreenKey)?null:{response:{status:"observation_required",reason:"checkpoint current screen before leaving or changing it; requested action was not executed",instruction:'Call log_observation now. Use decision="capture" for exact facts required as future inputs, stable locators, expected outcomes, or fixed prices/amounts; use context_only for screen inventory/current-state notes; use decision="none" only if nothing durable needs to be preserved. Then retry the same action.'},isMetaTool:!0}}isObservationStateChangingAction(e,n){if(n.isMobile&&Ma(e.name)){if(e.name!=="mobile_press_button")return q7.has(e.name);let r=String(e.args?.button??"").toUpperCase();return r==="BACK"||r==="HOME"}return H7.has(e.name)}onLoopProgressSignal(e,n,r){this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:"progress_milestone",iteration:r,details:{source:"LoopDetector",signal:e}}),e!=="repeated_side_effect"&&(this.sameGoalAbort.recordMilestone(),this.supervisorVerdictFreshness.recordProgress())}onLoopDetectorDiagnostic(e,n,r){if(e.kind==="repeated_side_effect_would_suppress"||e.kind==="repeated_side_effect_suppressed"){this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:e.kind,iteration:r,details:{source:"LoopDetector",...e}});return}this.baseDeps.sink.emit({kind:"log",ts:Date.now(),sessionId:n.id,level:"info",source:"LoopDetector",msg:"structural_force_block_diagnostics",data:e})}updateLoopDetector(e,n,r,s,a){let i=n.loopDetectorUpdate;if(i){if(i.url){let o=i.clickTargetSignature??(s&&C7.has(s)?e.actionKey(s,a??{}):void 0);e.updateUrl(i.url,o)}if(!r){let o=Ob()&&i.canvasDominant===!0&&typeof n.screenshotBase64=="string"&&n.screenshotBase64.length>0?{pixelHash:VC(n.screenshotBase64)}:void 0;e.updateScreenContent(i.screenContent,i.screenshotSize,s!==void 0&&fC.has(s),o)}if(i.drainTimedOut!==void 0){let o={url:i.url,drainTimedOut:i.drainTimedOut};e.recordDrainResult(o),this.progressBus.recordDrainResult(o)}}}async trackTapRetry(e,n,r,s,a,i,o){let l=n.args??{};if(n.name==="mobile_tap"||n.name==="mobile_long_press"){let c=e.recordTap(typeof l.screen=="string"?l.screen:"",Number(l.x??0),Number(l.y??0),typeof l.intent=="string"?l.intent:"",r.result.screenshot?.length??0);c.memoryProposal&&s.upsert&&(Hc(c.memoryProposal,i.map(d=>d.text))||(await s.upsert({id:fe("mem"),projectId:a,text:c.memoryProposal,source:"system",createdAt:Date.now(),updatedAt:Date.now()}),i.push({id:fe("mem"),projectId:a,text:c.memoryProposal,source:"system",createdAt:Date.now(),updatedAt:Date.now()}),this.log("info","TapRetryTracker","Memory saved",{text:c.memoryProposal.slice(0,120)})))}else o&&Ma(n.name)&&e.reset()}async setupScreencast(e){let n=[],r=Date.now(),s={count:0},a=null,i=this.baseDeps.createVideoRecorder?.();i?.start(this.sessionId);let o=f=>{f.action.status==="started"&&n.push({timestamp:Date.now()-r,actionName:f.action.actionName,label:f.action.intent,planStepIndex:f.action.planStepIndex})},l=f=>{this.baseDeps.screencastService?.showTapIndicator?.(f.normX,f.normY)},c=()=>{this.baseDeps.screencastService?.pausePolling?.()},d=()=>{this.baseDeps.screencastService?.resumePolling?.()};if(this.baseDeps.screencastService&&!e.config?.extensionPath)try{let f=!1,h=()=>{f||(f=!0,this.emit("screencast:started",{sessionId:this.sessionId}))};a=this.baseDeps.screencastService.onFrame(this.sessionId,p=>{s.count++,i?.addFrame(p.data,p.timestamp),h(),this.emit("screencast:frame",{sessionId:this.sessionId,data:p.data,timestamp:p.timestamp})}),await this.baseDeps.screencastService.startScreencast(this.sessionId),h(),this.log("info","VideoPipeline","recording_started",{sessionId:this.sessionId,hasRecorder:!!i}),this.on("action:progress",o),this.on("tap:indicator",l),this.on("screencast:pause-polling",c),this.on("screencast:resume-polling",d)}catch{}let u={unsubscribe:a,actionProgressHandler:o,tapIndicatorHandler:l,pausePollingHandler:c,resumePollingHandler:d,actionMarkers:n,screencastStartTime:r,projectId:e.projectId,frameCounter:s,videoRecorder:i};return this.activeScreencastState=u,this.screencastStopSignaled=!1,u}signalActiveScreencastStopped(e){!this.activeScreencastState||this.screencastStopSignaled||(this.screencastStopSignaled=!0,this.emit("screencast:stopped",{sessionId:this.sessionId,turnId:e,actionMarkers:this.activeScreencastState.actionMarkers}))}async teardownScreencast(e,n){if(!e)return;this.activeScreencastState!==e&&(this.activeScreencastState=e,this.screencastStopSignaled=!1);let{unsubscribe:r,actionProgressHandler:s,tapIndicatorHandler:a,pausePollingHandler:i,resumePollingHandler:o,actionMarkers:l,projectId:c,frameCounter:d,videoRecorder:u}=e;if(this.removeListener("action:progress",s),this.removeListener("tap:indicator",a),this.removeListener("screencast:pause-polling",i),this.removeListener("screencast:resume-polling",o),r?.(),this.baseDeps.screencastService){try{await this.baseDeps.screencastService.stopScreencast(this.sessionId),this.baseDeps.screencastService.stopDeviceRecording&&await this.baseDeps.screencastService.stopDeviceRecording(this.sessionId),this.baseDeps.screencastService.setActionMarkers?.(l)}catch{}this.signalActiveScreencastStopped(n);let f=d?.count??0;this.log(f>0?"info":"warn","VideoPipeline",f>0?"frames_captured":"frames_none",{sessionId:this.sessionId,turnId:n,framesReceived:f});let h;if(u&&u.isUnavailable?.())this.log("info","VideoPipeline","ffmpeg_unavailable",{sessionId:this.sessionId,turnId:n,framesReceived:f});else if(u&&this.baseDeps.uploadVideo)try{let p=await u.stop(),m=u.getLastError?.();if(this.log(p?"info":"warn","VideoPipeline",p?"ffmpeg_produced_file":"ffmpeg_no_file",{sessionId:this.sessionId,turnId:n,framesReceived:f,sizeBytes:p?.sizeBytes,...m?{ffmpegError:m}:{}}),p){let g=`videos/${c}/${this.sessionId}/${n}.mp4`,v=await this.baseDeps.uploadVideo(p.filePath,g),w=v&&typeof v=="object"?v:void 0;h=(typeof v=="string"?v:w?.url)??void 0,h&&u.cleanup(),this.log(h?"info":"warn","VideoPipeline",h?"upload_ok":"upload_failed",{sessionId:this.sessionId,turnId:n,r2Key:g,sizeBytes:p.sizeBytes,artifactStatus:h?"available":"unavailable",uploadStatus:w?.status??(h?"uploaded":"unknown"),attempts:w?.attempts??null,...w?.httpStatus?{httpStatus:w.httpStatus}:{},...h?{videoUrl:h}:{failureStage:"upload",localFileRetained:!0}})}}catch(p){this.log("warn","VideoPipeline","video_finalize_error",{sessionId:this.sessionId,turnId:n,error:p instanceof Error?p.message:String(p)})}return h&&this.emit("screencast:stopped",{sessionId:this.sessionId,turnId:n,actionMarkers:l,videoUrl:h}),this.activeScreencastState=null,this.screencastStopSignaled=!1,h}}async consultLoopBreaker(e,n,r,s,a,i,o,l,c){if(!n||!zb(n))return{extend:!1};if(!this.loopVisionEscalationActive())return this.log("info","BaseRuntime","loop_vision: breaker skipped (lane disabled)",{mechanism:n,iteration:a,skipReason:"kill_switch_off",spawnedByCoordinator:this.baseDeps.spawnedByCoordinator===!0}),{extend:!1};if(!this.baseDeps.supervisorService?.evaluateLoopBreaker)return this.log("warn","BaseRuntime","loop_vision: breaker skipped (no supervisor judge wired)",{mechanism:n,iteration:a,skipReason:"no_supervisor_service",spawnedByCoordinator:this.baseDeps.spawnedByCoordinator===!0}),{extend:!1};let d=x0(),u=d?I0:_h;if(this.loopVisionEscalation.decide(n,u)==="ceiling")return this.log("warn","BaseRuntime","loop_vision: extension ceiling reached, hard-blocking",{mechanism:n,grantsUsed:this.loopVisionEscalation.grantsUsed,maxGrants:u,iteration:a}),{extend:!1};let f=d&&this.loopVisionEscalation.grantsUsed>=1,h=this.loopVisionPrevProgress,p=f?{priorScreenshot:this.loopVisionPrevScreenshot,priorProgress:h}:void 0,m=c?[...this.supervisorActionLog,c]:[...this.supervisorActionLog],g=C0(n,r,m,l),v=await this.withObservationCall("supervisor_loop_breaker",()=>this.baseDeps.supervisorService.evaluateLoopBreaker(m,this.getSupervisorTaskDescription(o??""),i,g,void 0,p,this.sessionAuxMeter()));v.decision==="keep_block"&&v.reason==="timeout"&&En(this.baseDeps.observationArchive,"lifecycle",this.getObservationArchiveContext(),()=>({state:"incomplete",reasonCode:"supervisor_producer_timeout"}));let w=v.progress??null,E=v.decision==="continue",R=v.decision==="continue"?void 0:v.reason;this._lastLoopBreakerConsult={consulted:!0,keepBlockReason:R,deltaEvidence:w?.delta_evidence};let _=f&&this.loopVisionEscalation.grantsUsed>=_h,b=Ob()&&this._lastCanvasDominant,y=f&&k0(h,w,b),T=_&&E&&!y,k=E&&!T,I=f?{grantIndex:this.loopVisionEscalation.grantsUsed+1,priorDone:h?.done??null,done:w?.done??null,delta:h&&w?w.done-h.done:null,deltaPresent:y}:void 0;return this.baseDeps.sink.emit({kind:"supervisor_verdict",ts:Date.now(),sessionId:s.id,verdict:k?"continue":"block",message:k?`loop_vision CONTINUE (${n}) \u2014 false-block rescued`:T?"loop_vision keep_block (no_delta)":`loop_vision keep_block (${n}: ${R})`,iteration:a,actionLogSize:m.length,progress:w,...I?{differential:I}:{}}),k?(this.loopVisionEscalation.recordGrant(),e.grantExtension(n,Wb),d&&(this.loopVisionPrevScreenshot=i,this.loopVisionPrevProgress=w),this.log("info","BaseRuntime","loop_vision: judge CONTINUE granted extension",{mechanism:n,grantsUsed:this.loopVisionEscalation.grantsUsed,extensionActions:Wb,iteration:a,progress:w,...I?{differential:I}:{}}),{extend:!0}):T?(this.log("info","BaseRuntime","loop_vision: keep_block no_delta (differential)",{mechanism:n,iteration:a,priorProgress:h,progress:w,differential:I}),{extend:!1}):(this.log("warn","BaseRuntime","loop_vision: judge kept block (fail-closed)",{mechanism:n,reason:R,iteration:a,progress:w,...I?{differential:I}:{}}),{extend:!1})}buildPeriodicSupervisorHints(e){return R0(e,w0()?T0(this.appliedSupervisorVerdicts):void 0)}async applySupervisorVerdict(e,n,r,s){let a=this.currentSupervisorEvidence(),i=a.requiresStepBinding===!0,o=e.action==="redirect"?this.groundSupervisorRedirect(e.message,r):null,c=(e.action==="redirect"?b0(e,a):null)||(i&&o?.verdict==="ungrounded"&&this.supervisorRedirectGroundingEnforced()?"ungrounded_locator":null);if(e.action==="redirect"&&(i?(this.log("info","Supervisor","supervisor_redirect:proposal",{iteration:r,proposedMessage:e.message}),e={...e,message:c?bh:v0(a)}):c&&(e={...e,message:bh})),e.action==="redirect"&&!m0()){let h=this.redirectFlipFlop.observeRedirect(e.message);if(h){let p=f0();this.log("warn","Supervisor","Redirect flip-flop guard",{iteration:r,enforced:p,...h}),this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:p?"redirect_flipflop_guard":"redirect_flipflop_would_fire",iteration:r,details:`${h.trigger} strike ${h.strike} \u2192 ${h.escalation} (${h.redirectCount} redirects${h.directions?`, "${h.directions[0]}" \u2194 "${h.directions[1]}"`:""})`}),p&&(e=h.escalation==="block"?{action:"block",reason:y0(h),rawText:e.rawText}:{action:"wrap_up",message:g0(h),rawText:e.rawText})}}let d=p0()?this.sameGoalAbort.observeVerdict(e.action):null;if(d){let h=e.action==="redirect"?e.message:void 0;this.log("warn","Supervisor","Same-goal abort escalation",{iteration:r,escalation:d,strike:this.sameGoalAbort.fires,consecutiveRedirects:nd,originalMessage:h}),e=d==="block"?{action:"block",reason:`${nd*2} consecutive supervisor corrections produced no progress milestone; the agent ignored the abort steer, ending the run`,rawText:e.rawText}:{action:"abort",reason:`${nd} consecutive supervisor corrections produced no progress milestone`,rawText:e.rawText}}let u=e.action==="redirect"?c:null;this.appliedSupervisorVerdicts=S0(this.appliedSupervisorVerdicts,{action:e.action,iteration:r,message:E0(e)}),this.log("info","Supervisor","Applying verdict",{action:e.action,iteration:r}),this.baseDeps.sink.emit({kind:"supervisor_verdict",ts:Date.now(),sessionId:n.id,verdict:e.action,message:e.action==="block"||e.action==="abort"?e.reason:e.message,iteration:r,actionLogSize:this.supervisorActionLog.length});let f=_b(e);if(e.action==="redirect"){this.log("info","Supervisor","REDIRECT",{message:e.message}),u&&(this.log("info","Supervisor","supervisor_redirect:evidence_downgraded",{iteration:r,reason:u}),this.supervisorStandingConstraint=null,this.supervisorSteeringEvidence=null,this.steeringVeto.reset());let h=!!u||o?.verdict==="ungrounded"&&this.supervisorRedirectGroundingEnforced(),p=h?_C(f,u?bh:MC(e.message??"",o?.verdict==="ungrounded"?o.unobserved:[])):f;h||(this.supervisorSteeringEvidence=e.recoveryEvidence??null,this.steeringVeto.recordRedirect(e.message??"Change approach."),p.standingConstraint&&this.armSupervisorStandingConstraint(p.standingConstraint,r));let m=s[s.length-1];return m&&p.injectedFunctionResponseError&&(m.response={...m.response,status:"error",metadata:{...m.response?.metadata??{},error:p.injectedFunctionResponseError}}),{done:p.done}}if(e.action==="block"){this.log("warn","Supervisor","BLOCK",{reason:e.reason});let h=f.blockedReason,p={sessionId:n.id,id:fe("msg"),role:"model",text:`We stopped this run because the agent got stuck: ${h}. No usage charged.`,timestamp:Date.now(),actionName:"exploration_blocked",actionArgs:{attempted:`Supervisor intervention after ${this.supervisorActionLog.length} actions`,obstacle:h,question:"The supervisor stopped this session. Please review and retry."}};return await this.persistMessage(p),this.emit("message:added",{sessionId:n.id,message:p}),s.push({name:"exploration_blocked",response:{status:"awaiting_user_guidance"}}),{done:f.done,blockedReason:h}}if(e.action==="wrap_up"){this.log("info","Supervisor","WRAP_UP",{message:e.message});let h=s[s.length-1];h&&f.injectedFunctionResponseError&&(h.response={...h.response,status:"error",metadata:{...h.response?.metadata??{},error:f.injectedFunctionResponseError}})}if(e.action==="abort"){this.steeringVeto.recordRedirect(f.injectedFunctionResponseError??e.reason),f.standingConstraint&&this.armSupervisorStandingConstraint(f.standingConstraint,r);let h=s[s.length-1];h&&f.injectedFunctionResponseError&&(h.response={...h.response,status:"error",metadata:{...h.response?.metadata??{},error:f.injectedFunctionResponseError}})}return e.action==="continue"&&this.clearSupervisorStandingConstraint(),{done:f.done}}onIterationStart(e,n,r){}onIterationEnd(e){}onToolResponsesRecorded(e){}prepareModelTrace(e){return e}withCanvasStrategy(e,n,r){if(!Lp()||(n!==this._canvasStrategyActive&&(this.log("info","BaseRuntime",n?"canvas_strategy: injected (page is canvas-dominant)":"canvas_strategy: retracted (page no longer canvas-dominant)",{iteration:r,canvasDominant:n}),this._canvasStrategyActive=n),!n))return e;let s=e[e.length-1],a=s?.parts?.some(i=>i!=null&&typeof i=="object"&&"functionResponse"in i);if(s?.role==="user"&&!a){let i={...s,parts:[...s.parts??[],{text:qy}]};return[...e.slice(0,-1),i]}return[...e,{role:"user",parts:[{text:qy}]}]}getSupervisorTaskDescription(e){return e}hasBackgroundWork(){return!1}waitForBackgroundWork(){return Promise.resolve()}async onLoopExhausted(e,n){let r={sessionId:e.id,id:fe("msg"),role:"model",text:`I paused before finishing this run (step limit of ${n} reached). Reply "continue" to let me proceed, or clarify the exact target page/expected behavior.`,timestamp:Date.now()};await this.persistMessage(r),this.emit("message:added",{sessionId:e.id,message:r})}async consultRunBackstopJudge(e,n,r,s,a){let i=this.baseDeps.supervisorService;if(!i?.evaluateLoopBreaker)return this.log("warn","BaseRuntime","backstop:judge_unavailable (no supervisor judge wired)",{iteration:n,ceiling:a.ceiling,spawnedByCoordinator:this.baseDeps.spawnedByCoordinator===!0}),"unavailable";try{let l=`RUN BACKSTOP PROGRESS CHECK \u2014 this run has hit a safety ceiling (${dh(a.elapsedMs,a.iteration)}, ${a.ceiling}). Judge ONE thing: given the objective and the recent actions + current screenshot, is the agent GENUINELY PROGRESSING toward the objective \u2014 measurably closer than several steps ago \u2014 or WANDERING / not getting closer (recycling the same screens/URLs, repeating failed attempts, or off on an unrelated tangent)? Answer CONTINUE ONLY if you can point to concrete forward progress toward the objective; if it is not clearly getting closer, answer BLOCK. When unsure, answer BLOCK.`,c=await i.evaluateLoopBreaker([...this.supervisorActionLog],this.getSupervisorTaskDescription(s??""),r,l,void 0,void 0,this.sessionAuxMeter());return c.decision==="continue"?"progressing":c.reason==="block"||c.reason==="redirect"||c.reason==="wrap_up"?"wandering":"unavailable"}catch(o){return this.log("warn","BaseRuntime","backstop:judge_error \u2014 failing closed",{iteration:n,ceiling:a.ceiling,error:o?.message}),"unavailable"}}async handleRunBackstop(e,n,r,s,a="ceiling_cap"){let i=ch(e),o=s&&s.trim().length>0?`"${s.trim().slice(0,160)}"`:"this task",l="a series of navigation and interaction steps";try{let f=new Set,h=[];for(let p of[...this.supervisorActionLog].reverse())if(!(!p?.action||p.skipped||f.has(p.action))&&(f.add(p.action),h.push(p.intent?`${p.action} (${p.intent.slice(0,40)})`:p.action),h.length>=4))break;h.length>0&&(l=h.reverse().join(", "))}catch{}this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:"run_backstop",iteration:r,details:`terminate on ${e.ceiling} (${i}); reason=${a}; cumulativeTokens=${e.cumulativeTokens}`}),this.baseDeps.sink.emit({kind:"supervisor_verdict",ts:Date.now(),sessionId:n.id,verdict:"block",message:`run_backstop terminate (${e.ceiling}: ${i}; reason=${a})`,iteration:r,actionLogSize:this.supervisorActionLog.length});let d=`${a==="wander"?BC(o,e.elapsedMs,e.iteration):GC(e.elapsedMs,e.iteration)} What I tried: ${l}. Send a new message with a more specific target or expected result and I'll try again.`,u={sessionId:n.id,id:fe("msg"),role:"model",text:d,timestamp:Date.now(),actionName:"exploration_blocked",actionArgs:{attempted:`${this.supervisorActionLog.length} actions; ${i}`,obstacle:a==="wander"?`Run backstop: not getting closer to ${o} (${e.ceiling}).`:`Run backstop: hit the safety limit (${e.ceiling}; ${a}).`,question:"The run stopped at a safety limit. Adjust the target or steps and reply to try again."}};return await this.persistMessage(u),this.emit("message:added",{sessionId:n.id,message:u}),this.log("warn","BaseRuntime","run_backstop fired (terminate)",{ceiling:e.ceiling,reason:a,iteration:r,elapsedMs:e.elapsedMs,cumulativeTokens:e.cumulativeTokens}),!0}onLoopForceBlock(e,n,r){}async recordLoopBlockIssue(e,n,r){}async runLoop(e){let{session:n,maxIterations:r,snapshotOnly:s,isMobile:a,devicePlatform:i,taskDescription:o,supervisorHints:l,preserveAllPageSnapshots:c,runJsLoopRecoveryPolicy:d="warn_response"}=e,u=this.conversationTrace;this._endMechanism=void 0,this._lastLoopBreakerConsult=void 0,this._currentTaskDescription=o;let f=!1,h=!1,p,m,g=0,v=Math.floor(Date.now()/1e3),w=0,E=0,R=UC()&&n.kind==="assistant_v2",_=Date.now(),b=0,y=0,T=2,k=new Yc(B=>this.onLoopProgressSignal(B,n,w),{trackNovelScreenContent:!0,...n.config?.loopDetector??{},rearmUrlNovelty:pC()&&n.kind==="assistant_v2",repeatedSideEffectGuard:hC()},B=>this.onLoopDetectorDiagnostic(B,n,w));this.isExplorationMode()&&k.setMode("exploration"),this._activeLoopDetector=k,this._lastNavPageSettled=null,this._lastCanvasDominant=!1,this._canvasStrategyActive=!1;let I=new Jc,A;this.resetSteeringStateForNewRun(),this.resetLoopVisionState(),this.progressBus.resetSupervisorLog(),this.pendingSupervisorVerdict=null,this.resolvedSupervisorVerdict=null,this.toolDispatchInFlight=!1,this.supervisorVerdictFreshness.reset(),this.appliedSupervisorVerdicts=[],this.recentActionsForEvidence=[],this.recentClickTargets=[];let C=[];for(let B=1;B<=r;B++){if(w=B,!this._isRunning)throw new Error("cancelled");if(R){let ne=FC({elapsedMs:_>0?Date.now()-_:0,iteration:B,cumulativeTokens:b},jC(Nb,y));if(ne.kind==="terminate"){this.log("info","BaseRuntime","backstop:ceiling_reached",{ceiling:ne.ceiling,iteration:B,elapsedMs:ne.elapsedMs,cumulativeTokens:ne.cumulativeTokens,extensionsGranted:y});let ue="unavailable";y<lh&&(ue=await this.consultRunBackstopJudge(n,B,A,o,ne),this.log("info","BaseRuntime","backstop:judge_verdict",{verdict:ue,ceiling:ne.ceiling,iteration:B,extensionsGranted:y}));let Ee=$C(ue,y);if(Ee.action==="extend")y+=1,this.log("info","BaseRuntime","backstop:extended",{extensionsGranted:y,maxExtensions:lh,ceiling:ne.ceiling,iteration:B});else{let Oe=!1;try{Oe=await this.handleRunBackstop(ne,n,B,o,Ee.reason)}catch(pe){this.log("warn","BaseRuntime","run_backstop emit failed \u2014 failing open",{error:pe?.message,iteration:B}),Oe=!1}if(Oe){this.log("info","BaseRuntime","backstop:terminated",{reason:Ee.reason,verdict:ue,ceiling:ne.ceiling,iteration:B,extensionsGranted:y}),f=!0,h=!0,m="backstop",p=Ee.reason==="wander"?`Stopped: not getting closer to the objective (${ch(ne)}).`:`Stopped: hit the safety limit (${ch(ne)}).`,this.recordEndMechanism("run_backstop");break}}}}if(typeof process<"u"&&typeof process.memoryUsage=="function")try{let ne=process.memoryUsage();this.log("info","Runtime","iteration_memory",{iteration:B,rssMb:Math.round(ne.rss/1024/1024),heapUsedMb:Math.round(ne.heapUsed/1024/1024),heapTotalMb:Math.round(ne.heapTotal/1024/1024),externalMb:Math.round(ne.external/1024/1024),arrayBuffersMb:Math.round(ne.arrayBuffers/1024/1024)})}catch{}await this.onIterationStart(u,n,B),c||this.stripOldPageSnapshots(u,s);let L=this.getToolSet({isMobile:a,snapshotOnly:s,devicePlatform:i}),D=this.prepareModelTrace(this.withCanvasStrategy(this.systemPromptText?u.slice(1):u,this._lastCanvasDominant,B)),V=Fa(D),$=D.reduce((ne,ue)=>ne+(ue.parts??[]).reduce((Ee,Oe)=>{let pe=Oe.text?.length??0,Se=Oe.functionCall?JSON.stringify(Oe.functionCall).length:0,Pe=Oe.functionResponse?JSON.stringify(Oe.functionResponse.response??{}).length:0;return Ee+pe+Se+Pe},0),0);this.log("info","BaseRuntime","llm_call_preflight",{iteration:B,traceMsgCount:D.length,estimatedTraceChars:$});let Q=Date.now(),G;try{G=await this.withObservationCall("agent_step",()=>yt({model:this.model,system:this.systemPromptText??void 0,messages:V,tools:L,toolChoice:this.toolChoiceForTurn(L),temperature:.2,topP:.95,topK:40,maxOutputTokens:8192,maxRetries:this._isRunning?7:0,abortSignal:this._abortController?.signal}))}catch(ne){let ue=String(ne?.message||ne?.data?.error?.message||"");if(ue.includes("Unable to process input image")||ue.includes("INVALID_ARGUMENT")){this.log("warn","BaseRuntime","Gemini rejected image \u2014 retrying without screenshots",{error:ue.slice(0,200)});let Ee=D.map(pe=>({...pe,parts:(pe.parts||[]).filter(Se=>!Se.inlineData)})),Oe=Fa(Ee);G=await this.withObservationCall("agent_step_image_free_retry",()=>yt({model:this.model,system:this.systemPromptText??void 0,messages:Oe,tools:L,toolChoice:this.toolChoiceForTurn(L),temperature:.2,topP:.95,topK:40,maxOutputTokens:8192,maxRetries:this._isRunning?7:0,abortSignal:this._abortController?.signal}))}else throw ne}let U=Date.now()-Q;B===1&&this.recordStartupMilestone("first_llm_completed",{iteration:B,toolCallCount:G.toolCalls.length,textLength:G.text?.length??0});let ee=G.usage,H=(ee?.inputTokens??0)+(ee?.outputTokens??0);b+=(ee?.inputTokens??0)+(ee?.outputTokens??0);let O=ee?.inputTokenDetails?.cacheReadTokens??ee?.cachedInputTokens??G.providerMetadata?.google?.usageMetadata?.cachedContentTokenCount??0;if(H>0&&(this.tokenCount=H,this.emit("context:updated",{sessionId:n.id,tokenCount:H}),await this.baseDeps.chatRepo.updateSessionFields(n.id,{lastTokenCount:H}),this.baseDeps.sink.emit({kind:"llm_usage",ts:Date.now(),sessionId:n.id,runId:this.getCurrentRunId(),callKind:"agent_step",...this.planStepAttribution(),...this.llmSourceMeta(),...F0(G.toolCalls.map(ne=>ne.toolName),Eh()),model:n.config.model||"unknown",promptTokens:ee?.inputTokens??0,completionTokens:ee?.outputTokens??0,cachedInputTokens:O,totalTokens:H,durationMs:U,finishReason:G.finishReason??void 0,tokenCount:H,messageCount:u.length,systemPromptHash:this.systemPromptText?this.quickHash(this.systemPromptText):void 0,lastToolResults:C.map(ne=>({toolName:ne.name,status:ne.response?.status??"unknown"})),chosenActions:G.toolCalls.map(ne=>({toolName:ne.toolName,intent:typeof ne.input?.intent=="string"?ne.input.intent:void 0})),textResponse:typeof G.text=="string"?G.text.slice(0,200):void 0})),!this._isRunning)throw new Error("cancelled");let j=G.response.messages,z=Lb(j);for(let ne of z)u.push(ne);let K=G.toolCalls.map(ne=>({name:ne.toolName,args:ne.input??{},toolCallId:ne.toolCallId})),te=G.text;if(K.length===0){let ne=te?.replace(/[\x00-\x1f\x7f-\x9f]|<ctrl\d+>/g,"").trim();if(te&&!ne&&this.log("warn","BaseRuntime","Model returned garbage text, treating as empty response",{charCount:te.length}),ne){let Ee={sessionId:n.id,id:fe("msg"),role:"model",text:te.slice(0,6e3),timestamp:Date.now()};if(await this.persistMessage(Ee),this.emit("message:added",{sessionId:n.id,message:Ee}),f=!0,this.hasBackgroundWork()){this.log("info","BaseRuntime","Text-only response but background work pending \u2014 waiting silently (not persisting LLM text)",{text:te?.slice(0,100)}),await this.waitForBackgroundWork();continue}this.log("info","BaseRuntime","Text-only response, no background work \u2014 exiting loop",{text:te?.slice(0,100)});break}if(E++,this.hasBackgroundWork()){this.log("info","BaseRuntime","Empty response but background work pending \u2014 waiting silently"),await this.waitForBackgroundWork(),E=0;continue}if(this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:"empty_response",iteration:B,details:`attempt ${E}/${T}`}),g>0&&E<=T){this.log("info","BaseRuntime","Model returned empty response, nudging to continue",{stepIndex:g,attempt:E,maxAttempts:T});let Ee;if(a)Ee=(await this.baseDeps.mobileMcpService.takeScreenshot(this.sessionId)).base64;else{if(!this.baseDeps.computerUseService)throw new Error("[BaseRuntime] Browser nudge not available \u2014 no computerUseService");Ee=(await this.baseDeps.computerUseService.invoke({sessionId:n.id,action:"screenshot",args:{},config:{...n.config,projectId:n.projectId}})).screenshot}let pe=[{text:"You stopped without responding. Here is the current page state. Please continue with your task, or if you are done, call assistant_v2_report with a summary."}];s||pe.push({inlineData:{mimeType:"image/png",data:Ee}}),u.push({role:"user",parts:pe});continue}this.log("warn","BaseRuntime","Model returned consecutive empty responses, giving up",{emptyResponseCount:E});let ue={sessionId:n.id,id:fe("msg"),role:"model",text:g>0?`Model returned empty responses after ${g} action(s). This may be caused by rate limiting or a temporary API issue. You can retry by sending another message.`:"Model returned an empty response and could not start. This may be caused by rate limiting or a temporary API issue. You can retry by sending another message.",timestamp:Date.now()};await this.persistMessage(ue),this.emit("message:added",{sessionId:n.id,message:ue}),f=!0;break}if(E=0,te){let ne={sessionId:n.id,id:fe("msg"),role:"system",actionName:"assistant_v2_text",actionArgs:{iteration:B},text:te.slice(0,6e3),timestamp:Date.now()};await this.persistMessage(ne),this.emit("message:added",{sessionId:n.id,message:ne})}let X=[],q=[],P=null,F=!1,Y=new Set;if(a)for(let ne=0;ne<K.length-1;ne++)Ma(K[ne].name)&&K[ne].name!=="mobile_screenshot"&&Ma(K[ne+1].name)&&K[ne+1].name!=="mobile_screenshot"&&Y.add(ne);let ie=-1;for(let ne of K){if(ie++,!this._isRunning)break;g++;let ue={iteration:B,sessionId:this.sessionId,session:n,isMobile:a,snapshotOnly:s,devicePlatform:i,callIndex:ie,totalCalls:K.length,skipScreenshotSet:Y,lastScreenshotBase64:A,stepIndex:g,turnTimestamp:v};this.baseDeps.observationArchive&&(q[ie]={...this.getObservationArchiveContext(),projectId:n.projectId,toolCallId:ne.toolCallId,iteration:B,actionIndex:g});let Ee=this.preDispatchRefusal(ne,k,B);if(Ee!==null){this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:Ee.kind==="steering_veto"?"steering_veto":"step_regression_refused",iteration:B,details:Ee.details}),X.push({name:ne.name,response:Ee.response});continue}this.toolDispatchInFlight=!0;let Oe=Date.now(),pe=await this.handleToolCall(ne,ue);q[ie]&&pe.observationCaptureId&&(q[ie].captureId=pe.observationCaptureId);let Se=Date.now()-Oe;!pe.isMetaTool&&pe.response?.clickReceipt?.dispatch!=="not_dispatched"&&this.steeringVeto.recordExecuted(ne.name,ne.args??{}),this.noteFullSnapshotForRedirectGrounding(pe),this.applyLoopDetectorReset(pe,k,I);let Pe=null;if(pe.isMetaTool||(Pe=this.observeStepPage(ne,pe,k,B)),this.updateLoopDetector(k,pe,Y.has(ie),ne.name,ne.args??{}),Pe){this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:"loop_block",iteration:B,details:Pe.reason}),this.recordEndMechanism("loop_block");let xe={sessionId:n.id,id:fe("msg"),role:"model",text:"We stopped this run because it kept returning to a page it had already finished. No usage charged.",timestamp:Date.now(),actionName:"exploration_blocked",actionArgs:{attempted:`Returning to an already-completed step's page via "${ne.name}"`,obstacle:Pe.reason,question:"The run stopped because it kept going back to a page it had already finished. Adjust the steps and reply to continue."}};await this.persistMessage(xe),this.emit("message:added",{sessionId:n.id,message:xe}),X.push({name:"exploration_blocked",response:{status:"awaiting_user_guidance"}}),F=!0,f=!0,h=!0,p=Pe.reason,m="loop";break}if(!pe.isMetaTool&&!O7(pe)){let xe=k.check(ne.name,ne.args??{},B,!0);if(xe.action==="force_block")if((await this.consultLoopBreaker(k,xe.mechanism,xe.message,n,B,pe.screenshotBase64??A,o,l,pe.supervisorEntry)).extend)this.log("info","BaseRuntime","loop_vision: proceeding past force-block (extension granted)",{mechanism:xe.mechanism,iteration:B});else{this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:"loop_block",iteration:B,details:xe.message}),this.log("warn","BaseRuntime","Force-blocking loop",{message:xe.message,iteration:B}),this.onLoopForceBlock(n,xe.message,B),await this.recordLoopBlockIssue(n,xe.message,g),this.recordEndMechanism("loop_block");let Z=P0({mechanism:xe.mechanism,actionCount:this.supervisorActionLog.length,judge:this._lastLoopBreakerConsult}),N={sessionId:n.id,id:fe("msg"),role:"model",text:Z,timestamp:Date.now(),actionName:"exploration_blocked",actionArgs:{attempted:`Repeated "${ne.name}" on the same target (${O0[xe.mechanism]})`,obstacle:xe.message,question:"The run stopped because it was no longer making visible progress. Adjust the target or steps and reply to continue."}};await this.persistMessage(N),this.emit("message:added",{sessionId:n.id,message:N}),X.push({name:"exploration_blocked",response:{status:"awaiting_user_guidance"}}),F=!0,f=!0,h=!0,p=xe.message,m="loop";break}if(xe.action==="warn"){if(this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:"loop_warning",iteration:B,details:xe.message}),this.log("warn","BaseRuntime","Loop warning",{message:xe.message,iteration:B}),ne.name==="run_js"&&d==="partial_sitemap_salvage"){let N="",W="",de="";if(this.baseDeps.computerUseService){let Qe=await this.baseDeps.computerUseService.invoke({sessionId:n.id,action:"screenshot",args:{},config:n.config});N=Qe.screenshot,W=Qe.url??"",de=[Qe.aiSnapshot,Qe.domChanges].filter(Boolean).join(`
561
+ [END SUMMARY]`}]},g=f?[f,m,...p]:[m,...p];d.length=0,d.push(...g),this.conversationTrace=d,await this.persistConversationTrace(e,d);let v={sessionId:e.id,id:fe("msg"),role:"system",actionName:"context_summarized",text:"Chat context summarized",timestamp:Date.now()};return await this.persistMessage(v),this.emit("message:added",{sessionId:e.id,message:v}),this.lastContextSummaryTokenCount=r,this.lastContextSummaryMessageId=c,!0}async persistConversationTrace(e,n){await this.baseDeps.chatRepo.updateSessionFields(e.id,{conversationTrace:n})}getObservationArchiveContext(){return{sessionId:this.sessionId,runtimeId:this.sessionId,runId:this.getCurrentRunId(),stepIndex:this.currentPlanStepIndex()}}async withObservationCall(e,n){let r=this.baseDeps.observationScope;if(!r)return n();let s;try{s={...r.getContext(),...this.getObservationArchiveContext(),logicalCallId:fe("model_call"),logicalCallContext:"bound",callPurpose:e}}catch{return En(this.baseDeps.observationArchive,"lifecycle",{sessionId:this.sessionId},()=>({state:"incomplete",reasonCode:"observer_context_failed"})),n()}let a;try{return await r.runWithContext(s,()=>{try{a=n()}catch(i){a=Promise.reject(i)}return a})}catch{return a||(En(r.archive,"lifecycle",s,()=>({state:"incomplete",reasonCode:"observer_context_failed"})),n())}}recordObservationToolResponse(e,n,r,s){En(this.baseDeps.observationArchive,"tool_response",e,()=>zr({functionResponse:n,screenshotParts:r,occurrence:s,stage:"assembled_before_pruning"}))}currentPlanStepIndex(){}onBrowserActionDispatched(e){}currentPlanStepType(){}currentPlanStepText(){}planControlLabelsForAction(){}necessityContextText(){let e=this.currentPlanStepText()?.trim();if(e)return e;let n=this._currentTaskDescription?.trim();return n||void 0}planStepAttribution(){let e=this.currentPlanStepType();return e===void 0?{}:{planStepIndex:this.currentPlanStepIndex(),planStepType:e}}hasTypedCredentialThisRun(){return this._credentialTypedThisRun}typedCredentialNamesThisRun(){return[...this._typedCredentialNames]}resetCredentialTypingObservations(){this._credentialTypedThisRun=!1,this._typedCredentialNames=new Set}async executeBrowserAction(e,n){if(!this.browserActionExecutor)throw new Error("[BaseRuntime] Browser actions not available \u2014 no computerUseService");let r=e.args??{},s=typeof r.intent=="string"?r.intent.trim():void 0;if(Lk({config:n.session.config,actionName:e.name,text:r.text,value:r.value,inputType:r.inputType,label:r.label,matchedLabel:r.matchedLabel,intent:r.intent,screen:r.screen,stepText:r.stepText}))return this.log("info","BaseRuntime","executeBrowserAction: refusing email write in discovery run (AG-3588)",{sessionId:n.session.id,action:e.name,label:r.label}),{response:{status:"error",error:"Refused: discovery runs must not submit credentials, sign up, or fill email/login fields. This is expected \u2014 signup / authenticated flows belong in the post-scope Explorer, not in discovery. Treat this signup surface as a discovered area: call log_observation to record the form fields, validation rules, and what the surface offers (including requires_auth=true), then continue mapping other public/unauthenticated pages. When discovery is complete, finish with assistant_v2_report including the signup gate in discoveredAreas. Do NOT call exploration_blocked \u2014 a signup/auth gate is normal discovery output, not a failure. The Coordinator presents the scope checkpoint from your partial discovery (AG-3588)."},isMetaTool:!0};this.recordStartupMilestone("first_browser_tool_dispatched",{toolName:e.name,isMobile:!1,iteration:n.iteration}),this.onBrowserActionDispatched(e.name),n.session.config.extensionPath&&this.log("info","BaseRuntime","executeBrowserAction with extension",{sessionId:n.session.id,projectId:n.session.projectId,extensionPath:n.session.config.extensionPath,action:e.name});let a=this.drainAbortControllers.get(n.session.id);(!a||a.signal.aborted)&&(a=new AbortController,this.drainAbortControllers.set(n.session.id,a));let i=await this.browserActionExecutor.execute(n.session.id,e.name,r,n.session.projectId,{...n.session.config,unattendedRun:this.unattendedRun,planStepText:this.necessityContextText(),planControlLabels:this.planControlLabelsForAction()},{intent:s,stepIndex:n.stepIndex,planStepIndex:this.currentPlanStepIndex(),turnTimestamp:n.turnTimestamp,signal:a.signal,...this.baseDeps.observationArchive?{observationContext:{...this.getObservationArchiveContext(),toolCallId:e.toolCallId,iteration:n.iteration}}:{}});this.recordStartupMilestone("first_browser_tool_completed",{toolName:e.name,isMobile:!1,iteration:n.iteration});let{result:o,response:l,message:c}=i;this.captureVerbatimTypedValue(e,l,n.session.config);let d=l&&typeof l=="object"?l.verbatimInputSource:void 0;if(d&&c&&(c.actionArgs={...c.actionArgs??{},verbatimInputSource:d}),l&&typeof l=="object"&&l.status==="error"&&typeof l.error=="string"&&this.onBrowserActionError(e.name,l.error),Dc(e.name)){let g=l??{};if(!(g.status==="error"||g.error!=null||g.valueMismatch===!0)){this._credentialTypedThisRun=!0;let w=e.args?.credentialName;typeof w=="string"&&w.trim()&&this._typedCredentialNames.add(w.trim())}}i.drainResult&&i.drainResult.pendingAtStart>0&&this.log("info","BrowserActionExecutor","drain",{sessionId:n.session.id,actionName:e.name,iteration:n.iteration,pendingAtStart:i.drainResult.pendingAtStart,pendingAtEnd:i.drainResult.pendingAtEnd,waitedMs:i.drainResult.waitedMs,timedOut:i.drainResult.timedOut,aborted:i.drainResult.aborted,oldestAgeMs:i.drainResult.oldestAgeMs,projectId:n.session.projectId});let u=o.metadata?.clickReceipt,f=u?.addressing==="label"||!u&&r.label?`label=${u?.requestedTarget.label??r.label}`:r.ref?`ref=${r.ref}`:r.x!=null?`(${r.x},${r.y})`:void 0,h={action:e.name,intent:s,screen:typeof r.screen=="string"?r.screen:void 0,target:f,activeTab:o?.metadata?.activeTab,drainMs:i.drainResult?.waitedMs,clickReceipt:u,observedUrl:o.url},p=!n.snapshotOnly&&o.screenshot?[{inlineData:{mimeType:"image/png",data:o.screenshot}}]:void 0;o.metadata?.pageSettled!==void 0&&(this._lastNavPageSettled=o.metadata.pageSettled),this._lastCanvasDominant=o.canvasDominant===!0;let m=o.metadata?.navigateMeta;if(m?.originChanged===!0&&l){let g=eh(m);if(g){let v=g.redirectChain.length>0?g.redirectChain.join(" -> "):"(direct)";l.navigateNote=`navigate: requested ${g.requestedUrl}, redirected cross-origin to ${g.finalUrl} via ${v}. This destination looks like an authentication bridge (path matches ${g.matchedPattern}). The requested URL likely requires credentials Agentiqa does not have. Do NOT retry navigate to ${g.requestedUrl} in this turn \u2014 instead, surface this to the user (ask whether they have credentials for the preview, or want to provide a different URL).`,this.baseDeps.sink.emit({kind:"log",ts:Date.now(),sessionId:n.session.id,level:"info",source:"PreviewAuthDetector",msg:"preview_requires_auth",data:g})}else l.navigateNote=Sb(m)}else if(m&&l){let g=Eb(m);g&&(l.navigateNote=g,this.baseDeps.sink.emit({kind:"log",ts:Date.now(),sessionId:n.session.id,level:"info",source:"NavigateQueryDrop",msg:"navigate_dropped_query",data:{previousUrl:m.previousUrl,requestedUrl:m.requestedUrl,finalUrl:m.finalUrl}}))}return{response:l,parts:p,message:c,...i.observationCaptureId?{observationCaptureId:i.observationCaptureId}:{},...o.metadata?.observation?{observation:o.metadata.observation}:{},supervisorEntry:h,loopDetectorUpdate:{url:o.url,screenContent:l?.pageSnapshot,screenshotSize:n.skipScreenshotSet.has(n.callIndex)?void 0:o.screenshot?.length,canvasDominant:o.canvasDominant===!0,drainTimedOut:i.drainResult?.timedOut??!1,...l?.clickTarget?{clickTargetSignature:Fr(l.clickTarget)}:{}},screenshotBase64:o.screenshot,...o.captureMode&&{captureMode:o.captureMode},...typeof o.aiSnapshot=="string"&&{groundingSnapshot:o.aiSnapshot,groundingCanvasDominant:o.canvasDominant===!0},...o.snapshotKind&&{groundingSnapshotKind:o.snapshotKind},supervisorObservationUnavailable:typeof o.aiSnapshot!="string"&&!o.screenshot,...o.metadata?.noObservedEffect&&{noObservedEffect:!0},...o.metadata?.effectObserved!==void 0&&{effectObserved:o.metadata.effectObserved},...o.metadata?.elementFingerprint&&{writeElementFingerprint:o.metadata.elementFingerprint},...o.metadata?.verbatimGuardMismatch&&{writeGuardMismatch:!0},...o.metadata?.planLabelResolutions?.length&&{planLabelResolutions:o.metadata.planLabelResolutions},...o.metadata?.matchedLabel&&{writeMatchedLabel:o.metadata.matchedLabel},...o.metadata?.matchedName&&{clickMatchedName:o.metadata.matchedName}}}onBrowserActionError(e,n){}async executeMobileAction(e,n){if(!this.mobileActionExecutor)return{response:{status:"error",error:"Mobile executor not available"}};let r=e.args??{},s=typeof r.intent=="string"?r.intent.trim():void 0;this.recordStartupMilestone("first_browser_tool_dispatched",{toolName:e.name,isMobile:!0,iteration:n.iteration});let a=await this.mobileActionExecutor.execute(n.session.id,e.name,r,n.session.projectId,n.session.config,{intent:s,stepIndex:n.stepIndex,skipScreenshot:n.skipScreenshotSet.has(n.callIndex),turnTimestamp:n.turnTimestamp});this.recordStartupMilestone("first_browser_tool_completed",{toolName:e.name,isMobile:!0,iteration:n.iteration});let{result:i,response:o,message:l}=a,c=r.ref?`ref=${r.ref}`:r.x!=null?`(${r.x},${r.y})`:void 0,d={action:e.name,intent:s,screen:typeof r.screen=="string"?r.screen:void 0,target:c,activeTab:i?.metadata?.activeTab},u=!n.snapshotOnly&&i.screenshot?[{inlineData:{mimeType:"image/png",data:i.screenshot}}]:void 0;return{response:o,parts:u,message:l,supervisorEntry:d,loopDetectorUpdate:{url:i.url,screenContent:o?.pageSnapshot,screenshotSize:n.skipScreenshotSet.has(n.callIndex)?void 0:i.screenshot?.length},screenshotBase64:i.screenshot}}captureVerbatimTypedValue(e,n,r){if(!Es()||e.name!=="type_text_at"&&e.name!=="set_focused_input_value"||e.args?.fieldPurpose==="verification_code")return;let s=n;if(s?.status==="error"||s?.error!=null||s?.valueMismatch===!0||typeof s?.inputType=="string"&&s.inputType.toLowerCase()==="password")return;let a=e.name==="type_text_at"?e.args?.text:e.args?.value;if(typeof a!="string")return;let i=a.slice(0,4e3);if(i.trim().length===0||xi(i))return;let o=e.args?.inputRole,l=Jy(i,r)?"credential_identifier":o==="credential_identifier"||o==="user_supplied_payload"||o==="generated_test_value"||o==="other"?o:"other",c=typeof s?.typedIntoField=="string"&&s.typedIntoField.trim()||typeof e.args?.label=="string"&&e.args.label.trim()||typeof e.args?.ref=="string"&&`ref:${e.args.ref}`||"",d={sourceActionId:typeof e.toolCallId=="string"&&e.toolCallId.length>0?e.toolCallId:fe("write"),inputRole:l,...c?{fieldIdentity:c.slice(0,200)}:{}};this._capturedTypedInputs.push({value:i,source:d}),this._capturedTypedInputs.length>50&&this._capturedTypedInputs.shift(),s&&typeof s=="object"&&(s.verbatimInputSource=d),!this._capturedTypedValues.includes(i)&&(this._capturedTypedValues.push(i),this._capturedTypedValues.length>50&&this._capturedTypedValues.shift())}getCapturedTypedValues(){return this._capturedTypedValues}getCapturedTypedInputs(){return this._capturedTypedInputs}async executeAction(e,n){if((e.name==="type_text_at"||e.name==="set_focused_input_value")&&Wo({config:n.session?.config,text:e.name==="type_text_at"?e.args?.text:e.args?.value,inputType:typeof e.args?.inputType=="string"?e.args.inputType:void 0,label:e.args?.label,intent:e.args?.intent,screen:e.args?.screen,stepText:e.args?.stepText})&&(this._canonicalEmailWritten=!0,!this._emailVerificationExpected&&kk(e.args?.intent,e.args?.screen,e.args?.stepText)&&(this._emailVerificationExpected=!0),!this._canonicalEmailUsedForSignup&&Ck(e.args?.intent,e.args?.screen,e.args?.stepText)&&(this._canonicalEmailUsedForSignup=!0)),!ve("EMAIL_CODE_PROVENANCE")&&this._emailVerificationExpected&&e.args?.fieldPurpose==="verification_code"&&(e.name==="type_text_at"||e.name==="set_focused_input_value")){let o=String(e.name==="type_text_at"?e.args?.text??"":e.args?.value??""),l=U0({value:o,fetchedEmailTexts:this._fetchedEmailTexts});if(!l.allowed)return this.log("warn","BaseRuntime","email_verification_gate: ungrounded code write refused",{toolName:e.name,grounded:this._fetchedEmailTexts.length>0}),{response:{status:"error",error:l.reason},isMetaTool:!0}}let r=this.maybeBlockForObservationCheckpoint(e,n);if(r)return r;let s=n.isMobile&&Ma(e.name)?await this.executeMobileAction(e,n):await this.executeBrowserAction(e,n);this._canonicalEmailWritten&&Nk(s.loopDetectorUpdate?.screenContent)&&(this._emailVerificationExpected||(this._emailVerificationExpected=!0),this.recordRuntimeRevealedFact({kind:"capability_required",provenance:"runtime_observed",confidence:"high",source:"testingEmailPolicy.screenIndicatesVerificationPending",message:"The product screen indicates email verification is pending.",data:{capability:"email_verification",...typeof s.loopDetectorUpdate?.url=="string"?{url:s.loopDetectorUpdate.url}:{}}}));let i=q0({scope:n.session?.config?.browserNavigationScope,currentUrl:s.loopDetectorUpdate?.url,screenContent:s.loopDetectorUpdate?.screenContent});return i&&this.recordRuntimeRevealedFact(i),this.updateObservationScreenState(s.loopDetectorUpdate?.url,s.loopDetectorUpdate?.screenContent),s}isObservationCheckpointEnabled(e){return!1}updateObservationScreenState(e,n){let r=typeof e=="string"?e.trim():"",s=typeof n=="string"?n.trim():"";if(!r&&!s)return;let a=s?s.slice(0,2e3):"",i=`${r}|${this.quickHash(a)}`;i!==this.currentObservationScreenKey&&(this.currentObservationScreenKey=i)}markObservationCheckpoint(){this.currentObservationScreenKey&&this.checkpointedObservationScreenKeys.add(this.currentObservationScreenKey)}maybeBlockForObservationCheckpoint(e,n){return!this.isObservationCheckpointEnabled(n)||!this.currentObservationScreenKey||!this.isObservationStateChangingAction(e,n)||this.checkpointedObservationScreenKeys.has(this.currentObservationScreenKey)?null:{response:{status:"observation_required",reason:"checkpoint current screen before leaving or changing it; requested action was not executed",instruction:'Call log_observation now. Use decision="capture" for exact facts required as future inputs, stable locators, expected outcomes, or fixed prices/amounts; use context_only for screen inventory/current-state notes; use decision="none" only if nothing durable needs to be preserved. Then retry the same action.'},isMetaTool:!0}}isObservationStateChangingAction(e,n){if(n.isMobile&&Ma(e.name)){if(e.name!=="mobile_press_button")return q7.has(e.name);let r=String(e.args?.button??"").toUpperCase();return r==="BACK"||r==="HOME"}return H7.has(e.name)}onLoopProgressSignal(e,n,r){this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:"progress_milestone",iteration:r,details:{source:"LoopDetector",signal:e}}),e!=="repeated_side_effect"&&(this.sameGoalAbort.recordMilestone(),this.supervisorVerdictFreshness.recordProgress())}onLoopDetectorDiagnostic(e,n,r){if(e.kind==="repeated_side_effect_would_suppress"||e.kind==="repeated_side_effect_suppressed"){this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:e.kind,iteration:r,details:{source:"LoopDetector",...e}});return}this.baseDeps.sink.emit({kind:"log",ts:Date.now(),sessionId:n.id,level:"info",source:"LoopDetector",msg:"structural_force_block_diagnostics",data:e})}updateLoopDetector(e,n,r,s,a){let i=n.loopDetectorUpdate;if(i){if(i.url){let o=i.clickTargetSignature??(s&&C7.has(s)?e.actionKey(s,a??{}):void 0);e.updateUrl(i.url,o)}if(!r){let o=Ob()&&i.canvasDominant===!0&&typeof n.screenshotBase64=="string"&&n.screenshotBase64.length>0?{pixelHash:VC(n.screenshotBase64)}:void 0;e.updateScreenContent(i.screenContent,i.screenshotSize,s!==void 0&&fC.has(s),o)}if(i.drainTimedOut!==void 0){let o={url:i.url,drainTimedOut:i.drainTimedOut};e.recordDrainResult(o),this.progressBus.recordDrainResult(o)}}}async trackTapRetry(e,n,r,s,a,i,o){let l=n.args??{};if(n.name==="mobile_tap"||n.name==="mobile_long_press"){let c=e.recordTap(typeof l.screen=="string"?l.screen:"",Number(l.x??0),Number(l.y??0),typeof l.intent=="string"?l.intent:"",r.result.screenshot?.length??0);c.memoryProposal&&s.upsert&&(Hc(c.memoryProposal,i.map(d=>d.text))||(await s.upsert({id:fe("mem"),projectId:a,text:c.memoryProposal,source:"system",createdAt:Date.now(),updatedAt:Date.now()}),i.push({id:fe("mem"),projectId:a,text:c.memoryProposal,source:"system",createdAt:Date.now(),updatedAt:Date.now()}),this.log("info","TapRetryTracker","Memory saved",{text:c.memoryProposal.slice(0,120)})))}else o&&Ma(n.name)&&e.reset()}async setupScreencast(e){let n=[],r=Date.now(),s={count:0},a=null,i=this.baseDeps.createVideoRecorder?.();i?.start(this.sessionId);let o=f=>{f.action.status==="started"&&n.push({timestamp:Date.now()-r,actionName:f.action.actionName,label:f.action.intent,planStepIndex:f.action.planStepIndex})},l=f=>{this.baseDeps.screencastService?.showTapIndicator?.(f.normX,f.normY)},c=()=>{this.baseDeps.screencastService?.pausePolling?.()},d=()=>{this.baseDeps.screencastService?.resumePolling?.()};if(this.baseDeps.screencastService&&!e.config?.extensionPath)try{let f=!1,h=()=>{f||(f=!0,this.emit("screencast:started",{sessionId:this.sessionId}))};a=this.baseDeps.screencastService.onFrame(this.sessionId,p=>{s.count++,i?.addFrame(p.data,p.timestamp),h(),this.emit("screencast:frame",{sessionId:this.sessionId,data:p.data,timestamp:p.timestamp})}),await this.baseDeps.screencastService.startScreencast(this.sessionId),h(),this.log("info","VideoPipeline","recording_started",{sessionId:this.sessionId,hasRecorder:!!i}),this.on("action:progress",o),this.on("tap:indicator",l),this.on("screencast:pause-polling",c),this.on("screencast:resume-polling",d)}catch{}let u={unsubscribe:a,actionProgressHandler:o,tapIndicatorHandler:l,pausePollingHandler:c,resumePollingHandler:d,actionMarkers:n,screencastStartTime:r,projectId:e.projectId,frameCounter:s,videoRecorder:i};return this.activeScreencastState=u,this.screencastStopSignaled=!1,u}signalActiveScreencastStopped(e){!this.activeScreencastState||this.screencastStopSignaled||(this.screencastStopSignaled=!0,this.emit("screencast:stopped",{sessionId:this.sessionId,turnId:e,actionMarkers:this.activeScreencastState.actionMarkers}))}async teardownScreencast(e,n){if(!e)return;this.activeScreencastState!==e&&(this.activeScreencastState=e,this.screencastStopSignaled=!1);let{unsubscribe:r,actionProgressHandler:s,tapIndicatorHandler:a,pausePollingHandler:i,resumePollingHandler:o,actionMarkers:l,projectId:c,frameCounter:d,videoRecorder:u}=e;if(this.removeListener("action:progress",s),this.removeListener("tap:indicator",a),this.removeListener("screencast:pause-polling",i),this.removeListener("screencast:resume-polling",o),r?.(),this.baseDeps.screencastService){try{await this.baseDeps.screencastService.stopScreencast(this.sessionId),this.baseDeps.screencastService.stopDeviceRecording&&await this.baseDeps.screencastService.stopDeviceRecording(this.sessionId),this.baseDeps.screencastService.setActionMarkers?.(l)}catch{}this.signalActiveScreencastStopped(n);let f=d?.count??0;this.log(f>0?"info":"warn","VideoPipeline",f>0?"frames_captured":"frames_none",{sessionId:this.sessionId,turnId:n,framesReceived:f});let h;if(u&&u.isUnavailable?.())this.log("info","VideoPipeline","ffmpeg_unavailable",{sessionId:this.sessionId,turnId:n,framesReceived:f});else if(u&&this.baseDeps.uploadVideo)try{let p=await u.stop(),m=u.getLastError?.();if(this.log(p?"info":"warn","VideoPipeline",p?"ffmpeg_produced_file":"ffmpeg_no_file",{sessionId:this.sessionId,turnId:n,framesReceived:f,sizeBytes:p?.sizeBytes,...m?{ffmpegError:m}:{}}),p){let g=`videos/${c}/${this.sessionId}/${n}.mp4`,v=await this.baseDeps.uploadVideo(p.filePath,g),w=v&&typeof v=="object"?v:void 0;h=(typeof v=="string"?v:w?.url)??void 0,h&&u.cleanup(),this.log(h?"info":"warn","VideoPipeline",h?"upload_ok":"upload_failed",{sessionId:this.sessionId,turnId:n,r2Key:g,sizeBytes:p.sizeBytes,artifactStatus:h?"available":"unavailable",uploadStatus:w?.status??(h?"uploaded":"unknown"),attempts:w?.attempts??null,...w?.httpStatus?{httpStatus:w.httpStatus}:{},...h?{videoUrl:h}:{failureStage:"upload",localFileRetained:!0}})}}catch(p){this.log("warn","VideoPipeline","video_finalize_error",{sessionId:this.sessionId,turnId:n,error:p instanceof Error?p.message:String(p)})}return h&&this.emit("screencast:stopped",{sessionId:this.sessionId,turnId:n,actionMarkers:l,videoUrl:h}),this.activeScreencastState=null,this.screencastStopSignaled=!1,h}}async consultLoopBreaker(e,n,r,s,a,i,o,l,c){if(!n||!zb(n))return{extend:!1};if(!this.loopVisionEscalationActive())return this.log("info","BaseRuntime","loop_vision: breaker skipped (lane disabled)",{mechanism:n,iteration:a,skipReason:"kill_switch_off",spawnedByCoordinator:this.baseDeps.spawnedByCoordinator===!0}),{extend:!1};if(!this.baseDeps.supervisorService?.evaluateLoopBreaker)return this.log("warn","BaseRuntime","loop_vision: breaker skipped (no supervisor judge wired)",{mechanism:n,iteration:a,skipReason:"no_supervisor_service",spawnedByCoordinator:this.baseDeps.spawnedByCoordinator===!0}),{extend:!1};let d=x0(),u=d?I0:_h;if(this.loopVisionEscalation.decide(n,u)==="ceiling")return this.log("warn","BaseRuntime","loop_vision: extension ceiling reached, hard-blocking",{mechanism:n,grantsUsed:this.loopVisionEscalation.grantsUsed,maxGrants:u,iteration:a}),{extend:!1};let f=d&&this.loopVisionEscalation.grantsUsed>=1,h=this.loopVisionPrevProgress,p=f?{priorScreenshot:this.loopVisionPrevScreenshot,priorProgress:h}:void 0,m=c?[...this.supervisorActionLog,c]:[...this.supervisorActionLog],g=C0(n,r,m,l),v=await this.withObservationCall("supervisor_loop_breaker",()=>this.baseDeps.supervisorService.evaluateLoopBreaker(m,this.getSupervisorTaskDescription(o??""),i,g,void 0,p,this.sessionAuxMeter()));v.decision==="keep_block"&&v.reason==="timeout"&&En(this.baseDeps.observationArchive,"lifecycle",this.getObservationArchiveContext(),()=>({state:"incomplete",reasonCode:"supervisor_producer_timeout"}));let w=v.progress??null,E=v.decision==="continue",R=v.decision==="continue"?void 0:v.reason;this._lastLoopBreakerConsult={consulted:!0,keepBlockReason:R,deltaEvidence:w?.delta_evidence};let _=f&&this.loopVisionEscalation.grantsUsed>=this.loopVisionDeltaFloor(),b=Ob()&&this._lastCanvasDominant,y=f&&k0(h,w,b),T=_&&E&&!y,k=E&&!T,I=f?{grantIndex:this.loopVisionEscalation.grantsUsed+1,priorDone:h?.done??null,done:w?.done??null,delta:h&&w?w.done-h.done:null,deltaPresent:y}:void 0;return this.baseDeps.sink.emit({kind:"supervisor_verdict",ts:Date.now(),sessionId:s.id,verdict:k?"continue":"block",message:k?`loop_vision CONTINUE (${n}) \u2014 false-block rescued`:T?"loop_vision keep_block (no_delta)":`loop_vision keep_block (${n}: ${R})`,iteration:a,actionLogSize:m.length,progress:w,...I?{differential:I}:{}}),k?(this.loopVisionEscalation.recordGrant(),e.grantExtension(n,Wb),d&&(this.loopVisionPrevScreenshot=i,this.loopVisionPrevProgress=w),this.log("info","BaseRuntime","loop_vision: judge CONTINUE granted extension",{mechanism:n,grantsUsed:this.loopVisionEscalation.grantsUsed,extensionActions:Wb,iteration:a,progress:w,...I?{differential:I}:{}}),{extend:!0}):T?(this.log("info","BaseRuntime","loop_vision: keep_block no_delta (differential)",{mechanism:n,iteration:a,priorProgress:h,progress:w,differential:I}),{extend:!1}):(this.log("warn","BaseRuntime","loop_vision: judge kept block (fail-closed)",{mechanism:n,reason:R,iteration:a,progress:w,...I?{differential:I}:{}}),{extend:!1})}buildPeriodicSupervisorHints(e){return R0(e,w0()?T0(this.appliedSupervisorVerdicts):void 0)}async applySupervisorVerdict(e,n,r,s){let a=this.currentSupervisorEvidence(),i=a.requiresStepBinding===!0,o=e.action==="redirect"?this.groundSupervisorRedirect(e.message,r):null,c=(e.action==="redirect"?b0(e,a):null)||(i&&o?.verdict==="ungrounded"&&this.supervisorRedirectGroundingEnforced()?"ungrounded_locator":null);if(e.action==="redirect"&&(i?(this.log("info","Supervisor","supervisor_redirect:proposal",{iteration:r,proposedMessage:e.message}),e={...e,message:c?bh:v0(a)}):c&&(e={...e,message:bh})),e.action==="redirect"&&!m0()){let h=this.redirectFlipFlop.observeRedirect(e.message);if(h){let p=f0();this.log("warn","Supervisor","Redirect flip-flop guard",{iteration:r,enforced:p,...h}),this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:p?"redirect_flipflop_guard":"redirect_flipflop_would_fire",iteration:r,details:`${h.trigger} strike ${h.strike} \u2192 ${h.escalation} (${h.redirectCount} redirects${h.directions?`, "${h.directions[0]}" \u2194 "${h.directions[1]}"`:""})`}),p&&(e=h.escalation==="block"?{action:"block",reason:y0(h),rawText:e.rawText}:{action:"wrap_up",message:g0(h),rawText:e.rawText})}}let d=p0()?this.sameGoalAbort.observeVerdict(e.action):null;if(d){let h=e.action==="redirect"?e.message:void 0;this.log("warn","Supervisor","Same-goal abort escalation",{iteration:r,escalation:d,strike:this.sameGoalAbort.fires,consecutiveRedirects:nd,originalMessage:h}),e=d==="block"?{action:"block",reason:`${nd*2} consecutive supervisor corrections produced no progress milestone; the agent ignored the abort steer, ending the run`,rawText:e.rawText}:{action:"abort",reason:`${nd} consecutive supervisor corrections produced no progress milestone`,rawText:e.rawText}}let u=e.action==="redirect"?c:null;this.appliedSupervisorVerdicts=S0(this.appliedSupervisorVerdicts,{action:e.action,iteration:r,message:E0(e)}),this.log("info","Supervisor","Applying verdict",{action:e.action,iteration:r}),this.baseDeps.sink.emit({kind:"supervisor_verdict",ts:Date.now(),sessionId:n.id,verdict:e.action,message:e.action==="block"||e.action==="abort"?e.reason:e.message,iteration:r,actionLogSize:this.supervisorActionLog.length});let f=_b(e);if(e.action==="redirect"){this.log("info","Supervisor","REDIRECT",{message:e.message}),u&&(this.log("info","Supervisor","supervisor_redirect:evidence_downgraded",{iteration:r,reason:u}),this.supervisorStandingConstraint=null,this.supervisorSteeringEvidence=null,this.steeringVeto.reset());let h=!!u||o?.verdict==="ungrounded"&&this.supervisorRedirectGroundingEnforced(),p=h?_C(f,u?bh:MC(e.message??"",o?.verdict==="ungrounded"?o.unobserved:[])):f;h||(this.supervisorSteeringEvidence=e.recoveryEvidence??null,this.steeringVeto.recordRedirect(e.message??"Change approach."),p.standingConstraint&&this.armSupervisorStandingConstraint(p.standingConstraint,r));let m=s[s.length-1];return m&&p.injectedFunctionResponseError&&(m.response={...m.response,status:"error",metadata:{...m.response?.metadata??{},error:p.injectedFunctionResponseError}}),{done:p.done}}if(e.action==="block"){this.log("warn","Supervisor","BLOCK",{reason:e.reason});let h=f.blockedReason,p={sessionId:n.id,id:fe("msg"),role:"model",text:`We stopped this run because the agent got stuck: ${h}. No usage charged.`,timestamp:Date.now(),actionName:"exploration_blocked",actionArgs:{attempted:`Supervisor intervention after ${this.supervisorActionLog.length} actions`,obstacle:h,question:"The supervisor stopped this session. Please review and retry."}};return await this.persistMessage(p),this.emit("message:added",{sessionId:n.id,message:p}),s.push({name:"exploration_blocked",response:{status:"awaiting_user_guidance"}}),{done:f.done,blockedReason:h}}if(e.action==="wrap_up"){this.log("info","Supervisor","WRAP_UP",{message:e.message});let h=s[s.length-1];h&&f.injectedFunctionResponseError&&(h.response={...h.response,status:"error",metadata:{...h.response?.metadata??{},error:f.injectedFunctionResponseError}})}if(e.action==="abort"){this.steeringVeto.recordRedirect(f.injectedFunctionResponseError??e.reason),f.standingConstraint&&this.armSupervisorStandingConstraint(f.standingConstraint,r);let h=s[s.length-1];h&&f.injectedFunctionResponseError&&(h.response={...h.response,status:"error",metadata:{...h.response?.metadata??{},error:f.injectedFunctionResponseError}})}return e.action==="continue"&&this.clearSupervisorStandingConstraint(),{done:f.done}}onIterationStart(e,n,r){}onIterationEnd(e){}onToolResponsesRecorded(e){}prepareModelTrace(e){return e}withCanvasStrategy(e,n,r){if(!Lp()||(n!==this._canvasStrategyActive&&(this.log("info","BaseRuntime",n?"canvas_strategy: injected (page is canvas-dominant)":"canvas_strategy: retracted (page no longer canvas-dominant)",{iteration:r,canvasDominant:n}),this._canvasStrategyActive=n),!n))return e;let s=e[e.length-1],a=s?.parts?.some(i=>i!=null&&typeof i=="object"&&"functionResponse"in i);if(s?.role==="user"&&!a){let i={...s,parts:[...s.parts??[],{text:qy}]};return[...e.slice(0,-1),i]}return[...e,{role:"user",parts:[{text:qy}]}]}getSupervisorTaskDescription(e){return e}hasBackgroundWork(){return!1}waitForBackgroundWork(){return Promise.resolve()}async onLoopExhausted(e,n){let r={sessionId:e.id,id:fe("msg"),role:"model",text:`I paused before finishing this run (step limit of ${n} reached). Reply "continue" to let me proceed, or clarify the exact target page/expected behavior.`,timestamp:Date.now()};await this.persistMessage(r),this.emit("message:added",{sessionId:e.id,message:r})}async consultRunBackstopJudge(e,n,r,s,a){let i=this.baseDeps.supervisorService;if(!i?.evaluateLoopBreaker)return this.log("warn","BaseRuntime","backstop:judge_unavailable (no supervisor judge wired)",{iteration:n,ceiling:a.ceiling,spawnedByCoordinator:this.baseDeps.spawnedByCoordinator===!0}),"unavailable";try{let l=`RUN BACKSTOP PROGRESS CHECK \u2014 this run has hit a safety ceiling (${dh(a.elapsedMs,a.iteration)}, ${a.ceiling}). Judge ONE thing: given the objective and the recent actions + current screenshot, is the agent GENUINELY PROGRESSING toward the objective \u2014 measurably closer than several steps ago \u2014 or WANDERING / not getting closer (recycling the same screens/URLs, repeating failed attempts, or off on an unrelated tangent)? Answer CONTINUE ONLY if you can point to concrete forward progress toward the objective; if it is not clearly getting closer, answer BLOCK. When unsure, answer BLOCK.`,c=await i.evaluateLoopBreaker([...this.supervisorActionLog],this.getSupervisorTaskDescription(s??""),r,l,void 0,void 0,this.sessionAuxMeter());return c.decision==="continue"?"progressing":c.reason==="block"||c.reason==="redirect"||c.reason==="wrap_up"?"wandering":"unavailable"}catch(o){return this.log("warn","BaseRuntime","backstop:judge_error \u2014 failing closed",{iteration:n,ceiling:a.ceiling,error:o?.message}),"unavailable"}}async handleRunBackstop(e,n,r,s,a="ceiling_cap"){let i=ch(e),o=s&&s.trim().length>0?`"${s.trim().slice(0,160)}"`:"this task",l="a series of navigation and interaction steps";try{let f=new Set,h=[];for(let p of[...this.supervisorActionLog].reverse())if(!(!p?.action||p.skipped||f.has(p.action))&&(f.add(p.action),h.push(p.intent?`${p.action} (${p.intent.slice(0,40)})`:p.action),h.length>=4))break;h.length>0&&(l=h.reverse().join(", "))}catch{}this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:"run_backstop",iteration:r,details:`terminate on ${e.ceiling} (${i}); reason=${a}; cumulativeTokens=${e.cumulativeTokens}`}),this.baseDeps.sink.emit({kind:"supervisor_verdict",ts:Date.now(),sessionId:n.id,verdict:"block",message:`run_backstop terminate (${e.ceiling}: ${i}; reason=${a})`,iteration:r,actionLogSize:this.supervisorActionLog.length});let d=`${a==="wander"?BC(o,e.elapsedMs,e.iteration):GC(e.elapsedMs,e.iteration)} What I tried: ${l}. Send a new message with a more specific target or expected result and I'll try again.`,u={sessionId:n.id,id:fe("msg"),role:"model",text:d,timestamp:Date.now(),actionName:"exploration_blocked",actionArgs:{attempted:`${this.supervisorActionLog.length} actions; ${i}`,obstacle:a==="wander"?`Run backstop: not getting closer to ${o} (${e.ceiling}).`:`Run backstop: hit the safety limit (${e.ceiling}; ${a}).`,question:"The run stopped at a safety limit. Adjust the target or steps and reply to try again."}};return await this.persistMessage(u),this.emit("message:added",{sessionId:n.id,message:u}),this.log("warn","BaseRuntime","run_backstop fired (terminate)",{ceiling:e.ceiling,reason:a,iteration:r,elapsedMs:e.elapsedMs,cumulativeTokens:e.cumulativeTokens}),!0}onLoopForceBlock(e,n,r){}loopVisionDeltaFloor(){return _h}async recordLoopBlockIssue(e,n,r){}async runLoop(e){let{session:n,maxIterations:r,snapshotOnly:s,isMobile:a,devicePlatform:i,taskDescription:o,supervisorHints:l,preserveAllPageSnapshots:c,runJsLoopRecoveryPolicy:d="warn_response"}=e,u=this.conversationTrace;this._endMechanism=void 0,this._lastLoopBreakerConsult=void 0,this._currentTaskDescription=o;let f=!1,h=!1,p,m,g=0,v=Math.floor(Date.now()/1e3),w=0,E=0,R=UC()&&n.kind==="assistant_v2",_=Date.now(),b=0,y=0,T=2,k=new Yc(B=>this.onLoopProgressSignal(B,n,w),{trackNovelScreenContent:!0,...n.config?.loopDetector??{},rearmUrlNovelty:pC()&&n.kind==="assistant_v2",repeatedSideEffectGuard:hC()},B=>this.onLoopDetectorDiagnostic(B,n,w));this.isExplorationMode()&&k.setMode("exploration"),this._activeLoopDetector=k,this._lastNavPageSettled=null,this._lastCanvasDominant=!1,this._canvasStrategyActive=!1;let I=new Jc,A;this.resetSteeringStateForNewRun(),this.resetLoopVisionState(),this.progressBus.resetSupervisorLog(),this.pendingSupervisorVerdict=null,this.resolvedSupervisorVerdict=null,this.toolDispatchInFlight=!1,this.supervisorVerdictFreshness.reset(),this.appliedSupervisorVerdicts=[],this.recentActionsForEvidence=[],this.recentClickTargets=[];let C=[];for(let B=1;B<=r;B++){if(w=B,!this._isRunning)throw new Error("cancelled");if(R){let ne=FC({elapsedMs:_>0?Date.now()-_:0,iteration:B,cumulativeTokens:b},jC(Nb,y));if(ne.kind==="terminate"){this.log("info","BaseRuntime","backstop:ceiling_reached",{ceiling:ne.ceiling,iteration:B,elapsedMs:ne.elapsedMs,cumulativeTokens:ne.cumulativeTokens,extensionsGranted:y});let ue="unavailable";y<lh&&(ue=await this.consultRunBackstopJudge(n,B,A,o,ne),this.log("info","BaseRuntime","backstop:judge_verdict",{verdict:ue,ceiling:ne.ceiling,iteration:B,extensionsGranted:y}));let Ee=$C(ue,y);if(Ee.action==="extend")y+=1,this.log("info","BaseRuntime","backstop:extended",{extensionsGranted:y,maxExtensions:lh,ceiling:ne.ceiling,iteration:B});else{let Oe=!1;try{Oe=await this.handleRunBackstop(ne,n,B,o,Ee.reason)}catch(pe){this.log("warn","BaseRuntime","run_backstop emit failed \u2014 failing open",{error:pe?.message,iteration:B}),Oe=!1}if(Oe){this.log("info","BaseRuntime","backstop:terminated",{reason:Ee.reason,verdict:ue,ceiling:ne.ceiling,iteration:B,extensionsGranted:y}),f=!0,h=!0,m="backstop",p=Ee.reason==="wander"?`Stopped: not getting closer to the objective (${ch(ne)}).`:`Stopped: hit the safety limit (${ch(ne)}).`,this.recordEndMechanism("run_backstop");break}}}}if(typeof process<"u"&&typeof process.memoryUsage=="function")try{let ne=process.memoryUsage();this.log("info","Runtime","iteration_memory",{iteration:B,rssMb:Math.round(ne.rss/1024/1024),heapUsedMb:Math.round(ne.heapUsed/1024/1024),heapTotalMb:Math.round(ne.heapTotal/1024/1024),externalMb:Math.round(ne.external/1024/1024),arrayBuffersMb:Math.round(ne.arrayBuffers/1024/1024)})}catch{}await this.onIterationStart(u,n,B),c||this.stripOldPageSnapshots(u,s);let L=this.getToolSet({isMobile:a,snapshotOnly:s,devicePlatform:i}),D=this.prepareModelTrace(this.withCanvasStrategy(this.systemPromptText?u.slice(1):u,this._lastCanvasDominant,B)),V=Fa(D),$=D.reduce((ne,ue)=>ne+(ue.parts??[]).reduce((Ee,Oe)=>{let pe=Oe.text?.length??0,Se=Oe.functionCall?JSON.stringify(Oe.functionCall).length:0,Pe=Oe.functionResponse?JSON.stringify(Oe.functionResponse.response??{}).length:0;return Ee+pe+Se+Pe},0),0);this.log("info","BaseRuntime","llm_call_preflight",{iteration:B,traceMsgCount:D.length,estimatedTraceChars:$});let Q=Date.now(),G;try{G=await this.withObservationCall("agent_step",()=>yt({model:this.model,system:this.systemPromptText??void 0,messages:V,tools:L,toolChoice:this.toolChoiceForTurn(L),temperature:.2,topP:.95,topK:40,maxOutputTokens:8192,maxRetries:this._isRunning?7:0,abortSignal:this._abortController?.signal}))}catch(ne){let ue=String(ne?.message||ne?.data?.error?.message||"");if(ue.includes("Unable to process input image")||ue.includes("INVALID_ARGUMENT")){this.log("warn","BaseRuntime","Gemini rejected image \u2014 retrying without screenshots",{error:ue.slice(0,200)});let Ee=D.map(pe=>({...pe,parts:(pe.parts||[]).filter(Se=>!Se.inlineData)})),Oe=Fa(Ee);G=await this.withObservationCall("agent_step_image_free_retry",()=>yt({model:this.model,system:this.systemPromptText??void 0,messages:Oe,tools:L,toolChoice:this.toolChoiceForTurn(L),temperature:.2,topP:.95,topK:40,maxOutputTokens:8192,maxRetries:this._isRunning?7:0,abortSignal:this._abortController?.signal}))}else throw ne}let U=Date.now()-Q;B===1&&this.recordStartupMilestone("first_llm_completed",{iteration:B,toolCallCount:G.toolCalls.length,textLength:G.text?.length??0});let ee=G.usage,H=(ee?.inputTokens??0)+(ee?.outputTokens??0);b+=(ee?.inputTokens??0)+(ee?.outputTokens??0);let O=ee?.inputTokenDetails?.cacheReadTokens??ee?.cachedInputTokens??G.providerMetadata?.google?.usageMetadata?.cachedContentTokenCount??0;if(H>0&&(this.tokenCount=H,this.emit("context:updated",{sessionId:n.id,tokenCount:H}),await this.baseDeps.chatRepo.updateSessionFields(n.id,{lastTokenCount:H}),this.baseDeps.sink.emit({kind:"llm_usage",ts:Date.now(),sessionId:n.id,runId:this.getCurrentRunId(),callKind:"agent_step",...this.planStepAttribution(),...this.llmSourceMeta(),...F0(G.toolCalls.map(ne=>ne.toolName),Eh()),model:n.config.model||"unknown",promptTokens:ee?.inputTokens??0,completionTokens:ee?.outputTokens??0,cachedInputTokens:O,totalTokens:H,durationMs:U,finishReason:G.finishReason??void 0,tokenCount:H,messageCount:u.length,systemPromptHash:this.systemPromptText?this.quickHash(this.systemPromptText):void 0,lastToolResults:C.map(ne=>({toolName:ne.name,status:ne.response?.status??"unknown"})),chosenActions:G.toolCalls.map(ne=>({toolName:ne.toolName,intent:typeof ne.input?.intent=="string"?ne.input.intent:void 0})),textResponse:typeof G.text=="string"?G.text.slice(0,200):void 0})),!this._isRunning)throw new Error("cancelled");let j=G.response.messages,z=Lb(j);for(let ne of z)u.push(ne);let K=G.toolCalls.map(ne=>({name:ne.toolName,args:ne.input??{},toolCallId:ne.toolCallId})),te=G.text;if(K.length===0){let ne=te?.replace(/[\x00-\x1f\x7f-\x9f]|<ctrl\d+>/g,"").trim();if(te&&!ne&&this.log("warn","BaseRuntime","Model returned garbage text, treating as empty response",{charCount:te.length}),ne){let Ee={sessionId:n.id,id:fe("msg"),role:"model",text:te.slice(0,6e3),timestamp:Date.now()};if(await this.persistMessage(Ee),this.emit("message:added",{sessionId:n.id,message:Ee}),f=!0,this.hasBackgroundWork()){this.log("info","BaseRuntime","Text-only response but background work pending \u2014 waiting silently (not persisting LLM text)",{text:te?.slice(0,100)}),await this.waitForBackgroundWork();continue}this.log("info","BaseRuntime","Text-only response, no background work \u2014 exiting loop",{text:te?.slice(0,100)});break}if(E++,this.hasBackgroundWork()){this.log("info","BaseRuntime","Empty response but background work pending \u2014 waiting silently"),await this.waitForBackgroundWork(),E=0;continue}if(this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:"empty_response",iteration:B,details:`attempt ${E}/${T}`}),g>0&&E<=T){this.log("info","BaseRuntime","Model returned empty response, nudging to continue",{stepIndex:g,attempt:E,maxAttempts:T});let Ee;if(a)Ee=(await this.baseDeps.mobileMcpService.takeScreenshot(this.sessionId)).base64;else{if(!this.baseDeps.computerUseService)throw new Error("[BaseRuntime] Browser nudge not available \u2014 no computerUseService");Ee=(await this.baseDeps.computerUseService.invoke({sessionId:n.id,action:"screenshot",args:{},config:{...n.config,projectId:n.projectId}})).screenshot}let pe=[{text:"You stopped without responding. Here is the current page state. Please continue with your task, or if you are done, call assistant_v2_report with a summary."}];s||pe.push({inlineData:{mimeType:"image/png",data:Ee}}),u.push({role:"user",parts:pe});continue}this.log("warn","BaseRuntime","Model returned consecutive empty responses, giving up",{emptyResponseCount:E});let ue={sessionId:n.id,id:fe("msg"),role:"model",text:g>0?`Model returned empty responses after ${g} action(s). This may be caused by rate limiting or a temporary API issue. You can retry by sending another message.`:"Model returned an empty response and could not start. This may be caused by rate limiting or a temporary API issue. You can retry by sending another message.",timestamp:Date.now()};await this.persistMessage(ue),this.emit("message:added",{sessionId:n.id,message:ue}),f=!0;break}if(E=0,te){let ne={sessionId:n.id,id:fe("msg"),role:"system",actionName:"assistant_v2_text",actionArgs:{iteration:B},text:te.slice(0,6e3),timestamp:Date.now()};await this.persistMessage(ne),this.emit("message:added",{sessionId:n.id,message:ne})}let X=[],q=[],P=null,F=!1,Y=new Set;if(a)for(let ne=0;ne<K.length-1;ne++)Ma(K[ne].name)&&K[ne].name!=="mobile_screenshot"&&Ma(K[ne+1].name)&&K[ne+1].name!=="mobile_screenshot"&&Y.add(ne);let ie=-1;for(let ne of K){if(ie++,!this._isRunning)break;g++;let ue={iteration:B,sessionId:this.sessionId,session:n,isMobile:a,snapshotOnly:s,devicePlatform:i,callIndex:ie,totalCalls:K.length,skipScreenshotSet:Y,lastScreenshotBase64:A,stepIndex:g,turnTimestamp:v};this.baseDeps.observationArchive&&(q[ie]={...this.getObservationArchiveContext(),projectId:n.projectId,toolCallId:ne.toolCallId,iteration:B,actionIndex:g});let Ee=this.preDispatchRefusal(ne,k,B);if(Ee!==null){this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:Ee.kind==="steering_veto"?"steering_veto":"step_regression_refused",iteration:B,details:Ee.details}),X.push({name:ne.name,response:Ee.response});continue}this.toolDispatchInFlight=!0;let Oe=Date.now(),pe=await this.handleToolCall(ne,ue);q[ie]&&pe.observationCaptureId&&(q[ie].captureId=pe.observationCaptureId);let Se=Date.now()-Oe;!pe.isMetaTool&&pe.response?.clickReceipt?.dispatch!=="not_dispatched"&&this.steeringVeto.recordExecuted(ne.name,ne.args??{}),this.noteFullSnapshotForRedirectGrounding(pe),this.applyLoopDetectorReset(pe,k,I);let Pe=null;if(pe.isMetaTool||(Pe=this.observeStepPage(ne,pe,k,B)),this.updateLoopDetector(k,pe,Y.has(ie),ne.name,ne.args??{}),Pe){this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:"loop_block",iteration:B,details:Pe.reason}),this.recordEndMechanism("loop_block");let xe={sessionId:n.id,id:fe("msg"),role:"model",text:"We stopped this run because it kept returning to a page it had already finished. No usage charged.",timestamp:Date.now(),actionName:"exploration_blocked",actionArgs:{attempted:`Returning to an already-completed step's page via "${ne.name}"`,obstacle:Pe.reason,question:"The run stopped because it kept going back to a page it had already finished. Adjust the steps and reply to continue."}};await this.persistMessage(xe),this.emit("message:added",{sessionId:n.id,message:xe}),X.push({name:"exploration_blocked",response:{status:"awaiting_user_guidance"}}),F=!0,f=!0,h=!0,p=Pe.reason,m="loop";break}if(!pe.isMetaTool&&!O7(pe)){let xe=k.check(ne.name,ne.args??{},B,!0);if(xe.action==="force_block")if((await this.consultLoopBreaker(k,xe.mechanism,xe.message,n,B,pe.screenshotBase64??A,o,l,pe.supervisorEntry)).extend)this.log("info","BaseRuntime","loop_vision: proceeding past force-block (extension granted)",{mechanism:xe.mechanism,iteration:B});else{this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:"loop_block",iteration:B,details:xe.message}),this.log("warn","BaseRuntime","Force-blocking loop",{message:xe.message,iteration:B}),this.onLoopForceBlock(n,xe.message,B),await this.recordLoopBlockIssue(n,xe.message,g),this.recordEndMechanism("loop_block");let Z=P0({mechanism:xe.mechanism,actionCount:this.supervisorActionLog.length,judge:this._lastLoopBreakerConsult}),N={sessionId:n.id,id:fe("msg"),role:"model",text:Z,timestamp:Date.now(),actionName:"exploration_blocked",actionArgs:{attempted:`Repeated "${ne.name}" on the same target (${O0[xe.mechanism]})`,obstacle:xe.message,question:"The run stopped because it was no longer making visible progress. Adjust the target or steps and reply to continue."}};await this.persistMessage(N),this.emit("message:added",{sessionId:n.id,message:N}),X.push({name:"exploration_blocked",response:{status:"awaiting_user_guidance"}}),F=!0,f=!0,h=!0,p=xe.message,m="loop";break}if(xe.action==="warn"){if(this.baseDeps.sink.emit({kind:"agent_lifecycle",ts:Date.now(),sessionId:n.id,event:"loop_warning",iteration:B,details:xe.message}),this.log("warn","BaseRuntime","Loop warning",{message:xe.message,iteration:B}),ne.name==="run_js"&&d==="partial_sitemap_salvage"){let N="",W="",de="";if(this.baseDeps.computerUseService){let Qe=await this.baseDeps.computerUseService.invoke({sessionId:n.id,action:"screenshot",args:{},config:n.config});N=Qe.screenshot,W=Qe.url??"",de=[Qe.aiSnapshot,Qe.domChanges].filter(Boolean).join(`
562
562
 
563
563
  `)}let oe=de||pe.response?.pageSnapshot||pe.message?.a11ySnapshotText||"",Ie=L7(oe),Ue=W||pe.response?.url||"current page",Ne=Ie.length>0?`Visible controls left as navigation gaps: ${Ie.join(", ")}.`:"Visible controls left as navigation gaps: no labeled actionable controls were available in the captured snapshot.",Ze=`Partial sitemap recovered before repeated JavaScript extraction was suppressed.
564
564
  Captured page: ${Ue}.
@@ -883,7 +883,7 @@ SCOPE GUIDANCE:
883
883
 
884
884
  FORMATTING:
885
885
  - Do NOT use emojis in any text responses or summaries
886
- `+I+B+C}var jr=class t extends ia{deps;pendingUserMessages=[];_activeRun=void 0;_activeTestPlan=void 0;_activeAuthoredTestPlan=void 0;_activeCredentialRoleOverrides=[];activeRunIdForMessages(){return this._activeRun?.id}_runTokenTimestamp=Math.floor(Date.now()/1e3);_runStartEnvKey="";_currentStepIndex=null;_currentStepText=null;_maxStepEverActivated=0;_maxInRangeStepEverActivated=0;_stepEndUrls=new Map;_stepVisitedUrls=new Map;_stepRegressionFlaggedUrls=new Set;_lastObservedPageUrl=null;_stepRegressionLicenceSpentAtStep=null;_stepRegressionWorkingUrl=null;_stepRegressionBypasses=0;_stepRegressionGuardReportsRejected=0;_stepRegressionRefusals=0;_lastStepRegressionRefusalIteration=null;_lastStepRegressionRefusalSentence=null;_lastStepRegressionRefusalStep=null;_maxStepEverExecuted=0;_reverifyTaintedSteps=new Set;_projectCredentialNames=[];_projectCredentials=[];_loginIdentityRecords=[];_profileIdentityPartitions=null;_profileSeededIdentities=[];_triedCredentialNames=new Set;_lastFilledCredentialName=null;_credentialAuthDeflectionsIssued=0;_verifyStepBlockDeflectionsIssued=0;_offStepBlockDeflectionsIssued=0;_offStepBlockDeflectedCurrentSteps=new Set;_offStepBlockDeflectedDeclaredSteps=new Set;_reportObservationRun;_stepReplay=Ui();_sameValueWriteBreaker=new Xp;_runJsHuntBreaker=new uf;_verbatimPinStepIndex=null;_verbatimPinTargetKey=null;_verbatimPinElementFingerprint=null;_verbatimPinPendingOverrideNote=!1;_screenshots=[];_lastBrowserActionAt=null;_stepActionErrors=new Map;_stepObservedAnchors=new Map;_stepEventsObserved=new Set;_gateAuthoredNoteSpans=new Map;_stepUnnamedControls=new Map;_stepLabelDrift=p_();_stepLabelDriftSettled=new Set;_reportedIssuesThisRun=[];_decorativeSplitRejections=0;_transientEnvRetryState=E_();_suppressNotifications=!1;_editOnly=!1;_concludingTurnActive=!1;_concludingTerminalReason=null;_concludingBlockedContext=null;_concludingBlockedSummaryBase=null;_concludingTurnScope=null;_lastBlockKind=null;_browserUnobservableIssueFiled=!1;_concludingTurnUsed=!1;_temporaryCredentialRoleRequest=!1;_editOnlyEmptyProposals=0;static _maxEditOnlyEmptyProposals=2;_runMemory=new Map;_onRunMemoryUpdate=null;_forcedErrorSummary=null;_lastSeededSessionId=void 0;_startupNavigateLastMilestone=null;_startupNavigateLastElapsedMs=null;_startupStageTimings=[];_startupStageInFlight=null;get startupStageInFlight(){return this._startupStageInFlight}async startupStage(e,n,r){let s=Date.now();this._startupStageInFlight={stage:r,startedAt:s};let a=await ore(e,n,r),i=Date.now()-s;return this._startupStageTimings.push({stage:r,ms:i}),this._startupStageInFlight=null,i>=are&&this.log("warn","RunnerRuntime","startup_stage_slow",{stage:r,ms:i,sinceStartupMs:Date.now()-n,budgetMs:Pf}),a}_emittedVerdictAnalyticsRunIds=new Set;_verifyEvidenceStepIndexes=new Set;_autoVerifyEvidenceStepIndexes=new Set;_consecutiveBrowserShortCircuits=0;_consecutiveBrowserShortCircuitStep=null;_browserDispatchSeq=0;_retainedVerifyFrame=null;_retainedFrameAbstainedSteps=new Map;_currentStepStartedAtMs=null;_currentStepStartedIndex=null;_pinGroundingShadowStepIndexes=new Set;_groundedStateExtractCandidates=new Map;_groundedStateDifferentialCandidates=new Map;_groundedStateCountCandidates=new Map;_domGroundableCandidates=new Map;_groundedStateUrlCandidates=new Map;_predicateBasisCompileCache=new Map;_reobserveState=new Map;_reobservedSteps=new Set;_canonicalCaptureByStep=new Map;_canonicalCapturedBatches=new Set;_fullSnapshotByStep=new Map;_fullSnapshotCanvasByStep=new Map;_fullSnapshotOriginByStep=new Map;_verifyMarkerSignaledThisGeneration=!1;_verifyMarkerIsolationBlockCounts=new Map;_verifyEvidenceGateBlockCounts=new Map;_verifyOracleFailureStepIndexes=new Set;_verifyOracleFailureDetails=new Map;_stableVerifyPageSnapshot=null;_stableVerifySnapshotStep=null;_prepassSkippedStepIndexes=[];_incompleteRunCompleteRejections=0;_unauthoredGradeRunCompleteRejections=0;_reportPredicateBasisFailedSteps=new Set;_verificationConflictRejections=0;_unverifiedVerifyRunCompleteRejections=new Set;_unverifiedVerifySignalStepDeflections=new Set;_zeroGradeVerifyRejections=0;_verificationConflictReconciledSteps=new Set;_conflictReconcileSnapshotTaken=!1;_verifyObservationSeq=0;_lastVerifyObservationSeqByStep=new Map;_conflictBounceObservationSeq=null;_stepDeviations=new Map;_stepDeviationRejectedOnce=new Set;_stepClickedControlNames=new Map;_stepExpectedControls=new Map;_stepAdherenceContracts=new Map;_submittedVerdicts=new Map;_adherenceInterventions=new Map;_stepHadUnnamedClick=new Set;_migrationEligible=!1;_profileCaptureAllowed=!1;_observedLoginStepIndexes=new Set;_authLandingUrl=null;_lastObservedActionUrl=null;_captureAuthLandingNext=!1;_uploadStepResolvedPaths=new Map;_uploadAttachAttemptedSteps=new Set;_deterministicUploadDoneSteps=new Set;_deterministicUploadFailures=new Map;_deterministicUploadCooldownSteps=new Set;_pendingVerifyWaitAdvance=null;constructor(e,n){super(e,n),this.deps=n}onEndRun(){let e=this._lastSeededSessionId??this.sessionId;this.deps.computerUseService?.clearCredentials?.(e),this._lastSeededSessionId=void 0}repairDanglingToolCalls(e){for(let n=0;n<e.length;n++){let r=e[n];if(r.role!=="model")continue;let s=(r.parts??[]).filter(l=>l?.functionCall?.id).map(l=>({id:l.functionCall.id,name:l.functionCall.name}));if(s.length===0)continue;let a=new Set;for(let l=n+1;l<e.length&&e[l].role==="user";l++)for(let c of e[l].parts??[])c?.functionResponse?.id&&a.add(c.functionResponse.id);let i=s.filter(l=>!a.has(l.id));if(i.length===0)continue;this.log("info","RunnerRuntime","Repairing dangling tool calls",{count:i.length,tools:i.map(l=>l.name)});let o=n+1;for(;o<e.length&&e[o].role!=="user";)o++;(o>=e.length||!e[o].parts)&&(e.splice(n+1,0,{role:"user",parts:[]}),o=n+1);for(let l of i)e[o].parts.push({functionResponse:{name:l.name,id:l.id,response:{status:"skipped",reason:"execution stopped"}}})}}injectUserMessage(e){this.pendingUserMessages.push(e)}async resetForNextPlan(e={}){let n=e.keepMemory??!0;if(this._isRunning)throw new Error("resetForNextPlan: cannot reset while a run is in progress \u2014 stop() first");this._lastSeededSessionId&&(this.deps.computerUseService?.clearCredentials?.(this._lastSeededSessionId),this._lastSeededSessionId=void 0),this.unattendedRun=!1,this.log("info","RunnerRuntime","reset_for_next_plan",{keepMemory:n,hadActiveRun:this._activeRun!==void 0,traceLength:this.conversationTrace.length,pendingUserMessages:this.pendingUserMessages.length});try{await this.deps.computerUseService.cleanupSession(this.sessionId)}catch(r){this.log("warn","RunnerRuntime","reset_for_next_plan:cleanup_session_failed",{error:r instanceof Error?r.message:String(r)})}this.clearConversationTrace(),this.pendingUserMessages=[],this._screenshots=[],this._lastBrowserActionAt=null,this._reportedIssuesThisRun=[],this._transientEnvRetryState=E_(),this._currentStepIndex=null,this._maxStepEverActivated=0,this._maxInRangeStepEverActivated=0,this._maxStepEverExecuted=0,this._reverifyTaintedSteps.clear(),this._currentStepText=null,this._projectCredentialNames=[],this._projectCredentials=[],this._loginIdentityRecords=[],this._profileIdentityPartitions=null,this._profileSeededIdentities=[],this._triedCredentialNames=new Set,this._lastFilledCredentialName=null,this._credentialAuthDeflectionsIssued=0,this.resetCredentialTypingObservations(),this._verifyStepBlockDeflectionsIssued=0,this._offStepBlockDeflectionsIssued=0,this._offStepBlockDeflectedCurrentSteps.clear(),this._offStepBlockDeflectedDeclaredSteps.clear(),this._reportObservationRun=void 0,this.setEditOnly(!1),this._concludingTurnActive=!1,this._concludingTerminalReason=null,this._concludingBlockedContext=null,this._concludingBlockedSummaryBase=null,this._concludingTurnUsed=!1,this._concludingTurnScope=null,this._lastBlockKind=null,this._browserUnobservableIssueFiled=!1,this._suppressNotifications=!1,this._verifyEvidenceStepIndexes.clear(),this._stepActionErrors.clear(),this._stepObservedAnchors.clear(),this._stepEventsObserved.clear(),this._gateAuthoredNoteSpans.clear(),this._stepUnnamedControls.clear(),this._stepLabelDrift=p_(),this._stepLabelDriftSettled.clear(),this._stepReplay=Ui(),this._autoVerifyEvidenceStepIndexes.clear(),this._runJsHuntBreaker.reset(),this._offStepBlockDeflectionsIssued=0,this._offStepBlockDeflectedCurrentSteps.clear(),this._offStepBlockDeflectedDeclaredSteps.clear(),this._consecutiveBrowserShortCircuits=0,this._consecutiveBrowserShortCircuitStep=null,this._currentStepStartedAtMs=null,this._currentStepStartedIndex=null,this._verifyMarkerIsolationBlockCounts.clear(),this._verifyEvidenceGateBlockCounts.clear(),this._pinGroundingShadowStepIndexes.clear(),this._groundedStateExtractCandidates.clear(),this._groundedStateDifferentialCandidates.clear(),this._groundedStateCountCandidates.clear(),this._domGroundableCandidates.clear(),this._stepEndUrls.clear(),this._stepVisitedUrls.clear(),this._stepRegressionFlaggedUrls.clear(),this._lastObservedPageUrl=null,this._stepRegressionLicenceSpentAtStep=null,this._stepRegressionWorkingUrl=null,this._stepRegressionBypasses=0,this._stepRegressionGuardReportsRejected=0,this._stepRegressionRefusals=0,this._lastStepRegressionRefusalIteration=null,this._lastStepRegressionRefusalSentence=null,this._lastStepRegressionRefusalStep=null,this._groundedStateUrlCandidates.clear(),this._predicateBasisCompileCache.clear(),this._reobserveState.clear(),this._reobservedSteps.clear(),this._conflictReconcileSnapshotTaken=!1,this._verifyObservationSeq=0,this._lastVerifyObservationSeqByStep.clear(),this._conflictBounceObservationSeq=null,this._canonicalCaptureByStep.clear(),this._canonicalCapturedBatches.clear(),this._fullSnapshotByStep.clear(),this._fullSnapshotCanvasByStep.clear(),this._fullSnapshotOriginByStep.clear(),this._verifyOracleFailureStepIndexes.clear(),this._verifyOracleFailureDetails.clear(),this._stableVerifyPageSnapshot=null,this._stableVerifySnapshotStep=null,this._retainedVerifyFrame=null,this._retainedFrameAbstainedSteps.clear(),this._migrationEligible=!1,this._observedLoginStepIndexes=new Set,this._authLandingUrl=null,this._lastObservedActionUrl=null,this._captureAuthLandingNext=!1,this._uploadStepResolvedPaths=new Map,this._uploadAttachAttemptedSteps=new Set,this._deterministicUploadDoneSteps=new Set,this._deterministicUploadFailures=new Map,this._deterministicUploadCooldownSteps=new Set,this._pendingVerifyWaitAdvance=null,this._activeRun=void 0,this._activeTestPlan=void 0,this._activeAuthoredTestPlan=void 0,this._activeCredentialRoleOverrides=[],this._currentRunId=void 0,this._runMemory=new Map,this._onRunMemoryUpdate=null,this._checkEmailCallCount=0,this._canonicalEmailWritten=!1,this._emailVerificationExpected=!1,this._lastCheckEmailMessageId=null,this._sentEmailReplies=[]}emit(e,n){return super.emit(e,n)}async persistConversationTrace(e,n){this.stripOldScreenshots(n),await this.baseDeps.chatRepo.upsertSession({...e,updatedAt:Date.now(),conversationTrace:n})}extractErrorMessage(e){try{let r=e.match(/\{[\s\S]*\}/);if(r){let s=JSON.parse(r[0]);if(s.error?.message)return s.error.message;if(s.message)return s.message}}catch{}let n=e.match(/page\.goto:\s*(.+)/);return n?n[1]:e}supervisorGroundingPlanText(){return this._currentStepText??(this._currentStepIndex?this._activeTestPlan?.steps[this._currentStepIndex-1]?.text:void 0)??void 0}supervisorGroundingEnforcementLane(){return!0}isVerifyStep(e){return!e||!this._activeTestPlan?!1:this._activeTestPlan.steps[e-1]?.type==="verify"}findUnverifiedVerifyStepBefore(e){if(!this._activeTestPlan||!this._currentStepIndex||e<=this._currentStepIndex)return null;let n=this.isVerifyStep(this._currentStepIndex)?this._currentStepIndex:this._currentStepIndex+1;for(let r=n;r<e;r++)if(this.isVerifyStep(r)&&!this._verifyEvidenceStepIndexes.has(r))return r;return null}findFirstUnverifiedVerifyStep(){if(!this._activeTestPlan)return null;for(let e=1;e<=this._activeTestPlan.steps.length;e++)if(this.isVerifyStep(e)&&!this._verifyEvidenceStepIndexes.has(e))return e;return null}unresolvedOracleFailureStepsAnyProvenance(){return!aa()||!this._activeTestPlan?[]:[...this._verifyOracleFailureStepIndexes].filter(e=>this.isVerifyStep(e)).sort((e,n)=>e-n)}verifyStepsWithUnresolvedOracleFailureForLiveVerdict(){return this.unresolvedOracleFailureStepsAnyProvenance().filter(e=>!this.isArmOnlyOracleFailure(e))}isArmOnlyOracleFailure(e){return this._verifyOracleFailureDetails.get(e)?.via==="rollback"}hasLiveVerdictOracleFailure(e){return this._verifyOracleFailureStepIndexes.has(e)&&!this.isArmOnlyOracleFailure(e)}liveVerdictOracleFailureDetail(e){if(!this.isArmOnlyOracleFailure(e))return this._verifyOracleFailureDetails.get(e)}conflictOracleMix(e){let n=!1,r=!1;for(let s of e)this._verifyOracleFailureDetails.get(s)?.action===dr?n=!0:r=!0;return n&&!r?"pin":r&&!n?"wait":"mixed"}unresolvedWaitOracleFailureStepsForConcludingTurn(){return this.unresolvedOracleFailureStepsAnyProvenance().filter(e=>this.conflictOracleMix([e])==="wait")}hasUnresolvedVerifyOracleFailure(){return this.unresolvedWaitOracleFailureStepsForConcludingTurn().length>0}oracleConflictReason(e){switch(this.conflictOracleMix(e)){case"pin":return"the expected value was not found in the rendered page";case"wait":return"the wait oracle (wait_for_element) failed or timed out";default:return"the verification oracle failed (a wait timed out, or an expected value was absent from the page)"}}clientConflictReason(e){switch(this.conflictOracleMix(e)){case"pin":return"the expected value was not found on the page";case"wait":return"the expected content never appeared on the page";default:return"the expected content or value was not found on the page"}}clientReconcileClearedNote(e){return`"${e}" was present when the run re-checked at the end. The earlier check did not find it before timing out.`}clientReconcileEquivalentClearedNote(e,n){let r=n.matchType==="amount_currency"?"amount":"number",s=n.matchType==="amount_currency"&&n.currency?`"${n.matched}" with the currency ${n.currency}`:`"${n.matched}"`;return`"${e}" was not written on the page in that exact form. The page shows the same ${r} as ${s}, so the value is present in a different format and the earlier timeout is not a defect.`}clientStaleScopeWithheldNote(e){return`"${e}" \u2014 could not be re-confirmed at the end of the run because later steps changed the page. Reported as unconfirmed rather than passed or failed.`}clientGroundedNegativeNote(e){return`"${e}" was not found on the page, including a final re-check at the end of the run.`}clientNegationBailWithheldNote(e){return`${e.trim()?`"${e}" `:""}could not be re-checked automatically because this step is worded as a negative or absence condition. Every check the plan wrote for this step passed; the re-check itself is reported as unconfirmed rather than passed or failed.`}clientUrlGroundedWithheldNote(e,n){return`${e.trim()?`"${e}" `:""}could not be re-checked automatically, but the page address recorded for this step (${n}) matches the address the step expects, and every check the plan wrote for this step passed. The re-check itself is reported as unconfirmed rather than passed or failed.`}clientWithheldNote(e){return`"${e}" \u2014 could not be independently confirmed on this run. Reported as unconfirmed rather than passed or failed.`}clientAgentLiteralAbstainNote(e,n,r="grounded_criteria",s=!1){return r==="action_shaped"?`"${e}" \u2014 the run chose to wait for this text itself and it never appeared. ${s?"The plan itself states no checks for this step, and the check derived from its wording was met":"The plan itself states no checks for this step, and nothing else about it was reported as failing"}, so this is not counted as a failure: the step is reported as unconfirmed rather than passed or failed.`:`"${e}" \u2014 ${n==="plan_grounded"?"the run waited for this exact wording and did not find it \u2014 the page may present the same value differently":n==="agent_invented_plan_coincident"?"the run chose to wait for this text itself on this step \u2014 the plan mentions it elsewhere, not here \u2014 and it never appeared":"the run chose to wait for this text itself; it is not part of the test plan and never appeared"}. Every check this step defines was confirmed on the page, so the step is reported as unconfirmed rather than passed or failed.`}describeConflictedSteps(e,n){return e.map(r=>`step ${r} ("${n.steps[r-1]?.text??""}")`).join(", ")}canReconcileVerificationConflict(e,n,r){if(ve("VERIFY_CONFLICT_RECONCILE"))return!1;let s=this._verifyOracleFailureDetails.get(e);if(!s||s.action===dr||!Zr(s.action))return!1;let a=s.literal?.trim().toLowerCase();if(!a)return!1;let i=n.steps[e-1];if(Fd(i,this._runTokenTimestamp).toLowerCase().includes(a))return!1;let l=r.find(c=>c.stepIndex===e)?.criteriaResults??[];return!(l.length===0||!l.every(c=>c.passed===!0&&(c.note??"").trim().length>0))}classifyAbsenceVerifyConflict(e,n,r){let s=n.steps[e-1];if(!s||s.type!=="verify")return{verdict:"inapplicable",reason:"not_verify_step"};let a=this._verifyOracleFailureDetails.get(e);if(!a||a.action===dr)return{verdict:"inapplicable",reason:"no_wait_oracle"};if(!Zr(a.action))return{verdict:"inapplicable",reason:"not_wait_oracle"};let i=a.literal?.trim();if(!i)return{verdict:"inapplicable",reason:"no_literal"};if(!(ur(s.text,i)||(s.criteria??[]).some(u=>ur(u.check,i))))return{verdict:"inapplicable",reason:"positive_assertion",literal:i};let l=r.find(u=>u.stepIndex===e)?.criteriaResults??[];if(l.length===0||!l.every(u=>u.passed===!0&&(u.note??"").trim().length>0))return{verdict:"abstain",reason:"criteria_unsubstantiated",literal:i};let c=this._fullSnapshotByStep.get(e);if(!c)return{verdict:"abstain",reason:"no_full_snapshot",literal:i};if(this._fullSnapshotCanvasByStep.get(e))return{verdict:"abstain",reason:"canvas_dominant",literal:i};if(pr(c,i))return{verdict:"abstain",reason:"target_present",literal:i};if(Bre(c))return{verdict:"abstain",reason:"load_failure_after",literal:i};let d=this._fullSnapshotOriginByStep.get(e);return this.hasBeforePresence(i,e,d)?{verdict:"confirmed",reason:"before_present_after_absent",literal:i}:{verdict:"abstain",reason:"no_before_presence",literal:i}}hasBeforePresence(e,n,r){for(let[s,a]of this._fullSnapshotByStep){if(s>=n||this._fullSnapshotCanvasByStep.get(s))continue;let i=this._fullSnapshotOriginByStep.get(s);if(!(r&&i&&i!==r)&&pr(a,e))return!0}return!1}reconcileVerificationConflictToPass(e,n,r,s){if(!this._verificationConflictReconciledSteps.has(e)){let a=this._verifyOracleFailureDetails.get(e);this.log("warn","RunnerRuntime","run_complete:verification_conflict_reconciled",{stepIndex:e,stepText:n.steps[e-1]?.text??"",oracleAction:a?.action??"wait_for_element",oracleLiteral:a?.literal??"",oracleError:a?.error??"",cleanPass:r,basis:s}),this._verificationConflictReconciledSteps.add(e)}this._verifyOracleFailureStepIndexes.delete(e),this._verifyOracleFailureDetails.delete(e)}resolveLastExecutedStepIndex(e){let n=this._maxStepEverActivated;for(let r of e)r.status!=="skipped"&&(typeof r.stepIndex!="number"||!Number.isFinite(r.stepIndex)||r.stepIndex>n&&(n=r.stepIndex));return n}async reconcileVerifyConflictsFromSnapshot(e,n,r,s){let a=new Map,i=UM(),o=(p,m,g)=>(this.log("info","RunnerRuntime","verify_conflict_reconcile:inapplicable",{stepIndex:p,reason:m,literal:g??"",enforced:i}),a.set(p,{verdict:"inapplicable",reason:m,literal:g,basis:"none"})),l=[];for(let p of e){let m=this._verifyOracleFailureDetails.get(p);if(!m){o(p,"no_oracle_detail");continue}if(m.action===dr){o(p,"pin_page_grounding_oracle");continue}if(!Zr(m.action)){o(p,"not_wait_oracle");continue}let g=m.literal?.trim();if(!g){o(p,"no_literal");continue}let v=n.steps[p-1];if(v&&(ur(v.text,g)||(v.criteria??[]).some(w=>ur(w.check,g)))){o(p,"absence_intent",g);continue}if(D_(v,g)){o(p,"negation_marker",g);continue}if(!$i(g)){o(p,"literal_not_significant",g);continue}if(!jre(v,g)){o(p,"literal_not_plan_grounded",g);continue}l.push({idx:p,literal:g})}if(l.length===0)return a;let c="",d="retained_snapshot";if(i){if(this._conflictReconcileSnapshotTaken){for(let p of l)o(p.idx,"reconcile_budget_exhausted",p.literal);return a}this._conflictReconcileSnapshotTaken=!0,this.log("info","RunnerRuntime","verify_conflict_reconcile:snapshot_requested",{steps:l.map(p=>p.idx)});try{let p={name:"snapshot",args:{}},m=await this.executeObservedAction(p,r);c=typeof m.groundingSnapshot=="string"?m.groundingSnapshot:typeof m.response?.pageSnapshot=="string"?m.response.pageSnapshot:"",this.recordVerifyEvidenceFromToolResult(m,"snapshot"),d="fresh_snapshot"}catch(p){this.log("warn","RunnerRuntime","verify_conflict_reconcile:capture_failed",{steps:l.map(m=>m.idx),error:p instanceof Error?p.message:String(p)}),c=""}if(!c.trim()){for(let p of l)o(p.idx,"capture_failed",p.literal);return a}}else{let p=-1;for(let m of this._fullSnapshotByStep.keys())m>p&&(p=m);if(c=p>=0?this._fullSnapshotByStep.get(p)??"":"",!c.trim()){for(let m of l)o(m.idx,"no_retained_snapshot",m.literal);return a}}let u=ve("VERIFY_CONFLICT_RECONCILE"),f=Bne(),h=f&&i;for(let{idx:p,literal:m}of l){let g=pr(c,m),v=new Set,w=g||u?null:Ire(c,m,v);!w&&v.size>0&&this.log("info","RunnerRuntime","verify_conflict_reconcile:literal_typed_equivalence_refused",{stepIndex:p,literal:m,reasons:[...v],basis:d,enforced:h});let E=!g&&h&&w!==null;w&&(this.log("info","RunnerRuntime","verify_conflict_reconcile:literal_typed_equivalence",{stepIndex:p,literal:m,matchType:w.matchType,currency:w.currency??null,matched:w.matched,nodeText:w.nodeText??null,basis:d,enforced:E}),E||this.log("info","RunnerRuntime","verify_conflict_reconcile:literal_typed_equivalence_would_clear",{stepIndex:p,literal:m,matchType:w.matchType,currency:w.currency??null,matched:w.matched,nodeText:w.nodeText??null,basis:d,wouldBecome:"passed",withheldBy:f?"m1_shadow":"lane_off"}));let R=g||E;if(!R&&s>p){this.log("warn","RunnerRuntime","verify_conflict_reconcile:grounded_negative_withheld_stale_scope",{stepIndex:p,literal:m.slice(0,200),lastExecutedStepIndex:s}),o(p,FM,m);continue}a.set(p,{verdict:R?"cleared":"grounded_negative",reason:R?E?"literal_equivalent_in_snapshot":"literal_present_in_snapshot":"literal_absent_from_snapshot",literal:m,basis:d,...E&&w?{equivalence:w}:{}}),this.log("warn","RunnerRuntime",R?"verify_conflict_reconcile:would_clear":"verify_conflict_reconcile:would_fail",{stepIndex:p,stepText:n.steps[p-1]?.text??"",literal:m,basis:d,enforced:i})}return a}classifyVerifyConflictWithhold(e,n,r,s,a){if(s?.verdict==="grounded_negative")return{verdict:"inapplicable",reason:"grounded_negative"};let i=n.steps[e-1];if(!i||i.type!=="verify")return{verdict:"inapplicable",reason:"not_verify_step"};let o=this._verifyOracleFailureDetails.get(e);if(!o||o.action===dr)return{verdict:"inapplicable",reason:"no_wait_oracle"};if(!Zr(o.action))return{verdict:"inapplicable",reason:"not_wait_oracle"};let l=o.literal?.trim()??"";if(l&&(ur(i.text,l)||(i.criteria??[]).some(m=>ur(m.check,l))))return{verdict:"inapplicable",reason:"absence_intent"};if(l&&D_(i,l))return{verdict:"inapplicable",reason:"negation_marker"};if(!a)return{verdict:"inapplicable",reason:"no_post_bounce_observation"};let c=r.find(m=>m.stepIndex===e);if(c&&c.status!=="passed")return{verdict:"inapplicable",reason:"step_already_downgraded"};if(this._reobserveState.get(e)==="withheld")return{verdict:"inapplicable",reason:"reobserve_withheld"};let d=c?.criteriaResults??[];if(d.length===0)return{verdict:"inapplicable",reason:"no_graded_criteria"};if(!d.every(m=>m.passed===!0&&(m.note??"").trim().length>0))return{verdict:"inapplicable",reason:"criteria_unsubstantiated"};let u=[...this._fullSnapshotByStep.values()],f=i.criteria??[];if(!d.some((m,g)=>{if(m.strict===!1)return!1;let v=(m.observed??"").trim()||(f[g]?.expectedValue??"").trim();return!v||!$i(v)?!1:u.some(w=>pr(w,v))}))return{verdict:"inapplicable",reason:"no_corroborated_observation"};if(!l)return{verdict:"inapplicable",reason:"no_literal"};if(!$i(l))return{verdict:"inapplicable",reason:"literal_not_significant"};let p=d.map(m=>`${m.observed??""} ${m.note??""}`).join(`
886
+ `+I+B+C}var jr=class t extends ia{deps;pendingUserMessages=[];_activeRun=void 0;_activeTestPlan=void 0;_activeAuthoredTestPlan=void 0;_activeCredentialRoleOverrides=[];activeRunIdForMessages(){return this._activeRun?.id}_runTokenTimestamp=Math.floor(Date.now()/1e3);_runStartEnvKey="";_currentStepIndex=null;_currentStepText=null;_maxStepEverActivated=0;_maxInRangeStepEverActivated=0;_stepEndUrls=new Map;_stepVisitedUrls=new Map;_stepRegressionFlaggedUrls=new Set;_lastObservedPageUrl=null;_stepRegressionLicenceSpentAtStep=null;_stepRegressionWorkingUrl=null;_stepRegressionBypasses=0;_stepRegressionGuardReportsRejected=0;_stepRegressionRefusals=0;_lastStepRegressionRefusalIteration=null;_lastStepRegressionRefusalSentence=null;_lastStepRegressionRefusalStep=null;_maxStepEverExecuted=0;_reverifyTaintedSteps=new Set;_projectCredentialNames=[];_projectCredentials=[];_loginIdentityRecords=[];_profileIdentityPartitions=null;_profileSeededIdentities=[];_triedCredentialNames=new Set;_lastFilledCredentialName=null;_credentialAuthDeflectionsIssued=0;_verifyStepBlockDeflectionsIssued=0;_offStepBlockDeflectionsIssued=0;_offStepBlockDeflectedCurrentSteps=new Set;_offStepBlockDeflectedDeclaredSteps=new Set;_reportObservationRun;_stepReplay=Ui();_sameValueWriteBreaker=new Xp;_runJsHuntBreaker=new uf;_verbatimPinStepIndex=null;_verbatimPinTargetKey=null;_verbatimPinElementFingerprint=null;_verbatimPinPendingOverrideNote=!1;_screenshots=[];_lastBrowserActionAt=null;_stepActionErrors=new Map;_stepObservedAnchors=new Map;_stepEventsObserved=new Set;_gateAuthoredNoteSpans=new Map;_stepUnnamedControls=new Map;_stepLabelDrift=p_();_stepLabelDriftSettled=new Set;_reportedIssuesThisRun=[];_decorativeSplitRejections=0;_transientEnvRetryState=E_();_suppressNotifications=!1;_editOnly=!1;_concludingTurnActive=!1;_concludingTerminalReason=null;_concludingBlockedContext=null;_concludingBlockedSummaryBase=null;_concludingTurnScope=null;_lastBlockKind=null;_browserUnobservableIssueFiled=!1;_concludingTurnUsed=!1;_temporaryCredentialRoleRequest=!1;_editOnlyEmptyProposals=0;static _maxEditOnlyEmptyProposals=2;_runMemory=new Map;_onRunMemoryUpdate=null;_forcedErrorSummary=null;_lastSeededSessionId=void 0;_startupNavigateLastMilestone=null;_startupNavigateLastElapsedMs=null;_startupStageTimings=[];_startupStageInFlight=null;get startupStageInFlight(){return this._startupStageInFlight}async startupStage(e,n,r){let s=Date.now();this._startupStageInFlight={stage:r,startedAt:s};let a=await ore(e,n,r),i=Date.now()-s;return this._startupStageTimings.push({stage:r,ms:i}),this._startupStageInFlight=null,i>=are&&this.log("warn","RunnerRuntime","startup_stage_slow",{stage:r,ms:i,sinceStartupMs:Date.now()-n,budgetMs:Pf}),a}_emittedVerdictAnalyticsRunIds=new Set;_verifyEvidenceStepIndexes=new Set;_autoVerifyEvidenceStepIndexes=new Set;_consecutiveBrowserShortCircuits=0;_consecutiveBrowserShortCircuitStep=null;_browserDispatchSeq=0;_retainedVerifyFrame=null;_retainedFrameAbstainedSteps=new Map;_currentStepStartedAtMs=null;_currentStepStartedIndex=null;_pinGroundingShadowStepIndexes=new Set;_groundedStateExtractCandidates=new Map;_groundedStateDifferentialCandidates=new Map;_groundedStateCountCandidates=new Map;_domGroundableCandidates=new Map;_groundedStateUrlCandidates=new Map;_predicateBasisCompileCache=new Map;_reobserveState=new Map;_reobservedSteps=new Set;_canonicalCaptureByStep=new Map;_canonicalCapturedBatches=new Set;_fullSnapshotByStep=new Map;_fullSnapshotCanvasByStep=new Map;_fullSnapshotOriginByStep=new Map;_verifyMarkerSignaledThisGeneration=!1;_verifyMarkerIsolationBlockCounts=new Map;_verifyEvidenceGateBlockCounts=new Map;_verifyOracleFailureStepIndexes=new Set;_verifyOracleFailureDetails=new Map;_stableVerifyPageSnapshot=null;_stableVerifySnapshotStep=null;_prepassSkippedStepIndexes=[];_incompleteRunCompleteRejections=0;_unauthoredGradeRunCompleteRejections=0;_reportPredicateBasisFailedSteps=new Set;_verificationConflictRejections=0;_unverifiedVerifyRunCompleteRejections=new Set;_unverifiedVerifySignalStepDeflections=new Set;_zeroGradeVerifyRejections=0;_verificationConflictReconciledSteps=new Set;_conflictReconcileSnapshotTaken=!1;_verifyObservationSeq=0;_lastVerifyObservationSeqByStep=new Map;_conflictBounceObservationSeq=null;_stepDeviations=new Map;_stepDeviationRejectedOnce=new Set;_stepClickedControlNames=new Map;_stepExpectedControls=new Map;_stepAdherenceContracts=new Map;_submittedVerdicts=new Map;_adherenceInterventions=new Map;_stepHadUnnamedClick=new Set;_migrationEligible=!1;_profileCaptureAllowed=!1;_observedLoginStepIndexes=new Set;_authLandingUrl=null;_lastObservedActionUrl=null;_captureAuthLandingNext=!1;_uploadStepResolvedPaths=new Map;_uploadAttachAttemptedSteps=new Set;_deterministicUploadDoneSteps=new Set;_deterministicUploadFailures=new Map;_deterministicUploadCooldownSteps=new Set;_pendingVerifyWaitAdvance=null;constructor(e,n){super(e,n),this.deps=n}onEndRun(){let e=this._lastSeededSessionId??this.sessionId;this.deps.computerUseService?.clearCredentials?.(e),this._lastSeededSessionId=void 0}repairDanglingToolCalls(e){for(let n=0;n<e.length;n++){let r=e[n];if(r.role!=="model")continue;let s=(r.parts??[]).filter(l=>l?.functionCall?.id).map(l=>({id:l.functionCall.id,name:l.functionCall.name}));if(s.length===0)continue;let a=new Set;for(let l=n+1;l<e.length&&e[l].role==="user";l++)for(let c of e[l].parts??[])c?.functionResponse?.id&&a.add(c.functionResponse.id);let i=s.filter(l=>!a.has(l.id));if(i.length===0)continue;this.log("info","RunnerRuntime","Repairing dangling tool calls",{count:i.length,tools:i.map(l=>l.name)});let o=n+1;for(;o<e.length&&e[o].role!=="user";)o++;(o>=e.length||!e[o].parts)&&(e.splice(n+1,0,{role:"user",parts:[]}),o=n+1);for(let l of i)e[o].parts.push({functionResponse:{name:l.name,id:l.id,response:{status:"skipped",reason:"execution stopped"}}})}}injectUserMessage(e){this.pendingUserMessages.push(e)}async resetForNextPlan(e={}){let n=e.keepMemory??!0;if(this._isRunning)throw new Error("resetForNextPlan: cannot reset while a run is in progress \u2014 stop() first");this._lastSeededSessionId&&(this.deps.computerUseService?.clearCredentials?.(this._lastSeededSessionId),this._lastSeededSessionId=void 0),this.unattendedRun=!1,this.log("info","RunnerRuntime","reset_for_next_plan",{keepMemory:n,hadActiveRun:this._activeRun!==void 0,traceLength:this.conversationTrace.length,pendingUserMessages:this.pendingUserMessages.length});try{await this.deps.computerUseService.cleanupSession(this.sessionId)}catch(r){this.log("warn","RunnerRuntime","reset_for_next_plan:cleanup_session_failed",{error:r instanceof Error?r.message:String(r)})}this.clearConversationTrace(),this.pendingUserMessages=[],this._screenshots=[],this._lastBrowserActionAt=null,this._reportedIssuesThisRun=[],this._transientEnvRetryState=E_(),this._currentStepIndex=null,this._maxStepEverActivated=0,this._maxInRangeStepEverActivated=0,this._maxStepEverExecuted=0,this._reverifyTaintedSteps.clear(),this._currentStepText=null,this._projectCredentialNames=[],this._projectCredentials=[],this._loginIdentityRecords=[],this._profileIdentityPartitions=null,this._profileSeededIdentities=[],this._triedCredentialNames=new Set,this._lastFilledCredentialName=null,this._credentialAuthDeflectionsIssued=0,this.resetCredentialTypingObservations(),this._verifyStepBlockDeflectionsIssued=0,this._offStepBlockDeflectionsIssued=0,this._offStepBlockDeflectedCurrentSteps.clear(),this._offStepBlockDeflectedDeclaredSteps.clear(),this._reportObservationRun=void 0,this.setEditOnly(!1),this._concludingTurnActive=!1,this._concludingTerminalReason=null,this._concludingBlockedContext=null,this._concludingBlockedSummaryBase=null,this._concludingTurnUsed=!1,this._concludingTurnScope=null,this._lastBlockKind=null,this._browserUnobservableIssueFiled=!1,this._suppressNotifications=!1,this._verifyEvidenceStepIndexes.clear(),this._stepActionErrors.clear(),this._stepObservedAnchors.clear(),this._stepEventsObserved.clear(),this._gateAuthoredNoteSpans.clear(),this._stepUnnamedControls.clear(),this._stepLabelDrift=p_(),this._stepLabelDriftSettled.clear(),this._stepReplay=Ui(),this._autoVerifyEvidenceStepIndexes.clear(),this._runJsHuntBreaker.reset(),this._offStepBlockDeflectionsIssued=0,this._offStepBlockDeflectedCurrentSteps.clear(),this._offStepBlockDeflectedDeclaredSteps.clear(),this._consecutiveBrowserShortCircuits=0,this._consecutiveBrowserShortCircuitStep=null,this._currentStepStartedAtMs=null,this._currentStepStartedIndex=null,this._verifyMarkerIsolationBlockCounts.clear(),this._verifyEvidenceGateBlockCounts.clear(),this._pinGroundingShadowStepIndexes.clear(),this._groundedStateExtractCandidates.clear(),this._groundedStateDifferentialCandidates.clear(),this._groundedStateCountCandidates.clear(),this._domGroundableCandidates.clear(),this._stepEndUrls.clear(),this._stepVisitedUrls.clear(),this._stepRegressionFlaggedUrls.clear(),this._lastObservedPageUrl=null,this._stepRegressionLicenceSpentAtStep=null,this._stepRegressionWorkingUrl=null,this._stepRegressionBypasses=0,this._stepRegressionGuardReportsRejected=0,this._stepRegressionRefusals=0,this._lastStepRegressionRefusalIteration=null,this._lastStepRegressionRefusalSentence=null,this._lastStepRegressionRefusalStep=null,this._groundedStateUrlCandidates.clear(),this._predicateBasisCompileCache.clear(),this._reobserveState.clear(),this._reobservedSteps.clear(),this._conflictReconcileSnapshotTaken=!1,this._verifyObservationSeq=0,this._lastVerifyObservationSeqByStep.clear(),this._conflictBounceObservationSeq=null,this._canonicalCaptureByStep.clear(),this._canonicalCapturedBatches.clear(),this._fullSnapshotByStep.clear(),this._fullSnapshotCanvasByStep.clear(),this._fullSnapshotOriginByStep.clear(),this._verifyOracleFailureStepIndexes.clear(),this._verifyOracleFailureDetails.clear(),this._stableVerifyPageSnapshot=null,this._stableVerifySnapshotStep=null,this._retainedVerifyFrame=null,this._retainedFrameAbstainedSteps.clear(),this._migrationEligible=!1,this._observedLoginStepIndexes=new Set,this._authLandingUrl=null,this._lastObservedActionUrl=null,this._captureAuthLandingNext=!1,this._uploadStepResolvedPaths=new Map,this._uploadAttachAttemptedSteps=new Set,this._deterministicUploadDoneSteps=new Set,this._deterministicUploadFailures=new Map,this._deterministicUploadCooldownSteps=new Set,this._pendingVerifyWaitAdvance=null,this._activeRun=void 0,this._activeTestPlan=void 0,this._activeAuthoredTestPlan=void 0,this._activeCredentialRoleOverrides=[],this._currentRunId=void 0,this._runMemory=new Map,this._onRunMemoryUpdate=null,this._checkEmailCallCount=0,this._canonicalEmailWritten=!1,this._emailVerificationExpected=!1,this._lastCheckEmailMessageId=null,this._sentEmailReplies=[]}emit(e,n){return super.emit(e,n)}async persistConversationTrace(e,n){this.stripOldScreenshots(n),await this.baseDeps.chatRepo.upsertSession({...e,updatedAt:Date.now(),conversationTrace:n})}extractErrorMessage(e){try{let r=e.match(/\{[\s\S]*\}/);if(r){let s=JSON.parse(r[0]);if(s.error?.message)return s.error.message;if(s.message)return s.message}}catch{}let n=e.match(/page\.goto:\s*(.+)/);return n?n[1]:e}supervisorGroundingPlanText(){return this._currentStepText??(this._currentStepIndex?this._activeTestPlan?.steps[this._currentStepIndex-1]?.text:void 0)??void 0}supervisorGroundingEnforcementLane(){return!0}loopVisionDeltaFloor(){return ve("RUNNER_LOOP_VISION_DELTA_FROM_GRANT_2")?super.loopVisionDeltaFloor():1}isVerifyStep(e){return!e||!this._activeTestPlan?!1:this._activeTestPlan.steps[e-1]?.type==="verify"}findUnverifiedVerifyStepBefore(e){if(!this._activeTestPlan||!this._currentStepIndex||e<=this._currentStepIndex)return null;let n=this.isVerifyStep(this._currentStepIndex)?this._currentStepIndex:this._currentStepIndex+1;for(let r=n;r<e;r++)if(this.isVerifyStep(r)&&!this._verifyEvidenceStepIndexes.has(r))return r;return null}findFirstUnverifiedVerifyStep(){if(!this._activeTestPlan)return null;for(let e=1;e<=this._activeTestPlan.steps.length;e++)if(this.isVerifyStep(e)&&!this._verifyEvidenceStepIndexes.has(e))return e;return null}unresolvedOracleFailureStepsAnyProvenance(){return!aa()||!this._activeTestPlan?[]:[...this._verifyOracleFailureStepIndexes].filter(e=>this.isVerifyStep(e)).sort((e,n)=>e-n)}verifyStepsWithUnresolvedOracleFailureForLiveVerdict(){return this.unresolvedOracleFailureStepsAnyProvenance().filter(e=>!this.isArmOnlyOracleFailure(e))}isArmOnlyOracleFailure(e){return this._verifyOracleFailureDetails.get(e)?.via==="rollback"}hasLiveVerdictOracleFailure(e){return this._verifyOracleFailureStepIndexes.has(e)&&!this.isArmOnlyOracleFailure(e)}liveVerdictOracleFailureDetail(e){if(!this.isArmOnlyOracleFailure(e))return this._verifyOracleFailureDetails.get(e)}conflictOracleMix(e){let n=!1,r=!1;for(let s of e)this._verifyOracleFailureDetails.get(s)?.action===dr?n=!0:r=!0;return n&&!r?"pin":r&&!n?"wait":"mixed"}unresolvedWaitOracleFailureStepsForConcludingTurn(){return this.unresolvedOracleFailureStepsAnyProvenance().filter(e=>this.conflictOracleMix([e])==="wait")}hasUnresolvedVerifyOracleFailure(){return this.unresolvedWaitOracleFailureStepsForConcludingTurn().length>0}oracleConflictReason(e){switch(this.conflictOracleMix(e)){case"pin":return"the expected value was not found in the rendered page";case"wait":return"the wait oracle (wait_for_element) failed or timed out";default:return"the verification oracle failed (a wait timed out, or an expected value was absent from the page)"}}clientConflictReason(e){switch(this.conflictOracleMix(e)){case"pin":return"the expected value was not found on the page";case"wait":return"the expected content never appeared on the page";default:return"the expected content or value was not found on the page"}}clientReconcileClearedNote(e){return`"${e}" was present when the run re-checked at the end. The earlier check did not find it before timing out.`}clientReconcileEquivalentClearedNote(e,n){let r=n.matchType==="amount_currency"?"amount":"number",s=n.matchType==="amount_currency"&&n.currency?`"${n.matched}" with the currency ${n.currency}`:`"${n.matched}"`;return`"${e}" was not written on the page in that exact form. The page shows the same ${r} as ${s}, so the value is present in a different format and the earlier timeout is not a defect.`}clientStaleScopeWithheldNote(e){return`"${e}" \u2014 could not be re-confirmed at the end of the run because later steps changed the page. Reported as unconfirmed rather than passed or failed.`}clientGroundedNegativeNote(e){return`"${e}" was not found on the page, including a final re-check at the end of the run.`}clientNegationBailWithheldNote(e){return`${e.trim()?`"${e}" `:""}could not be re-checked automatically because this step is worded as a negative or absence condition. Every check the plan wrote for this step passed; the re-check itself is reported as unconfirmed rather than passed or failed.`}clientUrlGroundedWithheldNote(e,n){return`${e.trim()?`"${e}" `:""}could not be re-checked automatically, but the page address recorded for this step (${n}) matches the address the step expects, and every check the plan wrote for this step passed. The re-check itself is reported as unconfirmed rather than passed or failed.`}clientWithheldNote(e){return`"${e}" \u2014 could not be independently confirmed on this run. Reported as unconfirmed rather than passed or failed.`}clientAgentLiteralAbstainNote(e,n,r="grounded_criteria",s=!1){return r==="action_shaped"?`"${e}" \u2014 the run chose to wait for this text itself and it never appeared. ${s?"The plan itself states no checks for this step, and the check derived from its wording was met":"The plan itself states no checks for this step, and nothing else about it was reported as failing"}, so this is not counted as a failure: the step is reported as unconfirmed rather than passed or failed.`:`"${e}" \u2014 ${n==="plan_grounded"?"the run waited for this exact wording and did not find it \u2014 the page may present the same value differently":n==="agent_invented_plan_coincident"?"the run chose to wait for this text itself on this step \u2014 the plan mentions it elsewhere, not here \u2014 and it never appeared":"the run chose to wait for this text itself; it is not part of the test plan and never appeared"}. Every check this step defines was confirmed on the page, so the step is reported as unconfirmed rather than passed or failed.`}describeConflictedSteps(e,n){return e.map(r=>`step ${r} ("${n.steps[r-1]?.text??""}")`).join(", ")}canReconcileVerificationConflict(e,n,r){if(ve("VERIFY_CONFLICT_RECONCILE"))return!1;let s=this._verifyOracleFailureDetails.get(e);if(!s||s.action===dr||!Zr(s.action))return!1;let a=s.literal?.trim().toLowerCase();if(!a)return!1;let i=n.steps[e-1];if(Fd(i,this._runTokenTimestamp).toLowerCase().includes(a))return!1;let l=r.find(c=>c.stepIndex===e)?.criteriaResults??[];return!(l.length===0||!l.every(c=>c.passed===!0&&(c.note??"").trim().length>0))}classifyAbsenceVerifyConflict(e,n,r){let s=n.steps[e-1];if(!s||s.type!=="verify")return{verdict:"inapplicable",reason:"not_verify_step"};let a=this._verifyOracleFailureDetails.get(e);if(!a||a.action===dr)return{verdict:"inapplicable",reason:"no_wait_oracle"};if(!Zr(a.action))return{verdict:"inapplicable",reason:"not_wait_oracle"};let i=a.literal?.trim();if(!i)return{verdict:"inapplicable",reason:"no_literal"};if(!(ur(s.text,i)||(s.criteria??[]).some(u=>ur(u.check,i))))return{verdict:"inapplicable",reason:"positive_assertion",literal:i};let l=r.find(u=>u.stepIndex===e)?.criteriaResults??[];if(l.length===0||!l.every(u=>u.passed===!0&&(u.note??"").trim().length>0))return{verdict:"abstain",reason:"criteria_unsubstantiated",literal:i};let c=this._fullSnapshotByStep.get(e);if(!c)return{verdict:"abstain",reason:"no_full_snapshot",literal:i};if(this._fullSnapshotCanvasByStep.get(e))return{verdict:"abstain",reason:"canvas_dominant",literal:i};if(pr(c,i))return{verdict:"abstain",reason:"target_present",literal:i};if(Bre(c))return{verdict:"abstain",reason:"load_failure_after",literal:i};let d=this._fullSnapshotOriginByStep.get(e);return this.hasBeforePresence(i,e,d)?{verdict:"confirmed",reason:"before_present_after_absent",literal:i}:{verdict:"abstain",reason:"no_before_presence",literal:i}}hasBeforePresence(e,n,r){for(let[s,a]of this._fullSnapshotByStep){if(s>=n||this._fullSnapshotCanvasByStep.get(s))continue;let i=this._fullSnapshotOriginByStep.get(s);if(!(r&&i&&i!==r)&&pr(a,e))return!0}return!1}reconcileVerificationConflictToPass(e,n,r,s){if(!this._verificationConflictReconciledSteps.has(e)){let a=this._verifyOracleFailureDetails.get(e);this.log("warn","RunnerRuntime","run_complete:verification_conflict_reconciled",{stepIndex:e,stepText:n.steps[e-1]?.text??"",oracleAction:a?.action??"wait_for_element",oracleLiteral:a?.literal??"",oracleError:a?.error??"",cleanPass:r,basis:s}),this._verificationConflictReconciledSteps.add(e)}this._verifyOracleFailureStepIndexes.delete(e),this._verifyOracleFailureDetails.delete(e)}resolveLastExecutedStepIndex(e){let n=this._maxStepEverActivated;for(let r of e)r.status!=="skipped"&&(typeof r.stepIndex!="number"||!Number.isFinite(r.stepIndex)||r.stepIndex>n&&(n=r.stepIndex));return n}async reconcileVerifyConflictsFromSnapshot(e,n,r,s){let a=new Map,i=UM(),o=(p,m,g)=>(this.log("info","RunnerRuntime","verify_conflict_reconcile:inapplicable",{stepIndex:p,reason:m,literal:g??"",enforced:i}),a.set(p,{verdict:"inapplicable",reason:m,literal:g,basis:"none"})),l=[];for(let p of e){let m=this._verifyOracleFailureDetails.get(p);if(!m){o(p,"no_oracle_detail");continue}if(m.action===dr){o(p,"pin_page_grounding_oracle");continue}if(!Zr(m.action)){o(p,"not_wait_oracle");continue}let g=m.literal?.trim();if(!g){o(p,"no_literal");continue}let v=n.steps[p-1];if(v&&(ur(v.text,g)||(v.criteria??[]).some(w=>ur(w.check,g)))){o(p,"absence_intent",g);continue}if(D_(v,g)){o(p,"negation_marker",g);continue}if(!$i(g)){o(p,"literal_not_significant",g);continue}if(!jre(v,g)){o(p,"literal_not_plan_grounded",g);continue}l.push({idx:p,literal:g})}if(l.length===0)return a;let c="",d="retained_snapshot";if(i){if(this._conflictReconcileSnapshotTaken){for(let p of l)o(p.idx,"reconcile_budget_exhausted",p.literal);return a}this._conflictReconcileSnapshotTaken=!0,this.log("info","RunnerRuntime","verify_conflict_reconcile:snapshot_requested",{steps:l.map(p=>p.idx)});try{let p={name:"snapshot",args:{}},m=await this.executeObservedAction(p,r);c=typeof m.groundingSnapshot=="string"?m.groundingSnapshot:typeof m.response?.pageSnapshot=="string"?m.response.pageSnapshot:"",this.recordVerifyEvidenceFromToolResult(m,"snapshot"),d="fresh_snapshot"}catch(p){this.log("warn","RunnerRuntime","verify_conflict_reconcile:capture_failed",{steps:l.map(m=>m.idx),error:p instanceof Error?p.message:String(p)}),c=""}if(!c.trim()){for(let p of l)o(p.idx,"capture_failed",p.literal);return a}}else{let p=-1;for(let m of this._fullSnapshotByStep.keys())m>p&&(p=m);if(c=p>=0?this._fullSnapshotByStep.get(p)??"":"",!c.trim()){for(let m of l)o(m.idx,"no_retained_snapshot",m.literal);return a}}let u=ve("VERIFY_CONFLICT_RECONCILE"),f=Bne(),h=f&&i;for(let{idx:p,literal:m}of l){let g=pr(c,m),v=new Set,w=g||u?null:Ire(c,m,v);!w&&v.size>0&&this.log("info","RunnerRuntime","verify_conflict_reconcile:literal_typed_equivalence_refused",{stepIndex:p,literal:m,reasons:[...v],basis:d,enforced:h});let E=!g&&h&&w!==null;w&&(this.log("info","RunnerRuntime","verify_conflict_reconcile:literal_typed_equivalence",{stepIndex:p,literal:m,matchType:w.matchType,currency:w.currency??null,matched:w.matched,nodeText:w.nodeText??null,basis:d,enforced:E}),E||this.log("info","RunnerRuntime","verify_conflict_reconcile:literal_typed_equivalence_would_clear",{stepIndex:p,literal:m,matchType:w.matchType,currency:w.currency??null,matched:w.matched,nodeText:w.nodeText??null,basis:d,wouldBecome:"passed",withheldBy:f?"m1_shadow":"lane_off"}));let R=g||E;if(!R&&s>p){this.log("warn","RunnerRuntime","verify_conflict_reconcile:grounded_negative_withheld_stale_scope",{stepIndex:p,literal:m.slice(0,200),lastExecutedStepIndex:s}),o(p,FM,m);continue}a.set(p,{verdict:R?"cleared":"grounded_negative",reason:R?E?"literal_equivalent_in_snapshot":"literal_present_in_snapshot":"literal_absent_from_snapshot",literal:m,basis:d,...E&&w?{equivalence:w}:{}}),this.log("warn","RunnerRuntime",R?"verify_conflict_reconcile:would_clear":"verify_conflict_reconcile:would_fail",{stepIndex:p,stepText:n.steps[p-1]?.text??"",literal:m,basis:d,enforced:i})}return a}classifyVerifyConflictWithhold(e,n,r,s,a){if(s?.verdict==="grounded_negative")return{verdict:"inapplicable",reason:"grounded_negative"};let i=n.steps[e-1];if(!i||i.type!=="verify")return{verdict:"inapplicable",reason:"not_verify_step"};let o=this._verifyOracleFailureDetails.get(e);if(!o||o.action===dr)return{verdict:"inapplicable",reason:"no_wait_oracle"};if(!Zr(o.action))return{verdict:"inapplicable",reason:"not_wait_oracle"};let l=o.literal?.trim()??"";if(l&&(ur(i.text,l)||(i.criteria??[]).some(m=>ur(m.check,l))))return{verdict:"inapplicable",reason:"absence_intent"};if(l&&D_(i,l))return{verdict:"inapplicable",reason:"negation_marker"};if(!a)return{verdict:"inapplicable",reason:"no_post_bounce_observation"};let c=r.find(m=>m.stepIndex===e);if(c&&c.status!=="passed")return{verdict:"inapplicable",reason:"step_already_downgraded"};if(this._reobserveState.get(e)==="withheld")return{verdict:"inapplicable",reason:"reobserve_withheld"};let d=c?.criteriaResults??[];if(d.length===0)return{verdict:"inapplicable",reason:"no_graded_criteria"};if(!d.every(m=>m.passed===!0&&(m.note??"").trim().length>0))return{verdict:"inapplicable",reason:"criteria_unsubstantiated"};let u=[...this._fullSnapshotByStep.values()],f=i.criteria??[];if(!d.some((m,g)=>{if(m.strict===!1)return!1;let v=(m.observed??"").trim()||(f[g]?.expectedValue??"").trim();return!v||!$i(v)?!1:u.some(w=>pr(w,v))}))return{verdict:"inapplicable",reason:"no_corroborated_observation"};if(!l)return{verdict:"inapplicable",reason:"no_literal"};if(!$i(l))return{verdict:"inapplicable",reason:"literal_not_significant"};let p=d.map(m=>`${m.observed??""} ${m.note??""}`).join(`
887
887
  `).trim();return Qn(p,l)?{verdict:"withhold",reason:"unconfirmable_wait_oracle"}:{verdict:"inapplicable",reason:"literal_uncorroborated"}}criterionGroundedThisRun(e,n,r,s){if(!e)return!1;let a=e.groundingObservation;if(a&&typeof a.value=="string"&&a.value.trim().length>0){let i=!s||!a.runId||a.runId===s,o=a.stepIndex===void 0||a.stepIndex===r;if(i&&o)return!0}return this.criterionValueSeenOnOwnFrame(e,n,r)}criterionValueSeenOnOwnFrame(e,n,r){if(!e)return!1;let s=(e.observed??"").trim()||(n?.expectedValue??"").trim();if(!s||!$i(s))return!1;let a=this._fullSnapshotByStep.get(r);return typeof a!="string"||a.trim().length===0?!1:pr(a,s)}classifyAgentLiteralAbstain(e,n,r,s,a){if(ve("VERIFY_CONFLICT_RECONCILE"))return{verdict:"inapplicable",reason:"reconcile_family_disabled"};if(s?.verdict==="grounded_negative")return{verdict:"inapplicable",reason:"grounded_negative"};let i=n.steps[e-1];if(!i||i.type!=="verify")return{verdict:"inapplicable",reason:"not_verify_step"};let o=this._verifyOracleFailureDetails.get(e);if(!o||o.action===dr)return{verdict:"inapplicable",reason:"no_wait_oracle"};if(!Zr(o.action))return{verdict:"inapplicable",reason:"not_wait_oracle"};let l=o.literal?.trim();if(!l)return{verdict:"inapplicable",reason:"no_literal"};let c=o.timeoutMs,d=Fd(i,this._runTokenTimestamp).toLowerCase().includes(l.toLowerCase()),u=d?void 0:Ure(n,l,e,this._runTokenTimestamp),f=d?"plan_grounded":u?"agent_invented_plan_coincident":"agent_invented",h=(i.criteria??[]).length>0&&KM(i,l)&&!ur(i.text,l)&&!(i.criteria??[]).some(R=>ur(R.check,l))&&!D_(i,l);if(f==="plan_grounded"&&!h)return{verdict:"inapplicable",reason:"literal_plan_grounded",literal:l,timeoutMs:c,provenance:f};let p=i.criteria??[],m=p.length===0?"action_shaped":"grounded_criteria",g=r.find(R=>R.stepIndex===e),v=g?.criteriaResults??[],w=R=>({verdict:"inapplicable",reason:R,literal:l,timeoutMs:c,provenance:f,planMatchLocation:u,lane:m});return g?.status==="failed"?w(m==="action_shaped"?"action_shaped_step_graded_failed":"step_graded_failed"):g&&g.status!=="passed"?w("step_already_downgraded"):m==="action_shaped"?v.some(R=>R.passed!==!0)?w("action_shaped_criterion_graded_failed"):{verdict:"abstain",reason:"action_shaped_invented_wait_literal",literal:l,timeoutMs:c,provenance:f,planMatchLocation:u,lane:m,derivedCheckPassed:v.length>0}:v.length<p.length?w("plan_criteria_ungraded"):v.some(R=>R.passed!==!0)?w("criterion_graded_failed"):p.every((R,_)=>f==="plan_grounded"?this.criterionValueSeenOnOwnFrame(v[_],R,e):this.criterionGroundedThisRun(v[_],R,e,a))?{verdict:"abstain",reason:f==="plan_grounded"?Nf:"agent_invented_wait_literal",literal:l,timeoutMs:c,provenance:f,planMatchLocation:u,lane:m}:w(f==="plan_grounded"?"plan_literal_criteria_not_page_grounded":"criteria_ungrounded")}buildMissingEmailVerificationRejection(e,n){return e.args?.status!=="passed"||!en(n.session?.config??this._activeTestPlan?.config)?.canCheckInbox||!this._emailVerificationExpected||this._checkEmailCallCount>0?null:{response:{status:"error",error:"Account creation appears to require email verification but check_email was never called. Read the inbox via check_email and complete verification (click the verification link or enter the verification code) before reporting complete."},isMetaTool:!0,resetLoopDetector:!0}}buildVerifyEvidenceRequiredResult(e){let n=this._activeTestPlan?.steps[e-1]?.text??"the verify step";return this.noteStepReopenedForReverify(e),this._currentStepIndex=e,this._stepLabelDriftSettled.delete(e),this.noteStepActivated(e),this._currentStepText=n,{response:{status:"error",error:`Step ${e} is a VERIFY step and requires explicit verification evidence before advancing. Call screenshot or wait_for_element for: ${n}`},isMetaTool:!0,resetLoopDetector:!0}}async maybeIsolateVerifyMarker(e,n){if(!_l()||!this._verifyMarkerSignaledThisGeneration)return null;let r=this._currentStepIndex;if(r==null)return null;let s=this._verifyMarkerIsolationBlockCounts.get(r)??0;return s>=1?this.autoCaptureVerifyEvidence(n,r):(this._verifyMarkerIsolationBlockCounts.set(r,s+1),this.buildVerifyMarkerIsolationBlock(e.name))}buildVerifyMarkerIsolationBlock(e="browser_action"){if(!_l()||!this._verifyMarkerSignaledThisGeneration)return null;let n=this._currentStepIndex;return this.log("info","RunnerRuntime","verify_marker_isolation:blocked",{stepIndex:n,consecutiveBlocks:this._verifyMarkerIsolationBlockCounts.get(n??-1)??0}),{response:{status:"error",error:`The verify marker for step ${n} must stand alone \u2014 do not co-locate an action with it. Re-observe and capture evidence for step ${n} on a fresh turn (call screenshot or wait_for_element by itself), then evaluate the criteria.`},isMetaTool:!0,supervisorEntry:{action:e,intent:"not dispatched to the browser",skipped:"meta",skippedReason:"verify_marker_isolation"}}}async autoCaptureVerifyEvidence(e,n,r){r===void 0&&this.log("info","RunnerRuntime","verify_marker_isolation:auto_capture",{stepIndex:n});let s={name:"screenshot",args:{intent:`Auto-captured verification evidence for step ${n}`}},a=await this.executeObservedAction(s,e);if(this.recordVerifyEvidenceFromToolResult(a,"screenshot"),a.screenshotBase64&&this._screenshots.push({base64:a.screenshotBase64,actionName:"screenshot",timestamp:Date.now(),stepIndex:n,stepText:this._currentStepText??void 0,intent:typeof s.args?.intent=="string"?s.args.intent:void 0,kind:"full_frame"}),a.message&&this._activeRun&&(a.message={...a.message,runId:this._activeRun.id}),a.response&&typeof a.response=="object"){let i=a.response;i.note=r??`The verify marker for step ${n} was emitted with a co-located action across consecutive generations; Agentiqa captured verification evidence for step ${n} directly (a standalone screenshot taken while the step is active). Evaluate the criteria for step ${n} now, then continue.`}return await this.maybeCaptureCanonicalEvidence(e),a}async maybeBlockActionUntilVerifyEvidence(e,n){let r=e.name;if(!this.isVerifyStep(this._currentStepIndex)||this._verifyEvidenceStepIndexes.has(this._currentStepIndex)||Al(r)||k_(r))return null;let s=this._currentStepIndex,a=Dne(),i=this._verifyEvidenceGateBlockCounts.get(s)??0;if(a&&i>=1)return this.log("info","RunnerRuntime","verify_evidence_gate:standoff_capture",{stepIndex:s,action:r,priorBlocks:i}),this.autoCaptureVerifyEvidence(n,s,`Verification evidence for step ${s} was captured by the runtime after two refused actions. Grade this step's criteria from this frame now; if a precondition blocks the step (for example a sign-in wall), report that honestly instead of retrying the action.`);a&&(this._verifyEvidenceGateBlockCounts.set(s,i+1),this.log("info","RunnerRuntime","verify_evidence_gate:blocked",{stepIndex:s,action:r,count:i+1}));let o=this._currentStepText??this._activeTestPlan?.steps[s-1]?.text??"";return{response:{status:"error",error:`Step ${s} requires explicit verification evidence before any other browser action. Call screenshot or wait_for_element for: ${o}`},isMetaTool:!0,...a?{}:{resetLoopDetector:!0},supervisorEntry:{action:r,intent:"not dispatched to the browser",skipped:"meta",skippedReason:"verify_evidence_required"}}}activeClickStepConstraint(){if(ve("PLAN_OBEDIENCE"))return null;let e=this._currentStepIndex;if(e==null)return null;let n=this._currentStepText??this._activeTestPlan?.steps[e-1]?.text??"";if(!n||!Xne(n))return null;let r=this._stepAdherenceContracts.get(e);(!r||r.text!==n)&&(r={text:n,contract:cP(n)},this._stepAdherenceContracts.set(e,r),this.log("info","RunnerRuntime","plan_adherence:contract",{stepIndex:e,...r.contract}));let s=r.contract.expected.flatMap(i=>i.names),a=r.contract.forbidden.flatMap(i=>i.names);return s.length===0&&a.length===0?null:{stepIndex:e,stepText:n,expectedControls:s,forbiddenControls:a}}matchesForbiddenControl(e,n){return n.length>0&&Kh(e,n)}recordStepClick(e,n,r){if(r.length===0)return;let s=n.trim();if(!s)return;this._stepExpectedControls.has(e)||this._stepExpectedControls.set(e,r);let a=this._stepClickedControlNames.get(e)??[];a.push(s),this._stepClickedControlNames.set(e,a)}recordExpectedControlValueWrite(e,n){if(e.name!=="type_text_at"&&e.name!=="set_focused_input_value"||n.response?.status!=="ok"||n.response?.valueMismatch===!0)return;let r=typeof e.args?.text=="string"?e.args.text.trim():typeof e.args?.value=="string"?e.args.value.trim():"";if(!r)return;let s=this.activeClickStepConstraint();if(!s)return;let{stepIndex:a,expectedControls:i}=s;Kh(r,i)&&this.recordStepClick(a,r,i)}attachStepTextForEmailPreservation(e){if(e.name!=="type_text_at"&&e.name!=="set_focused_input_value")return;let n=this._currentStepText;typeof n!="string"||n.length===0||(e.args={...e.args??{},stepText:n})}verbatimWriteTargetKey(e){let n=e.args??{};return typeof n.label=="string"&&n.label.trim()?`label:${n.label.trim()}`:typeof n.ref=="string"&&n.ref.trim()?`ref:${n.ref.trim()}`:typeof n.x=="number"&&typeof n.y=="number"?`xy:${n.x},${n.y}`:null}noteIntentLiteralMismatch(e){if(!Ei(e.name))return;let n=Wc(e.name,e.args)??"",{quoted:r,mismatch:s}=cC({intent:e.args?.intent,text:n});s&&this.log("info","RunnerRuntime","type_text_at:intent_literal_mismatch",{stepIndex:this._currentStepIndex,tool:e.name,quoted:r,textHash:Kc(n)})}maybeBreakSameValueWrite(e){if(ve("RUNNER_SAME_VALUE_WRITE_BREAKER")||!Ei(e.name))return null;let n=this._currentStepIndex;if(typeof n!="number")return null;let r=Wc(e.name,e.args)??"";if(r.trim().length===0)return null;let s=zc(e.args),a=this._sameValueWriteBreaker.evaluate(n,s,Kc(r));if(!a.bounce)return null;let i=typeof e.args?.label=="string"?e.args.label.trim():"",o=typeof e.args?.ref=="string"?e.args.ref.trim():"",l=this._sameValueWriteBreaker.resolvedFieldFor(s),c=i||l||(o?`the element ${o}`:"the field you have focused");this.log("warn","RunnerRuntime","type_text_at:same_value_repeat",{stepIndex:n,tool:e.name,target:s,...i?{label:i}:{},...o?{ref:o}:{},count:a.count});let d=e.name==="set_focused_input_value"?"address the field directly with type_text_at by its visible label":"set the value natively via set_focused_input_value / a fill",u=`You have now written the SAME value into ${c} ${a.count} times in a row on step ${n}, and the field's value did not change between those writes \u2014 this write was not dispatched. Re-sending the identical value will not help. Check that args carries the value your intent describes (on a step with more than one value, the previous field's value is easy to re-send by mistake), and if the value IS right but will not commit, use a different entry path: click the field to focus it and use insertText, or select the existing content first (select-all, then type over it), or ${d}. If the field genuinely cannot accept the value, the step must FAIL \u2014 report it in run_complete with a note.`;return{response:{status:"same_value_write_refused",field:c,repeatedWrites:a.count,instruction:u},isMetaTool:!0,supervisorEntry:{action:e.name,intent:"not dispatched to the browser",skipped:"meta",skippedReason:"same_value_write_refused"}}}maybeBreakRunJsHunt(e){if(ve("RUNNER_RUNJS_HUNT_BREAKER"))return null;if(e.name!=="run_js")return this._runJsHuntBreaker.noteOtherAction(),null;let n=this._currentStepIndex;if(typeof n!="number")return null;let r=this._runJsHuntBreaker.evaluate(n);if(!r.bounce)return null;this.log("warn","RunnerRuntime","run_js:hunt_breaker",{stepIndex:n,count:r.count,accelerated:r.accelerated});let s=r.accelerated?"The previous run_js returned no signal (an error, or an empty value), so more scripting of the same shape will not produce evidence. ":"";return{response:{status:"error",error:`This is the ${r.count===2?"2nd":r.count===3?"3rd":`${r.count}th`} consecutive run_js on step ${n} and it was NOT executed \u2014 stop scripting the DOM to find text; the criterion must be graded from what is visible \u2014 take a screenshot or snapshot and grade honestly, including an honest fail. ${s}If the step asks you to ENTER text, type it into the field the step names rather than searching the page for it; if the step asks you to CHECK something, look at the page and report what you actually see.`,runJsHuntRefused:!0,consecutiveRunJs:r.count},supervisorEntry:{action:e.name,intent:"not dispatched to the browser",skipped:"meta",skippedReason:"run_js_hunt_refused"}}}noteRunJsHuntOutcome(e,n){if(ve("RUNNER_RUNJS_HUNT_BREAKER")||e.name!=="run_js")return;let r=this._currentStepIndex;typeof r=="number"&&this._runJsHuntBreaker.noteOutcome(r,n.response)}noteSameValueWriteOutcome(e,n){if(ve("RUNNER_SAME_VALUE_WRITE_BREAKER")||!Ei(e.name))return;let r=this._currentStepIndex;if(typeof r!="number")return;let s=n.response,a=s?.valueAfter,i=s?.typedIntoField;typeof a!="string"&&typeof i!="string"||this._sameValueWriteBreaker.noteWriteOutcome(r,zc(e.args),{...typeof i=="string"?{resolvedField:i}:{},...typeof a=="string"?{valueAfter:a}:{}})}enforceVerbatimInput(e){if(!Es()||e.name!=="type_text_at"&&e.name!=="set_focused_input_value")return;let n=this._currentStepIndex;if(typeof n!="number")return;let s=this._activeTestPlan?.steps[n-1]?.verbatimInput;if(typeof s!="string"||s.length===0)return;let a=e.name==="type_text_at"?"text":"value",i=typeof e.args?.[a]=="string"?e.args[a]:"",o=this.verbatimWriteTargetKey(e);if(this._verbatimPinStepIndex!==n)this._verbatimPinStepIndex=n,this._verbatimPinTargetKey=o,this._verbatimPinElementFingerprint=null;else if(!(o!==null&&o===this._verbatimPinTargetKey))return;i!==s&&(this._verbatimPinPendingOverrideNote=!0),e.args={...e.args??{},[a]:s}}async reconcileVerbatimPinAfterWrite(e,n,r){if(!Es()||e.name!=="type_text_at"&&e.name!=="set_focused_input_value")return;let s=this._currentStepIndex;if(typeof s!="number"||this._verbatimPinStepIndex!==s)return;let i=this._activeTestPlan?.steps[s-1]?.verbatimInput;if(typeof i!="string"||i.length===0)return;let o=typeof n.writeElementFingerprint=="string"?n.writeElementFingerprint:void 0,l=e.name==="type_text_at"?"text":"value";if((typeof e.args?.[l]=="string"?e.args[l]:"")===i){this._verbatimPinElementFingerprint===null&&Yb(o)&&(this._verbatimPinElementFingerprint=o??null),this.annotateVerbatimOverrideIfPending(n);return}if(!Yb(o)||o!==this._verbatimPinElementFingerprint)return;this.log("info","RunnerRuntime","verbatim_pin_reapplied",{stepIndex:s,via:e.name,reason:"same resolved element re-addressed; re-typing pinned payload"});let d=await this.executeAction({name:"set_focused_input_value",args:{value:i,expectedElementFingerprint:this._verbatimPinElementFingerprint}},r);if(d?.writeGuardMismatch){this.log("info","RunnerRuntime","verbatim_pin_reapply_skipped",{stepIndex:s,via:e.name,reason:"focus moved off pinned element before corrective (blur guard)"});return}d?.response&&(n.response=d.response,d.screenshotBase64&&(n.screenshotBase64=d.screenshotBase64),d.loopDetectorUpdate&&(n.loopDetectorUpdate=d.loopDetectorUpdate)),this._verbatimPinPendingOverrideNote=!0,this.annotateVerbatimOverrideIfPending(n)}annotateVerbatimOverrideIfPending(e){if(!this._verbatimPinPendingOverrideNote)return;this._verbatimPinPendingOverrideNote=!1;let n="This step carries a pinned verbatim input; the exact required text was typed instead of the provided draft. Treat the field value as correct \u2014 do not re-type it.",r=e.response;r&&typeof r=="object"&&(r.note=typeof r.note=="string"&&r.note.length>0?`${r.note} ${n}`:n)}resetVerbatimPinTracking(){this._verbatimPinStepIndex=null,this._verbatimPinTargetKey=null,this._verbatimPinElementFingerprint=null,this._verbatimPinPendingOverrideNote=!1}maybeRejectOffPlanLabelClick(e){if(e.name!=="click_at")return null;let n=typeof e.args?.label=="string"?e.args.label.trim():"";if(!n)return null;let r=this.activeClickStepConstraint();if(!r)return null;let{stepIndex:s,expectedControls:a,forbiddenControls:i}=r;if(this.recordStepClick(s,n,a),!this.matchesForbiddenControl(n,i))return null;let o=a.length>0?a:i,l=`Step ${s} instructs interacting with ${o.map(c=>`"${c}"`).join(" or ")}. You targeted "${n}". If the instructed control is absent or the step is impossible on the current page, the step must FAIL (report it failed in run_complete with a note), or call propose_update \u2014 do NOT substitute a different control or flow.`;return this._stepDeviationRejectedOnce.has(s)?(this.recordStepDeviation(s,`clicked "${n}" but step ${s} instructs ${o.map(c=>`"${c}"`).join(" or ")}`),null):(this._stepDeviationRejectedOnce.add(s),this.log("warn","RunnerRuntime","step_deviation_rejected",{stepIndex:s,expectedControls:a,forbiddenControls:i,attempted:n}),{response:{status:"step_deviation_rejected",expectedControls:a,attempted:n,instruction:l},isMetaTool:!0,supervisorEntry:{action:e.name,intent:"not dispatched to the browser",skipped:"meta",skippedReason:"step_deviation_rejected"}})}recordOffPlanResolvedClick(e,n){if(e.name!=="click_at"||typeof e.args?.label=="string"&&e.args.label.trim())return;let r=this.activeClickStepConstraint();if(!r)return;let{stepIndex:s,expectedControls:a,forbiddenControls:i}=r,o=typeof n.response?.clickTarget?.accessibleName=="string"?n.response.clickTarget.accessibleName.trim():"";if(!o){n.response?.clickTarget&&a.length>0&&this._stepHadUnnamedClick.add(s);return}if(this.recordStepClick(s,o,a),!this.matchesForbiddenControl(o,i))return;let l=a.length>0?a:i;this.recordStepDeviation(s,`clicked "${o}" but step ${s} instructs ${l.map(d=>`"${d}"`).join(" or ")}`);let c=`Off-plan: this click resolved to "${o}", but step ${s} instructs ${l.map(d=>`"${d}"`).join(" or ")}. If the instructed control is absent or the step is impossible, FAIL the step in run_complete \u2014 do NOT substitute a different control or flow.`;if(n.response&&typeof n.response=="object"){let d=typeof n.response.instruction=="string"?`${n.response.instruction} `:"";n.response.instruction=`${d}${c}`}}maybeInteractionClearsPresenceOracle(e,n){if(!aa())return;let r=e.name==="click_at",s=e.name==="type_text_at"||e.name==="set_focused_input_value";if(!r&&!s||this._verifyOracleFailureStepIndexes.size===0)return;let a=this._currentStepIndex;if(a==null)return;let i=n.response;if(!i||typeof i!="object"||i.status==="error"||Ii(i)||n.noObservedEffect===!0)return;let o=[];if(r){let l=i.clickTarget;l&&ws(l)&&(typeof l.accessibleName=="string"&&l.accessibleName.trim()&&o.push(l.accessibleName),typeof l.text=="string"&&l.text.trim()&&o.push(l.text));let c=i.clickedElement;c&&typeof c.text=="string"&&c.text.trim()&&o.push(c.text)}else{let l=i.typedIntoField;typeof l=="string"&&l.trim()&&o.push(l)}if(o.length!==0)for(let l of[...this._verifyOracleFailureStepIndexes]){if(l>a||a-l>1)continue;let c=this._verifyOracleFailureDetails.get(l);if(!c||c.action===dr||!Zr(c.action))continue;let d=c.literal?.trim();if(!d||!$i(d))continue;let u=o.find(p=>Qn(p,d));if(!u)continue;let f=this._activeTestPlan?.steps[l-1];if(!!f&&(ur(f.text,d)||(f.criteria??[]).some(p=>ur(p.check,d)))){this.log("warn","RunnerRuntime","interaction_clears_presence_oracle:absence_skip",{failStep:l,currentStep:a,literal:d,matchedIdentity:u});continue}if(!Jne()){this.log("warn","RunnerRuntime","interaction_clears_presence_oracle:would_clear",{failStep:l,currentStep:a,action:e.name,oracleAction:c.action,literal:d,matchedIdentity:u});continue}this._verifyOracleFailureStepIndexes.delete(l),this._verifyOracleFailureDetails.delete(l),this.log("warn","RunnerRuntime","interaction_clears_presence_oracle:cleared",{failStep:l,currentStep:a,action:e.name,oracleAction:c.action,literal:d,matchedIdentity:u})}}recordStepDeviation(e,n){let r=this._stepDeviations.get(e)??[];r.push(n),this._stepDeviations.set(e,r),this.log("warn","RunnerRuntime","step_deviation_recorded",{stepIndex:e,description:n})}bookStepCompletionDeviations(){if(!ve("PLAN_OBEDIENCE"))for(let[e,n]of this._stepClickedControlNames){if(n.length===0)continue;let r=this._stepExpectedControls.get(e)??[];if(r.length===0||this._stepHadUnnamedClick.has(e)||n.some(a=>Kh(a,r)))continue;let s=[...new Set(n)].map(a=>`"${a}"`).join(", ");this.recordStepDeviation(e,`step ${e} instructs ${r.map(a=>`"${a}"`).join(" or ")} but no click landed on it (clicked ${s})`)}}currentPlanStepIndex(){return this._currentStepIndex??void 0}currentPlanStepType(){let e=this._currentStepIndex;if(!(!e||!this._activeTestPlan))return this._activeTestPlan.steps[e-1]?.type}currentPlanStepText(){let e=this._currentStepIndex;return this._currentStepText??(e?this._activeTestPlan?.steps[e-1]?.text:void 0)??void 0}planControlLabelsForAction(){if(Vt("LABEL_DRIFT_DISCLOSURE"))return;let e=ND(this.planStepTextForDrift());return e.length?e:void 0}planStepTextForDrift(){let e=this._currentStepIndex;return typeof e!="number"?"":this._activeTestPlan?.steps[e-1]?.text??""}maybeUseAutoCapturedVerifyEvidence(e){let n=e.name;if(!this.isVerifyStep(this._currentStepIndex))return null;let r=this._currentStepIndex;if(!this._autoVerifyEvidenceStepIndexes.has(r)||!Al(n))return null;let s=this._activeTestPlan?.steps[r-1],a=s?Nl(s):!1,i=this._stableVerifyPageSnapshot??"";if(!kne(n))return this.log("info","RunnerRuntime","auto_verify_evidence:stand_down",{stepIndex:r,action:n,reason:"settle_action",ephemeralEvidenceStep:a}),null;if(this._consecutiveBrowserShortCircuitStep===r&&this._consecutiveBrowserShortCircuits>=yne)return this.log("info","RunnerRuntime","auto_verify_evidence:stand_down",{stepIndex:r,action:n,reason:"consecutive_short_circuit_cap",consecutive:this._consecutiveBrowserShortCircuits,ephemeralEvidenceStep:a}),null;a||this._autoVerifyEvidenceStepIndexes.delete(r);let o=this._currentStepText??this._activeTestPlan?.steps[r-1]?.text??"",l=this._stableVerifySnapshotStep;return{response:{status:"ok",note:l&&l!==r?`Stable page snapshot reused from step ${l}. The page state is unchanged \u2014 use it to evaluate: ${o}`:`Matching evidence for step ${r} was already captured in the previous action result. Use that evidence for: ${o}`,...i?{pageSnapshot:i}:{}},isMetaTool:!0,resetLoopDetector:!0,supervisorEntry:{action:n,intent:"reused retained verify snapshot (no browser dispatch)",skipped:"meta",skippedReason:"auto_verify_evidence"}}}writeVerifyOracleFailure(e,n,r,s,a){this._verifyOracleFailureStepIndexes.add(e),this._verifyOracleFailureDetails.set(e,{action:n,error:r,literal:typeof s=="string"&&s.trim().length>0?s:void 0,timeoutMs:Fre(r),via:a}),this.log("warn","RunnerRuntime","verify_gate:oracle_failure_recorded",{stepIndex:e,action:n,via:a})}recordVerifyEvidenceFromToolResult(e,n,r,s=!1){let a=typeof e.groundingSnapshot=="string"?e.groundingSnapshot:typeof e.response?.pageSnapshot=="string"?e.response.pageSnapshot:"",i=e.groundingSnapshotKind,o=typeof e.groundingCanvasDominant=="boolean"?e.groundingCanvasDominant:this.lastCanvasDominant,l=Gre(e.loopDetectorUpdate?.url),c=e.loopDetectorUpdate?.url??this._lastObservedActionUrl??void 0,d=e.captureMode;if(this.isVerifyStep(this._currentStepIndex)&&Al(n)){let m=this._currentStepIndex,g=aa(),v=Zr(n),w=s0(n),E=Ii(e.response),R=!s&&n==="wait_for_element"&&E&&o,_=!s&&!R&&(g?v&&E:n==="wait_for_element"&&typeof e.response?.error=="string");if(s||R)R&&!this._retainedFrameAbstainedSteps.has(m)&&(this._retainedFrameAbstainedSteps.set(m,{literal:typeof r=="string"?r:"",capturedAtMs:Date.now(),sourceStep:m,frameAgeMs:0}),this.log("warn","RunnerRuntime","verify_gate:oracle_failure_abstained",{stepIndex:m,action:n,reason:"canvas_dominant",literal:typeof r=="string"?r.slice(0,120):""}));else if(_)g&&this.writeVerifyOracleFailure(m,n,jb(e.response),r,"dispatch");else if(!(g&&w&&E)){if(n==="run_js"&&Kv(e.response)&&this.log("info","RunnerRuntime","verify_evidence:runjs_empty_would_withhold",{stepIndex:m}),this._verifyEvidenceStepIndexes.add(m),this._verifyObservationSeq+=1,this._lastVerifyObservationSeqByStep.set(m,this._verifyObservationSeq),this._verifyMarkerIsolationBlockCounts.delete(m),this._verifyEvidenceGateBlockCounts.delete(m),g&&w){let b=this._verifyOracleFailureDetails.get(m);(!b||b.action!==dr)&&(this._verifyOracleFailureStepIndexes.delete(m),this._verifyOracleFailureDetails.delete(m)),this._retainedFrameAbstainedSteps.delete(m)&&this.log("info","RunnerRuntime","auto_verify_evidence:retained_frame_abstain_cleared",{stepIndex:m,action:n})}}this.checkPinPageGrounding(m,a,i,o,d),this.rememberFullSnapshotForStep(m,a,i,o,l),this.maybeRecordUrlCandidate(m,c)}let u=this._currentStepIndex?this._currentStepIndex+1:null,f=u?this._activeTestPlan?.steps[u-1]:void 0,h=typeof e.response?.pageSnapshot=="string"?e.response.pageSnapshot:"";u&&f&&Cl(f)&&!this.isVerifyStep(this._currentStepIndex)&&(this.checkPinPageGrounding(u,a,i,o,d),this.rememberFullSnapshotForStep(u,a,i,o,l),this.maybeRecordUrlCandidate(u,c));let p=k_(n)&&this.isVerifyStep(this._currentStepIndex);if(u&&f?.type==="verify"&&!p&&ere(h,f))if(_l())this.log("info","RunnerRuntime","verify_fold:transient_premark_deferred",{nextStepIndex:u});else{this._verifyEvidenceStepIndexes.add(u),this._autoVerifyEvidenceStepIndexes.add(u),this.retainVerifyFrame(h,this._currentStepIndex??null,[u]);let m=`Captured matching evidence for upcoming verify step ${u}: ${f.text}. Use this evidence for the verify step before changing the form state.`;e.response={...e.response,verifyEvidence:m,pageSnapshot:`${h}
888
888
 
889
889
  [Verify evidence]