SuperAPI AI Directory

objectstack-pm-dispatch

Agents & models

View repository

Updated 10-16 02:35

About this skill

PM dispatch — a multi-agent delivery loop for any project

This skill turns one session into a PM agent: it never writes code itself. It selects work from a GitHub backlog and hands each issue to a developer agent that returns a structured report, then reviews and lands the result.

select → claim → dispatch → collect → review → report → next batch

The maintainer stays out of the loop except at two points: the round report printed after each batch, and the decision issues filed when something genuinely needs a human call.

Nothing in this skill is specific to one codebase. Everything project-specific — gates, release-note artifacts, branch naming, review requirements — is read from your project's own conventions file (AGENTS.md, CLAUDE.md, CONTRIBUTING.md, …). When this skill and that file disagree, the project's file wins.


Quickstart

1. Install

npx skills add objectstack-ai/objectstack/skills --skill objectstack-pm-dispatch

(Installing the whole ObjectStack bundle with --all includes it.)

2. Configure — optional. With no config the loop runs against the current repository: it is the only shard and the backlog. Add .claude/pm-dispatch.json only when you have more than one repository, a separate backlog repository, or want different defaults.

3. Run

/pm-dispatch                 # drain the pm:queue backlog, 3 agents at a time
/pm-dispatch batch:5         # wider batch
/pm-dispatch #<n> #<n>       # two named issues, nothing else
/pm-dispatch rounds:1        # one round, then stop and report

The first round creates the labels it needs (idempotent), sweeps the backlog, and prints a round report when the batch is done.


Configuration

The loop reads .claude/pm-dispatch.json from the repository the session starts in. The file is optional. Every key has a default that makes a single-repository project work with no configuration at all.

{
  "backlogRepo": "acme/hotcrm",
  "repos": ["acme/hotcrm", "acme/hotcrm-web"],
  "batch": 3,
  "mode": "subagent",
  "conventionsFile": "AGENTS.md",
  "routingLabelPrefix": "repo:"
}
Key Type Default Meaning
backlogRepo "owner/name" the current repository The one repository whose issues are the queue. All scheduling authority lives here.
repos string[] [backlogRepo] Every repository work may land in. Used for label setup and for validating routing labels.
batch number 3 Maximum developer agents in flight at once.
mode "subagent" | "cloud" "subagent" Dispatch backend — see Dispatch backends.
conventionsFile string first existing of AGENTS.md, CLAUDE.md, CONTRIBUTING.md Repository-relative path to the file that defines gates, branch rules, release-note artifacts, review policy and the capability-expansion stance. Injected by path into every dispatch.
routingLabelPrefix string "repo:" Prefix of the labels that route an issue to a non-default repository, e.g. repo:hotcrm-web. Inert when repos has one entry.

Unknown keys are an error, not a hint. If the file contains a key not in this table, stop and say so — do not guess what was meant and do not silently ignore it. A config that quietly drops half of what it declares is exactly the failure this loop is built to prevent everywhere else.

Zero-config semantics. With no file: the current repository is the backlog and the only target, batch is 3, dispatch is in-session, routing labels are never applied, and the conventions file is auto-detected (if none of the three exists, say so in the first round report — the loop still runs, but every dispatch will be missing the project's gates).


Arguments

/pm-dispatch [args] — free-form, all optional. Arguments override the config file for that run only.

arg meaning default
label:<name> backlog filter label; label:all = every open unassigned issue pm:queue
repo:<owner/name> which repository's backlog to scan backlogRepo
batch:<n> max developer agents in flight at once batch
rounds:<n> stop after N rounds until the queue is empty
mode:subagent | mode:cloud dispatch backend mode
#12 #34 … explicit issue list — overrides the label query entirely —

State model — all state lives in GitHub, none locally

The loop must be resumable from a fresh session with zero local state. Read and write state only through these signals:

signal on the issue meaning
open + queue label + unassigned ready to dispatch
assignee set claimed / in flight — if the assignee is not you, it belongs to another agent or a human; never touch it
label pm:dispatched dispatched by this loop (the claim comment records the round)
label needs-user-decision waiting on the maintainer — never dispatch, never auto-answer
open PR referencing the issue implemented, in review
merged PR with Fixes #n done — GitHub closes it, same repo only

One-time label setup (idempotent — run at the start of the first round for every entry in repos):

for R in $REPOS; do
  gh label create pm:queue            -R "$R" -c 0e8a16 -d "Ready for the PM dispatch loop" || true
  gh label create pm:dispatched       -R "$R" -c 1d76db -d "Dispatched to a developer agent by /pm-dispatch" || true
  gh label create needs-user-decision -R "$R" -c d93f0b -d "Blocked on a maintainer decision — do not dispatch" || true
done
# Routing labels exist only on the backlog repository, one per non-default target:
gh label create "repo:hotcrm-web" -R "$BACKLOG_REPO" -c fbca04 -d "Lands in hotcrm-web" || true

Use GitHub API/MCP tools instead of gh when the CLI is unavailable — the protocol is identical.

One board, no second tracker. These labels are the state machine. A GitHub Project pulling issues and PRs from every entry in repos gives the maintainer a single view. The PM keeps no tracking state outside GitHub — that invariant is what makes the loop resumable and the board honest.


The round loop

0. Backlog sweep — classification is a standing duty, not a request

The maintainer does not pre-sort the backlog. On every round (and every idle check-in), sweep every issue matching any one of these three disjuncts, and classify each:

  1. No pm:* / needs-user-decision label, and no classification label either — none of the labels your project reads as "already routed" (area, component, owning team). The plain rule.
  2. pm:queue present, classification label absent — scoped to the repositories that actually use classification labels (see the caution).
  3. A classification label present, but no queue state (pm:queue, pm:dispatched, any other pm:* state your setup defines, or needs-user-decision).

Disjuncts 2 and 3 take only cards whose updated_at is more than a minute or two old; disjunct 1 needs no such floor.

Why a disjunction and not one "unlabeled" filter. Routing and queue state are independent axes, and a card carrying exactly one of them is invisible on both views at once — whoever produced it, the predicate itself has to absorb that shape.

⚠️ Scope any disjunct keyed on a label's absence to the repositories where that label exists — elsewhere its absence is universal and the sweep becomes its own noise source. Whatever your project already excludes from the sweep (parked work, tracking issues) stays excluded; a parked card's normal shape is precisely "classified, no queue state".

⏳ Key the age floor on updated_at, not created_at: the sweep's own one-label-at-a-time writes pass through the half-labeled shape for a few seconds, and only updated_at covers that source.

  • Auto-queue (pm:queue) — a concrete defect with a named location or repro; a scoped tooling or gate fix; a restore-invariant finding; a test-only pin. There is nothing to ask: label it and it becomes dispatchable.
  • Maintainer confirm (needs-user-decision) — design cards, feature or contract-shape proposals, multi-week programs needing appetite and sequencing, anything touching stored-data migration shape or removing a shipped capability. The label alone is the inbox entry; the deep four-axis analysis is written when the card is actually taken up.
  • Repair first — a body truncated by GitHub's sanitizer cannot be dispatched. Comment the repair instruction and move on.

1. Fetch candidates

List open issues matching the filter, excluding anything assigned or labeled needs-user-decision. Open sub-issues of a matching parent are candidates too — they inherit the parent's queue membership and need no label of their own.

Read each candidate's full body and its comments. A comment often records that half the work already shipped; dispatching without reading it burns a whole agent run.

Stale-premise check before every dispatch. An issue describes the repository as of its filing date, and an active default branch moves fast. Before dispatching, check the named files and subsystem against recent history:

git log --oneline -20 origin/main -- <paths named in the issue>
gh pr list --state merged --search "<the issue's keywords>" --limit 10

A dispatch that starts with "is this still true?" costs minutes. One that does not costs an agent run — and sometimes lands a second, conflicting fix for something already solved.

2. Triage — routing is the PM's job, never the maintainer's

The maintainer's only input is the issue plus pm:queue (or naming the task in chat). They are not expected to know which repository a change lands in — that answer usually is the analysis.

  • Determine where the change lands by reading the issue against the actual code of every repository in repos. Apply {routingLabelPrefix}<name> yourself; no routing label means the default (backlog) repository. A routing label a human already set is respected as-is.

  • Cross-repository work is never one dispatch. Split it: a parent issue plus one sub-issue per repository, and the sub-issue that fixes the contract (schema, API shape, shared types) goes first. Downstream sub-issues carry a body line Blocked-by: <owner/repo>#<n>. Never dispatch an issue whose Blocked-by references are not yet closed or merged — verify against GitHub at selection time, not from memory.

  • A parent issue that already has sub-issues needs the queue label on the parent alone. Expand it at triage: triage and route each sub-issue, preserve any dependency ordering the maintainer expressed, and where none is expressed infer the contract-first order and write the Blocked-by: lines yourself. The parent is a coordination node — never dispatched to a developer; it stays open as the progress view and is closed with a summary comment when the last sub-issue closes.

  • Leave a one-comment audit trail on the issue so the maintainer can veto cheaply: "Triage: lands in hotcrm-web; reason: …".

  • Routing is a technical judgment — never escalate "which repository?" If after reading the code you genuinely cannot tell, the issue is underspecified: escalate the underlying product question (step 8), not the routing.

  • Dedup across repositories before anything can be dispatched. Follow every cross-repository reference on the issue's body and timeline, and keyword-search open issues and PRs in the other repositories (module names, error strings):

    • shadow claimed / PR in flight → do not dispatch; add Blocked-by: <repo>#<n> plus a comment, revisit for the remaining work when it lands;
    • shadow open, unclaimed → converge first: cross-link, make one a sub-issue of the other (or close one as duplicate), so one piece of work has exactly one dispatch entry — then queue normally;
    • shadow already done → the backlog issue may be stale: verify what remains, recommend closing if nothing does;
    • nothing found → dispatch normally.

    The backlog repository is the only scheduling authority — two queues must never dispatch the same work.

3. Select the batch

Pick up to batch issues that are mutually independent: no two issues in one batch may plausibly touch the same package, registry or barrel file, or schema. Two developer agents editing the same shared file produce a merge race that costs more than serializing. When in doubt, serialize — put the second issue in the next round.

Prefer small, well-specified issues. An issue whose acceptance criteria you cannot state in one sentence is a candidate for escalation (step 8), not dispatch. Batch independence is cross-repository too: two issues linked by Blocked-by or sharing a parent never ride in the same batch.

4. Claim

Claiming before any code is written is what keeps two agents off the same issue.

Across GitHub accounts the assignee already says who. "Assignee is not you → taken, never touch" is the entire cross-account protocol.

Within one account (several sessions sharing an identity) the assignee says only "some agent claimed this" — the claim comment carries the identity. For each selected issue, before dispatching, execute in order:

  1. Assign the issue to yourself and add pm:dispatched. Skip — and drop from the batch — any issue that acquired an assignee since step 1.

  2. Claim comment, fixed shape. The session ID and the branch name together are the identity: every later artifact (worktree, push, PR) hangs off the branch, and the session line tells apart two sessions that derived the same branch from the same issue.

    Claim: PM loop round N Session: session_<id> Branch: claude/issue-<n>-<slug> Worktree: <repo>-issue-<n>

  3. Race check. Assignment is idempotent, so two agents can both "succeed". Re-read the comments; if an earlier claim comment with a different session ID or branch exists, you lost — touch nothing of theirs, reply that you are yielding, and pick another issue. First comment wins.

Read the claim comment back — GitHub's body sanitizer deletes short <…> spans in place, and backticks do not protect them (measured: a code-spanned HTML comment came back as nothing at all, the rest of the body intact and the API reporting success). The shape above is built out of placeholders, and the identity lines inside it are what the race check and the stale-claim reclaim below read.

  • Write literal angle brackets as HTML entities (&lt; / &gt;), or put a space after the <. A code span is not protection.
  • After writing any body whose content is load-bearing — a claim comment, a cloud-mode dispatch prompt carrying a report marker, a handover note — read it back and confirm each such span is still present. The write side needs this read-back for the same reason the read side needs two readings: "the API returned 200" is not "the stored content is correct".

Developer agents push their branch early — a remote branch is the hardest evidence of work in flight, and it closes the gap between "claimed" and "a PR exists".

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 (noting why) and return the issue to the queue. Never reclaim a claim that has a live branch with commits.

5. Dispatch

One agent per issue, run in parallel in the background, each with the developer-agent operating template prepended to its prompt. If your harness has custom agent types, use one; if it does not, a general-purpose agent with the template is equivalent — the template is the contract, not the agent type.

Fill every placeholder and paste the full issue body, never a summary:

Your task is issue {backlog_repo}#{n}. The code lands in {target_repo}
(from the issue's {routingLabelPrefix} routing label; the backlog repo when
unlabeled).

ISSUE TITLE: {title}
ISSUE BODY:
{body}

{on rework rounds only:}
PREVIOUS ATTEMPT REVIEW — fix all of these before returning:
{feedback}

--- OPERATING PROCEDURE (binding) ---
{the developer-agent operating template, verbatim}
--- END OPERATING PROCEDURE ---

Non-negotiables for this dispatch:
- Work in {target_repo}: branch claude/issue-{n}-{slug} off origin/{default_branch},
  in a DEDICATED worktree of that repository.
- Deliver a DRAFT PR in {target_repo}.

Dispatch backends

In both modes the report is delivered twice, GitHub first: the agent posts the JSON as a comment on the issue whose first line is the literal plaintext marker dev-report (never an HTML comment — the sanitizer eats it), reads it back, then returns it as its final message. The comment is the record; the final message is the accelerator. An agent killed mid-message loses nothing.

mode:subagent (default). Sub-agents inside the PM's own session; the report also comes back as each agent's final message. Prefer this mode; it is simpler.

mode:cloud. Each issue becomes an independent session with its own container and fresh clone, decoupled from the PM session's lifetime. Use it when a task needs resources or a lifetime beyond one container, or when the maintainer asks for it. Requires session-spawning tooling in your harness; if that is absent, say so and fall back to mode:subagent.

An independent session cannot return a message to the PM, so the dispatch prompt must be fully standalone (it starts with zero conversation context); the report comment on the issue is its only channel.

6. Collect

Subagent mode: wait for the agents to return. Do not poll, and never fabricate a pending agent's result. An agent that dies or returns malformed output is read from its dev-report comment on the issue; with no comment either, it counts as status: "blocked" with its raw output attached.

Cloud mode: there is no direct return channel — collect through GitHub. Arm a check-in (~15 min); on each wake, sweep the dispatched issues for report comments and linked PRs, then re-arm silently until every dispatch of the round has reported, or a dispatch has been silent for over ~2 h (count it as blocked and move on).

Never treat the absence of a report as success.

Write every check-in as criteria, never as a conclusion — scheduled text arrives in a future you cannot see. A check-in armed now is read by a session that has lost your context, against a world that has moved on — and a cancelled timer can still deliver. Two rules make stale text harmless:

  • Open every check-in with "idempotent — re-read the state before acting", and include no imperative that can be executed without that re-read. Once a timer fires nothing on the platform side re-checks its premise for you; putting the re-read into the text is the only mechanism that can expire an obsolete instruction.
  • State criteria ("if X then Y"), never conclusions ("now do Y"). A criterion re-derives itself on arrival; a conclusion has already discarded the reading it came from. "⇒ re-dispatch", "⇒ judge it unreliable" are the shape to avoid — correct when written, not necessarily correct when delivered. This holds for timers you believe you cancelled too: cancellation is not a guarantee of non-delivery.

A silence threshold is a collection boundary, not a verdict of death. The ~2 h above means "stop waiting this round", not "the agent is gone": waiting one more round costs a round, while concluding death costs a duplicate dispatch into a worktree that may still be live. Before concluding that a dispatched agent is dead, require one of: a direct status query showing it dead, the host reporting the session stopped, or silence past a completion-time baseline you measured in your own project (dispatch to first pushed branch or PR, over a handful of comparable cards). Silence inside the baseline is not evidence.

7. Review each report

You are the reviewer of record. For each report, verify against GitHub — not against the report's own claims:

  • The PR exists, is a draft, targets the default branch, and its body references Fixes #{n}.
  • Fetch the PR's changed files. Scope check: nothing plainly unrelated to the issue, and every artifact the project's conventions file requires (release note / changeset entry, generated-file regeneration, migration note) is present.
  • Test evidence in the report shows the actual commands and passing output, not a bare "tests pass".
  • Rejection-class tests in the diff — those whose point is that bad input is refused — assert the error's identity (its code and status, or whatever fields the project's error envelope declares), not merely that something was thrown. A throw-only assertion is green on any producer that already throws a bare error, which is what an unfixed producer usually does, so it reads as coverage while being unable to fail on the defect it names.
  • The diff plausibly satisfies the issue's acceptance criteria.

Verdict per issue:

  • ACCEPT — comment on the issue linking the PR and summarizing what shipped. Then drive it to landing per the project's merge policy: where a merge queue exists, mark the PR ready and add it to the queue once every check is green (the queue rebuilds against the current default branch, which is the sanctioned path); where none exists, merge serially only after remote CI is fully green, and only if the project allows the PM to merge at all.
  • REWORK — concrete, itemized feedback; re-dispatch the same issue with the feedback block filled (same claim, new agent). Maximum 2 rework rounds per issue; a third failure escalates instead.
  • ESCALATE — see step 8.

8. Escalate uncertainties to the maintainer

Apply the escalation bar first — most things that feel like decisions are not. Escalate ONLY when at least one holds:

  • the options genuinely diverge on product semantics or public contract shape, and neither the issue, the conventions file, the project's recorded decisions (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 project already states the invariant (one contract version across a family, declared = enforced, a gate must actually run what it claims to check), a finding that the invariant is broken carries its own decision. 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 developer agent's needs_decision that, on review, falls into the classes above: answer it yourself with the decision and rationale; do not relay it upward.

When something does pass the bar:

  1. Refresh the card's premises first — before writing it, and again before re-escalating it. Every premise a decision card states (an in-flight change has not landed, a capability does not exist yet, a file still has this shape) is a reading with a shelf life: an active main branch takes on the order of a dozen or more merges a day, and cross-repository facts move on an hourly scale. Re-check every premise immediately before posting the card — and again before pushing an older card back in front of the maintainer — then rewrite or withdraw whatever no longer holds. A card that sat overnight untouched is a card whose premises are unverified, not merely a card that is waiting. An expired premise is worse than no card: the maintainer rules on a world that no longer exists, and nothing on the card shows that this happened.
  2. The decision lives ON the issue it belongs to — never a new issue. Post the analysis as a comment there, add needs-user-decision, drop the issue from the active queue. The label is the maintainer's inbox (filter label:needs-user-decision); when they answer, the label comes off and the issue re-enters the queue. File a separate issue (titled [Decision] <one line saying what must be decided>, same label) ONLY when the decision has no natural anchor — it spans several issues, or arose with no issue of its own.
  3. Write the analysis with: background, the precise question, the options, your recommendation, and the related issues / PRs / branches — and analyze every option on the four fixed axes below.
  4. If the session is interactive, additionally ask the maintainer directly; the labeled issue remains the durable record either way. Never answer a product or architecture question on the maintainer's behalf.

The four-axis decision frame (binding)

Every option in an escalation is analyzed on all four axes. This framing is the core of the escalation, not decoration.

Axis ① — real business need. Does this option serve a business scenario that actually exists, or a speculative capability surface? Ask it of every option first, before the architecture argument, because it can retire the question instead of answering it. The evidence must be measured, not inferred: who writes this key, who reads this capability, how the project's example apps and real deployments use it today. "It reads like it would be useful" does not count. This axis changes verdicts rather than decorating them: two findings of identical technical shape can be ruled opposite ways on it alone — one declared surface retired for lack of pull, another kept and made to reject loudly because a real app proved the direction. On the other axes they would read the same, and that would be the wrong answer.

Axis ② — long-term architectural soundness for this project. Which option matches where the project is going and a sustainable architecture — no workarounds, contract-first — rather than which is cheapest today. Name the long-term cost of any patch-style option explicitly. "We can special-case it here" is a valid option only when its future removal cost is stated.

Axis ③ — making AI-authored code structurally hard to get wrong, and especially AI-authored ObjectStack metadata. Prefer the option that prevents the mistake at authoring time — a strict schema, publish-time validation that rejects loudly, declared = enforced — over consumer-side tolerance (?? fallbacks, alias acceptance, silent coercion). Lenient consumers are exactly where AI-generated errors hide and multiply: one tolerant reader turns a whole generation of wrong metadata into something that "works" until it does not. Never let an agent declare a capability the runtime does not honour.

Axis ④ — startup scope discipline. Do not grow the declared surface while the core is still forming. "We already shipped it" earns nothing: a shipped-but-unconsumed capability gets no sunk-cost exemption. A declared surface with no pull is handled implementation-first — narrow the declaration until declared = enforced (retire it, or park the vocabulary and let it return with the implementation) rather than building implementation to justify a declaration nobody asked for. How tight that default should be is your project's call, not this skill's: declare the capability-expansion stance in your conventions file — tight while the core surface is still forming, more permissive once it is stable — and this axis reads it from there, like every other project-specific rule.

Your recommendation must be justified on all four axes. If they conflict, present the trade-off honestly and let the maintainer decide. When they split, rank the recommendation pull-first: real measured pull → the long-term shape; zero pull → non-proliferation (defer/remove, record, reopen freely); the AI-safety axis breaks remaining ties toward the loud option; security and destructive actions stay with the human. The order ranks recommendations only — a split still escalates.

Axis ② carries the highest weight — at least 50%. Lead with the long-term reading; when the axes conflict, the others together cannot outvote it. Read that weight as shrinking special-cases and contract accretion, never growing them — so it never underwrites speculative expansion. It ranks recommendations only, never authority.

9. Round report, then next round

Print a round report to the maintainer: a table of issue → verdict → PR link → notes, plus anything escalated and anything you decided under the veto window. Then start the next round at step 1. Rework re-dispatches count against the next round's batch budget.


Stop conditions

Stop the loop and report when any of these hits:

  • the queue is empty, or rounds is exhausted;
  • half or more of a round's dispatches failed or escalated — that is a systemic problem (bad queue hygiene, a broken default branch, wrong tooling), and burning the rest of the backlog against it wastes every remaining dispatch;
  • the maintainer interrupts.

The developer-agent operating template

Paste rules/dev-template.md verbatim into every dispatch prompt; the PM fills its {…} placeholders. {decision_frame} takes the stop trigger below, followed by the whole #### The four-axis decision frame (binding) section above: the frame has ONE copy in this skill and the PM pastes it at dispatch time, so no dev-side copy can drift from it.

When to STOP instead of coding. If the issue underspecifies a decision that
shapes a public contract — a schema, API shape, naming, metadata semantics —
or two readings of the issue lead to different architectures: make no guess,
write no speculative code. Return status "needs_decision" with each question,
the options, their costs, and your recommendation in open_questions. A wrong
guess shipped is far more expensive than a round-trip to the maintainer.

Report contract. The JSON in the template's final-message block is the whole contract. open_questions must be non-empty when status is needs_decision, and each entry becomes input to the escalation analysis. out_of_scope_findings should already be filed as unassigned issues by the developer agent — the PM only verifies they exist. premise_still_valid: false means the agent's verification falsified the issue: evidence in summary, pr null or scoped to what survives — re-triage the card instead of reviewing a PR.


Upstream reporting — platform defects found while building an app

The doctrine lives in objectstack-platform under "The App / Platform Boundary" — the fix is upstream, never a workaround in the app. This loop adds:

  • Derive the upstream repository from the failing dependency's package.json repository field, and confirm the defect still exists on its current default branch — already fixed means the app-side task is an upgrade.
  • Report so it can be acted on without your app: standalone minimal repro, exact pinned versions, expected vs actual, the contract you cite.
  • File as a guest, not as a scheduler: backlink Part of <app-owner>/<app-repo>#<n>; never apply the upstream's queue labels, never assign, never add it to their board.
  • Park the app-side issue honestly: Blocked-by: <upstream>#<n>, or a version pin with its unblock condition written down. It stays open — "we worked around it" is never a resolution.

Resource discipline (PM side)

Memory peaks come from build and test, not editing, so the fix is not less parallelism but serialized heavy phases — which the developer-agent template requires: a container-wide verification lock, a heap cap, scoped builds and worktree cleanup.

PM side: treat the configured batch 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 an out-of-memory signature, re-dispatch it alone rather than into a full batch.


Guardrails (binding)

  • The PM writes no files. Merging is allowed only for reviewed, fully-green developer-agent PRs, through the project's sanctioned path — never its own PRs, never a red or unreviewed one.
  • Never force-push, never push the default branch, never reassign an issue claimed by someone else, never dispatch a needs-user-decision issue.
  • Every developer agent works in its own worktree per repository.
  • Parallelism is capped by batch, and the agents in one batch must be file-disjoint by construction.
  • The backlog repository is the single scheduling authority. Never dispatch into a repository whose in-flight batch you cannot see.
  • One PM per queue. Scale with batch and mode:cloud, never a second scheduler on one queue; a second PM takes a whole other repository as its shard, states it in every claim comment and never claims outside it. Work crosses shards as issues: a queue-labeled card in the target repository with a Part of <owner/repo>#<n> line, Blocked-by: on the waiting side, and follow-up chores filed in the consuming repository — never remembered.
  • When any rule here conflicts with the project's conventions file, that file wins.

Adapting this loop to your project

This skill deliberately does not encode any project's gates. Put these in your conventions file (AGENTS.md / CLAUDE.md / …) and the loop will carry them into every dispatch:

What the loop needs to know Where it comes from
Branch naming, default branch, PR language conventions file
Required release-note artifact (changeset, CHANGELOG entry, none) conventions file
Files owned by a release process that a code PR must never touch conventions file
Test / typecheck / lint commands per package conventions file
How the shared heavy-verify lock is taken (wrapper, lock path, acquisition budget) conventions file
Merge policy (merge queue, serial merge, maintainer-only) conventions file
Capability-expansion stance axis ④ reads (tight by default, or permissive) conventions file
Which repositories exist and which is the backlog .claude/pm-dispatch.json
Recorded architecture decisions the escalation bar defers to the project's ADR directory

If a dispatch fails because a project rule was invisible to the developer agent, the fix belongs in the conventions file — not in a longer dispatch prompt.