Skip to content

feat(pm): fleet-write relay carries the base sync as pr_update_branch, confirmed by measure - #22457

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22052-relay-pr-update-branch-op
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22052-relay-pr-update-branch-op

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22052
Clause-②: no

What this lands

The sanctioned base sync — merging origin/main into a PR head — had no relay op, so a seat could only issue PUT /repos/{owner}/{repo}/pulls/{n}/update-branch as bare REST under its own session token, and the platform authored the merge commit under the seat's personal account (the card measured PR 22002's head fab444b4). The relay's closed op table now carries ONE new row, pr_update_branch, in the PR-ops cluster beside pr_request_reviewers / pr_ready: the same PUT, issued on the runner with the App installation token, so the merge commit leaves as objectstack-fleet[bot].

  • pull is required; expected_head_sha is required and must be a full 40-hex sha (FIELDS kind sha, SHA_SHAPE in ops.mjs): a short, uppercase, non-hex or 41-character spelling is refused by validate.mjs BEFORE the dispatch, naming the shape — never left to the platform's 422. A missing sha is refused as a missing required key, never defaulted to the current head.
  • The row spends pull-requests: GitHub's server-to-server permission ledger (github/docs, src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json, read 2026-10-09) lists PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch under pull_requests with access: write, server-to-server: true, additional-permissions: false. The mint step in fleet-write.yml already grants it, so the workflow is untouched.
  • The executor (execute.mjs, confirmUpdateBranch) confirms the write by MEASURE, never by the answer's sentence:
    • 202 "Updating pull request branch." is an acceptance, not a landing: the pull is polled (GET /pulls/{n}) every 3 s for up to 60 s until its head is no longer expected_head_sha; then GET /repos/{repo}/commits/{sha} is read and the row prints head OLD → NEW · merge commit author LOGIN (NAME) — the fleet-identity reading this card exists for, in the log and the step summary of every real sync. A window that expires is a FAILED action whose sentence says UNCONFIRMED (the merge may still land; read the pull; ⛔ never re-sent), and the stroke stops there, so a pr_ready queued behind the sync never runs on an unconfirmed head.
    • 422 is two different things, so the executor re-reads the pull: a head that is no longer the expected sha is FAILED naming both shas (the platform's "expected head sha didn't match"); otherwise GET /repos/{repo}/compare/{base}...{head} is read and behind_by 0 is a NO-OP landing (the row says "nothing to merge" and quotes the platform's sentence), while a head still behind its base, or a measure that cannot be read, is FAILED with the platform's sentence — never a no-op by default.
    • Queue-routing reading ③ is unchanged: an enqueued pull's "cannot update" answer now arrives as that FAILED row carrying the platform's sentence — still a read, now through the op's reported status.
    • The PUT is paced by write-pace.mjs like every other write verb (fleet-write PUT in the pace log; pinned in the wiring battery).
  • dispatch.mjs reads nothing back for the op (it carries no body); a success run IS the confirmation, and the seat reads the new head with one GET /repos/{repo}/pulls/{n} after. Pinned in the read-back-targets battery; no new sender, so no new relayMissProbe.
  • Reference files, every edit in place at zero net lines and under the 120-byte line cap: rest-channel.md lines 49–50 moved to the relay spelling with bare REST for this verb marked ⛔ (82/82 before and after); line 74 names the op (reading ③ kept); landing-operations.md §C names the op (101/101); platform-readings.md line 89 names the op (469/469) — that file was not on the claim's surface, but the card's pin (grep -rn 'update-branch' .claude scripts/pm returns the op, its tests and the ⛔ line only) could not hold with its old spelling, so it was moved too.

Pin, measured on the final tree: grep -rn 'update-branch' .claude scripts/pm returns rest-channel.md:50 (the ⛔ line), ops.mjs (the row and its header), validate.mjs and execute.mjs (their tests and the executor's header) — nothing else.

The PM's mechanism assumptions, measured

  1. Which ref the relay executes — CONFIRMED from fleet-write.yml: actions/checkout@v7 with no ref, on a repository_dispatch trigger, checks out the event's github.sha, the default branch's tip. So the relay runs main's validate.mjs / execute.mjs: a pr_update_branch stroke sent before this lands would be refused by main's validator ("not an op the relay executes") with zero writes. The merge commit's author on a real PR is therefore NOT MEASURED until merge — the seat measures on the first real sync after landing; that sync's run prints the reading in its row (head OLD → NEW · merge commit author LOGIN (NAME)) and its step summary, so the seat quotes it from there. Not measured here by bare REST (that is the defect) and the workflow was not changed to take code from the PR ref.
  2. Token permission — CONFIRMED sufficient (ledger quoted above): pull_requests: write, no additional permission; the mint step is untouched.
  3. Op table position — the row sits between pr_request_reviewers and pr_ready, inside the PR cluster; the table's tail (transfer) is untouched, so fleet-write: no relay op starts a workflow_dispatch run, so a seat cannot begin an unblock that only a workflow run produces (Shard Timings Refresh for p1 #22014) and it waits on a maintainer click or the weekly schedule #22369's workflow_dispatch row appends behind it without overlap. The executor's confirmation is its own function, not a branch of the GraphQL path.
  4. Senders — none added: the op travels through dispatch.mjs --actions-file; grep -l sendFleetWrite scripts/pm is unchanged and the no-run conformance battery still passes (27 cases).
  5. Line ratchet — 82/82 and 101/101 held with in-place rewrites, no re-wrap, no ceiling change; pnpm check:pm-skill-ratchet green; operative text carries no issue number (pnpm check:pm-skill-id-lint green, 34 files).

Verification

Self-tests, before → after on this tree (node scripts/pm/fleet-write/FILE.mjs --self-test, exit codes captured before any pipe):

  • validate.mjs: 94 → 99 cases across 9 batteries — new rows: the accepted shape; a short sha refused before the dispatch; uppercase / non-hex / 41-char refused; a missing expected_head_sha refused; the table's one PUT pinned by op and path, never reaching /labels or /assignees; the row's permission pinned to pull-requests with the sha in its body.
  • execute.mjs: 83 across 12 → 93 across 13 batteries — the new "base sync" battery (10): 202 polled off the expected sha with the author read (3 polls, 6 000 ms of fake clock, the PUT the one write); 422 + behind_by 0 as a no-op landing (exit 0, the platform's sentence quoted); 422 + head behind as FAILED naming the count and the platform's sentence (the enqueued "cannot update" reading); 422 + a moved head as FAILED naming both shas with no compare taken; an unreadable pull or compare as FAILED; a window that expires (60 000 ms of fake clock) as FAILED, UNCONFIRMED in its sentence, with the pr_ready behind it never attempted; any other status as FAILED; the wiring battery now pins fleet-write PUT in the pace log.
  • dispatch.mjs: 200 → 201 cases across 18 batteries — pr_update_branch is not in BODY_OPS, has no locator and yields no read-back target.
  • scripts/pm/write-pace.mjs --self-test: 113 cases across 12 batteries (execute.mjs is on its roster).
  • Seat CLI dry-runs through the real packer (dispatch.mjs --dry-run, route resolved to dispatch): the full-sha stroke packs and prints (exit 0, nothing sent); the 8-character sha and the missing sha are refused at exit 2 naming the reason, nothing sent.
  • Targeted eslint over the four relay files: exit 0 (a targeted run, not the repo-wide pnpm lint, which is CI's).

Gate sweep: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 47 commands from the 7-path change set, identical before and after the pre-PR merge (derived at 2b3e864b, re-derived at c3f353d9 against merge base 2b61f2d9). All 47 ran serially and detached on c3f353d9 (1 593 s in all, check:pm-dispatch-gates 1 174 s of it), each exit code captured before any pipe: 46 exit 0 on the first pass; pnpm --filter @objectstack/lint run check:doc-formula-expressions exit 3 = PREREQUISITE NOT MET (@objectstack/formula and @objectstack/lint not built; nothing measured), its own fix line run under scripts/pm/os-verify-lock.sh (turbo run build --filter=@objectstack/formula --filter=@objectstack/lint, VERDICT command-exit 0), then the gate re-run: exit 0. Reconciliation with the exit codes recorded (--ran): ✓ dispatch-gates --ran: 47 derived famil(ies) accounted for — 47 run, 0 NOT-MEASURED (a DERIVED zero — all 47 recorded an exit code and none of them is 3). Among them, every gate the dispatch named: check:pm-skill-ratchet, check:pm-skill-id-lint, check:pm-governed-merges, check:nul-bytes, check:pm-fleet-write-validate / -execute / -dispatch, check:pm-write-pace — all exit 0.

Acceptance notes

  • A 202 whose head never leaves the expected sha inside the window is a FAILED action on the runner (exit 5, the run concludes failure), so the seat's dispatch.mjs answers 5 "the run FAILED", not 6: a seat cannot read the executor's exit, only the run's conclusion, and the executor has no UNCONFIRMED exit of its own. The row's sentence says UNCONFIRMED and prescribes a read; the prescription for a failed run (never fall back, never re-send) is the same. The PM's suggested route (an UNCONFIRMED-style exit aligned with exitForResult) is met in the sentence, not in the exit number.
  • The 422 sentences in the self-test fixtures ("There are no new commits on the base branch.", "expected head sha didn't match current head ref.", an enqueued "cannot be updated") are FIXTURES, not measured platform text: no file in this repository records them and the docs do not document them, which is exactly why the executor judges a 422 by the compare's behind_by and the re-read head, never by the sentence.
  • The merge commit's author on a real PR: NOT MEASURED until merge (assumption 1 above).
  • fleet-write.yml's mint-step comment ("the op table has NO contents op — no file, ref or branch write") stays true for the contents grant it explains; ops.mjs's header now says where the one branch-moving row's merge comes from (the platform's own update-branch action, under pull-requests). The workflow file is untouched on purpose.
  • Git identity: the container's git author resolved to objectstack-fleet[bot] on every commit; the commit trailer pair is the model-free pair AGENTS.md prescribes.
  • After the pre-PR merge of origin/main (2 commits, none touching these files; pnpm-lock.yaml moved), pnpm install --frozen-lockfile was re-run; no package build is owed — the diff touches no published package and every gate here is a script-level self-test or repo scan.
  • Out of scope, noted and not filed: rest-channel.md line 57 still spells a bare direct merge (PUT .../pulls/{n}/merge) as a ✓ for a non-queued repository — the same identity question this card asked about update-branch, for a verb the relay refuses by construction (/merge$ is a refused family). carrier: the skills seat (this card's lane); noted, not filed.

维护者速读(草稿)

改了什么:舰队写中继多了一个 op pr_update_branch:把 main 合进某个 PR 的 head,这件事以前只能由席位用自己的账号裸调 GitHub 接口,合并提交就记在席位的个人账号名下;现在走中继,由 objectstack-fleet[bot] 发出。同时三份席位参考文件改成新拼写,裸调标为 ⛔。

为什么改:仓规定每一笔 GitHub 写都以舰队身份经 scripts/pm/ 发出;这条动词是唯一还在用个人账号写的口子(实测 PR 22002 的 head fab444b4 作者是席位的个人账号)。

风险与代价(含回滚):新增一行 op 表、一个确认函数与自测,不改工作流、不扩权限(GitHub 权限表列此接口为 pull_requests: write,已铸)。合并前中继跑的是 main 上的代码,所以"真实 PR 上合并提交作者是舰队"这条要等合并后首次真实同步才能实测;执行器会把新 head 与合并提交作者打印在 run 的汇总里。回滚 = revert 本 PR,一次提交。

席位意见:(留空)

你要做的:本 PR 受管面全在 .claude/**,Tier S:席内达档契约复核 PASS 后由席位入队,不需要你动手;合并后首次真实同步的作者读数由席位贴回卡上。


Generated by Claude Code

objectstack-fleet Bot and others added 2 commits October 9, 2026 09:42
…, confirmed by measure

The sanctioned merge of main into a PR head had no relay op, so the seat's
only route was bare REST under its session token and the merge commit was
authored by the seat's personal account. One row in the PR-ops cluster of the
closed op table now issues PUT /repos/{repo}/pulls/{n}/update-branch as the
fleet App (pull_requests: write in GitHub's ledger, so the mint step widens
nothing). expected_head_sha is required and must be a full 40-hex sha, refused
by the validator before the dispatch. The executor confirms a 202 by polling
the pull's head off the expected sha for up to 60 s and prints old -> new head
with the merge commit's author; a 422 is a no-op only when the compare
measures the head not behind its base, otherwise a FAILED action with the
platform's sentence, so the enqueued pull's "cannot update" reading survives
as a row. rest-channel.md, landing-operations.md and platform-readings.md
spell the op; bare REST for this verb is now marked refused.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JmWtcHfGbC4ncw4GFKWuRA
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Oct 9, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 9, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c3f353d9289a7cb8e4ad608d2a6d3152caa1c9b3
Local-runs: none

① Derived judgments

  • Accept set of the relay (the seats' internal write toolchain, not a published package): the closed op table gains exactly one row, pr_update_branch (pull required; expected_head_sha required, a new FIELDS kind refused by validate.mjs unless it is 40 lowercase hex), placed in the PR-ops cluster between pr_request_reviewers and pr_ready (ops.mjs:413). Right: a closed addition, refused before dispatch on every malformed shape; the table's one PUT is this verb and the self-test pins it (validate.mjs:562).
  • The write's confirmation is by measure, not by the platform's sentence (execute.mjs confirmUpdateBranch, outside the GraphQL landing branch): a 202 is polled off the expected head (3 s / 60 s window) and the merge commit's author is read and printed; a 422 is a no-op only when the re-read head still equals the expected sha and the compare's behind_by is 0, otherwise FAILED; an expired window is FAILED with UNCONFIRMED in its sentence and nothing after it attempted. Right — this is the card's "a 422 base unchanged is a no-op, not a failure" with the stricter reading that a 422 is proven, never assumed.
  • Public surface: none. No package publishes any of the seven files; Clause-②: no is correct.
  • Governed text (Tier S, .claude/**): rest-channel.md lines 49–50 and 74 rewritten in place to the relay spelling with the bare PUT …/update-branch marked ⛔ (line 50); landing-operations.md §C names the op (line 90); platform-readings.md line 89 follows (outside the claim's surface — accepted: the card's pin grep -rn 'update-branch' .claude scripts/pm could not hold otherwise, 0 net lines). All three files keep their line counts (82 / 101 / 469), no re-wrap, no ceiling change. The pin holds on the head: git grep update-branch over .claude and scripts/pm returns the op, its tests and the one ⛔ line.
  • fleet-write.yml untouched: GitHub's server-to-server ledger lists update-branch under pull_requests: write, already minted. Right by the dev's reading; the first real sync after landing measures it (the relay executes main's code — fleet-write.yml checks out the dispatch's github.sha — so the merge commit's author on a real PR is NOT MEASURED until merge; the seat takes that reading at its first base sync and posts it on the card).
  • Queue-routing reading ③ of rest-channel.md:74 (update-branch on an enqueued PR answers "cannot update") survives as the FAILED row carrying the platform's sentence. Right.

② Semver level

Clause-②: no on the claim and the PR body; skip-changeset is the correct declaration — scripts/pm/** and .claude/** publish nothing (the fast lane). No .changeset/*.md touched.

③ Boundary flags

  • Executor exit 5 (FAILED) rather than the PM's suggested UNCONFIRMED exit for an expired window: accepted — a seat reads only the run's conclusion, the row's sentence says UNCONFIRMED and prescribes a read, and the prescription (never fall back, never re-send) is the same.
  • 422 sentences in the fixtures are fixtures, not measured platform text: accepted — that is why the verdict is measured (re-read head + compare), never spelled.
  • Git identity: every commit on the branch is authored and committed by objectstack-fleet[bot] (the container's git config, read on the branch) with the model-free trailer pair — no deviation.
  • Targeted eslint over the four relay files instead of repo-wide pnpm lint: CI's Lint & Repo Gates is the measurement; in_progress on the head at this write, read at landing.
  • open_questions: none. Out-of-scope finding: rest-channel.md:57 still spells a bare direct merge (PUT …/pulls/{n}/merge) for a non-queued repository — a verb the relay refuses by construction; class b candidate with no measured write, carrier the skills seat — Acceptance notes and the seat post, not a card.

Implemented-by: claude/issue-22052-relay-pr-update-branch-op
Reviewed-by: session_01JmWtcHfGbC4ncw4GFKWuRA

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 10:44
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 587bd96 Oct 9, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22052-relay-pr-update-branch-op branch October 9, 2026 11:04
This was referenced Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant