project-dev-orchestration

project-dev-orchestration: design

How the engine works, and why. Living document. The day-to-day guide is user-guide.md.

Script names below are written bin/<name> (paths in this repo). Run them as pdo <name> … once bin/install --bin has put the pdo dispatcher on PATH, or by absolute path from an engine checkout.

Roles

Spawning

Routing

Signal Route
route:small / route:design / route:build label that route
needs-design label design
Epic whose body links a READY docs/design/*.md build (base: the epic’s docs/*-design branch)
Other Epic design
Bug, Task, single Feature small

ROUTE_SKILL maps a route to its skill. A command agent may push back: a “small” issue that turns out to need a migration or a cross-cutting UX decision stops and asks to be re-routed.

Epic phases. Phase sub-issues carry epic-phase. The queue excludes them and spawn refuses them without --allow-phase: the epic’s build agent works every phase. Independent follow-ups carry no epic-phase label and dispatch normally.

Queue (deterministic, no model judgement)

bin/queue ranks open issues in the managed repos (REPO_ROOT minus SELF_MANAGED_REPOS):

  1. Board Status = Claude Work This (CWT_STATUS) first, then P-level (P_LABELS, best first; unlabelled sorts last as P-).
  2. Within a level: Security-labelled, then Bugs, then issues whose parent epic is In Progress, then oldest first (a “created YYYY-MM-DD by” footer in the body wins over created_at, for migrated issues).

Concurrency and the usage budget

The watcher

Claude only acts when a turn is triggered, so the orchestrator isn’t self-sustaining. bin/watch is a token-free probe; the model only reads what changed.

Hibernation and close-out

A parked agent holds several hundred MB (the agent CLI, browser automation, MCP servers), and enough of them push the machine into swap, where low-memory reapers start killing background shells (including the watcher).

QC

PR priority labels

bin/pr-priority gives each open PR the best P-label among the issues it closes (closingIssuesReferences, or the body’s Closes/Fixes/Resolves/Part of #N for stacked and epic PRs, which GitHub doesn’t link). It also copies CARRY_LABELS (default client-requested), add-only. It never downgrades: a higher P-label already on the PR stays. It runs after gh pr create and on every queue refresh.

Intake

The intake skill classifies each report, runs one bounded, fact-only look (duplicates, related docs and memories, candidate files and entry points) into a “Starting points (unverified)” section with no root-cause guesses, asks at most one clarification round, and files through bin/file-issue (typed, labels validated, parent sub-issue attached, board status set). Agents never prompt it directly; they queue reports with bin/intake-submit, and only an explicit “inbox” run triages the queue.

Configuration

Resolution (one place: bin/lib.sh)

Project env keys

Key Default Meaning
ORG (required) GitHub owner of the repos and the board
PROJECT_NUMBER (none) org project board number; the queue and claim need it
REPO_ROOT (required) declare -A: repo name → local clone root
SELF_MANAGED_REPOS () repos another manager owns; queue and watch skip them
REPO_MANAGER () declare -A: self-managed repo → its manager’s workspace/session name (fleet)
MANAGER Orchestrator (<p>) with a board, else empty the project manager’s workspace/session name; empty = the human
MANAGER_SLUG a slug of MANAGER (else of the project name), e.g. orchestrator-acme the claim label is managed-by:<slug>; cut to 39 characters
CLAIM_ASSIGNS 0 1 = claim also assigns the gh login (the pre-0.6 lock); 0 leaves assignees to humans
PORTFOLIO_PRIORITY 3 rank across projects, 1 = highest (fleet advice)
RESOURCE_PROFILE medium light / medium / heavy: MB and usage weight per active agent
AGENT_MB / AGENT_USAGE_WEIGHT from the profile override the profile’s per-agent MB / usage weight
FLEET_ADVICE ignore honour: queue and watch apply fresh fleet-advice.json
ROUTE_SKILL (required for spawn) declare -A: route → first-prompt skill
DEFAULT_MODEL opus model alias for launched agents
BRIEF_TEMPLATE see Spawning dispatch brief template
PRIMARY_REPO (none) bare #n means this repo; default --repo for qc, shots, pr-agent, fixtures
GH_USER (none) pin agents’ gh to this login; unset = no pin
IDENTITY_HOOK (none) hook <dir> prints extra wrapper words for an agent in that checkout
TEAM_UP_HOOK (none) extra team-up steps; gets team-up’s unknown options
SYSREPORT_HOOK (none) extra sysreport lines
QC_HOOK (none) the QC servers (contract below)
QC_REPO PRIMARY_REPO repo bin/qc serves
QC_WORKTREE qc QC worktree dir name under the repo root
QC_PORTS (none) ports hibernation never stops
FIXTURE_EXT (none) the project’s fixture extension (a mismatch warns)
FIXTURE_EXT_BY_REPO () declare -A: repo → its fixture extension, for repos whose format differs; qc-fixture-push --repo uses it before FIXTURE_EXT
WATCH_EVENT_HOOK (none) executable the watcher runs each snapshot for project-specific events (contract below)
WATCH_EVENT_HOOK_TIMEOUT 120 seconds before the watcher abandons a hook run
FIXTURE_BRANCH qc-fixtures never-merged fixture branch
SCREENSHOT_DIR .omc/qa screenshot root inside a worktree
CARRY_LABELS client-requested issue labels copied onto PRs
CWT_STATUS Claude Work This board status that jumps the queue
P_LABELS P1-urgent P2-high P3-medium P4-low priority labels, best first
LABEL_QC_READY / LABEL_QC_DONE ready for QC / QC complete - ready to review QC state labels
SYSTEM_MANAGER_WORKSPACE System Manager named in the queue’s orchestrator-system reason
CONTROL_DIR the env’s config dir cwd of the standing orchestrator, QC and intake agents
ORCHESTRATE_SKILL / QC_SKILL / INTAKE_SKILL ${PDO_SKILL_PREFIX}project-orchestrate / …project-qc-review / …project-intake standing agents’ skills
AGENT_CLI claude agent CLI (only claude today)
MAX_ACTIVE, WEEKLY_CEILING, FIVE_HOUR_CEILING 3, 80, 90 concurrency and usage limits

PDO_SKILL_PREFIX (environment or config.env) defaults to /project-dev-orchestration:, the Claude Code plugin namespace. Set it to / when the skills are linked un-namespaced (bin/install --link).

Example project env (<config dir>/projects/acme.env, plain bash):

ORG=acme-co
PROJECT_NUMBER=3
declare -A REPO_ROOT=([acme-app]="$HOME/src/acme-app.git" [acme-engine]="$HOME/src/acme-engine")
PRIMARY_REPO=acme-app
declare -A ROUTE_SKILL=(
  [small]="${PDO_SKILL_PREFIX}project-small-feature-or-bug-fix"
  [design]="${PDO_SKILL_PREFIX}project-feature-design"
  [build]="${PDO_SKILL_PREFIX}project-feature-build"
)
QC_HOOK=hooks/acme-qc-hook        # relative: resolved against this config dir
QC_PORTS="3000 8080"

Hooks

Hooks are executables. They get PDO_HOME, PDO_PROJECT, PDO_ENV_DIR, PDO_STATE_DIR, ORG and PRIMARY_REPO in the environment, and may source "$PDO_HOME/bin/lib.sh" for helpers such as pdo_free_port.

QC_HOOK <stop|start|wait|status> [options]. bin/qc start runs: stop, then fetch + checkout + clean, then start, then writes $PDO_QC_STATE/current.json (merging $PDO_QC_STATE/hook-current.json if start wrote one), then wait. Options bin/qc doesn’t know (e.g. --fresh-db) are passed through. Environment: PDO_QC_STATE, PDO_QC_REPO, PDO_QC_ROOT, PDO_QC_WORKTREE, PDO_QC_PR, PDO_QC_REF, PDO_QC_SHA, PDO_QC_BASE, PDO_QC_PREV_SHA (the worktree’s previous head, to skip an unchanged dependency install) and PDO_QC_KEY (pr:<n> or ref:<branch>, e.g. to decide when to reseed a QC database). The hook should never use a database that command agents start and stop, and should clone a seed per served PR rather than roll migrations back.

Watcher event hook (WATCH_EVENT_HOOK). The extension point for project-specific signals (PR-comment commands, another service’s state). bin/watch runs it once per snapshot, after the PR index is read, with no arguments and a JSON context on stdin:

{"version": 1, "project": "acme", "org": "acme", "repos": ["widget"],
 "since": "2026-10-16T18:00:00Z", "now": "2026-10-16T18:05:00Z", "peek": false,
 "trusted_associations": ["COLLABORATOR", "MEMBER", "OWNER"], "state_dir": "/home/u/.local/state/pdo",
 "labels": {"qc_ready": "ready for QC", "qc_done": "QC complete - ready to review"},
 "prs": [{"repo": "widget", "number": 5, "url": "https://github.com/acme/widget/pull/5", "head": "feat/5",
          "head_sha": "abc123", "base": "main", "labels": ["ready for QC"], "draft": false,
          "mergeable": "MERGEABLE"}]}

It prints a JSON array of events on stdout, in the watcher’s own shapes, e.g. {"type": "qc_feedback", "repo": "widget", "number": 5, "id": 901, "author": "boss", "url": "…", "body": "…"}. The watcher merges them as if it had found them: a wake event ends --until-event, and an event with "quiet": true (or a quiet type) rides along with the next wake. Hook events skip the snapshot diff, so each must be emitted once. Items that aren’t objects with a string type are dropped with a warning; at most 200 are taken per run.

Robustness: a run is killed after WATCH_EVENT_HOOK_TIMEOUT seconds (default 120). A timeout, a non-zero exit, output that isn’t a JSON array, a missing file, or a GitHub outage (the hook is skipped when the PR index can’t be read) prints a warning on stderr and adds the quiet event event_hook_failed; the watcher carries on. The hook’s last few stderr lines are echoed with a watch: hook: prefix.

The project profile

The engine env above is for scripts. What agents need to know about a product repo lives in that repo, in .agents/project-profile.md on the default branch; the skills cite its sections as §<Section>.

Portfolio manager

One level above the per-project managers. “Observe and coordinate, observe the system’s constraints, and prioritise the managers’ allocations; not a strict slot queue, since projects need different amounts of resources.” It never does project work.

Coordination between managers

Several managers (project orchestrators, leads that self-manage a repo, people’s own agents) can work the same repos. Three GitHub-side mechanisms keep them from colliding; anyone’s agent can read them without the engine.

gh calls for another org run as the GH_USER of a project whose ORG owns the repo (gh auth token -u; never a switch).

Overlays

A private overlay is a config dir that sits first on PDO_CONFIG_DIRS: its project envs, prompts, hooks, config.env and any project-only scripts. An overlay can keep old script paths working with a shim per name that sets PDO_CONFIG_DIRS (and, if it wants the old default, PDO_PROJECT) and execs the engine’s script, so prompts, briefs and memories that name the old paths still work.

Lifecycle end state

A command agent runs until its PR is labelled ready for QC, then idles. After QC it wraps the PR up: fixes, final push and comment. Merging stays manual.

Borrowed ideas