Skip to content

gate: next reconciles by default, -cached opts out - #254

Closed
itsHabib wants to merge 1 commit into
mainfrom
claude/gate-next-live-default
Closed

gate: next reconciles by default, -cached opts out#254
itsHabib wants to merge 1 commit into
mainfrom
claude/gate-next-live-default

Conversation

@itsHabib

Copy link
Copy Markdown
Owner

Why

gate next is the operator's inbox. It was answering a strictly older question than the one it claims to answer.

The log records what gate decided. Nothing ever records that the merge later happened — gate emits an action artifact carrying the pinned merge command and never learns the outcome. So an un-reconciled projection can only accumulate, and every row it accumulates is a job already finished.

Measured on the operator's state, 2026-08-22:

rows
gate next (before) 149 — awaiting judgment 12 + ready to merge 137
of those, genuinely open 0

All 138 ready-to-merge rows were checked against gh pr list: zero still open. All 12 awaiting-judgment rows were spot-checked individually: every one merged or closed, the oldest on 2026-08-02 — nineteen days stale. A view whose whole contract is "what needs you" cannot default to that.

What

The reconcile already existed and was already correct. It just sat behind -live. This promotes it and gives the log-only projection its own flag.

command behaviour
gate next reconciled (was -live)
gate next -cached the log alone (was the default)
gate next -live accepted and ignored — pasted commands and older docs keep working

The default is safe offline. The reconcile fails safe: a repo whose fetch errors keeps its rows and marks them PRState: "unknown" with the reason (observe.reconcileLive), so a rate-limited or disconnected run degrades to the old output plus an honest marker — never to a hidden gap. That property is what makes this promotable to a default rather than a footgun.

cmdNext splits into runNext(args, fetch) so the flag routing is testable without a network and without a mutable package-level seam a test could leave swapped — the shape lookupOpenPRsContext already uses.

Verification

On the operator's real state, the same command before and after:

before (-cached):  awaiting judgment (18)   ready to merge (144)
after  (default):  awaiting judgment (7)

162 rows → 7, and the 7 are genuinely open.

Tests: TestNextReconcilesByDefault (a merged subject is dropped, and the seam is actually reached), TestNextCachedSkipsTheReconcile (-cached never touches the seam), TestNextLiveFlagStillAccepted (compat). The two existing verb tests now inject a seam reporting their fixture PR open, so they keep asserting the projection rather than accidentally asserting the reconcile.

gofmt, go vet, golangci-lint (0 issues), go test ./cmd/gate/... all green.

Note for review

This is the read half. -live reconciles in memory, re-paid on every call — the ledger still carries those 149 obligations as open forever. The durable fix is a closing artifact appended when ground truth says merged/closed, which is a new artifact kind in a hash-chained log and wants its own design pass. It is also the invariant the org TDD (#245) rests on: "holding the tip of that chain is being the role" only holds if a tip cannot claim work that finished three weeks ago. Filed here as context, not proposed in this PR.

🤖 Generated with Claude Code

`gate next` is the operator's inbox, and it was answering a strictly older
question than the one it claims to answer. The log records what gate DECIDED;
nothing ever records that the merge later happened, so an un-reconciled
projection can only accumulate. Measured on the operator's state 2026-08-22:
149 rows, and all 149 named work already merged or closed — some for nineteen
days. A view whose whole contract is "what needs you" cannot default to that.

The reconcile already existed and was already correct; it just sat behind
-live. This makes it the default and gives the log-only projection its own
flag:

  gate next            reconciled (was: -live)
  gate next -cached    the log alone (was: the default)
  gate next -live      accepted and ignored, so pasted commands keep working

The default is safe offline because the reconcile fails safe: a repo whose
fetch errors keeps its rows and marks them PRState "unknown" with the reason
(observe.reconcileLive), so a rate-limited or disconnected run degrades to the
old output plus an honest marker rather than to a hidden gap.

cmdNext splits into runNext(args, fetch) so the flag routing is testable
without a network and without a mutable package-level seam a test could leave
swapped — the shape lookupOpenPRsContext already uses. The two existing verb
tests inject a seam reporting their fixture PR open, so they keep asserting the
projection rather than accidentally asserting the reconcile.

On the operator's real state this takes the inbox from 162 rows to 7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@itsHabib

Copy link
Copy Markdown
Owner Author

@claude review this PR

@itsHabib

Copy link
Copy Markdown
Owner Author

@codex review this PR

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@claude

claude Bot commented Aug 23, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 1m 57s —— View job


PR Review

  • Gather context and understand the request
  • Read changed files
  • Run git diff to see full changes
  • Analyze code for correctness, safety, and adherence to principles
  • Post review findings

Head commit reviewed: c857628 (cmd/gate/main.go, cmd/gate/next_test.go, cmd/gate/README.md, docs/workbench-101.md)

Verdict: Ready to merge. The change is correct, the fail-safe property is real, the test coverage pins all three cases, and the backward compatibility guarantee is properly tested. A few observations below, none blocking.


What the PR Does

Promotes gate next reconciliation from opt-in (-live) to opt-out (-cached). The reconcile already existed and was correct — this just changes which path is default. The key facts:

  1. The fail-safe property is correct: a fetch error keeps the row with PRState: "unknown" and the reason, so a disconnected run degrades to the old output plus an honest marker — it never silently drops rows.
  2. The cmdNext/runNext split is the right shape — matches lookupOpenPRsContext's seam pattern already in this file.
  3. The discarded fs.Bool("live", ...) return value is intentional: the flag parses, its value is never read, and the default path reconciles regardless. Clean no-op.

Code Review

Flag routing (main.go:1927–1936)

if *cached && *asJSON {
    return observe.NextJSON(...)
}
if *cached {
    return observe.NextText(...)
}
if *asJSON {
    return observe.NextJSONLive(...)
}
return observe.NextTextLive(...)

All four cached × asJSON combinations are handled, line-of-sight, no else. Correct.

Test: TestNextReconcilesByDefault — pins the most important invariant: the seam IS called by default, and a merged subject (absent from the open set) is dropped. Both assertions are present and meaningful. ✓

Test: TestNextCachedSkipsTheReconcile — the fetch closure calls t.Fatal, which exits the goroutine but still requires a return nil, nil statement in Go. This is idiomatic. ✓

Test: TestNextLiveFlagStillAccepted — pins the compat promise at the right scope: flag parses without error AND the seam is reached. The seam check is load-bearing: if the implementation were wrong and -cached were set when -live was passed, called would be false and the test would catch it. ✓

Existing tests updated correctlyTestNextCommandEndToEnd and TestNextCommandRendersResolveLine now inject openFixture(42) and openFixture(42, 43) respectively. The fixture PR numbers match the seeded state exactly, so neither test accidentally asserts on reconcile-dropped rows. ✓

seedParkedInbox — uses panel_incomplete (not a ceiling park), which is the right fixture for reconcile testing: a ceiling park is filtered differently, and these tests care about the subject-match path that reconcile exercises. The deliberate difference from TestNextCommandEndToEnd's grant_tier_exceeded fixture is correct. ✓


Observations (non-blocking)

1. seedParkedInbox re-implements setup that TestNextCommandEndToEnd also has inline. The two differ intentionally (different park codes), so extraction was right. But TestNextCommandEndToEnd now inlines 25 lines of setup that are structurally similar. If a fourth test needs a parked inbox, consider generalizing the seed helper to take the park code as a parameter. Not worth doing now — three tests sharing a helper is already a good call.

2. The long block comment in runNext (main.go:1890–1901) explains the WHY of the default, including the measured data from 2026-08-22. Per the project's stated principle ("a hidden constraint, a subtle invariant, behavior that would surprise a reader"), this is justified: a reader who sees cached := fs.Bool(...) and no live pointer might flip the default back thinking it was a mistake. The comment prevents that. Fine here.

3. No test covers gate next -cached -json. The combined cached && asJSON branch is exercised only implicitly by the existing TestNextCommandEndToEnd upgrade (which now goes through runNext([]string{"-json"}, openFixture(42))), but -cached -json specifically is untested. Given the symmetry of the four-branch routing, this is very low risk — but noting it in case a future refactor breaks that branch silently.

4. Note in PR description is accurate. The PR correctly scopes itself to the read path and defers the durable closing artifact (the write-side fix) explicitly. That deferral is sound — this change is safe to ship independently.

@github-actions

Copy link
Copy Markdown

Reviewer: claude
Reviewed commit: 545e90a6818c40257a7bdef0b58f1861295515c5

@itsHabib

itsHabib commented Sep 3, 2026

Copy link
Copy Markdown
Owner Author

Closing as superseded by #258, which landed first and made the opposite call deliberately: gate next stays offline unless asked because escalate serve calls gate next -json on a hard-budgeted Slack path, and gate sweep plus supersession/mootness closure is how the offline projection learns what a live read would have told it. This PR's flip (reconcile by default, -cached opts out) would put a gh subprocess per repo back on that path and calls NextJSONLive/NextTextLive, which no longer exist. If a live default is still wanted, it should be a fresh PR that also decides whether escalate passes -cached.

@itsHabib itsHabib closed this Sep 3, 2026
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.

1 participant