ci(pm): make the half-state patrol callable instead of copied - #19225
Merged
os-elon-musk merged 6 commits intoSep 20, 2026
Merged
Conversation
The patrol's sweeper changes on five days in six, and every sibling board ran it from a hand-made copy: the cloud board's copy was three weeks stale, which left 23 of its 38 live half-states invisible. The install instructions were a hand-kept mirror of an import graph and were measured short twice. Replace the copy with a reference. `.github/actions/half-state-patrol` is a composite action carrying the repo-agnostic body of the patrol; a sibling board pins it to a sha and passes its own constants as `with:` inputs, so the code travels with the pin and no list can be short. objectstack's own workflow becomes the first consumer and calls it by local path, which keeps a PR editing the action proven before it merges. Two steps deliberately stay in the caller: `actions/setup-node`, because `check-node-version.mjs` derives its census from `.github/workflows/` only, and the closed-card sweep, because it is the one step that was never repo-agnostic and because this workflow holds CI's only `sweep-closed-cards.mjs --self-test` invocation, which `check-self-test-wired.mjs` reads from the same directory. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
A composite action's `run:` body is template-expanded before it is a script, and a composite action may not read `secrets` at all. Spelling the expression form inside a message string -- backslash-escaped, in prose -- is therefore a load-time `Unrecognized named-value: 'secrets'` that fails the whole action before its first line runs. Measured on this repo's runner (run 35481043482): the patrol job went red at action load while the caller's other steps kept running, which is what the step's `!cancelled()` guard is for. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 20, 2026
This was referenced Sep 20, 2026
…lf-state-patrol-reusable-workflow
…ist can see it The composite action ran the sweeper as `node "$SWEEPER"` with the path living only in a step `env:` value, so `scripts/pm/dispatch-gates.mjs` derived ZERO families for the patrol even after it learned to follow a `uses: ./.github/actions/NAME` into an action's steps: an env-carried script path is read by neither spelling. Four CONTROL assertions in that tool's self-test go red on it. The invocation now stands in the action's own checkout (`steps.sources.outputs.root`) and spells the sweeper literally, which is the only spelling the derivation reads -- measured, the variable-rooted spellings all derive zero. Moving cwd off `github.workspace` moves the SUBJECT half with it, so the caller's tree is now named explicitly through `PM_SWEEP_CHECKOUT`, which is the cwd the sweeper's three local git reads take. The script still comes from this action's pinned tree and the subject still comes from the caller's workspace; which file executes did not change. One behaviour moves: a workspace that cannot be shown to serve the swept board now refuses instead of running with H57 unresolved and the tracked-file oracle unreadable. The alternative -- leaving the knob unset with cwd off the workspace -- would validate the caller's board against this action's own objectstack tree, which is the failure the knob exists to remove. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 20, 2026
os-elon-musk
marked this pull request as ready for review
September 20, 2026 16:18
os-elon-musk
enabled auto-merge
September 20, 2026 16:18
os-elon-musk
deleted the
claude/issue-18471-half-state-patrol-reusable-workflow
branch
September 20, 2026 16:43
os-elon-musk
pushed a commit
that referenced
this pull request
Sep 20, 2026
PR #19225 made the half-state patrol callable: the patrol's steps now live in the composite action `.github/actions/half-state-patrol`, and the workflow `uses:` it. This branch adds the report-only `--census` leg of the citation gate to the OLD shape, so the two sides overlapped in two places. Conflict 1 (`on.pull_request.paths` + the `permissions:` note) is additive in both directions: the branch's `scripts/check-issue-citations.mjs` trigger row and main's `.github/actions/half-state-patrol/**` glob name different files, and main's rewritten `issues: write` paragraph is kept with this branch's `pull-requests: read` paragraph appended under it. Conflict 2 is not textual. Main emptied that region -- every step in it moved into the composite action -- and the census step is the one thing there that main did not relocate. It stays a step of the CALLER, and the placement is the argument rather than an accident: - `scripts/check-issue-citations.mjs` is objectstack-only, so inside the action its "Locate the patrol sources" step would either have to name it and refuse to run in every sibling that adopted the action, or not name it and fail on a missing file there. The repo-name gate only works in the caller. - The action runs its scripts from `steps.sources.outputs.root` (the tree the action ships from) while the board's checkout is `github.workspace`. A census of the WORKSPACE tree run from inside the action would, in a sibling, census this repo's release pages and report the count under the sibling's name. - `pull-requests: read` is granted by this workflow's `permissions:` block, which a composite action cannot carry and the action's inputs do not name. One behaviour had to be spelled rather than inherited: the step's guard is now `${{ !cancelled() && github.repository == '...' }}`. In the old shape the census sat above the only step that failed the job, so it ran whatever the sweep returned; the patrol is now one `uses:` step that goes red itself, and a default `success()` would have skipped the census on exactly the runs where the patrol is down. This is the guard main gave the closed-card sweep one step up, for the reason its own comment states. Nothing else changed: the blocking diff-scoped step in `lint.yml` and the `OS_GATE_MERGE_GROUP_BASE_SHA` declaration are untouched, and the census command stays a bare literal path so the gate derivation keeps seeing it. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…and refuses a foreign one (objectstack-ai#19288) Fixes objectstack-ai#19191 Clause-②: no `scripts/pm/check-half-states.mjs` took three readings off the LOCAL checkout with plain `execFileSync` git calls and no `cwd`, so each inherited `process.cwd()` while the board it reports on is named by `PM_SWEEP_REPO`. The filer measured both directions on the same board with the same script (objectstack-ai#19191, comment 5740947564): with `cwd` in objectstack, H17's trigger-file index validated objectui's candidate paths against **objectstack's 8888** tracked files (objectui had **7945**) and H57 read NOTHING for the whole run — and the row it never read was hiding a scheduled lane dead through five consecutive fires (objectstack-ai/objectui#10010). Internally consistent, externally wrong. ## 维护者速读(草稿) **改了什么** —— 给这个巡检工具加了一个环境变量 `PM_SWEEP_CHECKOUT`,用来回答「哪个本地检出服务于这块 board」。设了它,三处本地 git 读取(H17 的 tracked-file 判据、H57 的 workflow 文件、`origin`)就都在那棵 树上读;那棵树的 `origin` 不是被扫的仓时,整轮巡检响亮拒绝(退出码 3),⛔ 绝不静默换一棵树读。不设它, 行为与今天逐字一致 —— 只多了一句:H17 页脚现在写明它到底在哪棵树上读的(路径 + `origin`)。 **为什么改** —— 这不是「跑错目录」的操作失误,而是一个没有症状的读数缺陷。跨仓巡检是常态(工具住在 objectstack,board 可以是 objectui),而 H17 的页脚正是在告诉派发席位「派发前请拿你的文件面和这张清单求 交集」。清单是拿另一个仓的文件列表验过的,只在另一个仓存在的触发文件被静默丢弃 —— 席位读到的是一份干净 清单,而那份干净是假的。H57 那半反而是诚实的:它明说自己什么都没读,代价是那一轮真有红行没人看见 —— 本 PR 的实测里那是两行,其中一行连卡都没提到。 **风险与代价(含回滚)** —— 风险面很窄:变量不设时是逐字旧行为(自测里有一条专门盯这件事),巡检 workflow 本身不设它,所以线上那条 lane 的行为不变。新增的唯一失败模式是「变量设错路径」,而那正好是本卡 要的那个响亮拒绝。回滚 = revert 这两个 commit,没有数据迁移、没有已发布面(`scripts/pm/**` 不随任何包 发布,故无 changeset)。 **席位意见** —— **你要做的** —— 无需维护者动手。若希望巡检 workflow 把这棵树写明(目前不需要,因为 runner 的检出就是被扫 的仓),那是 `.github/workflows/half-state-patrol.yml` 的一行 env,已写在下面的 Acceptance notes 里,留给 单独一个由人合的改动 —— 本 PR ⛔ 不碰 workflow(objectstack-ai#19259、objectstack-ai#19225 正在改它)。 ## What changed, mechanically - **`PM_SWEEP_CHECKOUT`** — one knob, beside `PM_SWEEP_REPO` and `PM_SWEEP_CLOSED_FLOOR` in `--help`, carrying the PATH of the checkout that serves the board. `resolveSweepCheckout(env)` resolves and trims it; whitespace is unset. - **One git read site.** All three readers (`readTrackedFiles`, `readRepoRoot`, `readOriginUrl`) now go through a single `gitRead(args, extra)` helper that passes that path as `cwd`. That is the only `execFileSync` git call left in the file, and the self-test pins the count at one — so a fourth reader cannot be added later without the `cwd`. That is the card's mechanism assumption turned into a measured property instead of a belief. - **The refusal.** `checkoutPrerequisite(sweepRepo, env, originUrl)` is a pure verdict in the shape `reportPrerequisiteNotMet` prints: `null` when the knob is unset (today's behaviour is not a prerequisite) or when the named tree really serves the board, otherwise the file's own exit-3 PREREQUISITE NOT MET, named. It is answered FIRST in `sweep()` — ahead of the transport probe — so a foreign checkout costs zero requests and reads nothing. - **`localCheckoutServes` resolves the same way**: the knob leg first (it names the tree, so the `origin` read IN that tree is the authority), then `GITHUB_REPOSITORY` (which names the RUNNER's tree, i.e. exactly not the knob's), then the checkout's `origin`. H57 therefore judges when the knob names the right tree, instead of refusing for the whole run. - **The H17 footer names the tree** it read — path plus `origin` — beside its oracle size, in BOTH oracle states (read, and EMPTY BY FAILURE), knob set or not. A wrong-tree reading is internally consistent; the tree it names is the only thing that tells it from a right one. ## Readings | reading | before | after | |:--|--:|--:| | `scripts/pm/check-half-states.mjs` lines | 36,176 | 36,256 — net **+80**, budget +80 | | `--self-test` cases | 5,063 | 5,081 (+18), exit 0 | | battery roster | 6 batteries, each above its pin | unchanged, each above its pin | Derived gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at this branch → 39 runnable commands, all run locally with the exit code captured BEFORE any pipe, all 0. The per-command verdicts are in this card's `os-dev-report` comment. Control-character self-scan over the changed file: no match. No package contains `scripts/pm/**`, so no dependency-closure build and no package test suite is owed; repo-wide scans stay CI's. ## The firing pair, live Both legs ran at this branch's head against the objectui board, from the objectstack tool. **Knob = the tree that serves the board** — `PM_SWEEP_REPO=objectstack-ai/objectui` with `PM_SWEEP_CHECKOUT=/home/user/objectui`, detached under the shared heavy-verify lock (held 398s, waited 0s — shared-box seconds, not idle-box figures): **exit 0**, 502 half-states over 374 open pm-/p0-labeled issues. The H17 footer: ```text (read on 75 of 75 open `pm:on-hold` card(s); 8142 tracked file(s) in the oracle.) Read in `/home/user/objectui` (`origin` `https://github.com/objectstack-ai/objectui`). ``` 8142 is objectui's LIVE tracked count today (`git -C /home/user/objectui ls-files | wc -l` = 8142); the filer read 7945 on 2026-09-19 and the board has moved since. This worktree reads 9031, so the two trees are still 889 apart — the number that never used to appear is now the one printed, with the tree it came from beside it. H57 is judged rather than UNRESOLVED: `10 workflow(s) on the swept repo declare a schedule; 8 were judged against their latest event=schedule run and 0 are UNJUDGED rather than clean because that read failed.` It filed **two** red rows, neither reachable by any wrong-cwd sweep: - `.github/workflows/changeset-release.yml` — latest scheduled run `35493590744`, started 2026-09-20T06:11:39Z, concluded `failure`. This is the lane the card's re-run traced to objectstack-ai/objectui#10010, still dead today. - `.github/workflows/check-links.yml` — latest scheduled run `35489310168`, started 2026-09-20T04:30:22Z, concluded `failure`. Not named anywhere in the card. **Knob = a foreign tree** — same board, `PM_SWEEP_CHECKOUT=/home/user/objectstack`: **exit 3**, nothing swept, nothing spent: ```text check-half-states: PREREQUISITE NOT MET — PM_SWEEP_CHECKOUT="/home/user/objectstack" does not serve `objectstack-ai/objectui` this checkout's `origin` is `objectstack-ai/objectstack` while the sweep reads `objectstack-ai/objectui`. H17 validates every on-hold trigger path against that checkout and H57 classifies its workflow files there, so a foreign tree renders an index whose paths were checked against another repo. Fix: point PM_SWEEP_CHECKOUT at the checkout whose `origin` IS the swept board, or unset it and run the sweep from inside that checkout. ``` ## Reverse verification — both new pins can fail Each ran from the COMMITTED state, mutated through `scripts/ablation-replace.mjs` (which proves the write landed on disk and restores byte-exactly), and each turned exactly ONE case red out of 5,081. The committed blob is `f49d566ed276`. | ablation | mutation landed | case that went red | restore | |:--|:--|:--|:--| | strip the `cwd` from the one git-read site | anchor 1 to 0, blob `f49d566ed276` to `994882b2fcfc` | `objectstack-ai#19191 knob: …and that one site takes the knob as its cwd` — got false, want true | blob back to `f49d566ed276` == HEAD, `git diff HEAD` empty | | re-spell `readRepoRoot` with its own `execFileSync` git call — a fourth unrouted reader | anchor 1 to 0, blob `f49d566ed276` to `ecd24ab63593` | `objectstack-ai#19191 knob: ONE git read site in the file, so a fourth reader cannot skip the cwd` — got 2, want 1 | same | The first is the one that mattered most: before that pin existed, stripping the `cwd` left the whole 5,080-case suite green, i.e. the fix's central wiring had no test that could fail. The second is what makes the card's "the three readers are the ONLY local-tree reads" a property the suite enforces. ## Acceptance notes - **Knob name**: `PM_SWEEP_CHECKOUT`. It joins the established 12-member `PM_*` PM-tooling family (`PM_SWEEP_REPO`, `PM_SWEEP_CLOSED_FLOOR`, `PM_STATE_LABELS`, `PM_TOKEN`, …) rather than the product's `OS_{DOMAIN}_{NAME}` runtime family of Prime Directive objectstack-ai#9 — the card asked for a knob "beside `PM_SWEEP_REPO`", and `--help` groups the three under one heading where a reader looks. Flagged because that directive's wording is categorical; a maintainer who prefers `OS_PM_SWEEP_CHECKOUT` gets it for the price of one rename. - **The patrol workflow needs no env line, and this PR writes none.** On every real patrol fire the runner's own checkout IS the swept board (`PM_SWEEP_REPO: ${{ github.repository }}`, per-repo installs), so the knob would be a no-op there and `GITHUB_REPOSITORY` already answers `localCheckoutServes`. If it should be stated explicitly anyway, the exact line for the sweep step's `env:` block in `.github/workflows/half-state-patrol.yml` is `PM_SWEEP_CHECKOUT: ${{ github.workspace }}` — a separate, human-merged change, since objectstack-ai#19259 and objectstack-ai#19225 are open on that file. - **`origin/main` was NOT merged into this branch.** It moved to `e6a03e6` while this ran and touched `scripts/pm/check-half-states.mjs` in none of those commits (verified against a fetch into a ref this worktree owns, not against the shared `origin/main` pointer), so the line budget's 36,176 baseline still holds and no serial writer conflict exists. The merge queue rebuilds the PR onto current `main` and re-runs the required contexts there, which is where a jointly-wrong merge would surface. - **Noted, not filed**: `sweep()` and `sweepScheduledWorkflows` each perform their own `readOriginUrl()` / `readRepoRoot()` — two duplicate local git reads per sweep (microseconds, zero requests). Threading one reading through `sweepInto` would widen that function's signature, which is the kind of change this card was told to keep out of. Successor: whoever lands objectstack-ai#19177 (H59) or objectstack-ai#19160 (H52) is next on this writer. - **Nothing else in the file was touched**: objectstack-ai#19230, objectstack-ai#19177, objectstack-ai#19160, objectstack-ai#19203, objectstack-ai#19255 and objectstack-ai#19108 queue behind this card on the same writer. - `scripts/pm/**` publishes nothing from any released package, so this diff owes no changeset (`Clause-②: no`). No label writes. - Tier S: this stops at the draft PR. The owning seat writes the `## Contract review` record, reads `--pair`, and lands it. --- _Generated by [Claude Code](https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…st workflows (objectstack-ai#19284) Fixes objectstack-ai#19229 Authored by Claude Code, session `session_017ef78bLdybu3AffehKkhfk`. Six gates rooted their population at `.github/workflows` and none read `.github/actions/**`, so a command executed through a composite action was audited by nothing while every one of them printed a scope line that reads as coverage. The positive control is not vacuous: `.github/actions/setup-pnpm/action.yml` carries six `run:` steps today, and that file's own header named this gap as the reason it deliberately holds no `setup-node` step. ## What the repair is `scripts/pm/dispatch-gates.mjs` now follows `uses: ./.github/actions/NAME` out of a workflow and reads that action's `runs:` steps, so a command executed **through** a composite action is derived exactly as one executed inline. - **Attribution never moves.** The invocation is attributed to the CALLING workflow, because the caller is what CI schedules and what a `paths:` filter narrows. An action declares no `on:` block at all, so attributing to it would invent a schedule nobody wrote. The action file rides alongside as `viaAction` provenance a reader can open. - **Recursive, cycle-safe.** An action may `uses:` a sibling; a one-hop follow would re-open the same blind spot one level down. - **A `uses: ./…` with no action file behind it is a refusal, not a skip.** GitHub refuses to start such a job, so a derivation that dropped it quietly would describe a CI this repo does not have. - **Only `./.github/actions/**` is followed.** A third-party action's steps are not in this tree. A local action outside that root is a MISSING lead and is refused deliberately: the module's declared inherited population has to stay exactly equal to the trees it really opens, and a follow that could open any directory a workflow names could not be declared at all. The repair the card forbids was not taken: the four live-specimen CONTROL assertions on the sweeper family are **untouched** — no assertion was re-pointed at a different value-bearing family. ## The other five gates — each judged from its source, not from the card's grep | gate | verdict | reading | |---|---|---| | `scripts/check-node-version.mjs` | **extended** | Its population is `uses: actions/setup-node@` steps, and a composite action is a legal place to write one. The cost was already being paid in the tree rather than merely risked: `setup-pnpm/action.yml` and three workflows carry comments shaping the composition around this gate's blind spot. Both roots are read; both counts are printed. | | `scripts/check-workflow-step-name-quoting.mjs` | **extended** | Its subject is ` #` inside an unquoted `- name:` plain scalar. A composite action's steps carry `- name:` scalars parsed by the same YAML, in the same repo, under the same house style of writing issue numbers into step names. `setup-pnpm/action.yml` alone carried eight step names this gate could not see. | | `scripts/check-self-test-wired.mjs` | **extended** | Its subject is "a script CI RUNS whose self-test CI must run too". A step in `.github/actions/**` is run by CI in the calling job exactly as an inline one is, so rooting the corpus at `.github/workflows` alone made a directory boundary into a coverage boundary — and every `objectstack-ai#4690` floor here fires on an EMPTY population, never on one that is complete-minus-one. | | `scripts/check-self-test-workflow-commands.mjs` | **extended** | It consumes `collectPopulation` from the gate above and adds no walk of its own (its own-source pin forbids one). What it owns is the DECLARATION: it now declares and pins `.github/actions` beside `.github/workflows`, so the live coupling covers both roots instead of naming half the corpus to the dispatch derivation. | | `scripts/check-step-collectors.mjs` | **UNJUDGED row traced, then extended** | The card recorded this row as UNJUDGED because it names neither path spelling. Traced: its root is assembled as ``join('.github', 'workflows')`` (line 210 on the filing tree), which is why a literal grep found zero — the population was workflows-only all along. It genuinely belongs: the runner writes a composite action's `run:` body to a file and executes `bash -e` on it, so the abort-on-first-failure masking is the same defect in the same shell. The judgement is shared and only the walk to the steps differs — `stepGroups()` reads `jobs.JOB_ID.steps` and `runs.steps` alike. | `.github/actions/` **absent** is not a refusal in any of the five: a repo may legitimately hold no composite action. What keeps the second root from going quiet is a LIVE assertion in each gate's own `--self-test` (and, for `check-node-version.mjs`, which ships no self-test, the firing/dark control pair recorded below). ## Firing and dark controls Each extension has both: the hazard placed inside a composite action is flagged, and the SAME tree with the action file removed is green — which is what makes the first a reading about the second root rather than about the fixture. - `check-workflow-step-name-quoting` — battery 7, five cases: the ` #` hazard inside `action.yml` is flagged and named by its own path; the same tree with no `.github/actions/` is green AND not a refusal; a `README.md` beside an action is not an action. - `check-self-test-wired` — battery `the composite action corpus`, six cases: a `--self-test` run only inside an action counts as WIRED and the attribution names the action FILE; the same tree without it reports exactly one `self-test-not-run` finding. - `check-step-collectors` — the bare sequence inside a composite action is flagged as `runs (composite)`; the same pair routed through a collector is green; the real root walk finds a NESTED `action.yml` and names its path. - `check-node-version` — no `--self-test` ships, so the pair was driven by hand in a throwaway git tree (recorded here, ⛔ no temporary file left in the repo): ``` FIRING .github/actions/fixture/nested/action.yml pins node-version '20' against .nvmrc 22 -> exit 1, "• .github/actions/fixture/nested/action.yml:8 -- pins Node 20, but .nvmrc says 22" DARK same tree, .github/actions removed -> exit 0, "OK (1 setup-node step(s) across 1 workflow(s) and 0 composite action(s))" ``` - `dispatch-gates` — a fixture caller that invokes no check of its own derives two families **only** because the action's steps were read; with the reader answering `null` for that directory, zero families are derived and the absence is NAMED. Plus the live reading: the discovery pass really opens `.github/actions/setup-pnpm/action.yml` and really reads its six `run:` steps. ##⚠️ A measured boundary, stated rather than implied **A script path that reaches its command through a step `env:` value is derived by NEITHER spelling** — written inline in a workflow, or written in a composite action. The composite follow makes an action's step read exactly like an inline step, *including* where an inline step is already not derived. That is a different blind spot and it is pinned here so a green follow is not read as coverage of it. This matters for the card's own beneficiary. Measured against PR objectstack-ai#19225 at its head `68ca79ec9` (read-only; that PR was not touched): ``` uses found .github/actions/half-state-patrol runs: steps read 6 families via action (none) ``` The action spells the sweeper as `node "$SWEEPER"` with `SWEEPER: ${{ steps.sources.outputs.root }}/scripts/pm/check-half-states.mjs` in the step's `env:`. The follow reaches the steps; the matcher cannot name a script whose path is an unresolvable expression. So the four pinned CONTROL assertions on that family are restored by this change **only if the invocation is spelled so a reader can see it** — that is objectstack-ai#19225's own repair to make, and it is reported rather than taken here. ## Tests All readings taken on this branch at `bc1662577`. **The long battery, before and after.** `node scripts/pm/dispatch-gates.mjs --self-test`, run detached with `tail --pid` and the exit code captured by redirect, never through a pipe: ``` BEFORE origin/main e6a03e6 exit 0 1866 cases pass 613 s AFTER this branch bc16625 exit 0 1883 cases pass 633 s (+17 new cases) ``` **Reverse verification — the new cases can fail.** The composite follow was neutralised in the PRODUCTION path (an early return inside `followCompositeActions`), the mutation proved on disk before the reading was taken, and the file restored from `HEAD` afterwards: ``` on-disk proof OS_ABLATION_19229 occurrences 0 -> 1 blob HEAD=ee5794e17f01e0f64ef97d204aeaccbebed27b33 mutated=4ba2eb4b6d4e096dbad65212efc0372c55955c87 live reading discoverFamilies().compositeActions -> [] (was ['.github/actions/setup-pnpm/action.yml']) ablated run exit 1 — 7 of 1883 case(s) failed the command executed THROUGH a composite action is derived / the CALLING-workflow attribution the absence is NAMED / the follow recurses / a cycle terminates the live discovery really opens the tree / really reads the steps in it restored blob ee5794e == HEAD, `git diff HEAD` empty ``` ⛔ No temporary file is left in the repo: the ablation ran from a script outside it, carried a shell `trap` on EXIT, INT and TERM that restores the file, and restored with `git checkout HEAD -- PATH` (never a bare `git checkout --`, which restores from the index). **Derived gate families.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` over the real change set (10 paths, three-dot vs merge base `e6a03e649`) derived 51 families; all 51 were run with the exit code captured before any pipe, and reconciled back through `--ran`: ``` ✓ dispatch-gates --ran: 51 derived famil(ies) accounted for — 51 run, 0 NOT-MEASURED (a DERIVED zero — all 51 recorded an exit code and none of them is 3). ``` Every one exited 0, including all five extended gates and their self-tests, `check:nul-bytes`, `check:watch-hint-literal`, `check:declared-population-live` and `check:pm-dispatch-gates`. `pnpm lint` is CI's repo-wide run, not this PR's. ## Acceptance notes - `.github/actions/setup-pnpm/action.yml` and three workflows carried comments naming the old blind spot as a constraint. This diff is what makes them false, so they are corrected in it. ⛔ The separation itself is kept — the pins are already in place and moving them buys nothing — it is simply no longer forced. - PR objectstack-ai#19225's `action.yml` carries the same now-stale sentence about `check-node-version.mjs`. Not touched: that file belongs to an open PR. - `skip-changeset`: measured, not assumed — 70 published packages, zero `files[]` entries naming `scripts/` or `.github/`, root package `private: true`. --- _Generated by [Claude Code](https://claude.ai/code)_ Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…-only), plus the merged-result probe (objectstack-ai#19259) Fixes objectstack-ai#18224 Two gates were registered in the root manifest and invoked by **zero** workflows: `scripts/check-issue-citations.mjs` (delivered by objectstack-ai#18223 / card objectstack-ai#17512) and `scripts/check-merged-result.mjs` (delivered by objectstack-ai#18338 / card objectstack-ai#16287). Both cards' declared file surfaces excluded `.github/workflows/**`, so both devs correctly stopped and filed the wiring rather than widening. This PR is that wiring. ## The three entry points, and their postures | entry | lane | posture | | :--- | :--- | :--- | | the DIFF-scoped citation verdict | `lint.yml` · `Lint & Repo Gates` | **blocking** | | `check:merged-result` self-test | `lint.yml` · `Lint & Repo Gates` | **blocking** | | `check-issue-citations --census` | `half-state-patrol.yml` (scheduled) | **report-only, never blocking** | The census posture is a ruling, not a preference, and the card carries the measurement that forces it: **objectstack-ai#16783, objectstack-ai#16786 and objectstack-ai#16787 were measured RESOLVING on 2026-09-10 and 404 on 2026-09-14, with no change to this tree.** A tree-wide blocking verdict would therefore red a motionless repository because a third party deleted an issue. The diff-scoped half is the part an author owns, it is small, and it is the only thing that stops 2,785 unresolvable sites becoming 3,000. ## The manifest alias is NOT the verdict `package.json` maps `check:issue-citations` to `node scripts/check-issue-citations.mjs --self-test` and nothing else -- the shape every credential-needing gate in this manifest uses (`check:pm-half-states`, `check:pm-closed-card-sweep`), because a live mode needs a board and a credential. Wiring that alias alone would have run the self-test twice and scanned nothing. So the lint step holds **two** commands, self-test first: ``` pnpm check:issue-citations && node scripts/check-issue-citations.mjs ``` The second spelling is lint.yml's documented gate-invocation idiom (`dispatch-gates.mjs`'s own header names it), and the first is what `check-self-test-wired` requires of every script CI runs. `check:merged-result` needed no such split: the manifest key already IS `node scripts/check-merged-result.mjs --self-test`, which is the whole gate. ## `package.json` is deliberately untouched No new manifest key was added. Both keys already exist and both are now named by `lint.yml`, so the wiring needs no manifest edit, and leaving the file alone keeps this PR textually disjoint from PR objectstack-ai#18414, which adds a key two lines from where a new one would have gone. ## The non-step changes: `issues: read` AND `pull-requests: read` >⚠️ Heading corrected by the dispatching PM at 2026-09-20T07:25Z: this section was written when the block > gained ONE scope. It now adds TWO — `pull-requests: read` followed from a measurement taken after > the body was written (patrol run 35495222460 censused **3628** unresolvable sites on a token without it, > run 35496169133 on the fixed head censused **2167**, the exact full-scope reading; the 1460 difference is > exactly the `resolves-as-pull-request` tally, because `GET /issues` omits pull requests without that scope). > ⛔ Nothing else in this body was altered. The `Lint & Repo Gates` job's `permissions:` block gains `issues: read`. An explicit `permissions:` block sets every unnamed scope to `none`; this board is public today, but a **blocking** gate whose transport depends on repository visibility is a gate that goes red on a settings change no file in this repo can assert. Read-only, one scope wide: the gate never writes an issue, a comment, a label or an assignee. ## Acceptance 2 -- both directions measured, on the CI side PR objectstack-ai#18223 measured the red/green pair on the **gate** side. This card asks for the same pair on the **CI** side. Two instruments, both here. **A. The commits on this branch ARE the CI-side ablation.** `test(ci): ABLATION LEG 1 of 2` adds one citation naming a number beyond this board's allocation frontier, in a declared surface (`packages/**/src/**/*.ts`, comment-prose projection). `ABLATION LEG 2 of 2` removes it and restores the file to the byte. The run on the first head is the red reading; the run on the final head is the green one. Both run identifiers are recorded in the dev report on the card. **B. The exact command the new step holds, ablated locally with disk evidence.** Three legs, run from a committed state, each restored with `git checkout HEAD -- path` and proven by hash: ``` target packages/types/src/index.ts HEAD blob 0235d4e leg 1 unresolvable marker 0 -> 1 blob 0235d4e -> 2ac9ba3 exit 2 RED finding: [never-issued] packages/types/src/index.ts:8 #999999 beyond the allocation frontier (19258) -- this number was never minted leg 2 restored marker 1 -> 0 blob back to 0235d4e exit 0 GREEN `git diff HEAD` empty after the restore leg 3 resolvable cites objectstack-ai#17512 instead exit 0 GREEN board: probed (1 citations), frontier objectstack-ai#19258, 2 numbers resolve ``` Leg 3 is the control on leg 2: a green produced by a live board read, not by a run that never asked the board anything. ## Acceptance 3 -- where the census reports, how often, who pays Written into the step itself, and repeated here: - **Where.** The patrol run's **step summary** and job log, plus one `::warning::` carrying the site count. Deliberately **not** the anchor issue: that body is owned end to end by `check-half-states.mjs`'s generator, and a second writer is how half a generated body goes stale. - **How often.** This workflow's schedule -- four times a day, six hours apart (`37 1,7,13,19`) -- plus any `workflow_dispatch`, plus the `pull_request` runs the paths filter admits. A row for `scripts/check-issue-citations.mjs` was added to that filter for the reason the file already gives for its two siblings: a step whose script can change without the trigger firing is a step whose PR-time proof is a coincidence. - **Who pays the 159 requests/run.** This repository's own `secrets.GITHUB_TOKEN` core quota -- the same 5,000/hour the job already draws the live sweep from. Four runs a day is roughly 636 requests/day, under half a percent of a single hour's allowance. No PAT and no cross-repo credential, per this file's own standing rule. The step is gated on `github.repository == 'objectstack-ai/objectstack'`, exactly as the closed-card sweep above it is, because `half-state-patrol.yml` is copied verbatim into sibling repos that do not carry this script -- a copy must skip the step, not fail on a missing file. It is placed **after** the anchor write, unlike the closed-card sweep: the anchor is this patrol's product, the job has a 15-minute timeout, and a report-only reading must never be able to starve it. It always exits 0. ## Acceptance 4 -- the 422 wall This diff touches `.github/workflows/**`, which is outside the PM seat's arming channel: the seat's `auto_merge` answers **HTTP 422** for this PR. **It merges by a human.** That is the same wall as PR objectstack-ai#18096 and objectstack-ai#18341, it is not a tool fault, and it must not be retried. No seat should undraft this PR or arm auto-merge on it. Note that `.github/workflows/**` is *not* on the `GOVERNED_SURFACES` register in `scripts/pm/check-governed-merges.mjs`, so the governed-merge machinery is not what holds this one -- the 422 is. ## Placement, and the three in-flight PRs on these files Read before editing: objectstack-ai#18414 (`lint.yml` + `package.json`, green, awaiting a human merge), objectstack-ai#19024 (`lint.yml`), objectstack-ai#19225 (`half-state-patrol.yml`, draft and frozen). Only additions here; no existing step was moved, renumbered or reformatted. - In `lint.yml` the two steps go **above** the `objectstack-ai#15149` step-name-quoting step, which keeps that step's own documented placement ("immediately above" the duration-unit-keys step) true and keeps the duration-unit-keys step last among the gates. objectstack-ai#18414 inserts at the control-byte guard (line ~411) and objectstack-ai#19024 edits the typecheck lanes (lines ~5173 and ~6104), so all three hunks are disjoint. - In `half-state-patrol.yml` the census step goes between the summary publish and the final fail step. objectstack-ai#19225 rewrites that file wholesale into a composite action and is frozen behind a `pm:blocked` card; a textual conflict there is expected and was accepted at dispatch. ## Measurement this PR does not relay The card's prose carries three disagreeing counts for the manifest census. Re-measured on this branch's base `0f42d36ff`, with a firing control and a dark control: ``` root manifest check:* keys 165 not named by any workflow (name or path) 2 -> check:merged-result, check:issue-citations ...and not named by any other script 2 firing control 'check:nul-bytes' in a wf true dark control 'check:zznotreal' in a wf false ``` Both unreached keys are the two this PR wires, so the reading after this lands is 0. ## Acceptance notes - `check-self-test-wired` admits a script when a workflow names it directly or through a root manifest alias, repo-wide rather than per workflow, so the patrol's census step needs no second self-test invocation: the lint step above already runs it. - No changeset: nothing any package publishes moves. Workflow files ship in no package's `files[]`. --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: os-elon-musk <elon-musk@objectstack.ai>
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 free
to 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.
Part of #18471
Ruling on the card: batch #144 item 1, letter 只做②, comment 5713650969. Verbatim scope line:
This PR is the objectstack half only — the thing the sibling boards will call, plus this repo wired up as its first consumer. The cloud / objectui / hotcrm migrations live in repositories this session cannot reach, so the card stays open and
Part ofis deliberate.The measurement that is the card's argument
scripts/pm/check-half-states.mjsis 36,176 lines, read atorigin/main24d0542e6on 2026-09-20T01:17Z in this checkout. The ruling three days earlier cites 32,233. That is +12% in three days on a file every sibling board carried as a hand-made copy, and it is the whole case for the change: a copy of a file moving at that rate is behind by arithmetic, not by carelessness.What a sibling had to copy and keep current, at that same tree:
scripts/pm/check-half-states.mjsscripts/invoked-as.mjs.github/workflows/half-state-patrol.ymlAfter this lands, a sibling's install is the ~15-line caller shown below, and zero copied script lines.
Composite action, not a reusable workflow — and why
The ruling left the choice to the implementer. The repo holds exactly one precedent of each:
.github/actions/setup-pnpm(its only composite action) and.github/workflows/docker-publish.yml(its onlyworkflow_callfile). Both were read before choosing. One mechanical property decides it:uses: OWNER/REPO/.github/actions/NAME@SHAmakes the runner place the whole repository at that sha on disk and exposes it asgithub.action_path. GitHub documents that path as the way to "access other files in that action's repository". So the sweeper and the helper it imports arrive with the action, at the same sha, with no credential and no second checkout.workflow_calltransfers the YAML file only. The repository's contents are not placed on the runner, so a reusable patrol would have toactions/checkoutthis repo itself — and then the ref a sibling pins and the ref a sibling executes become two things kept equal by hand. That is the copy problem again, one layer down.The composite shape has no such seam, so the install list is deleted rather than lengthened. In its place the action's first step positively locates the sweeper and the helper under
github.action_path/../../..and fails by name when they are not there. A list of files to copy is a hand-kept mirror of an import graph; this repo measured that list wrong twice (two files until 2026-09-03 while the sweeper had imported a third for weeks, then short by four again a fortnight later). An assertion cannot be short from memory.The input surface — the only thing a sibling migration has to read
.github/actions/half-state-patrolgithub-tokensecrets.GITHUB_TOKEN. Needsissues: write(the one anchor PATCH) andcontents: read. ⛔ Never a cross-repo PAT.anchor-issue''tracking-labeled anchor issue in the calling repository. Empty makes the run say so loudly and fail; it never guesses a number.closed-floor''PM_SWEEP_CLOSED_FLOOR, aYYYY-MM-DDfloor for the sweeper's closed-card pass. Per-board by nature: a board's floor is a date in its own history. Empty means unset, the sweeper's own default.Derived, deliberately not inputs:
PM_SWEEP_REPOisgithub.repository, which inside an action invoked from another repo is the calling repository. A board identifier that can be typed is one that can be typed wrong, and a patrol sweeping the repo it was installed from would file a fully green report about the wrong board.github.event_name: apull_requestrun proves the sweep and never touches the board. That is a safety property, not a per-board constant, so no caller can opt out of it.Stays in the caller, by design: the
on:triggers (the cron minute is per-board — it is offset against that board's healer cycle),permissions,concurrency,timeout-minutes,actions/checkoutandactions/setup-node.A sibling board's whole install:
plus one
tracking-labeled anchor issue opened in that repo. ⛔ A sha, never@main—@mainis this same drift re-entered from the other side, with no reviewed moment and nothing to roll back to.Two steps that deliberately did NOT move, and the gates that decide it
Both are cases of the trap
.github/actions/setup-pnpmalready records for the same reason: a gate whose population comes from.github/workflows/keeps printing OK while auditing less.actions/setup-node.scripts/check-node-version.mjsscans.github/workflows/*.ymlonly and reports how many setup-node steps it audited. A step moved into the action would drop out of that census silently. Callers keep their own step with its literal pin — exactly what the setup-pnpm header prescribes. Because that gate cannot reach a sibling repo at all, the action reads the.nvmrcthat travelled with it and refuses a runner below that floor, naming the remedy. No second hand-written Node pin was introduced.scripts/check-self-test-wired.mjsbuilds its population from the scripts a workflow names, and this workflow holds CI's only invocation ofnode scripts/pm/sweep-closed-cards.mjs --self-test(lint.ymlrunscheck:pm-half-statesbut notcheck:pm-closed-card-sweep). Moving that step into the action would have left the gate green while auditing one script fewer. It is also the one step of the old file that was never repo-agnostic — it writes to cards under a ruling only this board has taken — so leaving it in the objectstack caller states plainly what itsif: github.repository == ...used to state obliquely.The one behaviour change
The closed-card sweep now runs after the patrol instead of before it, guarded by
!cancelled()rather than an implicitsuccess(). The anchor's content is unaffected — the half-state sweep still reads the board before this step touches it — and the patrol's product, the anchor write, can no longer be starved by anything this step does, which is what the old placement's own comment asked for and did not get.Verification
Measured here:
using: compositewith 7 steps and 3 inputs; the workflow keeps its three triggers,permissions,concurrencyand 4 steps. The foldedanchor-issuescalar collapses to one line, which is the property its comment warns about.pull_requestpaths:list gained.github/actions/half-state-patrol/**, so an edit to the patrol's body is exercised before it merges. That row is load-bearing: without it, moving the body out of the workflow would have moved it out of its own PR-time proof.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands ...and run; results are in the report on the card.NOT MEASURED — no runner executed any of this from a sibling repository, and nothing below should be read as a pass:
github.action_pathfor a cross-repo composite action. This is documented behaviour and is the premise of the whole design; it is not verifiable from inside this repo. It is asserted at runtime instead: the action's first step fails by name, naming the resolved root and the missing file, rather than proceeding into a patrol whose sweeper is absent.uses:this public repo's action pinned to a sha with no credential. The card and the ruling both measured the public/private fact behind it; the call itself is unexercised until a sibling migrates.with:plumbing.This PR's own
pull_requesttrigger runs the patrol on a real runner through the local action path, which is the closest available exercise of the composite body (step ordering,always(), the step outputs, the sweep and the rendered summary) and writes nothing to the board.Landing
Kept draft on purpose.
auto_mergeis measured permanently 422 on the.github/workflows/**surface, so this wants a human merge; the PM decides arming after reading the green predicate. No changeset: nothing under.github/**is shipped by any package'sfiles[].#18466 is not addressed here and remains open; the ruling absorbs it and a pointer is already on it.
Generated by Claude Code