Skip to content

feat(pm): add the stale finding label sweep (D1) - #17994

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-16904-finding-label-sweep-d1
Sep 13, 2026
Merged

os-project-manager merged 1 commit into
mainfrom
claude/issue-16904-finding-label-sweep-d1

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Part of #16904 — deliverable D1 only. The card stays open for D2 (the report-only patrol row in check-half-states.mjs), so this body carries no closing keyword.

What and why

The PM state model defines finding as transient — 「定级即离标」 — but nothing mechanises the strip, so a graded card keeps the marker and label:finding stops answering the one question it exists for. Three independent measurements on this card: 385 of 395 open carriers already graded, then 185 of 226 two days later, then 38 of 100 stripped by hand on objectstack.

This adds the one-time sweep, scripts/pm/sweep-stale-finding.mjs: dry run by default, --repo owner/name for either board, --write to act, bounded batches with a GET /rate_limit reading printed before and after each, a stop on the first 403/429 that prints --cursor=N to resume, and --self-test offline with no token. Each removal is the four-step write — read the card's labels, compute the survivor set, write, read back and diff — and any difference refuses to proceed rather than racing another actor.

It only ever subtracts one label: it adds nothing, synthesises no grade, posts no comment and touches no assignee. A card carrying finding with no grade is the genuinely ungraded pool and is left alone; a pm:seat post is never touched. Both are reported rather than silently skipped. ⛔ The script is not run against either board by this PR — the seat that owns a board runs it once, by hand, after reading a dry run.

One measured deviation from the ruled shape

The grading fixed step 3 as "write the full set". scripts/check-whole-set-label-write.mjs bans the whole-set replace across .github/workflows/**, .github/actions/** and scripts/** — its header carries the measurement, a label destroyed one second after it was written by another writer's whole-set replace. So step 3 is DELETE .../labels/finding, the one verb that names a single label and cannot rewrite a set. The four steps and the computed survivor set are unchanged (the survivor set is what step 4 diffs against); only the verb that realises it changed, and it strictly improves the property the read-back exists to detect. That gate is in this PR's derived set and passes.

The file surface stays at one new file: check-self-test-wired and check-self-test-workflow-commands both pass with no package.json entry, because their population is scripts a workflow names. ⚠️ The consequence, stated rather than left implicit: no workflow runs this self-test, so the instrument is unfired in CI until someone wires it — a one-line follow-up for the seat, alongside D2.

Acceptance

reading result
node scripts/pm/sweep-stale-finding.mjs --self-test exit 0 — 91 cases across 9 batteries, offline, no token
required case: finding + pm:queue is stale pass, and the row names the trigger
required case: finding + priority:p3 is stale (no state at all) pass
required case: finding alone is not stale pass — reported as the ungraded pool, not skipped
required case: a pm:seat post is never touched pass, whatever else it carries
required case: the four-step write refuses when the read-back differs pass — driven offline, a fixture re-adds a label after the write; the run stops on the first card and the second is never written to
required case: the 403/429 stop prints the cursor pass — a 403 fixture on card 2 stops the run, prints --cursor=22, exit 4, and card 3 is never touched
required case: --dry-run writes nothing (write-path spy) pass — every request the dry run issued is a GET, and not one of them is a per-card label read
ablation A: the ungraded verdict flipped to stale 7 of 91 cases red; restored to the byte-identical HEAD blob (5167082…), green again
ablation B: the dry-run mode gate deleted from the batch builder 5 of 91 cases red, write-path spy among them; restored, green again
live --dry-run against objectstack-ai/objectstack (exit captured before any pipe) exit 0 — listed 42 open cards carrying finding; 19 stale, 23 genuinely ungraded, 0 unjudged; rate_limit printed at run-start and run-end (core 14871/15000)
stale by trigger on that run 13 pm:blocked · 3 pm:on-hold · 2 priority-only · 1 pm:dispatched
control — a label that cannot be stale pm:seat: 13 open, 0 stale under the same exported predicate. Positive leg, so the zero is a measurement and not a silence: the same harness reads 19 stale over the finding population, matching the tool's own run
derived gates (dispatch-gates --commands, reconciled with --ran) 29 derived, 29 run, 0 NOT-MEASURED, 0 UNRUN, every one exit 0 — including check-whole-set-label-write, check-self-test-wired, check-self-test-workflow-commands, check-scripts-symbol-anchors, check-nul-bytes
pnpm lint (repo-wide, eslint . --no-inline-config) exit 0 at b9f45331, 1m12s — no narrowing claimed

Changeset: skip-changeset. scripts/pm/** ships in no package's files[], so nothing published moves.

Acceptance notes

  • The stale predicate is a disjunction (a grading state or a priority), as ruled. The triage seat's "state and priority" constraint was learned from filling a missing priority by hand and sweeping a decision card into the queue; it binds a sweep that writes a grade. This one only subtracts, so it cannot make that error, and under subtraction the conjunction would leave standing exactly the cards the index is wrongest about. Every stale row names its trigger, so the set stays auditable card by card.
  • The live reading shows the population has moved since the card's last measurement (62 stale on 09-12 → 19 stale of 42 carriers today), and that 16 of the 19 sit beside pm:blocked / pm:on-hold — the exact set the last hand pass deliberately left for "a lower-risk pass". The by-trigger breakdown in the report is what makes that visible before anyone writes.
  • noted, not filed: the sweep is rung 3 of the fix. While a grading write can leave the label behind, the population refills; the write-path half is already tracked separately.

Generated by Claude Code

`finding` is defined by the PM state model as transient — it comes off in the
same write as the grade — but nothing mechanises that, so both boards carry a
large population of graded cards still wearing it and the label stops answering
the one question it exists for.

This is the one-time sweep half: a dry-run-by-default `scripts/pm/` tool that
lists the open cards carrying `finding` beside a grading state or a priority,
and with `--write` removes only that one label, in bounded batches, with a
`/rate_limit` reading printed around each batch, stopping on the first 403/429
with a resume cursor. Each removal reads the card's labels, computes the
survivor set, subtracts the one label by name, and reads back and diffs; any
difference refuses to proceed. The genuinely ungraded pool and `pm:seat` posts
are left alone and reported.

`--self-test` runs the whole thing offline through an injected transport: 91
cases across 9 batteries, including a write-path spy proving a dry run writes
nothing and a 403 fixture proving the stop prints its cursor.

Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
Co-authored-by: Claude <noreply@anthropic.com>
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 13, 2026
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Head: b9f45331 (PR #17994, card #16904 — D1, Part of) — read at 2026-09-13T10:06Z by the skills seat at the contract-review tier (served model claude-fable-5-1, get_session external_metadata.last_served_model at 2026-09-13T08:08Z); default-tier build (model: opus) ⇒ compensating control.

① Seat-verified on the head (merge-base 00c332b4): one new file scripts/pm/sweep-stale-finding.mjs (1258 lines), nothing else. In a throwaway worktree: --self-test exit 0 (91 cases / 9 batteries); a live --dry-run --repo objectstack-ai/objectstack exit 0 — 42 carriers, 19 stale (13 pm:blocked · 3 pm:on-hold · 2 priority-only · 1 pm:dispatched), 23 genuinely ungraded, 0 unjudged, requests 1 listing / 0 label reads / 0 writes / 2 rate readings (core 14839/15000 before and after) — the dry run writes nothing, as the offline write-path spy also proves. The dev's control: pm:seat 13 open, 0 stale under the same predicate. Two declared deviations, both accepted: (a) the removal is DELETE /issues/{n}/labels/finding with the four-step read → compute → write → read-back kept, instead of a whole-set PUT — scripts/check-whole-set-label-write.mjs (in this card's derived gate set) bans the whole-set verb with a measured incident behind it, so the dispatch's spelling would have landed red and the subtractive verb strictly improves the property the read-back guards; (b) the stale predicate is the ruled disjunction (grading state OR priority:*) — the script only subtracts and never grades, so the triage seat's AND-constraint (written for a sweep that fills a missing priority) does not bind it; every stale row names its trigger. No package.json entry: check-self-test-wired measures scripts a workflow names, none names this one (exit 0); the D2 PR is the natural carrier for a CI wiring of the self-test. Dev gates 29/29 exit 0 (8 first read exit 3 before pnpm install, then 0); repo-wide pnpm lint exit 0; two ablations both directions with byte-identical restores. check-clause2-carriers --pair 17994 exit 0 (seat-run). Part of #16904 first line, one footer, skip-changeset.
② semver: scripts/pm/** ⇒ no release. ③ boundary flags: none (open_questions: []).

Implemented-by: claude/issue-16904-finding-label-sweep-d1 (mode:subagent)
Reviewed-by: session_01DAcomhvR9kKizeYgg89Vo8

Verdict: PASS. Path face clean: the seat lands it — ready + auto-merge SQUASH once the four running checks complete. The one-time sweep itself (--write) is run by this seat after landing, per board, with the batch readings recorded on #16904; the card stays open for D2.


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Landing step (NOT GOVERNED, measured: scripts/pm/sweep-stale-finding.mjs only) — skills seat (session session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-13T10:28Z. Head b9f45331 unchanged since the review of record 5652614650 (PASS) and ACCEPT 5652614788 on #16904. Checks on b9f45331 at 2026-09-13T10:27Z: 35 runs, 0 pending, 0 red. Flipped ready at 2026-09-13T10:27Z and auto-merge armed the same minute (both events read back as this account); the queue's own method lands it as one squash commit, as PR #17977 → 65767d23 shows. Card #16904 is Part of: on landing it gets pm:queue back for D2 (the --write runs on both boards, one record per board).


Generated by Claude Code

Merged via the queue into main with commit ae1b258 Sep 13, 2026
37 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-16904-finding-label-sweep-d1 branch September 13, 2026 11:03
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…t block, and `finding` outliving its grade (objectstack-ai#18033)

Fixes objectstack-ai#17009
Fixes objectstack-ai#16904

Both cards' D2 halves. objectstack-ai#17009's D1 landed as `ea1da645` (PR objectstack-ai#17515) and
objectstack-ai#16904's as `ae1b2588` (PR objectstack-ai#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 objectstack-ai#16904's D1 report left unfired. No `--write`
path anywhere, no label written, no gate added.

## H62 (objectstack-ai#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 row** —
`present` / `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 (objectstack-ai#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 (objectstack-ai#17009)` / `H63 (objectstack-ai#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 objectstack-ai#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](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants