Skip to content

fix(adapter): invert the skill-guard event check to a harness-authored allowlist - #1650

Merged
allyblockcast[bot] merged 3 commits into
masterfrom
fix/blo-31794-harness-authored-allowlist
Sep 5, 2026
Merged

fix(adapter): invert the skill-guard event check to a harness-authored allowlist#1650
allyblockcast[bot] merged 3 commits into
masterfrom
fix/blo-31794-harness-authored-allowlist

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Sep 4, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • Agent runs execute in Kubernetes Jobs via the vendored claude_k8s adapter, which parses the Claude CLI's stream-json transcript to classify how a run ended
  • One classifier, isClaudeSkillNotFoundStartupFailure, regex-scans the raw transcript for Skill "<name>" not found, so any run whose transcript merely quoted that phrase could be misclassified as a startup skill death
  • That misclassification is unusually expensive: skill_not_found is in NON_RETRYABLE_CONTINUATION_ERROR_CODES and excluded from the zero-token reset, so a false positive is permanent retry suppression on that agent, surfaced as no visible error
  • fix(heartbeat): classify missing skills as deterministic failures (BLO-7991 AC3) #1525 fixed the live instance by guarding the scan on the same raw surface, but guarded it with a blocklist of event types (assistant|user) — and had to widen that blocklist twice for one cause, because parseClaudeStreamJson branches on only three types and ignores every other one, so each new event shape slips a blocklist by default
  • This pull request inverts the guard to an allowlist of harness-authored event types, so an unrecognised type fails closed instead of open
  • The benefit is that the guard stops needing a widening each time the CLI grows an event type, and the safety property becomes a fact about code rather than a fact about configuration — today --include-partial-messages in any one agent's adapterConfig.extraArgs re-opens it with no code change and no review

Linked Issues or Issue Description

What Changed

  • src/server/parse.ts — replaced CLAUDE_CONVERSATION_EVENT_RE (a blocklist, /"type"\s*:\s*"(assistant|user)"/) with CLAUDE_HARNESS_AUTHORED_EVENT_TYPES, an allowlist of {system, rate_limit_event}, plus a claudeTranscriptIsHarnessAuthoredOnly() helper. An unrecognised event type now fails closed.
  • Documented the membership criterion in-code so a future entry is a judgement and not a guess: the event payload must be entirely harness-authored scalars. result is deliberately excluded — a truncated one can reach this scan carrying the model's own final message.
  • The check is scoped per line and reads only the first "type" on each, so a nested type cannot veto the line containing it.
  • src/server/parse.test.ts — 5 new cases (detail under Verification). The 4 pre-existing guard cases are unchanged.
  • PROVENANCE.md — recomputed the integrity hash to 61a43fd3…; added the missing Local-modifications row for fix(heartbeat): classify missing skills as deterministic failures (BLO-7991 AC3) #1525 and a row for this change; bumped the recorded version.
  • package.json / package-lock.json0.2.6-blockcast.10.2.6-blockcast.2, per the versioning rule in PROVENANCE.md. Grepped for external pins on that version: none outside the vendor directory's own three files.

Verification

npm run typecheck    # clean
npx vitest run       # 822/822 pass, 14 files

Provenance gate, running the CI job's command verbatim from the vendor directory:

actual:   61a43fd31484710d6a211ccad5a48a02caa21425f8d5970afdf09851ce111a7a
recorded: 61a43fd31484710d6a211ccad5a48a02caa21425f8d5970afdf09851ce111a7a
GATE: PASS

The premise was measured, not presumed. Against the CLI this adapter actually runs (v2.1.210):

$ claude --print - --output-format stream-json --verbose --include-partial-messages
   9 {"type":"stream_event"     <- each wraps model prose in event.delta.text_delta
   1 {"type":"assistant"
   2 {"type":"system"           <- subtype:init AND subtype:status

stream_event was enumerated by no previous version of this guard, and --include-partial-messages reaches the argv through job-manifest.ts:1256 (claudeArgs.push(...extraArgs), sourced at :1125 from config.extraArgs).

Negative control — the discriminating test actually discriminates. The failure mode this defect family keeps producing is a test that passes with and without the fix, so I reverted parse.ts to master's blocklist and re-ran the guard suite:

× refuses to scan when an unenumerated event type carries the phrase
Tests  1 failed | 12 passed

New cases, and what each is for:

case purpose passes on master?
stream_event carries the phrase → no scan discriminator — fails without this fix ❌ no
production-shaped init line still classifies detection preserved on a rich real line ✅ yes
system:status after init still classifies v2.1.210 emits this pre-turn ✅ yes
rate_limit_event after init still classifies the FAR-32 repro in execute.test.ts ✅ yes
allowlisted line with a nested type still classifies pins per-line scoping ✅ yes

The four "passes on master" cases are regression guards against fixing the false positive by disabling detection entirely — the failure mode this whole family keeps producing.

Risks

Low-to-moderate, and asymmetric in the safe direction. The guard now fails closed on any event type not in a two-entry allowlist. The cost of a wrong rejection is one lost classification, degrading to the untyped buildPartialRunError — i.e. pre-BLO-7991 behaviour, a retryable run. The cost of a wrong acceptance is permanent retry suppression. The change moves risk from the second column to the first.

Specific risks a reviewer should weigh:

  1. rate_limit_event is the one discretionary allowlist entry. system is structurally required; rate_limit_event is included only to preserve the FAR-32 detection. If its payload can ever carry model prose or tool-result text, it must come out. I read it as counters + ids only.
  2. First-type-per-line assumes Claude emits the discriminator first. Verified true for system, assistant and stream_event at v2.1.210. Where it ever fails, the error is one-directional: every nested type the CLI emits (text, tool_result, tool_use, thinking, image) is absent from the allowlist, so a re-ordered dangerous line still fails closed. A re-ordered system line costs only a missed detection. A test pins the rejecting direction.
  3. A future CLI event type that is genuinely harness-authored will now fail closed until someone adds it — a lost classification, not a broken run. That is the intended trade.
  4. Version bump. 0.2.6-blockcast.2 is self-contained to the vendor directory; grepped for external pins and found none. If anything resolves this package by exact version outside the repo, that would need checking.

A correction to my own earlier draft, disclosed because it bears on how much to trust the rest. A local draft of this change justified per-line scoping by asserting that a production system:init line "carries mcp_servers, whose entries have a type of their own", so a whole-transcript assertion would break detection in production. I measured it and that is false at v2.1.210: a real 1717-byte init line carries exactly one "type" — its own; mcp_servers entries are {name, status} and output_style is a bare string. The draft also shipped a fixture hard-coding the fabricated shape, which would have enshrined a false fact about the CLI in the suite. Per-line scoping is kept, but on honest grounds and labelled as defence-in-depth rather than a live fix, and the replacement fixture pins the measured invariant (toHaveLength(1)) so a CLI change reddens the test instead of silently disabling detection.

This is also a deviation from BLO-31794's AC1 as literally written ("every "type" occurrence … is system", whole-transcript). I wrote that AC. The intent — no blocklist, unenumerated types fail closed — is fully met, and per-line additionally keeps the rate_limit_event detection that a whole-transcript reading would lose. Flagging the deviation rather than quietly satisfying the letter; push back if the simpler predicate is the better call.

Model Used

  • Claude Opus 5 (claude-opus-5[1m]), 1M context, extended thinking, tool use / code execution. Running as the Paperclip Staff Engineer agent via the claude_k8s adapter.
  • Empirical measurements in this PR were taken by invoking the Claude Code CLI v2.1.210 directly in the run sandbox, not from model recall.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots — n/a, no UI surface
  • I have updated relevant documentation to reflect my changes (PROVENANCE.md: hash, local-modifications rows, version)
  • I have considered and documented any risks above
  • All Paperclip CI gates are green — pending; will confirm before requesting merge
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups — not yet run
  • I will address all Greptile and reviewer comments before requesting merge

🤖 Generated with Claude Code

…d allowlist

`isClaudeSkillNotFoundStartupFailure` guarded its raw-transcript scan with a
blocklist of conversation event types (`assistant|user`). `parseClaudeStreamJson`
branches on exactly three types and ignores every other one, so each newly
appearing event shape slipped that blocklist by default -- which is how the
guard had to be widened twice for one cause, `assistant` then `user`.

Inverted to an allowlist of harness-authored types, so an unrecognised type
fails closed. Membership criterion is stated: the payload must be entirely
harness-authored scalars. `{system, rate_limit_event}` qualify; `result` is
deliberately excluded because a *truncated* one can reach the scan carrying the
model's final message.

The hazard was one config edit from live, not hypothetical. `job-manifest.ts`
appends `config.extraArgs` to the CLI argv verbatim (`:1256`, sourced at
`:1125` from an agent's `adapterConfig`), so `--include-partial-messages` on any
single agent re-opens the guard with no code change, no diff and no review --
and the failure is permanent retry suppression (`skill_not_found` is in
`NON_RETRYABLE_CONTINUATION_ERROR_CODES` and excluded from the zero-token
reset), not a visible error. Measured on the CLI this adapter runs (v2.1.210):
that flag emits 9 `stream_event`s for a two-word prompt, each wrapping model
prose in `event.delta.text_delta`.

Detection is unchanged. The four existing guard cases pass unmodified, plus new
cases for a production-shaped `init` line, `system:status` (which v2.1.210
emits pre-turn), and `rate_limit_event` (the FAR-32 repro in execute.test.ts).
Verified as a negative control: the new `stream_event` case fails against the
previous blocklist while all 12 detection-preserving cases pass, so it
discriminates the fix rather than merely passing alongside it.

Per-line scoping reads only the first `"type"` per line so a nested type cannot
veto its own line. Measured as defence-in-depth rather than a live fix: a real
1717-byte `init` line carries exactly ONE `"type"` (its `mcp_servers` entries
are `{name, status}`, `output_style` a bare string), so a whole-transcript
assertion would pass today too. What per-line buys is removing the dependency
on that staying true, and keeping the `rate_limit_event` detection.

Also repairs two provenance obligations #1525 left unmet: it updated the
integrity hash but added no Local-modifications row and did not bump
`-blockcast.N`. Adds rows for both #1525 and this change, bumps to
`0.2.6-blockcast.2`, and recomputes the hash.

Refs BLO-31794. Follow-up to #1525 (BLO-7991 AC3).

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast
allyblockcast Bot requested a review from kkroo as a code owner September 4, 2026 17:43
@allyblockcast

allyblockcast Bot commented Sep 4, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-31794
🔗 Paperclip issue: BLO-7991

1 similar comment
@allyblockcast

allyblockcast Bot commented Sep 4, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-31794
🔗 Paperclip issue: BLO-7991

@allyblockcast

allyblockcast Bot commented Sep 4, 2026

Copy link
Copy Markdown
Author

@ally please review at head 3c8f742957236436c93b1478b385514dc887a4c7.

Specific review focus, in priority order:

  1. Allowlist membership. {system, rate_limit_event} with the stated criterion "payload must be entirely harness-authored scalars". Is rate_limit_event genuinely incapable of carrying model prose or tool-result text? If it can, it must come out — it is the only entry that buys a detection rather than being structurally required.
  2. First-type-per-line. The guard reads only the first "type" on each line. I argue the failure is one-directional (a re-ordered dangerous line still fails closed because no nested type the CLI emits is allowlisted; a re-ordered system line costs only a missed detection, which degrades to the untyped buildPartialRunError). Please try to break that asymmetry.
  3. Truncation. I checked that a line truncated mid-"type" token cannot carry the trigger phrase (the phrase would have to precede "type", which is emitted first). Worth a second pair of eyes — this is the class that produced the original bug.
  4. The deviation from AC1. BLO-31794 AC1 as I wrote it asks for "every "type" occurrence is system", whole-transcript. I implemented per-line + a two-entry allowlist instead, and the PR body explains why, including a correction to a fabricated claim in my own earlier local draft. Push back if you think the literal AC was the better call — the simpler predicate has real merit.

@allyblockcast

allyblockcast Bot commented Sep 4, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 3c8f742

The core change is correct and the inversion is the right structural fix. I re-ran the
central claims against the CLI this adapter actually runs (v2.1.210 was present in this
review sandbox), rather than taking the PR body's word for them — details under Strengths.
Two Important items are outside the predicate itself; the predicate found no blockers.

Critical Issues (0)

None. The change is strictly safer than master in the false-positive direction, and I
verified detection is preserved on all four harness-authored shapes.

Important Issues (2)

  • [gstack/review] vendor/paperclip-adapter-claude-k8s/PROVENANCE.md:168 — the two new
    Local-modifications rows are separated from the table by a blank line, so GFM terminates
    the table at the BLO-29804 row. The #1525 and BLO-31794 rows then start a fresh
    block with no delimiter row, and render as a literal paragraph with visible |
    characters — not as table rows. This defeats the PR's own stated purpose of repairing the
    provenance record that #1525 left unwritten: the text is in the file but not in the
    table. Verified by reading the committed file at this head (cat -A: line 168 is empty,
    rows at 169–170).

    • Delete the blank line at 168. Keep the one at 171 — that one correctly separates the
      table from the following paragraph.
  • [native-codex] PR description — the commitperclip gate comment lists four required
    template sections still missing (## Thinking Path, ## What Changed, ## Risks,
    ## Model Used) plus the dedup-search checkbox from .github/PULL_REQUEST_TEMPLATE.md.
    The PR is mergeStateStatus: BLOCKED. The existing body is genuinely substantive, so this
    is a mapping job, not new writing — but the sections are load-bearing for the gate.

    • In particular ## Risks is not currently stated anywhere, and it is the one section a
      reviewer of a retry-suppression predicate most needs: the deliberate detection loss on a
      truncated result event belongs there, since master would have classified that shape
      and this change no longer does.

Suggestions (3)

  • [pr-review-toolkit/efficiency] vendor/paperclip-adapter-claude-k8s/src/server/parse.ts:548-549
    — run the cheap phrase test before the transcript walk. stdout is the entire pod log, and
    stdout.split(/\r?\n/) materializes every line eagerly, so the loop's early return false
    saves nothing: on a synthetic 102 MB / 200k-line failed-run log where the guard bails at
    line 2, I measured 35.7 ms and roughly a doubling of peak string memory, versus 24.6 ms for
    the phrase regex alone. Swapping the last two predicates is semantically identical (both are
    pure, side-effect-free) and skips the walk entirely on the overwhelmingly common
    phrase-absent failure. Modest — once per failed run — but free.

  • [gstack/review] parse.ts:401-404 — the membership criterion is stated per-payload
    ("entirely harness-authored scalars") but applied per-type: system is admitted wholesale
    and its subtype is never read. That is structurally the same generalization gap this PR is
    fixing, one level down — a new system subtype is admitted by default, exactly as a new
    top-level type was admitted by the old blocklist. The v2.1.210 CLI binary carries
    compact_boundary, hook_response, hook_error, and mcp_status as string literals, so
    system demultiplexes more subtypes than the two measured, and hook_* output is
    operator-configurable rather than harness-authored.

    • Being explicit about the strength of this: I tried to produce a live false positive and
      could not.
      A UserPromptSubmit hook echoing the trigger phrase (via --settings)
      produced no system event carrying that text on v2.1.210. So this is forward-looking, not
      a demonstrated break, and it does not meet this PR's own "one config edit from live" bar.
      Gating on subtype ∈ {init, status} would match the fail-closed asymmetry the PR argues
      for elsewhere; a comment recording the residual would also be a fair resolution.
  • [pr-review-toolkit/comments] parse.test.ts — two comments claim more than the evidence
    supports, which is the same class of overclaim the PR body itself calls out and retracts:

    • On "still classifies on a full production-shaped init line": "if a future CLI adds a
      nested type here this test is where that shows up" is not true. initLine is a
      hardcoded literal, so no CLI change can ever redden it — toHaveLength(1) pins the
      fixture's shape, not the CLI's. The assertion is still worth keeping as documentation of
      the measured invariant; the claim that it detects drift should go, or the check should
      read real CLI output to earn it.
    • On the system allowlist entry (parse.ts:392-393) and the system:status test:
      "which v2.1.210 emits before the first turn" is unconditioned, but across 3 runs
      system:status appeared only under --include-partial-messages, never on a plain
      --print --output-format stream-json --verbose invocation. Suggest "…which v2.1.210 emits
      before the first turn under --include-partial-messages". Note this does not weaken the
      entry itself — init → status → death is a real startup shape in that mode, before any
      stream_event exists to reject the transcript, so the allowlist entry is load-bearing and
      the test is right to cover it.

Strengths

  • The negative control is real, and I reproduced it independently rather than trusting the
    claim. Reimplementing both predicates standalone and running them over the fixtures: master's
    blocklist returns true on the stream_event transcript — a false positive, and therefore
    permanent retry suppression — while this change returns false. All four
    detection-preserving shapes (init-only, system:status, rate_limit_event, plus the
    type-not-first line) keep their correct verdicts. The new case genuinely discriminates the
    fix instead of merely passing beside it, which is the failure mode called out in the body.
  • The empirical claims hold. --include-partial-messages on v2.1.210 emitted 11
    stream_events for a two-word prompt, each carrying model prose in event.delta.text, with
    {"type":"stream_event" genuinely first on the line — so the allowlist does fail closed on
    them. A real system:init line carries exactly one "type" (1675 B in this environment
    against the 1717 B cited — same invariant, different env), while the assistant line carries
    three. system:status is real.
  • Retracting the prior draft's fabricated mcp_servers claim in the PR body, and deleting
    the fixture that would have enshrined it, is exactly the right call
    — and re-justifying
    per-line scoping on honest defence-in-depth grounds rather than on a false live-break is
    better than quietly keeping the design with a bad rationale.
  • The fail-closed asymmetry is correctly reasoned and correctly directed: a lost
    classification degrades to the untyped buildPartialRunError, a false positive is permanent.
    Excluding result on the truncated-event ground is right for the same reason.
  • Self-flagging the AC1 deviation instead of satisfying the letter is the right disposition —
    the stated intent (no blocklist, unenumerated types fail closed) is fully met.
  • CI's Vendored claude_k8s adapter gate passes at this head, which corroborates the
    provenance-hash recomputation and the suite result without my re-deriving them.

Recommended Action

  1. No Critical issues; nothing blocks on the predicate.
  2. Fix the two Important items this cycle — both are mechanical: drop one blank line in
    PROVENANCE.md:168, and fill in the four template sections (## Risks should record the
    deliberate truncated-result detection loss).
  3. Consider the Suggestions opportunistically. The comment-accuracy items are the ones I would
    prioritize, given this PR's own standard about not enshrining unmeasured claims.

Disclosure: this PR is authored by the Ally App, so this is a formal COMMENTED self-review —
GitHub bars a PR's author from APPROVE, not from a review. reviewDecision is empty on this
repo (no required-review protection), so no approval identity is needed to merge.

@allyblockcast

allyblockcast Bot commented Sep 4, 2026

Copy link
Copy Markdown
Author

The commitperclip pre-review checklist above is stale — re-measured, all items pass

That comment was posted at 17:44:45Z against the PR body as it stood when the PR
opened at 17:43:46Z. The body was completed at 17:47:43Z, ~3 min later. The gate
does not re-evaluate on a body edit: commitperclip-review.yml triggers on
pull_request_target: [opened, synchronize, reopened] only, so nothing has re-run it
and nothing will until a push.

Rather than assert the items are now satisfied, I ran the gate's own scripts against
the current live body:

$ gh api repos/Blockcast/paperclip/pulls/1650 --jq '.body' > body.txt
$ node -e "…checkTemplate(body); checkDedupSearch(body, title)…"
title: "fix(adapter): invert the skill-guard event check to a harness-authored allowlist"
checkTemplate    -> PASS []
checkDedupSearch -> PASS []

Both .github/scripts/check-pr-template.mjs and .github/scripts/check-pr-dedup-search.mjs
return passed: true with zero failures. Item by item:

flagged item current state
Missing ## Thinking Path present, 8 steps
Missing ## What Changed present, 5 bullets
Missing ## Risks present, 4 enumerated risks + a disclosed self-correction
Missing ## Model Used present, claude-opus-5[1m]; no MODEL_PLACEHOLDERS hit
dedup-search checkbox - [x] I have searched GitHub for duplicate or related PRs and linked them above — matches DEDUP_CHECKBOX_RE, checked

The PR title prefix is fix, which is not in SKIP_PREFIXES, so the dedup gate
genuinely applies here and genuinely passes rather than being waived.

I am not pushing an empty commit to re-trigger it. The checklist is advisory, not a
merge gate: on Blockcast COMMITPERCLIP_ENABLED is ${{ secrets.COMMITPERCLIP_KEY != '' }}
and that secret is not provisioned, so the quality/security gate steps are skipped and
the commitperclip PR Review run concluded success. No check-run or commit status
corresponds to the checklist — the only status at this head is review/ally-comment: success.
A no-op commit would cost the full CI matrix and cancel the ~10 checks currently in flight
to change nothing.

Flagging it so the checklist is not read as an unaddressed blocker. If a reviewer wants
the gate itself re-run rather than my reproduction of it, say so and I will push a
whitespace commit — but that is a re-queue of all CI, so I would rather not guess.

Ally review follow-up on #1650.

`system` is a multiplexer, so admitting the type wholesale reproduced the
defect this PR fixes one level down: a new subtype admitted by default,
exactly as a new top-level type was admitted by the old blocklist. The
membership criterion is stated per payload, so it has to be applied per
(type, subtype) wherever a type demultiplexes.

Measured against the v2.1.210 binary the adapter runs, `system` carries at
least `init`, `status`, `compact_boundary`, `hook_response` and
`mcp_status`. Only the first two are admitted. `hook_response` is why this
is a live hole rather than future-proofing: the binary builds it as
`{type:"system",subtype:"hook_response",...,output,stdout,stderr}`,
embedding a hook process's raw stdout — operator-configured, not
harness-authored, and reachable through `--settings` via the same
`config.extraArgs` channel that motivated inverting this guard at all.
(`hook_error`, named in the review, is in no v2.1.210 string table and is
not a subtype at this version.) A `system` line with no readable subtype
fails closed.

Both new cases were run as negative controls and fail without the gate
while the other 78 pass. The hook fixture is single-quoted deliberately:
JSON.stringify escapes `"` to `\"` and the phrase regex does not match
across the backslash, so a double-quoted fixture would have passed
vacuously and proved nothing.

Also from the review:
- Run the phrase test before the transcript walk. Semantically identical
  (both predicates pure, neither regex `/g`), but it skips an eager split
  of the entire pod log on the common phrase-absent failure.
- PROVENANCE.md: delete the blank line that terminated the GFM table, so
  the #1525 and BLO-31794 rows render as rows rather than literal text.
- parse.test.ts: drop the claim that a hardcoded fixture detects CLI
  drift, and condition `system:status` on `--include-partial-messages`.

Integrity hash recomputed. Refs BLO-31794.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Sep 4, 2026

Copy link
Copy Markdown
Author

Addressed at f22f4d19. Both Important items fixed, all three Suggestions taken. One correction to the review and one place I went further than it suggested.

Important

  • PROVENANCE.md:168 — fixed, and verified structurally rather than by eye: the Local-modifications table is now one contiguous run of | lines (149–169) with both the #1525 and BLO-31794 rows inside it. The blank at what is now 170 is kept, since that one correctly ends the table.
  • PR template sections — already resolved before the review, which read the commitperclip comment (17:44:45Z) rather than the body (completed 17:47:43Z). The gate's own scripts return passed: true on the live body; commitperclip PR Review concluded success. No action taken, and deliberately no empty commit.

Suggestions

  • Predicate order (parse.ts:548) — taken. Confirmed safe to swap: neither regex carries /g, so neither .test nor .exec is stateful, and both predicates are pure.

  • Comment accuracy — both taken. The toHaveLength(1) assertion now says it pins the fixture's shape and explicitly not the CLI's, and system:status is now conditioned on --include-partial-messages, in the test and in parse.ts.

  • system subtype (parse.ts:401)taken, and it is stronger than forward-looking. You couldn't produce a live false positive; I think that's because the reproduction needs the payload, not the trigger. Reading the v2.1.210 binary directly:

    subtype:"hook_response",hook_id:e.hookId,hook_name:e.hookName,
    hook_event:e.hookEvent,output:e.output,stdout:e.stdout,stderr:e.stderr,...
    

    system:hook_response embeds a hook process's raw stdout verbatim. That is operator-configured, not harness-authored — and hooks arrive via --settings, which job-manifest.ts:1256 appends verbatim from config.extraArgs. That is the same one-config-edit-away channel this whole issue rests on, so leaving system wholesale would have left the issue's own argument standing against its own fix. Gated to {init, status}; unreadable subtype fails closed.

    One correction: hook_error is in no v2.1.210 string table (0 occurrences against 22 for hook_response, 76 compact_boundary, 8 mcp_status). It is not a subtype at this version.

    One residual, disclosed: status is admitted even though its payload can carry compact_result/compact_error, which derive from model output. Compaction cannot occur before the first turn, so any transcript reaching it also carries an assistant line that this guard rejects independently — reachable only if that ever stops holding.

Verification

824/824 (was 822), typecheck clean, provenance gate actual == recorded at 5cd89f4c.

Negative control run on the two new cases — both fail with the subtype gate reverted while the other 78 pass, so they discriminate the gate rather than passing beside it. Worth flagging one trap that caught me: my first hook fixture double-quoted the phrase, and JSON.stringify escapes " to \", which the phrase regex deliberately does not match across. It passed — vacuously, proving nothing. The committed fixture is single-quoted and asserts the phrase is present in a form the scan actually matches. That is the exact "test passes while missing the real failure mode" shape this issue's AC warns about, so it seemed worth naming rather than quietly fixing.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Review of the f22f4d19 delta

Reviewed head: f22f4d194ae2f6609b404c00f75387e74f790cf2 (delta since my 3c8f7429 review).
Everything below was measured against the v2.1.210 CLI and this instance's real run logs, not read off the diff.

Headline: the subtype gate closes a false positive that is live today and fleet-wide — my previous review was wrong to call it "forward-looking". But as written it also disables genuine detection on ~91% of production runs, and neither the code comment, the Risks section nor the PROVENANCE row says so. Ship the gate; fix the predicate or the claims.


First, a correction to my own prior review

I wrote: "I tried to produce a live false positive and could not… this is forward-looking, not a demonstrated break, and it does not meet this PR's own 'one config edit from live' bar."

That was wrong, and you were right that the reproduction needs the payload. Reproduced end-to-end:

$ cat hookq.json
{"hooks":{"SessionStart":[{"hooks":[{"type":"command",
  "command":"echo \"Skill 'my-skill' not found\""}]}]}}
$ echo hi | claude --print --output-format stream-json --verbose --settings hookq.json
{"type":"system","subtype":"hook_started",...}
{"type":"system","subtype":"hook_response",...,"output":"Skill 'my-skill' not found\n",
                                               "stdout":"Skill 'my-skill' not found\n",...}
{"type":"system","subtype":"init",...}

The phrase reaches the raw transcript in a form the scan actually matches (single quotes are not JSON-escaped — the double-quoted variant is the vacuous trap you disclosed, and I hit it on my first attempt too). Running the real predicate source from all three commits against that transcript with assistantContentSeen: false:

version verdict
master TRUE — false positive → permanent retry suppression
3c8f7429 (allowlist, system wholesale) TRUE — false positive
f22f4d19 (+ subtype gate) false — fails closed ✅

So the gate is load-bearing, not defence-in-depth. It is also stronger than the PR argues: the PR frames hooks as reachable "one config edit away" through --settings in extraArgs. They are not one edit away — Paperclip provisions hooks itself. This very run has a Paperclip-written PreToolUse hook in session/.claude/settings.json (paperclip-env-guard.mjs), and a real production pod log on this instance shows a SessionStart:resume hook whose hook_response.stdout is an nginx 503 HTML error page, embedded verbatim inside a system event. Arbitrary external text, in a system event, in production, today. system wholesale was indefensible and this is the right fix.


Important (1)

parse.ts:437 — the subtype allowlist loses genuine detection on the overwhelming majority of production runs, and the suite can't see it.

hook_started / hook_response are emitted before init, so they precede the point at which a startup skill death occurs. Measured on this instance's run logs:

pod logs containing "subtype":"init"      : 47
pod logs containing "hook_started"        : 43   (91%)

real production log, first three lines:
  1  {"type":"system","subtype":"hook_started",...}
  2  {"type":"system","subtype":"hook_response",...}
  3  {"type":"system","subtype":"init",...}

Feeding the repo's own canonical positive fixture (parse.test.ts:732) with the production preamble prepended:

transcript master 3c8f7429 f22f4d19
A) init + error line (the fixture's synthetic shape) TRUE TRUE TRUE
B) same failure, real production preamble TRUE TRUE false — detection LOST

Either hook line alone is sufficient; hook_started + init and hook_response + init each fail closed independently.

This is the safe direction — a lost classification degrades to the retryable buildPartialRunError — so it is not a correctness bug and I am not calling it Critical. But three statements in this PR are now inaccurate, and they are the statements a future reader will rely on:

  • Risks item 3 describes the detection loss as "a future CLI event type that is genuinely harness-authored will now fail closed". It is not future; it is the present, on ~91% of runs.
  • parse.ts:410-434 enumerates "at least five subtypes — init, status, compact_boundary, hook_response, mcp_status". hook_started is a sixth, and I measured it — it is the one that trips the gate first. An enumeration presented as measured should contain it.
  • PROVENANCE.md says "Detection is unchanged". On the production shape it is not.

The suite is green at 824/824 precisely because its only positive-detection fixture is the synthetic two-line shape. That is the same failure mode this PR's own AC warns about — a test that passes beside the real behaviour rather than exercising it.

Options, in my order of preference:

  1. Attribute the phrase to its line instead of requiring every line to be harness-authored. The genuine signal is the phrase on a line that cannot carry untrusted text; every false positive in this family is the phrase inside an event payload. I prototyped it reusing your two allowlist sets verbatim — 8 fixture shapes covering both production cases and four false-positive shapes (hook_response, assistant prose, stream_event delta, pre-assistant user/tool_result):

    f22f4d19 wrong on 1/8      alt wrong on 0/8
    

    Caveat, disclosed because it is the weak point of my own suggestion: the production log I inspected has zero bare non-JSON lines (18/18 are events), so where the phrase lands in a real production skill-not-found death — bare line vs. inside an event — is unverified. My case B follows the repo's fixture convention, not a measured production failure. I could not obtain a real one. Treat the prototype as a direction, not a drop-in.

  2. Or add hook_started to the allowlist (payload is {hook_id, hook_name, hook_event, uuid, session_id} — scalars) while keeping hook_response out. Cheaper, but only partial: it restores case B, and hook_name is still operator-derived.

  3. Or keep the gate as-is and correct the three claims above, plus add a production-shaped positive fixture that documents the loss (asserting false with a comment saying why). Least work, honest, and leaves BLO-7991 AC3 effectively inert in production — which should then be its own issue.


Suggestions (2)

  • parse.ts:434"Truncation drops the tail, not the head, so a genuine init line is either whole or too short to carry the trigger phrase." This holds for init, but the sentence sits under a comment block that now governs all subtypes. A truncated hook_response is the interesting case, and it fails closed on the subtype read regardless — worth saying, since truncation is the class that produced the original bug.
  • Binary string-table claims (hook_response ×22, hook_error ×0, etc.) — I could not verify these: strings is absent here and the 261 MB binary is packed, so grep -a returns 0 even for literals I proved present behaviourally (assistant, stream-json). Not a refutation — flagging that the counts rest on your measurement alone. I did corroborate hook_error's absence behaviourally: a hook exiting 3 with stderr output still produced subtype:"hook_response" (exit_code:3, outcome), never hook_error. Consistent with your correction.

Strengths

  • The hook_response reasoning was right, and right for the right reason. You inferred from the payload that my "couldn't reproduce it" was a weak negative; it was. Reading the construction shape rather than trusting a failed repro is the better instinct and it found a live fleet-wide hole.
  • Disclosing the double-quote/JSON.stringify trap — a fixture that passed vacuously and proved nothing — is exactly the disclosure that makes the rest of the verification trustworthy. I independently hit the same trap.
  • All three of my prior Important/Suggestion items are genuinely fixed: PROVENANCE.md:168 blank line removed (table is now one contiguous run), predicate order swapped with a correct purity//g justification, and both comment overclaims narrowed. The toHaveLength(1) comment now correctly says it pins the fixture rather than the CLI.
  • Ordering assumption verified independently: {"type":"system","subtype":"init" — type first, subtype second, on both a real production line and a fresh v2.1.210 invocation. With mcp_servers populated (2 failed servers) the init line still carries exactly one "type" and one "subtype".
  • I tried and failed to break the gate via mcp_status: two failing MCP servers produced no mcp_status event pre-turn, so excluding it costs nothing measurable.

CI at this head

Vendored claude_k8s adapter, policy, security-review, review, Helm chart green. ~13 still queued/in_progress (Build, e2e, General tests ×6, Typecheck). Nothing red.

Recommended action

No Critical issues; the delta is a strict safety improvement and should land. Before merge, resolve the Important item — my preference is option 1, but option 3 is a legitimate call if you'd rather not expand scope, provided the three inaccurate claims are corrected and the production-shaped loss is written down rather than left implied by a green suite.

Disclosure: this PR is authored by the Ally App, so this is a formal COMMENTED self-review — GitHub bars a PR's author from APPROVE, not from reviewing. reviewDecision is empty on this repo (no required-review protection), so no approval identity is needed to merge.

…ranscript

The subtype gate in f22f4d1 was right to exclude `system:hook_response`, but
pairing it with a whole-transcript veto silently disabled detection on the
large majority of production runs.

Paperclip provisions a SessionStart hook itself, so `hook_started` /
`hook_response` open the transcript BEFORE `init`. Measured on this instance's
pod logs: 6510 of 8036 `init`-carrying logs contain `hook_started` (81%), and
in a 399-log sample carrying both, the hook line preceded `init` 399/399 times.
A veto requiring EVERY line to be harness-authored therefore returned false on
all of them — no raw scan, no `skill_not_found` — while the suite stayed green,
because its only positive fixture was a synthetic two-line shape no production
run has. That is exactly the "fix the false positive by disabling detection
entirely" failure mode BLO-31794's acceptance criteria warn about.

Attribute the phrase to its line instead: it counts only when it sits on a line
the harness authored (an allowlisted event, or a bare non-event line, which in
stream-json mode is the CLI speaking outside the protocol — 0 of 6893 sampled
production lines are bare). Every false positive in this family is the phrase
INSIDE an event payload, so this is the more faithful invariant, and unknown
types still fail closed.

The classification logic is unchanged — lifted verbatim into
`claudeLineIsHarnessAuthored`. Only the quantifier moved, from "every line is
harness-authored" to "the phrase sits on a harness-authored line". So
`hook_response` stays excluded (adding it, or `hook_started`, would admit
operator text) and detection survives the preamble regardless.

Corrects three claims the review found inaccurate: the subtype enumeration
omitted `hook_started` (a sixth, and the one that trips first); PROVENANCE.md
said "Detection is unchanged" when on the production shape it was not; and the
loss was described as forward-looking when it was live and fleet-wide.

One deliberate narrowing, recorded in source: the phrase regex's `\s+` matches
a newline, so a phrase straddling two lines no longer matches. The CLI emits it
on one line, and the direction is safe — a lost classification degrades to the
retryable `buildPartialRunError`, whereas a false positive is permanent.

Negative control: the four new detection cases FAIL against the whole-
transcript veto while every false-positive case still passes, so they
discriminate the fix rather than passing beside it. 828/828 green (was 824).

Refs BLO-31794. PROVENANCE integrity hash recomputed.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Sep 4, 2026

Copy link
Copy Markdown
Author

Response to the f22f4d19 review — taking option 1, pushed 1c092cf

The Important item is correct and I reproduced it independently at larger scale. I did not take it on the review's measurement — I re-derived it from this instance's pod logs and from the real predicate source.

Confirming the finding

measurement your sample mine
logs with "subtype":"init" 47 8036
of those, containing hook_started 43 (91%) 6510 (81%)
hook line precedes init first-3-lines observation 399/399 in a sample carrying both

Then the real predicate (extracted verbatim from parse.ts, not reimplemented) against a transcript built from verbatim production bytes — lines 1–2 of a real .pod.ndjson, not a synthesized preamble:

DETECTED | A) synthetic fixture (init + error)
LOST     | B) REAL production preamble + init + error
LOST     | C) hook_started only + init + error
LOST     | D) hook_response only + init + error

So: confirmed, and confirmed that either hook line alone is sufficient. The gate as shipped disabled detection on ~81% of production runs, the suite could not see it, and my PROVENANCE row asserted the opposite. That is the exact "green suite beside the real behaviour" failure this issue's own AC warns about, so I'm treating it as in scope rather than deferring it.

What I changed — option 1

The predicate now asks "did the harness write the line the phrase is on?" rather than "is this transcript globally clean?".

The classification logic is unchanged — lifted verbatim into claudeLineIsHarnessAuthored. Only the quantifier moved: ∀ lines harness-authored∃ a harness-authored line carrying the phrase. Consequences:

  • hook_response stays out of the allowlist, so its output/stdout/stderr can never be scanned. Your option 2 (admit hook_started) would have restored case B but admits operator-derived hook_name, and still leaves hook_response vetoing — strictly worse on both axes.
  • Unknown types still fail closed, so the property this issue was filed for survives.
  • Untrusted text elsewhere no longer vetoes a real death — including after it, which I added a case for since the old veto was order-insensitive and so is this.

Negative control (the discipline this AC asks for): with the whole-transcript veto restored, the four new detection cases fail while every false-positive case still passes — so they discriminate the fix rather than passing beside it, and I have not traded safety for detection.

× still classifies behind the production hook preamble that precedes init
× still classifies when either hook line alone precedes init
× still classifies when an untrusted event follows the genuine death
× still classifies when an unreadable system event does not carry the phrase
✓ refuses to scan when a system event carries operator-configured hook output
✓ refuses to scan a system event with no readable subtype that carries the phrase
✓ refuses to scan when an unenumerated event type carries the phrase

828/828 green (was 824). Provenance hash recomputed to 8f77b1f5.

One existing test changed semantics — flagging rather than burying it

refuses to scan when a system event has no readable subtype asserted false on a transcript whose phrase was on a bare line, with an unrelated malformed system line elsewhere. Under attribution that unrelated line is no longer where the evidence is, so it returns true.

I did not flip the assertion. I split it into the two properties that were conflated:

  • the phrase on an unreadable-subtype line → still false (that line could be a truncated hook_response) — this is the real safety property, and it still holds;
  • an unreadable line not carrying the phrase → no longer vetoes a genuine death.

Worth stating explicitly: truncation cannot produce the dangerous shape, because subtype is emitted immediately after type, so a hook_response truncated anywhere far enough to include payload text still carries its subtype and fails closed.

The three inaccurate claims — all corrected

  1. hook_started was missing from the enumeration. You're right, and it's the one that trips first. Now listed as a sixth subtype.
  2. PROVENANCE "Detection is unchanged" — rewritten to state the loss, its measured scale, and the fix, rather than deleting the claim. (Its earlier "defence-in-depth, not a live fix" framing referred to nested types on an init line, which remains accurate; it did not license the detection-loss claim, and I've said so in the row.)
  3. Risks framed the loss as forward-looking. It was live and fleet-wide. Also corrected upward: hooks are not "one config edit away" via extraArgsPaperclip provisions them, which I verified in this very run's session/.claude/settings.json, and a real hook_response.output in the logs carries an operator status message verbatim. Your framing was stronger than mine and it's now recorded that way.

Suggestions

  • Truncated hook_response — taken; the comment now says the truncation case fails closed on the subtype read regardless of what its severed tail held.
  • Binary string-table counts — your caveat is fair and I'm not going to pretend otherwise: those counts rest on my measurement alone and you could not independently verify them. Correcting myself on a detail while I'm here: I first wrote that I'd removed them from the source comment, then checked — they were never in parse.ts at all, only in my Paperclip issue comment. Nothing to remove, and claiming a removal I hadn't made would have been the same species of unchecked assertion as the "Detection is unchanged" row. What the source does carry is the single load-bearing claim (hook_error absent at v2.1.210), which your behavioural corroboration — exit-3 hook still emitting hook_response — independently supports, so it no longer rests on the string table alone.

Your disclosed caveat — I could not settle it either, and it's worth recording

You flagged that where the phrase lands in a real production skill-death (bare line vs inside an event) is unverified. I tried to settle it and could not: scanning all 19,793 pod logs on this instance for the trigger phrase returns zero hits. There has never been a real skill-death in this corpus, so both your case B and the repo's fixture remain synthetic on that point.

What I can say is that the assumption is unchanged by this PR — master and f22f4d19 both scan bare lines identically (a line with no "type" was never rejected), so bare-line trust is pre-existing, not something attribution introduces. And it is corroborated in the safe direction: 0 of 6893 sampled production lines are bare, so in stream-json mode a bare line is the CLI speaking outside the protocol rather than a payload relaying model text. If that ever stops holding, the exposure is identical in all three versions.

Not merging — per the 2026-09-04 CEO ruling I don't merge on a non-success gate, and this hands to the Release Engineer once review passes. No marker request posted: the push fired synchronize on a non-draft PR, which triggers the automatic reviewer wake on its own.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 1c092cf

This head adopts option 1 from my f22f4d19 review — attributing the phrase to its line
rather than demanding a globally clean transcript. I re-derived the central claims by
extracting the predicate source from this exact head and running it, rather than reading
the diff; results under Strengths. Both prior findings are fixed and I found no blockers.

Prior Findings Dispositioned (2)

  • prior:3c8f742 important 1 — fixed — vendor/paperclip-adapter-claude-k8s/PROVENANCE.md:169 — the
    blank line that terminated the GFM table is gone. Fetched the file at this head: rows 163–169 are
    now one contiguous run (#1525 at 168, BLO-31794 at 169) and the first blank is at 170, which
    correctly separates the table from the following paragraph. Both new rows render as table rows.
  • prior:3c8f742 important 2 — fixed — PR body — all four required template sections are present
    (## Thinking Path, ## What Changed, ## Risks, ## Model Used), and the dedup-search checkbox
    is checked. ## Risks now records the deliberate detection narrowings rather than omitting them.

Critical Issues (0)

None.

Important Issues (0)

None. I tried to break the predicate on ten decisive shapes and could not — detail below.

Suggestions (2)

  • [gstack/review] vendor/paperclip-adapter-claude-k8s/src/server/parse.ts:497if (!match) return true
    trusts any line carrying no "type", and the justification given for it (:492, and the PROVENANCE
    row's "0 of 6893 sampled production lines are bare") is empirical. It is actually structural,
    and saying so would make it durable. job-manifest.ts:1725 builds the invocation as
    cat /tmp/prompt/prompt.txt | claude … | tee <podLogPath> | <failFastFilter> > /dev/null, with no
    2>&1 anywhere on that pipeline — so the file this predicate reads receives only Claude's stdout.
    Hook stderr, MCP-server stderr, the fail-fast [wrapper] message (written to /dev/stderr, and
    downstream of the tee regardless) and the prompt itself all bypass it by construction. That is why
    no bare line can carry untrusted text, and it is a much stronger statement than a sample.

    • Worth recording at :492 because it also names the exact future edit that would silently
      invalidate the trust decision: adding 2>&1 before the tee (to capture CLI diagnostics, say)
      would begin routing operator- and MCP-authored stderr into this surface as bare, trusted lines.
      A one-line note there turns that into a reviewable change rather than an invisible one.
  • [pr-review-toolkit/comments] parse.ts:557 and :646 both describe stdout as "the entire pod
    log"
    . Given the above it is the pod log's stdout stream — accurate for the assistant-prose
    hazard those comments are actually about (assistant events are on stdout), but it overstates the
    surface, and the overstatement is what makes the bare-line trust look riskier than it is.

    • Minor, and lower priority than the first item; the two are the same correction from opposite
      directions.

Strengths

  • The production-preamble fix is real, and the negative control genuinely discriminates it. I
    extracted the predicate from this head and from f22f4d19 and ran both over the same transcripts,
    rather than trusting either the diff or my own prior review:

    shape f22f4d19 1c092cfe wanted
    production preamble (hook_started, hook_response, init) + genuine death false — detection lost true detect
    synthetic two-line fixture + genuine death true true detect
    hook_response payload carrying the phrase false false reject

    So the new case fails against the revision it replaces, which is the discipline this PR set itself.

  • Ten decisive shapes, all correct. Beyond the table: hook_started alone before init,
    system:status after init, and an untrusted event after the genuine death all still classify;
    assistant prose, a stream_event delta, a pre-assistant user/tool_result, a truncated result,
    and a system line with no readable subtype are all still rejected. Unknown types fail closed.

  • The false-positive fixtures are non-vacuous, and the suite proves it rather than assuming it.
    This was the trap we both hit earlier: Skill "x" not found embedded in JSON becomes Skill \"x\",
    which CLAUDE_SKILL_NOT_FOUND_RE cannot match, so a payload fixture written with double quotes
    passes without the scan ever running. Every payload-embedded fixture here uses single quotes
    (:468, :749, :766, :786, :846, :984, :1102, :1138) while bare CLI lines keep double
    quotes, and each is pinned by an explicit expect(transcript).toContain(…) vacuity guard. I
    re-confirmed independently that all six of my own false-positive cases genuinely reached the scan
    before being rejected — a check I ran precisely because I could otherwise have repeated the mistake.

  • Attribution is the more faithful invariant, and the comment at :506-534 argues it correctly.
    Every false positive in this family is the phrase inside an event that can carry text the harness
    did not author; the genuine signal is the phrase on a line that cannot. "Did the harness write THIS
    line?" is the right question, and it is what lets unrelated untrusted text stop vetoing a real death.

  • The PROVENANCE row corrects its own prior claim instead of quietly overwriting it. It states
    plainly that the subtype gate as first shipped disabled detection in production and that the row
    previously claimed otherwise, gives the measurement (6510/8036, 81%; hook line first in 399/399),
    and separates what the earlier "defence-in-depth rather than a live fix" framing did and did not
    license. That is the disclosure that makes the rest of the record trustworthy.

  • Version bumped 0.2.6-blockcast.1.2 and the #1525 retroactive row added, closing the two
    process rules that PR left unwritten. Vendored claude_k8s adapter is green at this head, which
    covers the integrity-hash recomputation and the suite.

  • The \s+-across-newline narrowing is disclosed in the source rather than left for a reader to
    discover, and the direction is the safe one under a retry-killing code.

CI at this head

Vendored claude_k8s adapter, e2e, policy, review, security-review, Helm chart green;
Storybook visual regression skipped; ~12 still queued (Build, Typecheck, General tests ×6,
Canary Dry Run, others). Nothing red. mergeable_state is behind, so the branch needs an
update before it can land — no conflict.

Recommended Action

  1. No Critical issues.
  2. No Important issues — nothing blocks merge on review grounds.
  3. Consider the two Suggestions opportunistically; the parse.ts:497 one is the substantive of the
    pair, since it converts the bare-line trust decision from a sample into a structural invariant and
    names the edit that would break it.
  4. Update the branch (currently behind master) and let the queued lanes finish before merging.

Disclosure: this PR is authored by the Ally App, so this is a formal COMMENTED self-review —
GitHub bars a PR's author from APPROVE, not from reviewing. reviewDecision is empty on this repo
(rules/branches/master returns only merge_queue, no pull_request rule), so no approval identity
is required to merge and no gate is left unmet by the comment form.

@allyblockcast
allyblockcast Bot added this pull request to the merge queue Sep 5, 2026
Merged via the queue into master with commit d81c5f4 Sep 5, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants