Skip to content

Commit e2086fe

Browse files
os-zhuangclaude
andauthored
docs(pm-dispatch): 增补 2026-08-03 运行沉淀的八条 PM 操作经验 (#4882) (#4885)
按落点融入既有结构,纯插入不重写: - step 2 分诊:「无生产者」形状(声明了、消费端有读取、gate 全绿,却无人写值) - step 3 选批:同文件跨轮串行,推迟时把已知的坑当场钉到 issue 上 - step 5 派发:同日 main 变更写进派发词;新增 worktree 接手协议(宿主中断的 dev 不可恢复但 worktree 完好) - step 7 复核:dev 证伪前提是好报告的标志;`+0/-0` 先怀疑 NUL 字节 - 新增 Operational notes(实测坑位):gh-readonly-queue 判入队、队列踢出先认签名、 GraphQL 配额与 rate_limit 轮询、核验 main 一律用 origin/main Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny Co-authored-by: Claude <noreply@anthropic.com>
1 parent 55e7cec commit e2086fe

2 files changed

Lines changed: 160 additions & 0 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
---
3+
4+
docs(pm-dispatch): 增补 2026-08-03 全天运行沉淀的八条 PM 操作经验 (#4882)
5+
6+
`.claude/skills/pm-dispatch/SKILL.md` 按落点增补,不重写既有章节:
7+
8+
- **分诊(step 2)**:「无生产者」是一类要主动识别的 issue 形状 —— 声明了、消费端有读取、
9+
gate 全绿,却没有任何生产者写值,类型与 lint 只验消费方,所以这类洞在全绿下长期存活
10+
(#4704 / #4837 / #4839 / #4862 / #4867)。
11+
- **选批(step 3)**:同文件 issue 跨轮严格串行,且**推迟 ≠ 搁置** —— 已知的坑要在同一轮
12+
就钉到被推迟的 issue 上,否则下一次 sweep(可能是别的 PM)会照原文派出去(#4820/#4821
13+
`JSON.stringify` 键法会改掉类型强制语义)。
14+
- **派发(step 5)**:「main 当天已动过同一文件」要写进派发词(#4808/#4806#4820/#4822);
15+
新增「worktree 接手协议」—— 宿主中断(`/compact`)杀死的 dev 不可恢复但 worktree 完好,
16+
接手方式是派新 agent、禁止新建 worktree、先通读既有改动再决定取舍、补跑未跑的验证、
17+
认领与分支不变(#4700 / #4775)。
18+
- **复核(step 7)**:dev 证伪 issue 或派发词是好报告的标志,照单全收的报告反而要多看两眼
19+
(#4808 / #4813 / #4825 / #4790);PR diff 里的 `+0/-0` 先怀疑 NUL 字节再怀疑空文件,
20+
判据落在 blob 不落在 diff(#4870)。
21+
- **新增「Operational notes(实测坑位)」小节**:入队判据看 `gh-readonly-queue/*` 分支而非
22+
`auto_merge` 字段;队列踢出先认签名再决定重投,已修 flaky 的旧签名再现必须重新诊断
23+
(#4796 / #4856);GraphQL 配额 5000/时 易打满,读与评论走 REST、写操作排队轮询
24+
`rate_limit`;核验 main 的事实一律用 `origin/main`(共享检出可能落后数十提交)。
25+
26+
仅改内部 agent 协议文本,不发布任何包。

.claude/skills/pm-dispatch/SKILL.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,58 @@ gh label create repo:cloud -R objectstack-ai/objectstack -c c5def5 -d "Lands
6868
(Use the GitHub MCP tools instead of `gh` when the CLI is unavailable — the
6969
protocol is identical.)
7070

71+
## Operational notes(实测坑位)
72+
73+
队列与平台层的四条实测结论。共同点:**判据取命令的输出,不取 API 字段的字面值,
74+
也不取本地工作树的现状** —— 每一条都是在这一步上咬过人之后写下来的。
75+
76+
**1. 判断 PR 是否在合并队列,看 `gh-readonly-queue/*` 分支,不看 `auto_merge`
77+
字段。** 本仓 PR 入队后,REST 返回的 `auto_merge` 回落为 off(队列条目取代了挂起的
78+
auto-merge),该字段对「在不在队列里」零信息量,据它反推会得出「没入队,再入一次」
79+
的错误结论。唯一判据:
80+
81+
```bash
82+
git ls-remote --heads origin 'refs/heads/gh-readonly-queue/*'
83+
```
84+
85+
分支名里带着这一批被打包的 PR 号;没有匹配分支才是真的没入队。
86+
87+
**2. 队列踢出:先认签名,再决定重投。** 被踢出不等于 PR 有问题。今天 #4796 那条已知
88+
flaky 连踢五个互不相关的 PR,核对失败签名一致后原样重投,五个全部一次通过 —— 认签名
89+
的成本远低于逐个改 PR。但反过来是同等硬度的规则:止血 PR(#4856)合入后,**同一签名再次
90+
出现就不再是那条 flaky**,已修签名的再现是新问题,必须重新诊断,禁止条件反射式重投。
91+
给 flaky issue 追记命中次数,只在新信息改变修法作用域时才值得(第 3/4 次命中把靶面从
92+
一条用例扩到整个模板,值得记;纯计数不值得占用 issue 时间线)。
93+
94+
**3. GitHub MCP 的 GraphQL 配额(5000/时)极易打满,读操作与评论一律走 REST。**
95+
今天三次归零(峰值 10402/5000),每次卡死的都是 `enable_pr_auto_merge`、draft 状态
96+
切换、`list_issues` 这几个 GraphQL-only 操作 —— 配额一空,整个循环停在复核与入队上。
97+
规程三条:
98+
99+
- 读与评论优先 `curl` / `gh api` 走 REST(core 配额 15000/时,与 GraphQL **独立计**),
100+
只有确实没有 REST 对应物的写操作才花 GraphQL 配额;
101+
- 配额打满时把 GraphQL 写操作**排队而不是重试**,后台轮询 `rate_limit`
102+
`resources.graphql.remaining`,恢复即执行:
103+
104+
```bash
105+
gh api rate_limit --jq '.resources.graphql' # 或 curl https://api.github.com/rate_limit
106+
```
107+
108+
- 复核意见不等配额 —— 先用 REST 评论把结论发出去,入队、切 ready 这类 GraphQL 动作
109+
事后补;维护者拿到的信息不该被配额延迟。
110+
111+
**4. 核验 main 的事实用 `origin/main`,不用共享检出的工作树。** 共享检出的 HEAD 由
112+
别的 agent 摆布,可能落后 origin/main 数十提交(今天 PM 与一名 dev 都在落后 63 提交的
113+
树上 grep 出假阴性,据此差点判了错误的结论)。一律先 `git fetch origin main`,再:
114+
115+
```bash
116+
git grep <pattern> origin/main -- <paths> # 而不是在工作树里 grep
117+
git show origin/main:<path> # 看某个文件在 main 上的现状
118+
```
119+
120+
这条也写进派发词(step 5):dev 在自己的 worktree 里同样会踩,而 worktree 是从
121+
`origin/main` 切的、后续不会自己更新。
122+
71123
## Multi-repo coordination (backend / frontend / cloud)
72124

73125
The product spans three repos with a fixed dependency direction:
@@ -333,6 +385,18 @@ routing isn't already decided:
333385
branch, no PR is invisible — that is what the claim-first rule (step 4)
334386
exists to shrink.
335387

388+
**Recognize the "no producer" shape —「生产者在哪?」is a standing triage
389+
question.** One issue class is invisible to every automated check: a field is
390+
declared, consumers read it, types and gates are fully green — and **no code
391+
path ever writes it**. Five hits in one day: #4704 (`Seed.env`, six call sites
392+
drop it), #4837 (the liveness ledger's own criterion), #4839 (`session.roles`
393+
written nowhere in the repo), #4862 (flow triggers bulk-set `previous` without
394+
binding it), #4867. Type systems and lint validate the **consumer** side only,
395+
so a missing 生产者 survives indefinitely under a green tree. On any issue
396+
shaped `declared ≠ enforced`, ask where the producer is before routing it —
397+
the answer is usually the root cause, and it changes the issue's scope (and
398+
often its `domain:*` label) *before* dispatch rather than in the dev's report.
399+
336400
### 3. Select the batch
337401

338402
Pick up to `batch` issues that are **mutually independent**: no two issues in
@@ -343,6 +407,17 @@ the next round. Prefer small, well-specified issues; an issue with no acceptance
343407
criteria you can state in one sentence is a candidate for escalation (step 8),
344408
not dispatch.
345409

410+
**Same-file issues serialize strictly across rounds — and deferring is not
411+
shelving.** Two issues on one file ride in different rounds, no exception
412+
(#4820/#4821). The part that is easy to miss: while #4820 was in flight its dev
413+
established that the fix #4821's body proposes (a `JSON.stringify` key) would
414+
change type-coercion semantics and introduce a fresh silent defect. That
415+
warning **and** the `Blocked-by:` line were written onto #4821 in the same round
416+
#4820's review closed — not the next one. A deferred issue sits in the queue
417+
looking dispatchable to every sweep, including another PM's; whatever you
418+
learned about it is worthless until it is on the issue. Rule: when step 3 pushes
419+
an issue to a later round, record the known trap on it before the round ends.
420+
346421
### 4. Claim
347422

348423
All agents share one GitHub identity, so the assignee alone says "some agent
@@ -428,6 +503,42 @@ Follow your operating procedure (you are the os-dev agent). Non-negotiables:
428503
Return ONLY the JSON report defined in your agent definition.
429504
```
430505

506+
**Same-day churn on the issue's files goes INTO the prompt.** Step 1's
507+
stale-premise check protects against issues that aged; the same-day variant is
508+
main moving between filing and dispatch on the very file the issue quotes —
509+
#4808 was dispatched right after #4806 rewrote the same guard, #4820 right
510+
after #4822 touched the same file. Both prompts carried an explicit line
511+
(「基于合并后的代码工作,issue 引用的片段可能已变,先核对当前 main」), and both
512+
devs avoided rework that the issue's own snippets would have caused. Add that
513+
line whenever `git log origin/main --oneline -20 -- <paths>` shows a merge on
514+
the issue's files today, and tell the dev to verify against `origin/main`
515+
rather than any working tree (Operational notes 4) — the dev's worktree is cut
516+
from `origin/main` once and never refreshes itself.
517+
518+
#### Handing off an interrupted dev(worktree 接手协议)
519+
520+
`/compact`, and any host-level interruption of the PM session, kills running
521+
subagents together with their pending tool calls — #4700 and #4775 both died on
522+
the same second. **The agent is not resumable; its worktree, branch and commits
523+
are intact.** Never re-run the original dispatch prompt over that state: a fresh
524+
agent that follows it will try to create the worktree that already exists, or
525+
redo work already committed. Dispatch a **new** agent with these four additions
526+
instead:
527+
528+
- 「worktree `<repo>-issue-<n>` 已存在,⛔ 不要新建,`cd` 进去接着做」— the
529+
worktree-first rule is already satisfied; creating a second one splits the work;
530+
- read every existing commit **and** uncommitted change there **first**, then
531+
decide per hunk to keep or amend it — neither restart from scratch nor trust
532+
it blindly (the dead agent never reported, so nothing about it is verified);
533+
- re-run the verification the dead agent never reached, in full, and report its
534+
real output — an interrupted run leaves no test evidence at all;
535+
- the assignee, claim comment and branch stay untouched: this is a continuation
536+
of the existing claim, not a new one, and the claim comment records the
537+
handoff rather than being replaced.
538+
539+
Both #4700 and #4775 passed review on the first try after being handed off this
540+
way.
541+
431542
#### Resource limits — parallel agents share ONE container
432543

433544
Memory peaks come from **build + test**, not editing, so the fix is not less
@@ -493,6 +604,29 @@ against the report's own claims:
493604
- Test evidence in the report shows the actual commands and passing output,
494605
not a bare "tests pass".
495606
- The diff plausibly satisfies the issue's acceptance criteria.
607+
- **Did the dev verify the issue's premise?** A report that falsifies the
608+
issue — or your own dispatch prompt — is a sign of a *good* run; a report
609+
that accepts every stated cause at face value is the one to read twice. Four
610+
same-day cases: #4808 (the issue was half right — the real truncation was in
611+
pruning, not the TTL), #4813 (the technical rationale the PM supplied was
612+
disproved by measurement and the dev's was harder; the issue body's wrong
613+
attribution became #4873), #4825 (the issue's option 2 was killed by
614+
call-site evidence), #4790 (previous day, a fixed-window conversion
615+
rejected). When a dev corrects the PM, **acknowledge it in the open** — the
616+
correction belongs in the PR/issue comments so the next reader inherits the
617+
corrected premise, and a wrong premise still sitting in an issue body gets
618+
its own follow-up issue rather than being silently dropped.
619+
- **`+0/-0` in a PR diff is not proof of an empty file.** git renders a file
620+
as binary — zero added, zero removed — as soon as it contains a NUL byte.
621+
#4870's 347-line test file showed `+0/-0` and was briefly misread as an
622+
unfinished placeholder; it was one bare `0x00` in the body, which
623+
`pnpm check:nul-bytes` rejects by design. On any `+0/-0` entry suspect NUL
624+
first and an empty file second, and settle it on the blob rather than the
625+
diff (`git show <sha>:<path> | wc -l`). The fix always belongs in the
626+
source: write the escape sequence `\u0000`, which is byte-identical at
627+
runtime and is the convention `scripts/check-nul-bytes.mjs` enforces. A
628+
raw NUL is never the right authoring choice: it also makes the whole file
629+
invisible to grep, which is how the defect hides in the first place.
496630

497631
Verdict per issue:
498632

0 commit comments

Comments
 (0)