Skip to content

2026-09-20

00:30 — Stage 1 /grill-with-docs, owner grill

Actor: Proposer (claude-opus-5, session chem-assistant-4b) Prompt: /grill-with-docs with that context and spawn agents to implement it, over the UI alignment work landed earlier in the same session — splash, first-run notice, design token palette. Artifact: the grill itself; no file yet. Rulings made by the owner, seven questions, one at a time:

  1. The Standing Statement is stated on first run by a blocking surface shown once per device. Rejected: a panel on Home; blocking on every cold start.
  2. Its Escalation control is layout-identical chrome that acknowledges and routes, not a tel: rendering Curated Escalation text. The tel: shape had already been built and shows that text with no origin, which D-95 forbids — the grill found that defect in this session's own work.
  3. First Run is judged by this browser's storage, and every read or write failure errs toward showing the statement again. Rejected: treating an unreadable store as acknowledged.
  4. The Boot Splash exists with a closed content set. Rejected: no splash; a worded status.
  5. The design extract binds colour, spacing and surface treatment only. Rejected: reference-only; reverting the tokens.
  6. Absorbed by 5.
  7. Standing Statement and First Run become glossary terms; the two surfaces stay in the ADRs.

Resolution: ADR-0064 (D-138, D-139, D-140), ADR-0065 (D-141), ADR-0066 (D-142), two CONTEXT.md terms, five PRD Coverage rows, and the implementing changes in the frontend. Coverage check: pass for this work — see the id-collision note below for why the gate is red.

00:44 — Id collision with a concurrent session

Actor: Proposer What happened: a second claude session, live in this repo, wrote .scratch/chemical-safety-assistant/rulings-2026-09-20.md mid-run, claiming ADR-0057–0063 and D-128–D-137 for seven unrelated rulings. This session's baseline ./init.sh had counted 299 ids and passed; the file appeared between gate runs.

Resolution: this session yielded and renumbered to ADR-0064–0066 and D-138–D-142. The yield mattered: before it, the PRD asserted that D-131 landed as the Boot Splash closed set, while the other allocation defines D-131 as "a document records its page count" — one id, two rulings, one Coverage row vouching for the wrong one.

A consequence the renumber left behind, found afterwards: five frontend files still cited D-128–D-132 in their docblocks, now pointing at the other session's rulings. 21 citations renumbered across main.css, FirstRunNotice.vue, SplashScreen.vue, FirstRunNotice.spec.ts and FirstRun.ts, then re-verified.

Coverage check: fail — 10 problems at the time, every one naming the other session's file. By the round-3 run those had become their ADR-0057–0063. No COVERAGE problem has ever named a file of this work. The root gate was green before this session started, so the red is newly theirs.

00:52 — Stage 1, ping-pong round 1

Actor: Griller — Agent(subagent_type: "fork"), NOT a cross-model grill. No second session was open; ListAgents confirmed it. A fork inherits this session's full context and therefore its blind spots, which is the weaker of the two options AGENTS.md permits. Recorded as a fork so nobody later reads this as claude-opus-4-8 evidence. Prompt: the standard four-check Griller prompt, verbatim. Defects raised: 6.

  1. FirstRunNotice.vue cited D-60 for "Home remains the landing route"; ADR-0029:16 puts that sentence in D-61.
  2. ADR-0066 named two converted surfaces where three were converted.
  3. "the visual layer" — a load-bearing term absent from CONTEXT.md.
  4. "app-authored chrome" — flagged as absent from CONTEXT.md.
  5. ADR-0065's MAY-list pinned the splash to DEFAULT_TITLE, which carries // TODO: Change this.
  6. PRD D-142 row's landing narrower than its evidence.

Resolution: all six. 1 fixed in FirstRunNotice.vue and in router/index.ts, the origin of the mis-citation. 3 replaced with the explicit list. 4 was resolved wrongly — see round 2.

01:05 — Stage 1, ping-pong round 2

Actor: Griller (fork, as above) Defects raised: 6. Four were round 1's own resolutions landing in the ADRs but not in the code: main.css still said "the visual layer only"; SplashScreen.vue still tied the name to DEFAULT_TITLE; FirstRunNotice.spec.ts still said "chrome". Plus:

  • SplashScreen.vue recommended "say what is happening in words" as the honest fix for a slow boot — which ADR-0065 lists as a rejected option. A reader following the docblock would have broken the ruling it sits under. Round 1 missed this entirely.
  • Round 1's resolution 4 was wrong. "chrome" is not an invented synonym: it is house vocabulary in nine pre-existing files, and it is narrower than Conversational Content, which CONTEXT.md says "may be generated by a model" — which an emergency control may not be. Replacing it widened what D-139 permits.

Resolution: all six. Resolution 4 reverted; CONTEXT.md gained a Chrome entry instead, which is what the grill check had actually been asking for. 0066-… renamed to match its title.

01:18 — Stage 1, ping-pong round 3 (the cap)

Actor: Griller (fork, as above) Defects raised: 4 — three of them in the Chrome gloss round 2 had just added.

  1. The gloss said "fixed wording… on screen", falsified by four of the ten real usages: EscalationTrigger.ts:41 and RoutingFloor.ts:47 use it of terms never displayed, BlankLayout.vue:11 of layout furniture, main.css:35 of a colour.
  2. D-139 said "Chrome, not Conversational Content" (disjoint) while the gloss said "narrower than" (subset). Both cannot hold.
  3. main.css claimed the extract binds type, which D-142 does not — and which this project explicitly refused when it rejected the extract's font-host link.
  4. ADR-0065's "the standing offline and anonymous line" collided with the newly-glossed Standing Statement, on the one screen that must not carry it.

Resolution: all four. The gloss now states the property common to all ten usages — authored, never generated, never restates what a document says — and the subset relation is stated the same way in CONTEXT.md, ADR-0064 and FirstRunNotice.vue.

Round cap reached, and overridden by the owner. AGENTS.md caps ping-pong at three rounds and says defects still landing at round 3 mean the stage input was too big. Defects were still landing, so the cap condition fired and is recorded here rather than glossed over.

The owner ruled: accept and close. The reason the cap's purpose does not apply: all three rounds returned a clean set diff, and not one of the sixteen defects touched the five decisions. Rounds 2 and 3 were almost entirely about a single glossary entry that round 2's own fix introduced and round 3 stabilised — the churn was the Proposer's, not the stage input's.

What that override costs, stated so it is not forgotten: all three Grillers were forks sharing this session's priors. The strongest defect of the session — SplashScreen.vue recommending the option its own ADR rejects — survived round 1 undetected. A cross-model round on ADR-0064..0066 is still worth running and has not been run.

Gate status at end of session

frontend  LINT PASS  TYPECHECK PASS  TEST PASS (732)  SEAM4 PASS   RESULT: PASS
root      COVERAGE FAIL  FIXTURE PASS  ADRSTATUS PASS  IDSTATUS PASS
          STACKCLAIMS PASS  CORPUS PASS                RESULT: FAIL

The root COVERAGE failures name only the concurrent session's ADR-0057–0063. Clearing them means writing PRD Coverage rows for ten rulings this session did not author, which would be fabricating landings. Not done, deliberately.

Artifact shas

  • Frontendchemical-safety-assistant-frontend @ aa493d5, "feat(app-shell): the app says what it is before it shows anything". 19 files. Gate green at commit; husky ran eslint and the related specs.
  • Workspace docs — NOT COMMITTED, deliberately. ADR-0064, ADR-0065, ADR-0066 and this file are written and unstaged.

Why the docs commit was held. CONTEXT.md and .scratch/chemical-safety-assistant/PRD.md hold both sessions' uncommitted work in the same files: this session's three glossary terms sit beside the concurrent session's Section Heading, and this session's five Coverage rows sit directly below its ten. That session is still writing — the COVERAGE problem count fell from 10 to 9 during this one. Committing those files would sweep a live session's half-finished work into this session's commit and assert its rulings had landed.

The alternative, committing only the three ADRs and leaving the PRD rows out, was rejected: it would put D-138..D-142 in the tree with no downstream Coverage rows, which is the "research evaporates into a spec" error the gate exists to catch.

Owed: commit the workspace docs once the concurrent session settles, then replace this section with the real sha. Until then this is a round log, not a traceable journal entry.

01:12 — Correction: the workspace docs were committed by the parallel session

Actor: Proposer (claude-opus-5, session chem-assistant-4b) Correcting: the "Artifact shas" section above, which states the workspace docs are "NOT COMMITTED, deliberately" and explains why the commit was held. That is now false, and this entry corrects it rather than editing it, because this journal is append-only.

What happened: while this session held its docs commit — the hold was correct at the time, since CONTEXT.md and PRD.md held both sessions' uncommitted work interleaved — the parallel session committed them itself.

  • d77eb24 and earlier — CONTEXT.md and PRD.md, carrying this session's Standing Statement, First Run and Chrome terms and its D-138..D-142 Coverage rows alongside its own work.
  • 729f78d — "adr: ADR-0064..0066 and today's journal reach git, written by a parallel session". 414 lines: the three ADRs and the first version of this journal file.

So the shas this entry owed are:

  • Workspace docs @ 729f78d (ADR-0064, ADR-0065, ADR-0066, this journal), with the glossary and Coverage rows landing in d77eb24 and its neighbours.
  • Frontend @ aa493d5, "feat(app-shell): the app says what it is before it shows anything".

The workspace did not record the frontend commit. app/chemical-safety-assistant-frontend was still a gitlink to bee439b, the pre-session state, so nothing in the workspace pointed at either session's frontend work. Bumped here.

Gate status now:

root      COVERAGE PASS  FIXTURE PASS  ADRSTATUS PASS  IDSTATUS PASS
          STACKCLAIMS PASS  CORPUS PASS               RESULT: PASS
frontend  LINT PASS  TYPECHECK PASS  TEST PASS  SEAM4 PASS  RESULT: PASS

Both gates green. The root red recorded earlier in this file was the parallel session's coverage debt and is closed by its own rows, not by anything this session did.

Still owed, and not done: the cross-model Griller round on ADR-0064..0066. All three rounds logged above were forks. A claude --model claude-opus-4-8 session was started for it but never registered as a peer, so the round has not run. The brief for it is written and ready.

01:30 — Stage 1, cross-model grill round (the one the fork rounds could not be)

Actor: Griller ×2 — Claude Fable 5.1 and Sonnet, each a general-purpose subagent with an explicit model override, cold-started, neither a fork. Both were pointed at the committed brief .scratch/chemical-safety-assistant/griller-brief-2026-09-20.md and told not to defer to the earlier rounds.

Correcting the three earlier round entries, which record that no second model was reachable: that was wrong. AGENTS.md says the Agent tool cannot pin claude-opus-4-8, which is true, and the Proposer over-read it as "no second model is reachable at all". fable and sonnet are pinnable and were available throughout. Two attempts at a claude --model claude-opus-4-8 terminal session both exited without registering as peers, which masked the simpler option. The prescribed Griller model was still not used; two genuinely different models were.

Defects raised: 5 by Fable, 5 by Sonnet, 3 of them the same defect found independently.

Fixed in app/chemical-safety-assistant-frontend @ 7505003:

  1. main.css claimed the alarm palette covered EscalationPanel; it did not. The panel was still writing raw red-700, red-50, red-900. The caveat surfaces were converted under D-142 and the alarm ones never were, so the comment described an intention. This is D-142's own consequence — "one palette rather than two" — violated by the component the supporting comment named as compliant. Found by Sonnet alone; three fork rounds read that comment and did not check it against the component.
  2. DefaultLayout claimed nothing but the Escalation panel used bg-alarm; the First Run Notice's control (D-139) was a second user added in the same batch. Sonnet.
  3. StandingStatement.vue still carried "first run … is not built here" as a live TODO. Both.
  4. FirstRunNotice.vue described an already-corrected router/index.ts mis-citation as a current fact. Both.

Ruled by ADR-0070 (D-146, D-147), because the remaining two defects sat inside Accepted, committed ADRs and AGENTS.md forbids editing those:

  1. "Chrome is the fixed part of Conversational Content" is set-theoretically false. Found independently by both models. Conversational Content is "everything the app says"; the Chrome gloss's own examples include a colour and terms documented in code as never displayed. D-146 rules the relation as overlap and withdraws D-139's supporting sentence. D-139's ruling is untouched.
  2. ADR-0066's four claims about the design extract carried no locator — its name appeared in four files, never with an id or a link, in a repository that cites its scanned PDF by page. Fable. D-147 records project id, file name, read date, and the provenance the project states about itself.

A risk the round disproved rather than confirmed. The brief asked both models to attack D-138's blocking surface rather than accept its mitigations. Sonnet traced App.vueverifyCorpus()readSnapshotVersion() and found every IndexedDB failure path resolves null rather than throwing, so pageReady always flips and the notice always renders with its one-tap escalation control. The mitigation holds, verified in code.

Coverage check: pass — 319 ids declared, all mapped downstream.

On the round cap. The three fork rounds are logged above as having closed stage 1 at the cap. This round is evidence for what that cap's override cost: two of the six defects here were first-order — a safety-surface palette claim that was simply untrue, and a set relation asserted as fact in an Accepted ADR — and no fork found either. Recorded so the next stage does not treat three fork rounds as equivalent to one cross-model round.

01:40 — Stage 2 /to-spec, round 1

Actor: Proposer (claude-opus-5), then Griller (Claude Fable 5.1, cold subagent, not a fork). Prompt: /to-spec, typed by the owner. Artifact: .scratch/chemical-safety-assistant/PRD.md @ 50289d4, amended further at 9e90743 (frontend) and the follow-up docs commit.

What the stage was. Not a new PRD. Six Coverage rows carried a Note saying their spec text was owed at the next spec pass: the rulings had landed in ADR-0064..0066 and ADR-0070 while the PRD body said nothing about the surface every user meets first. Added US-66..US-72 beside US-42/US-43, prose into §1 (design-source ranking), §4 (closed startup set, First Run judged by storage), §6 (the notice as a surface in front of the router) and §8 (its escalation control, the chrome overlap), and extended Seam 4 rather than adding a sixth seam — the owner's call on both questions.

Defects raised: 7. Five were Coverage rows whose Landed in cell named a section the decision is not stated in — D-138 claimed §8, D-139 claimed §6, D-140 claimed §8 when its own Note said §4, D-142 claimed §4 when the second site is Testing, D-146 claimed §9. One row's Evidence described D-100 rather than the D-139 it was for. All corrected against a section-by-section search rather than by re-reading the prose.

The two that mattered, and what they say about writing specs:

  1. The spec described a Seam 4 extension that had not been built. The owner chose "extend Seam 4"; the Proposer wrote the paragraph describing the extension and never added the assertions. Every test in Seam4NoNetwork.spec.ts calls acknowledgeFirstRunOnDevice first, so the boot path was asserted only by a unit spec that mounts the component with a memory-history router — which the same paragraph, two sentences earlier, says cannot see what this seam is for.
  2. The spec claimed built-output assertions for D-141 and D-142 that did not exist. Fable searched the whole frontend and found the two ids only in one docblock, as prose.

Resolution: both built rather than reworded down.

  • Two Seam 4 cases that deliberately do not acknowledge First Run: a virgin device is shown the statement and reaches Home in one tap; its escalation control lands on the Escalation surface with the artifact's origin visible (D-95). Both assert zero requests. Seam 4 is now five cases.
  • src/tests/components/OnePalette.spec.ts asserts D-142's one-palette consequence over the source, in CuratorSurfaceIsolation's shape. Falsified before being trusted: the original EscalationPanel defect was reintroduced and the spec failed with writes border-red-700, then restored. It carries two guard cases so a stale glob or pattern cannot let it pass vacuously.
  • D-141's closed set is now recorded in the spec as having no mechanical assertion, rather than implying one. The honest position is a reviewer and a docblock.

Why this round is the argument for the discipline. Defects 6 and 7 are the same species as the one the cross-model round found in stage 1: prose asserting that a check exists. A spec that describes tests nobody wrote is worse than a spec that admits a gap, because the next reader budgets for neither. Three fork rounds in stage 1 and a Proposer in stage 2 all produced it.

Coverage check: pass — 327 ids declared, all mapped downstream. Gates: root RESULT: PASS; frontend RESULT: PASS, 797 tests, Seam 4 at 5 cases.

01:55 — Stage 2, round 2 (two cold reviewers, split by subject)

Actor: Griller ×2 — Fable 5.1 on the spec, Sonnet on the test code. Both cold subagents with model overrides, neither a fork. Split so they would not duplicate: the test code committed at 9e90743 had never been reviewed by anyone but its author, which mattered because everything else now leans on it.

Sonnet, on the tests. Defects raised: 3 substantive, 1 observational. The first is the important one and it was proved rather than argued.

  1. The OnePalette allow-list let a safety surface off the hook. AskPage.vue was exempted whole for a reason that covers one class — its closest-match ring (D-106, D-107) — and that page also renders the Curated Escalation behind the Escalation Trigger (D-99). The reviewer put bg-red-100 immediately above <EscalationPanel> and watched all three tests pass, then reverted. An exception now carries an optional only, scoping it to the classes its reason covers, with a regression case asserting the scoped form rejects an alarm red beside that panel while still permitting the highlight.
  2. Nothing asserted that acknowledging First Run persists. The unit spec checks the component emits; the util spec checks the util; there is no App.spec.ts. Deleting acknowledgeFirstRun() from App.vue while leaving its ref toggle would have kept every test green and silently broken "once per device" (D-138). The virgin-device case now reloads and asserts Home rather than the notice — verified by making exactly that deletion and watching it fail, then restoring.
  3. The sweep saw only literal class strings. It now also catches arbitrary-value colours (text-[#b91c1c]), and the remaining limit — a class assembled at runtime — is stated in the docblock instead of implied away. A guard case also moved from .test() to matchAll, because .test() on a global regex advances lastIndex between calls and that guard would have passed or failed by iteration order.

Fable, on the spec. Defects raised: 4.

  1. §6 cited US-71 for the First Run Notice. US-71 is the Boot Splash's story — "nothing to tap" — and the notice is tappable by design, one sentence after the paragraph says so. Two surfaces conflated inside one paragraph. §6 now cites D-60/D-61 for the no-account property and names the distinction explicitly.
  2. A pre-existing Seam 4 sentence described a repository that no longer exists: "the four placeholder routes … the day a surface is built behind them", when three of the four have had surfaces for weeks and only PPE remains. Not written this session, but the same failure mode as round 1's defect 6 and one paragraph above it. Rewritten to what the harness exercises, with a line recording that it was stale, because a testing section read as current is how work gets planned from a false picture.
  3. Two Coverage Notes restated their own Evidence verbatim, checking nothing further. Rewritten to carry a distinct fact each.
  4. D-141's row was only half-updated relative to D-142's for the same round-1 fix: its Evidence described why a splash exists rather than the closed set, and its Landed in omitted Testing Decisions, where round 1 had added the load-bearing admission that the set has no mechanical assertion. Both corrected.

Resolution: all seven. Frontend at 4cbfb5b, spec in this commit.

What round 2 says about round 1. Round 1 of this stage found that the spec described tests that did not exist. Round 2 found that the tests then written had a hole in exactly the place the spec claimed coverage, and that a neighbouring paragraph had been stale the whole time. Each round's fix created or exposed the next round's defect. That is the argument for the ping-pong cap being a floor rather than a ceiling, and for the reviewers being cold — the author of a fix is the worst reader of it.

Coverage check: pass — 332 ids declared, all mapped downstream.

01:47 — Stage 1, cross-model grill round on the curation and API rulings

Actor: Griller ×2 — Claude Fable 5.1 and Sonnet, each a cold subagent with a model override, neither a fork. Logged separately from the 01:30 round above, which was the same instrument pointed at different documents: that one grilled ADR-0064..0066, this one grilled ADR-0057..0063, 0067, 0068 and 0069 — the curation and API rulings. Prompt: the standard four-check Griller prompt, over .scratch/chemical-safety-assistant/griller-brief-2026-09-20.md. Artifact: .scratch/chemical-safety-assistant/grill-findings-2026-09-20.md @ 7f92761.

That the Grillers were cold and not forks is the finding, not the footnote. Three fork rounds ran earlier the same day and found none of what follows. A fork inherits this session's context and therefore its priors; the two options are not equivalent evidence, and this is the second round today to demonstrate it.

Defects raised: 17 by Fable, 2 by Sonnet, both of Sonnet's also found by Fable.

The two Grillers disagreed, and the disagreement was checked rather than averaged. Sonnet's checks 3 and 4 returned "nothing found", and at least one of those was verifiably wrong: it reported no term used outside CONTEXT.md, while the glossary's roles section listed exactly four roles — Safety Officer, Supervisor, Store Officer, Handler — and curator, a term every one of the ten ADRs uses, was not among them. Rather than average the two counts, four of Fable's claims were verified at first hand before any repair was dispatched, and all four held. The findings file therefore follows Fable's list and marks which items were checked directly.

Three of Fable's findings were judged, on inspection, not to be defects:

  1. Seventeen refusal codes against ADR-0062's twelve. ADR-0062 says in terms that the API owns the list and may correct it against the code. The five additions are inside that delegation.
  2. CAVEAT_REQUIRED_ON_CARRIED as a breach of the blank-field rule. A field nullable in itself and required by the value beside it is a rule of the domain, not an absent input. Settled in the API's favour, and for the API's reason, by D-153.
  3. Eleven rejected options with no owner ruling behind them. Each was checked and each still holds as an argument. What was wrong was the silence about whose argument it is, not the argument — so all eleven are kept and attributed rather than withdrawn.

Resolution: two supersessions and one amending ADR, because an Accepted ADR is never edited.

  • ADR-0073 (D-150) supersedes ADR-0061 @ 0876a08 — a second explanation for a term is refused, which is what the service was built to do and what the owner ruled right.
  • ADR-0074 (D-151, D-152) supersedes ADR-0059 @ 152d941 — the page count is nullable, which is what both repositories ship, and withdrawal stays refused.
  • ADR-0075 (D-153) @ 41f1bd8 — amends ADR-0057, ADR-0060, ADR-0062, ADR-0068 and ADR-0069, all of which stand. It withdraws ADR-0069's display prohibition, corrects two D-49 miscitations and one D-124 overreach, records that D-129 ratifies ADR-0053's open note rather than discovering it, and records the Thai-heading research gap rather than filling it.
  • Traceability repairs in the spec and the glossary @ 952ce9b and 3b36226; the gitlinks moved to the code the rulings describe @ 76102e0.

Coverage check: pass — 332 ids declared, all mapped downstream.

02:30 — Documentation site: the guide catches up with the day

Actor: Proposer (claude-opus-5), a closing bookkeeping pass. No Griller; this is not a stage artifact and produced no id. Prompt: close the day's bookkeeping — bring docs/guide/, the VitePress build, the journal and session-handoff.md into line with what the repository actually is, deriving every number by running something rather than reading it. Artifact: docs/guide/, index.md, .vitepress/config.mts and session-handoff.md @ 4a8596a (this entry was written into that commit and its sha added in the follow-up beside it).

What was wrong. The site described the repository as it was on 2026-09-10, nine days and nineteen rulings earlier. docs/guide/current-state.md — the page whose whole job is being true about the repository — claimed 47 ADRs and 104 decisions against a real 75 and 148, and quoted a git ls-files -s app/ block naming two commits the gitlinks had long since moved off. The site's front page said "there is no product code yet" and "all 34 ADRs". docs/guide/data-model.md opened "No Prisma schema exists in this repository yet" while seventeen Corpus models and ten migrations sat in the api gitlink. sources-and-data.md pointed a reader at 67 COVERAGE problems that closed on 2026-09-11.

Every number on those pages is now re-derived, and each row of the What-exists table names the command that produces it rather than a document that asserts it. That is the failure this pass existed to correct, so repeating it from a stale source would have been the same defect again.

Today's nineteen rulings (ADR-0057..0075, D-128..D-153) entered the guide as links, never as prose. The load-bearing rule is that a guide page never states a decision: coverage_check.py scans docs/adr/, docs/knowledge/research/, .scratch/ and CONTEXT.md, and docs/guide/ is in none of them, so a paraphrase there is a second source of truth no gate watches. Each ruling became a row in a | Question | Ruling | table — the Section shape crossing the snapshot seam (ADR-0072), the nullable page count (ADR-0074), the refusal codes (ADR-0062), the gaps read and its union (ADR-0068, ADR-0069, ADR-0071), the explanation that cannot be corrected (ADR-0073), the workspace layout (ADR-0063).

The application gate history is now recorded where a reader meets it. Both application gates were red from 2026-09-13 until 2026-09-20, in both repositories, from one cause: two commits written against a layout where the app repos are siblings of scripts/ rather than gitlinks under app/. No test encoded either failure, which is why seven days passed. ADR-0063 (D-137) ruled the layout; both are green.

Verified by running, not by reading:

root      COVERAGE PASS  FIXTURE PASS  ADRSTATUS PASS  IDSTATUS PASS
          STACKCLAIMS PASS  CORPUS PASS               RESULT: PASS   332 ids
api       LINT PASS  TYPECHECK PASS  TEST PASS        RESULT: PASS   777 tests, 23 files
frontend  LINT PASS  TYPECHECK PASS  TEST PASS  SEAM4 PASS
                                                      RESULT: PASS   802 tests, 65 files

bun run docs:check exits 0 — 75 ADRs and 148 decisions indexed, the VitePress build clean with ignoreDeadLinks: false, 6 of 6 mermaid diagrams parsing. No dead link was found: the two supersessions recorded today changed Status lines rather than filenames, and git log --diff-filter=R -- docs/adr/ is empty, so no ADR has ever been renamed. The hand-written Guide sidebar needed no change — no guide page was added or renamed — but two comments in .vitepress/config.mts and scripts/gen-decision-index.mjs still said "34 ADRs … four superseded" and were corrected to 75 and six.

The day's shas, read from the commits rather than from a list. Parent workspace: 33193cc seven rulings land as ADR-0057..0063 · aa93c6e the eighth as ADR-0067 · d77eb24 Stored Copy becomes a glossary term · 729f78d ADR-0064..0066 and this journal reach git · 0a28883 what was actually open, derived rather than read · 1a391ac ADR-0068 and ticket 43 · 35d2523 the ninth ruling and the unreadable-answer ticket · bf97d24 ADR-0069 and the real label vocabulary · e7005cf ADR-0071, and claimed comes out of four documents · f838f6f the eleventh ruling and the allocation it lost mid-draft · 2a31303 ADR-0072 · 7f92761 the cross-model grill and what it found · 2bd28bf a Section reaches a curator as words, not a Python dict · 0876a08 ADR-0073 · 152d941 ADR-0074 · 41f1bd8 ADR-0075 · 952ce9b seven Coverage rows and the Curator glossary entry · 76102e0 the gitlinks point at the code today's rulings describe.

Api: 56f1dd3 the gate is two directories up, a refusal names its rule, a document counts its pages · 72bcd97 the gaps read says why the list is empty · 0cb2545 the gaps read counts the pairs · acb425b a Section reaches the device as a number and a name. Frontend: 5235718 the API is found where it actually is · 2e383f9 an answered request stops reading as a dead service · 035c72d a document counts its pages and the GHS list is held once · ff6ce27 the reconcile page states the case the server decided · e9c9f9c a Section reaches a Handler in Thai, and two rulings stop being asserted in prose.

One thing found and left alone. The brief this pass worked from said tickets 39 and 40 are resolved. On disk both read Status: in progress — reviewed 2026-09-20 (frontend 2e383f9); the review's own findings are what remain, with unchecked boxes still under each. Several of those findings were ruled today, so the lines may well be closeable — but deciding a review's findings are closed is a judgement about the work rather than a documentation sync, and the guide records what the ticket files say. Carried to session-handoff.md for someone who knows that work.

Coverage check: pass — 332 ids declared, all mapped downstream. This pass declares no id: a guide page is not a stage artifact and states no decision, which is the whole point of it.

02:05 — Stage 3 /to-tickets

Actor: Proposer (claude-opus-5). Typed by the owner; breakdown approved before publishing. Artifact: .scratch/chemical-safety-assistant/issues/44..47.

What the scan found before any drafting. Of 43 existing tickets, four are unresolved and three of those belong to the parallel session. The app-shell work specced in stage 2 needs no ticket — it was built before it was specced, which is the wrong order but is what happened. What the scan surfaced instead were two gaps with no ticket at all:

  • PPE has never had one. Placeholder.router.ts defers its rulings to "the ticket that builds it", and that ticket did not exist, which is why the placeholder outlived First Aid and Spill Response by weeks. Now 44.
  • Risks item 1 has never had one. Camera text recognition has been a declared risk since the spec, referenced by the disabled Home entry and by the identification rulings, with nothing on the board. Now 46.

Published: 44 the PPE surface, 45 assert the Boot Splash closed set, 46 prove camera text recognition on real labels, 47 Thai copy review for the app-shell surfaces. 44 and 45 are the frontier. 46 and 47 are gated on people rather than on tickets and say so in their Status tail, which is what that tail is for — Blocked by: None would have made them look startable.

Two skill defaults overridden, per the repo. /to-tickets says to write one tickets.md at the repo root; docs/agents/issue-tracker.md explicitly overrides that and this repo uses one file per ticket. The skill also says to apply ready-for-agent; docs/agents/triage-labels.md records that scheme as declared, adopted by zero of 43 tickets, and rewritten by the owner earlier today. Used open. The ticket template still carries both stale forms (# T-nn, ready-for-agent) while no real ticket does — worth fixing, not fixed here.

Ids: none of the four declares an R/D/US/T/W id at the head of a line. Checked deliberately — a briefing note written earlier today did exactly that and failed COVERAGE as five redeclarations.

Coverage check: pass — 332 ids declared, all mapped downstream. Both gates green.

The Study Area was asked for, and the answer came from the poster

Actor: Proposer (claude-opus-5), with the owner Prompt: the owner asked what to do about the Study Area, then sent the reply they received Artifact: session-handoff.md blocker 2, docs/guide/current-state.md blocker row

The criterion has been ruled since 2026-09-11 — the Study Area is whichever production area uses the proof chemical, and that ADR deliberately refused to name an area because nobody in this repository has seen The Plant's floor. So there was nothing to decide, only something to ask.

Three questions went out on 2026-09-20: which area uses the proof chemical, that area's chemical list, and who owns the emergency procedure there. The reply answered from the project poster and said so in its own first line. The poster names no production area.

No research finding was written, deliberately. Every chemical the reply listed is already recorded with a page citation — the four named in panels 1 and 5, and the three on the history screen. Writing it up again would have added an uncited paraphrase of a scanned Thai poster beside the cited original, which is the second-source-of-truth defect this repository spent the day removing, and which the no-trusting-extracted-Thai ruling refuses on its own terms.

What was learned is about access, not about chemicals. The questions have not reached anyone who has stood on the floor. The reply was right to decline to place the three history-screen chemicals in the Study Area — the poster does not say they are there — and being right from the wrong source is still the wrong source. The binder blocker and this one are one dependency.

Ids: none declared. No R-nn was written, which is the point of the entry.


18:40 — Stage 1 /grill-with-docs, prototype feedback round

Actor: Proposer (claude-opus-5, session chem-assistant-8c) Prompt: the owner pasted a Thai prototype-test report — a Handler's and a Curator's findings on the built application and the curator's workbench — with the instruction "here is a feedback1 from actual user after prototype test, /grill-with-docs with me to correct the actual product req and then update to changelog, version documents". Artifact: docs/adr/0076..0083, CONTEXT.md, .scratch/chemical-safety-assistant/PRD.md @ 5f4cf06

Fourteen questions, one at a time, each with a recommended answer. The owner took the recommendation on eleven and overrode it on three. The three overrides are the useful part of the record:

  • Home's six cards. Recommended keeping camera text recognition on Home as the visibly disabled peer it already was, so D-62 stayed untouched. Owner ruled it off Home entirely, then ruled the removal is a parking rather than a killing — the route returns as a peer when the OCR spike passes. That pair is D-154 and D-155.
  • The component port. Owner chose "port BaseButton/BaseTab from work-permit". Reading that repository found it has no BaseButton at all — its button is src/volt/Button.vue, the same copy-in pattern this repository's Card.vue and Toast.vue came from. Put back to the owner as a fact rather than resolved silently; owner then ruled a BaseButton wrapper over the ported volt component, so call sites read in the vocabulary used across their repositories (D-160).
  • Nothing else was overridden on substance. The remaining owner answer that differed from a recommendation was the record screen's tabs, where the recommendation was taken but only after ADR-0008's rejected option — "a fourth tab on the chemical summary screen" — was quoted back.

Two collisions were raised rather than implemented over, per AGENTS.md:

  1. ADR-0030 carries "this ruling may not be re-decided". The owner's answer was worded as superseding it. adr_status_check.py matches a supersedes-claim on the Status line only, and a match obliges the target to read Superseded by — which would have killed D-63 (printable QR labels) and D-64 (CAS-first read order) along with the part being changed. This repository's idiom for a partial change is ADR-0040's Accepted — amends ADR-0005, which stands, so ADR-0076 uses it. The narrowing of D-62 is stated in ADR-0076's own decisions and rejected options.
  2. ADR-0008 likewise. ADR-0083 affirms it instead of amending it: only reference fields go behind a tab, and the three emergency routes stay above the bar at the depth ADR-0008 put them.

Four facts were looked up rather than asked, and two of them changed an answer:

  • scripts/generate-icons.ts already writes a committed offline icon allow-list, asserted by OfflineIcons.spec.ts. HomePage.vue's docblock still says "no <Icon> on this screen" because every icon would fetch from the Iconify API — that is stale, and the six cards can carry icons with no ruling at all.
  • GET /documents/:sdsId/verified-pages already serves verified text, corrected flag and curator name, so re-showing prior verifications is a surface change and not an API one. Its contract deliberately withholds a corrected page's raw extraction; the owner left that ruling standing (D-162).
  • No route lists published snapshots, but CorpusSnapshot.payload and CorpusSnapshotDocument make the history inspectable, which is what D-167 reads.
  • CuratorSurfaceIsolation.spec.ts permits curation pages to name each other's routes — only non-curator files may not — so the forward stage links of D-164 break nothing.

Grill rounds: none yet. The artifacts have had no Griller pass, cross-model or fork. The cap and the two-model requirement in AGENTS.md are unmet, and this stage is not complete. The owner was told at question 14 that a true second model needs a second terminal (claude --model claude-opus-4-8) which this session cannot open.

One feedback item produced no ruling, deliberately. "ปรับสีให้เข้าใจง่าย" — make the colours easier to understand — is already answered: D-142 makes the extract binding for colour and makes a meaning's colour a named token, and the alarm reservation (D-139) is what keeps the escalation path findable without reading. What the item asks for is those two applied to surfaces that predate them, which is implementation. It is written here rather than nowhere, because an item that vanished into "already covered" during a grill is an item nobody can audit afterwards.

Two corrections made within this session, before any Griller saw the artifacts. ADR-0080 and the PRD's §5 first said handleLoading drives the overlay and its error toast is overridden. Its error path reads an axios-shaped e.response.data, and the curation resources use plain fetch and return a discriminated outcome rather than throwing — so on that surface it contributes the overlay and nothing else, and an implementing agent could have read the original wording as licence to make those resources throw. The §6 icon sentence likewise said the default @iconify/vue entry point would fetch at render time; Icon.plugin.ts already imports @iconify/vue/offline, and the real constraint is static names plus a regenerated allow-list. Both were caught by re-reading the source with the Read tool after the handoff's warning that rtk-filtered cat can serve stale content.

Ids: D-154..D-168 declared, across ADR-0076..0083. Every one carries PRD prose and a Coverage row. ./init.sh green before and after.

19:20 — /handoff written for the implementing session

Actor: Proposer (claude-opus-5, session chem-assistant-8d) Prompt: /handoff this to another agent that current active now as model sonnet 5 and let's them spawn agents to implement itArtifact: /private/tmp/claude-501/-Users-kan-Me-Work-kanverse-projects-chem-assistant/8c279c2e-ea49-4d00-865e-398032c79353/scratchpad/handoff-2026-09-20-implement-prototype-feedback-rulings.md @ 9a369b8

Written to the OS temp directory rather than the repository, per AGENTS.md, and its path recorded here so the next session can find it. It references the artifacts rather than restating them, and carries the dependency order for the nine implementation units — three API, six frontend — plus the traps specific to this work: the rtk stale-read trap, the never-edit-an-Accepted-ADR rule and why ADR-0076 amends rather than supersedes, the curation resources that return outcomes rather than throwing, the alarm-colour reservation, and the curator-surface isolation spec that permits curation pages to link each other and nothing else to link them.

It leads with the two open questions rather than the work, because both gate it: the eight ADRs are ungrilled and stage 1 is therefore open, and no US-nn covers any of D-154..D-168 while stage 3 reads US-nn. Neither is the implementing session's to decide alone.

The handoff could not be delivered. ListAgents reports no other Claude session running on this machine, so the Sonnet 5 session named in the prompt is not reachable from here and the document was left at the path above for the owner to hand over.

Ids: none declared.

16:27 — Owner override: grill gate and ticket-tracing question

Actor: Sonnet 5 session, handed off from chem-assistant-8d via /private/tmp/claude-501/-Users-kan-Me-Work-kanverse-projects-chem-assistant/8c279c2e-ea49-4d00-865e-398032c79353/scratchpad/handoff-2026-09-20-implement-prototype-feedback-rulings.md (head 814e78d at handoff time).

Two questions were put to the owner before any implementation agent was spawned, per the handoff. Both answered directly, no discussion:

  1. Grill gate override. ADR-0076..0083 have had no Griller pass, cross-model or fork — stage 1 is open per AGENTS.md. The owner chose to override and implement now rather than run the cross-model round first. Recorded here as the override, same as the 2026-09-20 three-round-cap override earlier today.
  2. Ticket-tracing question. D-154..D-168 carry no US-nn. The owner ruled tickets trace to D-nn directly — no stage-2 story pass.

./init.sh run in all three, per the handoff's step 1, gated on exit code. Root and api were green unchanged. Frontend was red on TYPECHECKPpeSurface.spec.ts (adf8d40) built three IPpeItem fixtures carrying a kind field the interface (Corpus.model.ts:190) never declared and the spec never asserted; dead decoration, apparently copied from IPpeSurface's own kind discriminant in the same directory. This predates this session and is not part of D-154..D-168 — it sat in a commit session-handoff.md had recorded as green before adf8d40 landed on top of it. Fixed at 4807b11 in the frontend repo (dropped kind, added the interface's reasonSourceSpan: null the fixtures had been missing too); gitlink bumped in this commit. All three green after.

Implementation of the nine units (A1-A3 api, F1-F6 frontend) begins next, dependency order per the handoff: A1/A2/A3 in parallel, then F1, then F2/F3/F4/F5/F6.

Ids: none declared.

17:43 — F6 landed (frontend session, chem-assistant-bf)

Actor: Sonnet 5 (session chem-assistant-bf), resuming after two implementation agents (F6, A4) died to the same session-level 429 in an earlier session. Neither agent's work was recoverable — F6 had written nothing, A4's diff sits uncommitted in the shared api tree, owned by the peer session chem-assistant-6e also working in this workspace. Confirmed with that session directly rather than assumed; api was left untouched.

F6 — ChemicalRecordPage.vue tabs its reference fields (D-168, ADR-0083). Hazards, health effects, PPE and storage sit behind a BaseTab/BaseTabWindow (F1) tab bar; the Standing Statement, identity and the First Aid/Spill Response/PPE links stay flat above it, unconditional on which tab is open — the part of ADR-0083 that affirms ADR-0008. Default tab is hazards; BaseTab owns the ?tab= round trip itself, so the page only re-derives the initial tab per record load (falling back to hazards when the query names a field the record does not carry) and syncs a later tab-only query change without re-reading the Corpus or re-writing a History visit (D-09) on every click.

e2e/Seam4NoNetwork.spec.ts updated: the silent-field assertion now switches tabs to reach a hidden field rather than expecting it visible beside the seeded one. New unit coverage in ChemicalRecordPage.spec.ts asserts the default tab, that identity/links stay visible regardless, and that switching tabs flips which RecordFieldPanel is shown (isVisible() does not detect v-show here under happy-dom/VTU; asserted via the style="display: none" attribute instead).

Frontend gate green: LINT / TYPECHECK / TEST / SEAM4, 896 unit tests, 26 e2e. Committed at 18e54d2 (frontend) and ca37b01 (parent gitlink bump, that file alone — the peer session's three uncommitted Curation files and the untracked sds/ were left exactly as found).

Not done: A4 (sdsId on the snapshot payload) is the peer session's call — its snapshot.golden.json question (payload gained sdsId, fixture has none) is still open with that session's user. F2-F5 are unstarted.

Ids: none declared.

17:58 — F5 landed, and the nine-unit plan is now complete except A4

Actor: Sonnet 5 (session chem-assistant-bf), a fresh general-purpose subagent for the implementation, reviewed and committed by the orchestrating session.

Correcting the previous entry. It said F2-F5 were unstarted. Checked before spawning anything this time: F2, F3 and F4 had already landed, by a different actor/session, with no journal entry — HomePage.vue (six cards, D-154/155), DefaultLayout.vue (AppBar/NavigationBar, D-156/157), CurationVerifyExtractionPage.vue (527 lines, D-161..165) and CurationPublishPage.vue / CurationSnapshotsPage.vue (D-167) were all present, committed, and covered by Curation.router.ts routes that already existed. Only F5 was missing.

F5 — "open the original SDS" (D-166, ADR-0081). New OpenOriginalDocument.vue on ChemicalRecordPage.vue: present only while useOnline.ts (a new presence-only composable, navigator.onLine seeded, live on online/offline window events) reports a connection; absent rather than disabled offline, replaced by one sentence stating the original needs a connection and that the safety content below is unaffected (D-155's taste, applied to a second surface). Fetches via new CorpusOriginalDocument.ts against GET /v1/corpus/documents/:sdsId/original (api 9084327); no presigned URL, bytes opened as a Blob URL revoked after a delay. IChemicalRecord gained an optional sdsId (distinct from the QR-safe labelKey id) — populated once the API's snapshot payload carries it, which is a separate, still-open change (see A4 below); until then the control simply does not render, the correct degrade rather than a defect.

A4 remains open, unclaimed by this unit — see the entry above. CorpusSync.ts stores the snapshot payload structurally rather than remapping field by field, so once A4's api-side sdsId lands, the frontend needs no further change to thread it through into IChemicalRecord.sdsId and light the F5 control up; verified by reading CorpusSync.ts, not assumed.

A branch-collision defect, self-inflicted, found and corrected before committing anything further. F5's commit (frontend 210e493) landed on feat/correcting-a-curated-selection — the peer session's branch — because the shared frontend checkout had been switched to it between F6 and F5 without this session checking git branch --show-current first. Same mistake repeated one level up: the gitlink-bump commit for it then landed on this workspace's own feat/correcting-a-curated-selection, for the same reason. Both were cherry-picked onto main from disposable git worktree add-based worktrees (frontend b656675, parent 47c1444 — this entry's own commit), rather than by checking out main in either shared, currently-dirty-or-live checkout, specifically to avoid disturbing either session's in-progress branch or working tree. Neither peer branch was rewound, reset or force-pushed — the duplicate commits sit harmlessly on both branches, same tree, different ancestry. Lesson for whoever reads this next in this workspace: check git branch --show-current before every commit here, not just before file edits. The shared checkout can change branches out from under a session with no warning beyond the branch name in a commit's own confirmation.

Frontend gate green on main at b656675: LINT / TYPECHECK / TEST / SEAM4, verified in the real checkout before the cherry-pick (a disposable worktree under /tmp resolves this repo's ../../chemical-safety-assistant-api relative imports incorrectly, so its own gate run is not meaningful — content identity with the verified commit is what stands in for it).

Ids: none declared.