diff --git a/.changeset/published-pm-dispatch-skill.md b/.changeset/published-pm-dispatch-skill.md new file mode 100644 index 0000000000..7b4257e374 --- /dev/null +++ b/.changeset/published-pm-dispatch-skill.md @@ -0,0 +1,40 @@ +--- +--- + +docs(skills): publish `objectstack-pm-dispatch` — the project-agnostic core of the PM dispatch loop, installable by any ObjectStack project (#4607). + +The multi-agent delivery loop (backlog triage → claim → dispatch → structured +report → review → land) has only existed as repo-internal agent tooling under +`.claude/`. Third-party projects building on ObjectStack — hotcrm, customer +projects — run the same shape of work and had nothing to install. This adds the +generalized skill to the published `skills/` catalog: + +- **Config over hardcoding.** The loop reads an optional + `.claude/pm-dispatch.json` (`backlogRepo`, `repos`, `batch`, `mode`, + `conventionsFile`, `routingLabelPrefix`); with no file the current repository + is both the only shard and the backlog. Every project-specific gate — branch + naming, release-note artifact, test commands, merge policy — is read from the + project's own conventions file rather than baked into the skill, and that file + wins on conflict. +- **The developer-agent operating procedure is embedded as a template** the PM + pastes into each dispatch, so the loop works with no custom agent types: + worktree-first, scope = the issue, contract-first (no lenient consumer + fallback), the JSON report contract, `needs_decision` instead of guessing, and + the container resource discipline (shared heavy-verify `flock`, heap cap, + scoped builds, PID-only process operations, worktree cleanup). +- **The two-axis decision frame is kept verbatim** because both axes generalize: + ① long-term architectural soundness for this project, ② making AI-authored + code — especially AI-authored metadata — structurally hard to get wrong + (tighten the producer, never make the consumer tolerant). +- **New: upstream reporting.** What an app project does when it finds a + *platform* defect — stale-premise check against upstream first, minimal repro + with pinned versions and the contract being cited, **never** a tolerant + workaround in the app, an upstream issue backlinked with `Part of #N` and + carrying none of the upstream's queue labels, and the app-side task parked as + `Blocked-by:` or as a version pin with a written unblock condition. + +Catalog registration only — `metadata.domain: process`, no `metadata.internal`, +listed in `skills/README.md` and `content/docs/ai/skills-reference.mdx` via the +existing `build-skill-docs.ts` generator. The repo-internal `.claude` version is +untouched. Releases nothing: no published package's shipped files change +(`packages/spec` does not ship `scripts/`). diff --git a/content/docs/ai/skills-reference.mdx b/content/docs/ai/skills-reference.mdx index 14e9cd92ce..56a029f505 100644 --- a/content/docs/ai/skills-reference.mdx +++ b/content/docs/ai/skills-reference.mdx @@ -5,7 +5,7 @@ description: Reference for every ObjectStack AI skill — domain-scoped knowledg # AI Skills Reference -ObjectStack ships a set of **domain-specific skills** that teach AI assistants (Claude Code, GitHub Copilot, Cursor, …) the protocol's schemas, patterns, and constraints. This guide is the complete catalog. +ObjectStack ships a set of **domain-specific skills** that teach AI assistants (Claude Code, GitHub Copilot, Cursor, …) the protocol's schemas, patterns, and constraints — plus **process** skills that teach how work moves through a project rather than what to author. This guide is the complete catalog. **New to skills?** Read [AI Skills System](/docs/ai/skills) first for the conceptual overview, then return here for the per-skill reference. @@ -39,7 +39,7 @@ Skills are versioned as **one bundle**. You do not track or update each skill in {/* BEGIN GENERATED: skills (packages/spec/scripts/build-skill-docs.ts) — DO NOT EDIT */} -ObjectStack ships **9 domain-specific skills**. Each is self-contained — an AI assistant loads only the ones a task needs. +ObjectStack ships **10 skills** — one per authoring domain, plus process skills for how a project is delivered. Each is self-contained: an AI assistant loads only the ones a task needs. ## Quick Reference @@ -54,6 +54,7 @@ ObjectStack ships **9 domain-specific skills**. Each is self-contained — an AI | 7 | [API](#api) | `api` | `skills/objectstack-api/` | Design the server-side API surface that an ObjectStack runtime exposes — REST endpoints, auth providers, realtime channels, error envelopes, batch/versioning contracts. | | 8 | [i18n](#i18n) | `i18n` | `skills/objectstack-i18n/` | Author ObjectStack translation bundles — object/field labels, view text, app navigation strings, automation messages — and configure locale fallback, coverage reporting, and the per-locale source layout. | | 9 | [Formula](#formula) | `expression` | `skills/objectstack-formula/` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. | +| 10 | [PM Dispatch](#pm-dispatch) | `process` | `skills/objectstack-pm-dispatch/` | Run a project-manager dispatch loop over a GitHub backlog: triage and queue ready issues, claim each one, dispatch it to a parallel developer agent that returns a structured JSON report, review the results against GitHub, and drive accepted pull requests to landing — escalating to the maintainer only what genuinely needs a human decision. Ships the developer-agent operating template the loop injects into every dispatch (no custom agent types required) and the upstream-reporting procedure for platform defects an app project finds. | --- @@ -183,6 +184,20 @@ Do not use for SQL fragments (driver-native), cron schedules (cron dialect), or --- +### PM Dispatch + +**Domain** `process` · **Path** `skills/objectstack-pm-dispatch/` + +Run a project-manager dispatch loop over a GitHub backlog: triage and queue ready issues, claim each one, dispatch it to a parallel developer agent that returns a structured JSON report, review the results against GitHub, and drive accepted pull requests to landing — escalating to the maintainer only what genuinely needs a human decision. Ships the developer-agent operating template the loop injects into every dispatch (no custom agent types required) and the upstream-reporting procedure for platform defects an app project finds. + +Use when asked to "work through the backlog", "batch-dispatch issues", "派发 issue 给开发 agent", to stand up a multi-agent delivery loop in an ObjectStack app project, or to report a platform bug found while building an app. + +Do not use for authoring ObjectStack metadata (the domain skills cover that), for a single already-scoped change you can just make, or as a replacement for the project's own conventions file — that file always wins. + +**Tags:** `pm`, `dispatch`, `backlog`, `triage`, `multi-agent`, `delivery`, `github`, `escalation`, `upstream` + +--- + {/* END GENERATED: skills */} ## Skill anatomy diff --git a/content/docs/ai/skills.mdx b/content/docs/ai/skills.mdx index 055f7857be..5a32d81dfd 100644 --- a/content/docs/ai/skills.mdx +++ b/content/docs/ai/skills.mdx @@ -128,7 +128,7 @@ This format ensures AI assistants generate protocol-compliant code. ## The skill catalog -ObjectStack provides **nine domain-specific skills**, one per protocol domain — Platform, Data, Query, UI, Automation, AI, API, i18n, and Formula. +ObjectStack provides **nine domain-specific skills**, one per protocol domain — Platform, Data, Query, UI, Automation, AI, API, i18n, and Formula — plus **PM Dispatch**, a `process` skill that teaches no schema: it runs the multi-agent backlog → dispatch → review → land loop for a project built on the platform, and the procedure for reporting a platform defect upstream. The authoritative list (names, domains, and "use when / do not use" boundaries) is generated from each skill's `SKILL.md` frontmatter. See the **[AI Skills Reference](/docs/ai/skills-reference)** for the full catalog with per-skill detail. diff --git a/packages/spec/scripts/build-skill-docs.ts b/packages/spec/scripts/build-skill-docs.ts index b063fda7cb..e8da53641d 100644 --- a/packages/spec/scripts/build-skill-docs.ts +++ b/packages/spec/scripts/build-skill-docs.ts @@ -54,6 +54,7 @@ const DISPLAY: Array<{ name: string; label: string }> = [ { name: 'objectstack-api', label: 'API' }, { name: 'objectstack-i18n', label: 'i18n' }, { name: 'objectstack-formula', label: 'Formula' }, + { name: 'objectstack-pm-dispatch', label: 'PM Dispatch' }, ]; // ── Frontmatter parser ─────────────────────────────────────────────────────── @@ -178,7 +179,7 @@ function renderGuideBlock(skills: Skill[]): string { return [ begin, '', - `ObjectStack ships **${skills.length} domain-specific skills**. Each is self-contained — an AI assistant loads only the ones a task needs.`, + `ObjectStack ships **${skills.length} skills** — one per authoring domain, plus process skills for how a project is delivered. Each is self-contained: an AI assistant loads only the ones a task needs.`, '', '## Quick Reference', '', diff --git a/skills/README.md b/skills/README.md index ed90a41e5e..b8cee42612 100644 --- a/skills/README.md +++ b/skills/README.md @@ -12,7 +12,7 @@ npx skills add objectstack-ai/objectstack/skills --all The `/skills` subpath matters: it is the published catalog boundary — pointing the skills CLI at the repo root would also pick up repo-internal skills (#3101). -Each skill is self-contained: a `SKILL.md` with YAML frontmatter, plus a +Each **domain** skill is self-contained: a `SKILL.md` with YAML frontmatter, plus a `references/_index.md` that points into the authoritative Zod sources in `node_modules/@objectstack/spec/src/...` (the published `@objectstack/spec` package ships these `.zod.ts` sources, so the pointers resolve in consumer @@ -38,6 +38,7 @@ apps too). | [API](./objectstack-api/SKILL.md) | `api` | Design the server-side API surface that an ObjectStack runtime exposes — REST endpoints, auth providers, realtime channels, error envelopes, batch/versioning contracts. | | [i18n](./objectstack-i18n/SKILL.md) | `i18n` | Author ObjectStack translation bundles — object/field labels, view text, app navigation strings, automation messages — and configure locale fallback, coverage reporting, and the per-locale source layout. | | [Formula](./objectstack-formula/SKILL.md) | `expression` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. | +| [PM Dispatch](./objectstack-pm-dispatch/SKILL.md) | `process` | Run a project-manager dispatch loop over a GitHub backlog: triage and queue ready issues, claim each one, dispatch it to a parallel developer agent that returns a structured JSON report, review the results against GitHub, and drive accepted pull requests to landing — escalating to the maintainer only what genuinely needs a human decision. Ships the developer-agent operating template the loop injects into every dispatch (no custom agent types required) and the upstream-reporting procedure for platform defects an app project finds. | @@ -59,6 +60,10 @@ skills// # inert (but harmless) in consumer installs ``` +A `process` skill (`metadata.domain: process`) points at no Zod schema, so it +carries `SKILL.md` alone — `gen:skill-refs` only visits skills listed in its +`SKILL_MAP`, and there is nothing to map. + `SKILL.md` frontmatter fields: | Field | Purpose | @@ -66,8 +71,8 @@ skills// | `name` | Stable id (matches directory name). | | `description` | One paragraph — what the skill is for *and* what it is **not** for. | | `license` | `Apache-2.0`. | -| `compatibility` | Minimum `@objectstack/spec` version. | -| `metadata.domain` | One of: `platform`, `data`, `query`, `ui`, `automation`, `ai`, `api`, `i18n`, `formula`. | +| `compatibility` | Minimum `@objectstack/spec` version — or, for a `process` skill that binds to no schema, the tooling it needs. | +| `metadata.domain` | Authoring domain — one of: `platform`, `data`, `query`, `ui`, `automation`, `ai`, `api`, `i18n`, `expression` — or `process` for a delivery-process skill that teaches no schema. | | `metadata.tags` | Short comma-separated keywords for retrieval. | --- diff --git a/skills/objectstack-pm-dispatch/SKILL.md b/skills/objectstack-pm-dispatch/SKILL.md new file mode 100644 index 0000000000..3af57781c9 --- /dev/null +++ b/skills/objectstack-pm-dispatch/SKILL.md @@ -0,0 +1,807 @@ +--- +name: objectstack-pm-dispatch +description: > + Run a project-manager dispatch loop over a GitHub backlog: triage and queue + ready issues, claim each one, dispatch it to a parallel developer agent that + returns a structured JSON report, review the results against GitHub, and + drive accepted pull requests to landing — escalating to the maintainer only + what genuinely needs a human decision. Ships the developer-agent operating + template the loop injects into every dispatch (no custom agent types + required) and the upstream-reporting procedure for platform defects an app + project finds. Use when asked to "work through the backlog", + "batch-dispatch issues", "派发 issue 给开发 agent", to stand up a + multi-agent delivery loop in an ObjectStack app project, or to report a + platform bug found while building an app. Do not use for authoring + ObjectStack metadata (the domain skills cover that), for a single + already-scoped change you can just make, or as a replacement for the + project's own conventions file — that file always wins. +license: Apache-2.0 +compatibility: > + No @objectstack/spec dependency — process skill. Needs a GitHub repository + with issues enabled, and either the `gh` CLI or GitHub API tooling + (MCP server) with issue/label/PR write access. +metadata: + author: objectstack-ai + version: "1.0" + domain: process + tags: pm, dispatch, backlog, triage, multi-agent, delivery, github, escalation, upstream +--- + +# 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** + +```bash +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 #128 #131 # 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. + +```json +{ + "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](#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 and review policy. 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:` | backlog filter label; `label:all` = every open unassigned issue | `pm:queue` | +| `repo:` | which repository's **backlog** to scan | `backlogRepo` | +| `batch:` | max developer agents in flight at once | `batch` | +| `rounds:` | 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 the issue) | + +**One-time label setup** (idempotent — run at the start of the first round for +every entry in `repos`): + +```bash +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 issues carrying no `pm:*` / `needs-user-decision` label and +classify each: + +- **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 two-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: + +```bash +git log --oneline -20 origin/main -- +gh pr list --state merged --search "" --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}` + 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: #`. 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: #` 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 branch name is the key: every later + artifact (worktree, push, PR) hangs off it. + + > Claim: PM loop round N + > Branch: `claude/issue--` + > Worktree: `-issue-` + +3. **Race check.** Assignment is idempotent, so two agents can both "succeed". + Re-read the comments; if an earlier claim comment with a *different* branch + name exists, you lost — touch nothing of theirs, reply that you are yielding, + and pick another issue. **First comment wins.** + +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](#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. +- {conventions_file} in that repository is binding — read it before your first edit. +- The issue is already claimed; do not touch its assignee. +- Deliver a DRAFT PR in {target_repo} whose body starts with + "Fixes {backlog_repo}#{n}". Never merge anything. +- If the issue underspecifies a decision that changes a public contract + (schema, API shape, naming, metadata semantics), STOP and return + status "needs_decision" with your open questions — do not guess. +Return ONLY the JSON report defined in the operating procedure. +``` + +#### Dispatch backends + +**`mode:subagent` (default).** Sub-agents inside the PM's own session. Reports +come back directly as each agent's final message — a lossless channel. Prefer +this mode; it is simpler and nothing can be lost between agent and reviewer. + +**`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) +and must instruct the agent to **post the JSON report as a comment on the +issue**, prefixed with a machine-findable marker such as +``, in addition to opening the draft PR. + +### 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 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.** + +### 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". +- 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. + This applies to **developer-agent PRs dispatched by this loop only** — the + PM's own tooling PRs stay with the maintainer. +- **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. **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] `, same label) ONLY when + the decision has no natural anchor — it spans several issues, or arose with + no issue of its own. +2. Write the analysis with: background, the precise question, the options, your + recommendation, and the related issues / PRs / branches — **and analyze + every option on the two fixed axes below.** +3. 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 two-axis decision frame (binding) + +Every option in an escalation is analyzed on **both** axes. This framing is the +core of the escalation, not decoration. + +**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. + +Your recommendation must be justified on **both** axes. If they conflict, +present the trade-off honestly and let the maintainer decide. + +### 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 this **verbatim** into every dispatch prompt. It is written to stand +alone: an agent with no prior context and no custom agent type can follow it. +Placeholders in `{…}` are filled by the PM. + +````text +You are a developer agent. You were dispatched with exactly ONE GitHub issue. +Your entire deliverable is that issue implemented, pushed as a draft PR, plus +the JSON report below as your FINAL MESSAGE — it is parsed mechanically, so +return the JSON and nothing else. + +{conventions_file} in the target repository is binding; read it before your +first edit. It overrides this template wherever they disagree. The rules that +most often get missed: + +1. Worktree-first. Before any edit: + git worktree add ../-issue- -b claude/issue-- origin/{default_branch} + then cd there and install dependencies. Never edit a shared checkout — + other agents switch its HEAD under you. One worktree PER REPOSITORY if the + change spans siblings. +2. The issue is already claimed. Do not change assignees. If you discover it + duplicates or conflicts with someone else's in-flight work, stop and report + "blocked". +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. +4. Never force-push, never push the default branch, never merge anything. + Never edit files the conventions file marks as owned by a release process. +5. Contract-first. If the fix tempts you to add a lenient fallback in a + consumer (an alias `??`, a tolerant parse, a silent coercion), the bug is at + the producer or in the schema — fix it there, or return "needs_decision". + +Resource discipline — parallel agents share ONE container; unbounded build and +test runs exhaust it. Binding: + +1. Serialize the heavy phase. Wrap every build and test run in a shared + container-wide lock so editing parallelizes but memory peaks never stack: + flock -w 7200 /tmp/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, not the whole + repository, unless the task requires a full pass. Cap test parallelism + (e.g. vitest --maxWorkers=2). +4. Clean up: after the PR is up, remove your worktree + (git worktree remove --force). Leftover dependency trees exhaust the + container's disk, which fails as confusingly as running out of memory. +5. NEVER kill a process by name. A name-matched kill (pkill -f ) can take + down a parallel agent's run. Record the PID of what you start and operate on + that PID only (kill $PID; liveness via kill -0 $PID). A pgrep pattern can + match your own watcher and never terminate. + +Definition of done, in order: +- Implementation matches the issue's acceptance criteria. +- Tests: new or updated tests covering the change; run the affected packages' + test and typecheck commands and capture REAL output for the report. +- Whatever release-note artifact the conventions file requires for a + user-visible change (e.g. a changeset entry). +- Pushed: git push -u origin claude/issue-- (retry on network failure + with backoff). +- A DRAFT PR to the default branch, body starting "Fixes {backlog_repo}#", + written in the language the repository's PRs use. +- Tear down anything you started (dev servers, temporary processes) by PID. + +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. +Analyze every option on two fixed axes: +- Long-term architectural soundness for THIS project — which option matches a + sustainable architecture (no workarounds, contract-first), not which is + cheapest today. Name the long-term cost of any patch-style option. +- Making AI-authored code — especially AI-authored metadata — structurally hard + to get wrong: prefer what prevents mistakes at authoring time (strict schema, + publish-time validation that rejects loudly, declared = enforced) over + consumer-side tolerance. Lenient consumers are where AI-generated errors hide + and multiply. +Justify your recommendation on both axes; if they conflict, present the +trade-off and let the maintainer decide. + +Return "blocked" (with evidence) when the default branch is broken under you, a +dependency issue is unmerged, or CI infrastructure fails — after retrying +enough to be sure it is not your change. + +Final message — exactly this JSON, no prose around it: + +{ + "issue": , + "status": "done | rework | blocked | needs_decision", + "branch": "claude/issue--", + "pr": "", + "summary": "what was implemented, 2-4 sentences", + "tests": "commands run + pass/fail evidence (real output excerpts)", + "open_questions": [ + { "question": "…", "options": ["A …", "B …"], "recommendation": "A, because …" } + ], + "out_of_scope_findings": ["filed as #: one-line description"] +} + +Use "rework" for a partial result you know is incomplete (say why in summary). + +Practical trap when filing issues or PRs through the GitHub API: the body +sanitizer strips "<" followed by a letter as an HTML tag AT REST, which +destroys TypeScript generics. Write a space after each "<" and read the stored +body back to verify when a snippet is load-bearing. +```` + +--- + +## Report contract (what a developer agent returns) + +```json +{ + "issue": 123, + "status": "done | rework | blocked | needs_decision", + "branch": "claude/issue-123-short-slug", + "pr": "https://github.com/acme/hotcrm/pull/456 | null", + "summary": "what was implemented, 2-4 sentences", + "tests": "commands run + pass/fail evidence", + "open_questions": [ + { "question": "…", "options": ["A …", "B …"], "recommendation": "A, because …" } + ], + "out_of_scope_findings": ["filed as #457: …"] +} +``` + +`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. + +--- + +## Upstream reporting — platform defects found while building an app + +An app project (built on ObjectStack, or on any platform it does not own) +regularly trips over defects that are **not the app's to fix**. The temptation +is to make the app tolerate it and move on. Do not. A workaround in the app +hides the defect from the people who can fix it, ships a second source of truth +to every future author, and outlives the upstream fix by years — and when the +app's metadata is AI-authored, the tolerant path is copied into everything +generated next. + +**The upstream repository is derived, not configured.** Read it from the +failing dependency itself (its `package.json` `repository` field, or the +registry page). For `@objectstack/*` packages that is +`objectstack-ai/objectstack`. + +### 1. Stale-premise check first + +Before writing anything, establish that the defect still exists upstream: + +- reproduce against the **current** upstream default branch or the newest + published version, not the version the app happens to pin; +- search the upstream repository's open **and closed** issues and merged PRs + for the symptom, the error string, and the module name; +- read the upstream changelog between your pinned version and the latest. + +If it is already fixed, the app-side task is an **upgrade**, not a report. Say +so and stop. + +### 2. Write a report that can be acted on without your app + +- **Minimal repro** — the smallest metadata or code that shows the defect, + standalone, with no dependency on the app's own packages. If it cannot be + reduced, say exactly what part of the app is load-bearing and why. +- **Pinned versions** — the exact versions of every upstream package involved + (`pnpm list @objectstack/…`), plus runtime and OS if relevant. "Latest" is + not a version. +- **Expected vs actual**, and the **contract you are citing**: the schema line, + documented behaviour, or ADR the upstream is violating. A report that names + the contract gets triaged as a defect; one that does not gets triaged as a + question. +- **What you did NOT do**: state that the app has no workaround, so the + upstream can see the real blast radius. + +### 3. File it as a guest, not as a scheduler + +- Open the issue **in the upstream repository**, with a backlink line + `Part of /#` so both sides can navigate. +- **Never apply the upstream's queue labels** (`pm:queue`, `pm:dispatched`, …), + never assign it, never add it to their board. Their PM triages their backlog; + labeling it yourself injects work into a queue whose in-flight batch you + cannot see — the same collision the single-scheduling-authority ban exists + for. +- Do not open a fix PR upstream unless your project has the appetite and the + upstream's conventions invite it. If it does, that is an ordinary dispatch + against the upstream repository, governed by **their** conventions file. + +### 4. Park the app-side task honestly + +The app-side issue does not silently continue. Pick one and record it: + +- **`Blocked-by: /#`** in the body — the PM's + batch selection skips it until that issue closes; or +- **pin the current upstream version** with the unblock condition written down: + "pinned at 17.2.0; unpin when `#` ships in >= 17.3". A pin with + no recorded unblock condition is a workaround with better manners. + +Either way the app-side issue stays open and visible. **"We worked around it" +is never a resolution** — closing it is what makes the defect permanent. + +--- + +## Multiple PMs — shard by repository, never share a queue + +The claim protocol makes concurrent PMs *safe*, not *useful*: batch +independence is only checked within one PM's view, so two PMs on the same queue +can claim different issues that collide on shared files. Scale in this order: + +1. **One PM, bigger batch** (`batch:5`), heavy tasks via `mode:cloud` — adds + compute without adding schedulers. +2. **When one PM genuinely cannot keep up**, a second session takes a **whole + repository** as its shard (`/pm-dispatch repo:/`) — file + universes are disjoint by construction. A sharded PM states its shard in + every claim comment and **never claims outside it**. +3. **Multiple PMs on the same queue: prohibited.** All cost, no throughput. + +**Shard ownership is registered, never assumed.** A registry issue in the +backlog repository records which session owns which shard. A PM taking over a +shard comments there as its FIRST action, and again when handing off. An +unowned shard may be **caretaken** by the backlog PM, but the moment a shard is +registered to another session the caretaker stops dispatching into it — +in-flight claimed tasks finish under whoever claimed them, and everything else +belongs to the new owner. State the mode in claim comments so the registry and +the claims never disagree silently. + +**Work crosses shard lines; PMs never do.** + +- **Transfer via the target queue**: file the piece as an issue in the target + repository with the queue label and a source line + `Part of #`. The target shard's PM picks it up through its own + sweep — the queue label IS the inter-PM channel. +- **Dependencies via `Blocked-by:`** on the waiting side. +- **Follow-up chores belong to the consuming shard** — when an upstream change + lands, the dependent repository's adaptation issue is filed by the PM that + owns that repository; it knows its own surfaces. +- **Shared contract surfaces have one owner.** Anything touching the shared + schema or contract package transfers to the backlog PM regardless of who + needs it — only that PM sees that repository's in-flight batch and its + generated-artifact collisions. +- Cross-repository parent/sub-issue chains stay coordinated by the backlog PM; + sharded PMs coordinate only chains fully inside their shard. + +**Linkage chores are issues, not memory.** When an accepted PR's artifacts must +flow into another repository (a regenerated client, a version bump, a refreshed +build), file that follow-up in the consuming repository's backlog immediately, +blocked by the PR until it merges. Nobody remembers. + +--- + +## 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 +enforces with 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. +- 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 | +| Merge policy (merge queue, serial merge, maintainer-only) | 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.