From cc7d32ee1b88f7ce35378ad4e404c6303fc94831 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:43:26 +0000 Subject: [PATCH 1/3] docs(pm-dispatch): drop the two dev-side copies of the decision frame 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 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- .claude/agents/os-dev.md | 27 +++--------------- skills/objectstack-pm-dispatch/SKILL.md | 37 ++++--------------------- 2 files changed, 9 insertions(+), 55 deletions(-) diff --git a/.claude/agents/os-dev.md b/.claude/agents/os-dev.md index 42fc96f5d3..968d4fd10a 100644 --- a/.claude/agents/os-dev.md +++ b/.claude/agents/os-dev.md @@ -397,29 +397,10 @@ issue 对塑造公开契约的某个决定欠规格 —— spec/Zod schema、API —— 或两种读法通向两种架构时:不猜,不写投机代码。返回 `status: "needs_decision"`,把每个问 题连同选项、成本与你的推荐写进 `open_questions`。 -**Analyze every option on four fixed axes — this framing is the core of the escalation, -not decoration:** - -- **Real business need**(实际业务需求)— 该方案服务的是**真实存在的业务场景**,还是投 - 机性能力面?证据必须**实测** —— 谁在写这个键、谁在读这个能力、示例应用与真实部署 - 怎么 - 用;「读起来像有用」不作数。这条轴会改变结论,不是陪衬。 -- **Long-term soundness for THIS project**(项目长远合理性)— 哪个方案符合北极星方向与 - 可持续架构(no workarounds、contract-first)—— 补丁式选项的长期代价要明说。 -- **Making AI-written code — especially AI-authored metadata apps — hard to get wrong** - (防 AI 写代码犯错,尤其是 AI 编写的元数据 app)— 优先选在编写时点就结构性防错的方案 - (严格 schema、publish 时响亮拒绝的校验、declared = enforced),而非消费端宽容 —— 宽 - 容的消费端恰是 AI 生成错误藏身并扩散的地方。 -- **Startup scope discipline**(创业阶段不扩散需求)— **创业阶段聚焦原则**(维护者 - 2026-08-04:这是创业项目,核心能力优先):能力扩张默认从紧,无拉动的声明面按 - implementation-first 处置,已发布零消费的能力不因沉没成本获得豁免。 - -Your recommendation must be justified on all four axes;长远合理性权重恒 ≥50%(2026-09-01 -裁「四维分析中,长期合理应该权重最高,至少50%」):推荐以它领起,其余各轴合起来投不翻 -它,按本义读(缩小而非扩大特例/契约增生),⛔ 不为投机扩张背书;四轴冲突时推荐按拉动排 -序:实测拉动→长远,零拉动→不扩散,防错破平,安全与难逆恒人工(权重与序都只排推荐,⛔ -不授权、不移人工地板);权衡如实呈现交维护者拍板。`main` 在你脚下碎了、依赖未合并、CI -基础设施故障⇒`blocked`(附证据),重试到足以排除你的改动。 +升级分析的四轴决策框架由派发词携带 —— 派发的 PM 从自己那份副本填入(已发布模板里是 +`rules/dev-template.md` 的 `{decision_frame}` 槽位):每个方案逐轴分析,推荐也按那些轴 +给出理由;派发词没带,停下向 PM 索取,⛔ 不自拟一套轴。`main` 在你脚下碎了、依赖未合 +并、CI 基础设施故障⇒`blocked`(附证据),重试到足以排除你的改动。 ## 终报消息 —— 恰好这段 JSON,不带任何环绕散文 diff --git a/skills/objectstack-pm-dispatch/SKILL.md b/skills/objectstack-pm-dispatch/SKILL.md index 80355fb2e2..3cbbed0f50 100644 --- a/skills/objectstack-pm-dispatch/SKILL.md +++ b/skills/objectstack-pm-dispatch/SKILL.md @@ -620,8 +620,11 @@ Stop the loop and report when any of these hits: ## The developer-agent operating template Paste [`rules/dev-template.md`](./rules/dev-template.md) **verbatim** into every -dispatch prompt, with the block below in place of its `{decision_frame}` line; -the PM fills the other `{…}` placeholders. +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. ```text When to STOP instead of coding. If the issue underspecifies a decision that @@ -630,36 +633,6 @@ or two readings of the issue lead to different architectures: make no guess, write no speculative code. Return status "needs_decision" with each question, the options, their costs, and your recommendation in open_questions. A wrong guess shipped is far more expensive than a round-trip to the maintainer. -Analyze every option on four fixed axes: -- Real business need — does the option serve a business scenario that ACTUALLY - EXISTS, or a speculative capability surface? Ask this first. The evidence must - be MEASURED, not inferred: who writes this key, who reads this capability, how - the project's example apps and real deployments use it today. "It reads like it - would be useful" does not count. -- Long-term architectural soundness for THIS project — which option matches a - sustainable architecture (no workarounds, contract-first), not which is - cheapest today. Name the long-term cost of any patch-style option. -- Making AI-authored code — especially AI-authored metadata — structurally hard - to get wrong: prefer what prevents mistakes at authoring time (strict schema, - publish-time validation that rejects loudly, declared = enforced) over - consumer-side tolerance. Lenient consumers are where AI-generated errors hide - and multiply. -- Startup scope discipline — do not grow the declared surface: a - shipped-but-unconsumed capability gets no sunk-cost exemption. A declared - surface with no pull is handled implementation-first — narrow the declaration - until declared = enforced (retire it, or park the vocabulary until the - implementation arrives) rather than building implementation to justify the - declaration. How tight the default is comes from the project's conventions - file, not from this template. -Justify your recommendation on all four axes; if they conflict, present the -trade-off and let the maintainer decide, ranking the recommendation pull-first -(real pull → long-term shape; zero pull → non-proliferation; the AI-safety -axis breaks ties; security/destructive → always the maintainer); the ranking -orders recommendations only — a split still escalates. Long-term soundness -carries the highest weight, at least 50%: lead with it, the others together -cannot outvote it, and read it as shrinking special-cases and contract -accretion rather than as licence to expand speculatively; weight ranks -recommendations only, never authority. ``` **Report contract.** The JSON in the template's final-message block is the From a73e3feb38de2caa1218aa9c0578407745c78ce6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:47:09 +0000 Subject: [PATCH 2/3] tooling(pm): narrow the frame-sync COPIES table to the two surviving 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 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- scripts/check-skill-frame-freshness.mjs | 48 ++++++--- scripts/check-skill-frame-sync.mjs | 136 +++++++++++++++--------- 2 files changed, 117 insertions(+), 67 deletions(-) diff --git a/scripts/check-skill-frame-freshness.mjs b/scripts/check-skill-frame-freshness.mjs index 1203988631..8941bcf7b5 100644 --- a/scripts/check-skill-frame-freshness.mjs +++ b/scripts/check-skill-frame-freshness.mjs @@ -12,12 +12,12 @@ // ruled explicitly that they stay two scripts, and the reason is worth stating // because "just add it to the other script" is the obvious wrong move: // -// check:skill-frame-sync "are the FOUR COPIES IN THIS TREE isomorphic?" +// check:skill-frame-sync "are THIS TREE's COPIES isomorphic to each other?" // → compares copy against copy, one tree. // check:skill-frame-freshness "is THIS TREE's frame current with origin/main?" // → compares this tree against a remote ref. // -// They are independent: a tree that is 173 commits behind, whose four copies are +// They are independent: a tree that is 173 commits behind, whose copies are // CONSISTENTLY the old two-axis frame, is **green** on the sync gate — correctly // so, the copies really are isomorphic — while being exactly the defect #5866 // reports. The self-test pins that independence rather than asserting it: the @@ -37,10 +37,15 @@ // hypothetical, and the channel is still live — at the time this gate was // written the shared checkout was 59 commits behind on a different branch. // -// The frame lives in three files / four copies; COPIES and AXIS_MAP are imported -// from check-skill-frame-sync.mjs so that "the frame's structure" has exactly ONE -// definition. Forking those anchors into this script would reproduce, in the -// gates themselves, the hand-copied-text disease they exist to police. +// COPIES and AXIS_MAP are imported from check-skill-frame-sync.mjs so that "the +// frame's structure" has exactly ONE definition — how many copies there are, and +// which files hold them, is that table's answer and never a second list here. +// Forking those anchors into this script would reproduce, in the gates +// themselves, the hand-copied-text disease they exist to police. (It was four +// copies in three files until the 2026-09-03 batch-3 ruling, item 4 option B, +// dropped the two dev-side copies; nothing in this file had to move for that +// except the self-test fixtures below, which used to SPELL one of the dropped +// files instead of deriving it.) // // STRUCTURE, NEVER BYTES // ---------------------- @@ -131,6 +136,21 @@ const DEFAULT_FETCH_TIMEOUT_MS = 20_000; const FRAME_FILES = [...new Set(COPIES.map((c) => c.file))]; +/** + * The copy the self-test's file-level fixtures operate on — the one they delete, + * withhold from a ref, or make unparseable. DERIVED, never spelled: these cases + * named `.claude/agents/os-dev.md` until the 2026-09-03 reduction stopped it + * being a frame file, and a spelled path would have gone on asserting about a + * document the frame no longer lives in. Same rule as everything else in this + * file (#8024): locate the fixture the way the gate locates the real thing. + * + * First entry, so the choice is deterministic and non-empty for any COPIES table + * the sync gate can start up with (an empty one fails that gate first). + */ +const SAMPLE_COPY_ID = COPIES[0].id; +const SAMPLE_FRAME_FILE = COPIES[0].file; +const SAMPLE_FRAME_FILE_RX = new RegExp(SAMPLE_FRAME_FILE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + // --------------------------------------------------------------------------- // git plumbing // --------------------------------------------------------------------------- @@ -827,13 +847,13 @@ function selfTest() { label: 'stale tree + authoritative ref → ERROR naming the stale files (the #5866 shape)', run: () => evaluate({ root: dir, ref: current }), expect: 'error', - wants: [/STRUCTURALLY BEHIND/, /\.claude\/agents\/os-dev\.md/, /3 axes: long-term-soundness/, /4 axes: business-need/, new RegExp(REMEDY.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))], + wants: [/STRUCTURALLY BEHIND/, SAMPLE_FRAME_FILE_RX, /3 axes: long-term-soundness/, /4 axes: business-need/, new RegExp(REMEDY.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))], }); cases.push({ label: 'the SAME stale tree, fetch impossible → degrades to WARN, exit 0, same diagnosis', run: () => evaluate({ root: dir }), expect: 'warn', - wants: [/STRUCTURALLY BEHIND/, /\.claude\/agents\/os-dev\.md/], + wants: [/STRUCTURALLY BEHIND/, SAMPLE_FRAME_FILE_RX], alsoAssert: (v) => (v.clamped ? null : 'expected the verdict to be marked as clamped'), }); // The independence proof: the sync gate is GREEN on this very fixture. @@ -894,14 +914,14 @@ function selfTest() { // --- 6: a framework file main has and we do not -------------------------- { const { dir, b: current } = linear('missing-here', real, real); - git(['rm', '-q', '.claude/agents/os-dev.md'], { cwd: dir }); + git(['rm', '-q', SAMPLE_FRAME_FILE], { cwd: dir }); const without = commitAll(dir, 'drop a framework file'); setOriginMain(dir, current); cases.push({ label: 'a framework file exists on the ref but not in the tree → ERROR', run: () => evaluate({ root: dir, ref: current }), expect: 'error', - wants: [/MISSING from this working tree/, /os-dev\.md/], + wants: [/MISSING from this working tree/, SAMPLE_FRAME_FILE_RX], alsoAssert: () => (without ? null : 'fixture did not commit'), }); } @@ -910,9 +930,9 @@ function selfTest() { { const dir = makeRepo('missing-there'); temps.push(dir); - const partial = new Map([...real].filter(([f]) => f !== '.claude/agents/os-dev.md')); + const partial = new Map([...real].filter(([f]) => f !== SAMPLE_FRAME_FILE)); writeFiles(dir, partial); - const refSha = commitAll(dir, 'main without the dev-agent definition'); + const refSha = commitAll(dir, 'main without one framework file'); writeFiles(dir, real); commitAll(dir, 'tree adds it'); setOriginMain(dir, refSha); @@ -926,7 +946,7 @@ function selfTest() { // --- 8: the frame moved on main, our anchors predate it ------------------ { - const moved = withUnreadableCopy(real, 'internal-dev'); + const moved = withUnreadableCopy(real, SAMPLE_COPY_ID); const { dir, b: current } = linear('anchors', real, moved); git(['checkout', '-q', git(['rev-parse', 'HEAD~1'], { cwd: dir }).stdout.trim()], { cwd: dir }); setOriginMain(dir, current); @@ -940,7 +960,7 @@ function selfTest() { // --- 9: our own tree does not parse -------------------------------------- { - const broken = withUnreadableCopy(real, 'internal-dev'); + const broken = withUnreadableCopy(real, SAMPLE_COPY_ID); const { dir, b: current } = linear('broken-here', broken, real); git(['checkout', '-q', git(['rev-parse', 'HEAD~1'], { cwd: dir }).stdout.trim()], { cwd: dir }); setOriginMain(dir, current); diff --git a/scripts/check-skill-frame-sync.mjs b/scripts/check-skill-frame-sync.mjs index 788daab0e8..7025f97e80 100644 --- a/scripts/check-skill-frame-sync.mjs +++ b/scripts/check-skill-frame-sync.mjs @@ -1,13 +1,22 @@ #!/usr/bin/env node // check-skill-frame-sync — isomorphism gate for the escalation decision frame, -// which exists as FOUR hand-written copies across three files (#5798). +// which exists as TWO hand-written copies across two files (#5798, narrowed to +// two by the 2026-09-03 batch-3 ruling, item 4 option B — see COPIES below). // // The frame ("analyze every option on N fixed axes") is binding text for both // the PM agent and the dev agent, and it is written twice over: once for this // repo's internal agent protocol (`.claude/**`, never published) and once for // third-party projects (`skills/objectstack-pm-dispatch/SKILL.md`, which ships -// verbatim via `npx skills add objectstack-ai/objectstack/skills` and embeds its -// own dev-agent template). Nothing compared them. +// verbatim via `npx skills add objectstack-ai/objectstack/skills`). Nothing +// compared them. +// +// Both surviving copies are PM-side. The two dev-side copies this gate used to +// watch — the internal agent definition's block and the published skill's +// embedded dev template — are gone: a dispatching PM now pastes its own copy +// into the dev prompt's `{decision_frame}` slot, so the dev reads the frame the +// PM is holding rather than a third and fourth transcription of it. That is a +// REDUCTION IN COPIES, not a reduction in coverage — the anti-dormancy scan +// below is what keeps a dev-side copy from quietly coming back. // // The drift is not hypothetical. #5130 (2026-08-04) widened the internal frame // from two axes to three; the published mirror was untouched and stayed at two @@ -24,7 +33,7 @@ // frontmatter and generated indexes, never the body. // // WHAT IS COMPARED — structure, deliberately NOT wording: -// • axis COUNT (per copy, and equal across all four), +// • axis COUNT (per copy, and equal across every copy), // • axis NAME SEQUENCE, mapped through the explicit AXIS_MAP below (internal // Chinese ⇄ published English), order significant, // • the BINDING SENTENCE is in place in every copy, @@ -96,14 +105,23 @@ function soft(template) { } /** - * The four copies. `start` is the sentence that declares the frame, `binding` is + * The two copies. `start` is the sentence that declares the frame, `binding` is * the sentence that makes it binding; the axis entries are whatever lies between * them. Both anchors must match exactly once per file — an ambiguous anchor is * reported rather than silently resolved to the first hit. * + * Why two and not four (2026-09-03 batch-3 ruling, item 4 option B): the frame + * had a PM copy and a dev copy on each side of the publish boundary, and the two + * dev copies were transcriptions of the PM copy beside them. They are now filled + * in at dispatch time from the PM copy instead — the published dev template's + * `{decision_frame}` line, and the dispatch prompt the internal PM writes — so + * the text a dev reads is the text its dispatcher is holding, and cannot be a + * copy that stopped being updated. Adding a copy back is a COPIES edit, which + * the anti-dormancy scan below forces rather than merely invites. + * * EXPORTED for scripts/check-skill-frame-freshness.mjs (#5866), which asks the * OTHER invariant about the same documents — "is this working tree's copy of the - * frame still current with origin/main?" as opposed to this gate's "are the four + * frame still current with origin/main?" as opposed to this gate's "are the * copies in one tree isomorphic?". Two invariants, two scripts (the #5866 triage * ruled explicitly against merging them), but exactly ONE definition of what * "the frame's structure" is — forking these anchors into a second script would @@ -118,14 +136,6 @@ export const COPIES = [ start: soft('**每个方案必须沿%N%固定评估轴'), binding: soft('推荐意见必须基于这%N%轴给出理由'), }, - { - id: 'internal-dev', - file: '.claude/agents/os-dev.md', - lang: 'en', - what: 'internal dev-agent definition, "When to stop instead of code"', - start: soft('**Analyze every option on %N% fixed axes — this framing is the core of the escalation'), - binding: soft('recommendation must be justified on %Q% axes'), - }, { id: 'published-pm', file: 'skills/objectstack-pm-dispatch/SKILL.md', @@ -134,14 +144,6 @@ export const COPIES = [ start: soft('**and analyze every option on the %N% fixed axes below.**'), binding: soft('recommendation must be justified on %Q% axes'), }, - { - id: 'published-dev', - file: 'skills/objectstack-pm-dispatch/SKILL.md', - lang: 'en', - what: 'published PM skill, the EMBEDDED dev-agent template', - start: soft('Analyze every option on %N% fixed axes:'), - binding: soft('Justify your recommendation on %Q% axes'), - }, ]; /** @@ -195,8 +197,8 @@ function toCount(word) { } /** - * An axis entry starts either at a list bullet (three of the four copies) or at - * an `**Axis ① — …**` paragraph (the published PM copy). Both shapes are accepted + * An axis entry starts either at a list bullet (the internal PM copy) or at an + * `**Axis ① — …**` paragraph (the published PM copy). Both shapes are accepted * everywhere, so reformatting one copy into the other's shape does not blind the * gate. A blank line closes an entry; wrapped continuation lines belong to it. */ @@ -499,8 +501,11 @@ export function runAllChecks(copies, axisMap = AXIS_MAP, scanFiles = null) { if (r) results.push(r); } - // Two copies share the published file; sections must not overlap, or an anchor - // is reading the wrong instance and the comparison is vacuous. + // Copies MAY share a file (two did until the 2026-09-03 reduction); whenever + // they do, their sections must not overlap, or an anchor is reading the wrong + // instance and the comparison is vacuous. Kept as a live guard rather than + // deleted with the same-file pair: re-declaring one is a COPIES edit away, and + // this is the check that makes such a pair prove it compares two things. for (const a of results) { for (const b of results) { if (a === b || a.copy.file !== b.copy.file) continue; @@ -521,7 +526,7 @@ export function runAllChecks(copies, axisMap = AXIS_MAP, scanFiles = null) { const counts = new Set(results.map((r) => r.declared)); if (counts.size > 1) { problems.push( - `the four copies do not agree on the NUMBER of axes — this is the #5130 drift:\n` + + `the ${results.length} copies do not agree on the NUMBER of axes — this is the #5130 drift:\n` + results.map((r) => ` ${r.declared} axes ${r.copy.id} (${r.copy.file})`).join('\n') + `\n Every copy of the frame must be updated in the same PR, including the ` + `published mirror in skills/ that ships to third parties.`, @@ -648,7 +653,7 @@ function mutate(copies, id, from, to) { `self-test fixture drifted: mutation target not found in ${c.id} (${c.file}): ${from}`, ); } - // Both published copies share one file; keep the sibling's text in step. + // If two copies ever share one file, keep the sibling's text in step. return { ...c, text }; }).map((c, _i, all) => { const mutated = all.find((x) => x.id === id); @@ -674,7 +679,7 @@ function selfTest() { const cases = [ { - label: 'baseline: the four real copies are isomorphic', + label: 'baseline: the two real copies are isomorphic', copies: () => base, expect: 'green', }, @@ -682,27 +687,27 @@ function selfTest() { // The #5130 shape, incoherent half: bullets changed, prose count not. label: 'one copy loses an axis, its declared count unchanged → red (count vs entries)', copies: () => mutate( - base, 'internal-dev', - '- **Long-term soundness for THIS project**', - ' **Long-term soundness for THIS project**', + base, 'internal-pm', + '- **项目长远合理性**', + ' **项目长远合理性**', ), expect: 'red', - wants: [/internal-dev/, /declares 4 axes but 3 axis entries/], + wants: [/internal-pm/, /declares 4 axes but 3 axis entries/], }, { // The #5130 shape, coherent: one side becomes a consistent three-axis frame. label: 'one copy coherently rewritten to three axes → red (cross-copy count)', copies: () => { let c = mutate( - base, 'internal-dev', - '- **Long-term soundness for THIS project**', - ' **Long-term soundness for THIS project**', + base, 'internal-pm', + '- **项目长远合理性**', + ' **项目长远合理性**', ); - c = mutate(c, 'internal-dev', 'on four fixed axes', 'on three fixed axes'); - return mutate(c, 'internal-dev', 'on all four axes', 'on all three axes'); + c = mutate(c, 'internal-pm', '沿四条固定评估轴', '沿三条固定评估轴'); + return mutate(c, 'internal-pm', '基于这四条轴给出理由', '基于这三条轴给出理由'); }, expect: 'red', - wants: [/do not agree on the NUMBER of axes/, /#5130/, /3 axes {2}internal-dev/], + wants: [/do not agree on the NUMBER of axes/, /#5130/, /3 axes {2}internal-pm/], }, { // Regression pin for the draft defect described in soft(): the two-axis form @@ -711,20 +716,20 @@ function selfTest() { label: 'two-axis copy phrased "both axes" → red on the COUNT, not on extraction', copies: () => { let c = mutate( - base, 'published-dev', - '- Making AI-authored code — especially AI-authored metadata — structurally hard', - ' Making AI-authored code — especially AI-authored metadata — structurally hard', + base, 'published-pm', + '\n**Axis ③ — making AI-authored code', + '\n **Axis ③ — making AI-authored code', ); c = mutate( - c, 'published-dev', - '- Startup scope discipline — do not grow the declared surface', - ' Startup scope discipline — do not grow the declared surface', + c, 'published-pm', + '\n**Axis ④ — startup scope discipline.**', + '\n **Axis ④ — startup scope discipline.**', ); - c = mutate(c, 'published-dev', 'Analyze every option on four fixed axes:', 'Analyze every option on two fixed axes:'); - return mutate(c, 'published-dev', 'Justify your recommendation on all four axes', 'Justify your recommendation on both axes'); + c = mutate(c, 'published-pm', 'every option on the four fixed axes below.', 'every option on the two fixed axes below.'); + return mutate(c, 'published-pm', 'must be justified on **all four** axes.', 'must be justified on **both** axes.'); }, expect: 'red', - wants: [/do not agree on the NUMBER of axes/, /2 axes {2}published-dev/], + wants: [/do not agree on the NUMBER of axes/, /2 axes {2}published-pm/], unwanted: [/BINDING sentence is not in place/, /could not extract/], }, { @@ -768,12 +773,12 @@ function selfTest() { { label: 'the declaring sentence is removed → red (extraction failure, not a skip)', copies: () => mutate( - base, 'internal-dev', - '**Analyze every option on four fixed axes', - '**Weigh the options sensibly', + base, 'internal-pm', + '**每个方案必须沿四条固定评估轴', + '**每个方案应当妥善权衡', ), expect: 'red', - wants: [/internal-dev/, /could not extract the decision frame/], + wants: [/internal-pm/, /could not extract the decision frame/], }, { label: 'a frame count mention drifts from the frame → red', @@ -798,7 +803,7 @@ function selfTest() { wants: [/do not agree on the axis NAME SEQUENCE/], }, { - label: 'an undeclared fifth copy appears → red (anti-dormancy)', + label: 'an undeclared extra copy appears → red (anti-dormancy)', copies: () => base, extraScan: [{ file: 'skills/objectstack-somethingelse/SKILL.md', @@ -807,6 +812,31 @@ function selfTest() { expect: 'red', wants: [/looks like ANOTHER copy of the decision frame/], }, + { + // The regression pin for the 2026-09-03 reduction: the dev-side copies were + // deleted, and the thing that keeps them deleted is this scan. The fixture + // uses the REAL path whose declaration this change dropped + // (`.claude/agents/os-dev.md`) rather than a synthetic one, so a future PR + // that re-adds the block there is refused instead of being asked politely. + // + // ⚠️ MEASURED LIMIT, stated rather than implied: the scan's population is + // per FILE (`declaredFiles.has(f.file)`). A second, undeclared copy pasted + // back into a file that IS declared — the published SKILL.md, which keeps + // the published-pm copy — is NOT refused by this scan, and was not refused + // before this change either. That gap is why the published copy is named by + // heading at the paste site rather than re-transcribed: the paste + // instruction is what keeps a second published copy from being written, and + // review is what enforces it. Closing the gap mechanically needs a + // per-occurrence criterion this gate does not have today. + label: 'the dev-side copy returns to the now-undeclared agent definition → red (anti-dormancy)', + copies: () => base, + extraScan: [{ + file: '.claude/agents/os-dev.md', + text: 'Analyze every option on four fixed axes:\n- Real business need — ...\n', + }], + expect: 'red', + wants: [/looks like ANOTHER copy of the decision frame/, /os-dev\.md/], + }, { // The anti-false-positive direction. #5451 route B generalized the published // copy on purpose: no startup self-description, no this-repo issue numbers. From ae616fbc9132a4444925010eb195156874a49d1f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:58:27 +0000 Subject: [PATCH 3/3] docs(os-dev): rewrap the replacement sentence under the 120-byte line 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 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- .claude/agents/os-dev.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude/agents/os-dev.md b/.claude/agents/os-dev.md index 968d4fd10a..0be12bde8e 100644 --- a/.claude/agents/os-dev.md +++ b/.claude/agents/os-dev.md @@ -397,10 +397,10 @@ issue 对塑造公开契约的某个决定欠规格 —— spec/Zod schema、API —— 或两种读法通向两种架构时:不猜,不写投机代码。返回 `status: "needs_decision"`,把每个问 题连同选项、成本与你的推荐写进 `open_questions`。 -升级分析的四轴决策框架由派发词携带 —— 派发的 PM 从自己那份副本填入(已发布模板里是 -`rules/dev-template.md` 的 `{decision_frame}` 槽位):每个方案逐轴分析,推荐也按那些轴 -给出理由;派发词没带,停下向 PM 索取,⛔ 不自拟一套轴。`main` 在你脚下碎了、依赖未合 -并、CI 基础设施故障⇒`blocked`(附证据),重试到足以排除你的改动。 +升级分析的四轴决策框架由派发词携带 —— 派发的 PM 从自己那份副本填入(已发布模板里 +是 `rules/dev-template.md` 的 `{decision_frame}` 槽位):每个方案逐轴分析,推荐也按那 +些轴给出理由;派发词没带,停下向 PM 索取,⛔ 不自拟一套轴。`main` 在你脚下碎了、依赖 +未合并、CI 基础设施故障⇒`blocked`(附证据),重试到足以排除你的改动。 ## 终报消息 —— 恰好这段 JSON,不带任何环绕散文