Skip to content

Merge knowledge PRs through the publisher's local gate (design v8) - #227

Merged
tzhouam merged 1 commit into
mainfrom
kb/local-gate-merge
Sep 29, 2026
Merged

tzhouam merged 1 commit into
mainfrom
kb/local-gate-merge

Conversation

@tzhouam

@tzhouam tzhouam commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

PR 1 of the v8 knowledge-service change (design approved by sol 2026-09-29; canvas updated): the publisher is the only thing that merges knowledge PRs, and it runs the gate locally on the exact tree that would land. There is no GitHub merge queue, ruleset or kb-gate status wait.

  • Service: at pr_open it signs a verdict for the exact head and issues a merge item carrying it, at most one per repository. Acks lead to:
    • merged: with its post-merge check;
    • rebuild_needed: context moved, pages changed, conflicts or a manifest difference;
    • re-sign: a transient refusal;
    • gate_failed: a problem with the change itself goes to people and is never retried as is.
      An unknown post-merge check is re-run by the service itself (verify_merged).
  • local_gate: verdict signature, binding and auto only; single-repository scope; verify_change on merge-tree(main, head); merged-tree L1; the same checks on the actual merge commit.
  • Publisher merge:
    • cross-process flock single writer;
    • the local gate is re-run if main moves;
    • the control record is re-read before merging;
    • merge intent, then gh pr merge --merge --match-head-commit;
    • a success receipt only when GitHub shows MERGED; a PR that was only queued is dequeued and auto-merge cancelled (confirmed via GraphQL);
    • unclear outcomes keep the intent for recovery, and recovery never merges twice.
  • Tests: v7 queue/status tests are rewritten for the merge path, plus new end-to-end tests with real git.

The commit hook approved it; full suite and SPEC freshness pass locally.

🤖 Generated with Claude Code

The owner decided against GitHub's merge queue and rulesets. The publisher
is now the only thing that merges knowledge PRs, and it runs the gate itself
on the exact tree that would land.

- service: at pr_open it signs a verdict bound to the PR's exact head and
  issues a merge item carrying it, at most one per repository. There is no
  verdict comment, no wait for a GitHub kb-gate status and no enqueue. Acks:
  - merged: the merge SHA and the post-merge check result are recorded, and
    a failed post-merge check pauses the repository and goes to people.
  - refused because context moved, pages changed, the PR does not merge
    cleanly or the manifest differs: rebuild_needed, rebuilt on current main
    by the scheduler.
  - refused for a transient reason (main kept moving, control changed,
    git/gh trouble): back to pr_open, signed again.
  - refused because of the change itself: gate_failed, people decide. It is
    never retried as is.
  An unanswered merge item returns to pr_open when it expires. v7
  verdict_posted/queued records are signed again as pr_open.
  A receipt still counts when GitHub showed the merge first. A post-merge
  check of "unknown" (the publisher could not run it, or the merge was
  recovered after a crash) is run by the service on its own clone
  (verify_merged) and retried until it has an answer.
- local_gate: on the publisher's installed Copilot version it checks the
  verdict (signature, binding, auto only), that every path is a governed page
  of the item's own repository, runs verify_change on merge-tree(main, head)
  with the verdict handed over and no hold list, and checks the merged tree
  for new tree-level issues. post_merge_problems repeats this on the merge
  commit GitHub actually made.
- publisher merge action, under a cross-process flock for the whole round (a
  second instance does nothing):
  - verify the verdict; the PR must be open, target main and still have the
    signed head;
  - run the local gate, again if main moves (at most 3 times);
  - re-read the control record (a pause voids the item);
  - write a merge intent, then gh pr merge --merge --match-head-commit;
  - run the post-merge check.
  Nothing after the merge becomes a failure ack. Each round first recovers
  intents that have no completion record, so a lost receipt is re-sent and
  the PR is never merged twice. When gh pr merge itself errors, the outcome is
  unknown: the intent is kept and nothing is recorded or acked; recovery
  settles it from GitHub. A kb:hold label left by a v7 pause is removed (and
  confirmed gone) before the gate. Only a PR that GitHub shows as MERGED counts: if
  main has a merge queue or auto-merge (gh pr merge only enqueues),
  the PR is dequeued and auto-merge disabled, and the item is refused only
  once GitHub (GraphQL: mergeQueueEntry, autoMergeRequest) confirms neither
  is left; any failure while cancelling counts as unresolved; until then the intent stays and recovery keeps
  cancelling (or picks up a merge that happened meanwhile). An item with an unsettled
  intent is skipped until recovery hears from GitHub.
- gate_verifier.verify_change takes verdict= and check_holds= (the bundle is
  regenerated).
- external: its status set is derived from merge.IN_FLIGHT, so a PR whose
  merge is in flight is not judged again.

Tests for the v7 queue, status and enqueue mechanics are rewritten for the
merge path, and the human-approved withdrawal/queue-slot tests are removed:
a human-approved verdict never merges in v8, and that code goes in the
removal PR. New end-to-end tests with real git: a merge only after the local
gate passes, a refused change is never merged, a lost receipt is recovered
without a second merge, a second publisher does nothing, and a pause during
the gate stops the merge. Also tested: a delayed receipt with a failed post-merge
check, a merge reported as failed that did merge, pause -> resume -> merge, a
merge that only queued (queue membership modelled apart from auto-merge; a
failing dequeue; a command error while confirming), and a recovery lookup that fails before it succeeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-29T05:03:03.200142Z 3929000 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@tzhouam
tzhouam merged commit 1424d4a into main Sep 29, 2026
5 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3929000985

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"pr": int(intent["pr"]), "head_sha": intent["head_sha"], "branch": "", "error": "",
"merge_sha": "", "problems": [], "ok": True, "recovered": True,
"post_check": "unknown"}
if pr.get("state") == "MERGED" and merged and pr.get("headRefOid") == intent["head_sha"]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep a MERGED recovery pending until its commit SHA appears

When recovery observes state == "MERGED" but GitHub has not populated mergeCommit yet, this condition falls through to the failure branch and sends an ok=False receipt saying the already-merged PR was not merged by the publisher. The normal merge path explicitly anticipates this temporary missing-SHA state, but recovery instead moves the matching change set to gate_failed; because acknowledgements are collected before GitHub is polled, the service then skips retirement bookkeeping and post-merge verification for a change that actually landed. Treat this as a successful merge with an unknown post-check, or leave the intent pending until the SHA is available.

Useful? React with 👍 / 👎.

Comment on lines +569 to +572
if pending.get("kind") == "merge" and float(pending.get("expires_at") or 0) + CLOCK_SKEW < rt.clock():
# expired unanswered: the publisher never ran it (or its record
# is lost); sign again. A merge it did run still shows as merged.
rt.ledger.update_changeset(changeset["id"], status="pr_open", pending_item=None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not supersede a merge that may already be executing

A publisher can validate an item just before expiry and still be running its fetch/gate/merge after this timeout fires, since the service and publisher share no execution lock. Clearing the pending ID here allows a second merge item to be issued while the first can still merge; the first receipt is then ignored because its item ID no longer matches, and if the second publisher run sees the PR already merged its matching failure receipt can leave the ledger in gate_failed even though the PR landed. Expiry needs an explicit settlement/cancellation handshake rather than assuming the publisher never started the item.

Useful? React with 👍 / 👎.

Comment on lines +273 to +274
self._recover_merges()
return self._round(summary)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve ALLOW_POST and control checks during recovery

Recovery runs before _round checks either allow_post or the freshness and generation of the signed control record. For an OPEN intent that GitHub queued or configured for auto-merge, _recover_merges calls _cancel_pending_merge, which performs dequeue and disable-auto writes even when ALLOW_POST=0, the control record is stale, or the service has paused/changed modes. This makes a documented dry-run or stale-control round mutate GitHub; recovery should gate its write portion through the same controls as ordinary actions.

Useful? React with 👍 / 👎.

Comment on lines +81 to +82
except GateError as exc:
return [str(exc)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Report infrastructure GateErrors as transient failures

If merge-base, diff, or another local Git command fails because of an I/O, clone, or process problem, this converts the GateError into a normal gate-problem list. The publisher consequently emits a GateRefused receipt with nonempty problems, while _apply_merge_ack only applies its transient "git " classification when problems is empty, so the change is permanently marked gate_failed instead of being retried. Reserve problem lists for defects in the proposed change (such as a clean-merge conflict) and propagate operational Git failures as LocalGateError/PublishError.

Useful? React with 👍 / 👎.

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