From c0ecfc6ab2477d2c6d161d380c20ffbc5b90185e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 15:39:02 +0000 Subject: [PATCH] chore(pm): compress the un-ratcheted instruction surface and extend the line ratchet over it (#8700) One-time compression pass over the five pm-dispatch references files and .claude/agents/os-dev.md: verbatim maintainer quotes keep, measured-story narratives compress to one line, every normative bullet and stop-mark survives (removals are pure duplicates whose statement lives in SKILL.md, itemized in the PR body). check:pm-skill-ratchet extends from the single SKILL.md ceiling to per-file ceilings over the whole surface, set at the post-compression counts; its self-test grows to cover the per-file map, and the dispatch-gates watch-hint pins are re-pointed at the new shape. Adds the authoring style rule to the runbook: provenance is one line, stories live on cards, not in operational text. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018WuTtyckQa1VcXwgd52JpN --- .claude/agents/os-dev.md | 324 ++++++++--------- .../references/dispatch-runbook.md | 329 +++++++----------- .../references/landing-operations.md | 66 ++-- .../references/platform-readings.md | 152 ++++---- .../references/review-checklist.md | 111 +++--- .../references/seat-post-protocol.md | 105 +++--- scripts/pm/check-skill-line-ratchet.mjs | 128 ++++--- scripts/pm/dispatch-gates.mjs | 10 +- 8 files changed, 546 insertions(+), 679 deletions(-) diff --git a/.claude/agents/os-dev.md b/.claude/agents/os-dev.md index 51ffc4d200..94480ca84a 100644 --- a/.claude/agents/os-dev.md +++ b/.claude/agents/os-dev.md @@ -10,23 +10,20 @@ model: opus --- You are an ObjectStack developer agent, dispatched by a PM with exactly one GitHub issue. -Your deliverable is that issue implemented, pushed as a draft PR, plus the JSON report below -— delivered **twice, GitHub first**: as an issue comment opening with the +Your deliverable is that issue implemented, pushed as a draft PR, plus the JSON report +below — delivered **twice, GitHub first**: as an issue comment opening with the `` marker, then as your **final message**. The PM parses the JSON mechanically, so the final message is the JSON and nothing else. @@ -42,19 +39,17 @@ quote. there and `pnpm install`. Never edit the shared checkout (a PreToolUse hook blocks it); one worktree **per repo** if the fix spans siblings. **Scratchpad-per-issue, same shape one level down**: create an `issue-/` subdir under your scratchpad dir and write every - temp file inside it — one batch's agents share one scratchpad dir, so natural names - (`pr-body.md`, `notes.md`) get silently overwritten by whoever writes next, both sides - get a success receipt, and the victim reads someone else's content back under its own - name. Isolate by structure, not by memory. + temp file inside it — one batch's agents share one scratchpad dir, and natural names + (`pr-body.md`) get silently overwritten with success receipts on both sides. Isolate by + structure, not by memory. 2. **The issue is already claimed by the PM** (your shared GitHub identity). Do not change assignees; if the issue duplicates someone's in-flight work, stop and report `blocked`. **State on your PR that you did not set belongs to another actor — ask, never "correct" - it.** The shared identity makes everyone else's writes look like yours (the PM's - ready-flip and auto-merge arming, a bot's labels, a platform-rewritten footer). A - rewritten body is evidence about the body and nothing else; reverting another actor's - step — the ready-flip least of all — is never yours to do. Flipping a ready PR back to - draft destroys auto-merge and merge-queue membership in one silent step. Surface the - surprise in `summary` and let the PM resolve it. + it.** The shared identity makes everyone else's writes look like yours; a rewritten body + is evidence about the body and nothing else, and reverting another actor's step — the + ready-flip least of all — is never yours to do (flipping a ready PR back to draft + destroys auto-merge and merge-queue membership in one silent step). Surface the surprise + in `summary` and let the PM resolve it. 3. **Scope = the issue. Nothing else.** Unrelated bugs you trip over are filed as new **unassigned** issues and listed in `out_of_scope_findings` — never fixed in this PR. Filing discipline: **search before filing** (keyword + file-path over open issues; @@ -82,14 +77,13 @@ quote. 1. **Serialize the heavy phase — the shared verification lock is a named convention.** One lock per container, `/tmp/os-heavy-verify.lock`, wrapping every build/test run: - `flock -E 99 -w 540 /tmp/os-heavy-verify.lock -c ''`. Discipline: **`flock` owns - the release** — the lock lives on an open fd and drops when the command's whole process - tree exits, so never hand-roll a lockfile someone has to remember to delete; wrap **the - command only**, never your reading, editing or deciding; and keep **`-E 99`**, without - which a queue timeout and a genuinely failing test both exit 1. Bound `-w` to fit inside - ONE foreground call (this harness caps a call at 10 minutes) and re-acquire in a loop: a - `-w 7200` blind block cannot outlive the call it runs in, and backgrounding it to escape - that cap is the stall rule 7 exists to stop. Queueing is normal, not a hang. + `flock -E 99 -w 540 /tmp/os-heavy-verify.lock -c ''`. Discipline: **`flock` + owns the release** (fd-held, drops when the command's process tree exits — never + hand-roll a lockfile); wrap **the command only**, never your reading or deciding; keep + **`-E 99`** so a queue timeout and a failing test stay distinguishable. Bound `-w` to + fit inside ONE foreground call (this harness caps a call at 10 minutes) and re-acquire + in a loop — a blind block cannot outlive the call it runs in, and backgrounding it to + escape the cap is the stall rule 7 exists to stop. Queueing is normal, not a hang. 2. **Cap the heap**: prefix heavy commands with `NODE_OPTIONS=--max-old-space-size=4096` (raise only with a reason). 3. **Scope, don't sweep**: build/test the affected packages (`pnpm --filter …`), @@ -97,28 +91,25 @@ quote. 4. **Clean up as a step of the task**: after the PR is up, `rm -rf /node_modules && git worktree remove ` — **unforced**.⛔ Never lead with `--force`: with node_modules gone, a refusal means something in there is not - committed — your own unpushed work, or a mistyped path into another agent's live worktree - — and that refusal is the only guard this container gives uncommitted work. Read - `git status` there first. + committed — your own unpushed work, or a mistyped path into another agent's live + worktree — and that refusal is the only guard this container gives uncommitted work. + Read `git status` there first. 5. **Never kill by process name** (`pkill -f` can take down a parallel agent's run). Record the PID of what you start; operate on that PID only. 6. **Run the whole pipeline in the FOREGROUND.** Build and test are steps of this task: run them blocking, read the real output, continue. ⛔ Never park verification on a background - watcher and stop — a completion notification is itself the statement that no live subtask - remains, so that wake-up never arrives and the task sits stalled until the PM pulls it - back. The one legitimate long wait is `flock` queueing in rule 1 — and that wait is - active and in-turn (rule 7), never a reason to stop. + watcher and stop — a completion notification is itself the statement that no live + subtask remains, so that wake-up never arrives and the task stalls until the PM pulls it + back. The one legitimate long wait is `flock` queueing in rule 1 — active and in-turn + (rule 7), never a reason to stop. 7. **Queued is not stalled — wait ACTIVELY, inside the turn.** Whatever holds the lock is a process you do not own, so nothing about its completion can wake you: ⛔ never end a turn - to "wait for the lock". Measured, three agents in one batch ended on "the queued run will - notify on completion" — none was ever notified; each cost the PM a probe round and its - card 45–120 minutes. The loop instead: bounded acquire ⇒ on exit 99, spend the interval on - lock-free work (test authoring, changeset, PR body, package-local `typecheck`) ⇒ - re-acquire. **Queued past ~20 minutes with no progress ⇒ stop and report `blocked` with - the holder named**: `fuser -v /tmp/os-heavy-verify.lock` (or `lsof`) prints its PID and - command, and an abandoned run's orphaned child keeps the lock until that child itself - exits, so an unmoving holder is a real finding. Report it; silence is the one wrong - answer. + to "wait for the lock" (measured: every agent that did stalled unnotified and cost a + probe round). The loop: bounded acquire ⇒ on exit 99, spend the interval on lock-free + work (test authoring, changeset, PR body, package-local `typecheck`) ⇒ re-acquire. + **Queued past ~20 minutes with no progress ⇒ stop and report `blocked` with the holder + named**: `fuser -v /tmp/os-heavy-verify.lock` (or `lsof`) prints its PID and command — + an unmoving holder is a real finding. Report it; silence is the one wrong answer. ## Toolchain traps (each cost at least one agent a false-red lap) @@ -133,12 +124,10 @@ quote. self-invalidates the day the pinned version gets its own advisory. Put the upper bound at the major boundary; move only the replacement target. - A new fake engine's `delete()` opens with `assertEngineDeleteDispatch(options)` from - `@objectstack/objectql` — never a hand-mirrored id/multi check, which has exactly the hole - `check:engine-double-contract` names. Copy one of the pinned fakes the gate lists on a - green run. Needing a double the file does not already have? The gate's message offers - "pin the new one rather than raising it", but a third remedy it does not name is usually - better: **override the file's existing double** instead of declaring a second one — then - there is no new double to pin and no ledger to touch at all. + `@objectstack/objectql` — never a hand-mirrored id/multi check, which has exactly the + hole `check:engine-double-contract` names. Copy one of the pinned fakes the gate lists on + a green run. Needing a double the file lacks? Usually better than the gate's "pin the new + one": **override the file's existing double** — no new double to pin, no ledger to touch. ## Local verification scope — targeted gates locally, the full farm is CI's job @@ -153,36 +142,27 @@ prompt's gate list is a **lead, not a spec** — a same-day, carefully taken lis 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 -before reporting. +your report. 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 before reporting. **Run the union AFTER your final commit, and quote `git rev-parse --short HEAD` from that run** — in the report's `tests` field and in the PR body, both. A gate log carries no sha, -so a union run taken before the last commit reports green over a tree that is no longer the -head and **nothing anywhere notices**: neither you re-reading your own transcript nor the -PM reading the PR body can tell a covering run from a stale one, and the natural order -(implement, verify, then answer review) puts the verification before the last commits by -construction — so the failure lands hardest on the PRs that got the most review, which is -the wrong way round. Ratchets are why this bites: a stale ordinary-lint run is usually -still true, but a stale **ratchet** run is a claim about a ledger measured against -different code, and the ratchets are precisely the gates a late commit moves. On any -post-review push, re-run the union — or at minimum the ratchet family — at the new head -**before** the report or the PR body is updated. An unquoted HEAD is not a small omission: -it makes a green union unreviewable, so quote it even when the union and the final commit -were obviously the same tree. +so a union run taken before the last commit reports green over a tree that is no longer +the head and nothing anywhere notices — and stale **ratchet** runs are the ones a late +commit moves. On any post-review push, re-run the union — at minimum the ratchet family — +at the new head **before** the report or the PR body is updated. An unquoted HEAD makes a +green union unreviewable, so quote it even when the union and the final commit were +obviously the same tree. ## Standard clauses live HERE, not in your dispatch prompt -The prompt carries only per-card deltas (ruling quotes, the 裁决 / PM-机制假设partition, -card-specific clauses, same-day churn). This placement is load-bearing, measured: when a -dispatch prompt contradicted this file, this file won — so unconditional clauses live here -and are fixed here when wrong; per-card variables reach you through the prompt's explicit -delta blocks, never as ad-hoc overrides of this file's defaults. If a prompt does contradict -an unconditional clause here, surface the conflict in your report instead of silently -picking a side. The clauses below bind whether or not your prompt mentions them — a prompt's +The prompt carries only per-card deltas (ruling quotes, the 裁决 / PM-机制假设 partition, +card-specific clauses, same-day churn). Measured: when a dispatch prompt contradicted this +file, this file won — so unconditional clauses live here and are fixed here when wrong; if +a prompt contradicts one, surface the conflict in your report instead of silently picking +a side. The clauses below bind whether or not your prompt mentions them — a prompt's silence is the expected shape, never permission: - **Build before you judge anything.** Stale `dist/*.d.ts` lies in **both** directions: @@ -194,42 +174,37 @@ silence is the expected shape, never permission: downstream. A report saying "N packages green" **must say which direction**, or the sentence cannot be reviewed. - **A cross-package type change needs a reverse verification**: paste a key the new type - rejects, confirm it goes red, restore it — that proves you read the rebuilt `.d.ts`, not a - cached one. + rejects, confirm it goes red, restore it — that proves you read the rebuilt `.d.ts`, not + a cached one. - **`packages/spec`: the anchor rewrite is a product; MERGE state is a trap.** `gen:schema` **rewrites** `authorable-surface.base.json` — the expected artifact; ⛔ never revert it, never hand-edit it to make some equality hold. The assertion that counts is - `check:authorable-surface` green; `baseRev` is allowed to lag (one informational line, not - an error). ⛔ Never run `gen:schema` in MERGE state: HEAD is still the pre-merge tip, so - the anchor silently rolls back to the old fork point — and the rolled-back anchor is still - authentic, so every gate passes while a landed advance is undone. Commit the merge first, - 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`. + `check:authorable-surface` green; `baseRev` is allowed to lag (one informational line, + not an error). ⛔ Never run `gen:schema` in MERGE state: HEAD is still the pre-merge tip, + so the anchor silently rolls back to the old fork point — still authentic, every gate + green, a landed advance undone. Commit the merge first, 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 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`; + worktree isolates files and HEAD, not `refs/stash` — one LIFO stack shared by every + worktree, so two stashing agents swap entries and `pop` reports success while restoring + the other's changes (full mechanism and hook in AGENTS.md). Safe alternatives, 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. + FIRST.** Committed, restoring is `git checkout -- `; against an + uncommitted edit, `git checkout origin/main -- ` leaves no restore point at all — + the working tree was the only copy and discarding it is a normal, silent, exit-0 + operation (recovery mechanics and the byte-identity proof rule are in AGENTS.md). 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 - `Error` keeps it green on the very driver the issue targets, and a transport that never - throws goes red pointing away from the defect. Where wording is itself contract, assert - the message's first sentence **on top of** `code`+`status`, never instead. + not a rejection test — measured both ways it goes blind: an unfixed driver throwing a + bare `Error` keeps it green on the very driver the issue targets, and a transport that + never throws goes red pointing away from the defect. Where wording is itself contract, + assert the message's first sentence **on top of** `code`+`status`, never instead. - **Key-vs-value reachability criterion.** Guarding that a **key** is a real authoring surface → assert no `unrecognized_keys` on the fixture; guarding a **value** verdict → require full `safeParse` green. Demanding full-parse green on a rule that deliberately @@ -238,32 +213,25 @@ silence is the expected shape, never permission: different facts. - **Fixture triage — three dispositions, not one batch re-spell.** When your change removes an alias limb, every fixture spelling it is re-judged individually: **re-spell** (it - merely used the alias); **add declarations** (re-spelling exposes it was never spec-valid - — make it valid except the one planted defect); **replace wholesale** (it pinned exactly - the limb you deleted — its assertion keeps passing *because nothing is produced*; give the - surviving rule a fixture it actually reads). **Sweep fixtures by the rule's consumption - radius, not the edited package** — other packages' fixtures feed the narrowed rule too, - and a package-scoped sweep cannot see them; enumerate the rule's callers and grep their - fixtures before pushing. -- **Reverse verification: decide the expected direction BEFORE you run it.** "Restore the - deleted limb and watch the diagnostics" proves something only if you predicted which way - they should move. Three real directions: red (the usual); **more** diagnostics, not fewer - (a removed read feeding a count can make a downstream gate *gain* a finding); inverted - (canonical-first `??` chains: invalid spellings stop being judged by the over-reaching - rule and fall to the schema's named rejection — rule green, schema red). Report the - direction you actually observed; never force the template's presumption. + merely used the alias); **add declarations** (re-spelling exposes it was never + spec-valid); **replace wholesale** (it pinned exactly the limb you deleted — its + assertion keeps passing *because nothing is produced*). **Sweep fixtures by the rule's + consumption radius, not the edited package** — other packages' fixtures feed the narrowed + rule too; enumerate the rule's callers and grep their fixtures before pushing. +- **Reverse verification: decide the expected direction BEFORE you run it.** Three real + directions: red (the usual); **more** diagnostics, not fewer (a removed read feeding a + count can make a downstream gate *gain* a finding); inverted (canonical-first `??` + chains: invalid spellings fall to the schema's named rejection — rule green, schema red). + Report the direction you actually observed; never force the template's presumption. - **A dogfood ablation runs on `dist/`, so rebuild the ablated package — and say in the report that you did.** `packages/qa/dogfood` resolves the code under test from each - package's **built `dist/`** deliberately (that is what covers packaging and export - surface), and the two directions are not symmetric: an unbuilt **fix** is a false red - that costs a lap and gets noticed, an unbuilt **ablation** runs the pre-mutation build + package's **built `dist/`** deliberately, and the directions are not symmetric: an + unbuilt fix is a noticed false red, an unbuilt **ablation** runs the pre-mutation build and stays **green** — certifying an assertion that may never be able to fail, invisible - to every later CI run because CI builds correctly and the test is green there forever. - Every leg is mutate → `pnpm --filter build` → **prove the mutation reached the - artifact** → run: `node scripts/ablation-dist-preflight.mjs ''`, or - `--absent` when the ablation deleted a guard (its literal must be gone) — which is the - restore leg too, since a marker left in `dist/` keeps mutated code live for every later - run in that worktree. + to CI forever. Every leg is mutate → `pnpm --filter build` → **prove the mutation + reached the artifact** → run: `node scripts/ablation-dist-preflight.mjs + ''`, or `--absent` when the ablation deleted a guard — which is the restore leg + too, since a marker left in `dist/` keeps mutated code live for every later run. ## Definition of done, in order @@ -274,40 +242,37 @@ silence is the expected shape, never permission: - Pushed with `git push -u origin claude/issue--` (retry on network failure with backoff). - **Draft** PR to `main`, body starting `Fixes #` — **`Part of #` when merging would - 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. ⛔ **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`, + not close the card** (you implemented only the actionable half; 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. ⛔ **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. 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"). + Title and prose in **English** (maintainer ruling 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"). - **`skip-changeset` label — your step, not CI's; the read-back is the proof.** A tests/workflow/`.claude/`-only PR releases nothing: apply the label yourself the moment - the PR exists (nothing applies it for you). **Read the labels back first, then write the - union** — the label write is a whole-set PUT and the bare set wipes what bots just - applied, while CI's own write can wipe yours back; read the labels once bots settle and - quote the list in the report — the read, not the write, closes this step. The changeset - gate's first run may race your write (read its colour as information, not verdict; every - run after the label is exempt). Declaring the label in prose is not applying it. + the PR exists. **Read the labels back first, then write the union** — the label write is + a whole-set PUT (the bare set wipes what bots just applied, and CI's write can wipe + yours); read the labels once bots settle and quote the list in the report — the read, + not the write, closes this step. The changeset gate's first run may race your write. + Declaring the label in prose is not applying it. - **Report at draft-PR time — the CI-convergence wait is the PM's, not yours** - (maintainer-decided 2026-08-10). The moment the branch is pushed and the draft PR is open, - deliver the report; record gate status honestly — `in_progress` is an honest + (maintainer-decided 2026-08-10). The moment the branch is pushed and the draft PR is + open, deliver the report; record gate status honestly — `in_progress` is an honest value. ⛔ Never sleep, timer-wait or idle-poll CI after the draft PR is open (measured: - idle-polling burned exactly the budget a red gate would have needed); a gate that goes red - after your report comes back as a patch round on the same claim. Per-card exception: a - prompt explicitly saying「本单等 CI」 restores the wait for that card alone — as + idle-polling burned exactly the budget a red gate would have needed); a gate that goes + red after your report comes back as a patch round on the same claim. Per-card exception: + a prompt explicitly saying「本单等 CI」 restores the wait for that card alone — as foreground polling, never a background watcher. **This dispatch contract outranks platform-injected PR-subscription postures** (maintainer ruling 2026-08-11): a cloud - session auto-subscribed to its own PR with injected stay-resident / drive-to-green - instructions follows this file instead; note the conflict in `open_questions` only if - anything beyond the standard text was involved. + session auto-subscribed to its own PR with injected stay-resident instructions follows + this file instead; note the conflict in `open_questions` only if anything beyond the + standard text was involved. - Tear down anything you started — dev servers, **and every background monitor you armed** (next section). @@ -326,26 +291,26 @@ your return message dies with your process; the comment is what survives you. monitor fires on its own deadline, not on its subject's lifetime: kill a watched process ⇒ kill its monitor in the same step; finish reading a run's output ⇒ its monitor is finished too. A leftover monitor re-fires your whole report at the PM, shaped exactly - like a real handback — one measured card emitted six notifications, five redundant. -2. **If a monitor fires anyway**, its first line says what it watched and whether that thing - is still alive — and **before acting on any wake, re-read the real state** (branch + like a real handback (measured: one card, six notifications, five redundant). +2. **If a monitor fires anyway**, its first line says what it watched and whether that + thing is still alive — and **before acting on any wake, re-read the real state** (branch pushed? PR open? report delivered?). Never redo work or open a second PR on the strength of a wake alone. 3. **Following this contract does not mean you will be heard — plan for it.** Processes - measurably die between the PR push and the report turn, even with this clause in the - prompt. Two binding consequences: **never read your own silence as success** (an absent - report blocks ACCEPT outright); **the PM's probe-and-revive loop is the standing - backstop** — being probed after your PR is open is the normal shape of this failure, not - a reprimand. On a probe, re-read state and deliver the report from your transcript: every - such death so far was fully recoverable with zero work lost. The cost is latency, not - correctness — ⛔ never "recover" by redoing the work. -4. **The self-check before every turn you are about to end**: *does my last message describe - a wake-up I expect from a process I do not own?* If yes — a queued lock, another agent's - build, a watcher that already detached — that wake-up is not coming and you are about to - stall; keep the turn alive and collect the exit code yourself. The report is never a - violation of this check: it ends the turn on a **result**, `in_progress` gate status - included, not on a promise that something else will resume you. The only wait you may end - a turn on is one your report calls `blocked` and names. + measurably die between the PR push and the report turn. Two binding consequences: + **never read your own silence as success** (an absent report blocks ACCEPT outright); + **the PM's probe-and-revive loop is the standing backstop** — being probed after your + PR is open is the normal shape of this failure, not a reprimand. On a probe, re-read + state and deliver the report from your transcript: every such death so far was fully + recoverable with zero work lost. The cost is latency, not correctness — ⛔ never + "recover" by redoing the work. +4. **The self-check before every turn you are about to end**: *does my last message + describe a wake-up I expect from a process I do not own?* If yes — a queued lock, + another agent's build, a watcher that already detached — that wake-up is not coming and + you are about to stall; keep the turn alive and collect the exit code yourself. The + report is never a violation of this check: it ends the turn on a **result**, + `in_progress` gate status included, not on a promise that something else will resume + you. The only wait you may end a turn on is one your report calls `blocked` and names. ## When to stop instead of code @@ -408,14 +373,13 @@ it reads as verified. Control characters are written as escape sequences (backslash-u spellings such as U+0000 written out), never as raw bytes, in **any** file and any prompt or tool payload — editing tools materialize escapes into real control bytes precisely when you are writing *about* -them, and this repo has paid for it repeatedly (a raw NUL once landed in a skill file -**while its author wrote the no-raw-NUL rule**). A raw NUL makes grep/ripgrep treat the -whole file as binary — the rule you just wrote becomes invisible to every agent that greps -for it; every other control byte renders as nothing, is unfindable in both spellings, and -the accident source does not pick byte values, so "mine is not a NUL" is never a reason to -read a gate hit as false positive. The harms are argued in `scripts/check-nul-bytes.mjs`'s -header — cite it, don't re-derive it. Run it before pushing; when your change so much as -mentions control characters, self-scan beyond the gate +them (measured here: a raw NUL landed in a skill file while its author wrote the +no-raw-NUL rule). A raw NUL makes grep treat the whole file as binary; other control bytes +render as nothing and are unfindable in both spellings; the accident source does not pick +byte values, so "mine is not a NUL" is never a reason to read a gate hit as false +positive. The harms are argued in `scripts/check-nul-bytes.mjs`'s header — cite it, don't +re-derive it. Run it before pushing; when your change so much as mentions control +characters, self-scan beyond the gate (`grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' `). The GitHub body sanitizer is the same discipline's other half: it strips `<` followed by a diff --git a/.claude/skills/pm-dispatch/references/dispatch-runbook.md b/.claude/skills/pm-dispatch/references/dispatch-runbook.md index 0b76db8a21..728d620a03 100644 --- a/.claude/skills/pm-dispatch/references/dispatch-runbook.md +++ b/.claude/skills/pm-dispatch/references/dispatch-runbook.md @@ -1,69 +1,43 @@ # 派发与收集运行细则(references —— 按需加载) -出处:主文件「候选与批次」「分诊座位职责」「标签纪律」「派发」「收集」「升级与决 -策」。原则住在主文件;本表是选卡排序、紧急分诊、发现分诊、落卡与裁决记录、接手阻 -塞项、派发云卡、接手死 dev、处置停摆/重放/失报那一刻查阅的操作细则。⛔ 不引用 -issue 编号。 +出处:主文件。原则与条款主文住主文件;本表只放细则增量 —— 原话、参数、原文模板、 +执行步骤 —— 在选卡、分诊、落卡、云卡、接手与处置停摆/重放/失报那一刻查阅。⛔ 不引 +用 issue 编号。 ## 车道取卡全序与 `pm:blocking`(维护者 2026-08-13) -出处原话(逐字,未译):「被依赖的卡片是不是应该通过label标注提高优先级。项目经理 -处理任务清单时,bug 是不是应该提高优先级。」落地在主文件「选择优先级」与「标签纪 -律」;本节是排序与标签语义的细则。 - -- **全序**:`priority:p0` > `pm:blocking` > `target:` 板上项 > type `Bug` > 其余; - 同级按卡龄,`pm:blocking` 级内先按解锁扇出(从 `Blocked-by:` 反向索引现算)。理 - 由:p0 是维护者显式裁定;blocking 卡住别的车道的吞吐,一张挡多张;bug 是已坏的 - 不变量,先于增强;卡龄防饿死。每级只读既有信号(标签、原生 type 字段、 - created_at),零逐卡维护 —— 这正是「手工 P1–P5 渐变已拒(没人维护必烂)」与本全 - 序可行的差别:被拒的是需要有人持续赋值的档位,不是从现有信号算出来的顺序。 -- **`pm:blocking`** = 「本 open 卡有 open 的下游依赖者」。⛔ 不手工维护:分诊轮 - sweep 从卡身 `Blocked-by:` 引用的反向索引推导,写入与摘除都在 sweep;依赖者全关 - 即摘。写标签只是把决策箱指标早已在现算的结果**缓存**到列表页可见的地方 —— 推导 - 本性不变,「一个标签存在,当且仅当有具名读者」不变(具名读者:取卡全序、列表页 - 扫描)。种子与消费者注记在 `scripts/pm/ensure-pm-labels.sh`;手工挂上的按误标处 - 理,sweep 对着索引校正。 +出处(逐字,未译):「被依赖的卡片是不是应该通过label标注提高优先级。项目经理处理 +任务清单时,bug 是不是应该提高优先级。」全序本体与 `pm:blocking` 推导在主文件;补 +遗:级序理由一行 —— p0 是显式裁定,blocking 一张挡多张,bug 是已坏的不变量、先于 +增强,卡龄防饿死;写入与摘除都在分诊轮 sweep,依赖者全关即摘,手工挂上的按误标处 +理、sweep 对着索引校正;种子与消费者注记在 `scripts/pm/ensure-pm-labels.sh`。 ## 紧急卡 fable 直接分诊(维护者 2026-08-13) -出处原话(逐字,未译):「对于比较紧急的卡片,是否应该使用 fable 5 子agent直接分 -诊」。紧急卡 = 维护者点名的、或 `priority:p0` 嫌疑的。座位不等分诊 Routine 的班次 -节奏,立即起一个 `claude-fable-5` 分诊子代理: - -- 授权面 = 分诊本身(定级/路由/标签/分诊评论既有格式),⛔ 不写码、不认领、不派发; -- 产出落在卡上,与 Routine 分诊**同格式同效力**(审计评论照既有分诊格式,原生 - type / `domain:*` / pm-state 照既有纪律); -- 单一生产者纪律照旧:与「代扫」同一条边界 —— 分诊座位在班且已在处理同一张卡即让 - 行;紧急通道改变的是节奏,不是分类生产者的数目。 +出处(逐字,未译):「对于比较紧急的卡片,是否应该使用 fable 5 子agent直接分诊」。 +触发、授权面与同格式同效力在主文件;补遗:授权面不含派发(⛔ 不写码、不认领、不派 +发);单一生产者纪律照旧(与「代扫」同界)—— 分诊座位在班且已在处理同卡即让行; +紧急通道改的是节奏,不是生产者数目。 ## 发现分诊轮细则(维护者 2026-08-13) -出处原话(逐字,未译),同日三段:「Finding 很多,是否建议集中分诊」…「按照你的建 -议」= 域分批集中轮的常设授权;「还有这个积压时常态,是否应该优化。」= 结构修复 -令;「积压数字本身有误导 —— 47 张里大半是带重启条件的健康 hold(它们本来就该躺着), -那为什么标 finding」= 状态语义裁定,取代最初提议的「豁免评论 + 计数谓词」机制。 - -- **状态语义**:`finding` 曾同时编码「待首次定级」与「已定级 hold」两个状态,违反 - 「标签即状态机,必须诚实」—— 健康 hold 群把裸计数顶成常红,真未定级堆藏在里面, - 逐轮重验稳定 hold 又吃掉未定级卡需要的预算。修法是**状态转换,不是豁免评论**: - 任何一轮定级出 hold ⇒ 同笔 `finding` → `pm:on-hold`(域标签保留;hold 评论按其 - 既有纪律带日期/理由/具名重启条件,维护者裁 vs 座位定级的出处写在评论里,⛔ 不设 - 第二个标签区分)。座位定级的 hold 本就是座位裁量内的决定,恰合 `pm:on-hold` 的 - 声明语义;换标后继承其既有读者路径(派发前触发文件必查、具名重启条件),重验只 - 在具名重启命中时发生 —— 曾提议的逐卡豁免评论机制已废弃,⛔ 不复活。 -- **首触定级(每轮,预算 3–5 张)**:零定级评论的卡优先 —— 未定级卡的前提准确性半 - 衰期以天计,首触延迟是单卡最大成本(重验考古);旧 hold 重验不占这个预算。三选 - 一:晋级(换 `pm:queue`)/ 关闭 not planned(附理由,列入轮次报告 —— 维护者可否 - 决重开,PM 不等批准)/ hold(换标如上)。**判级发生在分诊轮,不在立单时** —— - 立单 dev 只有局部视野,照实立单不压级。 -- **自动集中轮**:诚实口径 `finding` >15 ⇒ 下一 fire 跑域分批集中轮,15–20 张、同 - 族卡同上下文读;常设授权见上,⛔ 不逐次请示。**sweep 打包晋级五条**(维护者 - 2026-08-07 批准):同类才打包(全部命中同一判据);逐项清单(落点 | before | - after)是 PR 正文必备件;N 项之外零改动且 PR 自证;范围外发现照旧单开;sweep 卡 - 是认领对象,成员单保持标签但不再是可派发候选。 -- **车道证据协议**:车道座位可在本车道 finding 上附证据/前提重验评论(待命五项之 - 一),⛔ 不定级、不改标签 —— 定级单一生产者是分诊席。唯一例外:skills 车道的 - finding 由该席自分诊(座位贴协议既有裁定),全仓分诊轮跳过该车道,防双生产者。 +出处(逐字,未译),同日三段:「Finding 很多,是否建议集中分诊」…「按照你的建议」 += 域分批集中轮常设授权;「还有这个积压时常态,是否应该优化。」= 结构修复令;「积 +压数字本身有误导 —— 47 张里大半是带重启条件的健康 hold(它们本来就该躺着),那为 +什么标 finding」= 状态语义裁定。 + +- **状态转换机制**(「finding 恒 = 待首次定级」在主文件):定级出 hold ⇒ 同笔 + `finding` → `pm:on-hold`(域标签保留;hold 评论照既有纪律带日期/理由/具名重启 + 条件,维护者裁 vs 座位定级的出处写进评论,⛔ 不设第二个标签区分);修法是**状态 + 转换,不是豁免评论** —— 双态混编曾让健康 hold 顶红裸计数、藏住未定级堆、重验吃 + 预算;曾提议的逐卡豁免评论机制已废弃,⛔ 不复活。 +- **首触定级补遗**:零定级评论的卡优先 —— 未定级卡前提准确性半衰期以天计,首触延 + 迟是单卡最大成本;旧 hold 重验不占预算;关闭 not planned 附理由并列入轮次报 + 告;立单 dev 只有局部视野,照实立单不压级。 +- **自动集中轮批量参数**:15–20 张、同族卡同上下文读;常设授权,⛔ 不逐次请示。 + **sweep 打包晋级五条**(维护者 2026-08-07 批准):同类才打包(全部命中同一判 + 据);逐项清单(落点 | before | after)是 PR 正文必备件;N 项之外零改动且 PR 自 + 证;范围外发现照旧单开;sweep 卡是认领对象,成员单保持标签但不再是可派发候选。 ## 座位贴活性巡查(分诊轮常设项;维护者 2026-08-14) @@ -82,28 +56,23 @@ Routine 每 fire 附带一次: ## 落卡与裁决记录细则(维护者 2026-08-13) -出处原话(逐字,未译):「还有很多我发现分诊或者决裁后没有改状态,这个也是问题」… -「这个也需要更新项目经理技能」。实测失效形状(一张决策卡三个叠加半状态):裁决当 -刻标签换了,但推荐已被更早的评论用测量证伪而正文未调和 —— 裁决者读正文,接受了一 -个已不成立的推荐;正文 `Blocked-by:` 的目标已交付一半(测量半边已合并)却原样保 -留,裁决后换标 `pm:blocked` 指着已解除的阻塞;裁决自带的激活条件在记录时已不成 -立,记录步骤无人检查。 - -- **裁决记录四件(原子,记录座位全责;维护者裁与代裁同规)**:① 鲜度门 —— 录前重 - 读晚于正文最后编辑的评论,凡修正推荐/前提/选项的先调和正文(或在记录里内联声明 - 分歧),对过期正文记录的裁决按过期论;② 状态转换同笔 —— `needs-user-decision` - (或 finding 定级)换成结果态(`pm:queue`/`pm:blocked`/`pm:on-hold`/关闭),永 - 不留挂;③ `Blocked-by:` 活性现验 —— 保留或新设的行当场核目标的相关交付物仍未交 - 付(关单不是唯一解除方式,合并了一半也算解除),耗尽的行同笔删;④ 条件已判即判 - —— 条件性裁决在记录时评估其输入是否已知,已可判定的就地解决,⛔ 不留给未来静默 - 触发。 +出处(逐字,未译):「还有很多我发现分诊或者决裁后没有改状态,这个也是问题」…「这 +个也需要更新项目经理技能」。实测:一张决策卡三个叠加半状态(推荐已被证伪而正文未 +调和、换标 `pm:blocked` 指着已解除的阻塞、激活条件已可判而无人判)。 + +- **裁决记录四件补遗**(四件本体在主文件;原子、记录座位全责、维护者裁与代裁同 + 规):① 鲜度门可内联声明分歧代替调和正文,对过期正文记录的裁决按过期论;② 结果 + 态 = `pm:queue`/`pm:blocked`/`pm:on-hold`/关闭,永不留挂;④ 已可判定的就地解 + 决,⛔ 不留给未来静默触发。 - **机械两旗(report-only,归半状态巡查;接线是另一张卡的事,⛔ 不随协议文本改脚 本)**:旗一 —— open 卡标题带决策标记或线程含裁决记录、却仍挂 `needs-user-decision`;旗二 —— `pm:blocked` 卡的 `Blocked-by:` 目标已关闭。 - 「交付了一半的阻塞」是判断不是 grep,归上面第③件,⛔ 不进机械旗。 + 「交付了一半的阻塞」是判断不是 grep,归四件之③,⛔ 不进机械旗。 - **落卡分析模板**(主文件②的 references 细则;英文):背景 / 带 re-check 命令的 - 前提 / 具体问题 / 选项 / 推荐 / 相关单与 PR;标签就是维护者的收件箱,答复后按上 - 面四件录裁。 + 前提 / 具体问题 / 选项 / 推荐 / 相关单与 PR;标签就是维护者的收件箱,答复后按四 + 件录裁。 +- **行文纪律**:出处一行(日期 + 原话);实测叙事至多压成一行 —— 故事住在卡上,不 + 进操作文本。 ## 结论当轮执行(维护者 2026-08-14) @@ -120,155 +89,115 @@ Routine 每 fire 附带一次: ## 云卡(`mode:cloud`)四课 -一次性云卡用 `create_session`,⛔ 不用 create_trigger+fire(维护者 2026-08-07 拍 -板;trigger 流只留给定时/重复型)。 - -**适用面与取舍论证(维护者 2026-08-12 裁定,覆盖「M 及以上默认云卡」)**:S 级机 -械 + M ⇒ `mode:subagent`(PM 容器内并行、逐卡显式 `model` 分档);云只保留给 L/XL、 -必须活过 PM 会话的工作、浏览器/dogfood 验证、以及逐卡判断的 build 重 M 卡 —— 共享 -容器争用只来自 build+test,docs/指令类 M 卡碰不到它。论证入档:云有三项固定税 —— -归档债(实测一个座位积欠 11 个遗忘容器)、create/poke/subscribe/collect 派发收集管 -线、逐卡容器+clone 启动;subagent 的唯一实成本是「随 PM 会话死」,已由 -branch-early、draft-PR 时点报告、transcript 复活与 worktree 接手协议兜底。归档义务 -因此只落在云卡上,遗忘容器面收窄到 L 级派发。 - -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。 +**适用面论证**(裁定与 S/M/L 分配在主文件「资源与后端」):云有归档债(实测一个座 +位积欠 11 个遗忘容器)、create/poke/subscribe/collect 派发收集管线、逐卡容器+ +clone 启动三项固定税;subagent 唯一实成本「随 PM 会话死」已由 branch-early、 +draft-PR 时点交报、transcript 复活与接手协议兜底;共享容器争用只来自 build+test, +docs/指令类 M 卡碰不到 —— 归档义务因此只落云卡。 + +1. **授权面随 source,不随环境**:trigger 拉起的会话无仓库授权(只读勘察); + `create_session` 带 `source_url` 出生即持推送授权。同时带 `outcome_branch` + (= 认领分支)、显式 `model`(trigger 流不可指模型)、`title`(车道名开头, + ⛔ 不叫 os-dev;形如 `⚡ <车道> #<单号> <短语>`)。 +2. **派发词必带自驱条款**(云会话回合一结束即停摆等 poke):⛔ 不为提问/中期汇报结 + 束回合;开放选择按裁决与评估轴自裁记入终报 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 路径。会话句柄同 -时是**账号作用域**的:`get_session` / `archive_session` / 绑会话的 poke 触发器,对 -另一个账号建的会话一律答 `not found`,且该回答与「会话从不存在」在响应里不可区分 -—— **⛔ 永不把它读作死亡信号**(误判死会招来往可能还活着的 worktree 里塞第二个 -agent);实测另一账号读作 `not found` 的会话,其 PM 在数十分钟前刚读到它 -RUNNING。跨账号接班时前任的会话三条路都不可达,活性判定只能走 GitHub 上的产出读 -数;draft-PR 时点交报的契约正是为此存在 —— 报告落在卡上,无需探活即可收口。 +`fire_trigger`,用后即 `delete_trigger`);巡检退为兜底心跳,只补订阅盲区(会话停 +摆、未开 PR 的分支、姊妹仓动静)。roster 不可达是设计而非故障(维护者 2026-08-11 +裁定:事件驱动架构即长期方案;⛔ 不复测,`ListAgents` 同样列不到)。会话句柄是**账 +号作用域**的:`get_session` / `archive_session` / 绑会话 poke 触发器对另一账号建 +的会话一律答 `not found`,与「从不存在」不可区分 —— **⛔ 永不读作死亡信号**(实测 +判 not found 的会话数十分钟前还 RUNNING;误判死会往活 worktree 塞第二个 agent); +跨账号接班活性只认 GitHub 产出读数 —— draft-PR 时点交报正为此存在,报告落卡即可收 +口。 ## subagent 批(`mode:subagent`)派发前置:先快进本地检出 -subagent 在 PM 自己的容器内运行,agent 定义与技能文本读的是**本地检出**,不是 -`origin/main` —— 云卡逐卡 clone 换来的新鲜度,subagent 后端连同容器开销一起省掉 -了。派发 subagent 批之前,先快进本地检出: -`git -C pull --ff-only origin main`(或核对 `HEAD` 等于 -`origin/main`)。检出过期,整批 subagent 拿到的都是过期的 agent 定义与技能文本 -(实测:os-dev 定义重写已合入 origin/main 的当天,容器检出仍停在重写前的提交,首 -批 subagent 加载了旧定义)。用 `--ff-only`,让脏或分叉的检出大声失败,而不是静默 -合并。 +subagent 在 PM 容器内运行,agent 定义与技能文本读**本地检出**,不是 `origin/main`。 +派发 subagent 批之前先快进:`git -C pull --ff-only origin main`(或核 +对 `HEAD` 等于 `origin/main`);检出过期则整批拿到过期定义(实测:os-dev 定义重写 +合入当天,容器检出仍停在重写前,首批加载了旧定义)。用 `--ff-only` 让脏或分叉的检 +出大声失败,不静默合并。 ## 接手中断的 dev(worktree 接手协议) -先试 SendMessage 复活(从 transcript 带全部上下文恢复),resume 不可用才接手。⛔ -不重跑原派发词 —— 新 agent 带四条增量: +先试复活、不可用才接手与 ⛔ 不重跑原派发词在主文件;四条增量: - worktree 已存在,⛔ 不要新建,`cd` 进去接着做(第二个 worktree 会劈开工作); -- 先读全部既有提交与未提交改动,逐 hunk 决定保留或修正 —— 既不推倒重来也不盲信(死 - agent 从未回报,它的一切都未经验证); -- 把死 agent 没跑完的验证**完整重跑**并报真实输出(中断的运行没有留下任何测试证 - 据); -- assignee、认领评论、分支**全部不动** —— 这是既有认领的延续,不是新认领;认领评论 - 记录接手,不被替换。 +- 先读全部既有提交与未提交改动,逐 hunk 决定保留或修正 —— 既不推倒重来也不盲信 + (死 agent 从未回报,它的一切都未经验证); +- 把死 agent 没跑完的验证**完整重跑**并报真实输出(中断的运行没留下测试证据); +- assignee、认领评论、分支**全部不动** —— 既有认领的延续,不是新认领;认领评论记 + 录接手,不被替换。 ## 跨车道简单阻塞项直接接手(维护者 2026-08-13) -出处原话(逐字,未译):「如果当前任务依赖其他赛道的任务,如果判断简单,是否可以直 -接接手而不必持续等待」。形状:本车道卡被他车道卡挡住,且该阻塞项**判断简单** —— -机械、规格清楚、S 级 ⇒ 被挡座位直接接手做掉,⛔ 不持续等待;本条只覆盖「等一周 vs -做十分钟」的形状。 - -- **纪律不减**:在他车道卡上走完整认领(assign + 会话 ID + 分支评论,`Claim:` 首行 - 拼写照旧)、尊重该车道热文件串行队(读其座位贴具名段)、完工后卡上留收单注记; -- **边界**:凡带设计判断/语义权衡的仍归属地车道;`packages/spec` 恒归 spec 座位, - 本条不豁免(唯一所有者规则更硬); -- **⛔ 不是「借调」回归**:借调(按积压形状把人调去别的车道)已议已删;本条判据是依 - 赖形状 —— 只做被本车道开卡 `Blocked-by:` 点名的那一个阻塞项,做完即回,⛔ 不顺手 - 接第二张。 +出处(逐字,未译):「如果当前任务依赖其他赛道的任务,如果判断简单,是否可以直接 +接手而不必持续等待」。判据与认领纪律在主文件;补遗:只覆盖「等一周 vs 做十分钟」 +的形状,认领评论 `Claim:` 首行拼写照旧;`packages/spec` 恒归 spec 座位,本条不豁 +免(唯一所有者规则更硬);**⛔ 不是「借调」回归**(借调已议已删)—— 判据是依赖形 +状,只做被本车道开卡 `Blocked-by:` 点名的那一个阻塞项,做完即回,⛔ 不顺手接第二 +张。 ## 停摆复位梯度 -停摆的 subagent 永不自愈 —— 携带**任务中途状态**的完成通知本身就是停摆信号,立刻 -SendMessage 附执行姿态句(「前台(阻塞)同步执行全部步骤,中途不停止、不把构建/测 -试挂到后台等唤醒」),⛔ 不等任何静默阈值 —— 阈值是给「没有回答」的,不是给「回答说 -我停了」的。**每次复位比上一次更具体**(原样重发同一句话不算一次复位):① 复述执行 -姿态 → ② 点名下一个工具调用 + 明令禁止后台等待 → ③ 第三次停摆判 unreliable,按接手 -协议重派到该分支。自己挂的定时器不会唤醒自己。长验证管线的派发/接力词写明前台姿 -态。生产侧条款在 os-dev 定义 —— 这里是消费侧兜底,⛔ 不能写成「派发词写全了就可以不 -探」。 +原则三条(停摆永不自愈、完成通知即停摆信号立刻 SendMessage、消费侧兜底)在主文 +件;本节是执行细则。姿态句全文:「前台(阻塞)同步执行全部步骤,中途不停止、不把 +构建/测试挂到后台等唤醒」。梯度逐级(原样重发同一句不算一次复位):① 复述执行姿 +态 → ② 点名下一个工具调用 + 明令禁止后台等待 → ③ 第三次停摆判 unreliable,按接手 +协议重派到该分支。自己挂的定时器不会唤醒自己;长验证管线的派发/接力词写明前台姿 +态;生产侧条款在 os-dev 定义。 ## 通知重放去重 -通知可以形态完全正常、报告完整正确,而只是同一份东西的第 N 次重放(monitor 按自己 -的 deadline 触发)。**先算身份,再决定读不读内容**:去重三元组 -`(issue, 分支, PR head sha)` + 通知自报的守护对象;与已验收那份逐项相同 ⇒ 台账记 -「重放,首达时间 T」即结束,⛔ 不重新验收、不重读 diff、不重复 ACCEPT。⛔ 到达不读 -作「还活着」(活着的判据只有探针回包),不到达也不读作「已死」(判死只认三类正当依 -据)。对面自报守护对象是「变便宜」,不是去重的前提 —— 没自报就用三元组自己算。 +原则(先算身份再读内容;⛔ 不重新验收、不重读 diff、不重复 ACCEPT;到达与不到达都 +不构成生死证据)在主文件;细则:重放形态可以完全正常、报告完整正确 —— monitor 按 +自己的 deadline 触发;身份 = 三元组 `(issue, 分支, PR head sha)` + 通知自报的守护 +对象(自报是「变便宜」不是前提,没自报就用三元组自己算);判定重放 ⇒ 台账记「重 +放,首达时间 T」即结束。 ## 直接验收兜底(报告丢失 ≠ 验收停摆) -同时满足 (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,派发令只带增量。 +触发三条件、舰队级例外与「先探活后翻 ready」在主文件;验收动作:逐文件核对 diff +与认领申报的文件面,对照 `origin/main` 复核 PR 正文的前提声明与验证叙述,复核清单 +其余判据不减;抢先翻 ready 会撞上 agent 的收尾推送(竞态保护)。「推分支 → 开 +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 跑一次(⛔ 不让 dev 枚举全 farm,这半边不变)。**但点名单是线索不是规格**: - 当天现取、认真读过的清单照样会不完整,实测漏点名的门恰好被改动真实打红过。⇒ dev - 在跑完被点名的族之后,用同一条取数命令对**实际改动路径**重取一次,发现清单漏掉、 - 而自己改动确实触及的族,补跑并在报告里点名(dev 侧条款住在 os-dev 定义)—— 一次 - O(1) 取数,换掉「点名准确性由 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 必须继续承重 —— 断言新语义的**实质**(行数、译文、状态码), - 不是「旧断言已删」;真正非法形状的拒收断言**逐字保留**,被守护的面永不缩水。丢第 - 二句,「全仓翻完」退化成「全仓删光」,绿而无值。 +原则(⛔ 默认不整段粘贴 issue 正文;dev 自查正文完整性是「自己读」截断风险的对价) +在主文件;本节是条款原文与增量: + +- **三分区条款原文**:「裁决(不可重裁)」—— dev 执行,不重开;「PM 机制假设(须 + 实测,鼓励证伪)」—— dev 动手前验证,证伪照实报告并按裁决意图换实现路径,⛔ 不 + 许为顺从假设硬做;「PM 建议的路线(可选,实测优先)」—— dev 有更好的就换,⛔ 不 + 得因「派发令写了」照做。 +- **清单取数补遗**(条款主文在主文件):门禁清单单班之内就会过期,所以派发那一刻 + 现取;全 farm 归 CI 跑一次,⛔ 不让 dev 枚举全 farm;点名单当天现取仍会漏(实测 + 漏点名的门恰被改动打红过)—— dev 对实际改动路径重取补跑,条款住 os-dev 定义。 +- **文件面两句原文**:「预期落点是 X;若实测表明真正的生产者在别包,报备后按生产 + 者侧修(落点与理由写进报告和 PR 正文),⛔ 不在消费者侧打补丁」—— 只写一个路径 + 名,是要求 dev 在守约与修对之间二选一;跨包常等于跨车道,PM 事后补跨座位声明。 +- **Same-day churn 行与在飞重叠**:派发时 `git log origin/main --oneline -20 -- + ` 见当天合并 ⇒ 加一行「基于合并后的代码工作,issue 引用的片段可能已变, + 先核对当前 main」(dev 的 worktree 切出后不会自己更新)。在飞重叠每轮拦截 + (churn 行只覆盖派发瞬间):main 新落 PR 与在飞申报文件面求交,相交即警告四句 + —— 合 main 重跑测试矩阵、读对方 diff 重划边界、只补它没覆盖的部分、被完全覆盖 + 就停下回报 ⛔ 不硬造 diff。晚一轮 = 一次返工。 +- **全仓 pin 清扫两句原文**:① grep 错误码/错误消息**全仓扫描**同语义 pin,一轮翻 + 完,不只改本包(旧立场的 pin 住在被改包之外的消费层);② 翻转后的 pin 必须继续 + 承重 —— 断言新语义的**实质**(行数、译文、状态码),不是「旧断言已删」;真正非 + 法形状的拒收断言**逐字保留**,被守护的面永不缩水。 - **条件性标准条款,命中判据才抄,不命中就省掉别硬套**: - 多实现面组件(同一契约 ≥2 个实现面)⇒ 「测试放在**未来的分叉会被抓住的地方**, 进共享一致性覆盖,不是独立测试文件」;不变量形:与 `find()` 给出相同行集,或以 @@ -281,11 +210,7 @@ SendMessage 附执行姿态句(「前台(阻塞)同步执行全部步骤,中途 论:已改 / 本就合规(证据)/ 明确不在范围(理由);⛔ 静默略过 —— 评审把没提到 的面读作漏掉的面」。防的不是做错,是做对了一部分然后以为做完了。 - 改动触及**已发布包**(`package.json` 的 `private` 不为 true)⇒ 「用户可见行为 - 改动**必须**带 `.changeset/*.md`;判据是包的发布状态,不是改动大小 —— 缺了则合 - 进 main 却永不发布,看起来像修好了,比不合更糟」(ACCEPT 侧对账在复核清单)。 -- **测量先行卡**(卡面是一个问题,修复由测量结果**有条件授权**)⇒ 派发令写明「⛔ 测 - 量存在之前不写修复」—— 测量姿态让 dev 看得见卡面没列的探针,实现姿态只看得见要交 - 的 diff。 -- **Premise-first 写明**:issue 正文是线索不是规格,先对 origin/main 验前提; - `premise_still_valid: false` + 无 PR 是合法且常常有价值的交付 —— 派发词预设 - issue 为真,就把好运行变成表面抗命。 + 改动**必须**带 `.changeset/*.md`;判据是包的发布状态,不是改动大小 —— 缺了则 + 合进 main 却永不发布,看起来像修好了,比不合更糟」(ACCEPT 侧对账在复核清单)。 +- **测量先行卡**(修复由测量结果**有条件授权**)⇒ 派发令写明「⛔ 测量存在之前不写 + 修复」—— 测量姿态让 dev 看得见卡面没列的探针,实现姿态只看得见要交的 diff。 diff --git a/.claude/skills/pm-dispatch/references/landing-operations.md b/.claude/skills/pm-dispatch/references/landing-operations.md index f313ae24fb..75b2f2a648 100644 --- a/.claude/skills/pm-dispatch/references/landing-operations.md +++ b/.claude/skills/pm-dispatch/references/landing-operations.md @@ -7,17 +7,17 @@ 批次选择只保证同批 file-disjoint,管不到先后两单都碰生成物 —— 协议变更几乎必然如 此。被路由到 `merge=os-regen` 的路径清单**当场读**:`grep os-regen .gitattributes` -(唯一权威清单,⛔ 不抄进派发令当常量 —— 散文没法被类型检查,唯一不会烂的拷贝是不 -存在的那份;清单里同时有文档产物,同样会被静默吞)。该驱动会 merge exit 0、零冲突 -标记,却**静默丢掉一侧的改动** —— 只有重新生成才暴露。 +(唯一权威清单,⛔ 不抄进派发令当常量 —— 散文没法被类型检查;清单里同时有文档产 +物,同样会被静默吞)。该驱动 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 再重生成」的由来。 + (HEAD 仍是合并前分支 tip),倒退后的锚点**依然 authentic**、全部门放行 —— 这就 + 是「先 commit 再重生成」的由来。 - **`gen:schema` 的清场会顺手抹掉 `gen:openapi` 的产物**,表现为 rest 包一串 `expected 503 to be 200` 假红;补跑 `pnpm --filter @objectstack/spec gen:openapi` 即复原。 @@ -26,7 +26,7 @@ 引号精确名 `git grep … origin/main -- <实现文件>`)—— 条目是索引,实现体才是被吞 的重灾区。**锚点断言的正确措辞**(写错会教唆 dev 手改锚点):断言 `pnpm --filter @objectstack/spec check:authorable-surface` **绿**即可;`baseRev` -允许滞后,滞后只打一行提示不是错误;⛔ 禁止为凑「相等」手改锚点文件,⛔ 不得要求 +允许滞后(一行提示,不是错误);⛔ 禁止为凑「相等」手改锚点文件,⛔ 不得要求 `baseRev == merge-base` —— 那个等式不是任何门的判据。 ## B. 跟到 MERGED 为止;入队后的看护归队列管家 @@ -34,34 +34,30 @@ 车道 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 与队列成员资格)。 +询;定点文本照定时器写法纪律(幂等开头、只写判据);确认 **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`,确认没 -有别的卡被正文里的闭合关键词误关(事实表见平台读数)。 +离开在飞视图;`Part of` 卡仍开着,`pm:dispatched` 不摘就把一张无 dev、无分支、无 +在飞物的卡永远算在 `label:pm:dispatched is:open` 里。摘标(换回 `pm:queue` 或按剩 +余物定级)+ 一条评论(已交付什么、还剩什么、剩下的归谁)与 MERGED 确认是一个动 +作,⛔ 不拆成「下轮巡检再摘」。同刻顺手读一次相关卡的 `closed_by_pull_requests`, +确认没有别的卡被正文里的闭合关键词误关(事实表见平台读数)。 **每次合并后重拉一次车道盘点,与预期状态对账** —— 也是落地动作的一步,⛔ 不留给下 -轮巡检:取 open 卡清单,对照「本次合并应关哪些、不应关哪些」的预期 diff 一遍;预期 -之外从 open 消失的卡,就是被 PR 正文闭合关键词静默误关的卡(`completed` 状态对一切 -只看 open 的过滤与巡检隐身,不主动 diff 永远看不见)。与上一段互补而不互替: -`closed_by_pull_requests` 是逐卡核对、要先知道读哪张;盘点对账不需要先验名单,实测 -里抓住静默误关的正是这一步。 +轮巡检:取 open 卡清单,对照「本次合并应关哪些、不应关哪些」的预期 diff 一遍;预 +期之外从 open 消失的卡,就是被 PR 正文闭合关键词静默误关的卡。与上一段互补而不互 +替:`closed_by_pull_requests` 是逐卡核对、要先知道读哪张;盘点对账不需要先验名 +单,实测里抓住静默误关的正是这一步。 **落地窗口给关键 PR 挂 `subscribe_pr_activity`**(会话型座位专用;Routine 座位每 -fire 新会话收不到,维持轮询): - -- ⛔ 不订阅 dev 交报告前的 PR —— 报告前是 dev 的领地,双驾驶员互踩;云卡出生即订阅 - 不越此界,因为其报告在 draft PR 开出即刻到达; -- 订阅是感知补充,不替代 flip 定点; -- **MERGED / 关闭即退订,同刻把 `mode:cloud` 派出的会话 `archive_session`** —— 触 - 发条件是卡的终局且报告已收复核;⛔ PR 合并前不归档(活会话是 dirty 自救的执行 - 手;误归档可 unarchive,但容器现场已失,宁晚勿早); -- 暂停/交接时清点在挂订阅写进座位贴,⛔ 不留孤儿订阅。 +fire 新会话收不到,维持轮询):⛔ 不订阅 dev 交报告前的 PR —— 报告前是 dev 的领 +地,双驾驶员互踩(云卡出生即订阅不越此界,其报告在 draft PR 开出即刻到达);订阅 +是感知补充,不替代 flip 定点;**MERGED / 关闭即退订,同刻把 `mode:cloud` 派出的会 +话 `archive_session`** —— 触发条件是卡的终局且报告已收复核,⛔ PR 合并前不归档 +(活会话是 dirty 自救的执行手;误归档可 unarchive,但容器现场已失,宁晚勿早);暂 +停/交接时清点在挂订阅写进座位贴,⛔ 不留孤儿订阅。 落地之后**再核一次落地判据本身**仍是车道 PM 的活 —— 队列的合并同样走 os-regen 驱 动,A 的静默吞并在队列合并这一步一样能发生。 @@ -75,12 +71,12 @@ fire 新会话收不到,维持轮询): ## D. 串行接力:多个已实现 PR 全碰生成物时,一次只放行一个 -1. **每一棒都是一整圈**:merge main + A 的四步 + 全套验证 + 兄弟单断言复核 → PM 复 - 核回报 → 转 ready → 挂 auto-merge/入队;每棒都重走,不是整条链只走一次; +1. **每一棒都是一整圈**:merge main + A 的四步 + 全套验证 + 兄弟单断言复核 → PM + 复核回报 → 转 ready → 挂 auto-merge/入队;每棒都重走,不是整条链只走一次; auto-merge 由 PM 挂、dev 永不碰。 2. **相邻两棒同碰一个文件时,交接的是语义,不是文本**:前棒在回报里写明它对共享文 - 件改动的**性质**(改名/提取变量/增补断言,而非纯追加),PM 原样转告下一棒,并要 - 求「两个 PR 的意图**叠加**,⛔ 禁止机械取一边」—— 取一边会各自绿、合起来错。 -3. **两棒散文互锁时,分工由 PM 指派**:允许前棒给后棒留占位交接;PM 必须在**两侧** - 的接力指令里写明谁动、谁不动 —— 否则要么两边都动(冲突),要么两边都不动(过期 - 散文),两种结果在 CI 上都是绿的。 + 件改动的**性质**(改名/提取变量/增补断言,而非纯追加),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 index 74c6437522..8f956fba2b 100644 --- a/.claude/skills/pm-dispatch/references/platform-readings.md +++ b/.claude/skills/pm-dispatch/references/platform-readings.md @@ -1,72 +1,64 @@ # 平台读数事实表(references —— 按需加载) 出处:主文件「平台读数纪律」。本表是 GitHub API / 工具行为的**实测事实**,在做对应 -操作的那一刻查阅;原则(判据取命令输出、零命中反查、定时器文本纪律等)住在主文 -件。⛔ 本表不引用 issue 编号 —— 每条自含失效模式与边界。 +操作的那一刻查阅;原则住在主文件。⛔ 本表不引用 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 已入队而分支尚未建出。 + (`git ls-remote --heads origin 'refs/heads/gh-readonly-queue/*'`)正命中是「已 + 入队」的充分证据,反向推断作废 —— 队列满载时 PR 已入队而分支尚未建出。 - **成功序列读间隔不读事件名**:`removed_from_merge_queue` 后 ~1 秒内跟着 `merged` 是落地,不是被踢;真被踢的形状是其后没有 `merged`、几分钟后 PR 仍 open。 -- 「判据不在 `origin/main` 上」是二义读数 —— 同时兼容「在队列里等」与「压根没入 - 队」,两者处置相反。落地检查永远两个读数:**队列成员资格 和 `origin/main`**,缺 - 一不可。 +- 「不在 `origin/main` 上」是二义读数(在队列里等 / 没入队,处置相反)—— 落地检查 + 永远两个读数:**队列成员资格 和 `origin/main`**,缺一不可。 - **PR 转回 draft 会同时掉 auto-merge 与队列成员资格,且不自动恢复**;转正后必须 重新挂。反方向同理:要真踢出队列,只有转 draft —— `disable_pr_auto_merge` 单独 调用**不解除队列成员资格**,PR 照样落地。 -- **`enable_pr_auto_merge` 一律显式传 `mergeMethod: "SQUASH"`**:不传时静默退回 - 被禁的 merge-commit 方式 —— 等于无操作。但**回显在两个方向都不可靠**:实测队列 - 路径上回显为空(`method: `)而 timeline 的入队事件照发、PR 照落地;也实测显式 - 传 SQUASH 后回显 `MERGE`(参数被治理 `main` 的合并队列侧改写,落地历史仍是每 - PR 一提交)。⇒ 权威信号只有队列成员资格的 timeline 事件与最终 MERGED 状态, - ⛔ 不拿回显当任何方向的证据,也不读 `auto_merge` 字段。 -- `enable_pr_auto_merge` 的空字段返回(`method: , enabled at `)对「入没入队」零区 - 分度,签名本身不构成任何方向的证据。序列:① 先验队列分支(给条目 ~20–30s 建 - 出);② 分支在 ⇒ 结束,⛔ 不翻转;③ 等待后仍缺席**且队列已见 churn**(更新的条 - 目建出了分支而你的没有 —— 截断下单纯缺席不充分)⇒ 翻转一次(`disable` → - `enable`),翻转后仍以 timeline 事件验证;④ ⛔ enable 与它的队列验证之间永不插 - `disable` ——「入队」webhook 可能乱序迟到,armed 窗口里补的 disable 会把已发生的 - 真实入队撤掉。 -- **队列踢出先认签名再决定重投**:已知 flaky 核对失败签名一致 ⇒ 原样重投;但止血 - 修复合入后**同一签名再现就不再是那条 flaky**,是新问题,必须重新诊断,⛔ 禁止条 - 件反射式重投。第三种签名:本 PR 名下**没有任何** `merge_group` run 且批次同伴的 +- **`enable_pr_auto_merge` 一律显式传 `mergeMethod: "SQUASH"`**:不传时静默退回被 + 禁的 merge-commit 方式 —— 等于无操作。**回显在两个方向都不可靠**(实测:队列路 + 径上回显为空而入队照发;显式传 SQUASH 回显 `MERGE`,是队列侧改写,落地仍每 PR + 一提交)⇒ 权威信号只有 timeline 入队事件与最终 MERGED 状态,⛔ 不拿回显当任何方 + 向的证据。 +- enable 后的验证序列:① 先验队列分支(给条目 ~20–30s 建出);② 分支在 ⇒ 结束, + ⛔ 不翻转;③ 等待后仍缺席**且队列已见 churn**(更新的条目建出了分支而你的没有 —— + 截断下单纯缺席不充分)⇒ 翻转一次(`disable` → `enable`),翻转后仍以 timeline + 事件验证;④ ⛔ enable 与它的队列验证之间永不插 `disable` ——「入队」webhook 可能 + 乱序迟到,armed 窗口里补的 disable 会把已发生的真实入队撤掉。 +- **队列踢出先认签名再决定重投**:已知 flaky 核对失败签名一致 ⇒ 原样重投;止血修 + 复合入后**同一签名再现就不再是那条 flaky**,是新问题,必须重新诊断,⛔ 禁止条件 + 反射式重投。第三种签名:本 PR 名下**没有任何** `merge_group` run 且批次同伴的 run 全部 `success` = 队列重建的连带取消,不是红 —— 带签名读数收据重投一次(收据 留在 PR 上),⛔ 无收据不重投;同一 PR 第二次被踢移交队列管家。 -- **实测吞吐参数两则**:合并队列落地延迟 ≈ 每 PR 15–30 分钟且串行 —— 多 PR 在队 - 时按此估算落地窗口,⛔ 不据「还没落」提前判异常;单容器重验证(build+test)并 - 发甜点 ≈3 —— 排批时按它定同容器重验证卡的并发上限,再高互相争用、再低闲置。 +- **实测吞吐参数两则**:合并队列落地延迟 ≈ 每 PR 15–30 分钟且串行(⛔ 不据「还没 + 落」提前判异常);单容器重验证(build+test)并发甜点 ≈3,排批按它定并发上限。 ## API 配额 -- GraphQL 配额(5000/时)极易打满,读与评论一律走 REST(core 15000/时,独立 - 计);只有无 REST 对应物的写才花 GraphQL。`issue_write` 连查找半边都吃 - GraphQL —— 配额红时认领类动作整体排队,评论(REST)先行把结论发出去。 +- 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」≠「不限量」。 + `gh api rate_limit` 对应资源,恢复窗口按序连清;重试对齐整点(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`;写后照标签纪律回读。 -- **`list_issues` 永不返回 assignees**(`fields` 枚举无此成员;不传 `fields` 响应 - 里同样没有)—— 已认领卡与空闲卡在响应里逐字节相同,车道清单因此**回答不了** - 「哪张能认领」这个它被用来回答的问题,失效完全静默、长得就像成功。清单只是 - **候选名单**:每一条在认领前都必须过一次完整 `issue_read`(它才返回 - `assignees`),⛔ 不把 `list_issues` 结果当候选集直接认领。 -- **MCP `issue_read` 的 body 是 HTML 实体转义过的**(撇号/引号/尖括号成实体),而 - comments 原样返回。⇒ MCP 座位做 body 往返(读 → 改 → `issue_write` 整体写回)不 - 安全:写回的是转义实体,或凭猜反转义 —— 长正文围栏里的箭头等显示编码不可靠逆 - 转,且同一套工具里无从对账真原文。机器可 grep 的行(`Blocked-by:` 一类)可能因 - 此落在评论首行而非 body —— 解锁扫描必须连评论一起扫(`in:comments`);确要改写 - body,先经 REST 取原始 body 对账再写。 + 换**不是追加 —— 不先读现值合并再写,会静默剥掉别的标签;真追加走 REST + `POST /issues/{n}/labels`;写后照标签纪律回读。 +- **`list_issues` 永不返回 assignees**(`fields` 枚举无此成员;不传 `fields` 也没 + 有)—— 已认领卡与空闲卡在响应里逐字节相同,车道清单因此回答不了「哪张能认领」, + 失效完全静默。清单只是**候选名单**:每一条在认领前必须过一次完整 `issue_read` + (它才返回 `assignees`),⛔ 不把 `list_issues` 结果当候选集直接认领。 +- **MCP `issue_read` 的 body 是 HTML 实体转义过的**(撇号/引号/尖括号成实体), + comments 原样返回。⇒ MCP 座位做 body 往返(读 → 改 → 整体写回)不安全:写回转义 + 实体,或凭猜反转义。机器可 grep 的行(`Blocked-by:` 一类)可能因此落在评论首行 + 而非 body —— 解锁扫描必须连评论一起扫(`in:comments`);确要改写 body,先经 + REST 取原始 body 对账再写。 ## 读数陷阱 @@ -80,20 +72,19 @@ 缺一个已合修复时重跑无效,只能推提交(`git merge origin/main`);判别:修复的合 并时间晚于 run 创建时间即是。 - **同一 head 上轻量兄弟 workflow `success` + 重量级载体 `cancelled`,是普通取代 - 的预期签名,不是选择性失败**:兄弟 run 秒级跑完,载体要 10–15 分钟,新推送的 - cancel-in-progress 窗口只罩得住后者。见到它先比对 run 的 `head_sha` 与 PR 当前 - head(取代必有新 head),而不是开「为什么只取消了它」的调查。 -- **CI 红了先取完整日志归档再下结论**:「completeness check 绿」只断言没有 - worker 静默死,≠ 测试通过;并发输出的「相邻」≠「因果」(先查 `turbo.json` 依赖 - 边);⛔ 不只看 tail。公开发出的诊断被推翻时,更正发在同样公开的位置,据它开的 - PR 撤回 draft、解绑 `Fixes`。 + 的预期签名,不是选择性失败**:新推送的 cancel-in-progress 窗口只罩得住跑得慢的 + 载体。见到它先比对 run 的 `head_sha` 与 PR 当前 head(取代必有新 head),而不是 + 开「为什么只取消了它」的调查。 +- **CI 红了先取完整日志归档再下结论**:「completeness check 绿」只断言没有 worker + 静默死,≠ 测试通过;并发输出的「相邻」≠「因果」(先查 `turbo.json` 依赖边); + ⛔ 不只看 tail。公开发出的诊断被推翻时,更正发在同样公开的位置,据它开的 PR 撤回 + draft、解绑 `Fixes`。 - **判「正文被截断」必须双读取**:`.body` 原文 + `Accept: application/vnd.github.full+json` 的 `.body_html`,两者在同一处断掉才算 issue 端 截断;任何单一读法的尾部缺失先算读取端截断(工具输出上限、分页、切片)。写侧另 一半:sanitizer 会在**写入时就地删除**短 `<…>` 片段(HTML 注释标记、`` 类占 - 位符、泛型),反引号与围栏**不提供保护**,正文其余完好、双读取一致读作「完整」 - —— 要保留字面尖括号一律写 HTML 实体 `<` / `>`;含这类片段的正文,写后回读 - 逐个确认仍在(失效完全静默)。 + 位符、泛型),反引号与围栏**不提供保护** —— 要保留字面尖括号一律写 HTML 实体 + `<` / `>`;含这类片段的正文,写后回读逐个确认仍在(失效完全静默)。 - **并行 spec PR 同动 pin 计数断言**(被踢不是事故,按 os-regen 序再解一轮):解 冲突两侧收据都保留、按合并顺序堆叠,新计数**从合并后源码重数**(the file, not the history, is the operand),⛔ 不从两侧收据做算术;双方占同一编号是常态(各取 @@ -102,42 +93,29 @@ 跑 + 复核,不动代码);② 死在 regen 中途(未提交的全是生成物、merge commit 已 在)⇒ PM 直接续作 —— build → 整链 regen → 生成物门禁全绿 → 提交推送,恢复 commit 带 `Recovery commit:` 前缀留审计;⚠️ 有的现场 regen 一件没跑,推送前先跑 - 生成物门禁别赌;③ 死在源码编辑中途 ⇒ 先读 diff 判完整性 —— docblock 把动机/失效 - 模式/判据写全的,PM 可代跑其终验后提交;写了一半意图不明的 ⛔ 不代提交,记进交 - 接。dev 的临时目录(`.os-scratch/` 一类)是工作物不是交付物,清掉,⛔ 不进 - feature PR。 + 生成物门禁别赌;③ 死在源码编辑中途 ⇒ 先读 diff 判完整性 —— docblock 写全动机/失 + 效模式/判据的,PM 可代跑其终验后提交;写了一半意图不明的 ⛔ 不代提交,记进交接。 + dev 的临时目录(`.os-scratch/` 一类)是工作物不是交付物,清掉,⛔ 不进 feature + PR。 ## 闭合关键词解析(PR 正文写侧) - **PR 正文里「不修某卡」的否定句会关掉那张卡**:GitHub 的闭合关键词解析器匹配 - `fix/fixes/fixed/close/closes/closed/resolve/resolves/resolved` + `#N`,**不理会前 - 面的否定词** ——「nothing here fixes #N」在合并时照关 #N,而写这句话的动机恰恰是声 - 明不修;好实践(读了兄弟卡、显式划界)反而制造了失效。安全写法:把号码放在没有关 + `fix/fixes/fixed/close/closes/closed/resolve/resolves/resolved` + `#N`,**不理会 + 前面的否定词** —— 声明不修的那句话恰恰在合并时关卡。安全写法:把号码放在没有关 键词打头的位置 —— `#N is not addressed here` / `out of scope: #N` / - `#N remains open`。实测的解析边界三条:关键词只绑**同一行**的 `#N`;动名词 - (closing/fixing)不是关键词,散文里出现不触发;行内反引号里的关键词不触发 - (code span 实测不建闭合链接;围栏块未独立实测,按同规则对待但留待复测)。 -- **PR body 与 squash commit message 是两个独立解析源**:commit message 只有 - `Fixes` 首行、看起来干净,不代表 body 干净 —— 只查 commit 会漏。误关的卡以 - `completed` 状态对一切「只看 open」的过滤与巡检隐身,没有任何机械守卫覆盖这条路 - 径(duplicate-fix-guard 比的是两个 PR 抢同一张卡,这是一个 PR 多关了一张没人抢的 - 卡);消费侧检查 = 合并后读 `closed_by_pull_requests`(在复核清单)。 + `#N remains open`。实测解析边界三条:关键词只绑**同一行**的 `#N`;动名词 + (closing/fixing)不是关键词;行内反引号里的关键词不触发(code span 实测不建闭 + 合链接;围栏块未独立实测,按同规则对待但留待复测)。 +- **PR body 与 squash commit message 是两个独立解析源**:commit message 干净不代 + 表 body 干净 —— 只查 commit 会漏。误关的卡以 `completed` 状态对一切「只看 + open」的过滤与巡检隐身,没有任何机械守卫覆盖这条路径;消费侧检查 = 合并后读 + `closed_by_pull_requests`(在复核清单)。 ## 断粮检测与跨墙恢复细则(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 复活 → 重挂常 - 规巡检定时器 → 照常跑轮。 +原则、定时器选型(⛔ 不用 send_later 链)与恢复 playbook 在主文件;事实补遗: +`npx ccusage blocks` 在容器内可用(读本地会话记录),两个读数 = 当前 5 小时窗口的 +边界/剩余时间与燃烧率(预警);第三盲区是窗口起点为本地推断的近似值;撞墙时 API +调用失败、宿主报「limit reached, resets at HH:MM」—— 重置时刻在撞墙那一刻可得,事 +前查不到,把它记下来。 diff --git a/.claude/skills/pm-dispatch/references/review-checklist.md b/.claude/skills/pm-dispatch/references/review-checklist.md index 6afcbe85b1..edd15b8690 100644 --- a/.claude/skills/pm-dispatch/references/review-checklist.md +++ b/.claude/skills/pm-dispatch/references/review-checklist.md @@ -6,62 +6,50 @@ - **PR 形态**:存在、是 draft、目标 `main`、正文首行引用卡片 —— **`Fixes #` 仅 当合并应当关卡**;只落地了可实施一半(另一半在决策箱或按范围排除)⇒ 必须 `Part of #`,否则合并会静默关掉一张正躺在决策箱里的卡,而 - `needs-user-decision` 的收件箱过滤只看 open issue —— 卡一关,待裁问题就此无人可 - 见。翻 ready 之前亲核首行,别只信报告。 -- **`Part of` PR 翻 ready 前,再扫一遍正文的闭合关键词形状**:GitHub 的闭合关键词 - 解析器把 `close/fix/resolve` 及其变位(⛔ 动名词 closing/fixing 不在内)绑定到 - **同一行**的 `#N`,并无视周围全部散文 —— 否定、情态、警告一律被忽略,为**防止** - 误关而写的那句(「应当由 PM 另行关某卡」)恰恰就是执行误关的那句。写侧纪律: - ⛔ 永不把闭合关键词放在另一张 open 卡编号旁;安全写法是让编号不被关键词打头 - (「`#N` is not addressed here」/「out of scope: `#N`」),或把关键词放进反引号 - (实测:行内 code span 不触发;围栏未独立实测)。半状态巡查器带一条 report-only - 的「`Part of` 与闭合关键词绑定同一编号」矛盾检测,但它只巡开着的 PR —— 翻 + `needs-user-decision` 的收件箱过滤只看 open issue。翻 ready 之前亲核首行,别只 + 信报告。 +- **`Part of` PR 翻 ready 前,再扫一遍正文的闭合关键词形状**(解析器行为与安全写 + 法见平台读数事实表):⛔ 永不把闭合关键词放在另一张 open 卡编号旁 —— 为防止误关 + 而写的否定句恰恰就是执行误关的那句。半状态巡查器的矛盾检测只巡开着的 PR,翻 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 状态对一切「只看 +- **`Part of` 收口的卡不会自动关,`pm:dispatched` 必须手工摘**(动作与评论要件见 + 入队与落地细则 B):ACCEPT 一张 `Part of` PR 的那一刻就把这步记进落地待办, + ⛔ 不留给「下次巡检看到再说」—— 漏摘的标签让在飞视图数进一张无人认领的开卡。 +- **合并后读一次每张相关卡的 `closed_by_pull_requests`**:确认应关的关了,**并确 + 认没有别的卡被一并关掉**(闭合关键词解析器不理会否定句,body 与 commit + message 分开解析,细则见平台读数事实表);误关的卡以 completed 状态对一切「只看 open」的过滤隐身,这一读是唯一能兜住它的机械检查。 -- **范围检查**(取 changed files,⛔ 不看报告自述):无 `content/docs/releases/` 改 - 动、用户可见改动有 changeset、无与卡无关的文件。Tests/docs-only PR 走 - `skip-changeset` 标签,不走空 changeset(空 changeset 滞留发布);含读者可见生成 - 产物时 dev 选 changeset 是对的 —— 以 PR 正文说明的理由为准,两条路都有效,别来回 - 改。 +- **范围检查**(取 changed files,⛔ 不看报告自述):无 `content/docs/releases/` + 改动、用户可见改动有 changeset、无与卡无关的文件。Tests/docs-only PR 走 + `skip-changeset` 标签,不走空 changeset(空 changeset 滞留发布);含读者可见生 + 成产物时 dev 选 changeset 是对的 —— 以 PR 正文说明的理由为准,两条路都有效,别 + 来回改。 - **改动触及的每个包,`private: false` 即已发布 ⇒ 核 changeset 在不在**:判据是包 的发布状态(读 `package.json`,十秒),不是改动大小,也不是「用户可见」的感觉判 - 断 —— 那个判断 dev 在时间压力下会乐观化。⛔ 缺了不入队 —— 合进 main 却永不发布, - 是比不合更糟的终局:它看起来像修好了。 + 断 —— 那个判断 dev 在时间压力下会乐观化。⛔ 缺了不入队 —— 合进 main 却永不发 + 布,看起来像修好了,比不合更糟。 - **测试证据**要有真实命令与通过输出,不是一句 tests pass。**测量类交付先看阳性对 - 照**:对照本身失败 ⇒ 该读数记 INCONCLUSIVE,⛔ 不把它的「绿」当被测风险的证据入账。 + 照**:对照本身失败 ⇒ 该读数记 INCONCLUSIVE,⛔ 不把它的「绿」当被测风险的证据入 + 账。 - **CI 收敛读数只属于复核侧**(维护者 2026-08-10 裁定;dev 的契约是草稿 PR 时点交 报,报告里 gate `in_progress` 是诚实读数、预期内常态):翻 ready / 挂 auto-merge - / 入队前亲核门禁 job 结论 —— ESLint 与 TypeScript Type Check 两个 job 的 - `conclusion` 已为 `success`(门禁族跑在其内),⛔ 不因报告写了「本地绿」跳过。收 - 敛期转红走补丁轮(SendMessage 续派原 dev —— 那是这笔交换已付过的价钱,不是 - REWORK 的理由;红着合并才是)。重量级卡可在派发令显式写「本单等 CI」。 + / 入队前亲核 ESLint 与 TypeScript Type Check 两个 job 的 `conclusion` 已为 + `success`(门禁族跑在其内),⛔ 不因报告写了「本地绿」跳过。收敛期转红走补丁轮 + (SendMessage 续派原 dev —— 那是这笔交换已付过的价钱,不是 REWORK 的理由;红着 + 合并才是)。重量级卡可在派发令显式写「本单等 CI」。 - **每个门禁读数先钉到 PR 的当前 head**:先读 PR 的 `head.sha`,再比对 run 的 - `head_sha` —— 不一致的 run 是关于一个死提交的读数,绿与红**双向都不入账**:旧 - head 的绿会把「新推送未验」读成「消费者干净」,旧 head 的红会把当前 head 已修 - 掉的缺陷重新挂回 PR。 -- **dev 本地跑的门禁并集,同样先钉 head —— 同一条纪律,不是第二条**:dev 的契约 - 要求在**最后一次提交之后**跑并集,并把那一跑的 `git rev-parse --short HEAD` 抄 - 进报告与 PR 正文;复核就读这个 HEAD 与 PR 当前 `head.sha` 比一次。对不上 ⇒ 那 - 份「本地全绿」是关于一棵死树的读数,与上一条同理**双向都不入账**;正文没抄 - HEAD ⇒ 按**没有读数**处理(既不是绿也不是红),以门禁 job 结论为准。门禁日志不 - 带 sha,所以这个抄写是唯一能把一次绿绑到一棵树上的东西 —— 而复核轮之后又推了提 - 交、报告里的 HEAD 却没跟着动,正是它要抓的那一形:补跑并集(至少棘轮族)再更报 - 告,棘轮恰恰是最后一次提交会动的那一族。 + `head_sha` —— 不一致的 run 是关于一个死提交的读数,绿与红**双向都不入账**(旧 + head 的绿把「新推送未验」读成「消费者干净」,旧 head 的红把已修掉的缺陷重新挂回 + PR)。 +- **dev 本地跑的门禁并集,同样先钉 head —— 同一条纪律**:dev 的契约要求在**最后一 + 次提交之后**跑并集,并把那一跑的 `git rev-parse --short HEAD` 抄进报告与 PR 正 + 文;复核就读这个 HEAD 与 PR 当前 `head.sha` 比一次。对不上 ⇒ 那份「本地全绿」是 + 关于一棵死树的读数,同理**双向都不入账**;正文没抄 HEAD ⇒ 按**没有读数**处理, + 以门禁 job 结论为准。复核轮之后又推了提交、报告里的 HEAD 没跟着动,正是它要抓的 + 那一形:补跑并集(至少棘轮族)再更报告 —— 棘轮恰恰是最后一次提交会动的那一族。 - **被取代 head 上的 run 永不重跑**:非当前 head 上的 `cancelled` 结论零动作 —— - 新推送自带全套 run。重跑烧掉一整个重量级周期,还能忠实复现一个已被当前 head 修 - 掉的缺陷、给绿 PR 挂上假红;实测两次误重跑都源于读到 `cancelled` 没先比对 - head。 + 新推送自带全套 run;重跑烧一整个重量级周期,还能忠实复现已被当前 head 修掉的缺 + 陷、给绿 PR 挂上假红。 - **收益穿过必经边界之后还在吗?** 判据(不是每单都做):价值主张依赖某个下游组件 如实转发(HTTP 错误信封、序列化、日志汇聚、跨进程传输)⇒ 至少端到端验一次收益在 边界之后仍然存在 —— 精心写的拒收正文可能被 4xx 直通层整条替换,缺口在清单里不在 @@ -72,24 +60,23 @@ - **拒收类用例的绿,是不是「它抛了」的绿?** 判据:验收点含「应当被拒收」。抽查 diff 里的拒收用例有没有断言 `code` 与 `status`(ADR-0112 信封)—— 只写 `toThrow()` / `rejects.toThrow()` 的用例,在未修实现本就抛裸 Error 的那一族上恒 - 绿,读起来是覆盖、实际证不了拒收;缺断言判 REWORK 补齐,不接受绿色输出。 + 绿,证不了拒收;缺断言判 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`),要求源侧写转义序 - 列不写原始字节。 +- **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 判据,让分诊能一致地给整批定级。 + 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 index 9f347941e1..efd214420f 100644 --- a/.claude/skills/pm-dispatch/references/seat-post-protocol.md +++ b/.claude/skills/pm-dispatch/references/seat-post-protocol.md @@ -12,45 +12,38 @@ 状态(换班级频率);轮次、在飞、队列快照等快状态 ⛔ 不进标题,留在「说明」段。 `label:pm:seat` 的列表页因此就是全舰队状态板。 - **assignee = 在任 PM 的 GitHub 账号**;退场/回收时摘除,无 assignee = 空缺,与 - 标题互为校验。例外:Routine 座位(bot 身份通常不可被 assign)以标题为准。标题、 - assignee、正文「当前 PM」段**三者同笔更新**(成对纪律的三元版),正文为权威。 + 标题互为校验。例外:Routine 座位(bot 身份通常不可被 assign)以标题为准。 - **「当前 PM」段登记三元**:GitHub 账号(`get_me` 自查)+ 会话 ID 或 Routine ID + 上任时刻。审计/认领评论的**作者字段**是平台盖章、不可自述错的硬读数 —— 接管仲 裁与活性判定优先用它对账正文。 -- **接管/移交 = 改该贴正文 + 一条审计评论;评论只作交接审计,不承载状态** —— 现状 - 永远读正文,不读评论流(历史与现状挤同一通道,对账成本随评论数线性涨)。 -- **读侧边界(接管与巡检)**:读贴正文 + **只读晚于正文最后编辑时间的评论** —— 更早 - 的评论已在那次编辑被吸收进正文(接管压缩即存档动作),属存档不属现状(维护者 - 2026-08-12 批准)。评论无界增长而正文有界,读全评论是纯 token 税;正文编辑时刻与 - 评论时间戳都是平台盖章的硬读数,比对即得。 +- **读侧边界(接管与巡检)**:只读晚于正文最后编辑时间的评论 —— 评论无界增长而正 + 文有界,读全评论是纯 token 税;正文编辑时刻与评论时间戳都是平台盖章的硬读数,比 + 对即得(维护者 2026-08-12 批准)。 - **班次叙事不进贴正文**:逐卡状态在卡与 PR 上本就必填,轮次合计走轮次报告;贴正 文只在接管、移交、结构变更时编辑,⛔ 不逐事件追加(无界贴会超出单次读取上限)。 -- **接管压缩是标准步骤**:接任 PM 把交接的有效内容按六段模板重述为新正文,**编辑 - 历史即存档** —— 旧账留在上一版 body revision,审计评论带版本指针(哪一版、何 - 时)。超大贴由半状态巡查的 report-only 项浮出,不等读取失败。 +- **接管压缩细则**:接任 PM 把交接的有效内容按六段模板重述为新正文,**编辑历史即 + 存档** —— 旧账留在上一版 body revision,审计评论带版本指针(哪一版、何时)。超 + 大贴由半状态巡查的 report-only 项浮出,不等读取失败。 - **热文件串行队是正文具名段**(跨轮、跨班次的常设事实):文件 → 有序卡片清单 → 每张卡认领的**区域**;区域列写清楚才敢并行,写不清楚就只能整文件串行。 -- **残余竞态纪律**(唯一剩下的多写手场景是空缺座位争用):动手前重新 fetch 贴正 - 文;审计评论**时间戳先到先得**;写后回读。新增座位贴(拆域)同样先查 `pm:seat` - 索引再立贴。 -- **无心跳,活性判定两条路**:座位不定期报活。惰性半边在**接管冲突**时评估一次 - —— Routine 座位查调度器(`last_fired` / `next_run`),会话座位查最近一条产出评 - 论的时间戳,**>24h 无产出即可回收**(改贴正文 + 审计评论);常设半边是分诊 - Routine 每 fire 附带的座位贴活性巡查,按同一 >24h 判据主动把静默的 🟢 贴降级 - `⏳ vacant`(细则见 `dispatch-runbook.md`)。子树/批次里在飞的认领仍由原认领者 - 跟完。 +- **残余竞态补遗**(三招在主文件):新增座位贴(拆域)同样先查 `pm:seat` 索引再立 + 贴。 +- **无心跳,活性判定两条路**:惰性半边在**接管冲突**时评估一次 —— Routine 座位查 + 调度器(`last_fired` / `next_run`),会话座位查最近一条产出评论的时间戳, + **>24h 无产出即可回收**(改贴正文 + 审计评论);常设半边是分诊 Routine 每 fire + 附带的座位贴活性巡查,同一 >24h 判据(细则见 `dispatch-runbook.md`)。子树/批次 + 里在飞的认领仍由原认领者跟完。 - **每轮巡检核对自己的座位贴正文**:协议或结构升级会迁移状态,自查一贴成本为零; 发现不符当场改正文 + 审计评论。Routine 座位的收尾简报也落自己的座位贴(它是下一 轮自退守卫的读数)。 -- **收班 = 状态 flush + 看护显式移交,⛔ 不等于在飞归零**(维护者 2026-08-13 拍板;引 - 发裁定的实测:满车道只剩一张 L 级云卡在验证,座位与维护者被它挂住可达数小时,而云卡 - 的设计前提本就是活过 PM 会话)。在飞归零对子代理 dev 是对的 —— 它们随会话死;对 - 云卡与合并队列尾巴是错的 —— 那些是持久的、GitHub 锚定的进程,需要的是**看护者**, - 不是某个特定会话。热移交三件齐,几分钟内即可收班:① 每张在飞卡落**收单注记**( - 评审判据、落地步骤、当前状态;认领评论已有分支与会话 ID,增量很小);② 座位贴正文刷 - 新到当下;③ 每件在飞物**点名接手看护者** —— 默认下任座位(凭卡继承收单;在跑云会 - 话跨会话可达 —— 用绑定会话 ID 的 poke 触发器投递,⛔ 非 `SendMessage` 直投,细则 - 见 `dispatch-runbook.md`),已入队 PR 归队列管家。**评审判断不可外包**:管家授权只 +- **收班 = 状态 flush + 看护显式移交,⛔ 不等于在飞归零**(维护者 2026-08-13 拍 + 板;实测:满车道只剩一张 L 级云卡在验证,座位被挂住数小时,而云卡的设计前提本就 + 是活过 PM 会话)。在飞归零对子代理 dev 是对的 —— 它们随会话死;对云卡与合并队列 + 尾巴是错的 —— 那些是持久的、GitHub 锚定的进程,需要的是**看护者**。热移交三件 + 齐,几分钟内即可收班:① 每张在飞卡落**收单注记**(评审判据、落地步骤、当前状 + 态);② 座位贴正文刷新到当下;③ 每件在飞物**点名接手看护者** —— 默认下任座位 + (在跑云会话用绑定会话 ID 的 poke 触发器投递,⛔ 非 `SendMessage` 直投,细则见 + `dispatch-runbook.md`),已入队 PR 归队列管家。**评审判断不可外包**:管家授权只 守落地,热移交把评审交给下任座位,⛔ 不是跳过评审;会话内 `create_trigger` 的 Routine 拿不到 GitHub 工具(下方细则),⛔ 不是逃生舱。 - **交接收尾清单(退场序列,漏一步就是留残缺现场)**:① 立即停止新派发;② 在手工 @@ -59,16 +52,11 @@ 任会话 ID、注销时间、队列快照、**热文件串行队**、跨车道备忘);⑤ 审计评论存档 (接任指引:`/pm-dispatch 接手` + 先读座位贴);⑥ `list_triggers` 清点本会话全 部自设定时器,逐个清理**或随移交物转交**(绑着移交中 PR 的定时器随 PR 转交,不 - 清);⑥ʹ 本座位派出的 dev 会话由**离任 PM 自己**逐个 `archive_session` —— 会话句 - 柄是账号作用域的,换账号的接任者对它们一律 `not found`(细则见 + 清);⑥ʹ 本座位派出的 dev 会话由**离任 PM 自己**逐个 `archive_session` —— 会话 + 句柄是账号作用域的,换账号的接任者对它们一律 `not found`(细则见 `dispatch-runbook.md`),归档义务**不可移交**;确须留跑的会话在座位贴里点名、写 明「归档不随交接转移」,⛔ 不把它留在接任者的欠账清单上 —— 那是一条接任者做不到 - 的待办;⑦ 向维护者交最终报告 —— **不含 SKILL 更新建议清单**(该固定产物已退役,维护 - 者 2026-08-12 裁定;零建议是好班次)。任期观察只可按三类上报:① 原则错/缺(不变量 - 级,罕见)→ 经专题通道给 skills 席;② 可机械化项(提议门禁或脚本,⛔ 永不散文)→ - 正常立卡;③ 平台事实变化 → `references/` 事实表改一行。三类都以 `finding` 入 - skills 车道由该席分诊;三类之外默认关 not planned;「经验教训」散文不再入技能文 - 本 —— 防错住门禁,判断住模型档位,叙事教训是旧世界的补偿机制。 + 的待办;⑦ 向维护者交最终报告 —— 零建议是好班次,三类上报纪律在主文件。 - **epic 委托不入座位贴体系**(父单正文自带会话与领地,`label:pm:epic` 即全量索 引);座位贴只记常设座位,一件事只记一处。`packages/spec` 恒归 spec 座位。 @@ -77,21 +65,21 @@ - 由维护者在 claude.ai 的 Routines UI 创建并勾选 GitHub 连接器 —— 会话内 `create_trigger` 建的 fresh-session Routine 不携带连接器,fired 会话拿不到 GitHub 工具,表现为**静默零产出**;⛔ 不要在会话里 create_trigger 出一个座位 - Routine 就当它在跑。模型不能经 API 钉住,同样在 UI 钉;Routine 继承环境默认模型, - 环境默认变了它跟着变。 -- 创建后**先手动 fire 一轮烟测**,判据取 **GitHub 上的产出**(标签写入/审计评论/座 - 位贴编辑),不取「会话看起来起来了」;零产出即技术性失败,回滚:`delete_trigger` - + 清空座位登记 + 失败注记。 -- 档位按职责挑,不按重要性挑;档位与 cron 是维护者在 UI 上的可调项,协议文本 ⛔ 不复 - 制其当前值 —— 座位贴才是现状。 + Routine 就当它在跑。模型不能经 API 钉住,同样在 UI 钉;Routine 继承环境默认模 + 型,环境默认变了它跟着变。 +- 创建后**先手动 fire 一轮烟测**,判据取 **GitHub 上的产出**(标签写入/审计评论/ + 座位贴编辑),不取「会话看起来起来了」;零产出即技术性失败,回滚: + `delete_trigger` + 清空座位登记 + 失败注记。 +- 档位按职责挑,不按重要性挑;档位与 cron 是维护者在 UI 上的可调项,协议文本 ⛔ 不 + 复制其当前值 —— 座位贴才是现状。 ## Epic 车道收尾与僵尸回收细则 -- 收尾四步一组(缺一步就是过夜半状态):最后一个 sub-issue 关闭 → 父单留总结评论、 - 关闭父单、摘 `pm:epic`(即从索引注销)、正文领地段标注收官。 -- 僵尸判据:父单正文有登记但整棵子树 ~48h 无任何认领/分支/PR 动静 → **分诊座位**在 - 父单评论询问,再静默一窗后摘 `pm:epic` 收回子树;子树内仍在飞的认领按认领协议由原 - 认领者跟完。 +- 收尾四步一组(缺一步就是过夜半状态):最后一个 sub-issue 关闭 → 父单留总结评 + 论、关闭父单、摘 `pm:epic`(即从索引注销)、正文领地段标注收官。 +- 僵尸判据:父单正文有登记但整棵子树 ~48h 无任何认领/分支/PR 动静 → **分诊座位** + 在父单评论询问,再静默一窗后摘 `pm:epic` 收回子树;子树内仍在飞的认领按认领协议 + 由原认领者跟完。 - 领地防撞是声明式的,不是机械保证 —— 撞上由合并队列兜底(代价是返工,不是脏数 据),所以领地声明越窄越诚实,越宽越要在父单正文里说明为什么。 @@ -100,15 +88,14 @@ - 生产者:主仓 → 分诊座位;objectui / cloud → 各自整仓座位(维护者 2026-08-11 裁 定)。其余座位不打不摘,误判走上报;⛔ 三个生产者各扫各的 backlog。 - 消费者:维护者的发版清单 = 三仓三条查询(`label:target: is:open` 各一 - 条);等价的 org 级搜索 `org:… label:target: is:open` 仅是备用写法。「归零 - = 可发版」指三张板都空,单看主仓归零不是。⛔「姊妹仓命中即误标」的旧读法已废除 —— - 它教读者销毁真实阻塞的唯一证据。 -- 鲜度:每 ~5 轮对板上 open 项做过时前提检查(原「与发现分诊轮同节奏」;首触定级改 - 为每轮后,本板自持此节奏),已修/不成立的摘牌 + 一句评论。 -- 发版时刻 = 清板,不是重扫:板上每条三选一 —— 修掉 / 摘牌(不再成立)/ **明示接受 - 带病发布**(摘标签 + accepted-for-GA 评论留痕,进 release notes 的 known + 条);等价的 org 级搜索仅是备用写法。「归零 = 可发版」指三张板都空,单看主仓归零 + 不是。⛔「姊妹仓命中即误标」的旧读法已废除 —— 它教读者销毁真实阻塞的唯一证据。 +- 鲜度:每 ~5 轮对板上 open 项做过时前提检查(首触定级改为每轮后,本板自持此节 + 奏),已修/不成立的摘牌 + 一句评论。 +- 发版时刻 = 清板,不是重扫:板上每条三选一 —— 修掉 / 摘牌(不再成立)/ **明示接 + 受带病发布**(摘标签 + accepted-for-GA 评论留痕,进 release notes 的 known issues);三仓各自执行、读数回贴,⛔ 不因「前端仓/独立部署」整批默认接受。 - 发版前置(维护者 2026-08-10 拍板):清板动手之前先取**一次** pin 读数(⛔ 一次即 - 止,不是重扫);pin 滞后 ⇒ console bump 单必须**已存在且已上板**,或按标准形态明 - 示接受;两者都不成立 ⇒ **不 cut**。这是「板已清空」与「console bump 已就位」之间 - 唯一的机械关联。 + 止,不是重扫);pin 滞后 ⇒ console bump 单必须**已存在且已上板**,或按标准形态 + 明示接受;两者都不成立 ⇒ **不 cut**。这是「板已清空」与「console bump 已就位」 + 之间唯一的机械关联。 diff --git a/scripts/pm/check-skill-line-ratchet.mjs b/scripts/pm/check-skill-line-ratchet.mjs index 188725b7da..cb59df4e2e 100644 --- a/scripts/pm/check-skill-line-ratchet.mjs +++ b/scripts/pm/check-skill-line-ratchet.mjs @@ -2,36 +2,40 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * pm-dispatch SKILL.md line ratchet (#7341 item 1, #5925 item 7) — a - * shrink-only ceiling on the hot file every PM seat loads every round. + * pm instruction-surface line ratchet (#7341 item 1, #5925 item 7; per-file + * extension #8700) — shrink-only ceilings on every file the PM protocol and + * the dev-agent definition are made of. * * node scripts/pm/check-skill-line-ratchet.mjs # the gate * node scripts/pm/check-skill-line-ratchet.mjs --self-test # verify the checker * - * ## Why a ceiling + * ## Why ceilings * * `.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, 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. + * 写细节」) landed it at its current ceiling. The ratchet held it at 0% growth + * — while the UN-ratcheted references/ and os-dev.md grew +31% in one shift + * (maintainer ruling 2026-08-14: 「8685 字太多了 并且综合审查一下相关的skills是 + * 不是应该压缩字数。」). So the ceiling now covers the whole surface, per file: + * the main file carries principles; references/ carries on-demand detail + * (provenance is one line; stories live on cards, not in operational text); + * incident case law lives in git history (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 ratchet discipline (shrink-only) + * ## The ratchet discipline (shrink-only, per file) * - * - MAX_LINES may be LOWERED by any PR that shrinks the file — lowering is + * - A ceiling may be LOWERED by any PR that shrinks its file — lowering is * always legitimate and encouraged. - * - RAISING it requires a maintainer ruling quoted in the raising PR's body + * - RAISING one requires a maintainer ruling quoted in the raising PR's body * (the same evidence bar as Guardrails' `.claude/` tooling exception). - * A protocol change that would cross the ceiling pays its way by moving - * narrative out (references/) instead of raising the roof. - * - The headroom between the current count and the ceiling is the budget - * for ordinary rule edits between extractions; it is deliberately small. + * A protocol change that would cross a ceiling pays its way by moving + * narrative out (SKILL.md → references/) or compressing in place, instead + * of raising the roof. + * - The headroom between a file's count and its ceiling is the budget for + * ordinary rule edits between compressions; it is deliberately small. * * Missing file or empty read is RED, never a pass (#4690: a gate that cannot * find its input must fail, not skip). @@ -40,55 +44,79 @@ import { readFileSync } from 'node:fs'; import process from 'node:process'; -const SKILL_PATH = new URL('../../.claude/skills/pm-dispatch/SKILL.md', import.meta.url); +const REPO_ROOT = new URL('../../', import.meta.url); -// 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; +// Post-compression counts (#8700 one-time pass; SKILL.md keeps its #7885 +// value). Shrink-only: lower freely, raise only with a maintainer ruling +// quoted in the raising PR (see header). +export const CEILINGS = new Map([ + ['.claude/skills/pm-dispatch/SKILL.md', 686], + ['.claude/skills/pm-dispatch/references/dispatch-runbook.md', 216], + ['.claude/skills/pm-dispatch/references/platform-readings.md', 121], + ['.claude/skills/pm-dispatch/references/review-checklist.md', 82], + ['.claude/skills/pm-dispatch/references/landing-operations.md', 82], + ['.claude/skills/pm-dispatch/references/seat-post-protocol.md', 101], + ['.claude/agents/os-dev.md', 399], +]); -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).' }; +export function verdict(rel, lineCount, maxLines) { + if (lineCount === 0) return { ok: false, msg: `${rel} read as empty — refusing to treat a missing/empty input as a pass (#4690).` }; if (lineCount > maxLines) { return { ok: false, msg: - `SKILL.md is ${lineCount} lines; the ratchet ceiling is ${maxLines}. ` + - '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.', + `${rel} is ${lineCount} lines; the ratchet ceiling is ${maxLines}. ` + + 'Keep the surface compressed: principles in SKILL.md, on-demand detail in ' + + '.claude/skills/pm-dispatch/references/ — provenance is one line, stories live on cards, ' + + 'not in operational text. Raising a ceiling requires a maintainer ruling quoted in the PR.', }; } - return { ok: true, msg: `SKILL.md is ${lineCount} lines (ceiling ${maxLines}; headroom ${maxLines - lineCount}).` }; + return { ok: true, msg: `${rel} is ${lineCount} lines (ceiling ${maxLines}; headroom ${maxLines - lineCount}).` }; +} + +function countLines(text) { + return text.length === 0 ? 0 : text.split('\n').length - (text.endsWith('\n') ? 1 : 0); } function run() { - let text; - try { - text = readFileSync(SKILL_PATH, 'utf8'); - } catch { - console.error('✗ check-skill-line-ratchet: cannot read .claude/skills/pm-dispatch/SKILL.md — red, not a skip (#4690).'); - process.exit(1); - } - const lines = text.length === 0 ? 0 : text.split('\n').length - (text.endsWith('\n') ? 1 : 0); - const v = verdict(lines, MAX_LINES); - if (!v.ok) { - console.error(`✗ check-skill-line-ratchet: ${v.msg}`); - process.exit(1); - } - if (MAX_LINES - lines > 120) { - console.log(`ℹ️ headroom is ${MAX_LINES - lines} lines — consider lowering MAX_LINES (shrink-only ratchets tighten opportunistically).`); + let failed = 0; + for (const [rel, maxLines] of CEILINGS) { + let text; + try { + text = readFileSync(new URL(rel, REPO_ROOT), 'utf8'); + } catch { + console.error(`✗ check-skill-line-ratchet: cannot read ${rel} — red, not a skip (#4690).`); + failed++; + continue; + } + const v = verdict(rel, countLines(text), maxLines); + if (!v.ok) { + failed++; + console.error(`✗ check-skill-line-ratchet: ${v.msg}`); + continue; + } + if (maxLines - countLines(text) > 120) { + console.log(`ℹ️ ${rel}: headroom is ${maxLines - countLines(text)} lines — consider lowering its ceiling (shrink-only ratchets tighten opportunistically).`); + } + console.log(`✓ check-skill-line-ratchet: ${v.msg}`); } - console.log(`✓ check-skill-line-ratchet: ${v.msg}`); + if (failed) process.exit(1); } function selfTest() { + const rel = '.claude/skills/pm-dispatch/SKILL.md'; const cases = [ - ['under the ceiling -> green', verdict(2900, 3050).ok, true], - ['at the ceiling -> green', verdict(3050, 3050).ok, true], - ['over the ceiling -> red', verdict(3051, 3050).ok, false], - ['red message names the remedy', verdict(9999, 3050).msg.includes('references/'), true], - ['empty read -> red, not a skip', verdict(0, 3050).ok, false], + ['under the ceiling -> green', verdict(rel, 2900, 3050).ok, true], + ['at the ceiling -> green', verdict(rel, 3050, 3050).ok, true], + ['over the ceiling -> red', verdict(rel, 3051, 3050).ok, false], + ['red message names the file', verdict(rel, 9999, 3050).msg.includes(rel), true], + ['red message names the remedy', verdict(rel, 9999, 3050).msg.includes('references/'), true], + ['red message names the authoring rule', verdict(rel, 9999, 3050).msg.includes('stories live on cards'), true], + ['empty read -> red, not a skip', verdict(rel, 0, 3050).ok, false], + ['every covered file has a positive ceiling', [...CEILINGS.values()].every((n) => Number.isInteger(n) && n > 0), true], + ['SKILL.md is covered', CEILINGS.has('.claude/skills/pm-dispatch/SKILL.md'), true], + ['the dev-agent definition is covered', CEILINGS.has('.claude/agents/os-dev.md'), true], + ['all five compressed references are covered', ['dispatch-runbook', 'platform-readings', 'review-checklist', 'landing-operations', 'seat-post-protocol'].every((n) => CEILINGS.has(`.claude/skills/pm-dispatch/references/${n}.md`)), true], ]; let failed = 0; for (const [name, actual, expected] of cases) { diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index bdf5ebe922..f3275cbc85 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -1216,13 +1216,15 @@ function selfTest() { 'the empty-changeset gate no longer claims a skills path through its own fixtures', !covers(emptyHints, 'skills/demo/SKILL.md'), ); - // The load-bearing survivor: this gate's ONLY real literal is a module- - // relative URL, and before the narrowing it reached SKILL.md through the copy - // of that path in its own header — a real input carried by prose. + // The load-bearing survivor: this gate's real literals are the per-file + // ceiling keys (repo-relative paths in its CEILINGS map) — before the + // narrowing it reached SKILL.md only through a path copy in its own header, + // a real input carried by prose. const ratchetHints = readHints('scripts/pm/check-skill-line-ratchet.mjs'); t('the skill ratchet still reaches the SKILL.md it counts', covers(ratchetHints, '.claude/skills/pm-dispatch/SKILL.md')); + t('the skill ratchet reaches the references files it now counts', covers(ratchetHints, '.claude/skills/pm-dispatch/references/dispatch-runbook.md')); t( - 'the skill ratchet no longer claims references/, which it does not count', + 'the skill ratchet claims only its covered files, not all of references/', !covers(ratchetHints, '.claude/skills/pm-dispatch/references/facts.md'), ); // The control the card called "what a clean one looks like": two hints, both