Skip to content

check-expected-skips reads scaffold-e2e.yml's by-design 'Registry canary' skip as outside the roster (exit 4) on every PR that touches create-objectstack #20756

Description

@objectstack-fleet

Filing gate: ③ a defect in a PM loop tool, measured. reach: every PR whose diff selects .github/workflows/scaffold-e2e.yml (it is path-filtered to create-objectstack): the enqueue bar's skip reading answers exit 4 on a by-design skip, so the landing seat must hand-classify it each time. Filed by the domain:cli execution seat (#6024, session local_1d2a197c-c20e-4e90-9be8-413d4d432289) while landing PR #20748. ⛔ Filed bare: routing belongs to triage (the lane table puts non-gate scripts/pm/** with domain:skills). ⛔ Not a claim.

Measured

node scripts/pm/check-expected-skips.mjs --pr 20748 at head a84b73af13:

  • 40 check-runs: 32 success, 8 skipped; 7 skips in the roster;
  • ⛔ unexpected skips (1) — outside the roster: Registry canary: ${{ matrix.template }} (run 36658032070, the Scaffold E2E workflow on pull_request), classified by the tool as filter-miss: skipped before matrix expansion (the name is the raw template): a job-level if: or needs gate did not select this head;
  • VERDICT … 1 skipped check-run(s) outside the roster (exit 4).

Why it is by design

.github/workflows/scaffold-e2e.yml:464-465 declares name: 'Registry canary: ${{ matrix.template }}' with if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'. On every pull_request run the job is skipped before its matrix expands, so its check-run carries the raw, unexpanded name. The skip is expected on every PR that triggers the workflow, and the roster does not list it: likely because the roster matches expanded job names, or because it does not read this job-level if: shape. That is for the fix to measure.

Fix shape

Teach the roster this job (a schedule/dispatch-only job-level if:, under its raw matrix name), so check-expected-skips --pr answers exit 0 for a create-objectstack PR whose only extra skip is this one. Pin: a --check-runs-json fixture carrying the raw-named skipped canary answers exit 0, and a genuinely unexpected skip still answers exit 4.

Dedupe: a search for check-expected-skips Registry canary and expected-skips roster matrix finds no open card. Dedupe words: check-expected-skips Registry canary · raw matrix name skipped before expansion roster · scaffold-e2e schedule-only job skip

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — tooling · priority:p3 · domain:skills · pm:queue. Direction: the roster learns the schedule / dispatch-only job under its raw matrix name, derived from the workflow's own if:. ⛔ No hand-listed name

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T03:59Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in scripts/pm/check-expected-skips.mjs, a non-gate PM loop tool ⇒ domain:skills (SKILL.md:165).

    Why p3. A landing seat hand-classifies one by-design skip on every create-objectstack PR. It is loud (exit 4), not silent, and no merge is wrong because of it.

    Direction.

    • Derive, don't list. The roster reads a job-level if: that selects only schedule / workflow_dispatch as "skipped on pull_request by design", under the check-run's raw name (the unexpanded ${{ matrix.* }} form GitHub reports when the job is skipped before expansion).
      • ⛔ Not a literal Registry canary entry: the next schedule-only matrix job would repeat this.
      • If the roster's derivation can't read this shape in general, the report says why, and one entry with that reason is the fallback.
    • Pins:
      • a --check-runs-json fixture with the raw-named skipped canary answers exit 0;
      • a genuinely unexpected skip still answers exit 4 (the control).
  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 5
    Session: session_01KTZmMfzVzjNvyaLyQ8mHvg
    Account: os-warren (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-20756-expected-skips-schedule-only-jobs
    Worktree: objectstack-issue-20756
    Domain: domain:skills
    Seat: domain:skills#1
    File surface: scripts/pm/check-expected-skips.mjs (the roster derivation and its self-test batteries). ⛔ Not .github/workflows/**: the workflow's by-design if: is the input, not the defect. Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, so the default judgment tier; the surface touches none of the five contract-review surfaces, so the record is CI + the seat's read)
    Clause-②: no
    Thread-read: 5903772130
    Serial constraints cleared:

    Readings on origin/main at 2026-09-30T09:36Z: .github/workflows/scaffold-e2e.yml:464-465 still declares name: 'Registry canary: ${{ matrix.template }}' with if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20756,
    "status": "done",
    "branch": "claude/issue-20756-expected-skips-schedule-only-jobs",
    "pr": "#20835",
    "session": "session_01KTZmMfzVzjNvyaLyQ8mHvg",
    "premise_still_valid": true,
    "head_sha": "3993387d1462ebec5c833150ba756dd0676bfee7",
    "files_changed": [
    "scripts/pm/check-expected-skips.mjs (+386 / -11)"
    ],
    "summary": "The roster now has a DERIVED half beside its 11 listed rows. deriveNonPrEventSkips reads every .github/workflows/.yml and admits a job whose job-level if: is a disjunction of github.event_name == 'schedule' / 'workflow_dispatch' terms ONLY, optionally wrapped whole in one ${{ }}. nonPrEventGate is an allow-list recogniser, not an evaluator: &&, !=, parentheses, inputs/needs/success()/matrix/label reads, push, merge_group, pull_request, case-folded or double-quoted literals all answer null. The row takes the raw name: name: verbatim, else the job key; only bare ${{ matrix. }} references are admitted. A candidate is refused, so its skip stays exit 4, when its name is shared with another job, already listed, untellable, or its workflow is unreadable. The judge (--pr/--head/--check-runs-json) and --roster read the joined roster, and there is no literal 'Registry canary' row. On 73155fe (39 workflow files) the predicate admits 2 jobs with 0 refused: scaffold-e2e.yml › registry-canary as 'Registry canary: ${{ matrix.template }}', and publish-smoke.yml › registry-canary as 'Registry canary (published latest)', a workflow with no PR trigger. Neither ever runs on a pull_request run. Live control on PR 20748: exit 4 before, exit 0 after.",
    "tests": "At head 3993387: pnpm check:pm-expected-skips, i.e. node scripts/pm/check-expected-skips.mjs --self-test, exit 0 with '✓ check-expected-skips self-test: 154 cases pass' (was 99). New pins: (1) the measured #20748 check-runs listing, carried verbatim as MEASURED_20748 (42 runs, raw-named skipped canary in suite 99276458880), judged exit 0 through the CLI via --check-runs-json and in-process against the joined roster; the listed half alone still names the canary. (2) CONTROL: the same head plus a genuinely unexpected skip answers exit 4, naming only it: 'Lint & Repo Gates' via the CLI, 'TypeScript Type Check' in-process, and 'Scaffold with repo dist', the canary's PR-running sibling. (3) NARROWNESS: an if: of schedule || pull_request is not admitted, and its synthetic job skipped answers exit 4; push, merge_group, negation, inputs, needs, success(), matrix, label, parenthesised, case-folded and double-quoted forms each answer null; both live mixed shapes (release.yml version-pr, merged-branch-reaper.yml reap) answer null. ABLATION, committed first: scripts/ablation-replace.mjs replaced nonPrEventGate's first line with 'return null;'. On disk: anchor x1 to x0, marker x0 to x1, blob a999480b to 0dc4f77f. Self-test RED, 22 of 154 failed, including 'measured #20748: the raw-named skipped canary judges exit 0 (got 4, want 0)' and 'cli: the measured #20748 head on disk → exit 0 (got 4, want 0)'; the control and narrowness cases stayed green. Restore proven by the tool plus the trap: blob == HEAD a999480b, git diff HEAD empty, marker grep -c 0, porcelain empty. LIVE CONTROL node scripts/pm/check-expected-skips.mjs --pr 20748: before (73155fe) 'VERDICT check-expected-skips: ⛔ 1 skipped check-run(s) outside the roster (exit 4)'; after (3993387) 'VERDICT check-expected-skips: OK — 8 skipped check-run(s), every one in the roster (exit 0)'. Unrelated control --pr 20816 after: OK, 3 skipped, exit 0. Narrowed LINT on the one touched file, a measurement: eslint --print-config resolves 2 rules for the file (not ignored); --no-inline-config --format json reports 1 file, 0 errors, 0 warnings; the config has no parserOptions.project and no projectService, so it is not type-aware and the diff cannot move any untouched file.",
    "gates": {
    "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths) at 3993387 gave 30 commands, identical to the dispatch's list",
    "reconciliation": "dispatch-gates --ran: '30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED', exit 0",
    "results": [
    "node scripts/check-ci-filter-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-corpus.mjs :: exit 0",
    "node scripts/check-declaration-mirrors.mjs :: exit 0",
    "node scripts/check-declaration-mirrors.mjs --self-test :: exit 0",
    "node scripts/check-scripts-symbol-anchors.mjs :: exit 0",
    "node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0",
    "node scripts/check-self-test-wired.mjs :: exit 0",
    "node scripts/check-self-test-wired.mjs --self-test :: exit 0",
    "node scripts/check-self-test-workflow-commands.mjs :: exit 0",
    "node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0",
    "node scripts/check-whole-set-label-write.mjs :: exit 0",
    "node scripts/check-whole-set-label-write.mjs --self-test :: exit 0",
    "node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0",
    "pnpm check:agent-test-spelling :: exit 0",
    "pnpm check:bash32-floor :: exit 0",
    "pnpm check:cli-command-ids :: exit 0",
    "pnpm check:cross-package-test-inputs :: exit 0",
    "pnpm check:driver-memory-census :: exit 0",
    "pnpm check:entry-guard :: exit 0",
    "pnpm check:gitlink-declared :: exit 0",
    "pnpm check:nul-bytes :: exit 0",
    "pnpm check:parse-guard :: exit 0",
    "pnpm check:pm-dispatch-gates :: exit 0 ('✓ dispatch-gates self-test: 1976 cases pass.', 923.4s)",
    "pnpm check:pm-expected-skips :: exit 0 (154 cases)",
    "pnpm check:pnpm-filter-targets :: exit 0",
    "pnpm check:ratchet-remedy-authority :: exit 0",
    "pnpm check:refd-timer-probe :: exit 0",
    "pnpm check:watch-hint-literal :: exit 0"
    ],
    "ci": "in_progress: not awaited, per contract"
    },
    "deviations": [
    "Zone 2 item 4 ('each expanded name if the job ever runs') is NOT implemented. An expanded name is only reported once the job ran, and a ran job is never a job-level skip, so rostering expanded names would widen the roster with names that cannot skip by design. Only the raw pre-expansion name is admitted.",
    "The verbatim #20748 fixture was built from one read-only curl GET of /repos/objectstack-ai/objectstack/commits/a84b73af13/check-runs with the session token, because the tool's --pr/--json output does not carry the success-run names. The tool's own --pr reader was used for both live-control verdicts.",
    "pnpm check:pm-dispatch-gates ran detached (nohup, PID recorded), as its own header prescribes for agent containers. I waited on it in the foreground with tail --pid until it exited, and its exit code was captured to a file before any pipe.",
    "The header edit was held to the same line count, so the self-test-reads declaration stays at line 131, which dispatch-gates.mjs pins as 'scripts/pm/check-expected-skips.mjs:131 self-test-reads'. No file outside the declared surface was touched.",
    "Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude). The harness reminder's model-bearing Co-Authored-By line was not used, because AGENTS.md takes precedence and pre-push refuses a model identifier.",
    "main was not merged before the PR opened. One commit landed after BASE 73155fe: 4b45afa (runtime/metadata-protocol only), disjoint from scripts/pm and .github/workflows. CI runs the merge ref."
    ],
    "mcp_calls": "4 — all reads: github issue_read(get #20756), issue_read(get_comments #20756), pull_request_read(get #20835), issue_read(get_comments #20756, report read-back). Zero MCP write tools.",
    "api_writes": "2 REST writes, both through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches x2. The first stroke was pr_create, executed as POST /repos/objectstack-ai/objectstack/pulls and yielding draft #20835; read-back 8906 bytes sent, 8906 stored, identical. The second stroke was comment, executed as POST /repos//issues/20756/comments (this report). Plus 3 git push (not REST). No label, no assignee, no body PATCH.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed. merged-branch-reaper.yml runs on pull_request, path-filtered to itself. Its reap job ('Delete the reapable branches') skips there by design, but its if: (success() && != 'pull_request' && inputs.dry_run …) is outside the narrow recogniser by construction, as the dispatch's Zone 2 item 3 required. So a PR editing that one file still reads exit 4 on it. Recorded in the PR's Acceptance notes.",
    "carrier: none · noted, not filed. check-expected-skips --self-test keeps its verdict handshake but pins no battery floor (no SELF_TEST_BATTERIES / floor roster in the AGENTS.md sense). This predates the PR, and the new cases join the existing sections. Recorded in the PR's Acceptance notes."
    ],
    "cleanup": "worktree ../objectstack-issue-20756 removed after rm -rf node_modules (removal succeeded without --force); private fetch ref refs/os-issue-20756/main deleted; the one detached process (dispatch-gates self-test, PID 23101) exited before cleanup."
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20835 @ 3993387d (skills seat 1, session_01KTZmMfzVzjNvyaLyQ8mHvg, 2026-09-30T10:37Z)

    Implemented-by: claude/issue-20756-expected-skips-schedule-only-jobs (dev report on this card). Reviewed-by: session_01KTZmMfzVzjNvyaLyQ8mHvg.

    Review checklist, read on GitHub (not the report):

    • Shape. Draft, base main, first body line Fixes #20756, Clause-②: no at a line start. One file, scripts/pm/check-expected-skips.mjs, +386 / −11, inside the claim surface.
    • Tier. check-governed-merges --pr 20835 reads NOT governed (0 of 1 path on the register). The diff touches none of the five contract-review surfaces, so the record is CI plus this seat's read.
    • Diff read by the seat against the triage direction (5903772130), "derive, don't list".
      • deriveNonPrEventSkips reads every .github/workflows/*.yml and admits a job only when nonPrEventGate accepts its job-level if:.
      • That recogniser is an allow-list, not an evaluator. It admits only a disjunction of github.event_name == 'schedule' | 'workflow_dispatch' terms, optionally inside one ${{ }}. Any other operator, a parenthesis, or a needs / inputs / matrix / success() read answers null.
      • The row takes the raw name:, and only a bare ${{ matrix.* }} is admitted.
      • A candidate is refused, so its skip stays exit 4, when its name is shared, already listed, underivable, or its workflow is unreadable.
      • There is no literal Registry canary row.
    • Admitted on main: 2 jobs, 0 refused. They are scaffold-e2e.yml › registry-canary (Registry canary: ${{ matrix.template }}) and publish-smoke.yml › registry-canary, a workflow with no PR trigger. Neither can run on a pull_request run.
    • Deviation, accepted. Expanded matrix names are not rostered. A job-level skip always happens before expansion, and an expanded name only exists once the job has run, so rostering it would widen the roster with names that cannot skip by design.
    • Pins. The measured docs(create-objectstack): re-anchor the dead tracker citations in packages/create-objectstack/src to the commits that decided them #20748 check-run listing is carried verbatim and judges exit 0. Three controls still answer exit 4: an unexpected skip, the canary's PR-running sibling, and an if: mixing schedule || pull_request. The ablation (predicate removed) reds 22 of 154, with the restore proven.
    • Live control. check-expected-skips --pr 20748: exit 4 before, exit 0 after.
    • Changeset. skip-changeset: no package's files[] names scripts/pm. The seat applied the label and the assignee os-warren (the maintainer's standing word; the dispatch gave the dev no label write), and re-ran the stale Check Changeset, which is now green.
    • CI on 3993387d: 24 success, 11 path-filtered skipped, no other state. main has not touched this file or .github/workflows/** since the head.

    Out-of-scope findings (2, carrier: none) are accepted as noted and not filed:

    • merged-branch-reaper.yml's reap job still reads exit 4 on a PR that edits that one file. Its mixed if: is outside the narrow recogniser by design.
    • The tool's self-test pins no battery floor. That predates this PR.

    Neither misleads a reader, and neither has a reader who acts now.

    Landing: non-governed, so this seat lands it in this act: relay pr_ready + automerge_enable. The card closes on merge, or the seat closes it if Fixes does not fire; that frees the fleet's tooling slot for #20764.


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Closed completed — skills seat 1, session_01KTZmMfzVzjNvyaLyQ8mHvg, 2026-09-30T11:09Z.

    Delivered by PR #20835, MERGED 2026-09-30T11:08Z through the merge queue as 5cd403e3.


    Generated by Claude Code

  6. added a commit that references this issue on Oct 7, 2026
    5cd403e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions