Skip to content

Commit 73d7001

Browse files
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/os-dev.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ model: opus
3434
- 同批 agents 共用一个 scratchpad 目录,自然命名的文件会被彼此静默覆盖。
3535
2. **卡 assignee 归 PM。** 派发原子对已把它设好;共享身份下该字段答不了是谁。
3636
- 你的身份位是认领评论里的分支;仓 CLAUDE.md 的 claim-first 已由 PM 的认领满足。
37-
- 恒不写卡的 assignee(空也开工,报 `summary`);PR assignee = 卡的:`--issue PR_NUMBER --assign LOGIN`。
37+
- 恒不写卡的 assignee(空也开工,报 `summary`);PR assignee = 卡的:`pr_create` 带 `assignees: [LOGIN]`。
3838
- 发现与他人在途工作重复,停下报 `blocked`。
3939
- PR 上不是你设置的状态属于另一个 actor:⛔ 永不去纠正;疑问进报告,挡住报 blocked。
4040
- 共享身份让所有人的写入都像你写的;被改写的 body 只是关于 body 的证据,不证明别的。
@@ -55,7 +55,7 @@ model: opus
5555
- dev ⛔ 不查重、不扫 open issues、不拉板,只附 3–5 个查重词;查询与命中数由立卡席写。
5656
- GitHub 写一律经 `scripts/pm/` 写工具;云容器里经 `fleet-write` 中继落为 `objectstack-fleet[bot]`。
5757
- 归属 = 文本里的 session ID,非 `user.login`;`git push` 不是 REST 写,不走中继。
58-
- 写预算四笔:`git push`、一次 `pr_create`(draft)、`label-write`(含 PR assignee)、`os-dev-report` 评论。
58+
- 写预算四笔:`git push`、`pr_create`(draft,含 assignee)、`label-write`(仅标签)、`os-dev-report` 评论。
5959
- 卡与线程只走 payload 档(公开仓单卡网页内嵌 JSON,拼写住 platform-readings)或单卡 REST 读。
6060
- 三类发现进报告交席位代立,不 `POST /issues`、⛔ 不静默弃报;PM 去重读数当既有事实用。
6161
- PR 正文 dev 只写一次,在开 PR 那一笔,⛔ 不 `PATCH`;事后要改的报告点名改法,席位代写。
@@ -350,7 +350,7 @@ model: opus
350350

351351
- issue 对塑造公开契约的决定欠规格(spec/Zod schema、API 形状、命名、元数据语义)时:不猜。
352352
- 两种读法通向两种架构时同此;⛔ 不写投机代码。
353-
- 返回 `status: "needs_decision"`,把每个问题连同选项、成本与你的推荐写进 `open_questions`。
353+
- 返回 `status: "needs_decision"`,每个问题带选项、真实代价与工作量、推荐,写进 `open_questions`。
354354
- 升级分析的四轴决策框架由派发词携带,PM 从自己那份副本填入。
355355
- 框架唯一副本在 pm-dispatch SKILL.md 〈升级与决策〉;派发词逐字粘贴,dev ⛔ 不留副本。
356356
- 每个方案逐轴分析,推荐也按那些轴给理由;派发词没带,停下向 PM 索取,⛔ 不自拟一套轴。

‎.claude/skills/pm-dispatch/references/execution-duties.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656

5757
- 归属 = session ID,⛔ 不认作者字段;`Account:` 派生自 assignee,不等即半态、持卡席下笔补正。
5858
- 卡 assignee 归 PM,dev 恒不写;PR assignee = 卡的,接管 ② 同笔换;读者 `is:pr is:open assignee:LOGIN`。
59-
- dev `pr_create` 同轮 `label-write.mjs --issue PR_NUMBER --assign LOGIN`;席位自有 PR 指派自己。
59+
- dev `pr_create` 带 `assignees: [LOGIN]`,同笔指派;席位自有 PR 指派自己。
6060
- 部分落地(PR 带 `Refs #N (item k)`,⛔ 不 `Fixes`)即释放:合入同笔回 `pm:queue` + 清 assignee。
6161
- 同笔 `Release:` 行点名已落项与余项去向;余项需换道/拆分加 `pm:retriage`,自队列重新认领。
6262
- 派发前按序执行原子对:① Assign @me,并把 `pm:dispatched` 与摘 `pm:queue` 放进同一次标签写入。

‎scripts/pm/fleet-write/dispatch.mjs‎

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,14 @@
207207
* executor polls the pull's head off `expected_head_sha`, or measures a
208208
* 422 as a no-op, before it calls the action landed — so a `success` run
209209
* IS that confirmation; the seat reads the new head with one
210-
* `GET /repos/{repo}/pulls/{n}` after, and this file reads nothing back.
210+
* `GET /repos/{repo}/pulls/{n}` after, and this file reads nothing back;
211+
* - a `pr_create` carrying `assignees` is read back at its body's locator
212+
* as before, and its row ALSO carries the pull's `assignees` as the
213+
* platform lists them (`assigneesReading`; `--json` carries them too) —
214+
* a READING for the seat's report, never a verdict: the leg's landing
215+
* was judged inside the run (the executor fails the action when the
216+
* answer lists fewer than sent), so a login sent and absent here is
217+
* another actor's later write, said on the row's line.
211218
*
212219
* ## The started run — the one outcome only the seat can name
213220
*
@@ -648,11 +655,24 @@ export function readBackTargets(payload) {
648655
if (!BODY_OPS.includes(a?.op) || typeof a.body !== 'string') return;
649656
const target = { action: i + 1, op: a.op, sent: a.body };
650657
for (const key of ['issue', 'comment_id', 'title', 'head']) if (key in a) target[key] = a[key];
658+
if (Array.isArray(a.assignees)) target.assignees = [...a.assignees];
651659
out.push(target);
652660
});
653661
return out;
654662
}
655663

664+
/**
665+
* A `pr_create` row's assignees (header): what the pull lists, and which of
666+
* the logins the action sent are not among them — a reading, never a
667+
* verdict. Pure.
668+
*/
669+
export function assigneesReading(target, stored) {
670+
const assignees = Array.isArray(stored?.assignees) ? stored.assignees.map((u) => (typeof u?.login === 'string' ? u.login : null)).filter(Boolean) : [];
671+
const sent = Array.isArray(target?.assignees) ? target.assignees : [];
672+
const assigneesMissing = sent.filter((login) => !assignees.some((l) => l.toLowerCase() === login.toLowerCase()));
673+
return { assignees, assigneesMissing };
674+
}
675+
656676
const replacementCount = (s) => (String(s ?? '').match(/\uFFFD/gu) ?? []).length;
657677

658678
/**
@@ -698,10 +718,18 @@ export function strokeReadBackState(rows) {
698718
/** The transcript line for one read-back row. Pure. */
699719
export function readBackLine(row) {
700720
const head = `fleet-write: read-back action ${row.action} ${row.op}${row.where ? ` ${row.where}` : ''}`;
701-
if (row.verdict === 'landed') return `${head}: ${row.sentBytes} byte(s) sent, ${row.storedBytes} stored — ${row.cls}; every byte sent is on the platform.`;
721+
if (row.verdict === 'landed') return `${head}: ${row.sentBytes} byte(s) sent, ${row.storedBytes} stored — ${row.cls}; every byte sent is on the platform.${assigneesText(row)}`;
702722
if (row.verdict === 'unverified') return `${head}: ⚠️ UNVERIFIED — ${row.why ?? `the stored body could not be read (${row.cls})`}.`;
703723
const fffd = row.replacements > 0 ? ` The stored body carries ${row.replacements} U+FFFD replacement character(s) the sent one does not — the mark of a byte-level split upstream.` : '';
704-
return `${head}: ✗ NOT STORED — sent ${row.sentBytes} byte(s), stored ${row.storedBytes}; first difference at byte ${row.offset}: sent ${row.sentContext} | stored ${row.storedContext}.${fffd}`;
724+
return `${head}: ✗ NOT STORED — sent ${row.sentBytes} byte(s), stored ${row.storedBytes}; first difference at byte ${row.offset}: sent ${row.sentContext} | stored ${row.storedContext}.${fffd}${assigneesText(row)}`;
725+
}
726+
727+
/** The assignees clause of a pr_create row's line (header): what the pull lists, and any login sent that is not among them — said, never judged. Pure. */
728+
export function assigneesText(row) {
729+
if (!Array.isArray(row.assignees)) return '';
730+
const listed = ` Assignees as the pull lists them: ${row.assignees.length ? row.assignees.join(', ') : 'none'}.`;
731+
const missing = Array.isArray(row.assigneesMissing) && row.assigneesMissing.length ? ` ⚠️ sent ${row.assigneesMissing.join(', ')} not among them — the leg landed inside the run, so read the pull's timeline for the later write.` : '';
732+
return `${listed}${missing}`;
705733
}
706734

707735
/** What the CLI prints on a `failure` the read-back measured. */
@@ -991,7 +1019,7 @@ export async function readBackStroke(payload, { dispatchedAt, annotations = [] }
9911019
continue;
9921020
}
9931021
taken.add(hit.id);
994-
rows.push({ ...judged(`${repo}#${hit.number}`, hit.body), ...(isPr ? {} : { foundBy: 'list' }) });
1022+
rows.push({ ...judged(`${repo}#${hit.number}`, hit.body), ...(isPr ? assigneesReading(target, hit) : { foundBy: 'list' }) });
9951023
continue;
9961024
}
9971025
// `comment`: only the body finds a new comment, so a body that matches nothing is unverified, never not-stored.
@@ -1239,10 +1267,10 @@ const SELF_TEST_BATTERIES = Object.freeze({
12391267
'the dispatch: one paced POST to the board repo with event_type and client_payload; 204 is acceptance, anything else a refusal': 7,
12401268
'the run-poller: the run named after the request id, its completion, its conclusion': 6,
12411269
'the ceilings: no run in the start window, no completion in the ceiling — UNCONFIRMED, never retried': 7,
1242-
'the read-back targets: every op the table lets carry a body — derived, each with a locator — and no other op is read back': 7,
1270+
'the read-back targets: every op the table lets carry a body — derived, each with a locator — and no other op is read back': 8,
12431271
"the verdict: post-stamped's own classifier, imported — declared normalisations land, a split, lost or truncated byte is NOT STORED at its first differing byte, an unreadable body is unverified, the PR-create footer forgiven on pr_create alone": 13,
12441272
"the round trip: the card's 41,699 bytes with a multi-byte character across every 16 KiB boundary of every stream, byte for byte through pack, the wire, the runner's env text, the validator and the executor; a per-chunk decode is NOT STORED": 8,
1245-
'the read-back end to end: after a success run each body at its locator — a corrupted read-back exits 4 through the CLI, an unreadable or unfound one 6, a body-less stroke reads nothing, never a retry': 17,
1273+
'the read-back end to end: after a success run each body at its locator — a corrupted read-back exits 4 through the CLI, an unreadable or unfound one 6, a body-less stroke reads nothing, never a retry': 20,
12461274
'the issue_create re-list: a list that lags the create reads back IDENTICAL on a bounded re-list; a real miss is still unfound (exit 6) after exactly the declared window; an unreadable re-list is unread; the create is never re-sent': 13,
12471275
"the run's annotations: after a success run carrying an op that creates or moves a card its check-run annotations are read first — an issue_create they name read back AT that number with NO re-list; absent, unreadable, ignored or past the cap, the re-list as before; a stroke with no such op reads none": 14,
12481276
'the wiring: the POST is paced and on the roster, the reads are not, the token never reaches the log': 5,
@@ -1634,6 +1662,7 @@ export async function selfTest() {
16341662
const mixed = { repo: 'objectstack-ai/objectstack', actions: [{ op: 'labels_add', issue: 1, labels: ['a'] }, { op: 'issue_patch', issue: 2, state: 'closed' }, { op: 'issue_patch', issue: 3, body: 'B' }, { op: 'pr_create', title: 'T', head: 'h', base: 'main' }, { op: 'pr_create', title: 'T', head: 'h2', base: 'main', body: 'P' }] };
16351663
t('only an action that SENT a body is a target, numbered as the stroke numbers it', readBackTargets(mixed).map((x) => [x.action, x.op]), [[3, 'issue_patch'], [5, 'pr_create']]);
16361664
t('…carrying the keys its locator needs', [readBackTargets(mixed)[0].issue, readBackTargets(mixed)[1].head, readBackTargets(mixed)[1].sent], [3, 'h2', 'P']);
1665+
t('a pr_create that sent assignees carries them on its target, for the row to compare against what the pull lists; one that sent none carries no such key', [readBackTargets({ repo: 'objectstack-ai/objectstack', actions: [{ op: 'pr_create', title: 'T', head: 'h', base: 'main', body: 'P', assignees: ['marchtian'] }] })[0].assignees, 'assignees' in readBackTargets(mixed)[1]], [['marchtian'], false]);
16371666
t("pr_update_branch carries no body — not in BODY_OPS, no locator, no target: its confirmation is the executor's, inside the run", [BODY_OPS.includes('pr_update_branch'), 'pr_update_branch' in READ_BACK_LOCATORS, readBackTargets({ repo: 'objectstack-ai/objectstack', actions: [{ op: 'pr_update_branch', pull: 1, expected_head_sha: '1'.repeat(40) }] })], [false, false, []]);
16381667
let fetched = 0;
16391668
const none = await readBackStroke({ repo: 'objectstack-ai/objectstack', actions: [{ op: 'labels_add', issue: 1, labels: ['a'] }] }, { dispatchedAt: T0 }, {
@@ -1795,6 +1824,13 @@ export async function selfTest() {
17951824
t("pr_create: found by owner:head; the platform's session-URL footer after every byte sent is success", [pr.state, pr.readBack?.rows?.[0]?.cls, pr.readBack?.rows?.[0]?.where], ['success', 'pr-create-footer-appended', 'objectstack-ai/objectstack#20600']);
17961825
const noPr = await drive({ ...OK_RUN, store: { [PULLS]: { status: 200, json: [] } } }, { stroke: prStroke });
17971826
t('…and a pull the head does not find is unverified, exit 6', [noPr.state, exitForResult(noPr)], ['unverified', EXIT_UNCONFIRMED]);
1827+
const prAssigned = strokeOf([{ op: 'pr_create', title: 'T', head: 'claude/issue-1-x', base: 'main', body: B, assignees: ['marchtian'] }]);
1828+
const pullListing = (assignees) => ({ [PULLS]: (q) => ({ status: 200, json: q.get('head') === 'objectstack-ai:claude/issue-1-x' ? [{ id: 31, number: 20600, created_at: at(4_000), body: `${B.replace(/\n+$/u, '')}${PR_TAIL}`, assignees }] : [] }) });
1829+
const withAssignee = await drive({ ...OK_RUN, store: pullListing([{ login: 'marchtian' }]) }, { stroke: prAssigned });
1830+
t("pr_create with assignees: the row carries the pull's assignees as the platform lists them, the line says so, and the verdict is the body's", [withAssignee.state, withAssignee.readBack?.rows?.[0]?.verdict, withAssignee.readBack?.rows?.[0]?.assignees, withAssignee.readBack?.rows?.[0]?.assigneesMissing, readBackLine(withAssignee.readBack.rows[0]).includes('lists them: marchtian')], ['success', 'landed', ['marchtian'], [], true]);
1831+
const unassigned = await drive({ ...OK_RUN, store: pullListing([]) }, { stroke: prAssigned });
1832+
t('…a login sent and absent on the pull is SAID on the line and in the row — the leg landed inside the run, so this is a reading, never a verdict', [unassigned.state, unassigned.readBack?.rows?.[0]?.verdict, unassigned.readBack?.rows?.[0]?.assigneesMissing, readBackLine(unassigned.readBack.rows[0]).includes('sent marchtian not among them')], ['success', 'landed', ['marchtian'], true]);
1833+
t('…and a pr_create that sent none carries the list the pull shows and nothing missing; an issue row carries neither', [pr.readBack?.rows?.[0]?.assignees, pr.readBack?.rows?.[0]?.assigneesMissing, 'assignees' in (good.readBack?.rows?.[0] ?? {})], [[], [], false]);
17981834
const labels = await drive(OK_RUN, { stroke: strokeOf([{ op: 'labels_add', issue: 1, labels: ['a'] }]) });
17991835
t('a stroke carrying no body reads nothing back: success, read-back none, no request beyond the dispatch and the run', [labels.state, labels.readBack?.state, labels.seen.filter((s) => !s.call.includes('/actions/runs') && s.call !== DISPATCH).length], ['success', 'none', 0]);
18001836
t("the callers' one vocabulary: failure + notStored → 4, failure → 5, unverified → 6", [exitForResult({ state: 'failure', notStored: true }), exitForResult({ state: 'failure' }), exitForResult({ state: 'unverified' })], [EXIT_NOT_STORED, EXIT_PLATFORM_REFUSAL, EXIT_UNCONFIRMED]);
@@ -2366,7 +2402,7 @@ export async function main(argv, deps = {}) {
23662402
not_stored: result.notStored === true,
23672403
run: result.run,
23682404
dispatched_at: new Date(result.dispatchedAt).toISOString(),
2369-
read_back: (result.readBack?.rows ?? []).map((r) => ({ action: r.action, op: r.op, where: r.where, verdict: r.verdict, class: r.cls, first_difference_byte: r.offset ?? null, sent_bytes: r.sentBytes ?? null, stored_bytes: r.storedBytes ?? null, found_by: r.foundBy ?? null })),
2405+
read_back: (result.readBack?.rows ?? []).map((r) => ({ action: r.action, op: r.op, where: r.where, verdict: r.verdict, class: r.cls, first_difference_byte: r.offset ?? null, sent_bytes: r.sentBytes ?? null, stored_bytes: r.storedBytes ?? null, found_by: r.foundBy ?? null, assignees: r.assignees ?? null, assignees_missing: r.assigneesMissing ?? null })),
23702406
annotations: (result.annotations?.rows ?? []).map((a) => ({ action: a.action, op: a.op, number: a.number, url: a.url })),
23712407
runs: (result.runs?.rows ?? []).map((r) => ({ action: r.action, op: r.op, workflow: r.workflow, verdict: r.verdict, class: r.cls, candidates: r.candidates ?? 0, run: r.run ? { id: r.run.id, url: r.run.url, status: r.run.status, conclusion: r.run.conclusion, created_at: r.run.created_at, actor: r.run.actor } : null })),
23722408
}),

0 commit comments

Comments
 (0)