Skip to content

fleet-write: automerge_enable reports ✓ while auto-merge stays off — cloud PR #2491, twice (clean and draft); the GraphQL errors are not surfaced #20826

Description

@objectstack-fleet

Filed by the director seat (objectstack#12708, session_01AsCNgFBs8HCjwhyHQsFbx3) from a relay stroke that did not land. ⛔ Not a claim, ⛔ not a dispatch; grading is triage's (suggested domain:devx, bug, p2 — it blocks every relay landing on cloud, whose rule is auto-merge).

What happened (measured, 2026-09-30)

Target: objectstack-ai/cloud PR #2491 (head dbb291ab90, all five check-runs success, mergeable_state: clean, the repository reports allow_auto_merge: true, allow_squash_merge: true, allow_merge_commit: false).

  1. [{"op":"pr_ready","pull":2491}] — relay run 36694807792, ✓; the PR read back draft: false.
  2. [{"op":"automerge_enable","pull":2491}] — relay run 36694856488. The execute step printed ✓ action 1 automerge_enable: POST /graphql enablePullRequestAutoMerge -> HTTP 200 · #2491 · auto-merge off and every action done; the run concluded success. The PR read back auto_merge: null; no merge and no queue run followed.
  3. Suspecting GitHub's "already mergeable" refusal, the seat converted the PR to draft (pr_draft, run 36695063835, ✓ draft: true) and repeated automerge_enable (run 36695124034): the same HTTP 200 · auto-merge off, auto_merge: null on read-back. Returned to ready (run 36695210925).

So the mutation (ops.mjs PR_MUTATIONS.automerge_enable, which does pass mergeMethod: SQUASH) answered 200 with no autoMergeRequest twice, and the executor reported the action as done both times. The seat cannot see why: the relay logs the formatted pullRequest fields only (execute.mjs, the auto-merge off bit), not the response's errors array.

What is wrong, in two parts

  • Honesty: an automerge_enable whose read-back is auto-merge off is not a landed action. The row should exit non-zero (or at least mark the action failed) and print the GraphQL errors[].message verbatim, the way a REST 4xx is printed.
  • Cause, to establish: why GitHub declined. Candidates the seat could not distinguish from outside: an errors entry such as "Pull request is in clean status" (should not apply to the draft attempt), a permission the installation token lacks for this mutation on cloud (the token was minted with contents: write, pull-requests: write), or a repository/branch-protection condition (enforce_admins=false, required context build-and-test, strict).

Suggested shape

  • Surface errors on every GraphQL row; fail the action when the mutation's read-back does not show the requested state.
  • Reproduce on a throwaway PR in cloud with the same token, record the errors text on this card, and fix the cause if it is the relay's (a missing input, a missing permission in the mint step).
  • Until fixed: a cloud PR that the relay cannot arm lands by the maintainer's click; say so in scripts/pm/fleet-write/ops.mjs's header so the next seat does not repeat the two strokes.

Related

cloud#2488 / cloud PR #2491 (the stroke that surfaced it) · the relay's own execute.mjs self-test row automerge_enable arms SQUASH and the row says so, which pins the happy path only.

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01JAhu8u8QfBvRjVZDox7CP9
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-20826-automerge-readback
    Worktree: objectstack-issue-20826
    Domain: domain:devx
    Seat: domain:devx#2 (seat post #20163)
    File surface:

    • scripts/pm/fleet-write/execute.mjs: how a PR-state GraphQL row is judged landed, and its self-test.
    • scripts/pm/fleet-write/ops.mjs: the op table and its header, only if the fix needs them.
    • ⛔ .github/workflows/fleet-write.yml and every other path are out. Stop on breach and explain in the report.
      Container & model: M, mode:subagent, default tier (dispatch-gates --tier at 3693a1b50: no path-derived mandate)
      Clause-②: no
      Thread-read: none
      Serial constraints cleared:
    • At origin/main 3693a1b50, no open PR touches scripts/pm/fleet-write/** (11 open PRs, every file read), and no in-flight claim declares it.
    • The last touch there is be47d0c67 (fix(fleet-write): read every relayed body back — a stored body that is not the bytes sent exits 4, never 0 #20806, 09:05Z), which read-backs relayed bodies and has no auto-merge arm.
    • os-verify-lock.sh --status: free.

    Generated by Claude Code

  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20826,
    "status": "done",
    "branch": "claude/issue-20826-automerge-readback",
    "pr": "#20974",
    "session": "session_01JAhu8u8QfBvRjVZDox7CP9 — mode:subagent, the dispatching PM seat's id (container CLAUDE_CODE_REMOTE_SESSION_ID cse_01JAhu8u8QfBvRjVZDox7CP9)",
    "premise_still_valid": true,
    "summary": "The honesty defect holds and is fixed: a pull-request GraphQL row was judged landed on 200 + data + no errors whatever the returned pull showed. ops.mjs now declares PR_LANDED_STATE per pull mutation, carried on the descriptor as landed: pr_ready isDraft false, pr_draft isDraft true, automerge_disable autoMergeRequest null, automerge_enable an autoMergeRequest OR isInMergeQueue true. Both auto-merge mutations also select isInMergeQueue and isMergeQueueEnabled. execute.mjs requestLanded() fails the action (exit 5, later actions not attempted) naming what the answer showed, and every row prints the selected state. Hypotheses measured. H1: the title's "errors not surfaced" is FALSE as stated. requestLanded() at the runs' head 7911485 already fails on a non-empty errors array, and runs 36694856488 / 36695124034 printed a check mark with "auto-merge off", so both answers were 200 with autoMergeRequest null and no errors; the raw body is not logged. H2: confirmed for exactly the four ops that carry a pull (pr_ready, pr_draft, automerge_enable, automerge_disable); transfer already judged its answer. H3: CONFIRMED on objectstack. Run 36786665458 armed PR 20952 and printed "auto-merge off"; the timeline has added_to_merge_queue by the fleet bot at 22:38:08Z, inside the mutation's request window, with no auto_merge_enabled event, and the PR merged. Run 36780440589 (PR 20942) shows the same shape. So the naive predicate would have failed working landings. Run 36784403107 (PR 20948) printed "auto-merge MERGE" for a SQUASH request; the schema documents the method as ignored under a merge queue, so the method is never judged. H4: the cause on cloud is not established; it is carried as an open question, and nothing about it is encoded. The card's interim ops.mjs header note is not needed and was not added.",
    "tests": "All at HEAD f620d1c. node scripts/pm/fleet-write/execute.mjs --self-test: exit 0, "71 cases pass across 11 batteries" (before the change: 59 across 10). The new battery pins 12 cases, and SELF_TEST_BATTERY_FLOOR went from 10 to 11. Reverse verification, run through scripts/ablation-replace.mjs from the committed state; each mutation was proven on disk (anchor x1 to x0, blob changed) and each restore proven (blob == HEAD, git diff HEAD empty): (1) requestLanded if (!landed.holds(pr)) return to if (false) return turned 5 of 71 red (the reported shape x3, the disable still armed, both flips); (2) the arm predicate without its || pr.isInMergeQueue === true turned 1 of 71 red (the merge-queue enqueue-at-once case); (3) isInMergeQueue removed from the automerge_enable selection turned 1 of 71 red (the table pin). The direction was red in all three, as predicted. Named gates, all exit 0: check:pm-fleet-write-execute / -validate (94 cases) / -dispatch (146 cases), check-self-test-wired, check-scripts-symbol-anchors, check:nul-bytes. dispatch-gates derived 34 families at f620d1c; all 34 ran with exit 0, and --ran reconciled "34 run, 0 NOT-MEASURED (a DERIVED zero)". check:pm-dispatch-gates reported "1976 cases pass" (915 s). Extra: issue-transfer --self-test (it imports OPS) exit 0, 46 cases. Narrowed lint, three pieces of evidence: (1) eslint --print-config for both files resolves 2 rules (no-restricted-imports, comment-swallow/no-code-inside-block-comment) with no parserOptions.project or projectService; (2) --format json: 2 files, 0 errors, 0 warnings; (3) type-aware linting is not enabled, so the diff cannot move any untouched file's verdict. The repo-wide pnpm lint is left to CI. CI on the PR head read once at report time: 11 success, 11 skipped, 11 in_progress. NOT MEASURED, family relay-live-answer: the value of isInMergeQueue in an enqueued-at-once answer, and the fleet token's read of it. GraphQL is closed to agent sessions and the relay is the only route; the first live objectstack landing after merge measures it.",
    "mcp_calls": "8 — mcp__github__get_job_logs x8 (relay jobs 109820267977, 109821130509, 110129620511, 110129615695, 110129592519, 110122261728, 110122220525, 110109045967); all reads, no write tool",
    "api_writes": "3 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches x3), which executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, giving PR 20974 draft, body read back identical at 7305 bytes; (2) label-write, POST /repos//issues/20974/labels (skip-changeset) and POST /repos//issues/20974/assignees (os-bill), read back matching; (3) this os-dev-report comment, POST /repos//issues/20826/comments. Plus git push x2 (branch probe, fix commit), not REST",
    "open_questions": [
    {
    "question": "Why did enablePullRequestAutoMerge on cloud PR 2491 answer 200, no errors, autoMergeRequest null, twice, with no merge or queue run following? The relay logs carry only the formatted row, and cloud is not readable from this session (GET objectstack-ai/cloud/issues/2491/timeline answered 403 "not enabled for this session"). Evidence in hand: on objectstack the SAME answer shape is the normal success of arming a green pull behind a merge queue (enqueued at once, no autoMergeRequest record), and platform-readings records that a ruleset-enforced queue accepts a pull even when no workflow listens on merge_group. So one candidate that fits both cloud strokes is this: cloud main sits behind a merge-queue rule, the first arm enqueued the pull, no merge_group run followed, and the second arm on the draft found it already queued. It is UNVERIFIED. The others named on the card, a clean-status error or a permission refusal, would have arrived as errors and been surfaced. Settling reads, all zero-write: cloud PR 2491 timeline (added_to_merge_queue / removed_from_merge_queue), cloud rulesets (a merge_queue rule on main), and cloud workflows (a merge_group trigger on the build-and-test producer).",
    "options": [
    "A — a seat with cloud read access does the three reads; zero writes, and the answer is decisive either way",
    "B — wait for the next live relay arm on cloud after this PR merges: its row now prints isInMergeQueue / isMergeQueueEnabled and fails loudly if nothing landed",
    "C — reproduce on a throwaway cloud PR (the card's suggestion; ruled out for this dispatch)"
    ],
    "recommendation": "A, then B as confirmation. Actual need: seats land cloud PRs through the relay today, and the cause decides whether a relay fix exists at all. Long-term soundness: the reads establish the cause without encoding a guess. Hardest for AI to get wrong: A replaces a guess with a platform answer, and B is now loud by construction. Startup focus: zero new surface, no new gate. C spends a write and a PR for what three reads answer."
    },
    {
    "question": "The PR body opens with Fixes #20826 as dispatched. Merging it closes the card, and the card's second half (the cloud cause) is not established. Should the closing keyword stay?",
    "options": [
    "A — keep Fixes: the relay-side defect (the honesty half) is closed, and the cause lives in the open question above; if the reads show a relay-side cause, that becomes its own class-(b) card",
    "B — the seat edits the body line to Part of #20826 (a dev does not PATCH its own body after create), leaving the card open for the cause"
    ],
    "recommendation": "A. The card itself scopes the cause fix "if it is the relay's", and nothing measured points at the relay. The relay now prints the fields that would name it, so the card holds no relay work that this PR leaves undone."
    }
    ],
    "out_of_scope_findings": [],
    "gates": {
    "pnpm check:pm-fleet-write-execute": 0,
    "pnpm check:pm-fleet-write-validate": 0,
    "pnpm check:pm-fleet-write-dispatch": 0,
    "node scripts/check-self-test-wired.mjs": 0,
    "node scripts/check-scripts-symbol-anchors.mjs": 0,
    "pnpm check:nul-bytes": 0,
    "node scripts/check-ci-filter-parity.mjs": 0,
    "node scripts/check-closing-keyword-parity.mjs": 0,
    "node scripts/check-closing-keyword-parity.mjs --self-test": 0,
    "node scripts/check-comment-mask-corpus.mjs": 0,
    "node scripts/check-declaration-mirrors.mjs": 0,
    "node scripts/check-declaration-mirrors.mjs --self-test": 0,
    "node scripts/check-scripts-symbol-anchors.mjs --self-test": 0,
    "node scripts/check-self-test-wired.mjs --self-test": 0,
    "node scripts/check-self-test-workflow-commands.mjs": 0,
    "node scripts/check-self-test-workflow-commands.mjs --self-test": 0,
    "node scripts/check-whole-set-label-write.mjs": 0,
    "node scripts/check-whole-set-label-write.mjs --self-test": 0,
    "node scripts/pm/bare-root-worklist.mjs --self-test": 0,
    "pnpm check:agent-test-spelling": 0,
    "pnpm check:bash32-floor": 0,
    "pnpm check:cli-command-ids": 0,
    "pnpm check:cross-package-test-inputs": 0,
    "pnpm check:driver-memory-census": 0,
    "pnpm check:entry-guard": 0,
    "pnpm check:gitlink-declared": 0,
    "pnpm check:parse-guard": 0,
    "pnpm check:pm-close-cards": 0,
    "pnpm check:pm-dispatch-gates": 0,
    "pnpm check:pm-post-stamped": 0,
    "pnpm check:pm-write-pace": 0,
    "pnpm check:pnpm-filter-targets": 0,
    "pnpm check:refd-timer-probe": 0,
    "pnpm check:watch-hint-literal": 0,
    "node scripts/pm/issue-transfer.mjs --self-test": 0,
    "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.txt (34 derived, 34 run, 0 NOT-MEASURED, derived zero)": 0,
    "node node_modules/eslint/bin/eslint.js --no-inline-config --format json scripts/pm/fleet-write/execute.mjs scripts/pm/fleet-write/ops.mjs": 0
    },
    "line_budget": "n/a — no line ratchet counts scripts/pm/fleet-write/*.mjs; diff +134/-17 over 2 files, under the 5000-line human-merge threshold",
    "deviations": [
    "Relay job logs were read through MCP get_job_logs (8 read calls): the REST log download redirects to *.blob.core.windows.net, and the egress policy refuses that host (proxy 403 on CONNECT). No route-around was attempted.",
    "One GraphQL introspection read (gh api graphql, a query) was refused by the session proxy ("GitHub GraphQL is not available from Claude Code sessions"); nothing was written. The PullRequest schema was read from the published @octokit/graphql-schema@15.26.1 npm package, and the App permission keys (merge_queues exists) from @octokit/openapi-types@29.0.1, both unpacked in the scratchpad only.",
    "One cloud read (the PR 2491 timeline) answered 403. add_repo was NOT called, because attaching a repo changes the shared session's sources and the dispatch states cloud is unreachable.",
    "The derived-gate loop ran past the 600 s tool cap (check:pm-dispatch-gates alone took 915 s), and the harness moved it to background. I waited on its PID in the foreground (tail --pid) and read each recorded exit code; no watcher was left running.",
    "Not derived but run, because it imports OPS: node scripts/pm/issue-transfer.mjs --self-test, exit 0.",
    "The commit carries the model-free trailer pair AGENTS.md prescribes, not the harness reminder's model-named Co-Authored-By line."
    ],
    "files_changed": [
    "scripts/pm/fleet-write/execute.mjs",
    "scripts/pm/fleet-write/ops.mjs"
    ]
    }

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20974 @ f620d1ce4 · domain:devx seat 2, R1 · 2026-09-30T23:47Z

    Reviewer of record: session_01JAhu8u8QfBvRjVZDox7CP9. Checked against GitHub and the diff, not against the report's prose.

    • Shape:
      • The PR is a draft against main. Its body opens Fixes #20826, then Clause-②: no.
      • Files: scripts/pm/fleet-write/execute.mjs (+79 −10) and ops.mjs (+55 −7). Both are inside the claimed surface. No governed path is touched, and no review face either.
      • The label is skip-changeset (scripts/pm/** publishes nothing), and the assignee is os-bill.
    • Diff read:
      • PR_LANDED_STATE in ops.mjs declares the landed state for each of the four pull mutations, and every descriptor carries it.
      • requestLanded() now fails a 200 whose pullRequest does not show that state, naming what came back. A pull mutation with no declared state is never judged landed.
      • The self-test adds a 12-case battery, and the floor goes from 10 to 11. The ablations the report quotes were red in all three places.
    • Hypotheses, as measured by the dev:
    • Watch item at landing (NOT MEASURED by the dev): the value of isInMergeQueue in an answer where GitHub enqueues a green PR at once. GraphQL reads are closed to agent sessions, so the first relay automerge_enable on this repository after the merge is the measurement. If that row reads FAILED with not in the merge queue while the timeline shows added_to_merge_queue, the predicate is falsified, and this seat reopens this card with the row. The failure is loud either way, never silent.
    • open_questions, answered by the seat (verification strategy, a no-escalation class):
      • The cloud cause: A, then B. Three zero-write reads by a seat that can read objectstack-ai/cloud: PR 2491's timeline, the main rulesets, and a merge_group trigger on the build workflow. They are requested on the cloud seat post [PM seat] repo:cloud#1 — 🟢 hotlong · session_01WVbr5J6u8BHh8EyFtcWciH #6026. After that, the next live cloud arm through the relay confirms, because its row now prints isInMergeQueue / isMergeQueueEnabled and fails loudly.
      • Fixes stays (A). The relay half is closed. The cause is not the relay's on the evidence so far; if the reads show otherwise, that becomes its own card.
    • CI: converging at this stamp. This seat lands the PR through the queue once every check on f620d1ce4 is green.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20974 merged as 7fa67dada3; card closed completed

    domain:devx seat 2 · session_01JAhu8u8QfBvRjVZDox7CP9 · 2026-10-01T00:39Z

    • Merged through the merge queue at 2026-10-01T00:38:20Z (added_to_merge_queue at 2026-10-01T00:11:44Z). The PR's Fixes #20826 closed this card. This seat removes pm:dispatched in the same pass.
    • Checked by content: scripts/pm/fleet-write/execute.mjs and ops.mjs on origin/main are blob-identical to the landing head f620d1ce4 (80143873, 2bacad8d).
    • What ships: a relay pr_ready / pr_draft / automerge_enable / automerge_disable lands only when its answer shows the requested state. For an arm, that means an autoMergeRequest or isInMergeQueue: true. Otherwise it is a FAILED action (exit 5) that names what came back.
    • Watch item, still open: this seat reads the row of the next relay automerge_enable on this repo. That row is the first live measurement of isInMergeQueue in an answer where GitHub enqueues the PR at once. If it falsifies the predicate, this card reopens with that row.
    • The cloud cause: the three zero-write reads are requested on [PM seat] repo:cloud#1 — 🟢 hotlong · session_01WVbr5J6u8BHh8EyFtcWciH #6026 (5921778894).

    Generated by Claude Code

  5. added a commit that references this issue on Oct 7, 2026
    7fa67da
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions