docs(pm-dispatch): keep two copies of the decision frame, not four - #14862

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies
Sep 3, 2026
Merged

docs(pm-dispatch): keep two copies of the decision frame, not four#14862
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Part of #14685
Part of #13597

Item 4 of decision batch 3, ruled B. From the ruling comment (5520452691), verbatim:

B — the four-axis decision frame keeps two copies (internal-pm and published-pm); the dev-side copies are replaced by a {decision_frame} placeholder the dispatching PM fills at dispatch time; check:skill-frame-sync watches two sites. Confidence gap carried: whoever executes verifies that the published-dev block has no reader other than the placeholder path before removing it.


1. The confidence gap, discharged BEFORE any edit

The ruling made removal conditional on a reader census. Run at the merge base 5ff5f9576, over .claude/ skills/ scripts/ .github/ packages/ docs/, for four things: the published-dev start marker (Analyze every option on ... fixed axes:), the literal published-dev, {decision_frame}, and the internal-dev start marker. Every hit, and what it is:

sitewhat it isdepends on the published-dev BLOCK?
skills/objectstack-pm-dispatch/SKILL.md:633-662the block itselfit IS the block
skills/objectstack-pm-dispatch/SKILL.md:623"with the block below in place of its {decision_frame} line"YES — the placeholder path, and the only one
skills/objectstack-pm-dispatch/rules/dev-template.md:99the {decision_frame} line the instruction above fillsvia that instruction only
scripts/check-skill-frame-sync.mjs:136-143the published-dev COPIES entrythe gate that watches it
scripts/check-skill-frame-sync.mjs:714,719,723,724,727self-test fixtures addressing the published-dev idthe gate's own fixtures
scripts/check-skill-frame-freshness.mjsimports COPIES / AXIS_MAP wholesale; five fixtures spelled .claude/agents/os-dev.mdreads the TABLE, never the block text
scripts/pm/dispatch-gates.mjs:15382-15397spawned import reading the FILE list out of COPIES for the tier mandatereads FILES, not copies
scripts/pm/check-dispatch-gates.mjs:202FRAME_MODULE watch-hint constantnames the module path only
scripts/check-skills-token-ratchet.mjs:334,345,359ceiling-ledger prosecomments, no behaviour
scripts/pm/check-skill-line-ratchet.mjs:455ceiling-ledger prosecomments, no behaviour

No test, generator, workflow, doc or fixture quotes the block by fingerprint. The gap resolves in favour of removal: the placeholder path and the two frame gates are the whole reader set, and both gates read the COPIES table rather than the prose.

The same census over the internal-dev start marker found exactly one non-gate hit — the block itself at .claude/agents/os-dev.md:400.

2. What changed, site by site

skills/objectstack-pm-dispatch/SKILL.md — the embedded dev template loses the frame copy: the declaring sentence through the binding sentence (old :633-662), which is precisely the span the gate's start and binding anchors delimit.

The stop trigger above it (old :627-632, "When to STOP instead of coding …") stays, and that is a judgement call worth naming. It is not part of the frame — the gate's anchors do not reach it — and it is the escalation TRIGGER rather than the analysis. rules/dev-template.md carries it nowhere else (grepped: the template mentions needs_decision only at :34 inside the contract-first rule and at :112 in the report schema), so deleting it with the frame would have removed the dev's instruction to stop at all, which the ruling did not ask for.

The paste instruction now names the surviving copy by its heading:

Paste rules/dev-template.md verbatim into every dispatch prompt; the PM fills
its {…} placeholders. {decision_frame} takes the stop trigger below, followed
by the whole `#### The four-axis decision frame (binding)` section above: the
frame has ONE copy in this skill and the PM pastes it at dispatch time, so no
dev-side copy can drift from it.

.claude/agents/os-dev.md — the frame block (old :400-421, declaring sentence through the binding sentence, which ran into the same paragraph as the unrelated blocked rule) collapses to one sentence. The blocked rule is preserved byte-for-byte. Neither :232 nor :303 is touched (PR #14808 holds them); the diff is one hunk at :400.

Three judgement calls in that one sentence:

  1. It says the frame comes from the PM's own copy, not specifically from rules/dev-template.md, because that template is the PUBLISHED skill's artifact. The internal PM composes dispatch prompts per .claude/skills/pm-dispatch/references/dispatch-runbook.md and has no {decision_frame} slot (grep: the token exists only under skills/objectstack-pm-dispatch/). The published slot is named parenthetically, as the brief asked, without claiming the internal lane uses it. See finding F5.
  2. It adds "if the dispatch prompt did not carry it, stop and ask; do not invent your own axes." os-dev.md's own doctrine is that standard clauses live in that file and prompt silence is normal — so moving the frame OUT of it opens a hole that a silent prompt would otherwise walk through.
  3. It is written in Chinese and deliberately avoids the phrases fixed axes, 固定评估轴, 条评估轴 and any WORD-axis decision frame form. Those are FINGERPRINTS in the sync gate's anti-dormancy scan, whose population is per file: os-dev.md stops being a declared file with this change, so any of those phrasings would have turned the gate red. Verified after the edit — grep -nP for all four patterns over os-dev.md returns nothing.

scripts/check-skill-frame-sync.mjsCOPIES 4 → 2.

- internal-pm .claude/skills/pm-dispatch/SKILL.md KEPT
- internal-dev .claude/agents/os-dev.md REMOVED
- published-pm skills/objectstack-pm-dispatch/SKILL.md KEPT
- published-dev skills/objectstack-pm-dispatch/SKILL.md REMOVED

Every spelled count is now derived: the header ("FOUR hand-written copies across three files"), the COPIES docblock, the axisEntryStarts docblock, the freshness-gate cross-reference, and the count-disagreement message (the four copies do not agree becomes the ${results.length} copies do not agree). report() and the success line already derived theirs from COPIES.length.

The same-file overlap guard is kept even though it is vacuous today — no two copies share a file any more. Re-declaring such a pair is one edit away, and that guard is what would make such a pair prove it compares two different sections rather than the same one twice.

scripts/check-skill-frame-freshness.mjs — it imported COPIES but its self-test SPELLED .claude/agents/os-dev.md in five fixtures (cases 1, 2, 6, 7 and the two withUnreadableCopy calls in 8 and 9). Those broke on the reduction, exactly as that file's own #8024 rule predicts. They now derive SAMPLE_COPY_ID / SAMPLE_FRAME_FILE from COPIES[0], so the next change to the table re-points the fixtures instead of stranding them.

3. The anti-dormancy scan still refuses a returning copy — measured, both directions

Committed first, then mutated, with an EXIT INT TERM trap restoring absolute paths; each mutation proved on disk by counting the injected marker before reading any verdict; each restore proved by git diff HEAD empty AND by comparing git hash-object against the path's HEAD blob.

Direction A — the removed text returns to the now-undeclared .claude/agents/os-dev.md. REFUSED. Marker occurrences after mutation: 1 (was 0). node scripts/check-skill-frame-sync.mjs exit 1:

✗ check-skill-frame-sync: 1 problem(s).
• .claude/agents/os-dev.md
looks like ANOTHER copy of the decision frame (matched /fixed axes/) but is
not declared in COPIES, so nothing keeps it in sync.

Restore: git diff HEAD empty; worktree blob 968d4fd10a653f013635a8a48bdee25a25efc648 equals the HEAD blob.

Direction B — the same text returns to the still-declared skills/objectstack-pm-dispatch/SKILL.md. NOT REFUSED (exit 0). This is a real, measured limit and it is not new: the scan skips any file present in declaredFiles, so a second undeclared copy inside an already-declared file has always been invisible to it. Restore proved the same way (blob 3cbbed0f501736a0db3a5ecdd9e6cda43d44b434).

What that limit means for this PR, stated rather than implied: what keeps a second published copy from being written is the paste instruction naming the surviving section by heading, plus review — not the scan. Closing it mechanically needs a per-occurrence criterion the gate does not have (the published file legitimately carries two more fixed axes fingerprints, at :534 and in the :539 heading). A comment next to the new self-test case says exactly this, so the next reader does not mistake direction B for a bug introduced here.

A self-test case pins direction A against the REAL path this change undeclared, rather than a synthetic one.

4. Counts (merge base 5ff5f9576 → head)

filebyteslinestokens (ceil bytes/4)
skills/objectstack-pm-dispatch/SKILL.md38807 → 36898 (−1909)753 → 726 (−27)9702 → 9225 (−477), ceiling 9708
.claude/agents/os-dev.md42743 → 41196 (−1547)469 → 450 (−19)— (line-ratcheted), ceiling 469
scripts/check-skill-frame-sync.mjs40961 → 43180 (+2219)960 → 990 (+30)not ratcheted
scripts/check-skill-frame-freshness.mjs46344 → 47480 (+1136)1065 → 1085 (+20)not ratcheted

Whole published package skills/objectstack-pm-dispatch/** (all markdown): 11540 → 11063 tokens, −477.

Two ratcheted files, both shrink; no ceiling row is edited, in either ratchet. The measured token saving is −477, not the −630 the card estimated — the difference is the stop trigger, kept for the reason in section 2. The script growth (+50 lines across two gates) is entirely the derivation constants, the new regression case and the comments recording the measured limit; neither script is under a ratchet.

5. Gates

Union derived at the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands:

dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit ae616fbc91 (/home/user/objectstack-14685-item4).
dispatch-gates: change set derived from git — 4 path(s) vs merge base 5ff5f9576 of 'origin/main' and HEAD

Every exit code captured by redirect before any pipe.

All 32 re-run at the final head ae616fbc91, after the last commit. 31 green, 1 NOT MEASURED, 0 red. The load-bearing ones, quoted from their own verdict lines:

commandexitits own verdict line
pnpm check:skill-frame-sync0✓ check-skill-frame-sync self-test: 13 cases pass, plus 5 dispatch-gates declaration cases. / ✓ check-skill-frame-sync: 2 copies of the decision frame are structurally isomorphic across 2 files
node scripts/check-skill-frame-freshness.mjs0✓ check-skill-frame-freshness: the decision frame in this tree is current with origin/main (fetched just now). (12 self-test cases pass)
node scripts/check-skills-token-ratchet.mjs0✓ check-skills-token-ratchet: skills/objectstack-pm-dispatch/SKILL.md is 9225 tokens (ceiling 9708; headroom 483).
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: .claude/agents/os-dev.md is 450 lines (ceiling 469; headroom 19).
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass.
pnpm --filter @objectstack/spec run check:skill-docs0✓ skills/README.md / ✓ content/docs/ai/skills-reference.mdx — nothing to regenerate
pnpm check:ratchet-remedy-authority0green
pnpm check:pm-skill-id-lint0green
pnpm check:nul-bytes0green (plus a direct grep -naP for control bytes over both edited markdown files: no match)
pnpm check:doc-authoring, check:role-word, check:agent-model-declared, check:agent-test-spelling, check:corpus-claim-drift, check:skill-compatibility, check:skill-identifier-liveness, check:watch-hint-literal, check:entry-guard, check:parse-guard, check:cli-command-ids, check:bash32-floor, check:pnpm-filter-targets, check:pm-governed-merges, check:cross-package-test-inputs0green
node scripts/check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-shard-attestation.mjs, check-whole-set-label-write.mjs, scripts/pm/bare-root-worklist.mjs --self-test0green
pnpm --filter @objectstack/lint run check:doc-formula-expressions0first run answered PREREQUISITE NOT MET (exit 3, @objectstack/formula then @objectstack/lint unbuilt). Prerequisite supplied via the shared verify lock (turbo run build --filter=@objectstack/formula, then --filter=@objectstack/lint) and re-run to a real verdict: ✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1365 TS blocks judged clean
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

One red was found and fixed rather than reported: on the first pass check:pm-skill-ratchet said ✗ check-skill-line-ratchet: .claude/agents/os-dev.md has 1 line(s) over the 120-byte budget: L400 (121B). The ratchet prices a per-line byte budget as well as a line ceiling. The replacement sentence was rewrapped at legal CJK break points, content byte-identical, and the whole union was re-derived and re-run at the resulting head — which is the head every number above comes from.

NOT MEASURED, with reasons:

  • node scripts/check-test-completeness.mjs — exit 3, its own text: "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … the local reading for this gate is NOT MEASURED. ⛔ It is not a red." CI tees the log and passes it; unreachable there.
  • The changeset-derived families (9 of them) do not apply: this PR is skip-changeset.claude/**, skills/** and scripts/** publish nothing from any package.
  • The CI-measured-only family reads the workflow event payload; no local run can produce a verdict for it.
  • The always-runs tail (workflows with no path filter) is CI's, not measured here.

6. Out-of-scope findings — NOT fixed here, for the seat to triage

Each is a claim that this change makes false, in a file this flight does not hold.

  • F1scripts/pm/dispatch-gates.mjs:8090why: '… the dev-agent definition is protocol semantics — every dispatched dev runs under it, and it carries an enforced copy of the decision frame'. The last clause is now false; the mandate itself still holds on the first clause, and the self-test still passes (it asserts the glob exists, not the reason).
  • F2scripts/pm/dispatch-gates.mjs:8095why: '… the published PM skill carries two enforced copies of the decision frame (check:skill-frame-sync COPIES) …'. One copy now.
  • F3scripts/pm/check-skill-line-ratchet.mjs:455// This file is one of the four check:skill-frame-sync COPIES, so a rule that …, in the os-dev.md ceiling ledger. That file is no longer a COPIES entry, which also weakens the stated reason for the +3 raise recorded there. Item 5 holds this file.
  • F4scripts/check-skills-token-ratchet.mjs:334, 345, 359 — three ledger comments stating the published SKILL.md "carries TWO enforced frame copies" and that "the two gate-pinned copies of the decision frame stay in this file". Historically true; now stale as a present-tense claim.
  • F5 the internal lane has no {decision_frame} slot. os-dev.md now expects the frame to arrive in the dispatch prompt, but .claude/skills/pm-dispatch/references/dispatch-runbook.md (派发词构造细则) does not tell the internal PM to paste it, and .claude/skills/pm-dispatch/SKILL.md is explicitly outside this flight. Until that lands, the internal dev's only protection is the stop-and-ask clause added in section 2. This is the one finding with live consequences.
  • F6 the removed os-dev.md block carried a verbatim maintainer quotation — the 2026-09-01 axis-weighting ruling — so this PR deletes a quotation site that item 2's quotation-policy flight may have surveyed. The ruling text survives at .claude/skills/pm-dispatch/SKILL.md:904 and .claude/skills/pm-dispatch/references/decision-analysis.md:50, and the RULE itself survives in both published and internal PM copies of the frame.

7. Posture

Draft, governed surface. No reviewers requested, no ready flip, no enqueue, no auto-merge. Labels skip-changeset and needs:contract-review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1


Generated by Claude Code

Batch-3 ruling, item 4, option B: the four-axis decision frame keeps two
copies (internal-pm and published-pm); the dev-side copies are replaced by
the {decision_frame} placeholder the dispatching PM fills at dispatch time.
- skills/objectstack-pm-dispatch/SKILL.md: the embedded dev-agent template
loses the frame copy (declaring sentence through binding sentence). The
stop trigger stays -- it is the escalation TRIGGER, not the frame, and
rules/dev-template.md carries it nowhere else. The paste instruction now
names the surviving section by heading.
- .claude/agents/os-dev.md: the frame block collapses to one sentence saying
the frame arrives in the dispatch prompt, that every option is analyzed on
its axes and the recommendation justified on them, and that a prompt
without it is a stop-and-ask rather than an invitation to invent axes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…copies
check-skill-frame-sync watched four copies across three files; the batch-3
ruling (item 4, option B) leaves two, both PM-side. The dev-side entries are
removed and every count that was spelled out is now derived from the table.
- COPIES loses internal-dev and published-dev; the header, the COPIES
docblock and the count-disagreement message stop spelling "four".
- The same-file overlap guard stays. It is vacuous today (no two copies share
a file) but re-declaring such a pair is one edit away, and it is the check
that makes such a pair prove it compares two different sections.
- Self-test: the three cases driven through the removed ids are retargeted at
the surviving copies, and a new case pins the reduction itself -- the
dev-side text reappearing in the now-undeclared agent definition is refused
by the anti-dormancy scan. Its measured limit is written next to it: the
scan's population is per FILE, so a second copy pasted back into a file that
is already declared is not refused, and was not before this change either.
- check-skill-frame-freshness imported COPIES but spelled one frame file's
path in five fixtures; those now derive it from the table, which is what
the file's own #8024 rule asks for.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… budget
check:pm-skill-ratchet reads a per-line byte budget as well as a line ceiling;
the first line of the replacement landed at 121B. Rewrapped at legal CJK break
points, content byte-identical.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Sep 3, 2026
@os-litantos-litant added skip-changeset PR has no user-facing published change; bypasses the changeset gate needs:contract-review and removed documentation Improvements or additions to documentation labels Sep 3, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review September 3, 2026 08:57
@os-zhuang
os-zhuang added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 7b19b9eSep 3, 2026
46 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-14685-decision-frame-two-copies branch September 3, 2026 11:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

docs(pm-dispatch): keep two copies of the decision frame, not four - #14862

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies
Sep 3, 2026
Merged

docs(pm-dispatch): keep two copies of the decision frame, not four#14862
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Part of #14685
Part of #13597

Item 4 of decision batch 3, ruled B. From the ruling comment (5520452691), verbatim:

B — the four-axis decision frame keeps two copies (internal-pm and published-pm); the dev-side copies are replaced by a {decision_frame} placeholder the dispatching PM fills at dispatch time; check:skill-frame-sync watches two sites. Confidence gap carried: whoever executes verifies that the published-dev block has no reader other than the placeholder path before removing it.


1. The confidence gap, discharged BEFORE any edit

The ruling made removal conditional on a reader census. Run at the merge base 5ff5f9576, over .claude/ skills/ scripts/ .github/ packages/ docs/, for four things: the published-dev start marker (Analyze every option on ... fixed axes:), the literal published-dev, {decision_frame}, and the internal-dev start marker. Every hit, and what it is:

sitewhat it isdepends on the published-dev BLOCK?
skills/objectstack-pm-dispatch/SKILL.md:633-662the block itselfit IS the block
skills/objectstack-pm-dispatch/SKILL.md:623"with the block below in place of its {decision_frame} line"YES — the placeholder path, and the only one
skills/objectstack-pm-dispatch/rules/dev-template.md:99the {decision_frame} line the instruction above fillsvia that instruction only
scripts/check-skill-frame-sync.mjs:136-143the published-dev COPIES entrythe gate that watches it
scripts/check-skill-frame-sync.mjs:714,719,723,724,727self-test fixtures addressing the published-dev idthe gate's own fixtures
scripts/check-skill-frame-freshness.mjsimports COPIES / AXIS_MAP wholesale; five fixtures spelled .claude/agents/os-dev.mdreads the TABLE, never the block text
scripts/pm/dispatch-gates.mjs:15382-15397spawned import reading the FILE list out of COPIES for the tier mandatereads FILES, not copies
scripts/pm/check-dispatch-gates.mjs:202FRAME_MODULE watch-hint constantnames the module path only
scripts/check-skills-token-ratchet.mjs:334,345,359ceiling-ledger prosecomments, no behaviour
scripts/pm/check-skill-line-ratchet.mjs:455ceiling-ledger prosecomments, no behaviour

No test, generator, workflow, doc or fixture quotes the block by fingerprint. The gap resolves in favour of removal: the placeholder path and the two frame gates are the whole reader set, and both gates read the COPIES table rather than the prose.

The same census over the internal-dev start marker found exactly one non-gate hit — the block itself at .claude/agents/os-dev.md:400.

2. What changed, site by site

skills/objectstack-pm-dispatch/SKILL.md — the embedded dev template loses the frame copy: the declaring sentence through the binding sentence (old :633-662), which is precisely the span the gate's start and binding anchors delimit.

The stop trigger above it (old :627-632, "When to STOP instead of coding …") stays, and that is a judgement call worth naming. It is not part of the frame — the gate's anchors do not reach it — and it is the escalation TRIGGER rather than the analysis. rules/dev-template.md carries it nowhere else (grepped: the template mentions needs_decision only at :34 inside the contract-first rule and at :112 in the report schema), so deleting it with the frame would have removed the dev's instruction to stop at all, which the ruling did not ask for.

The paste instruction now names the surviving copy by its heading:

Paste rules/dev-template.md verbatim into every dispatch prompt; the PM fills
its {…} placeholders. {decision_frame} takes the stop trigger below, followed
by the whole `#### The four-axis decision frame (binding)` section above: the
frame has ONE copy in this skill and the PM pastes it at dispatch time, so no
dev-side copy can drift from it.

.claude/agents/os-dev.md — the frame block (old :400-421, declaring sentence through the binding sentence, which ran into the same paragraph as the unrelated blocked rule) collapses to one sentence. The blocked rule is preserved byte-for-byte. Neither :232 nor :303 is touched (PR #14808 holds them); the diff is one hunk at :400.

Three judgement calls in that one sentence:

  1. It says the frame comes from the PM's own copy, not specifically from rules/dev-template.md, because that template is the PUBLISHED skill's artifact. The internal PM composes dispatch prompts per .claude/skills/pm-dispatch/references/dispatch-runbook.md and has no {decision_frame} slot (grep: the token exists only under skills/objectstack-pm-dispatch/). The published slot is named parenthetically, as the brief asked, without claiming the internal lane uses it. See finding F5.
  2. It adds "if the dispatch prompt did not carry it, stop and ask; do not invent your own axes." os-dev.md's own doctrine is that standard clauses live in that file and prompt silence is normal — so moving the frame OUT of it opens a hole that a silent prompt would otherwise walk through.
  3. It is written in Chinese and deliberately avoids the phrases fixed axes, 固定评估轴, 条评估轴 and any WORD-axis decision frame form. Those are FINGERPRINTS in the sync gate's anti-dormancy scan, whose population is per file: os-dev.md stops being a declared file with this change, so any of those phrasings would have turned the gate red. Verified after the edit — grep -nP for all four patterns over os-dev.md returns nothing.

scripts/check-skill-frame-sync.mjsCOPIES 4 → 2.

- internal-pm .claude/skills/pm-dispatch/SKILL.md KEPT
- internal-dev .claude/agents/os-dev.md REMOVED
- published-pm skills/objectstack-pm-dispatch/SKILL.md KEPT
- published-dev skills/objectstack-pm-dispatch/SKILL.md REMOVED

Every spelled count is now derived: the header ("FOUR hand-written copies across three files"), the COPIES docblock, the axisEntryStarts docblock, the freshness-gate cross-reference, and the count-disagreement message (the four copies do not agree becomes the ${results.length} copies do not agree). report() and the success line already derived theirs from COPIES.length.

The same-file overlap guard is kept even though it is vacuous today — no two copies share a file any more. Re-declaring such a pair is one edit away, and that guard is what would make such a pair prove it compares two different sections rather than the same one twice.

scripts/check-skill-frame-freshness.mjs — it imported COPIES but its self-test SPELLED .claude/agents/os-dev.md in five fixtures (cases 1, 2, 6, 7 and the two withUnreadableCopy calls in 8 and 9). Those broke on the reduction, exactly as that file's own #8024 rule predicts. They now derive SAMPLE_COPY_ID / SAMPLE_FRAME_FILE from COPIES[0], so the next change to the table re-points the fixtures instead of stranding them.

3. The anti-dormancy scan still refuses a returning copy — measured, both directions

Committed first, then mutated, with an EXIT INT TERM trap restoring absolute paths; each mutation proved on disk by counting the injected marker before reading any verdict; each restore proved by git diff HEAD empty AND by comparing git hash-object against the path's HEAD blob.

Direction A — the removed text returns to the now-undeclared .claude/agents/os-dev.md. REFUSED. Marker occurrences after mutation: 1 (was 0). node scripts/check-skill-frame-sync.mjs exit 1:

✗ check-skill-frame-sync: 1 problem(s).
• .claude/agents/os-dev.md
looks like ANOTHER copy of the decision frame (matched /fixed axes/) but is
not declared in COPIES, so nothing keeps it in sync.

Restore: git diff HEAD empty; worktree blob 968d4fd10a653f013635a8a48bdee25a25efc648 equals the HEAD blob.

Direction B — the same text returns to the still-declared skills/objectstack-pm-dispatch/SKILL.md. NOT REFUSED (exit 0). This is a real, measured limit and it is not new: the scan skips any file present in declaredFiles, so a second undeclared copy inside an already-declared file has always been invisible to it. Restore proved the same way (blob 3cbbed0f501736a0db3a5ecdd9e6cda43d44b434).

What that limit means for this PR, stated rather than implied: what keeps a second published copy from being written is the paste instruction naming the surviving section by heading, plus review — not the scan. Closing it mechanically needs a per-occurrence criterion the gate does not have (the published file legitimately carries two more fixed axes fingerprints, at :534 and in the :539 heading). A comment next to the new self-test case says exactly this, so the next reader does not mistake direction B for a bug introduced here.

A self-test case pins direction A against the REAL path this change undeclared, rather than a synthetic one.

4. Counts (merge base 5ff5f9576 → head)

filebyteslinestokens (ceil bytes/4)
skills/objectstack-pm-dispatch/SKILL.md38807 → 36898 (−1909)753 → 726 (−27)9702 → 9225 (−477), ceiling 9708
.claude/agents/os-dev.md42743 → 41196 (−1547)469 → 450 (−19)— (line-ratcheted), ceiling 469
scripts/check-skill-frame-sync.mjs40961 → 43180 (+2219)960 → 990 (+30)not ratcheted
scripts/check-skill-frame-freshness.mjs46344 → 47480 (+1136)1065 → 1085 (+20)not ratcheted

Whole published package skills/objectstack-pm-dispatch/** (all markdown): 11540 → 11063 tokens, −477.

Two ratcheted files, both shrink; no ceiling row is edited, in either ratchet. The measured token saving is −477, not the −630 the card estimated — the difference is the stop trigger, kept for the reason in section 2. The script growth (+50 lines across two gates) is entirely the derivation constants, the new regression case and the comments recording the measured limit; neither script is under a ratchet.

5. Gates

Union derived at the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands:

dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit ae616fbc91 (/home/user/objectstack-14685-item4).
dispatch-gates: change set derived from git — 4 path(s) vs merge base 5ff5f9576 of 'origin/main' and HEAD

Every exit code captured by redirect before any pipe.

All 32 re-run at the final head ae616fbc91, after the last commit. 31 green, 1 NOT MEASURED, 0 red. The load-bearing ones, quoted from their own verdict lines:

commandexitits own verdict line
pnpm check:skill-frame-sync0✓ check-skill-frame-sync self-test: 13 cases pass, plus 5 dispatch-gates declaration cases. / ✓ check-skill-frame-sync: 2 copies of the decision frame are structurally isomorphic across 2 files
node scripts/check-skill-frame-freshness.mjs0✓ check-skill-frame-freshness: the decision frame in this tree is current with origin/main (fetched just now). (12 self-test cases pass)
node scripts/check-skills-token-ratchet.mjs0✓ check-skills-token-ratchet: skills/objectstack-pm-dispatch/SKILL.md is 9225 tokens (ceiling 9708; headroom 483).
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: .claude/agents/os-dev.md is 450 lines (ceiling 469; headroom 19).
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass.
pnpm --filter @objectstack/spec run check:skill-docs0✓ skills/README.md / ✓ content/docs/ai/skills-reference.mdx — nothing to regenerate
pnpm check:ratchet-remedy-authority0green
pnpm check:pm-skill-id-lint0green
pnpm check:nul-bytes0green (plus a direct grep -naP for control bytes over both edited markdown files: no match)
pnpm check:doc-authoring, check:role-word, check:agent-model-declared, check:agent-test-spelling, check:corpus-claim-drift, check:skill-compatibility, check:skill-identifier-liveness, check:watch-hint-literal, check:entry-guard, check:parse-guard, check:cli-command-ids, check:bash32-floor, check:pnpm-filter-targets, check:pm-governed-merges, check:cross-package-test-inputs0green
node scripts/check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-shard-attestation.mjs, check-whole-set-label-write.mjs, scripts/pm/bare-root-worklist.mjs --self-test0green
pnpm --filter @objectstack/lint run check:doc-formula-expressions0first run answered PREREQUISITE NOT MET (exit 3, @objectstack/formula then @objectstack/lint unbuilt). Prerequisite supplied via the shared verify lock (turbo run build --filter=@objectstack/formula, then --filter=@objectstack/lint) and re-run to a real verdict: ✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1365 TS blocks judged clean
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

One red was found and fixed rather than reported: on the first pass check:pm-skill-ratchet said ✗ check-skill-line-ratchet: .claude/agents/os-dev.md has 1 line(s) over the 120-byte budget: L400 (121B). The ratchet prices a per-line byte budget as well as a line ceiling. The replacement sentence was rewrapped at legal CJK break points, content byte-identical, and the whole union was re-derived and re-run at the resulting head — which is the head every number above comes from.

NOT MEASURED, with reasons:

  • node scripts/check-test-completeness.mjs — exit 3, its own text: "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … the local reading for this gate is NOT MEASURED. ⛔ It is not a red." CI tees the log and passes it; unreachable there.
  • The changeset-derived families (9 of them) do not apply: this PR is skip-changeset.claude/**, skills/** and scripts/** publish nothing from any package.
  • The CI-measured-only family reads the workflow event payload; no local run can produce a verdict for it.
  • The always-runs tail (workflows with no path filter) is CI's, not measured here.

6. Out-of-scope findings — NOT fixed here, for the seat to triage

Each is a claim that this change makes false, in a file this flight does not hold.

  • F1scripts/pm/dispatch-gates.mjs:8090why: '… the dev-agent definition is protocol semantics — every dispatched dev runs under it, and it carries an enforced copy of the decision frame'. The last clause is now false; the mandate itself still holds on the first clause, and the self-test still passes (it asserts the glob exists, not the reason).
  • F2scripts/pm/dispatch-gates.mjs:8095why: '… the published PM skill carries two enforced copies of the decision frame (check:skill-frame-sync COPIES) …'. One copy now.
  • F3scripts/pm/check-skill-line-ratchet.mjs:455// This file is one of the four check:skill-frame-sync COPIES, so a rule that …, in the os-dev.md ceiling ledger. That file is no longer a COPIES entry, which also weakens the stated reason for the +3 raise recorded there. Item 5 holds this file.
  • F4scripts/check-skills-token-ratchet.mjs:334, 345, 359 — three ledger comments stating the published SKILL.md "carries TWO enforced frame copies" and that "the two gate-pinned copies of the decision frame stay in this file". Historically true; now stale as a present-tense claim.
  • F5 the internal lane has no {decision_frame} slot. os-dev.md now expects the frame to arrive in the dispatch prompt, but .claude/skills/pm-dispatch/references/dispatch-runbook.md (派发词构造细则) does not tell the internal PM to paste it, and .claude/skills/pm-dispatch/SKILL.md is explicitly outside this flight. Until that lands, the internal dev's only protection is the stop-and-ask clause added in section 2. This is the one finding with live consequences.
  • F6 the removed os-dev.md block carried a verbatim maintainer quotation — the 2026-09-01 axis-weighting ruling — so this PR deletes a quotation site that item 2's quotation-policy flight may have surveyed. The ruling text survives at .claude/skills/pm-dispatch/SKILL.md:904 and .claude/skills/pm-dispatch/references/decision-analysis.md:50, and the RULE itself survives in both published and internal PM copies of the frame.

7. Posture

Draft, governed surface. No reviewers requested, no ready flip, no enqueue, no auto-merge. Labels skip-changeset and needs:contract-review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1


Generated by Claude Code

Batch-3 ruling, item 4, option B: the four-axis decision frame keeps two
copies (internal-pm and published-pm); the dev-side copies are replaced by
the {decision_frame} placeholder the dispatching PM fills at dispatch time.
- skills/objectstack-pm-dispatch/SKILL.md: the embedded dev-agent template
loses the frame copy (declaring sentence through binding sentence). The
stop trigger stays -- it is the escalation TRIGGER, not the frame, and
rules/dev-template.md carries it nowhere else. The paste instruction now
names the surviving section by heading.
- .claude/agents/os-dev.md: the frame block collapses to one sentence saying
the frame arrives in the dispatch prompt, that every option is analyzed on
its axes and the recommendation justified on them, and that a prompt
without it is a stop-and-ask rather than an invitation to invent axes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…copies
check-skill-frame-sync watched four copies across three files; the batch-3
ruling (item 4, option B) leaves two, both PM-side. The dev-side entries are
removed and every count that was spelled out is now derived from the table.
- COPIES loses internal-dev and published-dev; the header, the COPIES
docblock and the count-disagreement message stop spelling "four".
- The same-file overlap guard stays. It is vacuous today (no two copies share
a file) but re-declaring such a pair is one edit away, and it is the check
that makes such a pair prove it compares two different sections.
- Self-test: the three cases driven through the removed ids are retargeted at
the surviving copies, and a new case pins the reduction itself -- the
dev-side text reappearing in the now-undeclared agent definition is refused
by the anti-dormancy scan. Its measured limit is written next to it: the
scan's population is per FILE, so a second copy pasted back into a file that
is already declared is not refused, and was not before this change either.
- check-skill-frame-freshness imported COPIES but spelled one frame file's
path in five fixtures; those now derive it from the table, which is what
the file's own #8024 rule asks for.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… budget
check:pm-skill-ratchet reads a per-line byte budget as well as a line ceiling;
the first line of the replacement landed at 121B. Rewrapped at legal CJK break
points, content byte-identical.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Sep 3, 2026
@os-litantos-litant added skip-changeset PR has no user-facing published change; bypasses the changeset gate needs:contract-review and removed documentation Improvements or additions to documentation labels Sep 3, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review September 3, 2026 08:57
@os-zhuang
os-zhuang added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 7b19b9eSep 3, 2026
46 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-14685-decision-frame-two-copies branch September 3, 2026 11:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(pm-dispatch): keep two copies of the decision frame, not four - #14862

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies
Sep 3, 2026
Merged

docs(pm-dispatch): keep two copies of the decision frame, not four#14862
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Part of #14685
Part of #13597

Item 4 of decision batch 3, ruled B. From the ruling comment (5520452691), verbatim:

B — the four-axis decision frame keeps two copies (internal-pm and published-pm); the dev-side copies are replaced by a {decision_frame} placeholder the dispatching PM fills at dispatch time; check:skill-frame-sync watches two sites. Confidence gap carried: whoever executes verifies that the published-dev block has no reader other than the placeholder path before removing it.


1. The confidence gap, discharged BEFORE any edit

The ruling made removal conditional on a reader census. Run at the merge base 5ff5f9576, over .claude/ skills/ scripts/ .github/ packages/ docs/, for four things: the published-dev start marker (Analyze every option on ... fixed axes:), the literal published-dev, {decision_frame}, and the internal-dev start marker. Every hit, and what it is:

sitewhat it isdepends on the published-dev BLOCK?
skills/objectstack-pm-dispatch/SKILL.md:633-662the block itselfit IS the block
skills/objectstack-pm-dispatch/SKILL.md:623"with the block below in place of its {decision_frame} line"YES — the placeholder path, and the only one
skills/objectstack-pm-dispatch/rules/dev-template.md:99the {decision_frame} line the instruction above fillsvia that instruction only
scripts/check-skill-frame-sync.mjs:136-143the published-dev COPIES entrythe gate that watches it
scripts/check-skill-frame-sync.mjs:714,719,723,724,727self-test fixtures addressing the published-dev idthe gate's own fixtures
scripts/check-skill-frame-freshness.mjsimports COPIES / AXIS_MAP wholesale; five fixtures spelled .claude/agents/os-dev.mdreads the TABLE, never the block text
scripts/pm/dispatch-gates.mjs:15382-15397spawned import reading the FILE list out of COPIES for the tier mandatereads FILES, not copies
scripts/pm/check-dispatch-gates.mjs:202FRAME_MODULE watch-hint constantnames the module path only
scripts/check-skills-token-ratchet.mjs:334,345,359ceiling-ledger prosecomments, no behaviour
scripts/pm/check-skill-line-ratchet.mjs:455ceiling-ledger prosecomments, no behaviour

No test, generator, workflow, doc or fixture quotes the block by fingerprint. The gap resolves in favour of removal: the placeholder path and the two frame gates are the whole reader set, and both gates read the COPIES table rather than the prose.

The same census over the internal-dev start marker found exactly one non-gate hit — the block itself at .claude/agents/os-dev.md:400.

2. What changed, site by site

skills/objectstack-pm-dispatch/SKILL.md — the embedded dev template loses the frame copy: the declaring sentence through the binding sentence (old :633-662), which is precisely the span the gate's start and binding anchors delimit.

The stop trigger above it (old :627-632, "When to STOP instead of coding …") stays, and that is a judgement call worth naming. It is not part of the frame — the gate's anchors do not reach it — and it is the escalation TRIGGER rather than the analysis. rules/dev-template.md carries it nowhere else (grepped: the template mentions needs_decision only at :34 inside the contract-first rule and at :112 in the report schema), so deleting it with the frame would have removed the dev's instruction to stop at all, which the ruling did not ask for.

The paste instruction now names the surviving copy by its heading:

Paste rules/dev-template.md verbatim into every dispatch prompt; the PM fills
its {…} placeholders. {decision_frame} takes the stop trigger below, followed
by the whole `#### The four-axis decision frame (binding)` section above: the
frame has ONE copy in this skill and the PM pastes it at dispatch time, so no
dev-side copy can drift from it.

.claude/agents/os-dev.md — the frame block (old :400-421, declaring sentence through the binding sentence, which ran into the same paragraph as the unrelated blocked rule) collapses to one sentence. The blocked rule is preserved byte-for-byte. Neither :232 nor :303 is touched (PR #14808 holds them); the diff is one hunk at :400.

Three judgement calls in that one sentence:

  1. It says the frame comes from the PM's own copy, not specifically from rules/dev-template.md, because that template is the PUBLISHED skill's artifact. The internal PM composes dispatch prompts per .claude/skills/pm-dispatch/references/dispatch-runbook.md and has no {decision_frame} slot (grep: the token exists only under skills/objectstack-pm-dispatch/). The published slot is named parenthetically, as the brief asked, without claiming the internal lane uses it. See finding F5.
  2. It adds "if the dispatch prompt did not carry it, stop and ask; do not invent your own axes." os-dev.md's own doctrine is that standard clauses live in that file and prompt silence is normal — so moving the frame OUT of it opens a hole that a silent prompt would otherwise walk through.
  3. It is written in Chinese and deliberately avoids the phrases fixed axes, 固定评估轴, 条评估轴 and any WORD-axis decision frame form. Those are FINGERPRINTS in the sync gate's anti-dormancy scan, whose population is per file: os-dev.md stops being a declared file with this change, so any of those phrasings would have turned the gate red. Verified after the edit — grep -nP for all four patterns over os-dev.md returns nothing.

scripts/check-skill-frame-sync.mjsCOPIES 4 → 2.

- internal-pm .claude/skills/pm-dispatch/SKILL.md KEPT
- internal-dev .claude/agents/os-dev.md REMOVED
- published-pm skills/objectstack-pm-dispatch/SKILL.md KEPT
- published-dev skills/objectstack-pm-dispatch/SKILL.md REMOVED

Every spelled count is now derived: the header ("FOUR hand-written copies across three files"), the COPIES docblock, the axisEntryStarts docblock, the freshness-gate cross-reference, and the count-disagreement message (the four copies do not agree becomes the ${results.length} copies do not agree). report() and the success line already derived theirs from COPIES.length.

The same-file overlap guard is kept even though it is vacuous today — no two copies share a file any more. Re-declaring such a pair is one edit away, and that guard is what would make such a pair prove it compares two different sections rather than the same one twice.

scripts/check-skill-frame-freshness.mjs — it imported COPIES but its self-test SPELLED .claude/agents/os-dev.md in five fixtures (cases 1, 2, 6, 7 and the two withUnreadableCopy calls in 8 and 9). Those broke on the reduction, exactly as that file's own #8024 rule predicts. They now derive SAMPLE_COPY_ID / SAMPLE_FRAME_FILE from COPIES[0], so the next change to the table re-points the fixtures instead of stranding them.

3. The anti-dormancy scan still refuses a returning copy — measured, both directions

Committed first, then mutated, with an EXIT INT TERM trap restoring absolute paths; each mutation proved on disk by counting the injected marker before reading any verdict; each restore proved by git diff HEAD empty AND by comparing git hash-object against the path's HEAD blob.

Direction A — the removed text returns to the now-undeclared .claude/agents/os-dev.md. REFUSED. Marker occurrences after mutation: 1 (was 0). node scripts/check-skill-frame-sync.mjs exit 1:

✗ check-skill-frame-sync: 1 problem(s).
• .claude/agents/os-dev.md
looks like ANOTHER copy of the decision frame (matched /fixed axes/) but is
not declared in COPIES, so nothing keeps it in sync.

Restore: git diff HEAD empty; worktree blob 968d4fd10a653f013635a8a48bdee25a25efc648 equals the HEAD blob.

Direction B — the same text returns to the still-declared skills/objectstack-pm-dispatch/SKILL.md. NOT REFUSED (exit 0). This is a real, measured limit and it is not new: the scan skips any file present in declaredFiles, so a second undeclared copy inside an already-declared file has always been invisible to it. Restore proved the same way (blob 3cbbed0f501736a0db3a5ecdd9e6cda43d44b434).

What that limit means for this PR, stated rather than implied: what keeps a second published copy from being written is the paste instruction naming the surviving section by heading, plus review — not the scan. Closing it mechanically needs a per-occurrence criterion the gate does not have (the published file legitimately carries two more fixed axes fingerprints, at :534 and in the :539 heading). A comment next to the new self-test case says exactly this, so the next reader does not mistake direction B for a bug introduced here.

A self-test case pins direction A against the REAL path this change undeclared, rather than a synthetic one.

4. Counts (merge base 5ff5f9576 → head)

filebyteslinestokens (ceil bytes/4)
skills/objectstack-pm-dispatch/SKILL.md38807 → 36898 (−1909)753 → 726 (−27)9702 → 9225 (−477), ceiling 9708
.claude/agents/os-dev.md42743 → 41196 (−1547)469 → 450 (−19)— (line-ratcheted), ceiling 469
scripts/check-skill-frame-sync.mjs40961 → 43180 (+2219)960 → 990 (+30)not ratcheted
scripts/check-skill-frame-freshness.mjs46344 → 47480 (+1136)1065 → 1085 (+20)not ratcheted

Whole published package skills/objectstack-pm-dispatch/** (all markdown): 11540 → 11063 tokens, −477.

Two ratcheted files, both shrink; no ceiling row is edited, in either ratchet. The measured token saving is −477, not the −630 the card estimated — the difference is the stop trigger, kept for the reason in section 2. The script growth (+50 lines across two gates) is entirely the derivation constants, the new regression case and the comments recording the measured limit; neither script is under a ratchet.

5. Gates

Union derived at the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands:

dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit ae616fbc91 (/home/user/objectstack-14685-item4).
dispatch-gates: change set derived from git — 4 path(s) vs merge base 5ff5f9576 of 'origin/main' and HEAD

Every exit code captured by redirect before any pipe.

All 32 re-run at the final head ae616fbc91, after the last commit. 31 green, 1 NOT MEASURED, 0 red. The load-bearing ones, quoted from their own verdict lines:

commandexitits own verdict line
pnpm check:skill-frame-sync0✓ check-skill-frame-sync self-test: 13 cases pass, plus 5 dispatch-gates declaration cases. / ✓ check-skill-frame-sync: 2 copies of the decision frame are structurally isomorphic across 2 files
node scripts/check-skill-frame-freshness.mjs0✓ check-skill-frame-freshness: the decision frame in this tree is current with origin/main (fetched just now). (12 self-test cases pass)
node scripts/check-skills-token-ratchet.mjs0✓ check-skills-token-ratchet: skills/objectstack-pm-dispatch/SKILL.md is 9225 tokens (ceiling 9708; headroom 483).
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: .claude/agents/os-dev.md is 450 lines (ceiling 469; headroom 19).
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass.
pnpm --filter @objectstack/spec run check:skill-docs0✓ skills/README.md / ✓ content/docs/ai/skills-reference.mdx — nothing to regenerate
pnpm check:ratchet-remedy-authority0green
pnpm check:pm-skill-id-lint0green
pnpm check:nul-bytes0green (plus a direct grep -naP for control bytes over both edited markdown files: no match)
pnpm check:doc-authoring, check:role-word, check:agent-model-declared, check:agent-test-spelling, check:corpus-claim-drift, check:skill-compatibility, check:skill-identifier-liveness, check:watch-hint-literal, check:entry-guard, check:parse-guard, check:cli-command-ids, check:bash32-floor, check:pnpm-filter-targets, check:pm-governed-merges, check:cross-package-test-inputs0green
node scripts/check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-shard-attestation.mjs, check-whole-set-label-write.mjs, scripts/pm/bare-root-worklist.mjs --self-test0green
pnpm --filter @objectstack/lint run check:doc-formula-expressions0first run answered PREREQUISITE NOT MET (exit 3, @objectstack/formula then @objectstack/lint unbuilt). Prerequisite supplied via the shared verify lock (turbo run build --filter=@objectstack/formula, then --filter=@objectstack/lint) and re-run to a real verdict: ✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1365 TS blocks judged clean
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

One red was found and fixed rather than reported: on the first pass check:pm-skill-ratchet said ✗ check-skill-line-ratchet: .claude/agents/os-dev.md has 1 line(s) over the 120-byte budget: L400 (121B). The ratchet prices a per-line byte budget as well as a line ceiling. The replacement sentence was rewrapped at legal CJK break points, content byte-identical, and the whole union was re-derived and re-run at the resulting head — which is the head every number above comes from.

NOT MEASURED, with reasons:

  • node scripts/check-test-completeness.mjs — exit 3, its own text: "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … the local reading for this gate is NOT MEASURED. ⛔ It is not a red." CI tees the log and passes it; unreachable there.
  • The changeset-derived families (9 of them) do not apply: this PR is skip-changeset.claude/**, skills/** and scripts/** publish nothing from any package.
  • The CI-measured-only family reads the workflow event payload; no local run can produce a verdict for it.
  • The always-runs tail (workflows with no path filter) is CI's, not measured here.

6. Out-of-scope findings — NOT fixed here, for the seat to triage

Each is a claim that this change makes false, in a file this flight does not hold.

  • F1scripts/pm/dispatch-gates.mjs:8090why: '… the dev-agent definition is protocol semantics — every dispatched dev runs under it, and it carries an enforced copy of the decision frame'. The last clause is now false; the mandate itself still holds on the first clause, and the self-test still passes (it asserts the glob exists, not the reason).
  • F2scripts/pm/dispatch-gates.mjs:8095why: '… the published PM skill carries two enforced copies of the decision frame (check:skill-frame-sync COPIES) …'. One copy now.
  • F3scripts/pm/check-skill-line-ratchet.mjs:455// This file is one of the four check:skill-frame-sync COPIES, so a rule that …, in the os-dev.md ceiling ledger. That file is no longer a COPIES entry, which also weakens the stated reason for the +3 raise recorded there. Item 5 holds this file.
  • F4scripts/check-skills-token-ratchet.mjs:334, 345, 359 — three ledger comments stating the published SKILL.md "carries TWO enforced frame copies" and that "the two gate-pinned copies of the decision frame stay in this file". Historically true; now stale as a present-tense claim.
  • F5 the internal lane has no {decision_frame} slot. os-dev.md now expects the frame to arrive in the dispatch prompt, but .claude/skills/pm-dispatch/references/dispatch-runbook.md (派发词构造细则) does not tell the internal PM to paste it, and .claude/skills/pm-dispatch/SKILL.md is explicitly outside this flight. Until that lands, the internal dev's only protection is the stop-and-ask clause added in section 2. This is the one finding with live consequences.
  • F6 the removed os-dev.md block carried a verbatim maintainer quotation — the 2026-09-01 axis-weighting ruling — so this PR deletes a quotation site that item 2's quotation-policy flight may have surveyed. The ruling text survives at .claude/skills/pm-dispatch/SKILL.md:904 and .claude/skills/pm-dispatch/references/decision-analysis.md:50, and the RULE itself survives in both published and internal PM copies of the frame.

7. Posture

Draft, governed surface. No reviewers requested, no ready flip, no enqueue, no auto-merge. Labels skip-changeset and needs:contract-review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1


Generated by Claude Code

Batch-3 ruling, item 4, option B: the four-axis decision frame keeps two
copies (internal-pm and published-pm); the dev-side copies are replaced by
the {decision_frame} placeholder the dispatching PM fills at dispatch time.
- skills/objectstack-pm-dispatch/SKILL.md: the embedded dev-agent template
loses the frame copy (declaring sentence through binding sentence). The
stop trigger stays -- it is the escalation TRIGGER, not the frame, and
rules/dev-template.md carries it nowhere else. The paste instruction now
names the surviving section by heading.
- .claude/agents/os-dev.md: the frame block collapses to one sentence saying
the frame arrives in the dispatch prompt, that every option is analyzed on
its axes and the recommendation justified on them, and that a prompt
without it is a stop-and-ask rather than an invitation to invent axes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…copies
check-skill-frame-sync watched four copies across three files; the batch-3
ruling (item 4, option B) leaves two, both PM-side. The dev-side entries are
removed and every count that was spelled out is now derived from the table.
- COPIES loses internal-dev and published-dev; the header, the COPIES
docblock and the count-disagreement message stop spelling "four".
- The same-file overlap guard stays. It is vacuous today (no two copies share
a file) but re-declaring such a pair is one edit away, and it is the check
that makes such a pair prove it compares two different sections.
- Self-test: the three cases driven through the removed ids are retargeted at
the surviving copies, and a new case pins the reduction itself -- the
dev-side text reappearing in the now-undeclared agent definition is refused
by the anti-dormancy scan. Its measured limit is written next to it: the
scan's population is per FILE, so a second copy pasted back into a file that
is already declared is not refused, and was not before this change either.
- check-skill-frame-freshness imported COPIES but spelled one frame file's
path in five fixtures; those now derive it from the table, which is what
the file's own #8024 rule asks for.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… budget
check:pm-skill-ratchet reads a per-line byte budget as well as a line ceiling;
the first line of the replacement landed at 121B. Rewrapped at legal CJK break
points, content byte-identical.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Sep 3, 2026
@os-litantos-litant added skip-changeset PR has no user-facing published change; bypasses the changeset gate needs:contract-review and removed documentation Improvements or additions to documentation labels Sep 3, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review September 3, 2026 08:57
@os-zhuang
os-zhuang added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 7b19b9eSep 3, 2026
46 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-14685-decision-frame-two-copies branch September 3, 2026 11:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(pm-dispatch): keep two copies of the decision frame, not four - #14862

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies
Sep 3, 2026
Merged

docs(pm-dispatch): keep two copies of the decision frame, not four#14862
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Part of #14685
Part of #13597

Item 4 of decision batch 3, ruled B. From the ruling comment (5520452691), verbatim:

B — the four-axis decision frame keeps two copies (internal-pm and published-pm); the dev-side copies are replaced by a {decision_frame} placeholder the dispatching PM fills at dispatch time; check:skill-frame-sync watches two sites. Confidence gap carried: whoever executes verifies that the published-dev block has no reader other than the placeholder path before removing it.


1. The confidence gap, discharged BEFORE any edit

The ruling made removal conditional on a reader census. Run at the merge base 5ff5f9576, over .claude/ skills/ scripts/ .github/ packages/ docs/, for four things: the published-dev start marker (Analyze every option on ... fixed axes:), the literal published-dev, {decision_frame}, and the internal-dev start marker. Every hit, and what it is:

sitewhat it isdepends on the published-dev BLOCK?
skills/objectstack-pm-dispatch/SKILL.md:633-662the block itselfit IS the block
skills/objectstack-pm-dispatch/SKILL.md:623"with the block below in place of its {decision_frame} line"YES — the placeholder path, and the only one
skills/objectstack-pm-dispatch/rules/dev-template.md:99the {decision_frame} line the instruction above fillsvia that instruction only
scripts/check-skill-frame-sync.mjs:136-143the published-dev COPIES entrythe gate that watches it
scripts/check-skill-frame-sync.mjs:714,719,723,724,727self-test fixtures addressing the published-dev idthe gate's own fixtures
scripts/check-skill-frame-freshness.mjsimports COPIES / AXIS_MAP wholesale; five fixtures spelled .claude/agents/os-dev.mdreads the TABLE, never the block text
scripts/pm/dispatch-gates.mjs:15382-15397spawned import reading the FILE list out of COPIES for the tier mandatereads FILES, not copies
scripts/pm/check-dispatch-gates.mjs:202FRAME_MODULE watch-hint constantnames the module path only
scripts/check-skills-token-ratchet.mjs:334,345,359ceiling-ledger prosecomments, no behaviour
scripts/pm/check-skill-line-ratchet.mjs:455ceiling-ledger prosecomments, no behaviour

No test, generator, workflow, doc or fixture quotes the block by fingerprint. The gap resolves in favour of removal: the placeholder path and the two frame gates are the whole reader set, and both gates read the COPIES table rather than the prose.

The same census over the internal-dev start marker found exactly one non-gate hit — the block itself at .claude/agents/os-dev.md:400.

2. What changed, site by site

skills/objectstack-pm-dispatch/SKILL.md — the embedded dev template loses the frame copy: the declaring sentence through the binding sentence (old :633-662), which is precisely the span the gate's start and binding anchors delimit.

The stop trigger above it (old :627-632, "When to STOP instead of coding …") stays, and that is a judgement call worth naming. It is not part of the frame — the gate's anchors do not reach it — and it is the escalation TRIGGER rather than the analysis. rules/dev-template.md carries it nowhere else (grepped: the template mentions needs_decision only at :34 inside the contract-first rule and at :112 in the report schema), so deleting it with the frame would have removed the dev's instruction to stop at all, which the ruling did not ask for.

The paste instruction now names the surviving copy by its heading:

Paste rules/dev-template.md verbatim into every dispatch prompt; the PM fills
its {…} placeholders. {decision_frame} takes the stop trigger below, followed
by the whole `#### The four-axis decision frame (binding)` section above: the
frame has ONE copy in this skill and the PM pastes it at dispatch time, so no
dev-side copy can drift from it.

.claude/agents/os-dev.md — the frame block (old :400-421, declaring sentence through the binding sentence, which ran into the same paragraph as the unrelated blocked rule) collapses to one sentence. The blocked rule is preserved byte-for-byte. Neither :232 nor :303 is touched (PR #14808 holds them); the diff is one hunk at :400.

Three judgement calls in that one sentence:

  1. It says the frame comes from the PM's own copy, not specifically from rules/dev-template.md, because that template is the PUBLISHED skill's artifact. The internal PM composes dispatch prompts per .claude/skills/pm-dispatch/references/dispatch-runbook.md and has no {decision_frame} slot (grep: the token exists only under skills/objectstack-pm-dispatch/). The published slot is named parenthetically, as the brief asked, without claiming the internal lane uses it. See finding F5.
  2. It adds "if the dispatch prompt did not carry it, stop and ask; do not invent your own axes." os-dev.md's own doctrine is that standard clauses live in that file and prompt silence is normal — so moving the frame OUT of it opens a hole that a silent prompt would otherwise walk through.
  3. It is written in Chinese and deliberately avoids the phrases fixed axes, 固定评估轴, 条评估轴 and any WORD-axis decision frame form. Those are FINGERPRINTS in the sync gate's anti-dormancy scan, whose population is per file: os-dev.md stops being a declared file with this change, so any of those phrasings would have turned the gate red. Verified after the edit — grep -nP for all four patterns over os-dev.md returns nothing.

scripts/check-skill-frame-sync.mjsCOPIES 4 → 2.

- internal-pm .claude/skills/pm-dispatch/SKILL.md KEPT
- internal-dev .claude/agents/os-dev.md REMOVED
- published-pm skills/objectstack-pm-dispatch/SKILL.md KEPT
- published-dev skills/objectstack-pm-dispatch/SKILL.md REMOVED

Every spelled count is now derived: the header ("FOUR hand-written copies across three files"), the COPIES docblock, the axisEntryStarts docblock, the freshness-gate cross-reference, and the count-disagreement message (the four copies do not agree becomes the ${results.length} copies do not agree). report() and the success line already derived theirs from COPIES.length.

The same-file overlap guard is kept even though it is vacuous today — no two copies share a file any more. Re-declaring such a pair is one edit away, and that guard is what would make such a pair prove it compares two different sections rather than the same one twice.

scripts/check-skill-frame-freshness.mjs — it imported COPIES but its self-test SPELLED .claude/agents/os-dev.md in five fixtures (cases 1, 2, 6, 7 and the two withUnreadableCopy calls in 8 and 9). Those broke on the reduction, exactly as that file's own #8024 rule predicts. They now derive SAMPLE_COPY_ID / SAMPLE_FRAME_FILE from COPIES[0], so the next change to the table re-points the fixtures instead of stranding them.

3. The anti-dormancy scan still refuses a returning copy — measured, both directions

Committed first, then mutated, with an EXIT INT TERM trap restoring absolute paths; each mutation proved on disk by counting the injected marker before reading any verdict; each restore proved by git diff HEAD empty AND by comparing git hash-object against the path's HEAD blob.

Direction A — the removed text returns to the now-undeclared .claude/agents/os-dev.md. REFUSED. Marker occurrences after mutation: 1 (was 0). node scripts/check-skill-frame-sync.mjs exit 1:

✗ check-skill-frame-sync: 1 problem(s).
• .claude/agents/os-dev.md
looks like ANOTHER copy of the decision frame (matched /fixed axes/) but is
not declared in COPIES, so nothing keeps it in sync.

Restore: git diff HEAD empty; worktree blob 968d4fd10a653f013635a8a48bdee25a25efc648 equals the HEAD blob.

Direction B — the same text returns to the still-declared skills/objectstack-pm-dispatch/SKILL.md. NOT REFUSED (exit 0). This is a real, measured limit and it is not new: the scan skips any file present in declaredFiles, so a second undeclared copy inside an already-declared file has always been invisible to it. Restore proved the same way (blob 3cbbed0f501736a0db3a5ecdd9e6cda43d44b434).

What that limit means for this PR, stated rather than implied: what keeps a second published copy from being written is the paste instruction naming the surviving section by heading, plus review — not the scan. Closing it mechanically needs a per-occurrence criterion the gate does not have (the published file legitimately carries two more fixed axes fingerprints, at :534 and in the :539 heading). A comment next to the new self-test case says exactly this, so the next reader does not mistake direction B for a bug introduced here.

A self-test case pins direction A against the REAL path this change undeclared, rather than a synthetic one.

4. Counts (merge base 5ff5f9576 → head)

filebyteslinestokens (ceil bytes/4)
skills/objectstack-pm-dispatch/SKILL.md38807 → 36898 (−1909)753 → 726 (−27)9702 → 9225 (−477), ceiling 9708
.claude/agents/os-dev.md42743 → 41196 (−1547)469 → 450 (−19)— (line-ratcheted), ceiling 469
scripts/check-skill-frame-sync.mjs40961 → 43180 (+2219)960 → 990 (+30)not ratcheted
scripts/check-skill-frame-freshness.mjs46344 → 47480 (+1136)1065 → 1085 (+20)not ratcheted

Whole published package skills/objectstack-pm-dispatch/** (all markdown): 11540 → 11063 tokens, −477.

Two ratcheted files, both shrink; no ceiling row is edited, in either ratchet. The measured token saving is −477, not the −630 the card estimated — the difference is the stop trigger, kept for the reason in section 2. The script growth (+50 lines across two gates) is entirely the derivation constants, the new regression case and the comments recording the measured limit; neither script is under a ratchet.

5. Gates

Union derived at the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands:

dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit ae616fbc91 (/home/user/objectstack-14685-item4).
dispatch-gates: change set derived from git — 4 path(s) vs merge base 5ff5f9576 of 'origin/main' and HEAD

Every exit code captured by redirect before any pipe.

All 32 re-run at the final head ae616fbc91, after the last commit. 31 green, 1 NOT MEASURED, 0 red. The load-bearing ones, quoted from their own verdict lines:

commandexitits own verdict line
pnpm check:skill-frame-sync0✓ check-skill-frame-sync self-test: 13 cases pass, plus 5 dispatch-gates declaration cases. / ✓ check-skill-frame-sync: 2 copies of the decision frame are structurally isomorphic across 2 files
node scripts/check-skill-frame-freshness.mjs0✓ check-skill-frame-freshness: the decision frame in this tree is current with origin/main (fetched just now). (12 self-test cases pass)
node scripts/check-skills-token-ratchet.mjs0✓ check-skills-token-ratchet: skills/objectstack-pm-dispatch/SKILL.md is 9225 tokens (ceiling 9708; headroom 483).
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: .claude/agents/os-dev.md is 450 lines (ceiling 469; headroom 19).
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass.
pnpm --filter @objectstack/spec run check:skill-docs0✓ skills/README.md / ✓ content/docs/ai/skills-reference.mdx — nothing to regenerate
pnpm check:ratchet-remedy-authority0green
pnpm check:pm-skill-id-lint0green
pnpm check:nul-bytes0green (plus a direct grep -naP for control bytes over both edited markdown files: no match)
pnpm check:doc-authoring, check:role-word, check:agent-model-declared, check:agent-test-spelling, check:corpus-claim-drift, check:skill-compatibility, check:skill-identifier-liveness, check:watch-hint-literal, check:entry-guard, check:parse-guard, check:cli-command-ids, check:bash32-floor, check:pnpm-filter-targets, check:pm-governed-merges, check:cross-package-test-inputs0green
node scripts/check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-shard-attestation.mjs, check-whole-set-label-write.mjs, scripts/pm/bare-root-worklist.mjs --self-test0green
pnpm --filter @objectstack/lint run check:doc-formula-expressions0first run answered PREREQUISITE NOT MET (exit 3, @objectstack/formula then @objectstack/lint unbuilt). Prerequisite supplied via the shared verify lock (turbo run build --filter=@objectstack/formula, then --filter=@objectstack/lint) and re-run to a real verdict: ✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1365 TS blocks judged clean
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

One red was found and fixed rather than reported: on the first pass check:pm-skill-ratchet said ✗ check-skill-line-ratchet: .claude/agents/os-dev.md has 1 line(s) over the 120-byte budget: L400 (121B). The ratchet prices a per-line byte budget as well as a line ceiling. The replacement sentence was rewrapped at legal CJK break points, content byte-identical, and the whole union was re-derived and re-run at the resulting head — which is the head every number above comes from.

NOT MEASURED, with reasons:

  • node scripts/check-test-completeness.mjs — exit 3, its own text: "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … the local reading for this gate is NOT MEASURED. ⛔ It is not a red." CI tees the log and passes it; unreachable there.
  • The changeset-derived families (9 of them) do not apply: this PR is skip-changeset.claude/**, skills/** and scripts/** publish nothing from any package.
  • The CI-measured-only family reads the workflow event payload; no local run can produce a verdict for it.
  • The always-runs tail (workflows with no path filter) is CI's, not measured here.

6. Out-of-scope findings — NOT fixed here, for the seat to triage

Each is a claim that this change makes false, in a file this flight does not hold.

  • F1scripts/pm/dispatch-gates.mjs:8090why: '… the dev-agent definition is protocol semantics — every dispatched dev runs under it, and it carries an enforced copy of the decision frame'. The last clause is now false; the mandate itself still holds on the first clause, and the self-test still passes (it asserts the glob exists, not the reason).
  • F2scripts/pm/dispatch-gates.mjs:8095why: '… the published PM skill carries two enforced copies of the decision frame (check:skill-frame-sync COPIES) …'. One copy now.
  • F3scripts/pm/check-skill-line-ratchet.mjs:455// This file is one of the four check:skill-frame-sync COPIES, so a rule that …, in the os-dev.md ceiling ledger. That file is no longer a COPIES entry, which also weakens the stated reason for the +3 raise recorded there. Item 5 holds this file.
  • F4scripts/check-skills-token-ratchet.mjs:334, 345, 359 — three ledger comments stating the published SKILL.md "carries TWO enforced frame copies" and that "the two gate-pinned copies of the decision frame stay in this file". Historically true; now stale as a present-tense claim.
  • F5 the internal lane has no {decision_frame} slot. os-dev.md now expects the frame to arrive in the dispatch prompt, but .claude/skills/pm-dispatch/references/dispatch-runbook.md (派发词构造细则) does not tell the internal PM to paste it, and .claude/skills/pm-dispatch/SKILL.md is explicitly outside this flight. Until that lands, the internal dev's only protection is the stop-and-ask clause added in section 2. This is the one finding with live consequences.
  • F6 the removed os-dev.md block carried a verbatim maintainer quotation — the 2026-09-01 axis-weighting ruling — so this PR deletes a quotation site that item 2's quotation-policy flight may have surveyed. The ruling text survives at .claude/skills/pm-dispatch/SKILL.md:904 and .claude/skills/pm-dispatch/references/decision-analysis.md:50, and the RULE itself survives in both published and internal PM copies of the frame.

7. Posture

Draft, governed surface. No reviewers requested, no ready flip, no enqueue, no auto-merge. Labels skip-changeset and needs:contract-review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1


Generated by Claude Code

Batch-3 ruling, item 4, option B: the four-axis decision frame keeps two
copies (internal-pm and published-pm); the dev-side copies are replaced by
the {decision_frame} placeholder the dispatching PM fills at dispatch time.
- skills/objectstack-pm-dispatch/SKILL.md: the embedded dev-agent template
loses the frame copy (declaring sentence through binding sentence). The
stop trigger stays -- it is the escalation TRIGGER, not the frame, and
rules/dev-template.md carries it nowhere else. The paste instruction now
names the surviving section by heading.
- .claude/agents/os-dev.md: the frame block collapses to one sentence saying
the frame arrives in the dispatch prompt, that every option is analyzed on
its axes and the recommendation justified on them, and that a prompt
without it is a stop-and-ask rather than an invitation to invent axes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…copies
check-skill-frame-sync watched four copies across three files; the batch-3
ruling (item 4, option B) leaves two, both PM-side. The dev-side entries are
removed and every count that was spelled out is now derived from the table.
- COPIES loses internal-dev and published-dev; the header, the COPIES
docblock and the count-disagreement message stop spelling "four".
- The same-file overlap guard stays. It is vacuous today (no two copies share
a file) but re-declaring such a pair is one edit away, and it is the check
that makes such a pair prove it compares two different sections.
- Self-test: the three cases driven through the removed ids are retargeted at
the surviving copies, and a new case pins the reduction itself -- the
dev-side text reappearing in the now-undeclared agent definition is refused
by the anti-dormancy scan. Its measured limit is written next to it: the
scan's population is per FILE, so a second copy pasted back into a file that
is already declared is not refused, and was not before this change either.
- check-skill-frame-freshness imported COPIES but spelled one frame file's
path in five fixtures; those now derive it from the table, which is what
the file's own #8024 rule asks for.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… budget
check:pm-skill-ratchet reads a per-line byte budget as well as a line ceiling;
the first line of the replacement landed at 121B. Rewrapped at legal CJK break
points, content byte-identical.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Sep 3, 2026
@os-litantos-litant added skip-changeset PR has no user-facing published change; bypasses the changeset gate needs:contract-review and removed documentation Improvements or additions to documentation labels Sep 3, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review September 3, 2026 08:57
@os-zhuang
os-zhuang added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 7b19b9eSep 3, 2026
46 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-14685-decision-frame-two-copies branch September 3, 2026 11:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

docs(pm-dispatch): keep two copies of the decision frame, not four - #14862

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies
Sep 3, 2026
Merged

docs(pm-dispatch): keep two copies of the decision frame, not four#14862
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Part of #14685
Part of #13597

Item 4 of decision batch 3, ruled B. From the ruling comment (5520452691), verbatim:

B — the four-axis decision frame keeps two copies (internal-pm and published-pm); the dev-side copies are replaced by a {decision_frame} placeholder the dispatching PM fills at dispatch time; check:skill-frame-sync watches two sites. Confidence gap carried: whoever executes verifies that the published-dev block has no reader other than the placeholder path before removing it.


1. The confidence gap, discharged BEFORE any edit

The ruling made removal conditional on a reader census. Run at the merge base 5ff5f9576, over .claude/ skills/ scripts/ .github/ packages/ docs/, for four things: the published-dev start marker (Analyze every option on ... fixed axes:), the literal published-dev, {decision_frame}, and the internal-dev start marker. Every hit, and what it is:

sitewhat it isdepends on the published-dev BLOCK?
skills/objectstack-pm-dispatch/SKILL.md:633-662the block itselfit IS the block
skills/objectstack-pm-dispatch/SKILL.md:623"with the block below in place of its {decision_frame} line"YES — the placeholder path, and the only one
skills/objectstack-pm-dispatch/rules/dev-template.md:99the {decision_frame} line the instruction above fillsvia that instruction only
scripts/check-skill-frame-sync.mjs:136-143the published-dev COPIES entrythe gate that watches it
scripts/check-skill-frame-sync.mjs:714,719,723,724,727self-test fixtures addressing the published-dev idthe gate's own fixtures
scripts/check-skill-frame-freshness.mjsimports COPIES / AXIS_MAP wholesale; five fixtures spelled .claude/agents/os-dev.mdreads the TABLE, never the block text
scripts/pm/dispatch-gates.mjs:15382-15397spawned import reading the FILE list out of COPIES for the tier mandatereads FILES, not copies
scripts/pm/check-dispatch-gates.mjs:202FRAME_MODULE watch-hint constantnames the module path only
scripts/check-skills-token-ratchet.mjs:334,345,359ceiling-ledger prosecomments, no behaviour
scripts/pm/check-skill-line-ratchet.mjs:455ceiling-ledger prosecomments, no behaviour

No test, generator, workflow, doc or fixture quotes the block by fingerprint. The gap resolves in favour of removal: the placeholder path and the two frame gates are the whole reader set, and both gates read the COPIES table rather than the prose.

The same census over the internal-dev start marker found exactly one non-gate hit — the block itself at .claude/agents/os-dev.md:400.

2. What changed, site by site

skills/objectstack-pm-dispatch/SKILL.md — the embedded dev template loses the frame copy: the declaring sentence through the binding sentence (old :633-662), which is precisely the span the gate's start and binding anchors delimit.

The stop trigger above it (old :627-632, "When to STOP instead of coding …") stays, and that is a judgement call worth naming. It is not part of the frame — the gate's anchors do not reach it — and it is the escalation TRIGGER rather than the analysis. rules/dev-template.md carries it nowhere else (grepped: the template mentions needs_decision only at :34 inside the contract-first rule and at :112 in the report schema), so deleting it with the frame would have removed the dev's instruction to stop at all, which the ruling did not ask for.

The paste instruction now names the surviving copy by its heading:

Paste rules/dev-template.md verbatim into every dispatch prompt; the PM fills
its {…} placeholders. {decision_frame} takes the stop trigger below, followed
by the whole `#### The four-axis decision frame (binding)` section above: the
frame has ONE copy in this skill and the PM pastes it at dispatch time, so no
dev-side copy can drift from it.

.claude/agents/os-dev.md — the frame block (old :400-421, declaring sentence through the binding sentence, which ran into the same paragraph as the unrelated blocked rule) collapses to one sentence. The blocked rule is preserved byte-for-byte. Neither :232 nor :303 is touched (PR #14808 holds them); the diff is one hunk at :400.

Three judgement calls in that one sentence:

  1. It says the frame comes from the PM's own copy, not specifically from rules/dev-template.md, because that template is the PUBLISHED skill's artifact. The internal PM composes dispatch prompts per .claude/skills/pm-dispatch/references/dispatch-runbook.md and has no {decision_frame} slot (grep: the token exists only under skills/objectstack-pm-dispatch/). The published slot is named parenthetically, as the brief asked, without claiming the internal lane uses it. See finding F5.
  2. It adds "if the dispatch prompt did not carry it, stop and ask; do not invent your own axes." os-dev.md's own doctrine is that standard clauses live in that file and prompt silence is normal — so moving the frame OUT of it opens a hole that a silent prompt would otherwise walk through.
  3. It is written in Chinese and deliberately avoids the phrases fixed axes, 固定评估轴, 条评估轴 and any WORD-axis decision frame form. Those are FINGERPRINTS in the sync gate's anti-dormancy scan, whose population is per file: os-dev.md stops being a declared file with this change, so any of those phrasings would have turned the gate red. Verified after the edit — grep -nP for all four patterns over os-dev.md returns nothing.

scripts/check-skill-frame-sync.mjsCOPIES 4 → 2.

- internal-pm .claude/skills/pm-dispatch/SKILL.md KEPT
- internal-dev .claude/agents/os-dev.md REMOVED
- published-pm skills/objectstack-pm-dispatch/SKILL.md KEPT
- published-dev skills/objectstack-pm-dispatch/SKILL.md REMOVED

Every spelled count is now derived: the header ("FOUR hand-written copies across three files"), the COPIES docblock, the axisEntryStarts docblock, the freshness-gate cross-reference, and the count-disagreement message (the four copies do not agree becomes the ${results.length} copies do not agree). report() and the success line already derived theirs from COPIES.length.

The same-file overlap guard is kept even though it is vacuous today — no two copies share a file any more. Re-declaring such a pair is one edit away, and that guard is what would make such a pair prove it compares two different sections rather than the same one twice.

scripts/check-skill-frame-freshness.mjs — it imported COPIES but its self-test SPELLED .claude/agents/os-dev.md in five fixtures (cases 1, 2, 6, 7 and the two withUnreadableCopy calls in 8 and 9). Those broke on the reduction, exactly as that file's own #8024 rule predicts. They now derive SAMPLE_COPY_ID / SAMPLE_FRAME_FILE from COPIES[0], so the next change to the table re-points the fixtures instead of stranding them.

3. The anti-dormancy scan still refuses a returning copy — measured, both directions

Committed first, then mutated, with an EXIT INT TERM trap restoring absolute paths; each mutation proved on disk by counting the injected marker before reading any verdict; each restore proved by git diff HEAD empty AND by comparing git hash-object against the path's HEAD blob.

Direction A — the removed text returns to the now-undeclared .claude/agents/os-dev.md. REFUSED. Marker occurrences after mutation: 1 (was 0). node scripts/check-skill-frame-sync.mjs exit 1:

✗ check-skill-frame-sync: 1 problem(s).
• .claude/agents/os-dev.md
looks like ANOTHER copy of the decision frame (matched /fixed axes/) but is
not declared in COPIES, so nothing keeps it in sync.

Restore: git diff HEAD empty; worktree blob 968d4fd10a653f013635a8a48bdee25a25efc648 equals the HEAD blob.

Direction B — the same text returns to the still-declared skills/objectstack-pm-dispatch/SKILL.md. NOT REFUSED (exit 0). This is a real, measured limit and it is not new: the scan skips any file present in declaredFiles, so a second undeclared copy inside an already-declared file has always been invisible to it. Restore proved the same way (blob 3cbbed0f501736a0db3a5ecdd9e6cda43d44b434).

What that limit means for this PR, stated rather than implied: what keeps a second published copy from being written is the paste instruction naming the surviving section by heading, plus review — not the scan. Closing it mechanically needs a per-occurrence criterion the gate does not have (the published file legitimately carries two more fixed axes fingerprints, at :534 and in the :539 heading). A comment next to the new self-test case says exactly this, so the next reader does not mistake direction B for a bug introduced here.

A self-test case pins direction A against the REAL path this change undeclared, rather than a synthetic one.

4. Counts (merge base 5ff5f9576 → head)

filebyteslinestokens (ceil bytes/4)
skills/objectstack-pm-dispatch/SKILL.md38807 → 36898 (−1909)753 → 726 (−27)9702 → 9225 (−477), ceiling 9708
.claude/agents/os-dev.md42743 → 41196 (−1547)469 → 450 (−19)— (line-ratcheted), ceiling 469
scripts/check-skill-frame-sync.mjs40961 → 43180 (+2219)960 → 990 (+30)not ratcheted
scripts/check-skill-frame-freshness.mjs46344 → 47480 (+1136)1065 → 1085 (+20)not ratcheted

Whole published package skills/objectstack-pm-dispatch/** (all markdown): 11540 → 11063 tokens, −477.

Two ratcheted files, both shrink; no ceiling row is edited, in either ratchet. The measured token saving is −477, not the −630 the card estimated — the difference is the stop trigger, kept for the reason in section 2. The script growth (+50 lines across two gates) is entirely the derivation constants, the new regression case and the comments recording the measured limit; neither script is under a ratchet.

5. Gates

Union derived at the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands:

dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit ae616fbc91 (/home/user/objectstack-14685-item4).
dispatch-gates: change set derived from git — 4 path(s) vs merge base 5ff5f9576 of 'origin/main' and HEAD

Every exit code captured by redirect before any pipe.

All 32 re-run at the final head ae616fbc91, after the last commit. 31 green, 1 NOT MEASURED, 0 red. The load-bearing ones, quoted from their own verdict lines:

commandexitits own verdict line
pnpm check:skill-frame-sync0✓ check-skill-frame-sync self-test: 13 cases pass, plus 5 dispatch-gates declaration cases. / ✓ check-skill-frame-sync: 2 copies of the decision frame are structurally isomorphic across 2 files
node scripts/check-skill-frame-freshness.mjs0✓ check-skill-frame-freshness: the decision frame in this tree is current with origin/main (fetched just now). (12 self-test cases pass)
node scripts/check-skills-token-ratchet.mjs0✓ check-skills-token-ratchet: skills/objectstack-pm-dispatch/SKILL.md is 9225 tokens (ceiling 9708; headroom 483).
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: .claude/agents/os-dev.md is 450 lines (ceiling 469; headroom 19).
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass.
pnpm --filter @objectstack/spec run check:skill-docs0✓ skills/README.md / ✓ content/docs/ai/skills-reference.mdx — nothing to regenerate
pnpm check:ratchet-remedy-authority0green
pnpm check:pm-skill-id-lint0green
pnpm check:nul-bytes0green (plus a direct grep -naP for control bytes over both edited markdown files: no match)
pnpm check:doc-authoring, check:role-word, check:agent-model-declared, check:agent-test-spelling, check:corpus-claim-drift, check:skill-compatibility, check:skill-identifier-liveness, check:watch-hint-literal, check:entry-guard, check:parse-guard, check:cli-command-ids, check:bash32-floor, check:pnpm-filter-targets, check:pm-governed-merges, check:cross-package-test-inputs0green
node scripts/check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-shard-attestation.mjs, check-whole-set-label-write.mjs, scripts/pm/bare-root-worklist.mjs --self-test0green
pnpm --filter @objectstack/lint run check:doc-formula-expressions0first run answered PREREQUISITE NOT MET (exit 3, @objectstack/formula then @objectstack/lint unbuilt). Prerequisite supplied via the shared verify lock (turbo run build --filter=@objectstack/formula, then --filter=@objectstack/lint) and re-run to a real verdict: ✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1365 TS blocks judged clean
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

One red was found and fixed rather than reported: on the first pass check:pm-skill-ratchet said ✗ check-skill-line-ratchet: .claude/agents/os-dev.md has 1 line(s) over the 120-byte budget: L400 (121B). The ratchet prices a per-line byte budget as well as a line ceiling. The replacement sentence was rewrapped at legal CJK break points, content byte-identical, and the whole union was re-derived and re-run at the resulting head — which is the head every number above comes from.

NOT MEASURED, with reasons:

  • node scripts/check-test-completeness.mjs — exit 3, its own text: "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … the local reading for this gate is NOT MEASURED. ⛔ It is not a red." CI tees the log and passes it; unreachable there.
  • The changeset-derived families (9 of them) do not apply: this PR is skip-changeset.claude/**, skills/** and scripts/** publish nothing from any package.
  • The CI-measured-only family reads the workflow event payload; no local run can produce a verdict for it.
  • The always-runs tail (workflows with no path filter) is CI's, not measured here.

6. Out-of-scope findings — NOT fixed here, for the seat to triage

Each is a claim that this change makes false, in a file this flight does not hold.

  • F1scripts/pm/dispatch-gates.mjs:8090why: '… the dev-agent definition is protocol semantics — every dispatched dev runs under it, and it carries an enforced copy of the decision frame'. The last clause is now false; the mandate itself still holds on the first clause, and the self-test still passes (it asserts the glob exists, not the reason).
  • F2scripts/pm/dispatch-gates.mjs:8095why: '… the published PM skill carries two enforced copies of the decision frame (check:skill-frame-sync COPIES) …'. One copy now.
  • F3scripts/pm/check-skill-line-ratchet.mjs:455// This file is one of the four check:skill-frame-sync COPIES, so a rule that …, in the os-dev.md ceiling ledger. That file is no longer a COPIES entry, which also weakens the stated reason for the +3 raise recorded there. Item 5 holds this file.
  • F4scripts/check-skills-token-ratchet.mjs:334, 345, 359 — three ledger comments stating the published SKILL.md "carries TWO enforced frame copies" and that "the two gate-pinned copies of the decision frame stay in this file". Historically true; now stale as a present-tense claim.
  • F5 the internal lane has no {decision_frame} slot. os-dev.md now expects the frame to arrive in the dispatch prompt, but .claude/skills/pm-dispatch/references/dispatch-runbook.md (派发词构造细则) does not tell the internal PM to paste it, and .claude/skills/pm-dispatch/SKILL.md is explicitly outside this flight. Until that lands, the internal dev's only protection is the stop-and-ask clause added in section 2. This is the one finding with live consequences.
  • F6 the removed os-dev.md block carried a verbatim maintainer quotation — the 2026-09-01 axis-weighting ruling — so this PR deletes a quotation site that item 2's quotation-policy flight may have surveyed. The ruling text survives at .claude/skills/pm-dispatch/SKILL.md:904 and .claude/skills/pm-dispatch/references/decision-analysis.md:50, and the RULE itself survives in both published and internal PM copies of the frame.

7. Posture

Draft, governed surface. No reviewers requested, no ready flip, no enqueue, no auto-merge. Labels skip-changeset and needs:contract-review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1


Generated by Claude Code

Batch-3 ruling, item 4, option B: the four-axis decision frame keeps two
copies (internal-pm and published-pm); the dev-side copies are replaced by
the {decision_frame} placeholder the dispatching PM fills at dispatch time.
- skills/objectstack-pm-dispatch/SKILL.md: the embedded dev-agent template
loses the frame copy (declaring sentence through binding sentence). The
stop trigger stays -- it is the escalation TRIGGER, not the frame, and
rules/dev-template.md carries it nowhere else. The paste instruction now
names the surviving section by heading.
- .claude/agents/os-dev.md: the frame block collapses to one sentence saying
the frame arrives in the dispatch prompt, that every option is analyzed on
its axes and the recommendation justified on them, and that a prompt
without it is a stop-and-ask rather than an invitation to invent axes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…copies
check-skill-frame-sync watched four copies across three files; the batch-3
ruling (item 4, option B) leaves two, both PM-side. The dev-side entries are
removed and every count that was spelled out is now derived from the table.
- COPIES loses internal-dev and published-dev; the header, the COPIES
docblock and the count-disagreement message stop spelling "four".
- The same-file overlap guard stays. It is vacuous today (no two copies share
a file) but re-declaring such a pair is one edit away, and it is the check
that makes such a pair prove it compares two different sections.
- Self-test: the three cases driven through the removed ids are retargeted at
the surviving copies, and a new case pins the reduction itself -- the
dev-side text reappearing in the now-undeclared agent definition is refused
by the anti-dormancy scan. Its measured limit is written next to it: the
scan's population is per FILE, so a second copy pasted back into a file that
is already declared is not refused, and was not before this change either.
- check-skill-frame-freshness imported COPIES but spelled one frame file's
path in five fixtures; those now derive it from the table, which is what
the file's own #8024 rule asks for.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… budget
check:pm-skill-ratchet reads a per-line byte budget as well as a line ceiling;
the first line of the replacement landed at 121B. Rewrapped at legal CJK break
points, content byte-identical.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Sep 3, 2026
@os-litantos-litant added skip-changeset PR has no user-facing published change; bypasses the changeset gate needs:contract-review and removed documentation Improvements or additions to documentation labels Sep 3, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review September 3, 2026 08:57
@os-zhuang
os-zhuang added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 7b19b9eSep 3, 2026
46 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-14685-decision-frame-two-copies branch September 3, 2026 11:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(pm-dispatch): keep two copies of the decision frame, not four - #14862

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies
Sep 3, 2026
Merged

docs(pm-dispatch): keep two copies of the decision frame, not four#14862
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Part of #14685
Part of #13597

Item 4 of decision batch 3, ruled B. From the ruling comment (5520452691), verbatim:

B — the four-axis decision frame keeps two copies (internal-pm and published-pm); the dev-side copies are replaced by a {decision_frame} placeholder the dispatching PM fills at dispatch time; check:skill-frame-sync watches two sites. Confidence gap carried: whoever executes verifies that the published-dev block has no reader other than the placeholder path before removing it.


1. The confidence gap, discharged BEFORE any edit

The ruling made removal conditional on a reader census. Run at the merge base 5ff5f9576, over .claude/ skills/ scripts/ .github/ packages/ docs/, for four things: the published-dev start marker (Analyze every option on ... fixed axes:), the literal published-dev, {decision_frame}, and the internal-dev start marker. Every hit, and what it is:

sitewhat it isdepends on the published-dev BLOCK?
skills/objectstack-pm-dispatch/SKILL.md:633-662the block itselfit IS the block
skills/objectstack-pm-dispatch/SKILL.md:623"with the block below in place of its {decision_frame} line"YES — the placeholder path, and the only one
skills/objectstack-pm-dispatch/rules/dev-template.md:99the {decision_frame} line the instruction above fillsvia that instruction only
scripts/check-skill-frame-sync.mjs:136-143the published-dev COPIES entrythe gate that watches it
scripts/check-skill-frame-sync.mjs:714,719,723,724,727self-test fixtures addressing the published-dev idthe gate's own fixtures
scripts/check-skill-frame-freshness.mjsimports COPIES / AXIS_MAP wholesale; five fixtures spelled .claude/agents/os-dev.mdreads the TABLE, never the block text
scripts/pm/dispatch-gates.mjs:15382-15397spawned import reading the FILE list out of COPIES for the tier mandatereads FILES, not copies
scripts/pm/check-dispatch-gates.mjs:202FRAME_MODULE watch-hint constantnames the module path only
scripts/check-skills-token-ratchet.mjs:334,345,359ceiling-ledger prosecomments, no behaviour
scripts/pm/check-skill-line-ratchet.mjs:455ceiling-ledger prosecomments, no behaviour

No test, generator, workflow, doc or fixture quotes the block by fingerprint. The gap resolves in favour of removal: the placeholder path and the two frame gates are the whole reader set, and both gates read the COPIES table rather than the prose.

The same census over the internal-dev start marker found exactly one non-gate hit — the block itself at .claude/agents/os-dev.md:400.

2. What changed, site by site

skills/objectstack-pm-dispatch/SKILL.md — the embedded dev template loses the frame copy: the declaring sentence through the binding sentence (old :633-662), which is precisely the span the gate's start and binding anchors delimit.

The stop trigger above it (old :627-632, "When to STOP instead of coding …") stays, and that is a judgement call worth naming. It is not part of the frame — the gate's anchors do not reach it — and it is the escalation TRIGGER rather than the analysis. rules/dev-template.md carries it nowhere else (grepped: the template mentions needs_decision only at :34 inside the contract-first rule and at :112 in the report schema), so deleting it with the frame would have removed the dev's instruction to stop at all, which the ruling did not ask for.

The paste instruction now names the surviving copy by its heading:

Paste rules/dev-template.md verbatim into every dispatch prompt; the PM fills
its {…} placeholders. {decision_frame} takes the stop trigger below, followed
by the whole `#### The four-axis decision frame (binding)` section above: the
frame has ONE copy in this skill and the PM pastes it at dispatch time, so no
dev-side copy can drift from it.

.claude/agents/os-dev.md — the frame block (old :400-421, declaring sentence through the binding sentence, which ran into the same paragraph as the unrelated blocked rule) collapses to one sentence. The blocked rule is preserved byte-for-byte. Neither :232 nor :303 is touched (PR #14808 holds them); the diff is one hunk at :400.

Three judgement calls in that one sentence:

  1. It says the frame comes from the PM's own copy, not specifically from rules/dev-template.md, because that template is the PUBLISHED skill's artifact. The internal PM composes dispatch prompts per .claude/skills/pm-dispatch/references/dispatch-runbook.md and has no {decision_frame} slot (grep: the token exists only under skills/objectstack-pm-dispatch/). The published slot is named parenthetically, as the brief asked, without claiming the internal lane uses it. See finding F5.
  2. It adds "if the dispatch prompt did not carry it, stop and ask; do not invent your own axes." os-dev.md's own doctrine is that standard clauses live in that file and prompt silence is normal — so moving the frame OUT of it opens a hole that a silent prompt would otherwise walk through.
  3. It is written in Chinese and deliberately avoids the phrases fixed axes, 固定评估轴, 条评估轴 and any WORD-axis decision frame form. Those are FINGERPRINTS in the sync gate's anti-dormancy scan, whose population is per file: os-dev.md stops being a declared file with this change, so any of those phrasings would have turned the gate red. Verified after the edit — grep -nP for all four patterns over os-dev.md returns nothing.

scripts/check-skill-frame-sync.mjsCOPIES 4 → 2.

- internal-pm .claude/skills/pm-dispatch/SKILL.md KEPT
- internal-dev .claude/agents/os-dev.md REMOVED
- published-pm skills/objectstack-pm-dispatch/SKILL.md KEPT
- published-dev skills/objectstack-pm-dispatch/SKILL.md REMOVED

Every spelled count is now derived: the header ("FOUR hand-written copies across three files"), the COPIES docblock, the axisEntryStarts docblock, the freshness-gate cross-reference, and the count-disagreement message (the four copies do not agree becomes the ${results.length} copies do not agree). report() and the success line already derived theirs from COPIES.length.

The same-file overlap guard is kept even though it is vacuous today — no two copies share a file any more. Re-declaring such a pair is one edit away, and that guard is what would make such a pair prove it compares two different sections rather than the same one twice.

scripts/check-skill-frame-freshness.mjs — it imported COPIES but its self-test SPELLED .claude/agents/os-dev.md in five fixtures (cases 1, 2, 6, 7 and the two withUnreadableCopy calls in 8 and 9). Those broke on the reduction, exactly as that file's own #8024 rule predicts. They now derive SAMPLE_COPY_ID / SAMPLE_FRAME_FILE from COPIES[0], so the next change to the table re-points the fixtures instead of stranding them.

3. The anti-dormancy scan still refuses a returning copy — measured, both directions

Committed first, then mutated, with an EXIT INT TERM trap restoring absolute paths; each mutation proved on disk by counting the injected marker before reading any verdict; each restore proved by git diff HEAD empty AND by comparing git hash-object against the path's HEAD blob.

Direction A — the removed text returns to the now-undeclared .claude/agents/os-dev.md. REFUSED. Marker occurrences after mutation: 1 (was 0). node scripts/check-skill-frame-sync.mjs exit 1:

✗ check-skill-frame-sync: 1 problem(s).
• .claude/agents/os-dev.md
looks like ANOTHER copy of the decision frame (matched /fixed axes/) but is
not declared in COPIES, so nothing keeps it in sync.

Restore: git diff HEAD empty; worktree blob 968d4fd10a653f013635a8a48bdee25a25efc648 equals the HEAD blob.

Direction B — the same text returns to the still-declared skills/objectstack-pm-dispatch/SKILL.md. NOT REFUSED (exit 0). This is a real, measured limit and it is not new: the scan skips any file present in declaredFiles, so a second undeclared copy inside an already-declared file has always been invisible to it. Restore proved the same way (blob 3cbbed0f501736a0db3a5ecdd9e6cda43d44b434).

What that limit means for this PR, stated rather than implied: what keeps a second published copy from being written is the paste instruction naming the surviving section by heading, plus review — not the scan. Closing it mechanically needs a per-occurrence criterion the gate does not have (the published file legitimately carries two more fixed axes fingerprints, at :534 and in the :539 heading). A comment next to the new self-test case says exactly this, so the next reader does not mistake direction B for a bug introduced here.

A self-test case pins direction A against the REAL path this change undeclared, rather than a synthetic one.

4. Counts (merge base 5ff5f9576 → head)

filebyteslinestokens (ceil bytes/4)
skills/objectstack-pm-dispatch/SKILL.md38807 → 36898 (−1909)753 → 726 (−27)9702 → 9225 (−477), ceiling 9708
.claude/agents/os-dev.md42743 → 41196 (−1547)469 → 450 (−19)— (line-ratcheted), ceiling 469
scripts/check-skill-frame-sync.mjs40961 → 43180 (+2219)960 → 990 (+30)not ratcheted
scripts/check-skill-frame-freshness.mjs46344 → 47480 (+1136)1065 → 1085 (+20)not ratcheted

Whole published package skills/objectstack-pm-dispatch/** (all markdown): 11540 → 11063 tokens, −477.

Two ratcheted files, both shrink; no ceiling row is edited, in either ratchet. The measured token saving is −477, not the −630 the card estimated — the difference is the stop trigger, kept for the reason in section 2. The script growth (+50 lines across two gates) is entirely the derivation constants, the new regression case and the comments recording the measured limit; neither script is under a ratchet.

5. Gates

Union derived at the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands:

dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit ae616fbc91 (/home/user/objectstack-14685-item4).
dispatch-gates: change set derived from git — 4 path(s) vs merge base 5ff5f9576 of 'origin/main' and HEAD

Every exit code captured by redirect before any pipe.

All 32 re-run at the final head ae616fbc91, after the last commit. 31 green, 1 NOT MEASURED, 0 red. The load-bearing ones, quoted from their own verdict lines:

commandexitits own verdict line
pnpm check:skill-frame-sync0✓ check-skill-frame-sync self-test: 13 cases pass, plus 5 dispatch-gates declaration cases. / ✓ check-skill-frame-sync: 2 copies of the decision frame are structurally isomorphic across 2 files
node scripts/check-skill-frame-freshness.mjs0✓ check-skill-frame-freshness: the decision frame in this tree is current with origin/main (fetched just now). (12 self-test cases pass)
node scripts/check-skills-token-ratchet.mjs0✓ check-skills-token-ratchet: skills/objectstack-pm-dispatch/SKILL.md is 9225 tokens (ceiling 9708; headroom 483).
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: .claude/agents/os-dev.md is 450 lines (ceiling 469; headroom 19).
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass.
pnpm --filter @objectstack/spec run check:skill-docs0✓ skills/README.md / ✓ content/docs/ai/skills-reference.mdx — nothing to regenerate
pnpm check:ratchet-remedy-authority0green
pnpm check:pm-skill-id-lint0green
pnpm check:nul-bytes0green (plus a direct grep -naP for control bytes over both edited markdown files: no match)
pnpm check:doc-authoring, check:role-word, check:agent-model-declared, check:agent-test-spelling, check:corpus-claim-drift, check:skill-compatibility, check:skill-identifier-liveness, check:watch-hint-literal, check:entry-guard, check:parse-guard, check:cli-command-ids, check:bash32-floor, check:pnpm-filter-targets, check:pm-governed-merges, check:cross-package-test-inputs0green
node scripts/check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-shard-attestation.mjs, check-whole-set-label-write.mjs, scripts/pm/bare-root-worklist.mjs --self-test0green
pnpm --filter @objectstack/lint run check:doc-formula-expressions0first run answered PREREQUISITE NOT MET (exit 3, @objectstack/formula then @objectstack/lint unbuilt). Prerequisite supplied via the shared verify lock (turbo run build --filter=@objectstack/formula, then --filter=@objectstack/lint) and re-run to a real verdict: ✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1365 TS blocks judged clean
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

One red was found and fixed rather than reported: on the first pass check:pm-skill-ratchet said ✗ check-skill-line-ratchet: .claude/agents/os-dev.md has 1 line(s) over the 120-byte budget: L400 (121B). The ratchet prices a per-line byte budget as well as a line ceiling. The replacement sentence was rewrapped at legal CJK break points, content byte-identical, and the whole union was re-derived and re-run at the resulting head — which is the head every number above comes from.

NOT MEASURED, with reasons:

  • node scripts/check-test-completeness.mjs — exit 3, its own text: "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … the local reading for this gate is NOT MEASURED. ⛔ It is not a red." CI tees the log and passes it; unreachable there.
  • The changeset-derived families (9 of them) do not apply: this PR is skip-changeset.claude/**, skills/** and scripts/** publish nothing from any package.
  • The CI-measured-only family reads the workflow event payload; no local run can produce a verdict for it.
  • The always-runs tail (workflows with no path filter) is CI's, not measured here.

6. Out-of-scope findings — NOT fixed here, for the seat to triage

Each is a claim that this change makes false, in a file this flight does not hold.

  • F1scripts/pm/dispatch-gates.mjs:8090why: '… the dev-agent definition is protocol semantics — every dispatched dev runs under it, and it carries an enforced copy of the decision frame'. The last clause is now false; the mandate itself still holds on the first clause, and the self-test still passes (it asserts the glob exists, not the reason).
  • F2scripts/pm/dispatch-gates.mjs:8095why: '… the published PM skill carries two enforced copies of the decision frame (check:skill-frame-sync COPIES) …'. One copy now.
  • F3scripts/pm/check-skill-line-ratchet.mjs:455// This file is one of the four check:skill-frame-sync COPIES, so a rule that …, in the os-dev.md ceiling ledger. That file is no longer a COPIES entry, which also weakens the stated reason for the +3 raise recorded there. Item 5 holds this file.
  • F4scripts/check-skills-token-ratchet.mjs:334, 345, 359 — three ledger comments stating the published SKILL.md "carries TWO enforced frame copies" and that "the two gate-pinned copies of the decision frame stay in this file". Historically true; now stale as a present-tense claim.
  • F5 the internal lane has no {decision_frame} slot. os-dev.md now expects the frame to arrive in the dispatch prompt, but .claude/skills/pm-dispatch/references/dispatch-runbook.md (派发词构造细则) does not tell the internal PM to paste it, and .claude/skills/pm-dispatch/SKILL.md is explicitly outside this flight. Until that lands, the internal dev's only protection is the stop-and-ask clause added in section 2. This is the one finding with live consequences.
  • F6 the removed os-dev.md block carried a verbatim maintainer quotation — the 2026-09-01 axis-weighting ruling — so this PR deletes a quotation site that item 2's quotation-policy flight may have surveyed. The ruling text survives at .claude/skills/pm-dispatch/SKILL.md:904 and .claude/skills/pm-dispatch/references/decision-analysis.md:50, and the RULE itself survives in both published and internal PM copies of the frame.

7. Posture

Draft, governed surface. No reviewers requested, no ready flip, no enqueue, no auto-merge. Labels skip-changeset and needs:contract-review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1


Generated by Claude Code

Batch-3 ruling, item 4, option B: the four-axis decision frame keeps two
copies (internal-pm and published-pm); the dev-side copies are replaced by
the {decision_frame} placeholder the dispatching PM fills at dispatch time.
- skills/objectstack-pm-dispatch/SKILL.md: the embedded dev-agent template
loses the frame copy (declaring sentence through binding sentence). The
stop trigger stays -- it is the escalation TRIGGER, not the frame, and
rules/dev-template.md carries it nowhere else. The paste instruction now
names the surviving section by heading.
- .claude/agents/os-dev.md: the frame block collapses to one sentence saying
the frame arrives in the dispatch prompt, that every option is analyzed on
its axes and the recommendation justified on them, and that a prompt
without it is a stop-and-ask rather than an invitation to invent axes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…copies
check-skill-frame-sync watched four copies across three files; the batch-3
ruling (item 4, option B) leaves two, both PM-side. The dev-side entries are
removed and every count that was spelled out is now derived from the table.
- COPIES loses internal-dev and published-dev; the header, the COPIES
docblock and the count-disagreement message stop spelling "four".
- The same-file overlap guard stays. It is vacuous today (no two copies share
a file) but re-declaring such a pair is one edit away, and it is the check
that makes such a pair prove it compares two different sections.
- Self-test: the three cases driven through the removed ids are retargeted at
the surviving copies, and a new case pins the reduction itself -- the
dev-side text reappearing in the now-undeclared agent definition is refused
by the anti-dormancy scan. Its measured limit is written next to it: the
scan's population is per FILE, so a second copy pasted back into a file that
is already declared is not refused, and was not before this change either.
- check-skill-frame-freshness imported COPIES but spelled one frame file's
path in five fixtures; those now derive it from the table, which is what
the file's own #8024 rule asks for.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… budget
check:pm-skill-ratchet reads a per-line byte budget as well as a line ceiling;
the first line of the replacement landed at 121B. Rewrapped at legal CJK break
points, content byte-identical.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Sep 3, 2026
@os-litantos-litant added skip-changeset PR has no user-facing published change; bypasses the changeset gate needs:contract-review and removed documentation Improvements or additions to documentation labels Sep 3, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review September 3, 2026 08:57
@os-zhuang
os-zhuang added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 7b19b9eSep 3, 2026
46 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-14685-decision-frame-two-copies branch September 3, 2026 11:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

docs(pm-dispatch): keep two copies of the decision frame, not four - #14862

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies
Sep 3, 2026
Merged

docs(pm-dispatch): keep two copies of the decision frame, not four#14862
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Part of #14685
Part of #13597

Item 4 of decision batch 3, ruled B. From the ruling comment (5520452691), verbatim:

B — the four-axis decision frame keeps two copies (internal-pm and published-pm); the dev-side copies are replaced by a {decision_frame} placeholder the dispatching PM fills at dispatch time; check:skill-frame-sync watches two sites. Confidence gap carried: whoever executes verifies that the published-dev block has no reader other than the placeholder path before removing it.


1. The confidence gap, discharged BEFORE any edit

The ruling made removal conditional on a reader census. Run at the merge base 5ff5f9576, over .claude/ skills/ scripts/ .github/ packages/ docs/, for four things: the published-dev start marker (Analyze every option on ... fixed axes:), the literal published-dev, {decision_frame}, and the internal-dev start marker. Every hit, and what it is:

sitewhat it isdepends on the published-dev BLOCK?
skills/objectstack-pm-dispatch/SKILL.md:633-662the block itselfit IS the block
skills/objectstack-pm-dispatch/SKILL.md:623"with the block below in place of its {decision_frame} line"YES — the placeholder path, and the only one
skills/objectstack-pm-dispatch/rules/dev-template.md:99the {decision_frame} line the instruction above fillsvia that instruction only
scripts/check-skill-frame-sync.mjs:136-143the published-dev COPIES entrythe gate that watches it
scripts/check-skill-frame-sync.mjs:714,719,723,724,727self-test fixtures addressing the published-dev idthe gate's own fixtures
scripts/check-skill-frame-freshness.mjsimports COPIES / AXIS_MAP wholesale; five fixtures spelled .claude/agents/os-dev.mdreads the TABLE, never the block text
scripts/pm/dispatch-gates.mjs:15382-15397spawned import reading the FILE list out of COPIES for the tier mandatereads FILES, not copies
scripts/pm/check-dispatch-gates.mjs:202FRAME_MODULE watch-hint constantnames the module path only
scripts/check-skills-token-ratchet.mjs:334,345,359ceiling-ledger prosecomments, no behaviour
scripts/pm/check-skill-line-ratchet.mjs:455ceiling-ledger prosecomments, no behaviour

No test, generator, workflow, doc or fixture quotes the block by fingerprint. The gap resolves in favour of removal: the placeholder path and the two frame gates are the whole reader set, and both gates read the COPIES table rather than the prose.

The same census over the internal-dev start marker found exactly one non-gate hit — the block itself at .claude/agents/os-dev.md:400.

2. What changed, site by site

skills/objectstack-pm-dispatch/SKILL.md — the embedded dev template loses the frame copy: the declaring sentence through the binding sentence (old :633-662), which is precisely the span the gate's start and binding anchors delimit.

The stop trigger above it (old :627-632, "When to STOP instead of coding …") stays, and that is a judgement call worth naming. It is not part of the frame — the gate's anchors do not reach it — and it is the escalation TRIGGER rather than the analysis. rules/dev-template.md carries it nowhere else (grepped: the template mentions needs_decision only at :34 inside the contract-first rule and at :112 in the report schema), so deleting it with the frame would have removed the dev's instruction to stop at all, which the ruling did not ask for.

The paste instruction now names the surviving copy by its heading:

Paste rules/dev-template.md verbatim into every dispatch prompt; the PM fills
its {…} placeholders. {decision_frame} takes the stop trigger below, followed
by the whole `#### The four-axis decision frame (binding)` section above: the
frame has ONE copy in this skill and the PM pastes it at dispatch time, so no
dev-side copy can drift from it.

.claude/agents/os-dev.md — the frame block (old :400-421, declaring sentence through the binding sentence, which ran into the same paragraph as the unrelated blocked rule) collapses to one sentence. The blocked rule is preserved byte-for-byte. Neither :232 nor :303 is touched (PR #14808 holds them); the diff is one hunk at :400.

Three judgement calls in that one sentence:

  1. It says the frame comes from the PM's own copy, not specifically from rules/dev-template.md, because that template is the PUBLISHED skill's artifact. The internal PM composes dispatch prompts per .claude/skills/pm-dispatch/references/dispatch-runbook.md and has no {decision_frame} slot (grep: the token exists only under skills/objectstack-pm-dispatch/). The published slot is named parenthetically, as the brief asked, without claiming the internal lane uses it. See finding F5.
  2. It adds "if the dispatch prompt did not carry it, stop and ask; do not invent your own axes." os-dev.md's own doctrine is that standard clauses live in that file and prompt silence is normal — so moving the frame OUT of it opens a hole that a silent prompt would otherwise walk through.
  3. It is written in Chinese and deliberately avoids the phrases fixed axes, 固定评估轴, 条评估轴 and any WORD-axis decision frame form. Those are FINGERPRINTS in the sync gate's anti-dormancy scan, whose population is per file: os-dev.md stops being a declared file with this change, so any of those phrasings would have turned the gate red. Verified after the edit — grep -nP for all four patterns over os-dev.md returns nothing.

scripts/check-skill-frame-sync.mjsCOPIES 4 → 2.

- internal-pm .claude/skills/pm-dispatch/SKILL.md KEPT
- internal-dev .claude/agents/os-dev.md REMOVED
- published-pm skills/objectstack-pm-dispatch/SKILL.md KEPT
- published-dev skills/objectstack-pm-dispatch/SKILL.md REMOVED

Every spelled count is now derived: the header ("FOUR hand-written copies across three files"), the COPIES docblock, the axisEntryStarts docblock, the freshness-gate cross-reference, and the count-disagreement message (the four copies do not agree becomes the ${results.length} copies do not agree). report() and the success line already derived theirs from COPIES.length.

The same-file overlap guard is kept even though it is vacuous today — no two copies share a file any more. Re-declaring such a pair is one edit away, and that guard is what would make such a pair prove it compares two different sections rather than the same one twice.

scripts/check-skill-frame-freshness.mjs — it imported COPIES but its self-test SPELLED .claude/agents/os-dev.md in five fixtures (cases 1, 2, 6, 7 and the two withUnreadableCopy calls in 8 and 9). Those broke on the reduction, exactly as that file's own #8024 rule predicts. They now derive SAMPLE_COPY_ID / SAMPLE_FRAME_FILE from COPIES[0], so the next change to the table re-points the fixtures instead of stranding them.

3. The anti-dormancy scan still refuses a returning copy — measured, both directions

Committed first, then mutated, with an EXIT INT TERM trap restoring absolute paths; each mutation proved on disk by counting the injected marker before reading any verdict; each restore proved by git diff HEAD empty AND by comparing git hash-object against the path's HEAD blob.

Direction A — the removed text returns to the now-undeclared .claude/agents/os-dev.md. REFUSED. Marker occurrences after mutation: 1 (was 0). node scripts/check-skill-frame-sync.mjs exit 1:

✗ check-skill-frame-sync: 1 problem(s).
• .claude/agents/os-dev.md
looks like ANOTHER copy of the decision frame (matched /fixed axes/) but is
not declared in COPIES, so nothing keeps it in sync.

Restore: git diff HEAD empty; worktree blob 968d4fd10a653f013635a8a48bdee25a25efc648 equals the HEAD blob.

Direction B — the same text returns to the still-declared skills/objectstack-pm-dispatch/SKILL.md. NOT REFUSED (exit 0). This is a real, measured limit and it is not new: the scan skips any file present in declaredFiles, so a second undeclared copy inside an already-declared file has always been invisible to it. Restore proved the same way (blob 3cbbed0f501736a0db3a5ecdd9e6cda43d44b434).

What that limit means for this PR, stated rather than implied: what keeps a second published copy from being written is the paste instruction naming the surviving section by heading, plus review — not the scan. Closing it mechanically needs a per-occurrence criterion the gate does not have (the published file legitimately carries two more fixed axes fingerprints, at :534 and in the :539 heading). A comment next to the new self-test case says exactly this, so the next reader does not mistake direction B for a bug introduced here.

A self-test case pins direction A against the REAL path this change undeclared, rather than a synthetic one.

4. Counts (merge base 5ff5f9576 → head)

filebyteslinestokens (ceil bytes/4)
skills/objectstack-pm-dispatch/SKILL.md38807 → 36898 (−1909)753 → 726 (−27)9702 → 9225 (−477), ceiling 9708
.claude/agents/os-dev.md42743 → 41196 (−1547)469 → 450 (−19)— (line-ratcheted), ceiling 469
scripts/check-skill-frame-sync.mjs40961 → 43180 (+2219)960 → 990 (+30)not ratcheted
scripts/check-skill-frame-freshness.mjs46344 → 47480 (+1136)1065 → 1085 (+20)not ratcheted

Whole published package skills/objectstack-pm-dispatch/** (all markdown): 11540 → 11063 tokens, −477.

Two ratcheted files, both shrink; no ceiling row is edited, in either ratchet. The measured token saving is −477, not the −630 the card estimated — the difference is the stop trigger, kept for the reason in section 2. The script growth (+50 lines across two gates) is entirely the derivation constants, the new regression case and the comments recording the measured limit; neither script is under a ratchet.

5. Gates

Union derived at the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands:

dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit ae616fbc91 (/home/user/objectstack-14685-item4).
dispatch-gates: change set derived from git — 4 path(s) vs merge base 5ff5f9576 of 'origin/main' and HEAD

Every exit code captured by redirect before any pipe.

All 32 re-run at the final head ae616fbc91, after the last commit. 31 green, 1 NOT MEASURED, 0 red. The load-bearing ones, quoted from their own verdict lines:

commandexitits own verdict line
pnpm check:skill-frame-sync0✓ check-skill-frame-sync self-test: 13 cases pass, plus 5 dispatch-gates declaration cases. / ✓ check-skill-frame-sync: 2 copies of the decision frame are structurally isomorphic across 2 files
node scripts/check-skill-frame-freshness.mjs0✓ check-skill-frame-freshness: the decision frame in this tree is current with origin/main (fetched just now). (12 self-test cases pass)
node scripts/check-skills-token-ratchet.mjs0✓ check-skills-token-ratchet: skills/objectstack-pm-dispatch/SKILL.md is 9225 tokens (ceiling 9708; headroom 483).
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: .claude/agents/os-dev.md is 450 lines (ceiling 469; headroom 19).
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass.
pnpm --filter @objectstack/spec run check:skill-docs0✓ skills/README.md / ✓ content/docs/ai/skills-reference.mdx — nothing to regenerate
pnpm check:ratchet-remedy-authority0green
pnpm check:pm-skill-id-lint0green
pnpm check:nul-bytes0green (plus a direct grep -naP for control bytes over both edited markdown files: no match)
pnpm check:doc-authoring, check:role-word, check:agent-model-declared, check:agent-test-spelling, check:corpus-claim-drift, check:skill-compatibility, check:skill-identifier-liveness, check:watch-hint-literal, check:entry-guard, check:parse-guard, check:cli-command-ids, check:bash32-floor, check:pnpm-filter-targets, check:pm-governed-merges, check:cross-package-test-inputs0green
node scripts/check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-shard-attestation.mjs, check-whole-set-label-write.mjs, scripts/pm/bare-root-worklist.mjs --self-test0green
pnpm --filter @objectstack/lint run check:doc-formula-expressions0first run answered PREREQUISITE NOT MET (exit 3, @objectstack/formula then @objectstack/lint unbuilt). Prerequisite supplied via the shared verify lock (turbo run build --filter=@objectstack/formula, then --filter=@objectstack/lint) and re-run to a real verdict: ✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1365 TS blocks judged clean
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

One red was found and fixed rather than reported: on the first pass check:pm-skill-ratchet said ✗ check-skill-line-ratchet: .claude/agents/os-dev.md has 1 line(s) over the 120-byte budget: L400 (121B). The ratchet prices a per-line byte budget as well as a line ceiling. The replacement sentence was rewrapped at legal CJK break points, content byte-identical, and the whole union was re-derived and re-run at the resulting head — which is the head every number above comes from.

NOT MEASURED, with reasons:

  • node scripts/check-test-completeness.mjs — exit 3, its own text: "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … the local reading for this gate is NOT MEASURED. ⛔ It is not a red." CI tees the log and passes it; unreachable there.
  • The changeset-derived families (9 of them) do not apply: this PR is skip-changeset.claude/**, skills/** and scripts/** publish nothing from any package.
  • The CI-measured-only family reads the workflow event payload; no local run can produce a verdict for it.
  • The always-runs tail (workflows with no path filter) is CI's, not measured here.

6. Out-of-scope findings — NOT fixed here, for the seat to triage

Each is a claim that this change makes false, in a file this flight does not hold.

  • F1scripts/pm/dispatch-gates.mjs:8090why: '… the dev-agent definition is protocol semantics — every dispatched dev runs under it, and it carries an enforced copy of the decision frame'. The last clause is now false; the mandate itself still holds on the first clause, and the self-test still passes (it asserts the glob exists, not the reason).
  • F2scripts/pm/dispatch-gates.mjs:8095why: '… the published PM skill carries two enforced copies of the decision frame (check:skill-frame-sync COPIES) …'. One copy now.
  • F3scripts/pm/check-skill-line-ratchet.mjs:455// This file is one of the four check:skill-frame-sync COPIES, so a rule that …, in the os-dev.md ceiling ledger. That file is no longer a COPIES entry, which also weakens the stated reason for the +3 raise recorded there. Item 5 holds this file.
  • F4scripts/check-skills-token-ratchet.mjs:334, 345, 359 — three ledger comments stating the published SKILL.md "carries TWO enforced frame copies" and that "the two gate-pinned copies of the decision frame stay in this file". Historically true; now stale as a present-tense claim.
  • F5 the internal lane has no {decision_frame} slot. os-dev.md now expects the frame to arrive in the dispatch prompt, but .claude/skills/pm-dispatch/references/dispatch-runbook.md (派发词构造细则) does not tell the internal PM to paste it, and .claude/skills/pm-dispatch/SKILL.md is explicitly outside this flight. Until that lands, the internal dev's only protection is the stop-and-ask clause added in section 2. This is the one finding with live consequences.
  • F6 the removed os-dev.md block carried a verbatim maintainer quotation — the 2026-09-01 axis-weighting ruling — so this PR deletes a quotation site that item 2's quotation-policy flight may have surveyed. The ruling text survives at .claude/skills/pm-dispatch/SKILL.md:904 and .claude/skills/pm-dispatch/references/decision-analysis.md:50, and the RULE itself survives in both published and internal PM copies of the frame.

7. Posture

Draft, governed surface. No reviewers requested, no ready flip, no enqueue, no auto-merge. Labels skip-changeset and needs:contract-review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1


Generated by Claude Code

Batch-3 ruling, item 4, option B: the four-axis decision frame keeps two
copies (internal-pm and published-pm); the dev-side copies are replaced by
the {decision_frame} placeholder the dispatching PM fills at dispatch time.
- skills/objectstack-pm-dispatch/SKILL.md: the embedded dev-agent template
loses the frame copy (declaring sentence through binding sentence). The
stop trigger stays -- it is the escalation TRIGGER, not the frame, and
rules/dev-template.md carries it nowhere else. The paste instruction now
names the surviving section by heading.
- .claude/agents/os-dev.md: the frame block collapses to one sentence saying
the frame arrives in the dispatch prompt, that every option is analyzed on
its axes and the recommendation justified on them, and that a prompt
without it is a stop-and-ask rather than an invitation to invent axes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…copies
check-skill-frame-sync watched four copies across three files; the batch-3
ruling (item 4, option B) leaves two, both PM-side. The dev-side entries are
removed and every count that was spelled out is now derived from the table.
- COPIES loses internal-dev and published-dev; the header, the COPIES
docblock and the count-disagreement message stop spelling "four".
- The same-file overlap guard stays. It is vacuous today (no two copies share
a file) but re-declaring such a pair is one edit away, and it is the check
that makes such a pair prove it compares two different sections.
- Self-test: the three cases driven through the removed ids are retargeted at
the surviving copies, and a new case pins the reduction itself -- the
dev-side text reappearing in the now-undeclared agent definition is refused
by the anti-dormancy scan. Its measured limit is written next to it: the
scan's population is per FILE, so a second copy pasted back into a file that
is already declared is not refused, and was not before this change either.
- check-skill-frame-freshness imported COPIES but spelled one frame file's
path in five fixtures; those now derive it from the table, which is what
the file's own #8024 rule asks for.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… budget
check:pm-skill-ratchet reads a per-line byte budget as well as a line ceiling;
the first line of the replacement landed at 121B. Rewrapped at legal CJK break
points, content byte-identical.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Sep 3, 2026
@os-litantos-litant added skip-changeset PR has no user-facing published change; bypasses the changeset gate needs:contract-review and removed documentation Improvements or additions to documentation labels Sep 3, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review September 3, 2026 08:57
@os-zhuang
os-zhuang added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 7b19b9eSep 3, 2026
46 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-14685-decision-frame-two-copies branch September 3, 2026 11:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

docs(pm-dispatch): keep two copies of the decision frame, not four - #14862

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies
Sep 3, 2026
Merged

docs(pm-dispatch): keep two copies of the decision frame, not four#14862
os-zhuang merged 4 commits into
mainfrom
claude/issue-14685-decision-frame-two-copies

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Part of #14685
Part of #13597

Item 4 of decision batch 3, ruled B. From the ruling comment (5520452691), verbatim:

B — the four-axis decision frame keeps two copies (internal-pm and published-pm); the dev-side copies are replaced by a {decision_frame} placeholder the dispatching PM fills at dispatch time; check:skill-frame-sync watches two sites. Confidence gap carried: whoever executes verifies that the published-dev block has no reader other than the placeholder path before removing it.


1. The confidence gap, discharged BEFORE any edit

The ruling made removal conditional on a reader census. Run at the merge base 5ff5f9576, over .claude/ skills/ scripts/ .github/ packages/ docs/, for four things: the published-dev start marker (Analyze every option on ... fixed axes:), the literal published-dev, {decision_frame}, and the internal-dev start marker. Every hit, and what it is:

sitewhat it isdepends on the published-dev BLOCK?
skills/objectstack-pm-dispatch/SKILL.md:633-662the block itselfit IS the block
skills/objectstack-pm-dispatch/SKILL.md:623"with the block below in place of its {decision_frame} line"YES — the placeholder path, and the only one
skills/objectstack-pm-dispatch/rules/dev-template.md:99the {decision_frame} line the instruction above fillsvia that instruction only
scripts/check-skill-frame-sync.mjs:136-143the published-dev COPIES entrythe gate that watches it
scripts/check-skill-frame-sync.mjs:714,719,723,724,727self-test fixtures addressing the published-dev idthe gate's own fixtures
scripts/check-skill-frame-freshness.mjsimports COPIES / AXIS_MAP wholesale; five fixtures spelled .claude/agents/os-dev.mdreads the TABLE, never the block text
scripts/pm/dispatch-gates.mjs:15382-15397spawned import reading the FILE list out of COPIES for the tier mandatereads FILES, not copies
scripts/pm/check-dispatch-gates.mjs:202FRAME_MODULE watch-hint constantnames the module path only
scripts/check-skills-token-ratchet.mjs:334,345,359ceiling-ledger prosecomments, no behaviour
scripts/pm/check-skill-line-ratchet.mjs:455ceiling-ledger prosecomments, no behaviour

No test, generator, workflow, doc or fixture quotes the block by fingerprint. The gap resolves in favour of removal: the placeholder path and the two frame gates are the whole reader set, and both gates read the COPIES table rather than the prose.

The same census over the internal-dev start marker found exactly one non-gate hit — the block itself at .claude/agents/os-dev.md:400.

2. What changed, site by site

skills/objectstack-pm-dispatch/SKILL.md — the embedded dev template loses the frame copy: the declaring sentence through the binding sentence (old :633-662), which is precisely the span the gate's start and binding anchors delimit.

The stop trigger above it (old :627-632, "When to STOP instead of coding …") stays, and that is a judgement call worth naming. It is not part of the frame — the gate's anchors do not reach it — and it is the escalation TRIGGER rather than the analysis. rules/dev-template.md carries it nowhere else (grepped: the template mentions needs_decision only at :34 inside the contract-first rule and at :112 in the report schema), so deleting it with the frame would have removed the dev's instruction to stop at all, which the ruling did not ask for.

The paste instruction now names the surviving copy by its heading:

Paste rules/dev-template.md verbatim into every dispatch prompt; the PM fills
its {…} placeholders. {decision_frame} takes the stop trigger below, followed
by the whole `#### The four-axis decision frame (binding)` section above: the
frame has ONE copy in this skill and the PM pastes it at dispatch time, so no
dev-side copy can drift from it.

.claude/agents/os-dev.md — the frame block (old :400-421, declaring sentence through the binding sentence, which ran into the same paragraph as the unrelated blocked rule) collapses to one sentence. The blocked rule is preserved byte-for-byte. Neither :232 nor :303 is touched (PR #14808 holds them); the diff is one hunk at :400.

Three judgement calls in that one sentence:

  1. It says the frame comes from the PM's own copy, not specifically from rules/dev-template.md, because that template is the PUBLISHED skill's artifact. The internal PM composes dispatch prompts per .claude/skills/pm-dispatch/references/dispatch-runbook.md and has no {decision_frame} slot (grep: the token exists only under skills/objectstack-pm-dispatch/). The published slot is named parenthetically, as the brief asked, without claiming the internal lane uses it. See finding F5.
  2. It adds "if the dispatch prompt did not carry it, stop and ask; do not invent your own axes." os-dev.md's own doctrine is that standard clauses live in that file and prompt silence is normal — so moving the frame OUT of it opens a hole that a silent prompt would otherwise walk through.
  3. It is written in Chinese and deliberately avoids the phrases fixed axes, 固定评估轴, 条评估轴 and any WORD-axis decision frame form. Those are FINGERPRINTS in the sync gate's anti-dormancy scan, whose population is per file: os-dev.md stops being a declared file with this change, so any of those phrasings would have turned the gate red. Verified after the edit — grep -nP for all four patterns over os-dev.md returns nothing.

scripts/check-skill-frame-sync.mjsCOPIES 4 → 2.

- internal-pm .claude/skills/pm-dispatch/SKILL.md KEPT
- internal-dev .claude/agents/os-dev.md REMOVED
- published-pm skills/objectstack-pm-dispatch/SKILL.md KEPT
- published-dev skills/objectstack-pm-dispatch/SKILL.md REMOVED

Every spelled count is now derived: the header ("FOUR hand-written copies across three files"), the COPIES docblock, the axisEntryStarts docblock, the freshness-gate cross-reference, and the count-disagreement message (the four copies do not agree becomes the ${results.length} copies do not agree). report() and the success line already derived theirs from COPIES.length.

The same-file overlap guard is kept even though it is vacuous today — no two copies share a file any more. Re-declaring such a pair is one edit away, and that guard is what would make such a pair prove it compares two different sections rather than the same one twice.

scripts/check-skill-frame-freshness.mjs — it imported COPIES but its self-test SPELLED .claude/agents/os-dev.md in five fixtures (cases 1, 2, 6, 7 and the two withUnreadableCopy calls in 8 and 9). Those broke on the reduction, exactly as that file's own #8024 rule predicts. They now derive SAMPLE_COPY_ID / SAMPLE_FRAME_FILE from COPIES[0], so the next change to the table re-points the fixtures instead of stranding them.

3. The anti-dormancy scan still refuses a returning copy — measured, both directions

Committed first, then mutated, with an EXIT INT TERM trap restoring absolute paths; each mutation proved on disk by counting the injected marker before reading any verdict; each restore proved by git diff HEAD empty AND by comparing git hash-object against the path's HEAD blob.

Direction A — the removed text returns to the now-undeclared .claude/agents/os-dev.md. REFUSED. Marker occurrences after mutation: 1 (was 0). node scripts/check-skill-frame-sync.mjs exit 1:

✗ check-skill-frame-sync: 1 problem(s).
• .claude/agents/os-dev.md
looks like ANOTHER copy of the decision frame (matched /fixed axes/) but is
not declared in COPIES, so nothing keeps it in sync.

Restore: git diff HEAD empty; worktree blob 968d4fd10a653f013635a8a48bdee25a25efc648 equals the HEAD blob.

Direction B — the same text returns to the still-declared skills/objectstack-pm-dispatch/SKILL.md. NOT REFUSED (exit 0). This is a real, measured limit and it is not new: the scan skips any file present in declaredFiles, so a second undeclared copy inside an already-declared file has always been invisible to it. Restore proved the same way (blob 3cbbed0f501736a0db3a5ecdd9e6cda43d44b434).

What that limit means for this PR, stated rather than implied: what keeps a second published copy from being written is the paste instruction naming the surviving section by heading, plus review — not the scan. Closing it mechanically needs a per-occurrence criterion the gate does not have (the published file legitimately carries two more fixed axes fingerprints, at :534 and in the :539 heading). A comment next to the new self-test case says exactly this, so the next reader does not mistake direction B for a bug introduced here.

A self-test case pins direction A against the REAL path this change undeclared, rather than a synthetic one.

4. Counts (merge base 5ff5f9576 → head)

filebyteslinestokens (ceil bytes/4)
skills/objectstack-pm-dispatch/SKILL.md38807 → 36898 (−1909)753 → 726 (−27)9702 → 9225 (−477), ceiling 9708
.claude/agents/os-dev.md42743 → 41196 (−1547)469 → 450 (−19)— (line-ratcheted), ceiling 469
scripts/check-skill-frame-sync.mjs40961 → 43180 (+2219)960 → 990 (+30)not ratcheted
scripts/check-skill-frame-freshness.mjs46344 → 47480 (+1136)1065 → 1085 (+20)not ratcheted

Whole published package skills/objectstack-pm-dispatch/** (all markdown): 11540 → 11063 tokens, −477.

Two ratcheted files, both shrink; no ceiling row is edited, in either ratchet. The measured token saving is −477, not the −630 the card estimated — the difference is the stop trigger, kept for the reason in section 2. The script growth (+50 lines across two gates) is entirely the derivation constants, the new regression case and the comments recording the measured limit; neither script is under a ratchet.

5. Gates

Union derived at the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands:

dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit ae616fbc91 (/home/user/objectstack-14685-item4).
dispatch-gates: change set derived from git — 4 path(s) vs merge base 5ff5f9576 of 'origin/main' and HEAD

Every exit code captured by redirect before any pipe.

All 32 re-run at the final head ae616fbc91, after the last commit. 31 green, 1 NOT MEASURED, 0 red. The load-bearing ones, quoted from their own verdict lines:

commandexitits own verdict line
pnpm check:skill-frame-sync0✓ check-skill-frame-sync self-test: 13 cases pass, plus 5 dispatch-gates declaration cases. / ✓ check-skill-frame-sync: 2 copies of the decision frame are structurally isomorphic across 2 files
node scripts/check-skill-frame-freshness.mjs0✓ check-skill-frame-freshness: the decision frame in this tree is current with origin/main (fetched just now). (12 self-test cases pass)
node scripts/check-skills-token-ratchet.mjs0✓ check-skills-token-ratchet: skills/objectstack-pm-dispatch/SKILL.md is 9225 tokens (ceiling 9708; headroom 483).
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: .claude/agents/os-dev.md is 450 lines (ceiling 469; headroom 19).
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass.
pnpm --filter @objectstack/spec run check:skill-docs0✓ skills/README.md / ✓ content/docs/ai/skills-reference.mdx — nothing to regenerate
pnpm check:ratchet-remedy-authority0green
pnpm check:pm-skill-id-lint0green
pnpm check:nul-bytes0green (plus a direct grep -naP for control bytes over both edited markdown files: no match)
pnpm check:doc-authoring, check:role-word, check:agent-model-declared, check:agent-test-spelling, check:corpus-claim-drift, check:skill-compatibility, check:skill-identifier-liveness, check:watch-hint-literal, check:entry-guard, check:parse-guard, check:cli-command-ids, check:bash32-floor, check:pnpm-filter-targets, check:pm-governed-merges, check:cross-package-test-inputs0green
node scripts/check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-shard-attestation.mjs, check-whole-set-label-write.mjs, scripts/pm/bare-root-worklist.mjs --self-test0green
pnpm --filter @objectstack/lint run check:doc-formula-expressions0first run answered PREREQUISITE NOT MET (exit 3, @objectstack/formula then @objectstack/lint unbuilt). Prerequisite supplied via the shared verify lock (turbo run build --filter=@objectstack/formula, then --filter=@objectstack/lint) and re-run to a real verdict: ✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 426 files / 1365 TS blocks judged clean
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

One red was found and fixed rather than reported: on the first pass check:pm-skill-ratchet said ✗ check-skill-line-ratchet: .claude/agents/os-dev.md has 1 line(s) over the 120-byte budget: L400 (121B). The ratchet prices a per-line byte budget as well as a line ceiling. The replacement sentence was rewrapped at legal CJK break points, content byte-identical, and the whole union was re-derived and re-run at the resulting head — which is the head every number above comes from.

NOT MEASURED, with reasons:

  • node scripts/check-test-completeness.mjs — exit 3, its own text: "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named … the local reading for this gate is NOT MEASURED. ⛔ It is not a red." CI tees the log and passes it; unreachable there.
  • The changeset-derived families (9 of them) do not apply: this PR is skip-changeset.claude/**, skills/** and scripts/** publish nothing from any package.
  • The CI-measured-only family reads the workflow event payload; no local run can produce a verdict for it.
  • The always-runs tail (workflows with no path filter) is CI's, not measured here.

6. Out-of-scope findings — NOT fixed here, for the seat to triage

Each is a claim that this change makes false, in a file this flight does not hold.

  • F1scripts/pm/dispatch-gates.mjs:8090why: '… the dev-agent definition is protocol semantics — every dispatched dev runs under it, and it carries an enforced copy of the decision frame'. The last clause is now false; the mandate itself still holds on the first clause, and the self-test still passes (it asserts the glob exists, not the reason).
  • F2scripts/pm/dispatch-gates.mjs:8095why: '… the published PM skill carries two enforced copies of the decision frame (check:skill-frame-sync COPIES) …'. One copy now.
  • F3scripts/pm/check-skill-line-ratchet.mjs:455// This file is one of the four check:skill-frame-sync COPIES, so a rule that …, in the os-dev.md ceiling ledger. That file is no longer a COPIES entry, which also weakens the stated reason for the +3 raise recorded there. Item 5 holds this file.
  • F4scripts/check-skills-token-ratchet.mjs:334, 345, 359 — three ledger comments stating the published SKILL.md "carries TWO enforced frame copies" and that "the two gate-pinned copies of the decision frame stay in this file". Historically true; now stale as a present-tense claim.
  • F5 the internal lane has no {decision_frame} slot. os-dev.md now expects the frame to arrive in the dispatch prompt, but .claude/skills/pm-dispatch/references/dispatch-runbook.md (派发词构造细则) does not tell the internal PM to paste it, and .claude/skills/pm-dispatch/SKILL.md is explicitly outside this flight. Until that lands, the internal dev's only protection is the stop-and-ask clause added in section 2. This is the one finding with live consequences.
  • F6 the removed os-dev.md block carried a verbatim maintainer quotation — the 2026-09-01 axis-weighting ruling — so this PR deletes a quotation site that item 2's quotation-policy flight may have surveyed. The ruling text survives at .claude/skills/pm-dispatch/SKILL.md:904 and .claude/skills/pm-dispatch/references/decision-analysis.md:50, and the RULE itself survives in both published and internal PM copies of the frame.

7. Posture

Draft, governed surface. No reviewers requested, no ready flip, no enqueue, no auto-merge. Labels skip-changeset and needs:contract-review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1


Generated by Claude Code

Batch-3 ruling, item 4, option B: the four-axis decision frame keeps two
copies (internal-pm and published-pm); the dev-side copies are replaced by
the {decision_frame} placeholder the dispatching PM fills at dispatch time.
- skills/objectstack-pm-dispatch/SKILL.md: the embedded dev-agent template
loses the frame copy (declaring sentence through binding sentence). The
stop trigger stays -- it is the escalation TRIGGER, not the frame, and
rules/dev-template.md carries it nowhere else. The paste instruction now
names the surviving section by heading.
- .claude/agents/os-dev.md: the frame block collapses to one sentence saying
the frame arrives in the dispatch prompt, that every option is analyzed on
its axes and the recommendation justified on them, and that a prompt
without it is a stop-and-ask rather than an invitation to invent axes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…copies
check-skill-frame-sync watched four copies across three files; the batch-3
ruling (item 4, option B) leaves two, both PM-side. The dev-side entries are
removed and every count that was spelled out is now derived from the table.
- COPIES loses internal-dev and published-dev; the header, the COPIES
docblock and the count-disagreement message stop spelling "four".
- The same-file overlap guard stays. It is vacuous today (no two copies share
a file) but re-declaring such a pair is one edit away, and it is the check
that makes such a pair prove it compares two different sections.
- Self-test: the three cases driven through the removed ids are retargeted at
the surviving copies, and a new case pins the reduction itself -- the
dev-side text reappearing in the now-undeclared agent definition is refused
by the anti-dormancy scan. Its measured limit is written next to it: the
scan's population is per FILE, so a second copy pasted back into a file that
is already declared is not refused, and was not before this change either.
- check-skill-frame-freshness imported COPIES but spelled one frame file's
path in five fixtures; those now derive it from the table, which is what
the file's own #8024 rule asks for.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
… budget
check:pm-skill-ratchet reads a per-line byte budget as well as a line ceiling;
the first line of the replacement landed at 121B. Rewrapped at legal CJK break
points, content byte-identical.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Sep 3, 2026
@os-litantos-litant added skip-changeset PR has no user-facing published change; bypasses the changeset gate needs:contract-review and removed documentation Improvements or additions to documentation labels Sep 3, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review September 3, 2026 08:57
@os-zhuang
os-zhuang added this pull request to the merge queueSep 3, 2026
Merged via the queue into main with commit 7b19b9eSep 3, 2026
46 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-14685-decision-frame-two-copies branch September 3, 2026 11:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-zhuang@claude