The composite action gains one input, `anchor-optional`, default `false`.
Declared `true`, an empty `anchor-issue` gives a ::notice::, exit 0, and the
findings in the run summary only; the anchor PATCH is skipped through the
resolve step's `configured` output. Left at the default, the empty-anchor
::error:: and exit 1 are unchanged. A non-numeric anchor fails in both modes,
and a value other than true or false is refused on every event.
objectstack's own caller keeps the default; its prose names the input for
sibling installs.
Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh
Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20798
Clause-②: no. CI wiring, no published contract, per ruling
5905998989.This executes decision A of the decision card #20793 (ruling
5905998989; the maintainer's answer, verbatim: "A: opt-in action input (Recommended)"), inside the maintainer's order on #18471. objectstack-ai/objectui#11174 is waiting on this card and switches onto the action once this lands, with the opt-in on and a sha pinned at that time.What changes
.github/actions/half-state-patrol/action.yml: one new declared input,anchor-optional, default'false'.anchor_optional. It accepts the six YAML 1.2 core-schema boolean spellings thatcore.getBooleanInputtakes. Any other value is::error::+ exit 1.id: anchorand one new block in front of the existing ones. An empty (or whitespace-only)anchor-issuewith the opt-in on writesconfigured=false, prints a::notice::, and exits 0. The existing empty-anchor block that follows it (::error::+ exit 1,action.yml:313-315onmain) is byte-identical. The non-numeric check is unchanged and runs in both modes. A usable anchor writesconfigured=true.&& steps.anchor.outputs.configured == 'true', so the opt-in never reaches the PATCH. H1 holds: without this guard, the opt-in PATCHes issue 0 (see the ablation below).configured == 'false'reading. The findings go to the run summary as before, since this step isif: always().descriptionnames the summary-only delivery.scripts/pm/check-half-states.mjsis untouched..github/workflows/half-state-patrol.yml(objectstack's own caller): prose only, and the default is kept. The sibling-install section now names the opt-in for a board that has no anchor by decision. The anchor-resolution comment says this caller keepsanchor-optionalat its loud default. Thewith:block is unchanged.The input as declared
The three modes: a runnable probe of the action's own shell
There is no action or workflow test harness for this action in the repo. H2 is falsified:
git grep -n "anchor-issue\|closed-floor"finds only the action itself and the caller's prose. No test, doc or script reads the input list. So this is measured with a probe,probe-anchor-modes.py, whose full source is at the end of this body. How it works:action.ymland runs every step's realrun:body in order. Each body is template-expanded and run the way the runner'sshell: bashruns it (bash --noprofile --norc -eo pipefail).if:with a small GitHub-expression evaluator. As the runner does, it wraps the expression insuccess() && (...)unless it names a status function.GITHUB_OUTPUT,GITHUB_STEP_SUMMARYandRUNNER_TEMPfiles.Number(ANCHOR_ISSUE)would target, computed by node.Run at
c35b098b1:python3 probe-anchor-modes.py .github/actions/half-state-patrol/action.yml .(exit 0).''::error::''false::error::''true::notice::' 'true::notice::'abc'::error::'abc'true::error::'9857''9857'true''''true''yes::error::''yes::error::''true::notice::''::error::The rows the card requires, quoted from that run:
The default mode is unchanged, measured against
main. The same probe was run on the base blob (41dcf1188:.github/actions/half-state-patrol/action.yml, blobfc455f1ef) and on this head, for off+empty, off+non-numeric, off+number, pr+off+empty and off+empty with sweep exit 3. The outputs were compared after dropping theoutputs {...}annotation, since the head's steps now also publishanchor_optionalandconfigured. Across all 66 lines, each scenario differs in one line only: the scenario header, which readsUNSET(default '-')on base (no such input) andUNSET(default 'false')on head. Every step line, annotation, summary line and job verdict is identical.Ablation of the Update guard (H1). This was run on a scratch copy of the committed
action.yml, never on the tree. The copy had the guard's&& steps.anchor.outputs.configured == 'true'removed;grep -cfound that text 0 times in the copy and 1 time in HEAD. The probe then read:So the opt-in needs the guard. On a real runner that PATCH answers 404 and turns the run red, which is the state this card exists to remove.
Gates (local, at
c35b098b1)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 39 commands, derived from merge base41dcf1188over the 2 changed paths.node scripts/pm/dispatch-gates.mjs --ran ran.listreconciles them as39 derived, 39 run, 0 NOT-MEASURED, 0 UNRUN, with an exit code recorded for every one.pnpm check:pm-dispatch-gatesis longer than the container's foreground cap, so it ran detached, as its header prescribes, under a foregroundtail --pidwait. It printed✓ check:pm-dispatch-gates --self-test: the exit contract holds in all three directions.and✓ dispatch-gates self-test: 1976 cases pass.(1018.6s), with 0✗lines.pnpm installfor the root package only (--filter @objectstack/spec-monorepo). 15 gates first exited 3 (PREREQUISITE NOT MET, theyamldependency) and are green on the re-run after it.check-half-states.mjsandcheck-issue-citations.mjs --censusare NOT MEASURED locally. Their argv carries runner-only values. This PR's ownhalf-state-patrol.ymlrun exercises the PR path on a real runner, because the action's directory is a trigger path. That run includes the new value check at its default.actionlint: NOT MEASURED. It is not on PATH and not innode_modules/.bin.check:workflow-status-functionsandcheck:workflow-step-name-quotingare green.@objectstack/spec-monorepo.pnpm ls -rfinds 81 workspace packages, 69 public, and none of them owns a changed path.check-empty-changeset.mjs --base 41dcf1188is green (0 changesets added).check-changeset-no-major.mjs --base 41dcf1188 --event EVENTis green, reading this body'sClause-②: noline. The disposition isskip-changeset. This dispatch did not allow label writes, so the seat applies it, andCheck Changesetreads red until then.Acceptance notes
anchor-optionalsits in "Locate the patrol sources", so it runs on pull_request runs too.caseinto the Resolve step (non-PR only).anchor-optional: true) where objectui's named its repository variable.workflow_dispatchof this branch would PATCH objectstack's live anchor. The first live reading of on+empty will be objectui's switch (ci(half-state-patrol): run objectstack's composite action pinned to a sha instead of checking out objectstackmain— objectui's half of objectstack-ai/objectstack#18471 (ruling 只做②) objectui#11174).anchor-optionalgets only the runner's "Unexpected input" warning, and the empty anchor still fails loudly. That is safe, but objectui's switch has to pin a sha that contains this change.The probe (
probe-anchor-modes.py)Generated by Claude Code