Skip to content

ci: install the parameterised half-state patrol from objectstack - #237

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-165-half-state-patrol-install
Aug 29, 2026
Merged

ci: install the parameterised half-state patrol from objectstack#237
os-zhuang merged 1 commit into
mainfrom
claude/issue-165-half-state-patrol-install

Conversation

@claude

@claude claude Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Fixes #165

Installs the parameterised half-state patrol from objectstack so this repo's board gets machine-swept for the dispatch protocol's label/assignee half-states. It never has been — and the read-only verification run below found 82 of them on the live board.

It is a trio, not the pair the card names

scripts/pm/check-half-states.mjs line 803 is:

import { isEntrypoint } from '../invoked-as.mjs';

scripts/invoked-as.mjs is a separate module one directory up. Copying only the two named files ships a sweeper that dies at import — a loudly red patrol, but a broken install.

Chosen: bring the third file, do not localise the import. That keeps check-half-states.mjs byte-identical to upstream, which is the single-writer premise the card rests on, and it follows the precedent that module's own header records: objectui already carries scripts/invoked-as.mjs under the same name, ported from the same source, and the header calls the pairing a cross-repo obligation. Localising the import would have been the one edit that makes future drift-detection between the copies impossible to do by hash.

This repo had no scripts/ tree at all; this PR creates it.

Diff size, stated plainly

file lines bytes
scripts/pm/check-half-states.mjs 12,966 816,622
scripts/invoked-as.mjs 260 12,654
.github/workflows/half-state-patrol.yml 330 17,178
total 13,556 846,454

That is what "verbatim, single-writer upstream" costs. Nothing was tidied, reformatted or trimmed. Zero localised lines — see the audit below for why that turned out to be enforceable rather than merely preferred.

Verbatim, certified by git rather than by eye

All three files have the same git blob OID and the same file mode as objectstack origin/main:

100644 73ef91c86275604660c650d861cb71594a672d5f  .github/workflows/half-state-patrol.yml
100644 d545f8d2e3b8e4d9b1afd12bba4fb76042422471  scripts/invoked-as.mjs
100644 9ffcdb7646c3b3aaceab2e81596f1c55c28a6984  scripts/pm/check-half-states.mjs

sha256, for a checkout that cannot see the other repo:

85d8a03f5754ac76d31a4ce322cd7f7b4739fbdc364a9592fedb5233549aaa9e  .github/workflows/half-state-patrol.yml
90f72bf45a2fd158b19d5774269eb96a6b8b61540251ce68c29ddce7c93bfaa7  scripts/invoked-as.mjs
449a0aec0cfa36738e0fc5651ec978faf846316522daeb9ca6efa1714daf6ac9  scripts/pm/check-half-states.mjs

Source: objectstack origin/main at a81aa9d. (Per-file authoring commits are not quotable — that checkout is shallow, so git log -- path returns the shallow boundary rather than real provenance. The hashes above are the honest identity statement.)

The evidence that matters: a real sweep, naming THIS repo

The upstream header warns that a copy with a hardcoded default could sweep the wrong board and write a fully green report about it. So the install was proved by running the sweep read-only against this repo — no anchor write, no label write, no state change.

$ GITHUB_REPOSITORY=objectstack-ai/objectos \
  node scripts/pm/check-half-states.mjs --format=markdown --provenance='...'
EXIT=0

The rendered summary line, verbatim from that run:

check-half-states: swept 8 open pm-/p0-labeled issue(s), 11 open issue(s) in the unscoped pass (H13–H15, H18), 5 open PR(s) (merge state read on 5 of 5 H16 candidate(s)) and 120 recently-merged PR(s) in objectstack-ai/objectos — 82 half-state(s) found.

It names objectstack-ai/objectos, and the rows are unmistakably this board: H19 on #75 (blocked on #74, closed 2026-08-18, and on #68, still open — a partial discharge), H26 on #75 (#68 is needs-user-decision, so that leg can never close), H14 on #68, and a long tail of H22 rows for closed cards still carrying pm:dispatched. The H17 trigger-file oracle read 565 tracked files — this repo's, via git ls-files in the checkout it runs in.

Resolution table, measured rather than reasoned:

environment swept repo source
runner (GITHUB_REPOSITORY alone, what Actions sets) objectstack-ai/objectos GITHUB_REPOSITORY
runner + the workflow's explicit belt-and-braces pass objectstack-ai/objectos PM_SWEEP_REPO
bare terminal in a local checkout, neither set objectstack-ai/objectstack default

That third row is the one residual objectstack-specific value in the tree, and it is discussed below.

Card acceptance criterion

$ node scripts/pm/check-half-states.mjs --self-test
EXIT=0
✓ check-half-states self-test: 1551 cases pass.

$ node scripts/invoked-as.mjs --self-test
EXIT=0
✓ invoked-as self-test: 11 cases pass (real symlink, different-name symlink,
  percent-encoding path, and both import directions).

⚠️ Nothing in this repo will run that first command after this PR merges. The self-test registry is tools/ci-scripts/run-self-tests.mjs; it scans .github/scripts/ top level only, so a script at scripts/pm/ is outside it by construction. Confirmed empirically rather than by reading — pnpm turbo run test still reports ✓ 4 self-test(s) passed, not 5. Cuts both ways: this PR does not trip the repo's unregistered-self-test rule, and the card's executable criterion is enforced by nothing going forward. Wiring it in would change this repo's gate topology and is deliberately not done here; see the report on #165 for the follow-up recommendation.

Testing the "change NOTHING here" assumption

The upstream header claims a sibling repo needs no edit at all. Audited against this repo's actual shape (three workflows before this one, no scripts/ tree). It holds — with one caveat that lands on the maintainer step, not on the files.

Verified clean:

  • Repo identity — resolved from github.repository / GITHUB_REPOSITORY, never hardcoded. Measured above.
  • Anchor expressionvars.HALF_STATE_ANCHOR_ISSUE || (github.repository == 'objectstack-ai/objectstack' && '9857') || ''. The literal is guarded by repo name, so here it evaluates to empty and the job refuses loudly. The dead branch is kept deliberately: it costs nothing, and removing it would make the two copies diverge by hash for no behavioural gain. The folded scalar survives the copy — YAML-parsed to a single-line expression, confirmed.
  • Actionsactions/checkout@v7 and actions/setup-node@v7 match what this repo's three existing workflows already pin. actions/github-script@v9 is new to this repo; first-party, but it is the one uses: here without local precedent.
  • Node — pins node-version: '22', matching .node-version and the >=22.12.0 floor. check-node-floor.mjs does not read workflows, so this adds no declaration it governs (its gate stays green, output below).
  • Permissionscontents: read + issues: write. No existing workflow here requests issues: write; if this repo's default workflow-token setting is read-only, confirm the first scheduled run can write before treating a failure as a code defect.
  • Concurrency / schedule — group half-state-patrol collides with nothing (deploy-docs, translations-* are the only groups here) and no other workflow uses schedule at all.
  • Label vocabulary — the predicates read pm:queue, pm:blocked, pm:dispatched, pm:on-hold, pm:blocking, pm:retriage, needs-user-decision, priority:p0, domain:*. Every one exists on this board, which the live run demonstrates by firing rows that use them.
  • No new gate surfacescripts/ is not a pnpm workspace (apps/*, tools/*), is in no turbo task's inputs, and there is no root eslint/prettier. The tree adds zero CI cost outside its own workflow.
  • No install — the sweeper imports only node:process, node:child_process and global fetch, and shells out to git ls-files once. The workflow runs no pnpm install, so it cannot fail on a lockfile.

The one caveat — a label this repo does not have. The recipe says to open a tracking-labelled anchor issue. This repo has 24 labels and tracking is not among them, so that instruction cannot be followed literally until someone creates the label. It is not load-bearing for correctness: tracking is consulted by exactly one predicate (H13_EXEMPT_LABELS), and H13 only fires on a card carrying a domain:* label with no pm-state label — so an anchor with no labels is safe from every predicate either way. ⛔ But do not substitute domain:docs to make it look triaged: that combination is precisely what makes the anchor issue appear as a finding in the sweep it hosts, two hours later.

Why nothing was localised, measured

The tempting edit is DEFAULT_SWEEP_REPO = 'objectstack-ai/objectstack' (the third row of the resolution table). It is unreachable on a runner — Actions always sets GITHUB_REPOSITORY, and the workflow passes PM_SWEEP_REPO on top — so it only affects a maintainer running the script by hand from a local checkout here, which would read objectstack's board.

Ablation, to find out whether "verbatim" is a preference or a constraint: commit first, mutate the constant to objectstack-ai/objectos, confirm the change landed on disk (injected line present 1, removed line present 0, blob hash 9ffcdb7 to 81bb724), run, restore via git checkout HEAD -- path, confirm restoration (git diff HEAD empty, hash back to 9ffcdb7).

MUTATED self-test EXIT=1
  ✗ sweep repo: the objectstack runner resolves to the pre-change constant
      (got "objectstack-ai/objectstack", want "objectstack-ai/objectos")
  ✗ sweep repo: …and with only GITHUB_REPOSITORY set, identically
      (got "objectstack-ai/objectstack", want "objectstack-ai/objectos")
✗ check-half-states self-test: 2 of 1551 case(s) failed.

RESTORED self-test EXIT=0
✓ check-half-states self-test: 1551 cases pass.

So localising that constant turns the card's own acceptance criterion red. Upstream pins it by name; verbatim is enforced, not merely asked for. Left alone.

Expected first-run behaviour, and why it is not a defect

Until a maintainer creates the anchor issue here and sets the repository variable HALF_STATE_ANCHOR_ISSUE (Settings, then Secrets and variables, then Actions, then Variables), a scheduled run will fail loudly at "Resolve the anchor issue" with No anchor issue configured for objectstack-ai/objectos. The sweep still runs and its findings are preserved in the run summary. That is the designed behaviour and this PR deliberately adds no fallback, no default anchor number and no silent skip.

The pull_request trigger is the exception and applies to this PR: it runs the sweep, skips the anchor resolution and the anchor write entirely, and publishes the rendered body to the run summary. A PR must never rewrite the board's pinned view.

Verification

All at a85b502, working tree clean, exit codes captured before any pipe.

command exit verdict line
node scripts/pm/check-half-states.mjs --self-test 0 ✓ check-half-states self-test: 1551 cases pass.
node scripts/invoked-as.mjs --self-test 0 ✓ invoked-as self-test: 11 cases pass
pnpm install --frozen-lockfile 0 Done in 2.8s using pnpm v10.28.2
pnpm turbo run build --force 0 Tasks: 1 successful, 1 total
pnpm turbo run test --force 0 ✓ 4 self-test(s) passed
node .github/scripts/check-locale-surface.mjs 0 ✓ every advertised URL has a source file and every source file is advertised
node .github/scripts/check-node-floor.mjs --self-test 0 ✓ self-test: 17 rule case(s), 24 satisfies case(s) and 18 range case(s)
node .github/scripts/check-node-floor.mjs 0 ✅ Every declared floor clears what the dependency tree requires
node apps/docs/scripts/gen-zh-hant.mjs --check 0 ✓ zh-Hant: 73 generated file(s) match the zh-Hans sources byte for byte.
control-byte scan of all three new files 1 (no match) clean

check-node-floor and gen-zh-hant --check were not in the dispatch list; they were re-derived from the actual diff (a .github/workflows/** edit) and run anyway. No changeset flow and no packages/ in this repo.


Generated by Claude Code

Adopts the patrol by verbatim copy, so this repo's board gets machine-swept
for the dispatch protocol's label/assignee half-states — it never has been.

Three files, not the two the card names: check-half-states.mjs:803 imports
`../invoked-as.mjs`, a separate module one directory up, so the two-file copy
fails at import. Bringing the third file keeps the sweeper byte-identical to
upstream rather than localising its import.

All three are byte-identical to objectstack origin/main (sha256 recorded in
the PR body). The file stays authored upstream; this copy tracks it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TUrhcggSFrYctvp5dsV1A
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

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