Decisions #
Record of choices. Status: assumed = a default chosen in the plan, stands unless Alex changes it; decided = confirmed. Change a decision by editing its row and adding a dated note below it.
| # | Decision | Choice | Status | Needed by | Alternatives and trade-offs |
|---|---|---|---|---|---|
| D1 | Script format | Line-based steps (see design/script-format.md) plus optional TypeScript for custom checks (call) |
assumed | Phase 2 | TypeScript functions only: no parser to write, but harder for Qian and the confirm page to read, and diffs are noisier. |
| D2 | Driver | MV3 Chrome extension; the runner talks to it through an interface so a Playwright backend can be added | assumed (Alex prefers the extension) | Phase 1 | Playwright first: can answer passkey prompts via a virtual authenticator and runs headless in CI, but is not "the browser the user has" and needs a separate session story. |
| D3 | Language and tooling | TypeScript, esbuild, vitest, no UI framework | assumed | Phase 0 | Plain JS: fewer tools, weaker refactoring. |
| D4 | Where it runs | Headed Chrome with a persistent profile on balrog; also runnable on a laptop |
assumed | Phase 5 | Headless in CI: cannot hold a real signed-in profile safely. |
| D5 | Bridge | Local Node process, websocket on 127.0.0.1, per-run token | assumed | Phase 1 | Chrome native messaging: no port, but harder to start and debug. |
| D6 | Results | run.json + screenshots on disk, static HTML report, verdict buttons in a small local page |
assumed | Phase 4 | A hosted dashboard: more to run, more to secure. |
| D7 | Repo | This repo (root/aite); BITE kept as archive and reference; new code beside it |
assumed | now | A fresh repo with BITE as a submodule. |
| D8 | Large files | The two zips (84 MB) are in git history. Options: Git LFS, rewrite history to drop them and keep them in an artifact store, or leave. | decided 2026-10-09: leave the zips in place (option A) | Phase 0 | LFS: needs the GitLab server's LFS enabled. Rewriting history: breaks existing clones (only two exist). Leaving: every clone is 160 MB. |
| D9 | Test accounts | Dedicated throwaway Bluesky accounts and a testnet wallet; sessions stay in the Chrome profile on balrog |
assumed | Phase 5 | Using Qian's or Alex's accounts: risks real data and sign-outs. |
| D10 | License | Apache 2.0, with a NOTICE crediting Google's BITE | assumed | Phase 0 | |
| D11 | Real input | click and type are done with trusted input: mouse and key events sent by the runner over Chrome's private debugging pipe (--remote-debugging-pipe), not by the extension. Downloads are saved to a folder the runner chooses per run. |
decided 2026-10-09 (measured in P1-5) | Phase 2 | Plain events from the extension: the wallet pay pop-up is blocked and React ignores some value writes (table below). --disable-popup-blocking: hides the problem. chrome.debugger in the extension: needs the debugger permission and shows an infobar. A Playwright backend (D2): same capability, much more to run. |
| D12 | Chrome and the profile | Chrome for Testing 155.0.8059.39, pinned in package.json (npm run chrome:install, path from npm run chrome:path). The runner starts it with --load-extension from a folder inside the persistent profile (<userDataDir>/aite-extension/, rebuilt each run), writes the per-run token to bridge.json there, opens a private debugging pipe, and stops it by asking it to quit (Browser.close), never by signal. |
decided 2026-10-09 (measured in P1-6) | Phase 5 | Branded Chrome: not installed here, so not measured; its documented loss of --load-extension (Chrome 137) would force the fallback below. Chromium: not installed. A fresh extension folder each run: measured, a new extension id and a leftover entry in the profile every run. Attaching to a Chrome that is already running: no debugging pipe, so no trusted input (D11) and no screenshots. |
| D13 | Self-check driver | The CI self-check runs the fixture scripts through aite run itself (the pinned Chrome for Testing, the real extension, trusted input), not through Playwright. Each fixture script says what it must do in a # expect: line, and the check compares. A deliberately broken build (--sabotage COMMAND) must make it fail. |
decided 2026-10-10 (P2-4) | Phase 2 | Playwright persistent context (what PLAN.md section 8 allowed): a second browser driver to install, pin and keep working, that would test a different launch path from the one aite run uses (flags, profile, pipe, extension folder), so a break in the real path could stay green. |
| D14 | Verify forms and permissions | (1) verify text: is exact and case-sensitive; the explicit loose forms are text-i: (whole text) and text~i: (a part), while text: and text~: in a click/type target stay loose (a person finds a button by its label whatever its casing). (2) verify TARGET attr:NAME equals:/contains: compares an attribute; for href and src the written and the resolved address both count. (3) verify download name:/name~: size-min: is answered by the runner from the run's download folder (results/<run id>/downloads/, set with Browser.setDownloadBehavior over the debugging pipe), so it needs no extension permission. (4) wait network-idle stays in v1: it is answered from Chrome's network events over the same pipe, also with no permission. (5) The permission allowlist stays tabs, scripting, alarms, storage; a test checks the extension's code uses exactly those gated APIs and none of debugger, downloads, webRequest or cookies. (6) pre-state is added to the protocol's reasons as reserved for P5-7. |
decided 2026-10-10 (P2-5) | Phase 2 | The downloads permission (reads the browser's list but not this run's files, and widens what the extension can see); chrome.debugger in the extension (the permission P1-1 forbids); cutting network-idle (it works without a permission, and the journeys need it). |
Notes #
-
2026-10-09, D8 measurements (P0-1): a fresh clone of
root/aiteis 160 MB (pack 78.6 MiB; the two zips are also in the working tree, 84 MB). Both zips are copied to a private backup folder (mode 0700) with checksums inSHA256SUMS; the repo copies match. GitLab reportslfs_enabled: true, butgit-lfsis not installed on this machine. Options and effect:Option Fresh clone Risk A. Leave as is 160 MB none; every clone carries 84 MB of archive twice (history + tree) B. git rmthe zips from the tree, keep history (recommended)about 79 MB none; nothing is rewritten, the zips stay in history and in the off-repo backup C. Rewrite history to drop the zips a few MB breaks both existing clones (Alex's and this one); needs a force-push D. Git LFS small needs git-lfs installed; the existing history still holds the zips unless also rewritten Nothing has been rewritten or removed. The decision needs an explicit yes from Alex. Until then P0-3 reads the archive from the working tree. -
2026-10-09, D9 to D10: unchanged.
-
2026-10-09, D8 decided (Alex): leave both zips where they are. No LFS, no history rewrite, no
git rm. Every clone stays about 160 MB; accepted. -
2026-10-09, D11 measurements (P1-5), Chrome for Testing 155, headless, on fixtures in
fixtures/spike/(real React 19 for the input). Tests:test/functional/spike.activation.test.ts.What Plain events from the extension (today's click)Trusted input over the debugging pipe Button whose handler calls window.open(the pay pop-up)blocked: the click succeeds, the page reports "blocked", no window opens opens; the extension's tabslist grows by oneDownload link ( <a download>)works: file with the right name, size and bytes works Download that script builds (Blob and hidden link) works works React input, value write in the extension's world state updates, but no key events fire (key handlers never run) not applicable React input, value write in the page's own world ignored by React (the input shows the text, the state is empty) not applicable React input, native-setter trick in the page's own world works, no key events not applicable Typing into a React input not applicable state equals the text; every key press seen (5 keys, 5 events) Typing accents and other scripts not applicable works, inserted as text Enter not applicable reaches the page as a key press --disable-popup-blockingwith a plain clickthe pop-up opens not applicable Why trusted input: the pay pop-up is the Phase 6 shop journey and cannot be opened without a user gesture, so plain events cannot pass it. Downloads need no gesture, but the same mechanism works for them. For typing, only trusted keys give a person's sequence of key events, which input masks, validators and Enter handlers depend on; value writes cover only part of it and behave differently depending on which JavaScript world they run in. One mechanism for every click and every keystroke is simpler to reason about than a mix. A trusted click also behaves like a person's: it lands on whatever is at that point, so a button under an overlay is not clicked, and a point outside the viewport does nothing. That is a feature (it finds real bugs) and it sets requirements below.
Why not the other ways:
--disable-popup-blockingmakes a plain click open the window, but then a test passes where a person is blocked, which is the failure this tool exists to catch.chrome.debuggeris the extension's own route to the same events, but it needs thedebuggerpermission (full control of any tab it can see), shows "aite started debugging this browser" on the window, and cannot be attached while DevTools is open on the tab; this was reasoned from Chrome's documented behaviour, not measured here. The debugging pipe costs no extension permission and opens no port.Permission cost: none. The manifest stays at
tabs,scripting,alarms,storageand the allowed hosts (P1-1's list is unchanged). The pipe exists only when the runner starts Chrome itself, so P1-6 must keep launching Chrome from the runner rather than attaching to one that is already running.What this requires of Phase 2 (P2-3):
act clickin the extension becomes a locate step: scroll the element into view, check it is visible and enabled, hit-test the centre (elementFromPointis the element or inside it; otherwise failnot-visiblenaming what covers it), and answer with its rectangle in viewport CSS pixels. The runner then sends the trusted click at the centre. Frames need their offsets added, which is not covered yet.typefocuses by a trusted click, selects all (Input.dispatchKeyEventwithcommands: ["selectAll"], measured: typing alone appends), then sends trusted keys (runner/input.tshastrustedClick,trustedTypeandallowDownloads, tested).- The runner sets the download folder for each run (
Browser.setDownloadBehavior) and waits for the file, so nothing lands in the profile's Downloads.
Not covered by this spike (stated, not hidden): headed Chrome, iframes and shadow DOM, drag and drop, IME composition, and several downloads in a row (Chrome may ask about "multiple automatic downloads").
-
2026-10-09, D12 measurements (P1-6), balrog, Chrome for Testing 155.0.8059.39, headless. Tests:
test/functional/profile.chrome.test.ts,launcher.test.ts,profile.test.ts,chromePath.test.ts.What Measured Flavours installed on balrog Chrome for Testing 155 only. No branded Google Chrome, no Chromium. --load-extensionat launch in a persistent profileworks; extension enabled, no errors; connects with that run's token (5 runs of 5) Extension folder: a fresh one each run a new extension id every run, and a new entry left in the profile's Preferences each run (3 runs, 3 entries) Extension folder: one stable folder inside the profile one entry, the same id every run, and the worker's own storage persists (its start counter went 1 then 2) Cookies and local storage across runs lost when Chrome was signalled 300 ms after SIGTERM; kept when asked to quit with Browser.close(it exits by itself in about 0.17 s, code 0)A second Chrome on a profile that is open exits in about 0.1 s with code 21, "Failed to create SingletonLock". The lock is a dangling symlink to host-pid, so a plain "does it exist" check misses itThe lock after a Chrome that crashed (SIGKILL) still there, but names a dead process; the next run is not blocked Token route bridge.jsonin the extension's folder, read by the extension as a packaged file at each connect: each run's new token was the one used; a bridge whose token the extension was not given was refused (exit 3 after the wait)Extensions.loadUnpackedover the debugging pipe, no--load-extensionworks, with or without --enable-unsafe-extension-debugging; the service worker runsDeveloper mode not needed to load at launch; needed only to reload a running extension (P1-1, P1-2 findings), which the runner never does navigator.webdrivertruebecause of the debugging pipe;falsewith--disable-blink-features=AutomationControlled(not applied)Why Chrome for Testing: it is the only flavour here, it is a pinned and reproducible binary, it keeps
--load-extension, and it never updates under the tests. The price: it is not the browser a person uses. Anything that depends on branded Chrome (Widevine, Google sign-in, platform passkey authenticators, Google's own updater) is not covered, and here it needs--no-sandbox. If a site the journeys need rejects it, Phase 5 will find out first.Why a stable folder inside the profile: one extension entry, a stable id, and the worker's storage survives. The token file is the only thing that changes between runs, and it is removed afterwards.
Why the polite quit: a persistent profile exists to keep signed-in sessions between runs, and a signalled Chrome loses them.
launchChrome().close()now asks over the pipe and waits up to 8 s, signals (SIGTERM, then SIGKILL) only if Chrome does not go, and then sweeps up helper processes.What the runner now does about it: refuses to start when another Chrome has the profile open (naming the process) instead of watching a second Chrome die; creates the profile folder private (mode 700, it holds sessions); deletes the token file when the run ends.
If a flavour drops
--load-extension(branded Chrome from version 137, per Chrome's documentation, not measured here): load withExtensions.loadUnpackedover the pipe instead. It works in 155 as measured above; on branded Chrome it may need--enable-unsafe-extension-debugging, which was not tested.Not covered by this spike (stated, not hidden): a headed persistent profile with real signed-in accounts (Phase 5), branded Chrome and Chromium, other operating systems, and two runs on different profiles at the same time.
-
2026-10-10, D11 in use (P2-3), Chrome for Testing 155, headless. Measured while building and testing the commands (
test/functional/commands.e2e.test.ts): trusted mouse and keys work for click, double click (click counts 1 and 2), typing after select-all, Enter/Tab/Arrow keys, check/uncheck, and a mouse drag that the page handles withmousedown/mousemove/mouseup; a button 1800 px down the page is scrolled to and clicked; a button under an overlay is refused (and nothing is clicked); a pay window opened by a real click is switched to and closed. Not measured, not claimed: HTML5 drag and drop (draggable,dragstart,drop), iframes and shadow DOM (the click point needs frame offsets), a page zoom other than 100 percent.wait network-idleuses Chrome's Network events on a session kept attached for the run; Chrome keeps a fetch open until its response body is read, so a page that never reads a response is not idle, as in DevTools. -
2026-10-11, P3-1 (lock file), no new D-number. Three choices made while building it: (1) a lock file that exists but cannot be used is never overwritten, because a person committed it and a silent rewrite would hide a real change; (2) the lock file is written also when the run fails after finding some targets, since the descriptor of a target that resolved is true whatever happens later; (3) descriptors are compared only by the shape in
shared/descriptor.ts, so an older extension that sends the short form is skipped rather than half-stored. Found on the way: a regex ending in\uffffwas written by the bundler as that raw character and Chrome refused the whole content script ("It isn't UTF-8 encoded"); the build now emits ASCII only (charset: "ascii") and a test checks the built files byte by byte. -
2026-10-11, P3-4 (repair scoring), how the held-out set was used. The first model had an evidence floor of 60 and counted a name only when the role matched. I wrote the held-out cases (
test/corpus/repair/holdout.json) after fixing the weights, from what a person would say, and ran them once. Two failed in a way that showed a defect, not a threshold: a text field matching on id, name attribute and placeholder scored 0.84 because of the floor (now 50), and a link turned into a button with the same words scored 0.53 because the name was ignored when the role differed (the name now counts whatever the role; the role has its own weight). Those two cases were moved totuning.json(they were used), the model was changed, and four fresh held-out cases were written before being run. Two of those four fell short of what a person would say (a restyled and moved search box gotdriftedat 0.745 instead ofrepaired; a button reworded to "Place order" with a new id gotnot-foundat 0.444 instead ofdrifted). They were not tuned for: they are recorded asknownMisswith the reason, and a test requires that every known miss is in the safe direction (confirm or nothing found, never a repair) and that no held-out case ever repairs to the wrong element. Held-out result: 6 of 8 as a person expected, 0 wrong repairs. Honesty note: I wroteshared/repairScore.tsbefore its tests; the tests take their oracle from locator-repair.md and a hand calculation (55/61), not from the code. -
2026-10-11, P3-2 / P3-3 / P3-5 (repair, drift fixture, freeze), choices worth knowing. (1) A match by a remembered field (data-testid, id, aria-label, name) is reported as
repaired, notfoundas the first draft of locator-repair.md said: if the script's own target found nothing, something changed, and a unique id or test id would otherwise let a reworded control pass unseen (adversarial-review F-03 and F-04). (2) Repair is tried only afternot-found; an ambiguous or hidden exact match is a problem with the script or the page, not something to guess at. (3) At most 2000 elements are looked at and the best 60 by a cheap comparison are described in full; the first version described all of them and took 89 s for 60 elements on a list of 5000 siblings, becausexpathOflisted the parent's children (it now walks siblings). (4) A wrapper with exactly the same words as an element inside it is dropped as a candidate: Chrome showed adivaround the control scoring the same as the control and making every drifted case ambiguous (jsdom had hidden it because the wrapper's neighbours were the control's old neighbours). (5) The drawn outline of a drifted candidate is P4-2 (its own spec says so); P3-3 delivers the candidate with its score in the report and run.json. (6) Under--freezea target the lock file does not know fails asunlockedwithout asking the page; the lock store is read only. -
2026-10-11, P4-1 / P4-2 (report and outline), choices. (1) The report has no script at all and a content security policy that allows none, instead of escaping and hoping: text from the page under test lands in step names, found values and labels, and an injection test (a property test over arbitrary strings plus a table of known attack strings) checks it comes out as text. (2) Pictures of failed and repaired steps are embedded as data URIs (checked to be PNG, at most 4 MB each); passing steps link to their file in shots/, so the report stays one readable file without growing with every step. (3) The outline is drawn by the extension as a temporary node of our own, not through Chrome's debugging Overlay domain, because it needs a label and the page must be asked for the element's rectangle anyway (P1-4 note). It is removed right after the picture, and removes itself after 15 s; the runner warns if any is left. (4) run.json carries
saw(the lines the terminal prints under a failure) so a report needs nothing but run.json to explain a failure. (5)shared/runjson.schema.jsonrejects unknown fields; the failure diagnosis object is only checked to be an object (its own schema is future work). -
2026-10-11, P4-3 / P4-5 / P4-6 (verdicts), choices and findings. (1) The verdict page is its own small local server (
aite verdict), not part of the script-free file report: it listens on 127.0.0.1 only, needs a per-session token (in the address for the page, in a header for the verdict), refuses a foreign Host (DNS rebinding), a foreign Origin and a cross-site request, takes a 16 KB JSON body about a known verdict on a step, serves two routes, has one script under a fresh CSP nonce, and cannot be framed. (2) The verdict is always written first, whatever GitLab says, and one attempt is made per click (no automatic retry); a click after a failure tries again. Two clicks at once wait for each other; two programs find each other's issue by its hidden key. (3) Token role. GitLab has no issues-only token scope. A project token with the Guest role is the narrowest that can still create an issue and upload a picture; it can close and delete issues it wrote itself (checked: 204) but not an admin's issue (403), nor delete the project or change its settings (403). A Reporter token was tried first and replaced. A real project needs the same: a Guest project token, and a reviewer should know it can delete its own issues. (4) The throwaway project is root/aite-verdict-tests (id 457, private); every verdict test's fetch goes through a guard that refuses any other project; its token lives in ~/.aite-verdict-test-token (mode 600) and in the CI variable AITE_TEST_GITLAB_TOKEN (masked); it expires in 90 days and issue 7 in the project is an admin-owned fixture that must stay open. (5) Flaky reruns the whole script from its start and nothing else; a rerun uses --freeze when the script has a lock file (so measuring never rewrites it) and --no-lock when it has none. Cleaning the pre-state of accounts is P5-7. -
2026-10-11, P5-1 / P5-3 (profiles and session-check), choices. (1) What makes a profile a real signed-in session is a setting on the profile,
account, not the script'sneeds sessionline or its folder: the rule "a script on it begins withcall session-check" then holds even for--profile NAMEand for scripts in any folder, and test profiles (no account) are untouched. (2) The check's logic is a pure function (runner/sessionCheck.ts) with a thin check module around it, so it is tested without a browser and against a real persistent profile with a cookie session (a script on a second profile entry with the same folder stands for a person signing in; deleting the profile's cookie files stands for expiry). (3) A page that asks to sign in is "signed out" even if it also shows the expected handle (a sign-in form that remembers who you were); the wrong account is only named when the page marks it as an account (an @name, or "as", "account:", "user:" before it), so unrelated domains on a page are not blamed. (4) Found:readcould not name the body, so a check could not read the whole page; the body is now a candidate for targets (a text target still prefers the innermost element). (5)aite profile openis a long-running command: it makes the folder private (mode 700, said when it had to change it), takesaite-launcher.lock(a second launcher is refused naming the first; a lock left by a dead process is taken over), refuses a Chrome that already has the profile open, and cleans up the token file, the bridge and the lock when Chrome closes or on Ctrl-C. (6) P5-1.b (the documented steps after a real reboot of balrog) is not done: nobody has rebooted; the runbook says so. -
2026-10-11, P5-6 (several profiles in one run), choices. (1) The modifier is
on:NAME, not theas:NAMEthe task text wrote:as:already names a value (as:balance) and a script using it must not change meaning. (2) One bridge, and so one extension connection, per profile (each Chrome's extension connects to its own bridge), instead of teaching one bridge to hold several connections and route: the protocol, the dispatcher, the reconnect and idempotency rules and their tests stay as they are; the runner holds a seat (bridge, Chrome, trusted input, screenshot) per profile and routes by theprofilethe engine puts on a request, which no browser ever sees. (3) The engine keeps a tab (and the tabstartopened) per profile, since tab numbers belong to a Chrome;startopens the default profile's, every other profile's first page is its owngoto. (4) Only the first profile's browser is observed (network, console, exceptions); a failure in another profile is diagnosed without that evidence and says why. (5) The lock file keeps the same target on another profile apart (text:\"Go\" on:bis its own entry), because the same words can be another element in another person's page. (6) "One profile disconnecting fails only its own steps" is read with the rule that has always held: the first failed command stops the test and the rest are skipped; what it means here is that the failure is that profile's command, asconnection-lost, named with its profile, the other profile's earlier steps keep their results, and nothing hangs. (7) A profile with anaccountneedscall session-checkas the first command done on it (agotobefore it is allowed, it only opens its page), for every profile the script uses. (8) Found while writing the gift flow:wait text-changesright after a click is racy (the page may have changed before the wait began); averify, which polls, is the right way to wait for a result. -
2026-10-11, page loads have a floor of 10 s (found by a CI flake). The self-check failed once in 20 on
capture.fail-missingwithfail timeout startinstead offail not-found capture: the script'stimeout 700msalso governed the page load, and a loaded machine took longer than that to open the page. 45 of the fixture scripts use a short header timeout (600 ms to 1 s) so that the failure they want is quick, and every one had the same exposure. The headertimeoutnow means how long to wait for elements and text;startandgotowait at least 10 s (more if the header says more; exactly the command's owntimeout:when it has one). Not changed: the same short timeout still applies to everything else, which is what those scripts are about. -
2026-10-11, P7-3 (scheduled runs), choices. (1) The one issue per failing script is found by the test's name, not the file path: the task text says both "keys by script path" and "renaming does not orphan the issue", and only a name that lives in the script survives a rename or a move. The cost: two scripts with the same
test "name"share an issue, so names must be unique. (2) An issue a person closed is left alone and the next failure opens a new one: reopening or commenting on something a person called done is the wrong kind of quiet. (3) A passing run does nothing at all (not even a comment on the open issue saying it is green again): a flaky test that passes between failures would otherwise fill the issue; the person closes it. (4)--freezealready needs a lock file (a target the lock does not know fails asunlocked), so a script must be run once interactively and its lock file committed before it is scheduled; the scheduler does not hide that. (5) The kill switch is checked before every script, so it stops a cycle after the script that is running, not in the middle of it; a person who needs the running script gone kills the process. (6) The budget stops the run by closing Chrome and writing a run.json with the test failed, no steps andstopped, because the engine reports a result only at the end; the step that was running is not named (a limit of the report, said in the issue). (7) A script that could not run at all (Chrome, config) opens no issue and makes the cycle exit 3, so the timer's own failure alert is the signal; an issue per broken machine would be noise. (8) The report is attached to the issue as an uploadedreport.html(GitLab serves uploaded HTML as a download, not a page) and may contain page text such as balances: the project must be private. -
2026-10-11, P7-1 (record mode), choices. (1) The recorder is injected over the debugging pipe (
Page.addScriptToEvaluateOnNewDocumentand aRuntimebinding), not through the extension: it survives navigations and other origins without a new message path from the extension to the bridge (which had no event sender yet), and no extension is loaded at all. The cost: it runs in the page's own world, so a hostile page could see it; recording is a person's own session, on pages they chose. (2) A draft has noverifylines and says so: a recorded script written by a machine from a person's clicks could only assert what it just saw, which is the kind of test that never fails. (3) A click on a field, a select or a checkbox is not recorded as a click; the change it makes is recorded instead, once (a click on the label of a checkbox used to give both). (4) The last resort target is the xpath, not the descriptor'scss, which is only the tag and classes and many elements share it. (5) Only trusted events count (a person's, not a script's): found by running it in a real Chrome. (6) The printer now writes a plain role word without quotes (role:button), as the documents do; both forms parse. -
2026-10-11, P7-2 / P7-5.c (
aite author), choices. (1) The model is a command the person gives (stdin to stdout), not a hard-wired client: there is no API key or model choice in this repository,claude -por any other tool works, and a test can use a node program. (2) The three rules of PLAN.md section 7 are checked by code, not asked for in the prompt: a weakened verify is the P6-8 comparison against the last script that was run; an invented expectation is any expected text that the journey does not contain (ignoring case and spacing; variables and numbers are not traced);callis limited to listed checks. The first two stop the loop and ask a person; parse errors and unlisted calls go back to the model. (3) The loop writes only the one script and never overwrites; the model's answer is text, so there is no tool for a page's instruction to reach, and a test (git status after a hostile page) shows nothing else changes. (4) Page text goes to the model inside one marked block that cannot be closed from inside; the sentence after the block says again that it is data. This lowers the risk; it does not remove it, which is why the output is a script a person reads. (5) Known gap: an expectation that is in the journey but used for the wrong thing passes the trace check; reading everyverifyagainst the journey (P7-2.b) is a person's job. (6) No real model was run for this task; that is manual QA M-07.