pm: two report-only half-state rows — H45 (pm:epic with pm:queue) and H46 (an open PR closing a card with no claim) - #15690
Conversation
H45 flags an open card carrying both `pm:epic` and `pm:queue` — reserved by a named epic PM and offered to every seat at once. A pure label intersection over the `pm:queue` page the sweep already reads, H3's shape and H3's cost. H46 flags an OPEN pull request (drafts included) whose body binds a closing keyword to an open card that either still carries `pm:queue`, or has no comment that is a `Claim:` whose `Branch:` line names the PR's head branch. The keyword extraction is H7's and H21's `closingKeywordTargets`; the claim pair is `CLAIM_COMMENT_MARKER` + `claimedBranches`, the predicates `check-clause2-carriers.mjs` reads. Nothing is re-spelled. Both rows are report-only and join the `state` band. H46 is judged AFTER H44 so the threads it buys cannot widen H44's corpus and change what an existing row reports. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
…ess-pr-rows Brings the branch onto current main so the verified tree and the pushed tree are the same tree; no conflict and no content change on this branch's one file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
|
ACCEPT — contract-tier in-seat review of PR #15690, head Implemented-by: Verified in a detached compare worktree at the PR head against merge-base H45's optional parent half is left out on the card's own budget condition — the sub-issues parent read is one call per Landing regime: pure code ⇒ ready + auto-merge in this same act (the queue's SQUASH governs whatever method the tool reports); closes #15667. Generated by Claude Code |
Fixes #15667
Two report-only rows on
scripts/pm/check-half-states.mjs, both in thestateband. No other file changes; report-only exit semantics are unchanged (a completed sweep still exits 0 whatever it found).H45 — reserved and handed over at once
An open card carrying both
pm:epicandpm:queue. H3's shape and H3's cost: a pure label intersection, no threshold, no timestamp.pm:epicsays a named epic PM has reserved the card;pm:queuesays it has been handed over and any seat may take it. Both at once and each reader is right about the label it read.Carriers are listed by construction — every
pm:queuecarrier is on a label page the sweep already pages — so the row buys no request and needs no gathering policy.H46 — implemented without a claim
An open pull request (drafts included) whose body binds a closing keyword to an open card that either
pm:queue— published as available while a PR is already set to close it; orClaim:whoseBranch:line names the PR's head branch.Part of #Nis not a closing keyword and never fires the row.Nothing is re-spelled. The keyword extraction is
closingKeywordTargets— H7's and H21's, so the three rows can never disagree about what GitHub will act on. The claim half isCLAIM_COMMENT_MARKERand the branch half isclaimedBranches, the pairscripts/pm/check-clause2-carriers.mjsreads. No existing row's predicate or wording is touched.Placement is load-bearing. H46 is judged after H44, because leg (b) adds threads to the shared comment cache and H44's corpus is whatever the rows above it already fetched. Judging H46 first would widen H44's input and change what an existing row reports.
Three unjudged states, each silent rather than clean (#4690): a bound number the sweep cannot see as an open card (closed, a PR, or beyond the listing ceiling); a thread that could not be read; and a PR with no head ref, on leg (b). The new summary clause states the coverage pair and says the count is a lower bound.
Live run — both repos, on this branch
GITHUB_TOKEN=… PM_SWEEP_REPO=OWNER/REPO node scripts/pm/check-half-states.mjs, at commit88d7a4f2d.objectstack-ai/objectstackobjectstack-ai/objectuiH45 reads 0 on both boards — measured, not assumed: no open card currently carries both labels, which is the state the sibling rule card describes as correct.
The 17 H46 rows in objectstack are all leg (b); leg (a) fired on nothing. PRs flagged:
seedisallowOrgOverride: falseandgetMetaItemgates it #15068manifest.dataseed read so a flat bundle collects each dataset once #15605 → cardAppPlugincollects every seed dataset TWICE on a flat-manifest bundle — themanifest.datalegacy fallback has no reference guard, and its siblingtranslationsblock does #15262./hook-bodyand./package.jsonas public subpath exports of @objectstack/cli #15611 → card 17.3.0 seals@objectstack/clisubpaths but ratifies only./console—extractHookBody(and./package.json) have no public entry, and an app's hook-body fidelity harness breaks with no replacement #15325ADR-0081iscloud ADR-0081— and ADR-0105 cites both spellings in one document #9072tenant.zod.tsidleTimeout / sessionTimeout publish a describe with no unit, while the JSDoc one line above says seconds #14519DUPLICATE_RECORDas a 409 HTTP response code, while every route reports the conflict asUNIQUE_VIOLATION#15362check:skill-examplesleavespackages/spec/.examples-build/behind, andcheck:docs-audit-scopethen fails its own self-test in the same working tree #15446os serve—fetch failed / other side closedevicted PR #15506 from the merge queue on a shard its diff cannot reach #15545check:error-status-conformancecannot derive theUNIQUE_VIOLATIONproducer — R5b's body group forbids nested braces, so documenting the code's real 409 fails the gate #15633os lint --eval'smeanScorestops reading 100 when nothing was generated #15659 → cardos lint --eval'smeanScorecounts a failed generation as 100 — a throwing generator getsscoreMetadata({}), which is 100 / grade A /valid: true#15578z.string()predicate slot at build time #15664 → card service-automation: adecisioncondition accepts a CEL envelope that neither validator can see — a malformed one evaluates tofalseSILENTLY at run time and takes the wrong branch #15572apisub-config soRestApiConfigSchemaowns its defaults #15673 → card [finding]RestServer.normalizeConfigstill discards the parsedapioutput — its??chain duplicatesRestApiConfigSchema's defaults key for key, and the validate-only reason has expired #14366oauth.applications.deleteresolves on the zero-byte 200 its route answers, instead of rejecting on every successful delete #15675 → card client SDKoauth.applications.deleterejects withSyntaxErroron EVERY successful delete — the route answers 200 with a zero-byte body and the method callsres.json()on it #15451Corroboration. Three of them were checked against the live board by hand, and H2 independently files
assignee set but no claim comment on the threadon the very same cards (#15321, #14025, #15068 — rows in the same sweep output). #15321's thread carries a comment readingClaiming this card.in prose, which is exactly the shape the maintainer's 2026-08-11 ruling makes non-machine-readable. So the row is reporting real half-states, not parser noise.A live control for the clean direction: this PR's own body binds
Fixes #15667, and #15667 carriespm:dispatchedwith a comment whose first line isClaim:and whoseBranch:line names this PR's head branch — so H46 is silent about this PR. The clean leg is exercised by the live board, not only by fixtures.H45's second half — measured, and left out
The card conditions the parent half on costing at most one read per
pm:epiccard. Measured 2026-09-05 against the live API:GET /repos/{owner}/{repo}/issues/{n}/parentreturns the parent issue with fulllabels(verified on skills/objectstack-ui "Navigation Item Types" documents 7 of the 9 live NavItemVariant members —actionandcomponenthave no row #13890, whose parent Published-skills factual sweep: verify every behavioral claim in skills/** against the implementation — program anchor #13658 came back carryingpm:epic), so the labels need no second read. A parentless card answers a documented404 No parent issue found. That is exactly one read per carrier — the card's numeric condition is met.pm:epiccards in objectstack, 0 in objectui.It is left out on the other condition, the budget one — no new fetch class beyond what the sweep already reads for PRs and cards:
/issues/{n}/parentis a new class, and the card anticipated exactly this (if H45's second half needs the sub-issues API, measure and say).pm:epicis not one of the seven label pages the sweep fetches, so the population does not exist yet. Folding apm:epicpage intoseenwould widen the input of every other row that reads it — H1, H2, H4, H9, H11, H24, H25, H29, H30 — which is a change to what existing rows report, and this card forbids that.So per the card's own disposition the half is left out and the cost is reported for the seat to file. The absence is disclosed in the H45 header as an unread surface, never a clean reading.
Verification
Every verdict below is the line the check itself printed; exit codes were captured before any pipe.
node scripts/pm/check-half-states.mjs --self-test— exit 0,✓ check-half-states self-test: 2223 cases pass.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 31 commands from the real changeset; all 31 run, results below.cmd > log 2>&1; EXIT=$?). The ones that print a verdict:✓ check-half-states self-test: 2223 cases pass.·✓ dispatch-gates self-test: 1445 cases pass.·✓ check-closing-keyword-parity --self-test: 24 assertions, 5 mutations of the shipped parsers each driven to red.— the parity guard extractsclosingKeywordRe()from the shipped bytes, so a green there is the statement that this PR did not move the keyword grammar ·✓ check-partof-closing-keyword self-test: 28 cases pass.·✓ check-self-test-wired: every one of the 171 script(s) CI runs that ship a --self-test has that self-test run by CI.·✓ check-nul-bytes --self-test: 75 assertions over a temp git repo (real scan() path)·✓ check:entry-guard: 214 scripts/ file(s) …·✓ check:parse-guard: 213 scripts/ file(s) …·✓ comment-mask corpus sweep …: 5970 files, 0 disagree, 0 unparseable. The whole battery ran underbash scripts/pm/os-verify-lock.sh, which reportedVERDICT command-exit 0 · held the lock 771s (12m51s) · waited 36s— shared-box seconds, as the wrapper itself says.pnpm check:partof-closing-keyword/node scripts/check-partof-closing-keyword.mjs— exit 2,NOT WIRED — neither PR_BODY nor PR_NUMBER is set. Recorded as NOT MEASURED, not as a pass; re-run with this PR's own body: withPR_BODYset to this very body it exits 0,✓ check:partof-closing-keyword: this PR carries no Part-of/closing-keyword contradiction.pnpm lint(eslint . --no-inline-config, the whole repo — the style authority of record) through the same lock wrapper: exit 0, eslint printed no problems.VERDICT command-exit 0 · held the lock 112s (1m52s) · waited 347s (5m47s). This is the full farm run, not a narrowing.grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the edited file — exit 1, zero hits (pnpm check:nul-bytesgreen above).Ablation — three legs, each proving a new row can go red
Each leg was mutated on the committed tree, the mutation was confirmed on disk (grep count of the removed anchor fell to 0 and of the injected text rose to 1, and the file's
git hash-objectmoved off itsHEADblob), the suite was run, and the leg was restored withgit checkout HEAD -- "$REPO_ROOT/scripts/pm/check-half-states.mjs"— the path absolute, resolved fromgit rev-parse --show-toplevel, andHEADnamed so the restore cannot read back the mutation through a written index — under atrap … EXIT INT TERM. Restoration is proven bygit diff HEADnaming no file and the hash returning to theHEADblobd6cad42a02647aad9037377863b4748a667195b4— not by the exit code of the restore. This script has no build step, so there is nodist/leg.H45: `pm:epic` + `pm:queue` -> findingpm:queuereadH46: a `pm:queue` card -> findingH46: a card whose only `Claim:` names another branch -> findingSelf-test battery
Both directions for each row, in the file's own style —
pm:epicalone,pm:epic+pm:dispatched,pm:epic+pm:queue, either label order, a third state alongside, and both adjacency cases against H3; for H46, a card claimed on this branch (clean, including the blockquote andClaimed:/Branches:spellings), apm:queuecard (fires), a claim naming another branch (fires),Part ofonly (clean), a keyword in backticks (clean), a bound number with no open card (unjudged), unconsulted and unreadable threads (unjudged), the per-card-number binding, the gathering policy, the list cap, the band, the registry-coverage proof, the count keys and the summary clause.Not governed,
skip-changesetscripts/pm/**publishes nothing from any package, so the label applies and is set on this PR. Draft, targetingmain; the seat reviews and flips it.🤖 Generated with Claude Code
https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Generated by Claude Code