diff --git a/.claude/agents/os-dev.md b/.claude/agents/os-dev.md index 56ec22379c..1d0aba1082 100644 --- a/.claude/agents/os-dev.md +++ b/.claude/agents/os-dev.md @@ -126,7 +126,12 @@ the farm exactly once either way. Your local pass: ① build closure first packages' own `pnpm test` / `pnpm typecheck`, scoped by `--filter`; ③ the gate families the dispatch prompt names, plus any you can see are implicated (a new fake engine ⇒ `check:engine-double-contract`; a new error code ⇒ `check:error-code-casing`; -`.claude/agents/**` ⇒ `check:agent-model-declared`; any edit ⇒ `check:nul-bytes`). The +`.claude/agents/**` ⇒ `check:agent-model-declared`; any edit ⇒ `check:nul-bytes`); ④ the +prompt's gate list is a **lead, not a spec** — a same-day, carefully taken list still +misses families. After the named families pass, re-derive once against your **actual** +changed paths (`node scripts/pm/dispatch-gates.mjs `), run any family it +surfaces that the prompt missed and your diff really touches, and name the addition in +your report — one O(1) derivation, and naming accuracy stops resting on the PM alone. The accepted cost is an occasional extra push-fix lap; the safety half lives with the PM, who reads the real gate-job conclusions after your report. ⛔ Not licence to skip the named families — they are the cheap half you still owe; what you no longer owe is waiting for CI @@ -164,14 +169,24 @@ silence is the expected shape, never permission: then regenerate (mechanized: `bash scripts/pm/os-regen-merge.sh`). Sister trap: `gen:schema`'s cleanup wipes `gen:openapi`'s output (bogus 5xx failures in rest); restore with `pnpm --filter @objectstack/spec gen:openapi`. -- **⛔ Take a fix out with `git checkout`, a patch file or a temp commit — NEVER - `git stash`.** The worktree isolates files and HEAD, not `refs/stash`, which is one LIFO - stack shared by every worktree: two agents stashing swap entries, `pop` reports success - while restoring the *other's* changes, and a following `git add -A` commits their - half-finished work into your PR. A hook blocks the mutating forms; alternatives, all - inside your own worktree: `git checkout origin/main -- `; - `git diff > /tmp/wip.patch && git checkout -- `; `git commit -am wip` then - `git reset --soft HEAD~1`. +- **⛔ Take a fix out with a temp commit or a patch file — NEVER `git stash`.** The + worktree isolates files and HEAD, not `refs/stash`, which is one LIFO stack shared by + every worktree: two agents stashing swap entries, `pop` reports success while restoring + the *other's* changes, and a following `git add -A` commits their half-finished work + into your PR. A hook blocks the mutating forms; safe alternatives, all inside your own + worktree: `git commit -am wip` then `git reset --soft HEAD~1`; + `git diff > /tmp/wip.patch && git checkout -- ` then `git apply /tmp/wip.patch`. +- **Doing reverse verification ("revert the fix, watch the diagnostics")? Commit the fix + FIRST.** Committed, restoring is `git checkout -- ` — the file comes + back out of a commit that really exists. Against an **uncommitted** edit, + `git checkout origin/main -- ` leaves no restore point at all: the working tree is + the only copy, the stash is banned above, and discarding local modifications is a + normal, silent, exit-0 operation — the change is simply gone, and every recovery so far + depended on the change still being in the session transcript. If you ever retype a lost + change, prove identity with `git diff` against a saved patch or `git hash-object ` + — a matching `--stat` insertion count is **not** byte-identity — then re-run the reverse + verification from the committed state, so the red/green numbers you report are + trustworthy. - **Rejection-class cases assert the envelope, not the throw.** Minimum assertion set: the error's **`code` AND `status`** (the ADR-0112 envelope). `expect(...).toThrow()` alone is not a rejection test — measured both ways it goes blind: an unfixed driver throwing a bare @@ -213,7 +228,14 @@ silence is the expected shape, never permission: not close the card** (you implemented only the actionable half; the other half sits in the decision box or was excluded by scope; say which half you left). ⛔ Never `Fixes` a card still in the decision box — merging silently closes it and the inbox filter only reads - open issues. Title and prose in **English** (GitHub artifacts are English per the + open issues. ⛔ **A negated closing sentence still closes the card it names**: GitHub's + closing-keyword parser matches `fix/fixes/fixed/close/closes/closed` and + `resolve/resolves/resolved` + `#` and ignores any negation in front — a body sentence written to + declare a card out of scope is exactly what closes it on merge. Keep closing keywords + away from other cards' numbers; write `# is not addressed here`, + `out of scope: #`, or `# remains open`. The PR body and the commit message are + parsed as **separate** sources — a clean commit message proves nothing about the body. + Title and prose in **English** (GitHub artifacts are English per the maintainer ruling of 2026-08-08 in AGENTS.md; a quoted Chinese ruling stays verbatim and untranslated — rewriting a quoted ruling is rewriting the ruling). Close the body with the **session-URL** attribution footer (see "Byte and sanitizer discipline"). diff --git a/.claude/hooks/guard-shared-stash.sh b/.claude/hooks/guard-shared-stash.sh index 6c512b3fb4..78b097ca61 100755 --- a/.claude/hooks/guard-shared-stash.sh +++ b/.claude/hooks/guard-shared-stash.sh @@ -29,10 +29,16 @@ # mirroring discipline guard-main-checkout-bash.sh already documents. # # Alternatives — no shared state, all of these work inside your own worktree: -# 1. clean re-read git checkout origin/main -- (restore: git checkout -- ) +# 1. temporary commit git commit -am wip (git reset --soft HEAD~1) # 2. patch file git diff > /tmp/wip.patch && git checkout -- # git apply /tmp/wip.patch (git apply -R to undo again) -# 3. temporary commit git commit -am wip (git reset --soft HEAD~1) +# 3. reverse verification: COMMIT THE FIX FIRST, then take it out with +# git checkout origin/main -- +# git checkout -- (restore from the commit) +# Against an UNCOMMITTED edit that same checkout silently DISCARDS the only copy at +# exit 0 — the working tree is the only place the change exists, and this hook has +# already ruled out the stash. Commit first; the restore then pulls the file back +# out of a commit that really exists (AGENTS.md carries the same rule). # 4. a second worktree for the comparison checkout # # Allowed through, deliberately: @@ -167,11 +173,13 @@ show up in your git status, which is why objectui#3430 swapped two agents' in-fl changes without an error. Use instead — no shared state, all inside your own worktree: - 1. clean re-read git checkout origin/main -- - git checkout -- # put your version back + 1. temporary commit git commit -am wip # git reset --soft HEAD~1 2. patch file git diff > /tmp/wip.patch && git checkout -- git apply /tmp/wip.patch # git apply -R to undo again - 3. temporary commit git commit -am wip # git reset --soft HEAD~1 + 3. reverse verification: COMMIT THE FIX FIRST, then + git checkout origin/main -- + git checkout -- # restore from the commit + (against an UNCOMMITTED edit that checkout silently discards the only copy, exit 0) 4. a second worktree for the comparison checkout Already allowed, no flag needed: diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index d8694a564d..0d43c935dc 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -90,8 +90,7 @@ UI 创建并勾 GitHub 连接器、UI 钉模型(会话内 create_trigger 的Rout - `needs-user-decision` vs `pm:on-hold` = **决定待做 vs 已做**,挂错招来重复升级或永久沉默 ;hold 没有重启条件 = 谁都无法合法退出的状态。**机会主义重启条件必须点名触发文件**(维护 者 2026-08-11 接受)——「下个碰这些文件的 PR 顺手带上」在命中那一刻没有读者;处方:hold 评论写 - 触发文件清单,车道座位贴设「派发前必查」段,派发卡文件面与清单相交时点名该单、顺手活列为申报 - 过的增项。 + 触发文件清单,车道座位贴设「派发前必查」段,派发卡文件面与清单相交时点名该单、顺手活列为申报过的增项。 - **`Blocked-by:` 行是机器可 grep 的反向索引**,一遍读喂三个职责:上游关单时放回被解锁的、按解 锁扇出排序选择、**在合并后的 ref 上重验每张回队卡的文件面**(⛔ 只做第一件)。**一个标签存在 ,当且仅当有具名读者**,无读者的标签必烂;已议已拒(维护者2026-08-11):「被依赖」是随上游关单衰 @@ -136,8 +135,7 @@ UI 创建并勾 GitHub 连接器、UI 钉模型(会话内 create_trigger 的Rout 行、被打断行为的 pin 反转为拒绝 pin(不是删除)、⛔ 同 PR 不做任何 promote/回退、冲突立 成 `needs-user-decision` 卡、该 PR 不挂 auto-merge 留异议窗口。 - **spec 改动的 fixture triage 必须跑消费包测试**(A 包的改动可让 B 包的 fixture 反着断言 - ,spec 范围内任何 sweep 都看不见)—— 动契约面的派发令点名消费包测试清单,报告要有各消费包真实 - 读数。 + ,spec 范围内任何 sweep 都看不见)—— 动契约面的派发令点名消费包测试清单,报告要有各消费包真实读数。 ## 多仓协调(五条规则) @@ -323,8 +321,10 @@ UI 创建并勾 GitHub 连接器、UI 钉模型(会话内 create_trigger 的Rout 最小字段一次拿回全车道 open 集,各状态本地求交 —— 按单一 pm 状态切片的查询看不见其它状态,是结 构性盲区。候选 = open、未 assign、无`needs-user-decision`;已排队父单的 open sub-issue 自动是 候选(`pm:epic` 父单的子树除外)。**每张候选读全文 + 全部评论**(裁决落在评论区,跳过评论就是跳 -过裁决;评论还可能记着一半工作已交付);**派发前做 stale-premise check**(issue 描述的是立单当天 -的仓 —— `git log --oneline -20 -- ` 花几分钟,不查则赔一次 agent-run)。 +过裁决;评论还可能记着一半工作已交付);**派发前做 stale-premise check —— 裁决同罪**(issue 与 +裁决描述的都是当时的仓:裁决是针对某个仓库状态的判断,写得权威、日期又近,恰恰更容易被当成现成 +事实 —— `git log --oneline -20 -- ` 之外,裁决实施卡再核被点名的动作在 `origin/main` 上 +还没被做掉;花几分钟,不查则赔一次 agent 运行)。 **批次独立性。** 一批内任两单不得可能碰同一个包/registry/barrel/spec schema;拿不准就串行。**同 文件单跨轮硬串行;延后不是搁置**(被延后那一刻就把已知的坑记到该 issue 上)。**维护者明示豁免同 @@ -404,7 +404,8 @@ dev 侧推分支要早 —— 远程分支是在飞工作最硬的证据。**Sta - **标准非协商条款 ⛔ 不抄进派发词** —— 已下沉进 `.claude/agents/os-dev.md`;无条件条款只能住 在角色文件(派发词与它冲突时它胜),错了就修那里,⛔ 不靠派发词临时覆盖。**清单、路径、行号在 派发那一刻从树上取**,⛔ 不从卡片/上次派发/记忆抄(取数 `node scripts/pm/dispatch-gates.mjs` - ;取数的是 PM 不是 dev —— dev 只跑被点名的门禁族,全 farm 归 CI);行级断言转述前必须自己重验。 + ;取数的是 PM 不是 dev —— dev 只跑被点名的族,全 farm 归 CI;但点名单是线索不是规格,dev 用同 + 一脚本对实际改动重取一次、补跑漏点名的族并报告点名,条款在 os-dev 定义);行级断言转述前必须自己重验。 - **文件面写两句**(预期落点 + 生产者在别包时报备后按生产者侧修,⛔ 不在消费者侧打补丁 );**same-day churn 行**(当天合并 ⇒ 先核对当前 main);**在飞重叠拦截**(每轮求交,相交即发四句 警告,被完全覆盖就停下回报 ⛔ 不硬造 diff)。 @@ -470,14 +471,14 @@ You are the reviewer of record —— **对 GitHub 核验,不对报告的自述 - **PR 形态与范围**:draft、目标 `main`、首行 **`Fixes #` 仅当合并应当关卡** ——半实施必 须 `Part of #`,否则合并静默关掉决策箱里的卡(收件箱过滤只看 open);翻 ready 前亲核首行。 - changed files 范围检查(⛔ 不看报告自述);tests/docs-only 走`skip-changeset` 标签;测试证据要 - 真实命令与输出。 + **`Fixes` 卡随关单自动离开在飞视图,`Part of` 卡合并后仍开着**:MERGED 的同一动作里摘 + `pm:dispatched` 换回 `pm:queue`(或按剩余物定级)+ 评论写明已交付/还剩/归谁。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 翻转 + 拒收断言仍在;收益穿过必经边界后还在吗(必要时端到端验一次) - 。 + 序;裁决实施 PR 查全仓 pin 翻转 + 拒收断言仍在;收益穿过必经边界后还在吗(必要时端到端验一次)。 - **证伪是好运行**:`premise_still_valid: false` 是再分诊输入不是失败;dev 纠正 PM要当众认;验 收判据被测量推翻 = 好运行,但过程要写在 PR 正文并附实测信噪比。 - **删除与二进制**:死代码删除在 `origin/main` 亲核引用面再 ACCEPT;`+0/-0` 先疑NUL。sweep 类 @@ -528,8 +529,7 @@ blockers**(三仓 `target:`之和,归零 = 三张板都空)。 续,⛔ 不跑到被自动压缩。四步有序:① 收工点 = 在飞归零(⛔ 不在复核中途压缩);② 收工存档 = 把只 存在于上下文里的判断 flush 到 GitHub(「不过夜」的既有义务);③ `/compact` 由维护者执行;④ 压缩 后再派下一波第一张,⛔ 不抢派。同席压缩保全会话绑定(订阅、定时器、座位贴会话 ID),换人轮换降为 -班末动作;Routine 座位不适用。前提是 GitHub 恒为唯一权威 —— 它失守时压缩从丢缓存退化为丢判断 -。 +班末动作;Routine 座位不适用。前提是 GitHub 恒为唯一权威 —— 它失守时压缩从丢缓存退化为丢判断。 ### 停止条件与待命 diff --git a/.claude/skills/pm-dispatch/references/dispatch-runbook.md b/.claude/skills/pm-dispatch/references/dispatch-runbook.md index f1c90ce60c..7370457cee 100644 --- a/.claude/skills/pm-dispatch/references/dispatch-runbook.md +++ b/.claude/skills/pm-dispatch/references/dispatch-runbook.md @@ -101,8 +101,12 @@ SendMessage 附执行姿态句(「前台(阻塞)同步执行全部步骤,中途 - **清单、路径、行号在派发那一刻从树上取**,⛔ 不从卡片/上次派发/记忆抄(门禁清单单 班之内就会过期)。取数:`node scripts/pm/dispatch-gates.mjs <改动路径>…` 产出本卡 该跑的门禁族,填进派发令;取数的是 PM 不是 dev —— dev 只跑被点名的几族,全 farm - 归 CI 跑一次,点名的准确性从此是 PM 独担的。卡片或分诊评论里的行级断言,转述进派 - 发令前必须自己重验一遍。 + 归 CI 跑一次(⛔ 不让 dev 枚举全 farm,这半边不变)。**但点名单是线索不是规格**: + 当天现取、认真读过的清单照样会不完整,实测漏点名的门恰好被改动真实打红过。⇒ dev + 在跑完被点名的族之后,用同一条取数命令对**实际改动路径**重取一次,发现清单漏掉、 + 而自己改动确实触及的族,补跑并在报告里点名(dev 侧条款住在 os-dev 定义)—— 一次 + O(1) 取数,换掉「点名准确性由 PM 独担」这个单点。卡片或分诊评论里的行级断言,转 + 述进派发令前必须自己重验一遍。 - **文件面写两句**:「预期落点是 X;若实测表明真正的生产者在别包,报备后按生产者侧 修(落点与理由写进报告和 PR 正文),⛔ 不在消费者侧打补丁」—— 只写一个路径名的派 发令,是要求 dev 在守约与修对之间二选一。跨包常常等于跨车道,PM 侧的对价是事后补 @@ -129,6 +133,12 @@ SendMessage 附执行姿态句(「前台(阻塞)同步执行全部步骤,中途 重验后逐面抄进派发令**(⛔ 不凭记忆),并带:「每一面都必须在 PR 正文有一个结 论:已改 / 本就合规(证据)/ 明确不在范围(理由);⛔ 静默略过 —— 评审把没提到 的面读作漏掉的面」。防的不是做错,是做对了一部分然后以为做完了。 + - 改动触及**已发布包**(`package.json` 的 `private` 不为 true)⇒ 「用户可见行为 + 改动**必须**带 `.changeset/*.md`;判据是包的发布状态,不是改动大小 —— 缺了则合 + 进 main 却永不发布,看起来像修好了,比不合更糟」(ACCEPT 侧对账在复核清单)。 +- **测量先行卡**(卡面是一个问题,修复由测量结果**有条件授权**)⇒ 派发令写明「⛔ 测 + 量存在之前不写修复」—— 测量姿态让 dev 看得见卡面没列的探针,实现姿态只看得见要交 + 的 diff。 - **Premise-first 写明**:issue 正文是线索不是规格,先对 origin/main 验前提; `premise_still_valid: false` + 无 PR 是合法且常常有价值的交付 —— 派发词预设 issue 为真,就把好运行变成表面抗命。 diff --git a/.claude/skills/pm-dispatch/references/landing-operations.md b/.claude/skills/pm-dispatch/references/landing-operations.md index cd4e7edb14..4ccae9bbd4 100644 --- a/.claude/skills/pm-dispatch/references/landing-operations.md +++ b/.claude/skills/pm-dispatch/references/landing-operations.md @@ -38,6 +38,13 @@ 确认 **MERGED** —— 每轮同时读队列分支与 `origin/main` 两个读数。ready 与 auto-merge 的顺序不可反(转回 draft 会同时掉 auto-merge 与队列成员资格)。 +**确认 MERGED 的同一动作里给 `Part of` 卡收口**:`Fixes` 卡 GitHub 代关、标签随卡 +离开在飞视图;`Part of` 卡仍开着,`pm:dispatched` 不摘就把一张无 dev、无分支、无在 +飞物的卡永远算在 `label:pm:dispatched is:open` 里。摘标(换回 `pm:queue` 或按剩余 +物定级)+ 一条评论(已交付什么、还剩什么、剩下的归谁)与 MERGED 确认是一个动作, +⛔ 不拆成「下轮巡检再摘」。同刻顺手读一次相关卡的 `closed_by_pull_requests`,确认没 +有别的卡被正文里的闭合关键词误关(事实表见平台读数)。 + **落地窗口给关键 PR 挂 `subscribe_pr_activity`**(会话型座位专用;Routine 座位每 fire 新会话收不到,维持轮询): diff --git a/.claude/skills/pm-dispatch/references/platform-readings.md b/.claude/skills/pm-dispatch/references/platform-readings.md index 4d56ca3c57..6f50a4e77d 100644 --- a/.claude/skills/pm-dispatch/references/platform-readings.md +++ b/.claude/skills/pm-dispatch/references/platform-readings.md @@ -83,6 +83,20 @@ 接。dev 的临时目录(`.os-scratch/` 一类)是工作物不是交付物,清掉,⛔ 不进 feature PR。 +## 闭合关键词解析(PR 正文写侧) + +- **PR 正文里「不修某卡」的否定句会关掉那张卡**:GitHub 的闭合关键词解析器匹配 + `fix/fixes/fixed/close/closes/closed/resolve/resolves/resolved` + `#N`,**不理会前 + 面的否定词** ——「nothing here fixes #N」在合并时照关 #N,而写这句话的动机恰恰是声 + 明不修;好实践(读了兄弟卡、显式划界)反而制造了失效。安全写法:把号码放在没有关 + 键词打头的位置 —— `#N is not addressed here` / `out of scope: #N` / + `#N remains open`。 +- **PR body 与 squash commit message 是两个独立解析源**:commit message 只有 + `Fixes` 首行、看起来干净,不代表 body 干净 —— 只查 commit 会漏。误关的卡以 + `completed` 状态对一切「只看 open」的过滤与巡检隐身,没有任何机械守卫覆盖这条路 + 径(duplicate-fix-guard 比的是两个 PR 抢同一张卡,这是一个 PR 多关了一张没人抢的 + 卡);消费侧检查 = 合并后读 `closed_by_pull_requests`(在复核清单)。 + ## 断粮检测与跨墙恢复细则(5 小时用量墙) - **检测**:`npx ccusage blocks` 在容器内可用(读本地会话记录),两个有用读数 —— diff --git a/.claude/skills/pm-dispatch/references/review-checklist.md b/.claude/skills/pm-dispatch/references/review-checklist.md index ef353a867a..4f54d579b2 100644 --- a/.claude/skills/pm-dispatch/references/review-checklist.md +++ b/.claude/skills/pm-dispatch/references/review-checklist.md @@ -8,12 +8,29 @@ `Part of #`,否则合并会静默关掉一张正躺在决策箱里的卡,而 `needs-user-decision` 的收件箱过滤只看 open issue —— 卡一关,待裁问题就此无人可 见。翻 ready 之前亲核首行,别只信报告。 +- **`Part of` 收口的卡不会自动关,`pm:dispatched` 必须手工摘**:`Fixes` 卡由 + GitHub 关闭时标签随卡一起离开在飞视图;`Part of` 卡合并后仍然开着,标签留在原 + 地,于是 `label:pm:dispatched is:open` 把一张没有 dev、没有分支、没有任何在飞物 + 的卡算进在飞。⇒ **PR MERGED 的同一动作里**把 `pm:dispatched` 换回 `pm:queue` + (或按剩余物定级),并留一条评论写明**已交付什么、还剩什么、剩下的归谁**。 + ACCEPT 一张 `Part of` PR 的那一刻就把这步记进落地待办,⛔ 不留给「下次巡检看到再 + 说」—— 漏摘的标签让在飞视图数进一张无人认领的开卡。 +- **合并后读一次每张相关卡的 `closed_by_pull_requests`**:确认 PR 应当关的卡关 + 了,**并确认没有别的卡被一并关掉** —— 闭合关键词解析器不理会否定句(正文里「不 + 修某卡」的那句话恰恰会关掉那张卡,细则见平台读数事实表),PR body 与 commit + message 分开解析,只查 commit 会漏;误关的卡以 completed 状态对一切「只看 + open」的过滤隐身,这一读是唯一能兜住它的机械检查。 - **范围检查**(取 changed files,⛔ 不看报告自述):无 `content/docs/releases/` 改 动、用户可见改动有 changeset、无与卡无关的文件。Tests/docs-only PR 走 `skip-changeset` 标签,不走空 changeset(空 changeset 滞留发布);含读者可见生成 产物时 dev 选 changeset 是对的 —— 以 PR 正文说明的理由为准,两条路都有效,别来回 改。 -- **测试证据**要有真实命令与通过输出,不是一句 tests pass。 +- **改动触及的每个包,`private: false` 即已发布 ⇒ 核 changeset 在不在**:判据是包 + 的发布状态(读 `package.json`,十秒),不是改动大小,也不是「用户可见」的感觉判 + 断 —— 那个判断 dev 在时间压力下会乐观化。⛔ 缺了不入队 —— 合进 main 却永不发布, + 是比不合更糟的终局:它看起来像修好了。 +- **测试证据**要有真实命令与通过输出,不是一句 tests pass。**测量类交付先看阳性对 + 照**:对照本身失败 ⇒ 该读数记 INCONCLUSIVE,⛔ 不把它的「绿」当被测风险的证据入账。 - **CI 收敛读数只属于复核侧**(维护者 2026-08-10 裁定;dev 的契约是草稿 PR 时点交 报,报告里 gate `in_progress` 是诚实读数、预期内常态):翻 ready / 挂 auto-merge / 入队前亲核门禁 job 结论 —— ESLint 与 TypeScript Type Check 两个 job 的