Skip to content

ci(pm): install the half-state patrol, ported from objectstack - #5984

Merged
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-5791-install-half-state-patrol
Aug 24, 2026
Merged

ci(pm): install the half-state patrol, ported from objectstack#5984
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-5791-install-half-state-patrol

Conversation

@yinlianghui-tw

Copy link
Copy Markdown
Collaborator

Fixes#5791

Ports the half-state patrol pair from objectstack (PR objectstack-ai/objectstack#11294) into this repo. Verified at 2510fd75d.

⚠️ The state: open finding — read this first

The card's standing instruction was to check the objectstack predicate for a state: open filter before landing, because ~816 closed cards here carry pm:dispatched. Both halves of that check came back, and the answer is split:

Verdict
Every label-driven collector (H1–H21, H23–H27)Already state=open.listIssues() pins state=open in the query string. The closed population is invisible to them. Nothing to add.
H22 onlyReads closed issues deliberately — it is the sweeper's single closed-card reader, state=closed&sort=updated, capped at CLOSED_ISSUE_WINDOW_PAGES = 4 (400 rows). Not a missing filter; a bounded window that is correct upstream and wrong here.

So the premise ("the filter may be absent") is half-falsified: the filter is present everywhere it was expected, and the real hazard is one predicate whose boundedness — not its state scope — is calibrated to a different board.

Re-measured on this repo, 2026-08-24

MeasurementValue
Closed cards carrying pm:dispatched, repo-wide815 (prior seat measured 813; card said ~816 — figure holds)
Closed issues total1,760
H22 window (4 pages)400 rows, spanning 2026-08-18T03:36:15Z2026-08-24T09:48Z = 6.2 days
…carrying pm:dispatched259
…carrying pm:queue86
…carrying pm:blocked / pm:on-hold1 / 1
Projected H22 rows on first run≈ 347 of 400 (~87%)

Upstream measured 129 of 500 (26%) on its own board and called that a live duty. Here it is ~87%. That inversion is not this repo being behind on a chore — stripping pm:* on close was never this lane's practice (#5791's own thread records two seats reading the same corpus and landing on opposite conventions). A row firing on ~87% of what it reads is the convention restated 347 times, and at ~300 chars/row it exceeds MARKDOWN_BODY_BUDGET (60,000) on its own, trimming every other predicate's findings out of the anchor body. Dead on arrival, exactly as the card predicted.

What was done about it

PM_SWEEP_CLOSED_WINDOW_PAGES: '0' on the workflow's sweep step disables the closed reader in this install.

  • The predicate is untouchedh22ClosedCardPmResidue still returns the same sentence for the same input, pinned by a test. Re-enabling is one variable, not a code change.
  • The script's default stays upstream's 4, so scripts/pm/check-half-states.mjs remains a straight re-sync target and a future verbatim copy cannot silently re-enable anything. The divergence lives in the workflow as one visible line.
  • A disabled reader renders as UNREAD, not clean. It does not report "H22 read 0" — that number would be indistinguishable from a clean reading of a surface nothing looked at (the #4690 shape the sweeper's own header argues at length). Pinned in both directions.
  • A malformed value is refused (exit 2), never silently defaulted back to 4.

⛔ Re-enabling is a backfill decision (strip the historical residue first), never a quiet default flip.

Dry run

The live sweep cannot run from this container — api.github.com returns 403 to raw fetch here, which is the #7412 class-1 condition the workflow header documents and the reason the caller had to move to a runner. The ported sweeper classifies that correctly rather than reporting an empty board:

$ node scripts/pm/check-half-states.mjs --probe # EXIT=3
check-half-states: PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential
`GET /rate_limit` with GITHUB_TOKEN/GH_TOKEN = prox… (len 14) -> HTTP 401.
The value carries no GitHub token prefix (`ghp_`/`gho_`/`ghs_`/`github_pat_`) — in agent
containers this is normally the proxy's own placeholder, not a credential.
Nothing was swept: no issue was listed, no predicate ran … It is not a clean board and it
is not a dirty one — it is no reading at all.

Exit 3, distinct from the unclassified failure's 2 — the "did not run ≠ clean board" property working.

The transport was then stubbed so the real code path (probe → list → predicates → render) runs offline against a miniature board of 12 closed cards shaped like this repo's:

A) closed reader ENABLED (upstream default, pages=4) EXIT=0
H22 rows rendered: 12 ← 12 of 12 closed cards, the ~87% pattern in miniature
summary: "H22 read 12 recently-closed issue(s)"
B) closed reader DISABLED (this install, pages=0) EXIT=0
H22 rows rendered: 0
summary: "H22 (closed-card `pm:*` state residue) is DISABLED in this install and NO closed
issue was read — this sweep therefore says NOTHING about closed-card residue:
that surface is UNREAD, not clean (see `resolveClosedWindowPages` and
`PM_SWEEP_CLOSED_WINDOW_PAGES` in `.github/workflows/half-state-patrol.yml`)."
C) malformed value EXIT=2
check-half-states: PM_SWEEP_CLOSED_WINDOW_PAGES="O" is not a non-negative integer.
Refusing to fall back to the default page count — silently re-opening the closed-card
reader would fill the anchor with residue nobody asked to see.

The first real run on a runner is this PR's ownhalf-state-patrol.yml subscribes to pull_request for exactly that purpose, and skips the anchor write while publishing the rendered body to the run summary.

Every adaptation, listed

These are the points where a later objectstack change will silently diverge. All are pinned by scripts/__tests__/check-half-states.test.ts.

#AdaptationWhy
1PM_SWEEP_CLOSED_WINDOW_PAGES: '0' in the workflowThe ~347-row measurement above. Script default unchanged.
2DEFAULT_SWEEP_REPOobjectstack-ai/objectuiUpstream's constant is objectstack-ai/objectstack. Carried over, a bare node scripts/pm/check-half-states.mjs here renders a fully green report about a different board — the wrong-board failure the parameterisation exists to prevent, arriving through the one input nobody sets.
3scripts/invoked-as.mjs ported alongsideThe sweeper imports it; it is objectstack-resident with no objectui equivalent. Without it the file cannot be loaded at all. Added to the workflow's paths: filter so a change to it exercises the patrol.
4Two self-test cases repinnedThey asserted objectstack throughDEFAULT_SWEEP_REPO because upstream the two strings were equal. Now pinned to the literal; the property asserted is unchanged.
5closedWindowDisabled added to summaryLine's JSDoc counts typetsconfig.scripts.json type-checks the test against inferred types; the producer declares the field rather than the consumer casting.
6Live half-state sweepNOT_A_GATE in dependabot-merge-gate.mjsPaired write forced by adding a workflow — scripts/__tests__/dependabot-merge-gate.test.ts requires every PR-producing check be classified. Report-only, and its path filter never matches a Dependabot bump.
7content/docs/guide/ci-cd-pipeline.md section + inventory rowPaired write forced by the workflow-inventory pin.

Repo-shape assumptions checked: no scripts/pm/ existed here (created — path parity keeps re-sync a straight copy); action versions are already identical (checkout@v7, setup-node@v7, github-script@v9, Node 22); the tracking label used for the anchor exists in the sweeper's own H13_EXEMPT_LABELS; readTrackedFiles's git ls-files oracle works here (5,035 tracked files in the dry run).

Not adapted, deliberately: the ANCHOR_ISSUE expression is carried verbatim. Its objectstack literal is guarded by github.repository, so it is inert here and resolves to empty — which is the designed loud failure below, not a bug.

Report-only

Per the ruling on the card, and pinned rather than left to review: scripts/__tests__/check-half-states.test.ts asserts the workflow contains no addLabels, removeLabel, setLabels, createComment, addAssignees or issues.create(; that permissions: is exactly contents: read + issues: write; that exit 1 is reachable only via the sweep's own non-zero exit code; and that both board-writing steps carry the pull_request guard.

⚠️ Manual maintainer step — the workflow fails loudly until it is done

Create a tracking-labeled anchor issue in this repo and set the repository variableHALF_STATE_ANCHOR_ISSUE to its number (Settings → Secrets and variables → Actions → Variables). Repository variables are a settings-surface write an agent cannot make.

Until then the scheduled job sweeps, preserves its findings in the run summary, and then fails with an actionable error. It will not guess a number and rewrite an unrelated card. PR runs are unaffected.

Gates run — all at 2510fd75d

GateVerdict line
vitest run --project unit scripts/__tests__Test Files 63 passed (63) · Tests 1679 passed (1679)
node scripts/pm/check-half-states.mjs --self-test✓ check-half-states self-test: 1077 cases pass.
pnpm run type-check:scriptsexit 0
node scripts/check-control-bytes.mjs✅ check-control-bytes: OK (scanned 4954 tracked text file(s); skipped 85 binary).
node scripts/check-changeset-presence.mjs✅ No source of a released package changed in this range, so no changeset is owed.
node scripts/check-doc-links.mjsLinks are valid across 13 scan roots.
node scripts/check-action-forward-parity.mjsexit 0
eslint --no-inline-config (4 changed files, --format json)files: 4 errors: 0 warnings: 0

No changeset — following check-changeset-presence.mjs's own verdict (6 files changed, 0 under the src/ of a released package). ⛔ No skip-changeset label: never a real mechanism in this repo (#4912/#3724).

Lint narrowing, declared. Repo-wide eslint . was not run; the four changed files were. The narrowing is measured, not assumed: the scanned population and file count come from eslint's own --format json output, and eslint --print-config reports no parserOptions.project and no projectService anywhere in the root config — type-aware linting is not enabled, so this diff cannot move the verdict on any file it does not touch. scripts/__tests__ ran in full above.

Ablation, with both legs verified on disk. The UNREAD, not clean assertions were proven falsifiable: replacing counts.closedWindowDisabled with false in summaryLine turned 4 cases red (got false, want true ×3, got true, want false ×1). The mutation was confirmed on disk by grepping both the injected and the removed text (injected 1, original 0) rather than by the editor's exit code, and the restore leg was confirmed the same way (injected 0, original 1) before re-running green at 1,077. The script executes from source with no build step, so no dist/ staleness applies.


Generated by Claude Code

Adopt the sweeper + workflow pair (scripts/pm/check-half-states.mjs and
.github/workflows/half-state-patrol.yml) from objectstack-ai/objectstack, plus
scripts/invoked-as.mjs, which the sweeper imports and which had no objectui
equivalent.
The port is not verbatim. H22 -- the sweeper's one closed-issue reader -- is
switched off in this install via PM_SWEEP_CLOSED_WINDOW_PAGES: '0'. Measured on
this board: 815 closed cards carry pm:dispatched, and ~347 of the 400 issues in
that reader's window carry some pm:* residue label (~87%, against the 26%
upstream measured on its own). Stripping pm:* on close was never this lane's
practice, so the row would report the convention rather than a defect and
consume the whole anchor body budget on its first run.
The script's own default stays upstream's 4 pages, so the predicate file
remains a straight re-sync; the divergence lives in the workflow as one visible
line. A disabled reader renders as "UNREAD, not clean" rather than "read 0", so
an unread surface can never be mistaken for a clean one.
DEFAULT_SWEEP_REPO is repointed at objectstack-ai/objectui so a bare terminal
run here sweeps this board instead of the one it was ported from.
Also classifies the new "Live half-state sweep" check as NOT_A_GATE in the
dependabot merge gate, and documents the workflow in ci-cd-pipeline.md -- both
paired writes that adding a workflow forces in this repo.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install the parameterised half-state patrol (sweeper + workflow pair from objectstack PR #11294)

2 participants

@yinlianghui-tw@claude