Skip to content

fix(pm): issue-transfer confirms a transfer on the target and reads a lagging old URL as a pending redirect - #21291

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-21020-issue-transfer-readback-target
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-21020-issue-transfer-readback-target

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21020
Clause-②: no

What changed

scripts/pm/issue-transfer.mjs confirmed a transfer by reading the source URL first. If the old URL still answered 200, it exited 4 with "the card did not move". GitHub keeps serving a moved card's old URL with 200 for minutes, so every measured transfer that landed reported exit 4. The read-back now confirms on the target. The old URL is printed beside the verdict and never decides it.

  • Finding the card on the target. By number when one is in hand: the number in the transferIssue answer (direct), or else the number the old URL's 301 names. Otherwise by the pre-read's title, among the target's cards updated since the transfer was sent. That is the relay path: its run reports the number only in a job log and step summary that a seat container cannot read. Pull requests are skipped. Two cards with the same title are not a confirmation.
  • Exit 0 + the target URL when the card answers from the target with the pre-read's title. An old URL that still answers 200 from the source is printed as a PENDING REDIRECT, never as "did not move". --json gains pending_redirect.
  • Bounded re-read. While the target answers without the card, it is re-read on TRANSFER_READ_BACK_DELAYS_MS = 3 s, 7 s, 15 s (25 s in all). Each re-read prints one line. The transfer is never re-sent. The schedule copies the measured list lag of the relay's issue_create read-back. The old URL's own lag is minutes, so the re-read does not wait for it.
  • Exit 4 is kept for a board that truly disagrees. That means: after the last re-read the target still lacks the card, AND the old URL, read again, still serves it from the source. Also exit 4: a 301 to another repository or to another number than the mutation answered, or a card with another title.
  • Exit 6 (UNCONFIRMED) when the target cannot be read (for example the cloud session gate's 403), when two cards share the title, or when the target lacks the card but the old URL has moved since. An unreadable target is not re-read.
  • Only a 301 supplies a number. A 307 or 404 at the old URL is printed as itself and never read as the move. New redirectTarget() also reads the repository from the /repos/OWNER/NAME/issues/N spelling.

scripts/pm/fleet-write/dispatch.mjs and execute.mjs are not touched, and neither is dispatch-gates.mjs.

Measurements (this session, 2026-10-02, read-only)

  • Mechanism assumption 1, confirmed. At c7396f1a, issue-transfer.mjs L407-411 sent a 200 from the old URL straight to EXIT_BOARD_DISAGREES, before the target was ever read.
  • Assumption 2, confirmed. The direct path held moved.number and used it only to compare against the redirect's number.
  • The five transferred cards' old URLs, today: all now answer 301, a day after the card measured four of them still answering 200.
    • The four cards moved to hotcrm carry Location: https://api.github.com/repos/objectstack-ai/hotcrm/issues/1972 through 1975.
    • objectstack#17163, moved to cloud (private), answers 301 with an empty Location and an empty url, so no number is available to this identity. That is the "301 naming no card" branch.
  • A cloud session cannot read a target repository that is not attached to it. GET /repos/objectstack-ai/hotcrm/issues/1972 answers 403 "GitHub access to this repository is not enabled for this session". The same happens:
    • with or without a caller Authorization header (the proxy replaces it: a bogus bearer still read objectstack#21020 with 200);
    • on the github.com web page and the MCP issue read;
    • GraphQL answers 403 "not available from Claude Code sessions", and /search/issues also answers 403.
    • So in such a seat the relay path ends at exit 6 and names the pending redirect. It no longer ends at exit 4. Exit 0 needs a readable target: an attached repository, or the direct transport outside the container.
  • NOT MEASURED: how a transfer sets the card's created_at / updated_at. The target was unreadable here. The title search lists sort=updated&since=SENT-minus-60s. It rests on a transfer updating the card, which is not yet measured: the transfer gives the card a new number and a timeline event. It does not use sort=created, because a transferred card is expected to keep its original creation date.

Tests: on 4b37ce1e

  • pnpm check:pm-issue-transfer (= node scripts/pm/issue-transfer.mjs --self-test) → exit 0: ✓ issue-transfer self-test: 63 cases pass across 8 batteries.
    • Before this change it read 46 cases across 7 batteries.
    • The roster gains the battery "the target decides …" (16 cases). The read-back battery goes from 8 to 9 cases. The floor goes from 7 to 8.
  • New pins:
    • The card's pin. The old URL lags while the target holds the card ⇒ exit 0, with to.url equal to the target URL and pendingRedirect: true, under both the direct and the relay transport.
    • Positive controls. The target never holds the card and the old URL is unchanged ⇒ still exit 4, after exactly 3 waits of 3000/7000/15000 ms and 4 target reads. One mutation, or one dispatch with zero POSTs from this process. Nothing re-sent.
    • The 301 path is unchanged: the same call sequence as before.
    • A lagging target is found on re-read 1 or 2.
    • A 403 target is exit 6, with no wait and no "did not move".
    • Two cards with the same title ⇒ exit 6.
    • A 301 to another repository ⇒ exit 4, and the target is not read.
    • An old URL that turned into a 301 after the window ⇒ exit 6, not 4.
    • The two measured 301 shapes are pinned.
  • Reworded pins: the old-URL 404 and 307 cases. The old rule "only the 301 is read as the move" holds: neither status supplies a number. With the target confirming the card, a 404 at the old URL is now exit 0, printed as itself. A 307 under the relay never causes a read of the number it names.
  • Ablations, through scripts/ablation-replace.mjs in wrap mode. Each mutation landed on disk (anchor 1 → 0, blob changed). Each was restored to blob == HEAD 7cd9be392f56 with git diff HEAD empty.
    • M1 puts back the old "the old URL answers 200 ⇒ exit 4" behaviour → 12 of 63 red, including both pins.
    • M2 turns off the re-read loop → 4 of 63 red.
    • M4 drops the pull-request filter from the title search → 7 of 63 red.
    • The first M1 and M4 attempts were refused by the tool itself because the replacement overlapped the anchor. The command never ran and nothing was measured. They were re-run with distinct anchors.
  • Lint. npx eslint --no-inline-config --format json scripts/pm/issue-transfer.mjs → exit 0: 1 file, 0 errors, 0 warnings.
    • The config eslint prints for this file has parserOptions {ecmaVersion: latest, sourceType: module}. No type-aware linting is set up, so this diff cannot change any verdict on an untouched file.
    • The repo-wide pnpm lint is left to CI.
  • Gates. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands on this tree derived 28 families. All 28 ran with exit 0.
    • Reconciliation via --ran (exit codes recorded): "28 derived famil(ies) accounted for — 28 run, 0 NOT-MEASURED (a DERIVED zero …)".
    • The extra run is check:pm-issue-transfer, the script's own suite.
  • No package is touched, so there is no build closure and no package test or typecheck. check:nul-bytes is OK, and a control-byte self-scan of the file found nothing.

Acceptance notes

  • CI never runs check:pm-issue-transfer. No workflow names it, and check:self-test-wired covers only scripts CI already runs. The local run above is this suite's only measurement. Carrier: none.
  • Under the relay, exit 0 in a cloud seat requires the target to be attached to the session (see Measurements). If the relay reported the new number somewhere a seat container can read, such as an annotation, the relay path could name the target URL even then. That is a change to what the relay emits, and it is left to the PM or maintainer. PR fix(pm/fleet-write): re-list a lagging issue list before an issue_create read-back says unfound #21278 lands on dispatch.mjs and is not touched here.
  • NOT MEASURED: the updated_at premise of the title search (above). The first relay transfer from a session that can read the target settles it. Its read-back line says "found by that title" when the premise holds.

Generated by Claude Code

…gging old URL as a pending redirect

The read-back read the SOURCE first and exited 4 ("the card did not move")
whenever the old URL still answered 200 -- which GitHub keeps doing for
minutes after a transfer lands. It now finds the card on the target: by the
number the transferIssue answer carried (direct), else the number the old
URL's 301 names, else by the pre-read's title among the target's cards
updated since the send (the relay, whose run reports the number only where a
seat container cannot read it). A card found there is exit 0 with the target
URL, the old URL's 200 printed as a pending redirect. The target is re-read on
a bounded schedule while it answers without the card; exit 4 is kept for a
target that still lacks the card after it while the old URL still serves it
from the source; an unreadable or ambiguous target is UNCONFIRMED (6). The
transfer is never re-sent.

Claude-Session: https://claude.ai/code/session_01FNKm1SmPpuJASnbjxWGtsJ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Oct 2, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 2, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 02:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 3196ef1 Oct 2, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21020-issue-transfer-readback-target branch October 2, 2026 02:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants