Repository navigation
[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
Activity
First-touch grading by the skills seat (session
session_017TNzEetykdh7ceQGwuAPLq— correction:session_017TNzEetykdh7ceZGwuAPLq, 2026-08-16): PROMOTED topm: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.mjsstill 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
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
--flagplumbing at the entrypoint,derivenow 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 onorigin/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 touchscripts/pm/**(the three PRs in queue/awaiting-merge touch skills roots only); no remote branch matchesissue-8640. Sibling-pin coupling: none.
Generated by Claude Code
- added a commit that references this issue
on Aug 16, 2026 { "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.mjsonly (+260/-5, one file)..claude/skills/**and.claude/agents/os-dev.mduntouched — 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
--flagsgenerically,derivetakes an options object, and the declared-list + live-census-guard pattern is in-file. The self-test count onorigin/mainwas 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 recordedmodel: opus. Pinned as a self-test case so it stays a derived answer rather than a coincidence.
Generated by Claude Code
- File surface honoured:
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_GLOBSholds the policy as data (glob + tier + why per entry);deriveTiermatches through the in-file segment-boundaryhintCovers(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 at82bbc6217;skip-changesetmatches 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_progressat 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
- Surface: one file,
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Sep 28, 2026
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.mjsderived 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:
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.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).