Repository navigation
Commit 73d7001
feat(pm/fleet-write): pr_create carries the PR assignee in the same relay stroke (#22686)
Fixes #22467
Clause-②: no
## What
The os-dev's separate PR-assignee write — `node
scripts/pm/label-write.mjs --repo … --issue PR --add skip-changeset
--assign LOGIN` — is refused by the auto-mode permission classifier
nondeterministically. Measured on the card's thread (comment
6096066282): two devs in one session, identical command text, identical
allowlist candidates (`Bash(node scripts/pm/label-write.mjs *)` on
`main`), one passed (relay run 38040085614) and one was refused before
any request left ("External System Writes", 0 requests); four of twelve
objectui PRs before that, same shape. In every measured case the same
dev's `pr_create` through `scripts/pm/fleet-write/dispatch.mjs
--actions-file` passed (pr_create three times that day, every
`post-stamped` report comment), so this takes the card's option B: the
relay's `pr_create` op carries `assignees` in the same stroke, and no
separate assign write exists for a classifier to refuse. Option A's
documentation half rides along (os-dev.md names the one remaining write
shape); C is not taken — it only moves the exposure to the seat.
## How — the four relay files
- `scripts/pm/fleet-write/ops.mjs` — `pr_create` gains optional
`assignees`, the typed list the `assign` row already takes (1–10 logins,
39 bytes each, none twice). With it the row is TWO requests in one
action: the draft create, then `POST
/repos/{o}/{r}/issues/{number}/assignees`. The leg's path carries the
placeholder `answerPlaceholder('number')` and the descriptor carries
`fromAnswer: 'number'` — the executor fills the placeholder from the
create's ANSWER, never from the request — and `assigned`, the logins the
answer must list. The row declares `alsoSpends: ['issues']`: `issues:
write` is a literal mint input on every stroke's token, so
`.github/workflows/fleet-write.yml` is untouched (verified:
`permission-issues: write` and `permission-pull-requests: write` are
literal inputs on every stroke; only `actions` is stroke-scoped).
- `scripts/pm/fleet-write/execute.mjs` — measured first, as the dispatch
asked: the executor runs an op's `requests` in order, but every path was
computed up-front from the action alone; no existing op threaded an
earlier ANSWER into a later request (the GraphQL ops read a node id
BEFORE their mutation; `confirmUpdateBranch` reads AFTER its PUT). The
smallest extension: the loop keeps the previous answer per action and
`fillFromAnswer` resolves a `fromAnswer` descriptor from it. An answer
without a positive-integer `number` is a FAILED action whose row says
the leg was NOT SENT (status `not sent`), the stroke stopped, exit 5.
`requestLanded` fails a 2xx whose `assignees` lacks a login sent —
GitHub documents an assignee the caller cannot apply as "silently
ignored" with the 201 still answered — naming the login; `resultOf`
prints the assignees as the platform lists them (`#N url · assignees
LOGIN`).
- `scripts/pm/fleet-write/validate.mjs` — no refusal code was needed
(the `assignees` field was already typed); the new pins: a non-array, a
non-string login, an empty list, a duplicate, eleven logins and a
40-character login are each refused by name; exactly ONE descriptor in
the whole table carries `fromAnswer`, it is never the first request of
its action, no path without `fromAnswer` carries a brace, every
`alsoSpends` grant is a literal (non-stroke-scoped) key of
`PERMISSIONS`, and `pr_create` without `assignees` is still one request.
- `scripts/pm/fleet-write/dispatch.mjs` — the pr_create read-back row
(found by `owner:head`, as before) also carries the pull's `assignees`
as the platform lists them and `assigneesMissing` (logins sent and
absent); `--json` carries `assignees` and `assignees_missing`. A READING
for the dev's report, never a verdict: the leg's landing is judged
inside the run, so a login missing here is another actor's later write,
said on the row's line.
## `.claude/agents/os-dev.md` — three lines in place, zero net lines
Line budget before/after: 402/402 against the 402 ceiling (`pnpm
check:pm-skill-ratchet`: headroom 0). The 120-byte line cap held: `:37`
116→118 bytes, `:58` 120→120, `:353` 113→120.
- `:37` — "PR assignee = 卡的:`pr_create` 带 `assignees: [LOGIN]`" (was
`--issue PR_NUMBER --assign LOGIN`).
- `:58` — the four strokes of the write budget: "`pr_create`(draft,含
assignee)、`label-write`(仅标签)" (was "一次
`pr_create`(draft)、`label-write`(含 PR assignee)"; the count is carried
by 四笔).
- seat rider: os-dev.md:353 — `成本` → `真实代价与工作量`, the split
`references/decision-analysis.md:25–26` made; to stay under the cap the
line's "把每个问题连同选项、成本与你的推荐写进" became "每个问题带选项、真实代价与工作量、推荐,写进".
## Evidence
Self-tests after the final commit `1e0dd1689` (exit 0 each, captured
before any pipe):
- `node scripts/pm/fleet-write/validate.mjs --self-test` → 128 cases
across 10 batteries (was 119).
- `node scripts/pm/fleet-write/execute.mjs --self-test` → 111 cases
across 15 batteries (was 100 across 14); the new battery "the assignees
leg" pins 11 cases, among them the fill from a fake first answer and the
second request's path.
- `node scripts/pm/fleet-write/dispatch.mjs --self-test` → 216 cases
across 19 batteries (was 212).
- `pnpm check:pm-skill-ratchet` → os-dev.md is 402 lines (ceiling 402;
headroom 0); `pnpm check:agent-model-declared`, `pnpm check:nul-bytes`
and `eslint --no-inline-config` on the four modules all exit 0.
Mutation proof — each leg through `scripts/ablation-replace.mjs` in WRAP
mode, the fix committed first, each restored to HEAD with blob hash
equal to HEAD's and `git diff HEAD` empty:
- A `prev = r;` → `prev = null;` in execute.mjs: 5 of 111 cases red (the
run, the row, the quiet partial, the not-sent case, the refused POST);
restored.
- B `if (missing.length) {` → `if (false && missing.length) {`: exactly
1 of 111 red — the quiet-partial pin — restored.
- C `fromAnswer: 'number', ` → `fromAnswerDropped: 'number', ` in
ops.mjs: validate 2 of 128 red (the one-descriptor pin and the brace
pin), execute 7 of 111 red; restored. A first attempt at C was a null
operation — the tool refused a replacement that was a substring of the
anchor (replacement count rose 0, not 1), so that self-test never ran
under mutation — and was redone with a distinct replacement; the first
reading is void.
Gates (derived with `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`, no paths, over the real change set;
reconciled with `--ran`):
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` over the change set at `1e0dd1689` (5 files
vs merge base `243dd3c62`): 47 commands. Run in full, each exit code
recorded before any pipe: 47 run, all exit 0. One of them, `pnpm
--filter @objectstack/lint run check:doc-formula-expressions`, first
exited 3 (PREREQUISITE NOT MET: `@objectstack/lint` not built — nothing
measured); after `pnpm exec turbo run build
--filter=@objectstack/formula --filter=@objectstack/lint` under
`scripts/pm/os-verify-lock.sh` (VERDICT command-exit 0) it re-ran with
exit 0 and the record was corrected.
- `node scripts/pm/dispatch-gates.mjs --ran ran.list --repo
objectstack-ai/objectstack`: "47 derived famil(ies) accounted for — 47
run, 0 NOT-MEASURED, 0 UNRUN" — the reconciliation is the tool’s own set
difference, not a count of mine.
- Among them the families the dispatch named: `check-ci-filter-parity`,
`check-closing-keyword-parity` (+ self-test),
`check-comment-mask-corpus`, `check-declaration-mirrors` (+ self-test),
`check-scripts-symbol-anchors` (+ self-test), `check-self-test-wired` (+
self-test), `check-self-test-workflow-commands` (+ self-test),
`check-whole-set-label-write` (+ self-test), the three `scripts/pm/*.mjs
--self-test`, `check:agent-model-declared`, `check:pm-skill-ratchet`,
`check:pm-skill-id-lint`, `check:pm-governed-merges`,
`check:pm-governed-prose` is NOT in this derivation (the tree at
`1e0dd1689` does not derive it for these paths; it is CI`s),
`check:pm-settings-deny-roster` likewise not derived, `check:nul-bytes`,
`check:doc-authoring`, `check:refd-timer-probe`,
`check:watch-hint-literal`, and the three fleet-write self-tests as
`check:pm-fleet-write-{validate,execute,dispatch}`.
- The repo-wide `pnpm lint` is CI’s run and was not run here; a targeted
`eslint --no-inline-config` over the four edited modules exits 0 (not
claimed as the repo-wide measurement).
## NOT MEASURED
- The relay runs on `main`, so this PR cannot exercise `pr_create` with
`assignees` live: the self-tests are the proof. The first live
`pr_create` carrying `assignees` after merge is the measurement — the
seat's first such stroke, its run's row (`… · assignees LOGIN`) and the
dispatch `--json` `read_back[].assignees`.
- This PR's own assignee still goes through the old shape (`label-write
… --add skip-changeset --assign marchtian`), because the relay on `main`
would refuse `assignees` as a key `pr_create` does not take; that
stroke's outcome is one more data point for the card and is recorded in
the dev report on the card.
## Acceptance notes
- Outside the declared file surface, not edited:
`.claude/skills/pm-dispatch/references/execution-duties.md:59` — "dev
`pr_create` 同轮 `label-write.mjs --issue PR_NUMBER --assign LOGIN`;席位自有
PR 指派自己。" — names the old shape and goes stale the day this lands;
carrier: the skills seat, a one-line in-place rewrite (for instance "dev
`pr_create` 带 `assignees: [LOGIN]`;席位自有 PR 指派自己。").
- The `assign` op (what `label-write` uses on cards) is unchanged: no
landing judgment was added there, because `label-write`'s own step ④
read-back already measures the assignee set against its target. Only the
new leg judges its answer.
- The readings this PR relies on: the `dispatch.mjs --actions-file`
stroke has passed in every measured case (the card's 6096066282:
pr_create three times on 2026-10-10 and every report comment), and the
refusal measured was on `label-write … --assign` only.
## 维护者速读(草稿)
**改了什么**:开发 agent 开 PR 时,把"把 PR 指派给谁"和"开 PR"合成一次中继写入:`pr_create` 多带一个
`assignees` 列表,中继在 PR 建好后立刻用平台答复的 PR 号去指派。不再有单独的"指派"写入。os-dev.md
三行原地改写,规则不增行。
**为什么改**:单独的指派命令会被开发会话的自动权限分类器随机拒绝——同一条命令,一个 dev 通过、另一个被拒;此前 objectui 12
个 PR 里 4 个因此没有负责人。按规矩被拒的写入没人能替它补做,PR 就一直没有负责人。而"开
PR"这条中继写入从未被拒过,所以把指派搭在它上面。
**风险与代价(含回滚)**:改动只在中继的 op 表、执行器与回读,自测 455
例全绿,三组突变证明新断言会变红。风险一:平台对"仓库不能指派的登录名"会静默忽略,执行器现在把这种情况判为失败并停下后续动作——这是刻意的响亮失败,不是回归。风险二:中继跑在
main 上,合并前无法实跑新 op,第一次真实使用即是实测。回滚:revert 本 PR 即可,老的 `label-write
--assign` 路径仍在。
**席位意见**:(留空)
**你要做的**:一个动作——合并后看席位第一次带 `assignees` 的 `pr_create`,确认 PR
上出现负责人;不需要改任何配置。
---
_Generated by [Claude
Code](https://claude.ai/code/session_01RdnZdPZH9ByduzPRWuH9tN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4b7c630 commit 73d7001
6 files changed
Lines changed: 219 additions & 24 deletions
File tree
- .claude
- agents
- skills/pm-dispatch/references
- scripts/pm/fleet-write
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
58 | | - | |
| 58 | + | |
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| |||
350 | 350 | | |
351 | 351 | | |
352 | 352 | | |
353 | | - | |
| 353 | + | |
354 | 354 | | |
355 | 355 | | |
356 | 356 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
59 | | - | |
| 59 | + | |
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
207 | 207 | | |
208 | 208 | | |
209 | 209 | | |
210 | | - | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
211 | 218 | | |
212 | 219 | | |
213 | 220 | | |
| |||
648 | 655 | | |
649 | 656 | | |
650 | 657 | | |
| 658 | + | |
651 | 659 | | |
652 | 660 | | |
653 | 661 | | |
654 | 662 | | |
655 | 663 | | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
656 | 676 | | |
657 | 677 | | |
658 | 678 | | |
| |||
698 | 718 | | |
699 | 719 | | |
700 | 720 | | |
701 | | - | |
| 721 | + | |
702 | 722 | | |
703 | 723 | | |
704 | | - | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
705 | 733 | | |
706 | 734 | | |
707 | 735 | | |
| |||
991 | 1019 | | |
992 | 1020 | | |
993 | 1021 | | |
994 | | - | |
| 1022 | + | |
995 | 1023 | | |
996 | 1024 | | |
997 | 1025 | | |
| |||
1239 | 1267 | | |
1240 | 1268 | | |
1241 | 1269 | | |
1242 | | - | |
| 1270 | + | |
1243 | 1271 | | |
1244 | 1272 | | |
1245 | | - | |
| 1273 | + | |
1246 | 1274 | | |
1247 | 1275 | | |
1248 | 1276 | | |
| |||
1634 | 1662 | | |
1635 | 1663 | | |
1636 | 1664 | | |
| 1665 | + | |
1637 | 1666 | | |
1638 | 1667 | | |
1639 | 1668 | | |
| |||
1795 | 1824 | | |
1796 | 1825 | | |
1797 | 1826 | | |
| 1827 | + | |
| 1828 | + | |
| 1829 | + | |
| 1830 | + | |
| 1831 | + | |
| 1832 | + | |
| 1833 | + | |
1798 | 1834 | | |
1799 | 1835 | | |
1800 | 1836 | | |
| |||
2366 | 2402 | | |
2367 | 2403 | | |
2368 | 2404 | | |
2369 | | - | |
| 2405 | + | |
2370 | 2406 | | |
2371 | 2407 | | |
2372 | 2408 | | |
| |||
0 commit comments