Repository navigation
Commit 587bd96
feat(pm): fleet-write relay carries the base sync as pr_update_branch, confirmed by measure (#22457)
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 #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](https://claude.ai/code/session_01JmWtcHfGbC4ncw4GFKWuRA)_
Co-authored-by: objectstack-fleet[bot] <332303061+objectstack-fleet[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>1 parent 33ae4bf commit 587bd96
7 files changed
Lines changed: 234 additions & 41 deletions
File tree
- .claude/skills/pm-dispatch/references
- scripts/pm/fleet-write
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
90 | | - | |
| 90 | + | |
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
89 | | - | |
| 89 | + | |
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
49 | | - | |
50 | | - | |
| 49 | + | |
| 50 | + | |
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| |||
71 | 71 | | |
72 | 72 | | |
73 | 73 | | |
74 | | - | |
| 74 | + | |
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
202 | 202 | | |
203 | 203 | | |
204 | 204 | | |
205 | | - | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
206 | 211 | | |
207 | 212 | | |
208 | 213 | | |
| |||
1132 | 1137 | | |
1133 | 1138 | | |
1134 | 1139 | | |
1135 | | - | |
| 1140 | + | |
1136 | 1141 | | |
1137 | 1142 | | |
1138 | 1143 | | |
| |||
1526 | 1531 | | |
1527 | 1532 | | |
1528 | 1533 | | |
| 1534 | + | |
1529 | 1535 | | |
1530 | 1536 | | |
1531 | 1537 | | |
| |||
0 commit comments