Skip to content

tooling(pm): close H45's parent-relation silence — a pm:epic card reserved under nothing delegated - #15755

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-15702-half-states-h45-parent-half
Sep 5, 2026
Merged

tooling(pm): close H45's parent-relation silence — a pm:epic card reserved under nothing delegated#15755
os-steve merged 2 commits into
mainfrom
claude/issue-15702-half-states-h45-parent-half

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15702

H45 reported one shape (pm:epic + pm:queue on one card) and carried a paragraph
naming the reading it could not afford: a pm:epic card whose PARENT does not carry
pm:epic. Both prices that paragraph named are now paid, declared, and measured.

Surface: scripts/pm/check-half-states.mjs only. Report-only, exit semantics unchanged.
H45's first half is byte-identical — same predicate, same sentence, same free population.

The two contract questions, as implemented

  1. The sub-issues parent read is a declared fetch class. subIssueParentPath(repo, n)
    is named and pinned the way pmLabelListingPath names the label pages — a request
    shape inlined in a loop body can only be asserted by re-typing it. One
    GET /repos/{owner}/{repo}/issues/{n}/parent per pm:epic carrier. Its 404
    No parent issue found is the endpoint's documented answer for a parentless card, so
    the caller counts it as a probe that ANSWERED; any other failure leaves the carrier
    UNJUDGED. No other member of the sub-issues family is called.
  2. The pm:epic enumeration is a separate population. It is read LAST in sweepInto,
    into a loop variable, and never enters seen. SEEN_LABEL_PAGES lifts the seven pages
    that do feed seen out of the loop body so the exclusion is checkable from outside the
    function instead of re-typed into an assertion. Evidence for "no other row's input
    moved" is in two forms below: a self-test ablation, and five live sweeps.

The row files under H45 — one row id, a second sentence. Every registry key matches
/^H\d+$/ (familyNumber parses it, familyRegistryCoverage scans for it), so there are no
sub-rows to name; a case pins that no key of another shape exists.

The reading taken on pm:epic semantics, and why

.claude/skills/pm-dispatch/SKILL.md, ## Epic 子树车道, makes pm:epic a two-node fact:
it sits on the delegated 父单 AND on every sub-issue, and 「label:pm:epic 即父子保留全集」
makes the index parents and children together. The reservation it publishes is the same
sentence at both ends: 「已由 epic PM 保留;其它 PM 永不取」.

So the row asks whether the node the reservation hangs from is delegated:

  • parent carries pm:epic -> clean.
  • parent does NOT -> finding, naming the parent ("reserved under a parent that is not
    delegated").
  • no parent (the 404) -> the card must then be the delegated node itself. A top-level
    carrier is the delegated node when EITHER free signal says so
    : it heads a subtree
    (sub_issues_summary.total > 0, which is what a 父单 is), or its body carries a
    session_… identifier — the machine half of the paired delegation signal
    「父单打 pm:epic + 正文写会话 ID 与声明的文件领地」, and the same registration the
    zombie rule reads (「父单正文有登记」). Only a carrier with NEITHER is reported as
    "reserved under nothing delegated".
  • no parent AND the card's own payload names one -> the two channels disagree; reported
    rather than silently excused, with no side taken.

The OR is not a preference — the live index refuses both single-signal readings, in
both directions (measured 2026-09-05 06:1xZ, table below): #11632 is a chartered epic with
a live owner registration in its body and ZERO sub-issues, so a sub-issue-only reading
calls it abandoned; #12243 and #11663 head 20 and 11 sub-issues with no session id in the
body, so a registration-only reading (or an AND) calls them abandoned. Under the OR all
five are clean, which is what the board actually is today.

The body half is a deliberately LOOSE anchor, and its looseness is safe in exactly one
direction: it is consulted ONLY to EXCUSE a top-level carrier, never to fire one. Its
error mode is a silence (stated in the banner and in the finding sentence, which calls the
row a LOWER BOUND), never the phantom accusation that keeps the seat-sticker body half out
of this file entirely.

Live run — both repos

Request cost is exactly one label page plus one parent read per carrier, and the sweep's
own open-listing counter shows it: objectstack 17 -> 18 pages, objectui 14 -> 15 pages.

Epic parent reads (H45): 5 of 5 open `pm:epic` card(s) had their sub-issues parent read
answered — a 404 `No parent issue found` IS an answer here, because parentless is what it
means. That index is enumerated as its own population and kept OUT of the label pages
every other row is judged over (`SEEN_LABEL_PAGES`), so this row buys one page plus one
read per carrier and moves no other row's input. A carrier whose parent read did not
answer is UNJUDGED rather than clean.

objectstack — 5 carriers, 5 parent reads, 0 flagged.

card parent read sub-issues body registration H45 second half
#13658 404 No parent issue found 1 yes clean
#12243 404 No parent issue found 20 no clean
#11663 404 No parent issue found 11 no clean
#11632 404 No parent issue found 0 yes clean
#9296 404 No parent issue found 6 yes clean

objectui — 0 carriers, 0 parent reads, 0 flagged; the clause renders unconditionally
(0 of 0), so an empty index reads as an empty index and never as a pass that skipped one.

Sweeps: PM_SWEEP_REPO=objectstack-ai/objectstack exit 0, 257 findings (05:47Z);
PM_SWEEP_REPO=objectstack-ai/objectui exit 0, 157 findings (05:55Z).

"No other row's input moved" — the evidence

Structural (the primary claim). The pm:epic listing is consumed by a for loop at
the very end of sweepInto, after every other row has finished judging. There is no path
by which one of its rows can reach seen, the shared comment cache, or the reference
corpus. SEEN_LABEL_PAGES is frozen, exported and pinned.

Ablation (the claim can fail). Three legs, each mutated on disk, proven on disk (deleted-
text and injected-text occurrence counts plus the blob hash against HEAD), run, then
restored from HEAD with the restore proven the same way (git diff HEAD empty AND the
blob hash back). No build step sits between the mutation and the run: the self-test imports
the same source path the mutation writes.

leg mutation result
row goes quiet the parent-lacks-pm:epic branch returns null RED, 4 of 2262 cases
fold into seen pm:epic added to SEEN_LABEL_PAGES RED, 2 of 2262 cases
OR becomes AND both delegation signals required RED, 2 of 2262 cases

The population cases are the ones that matter here, and they carry a CONTROL: a case
asserts that an epic carrier folded into the judged population really WOULD fire an
existing row, so the "verdicts unchanged" pin cannot be satisfied by a population that
could never have mattered.

Live before -> after. Five interleaved objectstack sweeps, per-row finding counts:

row 05:18 pristine* 05:25 PATCHED 05:33 pristine* 05:40 pristine 05:47 PATCHED
H2 17 17 17 17 17
H4 3 3 3 3 3
H5 9 9 9 9 9
H6 11 11 11 11 11
H8 9 9 9 8 9
H9 12 12 12 12 12
H10 2 2 2 2 2
H11 13 13 13 13 13
H12 3 3 3 3 3
H13 5 5 5 5 5
H14 29 29 29 29 29
H15 1 1 1 1 1
H19 22 23 23 23 23
H20 4 4 5 5 5
H21 1 1 1 1 1
H24 1 1 1 1 1
H26 22 22 22 22 22
H30 6 6 6 6 6
H31 3 3 3 3 3
H35 2 2 2 2 2
H36 16 16 11 11 6
H38 2 2 2 2 1
H43 NOT MEASURED* 5 NOT MEASURED* 5 5
H44 50 50 50 50 50
H46 17 17 17 19 18

18 rows are identical in all five runs. Every mover is a row over live PR/branch/seat
state, and each moves between two PRISTINE runs as well, which is the control:

  • H19 22 -> 23 and H20 4 -> 5 both happen between two runs of the UNCHANGED file.
  • H36 16 -> 11 likewise, and 11 -> 6 across the last pair.
  • H38 lost #6023, whose row read "a Claim: written 0.0h AFTER this post's last event"
    — an order comparison on a boundary, flipped by that seat editing its post.
  • H46 19 -> 18 and H8 8 -> 9 are the merged/open PR windows.

* The two starred runs executed the pristine file from OUTSIDE the repo, where H43's
governed register is not beside it. Those runs say NOT MEASURED — the governed register is not installed beside this file in their own words, so H43 is comparable only across
the in-place pair, where it reads 5 = 5. This is the file's own prerequisite discipline
catching a defect in my measurement method rather than in the board; the in-place pair
(05:40 pristine / 05:47 patched, same tracked path, swap and restore both proven by blob
hash) is the one that carries the before -> after claim.

objectui, same pair shape: 20 of 22 rows identical; the two movers are H16 (three PRs
entered merge conflict in the five-minute gap — and the run with MORE conflicts is the
PRISTINE one) and H11, off by one.

Self-test

node scripts/pm/check-half-states.mjs --self-test -> ✓ check-half-states self-test: 2262 cases pass. (2223 before this change; +39 written here, +1 generated by the shared
SUMMARY_CLAUSE_ANCHORS coverage loop picking up the new anchor).

Cases in both directions, one per shape ruled: parent carries pm:epic (clean) · parent
lacks it (finding, names the parent, names the shape, asks for ONE label write) ·
top-level heading a subtree (clean) · top-level with a body registration (clean) ·
top-level with neither (finding, says "no parent" and never invents a parent number) ·
each delegation signal read on its own · a body with no session id is not a registration ·
404 against the card's own parent object (finding) · the same via parent_issue_url ·
the contradiction outranking the delegation excuse · a card without pm:epic buying no
read and being declined even when handed a parent · an unread parent filing no row, with
the summary clause carrying the UNJUDGED disclosure · both adjacency directions against
H45's first half · the fetch-class path · the population separation and its control · both
count keys on SWEEP_COUNT_KEYS · the summary clause rendered unconditionally, with
numbers rather than undefined.

On the declared floors named in the dispatch: this file declares no battery floor —
the roster/floor mechanism lives in scripts/sync-template-versions.mjs
(SELF_TEST_BATTERY_FLOOR), not here, and nothing in the repo pins a case count for this
script. The count line is computed from cases.length and moved 2223 -> 2262. The
"79-case" figure in .github/workflows/lint.yml is a historical statement about what the
battery held when that step was added, not a floor.

Header registry: this file's per-row documentation lives in the banner above each
predicate (the prose list in the file header stops at H28 and has since). The H45 banner's
"what it deliberately does NOT report" paragraph is replaced, and the second half has its
own banner beside it.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived
the family from the real changeset (1 path, three-dot against merge base c2a336ca2), and
all 31 commands were run. 30 exit 0. The one non-zero is not a verdict:

node scripts/check-partof-closing-keyword.mjs   -> exit 2
check:partof-closing-keyword: NOT WIRED — neither PR_BODY nor PR_NUMBER is set, so this
run was handed no pull request and judged nothing. This is a wiring or usage failure, NOT
a verdict

It was then run properly, with this body in PR_BODY — see the check line below.

pnpm lint (eslint . --no-inline-config, the whole repo, not a narrowed run) through the
shared verify lock: os-verify-lock: VERDICT command-exit 0 · held the lock 102s.

All of the above was measured on 3b29a22f6, this branch's tip at the time of writing
(origin/main merged in first, so the derivation reads a current tree).

This PR is a draft and stays one: the seat reviews and flips it. Not governed; no publishable
package changes, so skip-changeset.


Generated by Claude Code

…eserved under nothing delegated

H45 reported one shape (`pm:epic` + `pm:queue` on one card) and documented the
one it could not afford: a `pm:epic` card whose PARENT does not carry the label.
Both prices that paragraph named are now paid and declared.

- a new, named fetch class: `subIssueParentPath` — one
  `GET /repos/{owner}/{repo}/issues/{n}/parent` per `pm:epic` carrier, whose
  404 `No parent issue found` is the documented ANSWER for a parentless card
  and is counted as a probe that answered. No other sub-issues endpoint.
- a SEPARATE `pm:epic` enumeration, read last in the sweep and never folded
  into `seen`. `SEEN_LABEL_PAGES` lifts the seven pages that do feed `seen`
  out of the loop body so the exclusion is checkable rather than re-typed.
- the row files under H45, one row id and a second sentence: the registry
  keys are `H<digits>` and admit no sub-rows.
- a top-level carrier is the delegated node when EITHER free signal says so —
  it heads a subtree, or its body carries the `session_…` registration. The OR
  is what the live index forced in both directions.
- a coverage pair (`epicParentCandidates` / `epicParentProbed`) and an
  unconditional summary clause, so an unread carrier reads as UNJUDGED.

H45's first half is untouched: same predicate, same sentence, same free
population. Report-only exit semantics unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox

os-steve commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

ACCEPT — in-seat review of PR #15755, head 3b29a22f6 (skills seat, 2026-09-05T06:3xZ).

Implemented-by: claude/issue-15702-half-states-h45-parent-half (os-dev subagent)
Reviewed-by: session_019RfFHiRCSs3JXLK4cwcfox (skills seat)

Verified in a detached compare worktree at the PR head, three-dot against origin/main, not from the report: one file, scripts/pm/check-half-states.mjs, +384 / −14 — the row commit plus one merge of main. The two contract questions the card set are answered as ruled: the sub-issues parent read is a declared fetch class (subIssueParentPathGET /repos/{owner}/{repo}/issues/{n}/parent, one call per pm:epic carrier), and the pm:epic index is its own population — enumerated last in sweepInto into a loop variable, never folded into seen, with SEEN_LABEL_PAGES pinning the seven pages that do feed seen and a self-test asserting that exact list and pm:epic's absence from it. The row: a pm:epic child under a parent without pm:epic files; a parent carrying pm:epic is clean; a 404 is an answer (parentless), and a parentless carrier is clean when either free signal says it is the delegated node (sub_issues_summary.total > 0, or a session_… registration in the body), files when neither does, and files as a channel DISAGREEMENT when the card's own payload names a parent the read denies; any other read failure leaves the carrier UNJUDGED (parent === undefined → no row), never clean. The REST helper attaches err.status, so the 404 branch is real; sub_issues_summary rides every listing row. Both count keys are on SWEEP_COUNT_KEYS, the summary clause has its anchor and renders on every run without undefined. The first half of H45 is byte-identical; no exit code changed; no H45b id invented. No model names in the diff or the PR body. Not governed (check-governed-merges --test exit 0); tier read: no path-derived mandate.

Seat gate runs on this head in the compare worktree: check-half-states --self-test 2262 cases (2223 on main, +39), governed exit 0. The dev's evidence beyond that: three ablation legs each red with the mutation proven on disk by blob hash and the restore proven the same way (one leg re-run after its first disk proof was declared void); 31 derived families, 30 exit 0 and the one non-zero (check-partof-closing-keyword exit 2, "NOT WIRED") re-run with the real PR body to exit 0 — and filed as #15761 for the devx lane, since dispatch-gates offers an env-carried guard as a runnable command; whole-repo lint through the lock; five interleaved live sweeps where every moving row is a board row that also moves between two pristine runs, and the row itself answered 5 of 5 parent reads with 0 flagged (objectui 0 of 0) at +1 page and +1 read per carrier. CI on 3b29a22f6: 35 check runs, 24 success, 11 skipped, none failing, Live half-state sweep among the successes; mergeable_state: clean.

Landing regime: pure code ⇒ ready + auto-merge in this same act (the queue's SQUASH governs whatever method the tool reports); Fixes #15702 closes the card on merge. #15744 (H18 prose + the pm:retriage label description, same file) unblocks when this lands.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 5, 2026 06:38
@os-steve
os-steve added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit b25a5fc Sep 5, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-15702-half-states-h45-parent-half branch September 5, 2026 07:00
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