Skip to content

ci(half-state-patrol): a declared anchor-optional input lets a board with no anchor issue run summary-only (default off) - #20812

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20798-patrol-anchor-optional
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20798-patrol-anchor-optional

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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'.

  • Locate the patrol sources judges the value once, on every event, and publishes it as anchor_optional. It accepts the six YAML 1.2 core-schema boolean spellings that core.getBooleanInput takes. Any other value is ::error:: + exit 1.
  • Resolve the anchor issue gains id: anchor and one new block in front of the existing ones. An empty (or whitespace-only) anchor-issue with the opt-in on writes configured=false, prints a ::notice::, and exits 0. The existing empty-anchor block that follows it (::error:: + exit 1, action.yml:313-315 on main) is byte-identical. The non-numeric check is unchanged and runs in both modes. A usable anchor writes configured=true.
  • Update the pinned anchor issue: its guard gains && 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).
  • Publish the rendered body to the run summary adds one line, and only on the explicit configured == 'false' reading. The findings go to the run summary as before, since this step is if: always().
  • The header gains a section, "An empty anchor: loud by default, summary-only by declaration". The action's top-level description names the summary-only delivery.
  • For every valid value, the pull_request path is unchanged: both anchor steps are still skipped there. The one exception is the value check above, which is a judgment call stated under Acceptance notes. scripts/pm/check-half-states.mjs is 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 keeps anchor-optional at its loud default. The with: block is unchanged.

The input as declared

anchor-optional:
  description: >-
    `true` declares that an EMPTY `anchor-issue` is this caller's supported configuration: that one case becomes a `::notice::` and a passing run, with the findings delivered to the run summary only. It exists for one consumer, objectui, whose maintainer declined an anchor there (#20793, decision A). The default, `false`, stays LOUD -- an empty anchor fails the run -- because without the declaration an empty value reads the same as a caller that lost its variable, and objectstack's own caller must go red when that happens. It never widens: a non-numeric `anchor-issue` fails either way, and a value other than true or false is refused on every run, pull_request included.
  required: false
  default: 'false'

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:

  • It loads action.yml and runs every step's real run: body in order. Each body is template-expanded and run the way the runner's shell: bash runs it (bash --noprofile --norc -eo pipefail).
  • It evaluates each if: with a small GitHub-expression evaluator. As the runner does, it wraps the expression in success() && (...) unless it names a status function.
  • It uses real GITHUB_OUTPUT, GITHUB_STEP_SUMMARY and RUNNER_TEMP files.
  • It stubs two steps that this diff does not change: the sweep (a stub report plus the scenario's exit code) and the github-script PATCH. For the PATCH it prints the issue number that 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).

scenario event anchor-issue anchor-optional Locate Resolve Update (the PATCH) summary line job
off+empty schedule '' unset exit 0 exit 1 ::error:: skipped none FAILURE
off-explicit+empty schedule '' false exit 0 exit 1 ::error:: skipped none FAILURE
on+empty schedule '' true exit 0 exit 0 ::notice:: skipped "No anchor issue configured ..." success
on+whitespace workflow_dispatch ' ' true exit 0 exit 0 ::notice:: skipped "No anchor issue configured ..." success
off+non-numeric schedule 'abc' unset exit 0 exit 1 ::error:: skipped none FAILURE
on+non-numeric schedule 'abc' true exit 0 exit 1 ::error:: skipped none FAILURE
off+number schedule '9857' unset exit 0 exit 0 runs, PATCHes issue 9857 none success
on+number schedule '9857' true exit 0 exit 0 runs, PATCHes issue 9857 none success
pr+off+empty pull_request '' unset exit 0 skipped skipped "Anchor write skipped ..." success
pr+on+empty pull_request '' true exit 0 skipped skipped "Anchor write skipped ..." success
bad-optional schedule '' yes exit 1 ::error:: skipped skipped none FAILURE
pr+bad-optional pull_request '' yes exit 1 ::error:: skipped skipped "Anchor write skipped ..." FAILURE
on+empty, sweep exit 3 schedule '' true exit 0 exit 0 ::notice:: skipped "No anchor issue configured ..." FAILURE (the final "sweep could not run" step)
off+empty, sweep exit 3 schedule '' unset exit 0 exit 1 ::error:: skipped none FAILURE

The rows the card requires, quoted from that run:

=== off+empty | event=schedule anchor-issue='' anchor-optional=UNSET(default 'false') sweep-exit=0
  - Locate the patrol sources                        exit 0  outputs {'anchor_optional': 'false'}
  - Check the workspace serves the swept board       exit 0
  - Run the live sweep                               exit 0  outputs {'exit_code': '0'}
      (stub) check-half-states exited 0
  - Resolve the anchor issue                         exit 1
      ::error::No anchor issue configured for objectstack-ai/objectui. The sweep RAN (see the run summary) but has nowhere to land. Open a `tracking`-labeled anchor issue in this repo and pass its number as the 'anchor-issue' input of the half-state-patrol action.
  - Update the pinned anchor issue                   skipped
  - Publish the rendered body to the run summary     exit 0
  - Fail the run if the sweep could not run          skipped
  summary: ### Half-state patrol — sweep exit 0
  JOB: FAILURE

=== on+empty | event=schedule anchor-issue='' anchor-optional='true' sweep-exit=0
  - Locate the patrol sources                        exit 0  outputs {'anchor_optional': 'true'}
  - Check the workspace serves the swept board       exit 0
  - Run the live sweep                               exit 0  outputs {'exit_code': '0'}
      (stub) check-half-states exited 0
  - Resolve the anchor issue                         exit 0  outputs {'configured': 'false'}
      ::notice::No anchor issue configured for objectstack-ai/objectui, which is a supported configuration for this caller: it declares 'anchor-optional: true' on the half-state-patrol action. The sweep RAN and its rendered body is in this run's summary; nothing was written to any issue, and the missing anchor does not fail this run. To deliver findings to a pinned issue instead, open a `tracking`-labeled anchor issue in this repo and pass its number as the 'anchor-issue' input.
  - Update the pinned anchor issue                   skipped
  - Publish the rendered body to the run summary     exit 0
  - Fail the run if the sweep could not run          skipped
  summary: ### Half-state patrol — sweep exit 0
  summary: _No anchor issue configured (`anchor-optional` is on and `anchor-issue` is empty): this summary is the ONLY delivery of the findings below — it notifies nobody, by accepted trade. Pass a `tracking` issue's number as `anchor-issue` to have them land on a pinned card instead._
  JOB: success

=== off+non-numeric | event=schedule anchor-issue='abc' anchor-optional=UNSET(default 'false') sweep-exit=0
  - Locate the patrol sources                        exit 0  outputs {'anchor_optional': 'false'}
  - Check the workspace serves the swept board       exit 0
  - Run the live sweep                               exit 0  outputs {'exit_code': '0'}
      (stub) check-half-states exited 0
  - Resolve the anchor issue                         exit 1
      ::error::The 'anchor-issue' input is 'abc', which is not an issue number.
  - Update the pinned anchor issue                   skipped
  - Publish the rendered body to the run summary     exit 0
  - Fail the run if the sweep could not run          skipped
  summary: ### Half-state patrol — sweep exit 0
  JOB: FAILURE

=== on+non-numeric | event=schedule anchor-issue='abc' anchor-optional='true' sweep-exit=0
  - Locate the patrol sources                        exit 0  outputs {'anchor_optional': 'true'}
  - Check the workspace serves the swept board       exit 0
  - Run the live sweep                               exit 0  outputs {'exit_code': '0'}
      (stub) check-half-states exited 0
  - Resolve the anchor issue                         exit 1
      ::error::The 'anchor-issue' input is 'abc', which is not an issue number.
  - Update the pinned anchor issue                   skipped
  - Publish the rendered body to the run summary     exit 0
  - Fail the run if the sweep could not run          skipped
  summary: ### Half-state patrol — sweep exit 0
  JOB: FAILURE

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, blob fc455f1ef) 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 the outputs {...} annotation, since the head's steps now also publish anchor_optional and configured. Across all 66 lines, each scenario differs in one line only: the scenario header, which reads UNSET(default '-') on base (no such input) and UNSET(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 -c found that text 0 times in the copy and 1 time in HEAD. The probe then read:

=== on+empty | event=schedule anchor-issue='' anchor-optional='true' sweep-exit=0
  - Update the pinned anchor issue                   exit 0
      (stub) actions/github-script@v9 would PATCH issue #0
=== on+whitespace | event=workflow_dispatch anchor-issue='  ' anchor-optional='true' sweep-exit=0
  - Update the pinned anchor issue                   exit 0
      (stub) actions/github-script@v9 would PATCH issue #0

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)

  • The gate list comes from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 39 commands, derived from merge base 41dcf1188 over the 2 changed paths.
    • All 39 exit 0 on the final run. node scripts/pm/dispatch-gates.mjs --ran ran.list reconciles them as 39 derived, 39 run, 0 NOT-MEASURED, 0 UNRUN, with an exit code recorded for every one. pnpm check:pm-dispatch-gates is longer than the container's foreground cap, so it ran detached, as its header prescribes, under a foreground tail --pid wait. 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 install for the root package only (--filter @objectstack/spec-monorepo). 15 gates first exited 3 (PREREQUISITE NOT MET, the yaml dependency) and are green on the re-run after it.
  • Workflow-valued families: check-half-states.mjs and check-issue-citations.mjs --census are NOT MEASURED locally. Their argv carries runner-only values. This PR's own half-state-patrol.yml run 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 in node_modules/.bin. check:workflow-status-functions and check:workflow-step-name-quoting are green.
  • Changeset: none, because nothing publishes. Both changed paths are owned only by the private root package @objectstack/spec-monorepo. pnpm ls -r finds 81 workspace packages, 69 public, and none of them owns a changed path. check-empty-changeset.mjs --base 41dcf1188 is green (0 changesets added). check-changeset-no-major.mjs --base 41dcf1188 --event EVENT is green, reading this body's Clause-②: no line. The disposition is skip-changeset. This dispatch did not allow label writes, so the seat applies it, and Check Changeset reads red until then.

Acceptance notes

  • One judgment call, stated rather than buried. The value check on anchor-optional sits in "Locate the patrol sources", so it runs on pull_request runs too.
    • For every valid value, the pull_request path is unchanged. The pr+off+empty and pr+on+empty rows match base, and both anchor steps are still skipped.
    • Only a misspelt opt-in behaves differently: it is refused on the PR that adds it, instead of passing that PR's run and first failing on the next scheduled run.
    • If the seat reads "the pull_request path stays unchanged" as ruling this out, the alternative is a mechanical move of the case into the Resolve step (non-PR only).
  • The notice differs from objectui#8740's wording in one clause. It says "the missing anchor does not fail this run" rather than "this run is NOT a failure". With the opt-in on, a sweep that could not run still fails the run in its final step (probe row "on+empty, sweep exit 3"). The notice names the setting (anchor-optional: true) where objectui's named its repository variable.
  • Not exercised on a real runner: the non-PR modes. A workflow_dispatch of 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 objectstack main — objectui's half of objectstack-ai/objectstack#18471 (ruling 只做②) objectui#11174).
  • A composite action does not refuse an input it does not declare. A caller that pins a sha from before this change and passes anchor-optional gets 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)

#!/usr/bin/env python3
# probe-anchor-modes.py ACTION_YML REPO_ROOT [scenario ...]
#
# Runs the half-state-patrol composite action's REAL `run:` bodies, in order, under
# the runner's step rules, once per anchor scenario:
#   - each `run:` is template-expanded (${{ ... }}) and executed as the runner's
#     `shell: bash` does: bash --noprofile --norc -eo pipefail FILE;
#   - each `if:` is evaluated by a small GitHub-expression evaluator, wrapped in
#     success() && (...) unless it names a status function, as the runner does;
#   - GITHUB_OUTPUT / GITHUB_STEP_SUMMARY / RUNNER_TEMP are real files.
# Two steps are STUBBED, both unchanged by the diff under test: the sweep (writes a
# report and publishes the scenario's exit code) and the github-script PATCH
# (prints the issue number Number(ANCHOR_ISSUE) would PATCH, computed by node).
import os, re, subprocess, sys, tempfile, yaml

ACTION, ROOT = sys.argv[1], os.path.abspath(sys.argv[2])
ONLY = sys.argv[3:]
action = yaml.safe_load(open(ACTION))
STEPS = action['runs']['steps']
DEFAULTS = {k: str(v.get('default', '')) for k, v in action['inputs'].items()}

TOK = re.compile(r"\s*(\|\||&&|==|!=|\(|\)|'(?:[^']|'')*'|[A-Za-z_][\w.-]*\(\)|[A-Za-z_][\w.-]*)")

def truthy(v):
    return v not in ('', None, False, 0)

def text(v):
    return 'true' if v is True else 'false' if v is False else '' if v is None else str(v)

def evaluate(expr, ctx):
    s, toks, i = expr.strip(), [], 0
    while i != len(s):
        m = TOK.match(s, i)
        if not m:
            raise ValueError('cannot tokenize: ' + s[i:])
        toks.append(m.group(1)); i = m.end()
    pos = [0]
    peek = lambda: toks[pos[0]] if pos[0] != len(toks) else None
    def take():
        pos[0] += 1
        return toks[pos[0] - 1]
    def primary():
        t = take()
        if t == '(':
            v = orx(); take(); return v
        if t.startswith("'"):
            return t[1:-1].replace("''", "'")
        if t.endswith('()'):
            return {'always()': True, 'success()': ctx['job_ok'], 'failure()': not ctx['job_ok']}[t]
        v = ctx
        for part in t.split('.'):
            v = v.get(part, '') if isinstance(v, dict) else ''
        return v
    def cmp():
        a = primary()
        if peek() in ('==', '!='):
            op, b = take(), primary()
            eq = text(a).lower() == text(b).lower()  # GitHub compares strings case-insensitively
            return eq if op == '==' else not eq
        return a
    def andx():
        v = cmp()
        while peek() == '&&':
            take(); r = cmp(); v = r if truthy(v) else v
        return v
    def orx():
        v = andx()
        while peek() == '||':
            take(); r = andx(); v = v if truthy(v) else r
        return v
    return orx()

def expand(s, ctx):
    return re.sub(r'\$\{\{(.*?)\}\}', lambda m: text(evaluate(m.group(1), ctx)), str(s), flags=re.S)

def should_run(step, ctx):
    cond = step.get('if')
    if cond is None:
        return ctx['job_ok']
    c = str(cond).strip()
    if c.startswith('${{'):
        c = c[3:-2]
    if not re.search(r'\b(always|success|failure|cancelled)\(\)', c):
        c = 'success() && (' + c + ')'
    return truthy(evaluate(c, ctx))

def scenario(name, event, anchor, optional, sweep_exit=0):
    tmp = tempfile.mkdtemp(prefix='hsp-probe-')
    summary = os.path.join(tmp, 'summary.md'); open(summary, 'w').close()
    inputs = dict(DEFAULTS, **{'github-token': 'probe-token', 'anchor-issue': anchor})
    if optional is not None:
        inputs['anchor-optional'] = optional
    ctx = {'job_ok': True, 'inputs': inputs, 'steps': {}, 'github': {
        'event_name': event, 'repository': 'objectstack-ai/objectui', 'workspace': ROOT,
        'action_path': os.path.join(ROOT, '.github/actions/half-state-patrol'),
        'run_id': '1', 'server_url': 'https://github.com', 'sha': '0' * 40}}
    print('=== %s | event=%s anchor-issue=%r anchor-optional=%s sweep-exit=%d'
          % (name, event, anchor, 'UNSET(default %r)' % DEFAULTS.get('anchor-optional', '-') if optional is None else repr(optional), sweep_exit))
    for step in STEPS:
        sid, label = step.get('id', ''), step['name']
        if not should_run(step, ctx):
            print('  - %-48s skipped' % label)
            ctx['steps'][sid] = {'outputs': {}}
            continue
        env = dict(os.environ, RUNNER_TEMP=tmp, GITHUB_STEP_SUMMARY=summary,
                   GITHUB_OUTPUT=os.path.join(tmp, 'out-' + (sid or 'x')), GITHUB_REPOSITORY='objectstack-ai/objectui')
        open(env['GITHUB_OUTPUT'], 'w').close()
        for k, v in (step.get('env') or {}).items():
            env[k] = expand(v, ctx)
        if sid == 'sweep':  # STUB: the sweep itself is not under test
            open(os.path.join(tmp, 'report.md'), 'w').write('os-half-state-sweep (probe stub report)\n')
            open(os.path.join(tmp, 'report.err'), 'w').write('')
            open(env['GITHUB_OUTPUT'], 'w').write('exit_code=%d\n' % sweep_exit)
            code, out = 0, '(stub) check-half-states exited %d' % sweep_exit
        elif 'uses' in step:  # STUB: the github-script PATCH
            n = subprocess.run(['node', '-p', 'Number(process.env.ANCHOR_ISSUE)'], env=env, capture_output=True, text=True).stdout.strip()
            code, out = 0, '(stub) %s would PATCH issue #%s' % (step['uses'], n)
        else:
            script = os.path.join(tmp, 'step.sh')
            open(script, 'w').write(expand(step['run'], ctx))
            cwd = expand(step.get('working-directory', ROOT), ctx)
            p = subprocess.run(['bash', '--noprofile', '--norc', '-eo', 'pipefail', script],
                               cwd=cwd, env=env, capture_output=True, text=True)
            code, out = p.returncode, (p.stdout + p.stderr).strip()
        outputs = dict(l.split('=', 1) for l in open(env['GITHUB_OUTPUT']).read().splitlines() if '=' in l)
        ctx['steps'][sid] = {'outputs': outputs}
        if code != 0:
            ctx['job_ok'] = False
        shown = {k: v for k, v in outputs.items() if k != 'root'}
        print('  - %-48s exit %d%s' % (label, code, ('  outputs ' + repr(shown)) if shown else ''))
        for line in out.splitlines():
            if line.startswith(('::', '(stub)', 'anchor:')):
                print('      ' + line)
    for line in open(summary).read().splitlines():
        if line.startswith(('###', '_')):
            print('  summary: ' + line)
    print('  JOB: %s' % ('success' if ctx['job_ok'] else 'FAILURE'))
    print()

SCENARIOS = [
    ('off+empty',            'schedule',     '',      None),
    ('off-explicit+empty',   'schedule',     '',      'false'),
    ('on+empty',             'schedule',     '',      'true'),
    ('on+whitespace',        'workflow_dispatch', '  ', 'true'),
    ('off+non-numeric',      'schedule',     'abc',   None),
    ('on+non-numeric',       'schedule',     'abc',   'true'),
    ('off+number',           'schedule',     '9857',  None),
    ('on+number',            'schedule',     '9857',  'true'),
    ('pr+off+empty',         'pull_request', '',      None),
    ('pr+on+empty',          'pull_request', '',      'true'),
    ('bad-optional',         'schedule',     '',      'yes'),
    ('pr+bad-optional',      'pull_request', '',      'yes'),
    ('on+empty+sweep-died',  'schedule',     '',      'true', 3),
    ('off+empty+sweep-died', 'schedule',     '',      None, 3),
]
for s in SCENARIOS:
    if not ONLY or s[0] in ONLY:
        scenario(*s)

Generated by Claude Code

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>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat review: domain:devx seat 2 (objectstack-ai/objectui#10917), head c35b098b1

Reviewed at 2026-09-30T08:11Z by session_01TdiauJaVCHuj45EzZGUxHh, against main at 5bed1f6ca. git diff --stat 41dcf1188 5bed1f6ca over both changed paths and scripts/pm/check-half-states.mjs is empty, so the dev's base reading still holds on today's tip.

Verdict: ACCEPT. This PR lands through the ordinary queue.

Independent readings (the seat's own runs, not the dev's)

  • The PR body's probe-anchor-modes.py was run against clean detached worktrees at c35b098b1 and 5bed1f6ca: exit 0, all 14 scenarios, and every verdict matches the body's table. on+empty and on+whitespace read configured=false, Update skipped, JOB success. Both non-numeric rows fail. pr+* skips both anchor steps. on+empty with sweep exit 3 fails.
  • Default mode against main: off+empty, off+non-numeric, off+number, pr+off+empty and off+empty with sweep exit 3 were compared with the outputs annotation, the UNSET header and the worktree path normalised. 66 lines each, diff empty.
  • Guard ablation: on a scratch copy, removing && steps.anchor.outputs.configured == 'true' (0 hits after, restored and git status clean) makes on+empty and on+whitespace read would PATCH issue #0. The guard is load-bearing.
  • Governed: check-governed-merges.mjs --pr objectstack-ai/objectstack#20812 reports 0 of 2 paths governed and 88 changed lines. No review-face path is touched (.github/** only), so no contract-review record is owed.

Ruling on the open question: A, keep the value check in "Locate the patrol sources". Decision A (5905998989) keeps the pull_request path unchanged for the configurations it names, and the probe shows it is for every valid value. Refusing a misspelt declaration on the PR that adds it follows the dispatch's never-widen rule. Otherwise a typo would first surface on a scheduled run as the no-anchor error, which names the wrong remedy. objectui#11174's switch PR is the first caller to pass the input, so its own pull_request run will read the check.

Seat writes: skip-changeset applied (read back ci/cd, size/s, skip-changeset), per the dev's no-publish proof (both paths belong only to the private root @objectstack/spec-monorepo). When CI is green on this head, the seat marks it ready and enqueues it.

Recorded, not acted on: the dev's pnpm install registered core.hooksPath=.githooks in the shared objectstack clone config. That is the repo's per-clone prepare step (AGENTS.md) and needs no revert.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants