diff --git a/.dev-loop/INGEST_REPORT.md b/.dev-loop/INGEST_REPORT.md
index 3a965e5..0e5c4f3 100644
--- a/.dev-loop/INGEST_REPORT.md
+++ b/.dev-loop/INGEST_REPORT.md
@@ -1,237 +1,150 @@
-# Knowledge flush — 13 insight(s) ingested (21 claimed, 2 dropped, 6 released)
+# Knowledge flush — 4 insight(s)
-Cross-Check: 1× independent adversarial `claude` CLI headless pass over the 5 new pages — it refuted the changed-files gate page's "prettier exits 0 on an empty match set" claim; re-measured against Prettier 3.7.4, confirmed the reviewer was right (unmatched operand exits **2**), and rewrote the page, report rows 3/5 and `log.md`. Other 5 claim groups verdicted sound. Limits: the reviewer's sandbox blocked repo reads, so source-quote-supports-directive and self-contradiction dimensions went unaudited (details in `## Cross-Check`).
-
-Queue drained under run id `20260827-125731-38371` (this session is the detached
-`hooks/auto-flush.sh` run; its step-0 acquire resolved re-entrantly to
-`already-owned`, not to a competing holder). 21 rows were claimed; 13 are
-ingested below, 2 are retired as out-of-layer, and 6 are released back to
-`pending` for a later flush because each needs its own page rather than a row,
-and rushing six more pages in one pass would have lowered the bar on all of them.
+Claimed queue ids: `c6c76b1cb2d35bf9`, `028fcf4648303397`, `1c4620e3aadaf0b7`, `56f4dbc8fb5997f8`.
+All four were handled (2 new pages, 3 amended pages); none dropped.
## Verified best-practice
-Every external claim below was live-fetched this session and quoted in the page's
-`Sources` block. Field evidence carries the repo, date, and the measured numbers.
-
-| # | Claim | Sources checked | How verified | Confidence |
-|---|-------|-----------------|--------------|------------|
-| 1 | `now()` is `transaction_timestamp()` (fixed at transaction start) while `clock_timestamp()` "changes even within a single SQL statement"; `RETURNING` yields computed defaults "without needing a separate database query" | postgresql.org `functions-datetime`, `dml-returning`, `transaction-iso` | Fetched; both key sentences quoted verbatim into the page | verified |
-| 2 | A boundary recomputed in a follow-up step is a *second, later* `now`, widening a `<= boundary` set | Field: `rtb-unified` `packages/orpc/src/routers/batch.ts` — codifies "one `now` per decision" and passes `now` into the boundary helper; its result type omits the boundary, which is the shape that invites recomputation | Read the invariant and the signature in the cited file | field-tested |
-| 3 | **[CORRECTED BY CROSS-CHECK]** The vacuous-pass shapes for `prettier --check` are: no operands (rc **0**), all operands ignore-filtered (rc **0**), and unsupported extensions with `--ignore-unknown` (rc **0**). A pattern/operand matching nothing exits **2** — it prints the success sentence *and* an unmatched-pattern error | prettier.io CLI + ignore docs; local measurement, Prettier 3.7.4 | The first draft generalised "empty match set ⇒ exit 0" from a field log where both messages appeared together. The independent reviewer flagged it; I then ran all seven cases against a real binary and rewrote the page around the measured table | verified (re-measured) |
-| 4 | zsh does not word-split unquoted parameter expansions by default, so `cmd $FILES` arrives as **one** operand | zsh FAQ ch. 3 (`SH_WORD_SPLIT`) | Fetched; quoted ("By default, zsh does not have that behaviour: the variable remains intact") | verified |
-| 5 | The zsh word-split operand exits **2**, but its log still carries the success sentence — so the log misleads even though the exit code does not | Field 2026-08-24 (`rtb-unified`, zsh) + local measurement 2026-08-27 | Field log showed both messages together; the local run reproduced it as `rc=2`. The page now says explicitly that this row fails loudly *unless* `--no-error-on-unmatched-pattern` is set. Probe placement re-confirmed: `.claude/tmp/` is `.gitignore`d, so a probe there passes at rc 0 | verified (re-measured) |
-| 6 | TypeScript applies excess-property/contextual typing to fresh object literals, so a value of a type can be constructed with the type's name absent from the text | typescriptlang.org handbook, *Object Types* | Fetched; confirmed the check follows from the contextual type, not from a written annotation | verified |
-| 7 | `tsc`'s program is `files` ∪ `include` ∪ transitive imports; `exclude` "only changes which files are included as a result of the `include` setting" and does not stop an imported file entering the program | typescriptlang.org TSConfig `#include`, `#exclude` | Fetched; the `exclude` sentence quoted (it sharpens the rule to "in the program", not "in `include`") | verified |
-| 8 | Consequence of 6+7 measured | Field 2026-08-24/25 (`rtb-unified`): `grep "DealViewer"` reported 3 construction sites, actual 8 — the missed set included production wiring `routers/deal.ts:38`; `ContractScopeActor` 7→~22. Separately, `packages/orpc/tsconfig.json` `include: ["src/**/*"]` produced 3 production + 13 api-test errors and **zero** for `__tests__/routers/deal.test.ts`, whose 6 sites appeared only as 6 failing tests | Counts recorded from the cited runs | verified |
-| 9 | cgroup v2: `memory.peak` is max usage since creation/reset; at `memory.max` "the OOM killer is invoked in the cgroup"; in `memory.events`, `max` counts times usage "was about to go over the max boundary" — **distinct** from `oom_kill` | docs.kernel.org cgroup-v2 admin guide | Fetched; all four quoted. This corrected the candidate, which had read a non-zero `max` as a kill; the page now states the distinction explicitly | verified |
-| 10 | An `exec`'d process joins the container's cgroup and is invisible to the application's own semaphore | kubernetes.io `manage-resources-containers`, `assign-memory-resource`, `kubectl exec` reference + field 2026-08-26 (review-bot pod, `limits.memory: 3Gi`): `memory.current` 2.54 GiB, `memory.peak` 3.0 GiB (at the limit), `memory.events: max 5`, while `maxConcurrentAgents: 20` reported free slots | Docs fetched; pod numbers from the cited measurement | verified |
-| 11 | Basename-keyed mutation backups collide across directories and restore cross-writes; an untracked file's `git diff` is empty whether restored or destroyed | Field 2026-08-21 (`rtb-unified`, NEWRTB-2936): restore wrote `schemas/deal.ts` into `routers/deal.ts` → `Cannot find module './common.js'`, `grep -c dealRouter` = 0; **both files were 154 lines**, so a line-count check passed; after re-keying, M9/M10 flipped SURVIVED→KILLED. Plus stryker mutant-states / pitest for the verdict vocabulary | Reproduced end to end in the cited run | field-tested |
-| 12 | A negative assertion is vacuous when the fixture never supplies the triggering input | Field 2026-08-25 (`rtb-unified`): with `staleQueuedJobIds: []` the code early-returned; the widening the assertion claimed to catch survived 116/116 green | Mutation applied and observed | field-tested |
-| 13 | A body-level (non-inline) review finding cites no file, so rebutting from an assumed file rejects real defects | Field 2026-08-19 (PR #327 r16): quote matched `report.py:393/416/425`, not the assumed `fill_plan.py:307` — sibling modules, one already fixed | Grep resolved the quote to the real site | field-tested |
-| 14 | Unifying two duplicate allowlists defaults to the union and silently widens each side | Field 2026-08-25 (`rtb-unified` PR #965): folding `DISPLAYABLE_ERROR_CODES` into `USER_FACING_ERROR_CODES` would have added `UNAUTHORIZED` + `VALIDATION_ERROR`, exposing raw server messages as inline UI errors; caught only by computing the difference first | Difference computed before the merge | field-tested |
-
-Not upgraded: nothing was marked `verified` on field evidence alone. Two pages
-carry `confidence: field-tested` (`mutation-harness-file-custody`,
-plus the pre-existing `evaluating-review-feedback`), and no candidate was
-recorded as `verified` without a fetched primary source.
+**1. `c6c76b1cb2d35bf9` — precedence tests must stage the competing condition at the deciding iteration** (→ `confidence: verified`)
+
+Claim: a test asserting that exit condition A wins over B must make B become true in
+the same poll/iteration in which A reaches its threshold; a B staged earlier makes the
+assertion hold under either ordering of the checks.
+
+- https://arxiv.org/abs/1909.04770 (Vera-Pérez, Danglot, Monperrus, Baudry, 2019) — fetched
+ this session. An undetected mutant has three causes, the first being that "the test
+ inputs are not sufficient to infect the state of the program". That is exactly this
+ failure: if B fires before A can activate, the reordering mutant is never reached in a
+ state where it can infect the outcome.
+- https://pitest.org/quickstart/basic_concepts/ — a surviving mutant means no test
+ distinguishes the mutated program; a kill is attributed to the covering test, which is
+ why the precedence test itself (not merely the file) must redden.
+- https://testing.googleblog.com/2021/04/mutation-testing.html — detection is measured by
+ inserting the fault and requiring failure, not by branch coverage.
+- Field measurement (dev-loop `watch-status.sh`, three "R6 precedence" bats cases): moving
+ the exit-8 block above the failed/done check left all three green. Re-staging the
+ competing status transition to the same tmux-stub capture count that confirms the
+ two-poll witness made the same swap red.
+
+**2. `028fcf4648303397` — never confirm a pane witness from a capture taken in the same iteration as a key-send** (→ merged as `verified` material into an existing `verified` page)
+
+Claim: when a poll loop both sends keys (auto-recover `Enter`, resend) and reads a state
+witness from the pane, it must skip the capture entirely on the iteration that sent keys.
+
+- Reproduced locally this session (tmux, macOS, `sh` pane): with the newest status line
+ reading `STATE=BLOCKED`, sending a command that worked 0.4s before printing left the
+ same-iteration `capture-pane` still showing `STATE=BLOCKED`; the next poll showed
+ `STATE=RUNNING`. The same sequence with an instantly-printing command had already
+ repainted within the same iteration — so the check's outcome is set by the target's work
+ time, which is why the gate belongs on "did this iteration send keys", not on a delay.
+- Mechanism already sourced on the target page: https://man7.org/linux/man-pages/man1/tmux.1.html
+ (`send-keys` writes keys into the pane; `capture-pane` copies visible contents — neither
+ reports consumption) and https://man7.org/linux/man-pages/man3/termios.3.html.
+- Field evidence: dev-loop code review of task `t3-blocked-consume`, finding F1 — the exit-8
+ "still blocked" witness was confirmed from a same-poll capture, so a just-repaired worker
+ could be escalated; gating on the recovery flag fixed it, and removing the gate under
+ mutation woke the witness one poll early.
+
+**3. `1c4620e3aadaf0b7` — graphify's installed hooks miss the `git pull` path** (→ `verified`)
+
+Claim: `graphify hook install` covers `post-commit` and `post-checkout` only, while the
+"PR merged upstream → `git pull`" path fires `post-merge`, so the graph goes stale while
+`hook status` reports installed.
+
+- https://git-scm.com/docs/githooks — fetched this session: `post-commit` "is invoked by
+ git-commit"; `post-merge` "is invoked by git-merge, which happens when a `git` `pull` is
+ done on a local repository"; `post-checkout` "is also run after git-clone, unless the
+ `--no-checkout` (`-n`) option is used".
+- Local reproduction (git 2.50.1, macOS): in a clone carrying all three hooks, a
+ fast-forward `git pull` fired `post-merge 0` alone; a divergent `git pull` that created a
+ merge commit also fired `post-merge 0` and **no** `post-commit`; a fresh `git clone` of
+ that repository carried no non-sample hooks.
+- Source read: `graphifyy 0.4.23` `hooks.py:186-187` installs `"post-commit"` and
+ `"post-checkout"` only; `grep -c post-merge hooks.py` → 0.
+- **Correction applied to the candidate's stated reasoning:** the submitted note said git
+ "does not run hooks on clone". Per the docs and the reproduction, `git clone` *does* run
+ `post-checkout` — the reason a clone gets no graph is that hooks are not copied by clone,
+ so none exist to run. The page carries the corrected reason.
+
+**4. `56f4dbc8fb5997f8` — a grounding gate's escape hatch must emit a gap record at the point it grants the pass** (→ `confidence: field-tested`)
+
+Claim: an escape hatch (`[no-wiki]`, a suppression comment) is the most valuable signal a
+knowledge base gets, and a gate that only decides pass/fail destroys it; the record must be
+emitted by the gate, not requested in prose.
+
+- https://docs.github.com/en/code-security/code-scanning/managing-code-scanning-alerts/resolving-code-scanning-alerts
+ — fetched this session: dismissing an alert requires choosing a reason, "the dismissal
+ comment is added to the alert timeline", it is readable as `dismissed_comment` on the
+ alerts API, and dismissed alerts stay in the Closed list for review. This is the canonical
+ shape of a *recorded* escape hatch.
+- https://github.blog/changelog/2025-07-01-delegated-alert-dismissal-for-code-scanning-is-now-generally-available/
+ — fetched this session: reviewers can "provide a comment when approving/rejecting alert
+ dismissal requests", and dismissal requests are created, listed and reviewed through
+ dedicated REST API endpoints — the review of a hatch use is itself recorded and readable
+ outside the UI.
+- Local field measurement (this repo, 1.22.0): `skills/wiki-plan/scripts/plan-gate.sh:166`
+ passes an ungrounded decision with `[ "$basis" = "[no-wiki]" ] && continue` and records
+ nothing, while `skills/wiki-plan/SKILL.md:135` asks in prose for the decision to be "noted
+ as an ingest candidate". Against 276 non-index wiki pages, `log.md` carries exactly one
+ `gap` entry (2026-07-11).
+- No external source states the general rule as a directive, so this stays **field-tested**
+ rather than verified; the GitHub precedent supports the mechanism, not the general claim.
## Existing-layer check
-Method: routed via `INDEX.md` → domain `index.md`; then built a full id+title
-index of all 265 pre-existing pages and probed it with concept greps
-(`clock_timestamp|clock skew`, `changed[- ]files|--ignore-unknown`, `tsconfig`,
-`contextual typ|excess property`, `set difference|allowlist`, `cgroup`,
-`basename|backup.*restore`, `2>&1`, `delta|baseline`) before deciding new vs merge.
-
-Pages read: testing-quality-source-text-wiring-assertions, testing-quality-tests-that-cannot-fail, backend-common-change-impact-call-site-enumeration, backend-common-change-impact-widening-a-closed-value-table, qa-process-evaluating-review-feedback, infrastructure-containers-host-cgroup-visibility, testing-quality-behavior-not-implementation
-
-Findings:
-
-- **Zero coverage** (→ new pages): changed-files-only gates, tsconfig/contextual
- typing, allowlist set-difference, app-clock-vs-DB-timestamp, exec-into-a-running-container.
- The concept greps returned no hits for these; the clock hits were incidental
- (offline sync, token handling) and none compared an app clock to a DB column.
-- **Already covered — one candidate all but retired.** The comment-stripping
- insight is `source-text-wiring-assertions` step 2 verbatim ("Make the
- assertion's subject the file with comments removed"), and its false-RED and
- negative/count false-GREEN shapes are already edge rows. Only the *empty-slice*
- consequence was new, so that alone was merged.
-- **Line-cap conflict handled without breaking the invariant.**
- `source-text-wiring-assertions` sits at exactly **120** body lines (the
- documented cap). Rather than add a row and violate maintenance invariant 5, the
- new nuance and the new field evidence were merged **in place** into an existing
- edge row and an existing source bullet. Body count re-measured after editing:
- still 120.
-- **No conflicts found.** Nothing ingested contradicts an existing directive.
- The one correction made was to a *candidate*, not to the wiki (item 9: the
- `memory.events` `max` counter is approaches-to-limit, not kills).
-- **Related links added both ways**: `tests-that-cannot-fail` ↔
- `mutation-harness-file-custody`; `widening-a-closed-value-table` ↔
- `compiler-as-call-site-inventory` (+ `errors-diagnostics-from-a-shared-code-path`);
- `host-cgroup-visibility` → `exec-added-processes-and-the-memory-budget`
- (from its existing self-monitoring row).
-- **Indexes/log updated**: 4 domain indexes (+5 "load when" rows), `log.md`
- appended. Root `INDEX.md` unchanged — no new domain.
-
-Gates run (the exact CI commands from `.github/workflows/test.yml`):
-`node scripts/wiki-structure-checks.js wiki` → **pages: 270, indexes: 13,
-findings: 0**; `node scripts/wiki-lint-prohibitions.js wiki` → **directives 72,
-compliant 72, violations 0** (the 1 `info` is pre-existing in
-`config/keys-ahead-of-their-consumer.md`, untouched); `bash scripts/check-versions.sh`
-→ `ok: dev-loop 1.11.2`. The `bats tests/` job was **not** run — bats is not
-installed on this machine, and this change touches only wiki markdown (no
-scripts or hooks), so that suite's subject is unchanged.
+Routed via `INDEX.md` → domain `index.md` → every page whose "load when" overlapped.
+
+Pages read: testing-quality-tests-that-cannot-fail, testing-quality-policy-at-several-return-sites, testing-quality-completion-predicates, testing-quality-surviving-mutant-equivalence-triage, infrastructure-agent-orchestration-pane-delivery-confirmation, infrastructure-agent-orchestration-code-graph-as-orientation-layer, platforms-processes-driving-a-tui-in-a-tmux-pane, qa-document-verification-spec-document-gates, infrastructure-agent-orchestration-session-completion-gates, infrastructure-agent-orchestration-autonomous-decision-rulings
+
+(Directory-level scans of `wiki/testing/quality/`, `wiki/platforms/processes/`,
+`wiki/infrastructure/agent-orchestration/` plus keyword sweeps for `no-wiki`, `post-merge`,
+`capture-pane`/`send-keys`, and `precedence` over the whole wiki preceded these reads.)
+
+| Insight | Overlap found | Outcome |
+|---------|---------------|---------|
+| 1 precedence | `tests-that-cannot-fail` carries a co-occurring-writer edge case (two writers of one flag) and `policy-at-several-return-sites` carries per-site mutation — both are about *coverage of one site*, neither about *which of two live conditions wins* | **New page**, cross-linked to both; no conflicting directive |
+| 2 pane witness | `pane-delivery-confirmation` already rules that a pane *diff* is not delivery evidence (echo direction). The new rule is the opposite direction — a stale capture *falsely confirming* a witness | **Merged** into that page (Do-this #6, 1 edge row, 1 Instead-of row, 2 sources); 1 pointer row added to `driving-a-tui-in-a-tmux-pane` |
+| 3 graphify hooks | `code-graph-as-orientation-layer` already gates on freshness and its Sources line already names `hook install` post-commit/post-checkout — the hook-coverage consequence was missing | **Merged** into that page (2 edge rows, 2 sources, 1 clause on directive 1) |
+| 4 escape hatch | `session-completion-gates` and `spec-document-gates` cover gate *authoring*; none covers what a gate does with its own exemptions. Keyword sweep for `no-wiki`/`escape hatch`/`knowledge gap` returned no owning page | **New page** in the existing `agent-orchestration` category |
+
+Conflicts flagged: none — no existing directive is contradicted.
+Related links added both ways: `tests-that-cannot-fail`, `policy-at-several-return-sites`,
+`completion-predicates` ↔ the new precedence page; `session-completion-gates`,
+`autonomous-decision-rulings`, `spec-document-gates` ↔ the new escape-hatch page.
+
+Lint after the edits: `wiki-structure-checks.js` → **278 pages, 13 indexes, 0 findings**;
+`wiki-lint-prohibitions.js` → no findings on any touched page (the 2 repo-wide violations it
+reports are pre-existing, in `plans/` and `tests/fixtures/`). New pages are 67 and 69 body
+lines; amended pages are 92, 92 and 65 — all under the 120-line cap.
## Open-PR check
-`gh pr list --repo choiyounggi/dev-loop --state open --search "head:knowledge/"`
-returned **no open PRs**, and a second unfiltered `gh pr list --state open`
-returned none either — the repository has zero open PRs at flush time. There
-were therefore no in-flight sibling branches to diff against, and no
-`git fetch origin
` / `git diff origin/main origin/ -- wiki/`
-comparisons to run.
+Listed with `gh pr list --repo choiyounggi/dev-loop --state open --search "head:knowledge/"` —
+12 open heads: #191, #190, #189, #188, #187, #186, #185, #183, #182, #181, #180, #179.
+Each head was fetched and its **added** wiki lines (`git diff pr-N -- wiki/`)
+grepped for `post-merge|graphify|graph.json|no-wiki|capture-pane|send-keys|precedence|knowledge gap`.
-Per-candidate verdict: **all 21 = `new`.** No `fold`, no `drop-as-pending-duplicate`.
-(The 2 drops recorded below are out-of-layer drops, not pending-duplicate drops.)
+| Candidate | Overlapping open head | Verdict |
+|-----------|----------------------|---------|
+| 1 precedence | none — #189's `proving-a-critical-section-is-lock-protected` and `sequential-dispatch-assumption-under-concurrency` are concurrency-window tests, not exit-condition ordering; its only `precedence` hits are Gradle property precedence (#179) | **new** |
+| 2 pane witness | none — #183's single `send-keys` hit is a pointer row in a stdin-vs-send-keys edge case | **new** |
+| 3 graphify hooks | #185 edits the *same page* but adds an unrelated row (`update` exits 1 on a >5,000-node HTML viz); #186 only mentions this page in an INGEST_REPORT dedup note | **new** (no content overlap; noted below as a textual merge risk) |
+| 4 escape hatch | none — #189's `gate-evidence-exit-code-class` is about a gate's own exit-code classes, not about recording exemptions | **new** |
+
+Merge-risk note for the reviewer: **#185 and this PR both append to
+`wiki/infrastructure/agent-orchestration/code-graph-as-orientation-layer.md`** (different
+edge-case rows and different source bullets). Whichever lands second may need a one-hunk
+textual merge; the content does not conflict semantically.
## Routing decision
-**New pages (5)**
-
-| Page | Domain/category | From | Why not an existing page |
-|------|-----------------|------|--------------------------|
-| `application-clock-vs-database-timestamps` | databases / transactions | `2b27d15d` + `bea92fdd` | No page compares an app clock to a DB column. `transactions` chosen over `schema-design` because the decisive content is transaction-time semantics (`now()` = transaction start ⇒ stamp order ≠ commit order) and the fix is a lock/isolation choice |
-| `changed-files-only-gates` | infrastructure / ci-cd | `ff041061` + `4b9af3a0` | Zero grep hits. Both candidates are the same defect (a gate green with an empty subject) from two directions, so they became one page rather than two |
-| `compiler-as-call-site-inventory` | backend / common / change-impact | `702dcf4e` + `94d55f2f` | `call-site-enumeration` is the sibling case (callers of a changed signature, Python positional-vs-keyword) and is at 80 body lines; the TS mechanism is *constructors of a type* with its own workflow, so per "one case per page" it is a separate page, cross-linked |
-| `mutation-harness-file-custody` | testing / quality | `6a9de235` + `41fa1c87` | `harness-reverse-controls` covers scoring a harness; nothing covers the harness's custody of the tree. Both candidates are that one case (keying, and the read window) |
-| `exec-added-processes-and-the-memory-budget` | infrastructure / containers | `7b9e8788` | `host-cgroup-visibility` is cross-pod read mechanics and explicitly routes self-monitoring elsewhere; `resource-limits-and-probes` is manifest authoring. This is a runtime preflight before adding load |
-
-No new category was created — all five landed in existing categories.
-
-**Merged into existing pages (5 candidates)**
-
-| Candidate | Merged into | Shape |
-|-----------|-------------|-------|
-| `91ef5d53` | `testing-quality-tests-that-cannot-fail` | +1 never-fails row, +1 Instead-of row, +1 source |
-| `f189f423` | `testing-quality-source-text-wiring-assertions` | In-place extension of 1 edge row + 1 source bullet (page at the 120-line cap) |
-| `bb6d8539` | `backend-common-change-impact-widening-a-closed-value-table` | +Do-this 6 & 7 (incl. a set-difference ruling table), +1 Instead-of row, +1 source |
-| `60a817ee` | `qa-process-evaluating-review-feedback` | +2 edge rows, +1 Instead-of row, +1 source |
-| `7b9e8788` | `infrastructure-containers-host-cgroup-visibility` | Cross-link from its self-monitoring row to the new page |
-
-**Dropped — out of layer (2, retired)**
-
-- `094dedf3` — a Figma MCP `inspect_node` → `get_dev_ready` children-fetch
- workaround. The server is a private, org-internal MCP plugin; the behavior is
- not publicly verifiable and the directive does not transfer to any other reader.
-- `e165a365` — an `/rtb:review` remote-fallback runbook naming
- `~/.claude/tools/rtb-remote-review.sh` and an internal pod. The transferable
- kernel ("a two-provider review gate degraded to one provider is not a passed
- gate") is already the subject of `qa-process-llm-review-pipelines`; what remains
- is machine-specific paths.
-
-**Released back to `pending` (6)** — each needs its own page, not a row, and is
-better served by a dedicated pass than by being appended here:
-`81dc1f98` (naming the carrier field/type when a plan says "wire A to B"),
-`b9ae304a` (`VAR="$(cmd 2>&1)"` mixing stderr into a value used as a path),
-`fdd0b3c6` (monitor markers anchored at line start; delta rather than absolute
-state; first cycle records a baseline),
-`c2adb2be` (positional-order assertions on rendered SQL predicates),
-`815e8cb9` (grep only *active* `DATABASE_URL` assignments, and confirm which
-dotenv file the tool loads, before a destructive DB command),
-`f1146adb` (CI ticket-key extraction scoped by changed-file intersection rather
-than by mention).
-
-## Decision Log
-
-**Intent.** Drain the harvested `★ Insight` queue into reviewable wiki knowledge
-without lowering the wiki's evidence bar. The queue held 21 rows accumulated over
-several days; the goal was correct routing and real verification, not a high
-ingest count.
-
-**Alternatives considered and rejected.**
-
-- *Ingest all 21 in this pass.* Rejected: six of them each need their own page,
- and writing six more pages in one pass would have produced thin, weakly-sourced
- entries. They are released to `pending`, not dropped, so the next flush takes
- them with a full budget.
-- *Append the two TypeScript candidates to `call-site-enumeration`.* Rejected:
- that page is the sibling case (callers of a changed signature, Python
- positional-vs-keyword). AGENTS.md requires one case per page, so the
- constructor-enumeration case became its own page, cross-linked both ways.
-- *Add a row to `source-text-wiring-assertions` for the empty-slice nuance.*
- Rejected: that page is at exactly the documented 120-line body cap, so adding a
- line would violate maintenance invariant 5. The nuance was merged **in place**
- into an existing edge row instead; body re-measured at 120.
-- *Drop the comment-stripping candidate entirely as a duplicate.* Rejected: its
- directive is already the page's step 2, but the empty-slice consequence
- (vacuous **green**, not the documented noisy red) was genuinely absent.
-- *Claim a cross-check exemption because this PR cannot merge itself.* Rejected —
- see below; the check found a real error, which is the argument against exempting.
-- *Push to `origin`* as the skill's snippet does. Not available: this contributor
- has no write access to `choiyounggi/dev-loop` (403). Used the pre-existing
- `fork` remote, which is how every prior knowledge branch here was published.
-- *Branch name from `git config user.name`.* The skill's ASCII sanitisation of a
- Korean name yields an empty string → `anon`, defeating the attribution the
- branch name exists for. Used the gh login, matching existing branch names.
-
-**Where reviewers should look hardest.**
-
-1. `infrastructure/ci-cd/changed-files-only-gates.md` — rewritten after the
- cross-check. The measured table is the load-bearing part; please sanity-check
- it against your own Prettier version, since the exit codes are version-visible
- behaviour rather than a documented contract.
-2. `databases/transactions/application-clock-vs-database-timestamps.md` step 5–6 —
- the claim that timestamp order is not commit order, and that the remedy is a
- lock/isolation level rather than finer clock resolution. `[추정]` on the MySQL
- `NOW()`/`SYSDATE()` row: taken from general MySQL semantics, not fetched this
- session like the PostgreSQL pages were.
-3. `widening-a-closed-value-table.md` Do-this 6–7 — this inserts a security-shaped
- concern (allowlist widening) into a page whose original subject was value
- tables. If that reads as two cases, it should be split.
-4. The 2 dropped candidates — if you consider private-tooling runbooks in scope
- for this wiki, they should be restored rather than retired.
-
-## Cross-Check
-
-Independent adversarial pass via `claude` CLI headless (separate process, no
-shared context), prompted to refute rather than confirm, over the five new pages'
-technical claims.
-
-**It found a real error, and the page was rewritten because of it.** The reviewer
-challenged the claim that `prettier --check` exits 0 on an empty match set,
-arguing an unmatched pattern errors by default and that exit-0 belongs to the
-ignore-filtered case. I resolved it by measurement rather than by argument —
-running all seven cases against Prettier 3.7.4 — and the reviewer was right:
-an unmatched operand exits **2** (while still printing the success sentence),
-whereas the genuine silent vacuous passes are no-operands, all-ignore-filtered,
-and `--ignore-unknown`-with-unsupported-extensions. The page, this report's
-rows 3 and 5, and the `log.md` entry were all corrected.
-
-Verdicts on the other five claim groups: **sound** (PostgreSQL clock semantics —
-noted as if anything *understated*; zsh word-splitting; TS contextual typing;
-`tsc` program membership incl. `exclude`-does-not-stop-imports; cgroup v2
-`max` vs `oom_kill` and `kubectl exec` cgroup placement).
-
-Stated limits of the check: the reviewer's sandbox denied it read access to
-`~/.dev-loop/repo/wiki`, so it adjudicated the six claims as quoted in its prompt
-and could **not** audit (b) whether each `Sources` quote supports the directive it
-is cited for, or (c) whether any page contradicts its own edge-case rows. Those
-two dimensions remain unreviewed by an independent party and are the residual
-risk in this PR. A first attempt also returned only the session's Stop-hook
-output rather than a verdict; that run was discarded rather than read as
-"no findings".
-
-## Review notes
-
-- PR-only, as required: no merge, no push to `main`.
-- Commit is under the contributor's own ambient git identity
- (`최영기 `, gh `dch0202-rsquare`); no assistant identity
- and no `Co-Authored-By` trailer. The branch uses the gh login because
- sanitizing the Korean `user.name` to ASCII yields an empty string, which the
- skill's snippet would have turned into `anon` — that would have defeated the
- attribution the branch name exists for.
-- Scope purity: only `wiki/**`, four domain indexes, `log.md`, and this report.
- Two untracked leftovers from earlier flushes
- (`.dev-loop/CROSSCHECK_FINDINGS.md`, `.dev-loop/fold-note-73.md`) were left
- untouched and unstaged.
+| Insight | Target | New category? |
+|---------|--------|---------------|
+| 1 | `testing/quality/precedence-between-competing-exit-conditions.md` (**new page**) | No — `testing/quality` already owns "can this test actually fail" |
+| 2 | `infrastructure/agent-orchestration/pane-delivery-confirmation.md` (**merge**), + 1 pointer row in `platforms/processes/driving-a-tui-in-a-tmux-pane.md` | No |
+| 3 | `infrastructure/agent-orchestration/code-graph-as-orientation-layer.md` (**merge**) | No |
+| 4 | `infrastructure/agent-orchestration/escape-hatch-uses-as-a-knowledge-gap-signal.md` (**new page**) | No — `agent-orchestration` already carries the gate-authoring pages (`session-completion-gates`, `autonomous-decision-rulings`); a `knowledge-base` category would hold one page and split gate knowledge across two places |
+
+Plumbing: `wiki/testing/index.md` +1 row; `wiki/infrastructure/index.md` +1 row and two
+extended "load when" lines (pane-delivery-confirmation, code-graph-as-orientation-layer);
+`log.md` +1 `ingest` entry.
diff --git a/log.md b/log.md
index e2ee9ca..5a1a698 100644
--- a/log.md
+++ b/log.md
@@ -104,3 +104,4 @@ Append-only. Format: `## [YYYY-MM-DD] > "$GAP_QUEUE"
+ continue
+ fi
+ ```
+
+3. **Make the hatch's reason a required, enumerated field**, and keep the
+ reasons that mean different things apart: *no page exists* is a knowledge gap,
+ *a page exists but was not found* is a routing defect, *out of scope for the
+ knowledge base* is neither. A single undifferentiated marker merges the three
+ into one unreadable pile.
+
+4. **Keep the hatch passing.** Its value is the honest signal it collects;
+ turning it into a failure buys a plan that cites the nearest unrelated page
+ instead, which the gate cannot detect.
+
+5. **Reconcile the two counts on a schedule.** Count hatch uses over a period
+ and compare against rows in the gap queue; the emitter is wired only while
+ the numbers match. A gate whose hatch fires and whose queue stays empty is
+ the failure this page exists to catch.
+
+## Edge cases
+
+| Case | Then |
+|------|------|
+| The gate's stdout is parsed by its caller | Append the record to a file path from the environment, never stdout — a new line in a parsed stream is a protocol change |
+| The gate runs in a subshell or a pipeline | Write with `>>` to an absolute path; a variable accumulated in a subshell is discarded at its exit |
+| The gate is a prose instruction in a skill document rather than a script | Move the recording into whichever script runs the check; an instruction to "note this as an ingest candidate" is executed only when the author remembers |
+| Several runs write the queue concurrently | Append single lines under the platform's atomic-append size and let the reader de-duplicate; a read-modify-write of the whole queue loses rows |
+| The same gap recurs every run | Keep the duplicates and de-duplicate at ingest time — repetition count is the priority signal for which page to write first |
+| The hatch is used because the knowledge exists but the author did not find it | Record it under the routing-defect reason; the fix is an index trigger line, not a new page |
+
+## Instead of
+
+| If you are about to | Do this instead | Why |
+|---------------------|-----------------|-----|
+| Document in the skill's prose that an escape-hatch use should be filed as an ingest candidate | Emit the row from the gate script at the moment it grants the pass | Prose relies on the author who was just let off to do extra work; the gate is already running and already has the decision text |
+| Have the gate print a warning line for an ungrounded decision | Append it to a durable queue as well | A warning scrolls past with the rest of the run output and is gone by the time anyone writes pages |
+| Count the gate's pass/fail results as the health metric for the knowledge base | Count hatch uses per run, and the gap queue's depth and age | Pass/fail measures whether plans satisfied the gate; only the hatch count measures what the knowledge base does not yet cover |
+
+## Sources
+
+- https://docs.github.com/en/code-security/code-scanning/managing-code-scanning-alerts/resolving-code-scanning-alerts — the canonical shape of a recorded escape hatch: dismissing an alert requires choosing a reason, "the dismissal comment is added to the alert timeline", the comment is readable as `dismissed_comment` on the alerts API, and dismissed alerts stay in the Closed list for later review
+- https://github.blog/changelog/2025-07-01-delegated-alert-dismissal-for-code-scanning-is-now-generally-available/ — the recorded hatch extends to the reviewer: a dismissal request can be approved or rejected with a comment, and dismissal requests are listed and reviewed through dedicated REST API endpoints, so the hatch and its review are both readable outside the UI
+- Field evidence 2026-09-16 (dev-loop 1.22.0): `skills/wiki-plan/scripts/plan-gate.sh:166` passes an ungrounded decision with `[ "$basis" = "[no-wiki]" ] && continue` and records nothing, while `skills/wiki-plan/SKILL.md:135` asks in prose for the decision to be "noted as an ingest candidate". Measured against a wiki of 276 non-index pages, `log.md` carries exactly one `gap` entry, dated 2026-07-11 — the prose instruction produced one record in two months of planning
diff --git a/wiki/infrastructure/agent-orchestration/pane-delivery-confirmation.md b/wiki/infrastructure/agent-orchestration/pane-delivery-confirmation.md
index 1e3008f..ddc4797 100644
--- a/wiki/infrastructure/agent-orchestration/pane-delivery-confirmation.md
+++ b/wiki/infrastructure/agent-orchestration/pane-delivery-confirmation.md
@@ -7,7 +7,7 @@ confidence: verified
sources:
- https://man7.org/linux/man-pages/man3/termios.3.html
- https://man7.org/linux/man-pages/man1/tmux.1.html
-last_verified: 2026-08-25
+last_verified: 2026-09-16
related: [platforms-shells-option-like-argument-values, infrastructure-agent-orchestration-session-completion-gates, platforms-processes-non-interactive-cli-invocation, infrastructure-agent-orchestration-unattended-worker-questions, platforms-processes-driving-a-tui-in-a-tmux-pane]
---
@@ -54,6 +54,13 @@ or escalate.
([platforms-shells-option-like-argument-values]).
5. **Capture the pane before and after with the same command and flags**, so a
redraw, resize, or scroll-region change is not read as new content.
+6. **Skip the pane check entirely on any iteration in which this loop sent
+ keys** — take no capture and leave the witness counters untouched until the
+ next poll. `send-keys` returns once tmux has written the keys to the pty; the
+ pane repaints only after the target reads them and produces output, so a
+ capture taken in the same iteration can still carry the pre-action screen.
+ Resume the check on the following poll, from a capture with no send in
+ between.
## Edge cases
@@ -70,6 +77,7 @@ or escalate.
| A bind is rejected for a pane/worktree mismatch | Pass the worktree alongside the pane on every bind; a pane identifier alone resolves against the coordinator's own checkout |
| The pane shows the prompt collapsed into a paste placeholder (`❯ [Pasted text #3]`) with no busy marker | Locate the **input box** — the region between the last two horizontal rules of the full `capture-pane -p` output — and look for the marker inside it, not in a fixed `tail -N` window: an unsubmitted paste renders its own remainder below the marker, so the marker's distance from the bottom grows with the payload and a fixed window is defeated by exactly the size it must detect; a whole-capture grep is also wrong, since it false-positives on a `[Pasted text` rendering still visible in the transcript above the input box. Once located: the body arrived as one bracketed-paste block and the submit key was consumed with it — send `Enter` as its own `send-keys` call and re-read. When the box's chrome cannot be located at all, that is *could not look*, not *nothing found*: report the check as unknown rather than a clean negative, or fall back to a deliberately oversized window only as a degraded check that announces itself as one. [platforms-processes-non-interactive-cli-invocation] owns the paste mechanism |
| A send helper reports a queued outcome and its own follow-up wait then reports pick-up | That pair is a confirmation: the wait observed the target take the input. A helper that reports delivery without a wait has observed only the write |
+| The same loop both repairs the pane (an auto-recover `Enter`, a resend) and reads a state witness from it | Gate the witness on the repair: when the repair fired this iteration, take no capture and advance no counter. The condition the repair just cleared is still the newest thing on screen, so a same-iteration capture confirms exactly the state that no longer holds |
## Instead of
@@ -81,6 +89,7 @@ or escalate.
| Resend on the first unchanged capture | Distinguish "busy" from "not delivered" before resending | Resending into a busy pane queues a duplicate that runs when the pane drains |
| Treat every failed bind the same way and retry it | Branch on the stage the failure names: wait-and-rebind for an occupied runtime, replace the agent for a dead one | The two look identical from outside — the pane renders in both cases — and retrying a dead agent spends units without ever succeeding |
| Size the pasted-marker window by raising N | Anchor the scan on the input box region | N must exceed the payload's own rendered tail, which is unbounded; raising N moves the threshold instead of removing it |
+| Press a recovery key and, in the same poll, read the pane to decide whether the condition is still present | Skip the read for that poll and decide on the next one | The keys are acknowledged before the target redraws, so the capture can return the pre-action screen and the witness counts a pane it just fixed as still broken |
## Sources
@@ -91,3 +100,5 @@ or escalate.
- Field reproduction 2026-08-05 (tmux 3.7b, macOS): a pane running `sleep 6` received `echo SECOND_PROMPT_MARKER`. Pane content changed (diff = YES) and the marker appeared once as echoed text, while the command's own output line count stayed 0; after the sleep drained, the command ran and the output line appeared
- Field observation 2026-08-25 (dev-loop 1.11.0 orchestrate, tmux, session `lo-1-dsr1`, task `t1-foundation` rework round r2): a 1713-byte single-line prompt — `send` returned `delivered` (exit 0) and a follow-up `state` returned `ready` while the pane sat at `❯ [Pasted text #10]…` with four further lines of the paste's remainder rendered below it, putting the marker **7th from the bottom** against the 6-line window; seven earlier sends of 1002–1520 bytes in the same run had succeeded; recovery was one `Enter` as its own `send-keys` call; the run only surfaced the stall ~10 minutes later via `watch-status.sh` exit 7 (`choiyounggi/dev-loop#145`)
- https://code.claude.com/docs/en/terminal-config — "Paste large content": the CLI collapses input over 800 characters or more than two lines to a `[Pasted text #N +M lines]` placeholder **in the input box**, which is what makes the input box the right anchor for the marker
+- Local reproduction 2026-09-16 (tmux 3.x, macOS, a `sh` pane): with the pane's newest status line reading `STATE=BLOCKED`, a `send-keys` of a command that worked for 0.4s before printing left the same-iteration `capture-pane` still showing `STATE=BLOCKED`, and the next poll showed `STATE=RUNNING`. The same sequence with an instantly-printing command had already repainted inside the same iteration — the outcome is set by the target's work time, which is why the gate belongs on "did this iteration send keys", not on a fixed delay
+- Field evidence 2026-09-16 (dev-loop code review, task `t3-blocked-consume`, finding F1): `watch-status.sh` confirmed its exit-8 "still blocked" witness from a capture taken in the same poll as the auto-recover Enter-press, so a worker that had just been unblocked could be escalated. Fixed by gating the witness on the same `ar_pressed` flag the recovery already sets; removing the gate again woke the witness one poll early under mutation
diff --git a/wiki/infrastructure/agent-orchestration/session-completion-gates.md b/wiki/infrastructure/agent-orchestration/session-completion-gates.md
index 137c5bc..8f31648 100644
--- a/wiki/infrastructure/agent-orchestration/session-completion-gates.md
+++ b/wiki/infrastructure/agent-orchestration/session-completion-gates.md
@@ -8,7 +8,7 @@ sources:
- https://code.claude.com/docs/en/hooks
- https://csf.tools/reference/nist-sp-800-53/r5/ac/ac-5/
last_verified: 2026-08-13
-related: [infrastructure-agent-orchestration-pane-delivery-confirmation, infrastructure-agent-orchestration-worktree-isolated-workers, platforms-processes-tool-diagnostics-without-a-failing-exit-code, infrastructure-agent-orchestration-dispatching-after-a-completion-report]
+related: [infrastructure-agent-orchestration-pane-delivery-confirmation, infrastructure-agent-orchestration-worktree-isolated-workers, platforms-processes-tool-diagnostics-without-a-failing-exit-code, infrastructure-agent-orchestration-dispatching-after-a-completion-report, infrastructure-agent-orchestration-escape-hatch-uses-as-a-knowledge-gap-signal]
---
# A Gate That Blocks a Worker Session from Ending Mid-Workflow
diff --git a/wiki/infrastructure/index.md b/wiki/infrastructure/index.md
index 86c2ac3..04cf436 100644
--- a/wiki/infrastructure/index.md
+++ b/wiki/infrastructure/index.md
@@ -14,7 +14,7 @@ Match your situation to a "load when" line; load only matching pages.
|------|-----------|
| [control-signals-vs-primary-artifacts](agent-orchestration/control-signals-vs-primary-artifacts.md) | An orchestrator is about to restart, discard, merge, or keep waiting on a worker based on a status file, a watcher's exit code, or a heartbeat; a monitor reports a worker dead while it is committing; a worker's status write produced no output and you must decide whether it landed; distinguishing alive-and-progressing from stalled from dead; several workers went quiet at once while every liveness check passes (usage-limit pause); a dispatch issued right after a worker's done signal fails runtime-unavailable |
| [shared-run-state](agent-orchestration/shared-run-state.md) | Several agent/worker sessions coordinate through files in one repository (status directory, briefs, escalations, claim files); choosing the path layout for that state; starting an orchestration in a repo that may already have one running; a watcher woke on a task id it did not create; the default branch moved during a run; a coordinator is about to reset a task's status file while re-delivering a prompt |
-| [pane-delivery-confirmation](agent-orchestration/pane-delivery-confirmation.md) | An orchestrator drives another program through a terminal multiplexer (`tmux send-keys` + `capture-pane`) and must decide whether the input was consumed, retry, or escalate; a pane diff is being used as delivery evidence; the target echoes but never runs the input; deciding *where* in a pane capture to search for a collapsed paste marker whose position depends on payload size, or what to report when the pane's input-box chrome cannot be located at all |
+| [pane-delivery-confirmation](agent-orchestration/pane-delivery-confirmation.md) | An orchestrator drives another program through a terminal multiplexer (`tmux send-keys` + `capture-pane`) and must decide whether the input was consumed, retry, or escalate; a pane diff is being used as delivery evidence; the target echoes but never runs the input; deciding *where* in a pane capture to search for a collapsed paste marker whose position depends on payload size, or what to report when the pane's input-box chrome cannot be located at all; the same loop sends keys and reads a state witness from the pane in one iteration |
| [session-completion-gates](agent-orchestration/session-completion-gates.md) | Writing a Stop/completion hook that blocks a worker session from ending while its phase is non-terminal; the gate fires on a worker that followed its own prompt; deciding the terminal phase set, the unknown-phase default, and how the gate bounds its own repetition; you are the worker the gate repeats on at an instructed pause and are deciding whether to advance your phase to silence it |
| [dispatching-after-a-completion-report](agent-orchestration/dispatching-after-a-completion-report.md) | A worker reported completion and the orchestrator wants to hand that same terminal or runtime slot its next task; a start/dispatch call fails with a runtime-unavailable-class error moments after a completion report; a task reached a terminal `failed` status with no worker having worked on it; deciding a settled dispatch's next owner (transfer, release, or retain) and how to retry a failed start without spending the task's attempt budget |
| [unattended-worker-questions](agent-orchestration/unattended-worker-questions.md) | A worker agent raises a question through its own interactive UI (a numbered chooser, a confirmation/trust/re-auth screen) with no human at that terminal; a worker is flagged stalled with a live terminal and no task-level error; a worker reports a decision it assumed rather than asked; designing the channel a worker uses to ask its coordinator for a decision |
@@ -22,7 +22,8 @@ Match your situation to a "load when" line; load only matching pages.
| [worktree-isolated-workers](agent-orchestration/worktree-isolated-workers.md) | Authoring the brief/output contract for parallel workers each confined to its own git worktree; workers stall at the same phase with no task-level error; deciding where shared or produced artifacts live and which direction (read vs write) a worktree guardrail stops; a guardrail escalates on read-only access to another worktree; the isolation guard is a Bash-command hook while workers also edit files with native Edit/Write tools |
| [autonomous-decision-rulings](agent-orchestration/autonomous-decision-rulings.md) | An unattended agent hits a decision its plan does not answer and must choose between stopping to ask and proceeding; a run stalls on questions no human needed to see; deciding which decision categories require a human; recording autonomous decisions for audit; resuming after interruption/compaction without re-dispatching completed work |
| [session-context-token-budget](agent-orchestration/session-context-token-budget.md) | Planning or running long-lived coordinator/worker agent sessions and deciding when to compact or clear context; a run's cost is dominated by cache reads; screenshots or large file reads are entering a long-lived session; choosing slot counts / per-phase token budgets for an orchestrated run |
-| [code-graph-as-orientation-layer](agent-orchestration/code-graph-as-orientation-layer.md) | A repository carries a locally built code knowledge graph (graphify `graphify-out/graph.json` or similar) and an agent is about to plan, decompose, or estimate the blast radius of a change; an orchestrator needs each parallel task's file set before dispatch; deciding whether a graph hit can stand as plan evidence; checking whether the graph is fresh enough to use |
+| [code-graph-as-orientation-layer](agent-orchestration/code-graph-as-orientation-layer.md) | A repository carries a locally built code knowledge graph (graphify `graphify-out/graph.json` or similar) and an agent is about to plan, decompose, or estimate the blast radius of a change; an orchestrator needs each parallel task's file set before dispatch; deciding whether a graph hit can stand as plan evidence; checking whether the graph is fresh enough to use; the tool's git hooks are installed and the graph must survive a `git pull` or a fresh clone |
+| [escape-hatch-uses-as-a-knowledge-gap-signal](agent-orchestration/escape-hatch-uses-as-a-knowledge-gap-signal.md) | Building or reviewing a gate that requires each decision to cite a source and offers a marker that lets an uncitable decision pass (`[no-wiki]`, a suppression comment, a dismissal reason); the knowledge base behind such a gate stops growing while plans keep meeting situations it does not cover; deciding where the record of an escape-hatch use is written and what reasons it distinguishes |
## ci-cd
diff --git a/wiki/platforms/processes/driving-a-tui-in-a-tmux-pane.md b/wiki/platforms/processes/driving-a-tui-in-a-tmux-pane.md
index 96f67ee..f0725a3 100644
--- a/wiki/platforms/processes/driving-a-tui-in-a-tmux-pane.md
+++ b/wiki/platforms/processes/driving-a-tui-in-a-tmux-pane.md
@@ -8,7 +8,7 @@ sources:
- https://man.openbsd.org/tmux.1
- https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap12.html
- https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap11.html
-last_verified: 2026-08-25
+last_verified: 2026-09-16
related: [platforms-processes-non-interactive-cli-invocation, platforms-processes-background-services, platforms-shells-portable-shell-scripts, infrastructure-agent-orchestration-pane-delivery-confirmation]
---
@@ -60,6 +60,7 @@ text from a variable rather than a fixed literal.
| Delivery must be confirmed but the program has no busy indicator and no artifact | Add one: have the wrapper echo a unique marker after processing, and search for that marker rather than for the prompt text |
| The pane's process has exited (shell prompt only) | The keys land on the shell and run as commands — check `#{pane_dead}` / the pane's current command before sending |
| A collapsed paste placeholder (`[Pasted text #N]`) has the paste's own remainder rendered below it | Not findable in a fixed last-N window — the marker's distance from the bottom grows with the payload; anchor on the **input box** (the region between the last two horizontal rules of the full capture) instead. See [infrastructure-agent-orchestration-pane-delivery-confirmation] for the full detection rule |
+| The same loop sends keys and then reads the pane for a state witness ("still blocked", "still waiting") | Skip the capture on the iteration that sent keys and read on the next poll — the pane repaints only after the target consumes the input, so a same-iteration capture can confirm the state the send just changed. See [infrastructure-agent-orchestration-pane-delivery-confirmation] |
## Instead of
diff --git a/wiki/qa/document-verification/spec-document-gates.md b/wiki/qa/document-verification/spec-document-gates.md
index 0cf00fb..76a8620 100644
--- a/wiki/qa/document-verification/spec-document-gates.md
+++ b/wiki/qa/document-verification/spec-document-gates.md
@@ -25,7 +25,8 @@ related:
testing-quality-checks-that-cannot-pass,
qa-deliverables-command-transcripts-in-a-document,
qa-deliverables-exclusivity-and-absence-claims,
- qa-document-verification-generated-reference-drift-gates,
+ qa-document-verification-generated-reference-drift-gates,,
+ infrastructure-agent-orchestration-escape-hatch-uses-as-a-knowledge-gap-signal
]
---
diff --git a/wiki/testing/index.md b/wiki/testing/index.md
index 4e65ac9..049eb67 100644
--- a/wiki/testing/index.md
+++ b/wiki/testing/index.md
@@ -43,6 +43,7 @@ Match your situation to a "load when" line; load only matching pages.
| [spec-artifact-checks](quality/spec-artifact-checks.md) | Authoring or reviewing the check itself: that a mapping table covers every rule/field/enum case, that ids resolve across documents; deciding whether a green check earned "verified" or only "present"; designing one negative control per check in a multi-check harness; parsing Markdown table rows programmatically in a doc-as-spec repo (deciding whether a passing gate is enough to *accept the deliverable* → wiki/qa/document-verification/spec-document-gates.md) |
| [schema-additions-under-a-golden-gate](quality/schema-additions-under-a-golden-gate.md) | Adding a node kind, variant, discriminator value, or field to a document format (IR, JSON Schema, spec artifact) whose only automated gate builds its negatives by mutating one committed golden example; the gate or the whole suite comes back green right after a schema change; deciding which negative each new schema keyword needs, and whether a green suite that never loads the schema is evidence at all |
| [policy-at-several-return-sites](quality/policy-at-several-return-sites.md) | One function applies the same policy at more than one of its own success returns (a CLI handler computing an exit code under `--strict`/`--check` and returning it from several branches, a controller stamping one header on several 200s); judging whether a suite covers such a flag when only one green test exists; choosing test cases by exit path rather than by input boundary; proving each site with a reversion of that site alone |
+| [precedence-between-competing-exit-conditions](quality/precedence-between-competing-exit-conditions.md) | A loop, monitor, or handler can end for more than one reason in the same pass (a confirmed-witness exit code and a task-failure code, a timeout and a cancellation) and a test claims one takes precedence; reviewing such a test, or judging whether it would catch a reordering of the checks; staging a competing condition so both are live at one decision point; proving the assertion by swapping the two checks |
| [generated-sql-property-assertions](quality/generated-sql-property-assertions.md) | Asserting on the SQL a query builder renders (`.toSQL()`, `sqlToQuery()`, a compiled-string snapshot) because CI has no database, and the property is semantic rather than textual ("a missing value must stay missing", "the tenant filter must stay applied"); such an assertion is green while a hand-seeded mutant that refills the missing value survives; choosing between top-level structural anchors, a function-name check, an aggregate-occurrence count, and a captured-alias binding — and knowing which refill position each one owns |
| [harness-reverse-controls](quality/harness-reverse-controls.md) | You built a harness that scores how well something is verified (mutation run, doc/spec gate suite, CI check matrix) and are about to cite its score in a commit, PR, README, or report; its verdicts come out uniform (every case caught, or every case green); deciding what control run proves the harness discriminates, how to score errored/never-ran cases, and what the harness's isolated working tree must contain |
| [value-preserving-refactor-assertions](quality/value-preserving-refactor-assertions.md) | Adding or reviewing the regression test for a refactor that replaced a hardcoded literal with a read from config/constants/SSOT while the value stayed the same; deciding what such a test can assert when the rendered output is byte-identical before and after; choosing a sentinel and the seam to substitute it through, and how to restore it; deciding when the value is too private for a test and the guard belongs in a static check instead; a sentinel assertion is red on code you believe correct |
diff --git a/wiki/testing/quality/completion-predicates.md b/wiki/testing/quality/completion-predicates.md
index 0b1b5ab..623419e 100644
--- a/wiki/testing/quality/completion-predicates.md
+++ b/wiki/testing/quality/completion-predicates.md
@@ -9,7 +9,7 @@ sources:
- https://pubs.opengroup.org/onlinepubs/9799919799/utilities/grep.html
- https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap09.html
last_verified: 2026-08-05
-related: [testing-quality-checks-that-cannot-pass, testing-quality-tests-that-cannot-fail, platforms-shells-portable-shell-scripts, platforms-processes-background-services]
+related: [testing-quality-checks-that-cannot-pass, testing-quality-tests-that-cannot-fail, platforms-shells-portable-shell-scripts, platforms-processes-background-services, testing-quality-precedence-between-competing-exit-conditions]
---
# A Predicate That Decides When Background Work Is Finished
diff --git a/wiki/testing/quality/policy-at-several-return-sites.md b/wiki/testing/quality/policy-at-several-return-sites.md
index 8bb9c85..a51486f 100644
--- a/wiki/testing/quality/policy-at-several-return-sites.md
+++ b/wiki/testing/quality/policy-at-several-return-sites.md
@@ -9,7 +9,7 @@ sources:
- https://pitest.org/quickstart/basic_concepts/
- https://stryker-mutator.io/docs/mutation-testing-elements/supported-mutators/
last_verified: 2026-08-06
-related: [testing-quality-tests-that-cannot-fail, testing-quality-minimum-case-set, testing-quality-harness-reverse-controls, backend-common-change-impact-call-site-enumeration]
+related: [testing-quality-tests-that-cannot-fail, testing-quality-minimum-case-set, testing-quality-harness-reverse-controls, backend-common-change-impact-call-site-enumeration, testing-quality-precedence-between-competing-exit-conditions]
---
# Covering a Policy Applied at Several Return Sites of One Handler
diff --git a/wiki/testing/quality/precedence-between-competing-exit-conditions.md b/wiki/testing/quality/precedence-between-competing-exit-conditions.md
new file mode 100644
index 0000000..00a3eb2
--- /dev/null
+++ b/wiki/testing/quality/precedence-between-competing-exit-conditions.md
@@ -0,0 +1,80 @@
+---
+id: testing-quality-precedence-between-competing-exit-conditions
+domain: testing
+category: quality
+applies_to: [general]
+confidence: verified
+sources:
+ - https://arxiv.org/abs/1909.04770
+ - https://pitest.org/quickstart/basic_concepts/
+ - https://testing.googleblog.com/2021/04/mutation-testing.html
+last_verified: 2026-09-16
+related: [testing-quality-tests-that-cannot-fail, testing-quality-policy-at-several-return-sites, testing-quality-completion-predicates, testing-quality-surviving-mutant-equivalence-triage, testing-quality-minimum-case-set, testing-quality-harness-reverse-controls]
+---
+
+# Asserting Which of Two Competing Exit Conditions Wins
+
+## When this applies
+
+A loop, monitor, or handler can end for more than one reason in the same pass —
+a witness-confirmed exit code and a task-failure code, a timeout and a
+cancellation — and a test claims one of them takes precedence. Also when
+reviewing such a test, or judging whether it would catch a reordering of the
+checks it names.
+
+## Do this
+
+1. **Stage the competing condition to become true in the same iteration the
+ condition under test reaches its threshold.** Drive it from a stub the loop
+ already calls, mutating fixture state as a side effect of a specific call
+ count — the Nth `capture-pane`, the Nth status read — so both conditions are
+ live at one decision point and the assertion depends on the order of the
+ checks.
+
+2. **Derive the staging iteration from the threshold, in the test.** A witness
+ that needs two polls to confirm decides at poll 2, so the competing
+ transition is staged at the same capture count the confirmation uses; writing
+ the number as a literal detaches the staging from the threshold it must track.
+
+3. **Prove the test by swapping the two checks in the source and requiring
+ exactly that test to redden.** Restore from a pre-swap copy and compare
+ hashes ([testing-quality-tests-that-cannot-fail] owns the restore rules).
+ Read the result:
+
+| Swap outcome | Read it as | Do |
+|--------------|------------|-----|
+| Only the precedence test reddens | The assertion discriminates on the order | Record the pair (swap → test) and move on |
+| Every test in the file reddens | The edit hit shared code, not the two blocks | Narrow the swap to the two branches and re-run |
+| The whole suite stays green | The competing condition is not live at the decision point | Move its staging to the deciding iteration, then re-run |
+| The suite stays green and no case can stage both conditions at once | The two orders are indistinguishable for this suite | Triage it as an equivalent mutant ([testing-quality-surviving-mutant-equivalence-triage]) |
+
+4. **Assert the loser's absence as well as the winner's code.** Require the exit
+ code of the winning condition *and* that the losing branch's own side effect
+ (its log line, status write, or cleanup) did not run — an implementation that
+ runs both and returns the first code passes a code-only assertion.
+
+## Edge cases
+
+| Case | Then |
+|------|------|
+| The competing condition can only be made true before the condition under test could activate | The case proves reachability, not precedence — name it for what it proves and add a separate staged case for the ordering |
+| Both exits are produced by one shared helper | Precedence lives at the call site that selects the argument; swap the call sites, not the helper's internals |
+| Both conditions produce the same exit code | Give them distinct codes before writing the precedence test; with one code the observable cannot separate the orders |
+| The stub has no call counter | Back the counter with a file the stub appends to, and read its line count as the iteration number — a subshell-local variable resets on each invocation |
+| The loop breaks on the first match, so ordering is the entire contract | Keep the staged case and add one case per condition alone, so a regression that drops a branch is distinguishable from one that reorders it |
+| The threshold is configurable at run time | Stage from the configured value the test passes in, and add a second case at a different threshold to prove the staging follows it |
+
+## Instead of
+
+| If you are about to | Do this instead | Why |
+|---------------------|-----------------|-----|
+| Make the competing condition true from the first iteration so it is "definitely active" | Stage it at the iteration the condition under test decides | Firing first means the loop exits before the contested comparison is reached, so the assertion holds under either order and the test cannot fail |
+| Read the source and call the precedence covered because the checks are in the right order | Swap the two blocks and require the test to redden | Reading confirms today's order; only the swap shows the suite would object to tomorrow's |
+| Treat a green run after the swap as "the order does not matter" | Check whether any case has both conditions live at one decision point first | An untriggered competitor never infects the program state, so the mutant survives for lack of input, not for lack of consequence |
+
+## Sources
+
+- https://arxiv.org/abs/1909.04770 — Vera-Pérez, Danglot, Monperrus, Baudry (2019): an undetected mutant has three possible causes, the first being that "the test inputs are not sufficient to infect the state of the program" — the case here, where the competing condition never coexists with the decision
+- https://pitest.org/quickstart/basic_concepts/ — a surviving mutant means no test distinguishes the mutated program; a kill is attributed to the covering test, so the precedence test must be the one that reddens
+- https://testing.googleblog.com/2021/04/mutation-testing.html — inserting a fault and requiring a test failure is what measures detection; coverage of the branch does not
+- Field evidence 2026-09-16 (dev-loop `watch-status.sh`, three "R6 precedence" bats cases): moving the exit-8 block above the failed/done check left all three green, because the competing task-failure status was staged before the two-poll witness could confirm. Re-staging the status transition to the same tmux-stub capture count that confirms the witness made the same swap red, and the rewritten cases stayed green on the unswapped source
diff --git a/wiki/testing/quality/tests-that-cannot-fail.md b/wiki/testing/quality/tests-that-cannot-fail.md
index fe33077..d21ca0d 100644
--- a/wiki/testing/quality/tests-that-cannot-fail.md
+++ b/wiki/testing/quality/tests-that-cannot-fail.md
@@ -16,7 +16,7 @@ sources:
- https://git-scm.com/docs/git-checkout
- https://git-scm.com/docs/git-restore
last_verified: 2026-08-27
-related: [testing-quality-minimum-case-set, testing-quality-behavior-not-implementation, testing-mocking-what-to-mock, testing-async-async-testing, testing-quality-checks-that-cannot-pass, testing-quality-spec-artifact-checks, testing-quality-harness-reverse-controls, testing-quality-schema-additions-under-a-golden-gate, testing-quality-differential-run-agreement, testing-quality-completion-predicates, testing-quality-guard-shape-vs-consequence, testing-quality-injected-clock-duration-assertions, testing-quality-write-path-assertions, testing-quality-value-preserving-refactor-assertions, testing-quality-unasserted-return-fields, testing-quality-stale-artifact-baselines, backend-common-change-impact-call-site-enumeration, platforms-shells-portable-shell-scripts, qa-document-verification-spec-document-gates, testing-quality-surviving-mutant-equivalence-triage, testing-quality-source-text-wiring-assertions, testing-quality-default-values-under-test, testing-mocking-captured-call-arguments, testing-quality-captured-log-message-assertions, testing-strategy-failing-test-first, testing-quality-mutation-harness-file-custody]
+related: [testing-quality-minimum-case-set, testing-quality-behavior-not-implementation, testing-mocking-what-to-mock, testing-async-async-testing, testing-quality-checks-that-cannot-pass, testing-quality-spec-artifact-checks, testing-quality-harness-reverse-controls, testing-quality-schema-additions-under-a-golden-gate, testing-quality-differential-run-agreement, testing-quality-completion-predicates, testing-quality-guard-shape-vs-consequence, testing-quality-injected-clock-duration-assertions, testing-quality-write-path-assertions, testing-quality-value-preserving-refactor-assertions, testing-quality-unasserted-return-fields, testing-quality-stale-artifact-baselines, backend-common-change-impact-call-site-enumeration, platforms-shells-portable-shell-scripts, qa-document-verification-spec-document-gates, testing-quality-surviving-mutant-equivalence-triage, testing-quality-source-text-wiring-assertions, testing-quality-default-values-under-test, testing-mocking-captured-call-arguments, testing-quality-captured-log-message-assertions, testing-strategy-failing-test-first, testing-quality-mutation-harness-file-custody, testing-quality-precedence-between-competing-exit-conditions]
---
# Proving a Test Can Fail