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 intoOct 2, 2026
Conversation
…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>
objectstack-fleet
Bot
deleted the
claude/issue-21294-fleet-write-annotation-numbers
branch
October 2, 2026 05:07
This was referenced Oct 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 isANNOTATED_OPS) and whose request LANDED, the executor prints ONE workflow command to stdout: anoticetitledfleet-write OPwhose message isfleet-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'snumber/html_url; thetransferIssueanswer'sissue.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/toolkitcommand.ts(escapeData/escapeProperty).One spelling (
scripts/pm/fleet-write/dispatch.mjs).relayAnnotationMessage(writer) andparseRelayAnnotation(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.matchRunAnnotationskeeps 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.dispatch.mjsand notexecute.mjsor a new module: adispatch.mjs→execute.mjsimport closes a static cycle throughlabel-write.mjs. A dynamic one, taken while a top-level-await entry such aslabel-write.mjsis still evaluating, is the deadlock shapedispatch.mjs's header records. A new file would sit outsideRELAY_FILESand outside every self-test gate's derived population (measured:dispatch-gates --commandson a newfleet-write/path derives none of thecheck:pm-fleet-write-*gates).execute.mjsalready reachesdispatch.mjsthroughlabel-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 readsGET /repos/objectstack-ai/objectstack/actions/runs/{id}/jobsand thenGET .../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 asresult.annotations(and in the CLI's--json). A stroke with no annotated op reads nothing extra.issue_createread-back: an action an annotation names is read AT that number: one GET, no list, no re-list. Otherwise theISSUE_CREATE_RELIST_DELAYS_MSre-list runs unchanged. If that GET fails, the result isunread(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_MSis unchanged, and the transfer is never re-sent. The PM's conditional ruling for a session that cannot read the target, verbatim:The premise holds: the executor reads the number from the
transferIssueanswer (data.transferIssue.issue.number/.url), the same objectrequestLandedalready holds to the target. Implemented as ruled. A 403 that is an exhausted rate limit is not this exception and stays exit 6.--jsongainsconfirmed_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, aftersendFleetWritereturned, to find the new number. That single list was protected from the measured list lag only becausesendFleetWrite'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 fromsent.annotationsfirst, 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 gatecheck:pm-issue-createis 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 itscontents: readgrant, so no workflow change is needed.scripts/pm/dispatch-gates.mjsis not touched either.Measured from this (cloud) container
check_run_urlends in/check-runs/110693116713(job id equals check-run id).GET /check-runs/110693116713/annotationsanswers 200 with 3 annotations: twoInput 'app-id' has been deprecatedwarnings and the runner'subuntu-latestnotice. There is no relay annotation yet, as expected before this lands.output.summaryisnull. The parser is pinned against those two foreign messages.documentation_urlon docs.anthropic.com, soclassifyHttpreads it asprerequisite. The 403 exception is therefore keyed on the status and "not an exhausted rate limit", not onclassifyHttp.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.main'sexecute.mjs, so the first live annotation appears on the first relayissue_create/transferafter this merges. Until then every reader takes its fallback, as ruled.Tests (at
4121efc7)check:pm-fleet-write-execute83 cases / 12 batteries (+1: annotations, 12 cases).check:pm-fleet-write-dispatch173 / 17 (+1: the run's annotations, 14).check:pm-issue-transfer75 / 9 (+1: the relay annotation, 12; CI does not run this one, see finding(pm tooling):check:pm-issue-transferis 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-create47 / 7 (+1: the relay annotation, 5). All exit 0, and each battery floor was raised by one.dispatch-gates --commandsderives from the actual 4-path change set (a subset of the 56), andpnpm check:pm-issue-transfer. That is 57 commands, every one exit 0.check:pm-dispatch-gatesran 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-commandsstays green (230 self-tests; the new self-test output prints no workflow command).eslint --no-inline-config --format jsonover the 4 files reports 4 files, 0 errors, 0 warnings. Population:eslint.config.mjs's**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}block andCOMMENT_SWALLOW_FILES. Invariance: the config sets noparserOptions.projectorprojectService(no type-aware linting), so this diff cannot move a verdict in an untouched file.scripts/ablation-replace.mjs(anchor hit 1 → 0, blob changed; restore proven blob == HEAD andgit diff HEADempty). Expected direction: red; observed: red every time.dispatch.mjsread-back ignores the annotation: 6 of 173 fail.issue-transfer.mjs403 exception removed: 3 of 75 fail.issue-transfer.mjstakes no annotation: 8 of 75 fail.execute.mjsemission line deleted: 3 of 83 fail.issue-create.mjstakes no annotation: 3 of 47 fail.Acceptance notes
actions/create-github-app-token@v3's inputapp-idis deprecated ("Use 'client-id' instead"). This is drift infleet-write.yml, adomain:specsurface. It is noted here and not filed. Carrier: whoever next editsfleet-write.yml.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