Skip to content

fleet-write: no op can transfer an issue to a sibling repo — add a transfer op (GraphQL transferIssue, token minted for source + target) and an issue-transfer door, so cards filed in the wrong repo move with their history instead of being rebuilt #19884

Closed

Description

@objectstack-fleet

Filing-gate category: ③ direct dispatch from the session operator (os-litant), instruction quoted verbatim below.
Reader: the domain:skills execution seat. The first consumer is waiting now: session https://claude.ai/code/session_01X7HwfPLpQtCixDMrRGkSbe must move four repo:objectui cards (#18397, objectstack-ai/objectui#10439, objectstack-ai/objectui#10438, objectstack-ai/objectui#10437) into objectstack-ai/objectui and will do it through this door once it lands.
Dedup (open + closed, MCP search_issues): the query "transfer issue to another repository fleet-write relay op" returned 5 hits, all closed fleet cards (#19641, #19701, #19739, #19762, #19774), none about transfer; the query "issue transfer" returned 0 hits. Control: "objectstack-fleet write identity" returned 5 hits including #19881, filed minutes earlier, so the channel is live.

Instruction (verbatim)

objectui 你负责迁移。fleet 中继不支持 GitHub 的 issue transfer, 应该立 skills 卡解决

What is missing

Rule 1 of .claude/skills/pm-dispatch/references/cross-repo-coordination.md says a card lives in the repo where its fix lands, and a card whose body still stands without objectstack moves on the spot. The same file's fallback line, 「transfer 不可用时重建:出处头 + 裸 #N 改全名 + 关源单为 moved」, is the only route today, because the fleet relay cannot transfer:

  • scripts/pm/fleet-write/ops.mjs has a closed op table (comment, comment_edit, labels_add, labels_remove, assign, unassign, issue_patch, issue_create, pr_create, pr_request_reviewers, pr_ready, pr_draft, automerge_enable, automerge_disable). It has no transfer op.
  • .github/workflows/fleet-write.yml mints the App token with repositories: set to the one repo the payload names. GitHub's transferIssue mutation needs the actor to hold issues write on both the source and the target repository, so even a new op row would be refused with the token as minted today.
  • REST has no transfer endpoint; the transfer exists only as the GraphQL transferIssue mutation.

Rebuilding a card loses its comment thread, timeline, cross-references and reactions, and leaves a closed husk behind. A real transfer keeps all of them, and GitHub redirects the old URL.

Fix (suggested route; measurement wins)

  1. ops.mjs: add a transfer op. Required keys: the source issue number and the target repo name (a closed enum of sibling repos under objectstack-ai, not free text). Optional key: create_labels_if_missing, default false. It spends issues: write on both repos. It is refused for a pull request, for a target outside the enum, and for a target equal to the source.
  2. fleet-write.yml: for the transfer op only, mint the token for the source and target repositories. Every other op keeps the single-repo narrowing. validate.mjs emits the second repo name; the payload is never read again for it.
  3. execute.mjs: resolve the issue and target-repo node ids, run transferIssue, and return the new number and URL.
  4. A door script (for example scripts/pm/issue-transfer.mjs, or a mode of an existing door) that paces through write-pace.mjs. It reads back the card at its new location, then prints the old and new numbers. It supports --dry-run and --self-test, and it uses the same exit ladder as issue-create, including 6 UNCONFIRMED (never re-run blind).
  5. Update the fallback line in cross-repo-coordination.md to point at the door. Rebuild stays only for the cases GitHub refuses (for example, a target repo outside the organisation).

Open questions for the seat to measure before building:

  • Does the fleet App's installation cover objectstack-ai/objectui, objectstack-ai/cloud and objectstack-ai/hotcrm?
  • What happens to labels with no same-named label in the target? repo:objectui and every domain:* label have no meaning there.
  • Are assignees kept when the assignee has no access to the target?

Acceptance

  • validate.mjs --self-test pins three things: the transfer row, the refusal of a target outside the enum, and the refusal of a pull request.
  • The workflow still narrows every non-transfer op to one repository, and a test or self-test pins it.
  • One live transfer is read back at its new URL, and the old URL redirects. The first live use is one of the four cards listed above.

Generated by Claude Code

Activity

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    pm:queueReady for the PM dispatch loop
    domain:skillsDomain lane — seat card indexed by label:pm:seat
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 23, 2026
  2. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2
    Session: session_01A22sUB3mUWs6M36VgfijBq
    Branch: claude/issue-19884-fleet-write-transfer-op
    Worktree: objectstack-issue-19884
    Domain: domain:skills
    Seat: domain:skills#1
    File surface: scripts/pm/fleet-write/{ops,validate,execute,dispatch}.mjs · .github/workflows/fleet-write.yml (the transfer-only two-repository mint) · a new door scripts/pm/issue-transfer.mjs with its package.json check:pm-issue-transfer row and its write-pace.mjs wiring entry · .claude/skills/pm-dispatch/references/cross-repo-coordination.md :25 (the fallback line) (stop on breach; explain in the report)
    Container & model: M — a relay op plus a token-scope change, judged by the relay self-tests and a dry run; the live transfer is the consumer session's, not this dispatch's, mode:subagent, model: opus — dispatch-gates --tier --repo objectstack-ai/objectstack on the surface prints "no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable"
    Clause-②: no
    Thread-read: none
    Serial constraints cleared: no open PR touches scripts/pm/fleet-write/** or .github/workflows/fleet-write.yml (22 open PRs' file lists read at 2026-09-23T15:19Z); PR #19890 (#19881's dev) touches SKILL.md + filing-gate.md, disjoint; newest landings on the surface cc2e951 (fail-closed liveness read) and f151ef2 of this date; #19881 (os-litant, in flight) shares no file with this card; the first live transfer belongs to the consumer session named on the card (the four repo:objectui cards), ⛔ not to this dispatch.

    Take basis: 立卡门 ③ — the session operator's verbatim instruction recorded on the card (session_01X7HwfPLpQtCixDMrRGkSbe, this date): 「objectui 你负责迁移。fleet 中继不支持 GitHub 的 issue transfer, 应该立 skills 卡解决」, with a named consumer waiting; p2 tooling with objectstack product P0 / P1 open (北极星「优先级」第 3 条, 1 · 28 at 2026-09-23T13:09Z) — the direct instruction and the waiting consumer are read above that rule, both for the maintainer's veto in the round report. Premise re-read on origin/main at 2026-09-23T15:19Z: fleet-write/ops.mjs names no transfer op (0 mentions), fleet-write.yml :106 mints repositories: ${{ steps.validate.outputs.repo_name }} — one repository, cross-repo-coordination.md :25 is the rebuild fallback — the card's readings stand. Dispatched as a subagent of this session with this comment as its only claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 19884,
    "status": "done",
    "branch": "claude/issue-19884-fleet-write-transfer-op",
    "pr": "#19902",
    "session": "session_01A22sUB3mUWs6M36VgfijBq",
    "premise_still_valid": true,
    "summary": "Premise re-read on origin/main dabf8d7: ops.mjs had 14 ops and no transfer, fleet-write.yml minted repositories from validate's single repo_name, and cross-repo-coordination.md line 25 was the rebuild fallback, so the premise stands. The relay now has a transfer row (GraphQL transferIssue, keys issue + target_repo, spends issues). Its target is a closed enum reusing GOVERNED_REPOS from check-governed-merges.mjs, and it never creates labels. validate.mjs computes the token list the workflow mints for (tokenRepositoriesOf, output repositories): one repository for every stroke, and source + ONE target for a transfer-only stroke. The self-test pins both halves and the workflow line. execute.mjs gates the sender on every repository the token reaches, refuses a pull or an already-moved card before the mutation, and accepts only an answer that lands on the target. The new door scripts/pm/issue-transfer.mjs moves one card by relay or direct mutation, fails closed with the installation remedy, and reads back the old URL's 301 plus the card at its new address. The door is wired into write-pace, check:pm-issue-transfer is added, and the reference line now points at the door. Live transfer is NOT MEASURED by design: the relay runs main's workflow.",
    "files_changed": [
    ".claude/skills/pm-dispatch/references/cross-repo-coordination.md (1 line for 1, 51/51 lines, 120 bytes)",
    ".github/workflows/fleet-write.yml (mint reads steps.validate.outputs.repositories; comments)",
    "package.json (check:pm-issue-transfer)",
    "scripts/pm/fleet-write/ops.mjs (transfer row, TRANSFER_TARGETS, target_repo enum, transferRemedy, ALLOWED_MUTATIONS)",
    "scripts/pm/fleet-write/validate.mjs (strokeErrors, validateStroke, tokenRepositoriesOf, repositories output, 2 new batteries)",
    "scripts/pm/fleet-write/execute.mjs (per-repository sender gate, transfer node-id resolution, landing check, remedy, 1 new battery)",
    "scripts/pm/issue-transfer.mjs (new door, 7-battery self-test with handshake)",
    "scripts/pm/write-pace.mjs (WIRED_WRITE_TOOLS += issue-transfer, census comment)"
    ],
    "tests": "All at HEAD 425752b (git rev-parse --short HEAD). Self-tests: check:pm-fleet-write-validate 94 cases pass across 9 batteries (baseline 74 across 7); check:pm-fleet-write-execute 59 across 10 (baseline 48 across 9); check:pm-issue-transfer 46 across 7 (new); check:pm-write-pace 113 across 12; check:pm-fleet-write-dispatch 101 across 11; check:pm-issue-create 42 across 6; every one exit 0. Red-before-green: validate went red 1 of 94 on the workflow pin until fleet-write.yml read the new output; write-pace went red on its roster pin until the door was tracked. Ablations: each mutation was landed through scripts/ablation-replace.mjs (anchor 1 to 0 on disk, blob changed) and restored to blob == HEAD with git diff HEAD empty. (1) execute gates only payload.repo: RED 4 of 59. (2) every stroke token list gains objectui: validate RED 4 of 94. (3b) door pull-request refusal disabled: RED 1 of 45. (3a) door 301 check disabled: GREEN on the first run. The mutation landed, but the 404 fixture failed later at the redirect-number check, so the test had no witness for the branch. It was fixed in 72bc4ea with a 307 fixture carrying a location, and the rerun went RED 2 of 46. eslint, narrowed: the population is the 5 changed .mjs files (eslint --print-config; the .md/.json files answer no matching configuration); --format json reports 5 files, 0 errors, 0 warnings; eslint.config.mjs enables no type-aware linting, so untouched files cannot move. NOT MEASURED: live transfer. Reason: the relay runs main's workflow; first live use is the consumer session's on one of the four repo:objectui cards.",
    "gates": "Derivation: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths, 8 changed paths) gave 70 commands, identical at 5d9c568 and 425752b. --ran reconciliation at 425752b: 70 derived, 68 run, 2 NOT-MEASURED, 0 UNRUN (exit 0). The 68 run all exited 0. They include every dispatch-named family: pm-fleet-write-validate/execute/dispatch, pm-write-pace, pm-issue-create, pm-skill-ratchet (cross-repo-coordination.md 51 lines, ceiling 51), pm-skill-id-lint, nul-bytes. Also: self-test-wired, required-contexts, stall-guard-budget/headroom, workflow-status-functions, workflow-step-name-quoting, shard-attestation, declared-population-live, check-comment-mask-corpus, and pm-dispatch-gates. pm-dispatch-gates: 1905 cases, battery 892.8 s, exit 0. It exceeds the foreground cap, so it ran detached to a log under recorded PID 9317; I waited on it in the foreground with tail --pid, read the exit from its exit file, and confirmed the process gone. check:pm-settings-deny-roster was named by the dispatch but is not in the derivation. It was run separately, self-test 36 cases then the gate "17 content-write tools declared = enforced", both exit 0. It ran in the read-only checkout after the worktree was removed; its two inputs (scripts/pm/check-settings-deny-roster.mjs and .claude/settings.json) hash-object on disk equal to 425752b's blobs, and this diff touches neither. NOT MEASURED (recorded exit 3, PREREQUISITE NOT MET, no monorepo dist): check:dual-build-cjs-loads and check:lean-entry-closure. check:dts-closure and check:sourcemap-no-sources-content exited 0 over only the 4 packages built locally (the lint/formula build ran under os-verify-lock for doc-formula-expressions), so read them as partial. All four are matched only via the root package.json in the Build Core filter; the diff changes one scripts row and no workspace package. CI Build Core owns them. PR CI at report time: 35 check runs, 15 success, 7 skipped, 13 in_progress, 0 red. State: in_progress.",
    "deviations": [
    "Base is dabf8d7, not the dispatch-time e9eb224: origin/main advanced before the worktree was cut. The two intervening commits (3bd221d objectql, dabf8d7 docs) touch nothing on this surface.",
    "No create_labels_if_missing key, although the card's suggested route step 1 had one. Nothing needs it, and creating repo:objectui or domain:* labels in objectui is noise; a dropped label is re-added with labels_add. Adding it later is one row.",
    "Added constraints beyond the card: a stroke carrying a transfer carries transfers alone, to ONE target, each card once; the executor also gates the sender on the target. Both keep the widened token to exactly transferIssue under GitHub's own two-sided write rule.",
    "validate's GitHub output key was renamed repo_name to repositories. Its only consumer is the workflow line, changed in the same commit.",
    "The reference line reads 「名册外或平台拒才重建」. Rebuild also covers a target outside the roster (for example www.objectos.ai, a single-lane repo not in GOVERNED_REPOS), which the card worded only as "GitHub refuses".",
    "check:pm-dispatch-gates ran detached, with the PID recorded and a foreground tail --pid wait, because its roughly 890 s exceeds the foreground cap. Four Build Core dist gates are declared narrowed (2 NOT MEASURED, 2 partial).",
    "In reply to the coordinator's check-in, I sent one status line via SendMessage (not GitHub).",
    "Commit trailers use the model-free pair AGENTS.md requires (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By."
    ],
    "mcp_calls": "0: no MCP GitHub tool was called. The only non-shell tool message was one SendMessage status reply to the coordinator.",
    "api_writes": "3, each a POST /repos/objectstack-ai/objectstack/dispatches through the fleet-write relay, landing as objectstack-fleet[bot]: (1) pr_create, which became POST /repos/objectstack-ai/objectstack/pulls (PR 19902, draft, run 35890311509); (2) label-write labels_add skip-changeset, which became POST /repos//issues/19902/labels (run 35890402651, read back MATCHES); (3) this os-dev-report comment via post-stamped, which becomes POST /repos//issues/19884/comments. git push is not REST. No PATCH of the PR body: it was read back byte-identical, 11553 bytes.",
    "open_questions": [
    {
    "question": "Card Q3: is an assignee kept when that person has no access to the target? GitHub's docs say only that assignees are retained. The door's read-back prints the labels kept and dropped, not the assignees.",
    "options": [
    "A: accept GitHub's behaviour and read it at the consumer's first live transfer. The shared assignee account has org-wide access, so the case cannot arise on the four objectui cards.",
    "B: add an assignee before/after line to the door's read-back now."
    ],
    "recommendation": "A. The real-need axis has no card with an assignee lacking target access, and B adds surface with no pull at startup stage."
    },
    {
    "question": "Card Q1: does the objectstack-fleet App installation cover objectui, cloud and hotcrm? A seat cannot read this (the installation endpoint answers 401 to a seat).",
    "options": [
    "A: the maintainer confirms or adds objectui (and later cloud and hotcrm) in the App's repository access before the consumer's first transfer.",
    "B: let the first live transfer fail closed (a failed mint, or the remedy printed) and fix it then."
    ],
    "recommendation": "A. The consumer is waiting and one settings check removes a failed round. B still costs nothing irreversible, because the door fails closed and never rebuilds."
    },
    {
    "question": "CI does not run check:pm-issue-transfer: lint.yml was outside the claimed file surface, and check:self-test-wired does not require it because no workflow runs the door.",
    "options": [
    "A: add a lint.yml step beside issue-create's. That is a new CI step and needs the maintainer to name it.",
    "B: leave it seat-run only."
    ],
    "recommendation": "B, per the four-axis framework's new-gate-defaults-to-no, since no maintainer has named it. The cost is that the door's 46 cases run only when someone runs them; the relay halves it depends on stay CI-run."
    },
    {
    "question": ".claude/settings.json has no allow rule for node scripts/pm/issue-transfer.mjs, so the consumer seat will meet a permission prompt or the classifier.",
    "options": [
    "A: the maintainer adds allow rules mirroring label-write (plain and --use-env-proxy spellings).",
    "B: leave it; seats approve per call."
    ],
    "recommendation": "A, the maintainer's file. The door writes only through the relay's closed op table and sender gate."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed. rest-channel.md line 11 says four write tools share the transport selector; there are now five doors. This is an incomplete member list, not class a.",
    "carrier: 承接者:无 · noted, not filed. The relay's static PERMISSIONS include contents write for the two auto-merge rows, so a transfer stroke's two-repository token carries it on the target as well. No row spends it; per-stroke permission narrowing would be a hardening, not a defect.",
    "carrier: 承接者:无 · noted, not filed. GOVERNED_REPOS lacks www.objectos.ai, which cross-repo-coordination.md line 8 lists as a single-lane repo, so the door refuses it as a target and the rebuild recipe covers it."
    ],
    "worktree_cleanup": "Done: node_modules removed, then git worktree remove without --force exited 0. Branch head 425752b equals the remote."
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — domain:skills seat 1 (session_01A22sUB3mUWs6M36VgfijBq, seat post #7623), 2026-09-23T16:49Z — PR #19902 (head 425752b205, draft, base main, first line Fixes objectstack-ai/objectstack#19884), report 5798842876.

    Checklist (read on GitHub): draft on main; Fixes names this card only; Clause-②: no at line start; ## 维护者速读(草稿) present; session-URL footer; 8 changed files (+1090 / −56) inside the claimed surface (relay op table / validator / executor, the workflow's mint line, the new door and its package.json row and write-pace wiring, one governed reference line); skip-changeset correct; check-governed-merges.mjs --pr 19902: Tier S (one .claude/** path), 1146 lines under the human-merge limb — the ## Contract review record is rendered in-seat at tier on the PR thread (PASS); mcp_calls 0; api_writes 3 through the relay. Check-runs on the head at the time of this comment: 26 success · 5 skipped · Test Core (1/6) and Lint & Repo Gates in progress — ready + auto-merge are armed only once every run reads success or a rostered skip.

    Spot-check readings of the seat's own: the full diff read from the branch; the self-tests re-run in a review worktree at 425752b — issue-transfer 46 / 7, fleet-write/validate 94 / 9, fleet-write/execute 59 / 10, write-pace 113 / 12, all exit 0; --dry-run on objectstack-ai/objectui#10432 → one transfer action to objectstack-ai/objectui, nothing sent. The token widening is exactly the card's (issues write on source + target for a transfer stroke alone), fenced by a closed target roster, transfer-only strokes to one target, the sender gate on both repositories and the workflow line pinned by the validator's self-test. Executes the session operator's instruction on the card 「fleet 中继不支持 GitHub 的 issue transfer, 应该立 skills 卡解决」.

    Deviations accepted: base dabf8d7 (main moved before the cut; nothing on the surface); no create_labels_if_missing key (labels the target lacks are dropped — GitHub's default, and creating repo:* / domain:* labels in objectui would be noise); the validator's GitHub output renamed to repositories with its one consumer in the same commit; the long check:pm-dispatch-gates battery run detached with a recorded PID; two Build Core dist gates NOT MEASURED locally (CI's). Not measured by construction: the live transfer — the relay runs main's workflow; the consumer session (session_01X7HwfPLpQtCixDMrRGkSbe) takes the first live use on one of objectstack-ai/objectui#10432 / objectstack-ai/objectui#10439 / objectstack-ai/objectui#10438 / objectstack-ai/objectui#10437.

    Open questions, dispositioned per the dev's recommendations: assignees on transfer — accept the platform's semantics; the App installation's coverage of objectui / cloud / hotcrm, the .claude/settings.json allow rule for node scripts/pm/issue-transfer.mjs, and whether CI runs check:pm-issue-transfer — all three are the maintainer's (round report), none blocks the landing. Out-of-scope notes (three, 承接者:无) stay on the PR's Acceptance notes; the contents: write ride-along on a transfer token is flagged to the maintainer as a hardening.

    Landing plan: every check green ⇒ check-expected-skips on the head ⇒ ready → auto-merge SQUASH through the CCR route, queue landing; at MERGED, Fixes closes this card and the seat strips pm:dispatched and the assignee in the same act; the review worktree is removed at that landing.


    Generated by Claude Code

  5. added a commit that references this issue on Sep 28, 2026
    3f3c0f7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:skillsDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions