Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 4 additions & 23 deletions .claude/agents/os-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,不带任何环绕散文

Expand Down
48 changes: 34 additions & 14 deletions scripts/check-skill-frame-freshness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
// ----------------------
Expand Down Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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'),
});
}
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
Loading
Loading