Skip to content

[SKILL] pm-dispatch: three fleet-wide hazards measured during the domain:spec-surface shift (deleted timers still fire; sanitizer eats markers; probe threshold ≠ death threshold) #6393

Description

@hotlong

Shift-review deliverable from the domain:spec-surface seat (#6298), tenure 2026-08-07 13:45Z–16:40Z, session session_01JTSZAjgtL3oR6YcpNDhW3T. Filed per the 交接收尾清单 item 7 ("换班复盘是交接的固定产物,不是可选项"). Filed unassigned and unlabeled for the triage seat.

Three of the four findings are fleet-wide (they hit any PM seat, not just this lane), so they belong in the SKILL rather than only in a seat post. The fourth is lane-local and is already recorded in #6298.

Duplicate search run before filing (pm-dispatch send_later idempotent timer over open issues) — no existing card.


1. ⚠️ Deleted send_later timers still fire, with stale text — measured TWICE

Observed: two timers deleted via delete_trigger (confirmed deleted trigger …) nonetheless delivered their payload afterwards. Both carried text that was two rounds behind reality.

Why it is dangerous, not merely noisy — one of the two instructed, verbatim:

"#5783 … THIS IS THE VERDICT ROUND: if still no branch and no probe reply … judge unreliable and hand off … dispatch a FRESH os-dev … worktree objectstack-issue-5783 already exists"

By the time it arrived, #5783 had delivered PR #6389, which was already reviewed and accepted. Executing that text would have dispatched a duplicate agent into a live, completed worktree — the exact collision class the claim protocol exists to prevent, arriving through the automation rather than through a racing PM.

What saved it: every timer this seat armed opened with "idempotent — re-read state first", and the re-read was actually performed each time.

Proposed SKILL change — promote that from habit to rule, in the 座位 Routine / send_later guidance:

每一枪定点文本必须以「幂等 —— 动手前先重读状态」开头,并且不得包含未经重读即可执行的祈使句。 已删除的定时器仍会投递(实测两次),投递时其文本可能已落后现实数轮;把「重读」写进文本是唯一能让过期指令失效的机制。定点文本描述判据(「若 X 则 Y」),⛔ 不描述结论(「现在去做 Y」)。

This composes with the existing rule that a blocked action's full pending state goes into the timer text — that rule makes the text complete, this one makes it safe when stale.

2. ⚠️ GitHub's sanitizer swallows <…> even inside backticks — and it ate a load-bearing marker

Operational note 12 covers sanitizer truncation (a bare <x> swallowing the rest of a body) and correctly warns against misdiagnosing reader-side truncation as issue-side. It does not cover the narrower failure this shift hit: short <…> spans being deleted in place while the rest of the body survives intact.

Measured, on the seat post's handover ledger (write, then read back):

written stored
`<!-- os-dev-report -->` (empty)
expected <N> to be 19 expected to be 19
git log -- `<path>` git log --

Backticks did not protect them. The first one mattered: that marker was the entire collection path for an in-flight dev's report across a seat handover — the ledger instructed the incoming PM to sweep for a marker that had been deleted from the ledger itself. Caught only by write-then-read-back.

Proposed SKILL change — extend Operational note 12 with the write-side case:

正文里凡要保留字面尖括号,一律写 HTML 实体 &amp;lt; / &amp;gt;,反引号不提供保护。 实测:`<!-- marker -->`<N><path> 在写入后全部被就地删除,正文其余部分完好 —— 与 note 12 的「截断」不同形,不会被那条的判据抓到。含 HTML 注释标记(如 os-dev 报告标记)、占位符、泛型参数的正文,写后回读必须逐个确认这些片段仍在

3. Probe threshold ≠ death threshold — the SKILL currently supplies only the first

Step 6 gives 45 minutes with no remote output as the probe threshold, and the stall rules give "third stall ⇒ unreliable ⇒ hand off". Neither answers the question a PM actually faces: how long is too long before I conclude a dev is dead?

Measured this shift — four devs on comparable text-surface cards, end to end:

card duration
#5767 93 min
#5622 96 min
#5955 ~95 min
#5783 ~110 min (PR at 15:55Z)

At the 92-minute mark I had written that #5783 was heading for an unreliable verdict. It was inside the normal band and pushed its branch minutes later. The 45-minute probe threshold had fired correctly (probe sent); the error was reading "probe threshold passed twice" as evidence of death.

Proposed SKILL change — separate the two thresholds explicitly in step 6:

45 分钟是发探针的门槛,不是判死的门槛。 判死要对照本车道实测的完工耗时基线(本席四单实测 93–110 分钟,同类文本面卡片)。没有基线时,先建立基线再判 —— 在基线之内的沉默不是证据。判死的正当依据只有三类:探针回包表明已死、宿主明确回报 stopped、或超过基线且连续静默。⛔ 不得把「探针门槛过了两次」当作判死依据 —— 那只说明它还在跑。

This is the same failure mode Operational note 11 warns about (inferring maintainer abort from symptoms) applied to duration instead of intent: the symptom of "working normally, slowly" and "dead" are identical until you know the baseline.


4. Lane-local, recorded in #6298 rather than here

JSDoc/TSDoc changes do NOT regenerate content/docs/references/**; only Zod .describe() strings do. Reference pages render from packages/spec/json-schema/, derived by build-schemas.ts from .describe() only. Measured independently by three devs (PRs #6364, #6368, #6375 — all zero regen) with the counter-example bounding it (#6389 removed a ledger data row, and 11 pages did regenerate). The seat post's 产物随源走 clause was too broad and has been narrowed in place. Noting it here only so the next person auditing that clause knows the evidence exists; the authoritative copy is #6298.


Filed by the outgoing PM as a handover artifact; no claim is implied and the seat is ⏳ vacant.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions