Skip to content

fix(scripts): check:single-claim-paths keeps its own two contracts — a failed board read exits PREREQUISITE NOT MET, and the self-test handshake is set by the verdict it certifies - #18986

Merged
os-elon-musk merged 2 commits into
mainfrom
claude/issue-18940-single-claim-paths-contracts
Sep 18, 2026
Merged

os-elon-musk merged 2 commits into
mainfrom
claude/issue-18940-single-claim-paths-contracts

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #18940

Clause-②: no

Two defects in one file, both of the same class — scripts/check-single-claim-paths.mjs
against the contracts it declares about itself. One commit each, so each is readable alone.
No other file is edited.

A. A transport failure exited 1, and this file defines 1 as the accusation

The live path read a PR's file list with nothing catching a throw. githubApi throws on any
non-ok response and fetch throws on a dead socket, so a 404, a dead credential or a dropped
connection escaped as an unhandled rejection and node exited 1 — which this file's own exit
register defines as "judged, an earlier open PR already claims a listed path". An accusation,
about a board the run never read, against an author who did nothing.

The file already forbade exactly that, in its own header: "a mis-wired gate must not read as an
accusation, because it would be red on every PR at once for something no author did." So this is
the file keeping its own rule, not a new policy.

The probe, run in an agent container. The card carried it unrun; this is the first run of it.

Before, at the base commit 26c73fb4e:

$ PR_NUMBER=99999999 GITHUB_REPOSITORY=objectstack-ai/objectstack node scripts/check-single-claim-paths.mjs
info  re-exec with --use-env-proxy: HTTPS_PROXY is set and node's fetch does not read it.
file:///home/user/objectstack-issue-18940/scripts/check-single-claim-paths.mjs:543
  if (!response.ok) throw new Error(`GitHub API ${response.status} for ${path}`);
                          ^
Error: GitHub API 404 for /repos/objectstack-ai/objectstack/pulls/99999999/files?per_page=100&page=1
    at async collect (.../check-single-claim-paths.mjs:502:16)
    at async .../check-single-claim-paths.mjs:901:66
Node.js v22.22.2
exit=1            == EXIT_CONFLICT: "an earlier open PR already claims a listed path"

After:

$ PR_NUMBER=99999999 GITHUB_REPOSITORY=objectstack-ai/objectstack node scripts/check-single-claim-paths.mjs
info  re-exec with --use-env-proxy: HTTPS_PROXY is set and node's fetch does not read it.
X check:single-claim-paths: PREREQUISITE NOT MET - the board was not read - GitHub API 404 for
  /repos/objectstack-ai/objectstack/pulls/99999999/files?per_page=100&page=1 - NOT a verdict,
  not a clean tree.

  Nothing below this line is a reading. This run never learned which paths any pull request
  claims, so it says nothing about whether one is claimed twice, and it accuses no author of
  anything. Exit 3 rather than 1: that code means an EARLIER open PR already claims a listed
  path, and a failed read is not evidence of any such PR. Exit 3 rather than 0 too - a board
  that could not be read is not a clean board.

  Usual causes, in the order worth checking: the token cannot read this repository, the PR
  number resolves to no pull request, or the request never left the container [...]
exit=3            == EXIT_PREREQUISITE_NOT_MET

Live reverse control, same tree, a PR number that really resolves — unchanged:

$ PR_NUMBER=18935 GITHUB_REPOSITORY=objectstack-ai/objectstack node scripts/check-single-claim-paths.mjs
OK check:single-claim-paths: PR #18935 modifies none of the 1 declared at-most-one-writer path(s),
   so there is nothing to serialise.
exit=0

Shape of the change:

  • The read only is wrapped. judge stays outside the try on purpose: it is pure and cannot
    throw on a transport, and a catch spanning it would relabel a real crash in the verdict layer
    as "the board was not read" — the same lie in the other direction.
  • The failure becomes a refusal through a pure handler, boardNotReadRefusal(error), returning
    { exit, lines } exactly as judge does. That is what lets the self-test drive the arm with
    no process exit and no network. The single process.exit stays at the dispatch, where every
    other verdict's exit already lives.
  • The code is EXIT_PREREQUISITE_NOT_MET, imported from the fleet's shared register, the way
    scripts/check-issue-citations.mjs and scripts/pm/post-stamped.mjs already spell it, so the
    wording family is one phrase across the tree: "PREREQUISITE NOT MET — the board was not read".
  • It stays non-zero. A board that could not be read is not a clean board, so the CI consumer
    (.github/workflows/single-claim-path-guard.yml) is still red on it. That is the right answer,
    and 0 would have been the anti-pattern the header names.
  • The header's exit register gains code 3 plus the paragraph saying why 1 could not be left to
    node's unhandled-rejection status.

The CI consumer needs no change, measured. Its only comment about an exit code, at
single-claim-path-guard.yml:94-96, names 2 for one specific condition (PR_NUMBER absent) and
does not enumerate the register, so there is nothing there to extend. The run: line is
unchanged and any non-zero exit is a red job.

B. The self-test handshake was set before the block that produces the verdict

selfTestReachedVerdict = true sat on the line directly above return (async () => { — the
block that runs every assertion, evaluates the battery floor and prints the verdict. The flag was
therefore true before a single case had run, so an early return or a throw anywhere inside that
block left it true and the dispatch's if (!selfTestReachedVerdict) branch was unreachable. The
one sentence the flag exists to make possible could never be said by this gate.

AGENTS.md, verbatim: "Set the flag as the self-test's last statement, after its success line
prints" and "SAY the self-test never reached its verdict. An exit code is not a handshake."

The assignment is now that last statement. The failing arm deliberately gets none: it calls
process.exit(1) on the next instruction, so no reader survives to consult the flag — the
handshake is for the SILENT ways out, which is exactly what the new position catches.

The negative control — red, then green, both legs on committed files

The instrument is scripts/ablation-replace.mjs, so the mutation and the restore are both proven
against the disk rather than against an exit code. The injected mutation is identical in both
legs: an early return; as the first statement inside the async verdict block.

Leg 1 — the fixed file (this branch's HEAD):

ablation-replace: anchor   x1 -> x0
ablation-replace: blob     8850395d7856f4299b96dd45b68916af6a0c9faa -> 8387e64acd8bacddc7a8b07b4cd991414edbe216
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 8850395d7856 -> 8387e64acd8b
ablation-replace: running: node scripts/check-single-claim-paths.mjs --self-test

X check-single-claim-paths self-test: selfTest() returned without reaching its verdict,
so no success line was printed. Exiting 0 here would report a self-test
that never finished as a self-test that passed.

ablation-replace: command exited 1
ablation-replace:   blob after restore  8850395d7856f4299b96dd45b68916af6a0c9faa
ablation-replace:   blob at HEAD        8850395d7856f4299b96dd45b68916af6a0c9faa
ablation-replace:   git diff HEAD       empty
ablation-replace: ok restored: blob == HEAD (8850395d7856) and `git diff HEAD` is empty

Leg 2 — the same injection on the pre-fix file (commit d4b1f2817, i.e. after A and before
B), restored into the tree with git restore --source=... --worktree so the index is untouched:

ablation-replace: anchor   x1 -> x0
ablation-replace: blob     2bedc9c313d74167c938a8c706adcfbae87a4b13 -> e5b5d1c042e6735f8e40eb874e3adcdca2f2429e
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 2bedc9c313d7 -> e5b5d1c042e6
ablation-replace: running: node scripts/check-single-claim-paths.mjs --self-test
ablation-replace: command exited 0
ablation-replace:   blob after restore  8850395d7856f4299b96dd45b68916af6a0c9faa
ablation-replace:   blob at HEAD        8850395d7856f4299b96dd45b68916af6a0c9faa
ablation-replace:   git diff HEAD       empty
ablation-replace: ok restored: blob == HEAD (8850395d7856) and `git diff HEAD` is empty

Leg 2 printed zero bytes from the self-test and exited 0. That is the defect, executed:
the runner had nothing to say, and "every case held" and "no case ran" were the same output. This
is why the triage asked for a RED negative control — without it, fixed and unfixed look identical
in the gate's own output on a healthy run.

git status --porcelain is empty after both legs, and the working blob equals the HEAD blob.

Self-test counts

batteries cases
base 26c73fb4e 8 74
this branch 9 93

SELF_TEST_BATTERY_FLOOR rises 8 to 9 with the roster, because leaving it at 8 would let the new
battery be deleted without a red — which is what the roster comment already says about deletion.
The measured base is 74 cases, not the 83 the dispatch carried; the 74 is also exactly the sum of
the eight declared floors, so the floors sat at the live counts before this change and still do.
The 19 new cases pin the code and the words, drive a throwing fake api through collect into the
same handler, keep a healthy api reaching a real verdict as the reverse control, and pin
structurally that the handler is reached after the LAST network read — the live one — so deleting
the live catch reds instead of being satisfied by the self-test's own calls to the handler.

No existing verdict moved: the CLEAN / CONFLICT / NOT WIRED pins are byte-identical and the three
older constants keep their values, asserted as a new case (the four codes are four distinct
values).

Census — who else carries this ordering bug

The card left this unmeasured and asked for it. Measured on this branch, and not fixed here.

Population: 105 files under scripts/ that both declare let selfTestReachedVerdict = false and
read it at the dispatch. Two independent mechanical criteria, which agree on the same 4
candidates: (1) a success line printed after the last flag assignment; (2) an assertion-registering
call (registerCase, batteryFloorFailures, checkSelfTestFloor) after the last flag assignment.
All 4 then read by hand, because the mechanical signal cannot tell "the verdict is decided at the
dispatch" apart from "assertions still run after the flag".

file flag line what follows the flag verdict
scripts/check-single-claim-paths.mjs 807 (pre-fix) the entire async block it returns, opened at 808: every assertion, the floor, the verdict print ORDERING BUG — fixed here
scripts/check-osv-exemptions.mjs 647 nothing inside selfTest(); the flag is its last statement. The floor call at 675 is at the DISPATCH, after the flag is read at 657 holds
scripts/typecheck-configs.mjs 350 same shape: flag last in selfTest(), floor at 377 in the dispatch, flag read at 357 holds
scripts/check-durability-degradation-log-level.mjs 5588 and 5592 nothing — set adjacent to each of its two verdict lines, green and red. The sinks at 6718 and 6851 belong to a SECOND self-test carrying its OWN flag, readSeamsReachedVerdict (5609) holds

The other 101 set the flag as the last statement of the function whose verdict it certifies.

So this is one card, not a class — which also answers the card's own question about whether
the B half should have been promoted to a census card. It should not.

Worth recording for the next reader: docs/audits/2026-09-self-test-shape-census.md scores this
very file HELD (its row, line 321). That is correct for the axis it measured — its probe
injects return; as the first statement of the dispatched function, which lands above the flag
assignment and therefore does leave the flag false. The ordering axis is a different injection
point, inside the block, and the published census is blind to it by construction. Neither reading
contradicts the other.

Changeset

skip-changeset, measured rather than assumed. Nothing published moves: 70 non-private packages
declare files[] and every one ships dist, README.md and CHANGELOG.md only — none ships
repo-root scripts/. The only reference to this script outside scripts/ is the private root
manifest's own check:single-claim-paths line. The positive control for the method is that same
scan finding 70 populated files[] arrays, so an empty result is a reading and not a silent miss.

Verification

  • pnpm check:single-claim-paths (the self-test) — pass, 93 cases, 9 batteries.
  • The 29 gate commands derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
    from this worktree, against the real change set (1 path, three-dot semantics vs merge base
    26c73fb4e). Every exit code captured redirect-then-$?, never through a pipe. All 29 exited 0, the slowest being
    pnpm check:pm-dispatch-gates at 745s (1848 self-test cases) under contention with a sibling
    worktree running the same gate. Reconciled with dispatch-gates.mjs --ran, each line recorded as
    command :: exit N: 29 derived, 29 run, 0 UNRUN, 0 NOT-MEASURED as a DERIVED zero rather than a
    claimed one, and none of the 29 exited 3.
  • Control-byte self-scan over the changed file (grep -naP over the C0 range plus DEL): no hits.
  • The derivation's own NOT MEASURED rows are left as it reports them, including the one for this
    very script's live invocation, whose argv takes a value only inside a CI run.

Reader test

A transport failure in CI reads PREREQUISITE NOT MET and a red job, never "an earlier PR claims
your path". A self-test that dies mid-block is reported as never reaching its verdict.

Acceptance notes

Observations from this file, noted and deliberately not filed and not fixed — none is a
reproducible defect, a declared-contract violation or a metadata-authoring trap:

  • The exit register's prose in the header is now four codes long and reads as a list; if a fifth
    ever arrives, the register would be better as a table. Style, no behaviour.
  • MAX_PAGES is 30 in both loops with one comment explaining the file-list ceiling; the open-PR
    loop borrows the same constant for a different endpoint's ceiling. Nothing observable follows
    from it today at this repo's open-PR count.

Generated by Claude Code

…o PREREQUISITE NOT MET instead of the accusation code

The live path read a PR's file list with nothing catching a throw, so any
transport failure — a 404 from a number that resolves to no pull request, a
dead credential, a dropped socket — exited on node's unhandled rejection
status, 1. This file defines 1 as "judged, an earlier open PR already claims
a listed path": an accusation about a board the run never read, against an
author who did nothing.

Its own header already forbade exactly that ("a mis-wired gate must not read
as an accusation, because it would be red on every PR at once for something
no author did"), so this is the file keeping its own rule.

The read — and only the read — is now wrapped. `judge` stays outside the try
on purpose: it is pure and cannot throw on a transport, and a catch spanning
it would relabel a real crash in the verdict layer as "the board was not
read". The failure is turned into a refusal by a pure handler that returns
`{ exit, lines }` exactly as `judge` does, so the self-test drives the arm
offline with no process exit and no network.

The code is EXIT_PREREQUISITE_NOT_MET, imported from the fleet's shared
register rather than numbered locally, the way check-issue-citations and
post-stamped already spell "the board was not read". It stays NON-ZERO: a
board that could not be read is not a clean board, so the CI consumer is
still red on it.

Probe, in an agent container, before and after
(PR_NUMBER=99999999 GITHUB_REPOSITORY=objectstack-ai/objectstack):
  before: unhandled rejection, stack trace, exit 1
  after:  "PREREQUISITE NOT MET — the board was not read — GitHub API 404
          for /repos/.../pulls/99999999/files... — not a verdict, not a
          clean tree", exit 3
Live reverse control, PR_NUMBER=18935: unchanged, clean verdict, exit 0.

Self-test: a new battery of 19 cases, roster floor raised 8 -> 9 batteries,
74 -> 93 cases. It pins the code and the words, drives a throwing fake api
through collect into the same handler, holds a healthy api reaching a real
verdict as the reverse control, and pins structurally that the handler is
reached after the LAST network read — the live one — so a deleted live catch
reds rather than leaving the pin satisfied by the self-test's own calls.

Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
… the verdict prints, not before the async block that produces it

`selfTestReachedVerdict = true` sat on the line directly above
`return (async () => {` — the block that runs every assertion, evaluates the
battery floor and prints the verdict. The flag was therefore true before a
single case had run, so an early return or a throw anywhere inside that block
left it true and the dispatch's `if (!selfTestReachedVerdict)` branch was
unreachable. The one sentence the flag exists to make possible — "selfTest()
returned without reaching its verdict" — could never be said by this gate.

AGENTS.md, verbatim: "Set the flag as the self-test's last statement, after
its success line prints" and "SAY the self-test never reached its verdict.
An exit code is not a handshake."

The assignment now is that last statement. The failing arm is left without
one deliberately: it calls process.exit(1) on the next instruction, so no
reader survives to consult the flag — the handshake is for the SILENT ways
out, which is what the new position catches.

Negative control (deliberate early return injected inside the block, on the
committed file, restored by blob hash): red, "selfTest() returned without
reaching its verdict", exit 1. Before this commit the identical injection
printed the same partial output and exited 0.

Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
@os-elon-musk os-elon-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 18, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 03310bc914f2f633f9d57c6b206bc0eae53b8b6d

① Derived judgments

  • Surface: scripts/check-single-claim-paths.mjs only (+160 / −5), two commits (A then B). Off the governed register; no published package moves; no contract's accept/reject set moves. No landing on the file since the claim; the proxy re-exec PR fix(scripts): route check:single-claim-paths through the session proxy so a seat can run it #18935 landed at 07:25Z is on the base and untouched.
  • A. The live board read is wrapped and any thrown read becomes boardNotReadRefusal(error) — a pure handler returning { exit: EXIT_PREREQUISITE_NOT_MET, lines } (the fleet's 3, imported from ./pm/check-half-states.mjs beside the proxy symbols), printed at the single dispatch site; judge stays outside the try. The text says 「the board was not read — — ⛔ not a verdict, not a clean tree」 and states why 3 rather than 1 (the accusation) and why 3 rather than 0; the header register gains the code. Seat probe on a scratch worktree at this head: PR_NUMBER=99999999 … ⇒ exit 3 with that line after the re-exec note (before, on main: an unhandled rejection, exit 1); the live control PR_NUMBER=18935 ⇒ exit 0 with the gate's clean verdict. The CI consumer needs no change (a non-zero exit is a red job; its only exit-code comment names 2 for one condition and enumerates nothing) — correct reading.
  • B. selfTestReachedVerdict = true moves from the line above return (async () => { to the last statement inside the block after the success line; the failing arm gets no assignment because it exits 1 on the next instruction. The negative control (an early return; injected as the first statement inside the block, through scripts/ablation-replace.mjs with the blob-hash restore): on the fixed head the runner prints 「returned without reaching its verdict」 and exits 1; on the pre-B commit the self-test printed zero bytes and exited 0 — the defect executed, then the fix shown to catch it. AGENTS.md :1039's contract now holds for this file.
  • Self-test: 74 cases / 8 batteries on the base (the dispatch quoted 83 / 8 — a number the seat relayed from the superseded PR fix(scripts): re-exec check:single-claim-paths through the session proxy so its verdict is readable outside CI #18945's head rather than measuring on main; the seat's correction) → 93 / 9 on this head, floor raised with the roster; the new battery pins the handler's exit, its text, and the integration shape (a throwing api escaping collect).
  • The census the card named, measured by the dev and read by the seat: 105 files carry the three-piece; two mechanical criteria and a hand read find ONE file with the ordering bug — this one — so B is not a class and no census card is owed. Correct disposition.
  • Seat spot-check: check-self-test-wired, check-scripts-symbol-anchors, check:entry-guard, check:parse-guard exit 0 on the head.

② Semver level

  • skip-changeset is correct: root scripts/ ships in no package's files[] (the dev's 70-package scan with a positive control). Clause-②: no on the claim (5727821701) and in the body; PM_SWEEP_REPO=objectstack-ai/objectstack node scripts/pm/check-clause2-carriers.mjs --pair 18986 at 2026-09-18T09:49Z: exit 0, one live claim, no widening tell.

③ Boundary flags

  • Dev flags: no open questions; one to file the dev left to the seat's judgment — scripts/measure-self-test-floor.mjs's probe injects return; as the FIRST statement of the dispatched function, which for this file sat ABOVE the flag assignment, so the 2026-09 self-test shape census scored this file HELD while its dispatch diagnostic was unreachable for every early exit inside the async block. The seat judges it class (a): a reproducible false clearance in an instrument AGENTS.md sends authors to; filed by the seat (number in the ACCEPT). Three noted, not filed (the register as prose; the shared MAX_PAGES; the live judgment running in the advisory workflow, already in the gate census's report-only worklist) — agreed. Deviation declared: the floor count disagreement, above. Gates: 29 derived / 29 run / 0 NOT-MEASURED at this head; check:pm-dispatch-gates 745 s detached with its exit captured. 0 MCP calls; 3 REST writes.
  • CI on this head at 2026-09-18T09:49Z: 16 success · 11 skipped · 6 in_progress — nothing red; the landing act reads it again. ⚠️ The landing channel is closed (this session's auto-mode classifier, record 5726459594 on PR fix(pm): a size refusal is a refusal — post-stamped stops reporting a real 422 as a missing route #18934) — this PR joins the four waiting for it.

Implemented-by: claude/issue-18940-single-claim-paths-contracts
Reviewed-by: session_01BTeBejoPUvRHN8WdAJC6oF

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Landing record — every pre-check met, blocked at the channel (skills seat, session_01BTeBejoPUvRHN8WdAJC6oF) · 2026-09-18T10:12Z

Every check on 03310bc914 completed with none red (35 runs: 24 success, 11 rostered skips; Lint & Repo Gates completed at 2026-09-18T10:10Z, read at 2026-09-18T10:11Z); the review of record is 5728290255 (PASS); --pair 18986 exit 0; ACCEPT 5728294955 on #18940, with its correction 5728309827 (the filed card is #18987). One file, scripts/check-single-claim-paths.mjs, and no other open draft on it. The landing act (POST …/pulls/18986/ccr/ready_for_review then PUT …/pulls/18986/ccr/auto_merge {"merge_method":"SQUASH"}) is not attempted here: the same act was refused by this session's auto-mode classifier on PR #18934 at 2026-09-18T07:03Z (record 5726459594 there) and a classifier refusal is 「no channel」 per references/rest-channel.md. The PR stays draft, ACCEPTED, and lands the moment a channel exists — this seat's once the maintainer allows the two CCR calls, or the maintainer's own hand. ⛔ Nothing here is a finding against the PR.


Generated by Claude Code

os-elon-musk pushed a commit that referenced this pull request Sep 18, 2026
…o a flag above the returned block is no longer a HELD

The probe injected `return;` as the FIRST statement of the dispatched function
and read one answer from it. A handshake set ABOVE the block that function
RETURNS stays unset under that injection, the dispatch refuses, and the row
read HELD -- while the same `return;` one statement lower, inside the block,
leaves the handshake SET and the run silent at exit 0. The 2026-09 census
cleared `scripts/check-single-claim-paths.mjs` HELD exactly that way, on the
revision that carried that ordering defect.

A file whose dispatched function returns a block is now read at both points and
HELD requires both. `HANDSHAKE-ABOVE-BLOCK` is the new verdict for "point 1
refused, point 2 exited 0", counted apart from HELD as ACCIDENT already is.

Measured on this base (223 rows, 217 with a resolvable entry): ONE row carries a
second point -- `check-single-claim-paths.mjs` -- so no other row's reading
moves, and every other row publishes `points: 1` with the reason it has one.

  before  --probe --only scripts/check-single-claim-paths.mjs -> 0 DEFEATED, 1 HELD, exit 0
  after   same command -> 1 HANDSHAKE-ABOVE-BLOCK, exit 0; point 1 refused,
          point 2 exited 0 printing 0 bytes
  control PR #18986's head (03310bc, the same file with the handshake moved
          inside the block) -> HELD at BOTH points, both baselines exit 0

The anchor is the returned `async () => {` IIFE at the dispatched function's own
statement level: masked, line-anchored and at depth 0. Deliberately unadmitted,
each with its reason in the docblock: an `await (async () => {` in the middle of
the body (an early return there skips a battery while the self-test still
reaches its verdict -- hole 1, never this one), a returned `.then()` callback
and an `async function` expression (no carrier in this tree). The header also
records what the point CANNOT read: work whose failure is silent, measured on
`check-system-context-census.mjs`, whose handshake sits 48 lines above its own
battery floor and where both runs print the same bytes.

Costs: the second pair of spawns runs only for a row that HELD at the first
point AND has a second point, so a red-baseline checkout costs what it did.

Controls, both directions: the pair of fixtures differing only in WHERE the
handshake line sits (above -> HANDSHAKE-ABOVE-BLOCK, inside -> HELD at both);
five anchor decoys ahead of the real block, one per rule; an awaited mid-body
block and a block-free self-test both yielding no second point; a one-point row
reading identically through the composer and through the probe; and a row
DEFEATED at the first point probed exactly once.

Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Landing attempt on the maintainer's word — refused a fourth time, with the allow rule on disk (skills seat, session_01BTeBejoPUvRHN8WdAJC6oF) · 2026-09-18T14:25Z

PR #19047 (.claude/settings.json +3 / −1: the two landing-call allow rules, Bash(curl -sS -X POST https://api.github.com/repos/objectstack-ai/objectstack/pulls/*/ccr/ready_for_review *) and the PUT …/ccr/auto_merge twin) merged at 2026-09-18T14:20Z (67624b78d, os-zhuang). The seat fast-forwarded its checkout to that commit (rules present at :51–:52 on disk), re-ran every pre-check on this PR at 2026-09-18T14:23Z — NOT governed, --pair 18986 exit 0, record 5728290255 on the head 03310bc914, CI 22 success · 9 rostered skips · 0 red, mergeable_state clean — and typed the landing act in the rules' literal-prefix shape: curl -sS -X POST https://api.github.com/repos/objectstack-ai/objectstack/pulls/18986/ccr/ready_for_review -H "Content-Type: application/json" -d '{}'. The session's auto-mode classifier refused it at 2026-09-18T14:24Z with the reason 「Merge Without Review」 — the same refusal as the three earlier ones today, which means no allow rule matched: this session's permission set was loaded when the session started and does not carry the two rules committed since. A classifier refusal is 「no channel」 (references/rest-channel.md :40); the seat does not re-issue the act by another route.

⇒ The rules are right and on main; what is missing is a session that loaded them. Either a fresh skills-seat session (its first act: this exact command on this PR) lands the eight ACCEPTED PRs, or the approver's hand does. Nothing here is a finding against the PR; it stays draft, ACCEPTED, every pre-check met.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Landing record — landed on the seat's record through the CCR route (skills seat, session_01BTeBejoPUvRHN8WdAJC6oF) · 2026-09-18T14:29Z

Channel: the two allow rules of PR #19047 (67624b78d, merged 2026-09-18T14:20Z) on disk, and the session's permission mode switched by the maintainer from auto to manual confirmation at 2026-09-18T14:25Z (the auto-mode classifier had refused the same act four times, the last at 14:24Z with the rules already on disk); the act typed in the rules' literal-prefix shape. Pre-checks re-run at 2026-09-18T14:23Z: NOT governed (scripts/check-single-claim-paths.mjs); --pair 18986 exit 0; the review of record is 5728290255 (PASS) on the head 03310bc914; CI on the head 22 success · 9 rostered skips · 0 red; mergeable_state clean; ACCEPT on #18940.

Acts: POST …/pulls/18986/ccr/ready_for_review HTTP 200 at 2026-09-18T14:26Z (timeline ready_for_review, draft: false read back); PUT …/pulls/18986/ccr/auto_merge {"merge_method":"SQUASH"} HTTP 200 at 2026-09-18T14:27Z (timeline auto_merge_enabled; the repository allows squash only — allow_squash_merge true, allow_merge_commit false — so the queue's merge is a squash whatever the response's merge_method field prints); the queue took it — timeline added_to_merge_queue at 2026-09-18T14:27Z. The queue rebuilds the PR onto the current main and re-runs the subscribing workflows; the merge is the queue's, ⛔ not this seat's click. Residue on the merge: #18940pm:done, unassigned.


Generated by Claude Code

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