Skip to content

feat(pm): two report-only patrol rows — the decision card's four-facet block, and finding outliving its grade - #18033

Merged
os-project-manager merged 5 commits into
mainfrom
claude/issue-17009-patrol-rows-facets-and-stale-finding
Sep 13, 2026
Merged

feat(pm): two report-only patrol rows — the decision card's four-facet block, and finding outliving its grade#18033
os-project-manager merged 5 commits into
mainfrom
claude/issue-17009-patrol-rows-facets-and-stale-finding

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #17009
Fixes #16904

Both cards' D2 halves. #17009's D1 landed as ea1da645 (PR #17515) and #16904's as ae1b2588 (PR #17994, plus the seat's one-time sweep, readings 5652947318); these two rows are the last deliverable on each card, so merging this closes both.

Two report-only rows in scripts/pm/check-half-states.mjs, one per card, plus the CI wiring #16904's D1 report left unfired. No --write path anywhere, no label written, no gate added.

H62 (#17009) — a decision card whose face carries no four-facet block

An open needs-user-decision card whose body carries no os-decision-facets marker, in either spelling (plain-text line or the HTML-comment form). Presence is a literal containment test over the raw body, which is the extraction rule the shape names for itself (「提取按字面 grep」); code-stripping and a positional read are refused by name in the docblock because each produces 「注释形读不到永不读作无四棱块」, the one direction of error the shape forbids. Body only: 「落卡即带」 makes the block part of the card face, so a marker in a comment is not the shape, and the row buys no thread.

The Prior rulings read: line is a second reading on the same rowpresent / unresolved / absent, in the row text, never a second row. Its writer is check-prior-rulings.mjs, coupled here by source text: both import directions were measured and neither runs (static is a TDZ cycle; dynamic deadlocks against this module's own top-level await). The docblock records both measurements so nobody retries them.

H63 (#16904) — a graded card still wearing finding

An open card carrying finding beside a grading state or any priority:*. The rule is reused by import, never copied — but the arrow is forced, not chosen: the sweep already imports PM_STATE_LABELS from the patrol and derives module-level constants from it, so the patrol importing the sweep throws ReferenceError: Cannot access 'PM_STATE_LABELS' before initialization the moment the patrol is the entry point, which it is on every run and every self-test. So the screen moved down into the module that owns the vocabulary, and sweep-stale-finding.mjs re-exports it under the names its callers and its 91-case self-test already use. The patrol's own self-test pins by source text that the sweep still aliases that definition and has not re-grown a copy.

The row does not contradict the pins it sits beside, and the docblock names each: H13's set carries finding (which is exactly why H13 is silent on a graded carrier — the card is visible, to the wrong reader); H22 drops it and this row is open-only; H29 drops it as a card KIND rather than a position, and this row explicitly does not claim otherwise.

Wiring: check:pm-stale-finding in package.json and a self-test step in lint.yml. check-self-test-wired population 208 → 209, green in both directions — the tool shipped a 91-case self-test that nothing in CI ran.

Acceptance greps, both directions

reading before after
H62 (#17009) / H63 (#16904) in check-half-states.mjs 0 / 0 3 / 3
lit control Decision-box dependency flags (instruction ④) 4 4
os-decision-facets reader in check-half-states.mjs 0 3
check:pm-stale-finding in package.json 0 1
check:pm-stale-finding in lint.yml 0 1
check-half-states --self-test cases 3806 3915
sweep-stale-finding --self-test cases 91 91
check-self-test-wired scripts CI runs 208 209
check:declared-population-live declaring families with an unreachable population 1 0

Untouched, proved by md5 of the whole exported function body — H4 1d13e919…, H13 fa79039c…, H14 1e88cafd…, H22 a45048e0…, H29 aaa6ea92…: identical before and after. The instruction-④ section, .claude/** and the write-path half of finding stripping are untouched.

Live readings, read-only, taken from this tree

H63: the landed sweep's own dry run against this board — 33 open cards carry finding, 0 stale, 33 genuinely ungraded, 0 unjudged; 1 listing, 0 writes, rate_limit core 14747 → 14746. Zero is the expected reading right after the seat's one-time sweep (19 stripped here, 36 on objectui), and it is what the row exists to watch refill.

H62: the decision inbox is 1 open card today, not the 27 the D1 measured — it fires on that one, second reading absent. ⚠️ No live specimen carries the marker, so the clean direction has no lit control on today's board; its controls are the self-test's fixtures, in both spellings.

Gates

Derived on the merged head 1d52dd97 with dispatch-gates --commands --repo objectstack-ai/objectstack (no paths): 74 families, 74 run, every one exit 0. Reconciled with --ran carrying a recorded exit code per family: 「74 derived famil(ies) accounted for — 74 run, 0 NOT-MEASURED (a DERIVED zero — all 74 recorded an exit code and none of them is 3)」.

Five build-reading families first answered PREREQUISITE NOT MET (exit 3) in a fresh worktree — recorded as NOT MEASURED, never as findings — and all five exit 0 after pnpm build under the shared verify lock (VERDICT command-exit 0 · held 391s · waited 0s, 73/73 tasks). One of the five, check:type-check-debt, refused a second time on a heap ceiling my own NODE_OPTIONS imposed (its own line says so: 「tsc runs under --max-old-space-size=4096 MB — the caller's NODE_OPTIONS, which is tighter」) and exits 0 at the CI-shaped 6144.

pnpm lint run repo-wide rather than narrowed: eslint --no-inline-config . --format json exit 0, 6723 files linted, 0 with findings — no narrowing claimed. Control-character sweep over every touched file: no match.

⚠️ origin/main moved again after this merge; the derivation says the tree is 1 commit behind on 3 files it derives from. The merge queue rebuilds and re-runs on that generation, which is where that half is measured.

Deviations, declared

  1. One file beyond the claim's surface. scripts/pm/sweep-stale-finding.mjs is edited to hand the screen down, because ruling ② mandates reuse by import and the only import direction that runs points the other way (measured; the crash is reproduced in the docblock). Its public API, its behaviour and its 91-case self-test are unchanged.
  2. No header-roster entry. The header roster stops at H28; every row since carries its own banner block above its predicate instead. These two follow that convention — adding two entries to a list 33 consecutive rows are absent from would make the header lie about its own coverage.
  3. Two more commits, both derivation bookkeeping the wiring forced, neither weakening a self-test.
    • Wiring the sweep into package.json makes it a declaring family for dispatch-gates, and check:declared-population-live reds on the module-level fixture's repo slug. Fixed by the gate's own remedy: the no-path-population marker (true — the tool's whole input is the GitHub API) plus moving the fixture into the self-test body, where the module-body mask reaches it.
    • dispatch-gates --self-test then red on one case of 1682 (CI run 34759389219, Lint & Repo Gates step 32): 「listed but no longer a member: check:pm-half-states」. The patrol's two sibling source-text pins make the family placeable by path (dispatch-gates scripts/pm/sweep-stale-finding.mjs ⇢ 「program text read by scripts/pm/check-half-states.mjs」), so it left the root-walk residue population and its ROOT_WALK_RESIDUE_LEDGER row went stale — which that ledger reds on by its own contract. The row is removed with a note; the placement is true and is the opposite of the extractWatchHints reads a noise-floor EXCLUSION constant as a watch hint: .changeset derives check:pm-half-states onto every changeset-adding card #15753 one the row was written about.
  4. origin/main merged (1d52dd97) because the derivation warned the tree was stale on a file it derives from — and it was load-bearing: the merged check-clause2-carriers.mjs adds check:pm-clause2-carriers to this card's family list (73 → 74).

skip-changeset: scripts/pm/**, a root package.json script and a workflow publish nothing from any released package.

Authored in Claude Code session session_01DAcomhvR9kKizeYgg89Vo8.


Generated by Claude Code

…acet block

The `os-decision-facets` marker is declared machine-findable by
references/decision-analysis.md 〈四棱卡面块固定形状〉 and, measured on
origin/main, is read by nothing: one hit in the whole tree, its own
declaration, against a positive control (needs-user-decision reaches seven
files under scripts/). This adds the first reader.

H62 is report-only: an OPEN card carrying the decision label whose BODY
carries no marker, in either spelling. Presence is a literal containment
test over the raw body, which is the extraction rule the shape names for
itself (「提取按字面 grep」); code-stripping and a positional test are both
refused by name because each produces 「注释形读不到永不读作无四棱块」, the
one direction of error the shape forbids.

The prior-ruling line the shape puts after the four facets is read as a
SECOND reading on the same row — present / unresolved / absent — and never
as a second row. Its writer is check-prior-rulings.mjs, coupled here by
source text rather than by import: both import directions were measured
and neither runs (static is a TDZ cycle, dynamic deadlocks against this
module's own top-level await).

Population is the unscoped open listing, for H31's reason; the row costs no
request and writes nothing. H4 / H13 / H14 / H22 / H29 are byte-unchanged.

Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
Co-authored-by: Claude <noreply@anthropic.com>
…ire the sweep's self-test in CI

The state model makes `finding` transient — 「观察类记录,恒 = 待首次定级;定级即
离标」 — and the population has been re-measured and hand-cleared three times
(385 of 395, then 185 of 226, then 38 by hand, then the one-time sweep to 0 on
both boards). It refills because the strip has to be passed by the caller on
every grading write. The write-path half is its own card; this is the read
half: H63 reports an open card carrying the marker beside a grading state or
any `priority:*`, so the refill is visible on the patrol instead of being
re-measured by whoever next looks.

The rule is REUSED, not copied — and the import can only point one way, which
was measured rather than assumed: the sweep already imports `PM_STATE_LABELS`
from the patrol and derives module-level constants from it, so the patrol
importing the sweep throws `ReferenceError: Cannot access 'PM_STATE_LABELS'
before initialization` the moment the patrol is the entry point, which it is on
every run. So the screen moves down into the module that owns the vocabulary,
and the sweep re-exports it under the names its callers and its 91-case
self-test already use — unchanged, and pinned by source text from the patrol's
own self-test so a copy cannot re-grow.

The row does not contradict H13 (its set carries the marker, which is why H13
is silent on a graded carrier), H22 (open-only) or H29 (a card KIND, not a
position); the docblock names each.

CI wiring the D1 report left unfired: `check:pm-stale-finding` in package.json
and a self-test step in lint.yml. `check-self-test-wired` population 208 → 209,
green both directions — the tool shipped a self-test nothing ran.

Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
Co-authored-by: Claude <noreply@anthropic.com>
Wiring `check:pm-stale-finding` into package.json makes this tool a DECLARING
family for `dispatch-gates`, and `check:declared-population-live` reds on it:
the module-level run fixture's repo slug is the only path-shaped literal in the
file, so the derivation read it as a declared population of one path that names
no tracked file — the same output a gate with no literals produces.

Both halves of the gate's own remedy: the `no-path-population` marker, which is
true of this tool (its whole input is the GitHub API), and the fixture moved
into the self-test body, where the module-body mask reaches it. Top-level VALUE
declarations are deliberately never masked — being unreferenced at module scope
is what a declared population IS — so the comment on the moved fixture says
⛔ do not hoist it.

`check:declared-population-live`: 250 declaring families with 1 unreachable →
249, all reaching. The sweep's 91-case self-test is unchanged.

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

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

CI red, being worked — skills seat (session session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-13T13:37Z. Lint & Repo Gates on head 56305203 failed at step 32 「PM dispatch-gates self-test」: 「dispatch-gates self-test: 1 of 1682 case(s) failed」 (job 103729366162, 2026-09-13T13:36Z); the 143 gates behind it never ran, so that red is a lower bound. This is the PR's own: the diff adds a check:pm-* script and a lint.yml self-test step and refactors sweep-stale-finding.mjs, which is exactly the surface that self-test pins. The dev has been told (foreground, still in its turn) to find the failing case in its own run, fix the root cause here — never the self-test — and push before reporting; the seat reviews the new head when the report lands. No re-run: a second identical failure would say nothing a fix does not.


Generated by Claude Code

CI red at `Lint & Repo Gates` step 32 (run 34759389219): `dispatch-gates
self-test: 1 of 1682 case(s) failed` — 「every gate CI runs whose own source
sweeps the repo root is DECLARED whole-tree, declared path-less, derivable by
path, or a justified row in ROOT_WALK_RESIDUE_LEDGER — listed but no longer a
member: check:pm-half-states」.

Root cause, measured: this PR's self-test reads two sibling sources as program
text — `sweep-stale-finding.mjs`, to pin that it still ALIASES the one
stale-`finding` screen rather than re-growing a copy, and `check-prior-rulings.
mjs`, to pin that the prior-ruling line's writer still prints the key the patrol
greps. The derivation reads those literals and now places the family BY PATH
(`dispatch-gates scripts/pm/sweep-stale-finding.mjs` ⇢ 「program text read by
scripts/pm/check-half-states.mjs」), so it left the residue population — and the
ledger reds on a listed family that is no longer a member, by its own contract:
a stale exclusion is an exclusion nobody measures.

The repair is the ledger's, ⛔ not the self-test's: the row goes, with a note
saying why the family left and why this placement is the OPPOSITE of the #15753
one the row was written about — that one came from a noise-floor constant and
said the opposite of what the constant declares, while these two literals are
exactly what the self-test reads. A card touching either sibling now really does
owe this gate.

`dispatch-gates --self-test`: 1682 of 1682 pass, exit 0.

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

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author
  • Served-tier: 1084/1084 claude-fable-5-1 — harness model stamp counted over this seat's own transcript (non-sidechain assistant messages a model served; <synthetic> harness notices excluded) at 2026-09-13T14:47Z; get_session external_metadata.last_served_model read claude-fable-5-1 at 2026-09-13T14:41Z.

Contract review

Head: 1d52dd97 (PR #18033, cards #17009 D2 and #16904 D2) — read at 2026-09-13T14:49Z by the skills seat at the contract-review tier. NOT GOVERNED, measured: scripts/pm/check-half-states.mjs (+672), scripts/pm/sweep-stale-finding.mjs (+55 −83), scripts/pm/dispatch-gates.mjs (+13 −8 in one ledger row), package.json (+1), .github/workflows/lint.yml (+32) ⇒ in-seat review, then ready + auto-merge by this seat.

① derived judgments — two report-only patrol rows and the CI wiring the D1 left unfired:

  1. H62 ([finding] #15929's class recurred on #16934: a decision card whose Governing text: omitted the ADR that already decides it (ADR-0131 D8) reached batch presentation with a recommendation to re-rule — the text-level governing-text step failed at filing and at presentation; a mechanical prior-ruling search is asked #17009): an open needs-user-decision card whose BODY carries no os-decision-facets marker in either spelling (plain line or HTML comment), read anywhere in the body and never code-stripped; the Prior rulings read: line is a second reading on the same row (present / unresolved / absent), never a second row. PR rows and unreadable label sets are excluded, closed cards are residue for H22/H39. The first reader the marker has had.
  2. H63 ([finding] label:finding is 2% precise as the "ungraded" index — 385 of 395 open cards carrying it are already graded, on both boards #16904): an open card carrying finding beside a grading state or any priority:* — the sweep's own screen, so the row that reports the population and the sweep that strips it read one rule.
  3. The sweep refactor is export-only, as the claim required: the screen and its vocabulary moved into check-half-states.mjs (the module the sweep already imported PM_STATE_LABELS from) and the sweep re-exports all eight public names unchanged — TARGET_LABEL, NOT_A_GRADE, GRADING_STATE_LABELS, NEVER_SWEPT_LABELS, PRIORITY_LABEL_SHAPE, priorityLabelsIn, gradeLabelsIn, screenCard. The import direction ruling ② named crashes on a TDZ read when the patrol is the entry point (reproduced before any edit; a dynamic import deadlocks on the pending top-level await), so the rule moved down to the owner and the patrol's self-test pins by source text that the sweep still aliases it.
  4. Wiring: check:pm-stale-finding in package.json and a self-test step in lint.yml; the sweep's live (writing) half never runs from CI.
  5. dispatch-gates.mjs: the ROOT_WALK_RESIDUE_LEDGER row for check:pm-half-states removed — the family became placeable by path once the patrol's self-test reads two sibling sources as program text, and the ledger reds on a listed non-member; this was the CI red on 56305203 and the ledger's own remedy.
  6. Deviations, declared, accepted: two files beyond the claimed surface (items 3 and 5, both forced and both measured); no header-roster entries (the roster stops at H28 and every row since carries its banner above the predicate — consistent); five commits; origin/main merged into the branch (the derivation refused a stale tree, and the merge really added a family).
  7. Five untouched predicate bodies byte-equal by md5 (H4, H13, H14, H22, H29).

Seat measurements on the head tree (git archive 1d52dd97): check-half-states.mjs --self-test → 3915 cases pass, exit 0 (base 3806; +53 H62, +56 H63). sweep-stale-finding.mjs --self-test → 91 cases pass, exit 0, unchanged by the move. --pair 18033 → exit 0 at 2026-09-13T14:47Z (cards #17009 and #16904 both agree). Checks on 1d52dd97 at 2026-09-13T14:47Z: 43 runs, 0 red, 1 running (Lint & Repo Gates, the context that was red on 56305203 and green on 9ece34ff) — the queue's own gate holds the merge until it is green.

② semver: nothing published; skip-changeset is right.

③ boundary flags: open_questions empty. Three 「noted, not filed」 items stand as notes: check:pm-closed-card-sweep is in package.json but named by no workflow (same two-line wiring, this seat's next wiring pass); the decision inbox is 1 open card today against D1's 27, so H62 fires on one card with the reading absent; check:type-check-debt refuses under a 4096 MB heap by design. Landing consequence: both cards close; scripts/pm/check-half-states.mjs frees; the #17932 hold stays on its own restart condition.

Implemented-by: claude/issue-17009-patrol-rows-facets-and-stale-finding
Reviewed-by: session_01DAcomhvR9kKizeYgg89Vo8

Verdict: PASS — both D2 halves as claimed (5653347020, 5653347348): two report-only rows, one rule for the stale-finding screen, the sweep's self-test fired in CI, no write path, no gate, no .claude/**.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment