Repository navigation
Nothing runs check-half-states.mjs --self-test in this repo — add one step to the build job #244
Description
Filed by the repo:objectos seat (objectstack#9831) at the ACCEPT of PR #237 (#165), answering an open question that PR's dev raised and correctly declined to act on. Graded pm:queue and adjudicated — the option is chosen, so this is transcription, not a design question.
The gap, confirmed empirically rather than by reading
PR #237 landed scripts/pm/check-half-states.mjs, which declares --self-test and carries 1551 cases. Nothing in this repo runs them.
The self-test registry is tools/ci-scripts/run-self-tests.mjs. It sets const SCRIPTS = join(ROOT, '.github/scripts') and scans that directory top level only (readdirSync without recursive, plus an isFile() filter). A script at scripts/pm/ is outside it by construction.
Proven by running it, not by inspection: after #237 landed, pnpm turbo run test still prints ✓ 4 self-test(s) passed — not 5.
This cuts both ways, and both halves matter:
- Good: ci: install the parameterised half-state patrol from objectstack #237 does not trip this repo's rule that a
.github/scripts/*.mjsdeclaring an unregistered--self-testreddens the requiredbuildjob. - Bad: Install the parameterised half-state patrol (sweeper + workflow pair from objectstack PR #11294) #165's own executable acceptance criterion —
--self-testgreen — is enforced by nothing from now on.
Direction, adjudicated — option B. Do not re-open it
Add a single step to the existing build job: node scripts/pm/check-half-states.mjs --self-test. One line, about a second, touching no registry and no scan surface.
The two rejected options and why, so nobody re-litigates:
- ⛔ Not "teach the registry about
scripts/pm/." The registry's top-level-only scan is load-bearing — it is what lets.github/scripts/lib/exist without tripping the unregistered-self-test rule. Widening the scan surface is a change to this repo's gate topology and is not worth taking on for one script. - ⛔ Not "leave it unenforced." The argument for leaving it is real — the file is authored upstream under a single-writer rule, objectstack runs the 1551 cases on every change there, and this copy is byte-identical by policy. But that depends on the copy staying byte-identical, and nothing here checks that either. The two silences compound: a hand-edit to this copy would be caught by no gate in either repo.
Why B buys more than it looks like
PR #237's own ablation is the evidence. Mutating DEFAULT_SWEEP_REPO in the copy turned the self-test red (2 of 1551 cases). So running these cases here is a drift detector for the class of edits that change behaviour — not a complete hash-pin, but it collapses the compounding silence above into one much narrower gap.
Scope
One step in .github/workflows/ci.yml's existing build job. ⛔ Do not touch tools/ci-scripts/run-self-tests.mjs. ⛔ Do not touch the three files #237 landed — they are verbatim upstream copies and their blob OIDs must keep matching objectstack origin/main.
Explicitly out of scope: the hash-pin
A true drift check — asserting this copy still matches upstream byte for byte — would require objectos CI to read objectstack's tree, which is a cross-repo credential question. That is a design decision deserving its own card, not a rider on this one. Not filed yet; file it if the drift half turns out to matter in practice.
Verification
The step should be demonstrated to actually run: after the change, the build job's log must show the self-test's own verdict line (✓ check-half-states self-test: 1551 cases pass.). A green build alone does not prove the step executed — quote the line.
Re-check
git show origin/main:.github/workflows/ci.yml | grep -n 'check-half-states'
pnpm turbo run test --force # does it still say 4 self-test(s), or 5?
Related: #165 (the install), PR #237 (where the gap was measured and the ablation run), #242 (the maintainer's anchor-variable step).
Activity
os-bill commentedon Sep 2, 2026
Claim: PM loop round 3⚠️ Note for the dev: this card's own change alters the build job, so its verification is partly self-referential — see the card's Verification section.
Session:
session_01ChPQM8jamxLUfUAxwFpJ8SBranch:
claude/issue-244-run-half-state-self-testWorktree:
objectos-issue-244Domain:
repo:objectos(single-lane repo — this seat does not producedomain:*)File surface:
.github/workflows/ci.yml(stop on breach; explain in the report). ⛔ Not governed surface —.github/**andscripts/**are explicitly off it, so this lands through the queue like any other PR.Container & model:
S,mode:subagent,model: opus. Tier by PM judgment —scripts/pm/dispatch-gates.mjsdoes not exist in this repo.Clause-②: no
Serial constraints cleared: none in flight. The sibling dispatched alongside (#249) is pinned to
content/docs/quickstart.mdxand cannot reach this file. No open PR touchesci.yml.Dispatch-time premise re-check — both legs hold at
origin/main@562b9e6(card filed at6aad277, five commits back):git show origin/main:.github/workflows/ci.yml | grep -n 'check-half-states'→ 0 hits. The gap is still open. Control-probed rather than accepting a bare zero: the same file yields thebuild:job at:46with named steps at:69and:91, so the pathspec reaches live content and the zero is real.tools/ci-scripts/run-self-tests.mjsstill scans top level only —SCRIPTS = join(ROOT, '.github/scripts')at:73,readdirSyncwithoutrecursiveat:123, and the file's own comment at:95-96states the top-level-only property is deliberate. So the "⛔ do not widen the registry scan" carve-out is still load-bearing, not a stale precaution: widening it is what would let.github/scripts/lib/trip the unregistered-self-test rule.Nothing is half-done and no part of this card has been overtaken.
Generated by Claude Code
os-bill commentedon Sep 2, 2026
Claim (dev): implementing #244.
Session:
session_01ChPQM8jamxLUfUAxwFpJ8SBranch:
claude/issue-244-run-half-state-self-test(pushed empty at562b9e6as a write-route probe before any edit — probe passed)Worktree:
objectos-issue-244File surface:
.github/workflows/ci.ymlonly. Assignee untouched (PM owns it).Re-verifying both premise legs at
562b9e6myself rather than inheriting the dispatch reading; carve-outs honoured (no edit totools/ci-scripts/run-self-tests.mjs, no edit to the three files PR #237 landed, no partial hash-pin).Generated by Claude Code
os-bill commentedon Sep 2, 2026
os-dev-report
{ "issue": 244, "status": "done", "branch": "claude/issue-244-run-half-state-self-test", "pr": "https://github.com/objectstack-ai/objectos/pull/259", "head_sha": "d3bd9c6", "premise_still_valid": true, "files_changed": [".github/workflows/ci.yml"], "summary": "Added one step to the existing build job in .github/workflows/ci.yml running `node scripts/pm/check-half-states.mjs --self-test` (adjudicated option B, transcribed). 23 added lines in one file, 3 of them the step and 19 a comment recording why it is a step and not a registry entry, what it buys (behaviour-drift detector) and what it is not (a hash-pin). Placed after `pnpm install --frozen-lockfile` and before the zh-Hant check: the script is zero-dependency (node builtins plus one relative import of scripts/invoked-as.mjs) and takes about a second, so it sits with the checks that need no build. `shell: bash` follows the repo's existing convention for --self-test steps (ci.yml:33, translations.yml:80). No changeset (this repo has no packages/ and no changeset flow); no labels applied (no skip-changeset mechanism here).", "premise_recheck": { "leg_1_ci_yml_has_no_caller": "HOLDS. `grep -n 'check-half-states' .github/workflows/ci.yml` at 562b9e6 gave 0 hits (grep exit 1). Control-probed rather than trusting a bare zero: same file yields `build:` at :46 and named steps at :33, :42, :69, :91, so the pathspec reaches live content.", "leg_2_turbo_test_says_four": "HOLDS. `pnpm turbo run test --force` printed exactly `✓ 4 self-test(s) passed` — not 5. `check-half-states` occurs 0 times anywhere in that run's output.", "leg_3_my_own_addition": "Checked beyond the card, and it does NOT falsify it. .github/workflows/half-state-patrol.yml IS a standing caller of the script, but of the live sweep, not of --self-test (its single `self-test` occurrence is at :248, inside a JS comment string). Repo-wide across all four workflows nothing invoked `check-half-states.mjs --self-test`. Corroborating: turbo.json's `test` task lists $TURBO_ROOT$/.github/scripts/** as an input and does not list scripts/pm/**.", "registry_carveout_still_load_bearing": "HOLDS. tools/ci-scripts/run-self-tests.mjs unchanged: SCRIPTS = join(ROOT, '.github/scripts') at :73, readdirSync without `recursive` at :123, deliberate per its own comment at :95-96." }, "self_test_verdict_line": { "line": "✓ check-half-states self-test: 1551 cases pass.", "observed_where": "LOCAL RUN ONLY, not CI. The step was parsed out of the committed YAML (run = `node scripts/pm/check-half-states.mjs --self-test`, shell = bash — extracted programmatically, not retyped) and executed from the repo root under the runner's own shell `bash --noprofile --norc -eo pipefail`. Exit 0, captured by redirect-then-capture, never through a pipe.", "what_is_NOT_claimed": "This is not evidence that the CI step fired. This change alters the very job that will run it, so CI's own execution is only observable after PR #259 opens; per the dispatch contract I report at draft-PR time and do not wait for CI. Expected once CI runs: the build job gains a 5th step named `Half-state sweeper self-test` whose log carries the line above." }, "gates": [ { "gate": "pnpm turbo run test --force --concurrency=2 (at d3bd9c6, via the shared verify lock)", "own_conclusion": "`✓ 4 self-test(s) passed` and `Tasks: 1 successful, 1 total`; lock's own line: `os-verify-lock: VERDICT command-exit 0 · held the lock 2s · waited 69s`. The count staying at 4 is the mechanical proof the registry was NOT touched." }, { "gate": "the new step, verbatim from the committed YAML, under the runner shell (at d3bd9c6)", "own_conclusion": "`✓ check-half-states self-test: 1551 cases pass.` exit 0" }, { "gate": "ci.yml YAML parse (PyYAML safe_load, at d3bd9c6)", "own_conclusion": "parses; build job has 10 steps, the new one 5th, immediately after `pnpm install --frozen-lockfile`" }, { "gate": "control-byte scan of the edited file (grep -naP over C0/DEL, at d3bd9c6)", "own_conclusion": "no match (grep exit 1); `file` reports UTF-8 text" } ], "ablation": { "claim_tested": "the 1551 cases can go red, so the added step is a live gate rather than a permanently-green one", "method": "Run on a THROWAWAY COPY in scratch (scripts/invoked-as.mjs + scripts/pm/check-half-states.mjs replicated), never on the repo file. This was deliberate: it honours the carve-out AND removes the restore leg entirely instead of relying on it.", "mutation_confirmed_on_disk": "anchored on the exact text, both directions: removed text count 1 -> 0, injected text count 0 -> 1; copy blob OID 9ffcdb76 -> b922f997. The copy was byte-identical to the repo file before mutation (same OID 9ffcdb7646c3b3aaceab2e81596f1c55c28a6984).", "rebuild": "none required — this is a plain node script executed directly, with no dist/ or build step between source and the run, so there is no stale-artifact path.", "result": "`✗ check-half-states self-test: 2 of 1551 case(s) failed.` exit 1, the two failures being the DEFAULT_SWEEP_REPO cases. Reproduces PR #237's documented 2-of-1551 result exactly. Re-run under `bash --noprofile --norc -eo pipefail`, still exit 1 — so a behaviour-changing edit reddens the build job." }, "carve_outs": { "run_self_tests_mjs": "NOT TOUCHED", "three_files_237_landed": "NOT TOUCHED. `git diff --name-only HEAD` over scripts/pm/check-half-states.mjs, scripts/invoked-as.mjs and .github/workflows/half-state-patrol.yml is empty; the pinned script's blob OID is 9ffcdb7646c3b3aaceab2e81596f1c55c28a6984 before and after all verification, identical to HEAD:scripts/pm/check-half-states.mjs.", "cross_repo_hash_pin": "NOT ATTEMPTED, not even partially.", "file_surface": "`.github/workflows/ci.yml` only — `git status --porcelain` showed that one file at every checkpoint. No breach." }, "deviations": [ "Heavy runs went through objectstack's scripts/pm/os-verify-lock.sh by absolute path. objectos has no lock entry point of its own, but the container is shared with parallel agents, so the objectstack entry point is the container's serialization mechanism. Both runs reported their own VERDICT line; the second waited 69s in the queue.", "The ablation was run on a throwaway copy rather than in place. This is a deviation from the usual mutate-in-place-then-restore shape, taken because the file is under a do-not-touch carve-out; it is strictly safer (no restore leg to fail) and the repo file's blob OID is unchanged as proof.", "The verdict line was observed locally only. No CI observation is reported, per the dispatch contract that the report lands at draft-PR time." ], "tests": "See `gates` and `ablation` above; all four gates re-run at the final commit d3bd9c6 with a clean tree (`git status --porcelain` empty). Not run: apps/docs type-check/build/lint — the diff is a workflow file, which is not an input to any turbo task (turbo.json's task inputs cover content/docs, .github/scripts and apps/docs/lib/i18n.ts) and `lint` is `next lint`, which does not read .github/workflows. This narrowing is declared, not silent: CI runs the farm regardless.", "mcp_calls": "6 — issue_read(get), issue_read(get_comments), add_issue_comment(claim), create_pull_request, pull_request_read(get, the mandated body read-back), add_issue_comment(this report)", "open_questions": [], "out_of_scope_findings": [ "NONE FILED, deliberately. The one candidate is the cross-repo hash-pin, and the card itself defers filing it ('Not filed yet; file it if the drift half turns out to matter in practice'), so filing it now would contradict the card rather than serve it. Recording the observation instead: after this PR the remaining gap is byte-level drift that does not change behaviour — an edit to the copy that alters comments or prose passes the 1551 cases. That residual is what a hash-pin would cover.", "Non-defect observation, no card warranted: turbo.json's `test` task inputs list $TURBO_ROOT$/.github/scripts/** but not scripts/pm/**, so turbo would replay a cached green for the ci-scripts test task across edits to scripts/pm/. This does not affect the added step, which is a plain CI step and not a turbo task — noted only so nobody later moves the step into turbo without noticing." ] }Generated by Claude Code
os-bill commentedon Sep 2, 2026
ACCEPT — PR #259
Reviewer of record:
session_01ChPQM8jamxLUfUAxwFpJ8S,repo:objectosseat, round 3. Checked against GitHub, not the report.main,Fixes #244✓get_files).github/workflows/ci.yml.github/**is not governed here. Clears for the queuebuild(required)successond3bd9c6Node floorsuccessOwnership & freshnesstranslations.ymlis path-filtered tocontent/docs, and this PR touches only.github/. Verified, not assumedThe card's real acceptance bar was CI, and the dev could not reach it. I did.
The card is explicit that a green build does not prove the step executed — it demands the self-test's own verdict line from the build job. The dev was honest about the limit of what it could observe: it extracted the
run:line programmatically from the committed YAML, executed it under the runner's own shell (bash --noprofile --norc -eo pipefail), got✓ check-half-states self-test: 1551 cases pass.exit 0, and then explicitly stated "this is not evidence that the CI step fired", because the change alters the very job that would run it.That gap is mine to close as reviewer, and the PR being open closes it. From the run's own job record:
So the step fires in real CI, at the intended position. To be precise about who verified what: I verified the step ran and passed in CI; the dev verified the verdict-line text locally. Together those cover the card's bar. I did not read the line out of the CI log itself — it sits early in a 2158-line log that tailing does not reach — and I am not claiming otherwise.
The ablation is the part that makes this a gate rather than decoration
A step that can never go red is worse than no step, because it reads as coverage. The dev proved the 1551 cases can fail — and did it on a throwaway copy in scratch rather than mutating the repo file, because that file is under a do-not-touch carve-out. That is better than the usual mutate-then-restore shape: it removes the restore leg entirely instead of depending on it. The mutation was anchored on exact text in both directions (removed 1→0, injected 0→1) with the copy's blob OID moving
9ffcdb76→b922f997, and the result reproduced #237's documented figure exactly:✗ 2 of 1551 case(s) failed, exit 1.Carve-outs verified by OID, not by assertion:
scripts/pm/check-half-states.mjsis byte-identical before and after all verification (9ffcdb7646c3b3aaceab2e81596f1c55c28a6984, matchingHEAD), andrun-self-tests.mjsis untouched. The mechanical proof of the latter is thatpnpm turbo run teststill prints✓ 4 self-test(s) passed, not 5 — the registry genuinely was not widened, which was the whole point of choosing a step over a registry entry.One reading beyond the card that could have falsified it, and didn't
The dev checked whether
half-state-patrol.ymlalready ran the self-test — it would have made this card moot. It does not: that workflow calls the live sweep, and its singleself-testoccurrence is at:248inside a JS comment string. Corroborated from a second angle:turbo.json'stestinputs list.github/scripts/**and notscripts/pm/**. Going looking for the thing that would invalidate your own card is the right instinct.On the 19:3 comment-to-code ratio: proportionate here and consistent with this repo's convention (
half-state-patrol.ymlcarries a far longer header). The comment records why a step and not a registry entry, what it buys (behaviour-drift detector) and what it explicitly is not (a hash-pin) — which is what stops the next agent from "fixing" it by widening the scan.⛔ Cross-repo hash-pin correctly not attempted, not even partially. Correctly not filed either: the card itself defers filing it until the drift half matters in practice, so filing now would contradict the card. The residual gap — a byte-level edit that changes no behaviour still passes — is recorded on the card rather than dropped.
⛔ No rework. Landing.
Generated by Claude Code