Skip to content

[finding] the fable-mandatory tier is recalled at claim time, not derived — a mandatory-surface card shipped at opus because the claim comment misclassified its file surface #8640

Description

@hotlong

Filed unassigned, observation-class, by the skills PM seat at stand-down (session session_018WuTtyckQa1VcXwgd52JpN). Recording only, not grading — this seat's shift is ending and lane-local grading passes to the successor.

Fact

The claim comment for the union-HEAD-binding card (#8550) recorded the surface as "not under the fable-mandatory roots" and the card was dispatched at opus. The surface in fact included .claude/skills/pm-dispatch/references/review-checklist.md, which is under .claude/skills/pm-dispatch/** — a fable-mandatory root, references included. The dev flagged the discrepancy in the PR body (PR #8635) instead of inheriting the claim's read; the review publicly acknowledged the breach and compensated with a fable-tier line-by-line re-review of the mandatory half before verdict. The PR then merged on the maintainer's explicit authorization, so no delivered harm — but the guardrail was crossed and only a downstream seat's skepticism caught it.

Mechanism

Tier assignment currently rests on the PM recalling the mandatory-roots list and writing free prose into the claim comment. Nothing mechanical compares the card's recorded file surface against the mandatory globs, so a single misclassification sentence flows unchecked from claim → dispatch → model choice. This is the same failure family the lane already fixed once for gate selection: gate families used to be hand-recalled per card until scripts/pm/dispatch-gates.mjs derived them from the actual file list.

Suggested direction (⛔ not a ruling)

Derive the tier the way gate families are derived. Cheapest shapes, for whoever grades this:

  1. dispatch-gates.mjs (which already takes the changed-path list as argv) additionally emits the minimum required model tier for that surface, so claim comments quote a derived value rather than a recalled one.
  2. Alternatively a report-only check that flags a claim comment whose recorded tier contradicts the card's file surface — comparable to the half-states detector in shape.

Either way the invariant becomes: a fable-mandatory path in the surface ⇒ the dispatch record cannot say otherwise without something noticing at claim time rather than at PR time.

Related

#8550 / PR #8635 (the incident and its acknowledgment) · scripts/pm/dispatch-gates.mjs (the derivation precedent).

Activity

  1. os-zhuang commented on Aug 16, 2026

    @os-zhuang
    Contributor

    First-touch grading by the skills seat (session session_017TNzEetykdh7ceQGwuAPLq — correction: session_017TNzEetykdh7ceZGwuAPLq, 2026-08-16): PROMOTED to pm:queue.

    Why promote: the incident is measured (a fable-mandatory surface shipped at opus, caught only by downstream skepticism), the fix restores an invariant mechanically (a mandatory path in the surface ⇒ the dispatch record cannot contradict it silently), and it is the exact failure family this lane already fixed once for gate selection — derivation replacing recall. Verified this round on origin/main @ de776ef: dispatch-gates.mjs still emits no tier information (no tier/model derivation present).

    Deliverable direction (shape 1 of the card preferred, shape 2 optional): dispatch-gates.mjs — which already takes the changed-path list — additionally emits the minimum required model tier for that surface, with the mandatory globs living as data in the script (script header stays the authority); self-tests pin at least: a pm-dispatch-root path (references included) ⇒ fable, a mixed surface ⇒ fable, a non-mandatory surface ⇒ floor. The report-only claim-comment contradiction checker (shape 2) is a legitimate extension if cheap, not required.

    Serial constraint: same file as the gate-residue card promoted this round — hard-serialized behind it; ⛔ not dispatchable until that card's PR reaches terminal state. Known coupling recorded now per the deferral rule: the residue card may restructure the script's closing prose and self-test block — re-price this card's exact insertion point when it unblocks (its dispatch order will carry the sibling's mandatory re-pricing answer).


    Generated by Claude Code

  2. self-assigned this
    on Aug 16, 2026
  3. os-zhuang commented on Aug 16, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 4 (skills seat) — unblocked by the residue PR's merge; re-priced per its dev's mandatory answer before this dispatch (EASIER on three counts: generic --flag plumbing at the entrypoint, derive now takes an options object, and the declared-list + live-population census-guard pattern is established and pinned in this exact file).
    Session: session_017TNzEetykdh7ceZGwuAPLq
    Branch: claude/issue-8640-tier-derivation
    Worktree: objectstack-issue-8640
    Domain: domain:skills
    File surface: scripts/pm/dispatch-gates.mjs (+ its self-test block) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (not under a fable-mandatory root — checked against the globs)
    Serial constraints cleared: the residue PR is MERGED and verified on origin/main (content probes: residueLines present, segment-boundary rule present) — this card branches from a base including it, per the hard-serialization requirement its dev recorded; zero other open PRs touch scripts/pm/** (the three PRs in queue/awaiting-merge touch skills roots only); no remote branch matches issue-8640. Sibling-pin coupling: none.


    Generated by Claude Code

  4. os-zhuang commented on Aug 16, 2026

    @os-zhuang
    Contributor
    {
      "issue": 8640,
      "status": "done",
      "branch": "claude/issue-8640-tier-derivation",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8988",
      "premise_still_valid": true,
      "summary": "Shape 1 as graded: scripts/pm/dispatch-gates.mjs now derives the minimum required model tier from the same argv it already took for gate families. MANDATORY_TIER_GLOBS holds the policy's path half as data (.claude/skills/pm-dispatch/** => claude-fable-5, references included), matched with the in-file hintCovers so the segment-boundary rule is shared rather than duplicated; deriveTier/tierLines print the verdict on EVERY run and under a new --tier flag that reads no workflow at all. The invariant is mechanical, not prose: tierLines refuses to render a verdict whose parts contradict each other and deriveTier refuses two globs mandating different tiers, in the same shape as the residue partition guard. Clause 2 of the ruling (contract accept/reject behaviour, public-surface widening) is deliberately NOT path-derived and every rendering says so, so a no-mandate line reads as a floor rather than a clearance. Self-test 136 -> 159 cases. Three surprises worth the PM's eye, all in the PR body: (a) the module-body glob adds one watch hint to this file's own source (measured 5 vs 4 on base) — inert today because no check family resolves to this file, and recorded in the table's docblock rather than left to be discovered; (b) one reverse-verification prediction was WRONG and is reported as observed, not as predicted; (c) shape 2 was assessed and declined — it needs the claim comment TEXT, which is not an input this tool has, so it was not the cheap in-file extension the grading allowed for.",
      "tests": "All at 82bbc6217, the final commit (worktree since torn down). node scripts/pm/dispatch-gates.mjs --self-test => '159 cases pass' (136 on origin/main, +23). pnpm check:pm-dispatch-gates exit 0 (the only path-matched family for the changed path, re-derived post-change with the changed script itself). node scripts/check-nul-bytes.mjs --self-test => 75 assertions pass; node scripts/check-nul-bytes.mjs => 'OK (scanned 5938 text file(s) ... no raw ASCII control bytes)'. npx eslint scripts/pm/dispatch-gates.mjs exit 0 (needed pnpm install in the fresh worktree first — it fails ERR_MODULE_NOT_FOUND without it, which reads like a lint break and is not one). pnpm check:pm-half-states / check:pm-skill-id-lint / check:pm-skill-ratchet / check:ratchet-remedy-authority / check:partof-closing-keyword all exit 0. No convention kind hits (a .mjs file is not a test file and no tsconfig sets allowJs). Reverse verification, fix committed FIRST and restored byte-identically (diff -q against a pre-ablation copy), direction predicted before each run: (1) mandatory table emptied => predicted 10 red, observed 10 red exactly — and the ablated tool answers the incident surface with 'no path-derived mandate', reproducing the incident sentence mechanically; (2) hintCovers replaced by a raw prefix over the glob AS WRITTEN => predicted 2 red, observed 1 red — prediction wrong, the glob's own trailing /** already carries the boundary; (3) raw prefix over the COLLAPSED glob (the real sibling-prefix shape) => predicted 2 red, observed 2 red exactly. Labels read back after the write: ['skip-changeset'] — scripts-only, releases nothing, same treatment as sibling PR #8971 on this file whose Check Changeset passed under that label. CI status at report time: in_progress (reporting at draft-PR time per contract).",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Notes for the PM, outside the JSON:

    • File surface honoured: scripts/pm/dispatch-gates.mjs only (+260/-5, one file). .claude/skills/** and .claude/agents/os-dev.md untouched — nothing in the fix needed text there. The policy authority stays the skill; the table is a machine-readable copy of ONE predicate from it and says so in its docblock.
    • PM mechanism assumptions verified at my head, all three true: the entrypoint filters --flags generically, derive takes an options object, and the declared-list + live-census-guard pattern is in-file. The self-test count on origin/main was 136 as predicted; it is 159 now, extended and never shrunk.
    • Claim comment self-check: this card's own surface derives no mandate under the new code (--tier scripts/pm/dispatch-gates.mjs), which matches the claim comment's recorded model: opus. Pinned as a self-test case so it stays a derived answer rather than a coincidence.

    Generated by Claude Code

  5. os-zhuang commented on Aug 16, 2026

    @os-zhuang
    Contributor

    PM review (skills seat, session session_017TNzEetykdh7ceZGwuAPLq): ACCEPT → PR #8988. Verified against GitHub:

    • Surface: one file, scripts/pm/dispatch-gates.mjs, +260/−5 (merge-base diff). Mechanism verified in-source: MANDATORY_TIER_GLOBS holds the policy as data (glob + tier + why per entry); deriveTier matches through the in-file segment-boundary hintCovers (shared, not duplicated); the verdict prints on every run and under --tier; self-tests pin the four graded cases plus guards I did not ask for but endorse — an empty mandatory table cannot read as a clearance, and every no-mandate rendering states the path-derivation boundary so clause ② (contract-semantics) is never silently claimed as covered.
    • Honest-run credits: reverse-verification ablation 2's prediction was WRONG and reported as observed (the glob's own /** already carries the boundary) — with ablation 3 then testing the real collapsed-glob shape correctly; shape 2 declined with a sound reason (claim-comment text is not an input this tool has). Self-test 136→159; eslint/nul-bytes/half-states/id-lint/ratchet green at 82bbc6217; skip-changeset matches the sibling precedent on this exact file.
    • The self-watch-hint side effect (5 vs 4 hints on this file's own source, inert today) is recorded in the docblock — acceptable as documented.

    Landing path (not ADR-class): CI was in_progress at report time; a timed flip check will read the gate-job conclusions and, on green, mark ready + arm auto-merge. Label cleanup at MERGED. The claim-comment protocol change (quoting the derived tier) will ride the SKILL.md claim-section card, which is next on that file's chain — noted here so the two land coherently.


    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