Skip to content

GitHub sign-in: device flow, read-only (build, units U0-U7) - #47

Draft
StepenkoAnatoli wants to merge 254 commits into
mainfrom
build/github-sign-in
Draft

StepenkoAnatoli wants to merge 254 commits into
mainfrom
build/github-sign-in

Conversation

@StepenkoAnatoli

Copy link
Copy Markdown
Owner

Draft: this PR builds the GitHub sign-in that the user approved on 2026-10-13. The design is docs/specification/github-sign-in.md (merged in #46).

How it works:

  • It uses the device flow with a read-only scope.
  • Tokens exist only in the app's vault.

The GitHub App is not registered yet (UD-1), so GITHUB_APP is empty and the feature reports "not configured". Nothing goes to github.com until U9 adds the constants.

What is in it

Unit Files State
U0 contracts src/main/github-app.ts, src/main/github-credential.ts, src/shared/github-account.ts, src/shared/params.ts, vault purpose frozen; red-first, 8 guard proofs
U1 wire client src/main/github-oauth.ts integrated after 4 review rounds; U1-R11 fixed by the lead
U2 account store src/main/github-account-store.ts integrated after 4 review rounds; U2-R15 pins ported by the lead
U3 reader and reasons src/main/github.ts, src/shared/github.ts, src/engine/application.ts (3 lines) ACCEPT
U4 renderer src/renderer/GitHubAccountPanel.tsx, src/renderer/github-text.ts, App.tsx, styles.css ACCEPT
U5 lifecycle service src/main/github-signin.ts ACCEPT after 4 rounds; one S4 open (U5-R18)
U6 main wiring src/main/index.ts ACCEPT; one S4 deferred (U6-R2)
U7 token invariant tests/github-signin-invariants.test.ts integrated; the lead fixed R17 and R18 after the two-round cap; R19 is a known limit
X2-1, X2-2 startup reconcile; src/main/vault.ts staging ACCEPT after their own fix rounds

How each unit was built and reviewed:

  • Each unit was reviewed refute-first.
  • Every new test was red before its fix and has a guard-removal proof.
  • The run ledger is docs/orchestration/2026-10-13-github-sign-in/RUN.md.

The branch also carries docs and tooling, but no app code:

  • the run folder;
  • the sub-agent dispatch adapter under docs/orchestration/smartrouter/, with its policy, ledger and calibration evidence.

Not done yet (why this is a draft)

  • U8: the Windows e2e (e2e/github-sign-in.spec.ts) has not been built.
  • fix-c1-c3: the wire's body phase does not observe the timeout signal. This is S3, reachable only through an injected fetcher; the production fetch is not exposed. The brief is ready (briefs/fix-c1-c3.md).
  • Still to come:
    • the re-verification of the U5 extra findings (T1-T8);
    • the integration review (spec, invariants, mutation, breaker);
    • the integration fix round (including the U5-R18 pin);
    • the docs unit.
  • U9 (the constants) waits on UD-1.
  • Auto-build Stages 6-9 (harden, audit, deliver, validate).

Merge rule (AGENTS.md):

  • every S1 and S2 is fixed and re-reviewed;
  • the Linux gate equals the 74-failure baseline;
  • Windows CI is green on the exact head;
  • no review thread is open.

Validation so far

  • Linux pinned gate on 683e9cd (after wave 2): typecheck, lint and build exit 0. 2138 tests ran, and the 74 failures are exactly the baseline set.
  • U6, U7, X2-1 and X2-2 landed after that gate. Each unit's own test files are green on the integration head. No full gate has run since 683e9cd.
  • This PR's Windows CI run is the first full Windows run of the integration head.

Remaining limits and risks

  • The app ships unconfigured until UD-1 is answered.
  • C-1..C-3 stay open until fix-c1-c3 lands.
  • The same awaited-cancel pattern at src/main/github-signin.ts:228 and src/main/github.ts:19,57,59 has not been judged yet. The integration review covers it.
  • U7-R19 is a known limit of the invariant suite.

🤖 Generated with Claude Code

StepenkoAnatoli and others added 30 commits October 13, 2026 11:29
…re the 16,000-character cut

What changed:
- src/main/github.ts: when the reader was given redact (main's vault.redact, U6 passes it), a file's decoded text is redacted whole, after the control-character check and before the 200-line and 16,000-character cuts. A redaction failure (REDACTION_UNAVAILABLE) falls to the existing catch and becomes GITHUB_UNAVAILABLE, the outcome src/main/engine.ts:60-61 gives it today. Without redact, nothing changes.
- tests/github-read.test.ts: a vault secret straddling the 16,000-character cut appears in no 8-character fragment of the output after main's final exact-value redaction, the excerpt is the cut of the redacted text, and line counts are unchanged; a redaction failure is GITHUB_UNAVAILABLE.
- docs/ARCHITECTURE.md: the U3 sentence gains the pre-redaction clause.
Why: auto-build Stage 5, unit U3, commit 3 of 3 (BREAKDOWN U3 row; spec 7.3, S-2, X-3). Private repositories are where secrets live; exact-value redaction cannot match a prefix the cut leaves, and the research document reader already keeps this invariant. It is a commit of its own so it reverts alone: the redact parameter was declared in commit 2, so a revert here leaves the constructor U6 will call unchanged.
What it touched: src/main/github.ts (the file branch of read()), tests/github-read.test.ts, docs/ARCHITECTURE.md. Requirements S-2 (X-3), T-5.
What you verified:
- verified: red first. With src/main/github.ts at commit 2 (stashed), both new tests failed: the output contained a fragment of the secret ("not to contain 'ghu_...'"), and the failing redactor's read resolved instead of rejecting. After the change, tests/github-read.test.ts 66 of 66.
- verified: guard removals, file restored and sha256-checked each time: X1 the pre-redaction line removed (both tests red); X2 redaction moved after the cut, onto the excerpt only (the straddle test red, a fragment found).
- verified: npm run typecheck, npm run lint, npx eslint e2e exit 0; tests/github-text.test.ts 23 of 23, tests/github-token-literals.test.ts 2 of 2, tests/github-app-constants.test.ts 4 of 4, tests/folder-free-chat.test.ts 13 of 13.
- verified after commit, recorded in the U3 report: the revert-alone check (the reverse of this commit applied to the working tree, typecheck and the reader tests run, then the files restored and sha256-checked).
What you got wrong:
- mistake: the new file-answer fixture returned a Response, not a Promise; where: tests/github-read.test.ts, fileAnswer; impact: npm run typecheck failed (exit 2) while vitest passed, caught before commit, nothing delivered affected; cause: I wrote the fixture as a synchronous function where the Fetcher type is async; fix: the fixture is async; verified: typecheck exit 0, 66 of 66, both guard proofs rerun red and restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…y read still gives RUN_CANCELLED

What changed:
- tests/github-read.test.ts (tests only): a failingResponse fixture, a 403 whose body sends some text on its first pull and then fails as a reset connection does (built from E-15 shapes, not a capture), and a case table for it with one row so far: Stop, then the reset. It gives RUN_CANCELLED after one request, no body text in the error's message or cause chain, and the body released.
Why: review finding U3-R1 (S3). The fix in the next commit makes a failed 403 body read end the read with the bytes that arrived instead of rejecting, so a Stop that fails the body must still win. This pin passes on the unchanged reader and lands before that change (T-1).
What it touched: tests/github-read.test.ts only, inserted after the Stop test at :260 (the cases at :10-84 and the X-3 tests at the end are untouched). Requirements F-14, T-5. No src/ change, so docs/ARCHITECTURE.md is not staged.
What you verified:
- verified: on the unchanged code, npx vitest run tests/github-read.test.ts -t 'fails part-way': 1 passed.
- verified: red under a named mutation, file restored and sha256-checked: M-R1a, read()'s Stop mapping removed from its catch (src/main/github.ts:109): "expected 'GITHUB_UNAVAILABLE' to be 'RUN_CANCELLED'".
- verified: npm run typecheck, npm run lint, npx eslint e2e exit 0; tests/github-read.test.ts 67 of 67, tests/github-text.test.ts 23 of 23, tests/github-token-literals.test.ts 2 of 2, tests/github-app-constants.test.ts 4 of 4.
What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…y is still a refusal, judged on the bytes that arrived

What changed:
- src/main/github.ts, boundedText: a rejected reader.read() now ends the bounded read with the bytes that arrived (reader.read().catch(() => undefined), then break), so request() classifies the 403 as spec 7.2 step 4 says: GITHUB_PERMISSION_MISSING if the phrase already arrived, otherwise GITHUB_FORBIDDEN_SIGNED_IN. The abort check right after the read is unchanged and still runs first, so a Stop or deadline that fails the body throws RUN_CANCELLED, which read() maps as today. The reader is still cancelled and released in the finally.
- tests/github-read.test.ts: three rows added to the U3-R1 table: part of a message then a reset (GITHUB_FORBIDDEN_SIGNED_IN), the integration phrase then a reset (GITHUB_PERMISSION_MISSING), a reset before any byte (GITHUB_FORBIDDEN_SIGNED_IN); each after one request, with no body text in the error and the body unlocked.
- docs/ARCHITECTURE.md: the U3 sentence says a body that fails part-way ends the read and the bytes that arrived decide.
Why: review finding U3-R1 (S3). A signed-in 403 whose body stream failed mid-read became GITHUB_UNAVAILABLE ("could not be retrieved or verified", retry never), so the model lost the permission and SAML guidance although the status was already a refusal; before U3 a 403 body was never read, so this failure was new. The lead offered (a) this fix or (b) amending spec 7.2 to make a failed read GITHUB_UNAVAILABLE; (a) is taken because it keeps step 4's two outcomes as written and needs no spec change. The reviewer's sketch rethrew a read error when the signal was aborted; that rethrow is not added, because the abort check after the read already throws RUN_CANCELLED first, and a rethrow would be a guard with no observable effect, so no removal proof.
What it touched: src/main/github.ts (boundedText, lines 12 and 17), tests/github-read.test.ts, docs/ARCHITECTURE.md (the U3 sentence in the "GitHub sign-in" paragraph, line 38). Requirements F-14, T-5; spec 7.2 step 4.
What you verified:
- verified: red first. On the unchanged code the three new rows failed: "expected 'GITHUB_UNAVAILABLE' to be 'GITHUB_FORBIDDEN_SIGNED_IN'" (twice) and "... to be 'GITHUB_PERMISSION_MISSING'"; the Stop pin (previous commit) passed. After the change, tests/github-read.test.ts 70 of 70.
- verified: guard removals, file restored and sha256-checked each time: G-R1a the .catch removed (the three rows red, GITHUB_UNAVAILABLE); G-R1b the bytes that arrived discarded on a failed read (the phrase row red, GITHUB_FORBIDDEN_SIGNED_IN). The pin's mutation M-R1a (read()'s Stop mapping removed) is still red after the change ("expected 'GITHUB_UNAVAILABLE' to be 'RUN_CANCELLED'").
- verified: npm run typecheck, npm run lint, npx eslint e2e exit 0; tests/github-text.test.ts 23 of 23, tests/github-token-literals.test.ts 2 of 2, tests/github-app-constants.test.ts 4 of 4.
- expected, not run here: the full suite is unchanged apart from these tests (run once at the end of the round).
What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…mes 403, since a signed-out 401 gets it too

What changed:
- src/shared/github.ts: the GITHUB_FORBIDDEN text drops " (403)": "GitHub refused this read, which was made without a GitHub sign-in, and did not say why. ..." The rest of the text, its code (FORBIDDEN) and retry (never) are unchanged.
- tests/github-text.test.ts: the word-for-word pin (TEXTS.GITHUB_FORBIDDEN) drops " (403)"; STATUSES.GITHUB_FORBIDDEN is [] (the text names no status), and the STATUSES comment says why.
- docs/ARCHITECTURE.md: the U3 sentence says the texts name no status that did not occur, and that the signed-out refusal, given for a 401 or a 403, names neither.
Why: review finding U3-R2 (S3), the spec contradiction the first U3 report escalated. Spec 7.2 step 5 sends a signed-out 401 or 403 to GITHUB_FORBIDDEN (src/main/github.ts:49, pinned for a 401 at tests/github-read.test.ts:148), and spec 7.4 forbids a text that names a status other than the one that occurred, yet the spec 7.4 table's GITHUB_FORBIDDEN text said "(403)". The reviewer's smallest fix is taken: the text names neither status. The alternative, a reason of its own for a signed-out 401, was set aside: it would add a member to GitHubErrorCodeSchema and a text the spec does not have, for a status GitHub has no cause to send to a request without Authorization.
For the lead: spec 7.4's table must get the same edit at integration (the GITHUB_FORBIDDEN row: delete " (403)"); this unit does not own docs/specification/github-sign-in.md.
What it touched: src/shared/github.ts (line 46), tests/github-text.test.ts (lines 21 and 27-30), docs/ARCHITECTURE.md (the U3 sentence, line 38). Requirements F-14, F-15; spec 7.4.
What you verified:
- verified: red first. With the test changed and src/shared/github.ts unchanged, tests/github-text.test.ts had 1 failing: "GITHUB_FORBIDDEN has the spec's code, retry and text, word for word" (the word-for-word toEqual). After the change, 23 of 23.
- verified: guard proofs, files restored and sha256-checked: G-R2a "(403)" put back in both the text and its word-for-word pin, so only the status rule can catch it: red, "expected Set{ '403' } to deeply equal Set{}"; G-R2b "(403)" put back in the text only: red at the word-for-word pin.
- verified: npm run typecheck, npm run lint, npx eslint e2e exit 0; tests/github-read.test.ts 70 of 70 (the signed-out 401 and 403 still give GITHUB_FORBIDDEN), tests/github-token-literals.test.ts 2 of 2, tests/github-app-constants.test.ts 4 of 4.
- verified: no other copy of the text exists in src, tests or e2e (grep "made without a GitHub sign-in": src/shared/github.ts, tests/github-text.test.ts and the spec only).
What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…tops at the first chunk

What changed:
- tests/github-read.test.ts, 'signed in, Stop during the bounded 403 body read gives RUN_CANCELLED' (:260): the stream counts its pull() calls and the test expects exactly 1, with a comment saying why.
Why: review finding U3-R4 (S4). The abort check inside boundedText's loop was not pinned: with it removed, read()'s catch still mapped the error to RUN_CANCELLED, and the read merely went on to 64 KiB on a transport that ignores abort. The pull count makes the check observable.
What it touched: tests/github-read.test.ts only (the one test). Requirements F-14, T-5. No src/ change, so docs/ARCHITECTURE.md is not staged.
What you verified:
- verified: green on the current code: 1 passed.
- verified: red under the named mutation, file restored and sha256-checked: M-R4, `if (signal.aborted) throw new Error('RUN_CANCELLED');` removed from boundedText's loop (src/main/github.ts:17): "expected 1725 to be 1".
- verified: the finding's claim, that the test as it stood stayed green under the same mutation: with the previous commit's test file in place, M-R4 left it passing (exit 0); the file was then restored and its sha256 checked.
- verified: npm run typecheck, npm run lint, npx eslint e2e exit 0; tests/github-read.test.ts 70 of 70, tests/github-text.test.ts 23 of 23, tests/github-token-literals.test.ts 2 of 2, tests/github-app-constants.test.ts 4 of 4.
What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…atches a bare host with a path

What changed:
- tests/github-text.test.ts: the URL check in 'no reason text ... holds a URL' uses URL_LIKE, /https?:\/\/|www\.|\b[a-z0-9-]+\.(?:com|org|io|net|dev)\/[^\s]/i, the pattern the reviewer proposed, instead of /https?:\/\/|www\./i. A new test gives it positive controls: "github.com/apps/moonaliza" and a scheme or www. URL match, and GITHUB_INVALID_URL's text, which the check also scans, still contains "HTTPS github.com repository" (no slash follows, so it stays legal).
Why: review finding U3-R5 (S4). A text gaining "github.com/apps/..." passed the forbidden-phrase test, and only the word-for-word pin caught it; spec 7.4 says no text contains a URL.
What it touched: tests/github-text.test.ts only. Requirements F-15, F-17; spec 7.4. No src/ change, so docs/ARCHITECTURE.md is not staged.
What you verified:
- verified: green on the current code: tests/github-text.test.ts 24 of 24.
- verified: red under named mutations, every file restored and sha256-checked: T6 (the reviewer's) "with Install on repositories in MoonAliza's GitHub dialog." replaced by "with Install on repositories at github.com/apps/moonaliza." in src/shared/github.ts and in the word-for-word pin, so only the URL check can see it: the forbidden-phrase test red; T6' the same in src/shared/github.ts only: the forbidden-phrase test red; G-R5 URL_LIKE narrowed back to the old pattern: the positive-control test red ("expected 'Install it at github.com/apps/moonali...' to match").
- verified: the finding's claim: with the test file as it was before this change, T6' left the forbidden-phrase test passing (exit 0); the file was restored and its sha256 checked.
- verified: npm run typecheck, npm run lint, npx eslint e2e exit 0; tests/github-read.test.ts 70 of 70, tests/github-token-literals.test.ts 2 of 2, tests/github-app-constants.test.ts 4 of 4.
What you got wrong: I first declared URL_LIKE after the test that uses it (it worked, since the callback runs later, but read backwards); I moved it above the test before committing. Nothing delivered was affected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
… are back on lines 10-25 and 10-84

What changed:
- tests/github-read.test.ts: the credential-seam import (SIGNED_OUT, GitHubCredential) moves from line 3 to the head of the "U3 signed-in reads" section, where its names are first used, with a comment saying why it sits there. Lines 1-84 are again byte-identical to the U0 base file (8f86232), so the existing cases are at :11-26 no longer but at :10-25 and :10-84, the lines spec 7.1, "Tests the builders owe", REQUIREMENTS F-13 and T-5 cite.
Why: review finding U3-R6 (S4). Commit 9550a85 inserted the import at line 3 and moved the cited cases down one line, which commit 4cce004 had avoided on purpose, and its report did not say so. The reviewer offered two remedies: the lead records the as-built lines in the spec, or the import moves below the existing cases if lint allows. The second needs no spec change and lint allows it (the config has no import-order rule), so it is taken; ES imports are hoisted, so nothing changes at run time.
What it touched: tests/github-read.test.ts only (the import's place). Requirements F-13, T-5. No src/ change, so docs/ARCHITECTURE.md is not staged.
What you verified:
- verified: before the change, `diff <(git show cabca2f:tests/github-read.test.ts | sed -n 10,84p) <(sed -n 10,84p tests/github-read.test.ts)` exited 1 (the shift); after it, lines 10-84 and 10-25 are identical to cabca2f's, and `diff <(git show 8f86232:tests/github-read.test.ts) <(sed -n 1,85p tests/github-read.test.ts)` shows only line 85, the blank line commit 4cce004 added after the base file's 84 lines.
- verified: npm run typecheck, npm run lint, npx eslint e2e exit 0; tests/github-read.test.ts 70 of 70, tests/github-text.test.ts 24 of 24, tests/github-token-literals.test.ts 2 of 2, tests/github-app-constants.test.ts 4 of 4.
- not applicable: a mutation proof; the change moves an import and has no behaviour to guard. The line check above is its proof.
What you got wrong: nothing to report in this commit. The shift itself was mine, in commit 9550a85, and is the finding this commit fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…he deadline too, and its signal checks can fail

What changed:
- tests/github-read.test.ts, 'a credential failure that is not a reason becomes GITHUB_UNAVAILABLE, and Stop or the deadline map as today':
  - a deadline case: AbortSignal.timeout is replaced for this case only (vi.spyOn, restored in a finally; the pattern tests/provider-transport.test.ts:518 already uses) by a controller's signal; a credential that aborts it and then throws GITHUB_RENEWAL_UNAVAILABLE gives GITHUB_TIMEOUT, and the reader asked for a 20,000 ms deadline;
  - both fakes now record signal.aborted as send() saw it and the test asserts [true, true] afterwards, instead of calling expect() inside send();
  - line 1 imports vi. The cases the spec cites stay on lines 10-84, unchanged.
Why: review finding U3-R7 (S4). The test's name promised the deadline, but only Stop was exercised; the finding took the 20 s AbortSignal.timeout as not injectable, but the existing spy pattern reaches it, so the name is made true rather than shortened. While proving it I found the test's own signal check could never fail: an expect() thrown inside send() is a failure read() maps like any other (RUN_CANCELLED, or GITHUB_TIMEOUT), so the test passed anyway; the recorded observations fix that.
What it touched: tests/github-read.test.ts only. Requirements T-5; spec 7.1 ("Stop and the deadline map as today"). No src/ change, so docs/ARCHITECTURE.md is not staged.
What you verified:
- verified: green on the current code: 1 passed; the file 70 of 70.
- verified: red under named mutations of src/main/github.ts, file restored and sha256-checked each time: M-R7a the deadline mapping removed from read()'s catch ("expected ... 'GITHUB_TIMEOUT' but got 'GITHUB_RENEWAL_UNAVAILABLE'"); M-R7b the deadline at 30 s ("expected "timeout" to be called with arguments: [ 20000 ]"); M-R7c the deadline left out of the read's signal ("expected [ true, false ] to deeply equal [ true, true ]"); M-R7d Stop left out of the read's signal ("expected [ false, true ] ...").
- verified: before this change M-R7c's twin went unseen: with the previous test file, M-R7d (Stop left out of the signal) left the test passing (exit 0), because the expect() inside send() was swallowed; the file was restored and its sha256 checked.
- verified: npm run typecheck, npm run lint, npx eslint e2e exit 0; tests/github-text.test.ts 24 of 24, tests/github-token-literals.test.ts 2 of 2, tests/github-app-constants.test.ts 4 of 4; lines 10-84 still identical to cabca2f's.
What you got wrong:
- mistake: the Stop case asserted signal.aborted with expect() inside the fake send(); where: tests/github-read.test.ts, this test, from commit 9550a85; impact: that check could never fail, so a reader that dropped Stop from send()'s signal passed it (M-R7d green on the old file); the outcome check (RUN_CANCELLED) still ran, and nothing delivered beyond the test is affected; cause: a throw inside the fake becomes the reader's mapped failure, not a test failure; fix: the fake records what it saw and the test asserts it afterwards, for both cases; verified: M-R7c and M-R7d now red, the file 70 of 70. I first wrote the new deadline case the same way and caught it when M-R7c stayed green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
What changed: tests/github-account-panel.test.tsx (new) holds three regression pins for the workbench, run against a fake AppApi under jsdom: (1) the workbench makes no github.* call while it loads, or before the GitHub dialog opens; (2) the Build-mode composer footnote is unchanged; (3) the conversation cloud dialog keeps its existing sentences. No source file changes.
Why: auto-build Stage 5, unit U4 of docs/orchestration/2026-10-13-github-sign-in/BREAKDOWN.md. The common brief requires regression pins to be committed before the change they guard. Spec 8.4 says "Build: unchanged" and adds one clause to the cloud dialog without replacing anything, and spec 8.1 keeps the sign-in out of the sidebar, so the status is read only when the dialog opens.
What it touched: tests/github-account-panel.test.tsx only. Requirements F-1, F-25 (spec 8.1, 8.4).
What you verified:
- verified: the three pins pass on the unchanged code (cabca2f): 3 of 3.
- verified: each pin turns red under its named mutation of src/renderer/App.tsx, run with only that test selected: (P1) App reads github.account.read on mount; (P2) the Build footnote gains a GitHub sentence; (P3) the clause replaces the cloud dialog's first sentence. 3 of 3 red, and App.tsx was restored with the same sha256 (cf822f20fd6c8301...) each time.
- verified: npm run typecheck, npm run lint and npx eslint e2e exit 0; tests/github-token-literals.test.ts (2 of 2) and tests/github-app-constants.test.ts (4 of 4) pass.
What you got wrong: I first wrote my mutation helper to the shared scratchpad as mutate.py, overwriting an older helper of the same name from an earlier run (the More models builders used it). Where: the session scratchpad, outside the repository. Impact: that older helper's content is lost; nothing in the repository, in any worktree or in this commit is affected, and no running process used it. Cause: I did not list the shared scratchpad before writing a generic file name. Fix: my helper now lives in a U4-only folder (scratchpad/u4/mutate.py), and the generic path no longer exists, so anything still expecting the old helper fails loudly instead of running mine. Verified: ls shows the path gone and the helper in u4/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…xts, the sidebar button, footnote, cloud clause and F-26 sentence

What changed:
- src/renderer/GitHubAccountPanel.tsx (new): the dialog's content for the four states of the frozen status DTO. Signed out: Sign in, what it does, the UD-4 and UD-3 disclosures, and the ended or outcome text. Pending: the code as selectable text, the time left (m:ss, then "Waiting for GitHub..."), Open GitHub, Cancel, the phishing and keep-open sentences, and the stay-signed-in sentence during Sign in again. Pending with committed true: only "GitHub confirmed the code. Finishing sign-in...", no button. Signed in: the login (or "account name unavailable"), the install text, Install on repositories, Sign in again, Sign out, the renewal-held notice, the short disclosures and the outcome text. Right after Sign out (a renderer-local flag) it shows the four-part F-12 help, or the incomplete variant when signOutIncomplete is true, above the signed-out offer. The panel reads github.account.read on mount and polls it every 1,000 ms only while pending (cleared on unmount and on a state change). It calls start only on Sign in or Sign in again, both disabled while any call is in flight. Replies are applied in the order their requests were sent. It renders only the DTO fields it knows; announcements go to one role="status" region and invoke failures to role="alert".
- src/renderer/github-text.ts (new): every sign-in text of spec 8.2-8.4, the 11 outcome texts and the 3 ended texts typed against the DTO, codeTimeLeft(), and the button labels.
- src/renderer/App.tsx: a GitHub button in .sidebar-bottom after Model profiles; <Modal title="GitHub account"> with the panel; githubDialog joins the shortcut guard. The close handler calls github.signIn.cancel only when the panel last reported an unconfirmed sign-in (a start in flight, or pending with committed false), and reports a refusal in the workbench banner. The composer footnote for general chat and for project Ask/Plan names GitHub sign-in; Build mode is unchanged. The conversation cloud dialog gains the UD-3 clause; Model profiles gains the F-26 sentence.
- src/renderer/styles.css: the panel's notices, actions, and the code (user-select: all, monospace).
- tests/github-account-panel.test.tsx: 49 behaviour tests beside the three pins of e2ae033.
- docs/ARCHITECTURE.md: U4's sentence in the "GitHub sign-in" paragraph.
Why: auto-build Stage 5, unit U4 of docs/orchestration/2026-10-13-github-sign-in/BREAKDOWN.md (F-1, F-12, F-18, F-25, F-26 renderer parts; spec 8, 2.3, 3.2, 3.3, 6.6). Alternatives set aside: (a) cancelling from the panel's unmount cleanup instead of App's close handler - rejected because the spec puts it in the close handler and an unmount is not always a close; (b) parsing each reply with the DTO schema in the renderer - rejected because the preload already parses every result against its method's schema, so the check could never fail in the app; the marker test instead plants forbidden fields and proves only known fields render.
What it touched: the files above. Requirements F-1, F-12 (text), F-18, F-21 (polling), F-25, F-26; pre-mortem items (1)-(5) of U4. No contract file changed. Main is not wired yet (U6), so until then the dialog shows the bridge's NOT_IMPLEMENTED message.
What you verified:
- verified, red first: the final test file run against the unchanged source (src/renderer restored to e2ae033 and the two new modules removed): 49 of 49 behaviour tests failed, each for its own reason (35 missing module, 12 no "GitHub" button, the F-26 sentence missing, the footnote still "public"); the 3 pins passed. Every file was restored and sha256-checked.
- verified: tests/github-account-panel.test.tsx 52 of 52, three runs; tests/workbench.test.tsx 14 of 14; all 11 UI test files 178 of 178; no act() warnings.
- verified, guard removals: 31 named mutations, each run with only its tests selected, all red with at least one selected test failing, and each file restored with the same sha256. They cover: poll only while pending; interval cleared; reply order; Sign in disabled in flight; close cancels only when unconfirmed; committed not unconfirmed; start in flight unconfirmed; unconfirmed reported from what is rendered, not from a late reply; shortcut guard; the committed view; replies rendered as JSON (the marker test); start on mount; an outcome missing or reworded; "revoked" in a text; the incomplete variant; the sign-out flag cleared on start; help only after the user's own sign-out; the error alert; the clock read when a code arrives; a failed close-cancel reported; the cloud clause; the F-26 sentence; the chat footnote; not-configured with a button; "Waiting for GitHub" at zero; the open targets; the ended text; the renewal notice; the stay-signed-in sentence; the sidebar button.
- verified: the three pins also turn red under their mutations on the changed code.
- verified: npm run typecheck, npm run lint, npx eslint e2e, npm run build and node scripts/check-handoff.mjs exit 0; tests/github-token-literals.test.ts 2 of 2 and tests/github-app-constants.test.ts 4 of 4.
- expected, not run here: the full suite comparison (run after this commit, reported with the unit); the live dialog against main needs U6, and its e2e is U8 on Windows CI.
What you got wrong:
- Mistake: the first red-first run loaded no test. Where: tests/github-account-panel.test.tsx, the per-test dynamic imports. Impact: that run gave no per-test result; nothing delivered was affected. Cause: Vite's import analysis resolves a literal dynamic import path at transform time. Fix: the import path is a variable marked @vite-ignore, typed with typeof import(). Verified: the rerun shows 46 behaviour tests failing one by one (49 in the final file) while the 3 pins pass.
- Mistake: two guard proofs (close cancels only when unconfirmed; the shortcut guard) first came back green. Where: my mutation helper in the session scratchpad. Impact: none in the code; the proofs were not valid until rerun. Cause: vitest -t reads the name as a regex, and "(DC-5)" and "Ctrl+O" selected no test. Fix: the helper escapes the name and reports a run with no test selected instead of calling it green. Verified: all 31 proofs rerun, each with at least one selected test failing.
- Mistake: tsc rejected two test lines. Where: the test file's OUTCOMES and ENDED maps were typed Record<string, string>, so OUTCOMES.expired was string | undefined under noUncheckedIndexedAccess. Impact: typecheck exit 2 before the commit; nothing committed. Fix: the maps are untyped object literals. Verified: npm run typecheck exits 0.
- Mistake: the panel's doc comment said "clipboard". Where: src/renderer/GitHubAccountPanel.tsx. Impact: U7's static guard (no clipboard in the panel) would have flagged a comment. Fix: reworded before this commit. Verified: grep finds no clipboard, localStorage or sessionStorage in the panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…app stylesheet

What changed: tests/github-account-panel.test.tsx gains a useAppStyles() helper, which loads src/renderer/styles.css into the test's jsdom document for one test and removes it when the test finishes, and one test: a pending panel's code element has the computed style user-select: all (spec 8.1). The pending test's comment now points to that test instead of claiming what the stylesheet does. No source file changes.
Why: review finding U4-R4 (S4). Spec 8.1 says the code is selectable text with user-select: all, but no test checked the rule: mutation N5 (delete it from styles.css) stayed green. jsdom parses the stylesheet and applies matching rules to getComputedStyle, so the test reads what the rendered element gets rather than matching the CSS text. Alternative set aside: the reviewer's regex over styles.css; it passes on a rule that no longer matches the element (a renamed class, a wrong parent selector), the computed style does not.
What it touched: tests/github-account-panel.test.tsx only. Requirement F-1 (spec 8.1).
What you verified:
- verified: the new test passes on the unchanged code; tests/github-account-panel.test.tsx 53 of 53.
- verified, mutation N5 (delete "user-select:all;-webkit-user-select:all;" from src/renderer/styles.css), with only this test selected: red, "expected 'auto' to be 'all'". styles.css restored with the same sha256 (61911fde7bd9...).
- verified: npm run typecheck, npm run lint and npx eslint e2e exit 0; tests/workbench.test.tsx 14 of 14; tests/github-token-literals.test.ts 2 of 2; tests/github-app-constants.test.ts 4 of 4.
What you got wrong:
- Mistake: the helper first read the stylesheet with readFileSync(new URL('../src/renderer/styles.css', import.meta.url)). Where: tests/github-account-panel.test.tsx, useAppStyles. Impact: the new test threw "The URL must be of scheme file"; nothing committed was affected. Cause: under the jsdom environment the global URL is jsdom's class, which node:fs does not accept as a file URL. Fix: the path is join(import.meta.dirname, '../src/renderer/styles.css'), which also keeps the test independent of the working directory. Verified: the file runs 53 of 53.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
… or Install is in flight

What changed: tests/github-account-panel.test.tsx gains one table test with two rows. In the signed-in view, Sign out (then Install on repositories) is held in flight with a deferred reply; Sign in again is disabled, a click on it sends no github.signIn.start, and it is enabled again once that call ends (Sign out refused, Install answered). No source file changes.
Why: review finding U4-R5 (S4). Spec 8.1 and the existing test's name say Sign in and Sign in again are disabled while any call is in flight, but the existing test held only a start in flight, so mutation N6 (disable them only while a start is in flight) stayed green. The other way to settle the finding, renaming the existing test to what it proves, was set aside because the spec states the broader rule and the panel already implements it.
What it touched: tests/github-account-panel.test.tsx only. Requirement F-2 / U-16 (spec 8.1).
What you verified:
- verified: both rows pass on the unchanged code; tests/github-account-panel.test.tsx 55 of 55; no act() warning in the output.
- verified, mutation N6 (disabled={busy !== null} -> disabled={busy === 'github.signIn.start'} in src/renderer/GitHubAccountPanel.tsx), with only these two rows selected: 2 of 2 red. The panel was restored with the same sha256 (243ec8fa09b6...).
- verified: npm run typecheck, npm run lint and npx eslint e2e exit 0; tests/workbench.test.tsx 14 of 14; tests/github-token-literals.test.ts 2 of 2; tests/github-app-constants.test.ts 4 of 4.
What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
… on screen while the status is polled

What changed: tests/github-account-panel.test.tsx gains one regression pin with two rows: a pending panel whose Open GitHub (github.open, row 1) or Cancel (github.signIn.cancel, row 2) is refused shows the refusal in its role="alert", and still shows it after three successful polls, with the code. Row 1 covers open(), which never goes through the reply tracker; row 2 covers change(), whose reply does. No source file changes.
Why: review finding U4-R1 (S3). The fix that follows clears a failed status read when a later reply is applied; the finding warns that clearing the action error the same way would make an Open GitHub refusal vanish a second later. The common brief requires a regression pin to be committed before the change it guards, so this pin lands first.
What it touched: tests/github-account-panel.test.tsx only. Spec 8.1 (invoke failures show reason.message).
What you verified:
- verified: both rows pass on the unchanged code (783d322); tests/github-account-panel.test.tsx 57 of 57.
- verified, mutation (in track, also call setError('') when a reply is applied), with only these two rows selected: 2 of 2 red. The panel was restored with the same sha256 (243ec8fa09b6...).
- verified: npm run typecheck, npm run lint and npx eslint e2e exit 0; tests/workbench.test.tsx 14 of 14; tests/github-token-literals.test.ts 2 of 2; tests/github-app-constants.test.ts 4 of 4.
What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…e a later reply is shown

What changed:
- src/renderer/GitHubAccountPanel.tsx: a failed status read is kept apart from a failed action. A new readError state holds the read's message; the reply tracker clears it whenever it applies a reply, and sets it only while no reply sent after that read has been applied (the same order rule replies already follow). The alert shows the action's failure, else the read's. An action's failure (error) is unchanged: it stays until the user acts again. track takes an isRead flag; for a call that is not a read it rethrows, so change() still reports its own failure.
- tests/github-account-panel.test.tsx: two behaviour tests. (1) A pending panel whose second read fails shows the failure with the code; the next read, GitHub confirmed, leaves exactly "GitHub confirmed the code. Finishing sign-in..."; the signed-in view after it has no alert. (2) A poll in flight when the user cancels, failing afterwards, raises no alert under "Sign-in cancelled. Nothing was saved."
- docs/ARCHITECTURE.md: U4's sentence says how the panel shows a failed read.
Why: review finding U4-R1 (S3). The poll's catch set the same error as an action, and only an action cleared it, so one transient read failure (an engine restart) stayed on screen for the rest of the sign-in, against spec 8.2's committed view, and in the signed-in view. Test (2) goes one step past the reviewer's suggested fix: with only "an applied reply clears it", a failed read that was sent before a later call's reply would still set the alert after that reply, and in the signed-out view no later poll would clear it. Alternative set aside: clearing error on every applied reply; the pin of a14b4e0 shows it would erase a refused Open GitHub or Cancel a second later.
What it touched: src/renderer/GitHubAccountPanel.tsx, tests/github-account-panel.test.tsx, docs/ARCHITECTURE.md. Spec 8.1 (invoke failures show reason.message), 8.2 (the confirmed view). No contract file changed.
What you verified:
- verified, red first: both new tests fail on the unchanged panel (sha256 243ec8fa...): (1) received "The engine is restarting. Try again when it is ready.GitHub confirmed the code. Finishing sign-in..."; (2) an alert element where none is expected. The other 57 passed.
- verified: tests/github-account-panel.test.tsx 59 of 59, three runs; no act() warning.
- verified, guard removals on the fixed panel (sha256 b6feb9cf...), each with only its tests selected, each red, and the panel restored with the same sha256: G-R1a no clear on an applied reply -> test (1) red; G-R1b no order check on a failed read -> test (2) red; G-R1c only error rendered -> "an invoke failure shows its message" red and test (1) red (two runs); G-R1d isRead check removed, so an action's failure is handled as a read -> the Cancel row of the a14b4e0 pin red; G-R1e error also cleared on an applied reply -> both pin rows red.
- verified: npm run typecheck, npm run lint and npx eslint e2e exit 0; tests/workbench.test.tsx 14 of 14; tests/github-token-literals.test.ts 2 of 2; tests/github-app-constants.test.ts 4 of 4; grep finds no clipboard, localStorage or sessionStorage in the panel.
- expected, not run: the live dialog against main (U6) and its e2e (U8).
What you got wrong:
- Mistake: I committed the R1 pin with only the Open GitHub row, then found while writing this fix that it could not catch G-R1d. Where: tests/github-account-panel.test.tsx, the pin of a14b4e0 (first committed as 53797cc). Impact: none delivered; the commit was local and unpublished, but a fix proved only against that pin would have left the isRead check unguarded. Cause: open() never goes through the reply tracker, so a pin on Open GitHub alone does not exercise the tracker's handling of an action's failure. Fix: I set the fix aside, amended the unpublished pin commit to add a Cancel row, re-proved both rows green on the unchanged panel and red under the pin's mutation, then reapplied the fix. Verified: G-R1d turns the Cancel row red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…accessibility tree

What changed:
- src/renderer/styles.css: .github-notices:empty{display:none} becomes .github-notices:empty{border:0;padding:0;margin:0}. The empty role="status" region stays rendered and only loses its frame, so it takes no visible space and is still a live region when an outcome arrives.
- tests/github-account-panel.test.tsx: one test, run with the app stylesheet loaded into jsdom (the useAppStyles helper of c7f32a0). A pending panel's status region is an accessible role="status" element while empty and not display:none; after Cancel the same element holds "Sign-in cancelled. Nothing was saved."; a signed-in panel with no outcome also has an accessible, empty status region.
- docs/ARCHITECTURE.md: U4's sentence names the region and that it stays in the accessibility tree while empty.
Why: review finding U4-R2 (S3). Spec 8.1 announces outcome changes in a role="status" element, but display:none took that element out of the accessibility tree whenever it was empty, so the main path (pending, Cancel, the outcome text) inserted the text and revealed the region in the same render, which screen readers often do not announce. The fix is the reviewer's.
What it touched: src/renderer/styles.css, tests/github-account-panel.test.tsx, docs/ARCHITECTURE.md. Spec 8.1. No contract file changed.
What you verified:
- verified, red first: on the unchanged stylesheet (sha256 61911fde...) the new test fails with "Unable to find an accessible element with the role "status""; the other 59 passed.
- verified: tests/github-account-panel.test.tsx 60 of 60, three runs.
- verified, guard removals on the fixed stylesheet (sha256 26c67760...), each with only this test selected, each red, and styles.css restored with the same sha256: G-R2a the empty region hidden with display:none again; G-R2b the empty region given visibility:hidden.
- verified, probe not committed: in jsdom with the stylesheet loaded, the empty region computes display block, padding-top 0px and margin-bottom 0px; with a notice, padding-top 8px and margin-bottom 14px. jsdom drops the border-left shorthand because it holds var(--action), so the frame's border was not observable there.
- verified: npm run typecheck, npm run lint, npx eslint e2e, npm run build and node scripts/check-handoff.mjs exit 0; tests/workbench.test.tsx 14 of 14; tests/github-token-literals.test.ts 2 of 2; tests/github-app-constants.test.ts 4 of 4.
- expected, not run: that a screen reader announces "Sign-in cancelled. Nothing was saved." after Cancel; no screen reader here, and the U8 e2e could assert the region is not display:none before Cancel.
What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
… over an earlier failed status read

What changed: tests/github-account-panel.test.tsx gains one regression pin. A pending panel whose second status read fails shows that failure in its role="alert"; the user then clicks Open GitHub, which is refused with a different message, and before any further poll the alert must show the refusal, with the code still on screen. No source file changes.
Why: review finding U4-R7 (S4). No test pinned which message the one alert gives when an action's failure and a failed read are both present, so swapping `error || readError` for `readError || error` (mutation GR1y) stayed green at 60 of 60. The user's own click is the one to answer: with the swap, a refusal of Open GitHub would hide behind a poll's failure until the next successful poll. The other way to settle the finding, a comment saying the order does not matter, was set aside because it does matter for that second. It lands before the U4-R6 fix, which rewrites the line the pin guards.
What it touched: tests/github-account-panel.test.tsx only. Spec 8.1 (invoke failures show reason.message).
What you verified:
- verified: the pin passes on the unchanged panel (sha256 b6feb9cfc9c9...); tests/github-account-panel.test.tsx 61 of 61, no act() warning.
- verified, mutation GR1y ({error || readError}</p> -> {readError || error}</p> in src/renderer/GitHubAccountPanel.tsx), with only this pin selected: 1 failed, 60 skipped. A no-op control under the same runner: 1 passed. The panel was restored with the same sha256 (b6feb9cfc9c9...).
- verified: npm run typecheck, npm run lint and npx eslint e2e exit 0; tests/workbench.test.tsx 14 of 14; tests/github-token-literals.test.ts 2 of 2; tests/github-app-constants.test.ts 4 of 4.
What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…en when the user clicks again

What changed: tests/github-account-panel.test.tsx gains one regression pin with two rows. In the signed-out view, Sign in is refused (ENCRYPTION_UNAVAILABLE's message) and the second click returns the start-failed outcome; in the pending view, Open GitHub is refused (INTERNAL_ERROR's message) and the second click opens the page. Each refusal is shown in the role="alert", and after the second click there is no alert while the view is unchanged. Row 1 covers change(), row 2 open(). No source file changes.
Why: review finding U4-R6 (S3). The fix that follows ties an action's failure to the view it was raised in, and keeps "until the user acts again" as the other half of its lifetime. While preparing that fix I found the other half unguarded: removing the clearing at the start of change() or open() left all 65 tests of the fix green. A refusal would then stand beside the result of the next click in the same view (the start-failed outcome under an encryption refusal; an opened page under "could not be completed"), and the view check cannot catch it because the view does not change. The common brief requires a regression pin before the change it guards, so this pin lands first.
What it touched: tests/github-account-panel.test.tsx only. Spec 8.1 (invoke failures show reason.message).
What you verified:
- verified: both rows pass on the unchanged panel (sha256 b6feb9cfc9c9...); tests/github-account-panel.test.tsx 63 of 63, no act() warning.
- verified, mutations on that panel with only these rows selected, the panel restored with the same sha256 after each: P-R6e (setBusy(method); setError(''); -> setBusy(method);) turns the Sign in row red; P-R6f (setBusy('github.open'); setError(''); -> setBusy('github.open');) turns the Open GitHub row red. Both fail at the assertion that no alert remains (line 586).
- verified: npm run typecheck, npm run lint and npx eslint e2e exit 0; tests/workbench.test.tsx 14 of 14; tests/github-token-literals.test.ts 2 of 2; tests/github-app-constants.test.ts 4 of 4.
What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…e view it was raised in

What changed:
- src/renderer/GitHubAccountPanel.tsx: an action's failure is stored with the view it was raised in, `{ message, view }`, where `view` is the state and, for a pending sign-in, whether GitHub confirmed the code (`pending:false`, `pending:true`, `signed-in`, ...). change() and open() tag it with the view of the click (the render's `view` the handler closed over), and still clear it at the click. The one alert shows the action's failure only while that view is shown, then a failed read (`alertText`). Polls of the same view keep the refusal; a change of state or of `committed` takes it off the screen.
- tests/github-account-panel.test.tsx: two table tests, rows Open GitHub and Cancel. (1) The action is refused at once (INTERNAL_ERROR's message for Open GitHub, ENGINE_UNAVAILABLE's for Cancel); the status then becomes pending with committed: true, and the panel's textContent must be exactly "GitHub confirmed the code. Finishing sign-in..."; then signed-in, with no alert. (2) The action is held in flight, a poll brings the confirmed view, and only then is it refused: the confirmed view still holds only its sentence.
- docs/ARCHITECTURE.md: U4's sentence says how long an action's failure is shown.
Why: review finding U4-R6 (S3), the rest of U4-R1. A failed action stayed through a change of state, so the confirmed view, which spec 8.2 limits to one sentence and gives no button, and the signed-in view after it, could carry an alert from the earlier pending view: Open GitHub failing (the user then types the address by hand) or Cancel refused during an engine restart while main keeps polling GitHub. Alternative set aside: the reviewer's second option, clearing the failure in track when an applied reply changes the view. Probed on the old panel, it passes test (1) but fails both rows of test (2): a refusal that arrives after the reply that changed the view is set after the clearing. Tagging at the click covers both.
What it touched: src/renderer/GitHubAccountPanel.tsx, tests/github-account-panel.test.tsx, docs/ARCHITECTURE.md. Spec 8.1 (invoke failures show reason.message), 8.2 (the confirmed view), 3.3 (the refusals). No contract file changed.
What you verified:
- verified, red first: all four new rows fail on the unchanged panel (sha256 b6feb9cfc9c9...), each at the confirmed-view assertion, receiving the refusal followed by the confirmed sentence; the other 63 tests pass.
- verified: tests/github-account-panel.test.tsx 67 of 67, three runs, no act() warning, on the fixed panel (sha256 0870b2c3c9df...).
- verified, guard removals on the fixed panel, whole test file each, the panel restored with the same sha256 after each: G-R6a view check removed -> the four new rows red, and the a14b4e0 pin rows and the U4-R7 pin stay green; G-R6b tag with the view shown when the refusal arrives (a ref) -> both rows of test (2) red; G-R6d the view key without committed -> all four new rows red; G-R6c the failed read first -> the U4-R7 pin red; G-R6e and G-R6f no clearing at the click in change() or open() -> the matching row of the 8b112c2 pin red; G-R6g also clear the failure on every applied reply -> both a14b4e0 pin rows red.
- verified: the reviewer's alternative, probed on the old panel: 2 failed (test 2's rows), 65 passed.
- verified: npm run typecheck, npm run lint and npx eslint e2e exit 0; tests/workbench.test.tsx 14 of 14; tests/github-token-literals.test.ts 2 of 2; tests/github-app-constants.test.ts 4 of 4; grep finds no clipboard, localStorage or sessionStorage in the panel.
- expected, not run: the live dialog against main (U6) and its e2e (U8).
What you got wrong:
- Mistake: I wrote this fix and its tests before finding that the clearing at the click, the other half of the failure's lifetime, had no test. Where: the order of this round's commits. Impact: none delivered; nothing was committed in that order, but committed so, the click-clears pin would have landed after the change it guards, against the common brief. Cause: I ran the "info" mutations G-R6e and G-R6f only after the fix was in place. Fix: I saved the fix aside, restored the panel and tests to HEAD, committed the pin first (8b112c2: green on b6feb9cf, red under both mutations), then reapplied the fix byte for byte (sha256 0870b2c3c9df...) and re-ran the red-first proof on top of the pin. Verified: the red-first run and every guard removal above were run after the reapply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…gment calls; the map's line-per-unit rule)

What changed: docs/specification/github-sign-in.md. The 7.4 GITHUB_FORBIDDEN row drops "(403)", as the code and its pin do. The phrase rule in "Tests the builders owe" is stated per sentence. "As built" records the wave-1 judgment calls the unit reviews left to the lead.
Why: U3-R2, U3-R3, U3-R8 and U3-R9, plus U4's build notes 1-6, were lead-owned at integration. The spec must say what the code does before U5-U8 build on it.
What you verified: verified - the spec row now matches src/shared/github.ts:46 word for word (read back); the per-sentence rule matches tests/github-text.test.ts; no token-shaped string.
What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…a third round for U1 and U2

What changed: RUN.md units table, next action, the decision to give U1 and U2 a third targeted round, and the log.
Why: checkpoint after wave 1 (lead-orchestrator: every status change of a unit is recorded).
What you verified: verified - the shas and test counts are the ones git log and the vitest runs printed.
What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…sifier, shape checks, fetchLogin, loopback fake

What changed:
- src/main/github-oauth.ts (new, spec 3.5 and 5): the stateless wire. oauthRequest is the only place OAuth parameters are placed (a form-encoded body, no query, Accept: application/json, E-21). requestDeviceCode sends scope=offline_access only when offlineAccess is true (DC-3). requestToken serves the poll and the refresh. fetchLogin reads GET /user and keeps only a valid login (GS-8). rateLimitWait has a 1 s floor and a ceiling (DC-12). headerSafeToken does the prefix and header-character check.
- Every request carries redirect 'manual', credentials 'omit', cache 'no-store', the owner's signal combined with its own timeout, and a 64 KiB body bound checked against content-length and while streaming. Unread bodies are cancelled.
- One classifier, in the order of spec 5.3: thrown or timed out, then 3xx, then rate limits (before the body is read), then oversize or unparseable, then `error` at any status, then 2xx and the shape check, then 5xx, then the rest. Bodies are parsed by media type, compared case-insensitively before `;`. A parse error is caught and dropped with no cause (DC-9).
- Shape checks of spec 5.5: the exact verification_uri, device_code, user_code, expires_in and interval at step 1; ghu_ and ghr_ tokens and integer expiries; non-expiring tokens; `unusable` carries its status (GS-2).
- Nothing in the module rejects, and no result carries response text. Each request is dispatched before the call returns, and the code that awaits the answer never holds the parameters or the Authorization value.
- tests/fixtures/fake-github-oauth.ts (new, T-2): an http server on 127.0.0.1:0.
  - fetcherFor maps https://github.com/login/ and https://api.github.com/ onto it through the real fetch, and throws on anything else.
  - Scripted answers: queued, fixed or by prefix; static or functions; status, headers, a JSON, form or raw body, chunked, a delay, a hang (before or mid-body) or a reset.
  - Per-request records: query keys and body keys apart, content type, header names and values (Authorization only by marker identity), the markers carried anywhere, and whether a client secret field appears anywhere.
  - markers() generates the run-time markers, and codeAnswer, tokenAnswer and errorAnswer build the answers. The answers are built from E-01/E-02/E-15 shapes, not a capture. U5 and U7 import this file read-only.
- tests/github-oauth.test.ts (new): 146 tests covering the spec's U1 list, plus the fixture's own behaviour.
- docs/ARCHITECTURE.md: U1's sentence appended to the "GitHub sign-in" paragraph.

Why: BREAKDOWN U1. It builds F-2, F-3 and F-4 (wire), S-4 and S-5 (the oauth and fetchLogin pins), and T-1 and T-2 (the fake), from spec 3.5, 5, 3.9 layer 1 and "Tests the builders owe". U5 and U7 need the wire and the fake.
- Set aside: putting the parameters in a query string, or falling back between placements (DR-9: a refresh token is single-use).
- Set aside: a request timeout supplied only by the caller for fetchLogin. It also takes its own timeout, as spec 5.1 requires of every request; the 3.5 comment names only the caller's.

What it touched:
- New files: src/main/github-oauth.ts, tests/github-oauth.test.ts, tests/fixtures/fake-github-oauth.ts.
- docs/ARCHITECTURE.md line 38: one appended sentence.
- No frozen U0 contract changed. No ADR is owed; the decisions are DR-9, DC-3, DC-9, DC-12, GS-2 and GS-8 in DECISIONS.md and the spec. No new domain term.

What you verified:
- verified: red first. With the module absent, the file failed to import. With a stub of the exact signatures that throws, 142 of the 145 tests then present were red, and the 3 green ones were fixture self-tests. With the fixture absent, the whole file was red, including the 4th fixture test added later.
- verified: 39 guard-removal mutations, each run on the final files. Each mutation turned its named tests red, and every file's sha256 matched after restore. The guards are: error before ok; rate limits first; exact verification_uri (removed, and weakened); header-safe characters; the access, refresh and expiry shapes; the length and prefix bounds; redirect, credentials, cache and Accept on the OAuth requests; redirect and credentials on the login read; the 1 s floor; parse errors replaced, and parse errors kept as cause; REQUEST_OFFLINE_ACCESS flipped (also red in github-app-constants); the offlineAccess parameter; refresh_token moved to the query; media type before `;` and case-insensitive; the declared and streaming bounds; the flat object; synchronous dispatch; the error-code bound; the device_code, user_code and expiry/interval checks at step 1; the login schema; the unread body and the declared-length body cancelled; row 7; row 2; the request timeout; a synchronous fetcher throw caught.
- verified: tests/github-oauth.test.ts gave 146 of 146, three times in a row and under two shuffled seeds. tests/github-token-literals.test.ts gave 2 of 2, and tests/github-app-constants.test.ts gave 4 of 4.
- verified: npm run typecheck (NODE_OPTIONS=--max-old-space-size=2048), npm run lint and npx eslint e2e all exit 0. Run with TMPDIR set to a private directory and cwd /home/user/task5-handoff/wt/gsi-u1.
- verified: the full suite JSON run, compared with the 74-failure Linux baseline, showed no new failure and no file without results (numbers in the unit report).
- expected: the refresh placement and the token-endpoint encoding match GitHub. These are inferences that DO-1, DO-2 and DO-3 check with the registered app.

What you got wrong:
- mistake: the fixture's reset() did not restart `seq`.
  - where: tests/fixtures/fake-github-oauth.ts reset(); found by the new fixture test that expects seq 1.
  - impact: the test was green alone and red in the full file, so it depended on order. Nothing was delivered.
  - cause: `seq` was added to the record after reset() was written, and reset() was not revisited.
  - fix: reset() sets seq = 0, and the field documents "since start or reset()".
  - verified: 146 of 146, three runs and two shuffled seeds.
- mistake: the escapes \u00A0, \u0100, \u00A1, \u00FF and \u00E9 in the test file were written as literal characters.
  - where: tests/github-oauth.test.ts, the headerSafeToken and device_code cases.
  - impact: lint failed with no-irregular-whitespace, and the cases depended on invisible characters. Nothing was delivered.
  - cause: the file-writing step turned printable non-ASCII escapes into literal characters.
  - fix: replaced them with \uXXXX escapes; grep -P '[^\x00-\x7F]' finds none in the new files.
  - verified: npm run lint exits 0, and the tests are green.
- mistake: the DC-9 assertion label printed the first 12 characters of each body.
  - where: tests/github-oauth.test.ts, the parse-failure loop.
  - impact: for the body that starts with the refresh marker, a failure message would have shown 8 characters of the marker, a run-time test value. It was seen only in the mutation run output, and nothing was committed.
  - cause: the label was chosen for readability without checking what each body starts with.
  - fix: the label is now the answer's index.
  - verified: G17 and G18 re-run red with the new label, and the suite is green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…treaming bound and fetchLogin's non-2xx

What changed:
- tests/github-oauth.test.ts: a watchedStream(total) helper beside watchedBody. It is a pull-based ReadableStream of `total` bytes in 16 KiB pieces with no content-length, recording the bytes pulled and whether it was cancelled.
- New test "a streamed body over 64 KiB with no content-length is cancelled when the streaming bound trips". An injected fetcher answers 200 application/json over a 200,000-byte watchedStream. requestToken gives {kind:'transient'} and requestDeviceCode gives {kind:'failed'}. Each stream was pulled for fewer than 200,000 bytes, so it was refused mid-stream and was still open, and each was cancelled.
- New test "fetchLogin: the body of a non-2xx answer, a 302 included, gives null and is cancelled unread". For 302, 401, 403 and 500, an injected fetcher answers with a watchedBody holding a login. fetchLogin gives null, and the body was cancelled.
- U1-R2, done before this commit: the U1 commit's message was reworded with `git commit --amend`, message only, with tree, parent, author and trailers unchanged; 7e0b2c3 became 289fe3d. Its got-wrong bullet now names the five escapes in ASCII, as backslash-u escapes of U+00A0, U+0100, U+00A1, U+00FF and U+00E9. Before, it held those characters literally, a no-break space among them.

Why: review finding U1-R1 (S2). Spec 5.1 says "The body is always cancelled or released". Two of the wire's four cancel sites had no test:
- readBounded's finally cancel, reached when the streaming bound trips (src/main/github-oauth.ts:116 and :119);
- loginOf's cancel of a non-2xx answer (:211).
Removing either left all 146 tests green, so an oversize streamed OAuth answer, or a 401/403/5xx from GET /user, could keep its stream and socket open until garbage collection. The two tests are regression pins of the code as it is. src/ is unchanged.
- Set aside: a single push-enqueued body over 64 KiB. A pull-based stream is refused while more data is pending, which is the "while streaming" case. The pulled-bytes assertion also shows that the cancel reaches an open stream; a stream already drained and closed would not call its cancel hook.

What it touched:
- tests/github-oauth.test.ts only: one helper and two tests.
- No src/ change, so docs/ARCHITECTURE.md is not staged. No ADR, map row or domain term owed.

What you verified:
- verified: both pins are green on the unchanged source, with tests/github-oauth.test.ts at 148 of 148.
- verified, R01 guard removal: src/main/github-oauth.ts:119 `finally { await reader.cancel().catch(() => {}); reader.releaseLock(); }` changed to `finally { reader.releaseLock(); }`. Result: red 1 of 148, the streamed-body test, at its `state.cancelled` assertion. Restored, with sha256 7cbf1a73... matching.
- verified, R02 guard removal: :211 `if (!response.ok) { await response.body?.cancel().catch(() => {}); return null; }` changed to `if (!response.ok) { return null; }`. Result: red 1 of 148, the fetchLogin non-2xx test, at status 302's cancelled assertion. Restored, with the sha256 matching.
- verified: each mutation ran alone (an exact single-match edit, then `npx vitest run tests/github-oauth.test.ts --reporter=json`, then restore and a sha256 check), with cwd /home/user/task5-handoff/wt/gsi-u1 and TMPDIR a private scratch directory.
- verified: npm run typecheck (NODE_OPTIONS=--max-old-space-size=2048), npm run lint and npx eslint e2e all exit 0. tests/github-token-literals.test.ts gives 2 of 2, and tests/github-app-constants.test.ts gives 4 of 4.
- verified: `grep -cP '[^\x00-\x7F]' tests/github-oauth.test.ts` gives 0.

What you got wrong:
- mistake: this commit's first message repeated the defect U1-R2 reports. Its U1-R2 line held the five characters literally, U+00A0 among them, instead of naming them.
  - where: the U1-R2 bullet under "What changed", line 7 of the first message of this commit (5527fa1 before the amend).
  - impact: the report showed invisible and non-ASCII characters where it meant to name escapes. It was caught in the worktree before review; the branch is not pushed, and no file changed.
  - cause: the message file was written by a step that turns backslash-u escapes into characters. My ASCII check printed 1 in a command chained with `;`, so the commit ran anyway.
  - fix: the message was amended, message only, to name the code points as U+XXXX. The commit command now runs only when the ASCII check finds nothing (chained with &&).
  - verified: `git log -1 --format=%B | grep -cP '[^\x00-\x7F]'` gives 0, and the tree is unchanged by the amend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…integer bound, and why it is kept

What changed:
- tests/github-oauth.test.ts, the "%s is malformed" table: two rows, a JSON expires_in of 10,000,000,000 and a JSON refresh_token_expires_in of 10,000,000,000.
- The "a step-1 answer with %s fails" table: one row, a JSON expires_in of 10,000,000,000.
- New test "an expiry of 10 digits is read in JSON as in a form body". 9,999,999,999 for both expiries gives tokens, in a JSON body and in a form body. The same test shows the reason for the bound: that expiry is a valid DateTimeSchema date-time; 1e12 s is not (+033715-...); and 2^53 - 1 s makes toISOString throw RangeError.
- tests/github-oauth.test.ts imports DateTimeSchema from src/shared/contracts for that check.

Why: review finding U1-R3 (S4). src/main/github-oauth.ts:148 bounds a JSON integer to at most 9,999,999,999, and no test covered it (mutation R42 left 146 of 146 green). The finding offered two remedies. I took option 1: keep the bound as the deliberate JSON twin of 5.5's form rule `^\d{1,10}$`, and pin it.
- Set aside: option 2, dropping `&& value <= 9_999_999_999` so that JSON follows 5.5's "an integer of at least 1" only. Number.isSafeInteger would then admit any expiry up to 2^53 - 1 s. Spec 4.2 stores accessExpiresAt and refreshExpiresAt as DateTimeSchema (z.iso.datetime). From about 2.5e11 s, receivedAt + expiry passes year 9999, and the schema refuses it. At 2^53 - 1 s, toISOString throws. A hostile or broken answer could then fail the account write (U2/U5) instead of being classified `malformed` at the wire, where 3.9 layer 1 puts the failure. The 10-digit bound keeps every accepted expiry inside the date-time range (at most about 317 years after receipt), in JSON as in a form body.
- Owed by the lead: the spec's "As built" should record that JSON integers share the form's 10-digit bound (9,999,999,999). I do not own the spec, so it is not changed here.

What it touched:
- tests/github-oauth.test.ts only: three table rows, one test and one import.
- No src/ change, so docs/ARCHITECTURE.md is not staged. No ADR, map row or domain term owed; the spec note is the lead's (above).

What you verified:
- verified: on the unchanged source, tests/github-oauth.test.ts gives 152 of 152.
- verified, R42 guard removal: src/main/github-oauth.ts:148 `value >= 0 && value <= 9_999_999_999 ? value : null;` changed to `value >= 0 ? value : null;`. Result: red 3 of 152, the three new 11-digit rows (two token rows malformed, one step-1 row failed). Restored, with sha256 7cbf1a73... matching.
- verified, R42b (an off-by-one boundary): `value <= 9_999_999_999` changed to `value < 9_999_999_999`. Result: red 1 of 152, the 10-digit test. Restored, with the sha256 matching.
- verified, R42c (the form bound): `const FORM_INTEGER = /^\d{1,10}$/;` changed to `/^\d{1,9}$/`. Result: red 1 of 152, the 10-digit test. Before this commit nothing pinned the form's 10-digit acceptance; only the 11-digit refusal was pinned. Restored, with the sha256 matching.
- verified: each mutation ran alone (an exact single-match edit, then `npx vitest run tests/github-oauth.test.ts --reporter=json`, then restore and a sha256 check), with cwd /home/user/task5-handoff/wt/gsi-u1 and TMPDIR a private scratch directory.
- verified: npm run typecheck (NODE_OPTIONS=--max-old-space-size=2048), npm run lint and npx eslint e2e all exit 0. tests/github-token-literals.test.ts gives 2 of 2, and tests/github-app-constants.test.ts gives 4 of 4. The test file has no non-ASCII byte.
- verified: the date-time figures, with node 24 and zod in this worktree. NOW + 9,999,999,999 s gives 2343-12-06T01:46:39.000Z (schema: true). NOW + 1e12 s gives +033715-10-12T09:46:40.000Z (schema: false). NOW + 2^53 - 1 s throws "RangeError: Invalid time value".

What you got wrong:
- mistake: a first draft of the test comment said "one more digit can pass year 9999".
  - where: tests/github-oauth.test.ts, the comment in the 10-digit test, before this commit.
  - impact: the comment would have overstated the margin. An 11-digit expiry (up to 99,999,999,999 s) still lands near year 5200, inside DateTimeSchema. The real edge is about 2.5e11 s. Nothing was committed with it.
  - cause: I took the 1e12 s example as the next digit without computing where year 9999 falls.
  - fix: the comment now says that an unbounded JSON expiry could be any safe integer, with 1e12 s and 2^53 - 1 s as the examples. The assertions use those two values.
  - verified: the figures above, and the test is green on the unchanged source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…Wait through requestToken and requestDeviceCode

What changed:
- tests/github-oauth.test.ts gains a test "through the wire, x-ratelimit-reset is measured from the injected clock, not the wall clock". The fake answers a token request and a step-1 request with a 403 carrying `x-ratelimit-remaining: 0` and `x-ratelimit-reset` set to the injected NOW/1000 + 120 (resetIn(120)). requestToken and requestDeviceCode each give {kind:'rate-limited', waitMs: 120_000}.

Why: review finding U1-R4 (S4). classify passes its receipt time from the injected clock to rateLimitWait (src/main/github-oauth.ts:159), but every x-ratelimit-reset case called rateLimitWait directly. The rate-limited answers sent through the wire carried retry-after or fell to the floor. Swapping in the wall clock was therefore unseen. In production both clocks are Date.now, but U5's tests run under clocks far from real time (T-2).
- The step-1 assertion goes beyond the finding's one requestToken case, because it closes a second gap. requestDeviceCode's result carries no receipt time, so before this commit nothing showed that it hands the injected clock to classify (R30b below).

What it touched:
- tests/github-oauth.test.ts only: one test.
- No src/ change, so docs/ARCHITECTURE.md is not staged. No ADR, map row or domain term owed.

What you verified:
- verified: on the unchanged source, tests/github-oauth.test.ts gives 153 of 153.
- verified, R30: src/main/github-oauth.ts:159 `const waitMs = rateLimitWait(response, receivedAt, limits);` changed to `rateLimitWait(response, Date.now(), limits)`. Result: red 1 of 153, the new test. Without the new test, with the file stashed to its state at 6a21f45, the same mutation was green 152 of 152. Restored, with sha256 7cbf1a73... matching.
- verified, R30b: requestDeviceCode's `classify(dispatch(fetcher, url, init), limits, now)` changed to `classify(..., limits, Date.now)`. Result: red 1 of 153, the new test. Without it, green 152 of 152. Restored, with the sha256 matching.
- verified, R30c (for contrast): requestToken's `classify(..., limits, now)` changed to `classify(..., limits, Date.now)`. Result: red 10 of 153, the new test and nine tests that check receivedAt. Restored, with the sha256 matching.
- verified: each mutation ran alone (an exact single-match edit, then `npx vitest run tests/github-oauth.test.ts --reporter=json`, then restore and a sha256 check), with cwd /home/user/task5-handoff/wt/gsi-u1 and TMPDIR a private scratch directory. After each stash, the test file's sha256 matched its copy.
- verified: npm run typecheck (NODE_OPTIONS=--max-old-space-size=2048), npm run lint and npx eslint e2e all exit 0. tests/github-token-literals.test.ts gives 2 of 2, and tests/github-app-constants.test.ts gives 4 of 4. The test file has no non-ASCII byte.

What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…ring, a number and true

What changed:
- tests/github-oauth.test.ts, the "%s is transient on a token request, and failed at step 1" table: four rows, JSON roots null, 'x', 7 and true.
- The "fetchLogin: %s gives null" table: two rows, a JSON null root and a JSON string root that is itself a valid login ('someone').

Why: review finding U1-R5 (S2). Spec 5.3 row 4 says an answer body must parse to a flat object of strings and numbers, and anything else is transient (failed at step 1); the module promises it never rejects (3.9 layer 1). Two guards enforce that rule and no test covered them: the `value === null` check in parseBody (src/main/github-oauth.ts:135) and its twin in loginOf (:216), and parseBody's `typeof value !== 'object'` check (:135). The only non-object root under test was a JSON array.
- Without the null check, a JSON null body makes requestToken, requestDeviceCode and fetchLogin reject with a TypeError, and U5's poll and refresh loops await them.
- Without the typeof check, a string, number or boolean root on a token request becomes {kind:'unusable', status:200}, which counts toward GS-2's three-in-a-row clear instead of a hold.
- The committed code handles every case; these are regression pins. src/ is unchanged.
- The string row in the fetchLogin table pins an outcome, not a guard: see the last bullet under "What you verified".

What it touched:
- tests/github-oauth.test.ts only: six table rows.
- No src/ change, so docs/ARCHITECTURE.md is not staged. No ADR, map row or domain term owed.

What you verified:
- verified: on the unchanged source, tests/github-oauth.test.ts gives 159 of 159; the six new rows are green.
- verified, N04 guard removal: :135 `typeof value !== 'object' || value === null || Array.isArray(value)` changed to `typeof value !== 'object' || Array.isArray(value)`. Result: red 1 of 159, "a JSON null is transient on a token request, and failed at step 1", with "TypeError: Cannot convert undefined or null to object" (the wire rejected). Restored, sha256 7cbf1a73... matching.
- verified, N04b guard removal: the same line changed to `value === null || Array.isArray(value)`. Result: red 3 of 159, the JSON string, number and true rows, each with {kind:'unusable', status:200} where {kind:'transient'} was expected. Restored, sha256 matching.
- verified, N26 guard removal: :216 with `value === null ||` removed. Result: red 1 of 159, "fetchLogin: a JSON null gives null", whose settle() held a rejection instead of {value:null}. Restored, sha256 matching.
- verified: without the new rows (the test file at HEAD 570780c put back for the run, then this version restored with its sha256 matching), N04, N04b and N26 are each green, 153 of 153.
- verified, N26b (for the record, not a pin): :216 with `typeof value !== 'object' ||` removed is green, 159 of 159, with or without the new rows. Object.hasOwn on a string, number or boolean is false, so a primitive root still gives null. That typeof check narrows `unknown` to `object` for the type checker (Object.hasOwn takes an object) and has no separate runtime effect, so no test can be red without it. The JSON string row pins the outcome.
- verified: each mutation ran alone (an exact single-match edit, then `npx vitest run tests/github-oauth.test.ts --reporter=json`, then restore and a sha256 check), with cwd /home/user/task5-handoff/wt/gsi-u1 and TMPDIR a private scratch directory.
- verified: npm run typecheck (NODE_OPTIONS=--max-old-space-size=2048), npm run lint and npx eslint e2e all exit 0. tests/github-token-literals.test.ts gives 2 of 2, and tests/github-app-constants.test.ts gives 4 of 4. The test file has no non-ASCII byte.

What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…aining-0 and hour bounds, error at 5xx, and exactly 64 KiB declared

What changed:
- tests/github-oauth.test.ts, the "rateLimitWait: %s" table: two rows. A 403 with remaining 0 and a reset exactly 3,600 s away gives the 60 s floor. A 429 with remaining 5 and a reset 1,800 s away gives the 60 s floor.
- The "the error %s is classified the same" test now also runs each of the 13 codes at 500 and 503, on a token request and at step 1. Its title names the six statuses.
- The "a body over 64 KiB is refused ... and a body of exactly 64 KiB is read" test: one injected-fetcher case. A 200 application/json Response of exactly 65,536 bytes that declares `content-length: 65536` gives tokens. The fetcher checks the header is on the Response it returns.

Why: review finding U1-R6 (S4). Four conditions in spec 5.1, 5.3 and 5.4 were built as specified, but no test covered them, so all tests stayed green under each mutation below:
- (a) 5.4 step 2 reads x-ratelimit-reset only when x-ratelimit-remaining is '0' (src/main/github-oauth.ts:67). Without that check, a 429 carrying a reset waits up to an hour instead of the floor.
- (b) 5.4 wants the reset "less than an hour away" (:67). The nearest row used 3,601 s; the boundary is 3,600 s.
- (c) 5.3 row 5 reads `error` "at any HTTP status" (:166), before row 7's 5xx. Error codes were tested only at 200, 400, 401 and 403.
- (d) 5.1 refuses a body only when its declared length is over 64 KiB (:110). Exactly 64 KiB was tested only as a chunked body with no content-length.
I took the finding as written; each item is what the spec says. src/ is unchanged.

What it touched:
- tests/github-oauth.test.ts only: two table rows, two statuses and a title in one loop, and one injected case.
- No src/ change, so docs/ARCHITECTURE.md is not staged. No ADR, map row or domain term owed.

What you verified:
- verified: on the unchanged source, tests/github-oauth.test.ts gives 161 of 161.
- verified, N01: :67 `< HOUR_MS` changed to `<= HOUR_MS`. Result: red 1 of 161, the exactly-an-hour row, with 3600000 where 60000 was expected. Restored, sha256 7cbf1a73... matching.
- verified, N02: :110 `Number(declared) <= maxBytes` changed to `Number(declared) < maxBytes`. Result: red 1 of 161, the 64 KiB test, at the new injected case, with {kind:'transient'} where tokens were expected. Restored, sha256 matching.
- verified, N03: :67 `else if (remaining === '0' && reset !== null` changed to `else if (reset !== null`. Result: red 1 of 161, the remaining-not-0 row, with 1800000 where 60000 was expected. Restored, sha256 matching.
- verified, N25: :166 `if (typeof error === 'string')` changed to `if (typeof error === 'string' && response.status < 500)`. Result: red 13 of 161, every code's row, each at "<code> at 500" with {kind:'transient'}. Restored, sha256 matching.
- verified: without the new pins (the test file at bb34a3c put back for the run, then this version restored with its sha256 matching), N01, N02, N03 and N25 are each green, 159 of 159.
- verified: each mutation ran alone (an exact single-match edit, then `npx vitest run tests/github-oauth.test.ts --reporter=json`, then restore and a sha256 check), with cwd /home/user/task5-handoff/wt/gsi-u1 and TMPDIR a private scratch directory.
- verified: npm run typecheck (NODE_OPTIONS=--max-old-space-size=2048), npm run lint and npx eslint e2e all exit 0. tests/github-token-literals.test.ts gives 2 of 2, and tests/github-app-constants.test.ts gives 4 of 4. The test file has no non-ASCII byte.

What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
… s limit, so no row passes on the timeout alone

What changed:
- tests/github-oauth.test.ts, the "fetchLogin: %s gives null" table: every row now runs under `limits` (requestTimeoutMs 5,000) instead of `quick` (50 ms). A one-line comment says why.
- Its "a hang past the timeout" row moved to its own test, "fetchLogin: a hang past the timeout gives null (the one fetchLogin case run under the short limit)", which keeps `quick`. The test count is unchanged at 161.

Why: review finding U1-R7 (S4). Every row of that table expects null, and a timed-out request also gives null. Under `quick`, a row passed whether or not its guard was present whenever the loopback round trip took over 50 ms, for example on a loaded Windows CI runner. GitHubLoginSchema (src/main/github-oauth.ts:217) is pinned only by two rows of this table ('a login that is not a GitHub login', 'a login that is a number'). The test at :566 uses a valid login, so it does not cover that guard. src/ is unchanged.
- Set aside: the finding's example of a per-row limits field. Only the hang row needs the short limit, so it became its own test. The table's rows and title are unchanged, and U1-R5's rows in it are untouched.
- Not changed: the OAuth test "a fetcher that throws synchronously, a reset socket, a hang past the timeout and a stalled body..." also runs its reset row under `quick`. A reset and a timeout both reach the same catch (:154), so no guard of its own can be masked there.

What it touched:
- tests/github-oauth.test.ts only: one table body, one row moved into its own test.
- No src/ change, so docs/ARCHITECTURE.md is not staged. No ADR, map row or domain term owed.

What you verified:
- verified: on the unchanged source, tests/github-oauth.test.ts gives 161 of 161. Every table row finishes in about 1 ms, and the hang test in about 52 ms.
- verified, the finding's case before this change (at 3196d01): :217-218 changed to return the raw `login`, then `npx vitest run tests/github-oauth.test.ts -t 'fetchLogin: '`. With `quick` at 50 ms: red 2 of 16 run, the two login rows. With the same src mutation and `quick` at `requestTimeoutMs: 1`: red 0. Every row passed on the timeout alone.
- verified, after this change: the same src mutation with `quick` at 50 ms gives red 2 of 16 run, the two login rows ({value:'-not a login'} and {value:42} where null was expected). With `quick` at 1 ms as well, it still gives red 2, the same two rows.
- verified: `quick` at 1 ms with no src mutation gives red 0 of 16 run, so the hang test passes at any short limit.
- verified, H1 guard removal for the hang test: fetchLogin's `signal: bounded(signal, limits)` changed to `signal`. Result: red 1 of 16 run, the hang test, at vitest's 15 s test timeout.
- verified: every mutation ran alone (an exact single-match edit, then the run above, then restore and a sha256 check). src/main/github-oauth.ts came back as 7cbf1a73... each time, and the test file as its own hash. cwd /home/user/task5-handoff/wt/gsi-u1, TMPDIR a private scratch directory.
- verified: npm run typecheck (NODE_OPTIONS=--max-old-space-size=2048), npm run lint and npx eslint e2e all exit 0. tests/github-token-literals.test.ts gives 2 of 2, and tests/github-app-constants.test.ts gives 4 of 4. The test file has no non-ASCII byte.

What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…wo tests that no timer can pass

What changed:
- tests/github-oauth.test.ts: a helper `failingBody` and two tests, both under `limits` (requestTimeoutMs 5,000), not `quick`.
  - S1 (:228), "a body whose read fails part-way, after the headers, is transient, failed at step 1, and null for fetchLogin, never a rejection". An injected fetcher returns a 200 application/json Response. Its pull-based stream enqueues the first half of the body, and the next pull calls `controller.error(...)`. requestToken gives transient, requestDeviceCode failed, and fetchLogin null. The stream's pull count must be 2 for each, so the read really started and then failed. No timer is involved.
  - S2 (:240), "an owner abort once the loopback headers are in, with the body stalled, ...". This is the real fetch path. The fake answers `hang: 'body'` on all three routes. A wrapper awaits the fake's Response, records that the headers arrived, aborts the owner and returns the Response, so undici errors the body mid-read. It expects transient, failed and null, and the headers flag must be true for all three. A round trip slow enough to time out first therefore fails the test instead of passing it.
  - Each test collects its three outcomes and asserts them once, so a red run shows every call.
- The stalled-body row of the test at :192 stays where it is; it now pins only undici's timeout.

Why: review finding U1-R9 (S4). readBounded's `catch { return null; }` (src/main/github-oauth.ts:119) turns an error during the body read, after the headers, into transient, failed or null. Without it the wire rejects, which breaks 3.9 layer 1 ("nothing here rejects"). Only the stalled-body row covered the guard, and it runs under `quick` (50 ms). When the loopback headers take over 50 ms, that row times out at row 1 (:154) and passes with the guard gone, the same class as U1-R7. No step-1 or fetchLogin case reached :119. src/ is unchanged.
- Taken as written: S1 and S2 are the reviewer's scratch tests. I added the pull count to S1 and the headers flag to S2, so each test proves it reached the read.

What it touched:
- tests/github-oauth.test.ts only: one helper and two tests, 49 lines added; 161 tests become 163.
- No src/ change, so docs/ARCHITECTURE.md is not staged. No ADR, map row or domain term owed.

What you verified:
- verified: on the unchanged source (sha256 7cbf1a73...), tests/github-oauth.test.ts gives 163 of 163. The two new tests also pass ten times in a row when run on their own.
- verified, M18 guard removal: :119 `} catch { return null; } finally {` changed to `} finally {`.
  - Whole file: red 3 of 163. They are S1, S2 and the old stalled-body test (TimeoutError).
  - S1 alone: red 1 of 1. All three outcomes are {error: TypeError 'terminated'} where transient, failed and null were expected.
  - S2 alone: red 1 of 1. All three outcomes are {error: AbortError 'This operation was aborted'}.
- verified, M18q (the masking case): M18 with `quick` set to requestTimeoutMs 1. Result: red 2 of 163, S1 and S2. The old stalled-body test passes, which reproduces the finding.
- verified, the timeout cannot pass S2: on the unchanged source, with `limits` set to requestTimeoutMs 1 and S2 run alone, it is red 1 of 1 with headers [false, false, false] where [true, true, true] was expected.
- verified: every mutation was an exact single-match edit, then `npx vitest run tests/github-oauth.test.ts --reporter=json` (with `-t` for the runs alone), then a restore and a sha256 check. src/main/github-oauth.ts came back as 7cbf1a73... and the test file as 60c2b237... each time. cwd /home/user/task5-handoff/wt/gsi-u1, TMPDIR a private scratch directory, Node 24.21.0.
- verified: npm run typecheck (NODE_OPTIONS=--max-old-space-size=2048), npm run lint and npx eslint e2e all exit 0. tests/github-token-literals.test.ts gives 2 of 2, and tests/github-app-constants.test.ts gives 4 of 4. The test file has no non-ASCII byte.

What you got wrong:
- mistake: my first version of S1 and S2 asserted the three calls one by one.
  - where: tests/github-oauth.test.ts, both new tests, before this commit.
  - impact: under M18 each test stopped at requestToken's assertion, so the proof did not show requestDeviceCode and fetchLogin rejecting. Nothing delivered was affected.
  - cause: I wrote the assertions before I looked at what a red run would print.
  - fix: each test now collects its three outcomes and asserts them once.
  - verified: the M18 runs above show all three outcomes as rejections, in both tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…inite-number rule, a digits-only reset, and fetchLogin's strict UTF-8

What changed (tests/github-oauth.test.ts, seven tests, 163 become 170):
- (a) A new test (:559), "a 201 is a 2xx like 200 (5.3 row 6)". A 201 token answer gives tokens, and a 201 step-1 answer gives the code.
- (b) Two rows in the "%s is transient on a token request, and failed at step 1" table: a JSON member that overflows to Infinity (`1e400`) and to -Infinity (`-1e400`).
  - Their body comes from a new helper `overflowing` (:283). It is a whole token answer and a whole step-1 answer at once, so both halves of each row depend on the rule.
  - The helper's comment records why the rule is kept (below).
- (c) Three rows in the "rateLimitWait: %s" table: a 403 with remaining 0 and a reset written as a fraction (`<epoch>.5`), as hex (`0x...`) and with an exponent (`...e1`). Each expects the 60 s floor. A comment says Number() reads each one as 120 s away (120.5 s for the fraction).
- (d) A row in the "fetchLogin: %s gives null" table: invalid UTF-8 (a 0xFF byte) in `bio`, outside the login, gives null.

Why: review finding U1-R10 (S4). The code builds four guards that no test covered; each mutation below stayed green at 161 of 161 (re-run here at 163, see below). GitHub is unlikely to send any of these answers, so the impact is low, but a guard nobody can see removed is not a guard.
- (a) src/main/github-oauth.ts:167 `response.ok` implements 5.3 row 6, "2xx". Only 200 carried tokens or a code in the tests.
- (b) :136 `Number.isFinite(item)`. Spec 5.3 row 4 says only "a flat object of strings and numbers", so the finite rule is my refinement. The finding asked to pin it with its reason or drop it through the lead. I pinned it. The reason:
  - JSON.parse reads a number literal beyond a double's range as Infinity or -Infinity, a value the body never wrote. The rule makes such a body unparseable (row 4: transient, failed at step 1) instead of reading it lossily.
  - It matters most for an expiry. An `expires_in` written as 1e400 is malformed without the rule, which clears the account on a refresh (6.5: `malformed -> clearIf`). With the rule it is transient, which only holds the account.
- (c) :67 `EPOCH_SECONDS.test(reset)` implements 5.4's "a valid epoch". Number() also accepts a fraction, `0x` hex and exponent forms.
- (d) :215 `fatal: true` in loginOf. parseBody's twin is already pinned by the "invalid UTF-8" row of the OAuth table.
- src/ is unchanged. These are regression pins: each is green on the unchanged code and red under its named mutation.
- Not changed: the finding's other survivors (M14, M17, M26, M45) are equivalent mutants, as the review itself records, so nothing pins them.

What it touched:
- tests/github-oauth.test.ts only: one helper, one test and six table rows, 24 lines added.
- No src/ change, so docs/ARCHITECTURE.md is not staged. No ADR, map row or domain term owed.

What you verified:
- verified: on the unchanged source (sha256 7cbf1a73...), tests/github-oauth.test.ts gives 170 of 170.
- verified, M30: :167 `if (response.ok)` changed to `if (response.status === 200)`. Result: red 1 of 170, the 201 test. The token half gave {kind:'unusable', status:201} and the step-1 half {kind:'failed'}, where tokens and a code were expected.
- verified, M23: `(typeof item === 'number' && Number.isFinite(item))` changed to `(typeof item === 'number')`. Result: red 2 of 170, both overflow rows, with {kind:'tokens', ...} where transient was expected.
  - With the table's two asserts swapped for one run (test file restored after, sha256 6ff46216... matching), both rows are red at step 1 too, with {kind:'code', ...} where failed was expected.
- verified, M06: `reset !== null && EPOCH_SECONDS.test(reset) && Number` changed to `reset !== null && Number`. Result: red 3 of 170, the three reset rows: 120500 (the fraction), 120000 (hex) and 120000 (the exponent), where 60000 was expected.
- verified, M44: loginOf's `new TextDecoder('utf-8', { fatal: true })` changed to `new TextDecoder('utf-8')`. Result: red 1 of 170, the invalid-UTF-8 row, with {value:'someone'} where {value:null} was expected.
- verified, without the new pins: with the test file put back to b30fc42 for the run and then restored (sha256 matching), M30, M23, M06 and M44 are each red 0 of 163.
- verified, the reason in (b): a throwaway test file, deleted after the run, sent a 200 JSON token answer with `"expires_in":1e400`. It gave transient on the unchanged source and malformed under M23. On Node 24.21.0, JSON.parse('{"a":1e400,"b":-1e400}') gives { a: Infinity, b: -Infinity }.
- verified: every mutation was an exact single-match edit, then `npx vitest run tests/github-oauth.test.ts --reporter=json`, then a restore and a sha256 check. src/main/github-oauth.ts came back as 7cbf1a73... each time. cwd /home/user/task5-handoff/wt/gsi-u1, TMPDIR a private scratch directory.
- verified: npm run typecheck (NODE_OPTIONS=--max-old-space-size=2048), npm run lint and npx eslint e2e all exit 0. tests/github-token-literals.test.ts gives 2 of 2, and tests/github-app-constants.test.ts gives 4 of 4. The test file has no non-ASCII byte.

What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…dy holds is a failure result, not a rejection

What changed:
- src/main/github-oauth.ts, readBounded: after `if (!response.body) return new Uint8Array(0);`, the line `if (response.body.locked) return null;` (:113).
  - A locked body becomes the same null a failed read gives: transient on a token request, failed at step 1, null from fetchLogin.
  - The doc comment now names that case. Such a body can be neither read nor cancelled here, so "always cancelled and released" now says "a body this function reads".
- tests/github-oauth.test.ts: a two-row test.each (:260), "a Response whose body is %s is transient, failed at step 1, and null for fetchLogin, never a rejection".
  - The rows are a body locked with getReader() and a body already read with `await response.text()`.
  - Each row feeds requestToken, requestDeviceCode and fetchLogin through an injected fetcher, wraps each call in settle(), and asserts all three outcomes at once.
  - The fetcher records `response.body.locked` and the test asserts it outside, where it cannot be swallowed. A failed expect inside a fetcher would be row 1 (transient) and would pass the test for the wrong reason.
- docs/ARCHITECTURE.md: the U1 sentence's "nothing in it rejects" now names the case: "not even on a body it cannot read (one that fails mid-read or that another reader already holds)".

Why: review finding U1-R8 (S3). readBounded called `response.body.getReader()` outside its try (:112 before this change). getReader throws `TypeError [ERR_INVALID_STATE]: Invalid state: ReadableStream is locked` on a locked body, so requestToken, requestDeviceCode and fetchLogin all rejected. That breaks the module's own promise (:6, "Nothing here rejects"), spec 3.9 layer 1, and the map sentence.
- Node's fetch never returns a locked body. An injected fetcher can, or an e2e replacement of globalThis.fetch that hands back one Response for repeated polls, and U5's detached `void this.poll(flow)` would then end on an unhandled rejection.
- Set aside: moving getReader into the try. getReader throws only when the stream is locked, so the explicit check is the same guard, and it keeps the reader non-optional in the finally.
- Set aside: cancelling the locked body first. cancel() on a locked stream only rejects, and the lock belongs to another reader.

What it touched:
- src/main/github-oauth.ts (readBounded and its comment, :106-113).
- tests/github-oauth.test.ts (one test.each, 20 lines; 170 tests become 172).
- docs/ARCHITECTURE.md (the U1 sentence in the "GitHub sign-in" paragraph, line 38).
- Spec 3.9 layer 1 and 5.1 ("the body is always cancelled or released": unchanged for every body the wire reads). No ADR: a bug fix with one obvious remedy.

What you verified:
- verified, red first: on the unchanged source (sha256 7cbf1a73...), both new rows failed, 2 of 172. In each row all three outcomes were {error: TypeError 'Invalid state: ReadableStream is locked', code ERR_INVALID_STATE} where transient, failed and null were expected. The locked assertion, which runs first, passed, so both bodies really were locked.
- verified: after the change (sha256 745eec88...), tests/github-oauth.test.ts gives 172 of 172.
- verified, G-R8 guard removal: the new line deleted. Whole file: red 2 of 172, both rows. The locked row alone: red 1 of 1. The already-read row alone: red 1 of 1.
- verified, the earlier pins still bite on the changed function:
  - U1-R9's M18 (:121 `} catch { return null; } finally {` changed to `} finally {`) gives red 3 of 172: the two R9 tests and the old stalled-body test.
  - U1-R10's M30, M23, M06 and M44 give red 1, 2, 3 and 1 of 172.
- verified: every mutation was an exact single-match edit, then `npx vitest run tests/github-oauth.test.ts --reporter=json` (with `-t` for the runs alone), then a restore and a sha256 check. src/main/github-oauth.ts came back as 745eec88... each time. cwd /home/user/task5-handoff/wt/gsi-u1, TMPDIR a private scratch directory, Node 24.21.0.
- verified: npm run typecheck (NODE_OPTIONS=--max-old-space-size=2048), npm run lint and npx eslint e2e all exit 0. tests/github-token-literals.test.ts gives 2 of 2, and tests/github-app-constants.test.ts gives 4 of 4. The changed src and test files have no non-ASCII byte, and src/ has no `client` + `_secret`.

What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
StepenkoAnatoli and others added 29 commits October 18, 2026 21:15
…cked by a PASS grade entry or an accepted reference at its tier; the lead-direct profile exempt (E2, E3)

what changed:
- route.mjs: the top-level policy key "evidence": "typed" (EVIDENCE_TYPED), the adopting switch in the shape of "difficulty-tiers": any other value is exit 2; absent, a policy loads as before (the fixtures host-policy-1..5). Under the key every qualified-for entry <category>[@t<k>] (bare reads @t1 under an enforced-tiers policy; untiered, the tier is ignored) needs a PASS grade entry for its category at tier >= k (a tierless grade is T1) or a resolved reference (E1) for it at tier >= k; PARTIAL and FAIL support nothing, free text supports nothing, and an entry with nothing typed behind it is exit 2 naming the profile and the entry. The validator still counts no passes. Extra references that support no entry are allowed. The lead-direct profile (economics.lead-direct-profile) is exempt from the rule (E3), not from E1: its typed lines are still resolved.
- route.test.mjs: three tests - the key and the refusal (host-policy-5 as it stands is refused at cc-fable-high fix@T2 once typed; the smallest typed-valid derivative loads; the onboarding state qualified-for [] loads), the support rules (PARTIAL, FAIL, tiers, untiered, extra references), and the exemption (it follows the economics key; the exempt profile's references still resolve).
why: deliverable A (E2, E3) of briefs/policy6-builder.md. E3's alternative, dropping the lead-direct profile's qualifications, would make `direct` unroutable for every bounded task (the lead's reason); the proposal has no exemption clause (friction item 23, README commit).
what it touched: docs/orchestration/smartrouter/route.mjs, docs/orchestration/smartrouter/route.test.mjs. No policy, ledger, usage, decision, outcome or calibration file. Not a declared code path: no docs/ARCHITECTURE.md change.
what you verified:
- red first (verified, cwd /home/user/task5-handoff/wt/sr-policy6; the new tests against the previous commit's route.mjs in a scratch copy): 127 tests, 124 pass, 3 fail - the three new tests.
- green (verified): 127 pass; the goldens and the host-policy-1..5 assertions unchanged.
- guard-removal (verified, scratch copies): the key's value check, the rule gated on the key (applied always: 15 tests red, the fixtures' included), the rule itself, PASS-only, tier >= k, the untiered tier-blindness, references as support, the E3 exemption - each turns at least one E2/E3 test red.
- node --check route.mjs exit 0; npx eslint route.mjs route.test.mjs exit 0 (verified).
what you got wrong:
- mistake: the E3 test's last assertion expected an owner refusal from the ref-fail reference; where: route.test.mjs, the E3 test; impact: the test would have asserted the wrong reason (nothing delivered affected); cause: resolveReference checks `accepted` before the owner, so ref-fail is refused as verification-failed first; fix: the assertion uses REF_T2 (accepted, owned by cc-opus-high); verified: the suite re-run, 127 pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…production-derived entries rewritten into typed references or dropped (the lead's table), with the table test and the revision-6 routing

what changed:
- host-policy.json: policy-revision host-policy-6 and "evidence": "typed" after "difficulty-tiers"; the lead's E5 table. cc-opus-high keeps build-ui@T3 (references sr-policy-2, -3, -4, sr-ci-item4, sr-policy-5, each 'would be T3 by the table'), review@T1 (sr-host-review, sr-host-rereview1, -2, 'would be T1'), fix@T2 (sr-host-fix1 seq 4, sr-host-fix2 seq 4, 'would be T2'), pin-tests; drops build-core@T3, build-tests@T3, research-collect@T2, whose revision-5 lines stay as prose with '; entry dropped in host-policy-6 (no outcome row to reference)'. cc-fable-high: fix@T2 dropped to bare fix (its U5 fix-round line suffixed), review@T1 gains the gsi-u5-rereview4 reference. cc-haiku-high: the fix-x2-1 line rewritten in place as 'outcome: fix-x2-1 fix@T3 accepted 2026-10-18 (trace/outcomes/fix-x2-1-3.json); would be T3 by the table: one pass at T3, not a qualification'. cc-haiku-low: docs-mechanical dropped. cc-fable-xhigh and every calibration-derived profile unchanged. Every reference resolves against the committed store (the validator loads the policy).
- route.test.mjs: the live policy's feature-off derivative is written beside a copy of the committed store (ledger, decisions, outcomes), since its references resolve against the policy file's directory (E1). Three tests: the table applied (only the revision, the key and four profiles change; dropped lines suffixed, the rewritten line in place, the references appended; no delegate holds build-core, build-tests, research-collect or docs-mechanical); every one of the thirty delegate entries checked by the validator (without its PASS grades it is refused, unless it stands on references, which then carry it alone above T1); the revision-6 routing (E6, computed with decide and asserted).
why: deliverables B and D of briefs/policy6-builder.md (E4, E5, E6). The 'would be' notes give each decision's permitted-file count; sr-policy-5's request had six permitted files (the brief's table says 'five and nine files', which counts the revision-5 line's units).
what it touched: docs/orchestration/smartrouter/host-policy.json, docs/orchestration/smartrouter/route.test.mjs. No adapter, ledger, usage, decision, outcome or calibration file. Not a declared code path: no docs/ARCHITECTURE.md change.
what you verified:
- red first (verified, cwd /home/user/task5-handoff/wt/sr-policy6; the new tests against the revision-5 policy in a scratch copy): 130 tests, 127 pass, 3 fail - the three revision-6 tests.
- green (verified): 130 pass; the goldens and the host-policy-1..5 assertions unchanged; the live-policy tests (P2-4, committed policy (i), Item 4, the rate CALIBRATION tests) green under revision 6.
- routing under revision 6 (verified, decide): fix@T1 -> cc-haiku-high ($3.1865, reviewer cc-sonnet-xhigh), fix@T2 -> cc-opus-high ($6.95; eligible now cc-opus-high alone), review@T1 -> cc-sonnet-xhigh ($3.09), build-ui undeclared (T2, 250000) -> cc-opus-high ($8.09), pin-tests@T1 -> cc-haiku-low ($1.06), each as under revision 5; build-core (T3), build-tests (T2), research-collect (T2) and docs-mechanical (T2 undeclared, T1 declared) blocked at their medians - every delegate at Gate 2 '<id> not qualified for <category>', cc-fable-xhigh at Gate 5 'direct unsuitable: <n> tokens > direct-max-tokens 15000'; at 15000 tokens each goes direct.
- guard-removal (verified, scratch copies of the policy): the key removed, build-core@T3 kept, cc-fable-high fix@T2 kept, the sr-host-fix references removed, a reference dated a day off, docs-mechanical kept - each turns the revision-6 tests red.
- npx eslint route.mjs route.test.mjs exit 0 (verified).
what you got wrong:
- mistake: the entry-count assertion said 26; where: route.test.mjs, 'every calibration-derived entry is supported'; impact: a false red in this commit's own test (nothing delivered affected); cause: miscounted the fourteen delegate profiles' entries (30); fix: 30; verified: suite re-run, 130 pass.
- mistake: the E6 test expected the bare Gate 2 reason for every delegate; where: the routing test; impact: false red; cause: Gate 2 appends a headroom shortfall ('; context: ...') for profiles whose headroom is below the request; fix: the reason must start with '<id> not qualified for <category>'; verified: suite re-run, 130 pass.
- mistake: expected undeclared docs-mechanical at T1; where: the routing test; impact: false red; cause: D3's undeclared default is T2 for a normal-risk category (the T1 floor only bounds a declared tier); fix: T2; verified: suite re-run, 130 pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…nces the lead would paste; every command validates the typed policy (E7)

what changed:
- route.mjs: learn() keeps, per outcome in a stream, its ledger seq and the tier its decision declares (null for none; categoryMap's additive field from E1). rate passes a reference builder: 'outcome: <task> <category>[@<declared tier>] accepted <UTC day of the outcome record> (<path>)', the path to trace/outcomes/<task>-<seq>.json relative to the policy file's directory, so E1 resolves it. A qualify proposal carries `references` (one per accepted outcome behind it) in qualification-stats.json, and its PROPOSAL line ends with '; typed evidence to paste: "<line>", "<line>", ...' (JSON strings: valid elements of the evidence array). The proposal's reason is unchanged, demotions carry no references, and decide's pending-proposal Uncertainty line is unchanged (it calls learn() without the builder).
- route.test.mjs: two tests - every command (decide, dispatch, outcome, status, rate, usage) refuses a policy with an unresolvable reference, exit 2 with the reason, and writes nothing; the qualify proposal's paste lines (tiered: q-1..q-3 fix@T2; untiered: u-1..u-3 build-core, bare) resolve when pasted, and the proposal is then gone.
why: deliverable A (E7) of briefs/policy6-builder.md. 'The typed reference line' is read as one line per accepted outcome behind the proposal (the qualification rules count passes; the validator needs one); the lead pastes what the rules call for.
what it touched: docs/orchestration/smartrouter/route.mjs, docs/orchestration/smartrouter/route.test.mjs. No policy, ledger, usage, decision, outcome or calibration file. Not a declared code path: no docs/ARCHITECTURE.md change.
what you verified:
- red first (verified, cwd /home/user/task5-handoff/wt/sr-policy6): 132 tests, 131 pass, 1 fail - the paste test. The every-command test was green before the change: the commands already load through loadPolicy; it pins that (its guards below).
- green (verified): 132 pass; the goldens, the host-policy-1..5 assertions and the existing qualify assertions (reason text) unchanged.
- guard-removal (verified, scratch copies): the paste suffix, the declared-tier-or-bare label, the path relative to the policy file, status validating, rate validating - each turns an E7 test red.
- node --check route.mjs exit 0; npx eslint route.mjs route.test.mjs exit 0 (verified).
what you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…nce (the two forms, resolution, E2-E4, the lead's table, the routing consequence from decide with the remedy), how the onboarding proposal was applied, the regenerated table, policy findings, friction items 23-25

what changed:
- README.md: a section 'Calibration-first evidence (host-policy-6)': the proposal and its unchanged draft status quoted, the two typed forms (grade entry; 'outcome: <task-id> <category>[@t<k>] accepted <ISO date> (<path>)'), resolution relative to the policy file's directory, the supported-entry rule with its refusal text, the lead-direct exemption, free text as prose, the lead's E5 table with every typed line, and the routing consequence (E6) as decide prints it (a default build-core with its gate walk, docs-mechanical at T1, fix at T2) with the remedy: run 4 gains one T1 host-pack cell for each of build-core, build-tests, research-collect and docs-mechanical. Under 'Qualification table': 'How the onboarding proposal was applied (host-policy-6)' (E1-E7, the builder's two choices named), 'The table (host-policy-6, generated from host-policy.json)' with a typed-references column and the revision-6 routing paragraph; the host-policy-5 table kept as history (its fixture). The file table names revision 6 and the host-policy-5 fixture; 'How to add evidence' and the learning loop describe the typed reference and the qualify proposal's paste lines. 'Policy findings (host-policy-6)', ten items (among them: the proposal read from SmartRouter main, the draft status, the owner check beyond E1, a host-policy-6 fixture will not load from trace/policies/, the four categories without a delegate). Friction items 23 (no clause for the lead's own profile), 24 (the outcome form carries no tier; this host added [@t<k>]) and 25 (the store's base: --trace in the proposal, the policy's directory here).
- route.test.mjs: three README tests - the host-policy-6 table row for row against host-policy.json (references column included), the calibration-first section (forms, status, rule texts, every typed line, decide's output on the live policy, the remedy), and the applied section, the findings and friction items 23-24.
why: deliverable E of briefs/policy6-builder.md.
what it touched: docs/orchestration/smartrouter/README.md, docs/orchestration/smartrouter/route.test.mjs. No adapter, policy, ledger, usage, decision, outcome or calibration file. Not a declared code path: no docs/ARCHITECTURE.md change.
what you verified:
- red first (verified, cwd /home/user/task5-handoff/wt/sr-policy6): the three README tests written before the README: 135 tests, 132 pass, 3 fail.
- green (verified): 135 pass; the host-policy-5 README tests (its table, now history, and the tiers section) still green.
- guard-removal (verified, scratch copies of the README): a table row's references, the remedy sentence, one gate-walk line, the E7 bullet, friction item 24's title, finding 5's fixture path - each turns one README test red.
- npx eslint route.mjs route.test.mjs exit 0 (verified).
what you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…cy file's directory ('..' or symlink); rate offers nothing to paste for a store outside it (sr-policy-6 fix round 1)

What changed: route.mjs gains inside(dir, path) - the path lies inside dir by its spelling (relative() neither
absolute nor '..'-led) and, when both exist, after symlinks (realpathSync). resolveReference refuses a reference
whose resolved file is not inside the policy file's directory, before any existence check: '<path> leaves the
policy file's directory <dir>: a reference names an outcome file under it' (exit 2 through loadPolicy). rate
computes the same for --trace against the policy directory: outside, a qualify proposal carries no references
and its PROPOSAL line ends '; no typed evidence to paste: the outcome store <dir> is outside the policy file's
directory <dir>, and a reference cannot leave it (E1)'. README: the (b) form says the path cannot leave the
directory, the resolution list names it first, the two E7 bullets give the note.

Why: review finding F1 (S2). referenceForm refused only absolute and backslash paths, so
'../x/trace/outcomes/<t>-<s>.json' and 'trace/../../x/...' resolved against any store, and a store never
committed with the policy (a temp trace) could back a live qualification; rate printed such '../' paste
lines when --trace lay outside the policy directory. A symlinked trace/ is the same class and is closed too.
Alternative set aside: refusing every '..' segment in referenceForm (form only) - it misses a symlink and
refuses harmless inside spellings; the finding's relative() rule plus realpath covers both.

What it touched: docs/orchestration/smartrouter/route.mjs (inside, resolveReference, rate),
route.test.mjs (tests 'E1 (F1)', 'E7 (F1)', 'README (F1)'; symlinkSync import), README.md (calibration-first
section, applied E7 bullet, learning loop). No policy, ledger, outcome, decision or calibration file.

What you verified: red first (verified) - 'E1 (F1)' and 'E7 (F1)' failed against the previous route.mjs (the
'../elsewhere' reference loaded; rate printed the '../forge/trace/outcomes/q-1-3.json' paste lines); 'README (F1)'
failed against the previous README. Green (verified): node --test docs/orchestration/smartrouter/route.test.mjs
138/138, cwd /home/user/task5-handoff/wt/sr-policy6. Guard proofs (verified, scratch copies): the spelling check
removed -> 'E1 (F1)' red at '../nowhere' ('does not exist' instead); the realpath check removed -> 'E1 (F1)' red at
the symlinked trace/; the resolver's call removed -> 'E1 (F1)' red; rate's storeInside forced true -> 'E7 (F1)' red;
the note removed -> 'E7 (F1)' red. node --check exit 0, eslint exit 0, route.mjs status sr-policy-6 on the live
policy exit 0 (all twelve references resolve) (verified).

What you got wrong and fixed: mistake: the first test insertion used String.replace with a replacement string
holding template-literal '$' sequences; where: scratch insertion step; impact: route.test.mjs garbled in the
working tree, nothing committed; cause: '$' patterns are special in a replacement string; fix: git checkout of
the file and an insertion by split/join; verified: diff 45 lines, suite 138/138.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
… record of its dispatch; a superseded one is refused, as rate counts it (sr-policy-6 fix round 1)

What changed: route.mjs factors the dispatch key of latestOutcomes into outcomeKey(r) and uses it in
resolveReference: once the outcome record behind the reference (<task> seq <seq>) is found, the latest record
for the same dispatch (latestOutcomes of the store) must be that record, else the reference is refused:
'<path> is superseded by outcome record <task> seq <n> (<value>) for the same dispatch (seq <d>)' (exit 2 through
loadPolicy). README: the resolution list and the applied E1 bullet name the rule.

Why: review finding F2 (S2). E1 resolved a reference to an 'accepted' outcome record that a later outcome record
for the same dispatch superseded, ignoring spec-v2 §10.5 and decision-trace.md §4 supersession, which the
adapter's own latestOutcomes() and rate apply; a qualification could rest on a dispatch the ledger records as
failed. Alternative set aside: a second copy of the latestOutcomes key in the resolver - one rule in one place
(outcomeKey) keeps the resolver and the learning loop from drifting apart.

What it touched: docs/orchestration/smartrouter/route.mjs (outcomeKey, latestOutcomes, resolveReference and its
comment), route.test.mjs (tests 'E1 (F2)', 'README (F2)'), README.md (calibration-first section, applied E1
bullet). No policy, ledger, outcome, decision or calibration file; the twelve committed references are each the
latest outcome of their dispatch and still resolve.

What you verified: red first (verified) - 'E1 (F2)' failed against the F1 commit's route.mjs (REF_T2 loaded
beside a copy of REF_STORE whose ref-t2 dispatch was recorded again as verification-failed, seq 4); 'README (F2)'
failed against the previous README. Green (verified): node --test docs/orchestration/smartrouter/route.test.mjs
140/140, cwd /home/user/task5-handoff/wt/sr-policy6. Guard proofs (verified, scratch copies): the supersession
check removed -> 'E1 (F2)' red; 'last' taken as the record itself -> 'E1 (F2)' red; outcomeKey made per record
(no dispatch key) -> 'E1 (F2)', 'F4: a superseding outcome ...' and three other rate tests red (the rule is
shared). node --check exit 0, eslint exit 0, route.mjs status sr-policy-6 on the live policy exit 0 (verified).
Residual (expected, not tested): an Outcome that carries its own ts and has no outcome record in the store is
resolved by its ts alone, so supersession cannot be checked for it; this host's Outcomes carry no ts.

What you got wrong and fixed: mistake: the first 'E1 (F2)' asserted rate's fix@T2 stream as accepted 0,
verification-failed 1; where: route.test.mjs 'E1 (F2)' before this commit; impact: a false red, nothing
delivered; cause: the copied store also holds ref-fail, a verification-failed fix@T2 on cc-opus-high; fix: assert
the shift, [1, 1] before the second outcome and [0, 2] after; verified: suite 140/140.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…olves its references against <root> (policyRoot), so revision 7 can pin the revision-6 assertions to a fixture

What changed: route.mjs gains policyRoot (the policy file's directory, except a fixture under <root>/trace/policies/ resolves against <root>); loadPolicy uses it; route.test.mjs gains "E1 (fixture root)"; README (the freeze rule under "How to add evidence" and policy finding 5 of host-policy-6).
Why: the builder's note (policy finding 5): under E1 a fixture copy of a typed policy would look for trace/policies/trace/outcomes/ and be refused, so the next revision could not pin the revision-6 assertions as every revision did. The alternative set aside: a --policy-root flag (a new flag, a non-goal) or test derivatives beside a store copy (works for tests only, not for the CLI's --policy).
What it touched: the E1 resolution base; no change to the F1 rule (the path still cannot leave the resolved root).
What you verified: the new test red at the integration head and green after the change (verified); guard proof: restoring dirname(resolve(path)) turns it red (verified); suite 141/141, eslint and node --check exit 0 (verified).
What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…s); the unit's four reports; usage reading 21:14Z; RUN.md integration entry

What changed: trace/outcomes/sr-policy-6-4.json, one ledger row, qualification-stats.json regenerated by the adapter, one usage reading, four report files, RUN.md.
Why: the onboarding unit passed refute-first review (ACCEPT after one fix round) and is integrated.
What you verified: route.mjs outcome exit 0 (verified); rate proposes nothing (verified); suite 141/141 on the integration head (verified).
What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
What changed: one RUN.md line. Why: the ledger records every outward step. What you verified: push landed (fe89efd on the remote). What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…the unit's six reports; RUN.md entry

What changed: trace/outcomes/sr-kit-4.json, one ledger row, qualification-stats.json regenerated by the adapter, six report files, RUN.md.
Why: the kit unit ended with one S4 open after two fix rounds; the lead closed it and accepted the unit (no S1-S3 open).
What you verified: route.mjs outcome exit 0 (verified); the kit's 57 tests OK and the regression gate through the hook on 229aa1f (verified).
What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…263000 from the accepted sr-kit outcome

What changed: host-policy.json (headroom-tested and the ratification line closing the profile's evidence); route.test.mjs (the host-policy-6 table test carries the one ratified raise and asserts the line); README (the two headroom mentions); qualification-stats.json as rate regenerates it without the proposal.
Why: rate proposed the raise under the never-lowered rule (largest completed run, rounded up); the lead ratifies by editing the policy, as for sr-policy-3, fix-x2-1 and sr-policy-5.
What you verified: suite 141/141 (verified); guard proof: reverting the raise turns the table test red (verified); rate prints no proposal after ratification (verified); lint and node --check exit 0 (verified).
What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…ct pack, onboarding proposal Item 2); usage reading 21:59Z

What changed: briefs/pack-builder.md; one usage reading. Why: the kit (PR #106) leaves the shared pack as a stub; the proposal needs it for cross-host comparison. What you verified: the kit's pack format read in full (verified). What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…uild-ui@T3) and dispatch (seq 2)

What changed: one decision file, two ledger rows. Why: the shared-pack unit is dispatched through the adapter. What you verified: decide printed DifficultyTier T3, route delegate cc-opus-high (verified); dispatch recorded as decided (verified). What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…ce check and the generated qualification table, onboarding proposal Items 4.3-4.4)

What changed: briefs/evidence-check-builder.md. Why: the kit produces the layout; the proposal's CI check and table are the last onboarding units. What you verified: the three evidence folders' scores shapes and run 3's adjudication_schema read (verified). What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…h, build-ui@T3) and dispatch (seq 2)

What changed: one decision file, two ledger rows. Why: the evidence-check unit is dispatched through the adapter. What you verified: decide printed DifficultyTier T3, route delegate cc-opus-high (verified); dispatch recorded as decided (verified). What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…n 4 waits for the seven-day reset

What changed: one RUN.md entry. Why: the ledger records every launch and the usage decision. What you verified: the two dispatch rows and the 21:59 reading (verified). What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
… the unit's six reports

What changed: trace/outcomes/sr-pack-3.json, one ledger row, qualification-stats.json regenerated by the adapter, six report files.
Why: the shared-pack unit ended with one S2 open after two fix rounds; the lead closed it (the reviewer's one-token pin change, red-first) and accepted the unit.
What you verified: route.mjs outcome exit 0 (verified); rate proposes nothing (1110316 < 1263000) (verified); the kit's 79 tests OK and the gate through the hook on the lead's commit (verified).
What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…ned as draft

What changed: one RUN.md entry. Why: the ledger records every outward step. What you verified: PR #107 exists and the branch head is aa10cb2 (verified). What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…ns); the unit's six reports

What changed: trace/outcomes/sr-evidence-3.json, one ledger row, qualification-stats.json regenerated by the adapter, six report files.
Why: the evidence-check unit ended with one S3 open after two fix rounds; the lead closed it (the legacy class closed to runs 1-3 by run id, red-first) and accepted the unit.
What you verified: route.mjs outcome exit 0 (verified); the check and table suites OK and the gate through the hook on the lead's commit a19b3ae (verified).
What you got wrong: nothing to report in this commit; the lead's three failed cuts at integration are recorded in a19b3ae's message.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…492000 from the accepted sr-evidence outcome

What changed: host-policy.json (headroom-tested and the ratification line closing the profile's evidence); route.test.mjs (the host-policy-6 table test carries the two ratified raises since the fixture and asserts both lines); README (the two headroom mentions); qualification-stats.json as rate regenerates it without the proposal.
Why: rate proposed the raise under the never-lowered rule; the lead ratifies by editing the policy, as for every accepted run that exceeded the figure.
What you verified: suite 141/141 (verified); guard proof: reverting the raise turns the table test red (verified); rate prints no proposal after ratification (verified); lint and node --check exit 0 (verified).
What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
… opened; the onboarding units delivered

What changed: one RUN.md entry. Why: the ledger records the phase end. What you verified: PR #108 exists at a19b3ae (verified). What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…r request files, the 03:01Z reading, the reviewer note

What changed: docs/orchestration/smartrouter/briefs/run4-plan.md (the lead's run 4 plan: stage 4a on subject X2-1, the T1 host-pack cells, the kit and adapter extensions they need, budget, order after the reset); four briefs (run4-pack-4a-builder, run4-kit-cases-builder, policy7-builder, run4-packs-t1-builder); briefs/requests/ with the four adapter request files; trace/usage.jsonl gains the 2026-10-19T03:01:34Z reading; reports/reviewer-note-2026-10-19.md; RUN.md entry.
Why: the onboarding units are delivered and the seven-day window is at 0.80, the Gate 4 limit, so no unit can be dispatched before the reset on 2026-10-24 18:00Z; the preparation that spends nothing is done now so the dispatch at the reset is a file read. Alternatives set aside are named in the plan (C-1..C-3 as the 4a subject; four fix-labelled cells with a hand mapping).
What you verified: `node route.mjs usage` recorded the reading (verified, exit 0); the four request files parse as JSON (verified); no code path touched, so no suite run (nothing to run).
What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…ed trace at the ratification instant

What changed: docs/orchestration/smartrouter/route.test.mjs, the test "usage windows (host-policy-3)": the copied trace's usage.jsonl and ledger.jsonl are cut to records at or before 2026-10-18T02:00:00Z before rate runs on them, with a comment saying why.
Why: the test asserted that rate considers no reading pair on the committed trace because "the ratification is after every observation". That stopped being true with the 2026-10-19T03:01:34Z reading: the pair 21:59:28Z -> 03:01:34Z is clean (sr-pack and sr-evidence inside it, nothing in flight at either reading; the five_hour reset at 00:20Z drops that window's pair) and rate considers it for seven_day (measured 0.015372 per Mtok, within 10 % of the ratified 0.015, so no proposal). Red at HEAD (seven_day considered 1 !== 0, cwd docs/orchestration/smartrouter), green with the cut; the test now depends on records no later record can change, as the neighbouring usage test already does. Alternative set aside: asserting considered 1 on the live trace, which would go red again at the next clean pair.
What it touched: the test file only; no map row, no ADR.
What you verified: node --test route.test.mjs 141 pass 0 fail (verified, cwd docs/orchestration/smartrouter); the red before the cut seen in the same cwd (verified); rate on the live trace prints "no proposals" and stats.usage.seven_day.considered 1 (verified).
What you got wrong: the 03:01Z reading was recorded and committed (8898284) without re-running the adapter suite; where: the run 4 preparation commit; impact: one red test on build/github-sign-in for 40 minutes and in the refresh worktree, nothing merged with it; cause: "no code path touched" read as "nothing to run", but a trace record is an input of the suite; fix: this commit; verified: the suite above.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…erged, #107/#108 retargeted, refresh #109, the red suite and its fix; run 4 plan follow-ups

What changed: RUN.md entry 03:35 UTC; run4-plan.md gains the follow-ups (the R-2 kit hardening after run 4; the dropped entries stay dropped).
Why: the ledger records the operator's three decisions, the merges, the red suite with its cwd and the refresh PR, so a resumed session reads it from disk.
What you verified: nothing to run (docs only).
What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…artRouter run 4; MoonAliza items 4 and 5); RUN.md resume point

What changed: NEXT-CHAT.md (state on disk, what does not travel, the two opening messages, the plan summary, the conventions, the standing constraints); RUN.md resume point 03:45 UTC (#107 and #108 merged, main's gate, #109 at the operator's merge, triggers cancelled, next action).
Why: the operator opens new chats for both streams; a new chat starts from a fresh container and reads only the repository.
What you verified: main's gate at ac2a7bb (verified, run_regression PASS); nothing else to run.
What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…artRouter PR remains open

What changed: NEXT-CHAT.md (the #109 row, main at 311c36f, Chat A's first step); one RUN.md line.
Why: the operator marked #109 ready and its CI was green, so the lead merged it; the handoff must not send the new chat to a closed PR.
What you verified: the merge result (verified, 311c36f).
What you got wrong: nothing to report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdJ51CTnHT4Zet6eScsBBh
…ocks, the reset trigger armed

What changed: one RUN.md entry (the reading, the reading source on this host, the scheduled task, the prepared npm ci, machine notes).
Why: the run 4 units wait for the seven-day reset (2026-10-24 18:00Z); the next session needs the trigger and the host facts on disk.
What it touched: RUN.md only.
What you verified: the reading from get_usage; the lockfile is identical at 16f637f, 01a8604 and the head; npm ci exit 0.
What you got wrong and fixed: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n 3, C-1..C-3, T2); RUN.md resume point

What changed: briefs/fix-c1-c3.md (the lead's design: race the fetch and every body read against the bounded signal, never await a body cancel, bound reads as well as bytes; rejected alternatives; red-first tests and guard proofs; pre-mortem); the adapter request smartrouter/briefs/requests/fix-c1-c3.json (fix, T2); a RUN.md resume point.
Why: item 4 of the handoff; the unit is dispatched when Gate 4 admits it (blocked until 2026-10-24 18:00Z).
What it touched: docs only.
What you verified: a decide dry run on a scratch copy of the trace passes Gates 1-3 for cc-opus-high and blocks at Gate 4 (verified); the stream-cancel semantics the brief relies on, checked in Node 24 (verified); the real ledger is unchanged (verified).
What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew briefs, 13 adapter requests, PR text; RUN.md resume point

What changed: briefs/u5-themes-reviewer.md (one reviewer per theme T1-T8: holds, reachable, forbidden, decision space); briefs/integration-review.md (spec, invariants, mutation, breaker, with the earlier findings assigned to reviewers); requests review-u5-t1..t8, review-int-{spec,invariants,mutation,breaker}, u8-build; reports/pr-body-github-sign-in.md; RUN.md resume point replaced.
Why: item 5's lead-only work proceeds now; delegated work waits for the seven-day reset.
What it touched: docs only (run folder and adapter request files).
What you verified: decide dry run of all 13 on a scratch trace copy (verified): T5-T7 (review@T1) reach Gate 4 on six delegates; the nine review@T3 requests and u8-build (build-tests@T1) fail Gate 2 on every delegate under host-policy-6; no Gate 1 or 3 failure. Secret scan of the new files: none (verified).
What you got wrong: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants