Skip to content

check:skill-frame-freshness is red on main — self-test fixture drifted after the principles-only rewrites, so the gate never scans #8024

Description

@huangyiirene

Filed by the domain:metadata PM seat on behalf of #8007's dev, which measured this but could not file it — the objectstack-ai org has no Claude GitHub App connection, so cloud dev containers are blocked from the GitHub API. Text is the dev's.

The defect

pnpm check:skill-frame-freshness fails on a clean main checkout (verified at e38db3df0), before any local changes:

Error: self-test fixture drifted — rule(s) matched nothing anywhere:
  .claude/skills/pm-dispatch/SKILL.md:      - **实际业务需求** — 每个方案先问
  .claude/agents/os-dev.md:                 - **实际业务需求** — 每个方案先问
  skills/objectstack-pm-dispatch/SKILL.md:  - **实际业务需求** — 每个方案先问
    at twoAxisFrameFiles (scripts/check-skill-frame-freshness.mjs:532)

The self-test asserts its fixture patterns still match the live files. The 2026-08-12 principles-only rewrites of SKILL.md and os-dev.md changed the three-axis wording — the current line reads **实际业务需求** — 它服务的是… — so the fixture pattern 每个方案先问 matches nothing and the gate dies in --self-test before scanning anything.

⚠️ Why this matters more than a red gate usually does

While it is red, every PR touching these files sails past a dead gate. The check is not failing on the files it guards — it is failing before it looks at them, so it provides no protection at all right now. A guard that cannot run is worse than an absent one, because its presence in the gate list reads as coverage.

Note the sibling gate check:skill-frame-sync passes — this is fixture drift in the freshness checker only, not a real frame divergence.

Suggested shape

Update the fixture patterns in scripts/check-skill-frame-freshness.mjs to the post-rewrite wording.

⭐ And consider deriving them from the live files the way frame-sync does, so the next rewrite cannot strand them. The sibling gate survived the same rewrite that broke this one, which is the argument for the derived approach rather than a second hand-maintained copy of the wording.

Establishment

Verified against an untouched checkout — the failure is identical on clean main, so it is genuinely pre-existing and not introduced by #8007's branch.

Routing

⚠️ The script lives in scripts/ but guards skill files — filed without a domain: label so the triage seat can route it (domain:devx or a skills-owning lane). ⛔ Not this seat's to fix.

Activity

  1. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Triage: routed domain:devx. The fix lands in scripts/check-skill-frame-freshness.mjs (update the self-test fixture patterns; the card's ⭐ suggestion — derive them from the live files the way the surviving sibling frame-sync does — is the shape that prevents the next stranding and is endorsed as the preferred route). The guarded files are skill roots, but the fix itself does not touch them — scripts/ gate class is devx.

    ⚠️ Boundary for the dev: if the diff turns out to need edits under .claude/skills/** or skills/**, stop and report — that path is ADR-class (human merge only) and changes the card's routing.

    Priority note for the devx seat: this is a dead gate on main — while red it protects nothing and its presence in the gate list reads as coverage. Treat as queue-top for the lane.


    Generated by Claude Code

  2. self-assigned this
    on Aug 12, 2026
  3. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Claim: PM loop round 5
    Session: session_01PaiisQMhsYxwa5ZX6Mfmv2
    Branch: claude/issue-8024-skill-frame-freshness-fixture
    Worktree: objectstack-issue-8024
    Domain: domain:devx
    File surface: scripts/check-skill-frame-freshness.mjs only. ⛔ NOT .claude/skills/**, NOT skills/**, NOT .claude/agents/** (stop on breach; explain in the report)
    Container & model: S, mode:cloud, model: opus
    Serial constraints cleared: no open PR touches this script. Sibling card #8020 claimed the same round is scripts/check-test-source-alias.mjs — different file.

    Premise reproduced locally on origin/main, and it is sharper than the card states:

    $ node scripts/check-skill-frame-freshness.mjs --self-test   → EXIT=1
        Error: self-test fixture drifted — rule(s) matched nothing anywhere
    $ node scripts/check-skill-frame-freshness.mjs               → EXIT=0
        ✓ the decision frame in this tree is current with origin/main
          4 copies across 3 files; 3 axes … the frame itself is unchanged
    

    The gate itself is healthy — it is simply never reached. package.json:44 is … --self-test && … , so the fixture failure short-circuits the real scan. That confirms the card's "dies before it looks at them" and adds the reassuring half: there is no actual frame divergence to chase. Consistent with check:skill-frame-sync passing.

    ⚠️ Routing note that decides the merge channel. The fix belongs in scripts/, so this is a normal-channel PR. It would become a human-merge PR the moment it touched .claude/skills/** or skills/** — and there is a tempting wrong way to "fix" this that does exactly that: editing the skill files back to the old wording so the stale fixture matches again. That inverts the defect (the skill files are correct post-rewrite; the fixture is stale) and changes the merge channel. Dispatched with that ruled out explicitly.


    Generated by Claude Code

  4. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor
    {
      "issue": 8024,
      "status": "done",
      "branch": "claude/issue-8024-skill-frame-freshness-fixture",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8109",
      "premise_still_valid": true,
      "summary": "Reproduced both exit codes on origin/main exactly as the dispatch stated: --self-test EXIT=1 (fixture drifted), the bare scan EXIT=0 (frame current, 4 copies / 3 files / 3 axes). The gate was healthy and never reached, because package.json joins the halves with &&. Took the card's starred route: the two-axis specimen is now DERIVED from the live documents (count sentences through each copy's own start/binding anchor, the axis entry through the shared entry parser plus AXIS_MAP, mentions through a shared mention scan) instead of re-spelled by a literal find/replace table. check-skill-frame-sync.mjs gains three exports for this (ENTRY_START, axisEntryStarts, frameCountMentions) and its own mention check now runs through the last of them, so the two scripts cannot disagree about what a mention is; cases 8 and 9, which broke a declaring sentence by spelling it out, are derived from the same anchors. No skills root touched: the diff is scripts/check-skill-frame-freshness.mjs and scripts/check-skill-frame-sync.mjs only, so the PR stays in the normal merge channel.",
      "tests": "BEFORE (clean worktree at d91c558): `node scripts/check-skill-frame-freshness.mjs --self-test` EXIT=1, `Error: self-test fixture drifted — rule(s) matched nothing anywhere`; `node scripts/check-skill-frame-freshness.mjs` EXIT=0, `the decision frame in this tree is current with origin/main ... 4 copies across 3 files; 3 axes`. AFTER: `pnpm check:skill-frame-freshness` EXIT=0 — self-test 12/12 AND the scan, which now runs at all. `pnpm check:skill-frame-sync` EXIT=0 — 12/12 plus its scan (`4 copies ... 4 count mention(s) agree; 42 markdown files scanned`). `pnpm check:nul-bytes` OK (7365 files). `node scripts/pm/dispatch-gates.mjs scripts/check-skill-frame-freshness.mjs scripts/check-skill-frame-sync.mjs` surfaces no additional named family. eslint NOT run: no node_modules exists in this container and a full workspace install for two dependency-free root scripts is a bad trade; the applicable rule block for **/*.mjs is no-restricted-imports on @objectstack/spec subpaths, which neither file imports. REVERSE VERIFICATION, direction predicted first in each case: (1) predicted red — restore origin/main's script, `pnpm check:skill-frame-freshness` EXIT=1 with the drift error and NO scan summary line, confirming the scan is never reached. (2) predicted loud-red, not vacuous-green — neuter the demotion so unmarkEntryLine returns the line unchanged: red, `un-marking the axis entry left it still matching ENTRY_START` at SKILL.md:604; bypass that assertion too: red, `the demoted copy reads unparseable`, expected 2 axes. (3) ACROSS THE REWRITE BOUNDARY, and this one came back INVERTED from my prediction: same three documents at ab19075^ (pre-rewrite) and at HEAD, each run through the old fixture and the new. New fixture GREEN 12/12 on both. Old fixture RED on both — I predicted green on the pre-rewrite documents it was written against. It had already stranded a second, independent way: its mention rule spelled 沿三条固定评估轴, which does not cover the bare 三条评估轴 mention in the same file, so its specimen was incoherent and the sync gate reported it, failing the independence case rather than throwing. Two stranding events in one hand-maintained copy.",
      "open_questions": [
        {
          "question": "The dispatch asked me to measure, not assume, whether deriving the fixture from the live files makes the freshness gate tautological. Answer: no, and the reason is which half is derived.",
          "options": [
            "A — derived: what is derived is how the SPECIMEN is manufactured, never what the gate compares; the gate's own comparison (this tree's structure vs origin/main) touches none of it, and the self-test still commits specimen and real documents as two commits in a temp repo and demands the older be called BEHIND",
            "B — hand-repaired: re-spell the fixture patterns to the post-rewrite wording"
          ],
          "recommendation": "A, implemented. The vacuity worry lands on the wrong half of the script: freshness compares a tree against a remote ref, and that code path never reads the fixture. A demotion that silently did nothing would make specimen and real identical and case 1 would fail 'expected error, got ok' — measured twice above, and it fails even earlier at the ENTRY_START assertion, naming file and line. The evidence against B is the third reverse verification: the hand-maintained table stranded TWICE, so re-spelling it schedules a third."
        }
      ],
      "out_of_scope_findings": [
        "filed as #8110: check:skill-frame-freshness --self-test runs nowhere in CI (the scan is CI-hostile by design, but the self-test half is not — it is pure self-consistency, no network, no ref), so a broken fixture is invisible until a human runs the gate; this is why the first stranding was never noticed"
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  5. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    ACCEPT — PR #8109. Reviewed against GitHub, not the report's self-description.

    check result
    path surface (get_files) 2 files, both scripts/ — check-skill-frame-freshness.mjs, check-skill-frame-sync.mjs. Zero .claude/skills/**, zero skills/**, zero .claude/agents/** ⇒ ruling 1 honoured and this stays in the normal merge channel
    CI 23 checks, zero failures
    labels skip-changeset (scripts ship in no published package)

    It took the derived route — and answered the objection I raised rather than dodging it

    My Block 3 asked whether deriving the fixture from the live files makes the gate tautological (always green because it compares the files to themselves), and said a correct hand-maintained fixture beats an elegant vacuous one. The PR has a section headed "WHY THIS IS NOT THE TAUTOLOGY IT LOOKS LIKE", and its answer is better than my question:

    what is derived is how the SPECIMEN IS MANUFACTURED, not what the gate compares. The gate's own comparison (this tree's structure vs origin/main's) never touches any of this.

    And it supplies the falsification rather than asserting it: the self-test still commits the demoted specimen and the real documents as two commits in a temp repo and demands the gate call the older one BEHIND — "a claim the derivation cannot fake: a demotion that silently did nothing would make specimen and real identical, and case 1 would fail with 'expected error, got ok' instead of quietly passing." The objection landed on the wrong half of the script, and it said so with evidence.

    Structural win: one definition instead of two

    ENTRY_START, axisEntryStarts() and frameCountMentions() are now exported from the sync gate and consumed by the freshness self-test, with the mention check rewritten to use the shared function — "One scan, one definition … so the two can never disagree about what counts as a mention." The root cause here was a second hand-kept copy of prose the gates exist to police; the fix removes the second copy rather than refreshing it. That is why the sibling gate survived the rewrite that killed this one, and now both share the surviving mechanism.

    It also repaired cases 8 and 9, which carried the same latent defect (hand-spelled sentences that would strand on the next rewrite) — unprompted, and correctly, since they would have been the next failure.

    Honest about its own limits

    It states plainly what it could not derive — narrative prose stating a count that neither gate reads ("analyzed on **all three** axes", 三轴冲突时) — and why demoting every "three" in a watched file would be wrong (it would hit sentences about unrelated axes). So the specimen is deliberately a structurally faithful two-axis tree rather than a copy-edited one. Naming the residue beats silently leaving it.

    And the new fixtureError names the file and line and distinguishes the two cases for the reader: "this is a change of STRUCTURE, not of wording — reword the frame freely, but adding, removing or reshaping an axis means editing the derivation". That is the "next rewrite costs a minute instead of a card" property I suggested, delivered.

    Marking ready and arming auto-merge. Tracked to MERGED with the closed_by_pull_requests check.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions