Uh oh!
There was an error while loading. Please reload this page.
ci(pm): install the half-state patrol, ported from objectstack - #5984
Merged
Conversation
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.
This was referenced Aug 24, 2026
yinlianghui-tw
marked this pull request as ready for review
August 24, 2026 11:04
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#5791
Ports the half-state patrol pair from objectstack (PR objectstack-ai/objectstack#11294) into this repo. Verified at
2510fd75d.state: openfinding — read this firstThe card's standing instruction was to check the objectstack predicate for a
state: openfilter before landing, because ~816 closed cards here carrypm:dispatched. Both halves of that check came back, and the answer is split:state=open.listIssues()pinsstate=openin the query string. The closed population is invisible to them. Nothing to add.state=closed&sort=updated, capped atCLOSED_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
pm:dispatched, repo-wide2026-08-18T03:36:15Z…2026-08-24T09:48Z= 6.2 dayspm:dispatchedpm:queuepm:blocked/pm:on-holdUpstream 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 exceedsMARKDOWN_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.h22ClosedCardPmResiduestill returns the same sentence for the same input, pinned by a test. Re-enabling is one variable, not a code change.scripts/pm/check-half-states.mjsremains 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.#4690shape the sweeper's own header argues at length). Pinned in both directions.⛔ 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.comreturns 403 to rawfetchhere, which is the#7412class-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: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:
The first real run on a runner is this PR's own —
half-state-patrol.ymlsubscribes topull_requestfor 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.PM_SWEEP_CLOSED_WINDOW_PAGES: '0'in the workflowDEFAULT_SWEEP_REPO→objectstack-ai/objectuiobjectstack-ai/objectstack. Carried over, a barenode scripts/pm/check-half-states.mjshere 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.scripts/invoked-as.mjsported alongsidepaths:filter so a change to it exercises the patrol.DEFAULT_SWEEP_REPObecause upstream the two strings were equal. Now pinned to the literal; the property asserted is unchanged.closedWindowDisabledadded tosummaryLine's JSDoccountstypetsconfig.scripts.jsontype-checks the test against inferred types; the producer declares the field rather than the consumer casting.Live half-state sweep→NOT_A_GATEindependabot-merge-gate.mjsscripts/__tests__/dependabot-merge-gate.test.tsrequires every PR-producing check be classified. Report-only, and its path filter never matches a Dependabot bump.content/docs/guide/ci-cd-pipeline.mdsection + inventory rowRepo-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); thetrackinglabel used for the anchor exists in the sweeper's ownH13_EXEMPT_LABELS;readTrackedFiles'sgit ls-filesoracle works here (5,035 tracked files in the dry run).Not adapted, deliberately: the
ANCHOR_ISSUEexpression is carried verbatim. Its objectstack literal is guarded bygithub.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.tsasserts the workflow contains noaddLabels,removeLabel,setLabels,createComment,addAssigneesorissues.create(; thatpermissions:is exactlycontents: read+issues: write; thatexit 1is reachable only via the sweep's own non-zero exit code; and that both board-writing steps carry thepull_requestguard.Create a
tracking-labeled anchor issue in this repo and set the repository variableHALF_STATE_ANCHOR_ISSUEto 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
2510fd75dvitest 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:scriptsnode 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.mjseslint --no-inline-config(4 changed files,--format json)files: 4 errors: 0 warnings: 0No changeset — following
check-changeset-presence.mjs's own verdict (6 files changed, 0 under thesrc/of a released package). ⛔ Noskip-changesetlabel: 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 jsonoutput, andeslint --print-configreports noparserOptions.projectand noprojectServiceanywhere 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 cleanassertions were proven falsifiable: replacingcounts.closedWindowDisabledwithfalseinsummaryLineturned 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 (injected1, original0) rather than by the editor's exit code, and the restore leg was confirmed the same way (injected0, original1) before re-running green at 1,077. The script executes from source with no build step, so nodist/staleness applies.Generated by Claude Code