diff --git a/.changeset/pm-dispatch-escalation-bar.md b/.changeset/pm-dispatch-escalation-bar.md new file mode 100644 index 0000000000..83bc57f27e --- /dev/null +++ b/.changeset/pm-dispatch-escalation-bar.md @@ -0,0 +1,4 @@ +--- +--- + +Internal agent tooling only (`.claude/` pm-dispatch escalation bar + API-body generics trap) — releases nothing. diff --git a/.claude/agents/os-dev.md b/.claude/agents/os-dev.md index 3913ba4cb3..6f67067ce3 100644 --- a/.claude/agents/os-dev.md +++ b/.claude/agents/os-dev.md @@ -33,6 +33,23 @@ rules that most often get missed: consumer (`??` alias, tolerant parse), the bug is at the producer or in the spec — fix it there, or return `needs_decision`. +**Resource discipline — parallel agents share ONE container; unbounded +build/test runs OOM it.** Binding rules: + +1. **Serialize the heavy phase.** Wrap every build and test run in the shared + verification lock, so editing parallelizes but memory peaks never stack: + `flock -w 7200 /tmp/os-heavy-verify.lock -c ''` + (one lock file per container; waiting on it 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 and test the affected packages + (`pnpm --filter build/test`), not the whole repo, unless the task + explicitly requires a full pass. Cap test parallelism: + vitest `--maxWorkers=2`, turbo `--concurrency=2`. +4. **Clean up when done**: after the PR is up, remove your worktree + (`git worktree remove --force`) — leftover `node_modules` trees + exhaust the container's disk, which fails as confusingly as OOM. + Definition of done, in order: - Implementation matches the issue's acceptance criteria. @@ -89,3 +106,10 @@ Final message — exactly this JSON, no prose around it: Use `status: "rework"` for a partial result you know is incomplete (say why in `summary`); the PM will review and re-dispatch with feedback. + +Practical trap when filing issues/PRs through the GitHub API: the body +sanitizer strips `<` followed by a letter as an HTML tag **at rest**, which +destroys TypeScript generics (`Assert>` is stored as `Assert>`). +Write generics with a space after each `<` — `Assert< Equal< 1, 2 > >` is +still valid TypeScript — and read the stored body back to verify when a +snippet is load-bearing. diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index 61005e5729..ec94231039 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -102,7 +102,23 @@ known case: accepting a `repo:objectui` PR ⇒ file a `pm:queue` issue in `objectstack` — "run `pnpm objectui:refresh` and land the console bump", referencing the merged PR, blocked-by it until it actually merges. -**4. One board, no second tracker.** The pm labels above are the state +**4. Multiple PM sessions shard by repo — never share one queue.** The +claim protocol makes concurrent PMs *safe*, not *useful*: batch +independence (file-disjointness) is only checked within one PM's view, so +two PMs on the same queue can claim different issues that collide on +shared files, and the merge queue is one lane regardless. Scaling order: + +1. One PM, bigger batch (`batch:5` is the maintainer's chosen operating + point, riding on the resource discipline above), heavy tasks via + `mode:cloud` — adds compute without adding schedulers. +2. When one PM genuinely can't keep up: a second session takes a **whole + repo** as its shard (`/pm-dispatch repo:objectstack-ai/objectui`) — + file universes are disjoint by construction. A sharded PM states its + shard in every claim comment and **never claims outside it**; cross-repo + parent/sub-issue chains stay with the main-backlog PM. +3. Multiple PMs on the SAME queue: prohibited — all cost, no throughput. + +**5. One board, no second tracker.** The pm labels above are the state machine; an org-level GitHub Project pulling issues/PRs from all three repos gives the maintainer a single view (filter by `repo:*` and `pm:*`). The PM maintains no tracking state outside GitHub — that invariant is what keeps @@ -201,6 +217,16 @@ execute atomically, in order: Dev agents push their branch early — a remote branch is the hardest evidence of work in flight, closing the gap between "claimed" and "PR exists". +**Multiple GitHub accounts (colleagues' Claude Code sessions) simplify +this, not complicate it.** Across accounts the assignee alone already says +*who*: `assignee isn't you → taken, never touch` is the entire cross-account +protocol, and it's already the rule. The claim-comment ritual (branch name, +round, race check) matters *within* one account's sessions. When several +accounts work the backlog, partition it the same way as multi-PM sharding — +by repo or by an agreed label per account — and record the assignment table +once in a pinned issue or the round report so nobody triages another +account's shard. + **Stale-claim reclaim**: a claim older than ~24 h whose promised branch does not exist on the remote and has no PR is presumed dead — comment asking, and after another window of silence, remove the assignee (note why) and return @@ -242,6 +268,19 @@ Follow your operating procedure (you are the os-dev agent). Non-negotiables: Return ONLY the JSON report defined in your agent definition. ``` +#### Resource limits — parallel agents share ONE container + +Memory peaks come from **build + test**, not editing, so the fix is not less +parallelism but serialized heavy phases: the os-dev definition requires every +build/test run to hold the container-wide verification lock +(`flock /tmp/os-heavy-verify.lock`), a `NODE_OPTIONS=--max-old-space-size` +heap cap, scoped `--filter` builds/tests, capped vitest/turbo workers, and +worktree cleanup after the PR is up. PM-side: treat `batch:3` as assuming +normal-sized tasks — for build-heavy ones (dependency-family upgrades, full +regression passes) drop to `batch:2`, or dispatch that issue via +`mode:cloud` so it gets its own container. If an agent dies with a +heap/OOM signature, redispatch it alone rather than into a full batch. + #### Dispatch backends **`mode:subagent` (default).** The `Agent` tool, as described above. The devs @@ -313,8 +352,36 @@ Verdict per issue: ### 8. Escalate uncertainties to the maintainer -Whenever a dev returns `needs_decision`, an issue is too vague to dispatch, or -rework has failed twice: +**First, apply the escalation bar — most things that FEEL like decisions are +not.** The maintainer's words: 「明显的问题直接修,不是事事都需要我确认」. +Escalate ONLY when at least one of these holds: + +- the options genuinely diverge on **product semantics or public contract + shape** and neither the issue, AGENTS.md, ADRs, nor existing code norms + determines the answer; +- the fix requires a **destructive or hard-to-reverse action** (stored-data + migration shape, deleting a shipped capability, force operations). + +Everything else is the PM's call — decide, dispatch, and give the maintainer +a **veto window instead of a permission gate**: state what you decided and +why in the issue comment and the round report; they can stop it, but you do +not wait for them. Named non-escalation classes (act immediately): + +- **Restore-invariant fixes.** When the repo already states the invariant — + one contract version across the family, declared = enforced, a gate must + actually compile/run what it claims to check — a finding that the + invariant is broken carries its own decision. A dual-version dependency + graph, an inert tripwire, an unwired gate: queue it, dispatch it, report + it. Asking "may I restore the invariant?" is the anti-pattern. +- **Sequencing and dependency ordering** between technical tasks. +- **Verification strategy** (what regression pass a risky-but-decided change + needs) — that is scoping the work, not deciding it. +- A dev's `needs_decision` that, on PM review, falls into the classes above: + answer the dev yourself with the decision and rationale; do not relay it + upward. + +Whenever a dev returns `needs_decision` that passes the bar above, an issue +is too vague to dispatch, or rework has failed twice: 1. **Default: the decision lives ON the issue it belongs to — never a new issue.** Post the analysis as a comment on that issue, add the