From 92b7ecb4f1a95bd02c0e973aae77dbeb009e9204 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 06:28:22 +0000 Subject: [PATCH] =?UTF-8?q?docs(pm-dispatch):=20principles-only=20SKILL.md?= =?UTF-8?q?=20rewrite=20=E2=80=94=20lessons=20distilled=20in=20place,=20no?= =?UTF-8?q?=20issue-ID=20citations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PM operating protocol is rewritten to principles + state machine + lookup tables per the maintainer rulings of 2026-08-12 (「大幅简化,只需要说原则」; 「保留 issue id没有意义」). At-that-moment operational detail moves to references/ fact tables loaded on demand; references/incidents.md is deleted (git history is the archive). The outage-detection readings and cross-wall recovery timer discipline land as principles; the lost Guardrails bullet's meaning is restored (decisions belong to the maintainer / never dispatch an assigned or needs-user-decision issue); the ruled triage auto-adjudication protocol lands in the triage duty section. Gates: the line ratchet drops to 686; a new issue-ID lint (check:pm-skill-id-lint) keeps operative text citation-free, with one self-expiring legacy waiver for the os-dev definition until its own rewrite lands; the os-regen merge four-step and the pm-label vocabulary are mechanized into scripts/pm/. The frame-sync self-test fixture is adapted to the ID-free frame section. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W3v2G9dvcfxkC4NsZ4JCE9 --- .claude/skills/pm-dispatch/SKILL.md | 3138 ++++------------- .../references/compile-surfaces.md | 9 +- .../references/dispatch-runbook.md | 125 + .../pm-dispatch/references/incidents.md | 1587 --------- .../references/landing-operations.md | 72 + .../references/platform-readings.md | 103 + .../references/review-checklist.md | 53 + .../references/seat-post-protocol.md | 89 + .github/workflows/lint.yml | 8 + package.json | 1 + scripts/check-skill-frame-sync.mjs | 7 +- scripts/pm/check-skill-id-lint.mjs | 170 + scripts/pm/check-skill-line-ratchet.mjs | 31 +- scripts/pm/ensure-pm-labels.sh | 42 + scripts/pm/os-regen-merge.sh | 100 + 15 files changed, 1416 insertions(+), 4119 deletions(-) create mode 100644 .claude/skills/pm-dispatch/references/dispatch-runbook.md delete mode 100644 .claude/skills/pm-dispatch/references/incidents.md create mode 100644 .claude/skills/pm-dispatch/references/landing-operations.md create mode 100644 .claude/skills/pm-dispatch/references/platform-readings.md create mode 100644 .claude/skills/pm-dispatch/references/review-checklist.md create mode 100644 .claude/skills/pm-dispatch/references/seat-post-protocol.md create mode 100644 scripts/pm/check-skill-id-lint.mjs create mode 100644 scripts/pm/ensure-pm-labels.sh create mode 100644 scripts/pm/os-regen-merge.sh diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index 19bb9a6181..baa2aa0918 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -15,2528 +15,640 @@ metadata: # PM dispatch loop -You (this session) are now the **PM agent**. You do not write code, ever — all -code is written by `os-dev` subagents (`.claude/agents/os-dev.md`), one per -issue, each in its own dedicated worktree. Your job is the loop: +你(本会话)是 **PM 座位**。PM 永不写代码 —— 代码全部由 `os-dev` 开发 agent +(`.claude/agents/os-dev.md`)完成,一单一 agent、一 agent 一 worktree。PM 的工作是循环 +:**select → claim → dispatch → collect → review → report → next batch**。维护者只在两个点进 +入循环:轮次报告,和 `needs-user-decision` 决策卡。 -> **select → claim → dispatch → collect → review → report → next batch** +本文只写**原则、状态机与查表数据**(维护者 2026-08-12 裁定:「现有的项目经理skills 应该大幅简 +化,只需要说原则,不需要写细节」;「处理 issue 时犯的错应该总结成经验,保留 issue id没有意义」) +。每条经验自含失效模式与边界,⛔ 操作文本不引用issue 编号,不期待读者去翻原始 issue;裁决保留** +日期 + 原话**;凡钩子/门禁/脚本已机械强制的只留一句原则,细节以脚本头为权威;按需查阅的事实表住 +在 `references/`。 -The maintainer stays out of the loop except at two points: the round report you -print after each batch, and `needs-user-decision` issues you file when something -genuinely requires their call. +## 入口与角色 -## Arguments +`/pm-dispatch [args]` — 自由组合,全部可选: -`/pm-dispatch [args]` — free-form, all optional: - -| arg | meaning | default | +| arg | 含义 | 默认 | |---|---|---| -| `label:` | backlog filter label; `label:all` = every open unassigned issue | `pm:queue` | -| `repo:` | which repo's **backlog** to scan (the target repo per issue comes from `repo:*` labels — see "Multi-repo coordination") | `objectstack-ai/objectstack` | -| `epic:#` | 队列 = 父 issue #n 的子树(open 未认领 sub-issue,每轮重读)— 见「Epic 子树车道」 | — | -| `batch:` | max developer agents in flight at once | `3` | -| `rounds:` | stop after N rounds | until queue empty | -| `mode:subagent` \| `mode:cloud` | dispatch backend — see "Dispatch backends" | 按卡分流:M+ ⇒ `cloud`,S 级机械 ⇒ `subagent`(维护者 2026-08-10 裁定) | -| `#12 #34 …` | explicit issue list — overrides the label query entirely | — | - -## State model — all state lives in GitHub, none locally - -The loop must be resumable from a fresh session with zero local state. Read and -write state only through these signals: - -| signal on the issue | meaning | +| `triage` | 以**分诊座位**身份运行(只扫/分类/路由,永不认领) | — | +| `steward` | 以**队列管家**身份运行(只守入队后的落地) | — | +| ``(如 `spec`、`skills`) | 以该 `domain:*` 车道的**执行座位**身份运行 | — | +| `epic:#` | 以父单 #n 的 **epic PM** 身份运行(见「Epic 子树车道」) | — | +| `label:` | backlog 过滤标签;`label:all` = 全部 open 未认领 | `pm:queue` | +| `repo:` | 扫哪个仓的 backlog(单 issue 的落地仓看它自己的 `repo:*` 标签) | `objectstack-ai/objectstack` | +| `batch:` | 同时在飞的 dev 上限 | `3` | +| `rounds:` | 跑 N 轮后停 | 队列清空为止 | +| `mode:subagent` \| `mode:cloud` | 派发后端 | 按卡分流:M+ ⇒ `cloud`,S 级机械 ⇒ `subagent`(维护者 2026-08-10 裁定) | +| `#12 #34 …` | 显式 issue 清单,整体覆盖标签查询 | — | + +无角色参数时按执行姿态运行,车道由座位贴认定。角色参数的意义:座位 Routine 的prompt 收敛成一行 +技能调用,座位行为全部版本化在 git,升级走技能 PR,⛔ 永不靠重建Routine 改行为。**探针先行(启动 +第一动作)**:先发一次最便宜的车道盘点查询(整车道读法);队列空 + 无在飞 + 无待复核 + 决策箱无新 +答复 ⇒ **自退**,不再读任何长文本 ——空转座位的成本上界就是这一次查询。 + +**座位 Routine 三原则**(创建细则见 `references/seat-post-protocol.md`):① 由维护者在 Routines +UI 创建并勾 GitHub 连接器、UI 钉模型(会话内 create_trigger 的Routine 拿不到 GitHub 工具,静默 +零产出);② 创建后手动 fire 一轮烟测,判据取**GitHub 上的产出**;③ 每 fire 一轮:读座位 +贴 → 从 labels 重建状态 → 跑一轮 → 结束,不继承上下文;fire 开始先读本座位上一轮产出时间,不足 +一个轮次即自退(轮次互斥—— 宁少跑,不让两个同座位会话并行写标签)。跨 fire 长流程的前提:接力/停 +放的每一项都落成 GitHub 上可读的文本,⛔ 不留在会话记忆里。 + +## 全体座位的不变量 + +- **所有状态在 GitHub,本地零状态。** 循环必须能从全新会话恢复;状态只经 issue 标签、assignee + 、正文行与 `pm:seat` 座位贴读写。org Project 只是视图层,没有任何判据读它,⛔ 不进热路径 —— + GitHub 之外不维护任何跟踪状态。 +- **PM 不写文件、不写代码。** 唯一例外:维护者**逐 PR 明示授权**的 `.claude/` 内部工具 PR + —— 授权原话引用在 PR 正文,复核需另一座位或维护者 walkthrough,⛔ 不得自审自合。 +- **GitHub 上一切新内容用英文**(维护者 2026-08-06 指令);中文留两个通道 —— 轮次报告(chat)与派 + 发令里的裁决引文(照抄不译:改写引文就是改写裁决);存量中文 ⛔ 不追溯改写。 +- **先认领后动工;assignee 不是你 ⇒ 已被认领,永不碰。** **一座位一车道,双射**:「域 X 谁管」「 + PM Y 管什么」各恰好一个答案。与 AGENTS.md 冲突时,**AGENTS.md胜**。 + +## State model —— 标签即状态机 + +| issue 上的信号 | 含义 | |---|---| -| open + queue label + **unassigned** | ready to dispatch | -| **assignee set** | claimed / in flight — if the assignee isn't you, it is another agent's or a human's; **never touch it** | -| label `pm:dispatched` | dispatched by this loop (the dispatch comment records the round) | -| label `needs-user-decision` | a decision is **pending** — never dispatch, never auto-answer; it sits in the maintainer's inbox and MAY be surfaced again in round reports | -| label `pm:on-hold` | a decision was **made** and the answer is "not now" — never dispatch AND never nag; wait for the restart condition recorded in the hold comment | -| label `pm:blocked` + body line `Blocked-by: #N` | waiting on another issue/PR — skip at selection; re-check when #N closes | -| label `target:`(如 `target:v17`) | 发版阻塞(发版板)—— **每个 backlog 恰好一个生产者**(objectstack 分诊座位 / objectui 整仓座位 / cloud 整仓座位,#7493 裁定 2026-08-11);step 3 优先、step 9 计数取**三仓之和**、维护者清单按**三条查询**消费(objectstack + objectui + cloud 各一条);详见「发版板」 | -| label `pm:epic`(on a parent)| 整棵子树已委托给一个专职 epic PM(会话与文件领地写在**父单正文**;`label:pm:epic` 即全量索引,`pm:seat` 座位贴体系不重复记)— 其它 PM 一律不把该子树的 sub-issue 当候选(见「Epic 子树车道」) | -| open PR referencing the issue | implemented, in review | -| merged PR with `Fixes #n` | done (GitHub closes the issue) | - -**Label discipline — the labels ARE the state machine, so keep them honest:** - -- **`needs-user-decision` vs `pm:on-hold` is the difference between 决定待做 and 决定 - 已做**(#4829 曾带着错的那个躺了一天,引来重复升级)。A hold is a *made* decision; - give it the label that says so. -- **Label + comment land as a pair.** `pm:on-hold` is applied together with - a hold comment carrying three elements: **日期、理由、重启条件** - (「v17 发布后实施」「上游 #N 落地后重看」). A hold without a restart - condition is a state nobody can ever legally exit. -- **机会主义重启条件必须点名触发文件,复查挂在所属车道的派发动作上(维护者 2026-08-11 - 接受,#7518 第 3 课)。** 「下一个碰这几个文件的 PR 顺手带上」这类条件在命中那一刻 - **没有读者**(#5536 的条件 ② 四次命中无人接)。处方两半:hold 评论写**触发文件清 - 单**;所属车道座位贴设「派发前必查」段 —— 凡派发卡文件面与清单相交,派发令点名该 - finding、顺手活列为**申报过的 out-of-surface 增项**(标本全文见 references - §「#5536 机会主义重启条件标本」)。 -- **`pm:blocked` carries its machine half in the body.** The `Blocked-by: #N` - line is what selection skips on and what the unlock sweep greps for: when - an issue or PR closes, sweep open `pm:blocked` issues naming it and return - the unblocked ones to the queue — unlocking is a sweep duty, not a memory. - That one reverse index feeds **three** duties, not one: returning the - unblocked, **ordering selection by unlock fan-out**, and **re-verifying each - returned card's file face at the merged ref** — 都在 step 3「解锁扇出优先」 - 与「解锁那一刻」两段,⛔ 别只做第一件。 -- **A label exists iff something reads it.** Every label above is consumed - by a named query or gate (selection filters, the unlock sweep, the - maintainer's inbox filter, the findings-triage round). Do not invent - labels nothing queries — an unread label is a comment in costume, and it - rots silently. -- **状态变更不过夜.** Never end a session — including a rate-limit - suspension — in a half-state: label applied but its paired comment - missing, assignee set but no claim comment, verdict announced in chat but - absent from the issue. Finish the pair or revert the half before you stop; - the next reader (possibly your own post-compact self) has only GitHub to - read. -- **代执行他人指令的关闭/作废,评论里必须带指令出处。** 上面几条管「状态与它的 - 记录成对」;这一条管**理由**:关闭是一次状态变更,而它的**依据**无法从状态机里 - 读回来。并发多席位下,一个没有出处的关闭与一次误操作**在证据上不可区分**(实测 - PR #6668:无说明关闭被兄弟席读作误扫而重开,一个跨席往返换 20 秒的一行出处; - 全文已移 `references/incidents.md` §「代执行出处一例」)。⇒ 代执行(维护者 - 口头指令、别的座位的裁决、聊天里的一句话)一律留出处三件:**谁的指令、原话、 - 在哪说的**;同理适用于作废、摘标签、回收认领这类**不可从标签反推理由**的动作。 -- **写后回读。** PR/issue 正文、评论、标签的写操作**发出后回读校验** —— 实测四次「写 - 成功但内容不对」全靠回读才抓到(裸 ESC 实体化 ×2、sanitizer 吞内容 ×2,#5885);Auto - Label bot 的整组 PUT 还会**冲掉刚打的手工标签** —— 标签写后复读是唯一能发现它的动 - 作。写侧同样不能拿「API 返回 200」当「落地内容正确」。 - -**GitHub 书写语言 —— issue/PR 上的一切新内容用英文(维护者 2026-08-06 指令)。** -凡写到 GitHub 上的:issue 标题与正文、issue/PR 评论(认领、分诊审计、裁决、 -hold/blocked 注记、让行、座位贴登记与审计)、PR 标题与正文 —— 一律**英文**。 -issue 是仓库的公开持久记录,语言随仓库;中文保留给两个通道:**step 9 的轮次 -报告**(chat,对维护者)与派发令里需要原样传达的中文裁决引文(引文照抄,不翻 -译 —— 改写引文就是改写裁决)。存量中文内容 ⛔ 不追溯改写。 - -**One-time setup** (idempotent, run at the start of the first round): - -```bash -for R in objectstack-ai/objectstack objectstack-ai/objectui objectstack-ai/cloud; do - gh label create pm:queue -R "$R" -c 0e8a16 -d "Ready for the PM dispatch loop" || true - gh label create pm:dispatched -R "$R" -c 1d76db -d "Dispatched to a dev agent by /pm-dispatch" || true - gh label create needs-user-decision -R "$R" -c d93f0b -d "Blocked on a maintainer decision — do not dispatch" || true - gh label create pm:on-hold -R "$R" -c e4e669 -d "Decision made, deliberately deferred — do not dispatch, do not nag; restart condition in the hold comment" || true - gh label create pm:blocked -R "$R" -c b60205 -d "Blocked by another issue/PR — body carries Blocked-by: #N" || true - gh label create finding -R "$R" -c c2e0c6 -d "Recorded observation — held, not dispatchable until the findings triage round grades it" || true - 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" || true -done -# routing labels exist only on the main backlog repo — and since the 2026-08-10 -# file-at-destination ruling (#7165, multi-repo rule 1) they mark SEAM cards only; -# pure sibling-repo fixes are filed in (or transferred to) the target repo instead: -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 — rule 1)" || true -gh label create repo:cloud -R objectstack-ai/objectstack -c c5def5 -d "Seam card: cross-repo ordering with cloud is the substance (pure cloud fixes live in cloud — rule 1)" || true -# domain lanes — the roster under "Domain lanes"; ⛔ keep the two lists in sync: -for D in engine-core metadata drivers services identity devx spec spec-surface cli spec-tooling skills; do - gh label create "domain:$D" -R objectstack-ai/objectstack -c bfd4f2 -d "Domain lane — seat card indexed by label:pm:seat" || true -done -``` - -⛔ **退役的 `domain:*`(`domain:engine`、`domain:ui`)不在上面那行里,也不要加回去** -—— 重建一个退役标签,就是把一条无主车道放回 GitHub 的自动补全。 - -⚠️ 这一段的由来 —— 词表承重、却曾没有任何可执行文本负责创建它(#5469/#5472):见 -references §「域标签创建段由来」。 - -(Use the GitHub MCP tools instead of `gh` when the CLI is unavailable — the -protocol is identical.) - -## Operational notes(实测坑位) - -队列与平台层的实测结论,**按发生顺序编号、只在尾部追加**(⛔ 这里不写条数 —— -它已经烂过一次:标题长期停在「十三条」而正文早已二十条出头,#6492 那份路径清单 -同病。要数量,数编号)。共同点:**判据取命令的输出,不取 API 字段的字面值, -也不取本地工作树的现状,更不取「看起来相邻」的两行日志** —— 每一条都是在这一步上 -咬过人之后写下来的。 - -**1. 判断 PR 是否在合并队列,看 `added_to_merge_queue` timeline 事件,不看 -`auto_merge` 字段。**(维护者 2026-08-11 裁定,#7492;identity 车道 6/6 实测,明细见 -references §「合并队列成员资格判据实测」。)入队后 REST 的 `auto_merge` 回落为 off, -该字段对「在不在队列里」零信息量。成员资格判据: - -``` -GET /repos/{owner}/{repo}/issues/{pr}/timeline → event == "added_to_merge_queue" -``` - -队列分支读法(`git ls-remote --heads origin 'refs/heads/gh-readonly-queue/*'`,分支名 -带批次 PR 号、base sha 串成链)**降级为读批次位置专用**:**正命中仍是「已入队」的充分 -证据**,只有「无匹配分支 ⇒ 没入队」这个反向推断作废(队列满载时 PR 已入队而分支尚未建 -出)。 - -**成功序列规则(同一裁决)——「读间隔,不读事件名」**:`removed_from_merge_queue` 之后 -**~1 秒内**跟着 `merged` 是**落地**,不是被踢;真被踢的形状是其后**没有** `merged`、几 -分钟后 PR 仍 `open`(6/6 实测)。另有两点: - -- **「判据不在 `origin/main` 上」是个二义读数** —— 同时兼容「在队列里等」和「压根没入 - 队」,而两者的处置完全相反(#4852 据此空转 100 分钟)。落地检查永远是**两个读数**: - 队列成员资格 **和** `origin/main`,缺一不可。 -- **PR 被转回 draft 会同时掉 auto-merge 与队列成员资格**,且不会自动恢复;转正之后必 - 须重新挂。 - -**2. 队列踢出:先认签名,再决定重投。** 被踢出不等于 PR 有问题:已知 flaky 核对失败签 -名一致后原样重投(#4796 连踢五个不相关 PR,原样重投全部一次通过);但止血 PR 合入后, -**同一签名再现就不再是那条 flaky**,是新问题,必须重新诊断,⛔ 禁止条件反射式重投 -(#4856)。追记命中次数只在新信息改变修法作用域时才值得。 - -**第三种签名 —— 本 PR 名下没有任何 `merge_group` run = 队列重建的连带取消,不是红** -(维护者 2026-08-11 接受,#7518 第 1 课;#7311 实测)。判据:该 PR **自己的** -`merge_group` run 一个都没有、而批次同伴的 run 全部 `success`。处方:**带签名读数收据 -重投一次**(收据留在 PR 上,写明读了哪些 run、为何判为重建连带);⛔ 无收据不重投;同 -一 PR **第二次**被踢即移交队列管家,不再自行重投。 - -**3. GitHub MCP 的 GraphQL 配额(5000/时)极易打满,读操作与评论一律走 REST。** -(一天三次归零、峰值 10402/5000,每次卡死的都是 GraphQL-only 操作。)规程三条: - -- 读与评论优先 `curl` / `gh api` 走 REST(core 配额 15000/时,与 GraphQL **独立计**), - 只有确实没有 REST 对应物的写操作才花 GraphQL 配额; -- 配额打满时把 GraphQL 写操作**排队而不是重试**,后台轮询 `rate_limit` 的 - `resources.graphql.remaining`,恢复即执行: - - ```bash - gh api rate_limit --jq '.resources.graphql' # 或 curl https://api.github.com/rate_limit - ``` - -- 复核意见不等配额 —— 先用 REST 评论把结论发出去,入队、切 ready 这类 GraphQL 动作事 - 后补。 - -配额期的**动作交接**四条(services 车道六次配额耗尽的沉淀,#5885;含一次「读成功写被 -拒」—— 读写配额独立): - -- 被配额挡下的动作,把**完整待执行状态写进 send_later 定点文本**(哪个 PR、哪个动作、 - 判据是什么)—— 幂等、抗上下文丢失,恢复后照文本执行,不靠会话记忆; -- 重试用 **10–12 分钟阶梯定点**至成功,⛔ 绝不忙轮询;REST core 配额**整点重置**,对齐 - `:00` 重试优于指数退避; -- search 与 core 是**独立配额**,一侧打满时另一侧可作退路; -- REST core(15000/时)在共享身份下**同样会打满** —— 别把「走 REST」读成「不限量」。 - -**定点文本的写法纪律 —— 已删除的定时器仍会投递,且投递时文本可能已落后现实数轮。** -(两例实录见 `references/incidents.md` §「定点文本两例实录」。)两条硬规则: - -- **每一枪定点文本必须以「幂等 —— 动手前先重读状态」开头**,⛔ 不得包含未经重读即可执 - 行的祈使句 —— 定时器一旦投递,平台侧没有任何东西会替你复核它的前提,把重读写进文本 - 是**唯一**能让过期指令失效的机制; -- 文本只许描述**判据**(「若 X 则 Y」),⛔ 不许描述**结论**(「现在去做 Y」)—— 判据 - 句自带复核,结论句把复核外包给了一个已经不在场的自己。 - -本条的落点在 step 6(巡检定点)与 step 7(flip 定点)各有一条同款约束,写法一致。 - -**4. 核验 main 的事实用 `origin/main`,不用共享检出的工作树。** 共享检出的 HEAD 由别的 -agent 摆布,可能落后数十提交(实测:落后 63 提交的树上 grep 出假阴性)。一律先 -`git fetch origin main`,再: - -```bash -git grep origin/main -- # 而不是在工作树里 grep -git show origin/main: # 看某个文件在 main 上的现状 -``` - -这条也写进派发词(step 5):dev 的 worktree 从 `origin/main` 切出后不会自己更新。 - -**5. `rerun_failed_jobs` 复用原 run 的提交与合并 ref,不会拿新的 main 重算。** 当红的 -原因是**基上缺一个已合并的修复**时,重跑本身就是无效的(#4852:直到 -`git merge origin/main` 推了新提交才拿到新的合并 ref)。判别方法:比对那个修复的合并 -时间与本 run 的创建时间 —— 前者晚于后者,就只能推提交,重跑多少次都没用。 - -> 与 `.github/workflows/rerun-safety-nightly.yml` **无关**:那查的是测试污染,不是重跑 -> 语义。 - -**6. 读数纪律 —— 四条各自产出过一个「我信了并据此行动」的错读数**(命令本身是否在回答 -你以为的那个问题;四例明细见 references §「读数纪律四例明细」): - -- **`cd X && cmd` 会短路**(路径不存在时 `cd` 失败、命令在当前仓继续执行,产出假读 - 数)。⛔ 跨仓一律 `git -C grep`,不要用 `cd`。 -- **`git grep -c | wc -l` 数的是文件数,不是命中数。** 要命中数就不要再套 - `wc -l`。 -- **裸名 grep 会被幸存家族当子串命中**(裸 `EmailTemplate` 命中活着的 - `EmailTemplateDefinition` 族)。退役核验一律**带引号精确名**;更硬的判据是查**声明 - 式**(`^(export )?(const|type|interface) \b`)而不是查提及。 -- **浅检出(shallow clone)上的历史读数不可信** —— `merge-base --is-ancestor` 给假 - 「非祖先」、`rev-list --count` 给截断值、`branch -r --contains` 零输出; - `--deepen=` 之后三条全反转。⇒ 跨仓 pin 核验先 deepen 再判,或直接走 REST - `compare`。 - -统一原则:**零命中必须用一个「确定存在的邻近词」反查**,证伪「扫描器坏了 / 路径错了」 -这个解释。没有这个反查,零命中不成立。 - -**7. CI 红了先拿完整日志归档,再下结论 —— 三条读日志的纪律。**(2026-08-03 最贵错误: -公开断定 OOM、被自己开的 PR #4853 推翻,真因是 #4796 的 5000ms 超时;全文见 references -§「#4845 误诊更正实录」。)三条各成一条: - -- **「completeness check 绿」≠「测试通过」** —— 它只断言没有 worker 静默死掉。 -- **turbo 并发输出的「相邻」≠「因果」** —— **先查 `turbo.json` 的依赖边**,再谈因果。 -- **不要只看日志 tail** —— 取完整日志归档再判。 - -诊断结论一旦公开发出又被推翻,**更正要发在同样公开的位置**,并把据它开的 PR 撤回 -draft、解绑 `Fixes`,免得错结论继续被当作已立案的事实引用。 - -**8. 共享基础设施类修复,入队前按「症状」复查 main,不按 issue 号。** -`duplicate-fix-guard.yml` 只拦同仓、同 issue 号的重复 `Fixes`(#4588 的产物);不同 -issue 号修同一个基础设施问题它看不见,后合者可能是静默回退(实录见 -`references/incidents.md` §「共享基础设施回退一例」)。规则:CI 配置、超时、构建脚 -本、门禁这类**共享基础设施**的修复,入队前跑 `git log --oneline origin/main -- -<该文件>`,确认在飞期间没有别人已经修掉;真撞上了,先比**数值与作用域**再决定关哪个 -—— 后合的那个可能是回退,不是改进。 - -**9. 立单前先查重:关键词、CVE/公告号、包名、报错串,各搜一遍再开 issue。** -(两例同日反例 #5039 / #4946 已移 `references/incidents.md` -§「立单查重两反例」。)边界要记清:duplicate-fix-guard 只拦两个 **PR** 声明同一个 -`Fixes #N`(第 8 条讲过它的覆盖面)—— 两条 **issue** 描述同一个问题,没有任何 -门禁能看见,查重只能发生在立单前、你自己手里。 - -**10. 合并前认门禁 job 的结论,不认聚合读数 —— advisory 门禁红着合并会毒化全仓。** -(#5584 标本见 `references/incidents.md` §「#5584 advisory 红合并毒化」。)两句纪律: - -- 合并前必须确认**承载门禁族的 job**(本仓即 ESLint、TypeScript Type Check —— - `check:engine-double-contract` 等门禁族都跑在其内)已达 **`completed: success`**,而 - 不是「暂时还没出现 failure」;step 7 ACCEPT 的 *once every check on the PR is green* - 要读成「每个门禁 job 的**结论**已出且为 success」,`in_progress` 不算数。 -- 「队列会把关」只对 **required** 检查成立;不在 required 集里的 advisory 门禁红着合并 - 进 main 就是**共享损伤**,**任何车道发现都要立即止血 + 立单**(#5615 止血 / #5617 治 - 理即此形,配置面归 #5617,流程面归这里)。 - -**11. dev 子代理自己死了 ≠ 维护者中止 —— 不得据推断立一道谁也不敢退的门。**(#5085 标 -本全文见 `references/incidents.md` §「#5085 推断中止误判对照」。)两句纪律: - -- 子代理消失(零推送 / 零分支 / 无报告)是**子代理的正常死法**,按 step 4 的 - **stale-claim reclaim** 处理(先探活 / SendMessage 复活,复活不成再回收重派),⛔ 不 - 得推断为维护者意图。 -- 「维护者中止」只在有**显式信号**的记录时才成立 —— 维护者原话,或宿主明确回报的 - *stopped by the user*。**判据是信号,不是症状**;没有显式信号就当死认领回收,⛔ 不要 - 立一道没有重启条件的门;真需要 hold 就照状态模型:`pm:on-hold` + 带**重启条件**的评 - 论成对落地。 - -**12. 判「正文被 sanitizer 截断」必须双读取 —— 单一读法的尾部缺失先算读取端截断。** -(三例误判实录见 `references/incidents.md` §「读取端截断三例误判」。)纪律: - -- 判截断前必须**双读取**,`body_html` 要带 full 媒体类型: - - ```bash - curl -s "https://api.github.com/repos///issues/" \ - -H 'Accept: application/vnd.github.full+json' # .body 原文 + .body_html 渲染版 - ``` - - **两者在同一处断掉**才算 issue 端截断;任何单一读法的尾部缺失都先假定是读取端截断 - (工具输出上限、分页、`[:N]` 切片)—— 与 notes 6「零命中必须邻近词反查」同一条纪律。 -- step 0 的 **Repair first** 是**停摆指令**,判据必须比其它分类更硬;已发出的重贴指令 - 若事后证伪,**要在同一处公开作废**(同 notes 7)。 -- **同一个 sanitizer 的第二种形状 —— 写侧的「就地删除」,上面两条判据抓不到它,反引号 - 也不保护**:短的 `<…>` 片段在**写入时**被就地删掉,正文其余完好、双读取一致读作 - 「完整」(三例实测全在反引号里被吃,含 `` 整段变空 —— 那是在 - 飞报告的**全部收集路径**;三例表格见 references §「sanitizer 写侧就地删除三例」)。 - 两条动作: - - - 正文里凡要保留字面尖括号,一律写 HTML 实体 `<` / `>`,⛔ 不靠反引号或围栏 —— - 实测它们不提供保护; - - 含 HTML 注释标记(如 ``)、`` / `` 一类占位符、泛 - 型参数的正文,**写后回读逐个确认这些片段仍在**,这是动作不是提醒 —— 失效完全静默, - 只有把存回的正文与你写的原文逐段对比才看得见。 - -**13. MCP 工具的两个参数语义陷阱 —— 过滤是 OR、labels 是整组替换**(#5925 两次实测, -notes 6「命令没在回答你以为的问题」的 API 参数版): - -- **MCP `list_issues` 的多标签过滤是 OR 不是 AND**:要 AND 就手工求交集,或走 REST - search ——`label:a label:b` 在 search 语法里才是 AND。 -- **`issue_write` 的 `labels` 参数是整组替换不是追加**:不先取现值合并再写,会静默剥 - 掉 `priority:p0` / `domain:*` —— 掉一个标签 = 状态机丢一位。追加用 REST - `POST /issues/{n}/labels`(真追加),或读-合-写三步;写后照 label discipline 回读。 - -**14. GraphQL 配额欠账要清单化,恢复窗口一次连清 —— `issue_write` 连读半边都吃 -GraphQL**(2026-08-08 一上午配额四度归零的沉淀,含一次「读成功写被拒」—— 读写配额独 -立): - -- **`issue_write` 的查找半边也是 GraphQL**:配额红时连「改标签」都失败在 `failed to - get issue ID` —— 认领类动作在配额红期就是排队,评论(REST)可先行落地把结论发出去; -- **欠账列成有序清单挂进巡逻词**,不靠记忆:恢复后按序连打(ready → auto-merge → - 入队可在同一恢复窗口一气完成); -- 每轮巡逻**先探一个最便宜的 GraphQL 写**当配额读数,红了立刻回事件流,不逐个试报错。 - -**15. 并行 spec PR 都动 pin 计数断言时,队列会踢后进者 —— 收据按合并顺序堆叠,计数从 -文件重数**(#6512 实测,同一分支解了两轮)。规程: - -- 解冲突时**两侧收据都保留**,按合并顺序堆叠,新计数**从合并后源码重数** - (`grep -c '^export type Iso'`),⛔ 不从两侧收据做算术 ——「the file, not the - history, is the operand」(#6526 收据注释原文); -- 双方都占用同一个 Iso 编号是常态(各取当时 max+1),重编号**后进侧**; -- 预期这种 PR 被踢不是事故:按 os-regen 四步再解一轮即可,不必预防性串行化两支 PR。 - -**16. findings sweep 晋级与 sweep 立卡,前提核查要对「此刻的 main」—— 同族 ADR 的 -phase 提交能整体吸收成员**(#6488 两成员被 ADR-0122 phase 2 整体吸收,派发后 dev 动工 -前置门才发现零剩余工作)。晋级/立卡不是免检通道:与派发一样要吃「前提对此刻 main 成 -立」判据,同族里有在飞或新落 ADR phase 提交时尤其要查其合并时间线。dev 的正确产出是 -**带证据的零实现停手**(空提交承载报告),PM 抽验后关卡 —— 流程产出的「好的失败」, -勿当返工计。 - -**17. 裁决明令的动作在实施中测出对向事实 —— 照裁决执行、如实反转 pin、不 promote、立 -独立决策卡、扣 auto-merge 留异议窗口**(#6483 裁决① vs ADR-0094 方向确认即此形)。处置 -形状全套缺一不可:按裁决字面执行;把被打断行为的功能 pin **反转为拒绝 pin**(不是删 -除);⛔ 不在同 PR 里做任何 promote/回退;PM 把冲突立成 `needs-user-decision` 卡 -(A/B/C + 推荐);该 PR **不挂 auto-merge**,合并前留给维护者显式异议窗口。既不拿新事 -实推翻已下的裁决,也不让新事实被合并流程静默碾过。 - -**18. 容器重启杀死在飞 dev —— 现场三态判读,四步 regen 中途的可机械续作**(2026-08-08 -一次重启同杀四个容器内 dev;云端工头独立容器不受影响)。恢复前先对每个现场三态判读: - -- **分支已推 + PR 已开**:只欠验收 —— CI 重跑 + step-7,不动代码; -- **死在四步 regen 中途**(工作树里未提交的全是生成物,merge commit 已在):PM 直接续 - 作 —— build → 整链 regen → `check:generated` 10/10 → 提交推送;恢复 commit 统一 - `Recovery commit:` 前缀留审计;⚠️ 有的现场 regen 一件没跑(#5628),推送前先跑一遍 - `check:generated` 别赌; -- **死在源码编辑中途**(未提交的是 src):先读 diff 判完整性 —— docblock 把动机/失效 - 模式/判据写全的(#5583 实测),PM 可代跑其终验(eager 重现路径 + 定向 + 全量)后提 - 交;写了一半、意图不明的,⛔ 不代提交,记进交接。 - - dev 的 `.os-scratch/` 一类临时目录是工作物不是交付物,清掉,⛔ 不进 feature PR。 - -**19. spec 改动的 fixture triage 必须跑消费包测试 —— B 包的坏 fixture 只有 -消费者面能跑红。** #6287 的映射变更让 metadata-protocol 一条 conformance fixture -反着断言,spec 范围内任何 sweep 都看不见它(PR #5046 同族:改在 A 包,坏 fixture -在 B 包)。派发令里「消费者面跑一遍」不是礼貌性复测,是唯一能抓到这类断裂的一步 -—— step 5 的派发模板对动契约面的单,消费包测试清单要点名列出,dev 报告里要有 -各消费包的真实读数。 - -**20. 硬前置要的仓不可达时 —— 请对应座位代跑并回贴读数,勿派勿硬做。** #4914 的 -裁决硬前置是 cloud/objectui 两仓裸名反查,实测本席凭证 `add_repo` cloud 被拒 -(工头会话同样看不见该仓)。处置:在 issue 上贴出**给对应座位的现成命令**(含 -notes 6 的对照反查),等读数回贴再派;⛔ 不因「查不了」就当「查过了干净」—— -不可达 ≠ 零命中,这是 notes 6「缺失类读数先证伪扫描器坏了」的跨仓版。 - -**21. `enable_pr_auto_merge` 的空字段返回(`method: , enabled at `)对「入没入队」零 -区分度 —— 先验成员资格;⛔ enable 与它的队列验证之间永不插 `disable`。**(处方并入 -notes 1:维护者 2026-08-11 裁定,#7492;正例 3 + 反例 3 + 对照组实测见 references -§「auto-merge 空字段返回正反实测」,乱序撤单反例见 §「#7655 armed 窗口 disable 撤单 -实测」。)**签名本身不构成任何方向的证据**,且「入队」webhook 可能**乱序迟到** —— 首次 -enable 可能已真实入队,armed 窗口里补的 `disable` 会把它撤掉(#7655)。序列:① 空字段 -返回 ⇒ **先验队列分支**(`git ls-remote --heads origin -'refs/heads/gh-readonly-queue/*'`,给条目 ~20–30s 建出);② 分支在 ⇒ 结束,⛔ 不翻转; -③ 等待后仍缺席**且队列已见 churn**(更新的条目建出 -了分支而你的没有 —— `max_entries_to_build` 截断下缺席单独不充分,notes 1 一字不变) -⇒ 翻转**一次**(`disable` → `enable`),翻转后仍以 notes 1 判据(timeline 事件)验证; -④ 已入队(成员资格已落定)后补 `disable` 不解除成员资格(只有转 draft 才解除),但 -**会清掉 auto-merge 旗** —— 踢出后不自动重挂(#7446/#7506,全文同节)。 - -The product spans three repos with a fixed dependency direction: -`objectstack` (backend; `packages/spec` is the single contract) → -`objectui` (frontend; its build flows back via `pnpm objectui:refresh`) and -`cloud`. The loop coordinates them with five rules: - -**1. Issues live where the fix lands**(维护者 2026-08-10 裁决,#7165;取代原 -「One main backlog」条款). An execution card is filed in — and lives in — the -repo whose code the fix changes. `objectui` / `cloud` each run their own -backlog under their own whole-repo seat (rule 4), with the same pm labels and -the same one-producer-per-backlog discipline(发版板一节,#6903). Two -deviations, both deliberate: - -- **Exception A — the maintainer's inbox stays `objectstack`.** The - maintainer keeps filing everything into `objectstack` without pre-routing; - the 分诊座位 routes at triage (round loop step 2), and for a card that - lands in a sibling repo routing now means **transferring the issue to that - repo** — GitHub issue transfer preserves the thread and redirects the old - URL. When transfer is unavailable to the session's credential (fine-grained - / app tokens cannot call it — #7167 实测 404), recreate at the destination - with a provenance header, rewrite bare `#N` references to full - `owner/repo#N` form, and close the source as moved(#7167 的迁移形状). -- **Exception B — seam cards stay in `objectstack`.** A card whose content - IS the cross-repo ordering — a contract-first chain per rule 2 (spec - change with downstream adaptation), a console-bump linkage chore (rule 3), - anything whose `Blocked-by:` line against an objectstack artifact is the - substance — stays in the main backlog and carries the `repo:*` label. - Test: would the body still make sense if objectstack did not exist? - Yes ⇒ file at destination; no ⇒ seam card. In-flight (`pm:dispatched`) - cards are never transferred mid-dispatch — they exit via their PR. - -The `repo:objectui` / `repo:cloud` labels are thereby narrowed to seam cards(⛔ do not -delete the labels; their descriptions carry the new meaning)。The dev still -branches/pushes/PRs **in the target repo**(one worktree per repo, as always)。To -drain a sibling backlog, run `/pm-dispatch repo:objectstack-ai/objectui` — since this -ruling that queue is that repo's primary backlog(存量 `repo:*` 转移卡已按 #7167 一次 -性迁移,只剩缝卡与在飞卡)。 - -**2. Contract-first splitting.** A cross-repo feature is never one dispatch. -Split it: a parent issue plus one sub-issue per repo (native GitHub -sub-issues), and the **spec/backend sub-issue goes first** — it fixes the -contract. Downstream sub-issues carry a body line -`Blocked-by: #`. The PM never dispatches an issue whose -`Blocked-by` references are not yet closed (issue) or merged (PR) — verify -against GitHub at selection time, not from memory. Batch independence is -cross-repo: two issues linked by `Blocked-by` or sharing a parent never -ride in the same batch. - -**Pin 滞后 ——「上游已合入」不等于「本仓已看见」(rule 2 的盲区)。** rule 2 只要求 -上游**已合并**;姊妹仓消费 framework 时还有第二个读数 —— 本仓的 pin 是否已覆盖那个 -commit(cloud#1116 标本全文见 references §「pin 滞后 cloud#1116 标本」)。规程两条: - -- **派发前核祖先关系**(REST `repos///compare/...` 的 `status` - / `ahead_by`)—— 本地 `merge-base --is-ancestor` 在 shallow 检出上解不出 pin 的 - commit、以 `fatal:` 退出,在 `&&` / `||` 链里会被读成「不是祖先」(notes 6 那类假 - 读数); -- 未覆盖 ⇒ 派发令要求 dev 在 **PR 正文留档分叉窗口与方向**,且 ⛔ **pin bump 不做 - rider**(`.objectstack-sha` 是共享文件,要走 `scripts/bump-objectstack.sh`,连带 - hono override 与 lockfile 重生)。 - -滞后本身已有读数:cloud 的 `pnpm check:pin-staleness`(**有意的 advisory**,不设阈 -值);它回答「落后多少」,**不是**「是否覆盖我这条裁决 commit」—— 后者只有派发前那 -一次祖先判断能回答。 - -**3. Linkage chores are issues, not memory.** When an accepted PR's -artifacts flow into another repo, the PM immediately files the follow-up in -the consuming repo's backlog instead of relying on anyone remembering. The -known case: accepting a `repo:objectui` PR ⇒ file a `pm:queue` issue in -`objectstack` — "run `pnpm objectui:refresh` and land the console bump", -referencing the merged PR, blocked-by it until it actually merges. 立单者是 -**接受那个 PR 的执行座位**(它才知道产物流向哪里);`domain:*` 仍由分诊座位补 -—— 链接类杂事天然带 `Blocked-by:`,等一个分诊周期不损失任何东西,而多一个 -`domain:*` 生产者会损失 rule 4 的全部机械保障。 - -**4. 纵向拆分:一个分诊 PM + N 个执行 PM,一人一车道双射**(维护者 2026-08-05 拍板, -#5472)。协调税降为**结构性防撞**,不靠自由文本申报互相躲;角色**纵向**拆开,所有权 -是**双射**(动因全文见 references §「rule 4 双射动因」): - -- **分诊 PM(全仓唯一)** — 只扫、只分类、只打标签(`domain:*` / `pm:queue` / - `finding` / `needs-user-decision` / `repo:*`)、只拆跨域 issue、只查重、 - **只转仓**(落点在姊妹仓的非缝卡按 rule 1 例外 A 转移过去 —— 转移是路由动作 - 的一种,不是越界)。 - ⛔ **永不认领、永不派发、永不写代码。** 它是 `domain:*` 的**唯一生产者**, - 于是「未打标签的 issue 谁都不得认领」这条纪律第一次有机械保障:标签只有一个 - 产出者,缺标签就意味着分诊还没走到它,而不是某个 PM 可以自己判一下。 -- **执行 PM(N 个)** — 信任标签、**跳过分诊**(round loop 的 step 0 与 step 2 - 分类半边不属于它),只在**本车道**认领与派发。发现标签错了**不自行改**:在 - issue 上留一句「疑似域误标:落点在 X 包」交分诊座位改。单一生产者是这套协议 - 唯一的防撞机制,两个生产者等于没有。 -- **双射** — 每个执行 PM 恰好持有**一个** `domain:*` 车道,每个车道恰好一个 - PM。「域 X 谁管」与「PM Y 管什么」都**恰好一个答案**,不需要读任何评论流。 -- **越界许可(旧条款的 borrowing)已删除**,本条是全文唯一一处提及,作墓碑用。突发 - 积压 → 调高该座位频率或 `batch`(`batch:5` 是维护者选定的运行点;重活走 - `mode:cloud`);持续积压 → **拆域**:改 SKILL 域表 + 座位表加行,**走 PR**。⛔ 不 - 借调 —— 那会把「这个域现在谁管」变回要翻评论才知道的事实。 -- **姊妹仓仍是整仓座位。** `repo:objectui` / `repo:cloud` 各占一行,同一套双射 - 与登记规则,接管方式不变(`/pm-dispatch repo:objectstack-ai/objectui`);域车道 - 是「同仓多 PM 并发」的切法,不是第二套仓库标签。自 rule 1 的 file-at-destination - 裁决起,姊妹仓座位是**本仓 backlog 的唯一分诊/定级生产者**(含 `target:`, - 见发版板一节),其收件箱包括按例外 A 转移进来的卡。 -- **维护者直派通道(2026-08-10 常设授权)。** 维护者当面指挥的 PM 会话在立卡/ - 裁卡时**直接路由**(打 `domain:*`,或按 rule 1 例外 A 直接转仓),审计评论逐字 - 引用授权指令(「以后你应该直接路由」)。这不是第二个自然生产者:它只对维护者 - 明示指挥下经手的卡成立;自然增量的扫描分诊仍是分诊座位的单通道。 -- **分诊座位空缺时**:欠账由任一**会话型**执行 PM **代扫** —— 只做分诊动作 - (标签、拆分、查重、审计评论),⛔ 代扫不解除双射,仍不得跨车道认领 —— 并在 - 自己座位贴「说明」段写明处于代扫状态。座位有主后代扫立即停止。 -- **分诊座位在任时,车道 PM ⛔ 不自跑 step 0 与发现分诊轮** —— 代扫仅限空缺期。 - 反例是 #5367:分诊 Routine 17:00 晋级、车道 18:08 持有,70 分钟内对同一 - finding 出了**相反处置**;两个分类产者并存,单一生产者的全部保障当场归零。 - 三条配套:`finding` 的**晋级/持有是分诊座位的单通道**(车道对定级有异议走 - 上报,不自行改);处置评论必须**连带改标签**(那次的处置评论写「留 finding」 - 而标签是 `pm:queue` —— 评论与标签失同步,正是 label discipline 成对纪律禁止 - 的半状态);车道在 finding 上只做两件事 —— 补充实测证据、上报异议。 - -**跨域例外路径 —— 唯一的越界通道。** 真拆不动的跨域单 PR(拆分成本大于收益, -判据同 rule 2 的 contract-first 拆分):由**分诊 PM 指定一个**车道 PM 认领,该 -认领评论**申报完整文件面**,并且只在这条路径上跑**定向在飞检查**(范围与触发 -条件见「Domain lanes」)。全局在飞检查因此从每轮常备税**降级为例外路径专用**: -旧条款要求每个 PM 每次批次选择都扫全仓在飞单,开销 O(PM 数 × 在飞数)、每轮 -重复,而判据只是自由文本申报 —— 越贵越不可靠。 - -**座位贴协议 —— 一座位一贴,单写手(维护者 2026-08-06 拍板,取代单正文座位表)。** -多写手共编一个 body 在机制上防不住互吞(实录见 `references/incidents.md` -§「单正文座位表互吞」),所以架构是**每个座位一张登记贴**: - -- **贴 = 座位**(分诊、队列管家、各 `domain:*`、姊妹仓整仓),打标签 **`pm:seat`**, - 正文固定六段模板(维护者 2026-08-11 接受,#7583):**范围 | 当前 PM | 继承台账 | - 热文件串行队 | 常设承诺 | 说明** —— 仅此六段,段段皆为**现值**,没有一段随时间 - 增长。`label:pm:seat` - 即全量索引(与 `pm:epic` 同构);#4604 只作**指针页**(座位 → 贴的静态索引, - 仅拆域加座位时更新)。**单写手**:贴正文只由**在任座位 PM** 编辑,互吞类 - 问题机制性消失,而不是纪律性缓解。 -- **标题即状态板(维护者 2026-08-06 拍板)**:座位贴标题固定格式 - `[PM seat] <座位名> — <状态>`,状态词表:`🟢 `(在任,账号已知)/ - `🟢 Routine`(Routine 座位)/ `⏳ vacant`(待认领)/ `⏸️ paused`(维护者暂停)。 - `label:pm:seat` 的列表页因此就是全舰队状态板,不必逐贴点开。**标题只放慢状态** - (在任者/空缺/暂停 —— 换班级频率);轮次、在飞、队列快照等快状态 ⛔ 不进标题, - 留在正文「说明」段。标题是正文「当前 PM」段的**派生视图**:两者**同笔更新**(成对 - 纪律),正文为权威(附带收益:标题改动是平台盖章的独立时间线事件)。 -- **assignee = 在任 PM 的 GitHub 账号(维护者 2026-08-06 拍板)**:接管时把座位贴 - assign 给自己的账号,退场/回收时摘除 —— 列表页显示头像,**无 assignee = 空缺**, - 与标题 `⏳ vacant` 互为校验。例外:Routine 座位(bot 身份通常不可被 assign) - 以标题 `🟢 Routine` 为准,assignee 留空。标题、assignee、正文「当前 PM」段 - **三者同笔更新**(成对纪律的三元版),正文为权威。 -- **「当前 PM」段固定登记三元(维护者 2026-08-06 要求可辨识 GitHub 用户)**:**GitHub - 账号**(`get_me` 自查 login —— 舰队实际多账号在岗,「共享单一身份」旧假设不再全真) - + **会话 ID 或 Routine ID** + **上任时刻**。平台盖章的硬读数:该座位审计/认领评论的 - **作者字段**不可自述错 —— 接管仲裁与活性判定优先用它对账正文。 -- **接管 / 移交 = 改该座位贴正文 + 在该贴留一条审计评论**;**评论只作交接审计, - 不承载状态** —— 不要靠读评论流对账现状(实测教训:#4604 曾三天累积 79 条登记 - 评论,「现状」与「历史」挤在同一通道,对账成本随评论数线性涨)。 -- **班次叙事不进贴正文(维护者 2026-08-11 接受,#7583)。** 逐卡状态叙事在卡与 PR 上 - 本来就是必填的,轮次合计走 step 9 的轮次报告 —— 贴正文只在**接管、移交、结构变更** - 时编辑,⛔ 不逐事件追加(#6019 曾累积至 ~61 KB 超出单次读取上限;六段模板段段现值, - 天然有界)。 -- **接管压缩是标准步骤(#7583;#6019 两次先例)**:接任 PM 把交接的有效内容按 - 六段模板重述为新正文,**编辑历史即存档** —— 旧账留在上一版 body revision 里, - 审计评论带指针(哪一版、何时)。实测:2026-08-11 的 #6019 接管把 ~61 KB 压至 - ~4.5 KB 现值,零信息丢失。机械守卫:`scripts/pm/check-half-states.mjs` 的 **H6** - (report-only,正文 >10 KB 的 `pm:seat` 贴)让超大贴在巡查里浮出,不等读取失败。 -- **残余竞态纪律(唯一还剩的多写手场景是空缺座位争用)**:动手前**重新 fetch - 贴正文**;审计评论**时间戳先到先得**(同 step 4 认领竞态的裁决方式);**写后 - 回读**核对。新增座位贴(拆域)同样先查 `pm:seat` 索引再立贴 —— 加贴与改贴 - 一样要先读现状。 -- **无心跳。** 座位不定期报活;活性是**惰性判定**,只在**接管冲突**时评估一次 - —— Routine 座位查调度器(`last_fired` / `next_run`),会话座位查它最近一条 - 产出评论的时间戳,**>24h 无产出即可回收**(改贴正文 + 一条审计评论)。子树/ - 批次里在飞的认领仍按认领协议由原认领者跟完。 -- **每轮巡检核对自己的座位贴正文。** 协议或结构升级会迁移状态(单正文表时代 - 实测过一次:spec 座位在迁移后被误记为「⏳ 待认领」,差点被惰性回收误伤)。 - 自查一贴的成本是零;发现不符,当场改正文 + 审计评论,不等冲突发生。这与 - 「从 labels 重建状态」同源:贴正文也是状态,读它、修它,不靠记忆。Routine - 座位的**收尾简报**也落自己的座位贴(它是下一轮自退守卫的读数)。 -- **热文件串行队 —— 正文里给它一个具名段。** 热文件的**排队顺序**是跨轮、跨班次的 - 常设事实,接任者必须能直接读到。段内三列:**文件 → 有序卡片清单 → 每张卡认领的是 - 哪个区域**;区域列是关键 —— 不相交区域写清楚才敢并行,写不清楚就只能整文件串行 - (`domain:metadata` 席一文件五卡、连续三次同文件派发零碰撞的实测见 references - §「热文件串行队实测」)。 -- **交接收尾清单(座位退场序列,漏一步就是给接任者留残缺现场)**: - 1. ⛔ 立即停止新派发(交接令生效即冻结); - 2. 在手工件清零 —— 在飞 dev 收单、已 ACCEPT 的 PR 跟到 MERGED 或明确移交; - 3. 全量枚举本车道队列 + 决策箱 + findings,形成接任台账; - 4. 本座位贴正文改写为「⏳ 待认领」+ 完整台账(前任会话 ID、注销时间、账面、 - 队列快照、**热文件串行队**、跨车道备忘); - 5. 审计评论存档(session ID + 接任指引:`/pm-dispatch 接手` + 先读座位贴); - 6. `list_triggers` 清理**或随移交物转交**本会话全部自设定时器(⛔ 不再重挂; - 守夜随座位移交 —— 绑着移交中 PR 的定时器随该 PR 转交,不清); - 7. 向维护者交最终报告(含本任期沉淀的 SKILL 更新建议清单,查重后立单 —— - 换班复盘是交接的固定产物,不是可选项)。 -- **epic 委托不入座位贴体系** —— `pm:epic` 父单正文自带会话与领地, - `label:pm:epic` 即全量索引(见「Epic 子树车道」)。座位贴只记常设座位,一件 - 事只记一处。 -- **`packages/spec` 恒归 spec 座位**(见下「shared contract surfaces have one - owner」),无论谁需要它。 - -**跨座位转移协议 —— 工作跨座位线,PM 永不跨。** 当一个座位的任务(或其父 -issue 的子任务)需要另一个座位范围内的改动 —— 姊妹仓座位与域座位同理: - -- **Transfer via the target queue**: file the piece as an issue in the - target repo with `pm:queue` and a source line (`Part of - #`). The target seat's PM picks it up through its own - backlog sweep — the queue label IS the inter-PM channel; PMs never need - to talk directly, and never dispatch into a repo whose in-flight batch - they cannot see(在飞可见性是本协议的全部前提:看不见的批次撞不掉)。 -- **Dependencies via `Blocked-by:`** on the waiting side; the waiting PM's - batch selection skips it until the upstream merges. -- **Follow-up chores belong to the consuming seat**: when the upstream - change lands (say spec gained a key), the dependent-repo adaptation issue - is filed by the PM of the repo/lane that consumes it — it knows its surfaces. -- **Shared contract surfaces have one owner**: anything touching - `packages/spec` transfers to the **`domain:spec` 座位** regardless of who - needs it — only that seat sees the spec queue's in-flight batch and the - generated-baseline collisions(`.gitattributes` 里路由到 `merge=os-regen` 的 - 那组路径,**条数以 `grep os-regen .gitattributes` 当场为准**,见「入队与落地 A」)。 -- 跨仓 parent/sub-issue 链的**拆分与排序**由**分诊座位**一次做完(rule 2 的 - contract-first 拆分 + `Blocked-by:` 行),各段的落地由各自座位按依赖顺序 - 自然接续 —— 没有第二个协调者,也不需要有。 - -**5. One board, no second tracker —— org Project 是视图层,不是权威层。** The pm -labels above are the state machine;org 级 Project 只是把三个仓聚合成维护者的单一 -视图: - -- **没有任何机器读它** —— 候选获取、认领、在飞检查、僵尸回收、轮次报告全部读 issue - 标签与 `pm:seat` 座位贴正文;一旦有判据读它,它就是 rule 5 禁止的第二个 tracker, - 而且是一个**没有历史**的 tracker。 -- **权威层坚持 issue 正文 + REST**(Project 只有 GraphQL 入口,配额实测极易打满 —— - notes 3),**Project 绝不进循环的热路径**;视图层可在配额耗尽时不可用而循环照跑。 -- PM 在 GitHub 之外**不维护任何跟踪状态** —— 这条不变量是循环可从零本地状态恢复、 - 看板不说谎的原因。 +| open + 队列标签 + **无 assignee** | 可派发 | +| **assignee 已设** | 已认领/在飞 —— 不是你的就永不碰 | +| `pm:dispatched` | 已派发(派发评论记轮次);与摘 `pm:queue` **同一次标签写入**成对落地 | +| `needs-user-decision` | 决定**待做** —— 永不派发、除已裁代裁通道外永不代答;维护者的收件箱 | +| `pm:on-hold` | 决定**已做**且答案是「不是现在」—— 不派发也不催;hold 评论带**日期、理由、重启条件** | +| `pm:blocked` + 正文行 `Blocked-by: #N` | 等上游 —— 选择期跳过,#N 关闭时由解锁扫描放回 | +| `finding` | 观察类记录 —— 持有态,待发现分诊轮定级,不占队列不进收件箱 | +| `target:` | 发版阻塞 —— 每个 backlog 恰好一个生产者,见「发版板」 | +| `pm:epic`(父单) | 子树已委托 epic PM(会话与领地在父单正文;`label:pm:epic` 即全量索引)—— 其它 PM 不把其 sub-issue 当候选 | +| `pm:seat` | 座位登记贴 —— 协议载体,不是待分诊的工作 | +| `priority:p0` | 插队:可超 `batch`、破轮次立即派发;⛔ 不豁免同文件串行与认领协议 | +| open PR 引用该单 | 已实现,复核中 | +| merged PR 带 `Fixes #n` | 完成(GitHub 关单) | + +**标签纪律 —— 标签就是状态机,必须诚实:** + +- `needs-user-decision` vs `pm:on-hold` = **决定待做 vs 已做**,挂错招来重复升级或永久沉默 + ;hold 没有重启条件 = 谁都无法合法退出的状态。**机会主义重启条件必须点名触发文件**(维护 + 者 2026-08-11 接受)——「下个碰这些文件的 PR 顺手带上」在命中那一刻没有读者;处方:hold 评论写 + 触发文件清单,车道座位贴设「派发前必查」段,派发卡文件面与清单相交时点名该单、顺手活列为申报 + 过的增项。 +- **`Blocked-by:` 行是机器可 grep 的反向索引**,一遍读喂三个职责:上游关单时放回被解锁的、按解 + 锁扇出排序选择、**在合并后的 ref 上重验每张回队卡的文件面**(⛔ 只做第一件)。**一个标签存在 + ,当且仅当有具名读者**,无读者的标签必烂;已议已拒(维护者2026-08-11):「被依赖」是随上游关单衰 + 变的推导属性,从行现算,⛔ 不落存储标签。 +- **状态变更不过夜**:标签挂了评论没跟上、assignee 设了没认领评论、结论只在 chat ——都是半状态 + ,结束会话(含限流悬挂)前补齐成对或回滚半边(机械辅助 + :`node scripts/pm/check-half-states.mjs`,report-only 巡查)。**代执行他人指令的关闭/作废,评 + 论带出处三件(谁的指令、原话、在哪说的)** —— 无出处的关闭与误操作在证据上不可区分,会被兄弟 + 席当误扫重开;同理适用于摘标签、回收认领等不可反推理由的动作。 +- **写后回读**:每次写正文、评论、标签,发出后读回校验 —— sanitizer 会静默吞内容, label bot 的 + 整组 PUT 会冲掉刚打的手工标签;「API 返回 200」≠「落地内容正确」。 + +**一次性建标签**:`bash scripts/pm/ensure-pm-labels.sh`(幂等)。⛔ 退役的`domain:engine` / +`domain:ui` 不在脚本里也不要加回去 —— 重建退役标签就是把无主车道放回自动补全;两者存量已清零 +但**标签对象仍在**,删除对象是 PM 的一步单独动作,删掉前别当它们不存在。 + +## 平台读数纪律 + +统一原则:**判据取命令的输出,不取 API 字段的字面值、不取本地工作树现状、不取「看起来相邻」的 +日志。** GitHub API / 工具行为的实测事实表(队列成员资格与 auto-merge、API 配额、读数陷阱、截 +断双读取、容器重启三态)在`references/platform-readings.md`,做对应操作的那一刻查阅。常驻原则: + +- **核验 main 用 `origin/main`**(先 fetch,再 `git grep origin/main -- ` / + `git show origin/main:`),⛔ 不用共享检出的工作树 —— 它的 HEAD由别的 agent 摆布。 +- **零命中必须用确定存在的邻近词反查**,否则零命中不成立;仓不可达同理,⛔ 不因「查不了」当「查 + 过了干净」—— 在 issue 上贴出给对应座位的现成命令,等读数回贴再派。 +- **定时器文本纪律(一切自设定时器适用)**:已删除的定时器仍会投递,投递时文本可能落后现实数 + 轮 —— 每一枪定点文本**以「幂等 —— 动手前先重读状态」开头**,只写判据(「若 X 则 Y」),⛔ 不写 + 结论、不含未经重读即可执行的祈使句。 +- **合并前认门禁 job 的结论,不认聚合读数**(承载门禁族的 job 须 + `completed: success`,`in_progress` 不算);advisory 门禁红着合并进 main 是共享损伤,任何车道 + 发现都立即止血 + 立单。落地判据永远两个读数:队列成员资格(timeline事件,⛔ 不 + 是 `auto_merge` 字段)和 `origin/main`;踢出重投先认签名(见事实表)。 +- **dev 自己死了 ≠ 维护者中止**:子代理消失(零推送/零分支/无报告)是正常死法,按stale-claim + reclaim 处理;「维护者中止」只在有显式信号(原话,或宿主回报stopped by the user)时成立 —— 判 + 据是信号不是症状,⛔ 不得据推断立一道没有重启条件的门。 +- **立单前查重**(关键词、CVE/公告号、包名、报错串各搜一遍 —— 两条 issue 描述同一个问题,没有 + 门禁能看见);**共享基础设施修复,入队前按「症状」复查 main,不按issue 号**(重复修复守卫只拦同 + 仓同号,后合者可能是静默回退;真撞上先比数值与作用域再决定关哪个)。 +- **sweep 晋级与立卡的前提要对「此刻的 main」**(同族 ADR 的 phase 提交能整体吸收成员;dev 带 + 证据的零实现停手是好产出,不当返工计)。**裁决明令的动作实施中测出对向事实** ⇒ 照裁决字面执 + 行、被打断行为的 pin 反转为拒绝 pin(不是删除)、⛔ 同 PR 不做任何 promote/回退、冲突立 + 成 `needs-user-decision` 卡、该 PR 不挂 auto-merge 留异议窗口。 +- **spec 改动的 fixture triage 必须跑消费包测试**(A 包的改动可让 B 包的 fixture 反着断言 + ,spec 范围内任何 sweep 都看不见)—— 动契约面的派发令点名消费包测试清单,报告要有各消费包真实 + 读数。 + +## 多仓协调(五条规则) + +产品横跨三仓,依赖方向固定:`objectstack`(后端;`packages/spec` 是唯一契约)→ `objectui`(前端,构 +建产物经 `pnpm objectui:refresh` 回流)与 `cloud`。 + +1. **Issue 住在修复落地的仓**(维护者 2026-08-10 裁决)。两个例外:**维护者收件箱恒 + 为 objectstack**(分诊时转仓路由;transfer 不可用时重建:出处头 + 裸 `#N` 改全名 + 关源单 + 为 moved);**缝卡留在 objectstack** 带 `repo:objectui` / `repo:cloud` —— 判据:正文抽 + 掉 objectstack 还成立就 file-at-destination,不成立才是缝卡。在飞卡 ⛔ 不中途转仓。 +2. **Contract-first 拆分。** 跨仓 feature 永不是一次派发:父单 + 每仓一 sub-issue, spec/后端 + 先行,下游带 `Blocked-by: #`;凡 `Blocked-by` 未关闭/未合并的不派发( + 对 GitHub 现验);被链接或同父的两单永不同批。**Pin 滞后是它的盲区**:本仓 pin 是否已覆盖那 + 个 commit 是第二个读数,派发前用 REST `compare` 核祖先关系(本地 `merge-base` 在浅检出上给 + 假「非祖先」);未覆盖 ⇒ 派发令要求 PR 正文留档分叉窗口,且 ⛔ pin bump 不做 rider(走专 + 用 bump 脚本连带 override 与 lockfile)。 +3. **联动杂事立单,不靠记忆。** 已验收 PR 的产物流向另一仓时,由**接受那个 PR 的执行座位**立即 + 在消费仓立后续单(带 `Blocked-by:`);`domain:*` 仍由分诊补。 +4. **纵向拆分:一个分诊 PM + N 个执行 PM,一人一车道双射**(维护者 2026-08-05 拍板)。分诊座位全 + 仓唯一,只扫/分类/打标签/拆分/查重/转仓,⛔ 永不认领、永不派发、永不写代码;执行座位信任标签 + 、只在本车道认领,误标**不自行改**、留言上报。⛔ 借调已删除:突发积压调频率或 `batch`,持续 + 积压拆域走 PR。姊妹仓是整仓座位(`repo:*`),是本仓 backlog 的唯一分诊/定级生产者。**维护者 + 直派通道** (2026-08-10 常设授权):维护者当面指挥的 PM 会话直接路由,审计评论逐字引用授权指 + 令,只对明示指挥的卡成立。分诊空缺时会话型执行 PM 可**代扫**(只做分诊动作,不跨车道认领,座 + 位贴注明),座位有主立即停止 —— 两个分类生产者并存,单一生产者的保障当场归零。 +5. **One board, no second tracker。** pm 标签就是状态机;org Project 只是维护者的聚合视图,权 + 威层坚持 issue 正文 + REST。 + +**跨座位转移**:工作跨座位线,PM 永不跨 —— 落到对方队列(目标仓立单带 `pm:queue` +出处行),依赖 +走 `Blocked-by:`;后续杂事由消费侧座位立;凡触 `packages/spec` 一律转`domain:spec` 座位(唯一所 +有者),不论谁需要它。 ## Domain lanes(同仓多 PM 并发) -车道是 rule 4 双射的落地层:**分诊座位的一次判断被缓存成机器可读的 -`domain:*` 标签**,执行座位的批次选择因此在**标签层**过滤,而不是在合并时才 -发现撞车。前提:每个 PM 是**自己的会话、自己的容器** —— 加一个 PM 是加算力, -不是加争用;防撞靠 域→包 的映射,不靠「希望两个 PM 挑到不同的活」。 - -**Anchoring rule.** The whole scheme rests on this one sentence: +**Anchoring rule(全案压在这一句上):** > Every package belongs to exactly **one** domain; an issue's `domain:*` > label is the domain of **the package the fix lands in**, decided at triage > by reading the code — **never guessed from the issue's title vocabulary**. - -The counter-example that makes it a rule: #4775 reads as automation, but the fix lands -in `packages/objectql/src/hook-wrappers.ts` ⇒ `domain:engine-core` — labeling by topic -would have routed it to a different PM than the one already inside that package. **If -you cannot say which file the fix touches, you have not triaged it yet, and it is not -labelable.** +> 说不出修复碰哪个文件,就还没分诊完,就不可标。 | 标签 | 包家族 | |:--|:--| -| `domain:engine-core` | `packages/objectql`、`packages/core`、`packages/formula`(CEL / `matches-filter` / RLS 谓词求值)、`plugin-pinyin-search`(`__search` 伴生列由 SchemaRegistry 声明、engine 把它 OR 进 `$search`,落点在编译/查询核心而非任何 driver;全局写钩子同 #4775 锚定) | -| `domain:metadata` | `packages/metadata*`(metadata service / registry / directory:加载、注册、持久化、缓存、目录)、`packages/platform-objects`(内置平台对象定义)—— 见下「`engine-core` 再拆 `metadata`」 | -| `domain:drivers` | `packages/drivers/driver-*`(`driver-memory` / `driver-mongodb` / `driver-sql` / `driver-sqlite-wasm`) | -| `domain:services` | `packages/services/*`、`packages/connectors/*`、`packages/triggers/*`(flow 触发器)、`packages/plugins/plugin-approvals`、`plugin-webhooks`、`plugin-email`、`plugin-reports`、`embedder-openai`、`knowledge-memory`、`knowledge-ragflow` | -| `domain:identity` | `packages/plugins/plugin-auth`、`plugin-security`、`plugin-sharing`、`plugin-audit` | -| `domain:devx` | `packages/lint`、`packages/sdui-parser`(仅 lint 消费)、`content/docs/**`、`apps/docs`、`scripts/`(门禁类)—— 其中 `packages/lint` / `content/docs/**` / `scripts/` 与 `domain:spec-tooling` 按「是否围着 spec 契约转」切分,见 `spec-tooling` 行;`skills/**` 已拆给 `domain:skills`(下一行) | -| `domain:skills` | 两个技能根:`.claude/skills/**`(内部 agent 技能,含本协议文件)+ `skills/**`(对外发布技能)—— 自 `domain:devx` 拆出(维护者 2026-08-11 裁定,#7548 第二裁:「我认为所有的 skills 任务应该单独一个项目经理负责,专题和我讨论」)。本域卡片与维护者走**专题讨论**通道;skills 更新是 ADR-class,人工审核合并(见 Guardrails)| -| `domain:spec` | `packages/spec` 的**语义面**:schema 形状、`contracts/**`、退役的行为半边、strictness 台账 —— 判据是**接受面变化**,见下「`spec` 一分为二」 | -| `domain:spec-surface` | `packages/spec` 的**文本面**:describe/JSDoc/墓碑迁移散文/错误 guidance 与 alias 表 —— 校验不变量卡,changeset 恒 patch,见下「`spec` 一分为二」 | -| `domain:spec-tooling` | `packages/spec` 的**机器面**:`packages/spec/scripts/**`、`packages/spec/docs/**`(契约门禁/生成器);与 `devx` 相交的 `packages/lint` / `scripts/` / `content/docs/**` 三面按判据切分 —— **围着 spec 契约转的工具链**(契约门禁/生成器/lint 规则/报错散文/references 管线)归本域,一般开发工具面留 `devx`(维护者 2026-08-09 裁决,#5469)。⛔ 不碰 `packages/spec/src/**/*.zod.ts` 与 strictness 台账;程序卡 #5163、座位贴 #6018,见下「`spec` 一分为二」 | -| `domain:cli` | `packages/cli`、`runtime`、`verify`、`qa`、`types`、`packages/rest`、`packages/mcp`、`packages/observability`、`packages/client`、`packages/client-react`(REST 线协议 SDK)、`packages/cloud-connection`、`packages/create-objectstack`、`packages/adapters/*`、`packages/plugins/plugin-hono-server`、`plugin-dev` | -| (无固定归属,按落点分诊) | `packages/apps/*`(`setup` / `studio` / `account`)、`packages/console`、`examples/*` —— 见下,**这一行是显式点名,不是遗漏** | - -`examples/**` belongs to the subsystem it exercises; anything that fits -nowhere is judged at triage by its principal landing site. A package missing -from the table is classified the first time it is triaged and the table -updated **by PR** — the taxonomy evolves deliberately, never per-claim. - -最后一行的三处兜底位:`examples/*` 照上一句;另两处的读法如下(表按 #5095 逐包 -核过真实消费方向补全,`packages/` 下余包都已落到上面某一行): - -- `packages/apps/*` 今天只是 app manifest + plugin 壳,内容仍从 - `@objectstack/platform-objects/apps` 再导出,落点可能在 platform-objects - (`metadata`)、这三个包本身、或控制台渲染面(`repo:objectui`)—— 按主要 - 落地站点在分诊时判定。 -- `packages/console` 是 `../objectui` 构建产物的落盘位:仓内只跟踪 - `package.json` / `README` / `CHANGELOG`,`dist/` 由 `scripts/build-console.sh` - 生成且⛔禁止手改。控制台 UI 的缺陷走 `repo:objectui`;仓内唯一可改面是 pin / - 刷新脚本(`build-console.sh`、`bump-objectui.sh`、`check-console-sha.mjs`、 - `check-objectui-pin-fresh.mjs`),归 `scripts/`(门禁类)那一行。 - -本表只覆盖**本仓(objectstack)的包**。`objectui` / `cloud` 是**整仓座位**, -各管自己仓里的 backlog(多仓协调 rule 1:issue 住在修复落地的仓);本仓里仅剩的 -`repo:*` 卡是缝卡(rule 1 例外 B),不另打 `domain:*` —— 域车道是「同仓多 PM -并发」的切法,不是第二套仓库标签。 - -### 词表 —— 在流通的 `domain:*` 标签与它们的座位贴 - -上面那张表回答「**这个包归哪个域**」;本表回答「**有哪些域标签、谁在座**」(#5469)。 -⛔ **新增或退役一个 `domain:*`,必须同批改本表** —— 在流通而不在本表 = 无主车道; -在本表而无人在座 = 两个座位都以为归自己的撞车面。 - -| `domain:*` | 座位贴 | 上面的包家族表 | -|:--|:--|:--| -| `domain:engine-core` | #6019 | ✅ 有行 | -| `domain:metadata` | #6367 | ✅ 有行 | -| `domain:drivers` | #6020 | ✅ 有行 | -| `domain:services` | #6021 | ✅ 有行 | -| `domain:identity` | #6022 | ✅ 有行 | -| `domain:devx` | #6023 | ✅ 有行 | -| `domain:spec` | #6017 | ✅ 有行 | -| `domain:spec-surface` | #6298 | ✅ 有行 | -| `domain:cli` | #6024 | ✅ 有行 | -| `domain:spec-tooling` | #6018 | ✅ 有行(2026-08-09 裁决后补,见下一段) | -| `domain:skills` | #7623 | ✅ 有行(2026-08-11 新立,#7548 第二裁) | - -**已退役(⛔ 分诊不再打;见到就是误标,按 rule 4 的误标路径上报):** - -| 退役标签 | 处置 | 2026-08-09 实测 | -|:--|:--|:--| -| `domain:engine` | 已按 #5472 拆为 `engine-core` / `drivers` | 0 单;⚠️ 标签对象仍在 | -| `domain:ui` | 维护者 2026-08-06 裁决退役 ⇒ 改用 `repo:objectui`,**不设别名** | 0 单(open + closed);⚠️ 标签对象仍在 | - -整仓座位是 `repo:*` 而非 `domain:*`:`repo:objectui` #6025、`repo:cloud` #6026 -(`domain:ui` 单独点名正因它是这条规则被绕过的实例)。 - -⚠️ **两条退役标签的存量已清零,但标签对象都还在仓库标签集里**(自动补全仍选得中)—— -**清零 ≠ 退役,删除标签对象是单独的一步**(两条退役指令都写明了删除,而两次都停在 -「不再打」);本表这两行是它们今天唯一的防线,**删除是 PM 的动作**(dev 侧无此工具), -⛔ 删掉之前不要把它们当成不存在。 - -### `domain:spec-tooling` —— 在册车道,三处争议面已按判据裁定(#5469) - -**⚠️ 读法**:本节是**沿革**,不是待办。生效的路由规则在上面的包家族表 -`spec-tooling` / `devx` 两行;本节解释那两行为什么长成那样,以及两条**已被 -事实推翻的旧裁决**为什么不能照抄执行。沿革全文(两条旧裁决原文、推翻前提的 -2026-08-09 实测、判据切分的三处明细)已移 -`references/incidents.md` §「domain:spec-tooling 沿革」。 - -**`engine` 一分为二(#5472,与 #5095 同批)。** 切分线就是上表:**`engine-core` = -编译/查询/元数据核心**,**`drivers` = 存储后端适配层**。存量迁移由分诊座位完成,清零 -后删除旧标签(见退役表);维护者 2026-08-05 对 `driver-memory` / `driver-mongodb` 族 -的**投入冻结指令**锚在 `drivers` 行,`formula` / `driver-sql` 不受影响(沿革全文见 -references §「engine 拆分沿革」)。 - -**`spec` 一分为二(维护者 2026-08-07 批准,座位贴 #6298)。** 切分纪律(拆分动因见 -references §「spec 拆分沿革」): - -- **判据是接受面,不是包、不是 diff 大小。** 这是 anchoring rule 在 - `packages/spec` 内的**显式例外**:一包三席(语义 / 文本 / 工具面,工具面 - 判据见下条与表行),前两席按「合法元数据集合变没变」分派 —— - 改动前能过校验的输入,改动后逐字节仍然同判 ⇒ `spec-surface`;否则 `spec`。 - 该判据与包归属同样机械(分诊读 diff 落点:语义行 vs 纯文本行)。反向红线: - **任何改变接受/拒绝行为的卡,不论多小,归 `domain:spec`**(#6245 size S, - 但把零校验放行改成 422 ⇒ 协议卡;#6235 只开一个 `visibleWhen` 键 ⇒ 协议卡)。 -- **与 `domain:spec-tooling`(#5163 程序卡,座位贴 #6018)的分界**:surface 改 - 「契约自己说的话」(`packages/spec` 源内文本行),tooling 改「围着契约转的 - 机器」(门禁/生成器/lint 与 docs 手写页;其席位范围 ⛔ 不碰 - `packages/spec/src/**/*.zod.ts`,与 surface 天然无交集)。同一缺陷两半分治的 - 先例:#6146(文档教了一个求值面从未绑定的根 —— surface)与 #6290(lint 一边 - 宣告该根合法一边拒收、还附错误修法 —— tooling 面)。与 `devx` 相争的三面 - (`packages/lint` / `scripts/` / `content/docs/**`)按同一「是否围着 spec - 契约转」判据切分 —— 维护者 2026-08-09 裁决(#5469),上表 `spec-tooling` - 行即其登记。 -- **产物随源走**:describe/JSDoc 改动会重生成 `content/docs/references/**` 与 - manifest 的 description 字段 —— 产物变更归触发它的**源 PR**(`check:generated` - 重生成提交,⛔ 手改);同一生成树在飞重生成 >1 张时按 #4675 四步序串行 - (#6224 扣压处置即此形,记录在 #6018)。 -- **卡点自报**(座位职责,不等积压被感觉到):① 水位 —— 本车道 `pm:queue` - 连续 3 天 >15 张或平均滞留 >48h ⇒ 提频/加 batch/按本条款继续拆;② 同一 - references 树在飞重生成 PR >2 ⇒ 打包成一列发,不加席位;③ 分诊脉冲 —— - 单次审计灌入 20+ ⇒ 一次性 sweep 专项吃掉,不改常设结构。 -- **迁移**:存量带 `domain:spec` 的文本面 open issue 由**分诊座位**按上述判据 - 一次性重标;拆分时已在飞的打包卡(sweep/工头)由原认领者跟完,不迁移。 -- **运行模式**:surface 席 sweep-first —— 一认领、一 PR、多单 `Fixes`,逐单 - 清单复审、零 rider,把认领/PR/CI/验收的固定开销摊薄到 1/N(试点 #6243)。 - -**`engine-core` 再拆 `metadata`(维护者 2026-08-07 拍板,座位贴 #6367)。** 二次切分 -仍按包边界(anchoring rule 无例外,与 spec 拆分不同):**`engine-core` = 编译/查询 -核心**,**`metadata` = 元数据机制与内置对象**(范围即包家族表两行)。配套纪律: - -- **红线**:改变元数据**格式/接受面**的卡照旧归 `domain:spec`(判据「合法集合变没 - 变」,#6245/#6235 先例);`/meta` HTTP 路由本体在 `packages/rest`,归 `domain:cli` - —— `metadata` 席只吃 engine 侧机制;跨半边的卡按主要落点判,拿不准 FLAG 回分诊。 -- **迁移与座位贴新立均已执行**(改标不改辖,先例 #6298;母席 #6019 范围随表收缩, - 单写手自更)—— 沿革全文见 references §「metadata 拆分沿革」。 - -**Label discipline —— 单一生产者。** `domain:*` 只由**分诊座位**在 backlog -sweep(round loop step 0)产出,全仓唯一(rule 4)。**打标签 ≠ 认领**:分诊座位 -打完从不认领,执行座位读到从不重打。**未打标签的 issue 任何人都不得认领** —— -那意味着分诊还没走到它,不是「可以自己判一下」;自己判一下就是把选择重新塞回 -单个 PM 的私有视野,车道协议当场归零。执行座位认为标签错了走上报(rule 4 的 -误标路径),⛔ 不自行改写 `domain:*`。 - -**Claim scope.** 执行 PM 的车道 = 它的座位贴(`label:pm:seat`,总入口 #4604)登记的 `domain:*`, -**恰好一个**;认领只发生在车道内。旧条款允许「一个域集合」,双射之后不再允许: -一个 PM 一个座位,「域 X 谁管 / PM Y 管什么」各只有一个答案。需要更细的分工就 -拆域(rule 4),不是给某个 PM 塞第二个域。 - -**Cross-domain issues.** 首选 rule 2 的 contract-first 拆分 —— 一域一 -sub-issue,各带自己的 `domain:*`,用 `Blocked-by:` 排序,**由分诊座位做**。 -拆分成本大于收益时走 rule 4 的**跨域例外路径**:分诊座位**指定**单一车道 PM -认领,该 PM 在认领评论里**申报完整文件面**,并跑下面的定向在飞检查。这是唯一 -的越界通道,**没有第二条** —— 旧条款那种「闲着的 PM 自行越界拿一单」的一次性 -许可已随 rule 4 删除。 - -**定向在飞检查(只在跨域例外路径上跑)。** - -- **触发条件**:**当且仅当**你正在认领一条由分诊座位指定的跨域例外单。日常 - 同域批次选择**不跑**这个检查。 -- **检查范围**:不是全仓,而是**该单申报文件面所触及的那几个域**。列出这些域 - 当前的 `pm:dispatched` 在飞单,读各自最新认领评论里的「文件面」申报,要求与 - 你的申报**不相交**;相交即让行(让那条单先落地),不要指望合并队列兜底。 -- **为什么不再每轮全局跑**:同域内批次独立性由该域自己的 step 3 保证,跨域相交只可能 - 从例外路径进来 —— 检查跟着例外走(旧全局扫越贵越不可靠)。 - -**The merge queue is still one shared serial resource.** Lanes buy parallel -authorship, not parallel landing: the flaky-test tax (#4796) scales linearly -with the number of PMs, and a red queue blocks every lane at once. Queue -health is therefore a shared duty — a PM that notices a flake fixes or files -it rather than re-queuing past it, whichever lane it came from. - -## Epic 子树车道(大任务委托专职 PM) - -Domain lanes 按「修复落点的包」**横向**切分;epic 车道是**纵向**的第二种切法: -一个大开发(父 issue + 一批 sub-issue)整体委托给一个专职 PM 会话,从立项跟到 -收尾。两者组合使用,不互斥:epic 内的 sub-issue 照常由分诊座位打 `domain:*` -标签(标签是共享路由信息,谁都可以读),但**认领权属于 epic PM**,不属于域 PM。 -双射的推论(座位表「每个 PM 恰好一个座位」):**一个 epic PM 会话不同时持有 -`domain:*` 座位** —— epic 就是它的座位,只是登记面在父单正文,不在座位表。 - -**启动**:`/pm-dispatch epic:#`。队列定义 = 父 issue #n 的 open、未认领 -sub-issue(递归)。**每轮重新读子树,不缓存清单** —— 这一条就是衍生任务的 -吸收机制:开发中新挂到父单下的 sub-issue,下一轮自动进入队列,零新标签、 -零额外登记。 - -**委托信号(标签 + 正文登记成对落地,同 label discipline)**: - -- 父 issue 打 `pm:epic`,同时**在父单正文**写清:会话 ID、**声明的文件领地** - (packages/ 目录清单)。⛔ **不入 `pm:seat` 座位贴体系** —— `label:pm:epic` - 就是全量索引,座位贴只记常设座位,一件事只记一处(rule 4 的座位贴协议)。 - 标签与正文登记仍是成对落地,缺一半就是 label discipline 禁止的过夜半状态。 -- 其它 PM(分诊 / 域座位)的候选获取(step 1)**跳过 `pm:epic` 父单的整棵 - 子树** —— 这是该标签的第一消费点;第二消费点是 `label:pm:epic` 这个索引 - 查询(领地盘点与僵尸回收都据它),第三是收尾流程。 -- epic 与域车道的**文件相交**:全局在飞检查已删除(rule 4),替代机制是 - **索引 + 正文** —— 域座位在批次选择时读一次 `label:pm:epic`(开销 O(epic - 数),不是 O(在飞数)),避开与本批相交的领地;epic PM 的每条认领评论照常 - 声明「文件面」。epic 不豁免任何一条认领纪律(assign + claim comment + - race check 全套照做)。 -- 触 `packages/spec` 的 sub-issue 照常受「shared contract surfaces have one - owner」约束 —— epic 委托不改变 spec 的单一所有者。 - -**衍生问题三分法。** epic 开发中冒出的每个新问题,用一个判据分流: -**「不修它,epic 的验收标准过不过得去?」** - -| 类型 | 判据 | 去向 | 谁派发 | -|:--|:--|:--|:--| -| in-scope 衍生 | 不修则 epic 验收不过 | 挂为父单 sub-issue(原生) | epic PM,下轮自动入队 | -| 顺带发现 | 与 epic 验收无关,只是路过看见 | 独立立单进**修复落地仓**的 backlog(多仓 rule 1;查重先行,`finding` 分诊纪律照旧) | 该仓的分诊/整仓座位定级 → 对应座位派发;epic 不吸收 | -| 触 spec / 公共契约 | 无论是否阻塞 epic | 按跨座位转移协议转 `domain:spec` 座位队列,epic 侧 sub-issue 写 `Blocked-by:` | spec 座位 | - -第一行防「衍生项没人管、epic 烂尾」;第二行防「epic 无限膨胀吞掉整个仓」; -第三行维持契约面的单一调度权。发现分诊轮的**归挂限定在 epic 内同样成立**: -仅有依赖关系、不属完成范围的,独立立单 + `Blocked-by:`,不得借 sub-issue -自动入队的通道把未分诊的东西塞进池子。每次分流留一行审计评论(「分流: -in-scope,验收依据 …」),维护者可否决。 - -**进度视图。** epic PM 每轮在父 issue 维护一份 checklist 汇总评论 -(sub-issue → 状态 → PR),待决项单独列出。维护者只看父单一个入口;决策 -本身仍锚在具体 sub-issue 上(step 8 不变),父单只汇总、不承载分析。 - -**收尾与僵尸回收。** 最后一个 sub-issue 关闭 → epic PM 在父单留总结评论、 -关闭父单、摘 `pm:epic`、正文的领地段标注收官 —— 四步是一组,缺一步就是过夜 -半状态(摘标签即从索引注销,座位表本就无行可销)。僵尸判据:父单正文有登记但 -整棵子树 ~48h 无任何认领/分支/PR 动静 → **分诊座位**(全仓唯一的扫描者,只动 -标签、不认领)在父单评论询问,再静默一窗后摘 `pm:epic` 收回子树;子树内仍在飞 -的认领按认领协议由原认领者跟完(stale-claim reclaim 的既有规则逐单适用)。 - -**残余风险(如实声明)。** epic 领地与域车道的文件相交靠「父单正文的领地声明 -+ 域座位每轮读一次 `label:pm:epic` 索引」防,是声明式的,**不是机械保证**; -撞上了由合并队列兜底(代价是返工,不是脏数据)。这与 domain lanes 的风险形状 -相同,epic 车道没有更差 —— 但也没有更好,所以领地声明写得越窄越诚实,越宽越 -要在父单正文里说明为什么。 - -## The round loop - -**谁跑哪一步(纵向拆分后的职责划分,rule 4)。** 同一个循环由两种座位分头跑, -不是每个 PM 都跑全套: - -| 步骤 | 座位 | -|:--|:--| -| **step 0**(backlog sweep,含发现分诊轮)、**step 2** 的分类/路由/拆分半边 | **分诊座位**(全仓唯一)—— 只产出标签、拆分与审计评论,⛔ 到此为止 | -| **step 1、3–9** | **执行座位**(每个恰好一个 `domain:*` 车道)—— 从读标签开始,不重做分诊 | - -分诊座位**永不进入 step 4 及其后的任何一步**(认领、派发、复核、入队全不碰); -执行座位读到的标签一律当作既成事实,除误标上报外不改。分诊座位空缺期间, -欠账按 rule 4 的**代扫**条款由会话型执行 PM 兜住(只做分诊动作,不跨车道认领)。 - -### 0. Backlog sweep — classification is a standing duty, not a request - -(**分诊座位的活** —— 执行座位跳过本步,rule 4。) -The maintainer does not pre-sort the backlog. On every round (and every -idle check-in), sweep every issue matching **任一**析取,并逐张分类: - -1. **无 `pm:*`、无 `needs-user-decision`、也无 `domain:*`** —— 全裸卡。今天的 - 规则,判据不变;`domain:*` 一项此前只活在实践里(带车道标签的单被读作 - 「已路由」而跳过),析取 3 出现后必须写明,否则两条互相吞掉。 -2. **有 `pm:queue` 但无 `domain:*`** —— ⚠️ **仅限 `objectstack-ai/objectstack`**。 -3. **有 `domain:*` 但无 pm-state**(`pm:queue` / `pm:dispatched` / `pm:blocked` / - `pm:on-hold` / `finding` / `needs-user-decision`)。 - -⏳ **析取 2、3 只取 `updated_at` 早于 ~2 分钟前的卡**(析取 1 无此限)。 - -**为什么 2、3 不是可选项。** `domain:*` 是**路由**、pm-state 是**状态机**,只带其一的 -单对两个视图**同时**不可见;析取 2 的半标注卡是协议自己按设计生产的 —— **纪律写在别处 -不够,判据本身必须兜住**(四次实测与两个生产机制见 `references/incidents.md` -§「半标注卡四次实测」)。 - -⚠️ **析取 2 必须 repo-scoped,否则它自己就是噪声源** —— 兄弟仓是整仓座位,车道标签在 -那里**根本不存在**,「有 `pm:queue`、无 `domain:*`」是它们每一张队列卡的正常形状 -(不限定一次扫进 objectui 38 + cloud 19 张,实测)。 - -⏳ **年龄下限键在 `updated_at`,不是 `created_at`** —— 半标注的第二来源是**一次标签 -写入**把老卡打成半标注(分诊自己打标签就是分开的两次写),按 `created_at` 判会漏掉 -它;代价是一条评论把卡推迟一轮,可接受。 - -**分类动作**(对上面选中的每一张;析取 2 选中的卡只欠 `domain:*`,补它即可, -⛔ 不重打已在位的 `pm:queue`): - -- **Auto-queue (`pm:queue`)**: a concrete defect with a named location or - repro; a scoped tooling/gate fix; a restore-invariant finding; a - test-only pin. Nothing to ask — label it and it becomes dispatchable. -- **Maintainer confirm (`needs-user-decision`)**: design cards, feature/ - contract-shape proposals, multi-week programs needing appetite and - sequencing, anything touching stored-data migration shape or removing a - shipped capability. The label is the inbox entry, and the card carries the - **required four-lens block from filing time** — one line per lens, spec in - step 8(维护者 2026-08-11 接受,#7498;⛔ 不留待有人接手再补)。The deep - per-option analysis is still written when the card is taken up. -- **Hold (`finding`)**: observation-class findings — dormant code, - unexercised drift, cosmetic polish; real, but nothing a user hits today. - The label keeps the record **inside the state machine** (a comment thread - or a side-ledger issue is exactly the silent state ADR-0049 bans, and a - second tracker rule 5 prohibits) without occupying the dispatch queue or - the maintainer's inbox. It is a held state, not a verdict — the findings - triage round below is where it gets one. -- **Repair first**: a body truncated by GitHub's sanitizer (bare `` - swallows the rest at rest) cannot be dispatched — comment the repair - instruction and move on. - -**扫描范围的三处排除**(#5474 试点定稿时补入,否则每轮都要现场判一次): -`tracking` 与 `status:parked` 的 issue(它们的状态由别的机制管,分诊不重判)、 -以及 **#4604 与全部 `pm:seat` 座位贴**(协议载体,不是待分诊的工作)。存量大时**每轮 -限量、优先最新**(试点用 ~15 条/轮),防一轮吃光存量把轮次拖过一个调度周期。 -⚠️ 排除项在析取 3 下更吃重:parked 单**正常形状**就是「带 `domain:*`、无 -pm-state」(2026-08-07 实测:3 张 `status:parked` 全部长这样),漏判排除就是每轮 -把它们重新扫回来一次。 - -#### 发现分诊轮 —— 队列的出水口(objectstack#4949) - -Prime Directive #10 是一个强力生产者,而循环原本只有「修掉」和「关重复」两条 -出路 —— 没有任何机制裁定「不值得修」,总量于是只涨不落(cloud 分片一日实测: -关 14 开 19,其中真缺陷当天闭环,涨出来的主要是观察类)。分诊轮补上出水口: - -- **触发**:每 ~5 轮一次;`finding` 存量超过 ~15 时插队执行。 -- **动作**:对每条 `finding`(以及久悬未分类的观察类 issue)先做过时前提 - 检查 —— main 一天 ~18 合并,发现的准确性半衰期以天计,囤积不是免费存档, - 是负利息存款 —— 然后三选一: - 1. **晋级**:摘 `finding` 换 `pm:queue`,进正常派发; - 2. **关闭(not planned)**:附一句理由,列入当轮轮次报告 —— 维护者可 - 否决重开,但 PM 不等批准(否决窗口,同 step 8 的模式); - 3. **持有**:留标签,记一行「为什么还留着」。 -- **判级发生在这里,不发生在立单时。** 立单的 dev 只有局部视野,判级最不准 - (cloud#1004 的「转义细节」实为 P0 过滤器旁路;cloud#897 自己写的影响面 - 评估就是错的)。所以 os-dev 的纪律是「照实立单、不自行压级」,分级的责任 - 归本轮。 -- **归挂限定**:发现开成已排队 issue 的 sub-issue **仅当**它真属父单完成 - 范围 —— 已排队父单的 sub-issue 会自动成为派发候选(step 1),把仅有依赖 - 关系的发现挂进去等于让未分诊的东西静默入池;那种情况独立立单 + - `Blocked-by:`(cloud#1045/#1046 之于 cloud#1050 即此形)。 -- **sweep 打包晋级**(试点 #6243 / PR #6288 一轮跑顺后定稿,维护者 - 2026-08-07 批准):晋级一批**同类**发现时,可以打包成**一张 sweep 卡** - 代替逐张入队 —— 一次认领、一个 PR、N 条 `Fixes`、逐项清单评审,把每单的 - 认领/PR/CI/验收固定开销摊薄到 1/N。这是「顺手修」的制度化替身:省同一笔 - 固定开销,但评审完整性不打折。打包判据与纪律,五条都是试点实测过的: - 1. **同类才打包**:全部命中同一判据(试点即「description 面在说谎或漂移」, - 全部校验不变量卡);不同性质的卡打包=一张 PR 里混多个评审面,禁止; - 2. **逐项清单是 PR 正文的必备件**:每项一行 落点 | before | after,评审 - 按行核,不按 diff 顺序读; - 3. **N 项之外零改动**,且 PR 自证(`git diff --stat` 文件数与清单一一 - 对应)—— 这是 sweep 与 rider 的分界线; - 4. **范围外发现照旧单开**(PD #10 不因打包而豁免;#6287 之于 #6288 即 - 此形); - 5. **卡片本身是认领对象**:sweep 卡入队、被认领,成员单保持 `pm:queue` - 标签但**不再是可派发候选**(选择期按 sweep 卡的成员清单排除,防双派; - PR 合并时 N 条 `Fixes` 齐关)。 - 适用面注记:`domain:spec-surface` 车道的默认运行模式即 sweep-first - (见「`spec` 一分为二」);其它车道按发现批次的同类度自行判断。 - -#### 发版板(`target:` —— 发版视角的常设轴,维护者 2026-08-06 拍板) - -维护者的原始痛点:「我想发版了,不可能清单上这么多问题都处理,哪些优先,我始终 -没有渠道知道」—— `pm:queue` 是工作蓄水池,不是发版清单;此前的历次一次性标注 -都因**无生产者、无消费者、无所有者**而腐烂。本节把发版轴接进常设机器: - -- **判据(二元;⛔ 不做 P1-P5 渐变 —— 渐变没人维护,一周就烂)**:对 defect 类 - issue 问一句「**不修它,当前 RC 能不能发?**」。判阻塞的四类:① 用户今天就会 - 撞的已发布面缺陷(数据错、静默丢、安全洞、跑不起来);② 公开契约的 - declared≠enforced(发出去就是撒谎的 API/schema);③ 存量数据/迁移形状 - (发布后修不回);④ 发布说明里需要为它道歉的(含「照文档抄即失败」的首小时 - 体验)。改进型、优化、重构、观察类 finding、内部工具、纯展示瑕疵默认**不 - 阻塞** —— 坐下一班车。 -- **拆分 / 分票时 `target:*` 随工作走,不随票号留 —— 对每一半重跑一遍上面那条二元 - 判据。** 默认是**继承**(义务跟着工作走);判定某一半不该继承时,**把理由按四类阻塞 - 写在那张卡上**(可证伪),⛔ 不默认不带 —— 写下的理由会被复核,不写的默认不会。生产 - 者不变,本条只是给拆分加一个必答项(#6806 标本全文见 references §「#6806 target - 拆分标本」)。 -- **每个 backlog 恰好一个生产者**(单一生产者纪律,作用域切对): - - **objectstack 主 backlog → 分诊座位**(step 0 分诊 defect 类新单时顺手判); - - **objectui 本地 backlog → objectui 整仓座位**:同一条二元判据 + 同四类阻塞,在 - objectui 仓里打 `target:`; - - **cloud 本地 backlog → cloud 整仓座位**(#6026;维护者 2026-08-11 裁定,#7493): - 同上,在 cloud 仓里打 —— rule 1(file-at-destination)的结构性后果,落点在 cloud - 的义务卡带着标签迁入(标本 cloud#1222,#7167 迁移)。 - 其余执行座位一律不打不摘 `target:`,误判走上报;⛔ 三个生产者各扫各的 - backlog,谁都不去扫别人的(step 0 的 repo-scoped 限定正为此)。无常设生产者的板必然 - 归零失真(实测读数已移 `references/incidents.md` §「objectui 板无生产者读数」)。 -- **存量清板姿态(一次性,每侧各一次)**:objectstack 已于 2026-08-06 全量清过 - 一次(309 条 → 46 条);objectui 的存量补一次等价的清板(#6904,判据与四类 - 阻塞照抄本节)。两侧各清完那一次之后**只有增量**,⛔ 永不再全量重扫 —— - 全量重扫正是此前历次一次性标注腐烂的那步。 -- **消费者三处**(a label exists iff something reads it):维护者的发版清单 = **三条 - 查询**(维护者 2026-08-11 裁定,#7493,取代此前的两条)—— - `repo:objectstack-ai/objectstack label:target: is:open`、 - `repo:objectstack-ai/objectui label:target: is:open` 与 - `repo:objectstack-ai/cloud label:target: is:open`;等价写法是一条 org 级搜索 - `org:objectstack-ai label:target: is:open`(仓内列表页不支持 `org:`,三条 - 查询是随处可用的写法)。 - ⛔ **「cloud 出现命中即误标信号」旧条款已废除(#7493)** —— 它教读者销毁真实阻塞的 - 唯一证据(cloud#1222 曾因此对板隐形 ~10 小时)。step 3 批次选择板上项优先;step 9 - 第四健康指标 = **三张板之和**,「归零 = 可发版」指三张都空,单看 objectstack 归零 - 不是可发版。为什么是三条不是一条加过渡态:file-at-destination 的结构性后果 —— 少读 - 任何一条的人按设计漏掉那一边(论证全文见 references §「发版清单三条查询论证」)。 -- **鲜度**:与发现分诊轮同节奏(每 ~5 轮)对板上 open 项做过时前提检查 —— - 已修/不成立的摘牌 + 一句评论(main 一天 ~18 合并,阻塞判断有半衰期)。 -- **发版时刻 = 清板,不是重扫**:板上每条三选一 —— 修掉 / 摘牌(不再成立)/ **明示 - 接受带病发布**(摘标签 + accepted-for-GA 评论留痕,进 release notes 的 known - issues)。姊妹仓在**自己仓里上板**(objectui 修复经 console bundle 随 pin bump 进 - 发布;cloud 独立部署,照样参与「归零 = 可发版」读数)⇒ 发版时刻的清单是**三条查询, - 三张板都要清到空**;三选一逐条适用,⛔ 不因「前端仓 / 独立部署」整批默认接受 —— - objectui / cloud 的三选一由各自整仓座位执行,读数回贴给发版清单。 - **发版前置条件(维护者 2026-08-10 拍板,#7275 Option A)**:清板动手之前,先取 - **一次** pin 读数(`.objectui-sha` 对 objectui main;⛔ 一次即止,不是重扫)。pin - 滞后 ⇒ console bump 单必须**已存在且已上板**(`target:`,由已拥有生产权的 - 座位执行),或按标准形态**明示接受**;两者都不成立 ⇒ **不 cut**。这是「板已清空」与 - 「console bump 已就位」之间唯一的机械关联(#7268 缺口标本与 #6162 判据辨析全文见 - references §「发版前置条件由来」)。 - -### 1. Fetch candidates - -**车道盘点的查询形状:整车道一次读全,本地求交 —— ⛔ 不按单一 pm 状态各查各的 -(维护者 2026-08-11 接受,#7518 第 5 课)。** 用 `list_issues` 带 -`labels: [domain:X]` + `fields: [number, labels, updated_at]` 一次拿回全车道 -open 集(无正文,比 `search_issues` 便宜约一个量级),候选、决策箱、blocked、 -finding 各态在本地按标签求交。按状态切片的查询看不见其它状态,而轮次报告的健康 -指标横跨数个状态 —— 实测盲区:`label:domain:services label:pm:queue no:assignee` -每轮报「decision box: 0」,而 #7243 带着 `needs-user-decision` 在同一车道里坐了 -一整班。整车道读法结构性消灭这类盲区,不是纪律性缓解。 - -List open issues matching the filter, excluding anything assigned or labeled -`needs-user-decision`. **Open sub-issues of a matching parent are candidates -too** — they inherit the parent's queue membership and need no label of their -own. **Exception: a parent carrying `pm:epic`** — its whole subtree belongs -to the epic PM registered **in that parent's own body**, and no other PM treats those sub-issues as -candidates(见「Epic 子树车道」;没有这条排除,本句的自动继承会让两个 PM -抢同一批子任务). Read each candidate's full body **and its comments — all of them, -before the issue can even be a candidate.** A comment may record that half the work -already shipped(#4075),and comments are where **rulings** land, not just progress -notes(#4829:正文读作直接修,评论区躺着维护者暂缓裁决与 ADR-0045 §3 机制记录)—— -裁决落在评论区,跳过评论就是跳过裁决(两例全文见 references §「候选评论全读两例」)。 -Triage, batch selection(steps 2–3)and the dispatch prompt all need the full picture. - -**Stale-premise check before every dispatch.** Issues describe the repo as of their -filing date; main moves ~18 merges a day. Before dispatching, check the named -files/subsystem against recent main history(`git log --oneline -20 -- `,或搜 -引用该 issue 关键词的已合 PR;同日三例 #4525 / #4379 / #4075)。A dispatch that starts -with "is this still true?" costs minutes; one that doesn't costs an agent-run. - -### 2. Triage — routing is the PM's job, never the maintainer's - -(**分诊座位的活**,rule 4 —— 执行座位在认领前只确认「标签已在」,不重做路由; -标签疑似错了走误标上报,不自行改写。) -The maintainer's only input is the issue itself plus `pm:queue` (or naming -the task in chat). They are **not** expected to know which repo a change -lands in — that answer usually *is* the analysis. For each candidate whose -routing isn't already decided: - -- **Determine where the change lands** by reading the issue against the - actual code of the three repos. Apply `repo:objectui` / `repo:cloud` - yourself; leave unlabeled for backend. A routing label a human already - set is respected as-is. -- **Cross-repo?** Do the split yourself per "Multi-repo coordination" rule - 2: file the parent + per-repo sub-issues, contract-first order, - `Blocked-by:` lines, routing labels on each sub-issue. -- **Parent issue that already has sub-issues** (the maintainer built the - structure themself): the queue label on the **parent alone** is enough — - sub-issues of a queued parent are candidates automatically, with no label - of their own. The PM expands the parent at triage: triage/route each - sub-issue individually, keep any dependency ordering the maintainer - expressed (`Blocked-by:` lines or native blocked-by relations), and where - none is expressed **infer the contract-first order and write the - `Blocked-by:` lines yourself** (audit-trail comment as usual). The parent - itself is a coordination node — **never dispatched to a dev**; it stays - open as the progress view and the PM closes it with a summary comment when - the last sub-issue closes. -- **Leave a one-comment audit trail** on the issue (English, per the - language policy), so the maintainer can veto cheaply: - "Triage: lands in objectui; rationale: …". - Optional but recommended, one extra line in that same comment: - `Size/model suggestion: , ` — a routing-time read - of the card's mechanical-vs-judgment weight, taken while the triage seat is - already inside the code. The executor seat consumes it in the claim - comment's 「Container & model」 line and may override it there with a - stated reason (step 4); the dispatch decision itself stays the PM's - (「Model tiering」, step 5 — including its mandatory `claude-fable-5` - clause, which no suggestion line can lower). -- Routing is a **technical judgment — never escalate "which repo?" to the - maintainer.** If after reading the code you genuinely cannot tell where a - change lands, the issue is underspecified: escalate the *underlying - product question* (step 8), not the routing. -- **Cross-repo dedup — check the sibling repos for this issue's shadow** - before it can be dispatched: follow every cross-repo reference on the - issue's body/timeline, AND keyword-search open issues/PRs in the other two - repos (module names, error strings). What you find decides the action: - - shadow **claimed / PR in flight** → do not dispatch; write - `Blocked-by: #` + a comment, revisit for *remaining* work when - it lands; - - shadow **open, unclaimed** → converge first: cross-link, make it a - sub-issue of the backlog issue (or close one as duplicate) so one thing - has exactly one dispatch entry — then queue normally; - - shadow **already done** → the backlog issue may be stale: verify what - remains, recommend closing if nothing does; - - **nothing found** → dispatch normally. - The main backlog is the only scheduling authority — two queues must never - dispatch the same work. Re-verify assignees (issue *and* linked shadows) - at claim time, not just at triage: the gap between them is where races - live. Known blind spot: work in a local worktree with no claim, no - branch, no PR is invisible — that is what the claim-first rule (step 4) - exists to shrink. - -**Recognize the "no producer" shape —「生产者在哪?」is a standing triage question.** -A field is declared, consumers read it, gates fully green — and **no code path ever -writes it**(一日五中 #4704 / #4837 / #4839 / #4862 / #4867,全文见 references -§「no-producer 一日五中」)。On any issue shaped `declared ≠ enforced`, ask where the -producer is before routing it — the answer is usually the root cause and changes the -issue's scope(and often its `domain:*` label)*before* dispatch. - -### 3. Select the batch - -Pick up to `batch` issues that are **mutually independent**: no two issues in -one batch may plausibly touch the same package, registry/barrel file, or spec -schema. Two dev agents editing the same shared file produce a merge race that -costs more than serializing. When in doubt, serialize — put the second issue in -the next round. Prefer small, well-specified issues; an issue with no acceptance -criteria you can state in one sentence is a candidate for escalation (step 8), -not dispatch. - -**Same-file issues serialize strictly across rounds — and deferring is not shelving.** -Two issues on one file ride in different rounds, no exception(#4820/#4821)。Rule: -when step 3 pushes an issue to a later round, **record the known trap on it before the -round ends** — a deferred issue looks dispatchable to every sweep, and whatever you -learned is worthless until it is on the issue(标本见 references §「同文件延后记坑 -标本」)。 - -**维护者豁免同文件串行时,替代纪律是四条,⛔ 不是「放开」。** 上一段的默认是硬串 -行;豁免只在维护者明示时发生,而被豁免掉的是**排队**,不是防撞 —— 少了替代纪律, -豁免就是把批次独立性直接删掉。2026-08-08/09 单班实测:#5543 / #6457 / #5929 三张卡 -同时改 `packages/objectql/src/engine.ts`,下面四条换来**三卡零冲突**: - -1. **互斥区域申报** —— 每张卡的认领评论把文件面写到**区域**级(函数 / 段落), - 不止文件名。与座位贴「热文件串行队」的区域列是同一份判据,两处分工不同:那里 - 记的是**跨轮常设**的排队顺序,这里记的是**本轮**的不相交证明; -2. **开 PR 前合一次 `main`**; -3. **兄弟卡落地后再合一次** —— 三卡的第二次合并各自发生在对方合入之后; -4. **冲突交由合并队列仲裁,⛔ PM 不手动排序** —— 队列本来就按合并后的树重算, - 手排是拿一次人工猜测换掉一次机械判定。 - -⛔ 豁免不外溢:认领协议、门禁、批次独立性的其余判据一条不减;没有维护者明示豁免 -时,默认仍是上一段的「不同轮次,无例外」。 - -**阻塞解除后要给延后的那一单重新定价 —— 放回队列不等于原价放回。** 上一段管 -「延后当轮就把坑记到被延后的 issue 上」;这一段管**前一单合入之后**:后一单的成本 -模型已经变了,而且方向不止一个。两个动作配对: - -- **派发前一单时**,派发令里带一条**必答项** —— 它的答案就是给后一单定价用的: - > 你的改动是否让 #X 变简单、变难、变得不必要,或完全无影响?明确回答,不要假设。 -- **派发被延后那一单之前**,用这个回答**重读它的选项与成本估计**,⛔ 不沿用立单时 - 的那一份。 - -一轮实测里四种方向各出现过、默认假设错了两次(实录见 `references/incidents.md` -§「重新定价四方向实录」)。适用判据:前后两单**共用 -同一个契约或数据表示**;形态迥异的批次(纯 UI、纯文档)里前后单往往不共享成本面,这 -一项问不出信息,不必强加。 - -**P0 插队(`priority:p0`)。** 带该标签的单可**超出 `batch` 上限、打破轮次节奏 -立即派发**(两例实测闭环:#5701、#5248)。豁免仅止于此:⛔ **不豁免同文件/同包 -串行**(插队单与在飞单文件面相交时,照样让行或等待),⛔ **不豁免认领协议全套** -(assign + claim comment + race check 一步不少)。此前协议通篇没有这个标签的 -消费者,违反「a label exists iff something reads it」—— 本段就是它的读取方。 - -**解锁扇出优先 —— 队列视图看不见「谁挡着谁」。** 阻塞关系只写在**被挡那一侧**的 -正文里(`Blocked-by: #N`),上游单身上没有任何痕迹:在队列视图里,一条链的链头 -与一张孤立的 p3 长得一模一样(2026-08-07 两个实测标本 #5702 / #6428 已移 -`references/incidents.md` §「解锁扇出两标本」)。 - -- **扇出是算出来的,不是声明的**:对开着的 `pm:blocked` issue 建一次 `Blocked-by: #N` - 的**反向索引** —— 解锁扫描本来就要做这一遍读,增量成本为零。 -- **选择优先级**:`priority:p0` 插队 > **扇出 ≥ 2 的上游单** > `target:` 板上项 > - **被任一开卡 `Blocked-by:` 点名的上游单**(扇出 ≥ 1;维护者 2026-08-11 裁定, - 记录在 #7498)> 普通队列项。点名关系在**选择时**从可 grep 的 `Blocked-by:` 行 - 现算 —— 即上面那份反向索引,解锁扫描每次关单本来就在读;推导所得,⛔ 不落任何 - 存储标签(下一条)。 -- ⛔ **优先是排序,不是豁免**:同文件串行、认领协议、批次独立性一条不减。 -- ⛔ **不要为此发明新标签**(没有读者的标签必然烂;扇出从协议已维护的数据里推出,单一 - 真相源不漂移)。**已议已拒,留档给后来者(维护者 2026-08-11,#7498 第二裁)**:给被 - 依赖卡打一揽子 `priority:*` / `pm:unblocks` 标签的方案被否 ——「被依赖」是会随上游 - 关单衰变的推导属性,无读者的标签被状态模型禁止;`pm:unblocks` 变体仅当被依赖卡群远 - 超今日 ~6 张时重议(论证全文见 references §「扇出标签已议已拒论证」)。 - -**解锁那一刻,两类断言同时最不可信 —— 反向索引的同一遍读要连带查这两件事**(关掉上游 -的那个合并,正是最可能顺手把你这张卡也修掉的提交): - -- **卡内文件面要在合并后的 ref 上重验**(`git grep` / `git show <合并 sha> -- <路径>`), - ⛔ 「上游已关/已合」不等于「卡还成立」—— 旧条款查的是上游的状态,不是卡在上游 ref - 上的前提(标本见 `references/incidents.md` §「#6413 解锁前提旧行号标本」;根因同 - notes 4)。 -- **PM 自己「这条裁决收窄/关掉了那张卡」的判断,是假设不是前提**,必须以「PM 机制假设 - (须实测,鼓励证伪)」身份进派发令(step 5 分区块),⛔ 不许写成既成事实(证伪实测见 - `references/incidents.md` §「#6190 收窄断言被证伪」);被证伪就**在同一张卡上公开 - 更正**,再重新分诊(#6644 —— step 7「dev 纠正 PM 要当众认」用在 PM 的判断上)。 - 诊断上)。 - -**发版板优先。** 批次选择时 `target:` 板上项排在普通队列项之前(位次见上面 -那条选择优先级;与 `priority:p0` 叠加 = 最高优先)。板上项照常受同文件串行与认领 -协议约束 —— 优先是排序,不是豁免。 - -### 4. Claim - -**Same-account scope note(多账号时代的读法,#7341 item 7)。** 本步里所有 -为「共享身份」而生的仪式 —— session-ID 行、认领评论的时间戳仲裁、「这个认领是不是 -我的」重读 —— 作用域是**同一个 GitHub 账号内的多个会话**。舰队已实际多账号在岗 -(座位贴协议的「当前 PM」段就是为此改的),跨账号只需要一条规则:**assignee 不是 -你 ⇒ 已被认领,永不碰** —— assignee 字段在跨账号时自己就能回答「谁」。⛔ 仪式一条 -不删:任何一个账号仍会并行多个会话,账号之内它们仍是唯一的仲裁器 —— 本注记只标 -作用域,不撤装备。 - -Within one account, all its agents share that GitHub identity, so the assignee -alone says "some agent claimed this" but never *which* — the claim comment -carries the identity. For -each selected issue, **before dispatching** (repo rule: claim before code), -execute as **one atomic pair**, in order: - -1. **Assign** to yourself (`@me`) and swap the state label — apply - `pm:dispatched` in the **same label write** that removes `pm:queue`; the - pair is atomic, never half (#7239 ruling 2026-08-10: the remove-only half - leaves in-flight cards that triage disjunction 3 re-pays full - comment-reads on every round). Skip — and drop - from the batch — any issue that acquired an assignee since step 1. -2. **Claim comment** (English, per the language policy), fixed shape — the - branch name is the key, - every later artifact (worktree, push, PR) hangs off it. The session ID is - NOT optional: under the shared identity it is the only line that lets a - later reader — including your own future self after a context reset — - answer "is this claim mine?". A claim without it caused the #4555/#4559 - duplicate (#4588): the second session saw its own shared name as assignee - and could not tell the claim was someone else's. +| `domain:engine-core` | `packages/objectql`、`packages/core`、`packages/formula`(CEL / `matches-filter` / RLS 谓词求值)、`plugin-pinyin-search`(落点在编译/查询核心) | +| `domain:metadata` | `packages/metadata*`(加载、注册、持久化、缓存、目录)、`packages/platform-objects` | +| `domain:drivers` | `packages/drivers/driver-*`(维护者 2026-08-05 对 `driver-memory` / `driver-mongodb` 族有投入冻结指令;`formula` / `driver-sql` 不受影响) | +| `domain:services` | `packages/services/*`、`packages/connectors/*`、`packages/triggers/*`、`plugin-approvals`、`plugin-webhooks`、`plugin-email`、`plugin-reports`、`embedder-openai`、`knowledge-*` | +| `domain:identity` | `plugin-auth`、`plugin-security`、`plugin-sharing`、`plugin-audit` | +| `domain:devx` | `packages/lint`、`packages/sdui-parser`、`content/docs/**`、`apps/docs`、`scripts/`(门禁类)—— 与 `spec-tooling` 相交的三面按「是否围着 spec 契约转」切分 | +| `domain:skills` | 两个技能根:`.claude/skills/**`(含本文件)+ `skills/**`(维护者 2026-08-11 裁定单设座位,与维护者走专题讨论;skills 更新 ADR-class,见 Guardrails) | +| `domain:spec` | `packages/spec` 的**语义面**:schema 形状、`contracts/**`、退役行为半边、strictness 台账 —— 判据是**接受面变化** | +| `domain:spec-surface` | `packages/spec` 的**文本面**:describe/JSDoc/墓碑散文/错误 guidance 与 alias 表 —— changeset 恒 patch | +| `domain:spec-tooling` | `packages/spec` 的**机器面**:`packages/spec/scripts/**`、`packages/spec/docs/**`;围着 spec 契约转的工具链(门禁/生成器/lint 规则/报错散文/references 管线)归此,一般开发工具面留 `devx`(维护者 2026-08-09 裁决);⛔ 不碰 `packages/spec/src/**/*.zod.ts` 与 strictness 台账 | +| `domain:cli` | `packages/cli`、`runtime`、`verify`、`qa`、`types`、`packages/rest`、`packages/mcp`、`packages/observability`、`packages/client*`、`cloud-connection`、`create-objectstack`、`packages/adapters/*`、`plugin-hono-server`、`plugin-dev` | +| (无固定归属,按落点分诊) | `packages/apps/*`、`packages/console`(dist 由脚本生成 ⛔ 手改;UI 缺陷走 `repo:objectui`,pin/刷新脚本归 `scripts/` 行)、`examples/*`(归它演练的子系统) | + +表未覆盖的包在首次分诊时归类并**走 PR 更新本表**;**新增或退役一个 `domain:*` 必须同批改本表 +**(在流通而不在表 = 无主车道)。座位在编情况以 `label:pm:seat` 索引为准,每 +个 `domain:*` 与 `repo:*` 恰一张座位贴。 + +**`packages/spec` 一包三席**(维护者 2026-08-07 批准;anchoring rule 的显式例外):语义/文本两席 +按「合法元数据集合变没变」分派 —— 改动前能过校验的输入,改动后逐字节同判 ⇒ `spec-surface`,否 +则 `spec`;**任何改变接受/拒绝行为的卡,不论多小,归`domain:spec`**。tooling 席改「围着契约转的 +机器」,与 surface 无交集。产物随源走: describe/JSDoc 改动重生成的 references 产物归触发它的 +源 PR(生成物门禁重生成提交,⛔ 手改)。`metadata` 自 `engine-core` 的二次切分按包边界(无例外); +改元数据**格式/接受面**的照旧归 `domain:spec`,`/meta` 路由本体在 `packages/rest` 归 +`domain:cli`;拿不准 FLAG 回分诊。surface 席默认 sweep-first 运行。 + +**单一生产者。** `domain:*` 只由分诊座位产出;打标签 ≠ 认领;**未打标签的 issue 任何人不得认领 +** —— 那意味着分诊还没走到,不是「可以自己判一下」。 + +**跨域例外路径(唯一的越界通道)。** 真拆不动的跨域单 PR 由**分诊座位指定**一个车道PM 认领;认 +领评论**申报完整文件面**;只在这条路径上跑**定向在飞检查**(范围 = 该单文件面触及的那几个域的 +在飞单,读各自认领评论的文件面申报,要求不相交,相交即让行)。日常同域批次选择不跑全局在飞检 +查 —— 同域独立性由本域批次选择保证,跨域相交只可能从例外路径进来。 + +**合并队列仍是一条共享串行资源。** 车道买到的是并行编写,不是并行落地;flaky 税随PM 数线性放大 +,红队列同时挡住所有车道 —— 谁发现 flake 谁修或立单,不绕行。 + +## 座位贴协议(一座位一贴,单写手) + +多写手共编一个 body 机制上防不住互吞(维护者 2026-08-06 拍板):**贴 = 座位**(标签`pm:seat`),正 +文固定六段现值(**范围 | 当前 PM | 继承台账 | 热文件串行队 | 常设承诺 +| 说明**,没有一段随时间增长),**只由在任座位 PM 编辑**,正文为权威 —— 标题 +(`[PM seat] <座位名> — <状态>`)与 assignee 是它的派生视图,三者同笔更新。接管/移交 = 改正 +文 + 一条审计评论,**评论只作审计不承载状态**;接管压缩是标准步骤(按六段模板重述现值,编辑历史 +即存档)。**热文件串行队是正文具名段**(文件 → 有序卡片清单 → 每张卡认领的区域;区域写不清就只 +能整文件串行)。无心跳,活性惰性判定(只在接管冲突时评估,会话座位 >24h 无产出可回收);每轮巡检核 +对自己的贴;竞态(空缺争用)三招 —— 动手前重 fetch 正文、审计评论时间戳先到先得、写后回读。状态 +词表、接管/退场收尾清单等细则见 `references/seat-post-protocol.md`;epic 委托不入座位贴体系 +;`packages/spec`恒归 spec 座位。 + +## Epic 子树车道 + +大开发(父单 + sub-issue 树)整体委托一个专职 PM 会话:`/pm-dispatch epic:#`。队列 = 子 +树 open 未认领 sub-issue,**每轮重读不缓存**(新挂的自动入队)。委托信号成对落地:父单 +打 `pm:epic` + 正文写会话 ID 与声明的文件领地;其它 PM 的候选获取跳过整棵子树;域座位批次选择 +时读一次 `label:pm:epic` 索引避开领地相交。epic PM 不同时持有`domain:*` 座位;认领纪律全套照 +做;触 `packages/spec` 的 sub-issue 照旧转 spec 座位。**衍生问题三分法**(判据:不修它,epic 验 +收过不过得去?):in-scope ⇒ 挂父单sub-issue 下轮自动入队;顺带发现 ⇒ 独立立单进修复落地 +仓 backlog(查重先行,⛔ 不借sub-issue 通道把未分诊的塞进池子);触 spec/公共契约 ⇒ 转 spec 座位 +队列 + epic 侧写`Blocked-by:`;每次分流留一行审计评论。进度视图:父单维护 checklist 汇总评论, +决策仍锚在具体 sub-issue。收尾四步与僵尸回收细则见 `references/seat-post-protocol.md`;领地防 +撞是声明式的 —— 撞上由合并队列兜底,所以领地声明越窄越诚实。 + +## 分诊座位职责 + +(执行座位 ⛔ 跳过本节动作 —— 读到标签当既成事实。) + +**Backlog sweep(常设职责,不等请求)。** 每轮扫任一析取命中的卡:① 全裸(无`pm:*`、 +无 `needs-user-decision`、无 `domain:*`);② 有 `pm:queue` 无 `domain:*` (**仅限主仓** —— 姊妹 +仓没有车道标签,这形状是它们队列卡的常态);③ 有 `domain:*` 无pm-state。②③ 只取 `updated_at` 早 +于 ~2 分钟的卡(半标注的第二来源是一次标签写入把老卡打成半标注),且不是可选项 —— 只带路由或状 +态机其一的卡对两个视图同时不可见,而半标注卡是协议自己按设计生产的。排除:`tracking`、 +`status:parked`(其正常形状恰是「带域标签无 pm-state」)、全部 `pm:seat` 贴;存量大时每轮限量、 +优先最新。 + +**分类动作**(每张三选一 + 一个修复通道):**`pm:queue`** —— 有具名落点或复现的具体缺陷、范围明 +确的工具/门禁修复、恢复不变量的 finding、test-only pin,无可问之事; **`needs-user-decision`** +—— 设计卡、feature/契约形状提案、需要 appetite 的多周程序、碰存量数据迁移形状或删除已发布能 +力的,落卡时**必带四棱卡面块**(见「升级与决策」),⛔ 不留待有人接手再补;**`finding`** —— 观察 +类(死代码、未演练漂移、抛光;真实但今天没有用户撞上),持有态待定级;**Repair first** —— 正文 +被 sanitizer 截断的卡不可派发,评论修复指令后跳过;它是停摆指令,判据必须比其它分类更硬(双读取 +),事后证伪要在同处公开作废。 + +**路由即分诊的技术判断,永不升级「哪个仓?」。** 读三仓代码定落点;跨仓的按contract-first 拆分; +父单已有子结构的,父单队列标签即可(sub-issue 自动成为候选),分诊时逐个展开路由、 +补 `Blocked-by:` 排序,父单是协调节点**永不派发**。每张留一条英文审计评论(「Triage: lands in +…; rationale: …」),可选带 `Size/model suggestion: , ` 行供执行席参 +考。跨仓查重(shadow 检查):跟跨仓引用 +关键词搜两姊妹仓 —— 在飞 ⇒ `Blocked-by:` 不派;open 未 +认领 ⇒ 先收敛成一个派发入口;已完成 ⇒ 卡可能过期。**「生产者在哪?」是常设分诊问题**:declared +≠ enforced 形状的卡先问谁在写这个字段 —— 答案通常就是根因,并在派发前改变卡的范围与域标签。 + +**发现分诊轮(队列的出水口)。** 触发:每 ~5 轮,或 `finding` 存量 >~15 插队。对每条先做过时前提 +检查(main 一天 ~18 合并,发现的准确性半衰期以天计),然后三选一:晋级(换 `pm:queue`)/ 关闭 not +planned(附理由,列入轮次报告 —— 维护者可否决重开,PM 不等批准)/ 持有(记一行为什么还留着)。**判 +级发生在这里,不在立单时**(立单的 dev 只有局部视野,照实立单不自行压级)。**sweep 打包晋级**(维 +护者 2026-08-07 批准)五条:同类才打包(全部命中同一判据);逐项清单(落点 | before | after) +是 PR 正文必备件; N 项之外零改动且 PR 自证;范围外发现照旧单开;sweep 卡本身是认领对象,成员单 +保持标签但不再是可派发候选。 + +**发版板(`target:`;运维细则见 `references/seat-post-protocol.md`)。** 判据二元(⛔ 不 +做优先级渐变 —— 渐变没人维护):「不修它,当前 RC 能不能发?」判阻塞四类:① 用户今天就撞的已发布 +面缺陷;② 公开契约 declared≠enforced;③ 存量数据/迁移形状(发布后修不回);④ 发布说明里要为它道歉 +的。拆分/分票时 `target:*` 随工作走,对每一半重跑二元判据,默认继承,不继承要把理由按四类写在卡 +上。**每个 backlog 恰好一个生产者**(主仓 → 分诊座位;objectui / cloud → 各自整仓座位),其余座 +位不打不摘,⛔ 谁都不扫别人的 backlog;维护者的发版清单 = **三仓三条查询之和**,「归零 = 可发版 +」指三张板都空。鲜度与发现分诊轮同节奏;**发版时刻 = 清板不是重扫**(修掉 / 摘牌 / 明示接受带 +病发布三选一);**发版前置**:清板动手前取一次 pin 读数,pin 滞后 ⇒ console bump 单已存在且已上 +板或明示接受,否则不 cut。 + +**高置信决策卡的分诊代裁(维护者 2026-08-12 裁定)。** 出处原话:「让他帮我从实际业务需求出发, +从平台长远合理性出发,从避免ai写代码犯错的角度出发,从创业阶段不扩散需求的角度出发综合分析……很 +多时候ai会有明确的建议,大部分建议我都会接受,这个是不是可以让分诊就处理掉。他没把握的才找我」 +;「我同意你的意见,并补充功能新增类的,adr类别的,协议变化类别的,还是需要人工决裁,如果是代码整 +理或者bug修改,具体的代码细节其实ai看的比我清楚,让我判断我也判断不了」。 + +- **人工地板(不论四棱是否同向,恒交维护者)**:功能新增类;ADR 类;协议/公开契约变化类;破坏性或难 + 回滚动作(存量数据迁移形状、删除已发布能力、force 操作);**安全/权限边界**(放宽访问控制、认 + 证流、RLS/共享语义、审计留痕的「修复」是伪装的产品决定);**门禁削弱**(降阈值、删必查项、 + 抬 ratchet 上限、跳过/隔离测试 —— AI 有把CI 弄绿的结构性动机,削弱农场必须是人的动作);**花 + 费/配额/舰队形态/默认模型档位** (动的是维护者的预算);**新增运行时第三方依赖**(供应链/许可/ + 维护义务)。 +- **代裁车道(分诊座位裁)**:代码整理(行为不变的重构)与 bug 修复。**机械边界测试**:改动是否** + 扩大接受集或公开面**?扩大 ⇒ 功能新增/协议变化 ⇒ 人工;接受集不变、拉回已声明契约(declared = + enforced 的恢复)⇒ bug/整理 ⇒ 代裁车道。 +- **置信门(全部成立才可代裁)**:① 四棱同向(全指向同一选项,任何分裂即升级);②不在人工地板上 + ;③ 不收窄、不推翻任何既有维护者裁决;④ 执行是**否决窗口不是许可门**—— 裁定 → 一次标签写入 + 换 `needs-user-decision` 为工作态 → 卡上贴四棱块 + 结论 + + `auto-adjudicated` 标记 → 轮次报告设**代裁清单**专节(聚合漂移的刹车);⑤ 代裁分析跑 + 在 `claude-fable-5`(与维护者手工流程同档)。 +- **回翻条款**:代裁卡实施中发现契约终究要动 ⇒ dev 停手,卡回`needs-user-decision` —— 报告分叉 + ,⛔ 永不静默重裁。 + +## 执行座位职责 + +### 候选与批次 + +**整车道一次读全,本地求交**(维护者 2026-08-11 接受):`list_issues` 带 +`labels: [domain:X]` + 最小字段一次拿回全车道 open 集,各状态本地求交 —— 按单一 pm状态切片的 +查询看不见其它状态,是结构性盲区。候选 = open、未 assign、无`needs-user-decision`;已排队父单 +的 open sub-issue 自动是候选(`pm:epic` 父单的子树除外)。**每张候选读全文 + 全部评论**(裁决落 +在评论区,跳过评论就是跳过裁决;评论还可能记着一半工作已交付);**派发前做 stale-premise +check**(issue 描述的是立单当天的仓 —— `git log --oneline -20 -- ` 花几分钟,不查则赔一 +次 agent-run)。 + +**批次独立性。** 一批内任两单不得可能碰同一个包/registry/barrel/spec schema;拿不准就串行。** +同文件单跨轮硬串行;延后不是搁置**(被延后那一刻就把已知的坑记到该 issue上)。**维护者明示豁免 +同文件串行时,替代纪律四条**(豁免的是排队,不是防撞):① 认领评论把文件面申报到**区域**级(函数/ +段落);② 开 PR 前合一次 main;③ 兄弟卡落地后再合一次;④ 冲突交合并队列仲裁,⛔ PM 不手动排序;豁 +免不外溢,无明示豁免默认硬串行。**阻塞解除后给延后单重新定价**:派发前一单时带必答项「你的改动 +让 #X 变简单/变难/变得不必要还是无影响?」,派发后一单前用这个回答重读它的选项与成本,⛔ 不沿用 +立单时那份(适用判据:前后单共用同一契约或数据表示)。 + +**选择优先级**:`priority:p0` 插队 > 解锁扇出 ≥2 的上游单 > `target:` 板上项 > 被任一开 +卡 `Blocked-by:` 点名的上游单 > 普通队列项(维护者 2026-08-11 裁定);扇出从`Blocked-by:` 反向 +索引现算,⛔ 不为此发明存储标签;**优先是排序,不是豁免**。**解锁那一刻两类断言同时最不可信**: +卡内文件面要在合并后的 ref 上重验(上游已合 ≠ 卡还成立—— 关掉上游的那个合并最可能顺手把你这张 +卡也修掉);PM 自己「这条裁决收窄/关掉了那张卡」的判断是假设不是前提,必须以机制假设身份进派发 +令,被证伪就在同一张卡公开更正再重新分诊。 + +### 认领(claim before code) + +同账号内的多会话共享 GitHub 身份,assignee 只回答「有 agent 认领了」,回答不了「哪个」—— 认领 +评论承载身份;跨账号只需一条:assignee 不是你 ⇒ 永不碰。每张选中的单,派发前按序执行**一个原子 +对**: + +1. **Assign @me + 状态标签对调**(`pm:dispatched` 与摘 `pm:queue` 同一次标签写入 ——只摘不换留 + 下的在飞卡会被分诊析取每轮重付全评论读);step 1 之后获得 assignee 的直接弃出本批。 +2. **认领评论**(英文),固定形状;session ID 不可省(共享身份下它是「这个认领是不是我的」唯一答 + 案);**首行以字面 `Claim:` 开头是机器判据**(维护者 2026-08-11 裁定;巡查谓词只认这一个拼写 + 且保持严格,修法是全舰队向文档拼写收敛,⛔ 不放宽谓词): + > Claim: PM loop round N > Session: `session_` > Branch: `claude/issue--` > Worktree: `-issue-` > Domain: `domain:` - > File surface: `` (stop on breach; explain in the report) + > File surface: `<预期触碰的目录>` (stop on breach; explain in the report) > Container & model: ``, `mode:subagent | mode:cloud`, `model: sonnet | opus | fable` - > Serial constraints cleared: `` - - **首行拼写是机器判据(维护者 2026-08-11 裁定,#7553):认领评论首行以字面 - `Claim:` 开头**(引用块前缀 `>` 可选,即上面模板原样)。巡查谓词 - (`scripts/pm/check-half-states.mjs` H2)只认这一个拼写且**保持严格** —— - `**Claim** —`、`认领:` 等在用变体实测 9/22 张卡读不出来(#7553);修法是 - 全舰队向文档拼写收敛,⛔ 不放宽谓词。 - - 「Container & model」行的判读规则见「Resource limits」的容器判定条与 step 5 的 - 「Model tiering」—— 尺寸与档位是同一次判读的两个输出,写在一行里,⛔ 不要 - 只写其中一个。分诊评论若带了 `Size/model suggestion` 行(step 2),认领时对着 - 它写:采纳即照抄,不采纳就在本行给一句理由 —— 覆盖权在执行席,留痕义务也在。 - - 最后一行是 services 车道一班 28 PR 零合并冲突的机制(#5885):把「查过串行 - 约束」从内心活动变成落在评论里的读数,竞态复读与串行判断都成了 30 秒的事 —— - 同包在飞单在 ~18 merges/日的环境下不点名就等于没查。 - - 「文件面」is **required** for 跨域例外路径的认领(rule 4 —— 那是唯一的 - 越界形态)and **recommended** for ordinary in-lane ones — - 它是**定向在飞检查**唯一的输入,也是 epic 领地相交判断的输入。The branch name must - carry the issue number: #5032's losing claim promised - `claude/issue-osv-fast-uri-hono-undici`, which - `git ls-remote --heads origin | grep issue-5032` cannot find — the #4588 - discoverability hole, reopened. - - **Assign + comment are one indivisible act — rate limits do not split - them.** "Assign now, comment when quota recovers" leaves exactly the - state the shared identity cannot interpret: an assignee with no owner. - If the comment cannot be posted, undo the assign (or never start); if - GraphQL quota is gone, queue the whole pair for later (Operational - notes 3) — never half of it. -3. **Race check — after your claim comment is up, re-read the whole - thread.** Assignment is idempotent, so two agents can both "succeed"; - the claim comments' **timestamps are the only tiebreaker**, whatever the - assignee field seems to say. An earlier claim comment with a *different* - session ID or branch means you lost: touch nothing of theirs, reply - "already claimed — yielding", and pick another issue. First comment wins. - The dev's own pre-code re-read is the same duty (os-dev rule 2), which is - what makes the claim protocol self-healing when a PM slips — #5032's - 20-second race specimen is in `references/incidents.md` - §「#5032 认领撞车与让行实录」. - - **Yielding is a handoff, not an exit.** The loser posts, together with its 让行 - comment, everything it already diagnosed — repro commands, dependency paths, traps - confirmed(#5032 → PR #5052)。The PM-side readings are the same duty(maintainer- - accepted 2026-08-11,#7518 item 4;measured on #7145):the loser's yield comment - also hands over the **board readings it already took** — in-flight same-file PRs, - region declarations, serial constraints cleared — so the winner does not re-scan - (全文见 references §「让行交接 PM 侧读数」)。 - -Dev agents push their branch early — a remote branch is the hardest evidence -of work in flight, closing the gap between "claimed" and "PR exists". - -**Multiple GitHub accounts simplify this, not complicate it.** Across accounts: -`assignee isn't you → taken, never touch` is the entire cross-account protocol; the -claim-comment ritual matters *within* one account's sessions. Several accounts working -the backlog partition it like multi-PM sharding(by repo or an agreed label per -account),recording the assignment table once in a pinned issue or the round report. - -**Stale-claim reclaim**: a claim older than ~24 h whose promised branch does -not exist on the remote and has no PR is presumed dead — comment asking, and -after another window of silence, remove the assignee (note why) and return -the issue to the queue. Never reclaim a claim that has a live branch with -commits. - -### 5. Dispatch - -One `Agent` call per issue, `subagent_type: "os-dev"` (fall back to -`general-purpose` with the same prompt if the custom agent isn't loaded), run -in parallel in the background. - -#### Model tiering(维护者 2026-08-10 裁定 —— 三档制,取代 2026-08-09 的两档制) - -⚠️ **本节已两次改写更旧的规则(单一 opus → 两档 → 三档),凡在别处读到旧形式,一律以 -本节覆盖,⛔ 不要几条并存着理解**(沿革见 references §「Model tiering 沿革」)。 - -**授权出处(维护者原话,逐字引用、未翻译;前两条出自 devx 席 2026-08-10 会话, -第三条是同日的裁决补遗,均落档在 #7341):** - -> 项目经理技能还需要考虑的是派任务时使用什么模型,也应该项目经理决定,最低下限 -> sonnet,最高可以 fable - -> 关于项目经理的技能,接受你的优化意见,并和前面两个优化任务集中处理。派发使用 -> 云卡片,用 Fable 5 模型 - -> 比如 更新 项目经理技能 必须要使用 Fable 5 - -**政策:派发模型是 PM 的逐卡显式决定 —— 下限 `sonnet`,默认判断档 `opus`,上限 -`fable`(`claude-fable-5`);`model` 逐次派发显式传参,永不省略;档位记进认领 -评论。** - -- **下限 `sonnet` ⇒ 机械卡。** 判据是「正确性由门禁农场机械判定」而非 - 「改动小」:单文件散文 / 注释修正、一处新增(one-spread additions)、死词表行 - 删除、alias / tombstone 台账维护。这类卡的失败模式是漏跑门,不是判断失误 —— - 而漏跑门是 CI 抓的,不是模型档位抓的。 -- **默认判断档 `opus` ⇒ M / L 卡、裁决实施卡、多面语义卡,以及任何带设计判断的 - 卡。** 边界情况上抬不下压:**拿不准就升一档**。一次错派低档的返工,贵过它省下 - 的那点额度。 -- **上限 `fable`(`claude-fable-5`)⇒ 最重的设计 / 编排卡。** 判据:交付物本身是 - 协议、流程或编排结构(多 PR 编排、跨座位协议改写、验收判据本身要被设计出来的 - 卡),或维护者点名。这一档由上面第一条原话开放(「最高可以 fable」),按卡 - 取用,⛔ 不是新的默认。 -- **⛔ 强制条款(无向下裁量权):凡改 `.claude/skills/pm-dispatch/**` 的卡,一律 - `model: "claude-fable-5"` 派发。** 出处是上面第三条原话 —— 本技能是全部 PM - 座位的操作系统,写它的档位不由逐卡判断,由裁决固定。 -- PM 座位自己留在更强的编排档:分诊、复核、决策成框才是它的判断付费的地方。 - -**档位必须显式传参,不能靠定义里的 pin 兜底。** 解析顺序四级: -`CLAUDE_CODE_SUBAGENT_MODEL` 环境变量 → **逐次派发的 `model` 参数** → agent 定义的 -`model:` frontmatter → 主会话模型。**每次派发都显式写 `model`**,让档位是这次派发的 -属性(参数在 frontmatter 之上;pin 只管你什么都不传的情形,且历史上被删过一次, -#6686;环境变量一旦被设会静默盖掉分档、仓内无显示,白名单挡下时回退**继承的模型** -—— 细账见 references §「model 参数解析四级细账」)。 - -**分档写进认领评论**(step 4 的容器判定行已经在给尺寸分级,顺手带上档位),这样 -选择可审计、交接会话不用重判。分诊评论若带了 `Size/model suggestion` 行 -(step 2),它是这次判读的**输入**,不是决定:采纳照抄,不采纳给一句理由 —— -派发档位始终是 PM 的显式决定。 - -#### Prompt template - -Fill every placeholder. **默认 ⛔ 不再整段粘贴 issue 正文** —— 派发词让 dev 自己去 -GitHub 读全文与全部评论(premise-first 本来就强制它读一遍),正文只粘那些「GitHub -上读不到、或读到了也会被读错」的部分: - -- **裁决原文逐字引用**(中文不翻译); -- **「裁决 / PM 机制假设 / PM 建议的路线」三分区**(下面那一段说明了为什么这个 - 分区不能省,以及第三块是怎么补上的); -- **卡特定条款**与**同日变更**(same-day churn)行。 - -⚠️ **代价与缓解:notes 12 的读取截断。** 长正文经工具读取可能被静默截断,粘贴时 -这个风险由 PM 承担,改为自读后落到 dev 身上 —— 所以派发词**必须**要求 dev 自查 -正文完整性(读到疑似截断就二次读取确认),这一条不是可选的礼貌话。先例:#6776 -的派发即按此形运行并成功。 - -⛔ **标准非协商条款不再逐条抄进派发词。** worktree / 分支纪律、build-first、 -filter 方向、ADR-0112 拒收断言、authorable-surface 锚点与 `gen:schema` MERGE 态禁令、 -foreground 姿态、英文政策、报告契约 —— 这些已**一次性下沉进 -`.claude/agents/os-dev.md`**(生产者侧修复,正是本文自己引用的 PD#12 直觉)。派发词 -只带**增量**。 - -**角色文件优先级是实测事实,不是文体偏好(#7055)—— 下沉是唯一能生效的修法。** 两条 -配套规则:**对每张卡都成立的无条件条款只能住在 `.claude/agents/os-dev.md`,错了就修 -那里**;**逐卡可变量走显式接口**(模板占位符与三分区),⛔ 不靠派发词临时覆盖角色文件 -的默认值(#7055 实测全文见 references §「#7055 角色文件优先级实测」)。下面模板里保 -留的每一条,要么是逐卡可变的,要么是评审侧对账时点名要看的: - -``` -Your task is issue {backlog_repo}#{n}. The code lands in {target_repo} -(from the issue's repo:* routing label; same repo when unlabeled). - -ISSUE TITLE: {title} - -⛔ READ THE ISSUE YOURSELF — this prompt does NOT contain the body. -Read {backlog_repo}#{n}'s full body AND all of its comments on GitHub before -your first edit (premise-first already requires that read; this just makes it -the only source). Then VERIFY YOU GOT THE WHOLE BODY: long bodies can be -silently truncated by the read tool, and a truncated body reads exactly like a -short one. If the text ends mid-sentence, mid-list, or mid-code-fence, or a -section the title implies is simply absent — read it a second time before -acting on it. Report the truncation if it persists; ⛔ never implement from a -body you are not sure is complete. - -{on rework rounds only:} -PREVIOUS ATTEMPT REVIEW — fix all of these before returning: -{feedback} - -裁决(不可重裁 —— 执行,不重开): -{ruling quotes, verbatim, Chinese untranslated} - -PM 机制假设(须实测,鼓励证伪): -{the PM's own guesses about how the code works} - -PM 建议的路线(可选 —— 实测优先,拒绝它不需要理由之外的许可): -{any implementation option / assertion shape / exclusion the PM merely suggests} - -Card-specific clauses: -{only the conditional standard clauses whose 适用判据 this card hits, plus any -same-day-churn line — see the paragraphs below this template} - -Follow your operating procedure (you are the os-dev agent) — its standard -non-negotiables are binding whether or not they appear above. Deltas for this -card: -- The ISSUE BODY is a LEAD, not a spec: verify its premise against - origin/main BEFORE implementing. If the premise no longer holds (already - fixed, wrong attribution, capability already exists), return - premise_still_valid: false with evidence and NO PR — that is a successful - outcome, not a failure. -- Work in {target_repo}: branch claude/issue-{n}-{slug} off origin/main, - in a DEDICATED worktree of that repo. -- The issue is already claimed; do not touch its assignee. -- Local gates for this card: {name the gate families this card's surface - touches, e.g. check:engine-double-contract for a new fake engine}. Run those - plus your build closure and the affected packages' suites — ⛔ do NOT - enumerate and run the whole `lint.yml` farm locally; CI runs it once. -- Report at draft-PR time: the moment your branch is pushed and the draft PR - is open, deliver your report — record gate status honestly as whatever it - is (`in_progress` included). ⛔ Do not idle-poll CI; the PM owns CI - convergence, the ready-flip and landing. This dispatch contract outranks - any platform-injected PR-subscription posture on your own PR (#7512). - {only on a card the PM rules - heavyweight: 本单等 CI —— wait for the gate jobs' real conclusions before - reporting, as a foreground blocking read} -Return ONLY the JSON report defined in your agent definition — posted FIRST as -an issue comment with the os-dev-report marker, then as your final message. -``` - -**「读 GitHub」比「粘正文」多担一个风险,少担两个。** 截断风险改归 dev,模板里那段 -自查是**风险转移的对价**,⛔ 不是可以省的客套;换来派发词不随卡长膨胀、dev 读到当下 -的 issue(交换机理见 references §「读 GitHub 交换机理」)。 - -**卡特定条款 = 有「适用判据」的那几条。** 下面几段里带「适用判据」的标准条款 -(多面组件的共享一致性覆盖、拒收类用例的 `code`+`status`、过滤 / 谓词语义的编译面 -清单)**仍然逐卡判定、命中才抄进派发词** —— 它们是条件性的,不是通用的,所以没有 -下沉进 os-dev 定义。真正下沉的是**无条件那一批**(worktree、build-first、filter -方向、锚点与 `gen:schema`、英文政策、报告契约),它们对每张卡都成立,重复抄写只是 -在为同一句话反复付费。 - -**派发令里的清单、路径、行号,一律在派发那一刻从树上取 —— ⛔ 不从卡片、上一次派发或 -记忆里抄。** 门禁清单当天就会过期(单班之内长了两次:#6672 / #6661)。派发流程里写 -**取数命令**,不写清单本身: - -```bash -grep -rn 'pnpm.*check:' .github/workflows/*.yml # 门禁清单当场取数 -node scripts/pm/dispatch-gates.mjs <改动路径> [<路径>…] # 文件面 → 该跑的门禁族,现场推导可贴 -``` - -⛔ **取数的是 PM,不是 dev** —— 产出是「本卡该跑的**那几族**」,填进模板那一行。 -本条 ⛔ 不是让 dev 去本地枚举全 farm:那条(#5738 era)已被 #6871 的「Local -verification scope」收窄否掉,farm 归 CI 跑一次。两条合起来才成立 —— dev 只跑被 -点名的几族,所以**点名的准确性从此是 PM 独担的**,凭记忆点名等于把那一族漏进 CI。 - -⚠️ 连「门禁清单 = `lint.yml`」都是记忆形状的断言(#6865 / #6673 实录已移 -`references/incidents.md` §「转述断言重验三例」)。⇒ **卡片或分诊评论里的行级断言, -转述进派发令之前必须自己重验一遍** —— 与 #6413、#6492、编译面复核串是同一条纪律的 -四个位置,派发令是它最后一道出口。 - -**下沉进 os-dev 定义的那三条验证条款(build-first、filter 方向、跨包反向验证)治 -的是同一个病:验证动作看起来做了,实际对被测风险失明。** 两种失明 —— **时序错** -(#6371,陈旧 `dist/*.d.ts` 假红假绿双向骗人)与**方向错**(#6218,pnpm 省略号的 -前后缀扫反消费方向)—— 成因不同、处方不同,一个 PR 可以同时踩中且症状互相掩盖 -(#6210 的「25 个包全绿」既不相关也不新鲜)。两段机理全文已移 -`references/incidents.md` §「两种失明机理」。 -⛔ **条款正文不在这里**:可执行措辞已住进 `.claude/agents/os-dev.md`(「Standard -clauses live HERE」一节),派发词⛔ 不再抄;留在本文的是**为什么** —— step 7 那条 -「N 个包全绿,问清方向与时序」要问得出口,PM 得先认识这个病。 - -**破坏性 / 契约收紧类改动以全仓门禁为准**;局部闭包只能用来加速迭代,⛔ 不能 -用来下结论。这一族的第三个成员是拒收用例的恒绿(#6142,见 step 7 复核清单)—— -三者的共同形状是「绿色输出 ≠ 该绿证明了被测风险」。 - -**派发令里的机制性指导分两个区块,措辞决定 dev 敢不敢证伪**(一班三次前提证伪 -#5561 / #5808 / #5669,全文见 references §「三分区措辞三次证伪」): - -- **「裁决(不可重裁)」**:维护者/PM 已拍板的方向与语义 —— dev 执行,不重开; -- **「PM 机制假设(须实测,鼓励证伪)」**:PM 对代码机制的判断 —— dev 动手前 - 验证,证伪了照实报告并按裁决意图换实现路径,⛔ 不许为了顺从假设硬做; -- **「PM 建议的路线(可选,实测优先)」**:PM 顺手给的实现选项、断言写法、排除面 - —— dev 有更好的就换,⛔ 不得因为「派发令写了」而照做。 - -不分区块的派发令里,机制假设穿着裁决的衣服 —— 两个方向都是返工。**第三块补的是最便 -宜的那一类:PM 顺口给的「看起来无害」的选项**(#6865 / #6893 两次证伪,实录见 -`references/incidents.md` §「便宜选项两次证伪」)。裁决那一块只写真裁决,凡是「我觉得 -可以这样」的一律降到第三块。 - -**文件面要写「预期落点 + 生产者在别包时怎么办」,⛔ 不能只写一个路径名** —— 只写路径 -名的派发令,是要求 dev 在「守约」与「修对」之间二选一(#5586 标本)。文件面写成两句: - -> 预期落点是 ``;若实测表明真正的生产者在别包,**报备后按生产者侧修**(落点与 -> 理由写进报告和 PR 正文),⛔ 不在消费者侧打补丁。 - -PM 侧的对价是**事后补声明**(#6532 先例);**跨包常常等于跨车道**,补的是跨座位声明, -走 rule 4 跨域例外与「跨座位转移协议」,本条不另造机制(#5586 全文见 references -§「#5586 文件面锚错标本」)。 - -**Same-day churn on the issue's files goes INTO the prompt.** Add the line「基于合并后 -的代码工作,issue 引用的片段可能已变,先核对当前 main」whenever `git log origin/main ---oneline -20 -- ` shows a merge on the issue's files today, and tell the dev to -verify against `origin/main` rather than any working tree(notes 4 —— dev 的 worktree -从 `origin/main` 切出后不会自己更新;#4808/#4806、#4820/#4822 标本见 references -§「same-day churn 两标本」)。 - -**In-flight overlap needs intercepting too — same-day churn only covers the dispatch -instant.** Rule: **every round, when you read `git log origin/main`, intersect each -newly-landed PR against every in-flight dispatch's declared file surface** — on any -intersection warn at once, with four instructions:「合 main 后重跑测试矩阵、读对方 -diff 重划边界、只补它没覆盖的部分、**被完全覆盖就停下回报,⛔ 不要硬造 diff**」. -One round late is one rework(#5322/#5335 实测与 notes 8 对偶关系见 references -§「在飞重叠拦截实测」)。 - -**A ruling that flips public semantics ships with a whole-repo pin sweep — say so -in the dispatch prompt.** Pins of the old position live **outside** the package being -changed — the consumer layers each hold a copy(REST envelope tests、objectql、runtime; -#5322 / #5365 标本见 references §「语义翻转 pin 清扫标本」)。Two lines belong in the -prompt: - -- **Flip them all in one pass**:「grep 错误码 / 错误消息(如 `INVALID_FILTER`) - **全仓扫描**同语义 pin,一轮翻完,不要只改本包」; -- **A flipped pin must keep bearing load**: assert the **substance** of the new - semantics (the reduced row count, the translated message, the status code) — - not "old assertion deleted". Refusal assertions for shapes that are *genuinely* - invalid stay **verbatim**: the guarded surface may never shrink. The two lines - are one rule — drop the second and "flip the whole repo" degrades into "delete - the whole repo's assertions", which is green and worthless. - -**多面组件:测试落点是共享一致性覆盖,不是一个新文件 —— 派发令的标准条款。** -适用判据:该组件对**同一个契约**有 ≥2 个实现面。满足时派发令带这一句(原话): - -> 测试放在**未来的分叉会被抓住的地方**,不是放在一个独立测试文件里。若本组件对同一 -> 契约有多个实现面,新用例进共享一致性覆盖。 - -实证:`driver-memory` 三个过滤面上一批缺陷活下来,只因**没有一条断言在问**;本轮 -三次派发(#5375 形状 / #5431 比较数类型 / #5445 算子)长成三条正交的轴共用一条 -不变量,第三条轴还带出「探针必须至少排除一行」(#5374)。实录全文已移 -`references/incidents.md` §「共享一致性覆盖三轴实录」。 - -> 不变量:**与 `find()` 给出相同的行集,或者以 `INVALID_FILTER` 拒收 —— 不允许有 -> 第三种、更安静的答案。** - -适用判据之外不要硬套:样本集中在「一个组件的多个实现面逐层收口」这类工作上,没有 -第二个实现面的活(纯 UI、文档、单面脚本)这条无处可绑,该省掉而不是改写它。 - -**拒收类用例的最低断言集是 `code` + `status`,不是「它抛了」—— 派发令的标准条款。** -适用判据:本单会**新增或改写拒收 / 错误类用例**(验收点里出现「应当被拒收」的活)。 -满足时派发令带这一句(原话): - -> 拒收类用例最低断言**错误的 `code` 与 `status`**(ADR-0112 信封)。 -> `expect(...).toThrow()` / `rejects.toThrow()` 单独使用**不构成**拒收测试;措辞本身 -> 是契约时(#5240「一个条件一种措辞」),首句断言**加在** `code`+`status` **之上**, -> 而不是代替它。本条约束你**新写或改写**的用例 —— 顺手回填存量套件不在本单范围内。 - -出处是 #6142(#6050)的反向验证实测:两种失明机制方向相反、同一个洞(裸 `Error` -⇒ 恒绿;从不抛的 transport ⇒ 红得不指向缺陷)—— 两段实测全文已移 -`references/incidents.md` §「拒收用例两种失明实测」。 -一句话:**一个在缺信封的实现上无法转红的拒收用例,读起来是覆盖,实际不是。** - -**过滤 / 谓词语义裁决:派发令枚举完整的编译面清单,PR 逐面申报 —— 派发令的标准 -条款(#5930 裁决的流程半边)。** 适用判据:本单会**改变一条过滤 / 谓词语义**(算子 -的 NULL 处理、组合子恒等、比较数形状、算子词表……)。满足时派发令**把编译面清单 -逐面抄进去** —— 清单连同其维护纪律与派发前复核串住 -`references/compile-surfaces.md`,派发前照它的复核串重验再抄,⛔ 不凭记忆 —— -并带这一句(原话): - -> 本单改的这条语义由**多个互相独立的编译器 / 求值器**各自实现。派发令列出的**每一 -> 面**都必须在你的 PR 正文里有一个结论:**已改** / **本就合规**(给出证据)/ -> **明确不在范围**(给出理由)。⛔ 不许静默略过 —— 评审把「没提到的面」一律读作 -> 「漏掉的面」,不读作「不需要改」。 - -**这条防的不是「做错」,是「做对了一部分然后以为做完了」。** 一个 `FilterCondition` -语义由 **5 个互相独立的实现**承载(清单见 `references/compile-surfaces.md`)⇒ -每条语义裁决的成本 ×5,而漏面**反复复发**,三次都留在代码注释里(#5146→#5903、 -#5326/#5335、#5905 —— 三例叙述见 `references/incidents.md` -§「编译面清单三次漏面」)。三次都不是难度问题,是没有一份清单在问「还有几面」。 - -**Issue 正文是线索,不是规格 — the dispatch wording is what makes an honest "the -premise is dead" cheap to return.** The prompt must state the premise-first requirement -explicitly(the template line above),and the PM must treat `premise_still_valid: -false` with no PR as a legitimate — often valuable — deliverable(一日四证 #4832 / -#4250 / #5047 / #4930,全文见 references §「premise-first 一日四证」)。A dev that -falsifies the issue — or the PM's own framing — is a good run(step 7 says so);a -prompt that presumes the issue is true converts that good run into apparent -disobedience. - -#### Handing off an interrupted dev(worktree 接手协议) - -`/compact` 与宿主级中断会连带杀死运行中的 subagent(#4700/#4775)。**先试 SendMessage -复活,再谈接手**(从 transcript 带全部上下文恢复,2026-08-05 三例全复活;见 step 6 -探活);仅当 resume 不可用时才走接手协议。Never re-run the original dispatch prompt -over that state — dispatch a **new** agent with these four additions instead: - -- 「worktree `-issue-` 已存在,⛔ 不要新建,`cd` 进去接着做」— the - worktree-first rule is already satisfied; creating a second one splits the work; -- read every existing commit **and** uncommitted change there **first**, then - decide per hunk to keep or amend it — neither restart from scratch nor trust - it blindly (the dead agent never reported, so nothing about it is verified); -- re-run the verification the dead agent never reached, in full, and report its - real output — an interrupted run leaves no test evidence at all; -- the assignee, claim comment and branch stay untouched: this is a continuation - of the existing claim, not a new one, and the claim comment records the - handoff rather than being replaced. - -Both #4700 and #4775 passed review on the first try after being handed off this -way. - -#### Resource limits — parallel agents share ONE container - -Memory peaks come from **build + test**, not editing, so the fix is not less -parallelism but serialized heavy phases: the os-dev definition requires every -build/test run to hold the container-wide verification lock -(`flock /tmp/os-heavy-verify.lock`), a `NODE_OPTIONS=--max-old-space-size` -heap cap, scoped `--filter` builds/tests, capped vitest/turbo workers, and -worktree cleanup after the PR is up. PM-side: treat `batch:3` as assuming -normal-sized tasks — for build-heavy ones (dependency-family upgrades, full -regression passes) drop to `batch:2`, or dispatch that issue via -`mode:cloud` so it gets its own container. If an agent dies with a -heap/OOM signature, redispatch it alone rather than into a full batch. - -**M 及以上默认云卡(维护者 2026-08-10 裁定,#7341 item 10 —— 取代 2026-08-07 的 -「重卡例外单容器」制)。** 原话逐字引用、未翻译: - -> 确认,M 及以上默认云卡,并进 #7341 - -派发后端因此是**批次选择时按尺寸分流**,不再是「验证重量命中判据才例外升舱」: - -- **M 及以上 ⇒ 默认 `mode:cloud` 单独派卡**(独享容器),⛔ 不混进共享容器批次。旧判 - 据清单自此是 **M+ 类的示例**,不再是触发清单 —— 一条都不命中的 M 卡照样走云卡(清 - 单与理由见 references §「云卡分流裁决理由」,实测背景见 §「云卡分流实测背景」)。 -- **只有 S 级机械卡留 `mode:subagent` 共享容器** —— 为琐碎卡单开容器是纯开销, - 规则的两个方向同等硬。**S 级但不机械**的卡(判断面在设计上,不在门禁上)按 - M 待遇走云卡 —— 与 Model tiering「尺寸不是档位的充分判据」同款读法。 -- **三条随行纪律与新默认同段生效**(点名以防散落):云卡 PR 出生即订阅(**硬步 - 骤** —— Dispatch backends 第 4 课);dev 报告的权威通道是 issue 评论 - (``,step 6);卡到终局即 `archive_session`(入队与落地 B)。 -- **判定写进认领评论,并带上模型档位**(step 5「Model tiering」)—— 尺寸/容器与档位 - 是同一次判读的两个输出,分开写只会漂移。形如「容器判定:S 级机械卡,`mode:subagent` - 共享容器,`model: sonnet`」/「L 级,`mode:cloud` 单容器,`model: opus`」/「PM 技能 - 批次卡,`mode:cloud` 单容器,`model: claude-fable-5`(Model tiering 强制条款)」。 - **S 级但不机械**照样写 `model: opus` —— 尺寸不是档位的充分判据,别让这一行的 - 「S 级」自动推出 sonnet。 - -#### Dispatch backends - -**`mode:subagent` — the S-grade mechanical lane.** The `Agent` tool, as -described above. The devs run inside the PM's own session container — which in -Claude Code on the web is already a cloud container, so the whole loop runs -server-side and survives the browser tab closing. Reports come back directly -as the subagent's final message — the accelerator beside the authoritative -issue comment (step 6). Since the 2026-08-10 ruling (「M 及以上默认云卡」, the -sizing section above) this mode is for S-grade mechanical cards, not the -default for everything. - -**`mode:cloud` — the default for M and above** (same ruling). Each issue -becomes an **independent cloud session** in the same environment — its own -container and fresh clone, decoupled from the PM session's lifetime. Also the -fallback for any card that needs resources/lifetime beyond one container, -whatever its size. Requires the `Claude_Code_Remote` MCP tools (available in -remote/web sessions; if absent, say so and fall back to `mode:subagent`). - -**一次性云卡用 `create_session`,⛔ 不用 create_trigger+fire**(维护者 -2026-08-07 拍板;trigger 流只保留给**定时/重复**型 —— 座位 Routine 一节)。 -实测四课(1–3 课 #6083 首派一天踩齐,第 4 课 2026-08-11 A/B),每一条都写进派发动作: - -1. **授权面随 source,不随环境。** trigger 拉起的会话**没有仓库授权**(只能只读 - 勘察);`create_session` 带 `source_url` 的会话**出生即持推送授权**。同时带 - `outcome_branch`(= 认领分支)、显式 `model`(trigger 流不可指模型)、`title` - (**以车道名开头,⛔ 不叫 os-dev**,维护者 2026-08-07 拍板;形如 - `⚡ <车道> #<单号> <短语>`)。403 面细节见 references §「云卡授权面第 1 课实测」。 -2. **派发词必须带自驱条款(回合终点约束)。** 云会话是对话形态,回合一结束就停摆 - 等 poke —— 不写这条,dev 会停在中期汇报或提问处。条款原文形:⛔ 不为提问/中期 - 汇报结束回合; - 开放选择按裁决与三轴/四棱自裁记入终报 open_questions;合法回合终点只有 - (a) 推送完成 + 终报 JSON 作为最后一条消息,或 (b) 硬阻塞详报。 -3. **交付通道:自开 PR + 订阅唤醒是正道,降级通道只属于 trigger 流**(误判沿革与 - 三例实测见 references §「云卡交付通道误判沿革」)。分流: - - **create_session 卡(常态)**:派发词要求 dev **自开 draft PR**(`Fixes #`, - 正文含验证记录)并把终报以 **issue 评论**(``)交付。 - **例外仍归 PM 代办**:未 attach 的姊妹仓够不着,跨仓跟进卡由 PM 代立(#5775)。 - - **trigger 拉起的会话(定时/重复型)**:维持降级通道 —— 推送 outcome branch + - 终报走报告 ref 或最后一条会话消息,PM 代开 draft PR、代转录。 -4. **云卡的 draft PR 一存在,立即对它挂 `subscribe_pr_activity` —— 硬步骤,不是随 - 行提醒。** 没订阅的云卡 PR 是轮询负债:A/B 实测,#7672 未订阅、合并靠轮询迟到数分 - 钟;#7655/#7657 已订阅、通知秒级送达零轮询(全文见 references §「云卡订阅 A/B 实测」)。 - -**监控与转向**:事件面交给 PR 订阅(上条),`get_session` 读实时状态(status / -model / `post_turn_summary`;IDLE + 分支未推送 = 停摆待 poke);投递消息用**绑定 -会话的 poke 触发器**(`create_trigger` 带 `persistent_session_id` + -`fire_trigger` + 用后即 `delete_trigger`)。巡检从主通知通道退为**兜底心跳**: -webhook 不保证送达(CI success/新推送/踢队可能缺席),定时器仍要挂,但频率可放 -宽,且每轮先核订阅已覆盖哪些面、只补盲区(会话停摆、未开 PR 的分支、姊妹仓动静)。 - -**跨会话消息面(维护者 2026-08-11 裁定):事件驱动架构即长期方案。** CCR 云会话 -**不注册**跨会话消息 roster —— `ListAgents` 列不到、`SendMessage` 直投 -not-reachable(可见门槛与实测见 references §「跨会话消息 roster 限制实测」)。 -云卡通道 = PR 订阅 webhook + 双落点报告契约 + 判据式定时器,如上;⛔ 不复测 -roster 路径、不提本地机迁移(2026-08-11 已查证收档,#7755)。 - -#### 座位 Routine 化(PM 侧的运行形态,#5472 第 5 点) - -上面两个 backend 决定**开发 agent** 跑在哪;这一段决定**PM 座位自己**怎么被 -唤醒 —— 它不是第三个 dispatch backend。目标形态:**每个座位一个 cron -Routine**(`create_new_session_on_fire: true`),频率**随该座位的队列深度独立 -调**(轻域每日,重域每小时)。每次 fire: - -1. 读**自己的座位贴正文**(`label:pm:seat` 索引,总入口 #4604 指针页;贴正文 - 即真相,⛔ 不读评论流); -2. **从 labels 重建状态** —— 没有本地状态,`pm:queue` / `pm:dispatched` / - `domain:*` / assignee / `Blocked-by:` 就是全部输入(见「State model」); -3. 跑一轮 —— 执行座位 step 1 → 9;分诊座位 step 0 与 step 2 的分类半边, - ⛔ 永不认领; -4. 结束会话。下一次 fire 从 GitHub 重建,不继承任何上下文。 - -**轮次互斥(防上一 fire 未完成时重入)。** fire 开始时先查**本座位上一轮的 -产出时间**(座位表活性判据的同一读数:该座位最近一条审计/认领评论,或 Routine -的 `last_fired`);距上一轮不足一个轮次时长即**自退**,不做任何写入 —— 宁可 -少跑一轮,不要两个同座位会话并行写标签。配套两条自限(#5474 试点定稿):**每轮 -限量、优先最新**(试点值 ~15 条),以及 step 0 那三处扫描排除。 - -⛔ **实测运维约束 —— fresh-session Routine 必须带 GitHub 连接器创建。** 会话内 -`create_trigger` 建的 Routine 不携带 GitHub 连接器,fired session 拿不到 -`mcp__github__*` 工具,表现为**静默零产出**(#5474 试点即此败,全文见 references -§「fresh-session Routine 连接器约束实测」)。因此: - -- 座位 Routine **由维护者从 claude.ai 的 Routines UI 创建**并勾上 GitHub - 连接器;⛔ 不要在会话里 `create_trigger` 出一个座位 Routine 就当它在跑。 -- 创建后**先手动 fire 一轮烟测**,判据取 **GitHub 上的产出**(标签写入 / 审计 - 评论 / 座位表编辑),不取「会话看起来起来了」;零产出即技术性失败,按回滚 - 处方 `delete_trigger` + 清空座位行 + 失败注记。 -- 模型不能经 API 钉住(`update_trigger` 返回 `model_update_disabled`,本部署 - 禁用工具侧改模型):Routine **继承环境默认模型**,环境默认变了它跟着变; - 要硬钉同样走 Routines UI。 - -**现役两例(都由维护者从 UI 创建、都先过一轮烟测)**:分诊座位(#5474,只扫/分类/打 -标签,⛔ 永不认领)与三仓队列管家(#5810,cron 与分诊错开半个周期,管「入队与落地 B」 -里入队之后的那一半)。**档位按职责挑,不按重要性挑**;档位与 cron 是维护者在 UI 上的 -可调项,本文 ⛔ 不复制其当前值,它的 `pm:seat` 座位贴才是现状(全文见 references -§「现役座位 Routine 两例」)。 - -**跨 fire 的长流程照旧可行,因为它们的状态本来就在 GitHub 上。**「串行接力」 -(step 7)一棒就是一整圈、棒间还夹一次 PM 复核,必然跨多个 fire;能跨得过去的 -原因是接力的交接物全是 GitHub 上的读数 —— draft/ready 状态、`auto-merge` 是否 -挂上、以及「预期红停放」那份**签名级**预期红清单写在 PR body 里。座位 Routine -因此对长流程只有一条额外要求:**接力/停放的每一项都必须落成 GitHub 上可读的 -文本,⛔ 不许把「下一棒该干什么」留在会话记忆里** —— 下一轮 fire 没有会话记忆 -可读,它只能重建。这与「从 labels 重建状态」是同一条纪律的两半。 - -### 6. Collect - -**报告通道统一(#7341 item 3):GitHub 是两种模式共用的真相源。** 每个 dev 的 -终报**同时**落两处 —— issue 评论(首行 `` 标记)+ 它自己 -通道的返回消息;评论是记录,返回消息是**加速器**。收集因此先读 GitHub:标记评论 -在而返回消息没到 = 报告完整(照常验收);返回消息到了 = 顺手用,省一次扫描; -两处都没有才进入探活 / 判死流程。这一条把 `mode:subagent` 从「返回消息是唯一 -通道」的单点上解下来 —— 会话销毁、进程重启丢的只是加速器,不再是报告本身。 - -**Subagent mode:** wait for the background task notifications — do not poll -for *results*, do not fabricate a pending agent's result. A dev that dies or -returns malformed output counts as `status: "blocked"` with its raw output -attached — **after** sweeping its issue for the `` -comment first: a dev that died between its GitHub write and its return -message has already reported. - -**探活是每轮巡检的固定动作 —— 完成通知不可靠,它的缺席什么都不证明。** 宿主进程重启 -会把运行中的 subagent 连同完成通知一起静默杀掉(2026-08-05 实测五飞三死零信号,全文 -见 references §「探活规程实测背景」)。规程五条: - -- 每次巡检对**每个已派发且报告未达**的 dev 发一次状态询问(SendMessage,措辞「回一段 - 简报后继续干活」,不改变任务);派发后 ~45 分钟无任何远程产出即到探活门槛。⛔ 已有 - 远程分支/PR 不豁免(见下「探活是常设兜底」)。 -- 两种回包都有价值:活着 → 拿到进度与阻塞点;「no active task; resumed from - transcript」→ agent 生前已死,**这次询问本身就是复活** —— 优先用它,resume 不可用 - 才走接手协议(step 5)。 -- 判据永远取正向证据(远程分支、PR、报告、探活回包),⛔ 绝不把「还没收到失败通知」 - 读作「还在跑」。 -- **定时器重挂是每次巡检的第一动作,不是最后一个**(维护者 2026-08-06 授权;一次漏挂 - 静默 ~100 分钟实测)。先挂后查,链条对中断免疫;重挂文本按 notes 3 写法纪律:以 - 「幂等 —— 动手前先重读状态」开头、只写判据不写结论。 -- **批量在飞期间,主巡检间隔不得长于 45 分钟**(同一授权);在飞清零的待命期可放宽到 - 60-70 分钟,有 dev 在飞即收紧回 ≤45,灭批频发期压到 20-30 分钟。 - -**探活是常设兜底,不是异常通道 ——「PR 已经开出来了」不是豁免,那恰恰是失败发生的那 -一格。** 触发判据:**探的是「报告未达」,不是「分支未出现」** —— 远程产出证明工作发生 -过,不证明 agent 还在(2026-08-08 实测 7/7 都死在开出正确 PR 之后,全文见 references -§「探活触发面证伪实测(7/7)」)。三条边界: - -- **生产侧的条款不能替代它**(逐字带终止条款的派发 4 个死 3 个)—— 兜底必须常设在 - 消费侧,⛔ 不能写成「派发词写全了就可以不探」。 -- **代价是延迟,不是正确性** —— ⛔ 永远不要拿「重新派发」回应探活缺位:先探,拿到 - 「no active task; resumed from transcript」一类回包再谈恢复。 -- 与判死门槛的分界一字不变:⛔ 不降低判死的三类正当依据;已有 PR 的那一格尤其要守住 - ——「报告丢失 ≠ 验收停摆」的条件二正是**探活确认已死或 ≥2h 无推送**,不是「PR 看着 - 能收了」。 - -**45 分钟是发探针的门槛,⛔ 不是判死的门槛 —— 两者必须分开**:探针的代价是一次 -SendMessage,判死的代价是**重新派发**,即往一个可能还活着的 worktree 里塞第二个 -agent(step 5「Handing off an interrupted dev」的碰撞面)。 - -- 判死的正当依据只有三类:**探针回包表明已死**(「no active task; resumed from - transcript」一类)、**宿主明确回报 stopped**、或**超过本车道基线且连续静默**。 - ⛔ 「探针门槛过了两次」不在其中 —— 它只说明它还在跑。 -- 「超过基线」里的基线是**你自己车道实测的完工耗时**,⛔ 不是本文里的任何常数。 - 没有基线就先建基线再判:同形态卡片各记一个「派发 → 推分支 / 开 PR」的端到端 - 耗时,三五单即可用。**在基线之内的沉默不是证据。** -- 两条实测基线**只是出处样例,⛔ 不是全车队常数** —— 卡片形态不同,区间没有理由 - 相同,driver 或 engine-core 的重活不适用它们。样例表(两席九单,端到端 - ~64 分钟到近 3 小时)与两个座位当天各一次的误判实录已移 - `references/incidents.md` §「判死基线样例与两次误判」。 - -- 与既有两个数字的关系:**45 分钟 = 探活门槛**(去问);**`mode:cloud` 的 ~2h 静默 = - 本轮收集边界**(记 `blocked`、本轮不再等);⛔ 两者都不是判死 ——「报告丢失 ≠ 验收 - 停摆」把 ≥2h 与探活确认已死并列成两个条件,正是这个区分。这与 notes 11 是同一失效 - 类换了个变量:知道基线之前,「正常但慢」与「已死」的读数完全相同。 - -**A stalled subagent is this half's most common failure, and it never self-heals** — a -completion notification is itself the statement that no live subtask remains(一夜 4 个 -agent 停摆 6 次的实测全文见 references §「Stall 复位梯度实测」)。Rules: - -- **State the execution posture in the dispatch/relay prompt** for any long verification - pipeline:「**前台(阻塞)同步执行全部步骤,中途不停止、不把构建/测试挂到后台等 - 唤醒**」. -- **A completion notification carrying a MID-TASK state IS the stall signal** — - SendMessage it back immediately with that posture line attached; ⛔ do not wait out any - silence threshold: a threshold is for *no* answer, not for an answer that says the - agent stopped. -- **每一次复位比上一次更具体 —— 原样重发同一句话不算一次复位。** 梯度:① 复述执行 - 姿态 → ② 点名下一个工具调用 + 明令禁止后台等待 → ③ 判 unreliable。自己挂的定时器 - 不会唤醒自己(2026-08-09 三例实测见同节)。 -- **A third stall means unreliable** — re-dispatch a fresh agent onto that branch under - "Handing off an interrupted dev" in step 5(worktree already exists, read every - existing commit first, re-run the verification in full, claim and assignee untouched). - -The **producer-side** half lives in `.claude/agents/os-dev.md`'s resource discipline — -these are the backstop, not the primary fix(PD #12). - -**通知到达 ≠ 有新东西发生 —— 第一眼读它是谁,不是读它带的 JSON。** 通知可以形态完全 -正常、报告完整正确,而只是同一份东西的第 N 次重放(#5330 一张卡 6 条通知、5 条重放, -全文见 references §「#5330 通知重放实测」)。处置: - -- **先算身份,再决定读不读内容。** 去重三元组:`(issue, 分支, PR head sha)` + 通知 - **自报**的守护对象;与本轮已验收的那份逐项相同 ⇒ 台账记一行「重放,首达时间 T」 - 即结束,⛔ 不重新验收、⛔ 不重读 diff、⛔ 不重复留 ACCEPT 评论。 -- ⛔ **不把它的到达读成「还活着」**(重放来自按自己 deadline 触发的 monitor);活着的 - 判据只有探针回包。 -- ⛔ **也不把它的不到达读成「已经死了」** —— 判死照旧只认三类正当依据。 -- **生产侧的自报是「变便宜」,不是前提**(os-dev 终止契约,#6586)—— 对面自报了就省 - 一步,没自报就用三元组自己算,⛔ 不把去重建立在「对面会自报」上。 - -**Cloud mode:** there is no direct return channel — collect through GitHub, -which since the report-channel unification is the same sweep as subagent -mode's, not a degraded special case. Arm a `send_later` check-in (~15 min); on -each wake, sweep the dispatched -issues for `` comments and linked PRs, then re-arm -silently until every dispatch of the round has reported or a dispatch has -been silent for over ~2 h (count it as `blocked` and move on). Never treat -the absence of a report as success. - -**座位 Routine 模式下的收集边界。** 一次 fire 就是一轮;报告通道统一后会话销毁丢的 -只是加速器,dev 终报由下一次 fire 从 issue 评论(``)照常收到。 -跨轮未收的 dispatch 由下一轮按同一判据处置(~2h 无报告即 `blocked`),`delete_trigger` -清理顺延。停摆纠偏的恢复动作是 SendMessage,需要**还活着的对面** —— 所以凡验证管线 -可能超过一个 fire 的活,**一开始就走 `mode:cloud`**,把恢复权交给下一轮的 GitHub 读数 -(阐释见 references §「座位 Routine 收集边界阐释」)。 - -**报告丢失 ≠ 验收停摆(直接验收兜底)。** dev 的 JSON 报告是证据来源之一, -不是验收的先决条件 —— 状态模型第一句就是「所有状态在 GitHub」。同时满足 -(a) draft PR 已存在且 CI 全绿、(b) 探活确认 agent 已死或 ≥2h 无任何推送、 -(c) 报告未达 —— 则 PM 直接按 PR 验收:逐文件核对 diff 与认领申报的文件面, -对照 `origin/main` 复核 PR 正文的前提声明与验证叙述,step 7 其余判据不变 -(2026-08-05 的 #5550/#5556 即此路径落地并合并)。顺序保护:agent 可能还 -活着时**先探活、后翻 ready** —— 抢先翻会与它的收尾推送竞态。 - -**死因可以是舰队级的,那一格里探活半边同时不可用**(两次全账号 token 断粮各打死四个 -在飞 dev)—— 此时三条件里能取的只剩 (a) 与 (c),PM 走直接验收照常收口(报告丢了, -PR body 就是报告)。「推分支 → 开 draft PR → 立即交报告」是**保险,不是效率优化**; -⛔ 但这不推出「把该顺序抄进派发令」:它是无条件条款,住在 `.claude/agents/os-dev.md` -的 Definition of done,派发令只带增量(实测全文见 references §「舰队级死因与即报 -保险实测」)。 - -### 7. Review each report - -You are the reviewer of record. For each report, verify against GitHub — not -against the report's own claims: - -- The PR exists, is a draft, targets `main`, and its body's first line - references the card — **`Fixes #{n}` only if merging it should close the - card**;否则 `Part of #{n}`(见下面「`Fixes` 还是 `Part of` 是 PM 的判断」)。 -- Fetch the PR's changed files. Scope check: no `content/docs/releases/` - edits, a `.changeset/*.md` is present for anything user-visible, no files - plainly unrelated to the issue. -- Test evidence in the report shows the actual commands and passing output, - not a bare "tests pass". -- **报告在草稿 PR 时点到达 —— CI 收敛读数自此只属于复核侧(#6644 L2,维护者 - 2026-08-10 裁定)。** dev 的契约是「推分支 → 开 draft PR → 立即交报告」,报告里的 - gate 状态照实记(`in_progress` 是诚实读数),⛔ 不等收敛 ——「报告到了、CI 还没绿」 - 是预期内常态。守门职责**移交**到本侧:arm auto-merge / 入队前**亲核门禁 job 的结论** - —— 不止聚合读数,要看 ESLint 与 TypeScript Type Check 两个具体 job 的 `conclusion` - 已为 `success`(notes 10;#5584)。这道读数是**唯一的一道**,⛔ 不要因为报告写了 - 「本地绿」就跳过它。收敛期间转红的门走补丁轮(SendMessage 续派原 dev)—— 那是这笔 - 交换已付过的价钱,不是 REWORK 的理由;红着合并才是。重量级卡可在派发令显式写 - 「本单等 CI」(step 5 模板)。决定性实测(#6041 / #6906)全文见 references - §「L2 即报契约与决定性实测」。 -- The diff plausibly satisfies the issue's acceptance criteria. -- **收益穿过它必经的那道边界之后还在吗?** 判据(不是每单都做):这批工作的价值主张 - 是否**依赖某个下游组件如实转发** —— HTTP 错误信封、序列化、日志汇聚、跨进程传输。 - 是,就至少**端到端验一次**收益在边界之后仍然存在。实例(#5423:REST 4xx 直通曾把 - 精心写的拒收正文**整条替换**成 `'Request failed'`,四个 PR 合入无人查过,缺口在 - 清单里不在代码里)全文已移 `references/incidents.md` §「收益穿过边界实例」。 -- **A ruling-implementation PR: were the old position's pins flipped repo-wide?** - Fetch the changed-file list, then grep the consumer layers (REST envelope - tests, objectql, runtime) for the ruling's error code / message and confirm no - copy of the old position survives — **and** that refusal assertions for - genuinely invalid shapes are still there (step 5's two lines). #5365 slipped - through exactly this review layer and was caught by CI instead: CI does catch - it, at the price of one extra lap. -- **拒收类用例的绿,是不是「它抛了」的绿?** 判据:本单验收点含「应当被拒收」。抽查 - diff 里的拒收用例有没有断言 `code` 与 `status`(ADR-0112 信封)—— 只写 `toThrow()` / - `rejects.toThrow()` 的用例,在**未修实现本来就抛裸 Error** 的那一族上恒绿(#6142 - 实测:`driver-sql` 删闸后 22 红中多数是裸 knex Error,`code` / `status` 均 - `undefined`),于是「28 例全绿」这种报告读起来是覆盖、实际证不了拒收。缺断言判 - REWORK 补齐,而不是接受绿色输出。本条是 step 5 那条标准条款在复核侧的对账。 -- **报告里的「N 个包全绿」,问清方向与时序,否则不算清扫证据。** 判据:本单含跨包 - 签名收窄 / 导出类型变更 / 契约收紧。两问 ——(1)过滤器用的是**前缀** `'...pkg'` - (下游消费者)还是后缀 `'pkg...'`(上游依赖)?方向没写就无法复核,#6210 的 - 「25 个包全绿」用的是后缀,CI 全仓随即红在 `@objectstack/dogfood`;(2)跑 - typecheck 之前**建过依赖闭包**吗?没建则读的是陈旧 `dist/*.d.ts`,那个绿可能是 - 假绿(#6371)。两问都没答案 ⇒ 以全仓门禁结论为准,或判 REWORK 要证据。本条是 - step 5 那两条验证条款在复核侧的对账。 -- **Did the dev verify the issue's premise?** The report's - `premise_still_valid` field makes the answer explicit — a `false` there - reopens triage rather than failing review. A report that falsifies the - issue — or your own dispatch prompt — is a sign of a *good* run; a report - that accepts every stated cause at face value is the one to read twice(同日四例 - #4808 / #4813 / #4825 / #4790,全文见 references §「premise 证伪四例」)。When a dev - corrects the PM, **acknowledge it in the open** — the correction belongs in the - PR/issue comments so the next reader inherits the corrected premise; a wrong premise - still sitting in an issue body gets its own follow-up issue rather than being - silently dropped. -- **验收判据本身也是前提的一部分,可被 dev 证伪。** dev 用测量推翻字面判据、换上等价 - 或更强的不变量门禁 = 好运行,照 ACCEPT;但推翻过程必须写在 PR 正文里,新判据要附 - main 语料上的实测信噪比(误报为零的证据),否则按 REWORK 要证据(#5452 标本见 - references §「#5452 验收判据证伪标本」)。 -- **Tests/docs-only PR 走 `skip-changeset` 标签,不走空 changeset**(空 changeset - 滞留发布,#4898);标签由 PM 在验收时打。历史坑通病(#5497/#5502,#5625 已根治): - **任何从事件载荷而非现状读判据的闸,rerun 都复现旧世界** —— 同形状的红先查该闸读的 - 是载荷还是现状(全文见 references §「事件载荷闸历史坑」)。边界:改动含读者可见的 - 生成产物时,dev 选 changeset 是对的 —— 以 PR 正文说明的理由为准,两条路都有效, - 别来回改。 -- **`+0/-0` in a PR diff is not proof of an empty file.** A NUL byte makes git render - the file as binary(#4870:347 行测试文件显示 `+0/-0`)。On any `+0/-0` entry suspect - NUL first, settle it on the blob(`git show : | wc -l`),and require the - source fix `\u0000`(`scripts/check-nul-bytes.mjs` 的约定;raw NUL 还让整个文件对 - grep 隐身)。全文见 references §「+0/-0 NUL 标本」。 -- **以「死代码 / 不可达」为由的删除,PM 在 `origin/main` 上自己核一次引用面,再 - ACCEPT。** 这份清单的其余各条都围绕「改动是否正确」构造,删除是另一回事:它比修改 - 难回滚,而且「这是死代码」是一个**断言,不是能从 diff 读出的事实** —— dev 的推理 - 可以成立仍然错,而带引号精确名的 `git grep`(origin/main 上)只花十秒。查法用 - Operational notes 6(带引号精确名、查声明式而非查提及):notes 6 说「怎么查才不会 - 假阴性」,本条说「什么时候必须查」。实测标本(#5445:两条映射删对了,引用面十秒 - 核毕)全文已移 `references/incidents.md` §「死代码删除核引用面标本」。 - -Verdict per issue: - -- **ACCEPT** — comment on the issue (English) linking the PR and summarizing - what shipped. Then drive it to landing (maintainer policy: review passed + - CI green ⇒ merge): once every check on the PR is green, **先按下面那段读一次 - PR 的路径面**(⛔ 碰 ADR 或技能根的 PR 在这里就到头了,不走这条路);路径面干净,才 - mark it ready for review and **add it to the merge queue** — the queue rebuilds the PR - against current `main` and lands it only if that result is green, which is - the repo's sanctioned path. Never `--auto`-merge outside the queue; where - no queue exists, merge serially per AGENTS.md §7 only after remote CI is - fully green. This applies to **dev-agent PRs dispatched by this loop - only** — the PM's own tooling PRs stay with the maintainer(唯一例外见 - Guardrails:维护者明示授权的 `.claude/` 工具 PR,授权引用于 PR 正文 + - walkthrough 复核,不得自审自合)。 - - **⛔ ACCEPT 在动手之前先按路径分叉 —— 碰 ADR 或技能根的 PR,终局动作不是 - 入队。** 这个判据必须落在**手动之前**,不能只写在 Guardrails 里等事后对照(#6732 - 即 ACCEPT 照章办事把一份全绿 ADR PR 推进了队列;全文见 references §「#6732 路径 - 分叉由来」)。维护者裁决原文见 Guardrails 那条(引用、未翻译)。 - - 翻 ready / 挂 auto-merge / 入队之前,先取一次 PR 的路径面 - (`pull_request_read` 的 `get_files`,⛔ 不看报告的自述)。**只要路径面里有一条 - 命中** `docs/adr/**`、`.claude/skills/**` 或 `skills/**`(skills 更新与 ADR - 同级,维护者 2026-08-11 裁定,#7548;引文见 Guardrails),ACCEPT 换一套终局 - 状态,三件缺一不可: - - 1. **复核结论照常写在 issue 上** —— 不能合不等于不复核,ACCEPT 的复核部分一字 - 不减; - 2. **PR 留给维护者** —— ⛔ 不合并、⛔ 不入队、⛔ 不挂 auto-merge。ready 或 draft - 都行,判据是**它必须看得见地悬着**,不是被悄悄停在一边; - 3. **轮次报告里单列一行「awaiting a human merge」** —— 这是它不会烂成静默积压 - 的唯一读数:在 GitHub 上,「等着人来合」与「被忘了」长得一模一样,区别只存在 - 于有没有人每轮把它念一遍。 - - 混合 diff(既动上述路径之一也动别的)走同一分叉 —— **一条命中就够**,⛔ 不按 - 比例判、⛔ 不用「主要改的是别的」放行;要拆就让 dev 把 ADR / skills 那部分单独 - 开一个 PR。 - 已经入队之后才读到这一条?撤回动作见 Guardrails 那条的 ⚠️:**只有转 draft 才真 - 的踢出队列**,`disable_pr_auto_merge` 单独调用不解除成员资格。 - - **`Fixes` 还是 `Part of` 是 PM 的判断,而且必须在入队之前核。** dev 的默认模板 - 写的是 `Fixes`;当这一单只落地了**可实施的那一半**(另一半 `needs_decision` 还在 - 维护者决策箱里,或按范围被有意排除),PR 首行必须是 **`Part of #N`** —— 否则合并 - 会**静默关掉一张正躺在决策箱里的卡**,而 `needs-user-decision` 的收件箱过滤只看 - **开着的** issue —— 卡一关,那个待裁问题就此无人可见。**翻 ready 之前亲核首行**, - 别只信报告(#6644;实测 #6190 / PR #6600 见 references §「#6190 Fixes 误标实测」)。 - - **卡的交付物里含系统性 sweep 时,ACCEPT 评论把 sweep 的产物成组列出。** 判据: - 这一单做的是 D5 式退场审计、消费半径 grep、语料扫描一类的**系统性排查**。此时 - 「范围外发现照旧单开」(PD #10)会一次性吐出一**批**卡 —— ACCEPT 评论里列成一块, - 并写明 sweep 判据,让分诊能一致地给整批定级,而不是逐张各判各的(#6644;#3682 一次 - 派发产出 5 张,实测见 references §「#3682 sweep 产物成组实测」)。 -- **REWORK** — concrete, itemized feedback; re-dispatch the same issue with - the feedback block filled (same claim, new dev agent). **Max 2 rework - rounds** per issue; a third failure escalates instead. - **补丁轮优先 SendMessage 续派原 dev,而不是新派一个。** 适用判据:认领未变、 - 原 dev 会话仍可达(活着,或能从 transcript 复活 —— step 6 的探活回包会告诉你)。 - 一班四次续派全部一轮成功(#5738/#5808 的 CI 红补丁、#5561 的分析→实现、#5808 - 的裁决→收尾),上下文保留省掉全部重验;新派 dev 则要从零重建现场。例外照旧: - 第三次停摆判 unreliable(step 6),或会话已不可 resume —— 走 step 5 的 worktree - 接手协议。 -- **ESCALATE** — see step 8. - -#### 入队与落地 —— ACCEPT 之后才是最容易丢单的一段 - -**A. 碰生成物的 PR,入队前必须先同步 + 整体重生成。** 第 3 步只保证**同一批内** -file-disjoint;它管不到**先后两单都碰 `packages/spec` 生成物**的情形 —— 而协议变更 -几乎必然如此。被路由到 `merge=os-regen` 的路径清单**当场读、不照抄**: - -```bash -grep os-regen .gitattributes # 唯一权威清单;⛔ 别把结果抄进派发令当常量 -``` - -⛔ **别只记住 `packages/spec/` 那几条** —— 清单里同时有**文档产物** -(`docs/` 与 `content/docs/references/**`),它们同样会被静默吞。 - -**这份清单不能有第二份拷贝 —— 本节曾亲自示范为什么(#6492:三个互相矛盾的读数、 -一小时内自漂移;实录见 `references/incidents.md` §「os-regen 清单第二份拷贝」)。** -所以本节只留取数命令: -散文没法被类型检查,唯一不会烂的拷贝是不存在的那份。同源条款见 step 5 的编译面 -清单(#5905)与 `Record` 反烂模式(#6322)。 - -清单里有一条需要额外分辨:`docs/audits/2026-07-unknown-key-strictness-ledger.counts.md` -是 #5107 加的 —— strictness 台账的**数字**转成了生成物(`gen:strictness-ledger`), -散文仍手写在同名的 `.md` 里。派 #4001 后续批次时要分清 —— **台账正文照常文本合并 -(它一直合得很干净),只有 `.counts.md` 走驱动**。 - -该驱动会让 merge **exit 0、零冲突标记**,却**静默丢掉一侧的改动** —— 只有重新生成 -才暴露。2026-08-03 一天内在 #4809 / #4846 / #4841 三个 PR 上各复现一次。要求 dev -按顺序做,四步缺一不可: - -1. `git merge origin/main`(⛔ 禁 rebase / force-push,AGENTS.md §3) -2. `git checkout origin/main -- <上述生成物>` -3. **先 commit 掉这次 merge,再整体重新生成**(⛔ 绝不做文本合并;⛔ 也绝不在 MERGE - 状态下跑 `gen:schema` —— 那会静默倒退锚点,见下一段与 #5370) -4. 断言**所有兄弟单的条目都还在** —— #4878 落地时是四条 step17 条目并存 - -一个比「条目还在」更硬的旁证:去查**上一单的实现体**是否完好(#4878 合并后核 -`conversions/registry.ts` 里 #4391 的 D2 conversion 仍有 16 处命中)。条目是索引, -实现体才是被吞的重灾区。命令形固定(此前是口头惯例,#5885 第 10 条入册): - -```bash -git grep "<兄弟单的符号/条目名>" origin/main -- <生成物路径> # 兄弟单条目仍在 -git grep "<上一单实现体符号>" origin/main -- <实现文件> # 实现体完好 -``` - -带引号精确名、查声明式而非提及(Operational notes 6 的查法在这里逐字适用)。 -驱动本身另有缺陷(把绝对路径写死进最后跑过 `pnpm install` -的那个 worktree),见 #4868。 - -**锚点(`authorable-surface.base.json`)的断言措辞 —— 写错会教唆 dev 手改锚点。** -接力模板曾用错误断言「baseRev == merge-base」派发,后棒实测证伪并纠正(#5304 / -#4650 沿革)。正确措辞(2026-08-05 夜后八棒全部沿用,零误报): - -> 断言 `pnpm --filter @objectstack/spec check:authorable-surface` **绿**即可。锚点 -> authenticity 的定义是两件事:`baseRev` 是 `origin/main` 的**祖先**,且它记录的 keys -> 与**该 commit** 的 authorable surface 逐行一致(`verifyCommittedSurfaceBase` -> 就查这两条)。`baseRev` **允许滞后** —— `gen:schema` 只在 keys 真的漂移时才重写它 -> (在 `main` 上 merge base 就是 HEAD,该文件**必然**落后自己的 surface 一个 PR), -> 滞后只打一行 `ℹ️`,不是错误。⛔ 禁止为了凑「相等」手改锚点文件 —— 那正是 #4650 -> 堵住的攻击本身;⛔ 不得要求 `baseRev == merge-base`,那个等式不是任何门的判据。 - -配套两个新陷阱(都已立单,派发词**引用**即可,⛔ 不要在接力单里顺手实现它们): - -- **#5370:merge 未 commit 就跑 `gen:schema`,会把锚点静默倒退回旧 merge-base。** - MERGE 状态下 HEAD 仍是合并前的分支 tip,`resolveSurfaceBase()` 解出的是分支的**旧** - 分叉点;倒退后的锚点**依然 authentic**(旧 rev 也是 origin/main 的祖先、keys 也对得 - 上),于是**全部门放行**,一次已合并的锚点推进被静默撤销。这就是四步序里「先 commit - merge,再整体重生成」的由来。 -- **#5371:`gen:schema` 的 `rmSync` 顺手抹掉 `gen:openapi` 的产物。** 之后跑 - `@objectstack/rest` 会拿到 5 条 `expected 503 to be 200` 的**假红**(`openapi.json` - 被清场,不是 rest 坏了)。派发词里直接给解法:`pnpm --filter @objectstack/spec - gen:openapi` 补回,或重跑一次完整 spec build。 - -**B. 跟到 MERGED 为止,不是跟到「已入队」为止 —— 但入队之后的看护已归专责座位。** -「auto-merge 已挂上」不是终点,维护者对此有过明确纠正;2026-08-06 起这一段按下表分工 -(维护者拍板设**三仓队列管家** Routine 座位,锚点 #5810,座位贴见 `pm:seat` 索引): - -| 谁 | 管什么 | -|---|---| -| **车道 PM**(权责不变) | 验收(step 7);**首次入队**(转 ready + 挂 auto-merge);确认 **MERGED** —— 每轮同时读**队列分支**与 `origin/main`(Operational notes 1) | -| **队列管家**(三仓一座,#5810) | 入队之后的看护:红/踢出的**签名分诊四分支**、队列停滞检测、跨仓 pin 链观测及其**机械产出**(窗口收口/发版后立 bump 单,#6162,见下) | - -车道 PM 的「首次入队」有一个标准动作:**ACCEPT 后立即挂 6–9 分钟的 send_later -flip 定点**,到点核对门禁 job 结论(notes 10)、绿即转 ready + 挂 auto-merge,未绿 -再阶梯重挂;#6644 L2 后,收敛读数、翻牌、入队的整段守门归这里。CI success webhook -不可靠(一班 13/13 全由定点驱动,#5885),⛔ 不坐等 webhook、不忙轮询。定点文本按 -notes 3 的**写法纪律**:以「幂等 —— 动手前先重读状态」开头、只写判据(「若 #N 的 -门禁 job 结论全绿 ⇒ 转 ready 并挂 auto-merge」),⛔ 不写结论 —— 窗口内 PR 可能已被 -他手处置(管家/踢出/转 `dirty`/先入队),而已删除的定时器仍会投递。 - -**落地窗口给关键 PR 挂事件订阅(`subscribe_pr_activity`,维护者 2026-08-07 拍板)。** -适用面:会话型座位、且 PR 已进入 PM 的落地窗口 —— ACCEPT 后,以及被压在多步落地序 -列里的单(补圈集、等上游 MERGED 的压后单);订阅把感知从「一个巡检周期的轮询滞后」 -缩到实时(动因与 #6072 实测见 references §「#6072 感知通道缺口实测」)。四条边界: -- ⛔ 不订阅 dev 交报告前的 PR —— 报告前是 dev 的领地,双驾驶员互踩(#6644 L2 后 - 报告在草稿 PR 开出即刻到达,云卡出生即订阅因此不越此界); -- 订阅是**感知补充**,不替代 flip 定点(上一段一字不变:CI success webhook - 依旧不可靠,转 ready 仍由定点驱动); -- **MERGED / 关闭即退订(`unsubscribe_pr_activity`),同刻把 `mode:cloud` 派出的那 - 个会话 `archive_session`(维护者 2026-08-10)。** 归档与「flip / 跟到 MERGED」同 - 级、不是可选清理:触发条件是**卡的终局**(PR MERGED 或卡作废)**且报告已收复核**; - 巡检顺手批扫 `list_sessions` 的 review_ready 存量(实测见 references §「云卡归档 - 欠账与 dirty 自救实测」)。⛔ PR 合并前不归档 —— 活会话是 dirty 自救的执行手 - (#7265/#7325);误归档可 `unarchive_session`,但容器现场已失,宁晚勿早; -- 仅会话型座位可用 —— webhook 投进订阅它的那个会话,Routine 座位每 fire 新 - 会话、收不到,维持轮询姿态。暂停/交接时清点在挂的订阅并写进座位贴,⛔ 不留 - 孤儿订阅。 - -车道 PM 因此**不再自挂 flaky 盯守定时器** —— 已入过队的 PR 被踢出后,认签名与原样重投 -是管家的活。管家的四分支照 #5810 的签名台账机械执行:**已知 flaky** ⇒ 原样重投; -**已修签名再现** ⇒ ⛔ 不重投,按 notes 2 判为新问题、通知车道重新诊断;**基上缺一个 -已合入的修复**(notes 5) ⇒ 指引 `merge origin/main` 推新提交、⛔ 重跑无效;**新签名** -⇒ ⛔ 不重投,在 PR 与其 `Fixes` issue 各留完整签名与初步判读。收到「新签名」或「已修 -签名再现」通知的车道 PM 按**原纪律**处置(notes 2 与 5,一字未改)—— 通知只换了谁先 -发现,没换谁负责修。 - -管家的授权面是**只守落地**:⛔ 永不合并、永不 ready/draft 切换、永不把没入过队的 PR -入队、不碰代码、不动认领。所以落地之后**再核一次落地判据本身**仍是车道 PM 的活 —— -队列的合并同样走 os-regen 驱动,A 里那个静默吞并在队列合并这一步一样能发生。 - -**签名台账 ⛔ 只有人工能升级。** 台账在 #5810 正文(三仓分表 + 跨仓通用共四张),是四 -分支的唯一判据,优先于任何一侧的现场判断;管家发现疑似新 flaky **只能在锚点单留一行 -提请**,⛔ 不自行加表,车道 PM 同样不加。追记纪律照 notes 2:纯计数不记,只有改变修法 -作用域时才记。 - -**双向让行 —— 同一个红两个座位都可能动手,谁先动谁处置。** 管家在处置任一 PR 前先读它 -**最近 30 分钟的评论**:车道已在处置就让行,留一行「队列管家让行」不再介入;车道反过来 -在动手前同样读一遍,管家已留处置评论的不重复诊断。双方**每次动作都留审计评论**(重投 -写签名与台账依据、拦截写判定、让行写让行),让行判据因此始终是 GitHub 上的一个读数, -不靠猜 —— 实测最紧的一次是车道回报早于管家读数 50 秒,少了这条纪律就是两份诊断。 - -**Pin 链观测的机械产出 —— 窗口收口即立单,不等发版红灯(维护者 2026-08-07 拍板, -#6162)。** required 新鲜度门(#3340)只防「带旧 pin 切版丢变更」,不防「切版现场才 -发现要 bump」(#6159 实测)。管家的 pin 链观测每轮附带机械产出,两个触发各产一张 -杂事单(⛔ 仍**只立单,不执行 bump** —— 授权面一字不变): - -- **objectui → objectstack(发版前侧)**:`.objectui-sha` 落后 objectui main **且** - objectui 合并队列已空(窗口收口判据)⇒ 在 objectstack 立/刷新 console bump 单 - (`pm:queue`;模板照 #6159:滞后读数、releasing changeset 清单、`bump-objectui.sh` - 口径、#6099 破坏性标注复核项、与 Version Packages PR 的顺序约束)。 -- **objectstack → cloud(发版后侧)**:观测到新 rc/正式 tag 族发布 ⇒ 在 cloud 队列立 - 同款 `.objectstack-sha` bump 单(cloud 的 `check:pin-staleness` 保持 advisory 不动 - —— 本条的产出是单,不是新门)。 - -三条边界:**单张封顶** —— 立单前先查同题 open 单,已有就追评刷新区间与读数,⛔ 不开 -第二张;**rule 3 仍是第一产者** —— 接受座位随手立联动单照旧,本条是窗口级兜底,撞上 -由单张封顶去重;**pin 工具链新形态**(digest 盲区一类)照旧只在锚点单提请,不自行扩面。 - -**依赖前棒才能转绿的 PR:draft 停放 + 一份精确的预期红清单。** 串行链里后棒常常先行 -实现(#5365 的四条进一致性表依赖 #5323 的 mongodb 归约才成立)。这种 PR **停在 -draft**,PR body 写两样东西:**精确的预期红清单**(逐条列失败测试名 + 报错签名)与 -**解除条件**(「依赖 PR #N 合入」)。每个 CI-failure webhook 到达时**与该清单比对** -—— 签名匹配则静默跳过,**出现新签名才是真问题**(#5365 的 REST 层红即此形)。依赖 -合入后走「最后一轮同步 → 红清零 → 转 ready → 入队」。这是 notes 2 在**故意红**上的 -对偶 —— 清单必须写到**签名级别**,「几条测试会红」不够用。 - -#### 串行接力 —— 同碰生成物的多个 PR,一次只放行一个 - -第 3 步的 batch 模型假设**同批 file-disjoint 并行**;当「多个已实现的 PR 全碰 -`packages/spec` 生成物」时该假设不成立,唯一可行形态是**串行接力**:一次只放行一个, -每合并一个就向下一棒发接力指令(先例:2026-08-04/05 夜 spec 车道十连落,#5304 → -#5365)。下面三条是那一夜固化的纪律。 - -**1. 每一棒都是一整圈,`auto-merge` 由 PM 挂、dev 永不碰。** 单棒循环:merge main + -上面 A 的四步重建 + 全套验证 + 兄弟单断言复核 → **PM 复核回报** → 转 ready → 挂 -auto-merge / 入队。ready 与 auto-merge 的顺序不可反 —— 转回 draft 会同时掉 auto-merge -与队列成员资格(Operational notes 1),而接力循环里**每一棒都要重走这一次**,不是整条 -链只走一次。ACCEPT 那一条只写了单 PR 的流程;链上每一棒逐棒照它执行。 - -**2. 相邻两棒同碰一个文件时,交接的是语义,不是文本。** 前棒在回报里写明它对共享文件 -改动的**性质**(改名 / 提取变量 / 增补断言,**而非纯追加**),PM **原样转告**下一棒, -并要求「两个 PR 的意图**叠加**,⛔ 禁止机械取一边」—— 取一边会「各自绿、合起来错」, -即 AGENTS.md §10「clean merge 不等于 working merge」的形态(实例 #5318/#5319 见 -references §「串行接力语义交接实例」)。 - -**3. 两棒散文互锁,分工由 PM 指派。** 允许前棒给后棒留占位交接(「本段由 #N 在其同步轮 -翻正 / 删除」):#5323 的族 1 段落 ↔ #5365、#5335 的「#5322 is its own ruling」pin 块 -都是这个形状。PM 必须在**两侧**的接力指令里写明谁动、谁不动 —— 否则要么两边都动 -(冲突),要么两边都不动(留下一段过期散文),而这两种结果**在 CI 上都是绿的**。 - -### 8. Escalate uncertainties to the maintainer - -**First, apply the escalation bar — most things that FEEL like decisions are -not.** The maintainer's words: 「明显的问题直接修,不是事事都需要我确认」. -Escalate ONLY when at least one of these holds: - -- the options genuinely diverge on **product semantics or public contract - shape** and neither the issue, AGENTS.md, ADRs, nor existing code norms - determines the answer; -- the fix requires a **destructive or hard-to-reverse action** (stored-data - migration shape, deleting a shipped capability, force operations). - -Everything else is the PM's call — decide, dispatch, and give the maintainer -a **veto window instead of a permission gate**: state what you decided and -why in the issue comment and the round report; they can stop it, but you do -not wait for them. Named non-escalation classes (act immediately): - -- **Restore-invariant fixes.** When the repo already states the invariant — - one contract version across the family, declared = enforced, a gate must - actually compile/run what it claims to check — a finding that the - invariant is broken carries its own decision. A dual-version dependency - graph, an inert tripwire, an unwired gate: queue it, dispatch it, report - it. Asking "may I restore the invariant?" is the anti-pattern. -- **Sequencing and dependency ordering** between technical tasks. -- **Verification strategy** (what regression pass a risky-but-decided change - needs) — that is scoping the work, not deciding it. -- A dev's `needs_decision` that, on PM review, falls into the classes above: - answer the dev yourself with the decision and rationale; do not relay it - upward. - -**第三档:带前提的裁决 —— 当分歧的关键是一个可被代码证伪的事实。** 上面把结果二分成 -「PM 自己裁」与「升级给维护者」;两者之间还有一档,用在**分歧的关键既不是产品口味也不是 -契约取向,而是一个代码能证伪的事实**时。三件套,**缺一不可**: - -1. **裁决**(选定一条路线), -2. **把裁决挂在一个具名、可证伪的前提上**,并在派发令里要求 dev **先验证前提再动手**, -3. **显式禁令**:「前提不成立就报 fork,⛔ 不许硬做,也不许悄悄退回另一个选项。」 - -值得单列的原因:它让 PM 在信息不全时**做决定而不靠猜** —— 决定自带检验;第 3 条 -最容易被省掉,省掉它就退化成最坏形态:前提不成立时 dev 自行改选,即**无人裁决**态。 -实例(#5373 裁 B、dev 以反向证据验证前提、最终 PR 零 spec 字节)全文见 -`references/incidents.md` §「第三档带前提裁决实例」。 - -**两条元判据 —— 一整族近似单默认不进决策箱(维护者 2026-08-07 决策箱第 2 轮拍板):** -族里第一张已裁过,后来的默认继承那条裁决,不逐张问 —— 重复立卡是拿维护者的时间买同 -一个答案。 - -- **静默丢弃的声明,默认并入既有拒收集。** 适用判据:一个**已声明的键**在组件的某一支上 - 被**静默忽略**,而更早的裁决已把同一个键在**兄弟支**上定成**响亮的编写期错误**。此时 - 新支**默认并入既有拒收集** —— 复用母单的裁决直接入队派发,⛔ 不为它另开决策箱槽位; - 只有两支之间存在**真实语义差异**时才重开。⚠️ **边界必须与本条同段读**:继承的是 - **裁决连同它的理由**,不是「拒收」两个字 —— 母单的理由**被实测为分支特有**时本条 - 不适用;判法固定:把母单的理由拿到新支上复核一遍,理由不成立就回正常升级路径。 - 正反出处(#5714→#5931 同族重复;#5739 理由被证伪、#5918 另裁是对的)全文见 - `references/incidents.md` §「决策继承正反例」。 -- **一个操作两个实现,默认治理侧胜出。** 适用判据:同一个操作存在两处实现,且两处行为 - 不一致。**带治理的那一侧**(权限闸、用户同意、去重、审计留痕)是**默认幸存者**,另一侧 - **改绑到它并删除** —— 不是两侧对齐,也不是保留双写。反向裁只在**产品语义明确要求**时 - 成立,且必须把那条语义写进裁决正文,而不是默认成立后再补理由。留着未治理的那一侧 - 等于给权限闸留一条旁路,而「声明即强制」要堵的正是那条旁路。定案与三例 - (cloud#896、cloud#1147、objectstack#4636)见 `references/incidents.md` - §「治理侧胜出三例」。 - -Whenever a dev returns `needs_decision` that passes the bar above, an issue -is too vague to dispatch, or rework has failed twice: - -1. **先刷新卡片的前提 —— 落卡与复升级都适用。** 落卡之前、复升级之前逐条复核前提, - 失效的就地改写或撤卡 —— **隔夜没动过的卡,默认按「前提未经验证」处理,不是按 - 「还在等答复」处理**(cloud#1148 / cloud#812 标本)。**模板必备件(#7341 item 8): - 卡上每条前提行自带一条 re-check 命令**(`git log origin/main --oneline -5 -- `、 - REST `compare`、带引号精确名的 `git grep`、`git ls-remote --heads origin | grep - `……),复升级时逐条**跑**一遍,零命中/变形的前提就地改写或撤卡。全文见 - `references/incidents.md` §「决策卡前提刷新全文」。 -2. **Default: the decision lives ON the issue it belongs to — never a new - issue.** Post the analysis as a comment on that issue, add the - `needs-user-decision` label, drop it from the active queue. The label is - the maintainer's inbox (filter `label:needs-user-decision` shows - everything awaiting them); when they answer, the label comes off and the - issue re-enters the queue. No bookkeeping issues accumulate. File a - separate issue (titled `[Decision] `, same label; legacy `[决策]` titles stay as-is) ONLY - when the decision has no natural anchor — it spans several issues (file - one, link it from each rather than duplicating the analysis) or arose - with no issue of its own. -3. The analysis, wherever it lands (English, per the language policy): - background / **premises, each line carrying its own re-check command - (point 1)** / the concrete question / options / your recommendation / - related issues, PRs, branches。**每个方案必须沿三条固定评估轴 - 分析,这是决策分析的核心原则,不是可选项:** - - **实际业务需求** — 每个方案先问:它服务的是**真实存在的业务场景**, - 还是投机性的能力面?判据来源要求**实测**——谁在写这个键、谁在读这个 - 能力、示例应用(showcase / CRM)与真实部署里的用法;「读起来像有用」 - 不作数。**创业阶段聚焦原则**(维护者 2026-08-04 指示:「我们是一个 - 创业项目,应该先专注于核心能力」):能力扩张默认从紧,无拉动的声明面 - 按 implementation-first 处置(退役,或停车、词表随未来实现回归), - 已发布但零消费的「能力」**不因沉没成本获得豁免**。这条轴会**改变 - 结论**,不是陪衬 —— 正反先例(#5021 / #4936)全文见 - `references/incidents.md` §「业务需求轴改判正反两例」。 - - **项目长远合理性** — 哪个方案符合北极星方向与可持续架构(Prime - Directive #5 no workarounds、#8 North Star、#12 contract-first), - 而不是眼下最省事;临时补丁式的选项要明说其长期代价。 - - **防 AI 写代码犯错,尤其是防 AI 写元数据 app 犯错** — 哪个方案让 - AI agent 在结构上*更难写错*:契约收紧(严格 Zod schema、publish 时 - 校验拒绝、错误响亮)优于消费端宽容(`??` 回退、静默容错)——宽容 - 恰恰是 AI 批量犯错被掩盖的温床;声明即强制(declared = enforced), - 绝不让 AI 能声明一个运行时不兑现的能力。 - 推荐意见必须基于这三条轴给出理由;三轴冲突时如实呈现权衡,交维护者 - 拍板。 - **四棱卡面块 —— 落卡与升级的必备件(维护者 2026-08-11 接受,#7498)。** - 每张 `needs-user-decision` 卡必须带一个四行分析块,每棱一行,step-0 - 落卡与 step-8 升级同一要求,⛔ 不留待维护者到场再补: - ① **platform long-term coherence** —— 每个选项缩小还是扩大特例/契约增生; - ② **measured business pull** —— 今天谁撞上;零拉动选项默认 defer/remove; - ③ **AI-agent error-resistance** —— 哪个选项最难被 AI 作者/消费者误用 - (闭合枚举优于自由结构、响亮拒绝优于静默容忍、不许伪造成功); - ④ **startup scope discipline** —— remove 优于 declare-and-maintain, - 每个已声明的键都是永久义务。 - 四棱是上面三条评估轴的**卡面序列化**,同一个框架,⛔ 不是第二套:②↔轴一、 - ①↔轴二、③↔轴三,④ 把轴一内嵌的创业阶段聚焦原则升为必答行。实证: - 2026-08-11 四棱复审改判存量决策账本 ~30 张中的 4 张(#7485、#7243、 - objectui#4165、objectui#3804)。 -4. If the session is interactive, additionally raise it via `AskUserQuestion`; - the labeled issue remains the durable record either way. **Never** answer - a product/architecture question on the maintainer's behalf. - -### 9. Round report, then next round - -Print a round report to the maintainer **in Chinese**(chat 通道,语言政策的 -显式例外 —— 报告不落 GitHub): a table of -issue → verdict → PR link → notes, plus anything escalated. Then start the -next round at step 1 (rework re-dispatches count against the next round's -`batch` budget). - -Track three **bounded** health metrics in the report — total open-issue count -is deliberately NOT one of them (discovery outruns closure in any debt-dense -area; that is the loop working, not failing): - -- **dispatchable inventory** — open `pm:queue` unassigned, and its trend; -- **decision inbox** — `needs-user-decision` count awaiting the maintainer; - 分诊座位的轮次简报还要**点名带开放下游依赖的决策卡**(从 step 3 那份 - `Blocked-by:` 反向索引现算;首例 #7496 ← objectui#4190)—— 维护者的注意力 - 是稀缺资源,这个旗标就是决策侧的优先级标记(维护者 2026-08-11,#7498; - 当日已以座位贴 #6015 常设指令先行生效,此处使其长期化); -- **finding median age** — aging findings mean the 发现分诊轮 is overdue; -- **release blockers** — open `target:` count and trend,取 objectstack、 - objectui 与 cloud **三条查询之和**(#7493;归零 = **三张板都空** = 可发版, - 单看 objectstack 归零不是;查询口径与清板协议见「发版板」)。 - -**波次收工点 —— 会话型座位的压缩节奏(维护者 2026-08-09 裁定,#6902 评论)。** -班内节奏是「一波任务处理完 → 收工存档 → `/compact` → 再继续」,⛔ 不是一直跑到被自动 -压缩:计划内压缩在时点与裁剪者两个维度上严格占优(成本读数见 `references/incidents.md` -§「计划内压缩成本依据」)。四步,顺序有含义: - -1. **收工点 = 在飞归零** —— 上一张 MERGED + 云卡 `archive_session` + 退订、决策箱 - 清空。⛔ 不在 step 7 复核中途压缩; -2. **收工存档 = 把任何只存在于上下文里的判断 flush 到 GitHub**(座位贴终检到现值)。 - 这本来就是「状态变更不过夜」的既有义务,此处只是清欠账,边际成本 ≈ 0; -3. **`/compact` 由维护者执行** —— agent 无法自压,这是机制现实,不是分工偏好; -4. **压缩后再派下一波第一张,⛔ 不抢派** —— 派发令、认领、竞态复读产生的上下文 - 属于新一波,不该生出来就立刻被摘要掉。 - -**换人轮换降级为班末动作**,不再是班内节奏:同席压缩**保全会话绑定**(PR 订阅、自绑 -定时器、座位贴会话 ID、云卡父子关系),token 账与换人等价(机理见 references -§「波次收工点换人轮换阐释」)。**Routine 座位不适用**(每 fire 从 GitHub 重建)。前提 -是开篇不变量 —— **GitHub 恒为唯一权威,上下文只是工作缓存**;它失守时,压缩就从 -「丢缓存」退化为「丢判断」。 - -**座位 Routine 没有交互通道。** fresh-session fire 没有对话对面的人,轮次报告 -因此走 Routine 的**完成通知**(`notifications`,fresh-session Routine 专有; -座位 Routine 创建时就该开)。需要留档的结论 —— 裁决、否决窗口项、分诊理由 —— -照常落在**对应的 issue** 上(step 8 的锚点规则不变);⛔ 不要把轮次报告堆进座位表 -评论,那正是「评论不承载状态」要防的东西。 - -## Stop conditions - -Stop the loop and report when any of these hits: - -- the queue is empty, or `rounds` is exhausted — **一次性调用适用;常设座位不适用, - 见下「待命姿态」**; -- **≥ half of a round's dispatches failed or escalated** — that is a systemic - problem (bad queue hygiene, broken main, wrong tooling), and burning the - rest of the backlog against it wastes every remaining dispatch; -- the maintainer interrupts. - -**待命姿态 —— 常设座位的队列清空 ≠ 退场。** 双射座位制下,座位 PM 的队列空了 -不是 stop condition,是切换姿态(#5925;此前协议只写「queue 空 → 停循环」,与 -座位模型直接矛盾):巡检间隔放宽到 **60–70 分钟**(与探活五条的待命节奏一致; -有任何 dev 在飞即收紧回 ≤45),待命职责五项 —— - -1. findings 分诊配合(执行座位侧:补实测证据、上报异议;分诊本身归分诊座位); -2. `pm:blocked` 解锁扫描(上游 issue/PR 关闭即回队;回队前在合并后的 ref 上重验 - 卡内文件面,并顺手更新扇出反向索引 —— 见 step 3 那两段); -3. 在飞/已入队 PR 跟到 MERGED(入队与落地 B 的车道半边); -4. 决策箱提醒 —— 仅在轮次报告中列出待决清单,⛔ 不 nag 维护者; -5. 跨车道备忘跟进(转席单、`Blocked-by:` 链的对侧动静)。 - -半状态巡查的机械辅助:`node scripts/pm/check-half-states.mjs` —— report-only 枚举 -label/assignee 半状态(H1–H5,清单在脚本头),任何座位手动跑;⛔ 不是门禁,产出是 -巡查输入。 - -退场只有两个入口:维护者的交接令(走座位贴协议的交接收尾清单),或座位被 -惰性回收。 + > Serial constraints cleared: `<点名同文件/同包的前驱 PR 与在飞认领;无则 "none">` + +末行把「查过串行约束」变成落在评论里的读数(同包在飞单不点名等于没查);文件面对跨域例外单**必 +填**、普通单建议(定向在飞检查与 epic 领地判断的唯一输入);分支名必须带 issue 号(否 +则 `ls-remote | grep issue-` 找不到)。**Assign 与评论是不可分割的一个动作,限流不拆它** +—— 评论发不出就撤 assign 或压根不开始;配额没了就把整对排队,永不留半个。 +3. **竞态复读**:自己的认领评论上墙后重读全线程 —— assignment 幂等,两个 agent 都会「成功」,** + 认领评论时间戳是唯一仲裁**;更早的评论带不同 session ID/分支 ⇒ 你输了,回「already claimed + — yielding」另选。**让行是交接不是退场**:连同让行评论交出已诊断的一切与 PM 侧已取的板面读 + 数(复现命令、依赖路径、已确认的坑、在飞同文件PR、区域申报、串行约束),赢家不必重扫。 + +dev 侧推分支要早 —— 远程分支是在飞工作最硬的证据。**Stale-claim reclaim**:认领 +>~24h、承诺的分支不存在、无 PR ⇒ 视为死认领 —— 评论询问,再静默一窗后摘 assignee +(注明原因)回队;有带提交的活分支的认领永不回收。 + +### 派发 + +一单一次 `Agent` 调用,`subagent_type: "os-dev"`,后台并行。 + +**Model tiering(维护者 2026-08-10 三档裁定;本节覆盖一切更旧档制)。** 授权原话:「项目经理技能 +还需要考虑的是派任务时使用什么模型,也应该项目经理决定,最低下限sonnet,最高可以 fable」;「比 +如 更新 项目经理技能 必须要使用 Fable 5」。政策:**派发模型是 PM 的逐卡显式决定** —— 下 +限 `sonnet`(机械卡:正确性由门禁农场机械判定,失败模式是漏跑门不是判断失误);默认判断 +档 `opus`(M/L、裁决实施、多面语义、任何带设计判断的卡;拿不准就升一档 —— 错派低档的返工贵过省 +下的额度);上限 `fable`(交付物本身是协议/流程/编排结构的最重卡,按卡取用不是新默认)。**⛔ 强制 +条款(无向下裁量权):凡改 `.claude/skills/pm-dispatch/**` 的卡,一 +律 `model: "claude-fable-5"` 派发。**档位**逐次派发显式传参,永不省略**(解析顺序:环境变 +量 → 派发参数 → 定义frontmatter → 主会话模型;pin 只管什么都不传的情形;被允许名单挡下时回退** +继承的模型**,正是 pin 要防的静默继承),连同尺寸写进认领评论「Container & model」行;分诊 +suggestion 行是输入不是决定,不采纳给一句理由。 + +**派发词原则(构造细则与条款原文见 `references/dispatch-runbook.md`)。** ⛔ 默认不整段粘 +贴 issue 正文 —— 让 dev 自己读 GitHub 全文与全部评论,并**必须**要求 dev 自查正文完整性(截断 +风险随「自己读」转移给 dev,自查是对价不是客套)。派发词只带增量: + +- **三分区,措辞决定 dev 敢不敢证伪**:「裁决(不可重裁)」执行不重开;「PM 机制假设(须实测,鼓励 + 证伪)」dev 动手前验证,证伪了照实报告并按裁决意图换路径;「PM 建议的路线(可选,实测优先)」有 + 更好的就换。凡「我觉得可以这样」的一律降到第三块 ——机制假设穿着裁决的衣服,两个方向都是返工 + 。 +- **标准非协商条款 ⛔ 不抄进派发词** —— 已下沉进 `.claude/agents/os-dev.md`;无条件条款只能住 + 在角色文件(派发词与它冲突时它胜),错了就修那里,⛔ 不靠派发词临时覆盖其默认值。**清单、路径 + 、行号在派发那一刻从树上取**,⛔ 不从卡片/上次派发/记忆抄(取数 + `node scripts/pm/dispatch-gates.mjs <改动路径>…`;取数的是 PM 不是 dev —— dev 只跑被点名的 + 门禁族,全 farm 归 CI);行级断言转述进派发令前必须自己重验。 +- **文件面写两句**(预期落点 + 生产者在别包时报备后按生产者侧修,⛔ 不在消费者侧打补丁 + );**same-day churn 行**(当天合并 ⇒ 先核对当前 main);**在飞重叠拦截**(每轮求交,相交即发四句 + 警告,被完全覆盖就停下回报 ⛔ 不硬造 diff)。 +- **翻转公开语义的裁决随卡带全仓 pin 清扫**,两句缺一不可(全仓一轮翻完;翻转后的pin 断言新语义 + 的实质,真非法形状的拒收断言逐字保留 —— 丢第二句就退化成「全仓删光」)。**条件性标准条款命中 + 判据才抄**:多实现面 ⇒ 共享一致性覆盖;拒收用例 ⇒ `code`+`status` 最低断言;过滤/谓词语 + 义 ⇒ 编译面清单逐面申报(⛔ 静默略过)。 +- **Premise-first 写明**:issue 正文是线索不是规格;`premise_still_valid: false` +无 PR 是合法 + 且常常有价值的交付 —— 派发词预设 issue 为真,就把好运行变成表面抗命。 + +**资源与后端。** 并行 agent 共享一个容器,内存峰值来自 build+test(os-dev 定义已强制验证锁 +/heap 上限/scoped filter/收尾清理);build 重的降 `batch:2` 或走`mode:cloud`;OOM 死的单独重派 +。**M 及以上默认云卡**(维护者 2026-08-10 裁定,原话:「确认,M 及以上默认云卡」):M+ ⇒ +`mode:cloud` 独享容器,⛔ 不混共享批次;只有 S 级**机械**卡留 `mode:subagent`;S 级但不机械(判 +断面在设计不在门禁)按 M 待遇 —— 尺寸不是档位的充分判据。判定连同档位写进认领评论。 + +**云卡四课**(细则见 `references/dispatch-runbook.md`;一次性云卡用`create_session`,⛔ 不 +用 create_trigger+fire —— 维护者 2026-08-07 拍板,trigger 流只留给定时/重复型):① 授权面 +随 source 不随环境(带 `source_url` + `outcome_branch` +显式 `model` + 车道名开头 +的 `title`);② 派发词必带**自驱条款**(⛔ 不为提问/中期汇报结束回合;合法回合终点只有推送完 +成 + 终报,或硬阻塞详报);③ 交付通道 = dev 自开draft PR + 终报以 issue 评论交付;④ **云卡 draft +PR 一存在立即`subscribe_pr_activity` —— 硬步骤**(未订阅的云卡 PR 是轮询负债)。CCR 云会话不注 +册跨会话消息 roster —— `SendMessage` 直投 not-reachable 是设计而非故障(维护者2026-08-11 裁定 +:事件驱动架构即长期方案),⛔ 不复测 roster 路径。**接手中断的dev**:先试 SendMessage 复活 +,resume 不可用才走 worktree 接手协议(细则同上表)——⛔ 不重跑原派发词;worktree 不新建; +逐 hunk 读既有工作再决定;验证完整重跑; assignee/认领评论/分支全不动(既有认领的延续,不是新认 +领)。 + +### 收集 + +**报告通道统一:GitHub 是两种模式共用的真相源。** dev 终报同时落 issue 评论(首行 +``)与自己通道的返回消息 —— 评论是记录,返回消息是加速器;收集先 +扫 GitHub,标记评论在 = 报告完整,两处都没有才进探活/判死。⛔ 永不把「没收到失败通知」读作「还 +在跑」,永不虚构未回报 agent 的结果,永不把报告缺席当成功。 + +**探活是每轮巡检的固定动作 —— 完成通知不可靠,缺席什么都不证明。** ① 定时器重挂是每次巡检的** +第一动作**(先挂后查,链条对中断免疫);② 在飞期间主巡检间隔 ≤45 分钟,待命期 60–70,有在飞即收紧 +;③ 对每个已派发未回报的 dev 发状态询问(「回一段简报后继续干活」),派发后 ~45 分钟无远程产出即 +到探活门槛;**已有远程分支/PR 不豁免** —— 探的是「报告未达」不是「分支未出现」,远程产出证明工 +作发生过,不证明 agent 还在(实测的常见死格恰是「开出正确 PR 之后」);④ 两种回包都有价值:活 +着 ⇒ 进度与阻塞点;「no active task; resumed from transcript」⇒ 生前已死,这次询问本身就是复活 +;⑤ 判据永远取正向证据(分支、PR、报告、探活回包)。 + +**45 分钟是发探针的门槛,⛔ 不是判死的门槛**(探针代价是一次消息;判死代价是往可能活着 +的 worktree 塞第二个 agent)。判死的正当依据只有三类:探针回包表明已死、宿主明确回报 stopped、 +超过**本车道实测基线**且连续静默 —— 基线是你自己车道记的「派发 →推分支/开 PR」端到端耗时(三 +五单即可用),⛔ 不是本文任何常数;基线之内的沉默不是证据。`mode:cloud` 的 ~2h 静默是**本轮收集 +边界**(记 `blocked` 移步下轮),也不是判死。 + +**失报与停摆的处置(细则见 `references/dispatch-runbook.md`)。** 三条原则: + +- **停摆永不自愈**:携带任务中途状态的完成通知本身就是停摆信号,立刻 SendMessage 附前台执行姿 + 态句,⛔ 不等任何静默阈值(阈值是给「没有回答」的);复位走梯度,每次比上一次更具体,第三次停摆 + 判 unreliable 按接手协议重派。这里是消费侧兜底,⛔ 不能写成「派发词写全了就可以不探」。 +- **通知重放先算身份再读内容**:去重三元组 `(issue, 分支, PR head sha)`,与已验收那份相同 ⇒ 记 + 「重放」即结束,⛔ 不重新验收不重复 ACCEPT;⛔ 到达不读作「还活着」,不到达也不读作「已死」。 +- **直接验收兜底(报告丢失 ≠ 验收停摆)**:(a) draft PR 在且 CI 全绿 + (b) 探活确认已死 + 或 ≥2h 无推送 + (c) 报告未达 ⇒ 直接按 PR 验收(逐文件核对 diff 与申报文件面,对 + 照 `origin/main` 复核前提与验证叙述,复核判据不减);先探活后翻 ready。舰队级死因(全账号断粮) + 下取 (a)+(c) 照常收口,PR body 就是报告。 + +### 复核 + +You are the reviewer of record —— **对 GitHub 核验,不对报告的自述核验**;逐项判据展开 +在 `references/review-checklist.md`,每份报告对着它过。骨架: + +- **PR 形态与范围**:draft、目标 `main`、首行 **`Fixes #` 仅当合并应当关卡** ——半实施必 + 须 `Part of #`,否则合并静默关掉决策箱里的卡(收件箱过滤只看 open);翻 ready 前亲核首行。 + changed files 范围检查(⛔ 不看报告自述);tests/docs-only 走`skip-changeset` 标签;测试证据要 + 真实命令与输出。 +- **报告在草稿 PR 时点到达,CI 收敛读数只属于复核侧**(维护者 2026-08-10 裁定): gate + `in_progress` 是诚实读数;翻 ready / 挂 auto-merge / 入队前亲核门禁 job 结论,⛔ 不因「本地 + 绿」跳过;收敛期转红走补丁轮(续派原 dev,不是 REWORK);重量级卡可在派发令写「本单等 CI」。 +- **绿色输出 ≠ 该绿证明了被测风险**:拒收用例查 `code`+`status` 断言;「N 个包全绿」问方向与时 + 序;裁决实施 PR 查全仓 pin 翻转 + 拒收断言仍在;收益穿过必经边界后还在吗(必要时端到端验一次) + 。 +- **证伪是好运行**:`premise_still_valid: false` 是再分诊输入不是失败;dev 纠正 PM要当众认;验 + 收判据被测量推翻 = 好运行,但过程要写在 PR 正文并附实测信噪比。 +- **删除与二进制**:死代码删除在 `origin/main` 亲核引用面再 ACCEPT;`+0/-0` 先疑NUL。sweep 类 + 交付的范围外产出在 ACCEPT 评论成组列出。 + +**判决**:**ACCEPT**(issue 英文评论链接 PR 总结)→ 驱动落地;**REWORK**(逐项反馈,同认领重派;补 +丁轮优先 SendMessage 续派原 dev —— 上下文保留省掉全部重验;最多2 轮,第三次升级);**ESCALATE**( +见「升级与决策」)。 + +**ACCEPT 之后的路径分叉(动手之前先分,不是事后对照)。** 翻 ready / 挂 auto-merge / 入队前,先 +取一次 PR 的路径面(`get_files`,⛔ 不看报告自述)。路径面**一条命中** `docs/adr/**`、 +`.claude/skills/**` 或 `skills/**` ⇒ ACCEPT 换终局三件套:① 复核结论照常写在 issue 上(不能 +合 ≠ 不复核);② PR 留给维护者 —— ⛔ 不合并不入队不挂auto-merge,必须**看得见地悬着**;③ 轮次报 +告单列「awaiting a human merge」(「等人来合」与「被忘了」在 GitHub 上长得一模一样)。混 +合 diff 一条命中就分叉,⛔ 不按比例判;要拆就让 dev 单独开 PR;已入队才读到本条 ⇒ 撤回只有 +转 draft。路径面干净的才mark ready → 入队(队列是唯一被认可的落地路径,⛔ 永不队列 +外 `--auto` 合并)。本段只适用本循环派发的 dev PR;PM 自己的工具 PR 留维护者。 + +**入队与落地(细则见 `references/landing-operations.md`,落地窗口查阅)。** 原则: + +- **碰生成物的 PR,入队前先同步 + 整体重生成** —— os-regen 驱动会零冲突标记地**静默丢掉一侧改 + 动**,只有重生成才暴露;四步序已机械化(`bash scripts/pm/os-regen-merge.sh`:**先 commit + merge 再重生成**,顺序防锚点静默倒退与`gen:openapi` 假红两个陷阱);重生成后断言兄弟单条目与 + 上一单**实现体**仍在;清单当场读 `grep os-regen .gitattributes` ⛔ 不抄进派发令当常量;⛔ 禁 + 止为凑相等手改锚点文件,⛔ 不得要求 `baseRev == merge-base`(允许滞后)。 +- **跟到 MERGED 为止;入队后的看护归队列管家。** 车道 PM 管验收、首次入队(ACCEPT 后挂 6–9 分 + 钟 flip 定点核门禁 job 结论,绿即转 ready + 挂 auto-merge —— CI success webhook 不可靠, + ⛔ 不坐等不忙轮询)、确认 MERGED(两个读数);落地窗口给关键 PR 挂`subscribe_pr_activity`( + ⛔ 不订阅 dev 交报告前的 PR —— 双驾驶员互踩;**MERGED/关闭即退订 + 云 + 卡 `archive_session`**,⛔ 合并前不归档,不留孤儿订阅);落地后再核一次落地判据仍是车道 PM 的 + 活(队列合并同样走 os-regen 驱动)。 +- **依赖前棒才能转绿的 PR:draft 停放 + 签名级预期红清单** + 解除条件(「几条测试会红」不够用 + );CI-failure 事件与清单比对,新签名才是真问题;依赖合入后同步 → 红清零→ 转 ready → 入队。** + 多个已实现 PR 全碰生成物 ⇒ 串行接力,一次只放行一个**:每一棒都是一整圈(每棒重走);相邻两棒交 + 接的是**语义不是文本**(意图叠加,⛔ 禁止机械取一边 —— 各自绿、合起来错);散文互锁的分工 + 由 PM 在**两侧**接力指令里写明谁动谁不动(两种漏法在 CI 上都是绿的)。 + +### 轮次报告与节奏 + +每轮向维护者打**中文**轮次报告(chat 通道,语言政策显式例外):issue → 判决 → PR 链接 → 备注的表 +,加升级项、代裁清单(分诊)、awaiting a human merge 项。健康指标四个(总 open 数刻意不在其 +中 —— 债密区发现快于关闭是循环在工作):**dispatchable inventory**(open `pm:queue` 未认领及趋 +势);**decision inbox**(待维护者数;分诊简报还要点名带开放下游依赖的决策 +卡 —— 从 `Blocked-by:` 反向索引现算);**finding median age**(老化 = 发现分诊轮欠账);**release +blockers**(三仓 `target:`之和,归零 = 三张板都空)。 + +**波次收工点(维护者 2026-08-09 裁定)。** 班内节奏:一波处理完 → 收工存档 → `/compact` → 再继 +续,⛔ 不跑到被自动压缩。四步有序:① 收工点 = 在飞归零(⛔ 不在复核中途压缩);② 收工存档 = 把只 +存在于上下文里的判断 flush 到 GitHub(「不过夜」的既有义务);③ `/compact` 由维护者执行;④ 压缩 +后再派下一波第一张,⛔ 不抢派。同席压缩保全会话绑定(订阅、定时器、座位贴会话 ID),换人轮换降为 +班末动作;Routine 座位不适用。前提是 GitHub 恒为唯一权威 —— 它失守时压缩从丢缓存退化为丢判断 +。 + +### 停止条件与待命 + +停:队列空或 `rounds` 用尽(**仅一次性调用适用**);一轮 ≥ 半数派发失败或升级(系统性问题,继续 +烧 backlog 是浪费);维护者打断。**常设座位队列清空 ≠ 退场**,是切换待命姿态:巡检放宽 60–70 分 +钟(有在飞收紧回 ≤45),待命五项 —— findings 配合(补证据、上报异议)、`pm:blocked` 解锁扫描(含回 +队前 ref 重验与扇出索引更新)、在飞/已入队 PR 跟到 MERGED、决策箱仅在报告中列出 ⛔ 不 nag、跨 +车道备忘跟进。退场只有两个入口:维护者交接令(走交接收尾清单),或被惰性回收。 + +## 队列管家职责 + +三仓一座,管**入队之后**的看护;授权面**只守落地**:⛔ 永不合并、永不 ready/draft 切换、永不把 +没入过队的 PR 入队、不碰代码不动认领。 + +- **红/踢出的签名分诊四分支**(判据唯一来源是管家座位贴的签名台账,优先于现场判断):已知 flaky + ⇒ 原样重投;已修签名再现 ⇒ ⛔ 不重投,判新问题通知车道重新诊断;基上缺已合修复 ⇒ 指引 merge + main 推新提交(重跑无效);新签名 ⇒ ⛔ 不重投,在 PR 与其`Fixes` 卡各留完整签名与初判。**台账 + 只有人工能升级**(疑似新 flaky 只在锚点单留提请,⛔ 不自行加表;纯计数不追记,只有改变修法作用 + 域时才记)。 +- **双向让行**:处置任一 PR 前先读它最近 ~30 分钟评论,对方已在处置就让行留一行;双方每次动作都 + 留审计评论(重投写签名与台账依据)—— 让行判据始终是 GitHub 上的读数。 +- **Pin 链观测的机械产出**(⛔ 只立单不执行 bump):objectui pin 落后且 objectui 队列已空(窗口 + 收口)⇒ 在本仓立/刷新 console bump 单;观测到新 tag 族发布 ⇒ 在 cloud 立`.objectstack-sha` + bump 单。三边界:单张封顶(先查同题 open 单,已有就追评刷新);联动单第一产者仍是接受座位,本条 + 是窗口级兜底;工具链新形态只提请不扩面。 + +## 断粮与跨墙恢复(5 小时用量墙) + +出处:维护者 2026-08-11 提问(「首先项目经理能不能查到相关的数据,其次是到达时间窗口工作就会停, +是否应该设置一个1小时的定时以监测时间窗口已经解锁」)。检测读数、盲区与恢复 playbook 细则 +见 `references/platform-readings.md`;常驻原则: + +- **检测**:`ccusage blocks` 给窗口边界与燃烧率,但只有单容器视野、估成本不估套餐余量(没有任何 + 面向 agent 的接口暴露账号级剩余额度);**权威的墙信号是失败本身**——撞墙报文里的重置时刻在那 + 一刻可得、事前查不到,把它记下来。 +- **跨墙定时器**:拿到重置时刻 ⇒ 一发定点(reset + 缓冲)优先;没有 ⇒ 挂每小时cron Routine,⛔ ** + 不用 send_later 链** —— send_later 是 run-once、fire 后自禁用(平台文档),投进死窗口的那一发 + 是否被重试未实测且文档未承诺,按保守设计一次性链条可能断在它存在的意义上;第一枪成功 + 的 fire 跑恢复后**删除 cron**(幸存 cron 是孤儿定时器)。fired 文本照定时器写法纪律。 +- **恢复 playbook 链的是既有规则**:逐个探在飞云卡 → 直接验收兜底或 transcript 复活→ 重挂常规 + 巡检 → 照常跑轮;draft-PR-early 契约守住时零信息丢失。 + +## 升级与决策 + +**先过升级门槛 —— 大多数「感觉像决定」的不是决定。** 维护者原话:「明显的问题直接修,不是事事 +都需要我确认」。只在至少一条成立时升级:选项在**产品语义或公开契约形状**上真实分歧, +且 issue/AGENTS.md/ADR/既有代码规范都定不了;或修复需要**破坏性/难回滚动作**。其余都是 PM 的 +裁量 —— 裁定、派发、给维护者**否决窗口而非许可门**。具名不升级类(立即行动):恢复不变量的修复( +发现本身自带决定 —— 问「可以恢复不变量吗」是反模式);技术任务间的顺序与依赖;验证策略 +;dev 的 `needs_decision` 经 PM 复核落进上述类的,PM 直接答复不上传。 + +**第三档:带前提的裁决** —— 分歧关键是一个可被代码证伪的事实时,三件套缺一不可:①裁决(选定路线 +);② 把裁决挂在具名、可证伪的前提上,派发令要求 dev 先验前提再动手; ③ 显式禁令「前提不成立就 +报 fork,⛔ 不许硬做,也不许悄悄退回另一个选项」—— 省掉第③ 条就退化成最坏形态:前提不成立 +时 dev 自行改选,即无人裁决。 + +**两条元判据(同族近似单默认不进决策箱,维护者 2026-08-07 拍板):** ① 静默丢弃的声明默认并入既 +有拒收集(兄弟支已裁成响亮编写期错误 ⇒ 新支复用母单裁决直接入队,只有真实语义差异才重开;**继承 +的是裁决连同理由** —— 母单理由被实测为分支特有时本条不适用,判法是把母单理由拿到新支复核一遍) +。② 一个操作两个实现且行为不一致 ⇒ **带治理的一侧**(权限闸、同意、去重、审计)默认胜出,另一侧 +改绑并删除 —— 不是对齐也不是双写;反向裁只在产品语义明确要求时成立且必须写进裁决正文;留着未治 +理侧等于给权限闸留旁路。 + +**落卡/升级流程**:① **先刷新卡片前提** —— 隔夜没动的卡默认按「前提未经验证」处理; **卡上每条 +前提行自带一条 re-check 命令**(`git log … -- `、REST compare、带引号精确名 grep、 +`ls-remote | grep `),复升级时逐条**跑**一遍,零命中/变形的就地改写或撤卡。② 决策默认 +**锚在所属 issue**(分析发评论、挂`needs-user-decision`、退出活动队列;标签就是维护者的收件箱, +答复后摘标回队);仅当决定无自然锚才单开 `[Decision] <一句话>` 卡。③ 分析(英文)含:背 +景 / 带 re-check命令的前提 / 具体问题 / 选项 / 推荐 / 相关单与 PR。 +**每个方案必须沿三条固定评估轴分析,这是决策分析的核心原则,不是可选项:** + +- **实际业务需求** — 它服务的是**真实存在的业务场景**,还是投机性能力面?判据要求**实测**(谁在 + 写这个键、谁在读、示例应用与真实部署的用法),「读起来像有用」不作数。**创业阶段聚焦原则**( + 维护者 2026-08-04 指示:「我们是一个创业项目,应该先专注于核心能力」):能力扩张默认从紧,无拉 + 动的声明面按 implementation-first 处置,已发布零消费的能力不因沉没成本获得豁免。这条轴会改 + 变结论,不是陪衬。 +- **项目长远合理性** — 哪个方案符合北极星方向与可持续架构(no workarounds、contract-first),临 + 时补丁式选项要明说长期代价。 +- **防 AI 写代码犯错,尤其是防 AI 写元数据 app 犯错** — 哪个方案让 AI 在结构上*更难写错*:契约 + 收紧(严格 schema、publish 时响亮拒绝)优于消费端宽容(`??` 回退、静默容错)—— 宽容恰是 AI 批 + 量犯错被掩盖的温床;声明即强制,绝不让 AI 声明一个运行时不兑现的能力。 + +推荐意见必须基于这三条轴给出理由;三轴冲突时如实呈现权衡,交维护者拍板。**四棱卡面块是落卡与升 +级的必备件**(维护者 2026-08-11 接受),每张 `needs-user-decision` 卡带四行,每棱一行,⛔ 不留待 +维护者到场再补:① platform long-term coherence(缩小还是扩大特例/契约增生);② measured business +pull(今天谁撞上;零拉动默认 defer/ remove);③ AI-agent error-resistance(闭合枚举优于自由结构、 +响亮拒绝优于静默容忍);④ startup scope discipline(remove 优于 declare-and-maintain,每个已声明 +的键都是永久义务)。四棱是三条评估轴的卡面序列化,同一个框架不是第二套;它也是分诊代裁置信门的 +输入(见分诊职责)。交互会话可另发 `AskUserQuestion`,带标签的 issue 恒为持久记录。 ## Guardrails (binding) -- PM writes **no files**. Merging is allowed for **reviewed, fully-green - dev-agent PRs via the merge queue only** (see the ACCEPT verdict) — never - its own PRs, never a red or unreviewed one, never bypassing the queue - where one exists. **唯一例外(#5925-6,维护者 2026-08-06 批准)**:维护者 - **明示授权**的 `.claude/` 内部工具 PR(先例 #5597/#5872),PM 可自行实施并 - 挂队 —— 授权出处(维护者原话/指令记录)**必须引用在 PR 正文**;复核仍需 - 另一个座位或维护者 walkthrough,⛔ 不得自审自合。授权是逐 PR(或逐项明示 - 指令)的,不是常设豁免。 -- **版本发布必须人工(维护者 2026-08-07 拍板,#6170)。** 任何 AI 座位(PM / dev / - Routine / 队列管家)⛔ 不得执行或触发发布动作:跑 `changeset publish` / - `pnpm run release`、推版本 tag、`workflow_dispatch` 触发 Release/发布类 - workflow、**合并 Version Packages(`chore: version packages`)PR** —— 发布只在 - 维护者亲手动作时发生。围绕发布的工作(发版板、pin bump、版本对账、发布状态 - 核验)照旧归座位;「发布」本身(把包推上 registry / 打版本 tag / 出 GitHub - Release / 推运行时镜像)不归任何座位。发现未经人工的发布痕迹(tag / npm 版本 - 凭空出现)按事故立案通知维护者,⛔ 不代跑任何「补救性发布」。先例:2026-08-07 - rc.4 —— release.yml 的 on-push 自动通道在无人指令下完整发出了 rc.4 - (#6169/#6170);机械通道的存在不构成授权 —— 读到这条的座位遇到那类通道, - 当缺陷上报,不当工具使用。 -- **ADR 由维护者确认、由维护者人工合并(维护者 2026-08-08 拍板,#6741)。** - 原文引用、未翻译: - - > **adr 只能由维护者自己确认,人工合并,ai 不得擅自合并。** - - 任何 AI 座位(PM / dev / Routine / 队列管家)对一个**改动了** `docs/adr/**` - **的 PR**,⛔ 不得执行以下任一动作:合并、加入合并队列、调 - `enable_pr_auto_merge`。起草 - ADR、推分支、开 PR 都可以;**确认它、让它落地,是维护者亲手的动作**。 - **「已复核 + 已批准 + 全绿」不构成例外** —— 本条与上面那条版本发布同形:一类 - 改动的落地是人的行为,绿灯只说明机器没意见。判据阐释、worked example **#6668** - 与反方向代价 #6191 / #6483、仓级权威落点(AGENTS.md,#6741 要求 1):见 - `references/incidents.md` §「ADR 人工合并条款判据阐释」。PM 侧的终局动作见 step 7 - ACCEPT 里的路径分叉 —— 那一段才是手真正会动的地方。 - ⚠️ **撤回机制,反着记会以为自己合规了:已入队的 PR 只有转回 draft 才真的被 - 踢出队列**,`disable_pr_auto_merge` 单独调用**不解除队列成员资格**,PR 照样 - 落地。实测 #6732:13:47Z 挂上 auto-merge 并入队,读到本条后当场反转 —— - disable **加转 draft**,再以远程读数确认它已离队且不在 `origin/main` 上。 -- **Skills 更新与 ADR 同级:由维护者人工审核、人工合并(维护者 2026-08-11 - 裁定,#7548)。** 原文引用、未翻译: - - > **所有 skills 的更新和 adr 类似,需要人工审核** - - 「所有 skills」读作两个技能根:`.claude/skills/**`(内部 agent 技能)与 - `skills/**`(对外发布技能)。任何 AI 座位(PM / dev / Routine / 队列管家)对 - 一个**改动了任一技能根**的 PR,⛔ 不得合并、不得加入合并队列、不得调 - `enable_pr_auto_merge`;起草、推分支、开 PR、复核照旧 —— 确认它、让它落地, - 是维护者亲手的动作。终局三件套(复核照写、PR 看得见地悬着、轮次报告单列 - 「awaiting a human merge」)、混合 diff 的一条命中即分叉、以及撤回机制 - (只有转 draft 才真的离队)全部照上条 ADR 执行;step 7 ACCEPT 的路径分叉 - 已把这两族路径写进同一读数。 - someone else, never dispatch a `needs-user-decision` issue. -- Every dev agent works in its **own worktree per repo** (enforced by - `guard-main-checkout.sh`; the os-dev definition repeats it). -- Parallelism is capped by `batch`. Dev agents for one batch must be - file-disjoint by construction (step 3) —— 唯一的松动是**维护者明示豁免**同文件 - 串行时的替代四条(step 3),那条路径要求申报降到**区域**级,不是取消不相交。 -- **分诊座位永不认领、永不派发、永不写代码** —— 它的产出只有标签、拆分、审计 - 评论(rule 4)。**执行座位只在自己那一个 `domain:*` 车道里认领**,唯一例外是 - 分诊座位指定的跨域例外单(且必须申报文件面 + 跑定向在飞检查)。 -- **`domain:*` 只有一个生产者**(分诊座位)。执行座位不改标签,只上报误标。 -- When any rule here conflicts with AGENTS.md, **AGENTS.md wins**. - -## Report contract (what os-dev returns) +- PM writes **no files**;合并只对**已复核全绿的 dev-agent PR 经合并队列**发生 —— 永不合自己 + 的 PR、永不合红的或未复核的、永不绕过队列。唯一例外(维护者 2026-08-06 批准):维护者逐 PR 明 + 示授权的 `.claude/` 内部工具 PR,授权原话引用在 PR 正文,复核需另一座位或维护者 walkthrough, + ⛔ 不得自审自合。 +- **版本发布必须人工**(维护者 2026-08-07 拍板)。任何 AI 座位 ⛔ 不得执行或触发发布动作 + :`changeset publish` / release 脚本、推版本 tag、`workflow_dispatch` 触发发布类 workflow、 + **合并 Version Packages PR**。围绕发布的工作(发版板、pin bump、对账、状态核验)照旧归座位; + 「发布」本身不归任何座位。发现未经人工的发布痕迹按事故立案,⛔ 不代跑补救性发布 —— 机械通道 + 的存在不构成授权,遇到那类通道当缺陷上报。 +- **ADR 由维护者确认、人工合并**(维护者 2026-08-08 拍板,原话:「adr 只能由维护者自己确认,人工 + 合并,ai 不得擅自合并」)。任何 AI 座位对改动 `docs/adr/**` 的 PR ⛔ 不得合并、入队、 + 挂 auto-merge;起草、推分支、开 PR 都可以。「已复核 + 已批准 + 全绿」不构成例外 —— 绿灯只说 + 明机器没意见。撤回机制别反着记:已入队的 PR 只有转draft 才真的离队。 +- **Skills 更新与 ADR 同级**(维护者 2026-08-11 裁定,原话:「所有 skills 的更新和adr 类似,需要 + 人工审核」)。「所有 skills」= 两个技能根 `.claude/skills/**` 与`skills/**`;终局三件套、混 + 合 diff 一条命中即分叉、撤回机制全部照 ADR 条执行(复核路径见「复核」的 ACCEPT 路径分叉)。 +- **决定属于维护者:永不代维护者回答产品/架构问题**(唯一例外:分诊职责里已裁的代裁车道,边界恰 + 与其置信门重合,不得更宽);**永不派发 assignee 是别人的 issue;永不派发 + 带 `needs-user-decision` 的 issue**。 +- Every dev agent works in its **own worktree per repo**(hook 强制;os-dev 定义重申);并行度 + 以 `batch` 封顶,同批 file-disjoint by construction(唯一松动是维护者明示豁免时的替代四条,申 + 报降到区域级,不是取消不相交)。**分诊座位永不认领、永不派发、永不写代码;执行座位只在自己那 + 一个车道认领**(唯一例外:分诊指定的跨域例外单);**`domain:*` 只有一个生产者**,执行座位不改标 + 签只上报误标。 +- 与 AGENTS.md 冲突时,**AGENTS.md wins**。 + +## Report contract(os-dev 返回什么) ```json { @@ -2550,19 +662,25 @@ label/assignee 半状态(H1–H5,清单在脚本头),任何座位手动跑;⛔ "open_questions": [ { "question": "…", "options": ["A …", "B …"], "recommendation": "A, because …" } ], - "out_of_scope_findings": ["filed as #457: …"] + "out_of_scope_findings": ["filed as #: one-line description"] } ``` -`premise_still_valid: false` with `pr: null` is a legitimate terminal report -(step 5): review it as a re-triage input — verify the dev's evidence, then -close, re-scope, or re-file the issue — never as a failed dispatch. -`open_questions` must be non-empty when `status` is `needs_decision`, and each -entry becomes input to a `[决策]` issue. `out_of_scope_findings` should already -be filed as unassigned issues by the dev per the filing discipline in the -os-dev definition (search-first, sub-issue attachment, `finding` labeling — -objectstack#4949; and since the file-at-destination ruling, filed **in the -repo where the fix lands** with a backlink — multi-repo rule 1). The PM verifies they exist **and cross-checks the round's -parallel reports against each other**: two devs auditing adjacent code file -twins within the hour (cloud#1054 duplicated cloud#1031 same-day), and only -the PM sees both reports. +`premise_still_valid: false` + `pr: null` 是合法终报 —— 当再分诊输入复核,永不当失败派发。 +`status: needs_decision` 时 `open_questions` 必须非空。`out_of_scope_findings` 应已由 dev 立 +成无 assignee 的卡(查重先行、归挂判据、`finding` 标注,立在修复落地仓并带回链);PM 核验它们存 +在,**并把同轮并行报告互相对读** —— 两个 dev 同一小时审相邻代码会立出孪生卡,只有 PM 同时看得 +见两份报告。 + +## 机械守卫索引(原则在此,细节以脚本头为权威) + +| 守卫 | 一句话 | +|:--|:--| +| `scripts/pm/check-skill-line-ratchet.mjs` | 本文件行数只降不升(`pnpm check:pm-skill-ratchet`);抬上限需维护者裁决引用在 PR 正文 | +| `scripts/pm/check-skill-id-lint.mjs` | 本技能与 os-dev 定义的操作文本 ⛔ 不引用 issue 编号(`pnpm check:pm-skill-id-lint`)—— 经验必须自含 | +| `scripts/pm/check-half-states.mjs` | label/assignee 半状态的 report-only 巡查 | +| `scripts/pm/dispatch-gates.mjs` | 文件面 → 该跑的门禁族(派发令取数) | +| `scripts/pm/os-regen-merge.sh` | 碰生成物 PR 的 merge 四步序(防静默吞并与锚点倒退) | +| `scripts/pm/ensure-pm-labels.sh` | pm 标签词表的幂等创建 | +| `check:skill-frame-sync` / `-freshness` | 三轴决策框架四份拷贝的同构与新鲜度 | +| `guard-main-checkout` / `guard-shared-stash` hooks | worktree-first 与 stash 禁令的机械面 | diff --git a/.claude/skills/pm-dispatch/references/compile-surfaces.md b/.claude/skills/pm-dispatch/references/compile-surfaces.md index 01d08bd0a5..5bfa7edb7c 100644 --- a/.claude/skills/pm-dispatch/references/compile-surfaces.md +++ b/.claude/skills/pm-dispatch/references/compile-surfaces.md @@ -1,9 +1,8 @@ # 过滤 / 谓词语义的编译面清单(references —— 按需加载) -出处:主文件 step 5「过滤 / 谓词语义裁决」标准条款 —— 命中该条款的派发令把本清单 -**逐面抄进去**,PR 逐面申报。清单正文自 SKILL.md 纯搬移(#5925 item 7),维护纪律 -与派发前复核串在正文内,一并适用:**这张表由 PR 维护、派发前先跑复核串重验,⛔ 不凭 -记忆抄**。 +出处:主文件「派发」的过滤/谓词语义标准条款 —— 命中该条款的派发令把本清单**逐面抄 +进去**,PR 逐面申报。维护纪律与派发前复核串在正文内,一并适用:**这张表由 PR 维护、 +派发前先跑复核串重验,⛔ 不凭记忆抄**。 编译面清单(逐面实测 @ `main` `48f98b0`,2026-08-07): @@ -15,7 +14,7 @@ | 4 | service-analytics filter-normalizer | `packages/services/service-analytics/src/strategies/filter-normalizer.ts:1235`(`lowerAnalyticsWhere`) | analytics / cube 侧 | | 5 | `formula` | `packages/formula/src/matches-filter.ts:73`(`matchesFilterCondition`) | RLS 写侧 `check` 与公式求值;JS 两值语义的基准面 | | 半面 | objectql `having-filter` | `packages/objectql/src/having-filter.ts:92` / `:98`(`applyHaving` / `matchesHaving`) | 聚合**后**过滤。算半面是因为词表是子集,**但申报义务不打折** —— 它是**唯一没有 conformance 表覆盖的面**(`FILTER_LOGIC_CASES` 不驱动 HAVING 路径),所以漏了它连门禁都不会红 | -| 冻结 | `driver-memory` / `driver-mongodb` | — | #5499 冻结投入:**pin-annotate,不翻转**。冻结面仍要申报,结论是「不在范围 + #5499」。现场注释见 `read-scope-sql.ts:176`、`having-filter.ts:41` | +| 冻结 | `driver-memory` / `driver-mongodb` | — | 维护者 2026-08-05 投入冻结:**pin-annotate,不翻转**。冻结面仍要申报,结论是「不在范围 + 冻结指令」。现场注释见 `read-scope-sql.ts:176`、`having-filter.ts:41` | **这张表本身由 PR 维护 —— 与域表同一纪律。** 增删一面(新驱动、新求值器、某面被合并 或退役、冻结状态变化)的那个 PR 顺手改这里,不留给下一次裁决重新数。清单**会**过期是 diff --git a/.claude/skills/pm-dispatch/references/dispatch-runbook.md b/.claude/skills/pm-dispatch/references/dispatch-runbook.md new file mode 100644 index 0000000000..691094aff4 --- /dev/null +++ b/.claude/skills/pm-dispatch/references/dispatch-runbook.md @@ -0,0 +1,125 @@ +# 派发与收集运行细则(references —— 按需加载) + +出处:主文件「派发」「收集」。原则住在主文件;本表是派发云卡、接手死 dev、处置停 +摆/重放/失报那一刻查阅的操作细则。⛔ 不引用 issue 编号。 + +## 云卡(`mode:cloud`)四课 + +一次性云卡用 `create_session`,⛔ 不用 create_trigger+fire(维护者 2026-08-07 拍 +板;trigger 流只留给定时/重复型)。 + +1. **授权面随 source,不随环境**:trigger 拉起的会话没有仓库授权(只能只读勘察); + `create_session` 带 `source_url` 的会话出生即持推送授权。同时带 + `outcome_branch`(= 认领分支)、显式 `model`(trigger 流不可指模型)、`title` + (以车道名开头,⛔ 不叫 os-dev;形如 `⚡ <车道> #<单号> <短语>`)。 +2. **派发词必带自驱条款**(云会话回合一结束就停摆等 poke,不写这条 dev 会停在中期 + 汇报或提问处):⛔ 不为提问/中期汇报结束回合;开放选择按裁决与评估轴自裁记入终报 + open_questions;合法回合终点只有 (a) 推送完成 + 终报,或 (b) 硬阻塞详报。 +3. **交付通道**:dev 自开 draft PR(`Fixes #`,正文含验证记录)+ 终报以 issue 评 + 论(os-dev-report 标记)交付;未 attach 的姊妹仓够不着,跨仓跟进卡由 PM 代立。 + trigger 拉起的定时会话维持降级通道:推送 outcome branch + 终报走最后一条会话消 + 息,PM 代开 draft PR。 +4. **云卡 draft PR 一存在,立即 `subscribe_pr_activity` —— 硬步骤**:未订阅的云卡 + PR 是轮询负债,订阅把感知从巡检周期缩到秒级。 + +监控与转向:`get_session` 读实时状态(IDLE + 分支未推送 = 停摆待 poke);投递消息 +用绑定会话的 poke 触发器(`create_trigger` 带 `persistent_session_id` + +`fire_trigger` + 用后即 `delete_trigger`);巡检退为兜底心跳,每轮先核订阅已覆盖哪 +些面、只补盲区(会话停摆、未开 PR 的分支、姊妹仓动静)。CCR 云会话不注册跨会话消息 +roster —— `ListAgents` 列不到、`SendMessage` 直投 not-reachable 是设计而非故障(维 +护者 2026-08-11 裁定:事件驱动架构即长期方案),⛔ 不复测 roster 路径。 + +## 接手中断的 dev(worktree 接手协议) + +先试 SendMessage 复活(从 transcript 带全部上下文恢复),resume 不可用才接手。⛔ +不重跑原派发词 —— 新 agent 带四条增量: + +- worktree 已存在,⛔ 不要新建,`cd` 进去接着做(第二个 worktree 会劈开工作); +- 先读全部既有提交与未提交改动,逐 hunk 决定保留或修正 —— 既不推倒重来也不盲信(死 + agent 从未回报,它的一切都未经验证); +- 把死 agent 没跑完的验证**完整重跑**并报真实输出(中断的运行没有留下任何测试证 + 据); +- assignee、认领评论、分支**全部不动** —— 这是既有认领的延续,不是新认领;认领评论 + 记录接手,不被替换。 + +## 停摆复位梯度 + +停摆的 subagent 永不自愈 —— 携带**任务中途状态**的完成通知本身就是停摆信号,立刻 +SendMessage 附执行姿态句(「前台(阻塞)同步执行全部步骤,中途不停止、不把构建/测 +试挂到后台等唤醒」),⛔ 不等任何静默阈值 —— 阈值是给「没有回答」的,不是给「回答说 +我停了」的。**每次复位比上一次更具体**(原样重发同一句话不算一次复位):① 复述执行 +姿态 → ② 点名下一个工具调用 + 明令禁止后台等待 → ③ 第三次停摆判 unreliable,按接手 +协议重派到该分支。自己挂的定时器不会唤醒自己。长验证管线的派发/接力词写明前台姿 +态。生产侧条款在 os-dev 定义 —— 这里是消费侧兜底,⛔ 不能写成「派发词写全了就可以不 +探」。 + +## 通知重放去重 + +通知可以形态完全正常、报告完整正确,而只是同一份东西的第 N 次重放(monitor 按自己 +的 deadline 触发)。**先算身份,再决定读不读内容**:去重三元组 +`(issue, 分支, PR head sha)` + 通知自报的守护对象;与已验收那份逐项相同 ⇒ 台账记 +「重放,首达时间 T」即结束,⛔ 不重新验收、不重读 diff、不重复 ACCEPT。⛔ 到达不读 +作「还活着」(活着的判据只有探针回包),不到达也不读作「已死」(判死只认三类正当依 +据)。对面自报守护对象是「变便宜」,不是去重的前提 —— 没自报就用三元组自己算。 + +## 直接验收兜底(报告丢失 ≠ 验收停摆) + +同时满足 (a) draft PR 已存在且 CI 全绿、(b) 探活确认已死或 ≥2h 无任何推送、(c) 报 +告未达 ⇒ PM 直接按 PR 验收:逐文件核对 diff 与认领申报的文件面,对照 `origin/main` +复核 PR 正文的前提声明与验证叙述,复核清单其余判据不减。顺序保护:agent 可能还活着 +时**先探活、后翻 ready**(抢先翻会与它的收尾推送竞态)。死因可以是舰队级的(全账号 +断粮同杀所有在飞 dev,探活半边同时不可用)—— 此时取 (a)+(c) 照常收口,PR body 就是 +报告。「推分支 → 开 draft PR → 立即交报告」是保险不是效率优化,它住在 os-dev 定义 +的 Definition of done,派发令只带增量。 + +## 派发词构造细则 + +⛔ 默认不整段粘贴 issue 正文 —— 让 dev 自己读 GitHub 全文与全部评论(premise-first +本来就要求),并**必须**要求 dev 自查正文完整性:长正文经工具读取可能被静默截断, +截断风险随「自己读」转移给 dev,这条自查是风险转移的对价,不是客套。派发词只带增 +量: + +- **三分区,措辞决定 dev 敢不敢证伪**:「裁决(不可重裁)」= 维护者/PM 已拍板的方 + 向与语义 —— dev 执行,不重开;「PM 机制假设(须实测,鼓励证伪)」= PM 对代码机制 + 的判断 —— dev 动手前验证,证伪了照实报告并按裁决意图换实现路径,⛔ 不许为了顺从假 + 设硬做;「PM 建议的路线(可选,实测优先)」= PM 顺手给的实现选项/断言写法/排除面 + —— dev 有更好的就换,⛔ 不得因为「派发令写了」而照做。凡「我觉得可以这样」的一律降 + 到第三块 —— 不分区的派发令里,机制假设穿着裁决的衣服,两个方向都是返工。 +- **标准非协商条款 ⛔ 不抄进派发词** —— 已一次性下沉进 `.claude/agents/os-dev.md`。 + 角色文件优先级是实测事实:派发词与角色文件冲突时角色文件胜,所以对每张卡都成立的 + 无条件条款只能住在角色文件、错了就修那里;逐卡变量走模板占位符与三分区,⛔ 不靠派 + 发词临时覆盖角色文件默认值。 +- **清单、路径、行号在派发那一刻从树上取**,⛔ 不从卡片/上次派发/记忆抄(门禁清单单 + 班之内就会过期)。取数:`node scripts/pm/dispatch-gates.mjs <改动路径>…` 产出本卡 + 该跑的门禁族,填进派发令;取数的是 PM 不是 dev —— dev 只跑被点名的几族,全 farm + 归 CI 跑一次,点名的准确性从此是 PM 独担的。卡片或分诊评论里的行级断言,转述进派 + 发令前必须自己重验一遍。 +- **文件面写两句**:「预期落点是 X;若实测表明真正的生产者在别包,报备后按生产者侧 + 修(落点与理由写进报告和 PR 正文),⛔ 不在消费者侧打补丁」—— 只写一个路径名的派 + 发令,是要求 dev 在守约与修对之间二选一。跨包常常等于跨车道,PM 侧的对价是事后补 + 跨座位声明。 +- **Same-day churn 进派发词**:派发时 `git log origin/main --oneline -20 -- + ` 见到当天合并 ⇒ 加一行「基于合并后的代码工作,issue 引用的片段可能已变, + 先核对当前 main」(dev 的 worktree 从 origin/main 切出后不会自己更新)。**在飞重 + 叠也要拦截**(churn 行只覆盖派发瞬间):每轮读 main 新落 PR 与每个在飞派发的申报 + 文件面求交,相交立即警告四句 —— 合 main 重跑测试矩阵、读对方 diff 重划边界、只补 + 它没覆盖的部分、被完全覆盖就停下回报 ⛔ 不硬造 diff。晚一轮 = 一次返工。 +- **翻转公开语义的裁决随卡带全仓 pin 清扫**,两句缺一不可:① grep 错误码/错误消息 + **全仓扫描**同语义 pin,一轮翻完,不要只改本包(旧立场的 pin 住在被改包之外的消费 + 层);② 翻转后的 pin 必须继续承重 —— 断言新语义的**实质**(行数、译文、状态码), + 不是「旧断言已删」;真正非法形状的拒收断言**逐字保留**,被守护的面永不缩水。丢第 + 二句,「全仓翻完」退化成「全仓删光」,绿而无值。 +- **条件性标准条款,命中判据才抄,不命中就省掉别硬套**: + - 多实现面组件(同一契约 ≥2 个实现面)⇒ 「测试放在**未来的分叉会被抓住的地方**, + 进共享一致性覆盖,不是独立测试文件」;不变量形:与 `find()` 给出相同行集,或以 + `INVALID_FILTER` 拒收 —— 不允许有第三种、更安静的答案。 + - 新增/改写拒收类用例 ⇒ 「最低断言错误的 `code` 与 `status`(ADR-0112 信封); + `toThrow()` / `rejects.toThrow()` 单独使用不构成拒收测试;措辞本身是契约时首句 + 断言**加在** `code`+`status` 之上;只约束新写/改写的用例,不回填存量」。 + - 改过滤/谓词语义 ⇒ 把 `references/compile-surfaces.md` 的编译面清单**按其复核串 + 重验后逐面抄进派发令**(⛔ 不凭记忆),并带:「每一面都必须在 PR 正文有一个结 + 论:已改 / 本就合规(证据)/ 明确不在范围(理由);⛔ 静默略过 —— 评审把没提到 + 的面读作漏掉的面」。防的不是做错,是做对了一部分然后以为做完了。 +- **Premise-first 写明**:issue 正文是线索不是规格,先对 origin/main 验前提; + `premise_still_valid: false` + 无 PR 是合法且常常有价值的交付 —— 派发词预设 + issue 为真,就把好运行变成表面抗命。 diff --git a/.claude/skills/pm-dispatch/references/incidents.md b/.claude/skills/pm-dispatch/references/incidents.md deleted file mode 100644 index 40ceea82f5..0000000000 --- a/.claude/skills/pm-dispatch/references/incidents.md +++ /dev/null @@ -1,1587 +0,0 @@ -# pm-dispatch 案例实录(references —— 按需加载) - -⚠️ **APPEND-ONLY。** 本文件是 `.claude/skills/pm-dispatch/SKILL.md` 的案例叙述 -外置层:主文件留「规则 + 一行锚点 + 指针」,长叙述住这里(#5925 item 7,维护者 -批准的纯搬移纪律)。三条读写规则: - -- **只在尾部追加,不改写存量** —— 与 Operational notes 的追加纪律同款。新案例 - 默认落这里,不再灌回主文件;主文件增长率因此归零。 -- 每节标题即主文件指针的锚文本(§「…」逐字对应);「出处」行指回主文件的规则段。 -- 搬入的正文**逐字节保留**原文(纯搬移零改写);行文里的相对指代(「上面」「见下」 - 等)仍指其在主文件里的原位置,出处行会注明。 - -## 定点文本两例实录 - -出处:Operational notes 3「定点文本的写法纪律」—— 两种形态、同一个后果的两例。 - -- `domain:spec-surface` 席**两枪已 `delete_trigger`**(回包确认 `deleted trigger …`) - 的定时器**照样投递**,文本都落后现实两轮。其中一枪写着「#5783 …… 判为不可靠、 - 交接、**重新派发一个 fresh os-dev**,worktree `objectstack-issue-5783` 已存在」; - 投递时 #5783 的 PR #6389 早已交付并通过复核 —— 照文本执行就是把一个重复 agent - 塞进一个活着且已完工的 worktree,正是认领协议要防的碰撞类,只不过这次是**从 - 自动化里**来的,而不是从抢跑的 PM 那里。 -- `domain:devx` 席的一枪**没被删,是被现实追上**:21:3x 挂、22:1x 投递,文本写的 - 「两个 dev 静默结束、未开 PR、未交报告 ⇒ 判定失效 ⇒ 重新派发一个新 dev」在投递 - 时前提已被推翻(两个 dev 都已回话正常推进,其中一个的 PR 已合并)。照做会向两个 - 活着且已有成果的任务各塞一个重复 dev。删与没删是两条路径,终点是同一个。 - -## domain:spec-tooling 沿革 - -出处:「Domain lanes」的 `domain:spec-tooling` 节 —— 生效规则在主文件包家族表,此处为沿革全文。 - -维护者 2026-08-06 裁决(#5469,原文引用、未翻译): - -> `domain:spec-tooling` 判为 **#5163 存续期的临时 program 车道**,不进 SKILL 包家族 -> 域表;其存量单由分诊按现行域表重标,重标完成后该标签退役 - -⚠️ **该裁决所依据的前提(「临时、待退役」)在其后三天被反向的事实推翻,所以本节 -记录的是现状,不是那条裁决的执行结果**(2026-08-09 实测): - -- 裁决当天 15:03Z(裁决后约 9 小时)**新立了座位贴 #6018**,该席至今在任、经历 - 一次移交、一个任期内落了 9 个 PR; -- **2026-08-07 维护者批准的 `spec` 拆分**(座位贴 #6298)在本文里写进了 - `spec-surface` ↔ `spec-tooling` 的分界判据 —— 即维护者本人在裁决次日签发的 - 文本,把它当作活车道在用; -- 标签仍在被分诊打:当前 **10 单 open**(#6833 / #6797 / #6751 / #6635 / #6350 / - #6232 / #6221 / #5828 / #5757 / #5163,其中 #6797、#6350 已 `pm:dispatched`), - 67 单 closed,最近一次新打在 2026-08-08。 - -⇒ 该标签是**在册车道,分诊照常打**。 - -**维护者 2026-08-09 裁决(#5469,取代上面 2026-08-06 那条):判据切分,给行。** -拖住补行的从来不是「它是否存在」,而是它与 `domain:devx` 的三处文件面重叠未裁 —— -裁完即补,包家族表的 `spec-tooling` 行就是该裁决的登记: - -- **无争议、可直接路由的两处**:`packages/spec/scripts/**`、`packages/spec/docs/**` - ⇒ `domain:spec-tooling`。devx 从未声明这两处,依据是 #6018 座位贴的 Scope 段与 - 上面 2026-08-07 的 surface / tooling 分界(tooling 改「围着契约转的机器」)。 -- **曾争议的三处**:`packages/lint`、`content/docs/**`、`scripts/` —— 座位贴 #6018 - 与 #6023 **同时声明**这三处。这不是纸面问题,2026-08-09 实测两侧都在落地: - `domain:spec-tooling` 的 #6778 整单落在 `packages/lint/src/`(PR #6831),而同期 - `domain:devx` 的 #5957 / #5330 / #6381 也落在 `packages/lint`。 -- ⇒ **按「是否围着 spec 契约转」逐卡判**:契约门禁/生成器/lint 规则/报错散文/ - references 管线 ⇒ `spec-tooling`;一般开发工具面 ⇒ `devx`。这是 anchoring rule - 在 `packages/spec` 内那条显式例外的**延伸,不是第二套规则** —— 2026-08-07 拆分 - 已写下判据的前半(「tooling 改围着契约转的机器」),本裁决把同一句话铺到这三处。 - 拿不准的按 rule 4 误标路径 FLAG 回分诊,⛔ 不由 dev 代拍。 - -## 重新定价四方向实录 - -出处:step 3「阻塞解除后要给延后的那一单重新定价」—— 一轮之内四种方向各出现过。 - -本轮四种方向各出现过。**变便宜(且 issue 自己的成本估计同时过期)**:#5375(#5345) -去掉了「cube 风格数组也可作为输入」这条腿,`{member, operator, values}` 三元组自此纯属 -私有中间表示,#5373 的 B 路线因此从正文写的「工作量最大」降为不跨 spec 的内部改动。 -**没变**:#5431(#5373)对 #5374 —— dev 明确回报「**没有**让它变简单,也**没有**顺带 -修好它」,调用点现在收到真值而非字符串化的值,但「`{$not: 'x'}` 约束不了任何东西」在 -算子层,与比较数编码正交。 - -默认假设(「前一单大概让它变简单了」)本轮**错了两次、对了一次**,而两个方向的代价 -不对称:误以为变简单 → dev 按缩小的范围做,漏修;误以为没变 → 走一条已经没必要的贵 -路线。所以这不能由 PM 推,只能由在飞那单的 dev 答 —— 本轮正是该必答项的**否定**回答 -直接决定了 #5374 不能缩范围(见 PR #5445 的「范围之外」段)。 - -## 编译面清单三次漏面 - -出处:step 5「过滤 / 谓词语义裁决」标准条款 —— 三次漏面,全部留在代码注释里。 - -- **#5146 → #5903**:裁决只落到面 1,面 2 是**不继承面 1 的独立编译器**,于是同一个 - 驱动的两种连接模式对同一条过滤给出两种答案。现场记录在 - `driver-turso/src/remote-transport.ts:1731`:「LOCAL mode inherits that fix - (`TursoDriver extends SqlDriver`), this independent compiler inherited none of - it」。 -- **#5326 / #5335**:面 3 与面 4 各**又花一圈**才对齐,记录在 - `spec/src/data/filter.zod.ts:370`。 -- **#5905**:#5298 的裁决由 PR #5962 落到 driver-sql / formula / service-analytics - 与 conformance 表,**唯独漏了 HAVING 面** —— `objectql/src/having-filter.ts:37` 的 - 原话是「was not in that PR's inventory, which left this file as the lone - holdout」。**「inventory」这个词本身就是本条款的缺席证明**:那次派发确实有一份清 - 单,只是它不完整,而没有任何机制要求它完整。 - -三次都不是难度问题,是**没有一份清单在问「还有几面」**。 - -## 便宜选项两次证伪 - -出处:step 5 派发令三分区的「第三块」—— 同一班两次把便宜选项写成已裁定,两次被 dev 证伪。 - -#6865 的卡自带 -一条「断言 job 上没有 `if:`」的验收写法,照做会把**四个正确的 job** 判红;#6893 的 -派发令把「把 `content/docs/releases/**` 排除出审计范围」写成「亦可辩护」的选项, -而那正是 #4920 明确否决的 option A —— `scripts/docs-audit/check-audit-scope.mjs` -在该目录**离开审计范围时直接 `process.exit(1)`**,脚本注释逐字点了 #4920 与 #6893。 - -## os-regen 清单第二份拷贝 - -出处:「入队与落地 A」—— #6492 的三读数矛盾实录;文中「(见下)」指主文件 A 节的四步序。 - -协议此处一度 -内嵌一份路径拷贝,于是同一件事有了三个互相矛盾的读数:散文说「八条」、紧随其下的 -代码块列**九**条、`.gitattributes` 实际路由**十**条(缺的是 -`packages/spec/authorable-defaults/**`)。更要命的是漂移**还在加速**:#6492 分诊 -两次测量之间(同一天,相隔约一小时)清单本身又动过,两次读数就不一样。一份「读起来 -完整、实际不完整」的清单比没有清单更贵 —— 派发令照它枚举,dev 拿到的是一张自称 -齐全的漏项检查表,而 os-regen 的失败是**静默**的(见下)。 - -## 判死基线样例与两次误判 - -出处:step 6「45 分钟是发探针的门槛,⛔ 不是判死的门槛」—— 样例基线表与两次误判;文中「上面五条」指主文件探活规程五条。 - - | 出处(车道 / 日期) | 卡片形态 | 实测端到端 | - |---|---|---| - | `domain:spec-surface` 席,2026-08-07(#6393) | 文本面卡:#5767 / #5622 / #5955 / #5783 | 93 / 96 / ~95 / ~110 分钟 | - | `domain:devx` 席,2026-08-07(#6393 认领评论) | 混合:#6251 / #6038 / #6405 / #6359 | ~67 / ~64 / ~160 / ~170 分钟(后两单含长 CI 等待) | - - 合两席九单:同一天、同一套工具下,端到端跨越 **~64 分钟到近 3 小时**。凡把单一 - 数字当判死线的读法,都会在这个跨度里翻车 —— 所以要建的是**你那一栏**的基线。 -- 两个座位当天各误判一次,都栽在这条线上:`domain:spec-surface` 席在 92 分钟处 - 写下「#5783 将判为不可靠」,而它在基线之内、几分钟后就推了分支;`domain:devx` - 席在派发 2 小时处判两个 dev「静默结束」并把「重新派发」写进了下一枪定点,而两个 - 都在做深度取证。后者靠**先 SendMessage 问状态、而不是直接重派**救回 —— 那正是 - 上面五条的第一条。 - -## 第三档带前提裁决实例 - -出处:step 8「第三档:带前提的裁决」—— 实例与单列理由全文(#7498 的行数预算搬移); -文中「上面」指主文件该节。 - -实例:#5373 给了 A/B/C 三条路,正文写「这是本面数据表示的公开形状,请 PM/维护者裁」—— -按上面的门槛读,它像要升级的那一类。实际裁了 **B**,前提是「这个三元组现在是纯内部表示」。 -dev 用五项检查验证了它,其中一条是**反向证据**:`spec/src/api/analytics.test.ts` 与 -`runtime/src/http-dispatcher.test.ts` 各有一条测试断言 cube 风格的 `filters` 数组会被 -**拒收** —— 直接证明该三元组没有线上格式。最终 PR 未触碰一个 spec 字节。 - -值得单列的原因:它让 PM 在信息不全时**做决定而不靠猜** —— 决定自带检验。既不是「自己拍 -了算」(那要赌前提),也不是「升级」(那要占用维护者时间去回答一个代码可以回答的问题)。 -第 3 条最容易被省掉,而省掉它就退化成最坏形态:前提不成立时 dev 自行改选,那正是**无人 -裁决**的状态,且没有任何读数会显示它发生过。 - -## 决策继承正反例 - -出处:step 8「两条元判据」第一条「静默丢弃的声明,默认并入既有拒收集」—— 正反出处全文。 - -出处:#5714 把 `pool`-on-sqlite 裁成编写期错误之后,#5931(`memory` 支)仍占了一个 -槽位,而它只是那条裁决的一词之差的外延;同族重复整周都在发生。反例:#5739(维度侧) -的理由是「拒收会连带拒掉今天已经能跑的查询」,该理由在**度量面上被证伪**,所以 #5918 -另裁一次是对的。别让「同一个键」这个表面相似度替你做判断。 - -## 治理侧胜出三例 - -出处:step 8「两条元判据」第二条「一个操作两个实现,默认治理侧胜出」—— 定案与三例全文。 - -出处:cloud#896(hostname)即此形定案;cloud#1147 的三个待答问题(重装 = UPSERT、 -卸载 = 软停用、外部词表 = manifest id)按本条全部落在治理侧;objectstack#4636 的 -B 选项是同一形状。 - -## 业务需求轴改判正反两例 - -出处:step 8 评估轴「实际业务需求」—— 先例全文(#7498 的行数预算搬移)。 - -已发布但零消费的「能力」**不因沉没成本获得豁免**:#5021(主题排版 9 组)、 -#4988(交互配置 22 站点)、#4834(plugin-runtime 五 schema)是先例。 -这条轴会**改变结论**,不是陪衬,正反两例都有:#5021 因无业务拉动裁 -退役,#4936 则因 showcase 自证了业务方向而裁「响亮拒绝而非退役」—— -只看后两条轴,这两单会得出同一个答案,那是错的。 - -## 两种失明机理 - -出处:step 5 Prompt template「下沉进 os-dev 定义的那三条验证条款」—— 两段机理全文 -(#7631 批次的行数预算搬移);文中条款正文住 `.claude/agents/os-dev.md`。 - -- **时序错(#6371)** —— 包集合对,但读的是过期事实。新 worktree `pnpm install` - 之后直接 `typecheck` / `test`,跨包依赖的 `dist/*.d.ts` 要么不存在要么陈旧。 - 它**两个方向都骗人,且骗法不对称**:*假红*只烧时间(dev 去修一个不存在的问题 - —— #6203 烧了三个来回,#5962 报了 15 个包的红);*假绿*更危险 —— 收窄了导出 - 类型,下游读的还是**旧** `.d.ts`,于是下游 typecheck 报绿,而那个绿**什么也没 - 证明**,dev 会把它当作「消费半径已清扫」写进 PR,复核方从报告里看不出区别。 - 同一天连咬三个 dev(#5962 / #6210 / #6203)。正面样本是 #6212 的 A+E 批:PR 里 - 点名「driver-sqlite-wasm 读的是 driver-sql 重建后的 `dist/*.d.ts`」,先 build 再 - typecheck,**并且**用一次故意的反向验证坐实自己确实读到了新 `.d.ts`。 -- **方向错(#6218)** —— 扫的包集合本身就不相关。pnpm 的省略号**有方向**: - 后缀 `'pkg...'` = 该包 + **它依赖的**(上游闭包),前缀 `'...pkg'` = 该包 + - **依赖它的**(下游消费者)。#6210 收窄五个驱动的方法签名后按后缀扫,报告 - 「25 个包全绿」,CI 全仓 typecheck 随即红在 `@objectstack/dogfood`(120 个任务 - 118 绿)—— 那句「全绿」在方法论上与被测风险完全无关,却读起来像一次充分的清扫。 - -## 共享一致性覆盖三轴实录 - -出处:step 5「多面组件:测试落点是共享一致性覆盖」—— 实证全文(#7631 批次的行数 -预算搬移);不变量引文与适用判据留在主文件。 - -`driver-memory` 有三个过滤面(`find` 的 live path、参考匹配器、analytics/cube 面)。让 -那批缺陷活下来的不是难度,是**没有一条断言在问** —— #5345 之前,共享一致性表 -`FILTER_LOGIC_CASES` 只盯住其中两个。本轮三次派发都写了这条,三次都兑现,并且长成 -**三条正交的轴共用一条不变量**:#5375 是过滤器**形状**(组合子、算子词表)、#5431 是 -**比较数类型**(布尔 / null / 数字样字符串)、#5445 是**算子**(每个声明的算子编译出的 -谓词是否真的排除行)。 - -第三条轴还带了「declared = enforced」的另一半:`ANALYTICS_FILTER_CAPABILITIES` 声明的 -**每一个**算子都被驱动着走两条路并必须一致,且**探针必须至少排除一行** —— 否则「一致」 -什么也证明不了,那正是 #5374 的形状。 - -## 拒收用例两种失明实测 - -出处:step 5「拒收类用例的最低断言集是 `code` + `status`」—— #6142(#6050)反向 -验证实测全文(#7631 批次的行数预算搬移);条款原话与「一句话」结论留在主文件。 - -- **裸 `Error` ⇒ 恒绿。** 删掉拒收闸后 `driver-sql` 28 例红 22,**多数红在抛出 knex 的 - 裸 `Undefined binding(s)`** —— 一个 `code` / `status` 均为 `undefined` 的 Error。未修的 - 驱动本来就抛,缺的只是信封:只断言「它抛了」的用例,**在本单所针对的那个驱动上保持 - 绿色**。 -- **从不抛的 transport ⇒ 红,但红得不指向缺陷。** 同一次删闸,`driver-turso` remote - 29 例红 20,**20 个全部**红在「本该拒收却编译出了 SQL」—— 该 transport 从不抛。只断言 - 抛出的用例在这里报的是「promise 没有 reject」,说的是**没抛**而不是**没信封**,分不开 - 「拒收了但信封错」与「根本没拒收」—— 而这正是这一族的两个缺陷。 - -## 收益穿过边界实例 - -出处:step 7 复核清单「收益穿过它必经的那道边界之后还在吗?」—— 实例全文(#7631 -批次的行数预算搬移);判据句留在主文件。 - -实例:整条 filter 链的价值是 -「拒收要说清楚作者错在哪」,而 `packages/rest/src/rest-server.ts` 的两处 4xx 直通**曾**把 -`message.length >= 500` 的错误**整条替换**成 `'Request failed'` —— 不是截断; -`driver-sql` 的 `$null` 拒收实测 606 字符,越线,于是 REST 客户端拿到的是 -`{ "code": "INVALID_FILTER", "error": "Request failed" }`:`code` 到了,**正文一个字 -也没到**。反直觉的一层:不带 `status` 时原文完整直通,带上 `status: 400`(#4436 为进 -ADR-0112 信封特意加的)反而被这道闸门吞掉 —— 写得越精确的拒收越确定送不到。**四个 PR -合入,没有任何人在复核里查过这件事**,直到一个 dev 在做别的单时顺手撞上(#5423,已按 -「截断而非替换」修掉)。缺口不在那段代码,在清单里:本条补的就是它。 - -## 死代码删除核引用面标本 - -出处:step 7 复核清单「以『死代码 / 不可达』为由的删除」—— #5445 标本全文(#7631 -批次的行数预算搬移);imperative 与查法指引留在主文件。 - -#5445 删掉 -`memory-analytics.ts` 里两条映射(`'inDateRange': '$gte'`,注释自称 "Will need special -handling" 却无任何调用点实现;`'notSet': '$exists'`,方向还是反的)。dev 给的推理成立 -(无条目降级到该名、`timeDimensions` 走 Stage 2、两个出口都只消费 `normalizeFilters` -的输出),但推理可以错,而 grep 只花十秒: - -``` -git grep -n "inDateRange\|'notSet'" origin/main -- 'packages/**/*.ts' -``` - -确认 `driver-memory` 内只有被删的那两行引用,其余命中全部落在 `service-analytics` -—— **另一个包、另一套 strategy**,不受影响。 - -## 代执行出处一例 - -出处:State model「代执行他人指令的关闭/作废,评论里必须带指令出处」—— 实测全文 -(#7631 批次的行数预算搬移)。 - -PR #6668(一份 draft、全绿的 ADR 草案 —— 因未合并,它提案的那个编号从未签发) -被兄弟席位无说明关闭,本席读作误扫,重开 -并发跨席询问;维护者随后说明那是**他本人的指令**,遂按原话重新关闭并把出处附在 -PR 上。写那行出处要 20 秒,查「这是不是误操作」要一个跨席往返。 -(同一个 PR 的另一半沿革 —— 维护者以「没人要这个能力」结案 —— 在 Guardrails 的 -ADR 条款里,那条讲的是**决定**,本条讲的是**执行记录**。) - -## 解锁扇出两标本 - -出处:step 3「解锁扇出优先」—— 2026-08-07 解锁扫描实测两标本(#7631 批次的行数 -预算搬移)。 - -**#5702** 自 08-06T06:47Z -起可派发、压了约两天没人扫,身后排着 #5814 / #5893 / #6337 三张;**#6428** 更尖锐 -—— 它不带任何优先级标签,却挡着 #5491 + #5492(维护者当日裁定必须同批落地的 -v17 安全批的两半),#5492 自己又挡着 #5493。 - -## objectui 板无生产者读数 - -出处:发版板「每个 backlog 恰好一个生产者」—— objectui 行的立据读数(#7631 批次 -的行数预算搬移)。 - -为什么补这条(实测 2026-08-09):objectui 的 `target:v17` 只在 2026-08-06/07 -那次审计用过 7 次且全部已闭,此后本地 backlog 长到 117 open / 48 `pm:queue` -(立单读数;#6906 的 PR 复测 119 / 52)而板上 open **0** 条 —— 「随 console bundle 入板」有消费方却**没有常设生产 -者**,而控制台正是从这个 backlog 发出去的。 - -## #5032 认领撞车与让行实录 - -出处:step 4「Race check」与「Yielding is a handoff」—— #5032 实录全文(#7631 批次 -的行数预算搬移)。 - -#5032 is -the 20-second version: claims at 00:39:44 and 00:40:04, the later one -composed **without re-reading the thread** — and both aimed at -`pnpm-lock.yaml`, where two parallel re-resolutions produce mutually -unmergeable diffs. The dispatched dev caught the collision on its own -pre-code re-read and stood down with zero files touched. -In #5032 the yield handoff (the offline -scanner repro, `pnpm why` chains for all three packages, and live proof -that the existing undici override's exclusive upper bound had -self-invalidated) was consumed directly by the winning dev, whose PR -#5052 was up within half an hour of the yield. - -## 半标注卡四次实测 - -出处:step 0「为什么 2、3 不是可选项」—— 四次实测与两个生产机制全文(#7631 批次 -的行数预算搬移)。 - -同形已四次实测(两次各停滞整日,一次同日六张 11:00–13:55Z 立、16:47Z 才被两个 PM -会话**手工**报回来才捞起)。 - -- 析取 2 的生产者是**协议本身**,不是某个车道的坏习惯:跨座位转移卡(转出方 - 按转移协议自带 `pm:queue`)与队列管家 Routine 的队列健康卡,**按设计**预先 - 带队列标签,又**按单一生产者规则不准自打 `domain:*`**(管家原话:「no - `domain:*` / `repo:*` label applied — routing labels are the triage seat's - single-producer territory. Only `pm:queue` is set here.」)。于是它们在看板上 - 显示可派、实际**谁都不能认领**。 -- 析取 3 那张实测卡是**分诊自己写的纪律的反例**:同一条「立单方别自打 - `domain:*`、留给分诊」当天早上刚写在另一张卡上,几小时后 PM 立单时照犯, - 卡片对队列与扫描同时隐身 ~69 分钟。 - -## #5085 推断中止误判对照 - -出处:Operational notes 11 —— #5085 全文(#7631 批次的行数预算搬移)。 - -#5085 的 -dev 子代理零推送、零分支就没了(子代理正常的 `/compact`/中断死法,step 6 的探活与 -step 5「Handing off an interrupted dev」讲的就是它)。前任 PM 把它**推断**成 -「维护者手动中止」,设了「是否重派等维护者示意」的门:该门从 08-05 07:00Z 立到 -08-06 02:42Z 解除,**近 20 小时**压着一个 p0-邻近的真 bug,直到维护者本人确认 -「没有中止」才发现是误判。同一条 #5085 上两种都出过:08-05 07:00Z 那次是推断 -(误判,门压近 20 小时),08-06 04:12Z 那次是宿主信号(真中止,维护者两分钟后 -示意重派、门即解除)。两次的症状(零推送、无分支)完全一样。那次 -的门只写在「认领解除」评论里、标签退回了 `pm:queue`,于是队列视图显示可派发而 -谁也不敢派。 - -## 读取端截断三例误判 - -出处:Operational notes 12 —— 三例实录全文(#7631 批次的行数预算搬移)。 - -#5148 / #5149(2026-08-05,分诊座位)与 #5164(cli 车道 PM)被判为「正文已被 GitHub -sanitizer 截断,不可分诊/派发」,据此挂起并要求原作者重贴。事后以两种读法复核 —— -REST 取 `body`(原文 4321 / 5183 / 4181 字符)+ 取 `body_html`(渲染版)—— **三条 -正文都完整**,`` / `` 一类占位符全部落在行内代码或围栏内、未被吞。 -三条判读均不成立,真因是**读取端(工具输出)截断**被误读成 issue 端截断;代价是 -三条 issue 各白停摆 1–2 天(#5148 是有脚本化复现的可入队缺陷,#5149 / #5164 是 -应当尽早进维护者决策箱的裁决卡),外加三张打给作者的假工单。 - -## #5584 advisory 红合并毒化 - -出处:Operational notes 10 —— #5584 全文(#7631 批次的行数预算搬移)。 - -PR #5584 的新测试文件触发 `check:engine-double-contract`(这一族门禁挂在 **ESLint -job** 里),该 job 19:53Z 结论 `failure`,而 PR 在红了 **19 分钟后照常过队合并**。 -合并后,`main` 上这条红被 merge ref 带进**每一个后续 PR 的 ESLint job**,#5601 等 -直接中招;热修 #5615 才解除,治理侧另立 #5617。 - -## 立单查重两反例 - -出处:Operational notes 9 —— 两例全文(#7631 批次的行数预算搬移)。 - -#5039 是反例:为一批 OSV 公告立单前没有做任何搜索,而 #5032 六分钟前已为同 -一批公告立单、且分析更全(逐包 `pnpm why` 归因 + main 上的逐条复现证据), -两分钟后 #5039 只能关成 duplicate。代价不止两分钟:两条单各自吸走了一次认 -领,直接诱发了 #5032 上相隔 20 秒的认领撞车(见 step 4)。同日另一例: -#4946 与 #4945 各自为同一条 brace-expansion 公告立单,后立者关 duplicate。 - -## 共享基础设施回退一例 - -出处:Operational notes 8 —— 边界外一例全文(#7631 批次的行数预算搬移)。 - -PR #4864(本线)与 PR #4856(另一车道)修的是**同一个 -基础设施问题**,却挂在不同 issue 号下,门禁看不到任何重复。更糟的是 #4856 先合了 -`testTimeout: 60_000`,**#4864 若合进去会把它降回 30s**,即一次静默回退。 - -## 单正文座位表互吞 - -出处:「座位贴协议」首段 —— 单正文时代的互吞机制全文(#7631 批次的行数预算搬移)。 - -座位登记曾是 #4604 单正文里的一张表,12 个座位共编一个 body;issue 正文更新是 -**全文覆盖**(PATCH 整体替换,无条件写入/CAS),从过期快照出发编辑 = 静默回滚 -其它座位的行 —— 与 os-regen 静默吞并同形:操作成功、零冲突标记、丢一侧改动, -08-06 一天内多次实测互吞。 - -## 计划内压缩成本依据 - -出处:step 9「波次收工点」—— 成本读数全文(#7631 批次的行数预算搬移)。 - -成本依据(#6806 云卡在飞约 2 小时的读数):`cache_read` **10.18M**, -未命中 input 仅 **6.9K** —— 边际每轮成本 ∝ 已积累上下文长度,随班龄单调增长;而 -**「不压缩」并不保全上下文**:自动压缩时点不可选、裁剪者不知道哪些判断链重要。 -计划内压缩在两个维度上严格占优(时点取在飞归零,裁剪者是知道轻重的在任 PM)。 - -## 转述断言重验三例 - -出处:step 5「派发令里的清单、路径、行号,一律在派发那一刻从树上取」的 ⚠️ 段 —— -实测明细(#7631 批次的行数预算搬移)。 - -连「门禁清单 = `lint.yml`」这句本身都是记忆形状的断言:该卡实测 61 条 -`pnpm check:*` 在 `lint.yml`,另有 7 条散在 `ci.yml` / `spec-liveness-check.yml` / -`validate-deps.yml` / `release.yml` / `showcase-smoke.yml`。同形错误刚在 #6865 上 -发生过 —— 转述进派发令的六个 required-context 名里,**四个其实住在 `ci.yml`**。 -#6673 的「第三处提示串在 `protocol.ts:4780`」实测落在另一个函数、另一条轴上。 - -## #6413 解锁前提旧行号标本 - -出处:step 3「解锁那一刻」第一条 —— #6413 全文(#7631 批次的行数预算搬移)。 - -#6413 是标本:#5055 的 PR #6385 于 -17:39:30Z 合入,分诊席 17:54:18Z(**15 分钟后**)贴出「前提已核」并援引 -`widget-contract.mdx:14/:181/:184`、`quick-reference.mdx:55` —— 那些行号**只**匹配 -`f7bd4e235^`(合并前的父提交),#6385 本身已经重写了其中一页(+62/−156)、改指了 -另一页的行、删掉了该行链接的生成页。车道席 23:17:06Z 的解锁裁决又把这份未经复核的 -说法原样带了下去。两处旧条款(step 1 陈旧前提检查、解锁扫描)**字面上都被满足了** -—— 它们查的是**上游的状态**,不是**卡在上游 ref 上的前提**。最后是 dev 用 -premise-first 纪律顶回来的:`premise_still_valid: false`、零字节改动。 - -## #6190 收窄断言被证伪 - -出处:step 3「解锁那一刻」第二条 —— 实测全文(#7631 批次的行数预算搬移)。 - -实测:`domain:metadata` 席在 #6190 的解锁评论里断言该卡已被 -PR #6478「实质收窄」,dev 验证**证伪**了它 —— #6478 只关掉了 overlay 那一层, -`allowRuntimeCreate` 那层仍在铸 org-scoped 行,原症状完好。 - -## L2 即报契约与决定性实测 - -出处:step 7 复核清单「报告在草稿 PR 时点到达」—— 全文含选 B 弃 D 决定性实测(#7754 批次的行数预算搬移)。 - -- **报告在草稿 PR 时点到达 —— CI 收敛读数自此只属于复核侧(#6644 L2,维护者 - 2026-08-10 裁定)。** dev 的契约是「推分支 → 开 draft PR → 立即交报告」,报告里 - 的 gate 状态照实记(`in_progress` 是诚实读数),⛔ 不等收敛 —— 所以「报告到了、 - CI 还没绿」是**预期内**的常态,不是异常。选 B(即报)弃 D(前台等到收敛)的 - 决定性实测(2026-08-10):4 个在飞 dev 死 2 个(#6041、#6906),死点全在 - **活干完、报告未达**之间 —— #6906 连 commit 都打好了、分支未推;前台等待防不住 - 进程重启,把报告时点提前到 push + draft PR 即刻才把这扇窗压到最小。守门职责 - **移交**到本侧,不是删除:arm auto-merge / 入队前**亲核门禁 job 的结论** —— - 不止 `pull_request_read get_status` 那个聚合读数,要看 ESLint 与 TypeScript - Type Check 这两个具体 job 的 `conclusion` 已为 `success`(门禁族都跑在它们 - 里面,Operational notes 10;#5584 的 advisory 红就是没读结论、红着合并进 main - 的)。这道读数现在是**唯一的一道**(本地门禁已按面收窄,step 5 / os-dev「Local - verification scope」;dev 侧的收敛等待已随 L2 移除),⛔ 不要因为报告写了 - 「本地绿」就跳过它。收敛期间转红的门走补丁轮(SendMessage 续派原 dev,REWORK - 那条)—— 多花的 push-fix 回合是这笔交换**已经付过**的价钱,不是 REWORK 的 - 理由;红着合并才是。PM 侧与之配对的机械动作是「入队与落地 B」的 flip 定点 + - 队列看护 —— 那一段自此是 L2 的 PM 半边;派发令可对重量级卡显式写「本单等 CI」 - (step 5 模板的每单覆盖条款),只有那时 dev 侧的收敛等待才回来。 - -## premise 证伪四例 - -出处:step 7 复核清单「dev 是否验证了前提」—— 同日四例全文(#7754 批次的行数预算搬移)。 - - that accepts every stated cause at face value is the one to read twice. Four - same-day cases: #4808 (the issue was half right — the real truncation was in - pruning, not the TTL), #4813 (the technical rationale the PM supplied was - disproved by measurement and the dev's was harder; the issue body's wrong - attribution became #4873), #4825 (the issue's option 2 was killed by - call-site evidence), #4790 (previous day, a fixed-window conversion - rejected). When a dev corrects the PM, **acknowledge it in the open** — the - correction belongs in the PR/issue comments so the next reader inherits the - corrected premise, and a wrong premise still sitting in an issue body gets - its own follow-up issue rather than being silently dropped. - -## #5452 验收判据证伪标本 - -出处:step 7 复核清单「验收判据本身也是前提」—— #5452 标本全文(#7754 批次的行数预算搬移)。 - -- **验收判据本身也是前提的一部分,可被 dev 证伪。** #5452 的 issue 把验收写成 - 「某条字面 grep 归零」,dev 实测证明该 pattern 修前修后命中数不变(修好的 - 正确输出同样匹配它),于是改钉真不变量(行内代码跨度花括号配平)做门禁, - 并因此多抓出 2 处 issue 的 grep 天然看不见的同根因缺陷。评审姿势:dev 用 - 测量推翻字面判据、换上等价或更强的不变量门禁 = 好运行,照 ACCEPT;但推翻 - 过程必须写在 PR 正文里,且新判据要附在 main 语料上的实测信噪比(误报为零 - 的证据),否则按 REWORK 要证据。 - -## 事件载荷闸历史坑 - -出处:step 7 复核清单「skip-changeset」—— #5497/#5502/#5625 历史坑全文(#7754 批次的行数预算搬移)。 - -- **Tests/docs-only PR 走 `skip-changeset` 标签,不走空 changeset**(空 - changeset 滞留发布,#4898)。标签由 PM 在验收时打。历史坑(#5497/#5502 - 实测):该闸曾从**事件载荷**读标签,rerun 重放旧载荷看不见新标签,得靠 - 「摘掉再打回」制造新 labeled 事件 —— **#5625(#5580)已根治**,闸门改为 - 实时读 PR 标签,rerun 即翻绿。留此一条是因为它是一类通病的标本:**任何 - 从事件载荷而非现状读判据的闸,rerun 都复现旧世界** —— 撞上同形状的红, - 先查该闸读的是载荷还是现状,再决定是补事件还是改闸。 - 边界:改动若含读者可见的生成产物(如参考文档),dev 选 changeset 而非 - 标签是对的 —— 以 PR 正文说明的理由为准,两条路都有效,别来回改。 - -## +0/-0 NUL 标本 - -出处:step 7 复核清单「`+0/-0` 不是空文件证明」—— #4870 标本与修法全文(#7754 批次的行数预算搬移)。 - -- **`+0/-0` in a PR diff is not proof of an empty file.** git renders a file - as binary — zero added, zero removed — as soon as it contains a NUL byte. - #4870's 347-line test file showed `+0/-0` and was briefly misread as an - unfinished placeholder; it was one bare `0x00` in the body, which - `pnpm check:nul-bytes` rejects by design. On any `+0/-0` entry suspect NUL - first and an empty file second, and settle it on the blob rather than the - diff (`git show : | wc -l`). The fix always belongs in the - source: write the escape sequence `\u0000`, which is byte-identical at - runtime and is the convention `scripts/check-nul-bytes.mjs` enforces. A - raw NUL is never the right authoring choice: it also makes the whole file - invisible to grep, which is how the defect hides in the first place. - -## #6732 路径分叉由来 - -出处:step 7 ACCEPT 路径分叉 —— #6732 由来全文(#7754 批次的行数预算搬移)。 - - 入队。** - 上一段那条一般规则**没有错,而正是它正确执行的结果触发了被禁止的动作** —— - #6732 不是谁「决定要合 ADR」,是 ACCEPT 照章办事,把一份已复核、全绿的 dev PR - 推进了合并队列。所以这个判据必须落在**手动之前**,而不是只写在 Guardrails 里 - 等人事后对照:一个先读 Guardrails、再读 ACCEPT 的 PM,做的是 ACCEPT 说的事, - 因为手在这里。维护者裁决原文见 Guardrails 那条(引用、未翻译)。 - -## #6190 Fixes 误标实测 - -出处:step 7 ACCEPT「`Fixes` 还是 `Part of`」—— #6190 / PR #6600 实测全文(#7754 批次的行数预算搬移)。 - - **开着的** issue:卡一关,那个待裁问题就此无人可见,且没有任何读者会知道它消失过。 - 实测:#6190 的 dev 把 loud-log 那一半开成 PR #6600 时带的是 `Fixes`,复核时抓到、 - 入队前改掉(dev 改后回读:`Fixes objectstack` 命中 0 次);若照原样合了,那个 - 两问的契约裁决就从收件箱里蒸发了。**翻 ready 之前亲核首行**,别只信报告(#6644)。 - -## #3682 sweep 产物成组实测 - -出处:step 7 ACCEPT「sweep 产物成组列出」—— #3682 实测全文(#7754 批次的行数预算搬移)。 - - 「范围外发现照旧单开」(PD #10)会一次性吐出一**批**卡:#3682(ADR-0106,L, - `mode:cloud`)一次派发就产出 3 张退场审计 finding(#6599 / #6601 / #6603)+ 1 张 - 跨座位转席卡(#6622)+ 1 条决策箱上报,另有 3 处 ADR 自己没点名的退场在 PR 内修掉。 - 这个量级下,分诊席需要它们**作为一个集合**到达,并且要知道**这次 sweep 的判据是 - 什么** —— 否则就是一小时内飘来五张互不相干的卡,只能一张张重新推断关系。所以 - ACCEPT 评论里列成一块,并写明 sweep 判据,让分诊能一致地给整批定级,而不是逐张 - 各判各的(#6644)。 - -## 串行接力语义交接实例 - -出处:step 7「串行接力」纪律 2 —— #5318/#5319 实例全文(#7754 批次的行数预算搬移)。 - -并要求「两个 PR 的意图**叠加**,⛔ 禁止机械取一边」。实例:#5318 与 #5319 同动 -`packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`,#5319 逐行号核过 -#5318 新增的十项元素、并实测两者的交互 —— 没有 #5319 的 preprocess 修复,#5318 的新 -断言在 view 上是空转的。取一边会「各自绿、合起来错」,即 AGENTS.md §10「clean merge -不等于 working merge」落在同一份测试文件上的形态。 - -## 决策卡前提刷新全文 - -出处:step 8 升级序列第 1 条 —— 前提刷新与 re-check 命令必备件全文(#7754 批次的行数预算搬移)。 - -1. **先刷新卡片的前提 —— 落卡与复升级都适用。** 决策卡写下的每条前提(某个在飞 - PR 还没合、某能力还不存在、某文件还是那个形状)都是**有保质期的读数**:main - 一天 ~18 合并,跨仓事实按小时变。落卡**之前**、以及把一张旧卡重新推到维护者 - 面前**之前**,逐条复核一遍,失效的就地改写或撤卡 —— **隔夜没动过的卡,默认按 - 「前提未经验证」处理,不是按「还在等答复」处理。** 出处是决策箱第 2 轮的实测: - cloud#1148 的 A/B 卡在**写下前 ~50 分钟**就已失效(它等的那个上游 PR 已经合了), - cloud#812 一张卡带三条过时前提。前提过期的卡比没有卡更贵 —— 维护者会照着一个 - 不存在的世界做裁决,而卡面上没有任何读数会显示这件事发生过。 - **模板必备件(#7341 item 8):卡上每条前提行自带一条 re-check 命令** —— - `git log origin/main --oneline -5 -- `、REST `compare`、带引号精确名的 - `git grep`、`git ls-remote --heads origin | grep `……写卡的人当场就有 - 这条命令(它就是建立该前提用的那条),抄上去的成本是一行;省掉它,上面那次 - 复核就从「跑命令」退回「重做研究」,而研究没人重做,卡就带着死前提上桌。 - 复升级时逐条**跑**一遍即可,零命中/变形的前提就地改写或撤卡。 - -## 波次收工点换人轮换阐释 - -出处:step 9「波次收工点」—— 换人轮换降级为班末动作的机理全文(#7754 批次的行数预算搬移)。 - -**换人轮换降级为班末动作**(换视角带来的免费证伪 —— step 5「dev 证伪 PM」那条的 -PM 侧对偶),不再是班内节奏:同席压缩与换人的 token 账等价,但压缩**保全会话 -绑定** —— PR 订阅、`send_later` 自绑定定时器、座位贴登记的会话 ID、对云卡的父子 -关系全部不动,三处同笔的接管协议也免了。**Routine 座位不适用**:它每 fire 一个 -新会话,本来就是从 GitHub 重建(见「座位 Routine 化」)。本条唯一的前提是本文 -开篇那条不变量 —— **GitHub 恒为唯一权威,上下文只是工作缓存**;它失守时,压缩就 -从「丢缓存」退化为「丢判断」。 - -## ADR 人工合并条款判据阐释 - -出处:Guardrails「ADR 由维护者确认、由维护者人工合并」条 —— 判据阐释、worked example 与权威落点全文(#7754 批次的行数预算搬移)。 - - 改动的落地是人的行为,绿灯只说明机器没意见。为什么偏偏是 ADR:按 AGENTS.md - PD #13,accepted ADR **就是**那个决定本身,合并它等于采纳一个治理立场 —— - 正好是「CI 绿」零信息量的那一类。worked example **#6668**:一份彻底、全绿、 - 测量无误的 ADR 草案,维护者以**没人要这个能力**为由关掉,那是任何门禁都评不 - 出来的判据;反方向的代价见 #6191 / #6483(一次没有 ADR 的 ADR 级反转,至今 - 还在拆)。PM 侧的终局动作见 step 7 ACCEPT 里的路径分叉 —— 那一段才是手真正 - 会动的地方。本条的仓级权威落点按 #6741 要求 1 是 AGENTS.md(PD #13 旁;截至 - 本条写入时那一半尚未落地,落地后照本节末行的既有优先序以 AGENTS.md 为准, - 这里是 PM 循环侧的执行拷贝)。 -## 探活规程实测背景 - -出处:step 6「探活是每轮巡检的固定动作」—— 五条规程的实测背景全文(#7754 批次的行数预算搬移)。 - -**探活是每轮巡检的固定动作 —— 完成通知不可靠,它的缺席什么都不证明。** -下面的停摆纠偏处理「带任务中状态的通知到了」;这一条处理更隐蔽的另一半: -**通知根本不来**。宿主进程重启会把运行中的 subagent 连同其完成通知一起 -静默杀掉 —— 2026-08-05 实测,五个「在飞」dev 里三个(#5050/#5515/#5483) -已死数小时,批次视图仍显示 5/5,实际吞吐 2/5,零信号。规程五条: - -- 每次巡检(定时器唤醒、轮间隙)对**每个已派发且报告未达**的 - dev 发一次状态询问(SendMessage,措辞「回一段简报后继续干活」,不改变 - 任务);派发后 ~45 分钟无任何远程产出即到探活门槛。⛔ 已有远程分支/PR - 不豁免 —— 触发面为什么这么宽,见下面「探活是常设兜底」。 -- 两种回包都有价值:活着 → 拿到进度与阻塞点;**「no active task; resumed - from transcript」→ agent 生前已死,这次询问本身就是复活** —— 从其 - transcript 带全部上下文恢复,比 worktree 接手协议(step 5)便宜得多, - 优先用它;resume 不可用时才走接手协议。 -- 判据永远取正向证据(远程分支、PR、报告、探活回包),⛔ 绝不把「还没 - 收到失败通知」读作「还在跑」。 -- **定时器重挂是每次巡检的第一动作,不是最后一个**(维护者 2026-08-06 - 授权)。巡检执行到一半被打断(穿插提问、事件风暴、会话中断)时,排在 - 末尾的重挂会整个丢失,守夜链就此断裂 —— 2026-08-06 实测:一次漏挂让 - 四连灭批静默了 ~100 分钟而不是探活门槛设计的 ≤45 分钟。先挂后查,链条 - 对中断免疫;挂错了间隔可以在本轮末尾用 delete_trigger + 重挂修正,但 - 「没挂」无法被本轮以外的任何机制补救。重挂的那一枪按 **notes 3 的定点 - 文本写法纪律**写:以「幂等 —— 动手前先重读状态」开头、只写判据不写结论。 - 巡检定点是最容易写成祈使句的一类(「⇒ 判为不可靠 ⇒ 重新派发」),也是 - 投递时最可能已经过期的一类 —— 在飞的 dev 在两次唤醒之间会推分支、开 PR、 - 交报告,而已 `delete_trigger` 的定时器仍会投递(notes 3 实测两次)。 -- **批量在飞期间,主巡检间隔不得长于 45 分钟**(同一授权)。探活门槛是 - 45 分钟,巡检间隔一旦超过它,门槛就成了写在纸上的数字 —— 最坏情形下 - 一个派发后即死的 agent 要等到下一轮巡检才被发现,静默窗口 = 巡检间隔, - 而非门槛值。在飞清零的待命期可放宽到 60-70 分钟;有任何 dev 在飞即收紧 - 回 ≤45,灭批频发期(如宿主重启风暴)进一步压到 20-30 分钟。 - -## 探活触发面证伪实测(7/7) - -出处:step 6「探活是常设兜底」—— 2026-08-08 触发面证伪实测(7/7)全文(#7754 批次的行数预算搬移)。 - -**探活是常设兜底,不是异常通道 —— 而「PR 已经开出来了」不是豁免,那恰恰是失败 -发生的那一格。** 上面第一条的触发面曾挂在「已派发且**尚无**远程分支/PR」上; -2026-08-08 的实测把那个口径证伪了:当天 **7 / 7** 个派发都是**在开出正确的 PR -之后**没能干净交回(#6586 / #6747)。失败的那一批**全都有远程分支、有 PR、有提交** -—— 按旧口径,它们一个也不会被探到。所以触发判据是现在这个写法:**探的是「报告 -未达」,不是「分支未出现」**;远程产出只把一个 dev 从「可能还没开始」挪到「可能死在 -收尾上」,它从来不是活着的证据(这就是上面第三条「判据永远取正向证据」里,PR 的存在 -算哪一种正向证据的答案:它证明工作发生过,不证明 agent 还在)。 - -- **生产侧的条款不能替代它。** 那批里有 4 个的派发词逐字带着终止条款,其中 **3 个 - 照样死了**(3/4)。成因在文档够不着的地方,所以兜底必须常设在消费侧,⛔ 不能写成 - 「派发词写全了就可以不探」。 -- **代价是延迟,不是正确性 —— 这既是它便宜的原因,也是回应只能是探针的原因。** - 至今每一例都能从 transcript 完整复活,**零工作丢失**:PR 在、分支在、提交在,缺的 - 只有那段 JSON。所以 ⛔ 永远不要拿「重新派发」回应它 —— 往一个**可能还活着**的 - worktree 里塞第二个 agent,是用一个只花时间的问题去换一个会毁东西的问题 - (step 5「Handing off an interrupted dev」的碰撞面)。先探,拿到「no active task; - resumed from transcript」这类回包再谈恢复。 -- 与判死门槛的关系一字不变:本条只把**探针**的适用面铺满,⛔ 不降低判死的三类正当 - 依据(见下一段)。已有 PR 的那一格尤其要守住这个分界 —— PR 全绿会让人很想直接跳到 - 「报告丢失 ≠ 验收停摆」那条兜底验收,而那条的三个条件里第二条正是**探活确认已死或 - ≥2h 无推送**,不是「PR 看着能收了」。 - -## Stall 复位梯度实测 - -出处:step 6「A stalled subagent」—— 一夜 6 次停摆与复位梯度实测全文(#7754 批次的行数预算搬移)。 - -**A stalled subagent is this half's most common failure, and it never -self-heals.** When a dev stops mid-task reasoning that "a background watcher will -wake me", **that watcher never fires** — a completion notification is itself the -statement that no live subtask remains. Four agents stalled 6 times across the -2026-08-04/05 night, every one recovered by hand, ~1.5–2 h lost. Three rules: - -- **State the execution posture in the dispatch/relay prompt** for any long - verification pipeline:「**前台(阻塞)同步执行全部步骤,中途不停止、不把构建/ - 测试挂到后台等唤醒**」. -- **A completion notification carrying a MID-TASK state IS the stall signal** — - "build still in progress", "I'll resume when…". SendMessage it back - immediately with that posture line attached; ⛔ do not wait out any silence - threshold (the cloud-mode ~2 h below): a threshold is for *no* answer, not for - an answer that says the agent stopped. -- **每一次复位比上一次更具体 —— 原样重发同一句话不算一次复位。** 2026-08-09 单班 - 三个 dev 各自以「等我挂的后台定时器唤醒」结束回合(**自己挂的定时器不会唤醒 - 自己**:完成通知本身就是「没有活的子任务了」这句声明)。两个在**点名该机制**的 - 第一枪探针后恢复;第三个**在被告知之后立刻重复了同一个停摆**,直到第三枪 - **点名下一个该发的工具调用**、并**明令禁止任何后台等待**才恢复。所以复位有梯度: - ① 复述执行姿态 → ② 点名下一个工具调用 + 明令禁止后台等待 → ③ 判 unreliable - (下一条)。把 ① 原样再发一遍只是把同一个失败重放一次,却会把三次停摆的计数 - 用掉一次 —— 梯度不是礼貌,是让第三次真的携带新信息。 -- **A third stall means unreliable** — re-dispatch a fresh agent onto that - branch under "Handing off an interrupted dev" in step 5 (worktree already exists, - read every existing commit first, re-run the verification in full, claim and - assignee untouched). - -The **producer-side** half of this rule lives in `.claude/agents/os-dev.md`'s -resource discipline — fixing it at the producer beats patching it at the PM -(Prime Directive #12's instinct, applied to agent protocol); these three are the -backstop, not the primary fix. - -## #5330 通知重放实测 - -出处:step 6「通知到达 ≠ 有新东西发生」—— #5330 重放实测与处置细则全文(#7754 批次的行数预算搬移)。 - -**通知到达 ≠ 有新东西发生 —— 第一眼读它是谁,不是读它带的 JSON。** 上一条处理 -「通知带着任务中状态」;这一条处理另一半:通知**形态完全正常**、报告**完整且正确**, -而它只是同一份东西的第 N 次重放。#5330 一张卡发出 **6** 条通知,其中 **5 条是同一份 -完整 JSON 报告的重放**(PR #6703;有一条来自一个盯着 agent 自己早已 `TaskStop` 掉的 -运行的 monitor)。它们在到达那一刻与真完成**完全同形** —— 同结构、同载荷 —— 于是每 -一条都被从头验收了一遍才发现是重复。**这个成本按到达次数计,由读的人付**:N 条通知 -= N 次全套复核,除非第一眼读的是身份而不是内容。PM 侧的处置: - -- **先算身份,再决定读不读内容。** 去重三元组:`(issue, 分支, PR head sha)` + 通知 - **自报**的守护对象。与本轮已验收过的那份逐项相同 ⇒ 在轮次台账上记一行「重放, - 首达时间 T」就结束,⛔ 不重新验收、⛔ 不重读 diff、⛔ 不重复留 ACCEPT 评论(重复的 - ACCEPT 评论会把审计线变成两条互相印证的假象)。 -- ⛔ **不把它的到达读成「还活着」。** 重放来自一个按**自己的 deadline** 触发的 - monitor,与它的主体是否还在跑无关 —— #5330 那条守的正是一个已被取消的运行。活着 - 的判据只有一个:探针回包。 -- ⛔ **也不把它的不到达读成「已经死了」。** 这是同一枚硬币的另一面,与上面「绝不把 - 『还没收到失败通知』读作『还在跑』」同源:重放一多,「最近有动静」这种读数就彻底 - 失效,两个方向都不能再从通知节奏里读出状态。判死照旧只认那三类正当依据。 -- **生产侧的自报是「变便宜」,不是前提。** dev 侧的对账写在 - `.claude/agents/os-dev.md` 的终止契约里(#6586):monitor 若仍触发,首行先自报它守 - 的是什么、那东西还活不活着,再给 JSON。⛔ 但不要把去重建立在「对面会自报」上 —— - 同一批实测里,逐字携带终止条款的 4 个派发死了 3 个,携带率打不穿的成因同样打不穿 - 这条。对面自报了就省一步,没自报就用上面那个三元组自己算。 - -## 座位 Routine 收集边界阐释 - -出处:step 6「座位 Routine 模式下的收集边界」—— 机理全文(#7754 批次的行数预算搬移)。 - -**座位 Routine 模式下的收集边界。** 一次 fire 就是一轮,fire 结束会话即销毁, -`mode:subagent` 的**返回消息**通道随会话一起消失。报告通道统一之后这不再是报告 -丢失:dev 的终报同时落在 issue 评论(``),**下一次 fire 从 -GitHub 照常收到** —— 会话销毁丢的只是加速器。真正的边界因此移到**干活本身**: -一个在 fire 结束时还没跑完的 dev(既无评论也无返回消息)只能靠下一轮读 GitHub, -见下一段的取舍。跨轮未收的 dispatch 由下一轮按同一判据处置(~2h 无报告即 -`blocked`),`delete_trigger` 的清理也顺延到收到报告的那一轮。 - -上面那三条**停摆纠偏**在 fire 内照常适用,但要注意它们的恢复动作是 -`SendMessage` —— 那需要一个**还活着的对面**。fire 结束后没有可唤醒的 subagent, -停摆与「会话已销毁」在 GitHub 上是同一个读数(既无报告也无 PR)。所以座位 -Routine 的取舍是:凡验证管线可能超过一个 fire 的活,**一开始就走 `mode:cloud`**, -把恢复权交给下一轮的 GitHub 读数,而不是赌它能在本轮内被唤醒。 - -## 舰队级死因与即报保险实测 - -出处:step 6「死因可以是舰队级的」—— token 断粮实测与即报顺序的保险论证全文(#7754 批次的行数预算搬移)。 - -**死因可以是舰队级的,那一格里探活半边同时不可用。** 2026-08-08/09 单班两次 -**全账号 token 断粮**(~11:0x–12:10Z、~16:0x–17:1x),每次一口气打死四个在飞 -dev —— 没有可探的对面,也没有可发的探针,三条件里能取的读数只剩 (a) 与 (c)。四张卡 -**零信息损失**的唯一原因是**分支已推、draft PR 已开、且 PR 正文自带验证证据** —— -PM 走本条直接验收照常收口(报告丢了,PR body 就是报告)。⇒「推分支 → 开 draft PR -→ 立即交报告」这个顺序是**保险,不是效率优化**:agent 的死亡是常态而非异常, -而它可以在任意时刻、成批地发生 —— #6644 L2 把报告时点提前到草稿 PR 开出即刻, -正是把这份保险的空窗压到最小(2026-08-10 实测:4 个在飞 dev 死 2 个,死点全在 -「活干完、报告未达」之间)。⛔ 但这**不**推出「把该顺序抄进派发令」:它是 -无条件条款,已住在 `.claude/agents/os-dev.md` 的 Definition of done -(push → draft PR → 报告即刻,CI 收敛归 PM),按 step 5 的下沉纪律派发令只带增量; -本条是它在 PM 侧的**读法** —— 知道为什么那个顺序值钱,才不会在 dev 报告缺席时 -误判为「要重派」。 -## Model tiering 沿革 - -出处:step 5「Model tiering」—— 三次改版沿革全文(#7754 批次的行数预算搬移)。 - -⚠️ **本节已两次改写更旧的规则,读到这里请以本节为准。** 最早的形式是 -「**pass `model: "opus"` on every dev dispatch**」;2026-08-09 改为 sonnet / opus -两档;2026-08-10 维护者裁定扩为**三档,并把档位决定权明确交给 PM**。凡在别处 -(旧交接笔记、座位贴、他人转述)读到前两种形式,一律以本节覆盖它,⛔ 不要几条 -并存着理解。 - -## model 参数解析四级细账 - -出处:step 5「Model tiering」显式传参条 —— 解析顺序两个方向的后果全文(#7754 批次的行数预算搬移)。 - -**档位必须显式传参,不能靠定义里的 pin 兜底。** 实测的解析顺序有四级(2026-08-09 -对照 Claude Code subagent 文档核过):`CLAUDE_CODE_SUBAGENT_MODEL` 环境变量 → -**逐次派发的 `model` 参数** → agent 定义的 `model:` frontmatter → 主会话模型。 -两个方向的后果都要记住: - -- `.claude/agents/os-dev.md` 的 `model: opus`(#6686 / PR #6688,由 #6836 的 - `check:agent-model-declared` 守着)**不会**否决你传的 `model: "sonnet"` —— - 参数在 frontmatter 之上,本节的分档因此确实生效,不是一纸空文。 -- 反过来,那条 pin 只管**你什么都不传**的情形。省略 `model` 不等于「按 os-dev 的 - opus 走」这句话今天恰好成立,但它成立的理由是那行 pin,而 pin 的历史正是被删过 - 一次(#6686:四个 dev 连坐同一堵额度墙,三个留下未验证的 worktree)。所以 - **每次派发都显式写 `model`**,让档位是这次派发的属性,而不是某个文件此刻的状态。 -- ⚠️ `CLAUDE_CODE_SUBAGENT_MODEL` 压在两者之上。本容器当前**未设置**(2026-08-09 - 实测),但它一旦被设,会静默盖掉你所有的分档决定且仓内无任何显示。另有一条静默 - 降级:传入的档位若被组织的 `availableModels` 白名单挡下,回退的是**继承的模型**, - 不是 frontmatter 的 pin。 - -## #7055 角色文件优先级实测 - -出处:step 5「角色文件优先级是实测事实」—— #7055 实测全文(#7754 批次的行数预算搬移)。 - -**角色文件优先级是实测事实,不是文体偏好(#7055)—— 下沉因此是唯一能生效的修 -法。** 一条逐字写进派发词的禁令(⛔ 不许 `--force`)输给了角色文件里过时的处方: -dev 把角色文件内化为「事情怎么做」,派发词的临时条款在它旁边读起来像建议。⇒ 两条 -配套规则:**对每张卡都成立的无条件条款只能住在 `.claude/agents/os-dev.md`,错了就 -修那里**(在派发词里加一条对冲条款修不了它 —— 实测会输);**逐卡可变量走显式接口** -(模板占位符与三分区),⛔ 不靠派发词临时覆盖角色文件的默认值。下面模板里保留的 -每一条,要么是逐卡可变的,要么是评审侧对账时点名要看的: - -## 读 GitHub 交换机理 - -出处:step 5「读 GitHub 比粘正文」—— 交换方向论证全文(#7754 批次的行数预算搬移)。 - -**「读 GitHub」比「粘正文」多担一个风险,少担两个 —— 这笔交换是有方向的。** -粘贴正文时 PM 替 dev 读了一遍,截断风险归 PM(notes 12);改为自读之后,截断风险 -归 dev,所以模板里那段自查是**风险转移的对价**,⛔ 不是可以省的客套。换来的是: -派发词不再随卡的长度线性膨胀,而且 dev 读到的是**当下**的 issue —— 包括派发之后 -才追加的评论和维护者补充,那正是粘贴式派发永远看不见的一面。 - -## 三分区措辞三次证伪 - -出处:step 5「派发令里的机制性指导分两个区块」—— #5561/#5808/#5669 三次证伪全文(#7754 批次的行数预算搬移)。 - -**派发令里的机制性指导分两个区块,措辞决定 dev 敢不敢证伪。** 一班三次前提证伪 -(#5885)都发生在 PM 附带的机制说明上:#5561(「注册告警无需动 spec」—— 实测 -Zod default 抹掉未声明,不可表示)、#5808(「500 自动进 withhold 路径」—— -启发式 11/11 不认)、#5669(「数组 where 闸门不看」—— 下沉后逐字同谓词)。三次 -dev 都用实测顶回并保住了裁决意图 —— 因为派发令把两类内容分开标注了: - -## 三分区第三块由来 - -出处:step 5 三分区「第三块」—— 由来与代价论证全文(#7754 批次的行数预算搬移)。 - -不分区块的派发令里,机制假设穿着裁决的衣服,dev 要么盲从错误假设、要么连裁决 -一起重开 —— 两个方向都是返工。 - -**第三块是 2026-08-09 单班补的:前两块漏掉了最便宜的那一类 —— PM 顺口给的一个 -「看起来无害」的选项。** 同一班被证伪两次,两次 dev 拒绝都是对的(#6865 / -#6893,实录见 `references/incidents.md` §「便宜选项两次证伪」)。 -⇒ **把一个便宜选项写成已裁定,恰好招来相反的结果**:dev 要么照做产出一个红,要么 -为了顶回来花掉一轮往返。裁决那一块只写真裁决,凡是「我觉得可以这样」的一律降到 -第三块 —— 措辞的成本是零,读错的成本是一轮。 - -## #5586 文件面锚错标本 - -出处:step 5「文件面要写预期落点」—— #5586 标本与跨座位声明全文(#7754 批次的行数预算搬移)。 - -**文件面要写「预期落点 + 生产者在别包时怎么办」,⛔ 不能只写一个路径名。** 这是 -第二块最常见、也最贵的一个实例。#5586 的派发令把文件面锚在**消费者** -`packages/core/src/utils/filter-tokens.ts`,而那条文法的**生产者**在 -`packages/spec/src/data/context-tokens.zod.ts`;dev 在生产者侧修是对的(os-dev 的 -contract-first 条款本来就要求它这么做),因而突破了申报的文件面。**只写一个路径名 -的派发令,是在要求 dev 在「守约」与「修对」之间二选一** —— 而按它自己的定义,那 -两条本该是同一条。所以文件面写成两句: - -> 预期落点是 ``;若实测表明真正的生产者在别包,**报备后按生产者侧修**(落点与 -> 理由写进报告和 PR 正文),⛔ 不在消费者侧打补丁。 - -PM 侧的对价是**事后补声明**:#5586 那次判偏离成立,按 #6532 先例补了跨席声明 -(#6017)。要一起记住的机械事实是**跨包常常等于跨车道** —— 这一例的消费者在 -`domain:engine-core`、生产者在 `packages/spec`(「shared contract surfaces have one -owner」恒归 spec 座位),所以补的是**跨座位**声明,而不是随手越界;走的路径是 -rule 4 的跨域例外与「跨座位转移协议」,本条不另造机制。 - -## same-day churn 两标本 - -出处:step 5「Same-day churn goes INTO the prompt」—— #4808/#4806、#4820/#4822 标本全文(#7754 批次的行数预算搬移)。 - -**Same-day churn on the issue's files goes INTO the prompt.** Step 1's -stale-premise check protects against issues that aged; the same-day variant is -main moving between filing and dispatch on the very file the issue quotes — -#4808 was dispatched right after #4806 rewrote the same guard, #4820 right -after #4822 touched the same file. Both prompts carried an explicit line -(「基于合并后的代码工作,issue 引用的片段可能已变,先核对当前 main」), and both -devs avoided rework that the issue's own snippets would have caused. Add that -line whenever `git log origin/main --oneline -20 -- ` shows a merge on -the issue's files today, and tell the dev to verify against `origin/main` -rather than any working tree (Operational notes 4) — the dev's worktree is cut -from `origin/main` once and never refreshes itself. - -## 在飞重叠拦截实测 - -出处:step 5「In-flight overlap needs intercepting too」—— #5322/#5335 实测与 notes 8 对偶全文(#7754 批次的行数预算搬移)。 - -**In-flight overlap needs intercepting too — same-day churn only covers the -dispatch instant.** The paragraph above handles "main moved before takeoff"; -main lands ~18 merges a day, so it moves **after** takeoff just as often. -#5322's agent launched at 23:17Z and #5335 merged 32 minutes into that flight -(`merged_at` 2026-08-04T23:49:44Z) — the same two compilers, two of the same -four cells. The PM's routine check read `git log -origin/main`, spotted the overlap and sent an immediate SendMessage warning; the -agent narrowed its scope twice and dropped its own design in favour of a minimal -diff replayed inside the other PR's structure. Rule: **every round, when you -read `git log origin/main`, intersect each newly-landed PR against every -in-flight dispatch's declared file surface** — on any intersection warn at once, -with four instructions:「合 main 后重跑测试矩阵、读对方 diff 重划边界、只补它没覆盖 -的部分、**被完全覆盖就停下回报,⛔ 不要硬造 diff**」. One round late is one rework. - -This is Operational notes 8's sister paragraph: notes 8 is the PM re-checking -main **by symptom before enqueuing its own** shared-infrastructure fix; this one -is the PM re-checking it **on behalf of someone else's in-flight agent**. Same -fact that main keeps moving, two different victims — and only the PM can see the -second one, because the flying agent has no view of `origin/main` moving under it. - -## 语义翻转 pin 清扫标本 - -出处:step 5「A ruling that flips public semantics」—— #5322/#5365 标本全文(#7754 批次的行数预算搬移)。 - -in the dispatch prompt.** When a maintainer ruling changes a public semantic -(#5322 reclassified the empty combinator from *refused* to the **boolean identity -element**), pins of the old position live **outside** the package being changed: -the consumer layers each hold a copy — REST envelope tests, objectql, runtime. -#5365's first lap flipped only the service-analytics layer and the copy in -`packages/rest/src/analytics-filter-refusal-envelope.test.ts` went red in CI -(`expected 200 to be 400`); a second lap cleared it. Two lines belong in the -prompt: - -## premise-first 一日四证 - -出处:step 5「Issue 正文是线索,不是规格」—— 一日四证全文(#7754 批次的行数预算搬移)。 - -**Issue 正文是线索,不是规格 —— and the dispatch wording is what makes an -honest "the premise is dead" cheap to return.** Step 1's stale-premise check -is the PM's sample; the dev's verification is the real thing, so the prompt -must state the premise-first requirement explicitly (the template line -above), and the PM must treat `premise_still_valid: false` with no PR as a -legitimate — often valuable — deliverable. Evidence from one working day: -#4832 (dispatched; the dev found the premise had already expired), #4250 -(the issue's minimum ask had long shipped via the stall-guard series — the -premise check instead surfaced that the guard's SIGKILL escalation path had -never once executed, a real defect the issue never named), #5047 (the -claimed 「enable/disable 重启即失」 was disproven with file:line evidence — -persistence existed by design — and verification narrowed the work to the -real empty-env seed bug PR #5117 fixed), #4930 (two of the three "silently -green" claims were wrong: the scripts went red with misleading messages, and -the fix was re-scoped to the dev's measurements). A dev that falsifies the -issue — or the PM's own framing — is a good run (step 7 says so); a prompt -that presumes the issue is true converts that good run into apparent -disobedience. - -## 云卡分流裁决理由 - -出处:「Resource limits」M+ 云卡默认第一条 —— 裁决理由与旧判据清单全文(#7754 批次的行数预算搬移)。 - -- **M 及以上 ⇒ 默认 `mode:cloud` 单独派卡**(独享容器),⛔ 不混进共享容器批次。 - 裁决理由(维护者同日讨论留档):全程可见、可直接对话干预,价值高于逐卡容器 - 启动的开销 —— 对任何非琐碎的卡这笔账都成立。旧判据清单(`size/l` / `size/xl`; - 全量重生成类,#5837 分片即此形;验证半径跨 3 个以上包的全量测试;dogfood / - 浏览器验证;依赖族升级、全量回归;预计持 heavy-verify 锁超过 ~10 分钟)自此是 - **M+ 类的示例**,不再是触发清单 —— 一条都不命中的 M 卡照样走云卡。 - -## 云卡分流实测背景 - -出处:「Resource limits」M+ 云卡默认 —— 夜班共享容器实测背景全文(#7754 批次的行数预算搬移)。 - -- 实测背景(2026-08-06/07 夜班):9 dev 共享一容器,重验证在 flock 后串行, - 一张重卡(#5837 级,数十分钟级验证管线)拖长**整批**墙钟;把它单容器化, - 批内轻卡不再排它的队,重卡自己也不用和八个邻居分内存。 - -## 云卡授权面第 1 课实测 - -出处:「Dispatch backends」第 1 课 —— 403 细节与参数出处全文(#7754 批次的行数预算搬移)。 - -1. **授权面随 source,不随环境。** trigger 拉起的会话**没有仓库授权** —— - clone(匿名只读)可用,push / 开 PR / 发评论全 403(`not in this - session's authorized repository set`),`permission_mode: auto` 下也没有 - 可弹的授权窗,dev 只能做只读勘察。`create_session` 带 `source_url` 的 - 会话**出生即持推送授权**。同时带 `outcome_branch`(= 认领分支,平台托管 - 推送)与显式 `model`(trigger 流不可指模型 —— sonnet 默认惊吓即此出处)、 - `title`(客户端卡片名 —— **以车道名开头,⛔ 不叫 os-dev**,维护者 - 2026-08-07 拍板:多车道并行时卡片按车道可扫;形如 - `⚡ spec #5599 view 身份前置(裁 B)`,即 `⚡ <车道> #<单号> <短语>`)。 - -## 云卡交付通道误判沿革 - -出处:「Dispatch backends」第 3 课 —— 初版误判与三例实测推翻全文(#7754 批次的行数预算搬移)。 - -3. **交付通道:自开 PR + 订阅唤醒是正道,降级通道只属于 trigger 流。** - 初版条款以为云会话一律没有 GitHub API 工具 —— 对 `create_session` 卡是 - **过度保守的误判**(2026-08-07 下午三例实测推翻:#5599 会话自发 issue - 评论、#5775 会话自立两张 issue、#6243 会话自开 PR #6288),没有工具的只是 - **trigger 拉起**的会话(与第 1 课的 403 同源)。据此分流: - - **create_session 卡(常态)**:派发词要求 dev **自开 draft PR** - (`Fixes #`,正文含验证记录)并把终报以 **issue 评论** - (``)交付;PM 在派发后立即对该 PR(或预期分支的 - PR)挂 `subscribe_pr_activity` —— dev 的完成动作即 webhook,通知延迟从 - 「≤巡检间隔」降到秒级。**例外仍归 PM 代办**:会话未 attach 的姊妹仓 - (源仓之外)依旧够不着 —— 跨仓跟进卡由 PM 代立(#5775 的 objectui - 跟进卡即此形)。 - - **trigger 拉起的会话(定时/重复型)**:维持降级通道 —— 推送 outcome - branch + 终报走报告 ref(空提交信息)或最后一条会话消息,PM 代开 - draft PR、代转录(权限面不因此放大)。附一条实测:报告 ref 用完后 - PM 侧 `push --delete` 会被 git 代理 403(推送授权不含删 ref),清理 - 要走有权限的通道或留给维护者。 - -## fresh-session Routine 连接器约束实测 - -出处:「座位 Routine 化」—— 连接器缺失致静默零产出的 #5474 实测全文(#7754 批次的行数预算搬移)。 - -⛔ **实测运维约束 —— fresh-session Routine 必须带 GitHub 连接器创建。** 经 CCR -**会话内** `create_trigger` 创建的 Routine **不携带** GitHub 连接器(平台限制: -connector grant 只能传递调用会话自身持有的,CCR 平台注入的 github 工具不在其 -列),fired session 因此拿不到 `mcp__github__*` 工具 —— 连自退守卫的第一步(读 -#4604)都执行不了,表现为**静默零产出**。#5474 的分诊座位试点 2026-08-05 正是 -这样失败并回滚的:烟测轮近 50 分钟零标签、零评论、零审计,与创建时平台给出的 -警告完全吻合。因此: - -## 现役座位 Routine 两例 - -出处:「座位 Routine 化」—— 现役两例与管家档位论证全文(#7754 批次的行数预算搬移)。 - -**现役两例(都由维护者从 UI 创建、都先过一轮烟测)。** 首例是**分诊座位**(#5474): -只扫/分类/打标签,⛔ 永不认领。第二例是维护者 2026-08-06 拍板的**三仓队列管家**(锚点 -#5810,座位贴在 `pm:seat` 索引,cron 与分诊错开半个周期),管「入队与落地 B」里入队之后的那一 -半:签名分诊四分支、队列停滞检测、跨仓 pin 链观测(含 #6162 的机械立单,见「入队与 -落地 B」)。**档位按职责挑,不按重要性挑** —— -管家的正确性主要来自**查表**(#5810 的签名台账 + 座位贴说明段,两者都优先于它的现场 -判断)与**机械兜底**(每轮限量、双向让行、只守落地的授权面),判断面窄、判例法已写死, -因此**不需要最强档**;吃最强档的是要现场设计取舍的执行座位。档位与 cron 一样是维护者 -在 UI 上的可调项(上一条),试点判据不达标即升档 —— 本文 ⛔ 不复制其当前值,它的 -`pm:seat` 座位贴才是现状。 -## #6806 target 拆分标本 - -出处:发版板「拆分 / 分票时 `target:*` 随工作走」—— #6806 两面标本全文(#7754 批次的行数预算搬移)。 - -- **拆分 / 分票时 `target:*` 随工作走,不随票号留 —— 对每一半重跑一遍上面那条 - 二元判据。** #6806 是 `target:v17` 的 #5495 拆出的引擎侧残余:拆分把**工作**移了 - 出去,发版目标却留在原地,于是板上同时有**一张不会动的卡**(父单的剩余范围已被 - 裁定 parked)和**一张看不见的卡**(真正在兑现 v17 义务的那一半)。查 - `label:target:v17` 的人两头都读错,而两个错误方向相反、互相掩盖。默认是**继承** - (义务跟着工作走);判定某一半不该继承时,**把理由按上面四类阻塞写在那张卡上** - (可证伪),⛔ 不默认不带。#6806 正是两面的标本:它正文里写过「why `pm:queue` - without `target:v17`」的理由,该理由后来被重判、标签补上,两张卡今天都带 - `target:v17` —— 写下的理由会被复核,不写的默认不会。生产者不变(分诊座位 / - objectui 整仓座位),拆分本来就是它做的,本条只是给它加一个必答项。 - -## 发版清单三条查询论证 - -出处:发版板「消费者三处」—— 三条查询的结构性论证与 cloud#1222 实测全文(#7754 批次的行数预算搬移)。 - -- **消费者三处**(a label exists iff something reads it):维护者的发版清单 = - **三条查询**(与 `pm:seat` 状态板同构,标签即看板;维护者 2026-08-11 裁定, - #7493,取代此前的两条)—— - `repo:objectstack-ai/objectstack label:target: is:open`、 - `repo:objectstack-ai/objectui label:target: is:open` 与 - `repo:objectstack-ai/cloud label:target: is:open`;等价写法是一条 - org 级搜索 `org:objectstack-ai label:target: is:open`(GitHub 全局 - 搜索页支持 `org:`,仓内 issue 列表页不支持 —— 所以三条查询是随处可用的那个 - 写法,org 级只是省两次切换)。 - ⛔ **「cloud 出现命中即误标信号」旧条款已废除(#7493)** —— 它教读者把一张 - 正确带标的迁移卡当误标清理,等于销毁真实阻塞的唯一证据。实测代价:cloud#1222 - (自 #5852 按 #7167 迁入,带 `pm:queue` + `target:v17`)在两条查询的旧口径下 - **对整个板隐形 ~10 小时**,板读作「v17 clear」而 v17 阻塞卡开着。 - step 3 批次选择板上项优先;step 9 轮次报告第四健康指标 = **三张板之和**, - 「归零 = 可发版」指三张都空,单看 objectstack 归零不是可发版。 - **为什么是三条,而不是一条加过渡态**:rule 1 自 #7165 起是 file-at-destination - —— 落点在 objectui / cloud 的执行卡就**长在**那个仓,它的 `target:` 由 - 该仓整仓座位在它自己的仓里生产(见上面「每个 backlog 恰好一个生产者」)。 - 那两条查询因此是这套所有权模型的**结构性后果**,不是存量迁移的残留、 - 也不会随哪一次清仓消失;少读任何一条的人**按设计**漏掉那一边, - 而漏掉的读数看上去和「板已清空」完全一样。 - -## 发版前置条件由来 - -出处:发版板「发版时刻 = 清板」条 —— #7268 缺口标本、#6162 判据辨析与 #7275 裁决全文(#7754 批次的行数预算搬移)。 - -- **发版时刻 = 清板,不是重扫**:板上每条三选一 —— 修掉 / 摘牌(不再成立)/ - **明示接受带病发布**(摘标签 + 一句 accepted-for-GA 评论留痕,进 release - notes 的 known issues)。姊妹仓同标签:objectui **在自己仓里上板**,生产者是 - objectui 整仓座位(见上),修复经 console bundle 随 pin bump 进这次发布; - cloud 同样**在自己仓里上板**(#7493;它独立部署,修复不随 console bundle 走, - 但板上项照样参与「归零 = 可发版」的读数)。⇒ 发版时刻的清单因此是**三条查询** - (口径见上面「消费者三处」),**三张板都要清到空**;三选一对三张板**逐条** - 适用,⛔ 不因为「那是前端仓 / 独立部署」就整批默认接受 —— objectui / cloud - 板上项的三选一由各自整仓座位执行,读数回贴给发版清单。 - ⚠️ **「随 pin bump 进这次发布」是机制事实,不是自动的流程保证 —— 流程半边由 - 下面的发版前置条件补上(#6906 交付项 2 查出缺口、另立 #7275 裁决)。** - 队列管家的 #6162 机械产出(见「入队与落地」B)判据是**窗口收口** - (`.objectui-sha` 落后 objectui main **且** objectui 合并队列已空),不是发版 - 时刻;而且它立的是 `pm:queue` 单,**按构造不带 `target:`** —— 那张 - bump 单因此既不在上面三条查询里,也没有对应的「明示接受」摘牌形态(#7268 是 - 2026-08-10 的实测标本:`pm:queue` 独一份)。发版**记录**另有硬门兜底 - (`check:objectui-pin-fresh` —— 发版 PR 上 required、发布路径上 enforcing, - #3340 / #6170),所以陈旧 pin **发不出去**;缺的是发版时刻那张单或那次豁免, - 由本条补上: - **发版前置条件(维护者 2026-08-10 拍板,#7275 Option A)**:清板动手之前, - 先取**一次** pin 读数(`.objectui-sha` 对 objectui main;⛔ 一次即止,不是 - 重扫)。pin 滞后 ⇒ console bump 单必须**已存在且已上板**(`target:`; - 上板由已拥有该标签生产权的座位执行 —— bump 单立在 objectstack,即分诊座位, - 单一生产者纪律不因此多一个写者),或按上面的标准形态**明示接受** - (accepted-for-GA 评论留痕)。两者都不成立 ⇒ 不 cut。这条前置就是「板已清空」 - 与「console bump 已就位」之间唯一的机械关联 —— 跳过读数就回到 #7268 那种 - 板上看不见 pin 滞后的无声状态。 - -## 候选评论全读两例 - -出处:step 1「Read each candidate's full body and its comments」—— #4075 / #4829 两例全文(#7754 批次的行数预算搬移)。 - -before the issue can even be a candidate.** A comment may record that half -the work already shipped (#4075's step 1 had been merged for three days; -the claim went out without reading the comment that said so). Comments are -also where **rulings** land, not just progress notes: #4829's body reads as -a straightforward "delete the access gate" fix, while its thread held the -maintainer's 2026-08-03 暂缓处理 verdict AND the recorded finding that the -gate is ADR-0045 §3 (Accepted) mechanism with four pin tests. A PM that -read only the body recommended deleting an accepted ADR's mechanism and -dispatched it — only the dev's stop-and-refuse prevented the patch (the -maintainer later re-decided on the corrected analysis; that is the process -working *despite* the skipped read, not because of it). 裁决落在评论区, -跳过评论就是跳过裁决。Triage, batch selection (steps 2–3) and the dispatch -prompt all need the full picture. - -## no-producer 一日五中 - -出处:step 2「Recognize the "no producer" shape」—— 一日五中全文(#7754 批次的行数预算搬移)。 - -**Recognize the "no producer" shape —「生产者在哪?」is a standing triage -question.** One issue class is invisible to every automated check: a field is -declared, consumers read it, types and gates are fully green — and **no code -path ever writes it**. Five hits in one day: #4704 (`Seed.env`, six call sites -drop it), #4837 (the liveness ledger's own criterion), #4839 (`session.roles` -written nowhere in the repo), #4862 (flow triggers bulk-set `previous` without -binding it), #4867. Type systems and lint validate the **consumer** side only, -so a missing 生产者 survives indefinitely under a green tree. On any issue -shaped `declared ≠ enforced`, ask where the producer is before routing it — -the answer is usually the root cause, and it changes the issue's scope (and -often its `domain:*` label) *before* dispatch rather than in the dev's report. - -## 同文件延后记坑标本 - -出处:step 3「Same-file issues serialize strictly across rounds」—— #4820/#4821 标本全文(#7754 批次的行数预算搬移)。 - -**Same-file issues serialize strictly across rounds — and deferring is not -shelving.** Two issues on one file ride in different rounds, no exception -(#4820/#4821). The part that is easy to miss: while #4820 was in flight its dev -established that the fix #4821's body proposes (a `JSON.stringify` key) would -change type-coercion semantics and introduce a fresh silent defect. That -warning **and** the `Blocked-by:` line were written onto #4821 in the same round -#4820's review closed — not the next one. A deferred issue sits in the queue -looking dispatchable to every sweep, including another PM's; whatever you -learned about it is worthless until it is on the issue. Rule: when step 3 pushes -an issue to a later round, record the known trap on it before the round ends. - -## 扇出标签已议已拒论证 - -出处:step 3「解锁扇出优先」—— 不发明新标签的论证与 #7498 第二裁全文(#7754 批次的行数预算搬移)。 - -- ⛔ **不要为此发明新标签**。`pm:blocking` 之类需要一个生产者,而没有读者的标签 - 必然烂(「a label exists iff something reads it」)。扇出可以从协议已经在维护的 - 数据里推出来,推它就只有一个真相源,也就不会漂移;顺带把激励摆正了 —— 解别人 - 锁的活先做,吞吐是复利。**已议已拒,留档给后来者(维护者 2026-08-11,#7498 - 第二裁)**:给被依赖卡打一揽子 `priority:*` / `pm:unblocks` 标签的方案被否 —— - 「被依赖」是推导出来、会随上游关单衰变的属性(正是 `target:v17` 裁决拒绝过的 - 渐变烂形状);无读者的标签被状态模型禁止;发版关键链已由 `target:v17` 的 - contract-first 传播规则覆盖。`pm:unblocks` 变体仅当被依赖卡群远超今日 ~6 张时 - 重议。 - -## 让行交接 PM 侧读数 - -出处:step 4「Yielding is a handoff」—— #5032 / #7145 交接读数全文(#7754 批次的行数预算搬移)。 - - **Yielding is a handoff, not an exit.** The loser posts, together with - its 让行 comment, everything it already diagnosed — repro commands, - dependency paths, traps confirmed (#5032's handoff was consumed directly - by the winning dev, whose PR #5052 was up within half an hour — details - in the same incidents section). A yield that discards its diagnosis - re-bills the whole investigation to the winner. - The PM-side readings are the same duty (maintainer-accepted 2026-08-11, - #7518 item 4; measured on #7145 — two same-account sessions claimed 69 - seconds apart, the assignee field read as "mine" to both, and the claim - comments' timestamps + session IDs were the only tiebreaker): the loser's - yield comment also hands over the **board readings it already took** — - in-flight same-file PRs, region declarations, serial constraints cleared — - so the winner does not re-scan. This is the PM-to-PM form of CLAUDE.md's - re-read-the-comments rule. -## pin 滞后 cloud#1116 标本 - -出处:多仓协调 rule 2「Pin 滞后」—— cloud#1116 分叉窗口标本全文(#7754 批次的行数预算搬移)。 - -**Pin 滞后 ——「上游已合入」不等于「本仓已看见」(rule 2 的盲区)。** rule 2 只要求 -`Blocked-by:` 的上游**已合并**;姊妹仓消费 framework 时还有第二个读数 —— 本仓的 pin -是否已覆盖那个 commit。cloud#1116 的裁决来自 framework #5347、落地于 framework #5368 -(`9c5abf4e9`),而 cloud 的 `.objectstack-sha` 停在 `586d6f701a16`,`9c5abf4e9` -**不是它的祖先**(立单时 framework main 领先 pin 87 个 commit)。于是 cloud#1117 合入 -后到下一次 pin bump 之前,同一个 `TursoDriver` 仍有分叉窗口,只是**方向反了**:remote -抛 400,local(继承 `SqlDriver`)仍编译 `IS NULL` —— fail-closed 的一侧先到,不是新洞, -pin 前移即自动收敛。规程两条: - -- **派发前核祖先关系**(REST `repos///compare/...` 的 `status` - / `ahead_by`)—— 本地 `merge-base --is-ancestor` 在 shallow 检出上解不出 pin 的 - commit、以 `fatal:` 退出,而它在 `&&` / `||` 链里会被读成「不是祖先」,正是 - Operational notes 6 那类假读数; -- 未覆盖 ⇒ 派发令要求 dev 在 **PR 正文留档分叉窗口与方向**,且 ⛔ **pin bump 不做 - rider**:`.objectstack-sha` 是共享文件、要走 `scripts/bump-objectstack.sh`(连带 hono - override 与 lockfile 重生),塞进这一单会把一个独立的、必冲突的改动变成 rider。 - -滞后**本身**已有读数,不必自己算:cloud 的 `scripts/check-pin-staleness.sh` -(`pnpm check:pin-staleness`,test.yml 里以 `continue-on-error` 跑)每次 CI 都报两个 pin -各落后 main 多少 commit。但它是**有意的 advisory**(不设阈值,`--max-behind N` 需显式 -传 —— pin bump 是深思熟虑的动作),且它回答的是「落后多少」,**不是**「是否覆盖我这条 -裁决 commit」;后者只有派发前那一次祖先判断能回答。 - -## rule 4 双射动因 - -出处:多仓协调 rule 4 —— 纵向拆分的动因论证全文(#7754 批次的行数预算搬移)。 - -**4. 纵向拆分:一个分诊 PM + N 个执行 PM,一人一车道双射**(维护者 -2026-08-05 拍板,#5472)。The claim protocol makes concurrent PMs *safe*, not -*useful* on its own: batch independence (file-disjointness) is only ever -checked inside one PM's own view, so two PMs on the same queue can claim -issues that collide on shared files — and「谁来分诊」原本是每个 PM 各做一遍的 -重复劳动。objectstack 是最大的仓,单个 PM 的认知吞吐不够,同仓多 PM 必须保留; -所以把协调税**降为结构性防撞**,而不是靠自由文本申报互相躲。角色**纵向**拆开, -所有权是**双射**: - -## 热文件串行队实测 - -出处:「座位贴协议」热文件串行队 —— domain:metadata 席一文件五卡实测全文(#7754 批次的行数预算搬移)。 - -- **热文件串行队 —— 正文里给它一个具名段。** 批次独立性(step 3)是**每轮**现算的, - 但一个热文件的**排队顺序**是**常设事实**:它跨轮、跨班次存在,接任者必须能直接 - 读到,而不是从一堆认领评论里重新推。段内三列:**文件 → 有序卡片清单 → 每张卡认领 - 的是哪个区域**。实测:`domain:metadata` 席一个任期里, - `packages/metadata-protocol/src/protocol.ts` 一个文件背了**五**张卡 - (#6215 → #6190 → #6563 → #6479 → #5079),另有别的座位的 #5839 认领压在第三个区域上; - 该席临时用了这么一段,正是它让**连续三次同文件派发零碰撞**(#6644)。区域列是关键 - —— 同一文件的不相交区域可以并行,写清楚才敢并行,写不清楚就只能整文件串行。 - -## engine 拆分沿革 - -出处:「Domain lanes」engine 一分为二 —— 拆分动因与迁移纪律全文(#7754 批次的行数预算搬移)。 - -**`engine` 一分为二(#5472,与 #5095 同批)。** 旧 `domain:engine` 同时覆盖 -objectql + metadata\* + platform-objects + core + formula + 全部 `driver-*`, -在双射之下**一个 PM 吃不下**(它是全仓最大的一块,且 driver 族的落地节律与 -查询/元数据核心完全不同)。切分线就是上表:**`engine-core` = 编译/查询/元数据 -核心**,**`drivers` = 存储后端适配层**。两条配套纪律: - -- **迁移**:存量带 `domain:engine` 的 open issue 由**分诊座位**按落点逐条改标为 - `engine-core` / `drivers`,清零后删除旧标签 —— 双射要求「域 X 谁管」有唯一 - 答案,一个仍在流通的旧标签就是一个无主车道。 -- **座位贴同批新立**:`domain:engine` 那一个座位一分为二,各自的范围段 - 照抄上表(维护者 2026-08-05 对 `driver-memory` / `driver-mongodb` 族的投入 - 冻结指令锚在 `drivers` 那一行,`formula` / `driver-sql` 不受影响)。 - -## spec 拆分沿革 - -出处:「Domain lanes」spec 一分为二 —— 拆分动因全文(#7754 批次的行数预算搬移)。 - -**`spec` 一分为二(维护者 2026-08-07 批准,座位贴 #6298)。** 旧 `domain:spec` -同时覆盖「改接受面」与「改契约自述文本」两类节律完全不同的活:前者量小、 -风险高、要吃版本窗口裁决;后者量大、机械、天然适合 sweep 打包 —— 混在一席, -文本债持续积压(truth-sweep 审计开采一天可灌 5-10 张)。切分纪律: - -## metadata 拆分沿革 - -出处:「Domain lanes」engine-core 再拆 metadata —— 拆分动因与迁移纪律全文(#7754 批次的行数预算搬移)。 - -**`engine-core` 再拆 `metadata`(维护者 2026-08-07 拍板,座位贴 #6367)。** -首拆后的 engine-core 仍是全仓最大、增长最快的车道:编译/查询核心与元数据 -机制(service / registry / directory + 内置平台对象)的落地节律不同 —— 前者 -深、串行、常挂 ★,后者以机制修缮与观测型 finding 为主,天然可并行。二次 -切分仍按包边界(anchoring rule 无例外,与 spec 拆分不同):**`engine-core` = -编译/查询核心**(objectql / core / formula / plugin-pinyin-search), -**`metadata` = 元数据机制与内置对象**(`packages/metadata*`、 -`packages/platform-objects`)。配套纪律: - -- **红线**:改变元数据**格式/接受面**的卡照旧归 `domain:spec`(协议席,判据 - 「合法集合变没变」,#6245/#6235 先例);`/meta` HTTP 路由本体在 - `packages/rest`,归 `domain:cli` —— `metadata` 席只吃 engine 侧机制。跨半边 - 的卡按主要落点判,拿不准 FLAG 回分诊。 -- **迁移**:存量带 `domain:engine-core` 的 open issue 由**分诊座位**按落点 - 逐条改标(只读分类审计留证,逐卡迁移评论);已在飞(`pm:dispatched`)的 - **改标不改辖** —— 标签随分类走,收尾与复核仍归原认领会话(先例 #6298 - 说明段)。 -- **座位贴新立**:#6367,范围段照抄上表;母席 #6019 范围随表收缩,由其在任 - PM 自行更新正文(单写手规则),分诊座位只留知会评论。 -## sanitizer 写侧就地删除三例 - -出处:Operational notes 12 —— 写侧三例表格与判据推导全文(#7754 批次的行数预算搬移)。 - -**12. 判「正文被 sanitizer 截断」必须双读取 —— 单一读法的尾部缺失先算读取端截断。** -(三例误判各停摆 1–2 天、外加三张假工单;实录已移 `references/incidents.md` -§「读取端截断三例误判」。)两句纪律: - -- 判截断前必须**双读取**,`body_html` 要带 full 媒体类型才拿得到: - - ```bash - curl -s "https://api.github.com/repos///issues/" \ - -H 'Accept: application/vnd.github.full+json' # .body 原文 + .body_html 渲染版 - ``` - - **两者在同一处断掉**才算 issue 端截断;任何单一读法的尾部缺失都先假定是读取端 - 截断(工具输出上限、分页、`[:N]` 切片)。这与 notes 6「零命中必须用一个确定存在 - 的邻近词反查」是同一条纪律的另一半 —— **缺失类读数在下结论前都要先证伪「扫描器 - 坏了」这个解释**。 -- step 0 的 **Repair first** 是**停摆指令**,成本由作者承担,所以它的判据必须比 - 其它分类更硬:误判一次的代价是一条可入队缺陷躺一天,外加一条打给作者的假工单。 - 已发出的重贴指令若事后证伪,**要在同一处公开作废**(同 notes 7:诊断结论一旦 - 公开发出又被推翻,更正要发在同样公开的位置)。 -- **同一个 sanitizer 的第二种形状 —— 写侧的「就地删除」,上面两条的判据抓不到它, - 反引号也不保护。** 上面两条管的是**读侧**误判(把读取端截断当成 issue 端截断), - ⚠️ 一字不改、依旧成立;这一条是新增的**另一种失效形态**,不是对它的修正:短的 - `<…>` 片段在**写入时**被就地删掉,正文其余部分完好无损 —— 没有「断掉的位置」, - 所以「两者在同一处断掉」这个判据在它身上恒假,双读取会一致地告诉你「正文完整」, - 而它确实完整,只是少了几个片段。2026-08-07 `domain:spec-surface` 席在座位贴的 - 交接台账上写后回读实测三例,三例都在反引号里、三例都被吃掉: - - | 写入 | 存回 | - |---|---| - | `` | (整段变成空) | - | `expected to be 19` | `expected to be 19` | - | `git log -- ` | `git log -- ` | - - 第一例的代价:被吃掉的标记是在飞 dev 报告的**全部收集路径**(step 6 - `mode:cloud` 的收集判据)—— 只有写后回读抓到了它。两条动作: - - - 正文里凡要保留字面尖括号,一律写 HTML 实体 `<` / `>`,⛔ 不靠反引号或 - 围栏 —— 实测它们不提供保护; - - 含 HTML 注释标记(如 ``)、`` / `` / `` - 一类占位符、泛型参数的正文,**写后回读逐个确认这些片段仍在**,这是动作不是提醒。 - label discipline 的「写后回读」是同一条纪律的上位(#5885 那两次「sanitizer 吞 - 内容」即本形态),本条给的是它的**具体形状与判据**:失效完全静默 —— API 返回 - 成功,渲染页看不出缺口,只有把存回的正文与你写的原文逐段对比才看得见。 - -## auto-merge 空字段返回正反实测 - -出处:Operational notes 21 —— 正反 3+3 例与对照组实测全文(#7754 批次的行数预算搬移)。 - -**21. `enable_pr_auto_merge` 的空字段返回(`method: , enabled at `,对照正常形态 -`method: MERGE, enabled at <时间戳>`)对「入没入队」零区分度 —— 以队列读数为准, -只在成员资格确实缺席时翻转一次。**(处方并入 notes 1 的读数纪律:维护者 -2026-08-11 裁定,#7492;吸收 #7518 第 2 课的第二时序形态。)identity 车道 -2026-08-06/07 三例首测(#6207)之后,后续班次对全绿 PR 测得**反例 3/3**: - -- **原三例**(#6034/#6092/#6197,checks 已全绿、`mergeable_state: clean`):空字段 - 返回,auto-merge 确实被武装(随后 `disable` 能成功返回,反证武装生效),但 PR - 不入队 —— 无 `added_to_merge_queue` 事件;翻转一次后入队落地(#6034 识别出签名 - 前静默停摆约 1 小时)。 -- **反例 3/3**(#7506/#7508/#7605,同样全绿):同一个空字段返回,PR **照常立即 - 入队**(`added_to_merge_queue` webhook 秒级到达),未翻转、首过落地。#7446 还 - 测得 ready-flip 后同形:空收据 + 立即入队(#7518 第 2 课)。 -- ⇒ **签名本身不构成任何方向的证据**;有信息量的只有队列读数。**处方**:先按 - notes 1 读成员资格(timeline 事件;队列分支**正命中**亦充分,注意 - `max_entries_to_build` 截断 —— 分支缺席单独不充分),成员资格**确实缺席**才 - `disable` → `enable` 翻转一次,翻转后仍以 notes 1 判据验证。⛔ 对已入队的 PR - 补一记 `disable` **不解除队列成员资格**(只有转 draft 才解除,见 notes 1 / - Guardrails 的撤回机制)、PR 照常落地,但**会清掉 auto-merge 旗** —— 此后它 - 若被队列踢出将不会自动重挂(#7446 与 #7506 各实测一半)。 -- **对照组**:对 checks 还在跑(`blocked`)的 PR 调同一工具,返回完整字段、行为 - 正常(绿后自动入队)—— #6086 / #6067 / #6107。工具行为在平台侧,仓内能做的 - 就是把读数纪律钉在这里,免得每个新 PM 会话重踩一遍。 -## #5536 机会主义重启条件标本 - -出处:State model label discipline —— #5536 四次命中无人接的标本全文(#7754 批次的行数预算搬移)。 - -- **机会主义重启条件必须点名触发文件,复查挂在所属车道的派发动作上(维护者 - 2026-08-11 接受,#7518 第 3 课)。** 「下一个碰这几个文件的 PR 顺手带上」这类 - 条件在命中那一刻**没有读者** —— 路过的人在文件里,不知道卡存在;#5536 的条件 ② - 已四次命中无人接(#6042、`e2798fa`、`d538647fc`、`06be54ec3`,其中两次同日)。 - 处方两半:持有的 finding 把**触发文件清单**写进 hold 评论;所属车道的座位贴设 - 「派发前必查」段 —— 凡派发的卡文件面与清单相交,派发令点名该 finding、把顺手活 - 列为**申报过的 out-of-surface 增项**。检查挂在**派发动作**上才有读者;写进 - 座位贴才活得过交接(#5536 本班已照此落地:座位贴 #6021 + 卡上公示)。 - -## 域标签创建段由来 - -出处:One-time setup「为什么这一段以前不存在」—— 词表承重而无创建者的沿革全文(#7754 批次的行数预算搬移)。 - -⚠️ **为什么这一段以前不存在,以及为什么它不是可选的。** 本块此前**一个 `domain:*` -都不创建**,只创建 `pm:*` / `finding` / `needs-user-decision` / `repo:*`;而同文的 -label discipline 又规定「未打标签的 issue 任何人都不得认领」,`domain:*` 是分诊座位 -**唯一生产的机器判据**。也就是说这套词表是**承重的,却没有任何一处可执行文本负责 -把它创建出来** —— 现存的域标签全部是历史上手工点出来的,词表与实际标签集之间没有 -任何机械对账,两边各自漂移(#5472 施工现场记录,归挂 #5469)。#5469 发现的两个 -未入表条目就是这个缺口的产物,不是谁一时疏忽。 - -## 合并队列成员资格判据实测 - -出处:Operational notes 1 —— 6/6 实测、批次链读法与 #4852 空转全文(#7754 批次的行数预算搬移)。 - -**1. 判断 PR 是否在合并队列,看 `added_to_merge_queue` timeline 事件,不看 -`auto_merge` 字段。**(成员资格判据修订:维护者 2026-08-11 裁定,#7492。原判据 -`gh-readonly-queue/*` 分支**充分而不必要** —— 队列满载时 PR 已入队而分支尚未建出, -按旧判据读出「没入队」的假阴性,据此重投就是重投一张已在队列里的 PR。timeline -事件判据 identity 车道一班 6/6 实测:#7333/#7346/#7389/#7400/#7449/#7471, -含一轮分支判据会答错的。)本仓 PR 入队后,REST 返回的 `auto_merge` 回落为 off -(队列条目取代了挂起的 auto-merge),该字段对「在不在队列里」零信息量,据它反推 -会得出「没入队,再入一次」的错误结论。成员资格判据: - -``` -GET /repos/{owner}/{repo}/issues/{pr}/timeline → event == "added_to_merge_queue" -``` - -队列分支读法**降级为读批次位置专用**(排序只有那里可见,⛔ 不再作成员资格判据): -`git ls-remote --heads origin 'refs/heads/gh-readonly-queue/*'`,分支名里带着这一批 -被打包的 PR 号,base sha **串成链**(`pr-4878-<链上一条的结果 sha>`),顺着链读得出 -自己排第几;**正命中仍然是「已入队」的充分证据**,只有「无匹配分支 ⇒ 没入队」这个 -反向推断作废。 - -**成功序列规则(同一裁决)——「读间隔,不读事件名」**:`removed_from_merge_queue` -之后 **~1 秒内**跟着 `merged` 是**落地**,不是被踢(6/6 落地全是这个形状:#7346 -08:12:08Z→08:12:09Z、#7333 08:27:50Z→08:27:51Z);真被踢的形状是 -`removed_from_merge_queue` 之后**没有** `merged`、几分钟后 PR 仍 `open`(#7333 -07:56:20Z 一度被误读为踢出,靠时间差纠正)。两个结局共用同一个事件名,不读时间差 -就会把落地报成弹出、或对着真弹出干等 —— 两个方向的错各自都发生过。另有两点在同一 -处咬过人: - -- **「判据不在 `origin/main` 上」是个二义读数。** 它同时兼容「在队列里等」和「压根 - 没入队」,而两者的处置完全相反(前者等,后者要动手)。#4852 的 auto-merge 从 10:15 - 就挂着,每轮只查 main、判为「排队中」,实际它因 CI 红从未入队 —— 空转 **100 分钟**。 - 落地检查永远是**两个读数**:队列成员资格 **和** `origin/main`,缺一不可。 -- **PR 被转回 draft 会同时掉 auto-merge 与队列成员资格**,且不会自动恢复;转正之后 - 必须重新挂。 - -## GraphQL 配额规程全文 - -出处:Operational notes 3 —— 配额规程与定点文本纪律的原始全文(#7754 批次的行数预算搬移)。 - -**3. GitHub MCP 的 GraphQL 配额(5000/时)极易打满,读操作与评论一律走 REST。** -今天三次归零(峰值 10402/5000),每次卡死的都是 `enable_pr_auto_merge`、draft 状态 -切换、`list_issues` 这几个 GraphQL-only 操作 —— 配额一空,整个循环停在复核与入队上。 -规程三条: - -- 读与评论优先 `curl` / `gh api` 走 REST(core 配额 15000/时,与 GraphQL **独立计**), - 只有确实没有 REST 对应物的写操作才花 GraphQL 配额; -- 配额打满时把 GraphQL 写操作**排队而不是重试**,后台轮询 `rate_limit` 的 - `resources.graphql.remaining`,恢复即执行: - - ```bash - gh api rate_limit --jq '.resources.graphql' # 或 curl https://api.github.com/rate_limit - ``` - -- 复核意见不等配额 —— 先用 REST 评论把结论发出去,入队、切 ready 这类 GraphQL 动作 - 事后补;维护者拿到的信息不该被配额延迟。 - -配额期的**动作交接**四条(services 车道一班六次配额耗尽的沉淀,#5885;含一次 -「读成功写被拒」卡在转 ready 半途 —— 读写配额独立,写被拒不代表读也死了,反之 -亦然): - -- 被配额挡下的动作,把**完整待执行状态写进 send_later 定点文本**(哪个 PR、哪个 - 动作、判据是什么)—— 幂等、抗上下文丢失,恢复后照文本执行,不靠会话记忆; -- 重试用 **10–12 分钟阶梯定点**至成功,⛔ 绝不忙轮询;REST core 配额是**整点 - 重置**,对齐 `:00` 重试优于指数退避(实测一次盲退避白等半个窗口); -- search 与 core 是**独立配额**,一侧打满时另一侧可作退路(用 search 拿清单、 - 用 core 读详情,或反之); -- REST core(15000/时)在共享身份下**同样会打满** —— 本条的三份判据(rate_limit - 读数、整点重置、独立计费)对它一体适用,别把「走 REST」读成「不限量」。 - -**定点文本的写法纪律 —— 已删除的定时器仍会投递,且投递时文本可能已落后现实数轮。** -上面第一条让定点文本**完整**(带全待执行状态),这一条让它**过期时仍然安全**;两条 -是同一枪的两面,都成立才够用。2026-08-07 跨两个座位三次实测,两种形态、同一个后果 -—— 已删定时器照样投递×2、未删但被现实追上×1(两例实录见 -`references/incidents.md` §「定点文本两例实录」)。 - -两条硬规则: - -- **每一枪定点文本必须以「幂等 —— 动手前先重读状态」开头**,⛔ 不得包含未经重读 - 即可执行的祈使句。三次都没出事的唯一原因就是这句在文本里、且重读**真的被执行**; - 定时器一旦投递,平台侧没有任何东西会替你复核它的前提 —— 把重读写进文本是**唯一** - 能让过期指令失效的机制。 -- 文本只许描述**判据**(「若 X 则 Y」),⛔ 不许描述**结论**(「现在去做 Y」)。 - 「⇒ 重新派发」「⇒ 判为不可靠」「⇒ 打回不 arm」这类祈使句正是要禁的形态:它们在 - 写下的那一刻可能是对的,投递时未必还是,而祈使句把「判据可能已变」这件事从文本里 - 抹掉了 —— 判据句自带复核,结论句把复核外包给了一个已经不在场的自己。 - -本条的落点在 step 6(巡检定点)与 step 7(flip 定点)各有一条同款约束,写法一致。 - -## 读数纪律四例明细 - -出处:Operational notes 6 —— 四条错读数的实测明细全文(#7754 批次的行数预算搬移)。 - -**6. 读数纪律 —— 四条各自产出过一个「我信了并据此行动」的错读数。** 第 4 条管的是 -「在哪棵树上读」,这一条管的是「命令本身是否在回答你以为的那个问题」。 - -- **`cd X && cmd` 会短路。** Bash 工具每次调用 cwd 重置;`cd /home/user/objectui && - git grep ...` 在路径不存在时 `cd` 失败、整条命令继续,于是**在当前仓里执行**,产出 - 假的「objectui 零消费方」。⛔ 跨仓一律 `git -C grep`,不要用 `cd`。 -- **`git grep -c | wc -l` 数的是文件数,不是命中数。** 曾据此得出「分支比 main - 命中更多」的荒谬结论。要命中数就不要再套 `wc -l`。 -- **裸名 grep 会被幸存家族当子串命中。** 核验 `system/EmailTemplate` 是否已退役时, - 裸名命中的是仍然活着的 `EmailTemplateDefinition` 一族。退役核验一律**带引号精确 - 名**;更硬的判据是查**声明式**(`^(export )?(const|type|interface) \b`)而不是 - 查提及 —— 注释、pin 测试的断言词、迁移散文里出现该名是**正常且应当的**。 -- **浅检出(shallow clone)上的历史读数不可信 —— 一个假「非祖先」加两个被截断的数。** - 队列管家核跨仓 pin 链时实测:`git merge-base --is-ancestor origin/main` 以 - **exit 1** 退出(直接读作「不是祖先」)、`git rev-list --count ..origin/main` - 给出被浅历史截断的值(实测 50)、`git branch -r --contains ` **零输出**; - `git fetch origin main --deepen=` 之后同样三条给出 exit 0、79、有输出。⇒ 跨仓 - pin 核验先 deepen 再判,或直接走 REST `compare`(多仓协调 rule 2 第一条同源,论证 - 不重复 —— 那里讲的是 `fatal:` 退出在 `&&` 链里被读成「不是祖先」,这一条讲它还能 - 不报错地给出一个**看起来正常的错数字**)。 - -统一原则:**零命中必须用一个「确定存在的邻近词」反查**,证伪「扫描器坏了 / 路径错了」 -这个解释。没有这个反查,零命中不成立。 - -## #4845 误诊更正实录 - -出处:Operational notes 7 —— 2026-08-03 OOM 误诊三重错误全文(#7754 批次的行数预算搬移)。 - -**7. CI 红了先拿完整日志归档,再下结论 —— 三条读日志的纪律。** 这是 2026-08-03 当天 -最贵的错误:公开断定四次 CI 红是**内核 OOM-killer 杀掉 DTS 构建**,据此开了 PR #4853, -然后被 #4853 自己的 CI 推翻(它挂着新参数跑,红得一模一样)。真因是 #4796 那一族的 -5000ms 超时,由 #4856 修掉。完整更正见 #4845。三个叠加的错误各成一条: - -- **「completeness check 绿」≠「测试通过」。** `check-test-completeness.mjs` 只断言 - 没有 worker 静默死掉;workflow 自己的注释写着 *"A red suite plus a GREEN - completeness check means real test failures"*。 -- **turbo 并发输出的「相邻」≠「因果」。** `test` 的 `dependsOn` 只有 `["^build"]` - (只含上游),`packages/spec` 没有 `pretest`,所以 `--concurrency=4` 下 `spec#build` - 与 `spec#test` 同时在跑,GitHub 又给整组打同一个时间戳。`X start` 紧接着 - `ELIFECYCLE` 完全可能来自两个无关进程。**先查 `turbo.json` 的依赖边**,再谈因果。 -- **不要只看日志 tail。** 那次的 ~10 KB 尾巴被 `gen:schema` 的 1675 行清单吃光,真正 - 的失败行根本不在里面。取完整日志归档再判。 - -诊断结论一旦公开发出又被推翻,**更正要发在同样公开的位置**,并把据它开的 PR 撤回 -draft、解绑 `Fixes`,免得一个错结论继续被当作已立案的事实引用。 - -## #7655 armed 窗口 disable 撤单实测 - -出处:Operational notes 21 —— 2026-08-11 班次乱序 webhook 正反两例(#7755 的 notes-21 修订叙事)。 - -notes 21 旧处方「成员资格确实缺席才翻转一次」在 2026-08-11 班次测出**处方内部的时序 -缺陷**:GitHub 的「added to merge queue」webhook 可能**乱序迟到**,在首次空字段 -enable 已经真实入队**之后**才到达 —— 于是「缺席」读数是假的,打进 armed 窗口的 -`disable` 会把真实入队撤掉。 - -- **反例 #7655**:enable → 空字段返回 → `disable`(以为没入队)→ 乱序的 - 「added to queue」通知到达 → 队列分支消失 —— 真实入队被那记 disable 撤销, - 多付一整圈翻转才落地。 -- **正例 #7657(同日)**:按修订序列 —— 空字段后先 - `git ls-remote --heads origin 'refs/heads/gh-readonly-queue/*'` 验队列分支 - (给条目 ~20–30s 建出),分支在即收手 —— 零摩擦首过落地。 - -修订以 amend-in-place 并入 notes 21 现行序列(取代旧处方,不另立竞争条目); -「⛔ enable 与它的队列验证之间永不插 disable」是 #7655 的直接产物。与 -§「auto-merge 空字段返回正反实测」的关系:该节讲**签名零区分度**(读什么), -本节讲**读数与动作的时序**(什么时候动手)。 - -## 跨会话消息 roster 限制实测 - -出处:Dispatch backends「跨会话消息面」段 —— 平台限制读数与维护者裁定(#7755,2026-08-11)。 - -- CCR 创建的云会话**不注册**跨会话消息 roster:`ListAgents` 不列出、`SendMessage` - 按会话 ID 直投返回 not-reachable。实测时官方文档点名的门槛**全部满足**: - Claude Code 2.1.227 ≥ 2.1.224;四个可关停该特性的环境变量一个都没设;同一批 - 读数内,进程内 subagent 可列出、可投递(对照组,证明工具面本身是活的)。 -- 官方文档(code.claude.com/docs/en/cross-session-messaging):云会话仅当**发送侧** - 具备「cloud access —— 第一方 Anthropic API 上的 claude.ai 登录」时才可见 —— - CCR 容器凭证当前不满足此条件,限制在平台侧,不在配置侧。 -- 维护者裁决(2026-08-11,spec 车道会话,原文):「当前事件驱动架构即长期方案」/ - 「云卡通信课题就此收档 … 不再追」/「没有计划 把 PM 席迁到常驻本地机」。 - ⇒ 云卡通道 = `subscribe_pr_activity` webhook + 双落点报告契约(issue 评论 + - 会话终报)+ 判据式定时器;⛔ 不再复测 roster 路径、不再提本地机迁移。 - -## 云卡订阅 A/B 实测 - -出处:Dispatch backends 第 4 课 —— 订阅升格为硬步骤的同日 A/B 依据(#7755,2026-08-11)。 - -- **#7672(未订阅)**:PR 合并由轮询发现,迟到数分钟 —— 感知滞后整整一个巡检周期。 -- **#7655/#7657(已订阅)**:CI 失败与合并通知**秒级**投递,整段接力 - (红 → 修 → 绿 → 落地)全程事件驱动、零轮询。 -- ⇒ 「云卡创建即对其 PR 挂 `subscribe_pr_activity`」由随行纪律 bullet 升格为 - `mode:cloud` 交付契约的**编号硬步骤**:没订阅的云卡 PR 就是轮询负债。 - -## #6072 感知通道缺口实测 - -出处:入队与落地 B「落地窗口挂事件订阅」段 —— 适用面动因与 #6072 出处(#7755 的行数对冲搬移)。 - -这类单的 base 会在等待期间被 main 甩开,冲突转换与 CI 红正是 PM 可动作的事件;订阅把感知从 -「一个巡检周期的轮询滞后」缩到实时(出处:#6072 压后待放期间起冲突,维护者先于 -PM 看到 —— 感知通道缺口实测)。 - -## 云卡归档欠账与 dirty 自救实测 - -出处:入队与落地 B 归档 bullet(#7755 的行数对冲搬移;dirty 自救半边并入自 -`claude/pm-dispatch-spec-5ls2tr` @ `bbe056b` 的 rider 段,原目标 #7431 已关闭)。 - -- **欠账实测**:归档是 ACCEPT 收尾的**固定动作序列**的一环,不是可选的清理 —— - 实测代价:一个座位曾累积 **11 个**已完成未归档的空转容器 —— 只会被问出来, - 不会被发现。 -- **rider 段原文(bbe056b)**:「**云卡归档(维护者 2026-08-10)**:PR 已 MERGED/关闭 - **且**报告已收复核 ⇒ `archive_session`;巡检顺手批扫 `list_sessions` 的 - review_ready 存量。⛔ PR 合并前不归档 —— 活会话是 dirty 自救的执行手 - (#7265/#7325 实测:PM 一条 PR 评论唤醒完工会话 merge+regen+重推);误归档可 - `unarchive_session`,但容器现场已失,宁晚勿早。」规则半边已并入主文件归档 - bullet,本节存叙事与出处。 diff --git a/.claude/skills/pm-dispatch/references/landing-operations.md b/.claude/skills/pm-dispatch/references/landing-operations.md new file mode 100644 index 0000000000..cd4e7edb14 --- /dev/null +++ b/.claude/skills/pm-dispatch/references/landing-operations.md @@ -0,0 +1,72 @@ +# 入队与落地细则(references —— 按需加载) + +出处:主文件「复核」的入队与落地原则。本表是 ACCEPT 之后把 PR 送到 MERGED 的操作 +细则,在落地窗口查阅;⛔ 不引用 issue 编号。 + +## A. 碰生成物的 PR:入队前先同步 + 整体重生成 + +批次选择只保证同批 file-disjoint,管不到先后两单都碰生成物 —— 协议变更几乎必然如 +此。被路由到 `merge=os-regen` 的路径清单**当场读**:`grep os-regen .gitattributes` +(唯一权威清单,⛔ 不抄进派发令当常量 —— 散文没法被类型检查,唯一不会烂的拷贝是不 +存在的那份;清单里同时有文档产物,同样会被静默吞)。该驱动会 merge exit 0、零冲突 +标记,却**静默丢掉一侧的改动** —— 只有重新生成才暴露。 + +四步序已机械化:`bash scripts/pm/os-regen-merge.sh` —— ① `git merge origin/main` +(⛔ 禁 rebase / force-push);② 生成物 `git checkout origin/main -- <路径>`;③ +**先 commit 掉 merge**;④ 整链重生成 + 生成物门禁全绿。顺序防两个陷阱: + +- **MERGE 状态下跑 `gen:schema` 会把 authorable-surface 锚点静默倒退回旧分叉点** + (HEAD 仍是合并前分支 tip),倒退后的锚点**依然 authentic**、全部门放行,一次已 + 合并的锚点推进被静默撤销 —— 这就是「先 commit 再重生成」的由来。 +- **`gen:schema` 的清场会顺手抹掉 `gen:openapi` 的产物**,表现为 rest 包一串 + `expected 503 to be 200` 假红;补跑 `pnpm --filter @objectstack/spec + gen:openapi` 即复原。 + +重生成后**断言所有兄弟单的条目都还在**;更硬的旁证是查**上一单的实现体**完好(带 +引号精确名 `git grep … origin/main -- <实现文件>`)—— 条目是索引,实现体才是被吞 +的重灾区。**锚点断言的正确措辞**(写错会教唆 dev 手改锚点):断言 +`pnpm --filter @objectstack/spec check:authorable-surface` **绿**即可;`baseRev` +允许滞后,滞后只打一行提示不是错误;⛔ 禁止为凑「相等」手改锚点文件,⛔ 不得要求 +`baseRev == merge-base` —— 那个等式不是任何门的判据。 + +## B. 跟到 MERGED 为止;入队后的看护归队列管家 + +车道 PM 的权责:验收(复核清单);**首次入队** —— ACCEPT 后挂 6–9 分钟 flip 定 +点,到点核门禁 job 结论(承载门禁族的 job `completed: success`),绿即转 ready + +挂 auto-merge,未绿阶梯重挂;CI success webhook 不可靠,⛔ 不坐等 webhook、不忙轮 +询;定点文本照定时器写法纪律(幂等开头、只写判据 —— 窗口内 PR 可能已被他手处置); +确认 **MERGED** —— 每轮同时读队列分支与 `origin/main` 两个读数。ready 与 +auto-merge 的顺序不可反(转回 draft 会同时掉 auto-merge 与队列成员资格)。 + +**落地窗口给关键 PR 挂 `subscribe_pr_activity`**(会话型座位专用;Routine 座位每 +fire 新会话收不到,维持轮询): + +- ⛔ 不订阅 dev 交报告前的 PR —— 报告前是 dev 的领地,双驾驶员互踩;云卡出生即订阅 + 不越此界,因为其报告在 draft PR 开出即刻到达; +- 订阅是感知补充,不替代 flip 定点; +- **MERGED / 关闭即退订,同刻把 `mode:cloud` 派出的会话 `archive_session`** —— 触 + 发条件是卡的终局且报告已收复核;⛔ PR 合并前不归档(活会话是 dirty 自救的执行 + 手;误归档可 unarchive,但容器现场已失,宁晚勿早); +- 暂停/交接时清点在挂订阅写进座位贴,⛔ 不留孤儿订阅。 + +落地之后**再核一次落地判据本身**仍是车道 PM 的活 —— 队列的合并同样走 os-regen 驱 +动,A 的静默吞并在队列合并这一步一样能发生。 + +## C. 依赖前棒才能转绿的 PR:draft 停放 + 签名级预期红清单 + +串行链里后棒常常先行实现。这种 PR **停在 draft**,PR body 写两样:**精确的预期红 +清单**(逐条失败测试名 + 报错签名 ——「几条测试会红」不够用)与**解除条件**(「依 +赖 PR #N 合入」)。每个 CI-failure 事件与清单比对 —— 签名匹配静默跳过,**新签名才 +是真问题**。依赖合入后:最后一轮同步 → 红清零 → 转 ready → 入队。 + +## D. 串行接力:多个已实现 PR 全碰生成物时,一次只放行一个 + +1. **每一棒都是一整圈**:merge main + A 的四步 + 全套验证 + 兄弟单断言复核 → PM 复 + 核回报 → 转 ready → 挂 auto-merge/入队;每棒都重走,不是整条链只走一次; + auto-merge 由 PM 挂、dev 永不碰。 +2. **相邻两棒同碰一个文件时,交接的是语义,不是文本**:前棒在回报里写明它对共享文 + 件改动的**性质**(改名/提取变量/增补断言,而非纯追加),PM 原样转告下一棒,并要 + 求「两个 PR 的意图**叠加**,⛔ 禁止机械取一边」—— 取一边会各自绿、合起来错。 +3. **两棒散文互锁时,分工由 PM 指派**:允许前棒给后棒留占位交接;PM 必须在**两侧** + 的接力指令里写明谁动、谁不动 —— 否则要么两边都动(冲突),要么两边都不动(过期 + 散文),两种结果在 CI 上都是绿的。 diff --git a/.claude/skills/pm-dispatch/references/platform-readings.md b/.claude/skills/pm-dispatch/references/platform-readings.md new file mode 100644 index 0000000000..4d56ca3c57 --- /dev/null +++ b/.claude/skills/pm-dispatch/references/platform-readings.md @@ -0,0 +1,103 @@ +# 平台读数事实表(references —— 按需加载) + +出处:主文件「平台读数纪律」。本表是 GitHub API / 工具行为的**实测事实**,在做对应 +操作的那一刻查阅;原则(判据取命令输出、零命中反查、定时器文本纪律等)住在主文 +件。⛔ 本表不引用 issue 编号 —— 每条自含失效模式与边界。 + +## 队列成员资格与 auto-merge + +- 判「在不在合并队列」看 timeline 事件 `added_to_merge_queue`(REST + `GET /repos/{owner}/{repo}/issues/{pr}/timeline`),⛔ 不看 `auto_merge` 字段 —— + 入队后它回落为 off,零信息量(维护者 2026-08-11 裁定)。队列分支读法 + (`git ls-remote --heads origin 'refs/heads/gh-readonly-queue/*'`)正命中仍是 + 「已入队」的充分证据,反向推断作废 —— 队列满载时 PR 已入队而分支尚未建出。 +- **成功序列读间隔不读事件名**:`removed_from_merge_queue` 后 ~1 秒内跟着 + `merged` 是落地,不是被踢;真被踢的形状是其后没有 `merged`、几分钟后 PR 仍 + open。 +- 「判据不在 `origin/main` 上」是二义读数 —— 同时兼容「在队列里等」与「压根没入 + 队」,两者处置相反。落地检查永远两个读数:**队列成员资格 和 `origin/main`**,缺 + 一不可。 +- **PR 转回 draft 会同时掉 auto-merge 与队列成员资格,且不自动恢复**;转正后必须 + 重新挂。反方向同理:要真踢出队列,只有转 draft —— `disable_pr_auto_merge` 单独 + 调用**不解除队列成员资格**,PR 照样落地。 +- `enable_pr_auto_merge` 的空字段返回(`method: , enabled at `)对「入没入队」零区 + 分度,签名本身不构成任何方向的证据。序列:① 先验队列分支(给条目 ~20–30s 建 + 出);② 分支在 ⇒ 结束,⛔ 不翻转;③ 等待后仍缺席**且队列已见 churn**(更新的条 + 目建出了分支而你的没有 —— 截断下单纯缺席不充分)⇒ 翻转一次(`disable` → + `enable`),翻转后仍以 timeline 事件验证;④ ⛔ enable 与它的队列验证之间永不插 + `disable` ——「入队」webhook 可能乱序迟到,armed 窗口里补的 disable 会把已发生的 + 真实入队撤掉。 +- **队列踢出先认签名再决定重投**:已知 flaky 核对失败签名一致 ⇒ 原样重投;但止血 + 修复合入后**同一签名再现就不再是那条 flaky**,是新问题,必须重新诊断,⛔ 禁止条 + 件反射式重投。第三种签名:本 PR 名下**没有任何** `merge_group` run 且批次同伴的 + run 全部 `success` = 队列重建的连带取消,不是红 —— 带签名读数收据重投一次(收据 + 留在 PR 上),⛔ 无收据不重投;同一 PR 第二次被踢移交队列管家。 + +## API 配额 + +- GraphQL 配额(5000/时)极易打满,读与评论一律走 REST(core 15000/时,独立 + 计);只有无 REST 对应物的写才花 GraphQL。`issue_write` 连查找半边都吃 + GraphQL —— 配额红时认领类动作整体排队,评论(REST)先行把结论发出去。 +- 配额打满:待执行写操作**排成有序清单挂进巡逻词**(不靠记忆),轮询 + `gh api rate_limit` 的对应资源,恢复窗口按序连清(ready → auto-merge → 入队可 + 一气完成);重试对齐整点(REST core 整点重置)优于指数退避,⛔ 绝不忙轮询。 + search 与 core 是独立配额,一侧打满另一侧可作退路;REST core 在共享身份下同样 + 会打满 ——「走 REST」≠「不限量」。 +- **MCP 参数两陷阱**:`list_issues` 多标签过滤是 **OR** 不是 AND(要 AND 走 REST + search 的 `label:a label:b`,或本地求交);`issue_write` 的 `labels` 是**整组替 + 换**不是追加 —— 不先读现值合并再写,会静默剥掉别的标签(状态机丢位);真追加走 + REST `POST /issues/{n}/labels`;写后照标签纪律回读。 + +## 读数陷阱 + +- **读数四坑**:`cd X && cmd` 会短路(路径不存在时命令在当前仓继续执行,产出假读 + 数)—— 跨仓一律 `git -C `;`git grep -c | wc -l` 数的是文件数不是命 + 中数;裸名 grep 被幸存家族当子串命中 —— 退役核验带引号精确名,更硬的判据是查声 + 明式(`^(export )?(const|type|interface) \b`)而不是查提及;浅检出上的历 + 史读数不可信(`merge-base --is-ancestor` 假「非祖先」、`rev-list --count` 截 + 断、`branch -r --contains` 零输出)—— 先 `--deepen` 再判,或走 REST `compare`。 +- `rerun_failed_jobs` 复用原 run 的提交与合并 ref,不拿新 main 重算 —— 红因是基上 + 缺一个已合修复时重跑无效,只能推提交(`git merge origin/main`);判别:修复的合 + 并时间晚于 run 创建时间即是。 +- **CI 红了先取完整日志归档再下结论**:「completeness check 绿」只断言没有 + worker 静默死,≠ 测试通过;并发输出的「相邻」≠「因果」(先查 `turbo.json` 依赖 + 边);⛔ 不只看 tail。公开发出的诊断被推翻时,更正发在同样公开的位置,据它开的 + PR 撤回 draft、解绑 `Fixes`。 +- **判「正文被截断」必须双读取**:`.body` 原文 + `Accept: + application/vnd.github.full+json` 的 `.body_html`,两者在同一处断掉才算 issue 端 + 截断;任何单一读法的尾部缺失先算读取端截断(工具输出上限、分页、切片)。写侧另 + 一半:sanitizer 会在**写入时就地删除**短 `<…>` 片段(HTML 注释标记、`` 类占 + 位符、泛型),反引号与围栏**不提供保护**,正文其余完好、双读取一致读作「完整」 + —— 要保留字面尖括号一律写 HTML 实体 `<` / `>`;含这类片段的正文,写后回读 + 逐个确认仍在(失效完全静默)。 +- **并行 spec PR 同动 pin 计数断言**(被踢不是事故,按 os-regen 序再解一轮):解 + 冲突两侧收据都保留、按合并顺序堆叠,新计数**从合并后源码重数**(the file, not + the history, is the operand),⛔ 不从两侧收据做算术;双方占同一编号是常态(各取 + 当时 max+1),重编号后进侧。 +- **容器重启杀死在飞 dev,现场三态判读**:① 分支已推 + PR 已开 ⇒ 只欠验收(CI 重 + 跑 + 复核,不动代码);② 死在 regen 中途(未提交的全是生成物、merge commit 已 + 在)⇒ PM 直接续作 —— build → 整链 regen → 生成物门禁全绿 → 提交推送,恢复 + commit 带 `Recovery commit:` 前缀留审计;⚠️ 有的现场 regen 一件没跑,推送前先跑 + 生成物门禁别赌;③ 死在源码编辑中途 ⇒ 先读 diff 判完整性 —— docblock 把动机/失效 + 模式/判据写全的,PM 可代跑其终验后提交;写了一半意图不明的 ⛔ 不代提交,记进交 + 接。dev 的临时目录(`.os-scratch/` 一类)是工作物不是交付物,清掉,⛔ 不进 + feature PR。 + +## 断粮检测与跨墙恢复细则(5 小时用量墙) + +- **检测**:`npx ccusage blocks` 在容器内可用(读本地会话记录),两个有用读数 —— + 当前 5 小时窗口的边界/剩余时间,与燃烧率(预警)。三个盲区连着读:单容器视野(云 + 卡在自己容器里烧同一账号的额度,这里看不见);估的是成本不是套餐余量(没有任何面 + 向 agent 的接口暴露账号级剩余额度);窗口起点是本地推断的近似值。**权威的墙信号是 + 失败本身**:撞墙时 API 调用失败、宿主报「limit reached, resets at HH:MM」—— 重置 + 时刻通常在撞墙那一刻可得,只是事前查不到;把它记下来。 +- **定时器选型**:拿到重置时刻 ⇒ 一发定点(reset + 缓冲)优先,一枪精确胜过逐小时轮 + 询;没有重置时刻 ⇒ 挂每小时 cron Routine(`create_trigger`,cron 型),⛔ 不用 + send_later 链 —— send_later 是一次性触发器,fire 后自灭(平台文档:run-once, + fire 后自禁用),投进死窗口的那一发是否会被平台重试**未实测且文档未承诺**,按保守 + 设计,一次性链条可能在断粮窗口内烧掉唯一一发而断链,恰好断在它存在的意义上; + cron 每小时重发,第一枪成功的 fire 跑恢复,然后**删除 cron**(自清理是纪律的一部 + 分 —— 幸存 cron 是孤儿定时器)。fired 文本照定时器写法纪律:幂等开头、只写判据。 +- **恢复 playbook**(链的是既有规则不是新规则):逐个探在飞云卡(它们死在墙上; + draft-PR-early 契约守住时零信息丢失)→ 走直接验收兜底或 transcript 复活 → 重挂常 + 规巡检定时器 → 照常跑轮。 diff --git a/.claude/skills/pm-dispatch/references/review-checklist.md b/.claude/skills/pm-dispatch/references/review-checklist.md new file mode 100644 index 0000000000..ef353a867a --- /dev/null +++ b/.claude/skills/pm-dispatch/references/review-checklist.md @@ -0,0 +1,53 @@ +# 复核清单细则(references —— 按需加载) + +出处:主文件「复核」。You are the reviewer of record —— 对 GitHub 核验,不对报告的 +自述核验;本表逐项展开每份 dev 报告的复核判据。⛔ 不引用 issue 编号。 + +- **PR 形态**:存在、是 draft、目标 `main`、正文首行引用卡片 —— **`Fixes #` 仅 + 当合并应当关卡**;只落地了可实施一半(另一半在决策箱或按范围排除)⇒ 必须 + `Part of #`,否则合并会静默关掉一张正躺在决策箱里的卡,而 + `needs-user-decision` 的收件箱过滤只看 open issue —— 卡一关,待裁问题就此无人可 + 见。翻 ready 之前亲核首行,别只信报告。 +- **范围检查**(取 changed files,⛔ 不看报告自述):无 `content/docs/releases/` 改 + 动、用户可见改动有 changeset、无与卡无关的文件。Tests/docs-only PR 走 + `skip-changeset` 标签,不走空 changeset(空 changeset 滞留发布);含读者可见生成 + 产物时 dev 选 changeset 是对的 —— 以 PR 正文说明的理由为准,两条路都有效,别来回 + 改。 +- **测试证据**要有真实命令与通过输出,不是一句 tests pass。 +- **CI 收敛读数只属于复核侧**(维护者 2026-08-10 裁定;dev 的契约是草稿 PR 时点交 + 报,报告里 gate `in_progress` 是诚实读数、预期内常态):翻 ready / 挂 auto-merge + / 入队前亲核门禁 job 结论 —— ESLint 与 TypeScript Type Check 两个 job 的 + `conclusion` 已为 `success`(门禁族跑在其内),⛔ 不因报告写了「本地绿」跳过。收 + 敛期转红走补丁轮(SendMessage 续派原 dev —— 那是这笔交换已付过的价钱,不是 + REWORK 的理由;红着合并才是)。重量级卡可在派发令显式写「本单等 CI」。 +- **收益穿过必经边界之后还在吗?** 判据(不是每单都做):价值主张依赖某个下游组件 + 如实转发(HTTP 错误信封、序列化、日志汇聚、跨进程传输)⇒ 至少端到端验一次收益在 + 边界之后仍然存在 —— 精心写的拒收正文可能被 4xx 直通层整条替换,缺口在清单里不在 + 代码里。 +- **裁决实施 PR:旧立场的 pin 全仓翻转了吗?** grep 消费层(REST 信封测试、 + objectql、runtime)确认旧立场零拷贝,**且**真正非法形状的拒收断言仍逐字在(派发 + 侧两句的对账)。 +- **拒收类用例的绿,是不是「它抛了」的绿?** 判据:验收点含「应当被拒收」。抽查 + diff 里的拒收用例有没有断言 `code` 与 `status`(ADR-0112 信封)—— 只写 + `toThrow()` / `rejects.toThrow()` 的用例,在未修实现本就抛裸 Error 的那一族上恒 + 绿,读起来是覆盖、实际证不了拒收;缺断言判 REWORK 补齐,不接受绿色输出。 +- **「N 个包全绿」问清方向与时序**,否则不算清扫证据。判据:含跨包签名收窄/导出类 + 型变更/契约收紧。两问:(1) filter 用的是**前缀** `'...pkg'`(下游消费者)还是后 + 缀 `'pkg...'`(上游依赖)?方向没写就无法复核;(2) typecheck 之前**建过依赖闭 + 包**吗?没建则读的是陈旧 `dist/*.d.ts`,绿可能是假绿。两问都没答案 ⇒ 以全仓门禁 + 结论为准,或判 REWORK 要证据。 +- **dev 验证过 issue 的前提吗?** `premise_still_valid: false` 是**再分诊输入**不是 + 失败 —— 证伪 issue(或 PM 自己的派发框架)的报告是好运行;照单全收每个声称原因的 + 报告才要读两遍。dev 纠正 PM 要**当众认**:更正落在 PR/issue 评论,错前提的正文另 + 立跟进卡,⛔ 不静默丢弃。 +- **验收判据本身也可被证伪**:dev 用测量推翻字面判据、换上等价或更强的不变量门禁 = + 好运行照 ACCEPT;但推翻过程必须写在 PR 正文并附 main 语料上的实测信噪比(误报为 + 零的证据),否则按 REWORK 要证据。 +- **`+0/-0` 不是空文件的证明**:NUL 字节让 git 当二进制渲染、还让整个文件对 grep 隐 + 身 —— 先疑 NUL,以 blob 定论(`git show : | wc -l`),要求源侧写转义序 + 列不写原始字节。 +- **以「死代码/不可达」为由的删除,PM 在 `origin/main` 上自己核一次引用面再 + ACCEPT**(带引号精确名、查声明式)——「这是死代码」是断言不是 diff 里的事实;删除 + 比修改难回滚,而这一查只花十秒。 +- **卡的交付物含系统性 sweep 时**(退场审计、消费半径 grep、语料扫描一类),ACCEPT + 评论把范围外产出**成组列出**并写明 sweep 判据,让分诊能一致地给整批定级。 diff --git a/.claude/skills/pm-dispatch/references/seat-post-protocol.md b/.claude/skills/pm-dispatch/references/seat-post-protocol.md new file mode 100644 index 0000000000..9be92ecc4a --- /dev/null +++ b/.claude/skills/pm-dispatch/references/seat-post-protocol.md @@ -0,0 +1,89 @@ +# 座位贴协议细则(references —— 按需加载) + +出处:主文件「座位贴协议」。原则(一座位一贴、单写手、正文为权威)住在主文件;本 +表是接管/移交/回收那一刻要查的**协议细则**。多写手共编一个 body 机制上防不住互 +吞,所以架构是每个座位一张登记贴(维护者 2026-08-06 拍板)。 + +- **贴 = 座位**,标签 `pm:seat`,正文固定六段:**范围 | 当前 PM | 继承台账 | 热文 + 件串行队 | 常设承诺 | 说明** —— 仅此六段,段段皆**现值**,没有一段随时间增长 + (维护者 2026-08-11 接受)。 +- **标题即状态板**:固定格式 `[PM seat] <座位名> — <状态>`,状态词表 + `🟢 `(在任)/ `🟢 Routine` / `⏳ vacant` / `⏸️ paused`。标题只放慢 + 状态(换班级频率);轮次、在飞、队列快照等快状态 ⛔ 不进标题,留在「说明」段。 + `label:pm:seat` 的列表页因此就是全舰队状态板。 +- **assignee = 在任 PM 的 GitHub 账号**;退场/回收时摘除,无 assignee = 空缺,与 + 标题互为校验。例外:Routine 座位(bot 身份通常不可被 assign)以标题为准。标题、 + assignee、正文「当前 PM」段**三者同笔更新**(成对纪律的三元版),正文为权威。 +- **「当前 PM」段登记三元**:GitHub 账号(`get_me` 自查)+ 会话 ID 或 Routine ID + + 上任时刻。审计/认领评论的**作者字段**是平台盖章、不可自述错的硬读数 —— 接管仲 + 裁与活性判定优先用它对账正文。 +- **接管/移交 = 改该贴正文 + 一条审计评论;评论只作交接审计,不承载状态** —— 现状 + 永远读正文,不读评论流(历史与现状挤同一通道,对账成本随评论数线性涨)。 +- **班次叙事不进贴正文**:逐卡状态在卡与 PR 上本就必填,轮次合计走轮次报告;贴正 + 文只在接管、移交、结构变更时编辑,⛔ 不逐事件追加(无界贴会超出单次读取上限)。 +- **接管压缩是标准步骤**:接任 PM 把交接的有效内容按六段模板重述为新正文,**编辑 + 历史即存档** —— 旧账留在上一版 body revision,审计评论带版本指针(哪一版、何 + 时)。超大贴由半状态巡查的 report-only 项浮出,不等读取失败。 +- **热文件串行队是正文具名段**(跨轮、跨班次的常设事实):文件 → 有序卡片清单 → + 每张卡认领的**区域**;区域列写清楚才敢并行,写不清楚就只能整文件串行。 +- **残余竞态纪律**(唯一剩下的多写手场景是空缺座位争用):动手前重新 fetch 贴正 + 文;审计评论**时间戳先到先得**;写后回读。新增座位贴(拆域)同样先查 `pm:seat` + 索引再立贴。 +- **无心跳,活性惰性判定**:座位不定期报活;只在**接管冲突**时评估一次 —— + Routine 座位查调度器(`last_fired` / `next_run`),会话座位查最近一条产出评论的 + 时间戳,**>24h 无产出即可回收**(改贴正文 + 审计评论)。子树/批次里在飞的认领仍 + 由原认领者跟完。 +- **每轮巡检核对自己的座位贴正文**:协议或结构升级会迁移状态,自查一贴成本为零; + 发现不符当场改正文 + 审计评论。Routine 座位的收尾简报也落自己的座位贴(它是下一 + 轮自退守卫的读数)。 +- **交接收尾清单(退场序列,漏一步就是留残缺现场)**:① 立即停止新派发;② 在手工 + 件清零 —— 在飞 dev 收单、已 ACCEPT 的 PR 跟到 MERGED 或明确移交;③ 全量枚举本车 + 道队列 + 决策箱 + findings,形成接任台账;④ 贴正文改「⏳ vacant」+ 完整台账(前 + 任会话 ID、注销时间、队列快照、**热文件串行队**、跨车道备忘);⑤ 审计评论存档 + (接任指引:`/pm-dispatch 接手` + 先读座位贴);⑥ `list_triggers` 清点本会话全 + 部自设定时器,逐个清理**或随移交物转交**(绑着移交中 PR 的定时器随 PR 转交,不 + 清);⑦ 向维护者交最终报告,含本任期沉淀的 SKILL 更新建议清单(查重后立单 —— 换 + 班复盘是交接的固定产物)。 +- **epic 委托不入座位贴体系**(父单正文自带会话与领地,`label:pm:epic` 即全量索 + 引);座位贴只记常设座位,一件事只记一处。`packages/spec` 恒归 spec 座位。 + +## 座位 Routine 创建细则 + +- 由维护者在 claude.ai 的 Routines UI 创建并勾选 GitHub 连接器 —— 会话内 + `create_trigger` 建的 fresh-session Routine 不携带连接器,fired 会话拿不到 + GitHub 工具,表现为**静默零产出**;⛔ 不要在会话里 create_trigger 出一个座位 + Routine 就当它在跑。模型不能经 API 钉住,同样在 UI 钉;Routine 继承环境默认模型, + 环境默认变了它跟着变。 +- 创建后**先手动 fire 一轮烟测**,判据取 **GitHub 上的产出**(标签写入/审计评论/座 + 位贴编辑),不取「会话看起来起来了」;零产出即技术性失败,回滚:`delete_trigger` + + 清空座位登记 + 失败注记。 +- 档位按职责挑,不按重要性挑;档位与 cron 是维护者在 UI 上的可调项,协议文本 ⛔ 不复 + 制其当前值 —— 座位贴才是现状。 + +## Epic 车道收尾与僵尸回收细则 + +- 收尾四步一组(缺一步就是过夜半状态):最后一个 sub-issue 关闭 → 父单留总结评论、 + 关闭父单、摘 `pm:epic`(即从索引注销)、正文领地段标注收官。 +- 僵尸判据:父单正文有登记但整棵子树 ~48h 无任何认领/分支/PR 动静 → **分诊座位**在 + 父单评论询问,再静默一窗后摘 `pm:epic` 收回子树;子树内仍在飞的认领按认领协议由原 + 认领者跟完。 +- 领地防撞是声明式的,不是机械保证 —— 撞上由合并队列兜底(代价是返工,不是脏数 + 据),所以领地声明越窄越诚实,越宽越要在父单正文里说明为什么。 + +## 发版板运维细则(`target:`) + +- 生产者:主仓 → 分诊座位;objectui / cloud → 各自整仓座位(维护者 2026-08-11 裁 + 定)。其余座位不打不摘,误判走上报;⛔ 三个生产者各扫各的 backlog。 +- 消费者:维护者的发版清单 = 三仓三条查询(`label:target: is:open` 各一 + 条);等价的 org 级搜索 `org:… label:target: is:open` 仅是备用写法。「归零 + = 可发版」指三张板都空,单看主仓归零不是。⛔「姊妹仓命中即误标」的旧读法已废除 —— + 它教读者销毁真实阻塞的唯一证据。 +- 鲜度:与发现分诊轮同节奏(每 ~5 轮)对板上 open 项做过时前提检查,已修/不成立的摘 + 牌 + 一句评论。 +- 发版时刻 = 清板,不是重扫:板上每条三选一 —— 修掉 / 摘牌(不再成立)/ **明示接受 + 带病发布**(摘标签 + accepted-for-GA 评论留痕,进 release notes 的 known + issues);三仓各自执行、读数回贴,⛔ 不因「前端仓/独立部署」整批默认接受。 +- 发版前置(维护者 2026-08-10 拍板):清板动手之前先取**一次** pin 读数(⛔ 一次即 + 止,不是重扫);pin 滞后 ⇒ console bump 单必须**已存在且已上板**,或按标准形态明 + 示接受;两者都不成立 ⇒ **不 cut**。这是「板已清空」与「console bump 已就位」之间 + 唯一的机械关联。 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c8b8641db7..2e9bbb6f68 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -161,6 +161,14 @@ jobs: - name: PM skill line ratchet run: pnpm check:pm-skill-ratchet + # pm-skill issue-ID lint (maintainer ruling 2026-08-12): operative + # agent-protocol text (.claude/skills/pm-dispatch/** and + # .claude/agents/os-dev.md) carries lessons self-contained — no issue-ID + # citations; rulings keep date + verbatim quote. The script header is + # authoritative, including the one self-expiring legacy waiver. + - name: PM skill issue-ID lint + run: pnpm check:pm-skill-id-lint + # Docs/skills authoring guard (#2035 / ADR-0059): TS code blocks in # Markdown/MDX are not type-checked or ESLinted, so skills/ and # content/docs/ can drift back to teaching the bare `: Page = {}` literal diff --git a/package.json b/package.json index 99e8b3a25f..264a258986 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "check:skill-compatibility": "node scripts/check-skill-compatibility-version.mjs --self-test && node scripts/check-skill-compatibility-version.mjs", "check:agent-model-declared": "node scripts/check-agent-model-declared.mjs --self-test && node scripts/check-agent-model-declared.mjs", "check:pm-skill-ratchet": "node scripts/pm/check-skill-line-ratchet.mjs --self-test && node scripts/pm/check-skill-line-ratchet.mjs", + "check:pm-skill-id-lint": "node scripts/pm/check-skill-id-lint.mjs --self-test && node scripts/pm/check-skill-id-lint.mjs", "check:adr-anchors": "node scripts/check-adr-anchors.mjs --self-test && node scripts/check-adr-anchors.mjs", "check:adr-links": "node scripts/check-adr-links.mjs --self-test && node scripts/check-adr-links.mjs", "check:adr-merge-approval": "node scripts/check-adr-merge-approval.mjs --self-test && node scripts/check-adr-merge-approval.mjs", diff --git a/scripts/check-skill-frame-sync.mjs b/scripts/check-skill-frame-sync.mjs index 9f334009f2..c03fa8c01b 100644 --- a/scripts/check-skill-frame-sync.mjs +++ b/scripts/check-skill-frame-sync.mjs @@ -702,10 +702,13 @@ function selfTest() { { // The anti-false-positive direction. #5451 route B generalized the published // copy on purpose: no startup self-description, no this-repo issue numbers. - // A gate that reads those as drift would be reverted, so prove it does not. + // (Since the 2026-08-12 principles-only rewrite the internal copy carries no + // issue numbers either — check-skill-id-lint enforces that — so this fixture + // mutates wording only.) A gate that reads wording as drift would be + // reverted, so prove it does not. label: 'wording-only divergence (the #5451 generalization) → stays GREEN', copies: () => { - let c = mutate(base, 'internal-pm', /#\d{4}/g, '#0000'); + let c = mutate(base, 'internal-pm', '长期代价', '长远代价'); c = mutate(c, 'internal-pm', '创业项目', '项目'); return mutate(c, 'internal-pm', '不作数', '不算证据'); }, diff --git a/scripts/pm/check-skill-id-lint.mjs b/scripts/pm/check-skill-id-lint.mjs new file mode 100644 index 0000000000..bbcc69a92f --- /dev/null +++ b/scripts/pm/check-skill-id-lint.mjs @@ -0,0 +1,170 @@ +#!/usr/bin/env node +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * pm-skill issue-ID lint — operative agent-protocol text must not cite issue + * numbers. + * + * node scripts/pm/check-skill-id-lint.mjs # the gate + * node scripts/pm/check-skill-id-lint.mjs --self-test # verify the checker + * + * ## Why + * + * Maintainer ruling, 2026-08-12 (verbatim, untranslated): 「立一张结构卡,我觉的 + * 处理 issue 时犯的错应该总结成经验,保留 issue id没有意义,如果ai去查原始issue, + * 得不偿失。」 Incident learnings are distilled into the rules themselves as + * self-contained lessons (failure mode + discipline + boundary); an issue-ID + * citation invites the reader to dereference history, which costs more than it + * returns. Maintainer-ruling provenance keeps DATE + VERBATIM QUOTE — numbers go. + * + * ## What it scans + * + * Every .md file under .claude/skills/pm-dispatch/ (the PM operating protocol, + * including its references/) plus .claude/agents/os-dev.md (the dev-side twin). + * The pattern is /#[0-9]{3,}/ — real issue/PR numbers in this repo are 3+ digits, + * while placeholders (`#`, `#N`, `epic:#`) and small literals (`#12 #34` + * in the argument-syntax example) stay legal. + * + * ## The one legacy waiver, and how it expires by itself + * + * .claude/agents/os-dev.md is rewritten to the same standard in its OWN PR (the + * two PRs are deliberately independent — no cross-PR ordering). Until that + * rewrite lands, this gate would be red on either merge order. So os-dev.md + * carries an EXACT legacy count: the file passes only when its match count is + * exactly the recorded pre-rewrite value (untouched legacy) or exactly 0 + * (rewritten). Any other count is red — you cannot add an ID, and a partial + * cleanup must finish the job. Once the rewrite lands the entry is dead weight + * with zero tolerance (0 matches required); deleting the entry then is a + * one-line cleanup, not a prerequisite. + * + * Missing file or empty read is RED, never a pass — a gate that cannot find its + * input must fail, not skip. + */ + +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import process from 'node:process'; + +const REPO_ROOT = join(fileURLToPath(import.meta.url), '..', '..', '..'); + +export const SCAN_ROOT = '.claude/skills/pm-dispatch'; +export const EXTRA_FILES = ['.claude/agents/os-dev.md']; +export const ID_PATTERN = /#[0-9]{3,}/g; + +// Exact-count legacy waiver (see header): pass iff count === legacy || count === 0. +export const LEGACY_EXACT = new Map([ + ['.claude/agents/os-dev.md', 81], +]); + +function mdFilesUnder(dir) { + const out = []; + for (const entry of readdirSync(dir)) { + const p = join(dir, entry); + const st = statSync(p); + if (st.isDirectory()) out.push(...mdFilesUnder(p)); + else if (entry.endsWith('.md')) out.push(p); + } + return out; +} + +export function verdictFor(rel, matchCount) { + const legacy = LEGACY_EXACT.get(rel); + if (legacy !== undefined) { + if (matchCount === 0 || matchCount === legacy) return { ok: true }; + return { + ok: false, + msg: + `${rel}: ${matchCount} issue-ID citation(s); the legacy waiver allows exactly ${legacy} ` + + '(the untouched pre-rewrite file) or 0 (the principles-only rewrite). Do not add IDs; ' + + 'a partial cleanup must go to zero.', + }; + } + if (matchCount > 0) { + return { + ok: false, + msg: + `${rel}: ${matchCount} issue-ID citation(s) — operative text carries lessons ` + + 'self-contained (failure mode + discipline + boundary); rulings keep date + verbatim ' + + 'quote, numbers go (maintainer ruling 2026-08-12).', + }; + } + return { ok: true }; +} + +function run() { + const targets = []; + try { + targets.push(...mdFilesUnder(join(REPO_ROOT, SCAN_ROOT))); + } catch { + console.error(`✗ check-skill-id-lint: cannot read ${SCAN_ROOT} — red, not a skip.`); + process.exit(1); + } + for (const extra of EXTRA_FILES) targets.push(join(REPO_ROOT, extra)); + + let failed = 0; + let scanned = 0; + for (const abs of targets) { + const rel = relative(REPO_ROOT, abs).replace(/\\/g, '/'); + let text; + try { + text = readFileSync(abs, 'utf8'); + } catch { + console.error(`✗ check-skill-id-lint: cannot read ${rel} — red, not a skip.`); + failed++; + continue; + } + if (text.length === 0) { + console.error(`✗ check-skill-id-lint: ${rel} read as empty — red, not a skip.`); + failed++; + continue; + } + scanned++; + const count = (text.match(ID_PATTERN) ?? []).length; + const v = verdictFor(rel, count); + if (!v.ok) { + failed++; + console.error(`✗ check-skill-id-lint: ${v.msg}`); + // name the first few offending lines so the fix needs no second scan + let shown = 0; + const lines = text.split('\n'); + for (let i = 0; i < lines.length && shown < 5; i++) { + if (ID_PATTERN.test(lines[i])) { + console.error(` ${rel}:${i + 1}: ${lines[i].trim().slice(0, 120)}`); + shown++; + } + ID_PATTERN.lastIndex = 0; + } + } + } + if (failed) process.exit(1); + console.log(`✓ check-skill-id-lint: ${scanned} file(s) clean (pattern ${ID_PATTERN}).`); +} + +function selfTest() { + const cases = [ + ['clean file -> green', verdictFor('x.md', 0).ok, true], + ['one ID -> red', verdictFor('x.md', 1).ok, false], + ['legacy file at exact count -> green', verdictFor('.claude/agents/os-dev.md', 81).ok, true], + ['legacy file at zero -> green', verdictFor('.claude/agents/os-dev.md', 0).ok, true], + ['legacy file grew -> red', verdictFor('.claude/agents/os-dev.md', 82).ok, false], + ['legacy file partially cleaned -> red', verdictFor('.claude/agents/os-dev.md', 40).ok, false], + ['red message names the standard', verdictFor('x.md', 2).msg.includes('self-contained'), true], + ['placeholders stay legal', ('# #N epic:# #12 #34'.match(ID_PATTERN) ?? []).length, 0], + ['real IDs match', ('see #4650 and #12345'.match(ID_PATTERN) ?? []).length, 2], + ]; + let failed = 0; + for (const [name, actual, expected] of cases) { + const ok = actual === expected; + if (!ok) failed++; + console.log(` ${ok ? '✓' : '✗'} ${name}`); + } + if (failed) { + console.error(`✗ check-skill-id-lint self-test: ${failed} of ${cases.length} case(s) failed.`); + process.exit(1); + } + console.log(`✓ check-skill-id-lint self-test: ${cases.length} cases pass.`); +} + +if (process.argv.includes('--self-test')) selfTest(); +else run(); diff --git a/scripts/pm/check-skill-line-ratchet.mjs b/scripts/pm/check-skill-line-ratchet.mjs index 092b63bbae..188725b7da 100644 --- a/scripts/pm/check-skill-line-ratchet.mjs +++ b/scripts/pm/check-skill-line-ratchet.mjs @@ -12,13 +12,15 @@ * * `.claude/skills/pm-dispatch/SKILL.md` is read in full by every seat session * and every Routine fire. It reached 3,013 lines (~235 KB) before #7341's - * extraction moved the long incident narratives into - * `.claude/skills/pm-dispatch/references/` (loaded on demand), landing the - * main file at 2,997 lines. #5925 item 7's approved intent is that the main - * file's growth rate goes to ZERO: new rules stay in main, new case law lands - * in `references/incidents.md` (append-only). Without a gate that intent - * erodes one well-meaning paragraph at a time — the same way the file got to - * 3,000 in the first place. + * extraction, and 2,568 before the #7885 principles-only rewrite (maintainer + * ruling 2026-08-12: 「现有的项目经理 skills 应该大幅简化,只需要说原则,不需要 + * 写细节」) landed it at the current ceiling. The main file carries principles, + * the state machine and lookup tables ONLY; at-that-moment operational detail + * lives in `.claude/skills/pm-dispatch/references/` (loaded on demand), and + * incident case law lives in git history, not in any file (same ruling: + * issue-ID dereference is deprecated — see check-skill-id-lint.mjs). Without a + * gate that intent erodes one well-meaning paragraph at a time — the same way + * the file got to 3,000 in the first place. * * ## The ratchet discipline (shrink-only) * @@ -40,12 +42,11 @@ import process from 'node:process'; const SKILL_PATH = new URL('../../.claude/skills/pm-dispatch/SKILL.md', import.meta.url); -// Post-#7754-compression count: 2,568 — the #7631 batch's PR₂ moved the -// remaining narratives/沿革/worked examples into references/incidents.md, -// keeping every imperative + criteria/command + one-line case anchor in the -// main file. Shrink-only: lower freely, raise only with a maintainer ruling -// quoted in the raising PR (see header). -export const MAX_LINES = 2568; +// Post-#7885-rewrite count: 686 — the principles-only edition (single file, +// per-role duty sections, zero issue-ID citations; references/ carries the +// on-demand fact tables). Shrink-only: lower freely, raise only with a +// maintainer ruling quoted in the raising PR (see header). +export const MAX_LINES = 686; export function verdict(lineCount, maxLines) { if (lineCount === 0) return { ok: false, msg: 'SKILL.md read as empty — refusing to treat a missing/empty input as a pass (#4690).' }; @@ -54,8 +55,8 @@ export function verdict(lineCount, maxLines) { ok: false, msg: `SKILL.md is ${lineCount} lines; the ratchet ceiling is ${maxLines}. ` + - 'Move narrative to .claude/skills/pm-dispatch/references/ (incidents.md is append-only case law) ' + - 'instead of growing the hot file — raising the ceiling requires a maintainer ruling quoted in the PR.', + 'Keep the hot file principles-only: move at-that-moment operational detail to ' + + '.claude/skills/pm-dispatch/references/ — raising the ceiling requires a maintainer ruling quoted in the PR.', }; } return { ok: true, msg: `SKILL.md is ${lineCount} lines (ceiling ${maxLines}; headroom ${maxLines - lineCount}).` }; diff --git a/scripts/pm/ensure-pm-labels.sh b/scripts/pm/ensure-pm-labels.sh new file mode 100644 index 0000000000..fb32cd7ab1 --- /dev/null +++ b/scripts/pm/ensure-pm-labels.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +# +# ensure-pm-labels.sh — idempotent creation of the pm-dispatch label vocabulary. +# Run once at the start of a first round (safe to rerun any time). +# +# bash scripts/pm/ensure-pm-labels.sh +# +# The label set IS the PM state machine (.claude/skills/pm-dispatch/SKILL.md, +# "State model"): every label here is consumed by a named query or gate. +# ⛔ The retired lanes (domain:engine, domain:ui) are deliberately ABSENT — +# recreating a retired label puts an ownerless lane back into GitHub's +# autocomplete. Their leftover label OBJECTS may still exist in the repos; +# deleting those objects is a separate, deliberate PM action. +# +# Requires the `gh` CLI. Where only the GitHub MCP tools are available, mirror +# these creations through them — the protocol is identical. + +set -uo pipefail + +for R in objectstack-ai/objectstack objectstack-ai/objectui objectstack-ai/cloud; do + gh label create pm:queue -R "$R" -c 0e8a16 -d "Ready for the PM dispatch loop" 2>/dev/null || true + gh label create pm:dispatched -R "$R" -c 1d76db -d "Dispatched to a dev agent by /pm-dispatch" 2>/dev/null || true + gh label create needs-user-decision -R "$R" -c d93f0b -d "Blocked on a maintainer decision — do not dispatch" 2>/dev/null || true + gh label create pm:on-hold -R "$R" -c e4e669 -d "Decision made, deliberately deferred — do not dispatch, do not nag; restart condition in the hold comment" 2>/dev/null || true + gh label create pm:blocked -R "$R" -c b60205 -d "Blocked by another issue/PR — body carries Blocked-by: #N" 2>/dev/null || true + gh label create finding -R "$R" -c c2e0c6 -d "Recorded observation — held, not dispatchable until the findings triage round grades it" 2>/dev/null || true + 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 + +# 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 +gh label create repo:cloud -R objectstack-ai/objectstack -c c5def5 -d "Seam card: cross-repo ordering with cloud is the substance (pure cloud fixes live in cloud)" 2>/dev/null || true + +# Domain lanes — the roster lives in SKILL.md's domain table; keep both in sync +# BY PR whenever a lane is added or retired. +for D in engine-core metadata drivers services identity devx spec spec-surface cli spec-tooling skills; do + gh label create "domain:$D" -R objectstack-ai/objectstack -c bfd4f2 -d "Domain lane — seat card indexed by label:pm:seat" 2>/dev/null || true +done + +echo "✓ ensure-pm-labels: label vocabulary ensured (idempotent)." diff --git a/scripts/pm/os-regen-merge.sh b/scripts/pm/os-regen-merge.sh new file mode 100644 index 0000000000..50b47248b5 --- /dev/null +++ b/scripts/pm/os-regen-merge.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +# +# os-regen-merge.sh — the mechanized merge sequence for a branch that touches +# os-regen-driven generated artifacts. Run INSIDE the feature branch's worktree. +# +# bash scripts/pm/os-regen-merge.sh +# +# ## Why a script (and why the ORDER is the whole point) +# +# Paths routed to `merge=os-regen` in .gitattributes merge with exit 0 and zero +# conflict markers while SILENTLY DROPPING one side's changes — only a full +# regeneration exposes the loss. And running `gen:schema` while the tree is +# still in MERGE state silently rolls the authorable-surface anchor back to the +# branch's old fork point; the rolled-back anchor is still *authentic*, so every +# gate passes while a landed advance is quietly undone. Both traps are ordering +# traps, so the fix is a fixed order, executed mechanically: +# +# 1. git merge origin/main (⛔ never rebase / force-push) +# 2. git checkout origin/main -- (take main's side) +# 3. COMMIT THE MERGE FIRST +# 4. regenerate the whole chain, then run the generated-artifact gates and +# assert every sibling PR's entries — and the previous PR's IMPLEMENTATION +# BODY — still exist (quoted-exact-name git grep against origin/main). +# +# This script performs steps 1–3 and prints step 4 (the regen chain varies by +# what the branch touches; running it blind would hide a red). Sister trap for +# step 4: `gen:schema`'s cleanup wipes `gen:openapi`'s output, which shows up as +# bogus 5xx assertions in the rest package — rerun +# `pnpm --filter @objectstack/spec gen:openapi` to restore. +# +# The os-regen path list is read from .gitattributes AT RUN TIME — the one copy +# that cannot rot is the one that does not exist. + +set -euo pipefail + +if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "✗ not inside a git worktree" >&2 + exit 1 +fi + +branch="$(git rev-parse --abbrev-ref HEAD)" +case "$branch" in + main | HEAD) + echo "✗ refusing to run on '$branch' — run inside the feature branch's worktree" >&2 + exit 1 + ;; +esac + +if [ -n "$(git status --porcelain)" ]; then + echo "✗ working tree not clean — commit or drop local changes first (⛔ never git stash)" >&2 + exit 1 +fi + +echo "→ fetching origin/main" +git fetch origin main + +# The single authoritative list, read at run time. +mapfile -t regen_paths < <(grep 'merge=os-regen' .gitattributes | awk '{print $1}') +if [ "${#regen_paths[@]}" -eq 0 ]; then + echo "✗ no merge=os-regen entries found in .gitattributes — refusing to guess" >&2 + exit 1 +fi +echo "→ os-regen paths (from .gitattributes, ${#regen_paths[@]} patterns):" +printf ' %s\n' "${regen_paths[@]}" + +echo "→ step 1: git merge origin/main" +if ! git merge --no-edit origin/main; then + echo "✗ merge stopped on conflicts in NON-generated files — resolve those by hand" >&2 + echo " (semantic merge, both intents stack), then rerun this script to redo the" >&2 + echo " generated-artifact half. ⛔ Do not resolve generated files textually." >&2 + exit 1 +fi + +echo "→ step 2: taking origin/main's side of every generated artifact" +for p in "${regen_paths[@]}"; do + # a pattern may match nothing on this branch — that is fine + git checkout origin/main -- "$p" 2>/dev/null || true +done + +echo "→ step 3: committing the merge BEFORE any regeneration" +if [ -n "$(git status --porcelain)" ]; then + git add -A + git commit --no-edit -m "merge origin/main (os-regen artifacts taken from main; regeneration follows)" +else + echo " (merge left no additional changes to commit)" +fi + +cat <<'EOF' +→ step 4 (yours, in this order — the script stops here on purpose): + 1. build the spec build closure, then regenerate the WHOLE chain + (e.g. pnpm --filter @objectstack/spec build && the gen:* scripts your + surface touches; gen:schema's cleanup wipes gen:openapi's output — rerun + pnpm --filter @objectstack/spec gen:openapi after it); + 2. run the generated-artifact gates until fully green; + 3. assert every sibling PR's entries AND the previous PR's implementation + body still exist: quoted-exact-name `git grep "" origin/main -- ` + — entries are the index, the implementation body is what gets swallowed; + 4. commit the regeneration as its own commit and push. +EOF