Commit bada8d3
fix(pm): fleet-write relay reports created and moved card numbers as check-run annotations; the read-backs read them first (#21312)
Fixes #21294
Clause-②: no
## What this changes
Per the triage ruling on #21294 (comment 5944826620, "Ruling: emit
annotations"): the relay reports each card it creates or moves on the
job's check run, and the read-backs read that first.
- **Emission (`scripts/pm/fleet-write/execute.mjs`).** For every action
whose op creates or moves a card (`issue_create`, `transfer`; the table
is `ANNOTATED_OPS`) and whose request LANDED, the executor prints ONE
workflow command to stdout: a `notice` titled `fleet-write OP` whose
message is `fleet-write action=I op=OP number=N url=URL`. The number and
url come from the platform's ANSWER to that request (the created issue's
`number` / `html_url`; the `transferIssue` answer's `issue.number` /
`issue.url`), never from the request. No other op emits one. An answer
with no number and url, or one whose url names another repository than
the op lands on, emits nothing and the log says so. Escaping follows
actions/toolkit `command.ts` (`escapeData` / `escapeProperty`).
- **One spelling (`scripts/pm/fleet-write/dispatch.mjs`).**
`relayAnnotationMessage` (writer) and `parseRelayAnnotation` (reader)
sit side by side, and the executor imports both. It emits only a message
the parser reads back as exactly that number, url and landing
repository. `matchRunAnnotations` keeps a row only when the stroke's
action at its index has that op and its url names the repository that op
lands on. When one action has two different rows, both are dropped.
- Why `dispatch.mjs` and not `execute.mjs` or a new module: a
`dispatch.mjs` → `execute.mjs` import closes a static cycle through
`label-write.mjs`. A dynamic one, taken while a top-level-await entry
such as `label-write.mjs` is still evaluating, is the deadlock shape
`dispatch.mjs`'s header records. A new file would sit outside
`RELAY_FILES` and outside every self-test gate's derived population
(measured: `dispatch-gates --commands` on a new `fleet-write/` path
derives none of the `check:pm-fleet-write-*` gates). `execute.mjs`
already reaches `dispatch.mjs` through `label-write.mjs`, so its new
direct import adds no edge to any cycle.
- **The read (`sendFleetWrite`).** After a success run carrying an
annotated op, it reads `GET
/repos/objectstack-ai/objectstack/actions/runs/{id}/jobs` and then `GET
.../check-runs/{id}/annotations`. It matches the rows and prints one
line for each row, each ignored row, each annotated action no row names,
and an unreadable read. It hands them on as `result.annotations` (and in
the CLI's `--json`). A stroke with no annotated op reads nothing extra.
- **`issue_create` read-back:** an action an annotation names is read AT
that number: one GET, no list, no re-list. Otherwise the
`ISSUE_CREATE_RELIST_DELAYS_MS` re-list runs unchanged. If that GET
fails, the result is `unread` (exit 6), and the code still does not
re-list: the fallback is for an absent annotation only.
- **Transfer read-back (`scripts/pm/issue-transfer.mjs`).** Under the
relay, the number now comes from the annotation, then the 301, then the
title. With the annotated number, the card is read on the target by
number and no title search is made. `TRANSFER_READ_BACK_DELAYS_MS` is
unchanged, and the transfer is never re-sent. The PM's conditional
ruling for a session that cannot read the target, verbatim:
> 前提成立 ⇒ 注解等价于 direct 路径里 `transferIssue` 的 `moved` 应答:读回先按注解的号去 GET
目标;目标 200 且同题 ⇒ exit 0;目标 403(本会话未挂该仓)⇒ **仍 exit 0**,打印 `confirmed_by:
relay-annotation` 与目标 URL、源 URL 读数作旁证(pending redirect 照印);目标 404 或同题不符
⇒ 维持现有 exit 4 / 6 判定。
The premise holds: the executor reads the number from the
`transferIssue` answer (`data.transferIssue.issue.number` / `.url`), the
same object `requestLanded` already holds to the target. Implemented as
ruled. A 403 that is an exhausted rate limit is not this exception and
stays exit 6. `--json` gains `confirmed_by` (`target` |
`relay-annotation`).
- **In-place fix, outside the claim's declared surface
(`scripts/pm/issue-create.mjs`).** Its relay path listed the target's
issues ONCE, after `sendFleetWrite` returned, to find the new number.
That single list was protected from the measured list lag only because
`sendFleetWrite`'s own read-back had already waited out the re-list.
Once that read-back reads the annotated number instead, the protection
is gone. The fix: take the number from `sent.annotations` first, and
fall back to the existing title list only when no annotation names it.
Evidence: the new case "the control — no annotation, the same lagging
list — is UNCONFIRMED (6)" beside the annotated case, which exits 0 with
zero list reads; ablation 5 below. Same defect family as the card, a
mechanical change of the shape the ruling fixed, no open PR touches the
file (every open PR's file list read), and its gate
`check:pm-issue-create` is already in the dispatch's derived list.
**Not touched:** `.github/workflows/fleet-write.yml`. The notice is a
workflow command printed by the existing execute step. The measured
relay run below already carries annotations from the same job under its
`contents: read` grant, so no workflow change is needed.
`scripts/pm/dispatch-gates.mjs` is not touched either.
## Measured from this (cloud) container
- Relay run 36960568160: its jobs list answers job 110693116713, whose
`check_run_url` ends in `/check-runs/110693116713` (job id equals
check-run id). `GET /check-runs/110693116713/annotations` answers 200
with 3 annotations: two `Input 'app-id' has been deprecated` warnings
and the runner's `ubuntu-latest` notice. There is no relay annotation
yet, as expected before this lands. `output.summary` is `null`. The
parser is pinned against those two foreign messages.
- A repository not attached to this session answers 403 with
`documentation_url` on docs.anthropic.com, so `classifyHttp` reads it as
`prerequisite`. The 403 exception is therefore keyed on the status and
"not an exhausted rate limit", not on `classifyHttp`.
- Platform cap: "10 warning annotations, 10 error annotations, and 10
notice annotations per step" and "50 annotations per job"
(actions/toolkit `docs/problem-matchers.md`; docs.github.com is not
reachable from this container). Past the cap, the later actions carry no
annotation and each one falls back on its own. This is stated in both
docblocks.
- **NOT MEASURED, live:** the relay runs `main`'s `execute.mjs`, so the
first live annotation appears on the first relay `issue_create` /
`transfer` after this merges. Until then every reader takes its
fallback, as ruled.
## Tests (at `4121efc7`)
- Self-tests: `check:pm-fleet-write-execute` 83 cases / 12 batteries
(+1: annotations, 12 cases). `check:pm-fleet-write-dispatch` 173 / 17
(+1: the run's annotations, 14). `check:pm-issue-transfer` 75 / 9 (+1:
the relay annotation, 12; CI does not run this one, see #21295, so it
was run here). `check:pm-issue-create` 47 / 7 (+1: the relay annotation,
5). All exit 0, and each battery floor was raised by one.
- Pins the ruling names: both read-back batteries gain the annotation
case with no re-list or title search, and both keep the fallback case
(absent, unreadable, ignored, past the cap).
- Gates: the union of the dispatch's 56 named commands, the 37 that
`dispatch-gates --commands` derives from the actual 4-path change set (a
subset of the 56), and `pnpm check:pm-issue-transfer`. That is 57
commands, every one exit 0. `check:pm-dispatch-gates` ran detached:
"dispatch-gates self-test: 1976 cases pass", exit 0. `dispatch-gates
--ran`: "37 derived famil(ies) accounted for — 37 run, 0 NOT-MEASURED".
`check-self-test-workflow-commands` stays green (230 self-tests; the new
self-test output prints no workflow command).
- Lint, narrowed: `eslint --no-inline-config --format json` over the 4
files reports 4 files, 0 errors, 0 warnings. Population:
`eslint.config.mjs`'s `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` block and
`COMMENT_SWALLOW_FILES`. Invariance: the config sets no
`parserOptions.project` or `projectService` (no type-aware linting), so
this diff cannot move a verdict in an untouched file.
- Ablations, each through `scripts/ablation-replace.mjs` (anchor hit 1 →
0, blob changed; restore proven blob == HEAD and `git diff HEAD` empty).
Expected direction: red; observed: red every time.
1. `dispatch.mjs` read-back ignores the annotation: 6 of 173 fail.
2. `issue-transfer.mjs` 403 exception removed: 3 of 75 fail.
3. `issue-transfer.mjs` takes no annotation: 8 of 75 fail.
4. `execute.mjs` emission line deleted: 3 of 83 fail.
5. `issue-create.mjs` takes no annotation: 3 of 47 fail.
## Acceptance notes
- Every relay run carries two warnings that
`actions/create-github-app-token@v3`'s input `app-id` is deprecated
("Use 'client-id' instead"). This is drift in `fleet-write.yml`, a
`domain:spec` surface. It is noted here and not filed. Carrier: whoever
next edits `fleet-write.yml`.
- The claim's declared file surface does not name
`scripts/pm/issue-create.mjs` (the in-place fix above). The owning seat
may amend it; this run posts no claim.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FNKm1SmPpuJASnbjxWGtsJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 1caa603 commit bada8d3
4 files changed
Lines changed: 680 additions & 69 deletions
0 commit comments