gate: bind decisions to a decider, record what landed, and measure the bypass - #249
gate: bind decisions to a decider, record what landed, and measure the bypass#249itsHabib wants to merge 7 commits into
Conversation
|
@claude review |
|
Claude finished @itsHabib's task in 4m 23s —— View job PR #249 Review
|
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Reviewer: claude |
|
@claude review Round 1 fixes pushed at
Both observations are now comments in the code (the |
…xcluded Review round 1 on #249. P1 — the bypass rows attributed the merge to the PR's AUTHOR. `gh pr list --json author` is the creator; `lookupLanding` on the single-PR path already read `merged_by`. So "who merged this without authorization" — the one question a bypass row exists to answer — named the wrong person on any repo where authors do not self-merge. Now reads `mergedBy`. P2 — the 500-PR sweep ceiling was silent. On a busy window the oldest merges would be dropped and the reader would see "0 bypasses" over half a window with nothing saying so. The limit is recorded on the coverage artifact and printed, and a sweep that reaches it says INCOMPLETE. A report whose scope the reader cannot check is worse than none — the same reason the artifact already states its basis. P3 — authorizations issued before the window were dropped from authorized_never_landed silently. That is the one class meaning "something was authorized and never closed", so an authorization from 60 days ago simply did not appear in a 30-day sweep and the reader saw absence rather than exclusion. The list stays window-scoped, like every other class; the exclusion is now counted and rendered. Also two comments the reviewer found ambiguous: the resume command's `-who NAME` placeholder does not become the decider (the persisted judgment's attribution stands; the flag is only checked non-empty), and sweepReceipts appends receipts before the coverage artifact deliberately — a failed coverage append leaves the proven discharges durable and the sweep re-runnable, where the reverse order could claim discharges that were never written. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @itsHabib's task in 3m 37s —— View job PR #249 — Round 2 Review
All three P1/P2/P3 fixes are correct, and both observations now carry exactly the comments that were requested. Two minor notes on implementation semantics below — neither is blocking. P1 — Verified correct
P2 — Verified correct; one implementation note
One conservatism: on a repo with 500+ total merged PRs, the raw count hits the ceiling even if every window PR is present in the newest 500. The INCOMPLETE message says "narrow -since and re-run" — correct advice for real truncation, but a false positive here does nothing worse than prompt an unnecessary re-run with a shorter window (which self-clears by showing a complete scan). Acceptable. P3 — Verified correct; one implementation note
The operator's natural response (widen Both observations — doneThe What's verified correct (round 2 additions)
Round 2 is clean. Residual observations are pre-existing design tradeoffs, not defects introduced by the fixes. | Branch |
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Review round 2 on #249, the one residual worth taking. The out-of-window outstanding count did not consult the receipt index, so an authorization issued AND discharged before the window — merge landed, receipt written, landing filtered out by the window — was counted as outstanding. Benign and self-correcting (widening -since moves it to authorized_and_landed), but it inflates the one number whose entire meaning is 'something was authorized and never closed', on a PR whose subject is that these numbers be honest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@claude review Round 2 fix pushed at
Deferred, with reasons:
This is fix-round 2 of 2 under the repo's review-cycle discipline; anything further goes to the judge as recorded residual. |
|
Claude finished @itsHabib's task in 1m 30s —— View job PR #249 — Round 3 Review
Fix verified —
|
Two items from the review panel, both in the same direction as the P1 all three reviewers found: never hide a live park. `absorbSubjectClosed` closed a subject on the mere existence of the artifact, ignoring its `state`. `sweep` is the only writer today and always writes `not_open`, but the kind is now whitelisted against the three states that actually mean finished. An unrecognised state — a future writer meaning something else, or a typo — leaves the row VISIBLE. A stale row is recoverable by looking at the screen; a hidden park is not. Also records why `receipt: superseded` is a closing fact, which the panel flagged as needing confirmation against #249. Verified against that branch: OutcomeSuperseded is "the PR merged, but at a DIFFERENT head than the one authorized" — a fact about the PR, not about the authorization. It makes the receipt a bad discharge and a perfectly good closing fact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…xcluded Review round 1 on #249. P1 — the bypass rows attributed the merge to the PR's AUTHOR. `gh pr list --json author` is the creator; `lookupLanding` on the single-PR path already read `merged_by`. So "who merged this without authorization" — the one question a bypass row exists to answer — named the wrong person on any repo where authors do not self-merge. Now reads `mergedBy`. P2 — the 500-PR sweep ceiling was silent. On a busy window the oldest merges would be dropped and the reader would see "0 bypasses" over half a window with nothing saying so. The limit is recorded on the coverage artifact and printed, and a sweep that reaches it says INCOMPLETE. A report whose scope the reader cannot check is worse than none — the same reason the artifact already states its basis. P3 — authorizations issued before the window were dropped from authorized_never_landed silently. That is the one class meaning "something was authorized and never closed", so an authorization from 60 days ago simply did not appear in a 30-day sweep and the reader saw absence rather than exclusion. The list stays window-scoped, like every other class; the exclusion is now counted and rendered. Also two comments the reviewer found ambiguous: the resume command's `-who NAME` placeholder does not become the decider (the persisted judgment's attribution stands; the flag is only checked non-empty), and sweepReceipts appends receipts before the coverage artifact deliberately — a failed coverage append leaves the proven discharges durable and the sweep re-runnable, where the reverse order could claim discharges that were never written. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review round 2 on #249, the one residual worth taking. The out-of-window outstanding count did not consult the receipt index, so an authorization issued AND discharged before the window — merge landed, receipt written, landing filtered out by the window — was counted as outstanding. Benign and self-correcting (widening -since moves it to authorized_and_landed), but it inflates the one number whose entire meaning is 'something was authorized and never closed', on a PR whose subject is that these numbers be honest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3c63b7d to
dac4706
Compare
* feat(gate): close the inbox by supersession and mootness `gate next` showed 164 rows against 3 open pull requests. 14 were parked and 150 were ready-to-merge, and the dominant leak was not the one the brief assumed: supersession already worked (the reducer folds runs by subject, and 107 of the log's 366 parks are discharged that way). What the log could not see is a PR ENDING. Every action gate writes is dry_run/would_merge — gate authorizes and an executor acts — so once the emitted command landed the PR, nothing ever said so and the row stood forever. Three parts. **One subject-scoped reduction** (`observe/closure.go`). The fold and the closure index are built once and consumed by the parked projection, the ready projection, the sweep's work list, and the audit metric — the extraction a follow-up named as the durable fix for gate's three independently-derived "is this park still open" notions. `TestParkDischargeAgreesWithTheInbox` pins that two of them cannot disagree. **Rows are classified, never dropped.** superseded / moot / stale (a PR still open whose head moved past the authorized SHA — owed work, not finished work, and deliberately not folded into moot). Counts always project; `-all` shows the rows. A discharged park carries no judge/resolve command, so a one-shot judgment cannot be spent on a settled question. **`gate sweep` records mootness.** `next -live` already discovered it on every invocation and threw it away; this persists the same batched open-PR read — no new GitHub client — as a `subject_closed` artifact parented to the terminal its row stands on, so the store's absent-parent guard makes a re-run a no-op. It records only what that read proves (`not_open`); the merge commit, actor, and clock are receipt/reconcile's claim to make from the platform. It is a separate verb rather than a flag on `next` because it writes, and `next -json` is on escalate serve's Slack path under a hard budget. `gate audit` reports by-judgment vs by-supersession after the chain check, without touching the exit code: 245 / 107 / 14 on the live ledger, a 29% supersession share. Verified against the live ledger read-only: `sweep -dry-run` closes 161 of 164, leaving exactly the 3 `-live` shows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(gate): a closing fact must not settle a terminal it predates Self-review found a real defect in the closure index, and it is the mirror image of the bug this PR exists to fix. A pull request can be closed and REOPENED — this repo's own review-cycle rule says a PR past its cap is "closed and re-opened fresh" — and the re-gated PR then parks again AFTER a sweep recorded the closure. The index was last-writer-wins by subject with no ordering, so that stale closing fact mooted the fresh park forever: a live merge-authorization question silently hidden, which is exactly the failure mode the moot class was added to prevent, running backwards. closingFact now carries its position in the log and `settles` requires it to POSTDATE the terminal it is asked to settle. Pinned in both directions: a park, a re-authorization, and the audit metric all stay live across a reopen, and a closure that legitimately postdates its terminal still moots it. Also: sweep now shares observe's tested parallel fan-out (`OpenSets`) instead of its own sequential loop — 19 repos in 1.9s rather than serially. Same result on the live ledger: 161 of 164 closed, leaving the 3 that are genuinely open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(gate): a wrongly-mooted subject is also unsweepable Codex's P1 on #258 named a consequence worth pinning separately from the fix: the sweep's work list IS the live rows, so a subject the reduction wrongly mooted could never be repaired by re-running the sweep. Assert the reopened subject is back on LiveSubjects, not merely back on screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(gate): only a recognised closing state settles a subject Two items from the review panel, both in the same direction as the P1 all three reviewers found: never hide a live park. `absorbSubjectClosed` closed a subject on the mere existence of the artifact, ignoring its `state`. `sweep` is the only writer today and always writes `not_open`, but the kind is now whitelisted against the three states that actually mean finished. An unrecognised state — a future writer meaning something else, or a typo — leaves the row VISIBLE. A stale row is recoverable by looking at the screen; a hidden park is not. Also records why `receipt: superseded` is a closing fact, which the panel flagged as needing confirmation against #249. Verified against that branch: OutcomeSuperseded is "the PR merged, but at a DIFFERENT head than the one authorized" — a fact about the PR, not about the authorization. It makes the receipt a bad discharge and a perfectly good closing fact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(gate): revalidate the terminal under the lock before closing a subject Two P1/P2s from codex's second round, both real, and the second exposed a weakness in my own test. **P1 — read-to-append race.** The sweep reads its work list, fetches GitHub (seconds), then appends. A PR reopened and re-gated inside that window gets a NEW terminal, and the closure lands AFTER it in the log — mooting the fresh park, since a closing fact settles by log order. The absent-parent guard allowed it, being keyed on the OLD terminal. This is the same defect the ordering rule fixed, arriving through a race rather than through history. `recordClosed` now passes a `stillNewest` check to AppendIfAbsentParentWhereAfterAudit, evaluated inside the store lock, so no terminal can land between the check and the append. It reduces through observe's shared fold (`NewestTerminal`) rather than forming a second opinion about which terminal is current. A re-gated subject is reported, not failed: the fresh terminal is the current truth and a later sweep can close it on its own merits. **P2 — the sentinel is wrapped.** The store returns `fmt.Errorf("%w: ...", ErrAlreadyExists)`, so `err ==` silently never fired and a correctly deduplicated concurrent sweep would have reported as a hard failure. Now `errors.Is`, matching every other caller. TestSweepIsIdempotent could not have caught it: the work list is the LIVE rows, so after the first sweep there is nothing left to check and its `Already` assertion looped over an empty slice. That vacuity is now asserted directly, and TestSweepReportsAWrappedDuplicate reaches the path by racing two closures on one terminal. Both new tests were confirmed to fail against their specific bug and pass against the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…xcluded Review round 1 on #249. P1 — the bypass rows attributed the merge to the PR's AUTHOR. `gh pr list --json author` is the creator; `lookupLanding` on the single-PR path already read `merged_by`. So "who merged this without authorization" — the one question a bypass row exists to answer — named the wrong person on any repo where authors do not self-merge. Now reads `mergedBy`. P2 — the 500-PR sweep ceiling was silent. On a busy window the oldest merges would be dropped and the reader would see "0 bypasses" over half a window with nothing saying so. The limit is recorded on the coverage artifact and printed, and a sweep that reaches it says INCOMPLETE. A report whose scope the reader cannot check is worse than none — the same reason the artifact already states its basis. P3 — authorizations issued before the window were dropped from authorized_never_landed silently. That is the one class meaning "something was authorized and never closed", so an authorization from 60 days ago simply did not appear in a 30-day sweep and the reader saw absence rather than exclusion. The list stays window-scoped, like every other class; the exclusion is now counted and rendered. Also two comments the reviewer found ambiguous: the resume command's `-who NAME` placeholder does not become the decider (the persisted judgment's attribution stands; the flag is only checked non-empty), and sweepReceipts appends receipts before the coverage artifact deliberately — a failed coverage append leaves the proven discharges durable and the sweep re-runnable, where the reverse order could claim discharges that were never written. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review round 2 on #249, the one residual worth taking. The out-of-window outstanding count did not consult the receipt index, so an authorization issued AND discharged before the window — merge landed, receipt written, landing filtered out by the window — was counted as outstanding. Benign and self-correcting (widening -since moves it to authorized_and_landed), but it inflates the one number whose entire meaning is 'something was authorized and never closed', on a PR whose subject is that these numbers be honest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dac4706 to
65a8efe
Compare
A judgment is the one verdict a person or a delegated provider authors
rather than a verifier computes, and until now the record could not say
which. All 237 judgment artifacts in the live ledger carry the identical
body key set — confidence, decision, producer, source, subject, tier, why
— with no `who` anywhere, so a human approval and an agent-composed one
are indistinguishable and the operator's 129 `producer.impl=operator`
approvals name nobody. That is the separation-of-duties gap.
Verdict gains an optional `decider` {who, method, at}: the identity, the
CHANNEL it was established through, and the decider's own clock (distinct
from the envelope's append time — a phone tap and its later append are two
different, both-true facts). Method is a closed vocabulary —
cli-operator, slack-interactive, auto-<provider> — and names the channel,
never an authenticated claim; gate authenticates none of them, and
pretending otherwise would be the more dangerous error.
The binding is enforced on the WRITE path only (RequireDecider, ahead of
the judgment append) and every reader stays tolerant, because the ~237
judgments recorded before the field existed must still explain and
re-reduce; a reducer that rejected them would trade one unanswerable
question for a log that cannot be read at all. explain renders the
attribution, and spells absence as the literal "unattributed" rather than
omitting the line — a blank tells a reader nothing about whether the
question was even asked.
- contracts: Verdict.Decider + method constants; schema v0.3.0 -> v0.4.0,
additive and omitempty so existing bodies still validate.
- contracts/escalation: Resolution gains method, optional for the same
reason.
- gate: -who/-method on judge and resolve; -auto derives its own decider
(the resolved provider wrapper + model) and refuses a claimed one;
the submitted-artifact path records the SUBMITTER, since the model did
not choose to submit it.
- escalate: the Slack transport declares slack-interactive as its own
fact, so who survives structurally instead of only as prose in why.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
itsHabib/ivy#22 (run_fe7ac73ddb7c59a7): judgment jdg_b65ffd12563e8d85 written at 02:01:14Z, then nothing. No verdict, no action, no resolution. The PR stayed open, `gate next` still reported the run as parked, and the Slack card said gate had authorized the merge. Cause: `serve.process` gave ONE 25s budget to the grant lookup (`gate next -json`, a full-log projection) and the decision (`gate resolve`, which appends judgment -> verdict -> action as separate artifacts), and `exec.CommandContext` SIGKILLs on expiry. A deadline landing mid-sequence does not cancel a decision, it strands one. The two outcomes are not symmetric: too short destroys an authorization and needs a human to notice; too long delays a card that was already acked. - serve: separate budgets. The lookup is a read that can be killed for free; the decision writes durable state and gets a hang guard, not a latency budget. Raising the old number alone would only have widened the window — it is harmless now because of the two changes below. - stamp: ONE small budget for the whole attempt instead of one per gh call (two sequential calls could spend double), and gh is resolved before invocation so an unresolvable binary reports as itself. - gate: the ordering is now a precondition of the payload, not a convention. stamp.Post requires the action artifact's chain hash, which exists only once that artifact is durable — so no GitHub call can precede the authorization it decorates. Pinned by test. - gate: the stamp's outcome is REPORTED on the result JSON, not only logged to a stderr nobody reads. "Best-effort" was half true: the card that says "gate authorized the merge" had no way to know no status was posted. - gate next: a run holding a judgment with no outcome is its own row — "judged but not authorized" — with the command that finishes it. The state was always recoverable (the judgment path RESUMES a persisted judgment rather than refusing it as a duplicate); nothing said so, and the run also read as parked, sending the operator to judge a park whose one judgment was already spent. Verified against the live ledger: it finds exactly ivy#22 across 4,900+ artifacts. - escalate serve: a third fail-closed startup gate beside the signing secret and the allowlist. Those refuse an ingress that cannot AUTHENTICATE a tap; this refuses one that cannot COMPLETE it. gh is checked even though escalate never invokes it — gate does, and under launchd's default PATH it was unresolvable, so every phone approval failed to stamp while the card reported success. A judgment recorded before the decider binding stays completable: resume loads the persisted judgment and never rebuilds it, so the write-path decider requirement cannot strand a decision already in the log. Pinned by test against ivy#22's exact shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate's log answers "was this merge authorized" completely and "did it merge" not at all. All 163 action artifacts in the live ledger carry dry_run: true and the outcome would_merge, because gate authorizes and an executor acts. So state can prove a merge was allowed and cannot say whether it happened, when, as which commit, or by whom — and, worse, cannot prove the negative: that nothing merged AROUND the gate. Two artifacts close that. `gate receipt -run <id>` discharges one authorization with what landed. Nothing about the landing comes from the caller: the merge commit, the actor, and the timestamp are read back from the GitHub API, so the record carries an INDEPENDENT clock and an independent actor rather than an executor's account of its own behavior — which is exactly the claim a receipt exists to check. The classification is head-to-head, not "did it merge": a PR that merged at a head this action never saw is recorded `superseded`, never as a clean discharge, because joining on the PR number alone would rebuild one layer up the laundering --match-head-commit exists to prevent. One receipt per action is structural — the substrate's absent-parent guard refuses the second, so no code path can double- discharge. Gate still performs no merges. It authorizes, an executor acts, a receipt records; collapsing those would put the decision and the effect in one process, which is the separation the design rests on. `gate reconcile -repo R [-since]` reads the PLATFORM first and asks what gate can account for, so an absence of gate artifacts becomes visible as an absence instead of as silence. It classifies authorized-and-landed, authorized-never-landed, and landed-without-authorization, and writes a coverage artifact. Merges predating adoption are classified separately and never counted as bypasses — reporting history alongside a real bypass is how a real bypass gets ignored. The adoption boundary defaults to a fact state already holds (the first time gate ran on that repo), so the common case needs no config file, no custody surface, and nothing to remember; -effective-from overrides it. Reconcile also sweeps receipts for authorizations it proved landed at the authorized head — the backstop for an executor that never wrote back — and only that class, since inventing an outcome for a superseded or abandoned one is the fabrication a receipt prevents. The artifact bodies stay gate-internal: gate is the only writer and the only reader today. Promotion to contracts/ with an embedded schema is the repo's lazy-migration trigger — the first time a second tool touches one. `gate audit` now reports both anomalies as first-class findings alongside the chain check, and keeps them apart: a tampered chain means the record cannot be trusted and still fails; an unreceipted authorization means the record is trustworthy and incomplete, and exits 0. When no repo has been reconciled it says merge-without-authorization is UNMEASURED rather than reporting zero. Against the live ledger it reports 163 authorizations with no receipt and 238 unattributed judgments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rruption-recovery rule DESIGN.md gains the three new artifact-contract consequences (a decision names its decider; an authorization is discharged by a receipt whose facts are the platform's; a control that cannot prove the negative has not been measured), the integrity-vs-accountability split in the tamper model, and an Interruption and recovery section stating what a killed run leaves and how it is finished. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xcluded Review round 1 on #249. P1 — the bypass rows attributed the merge to the PR's AUTHOR. `gh pr list --json author` is the creator; `lookupLanding` on the single-PR path already read `merged_by`. So "who merged this without authorization" — the one question a bypass row exists to answer — named the wrong person on any repo where authors do not self-merge. Now reads `mergedBy`. P2 — the 500-PR sweep ceiling was silent. On a busy window the oldest merges would be dropped and the reader would see "0 bypasses" over half a window with nothing saying so. The limit is recorded on the coverage artifact and printed, and a sweep that reaches it says INCOMPLETE. A report whose scope the reader cannot check is worse than none — the same reason the artifact already states its basis. P3 — authorizations issued before the window were dropped from authorized_never_landed silently. That is the one class meaning "something was authorized and never closed", so an authorization from 60 days ago simply did not appear in a 30-day sweep and the reader saw absence rather than exclusion. The list stays window-scoped, like every other class; the exclusion is now counted and rendered. Also two comments the reviewer found ambiguous: the resume command's `-who NAME` placeholder does not become the decider (the persisted judgment's attribution stands; the flag is only checked non-empty), and sweepReceipts appends receipts before the coverage artifact deliberately — a failed coverage append leaves the proven discharges durable and the sweep re-runnable, where the reverse order could claim discharges that were never written. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review round 2 on #249, the one residual worth taking. The out-of-window outstanding count did not consult the receipt index, so an authorization issued AND discharged before the window — merge landed, receipt written, landing filtered out by the window — was counted as outstanding. Benign and self-correcting (widening -since moves it to authorized_and_landed), but it inflates the one number whose entire meaning is 'something was authorized and never closed', on a PR whose subject is that these numbers be honest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebasing onto main brought `grant-callback` alongside this branch's `receipt` and `reconcile`, pushing main's switch past the cyclop ceiling (21 > 20). A table keeps dispatch flat: adding a verb costs one row and no branch, and the handlers keep their own policy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
65a8efe to
3afe854
Compare

Four audit gaps in gate's decision record, each closed by a separate commit. Every claim below was verified against the live ledger (
~/dev/gate/state/log.jsonl, 4,900+ artifacts), not inferred.1.
whobinding — a judgment named nobody (25d6df0)All 237 judgment artifacts in the ledger carry the identical body key set:
confidence, decision, producer, source, subject, tier, why. Nowho, anywhere. So a human approval and an agent-composed one are indistinguishable in the record, and the operator's 129producer.impl=operatorapprovals name nobody. That is the separation-of-duties gap.Verdictgains an optionaldecider{who, method, at}.methodis the channel the identity was established through —cli-operator,slack-interactive,auto-<provider>— and names the channel, never an authenticated claim; gate authenticates none of them. The same operator string can be typed by an agent at a shell or produced by a signature-verified Slack callback, and only the channel separates those.atis the decider's own clock, deliberately distinct from the envelope's append time: a phone tap and its later append are two different, both-true facts.Enforced on the write path only (
RequireDecider, ahead of the append). Every reader stays tolerant, because the 237 judgments recorded before the field existed must still explain and re-reduce — a reducer that rejected them would trade one unanswerable question for a log that cannot be read at all.explainrenders absence as the literalunattributedrather than omitting the line; a blank tells a reader nothing about whether the question was even asked.-autoderives its own decider (resolved provider wrapper + model) and refuses a claimed one. The submitted-artifact path records the submitter, since the model did not choose to submit it. Schemav0.3.0 → v0.4.0, additive andomitempty.2.
receipt— gate could not say what landed (223b887)All 163 action artifacts carry
dry_run: true/would_merge. State can prove a merge was allowed and cannot say whether it happened, when, as which commit, or by whom.gate receipt -run <id>discharges one authorization. Nothing about the landing comes from the caller — merge commit, actor, and timestamp are read back from the GitHub API, giving the record an independent clock and an independent actor rather than an executor's account of its own behavior, which is exactly the claim a receipt exists to check. Classification is head-to-head, not "did it merge": a PR merged at a head the action never saw issuperseded, never a clean discharge, because joining on the PR number alone would rebuild — one layer up, in the reporting — the laundering--match-head-commitprevents. One receipt per action is structural (the store's absent-parent guard).Gate still performs no merges. Gate authorizes, an executor acts,
gate receiptrecords. Collapsing those would put the decision and the effect in one process.3.
reconcile— gate could not prove the negative (223b887)Every other surface reads gate's own decisions back and can only report what gate did.
gate reconcile -repo R [-since]reads the platform first and asks what gate can account for, so an absence of artifacts becomes visible as an absence rather than as silence. It writes acoverageartifact: authorized-and-landed, authorized-never-landed, landed-without-authorization.Pre-adoption merges are classified separately and never counted as bypasses — reporting history alongside a real bypass is how a real bypass gets ignored. The boundary defaults to a fact state already holds: the first artifact naming the repo, i.e. the control took effect when the control first ran. No config file, no custody surface, nothing to remember, and it cannot go stale.
-effective-fromoverrides.The basis is stated on the artifact (
merged-pull-requests): a direct push to the protected branch has no PR and no head to join by, so it is outside this claim and belongs to branch protection. A report that quietly omitted it while implying it covered everything would be worse than no report.gate auditreports both anomalies as first-class findings, and keeps integrity and accountability apart: a tampered chain still fails; an incomplete record prints and exits 0. Before any reconcile it says UNMEASURED, not zero.Live output today:
4. A killed resolve stranded an authorization (
d56fdb5)itsHabib/ivy#22,run_fe7ac73ddb7c59a7: judgmentjdg_b65ffd12563e8d85written at02:01:14Z, then nothing. No verdict, no action, no resolution. The PR stayed open,gate nextstill called the run parked, and the Slack card said gate had authorized the merge.serve.processgave one 25s budget to the grant lookup (gate next -json, a full-log projection over a ledger that only grows) and the decision (gate resolve, which appends judgment → verdict → action separately), andexec.CommandContextSIGKILLs on expiry. A deadline landing mid-sequence does not cancel a decision, it strands one.Why raising
resolveTimeoutwould have been the wrong fix: it widens the window rather than closing it. The window is only harmless once (a) the decision cannot lose a race to the network and (b) a strand is visible and finishable. So:stamp.Postrequires the action artifact's chain hash, which exists only once that artifact is durable — so no GitHub call can precede the authorization it decorates. Not a convention about call order a later edit could invert. Pinned by test.ghcall (two sequential calls could spend double), andghis resolved before invocation so an unresolvable binary reports as itself.gate nextnames the state:judged but not authorized, with the command that finishes it. Verified against the live ledger — it finds exactly ivy#22 across 4,900+ artifacts.Is ivy#22's one-shot judgment spent?
No. Reasoned from the code, not by running
gate judge.applyJudgmentchecksartifactForParent(arts, KindJudgment, escalationID)first and, when a judgment exists, takesresumeJudgment— which loads the persisted judgment, re-checks its grant lineage, refuses only a retry whose-decisioncontradicts the record, then re-reduces and acts.judgment_duplicatefires only from a fresh append racing an existing one, or fromfinishJudgmentwhen an outcome already exists. Runrun_fe7ac73ddb7c59a7has a judgment and no reduced verdict and no action, so it takes the resume path and completes. Not a P1; the gap was that nothing said so, which commitd56fdb5fixes.A judgment recorded before the decider binding stays completable for the same reason: resume never rebuilds the judgment, so the new write-path requirement cannot strand a decision already in the log. Pinned by a test built on ivy#22's exact shape.
Tests
verify: decider vocabulary (empty who, unknown method, bareauto-prefix, non-RFC3339 clock all rejected);RequireDeciderbounds judgments only;Reducestays tolerant of unattributed legacy judgments.gate: a judgment artifact carries its decider on the wire; an unattributed one is refused before the one-shot is spent and the escalation stays judgeable; the CLI flag matrix.ledger: head-vs-merge classification (merged / superseded / abandoned / failed / not-landed); the platform clock stays distinct from gate's; the three reconcile classes; the effective-from boundary at ±1s and exactly on it; findings never include pre-adoption.gate: a receipt discharges exactly one action (second refused, no state mutation); audit findings appear and clear.observe: stranded-run detection, plus the negatives — any completed outcome, and a re-park, are not stranded.escalate: the binary-resolution self-check fires and namesgh,gate/authorized, and PATH; an absolute-gateis taken literally.gofmt,go vet,golangci-lint(0 issues),go test, andgo test -raceall clean.Notes for review
cmd/flare(including the launchd plist templates incmd/flare/scripts/) and the escalate card rendering. This PR therefore does not fix the plistPATHtemplate — the live plist is hand-patched andflare-launchd.sh installwill still regress it.cmd/escalate/internal/serve/serve.gois touched here for the budget seam only; expect a rebase.receipt/coveragebodies stay gate-internal: gate is the only writer and reader today. Promotion tocontracts/with an embedded schema is the repo's lazy-migration trigger — the first time a second tool touches one.ghcall sites still resolve via PATH rather than an absolute path pinned at startup. That is the repo's own test seam (docs/features/tier-aware-panel/driver.md: a fakeghon PATH observes the argv with zero production change). The failure is instead made loud — resolved-and-named at the call site, and refused at ingress startup.🤖 Generated with Claude Code