From ad089549cd49727a73ca1db1288a775ba06a09a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 01:07:13 +0000 Subject: [PATCH] feat(pm): clause-2 enqueue gate + needs:contract-review re-review chain SKILL.md (queue-gate + review-chain rules 1-5, ceiling 682 held at 681 via same-file rewrap funding), dispatch-gates.mjs (CONTRACT_REVIEW_TIER single source, SUSPECT_TIER_GLOBS, --tier clause-2 suspicion hint + three-way self-test), check-dispatch-gates.mjs (stale hint-measurement note refreshed), ensure-pm-labels.sh (needs:contract-review label seed, main repo only). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017TNzEetykdh7ceZGwuAPLq --- .claude/skills/pm-dispatch/SKILL.md | 29 ++++---- scripts/pm/check-dispatch-gates.mjs | 5 +- scripts/pm/dispatch-gates.mjs | 110 +++++++++++++++++++++++++--- scripts/pm/ensure-pm-labels.sh | 9 +++ 4 files changed, 128 insertions(+), 25 deletions(-) diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index 782b2cb41d..a6dcc3562c 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -309,8 +309,7 @@ hold 评论纪律见状态模型,hold 重验只在 `Restart-when:` 命中时发 回滚动作(存量数据迁移形状、删除已发布能力、force 操作);**安全/权限边界**(放宽访问控制、认 证流、RLS/共享语义、审计留痕的「修复」是伪装的产品决定);**门禁削弱**(降阈值、删必查项、 抬 ratchet 上限、跳过/隔离测试 —— AI 有把CI 弄绿的结构性动机,削弱农场必须是人的动作);**花 - 费/配额/舰队形态/默认模型档位** (动的是维护者的预算);**新增运行时第三方依赖**(供应链/许可/ - 维护义务)。 + 费/配额/舰队形态/默认模型档位** (动的是维护者的预算);**新增运行时第三方依赖**(供应链/许可/维护义务)。 - **代裁车道(分诊座位裁)**:代码整理(行为不变的重构)与 bug 修复。**机械边界测试**:改动是否** 扩大接受集或公开面**?扩大 ⇒ 功能新增/协议变化 ⇒ 人工;接受集不变、拉回已声明契约(declared = enforced 的恢复)⇒ bug/整理 ⇒ 代裁车道。 @@ -318,8 +317,7 @@ hold 评论纪律见状态模型,hold 重验只在 `Restart-when:` 命中时发 ;③ 不收窄、不推翻任何既有维护者裁决;④ 执行是**否决窗口不是许可门**—— 裁定 → 一次标签写入 换 `needs-user-decision` 为工作态 → 卡上贴四棱块 + 结论 + `auto-adjudicated` 标记 → 轮次报 告设**代裁清单**专节(聚合漂移的刹车);⑤ 代裁分析跑在 `claude-fable-5`(与维护者手工流程同档)。 -- **回翻条款**:代裁卡实施中发现契约终究要动 ⇒ dev 停手,卡回`needs-user-decision` —— 报告分叉 - ,⛔ 永不静默重裁。 +- **回翻条款**:代裁卡实施中发现契约终究要动 ⇒ dev 停手,卡回`needs-user-decision` —— 报告分叉,⛔ 永不静默重裁。 ## 执行座位职责 @@ -398,7 +396,7 @@ dev 侧推分支要早 —— 远程分支是在飞工作最硬的证据。**死 默认)。**⛔ 强制条款两条**:① 凡改 `.claude/skills/pm-dispatch/**` 的卡一律 `model: "claude-fable-5"`;② 凡**改变契约接受/拒绝行为或扩大公开面**的卡(`domain:spec` 语义面; 判据即分诊代裁的机械边界测试与 spec 席内分派判据,⛔ 不另抄第二份)一律 `claude-fable-5`(维护者 -2026-08-12 裁定,原话:「同意,就按语义面收窄,立卡并通知 spec 席」)—— 契约错毒化一切下游,全仓最贵 +2026-08-12 裁定,原话:「同意,就按语义面收窄,立卡并通知 spec 席」)—— 契约错毒化一切下游,全仓最贵(条款②嫌疑面 `--tier` 有派发时提示;无论派什么档,入队前按 diff 过条款②闸门,见「入队与落地」) 。两条唯一降档出口:**额度耗尽豁免**(维护者 2026-08-13 原话:「fable 如果用完了,可以用 opus」):仅 当 fable 实测不可用(额度耗尽/限流;墙杀在中途,重派时同样可降)才落 `opus`,⛔ 不再往下,档位与理由记 入认领评论「Container & model」行。明确不变(合并后按席内分派面适用):spec 文本面卡照旧 @@ -506,6 +504,12 @@ not-reachable 是设计非故障(维护者 2026-08-11 裁定)⛔ 不复测;**接 **入队与落地(细则见 `references/landing-operations.md`,落地窗口查阅)。** 原则: +- **条款②入队闸门(维护者 2026-08-16 批准)**:翻 ready / 入队前先取 PR **实际 diff** —— diff 是事实,卡片语义是预测;派发时 `--tier` 的契约嫌疑行是提示不是裁定(尽力从卡片内容判档, + 无论派什么档,入队前按 diff 过闸门)。diff 触及契约面(`packages/spec/src/**`,含 error-code-ledger 与 `*.zod.ts` 契约 schema)且派发档位低于**契约复审档位**(以 dispatch-gates 常量 + `CONTRACT_REVIEW_TIER` 为准 —— 档位单源,⛔ 本文与标签不写模型名,模型升级只改一行一个文件)⇒ ⛔ 禁止入队。派发席职责止于:卡上记一行认定、挂 `needs:contract-review`(标签命名审的对象,恒英文)、停手;⛔ 禁止自查放行。 +- **`needs:contract-review` 复审链**:复审资格双条件,同时满足 —— ① 跑在契约复审档位;② 非该卡派发席(犯规席在结构上无资格补救自己的犯规)。归属:常设 = 分诊席(Routine 模型由维护者在 Routines UI + 钉在契约复审档位),分诊轮新增子轮清该标签 —— 只审契约增量 diff、结论一行写在卡上、清标签后卡方可入队;每小时一轮即天然攒批;过渡期(分诊 Routine 未建成前)由 skills 席代行。**降档保险丝**: + 子轮开场自检当前模型,非契约复审档位 ⇒ 该子轮整体跳过、标签原样留置 —— 卡在队列外等待是安全态;契约复审 ⛔ 不适用额度耗尽豁免降档(豁免的对象是派发;复审的存在意义就是补偿一次低于地板的派发)。 - **碰生成物的 PR,入队前先同步 + 整体重生成** —— os-regen 驱动会零冲突标记地**静默丢掉一侧改 动**,只有重生成才暴露;四步序已机械化(`bash scripts/pm/os-regen-merge.sh`:**先 commit merge 再重生成**,顺序防锚点静默倒退与`gen:openapi` 假红两个陷阱);重生成后断言兄弟单条目与 @@ -555,8 +559,7 @@ not-reachable 是设计非故障(维护者 2026-08-11 裁定)⛔ 不复测;**接 ## 断粮与跨墙恢复(5 小时用量墙) 出处:维护者 2026-08-11 提问(「首先项目经理能不能查到相关的数据,其次是到达时间窗口工作就会停, -是否应该设置一个1小时的定时以监测时间窗口已经解锁」)。检测读数、盲区与恢复 playbook 细则 -见 `references/platform-readings.md`;常驻原则: +是否应该设置一个1小时的定时以监测时间窗口已经解锁」)。检测读数、盲区与恢复 playbook 细则见 `references/platform-readings.md`;常驻原则: - **检测**:`ccusage blocks` 给窗口边界与燃烧率,但只有单容器视野、估成本不估套餐余量(没有任何 面向 agent 的接口暴露账号级剩余额度);**权威的墙信号是失败本身**——撞墙报文里的重置时刻在那 @@ -579,15 +582,13 @@ not-reachable 是设计非故障(维护者 2026-08-11 裁定)⛔ 不复测;**接 **第三档:带前提的裁决** —— 分歧关键是一个可被代码证伪的事实时,三件套缺一不可:①裁决(选定路线 );② 把裁决挂在具名、可证伪的前提上,派发令要求 dev 先验前提再动手; ③ 显式禁令「前提不成立就 -报 fork,⛔ 不许硬做,也不许悄悄退回另一个选项」—— 省掉第③ 条就退化成最坏形态:前提不成立 -时 dev 自行改选,即无人裁决。 +报 fork,⛔ 不许硬做,也不许悄悄退回另一个选项」—— 省掉第③ 条就退化成最坏形态:前提不成立时 dev 自行改选,即无人裁决。 **两条元判据(同族近似单默认不进决策箱,维护者 2026-08-07 拍板):** ① 静默丢弃的声明默认并入既 有拒收集(兄弟支已裁成响亮编写期错误 ⇒ 新支复用母单裁决直接入队,只有真实语义差异才重开;**继承 的是裁决连同理由** —— 母单理由被实测为分支特有时本条不适用,判法是把母单理由拿到新支复核一遍) 。② 一个操作两个实现且行为不一致 ⇒ **带治理的一侧**(权限闸、同意、去重、审计)默认胜出,另一侧 -改绑并删除 —— 不是对齐也不是双写;反向裁只在产品语义明确要求时成立且必须写进裁决正文;留着未治 -理侧等于给权限闸留旁路。 +改绑并删除 —— 不是对齐也不是双写;反向裁只在产品语义明确要求时成立且必须写进裁决正文;留着未治理侧等于给权限闸留旁路。 **落卡/升级流程**:① **先刷新卡片前提** —— 隔夜没动的卡默认按「前提未经验证」处理;**卡上每条前提 行自带一条 re-check 命令**(`git log … -- `、REST compare、带引号精确名 grep、`ls-remote | @@ -621,8 +622,7 @@ pull(今天谁撞上;零拉动默认 defer/ remove);③ AI-agent error-resistanc - PM **不写任何文件**;合并只对**已复核全绿的 dev-agent PR 经合并队列**发生 —— 永不合自己 的 PR、永不合红的或未复核的、永不绕过队列。唯一例外(维护者 2026-08-06 批准):维护者逐 PR 明 - 示授权的 `.claude/` 内部工具 PR,授权原话引用在 PR 正文,复核需另一座位或维护者 walkthrough, - ⛔ 不得自审自合。 + 示授权的 `.claude/` 内部工具 PR,授权原话引用在 PR 正文,复核需另一座位或维护者 walkthrough,⛔ 不得自审自合。 - **版本发布必须人工**(维护者 2026-08-07 拍板)。任何 AI 座位 ⛔ 不得执行或触发发布动作 :`changeset publish` / release 脚本、推版本 tag、`workflow_dispatch` 触发发布类 workflow、 **合并 Version Packages PR**。围绕发布的工作(发版板、pin bump、对账、状态核验)照旧归座位; @@ -636,8 +636,7 @@ pull(今天谁撞上;零拉动默认 defer/ remove);③ AI-agent error-resistanc 人工审核」)。「所有 skills」= 两个技能根 `.claude/skills/**` 与`skills/**`;终局三件套、混 合 diff 一条命中即分叉、撤回机制全部照 ADR 条执行(复核路径见「复核」的 ACCEPT 路径分叉)。 - **决定属于维护者:永不代维护者回答产品/架构问题**(唯一例外:分诊职责里已裁的代裁车道,边界恰 - 与其置信门重合,不得更宽);**永不派发 assignee 是别人的 issue;永不派发 - 带 `needs-user-decision` 的 issue**。 + 与其置信门重合,不得更宽);**永不派发 assignee 是别人的 issue;永不派发带 `needs-user-decision` 的 issue**。 - 每个 dev agent 都在**每仓专属的自有 worktree** 里干活(hook 强制;os-dev 定义重申);并行度 以 `batch` 封顶,同批**按构造文件面不相交**(唯一松动是维护者明示豁免时的替代四条,申 报降到区域级,不是取消不相交)。**分诊座位永不认领、永不派发、永不写代码;执行座位只在自己那 diff --git a/scripts/pm/check-dispatch-gates.mjs b/scripts/pm/check-dispatch-gates.mjs index 01ad7970ec..c3043f9a29 100644 --- a/scripts/pm/check-dispatch-gates.mjs +++ b/scripts/pm/check-dispatch-gates.mjs @@ -59,7 +59,10 @@ * Those three are real reads and still cover three of the largest directories * in the tree, so a directly-wired gate would print MATCHED for every card * under them — a smaller fabrication than the fixture one, of the same kind. - * The spec filter path from the incident above no longer matches at all. + * The spec filter path from the incident above no longer matches via fixtures; + * it matches again today through a declared module-body constant (the clause-② + * suspect glob), pinned as deliberate in the tool's own self-test and inert for + * gate matching for the same reason as the tier globs beside it. * * A separate gate file is also what the other two pm gates look like * (check-skill-line-ratchet.mjs, check-skill-id-lint.mjs). Its watch hints are diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index 995ea738d6..c73c7cbecc 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -1451,7 +1451,10 @@ export function residueLines({ discovered, matched, undetermined, silent, unfilt * `claude-fable-5`. That is judged from the card's CONTENT — what the change * does to the contract — and a path cannot answer it. An ordinary-looking * surface (one package's source file) is the NORMAL shape of a clause-② - * card. + * card. The closest a path can honestly get is SUSPICION: + * SUSPECT_TIER_GLOBS below marks the contract surface itself, and `--tier` + * prints a hint for it — never a verdict. The enforcement lives one step + * later, in the PM skill's enqueue gate over the PR's ACTUAL diff. * * A path derivation that pretended to cover clause ② would produce the failure * this whole file is written against, one level up: a "no mandate" line read as @@ -1492,9 +1495,10 @@ export function residueLines({ discovered, matched, undetermined, silent, unfilt * * ## One measured side effect of putting a path in a MODULE BODY * - * Comment masking cannot reach a module-body string, so this glob is now a - * watch hint of this file's own source: measured, `extractWatchHints` yields 5 - * hints here against 4 on the base, the new one being the glob itself. It is + * Comment masking cannot reach a module-body string, so this glob — and the + * suspect glob below — is a watch hint of this file's own source: measured, + * `extractWatchHints` yields 6 hints here against 4 on the base, the new ones + * being the globs themselves. They are * inert today because no check family resolves to THIS file — the gate that * covers it is `check:pm-dispatch-gates`, which resolves to * `check-dispatch-gates.mjs` and matches this file through that file's one @@ -1518,6 +1522,43 @@ export const MANDATORY_TIER_GLOBS = [ }, ]; +/** + * Clause ②'s single source of truth for the CONTRACT-REVIEW tier: the tier a + * card that changes contract accept/reject behaviour or widens the public + * surface must be dispatched at, and the tier the `needs:contract-review` + * re-review sub-round must itself be running at (its opening self-check reads + * this). Declared HERE and only here, as a constant, so a model upgrade is a + * one-line change in one file. The review label deliberately names WHAT is + * reviewed, never a model (maintainer, 2026-08-16: 「needs:fable-review 这个标 + * 签不好,下次模型升级怎么办」), and the PM skill's prose points at this + * constant instead of spelling a model name. + */ +export const CONTRACT_REVIEW_TIER = 'claude-fable-5'; + +/** + * The globs that make a surface a clause-② SUSPECT — a HINT, never a verdict. + * + * Clause ② is judged from a card's CONTENT; no path predicate can decide it + * (the docblock above MANDATORY_TIER_GLOBS says why pretending otherwise would + * recreate the incident class this file exists against). What a path CAN say + * is where such cards normally land: `packages/spec/src/**` is the contract + * surface itself — the error-code ledger and the `*.zod.ts` contract schemas + * live there, and the measured incident shape (a below-tier dispatch flipping + * accept-to-reject behaviour in the ledger, the same hole passed three times + * in one day) sat exactly under it. So `--tier` prints a suspicion line for + * these paths: judge the tier from the card content as best you can, and + * whichever tier is dispatched, the PR's ACTUAL diff passes the clause-② + * enqueue gate before the card may enqueue — the diff is a fact; the card's + * semantics were a prediction. The gate itself lives in the PM skill + * (入队与落地); this output only points at it. + */ +export const SUSPECT_TIER_GLOBS = [ + { + glob: 'packages/spec/src/**', + why: 'the contract surface (error-code ledger, *.zod.ts contract schemas) — the normal landing zone of a clause-② card', + }, +]; + /** The tier floor for a card with no mandate — the ruling's 最低下限. */ export const TIER_FLOOR = 'sonnet'; @@ -1532,12 +1573,16 @@ export const TIER_DEFAULT = 'opus'; * this file encodes no ordering over tiers, so choosing between them would be a * guess printed as a derivation. */ -export function deriveTier(paths, globs = MANDATORY_TIER_GLOBS) { +export function deriveTier(paths, globs = MANDATORY_TIER_GLOBS, suspectGlobs = SUSPECT_TIER_GLOBS) { const hits = []; + const suspects = []; for (const p of paths) { for (const g of globs) { if (hintCovers(g.glob, p)) hits.push({ path: p, glob: g.glob, tier: g.tier, why: g.why }); } + for (const g of suspectGlobs) { + if (hintCovers(g.glob, p)) suspects.push({ path: p, glob: g.glob, why: g.why }); + } } const tiers = [...new Set(hits.map((h) => h.tier))]; if (tiers.length > 1) { @@ -1546,7 +1591,7 @@ export function deriveTier(paths, globs = MANDATORY_TIER_GLOBS) { 'two globs cover it with different tiers and this script orders no tiers', ); } - return { mandatory: hits.length > 0, tier: tiers[0] ?? null, hits, declared: globs.length }; + return { mandatory: hits.length > 0, tier: tiers[0] ?? null, hits, suspects, declared: globs.length }; } /** @@ -1556,7 +1601,7 @@ export function deriveTier(paths, globs = MANDATORY_TIER_GLOBS) { * as anything else. */ export function tierLines(result) { - const { mandatory, tier, hits, declared } = result; + const { mandatory, tier, hits, declared, suspects = [] } = result; if (mandatory !== hits.length > 0 || mandatory !== Boolean(tier)) { throw new Error( `tier verdict is self-contradictory: mandatory=${mandatory}, tier=${tier ?? 'none'}, ` + @@ -1566,11 +1611,23 @@ export function tierLines(result) { const clause2 = ' Clause ② is NOT reachable from paths: a card that changes contract accept/reject behaviour or widens the public' + ' surface is fable-mandatory too, judged from the card CONTENT. This line is a FLOOR, never a clearance.'; + // The suspicion tail prints only on a hit — unlike the clause-② note above, + // which prints always: "no suspicion" and "no suspect table" must not share a + // spelling, and the note is what keeps silence from reading as a clearance. + const suspicion = suspects.length === 0 + ? [] + : [ + ` Clause ② SUSPECT surface — a hint, not a verdict: judge the tier from the card CONTENT as best you can` + + ` (a card changing contract accept/reject behaviour or widening the public surface is ${CONTRACT_REVIEW_TIER});` + + ` whichever tier is dispatched, the PR's actual diff passes the clause-② enqueue gate before the card may enqueue.`, + ...suspects.map((s) => ` - ${s.path} ⇢ '${s.glob}' — ${s.why}`), + ]; if (!mandatory) { return [ `Model tier — no path-derived mandate: the surface hits none of the ${declared} declared glob(s), derived here, not recalled.`, ` The tier stays the PM's per-card judgment call (floor ${TIER_FLOOR} · default ${TIER_DEFAULT} · ceiling fable).`, clause2, + ...suspicion, ]; } return [ @@ -1579,6 +1636,7 @@ export function tierLines(result) { ' The only exit is the measured quota exemption (fable unavailable ⇒ opus, never lower), recorded with its reason' + " in the claim comment's `Container & model` line.", clause2, + ...suspicion, ]; } @@ -2154,10 +2212,15 @@ function selfTest() { // card editing the tool must still derive it. t('the dispatch-gates gate still reaches the tool it runs', covers(readHints('scripts/pm/check-dispatch-gates.mjs'), 'scripts/pm/dispatch-gates.mjs')); // And this file, the worst specimen in the card's table: the directory it - // really reads survives, the fixtures naming other packages do not. + // really reads survives, the fixtures naming other packages do not. The spec + // contract surface DOES hint now — via the declared module-body suspect glob + // (a real constant, not a fixture; inert for gate matching for the reason the + // MANDATORY_TIER_GLOBS docblock records) — so the fixture-masking claim is + // pinned on a path only fixtures name. const ownHints = readHints('scripts/pm/dispatch-gates.mjs'); t('this tool still hints the workflow directory it reads', covers(ownHints, '.github/workflows/lint.yml')); - t('this tool no longer hints the spec paths its own fixtures name', !covers(ownHints, 'packages/spec/src/data/filter.zod.ts')); + t('this tool hints the spec contract surface via the DECLARED suspect glob', covers(ownHints, 'packages/spec/src/data/filter.zod.ts')); + t('this tool still does not hint the paths only its fixtures name', !covers(ownHints, 'packages/objectql/src')); // The LIVE trailing-dot specimen (#8534): this gate spells its own filename as // the last word of a sentence, in a module-body array element that comment // masking cannot reach, so the hint carried the period. Pinned live because @@ -2520,6 +2583,35 @@ function selfTest() { t('every declared glob carries the tier it mandates and a reason', MANDATORY_TIER_GLOBS.every((g) => g.glob && g.tier && g.why)); t('the incident file is a real file, so the references case is a live claim and not a fixture', existsSync(join(ROOT, '.claude/skills/pm-dispatch/references/review-checklist.md'))); + // ── Clause-② suspicion (the enqueue-gate card): hit / no hit / wording ──── + // + // The wording cases are not decoration — the suspicion line is quoted into + // claim comments, and its one invariant is that a HINT must not be readable + // as a verdict (nor harden the no-mandate line into a clearance). + const suspectHit = fableOf(['packages/spec/src/api/error-code-ledger.zod.ts']); + t('a spec contract path is a clause-② SUSPECT, with its provenance recorded', suspectHit.suspects.length === 1 && suspectHit.suspects[0].glob === 'packages/spec/src/**'); + t('a suspect is NOT a mandate — suspicion must not harden into a path verdict', suspectHit.mandatory === false && suspectHit.tier === null); + const suspectRendered = tierLines(suspectHit).join('\n'); + t('the suspicion rendering says SUSPECT and names the offending path', suspectRendered.includes('SUSPECT') && suspectRendered.includes('packages/spec/src/api/error-code-ledger.zod.ts')); + t('the suspicion rendering is a hint, not a verdict, in those words', suspectRendered.includes('a hint, not a verdict')); + t('the suspicion rendering sends the seat to the card CONTENT for the tier call', suspectRendered.includes('judge the tier from the card CONTENT')); + t('the suspicion rendering routes EVERY dispatch through the enqueue gate on the ACTUAL diff', suspectRendered.includes('whichever tier is dispatched') && suspectRendered.includes('enqueue gate')); + t('the suspicion rendering names the contract-review tier from its single-source constant', suspectRendered.includes(CONTRACT_REVIEW_TIER)); + const noSuspicion = fableOf(['packages/runtime/src/kernel.ts']); + t('an ordinary non-contract surface raises no suspicion', noSuspicion.suspects.length === 0); + t('no suspicion ⇒ no suspect line — absence and clearance must not share a spelling with a hit', !tierLines(noSuspicion).join('\n').includes('SUSPECT')); + const mandatedAndSuspect = fableOf(['.claude/skills/pm-dispatch/SKILL.md', 'packages/spec/src/data/filter.zod.ts']); + t('a mandated surface still prints its suspect paths — the enqueue gate reads diffs, not dispatch tiers', mandatedAndSuspect.mandatory && mandatedAndSuspect.suspects.length === 1 && tierLines(mandatedAndSuspect).join('\n').includes('SUSPECT')); + t('a verdict built without a suspects field still renders (suspicion defaults empty)', tierLines({ mandatory: false, tier: null, hits: [], declared: 1 }).length === 3); + // Same liveness guards as the mandatory table: dead data reading as + // protection is the incident class itself. + const deadSuspects = SUSPECT_TIER_GLOBS.filter( + (g) => !existsSync(join(ROOT, g.glob.replace(/\*\*?/g, '').replace(/\/+$/, ''))), + ); + t(`every declared suspect glob names a path this tree really has (dead: ${deadSuspects.map((g) => g.glob).join(', ') || 'none'})`, deadSuspects.length === 0); + t('the suspect table is not empty and every entry carries its reason', SUSPECT_TIER_GLOBS.length > 0 && SUSPECT_TIER_GLOBS.every((g) => g.glob && g.why)); + t('the contract-review tier constant is a non-empty model id — the single source the PM skill points at', typeof CONTRACT_REVIEW_TIER === 'string' && CONTRACT_REVIEW_TIER.length > 0); + let failed = 0; for (const [name, cond] of cases) { if (!cond) failed++; diff --git a/scripts/pm/ensure-pm-labels.sh b/scripts/pm/ensure-pm-labels.sh index f7005e0246..100eec022c 100644 --- a/scripts/pm/ensure-pm-labels.sh +++ b/scripts/pm/ensure-pm-labels.sh @@ -39,6 +39,15 @@ for R in objectstack-ai/objectstack objectstack-ai/objectui objectstack-ai/cloud gh label create pm:epic -R "$R" -c 5319e7 -d "Parent delegated to a dedicated epic PM (session + territory in the parent's own body) — other PMs never dispatch into its subtree" 2>/dev/null || true done +# needs:contract-review — the clause-② enqueue gate's re-review chain (SKILL.md +# 入队与落地): a PR whose ACTUAL diff touches the contract surface but was +# dispatched below the contract-review tier waits outside the queue under this +# label until the review sub-round clears it. Named consumers: the enqueue gate +# and the triage sub-round's label query. Main repo only — the contract surface +# (packages/spec) lives here. The label names WHAT is reviewed, never a model; +# the tier's single source is CONTRACT_REVIEW_TIER in scripts/pm/dispatch-gates.mjs. +gh label create needs:contract-review -R objectstack-ai/objectstack -c d93f0b -d "Clause-② enqueue gate: contract-surface diff dispatched below the contract-review tier — blocked from enqueue until the review sub-round clears it" 2>/dev/null || true + # Routing labels exist only on the main backlog repo, and mark SEAM cards only # (file-at-destination ruling: pure sibling-repo fixes live in the target repo). gh label create repo:objectui -R objectstack-ai/objectstack -c fbca04 -d "Seam card: cross-repo ordering with objectui is the substance (pure objectui fixes live in objectui)" 2>/dev/null || true