Skip to content

fix(pm): fleet-write relay reports created and moved card numbers as check-run annotations; the read-backs read them first - #21312

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21294-fleet-write-annotation-numbers
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21294-fleet-write-annotation-numbers

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 finding(pm tooling): check:pm-issue-transfer is declared in package.json but no workflow runs it — the issue-transfer self-test (63 cases) is enforced only by hand; wire it into the pm roster or remove the entry #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

claude added 3 commits October 2, 2026 03:40
…k-run annotations

Claude-Session: https://claude.ai/code/session_01FNKm1SmPpuJASnbjxWGtsJ
Co-authored-by: Claude <noreply@anthropic.com>
…es pin both arms

Claude-Session: https://claude.ai/code/session_01FNKm1SmPpuJASnbjxWGtsJ
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FNKm1SmPpuJASnbjxWGtsJ
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants