docs/template/delivery

delivery-orchestrator.md

docs/template/delivery/delivery-orchestrator.md

Delivery Orchestrator

This repo now includes a small repo-local delivery orchestrator for stacked ticket work.

Read this document in full before executing any orchestrator command. The step order below is mandatory and must not be inferred from first principles.

Critical Step Order

For every code ticket, these steps must run in this exact sequence:

  1. start — create worktree, materialize handoff
  2. Write the failing behavior test and commit it with a [red] suffix
  3. post-red — verify and record the [red] commit before implementation
  4. Implement + verify (bun run verify:quiet inner loop, then bun run ci:quiet before open-pr)
  5. post-verify [clean|patched] — self-audit
  6. write-subagent-adversarial-review — primary agent authors the filled adversarial prompt (required for code tickets when subagentReview is not "disabled")
  7. subagent-review — advisory subagent pass against that exact prompt (programmatic runner with --subagent, or recorder mode)
  8. reconcile-subagent-review — compare ledger rows to git state since reviewedHeadSha; hard-block silent lies before publish (also runs inside open-pr)
  9. open-pr — publish the PR (never before subagent review and reconciliation gates complete)
  10. poll-review — external AI review window
  11. record-review — only needed when poll-review leaves ticket in needs_patch
  12. advance — move to next ticket

Tickets declare Red: required or Red: skip in their metadata block. Code tickets use Red: required; tickets with no testable behavior may declare Red: skip, and doc-only branches also skip post-red structurally.

post-red must precede implementation. write-subagent-adversarial-review must precede subagent-review. subagent-review must precede reconcile-subagent-review. reconcile-subagent-review must precede open-pr. open-pr must precede poll-review. Skipping or reordering these steps is not supported.

Phase 14 changes (subagent-review fidelity)

Phase 14 makes the subagent-review ledger semantically honest. Operator-facing docs and skills should describe the same contract:

Programmatic subagent runners

subagent-review can invoke a headless runner when --subagent (or subagentRunner in config) names one of:

runnerKindBinaryVerified invocation (prompt = filled *-subagent-review.prompt.md bytes)
claude-cliclaudeclaude -p <prompt>
codex-clicodexcodex exec [--output-last-message <path>] --color never <prompt>
cursor-cliagentagent --print --trust --output-format text --workspace <worktree> <prompt>

Prerequisites: install the binary on PATH. For cursor-cli, run agent login or set CURSOR_API_KEY before delivery. The orchestrator runs the command in the ticket worktree, persists stdout to *-subagent-review.report.md, stderr to *-subagent-review.trace.log, and records honest skipped rows when a runner is unavailable, rate-limited, or violates the advisory-only contract (any file write in the worktree).

Fallback order: try the operator-selected runner first, then each other programmatic runner in stable order (claude-clicodex-clicursor-cli, with the requested runner moved to the front). Ledger rows record runnerKind (what ran) and fallbackFrom (what was requested when fallback fired).

Stance

The orchestrator is repo tooling, not app runtime code.

That means:

This keeps the product boundary honest. The delivery tool is a maintainer workflow helper, not app runtime code.

Module Structure

After EE11, tools/delivery/ is decomposed into focused single-concern modules. orchestrator.ts is a pure re-export barrel with no logic — it exists only so external callers can import from one stable path.

ModuleConcern
types.tsAll shared TypeScript types and interfaces
env.ts.env parsing (parseDotEnv) and environment readiness helpers
runtime-config.tsOrchestratorConfig loading, resolution, package-manager inference, and invocation formatting
context.tsPlain DeliveryOrchestratorContext construction from resolved config and platform adapters
format.tsHuman-readable status formatting (formatStatus, formatCurrentTicketStatus, etc.) with config input
platform.tsRaw platform primitives: spawnSync, git worktree parsing, shell helpers
platform-adapters.tscreatePlatformAdapters(config) factory that binds runtime/config to platform primitives
planning.tsBranch and worktree naming (deriveBranchName, deriveWorktreePath, findExistingBranch)
state.tsState persistence (loadState, saveState, normalizeDeliveryStateFromPersisted)
ticket-flow.tsTicket lifecycle transitions, handoff artifact writing, materializeTicketContext
notifications.tsTelegram notification events and formatting
pr-metadata.tsPR title/body construction and AI-review section builders
review.tsReview polling lifecycle, fetcher/triager adapters, artifact parsing
cli-runner.tsrunDeliveryOrchestrator dispatch switch and explicit command-helper wiring
cli.tsArgument parsing (parseCliArgs, getUsage)
subagent-runner.tsRunner types and functions (tryRunner, buildRunnerArtifact, validateRunnerArtifact)
orchestrator.tsPure re-export barrel — no logic

Each source module has a corresponding test file under tools/delivery/test/. Import from source modules in tests, not from the barrel.

Runtime Context Boundary

Runtime config is not stored in a module-level singleton. The CLI loads orchestrator.config.json, resolves defaults once, builds a DeliveryOrchestratorContext, and passes that context or its config field into helpers that need runtime values.

The context is intentionally small:

This keeps dependency direction visible at call sites. Helpers that need branch or runtime behavior accept ResolvedOrchestratorConfig; helpers that need git, GitHub, filesystem, or process adapters accept DeliveryOrchestratorContext. Tests use local config/context fixtures instead of mutating global module state.

runtime-config.ts still owns config loading and validation, but it does not export _config, initOrchestratorConfig, or getOrchestratorConfig. platform-adapters.ts no longer reads runtime config directly; all adapter methods close over the config passed to createPlatformAdapters(config). Formatters receive config explicitly so status rendering and boundary guidance do not depend on hidden process state.

Configurable Core

The orchestrator core now reads orchestrator.config.json at the repo root so branch, plan-root, runtime-internal, bootstrap defaults, ticket-boundary behavior, and review policy are not hardcoded:

{
  "defaultBranch": "main",
  "deliveryBaseBranch": "main",
  "closeoutBranch": "main",
  "planRoot": "docs",
  "runtime": "bun",
  "packageManager": "bun",
  "ticketBoundaryMode": "cook",
  "reviewPolicy": {
    "subagentReview": "skip_doc_only",
    "prReview": "skip_doc_only"
  },
  "prReviewAgents": [
    { "login": "coderabbitai", "name": "coderabbit" },
    { "login": "qodo-merge", "name": "qodo" }
  ],
  "subagentRunner": "codex-cli",
  "primaryAgent": "claude"
}

Subagent selection precedence is --subagent flag > subagentRunner config field > hard error. Valid subagentRunner / --subagent values: claude-cli, codex-cli, cursor-cli. SoA ships no built-in silent default: a fresh repo with neither flag nor config field set is required to make the choice explicit before any subagent review runs. primaryAgent is free-form (e.g. claude, codex, cursor, composer, copilot, aider) and is recorded on every ledger row as primaryAgent; absent flag/config defaults to "unknown". Cross-family review (e.g. claude primary + codex-cli or cursor-cli subagent) is the documented best practice but not enforced — the operator chooses, the ledger records both fields so cross-family achievement is computable post-hoc.

All fields are optional. When the file is absent, the orchestrator infers sensible defaults:

Valid reviewPolicy stage values are:

Invalid values and unknown keys are rejected at config load with a clear error.

reviewPolicy.subagentReview governs the pre-PR internal agent review step (subagent-review command). reviewPolicy.prReview governs the external AI PR review polling window. prReviewAgents is a list of { login, name } entries used by the fetcher script to identify external review bots by GitHub login. Runner selection for subagent-review is done at invocation time via --subagent <claude-cli|codex-cli|cursor-cli> — not in config.

Branch roles are separate. defaultBranch is the repo-primary branch used for source links and repo-level references. deliveryBaseBranch is the first-ticket delivery base and primary worktree branch. closeoutBranch is the branch that closeout-stack verifies, resets, pushes, and comments against. Keep all three explicit in checked-in config:

{
  "defaultBranch": "main",
  "deliveryBaseBranch": "main",
  "closeoutBranch": "main"
}
{
  "defaultBranch": "main",
  "deliveryBaseBranch": "main",
  "closeoutBranch": "staging"
}
{
  "defaultBranch": "main",
  "deliveryBaseBranch": "release-next",
  "closeoutBranch": "release-next"
}

Any promotion between configured branches is manual and outside the SoA closeout command. /soa update runs the sync migration that derives missing deliveryBaseBranch and closeoutBranch values from the previous defaultBranch value, preserving existing consumer targets.

Supported ticketBoundaryMode values are:

The internal convention below planRoot is fixed: {planRoot}/product/delivery/<phase>/implementation-plan.md. Only the top-level directory name is configurable.

The supported operator entrypoint is bun run deliver --plan .... The orchestrator core is intentionally generic but does not attempt to be a fully validated multi-runtime CLI package.

Plan-Driven, Not Phase-Hardcoded

The engine is generic. It does not fundamentally belong to Phase 02.

What is phase-specific is:

So the orchestrator takes a plan path:

That is the canonical interface. The tool is primarily AI-facing, so the explicit plan artifact is more important than a phase nickname.

What It Owns

The orchestrator owns process mechanics:

The orchestrator does not own AI-review detection heuristics or triage judgment.

That boundary is intentional. The repo-local soa-pr-review skill under .agents/skills/pr-review/ already defines the repo stance for AI review:

So the orchestrator only consumes the skill hook contracts:

In consumer repos, the default hook lookup prefers .son-of-anton/.agents/skills/pr-review/scripts/... when the subtree is present, then falls back to .agents/skills/pr-review/scripts/... for the source repo. This avoids collisions when a consumer repo already has its own .agents/skills/pr-review directory.

In this repo, supported external AI-review vendors are currently:

Other vendors are out of scope unless the repo-local soa-pr-review skill is deliberately expanded.

For sonarqube, the repo-local fetcher reads GitHub check-run annotations rather than native PR review threads and intentionally keeps only failed-check annotations in the normalized fetch artifact. Lower-severity warning annotations remain available in SonarQube itself but do not enter the orchestrator triage loop by default.

The absence of pr-review comments after the final 12-minute polling check is not itself a blocker. In that case, the orchestrator records the review as clean, updates the PR metadata, and continues unless another real ambiguity or prerequisite issue exists.

Doc-only PRs (where the diff touches only .md files) skip the review window only when reviewPolicy.prReview is "skip_doc_only" (or the stage is fully "disabled" for all PRs). External AI agents review code; the developer reads docs. When open-pr detects a doc-only diff, it sets a doc_only flag in state, and poll-review uses the configured policy to decide whether to auto-record skipped immediately or wait through the normal review window.

When the triager hook resolves to clean or patched, poll-review records that result immediately. When it resolves to needs_patch, the ticket moves into an intermediate needs_patch state with the saved fetch/triage artifacts and triage note. From there the follow-up must conclude as either patched or operator_input_needed. PR body updates remain best-effort in either case.

Review artifact persistence now follows a hard split:

At this point in the repo, poll-review, record-review, triage-ticket, and triage-standalone are intentionally thin mode-specific shells around the same post-PR lifecycle helpers. Ticket-linked flow still owns stacked state transitions and standalone flow still owns PR discovery plus author-body preservation, but the semantic review handling between those edges is shared.

Ticket PR triage (done tickets)

poll-review only targets tickets in in_review. After a ticket is done, use triage-ticket <ticket-id> when external AI review comments arrived late and you want to re-fetch, re-run the repo triager, persist updated artifacts under the plan reviews directory, refresh delivery state (while keeping the ticket done), and refresh the PR body (best-effort). The old reconcile-late-review command remains a backwards-compatible alias.

Run it from a worktree where .agents/delivery/<plan-key>/state.json for that plan is authoritative (this repo does not discover state across worktrees for you). The ticket must still have a stored prNumber. The command uses a short single-interval poll so the first check runs immediately; re-run if vendors are still in flight.

Post-phase advisory-observation triage

After the full stacked phase is closed out onto the configured closeoutBranch, run advisory-observation triage before starting the next phase. The /soa wrapper is the user-facing entrypoint:

/soa triage-advisory-observations phase-16

The underlying orchestrator command is:

bun run deliver --plan docs/product/delivery/phase-16/implementation-plan.md \
  triage-advisory-observations --dispositions <path>

This is a primary-agent patching lane, not an advisory-only lane. During triage, the primary agent reads each parsed advisory observation, decides whether it is prudent to fix, and applies patches directly to the configured closeoutBranch where prudent. The triage-advisory-observations command itself is a state recorder — it scans completed subagent-review report sidecars, parses the Advisory Observations section (excluding Actionable findings), aligns the parsed observations with the operator’s explicit dispositions, and writes the triage artifact at docs/product/delivery/<phase>/advisory-observation-triage.json. The primary agent is the one that applies any code/doc patches before recording dispositions. The command does not infer dispositions and never patches automatically — but the operator running the command is expected to patch where prudent and record disposition: patched with the resulting patch.commitSha.

The “must not apply patches” rule that applies to the subagent review runner (advisory-only contract; file writes in the worktree trigger outcome: skipped) does NOT apply to this post-phase triage lane. The primary agent owns patches here just like it owns patches during a normal ticket implementation.

Disposition vocabulary (recorded in advisory-observation-triage.json under each entry’s disposition field):

DispositionMeaning
patchedPrimary agent applied a prudent fix. Requires patch.commitSha; patch.files is optional.
rejectedObservation is not actionable (markdown artifact, false positive, scope drift, or filed as a follow-up ticket via followUpReference).
already-coveredObservation is already addressed by behavior in a sibling ticket or earlier phase. Rationale should name where.
requires-human-reviewThe only escape hatch. Genuinely ambiguous; developer must decide. Not a synonym for skip or hold.

Legacy deferred and converted-to-ticket from schema v1 are auto-migrated on read (deferredrequires-human-review, converted-to-ticketrejected with followUpReference preserved). New triage artifacts emit schema v2 with a summary block and the entries under dispositions.

The dispositions input file is gitignored under .agents/delivery/<plan-key>/advisory-observation-dispositions.json. See docs/template/delivery/advisory-observation-dispositions-template.json for the canonical shape — the primary agent fills it in (one entry per parsed observation), runs the command, and the command emits the triage artifact that lands in docs/product/delivery/<phase>/.

Closeout/status summaries may warn when advisory observations are untriaged or when clean/completed subagent-review evidence is suspiciously missing or empty. Those warnings preserve the boundary: Actionable findings still govern pre-PR reconciliation blockers, while Advisory Observations require later operator disposition.

Ticket Context Reset

The orchestrator also owns the repo-side context reset contract for stacked ticket work.

When a ticket starts, the orchestrator writes a handoff artifact under:

That handoff is the narrow context that the next ticket worker should begin from alongside the current repo state and required docs.

start must also leave the started ticket worktree locally self-sufficient for active-ticket continuation. In practice that means the target worktree receives:

This is intentionally bounded context, not whole-phase mirroring.

For the first ticket in a new phase, there may be no prior-ticket handoff or review artifacts beyond the implementation plan, the ticket doc, and current repo state. That is expected, not a blocker.

The handoff includes:

This does not automatically create a brand-new agent session, but it is the current repo mechanism for reducing reasoning carryover between tickets while preserving stacked branch continuity.

For ticket 01, start is the command that initializes the first ticket context for the phase. After start, read the locally materialized handoff artifact from the started ticket worktree; before that, do not treat the absence of a prior-ticket handoff as missing workflow state.

No read-ahead during the review window. The agent does nothing while waiting on external AI review. The wait is free (LLM idle during subprocess sleep). Read-ahead during the window burns context that is dead weight at the next ticket boundary. Be sabaai sabaai.

Runtime Policy Overrides

Pass explicit flags to override delivery policy for a single run without editing orchestrator.config.json. The resolved policy persists in state.json as runPolicy and governs the entire run.

Available flags:

FlagValuesEffect
--boundary-modecook|gatedOverride ticket-boundary mode
--subagent-review-policyrequired|skip_doc_only|disabledOverride subagent review gate
--pr-review-policyrequired|skip_doc_only|disabledOverride PR review gate
--subagentclaude-cli|codex-cli|cursor-cliDeclare execution agent identity; tries preferred first, then other programmatic runners, then honest skip
--baselineorchestrator|run-policyResolve divergence on resume (see below)

Divergence recovery: If orchestrator.config.json changes between runs, resume detects drift on the four bounded policy fields and refuses to continue until the operator resolves it:

# Adopt current repo config as the new active policy:
bun run deliver --plan <plan> --baseline orchestrator <command>

# Re-apply the persisted runPolicy — it governs execution for this invocation (not just state):
bun run deliver --plan <plan> --baseline run-policy <command>

Both baselines accept additional override flags to fine-tune the resolved policy before persisting it.

Status output: The active persisted runPolicy appears as run_policy=... [persisted] in status output, below the config-baseline boundary_mode and review_policy lines. Divergence between those lines is resolved with --baseline.

Ticket Boundary Modes

EE7 makes the ticket-boundary policy explicit.

For gated, the canonical resume prompt is:

Immediately execute `bun run deliver --plan <plan> start`, read the generated handoff artifact as the source of truth for context, and implement <next-ticket-id>.

Operator reset guidance in gated:

Handoff artifact modified_sections. The handoff now includes a ## Modified Sections block extracted from the ticket’s ## Scope section. Read only the file sections listed there — do not re-read full files. This keeps per-ticket context bounded as implementation files grow across the phase.

That policy applies only to ticket-linked delivery PRs. Standalone manual triage-standalone runs for non-ticket PRs do not have a next-ticket boundary, so there is no analogous look-ahead rule there.

Syncing Existing Work

If a phase was already partially delivered before the orchestrator was introduced, sync can infer progress from the repo when the local state file is absent:

That inference is intentionally conservative. It reconstructs enough state to resume a stacked phase without requiring a fresh restart.

Post-verify (ticket stacks)

After build mode (implementation and automated verification), the agent switches to post-verify review mode: a deliberate pass over the diff and ticket acceptance before publishing the branch for external AI code review. Stay in the same implementation session — this is a mode switch, not a handoff. For code tickets, build mode begins only after post-red records the failing-test commit.

Use the verification commands with two distinct purposes:

The post-verify command records that the post-verify review pass completed (ticket status, outcome, and timestamp in local delivery state). It does not run checks or read the diff; the agent performs verification in build mode and the diff review in post-verify mode, then invokes this command.

The command accepts an optional outcome argument. When the outcome is patched, record one or more patch-commit SHAs so the PR body can link the exact post-verify follow-up commits:

bun run deliver --plan <plan> post-verify          # defaults to "clean"
bun run deliver --plan <plan> post-verify clean    # no changes during post-verify review
bun run deliver --plan <plan> post-verify patched <sha...>  # post-verify review found and fixed issues

When omitted, outcome defaults to clean. The status command renders the outcome alongside the completion timestamp. A recorded post-verify patch commit must use a subject suffix of [post-verify].

Before post-verify, confirm at least:

Then run post-verify, then (if subagentReview is enabled) write-subagent-adversarial-review, then subagent-review, then reconcile-subagent-review, then open-pr.

Subagent adversarial review (ticket stacks)

When reviewPolicy.subagentReview is "required" or "skip_doc_only", code tickets must complete the two-step pre-PR subagent gate before open-pr: prompt authoring, then advisory runner review.

Role split:

Step 1 — Author the prompt (write-subagent-adversarial-review):

  1. Read docs/template/delivery/adversarial-review-template.md. Fill in invariants, attack surfaces (including the seven diff-derived classes), and diff context from the current ticket diff and spec. This is primary-agent work — the subagent does not author its own brief.
  2. Record the filled prompt:
bun run deliver --plan <plan> write-subagent-adversarial-review
# optional: --prompt-file <path> when the filled template already exists on disk

The command persists the prompt under reviews/<ticket>-subagent-review.prompt.md, commits it from the ticket worktree, and stores subagentAdversarialPromptPath on the ticket in delivery state.

Step 2 — Run advisory review (subagent-review):

Runner selection: The execution agent declares its own identity via --subagent <claude-cli|codex-cli|cursor-cli>. The CLI tries the preferred runner first, falls back to the other programmatic runners in order, and records an honest skipped artifact if none are available. No config change is needed when switching agent platforms (Claude Code, Codex, Cursor Agent CLI, etc.). The verified headless form for cursor-cli is agent --print --trust --output-format text --workspace <worktree> <prompt>.

The runner receives the exact bytes from reviews/<ticket>-subagent-review.prompt.md, invokes verified headless forms (claude -p / codex exec / agent --print --trust --output-format text --workspace <worktree>), persists runner prose to reviews/<ticket>-subagent-review.report.md, and writes a SubagentRunnerArtifact to reviews/<ticket>-subagent-review.ledger.json whose filledPrompt and rawOutput fields are repo-relative paths to those sidecars (not embedded text). The orchestrator stages, commits, and pushes the JSON plus sidecar files from the ticket worktree. open-pr fails closed when subagentReview is not "disabled" and a non-skipped outcome is recorded but the artifact file is missing.

Advisory-only contract: The runner must not write files. If the worktree has new modifications after the runner exits, the CLI records outcome: skipped with terminatedReason: advisory_violation — not a completed clean review. Non-zero exit codes, empty output, and rate-limit signatures are also recorded as non-completed termination; the CLI refuses outcome: clean unless terminatedReason is completed.

  1. Stay idle. No read-ahead. Wait for the runner subprocess to exit before doing anything else.
  2. The primary agent reads findings, applies any prudent patches, then records:
bun run deliver --plan <plan> subagent-review clean    # no primary-agent patches from subagent findings
bun run deliver --plan <plan> subagent-review patched <sha...>  # primary agent applied subagent-driven fixes
bun run deliver --plan <plan> subagent-review record-deferred --reason "<rationale>"  # conscious deferral

Without --subagent, the CLI is a state recorder only and does not invoke a runner. With --subagent, the CLI invokes the runner against the persisted prompt, enforces the advisory-only contract, writes the runner artifact, and records the detected outcome. Primary-agent patch commits that respond to subagent findings must use a subject suffix of [subagent-review].

Step 3 — Reconcile ledger vs git (reconcile-subagent-review):

After the primary agent finishes any subagent-driven patches (or records deferral), run reconciliation before publishing:

bun run deliver --plan <plan> reconcile-subagent-review

The command compares commits since the row’s reviewedHeadSha against the reviewed file set and the report’s actionable-findings section. It hard-blocks when the ledger would silently lie:

Resolution paths are named in the error text: amend the patch subject, subagent-review record-deferred --reason "...", or open-pr --ack-reconciliation <patched|deferred|clean> [--commit <sha>] [--reason "<text>"]. open-pr runs the same gate internally; use the explicit step for diagnostics.

Doc-only tickets auto-skip subagent review only when reviewPolicy.subagentReview is "skip_doc_only".

When reviewPolicy.subagentReview is "disabled", open-pr does not require subagent_review_complete status and tickets at verified may proceed directly to open-pr.

If the subagent is unavailable, set subagentReview: "disabled" in orchestrator.config.json to bypass the gate.

Commands

Use the supported repo command:

bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md status

Worktree guard

Most commands require execution from the active ticket’s worktree — the directory set as worktreePath in the ticket state. Running a guarded command from any other directory fails immediately with the exact recovery command:

Error: Command '<cmd>' for ticket <id> must be run from its worktree.
Current directory: <cwd>
Expected worktree: <worktreePath>
Recovery: cd <worktreePath> && bun run deliver --plan <plan> <cmd>

Commands exempt from the worktree guard (safe to run from any directory):

All other commands are guarded. The guard fires after loadState so the expected worktree path is always derived from recorded state, not from the invoking directory.

Stable workflow contract boundary

Some delivery-tool workflow and state-guard failures carry a stable machine-readable identity in addition to the human-readable operator message. This boundary is intentionally narrow:

These codes are a contributor contract for tests and automation. The human message is still the operator-facing guidance and may be clarified or rewritten without changing the contract identity.

Current examples:

This is not a repo-wide error framework. Low-level config, runtime, platform, and general process failures remain plain errors unless a later phase expands the boundary deliberately.

Optional-DI extension rule

When adding a new optional dependency hook to a delivery helper, optional means the existing behavior must remain unchanged when the hook is omitted. New behavior runs only when the hook is explicitly supplied.

Treat this as an extension rule, not a suggestion:

Testing stance for stable workflow contracts

For contract-bearing workflow/state guards, assert the stable code first and only then check narrow message content that is intentionally part of the operator guidance.

Good test stance:

Bad test stance:

Outside this narrow workflow-contract boundary, tests may still assert prose directly when no machine-readable contract exists.

Available commands:

Separate post-delivery closeout command:

For a fresh phase start, start initializes ticket 01 context. Do not expect prior PR/review handoff state for the first ticket.

status output format

status always prints one next command derived from the active ticket’s current status:

Active ticket: <id> — <title>
Status: <state>
Next command: bun run deliver --plan <path> <next-command>

When all tickets are done, it prints the phase-complete signal instead:

Phase complete. Awaiting developer review.

No next command is printed in that case. A cook-mode agent self-terminates; the developer controls closeout.

post-verify — doc-only early failure

When post-verify is run on a doc-only ticket (a ticket whose branch diff touches only .md or .json files) and there are no commits on the branch ahead of origin, the command fails immediately with:

Error: No commits on branch for doc-only ticket <id>. Add or update documentation files before continuing.

This prevents the ticket from silently advancing to open-pr with an empty diff. The check runs before any state update.

advance — phase-complete signal

When advance marks the final ticket done and no pending tickets remain, it prints:

Phase complete. Awaiting developer review.

No next command follows. In cook mode the agent self-terminates at this point; gated mode was already stopped. The developer runs closeout-stack to merge.

Typical Flow

Default cook flow (with repo-default skip_doc_only review policy):

bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md start
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md post-red
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md post-verify [clean|patched] [patch-commit-sha ...]
# for code tickets when subagentReview is enabled:
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md write-subagent-adversarial-review
# pass --subagent <claude-cli|codex-cli|cursor-cli> to run the advisory subagent programmatically, then record:
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md subagent-review [clean|patched] [patch-commit-sha ...]
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md reconcile-subagent-review
# for doc-only tickets under skip_doc_only, subagent-review auto-records skipped (no prompt step)
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md open-pr
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md poll-review
# poll-review auto-records clean/skipped when prReview is disabled or no findings detected — skip record-review in those cases
# only run record-review when poll-review leaves the ticket in needs_patch state
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md record-review PN.NN patched "patched the two actionable correctness issues"
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md advance

With subagentReview: "required" in orchestrator.config.json, the subagent step is mandatory before open-pr for code tickets:

bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md start
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md post-red
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md post-verify [clean|patched] [patch-commit-sha ...]
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md write-subagent-adversarial-review
# execution agent passes --subagent <its-identity>; runner returns findings prose only; primary agent patches if prudent, then record:
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md subagent-review [clean|patched] [patch-commit-sha ...]
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md reconcile-subagent-review
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md open-pr
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md poll-review
bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md advance

gated flow:

bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md advance
# reset context now; prefer /clear and use /compact only when compressed carry-forward is intentional
# next agent session prompt:
# Immediately execute `bun run deliver --plan docs/product/delivery/phase-NN/implementation-plan.md start`, read the generated handoff artifact as the source of truth for context, and implement PN.NN+1.

After the developer has reviewed the full stacked PR chain and is ready to merge it, use:

bun run closeout-stack --plan docs/product/delivery/phase-NN/implementation-plan.md

closeout-stack is intentionally separate from deliver. It handles stacked PR merge choreography rather than ticket implementation state: for each reviewed slice in ticket order, it runs git merge --squash locally (a 3-way merge, robust against parent-branch patches), commits with the PR title, pushes to closeoutBranch, closes the PR, and deletes the remote branch. This produces one squash commit per ticket on the configured closeout target without rebasing child branches. When squash hits conflicts (often after prior tickets landed as new squash SHAs), it resets to origin/<closeoutBranch> and replays the PR using gh pr view’s commit list and sequential git cherry-pick instead (merge commits use -m 1), which may create more than one commit for that ticket.

For a non-ticket PR, run the manual standalone path:

bun run deliver triage-standalone
# or: bun run deliver triage-standalone --pr 32

For standalone PRs, the internal review contract is behavior-first, not state-recorded:

In standalone mode, post-verify and subagent-review are expected preflight discipline, not orchestrator gates. The orchestrator can tell the agent to do them, but without standalone state it cannot verify, audit, or block on them. Only standalone triage-standalone is an orchestrator-visible gate today.

The ticket-only commands post-verify, subagent-review, open-pr, poll-review, record-review, and advance do not apply to standalone PRs because there is no ticket state to update. That architectural constraint does not remove the underlying review discipline; it does mean the post-verify review pass and optional subagent pass remain guided discretion in standalone mode rather than durable workflow state.

If standalone delivery ever needs true post-verify or subagent-review gate semantics, add a lightweight standalone state artifact first. Do not present soft preflight discipline as a hard gate without durable evidence.

If a parent ticket was squash-merged onto the configured closeout branch, run:

bun run deliver restack

from the current child ticket worktree before continuing review. restack infers the delivery plan and current ticket from the checked-out branch, fetches origin, rebases away the old parent ancestry, and updates the open PR base/body so GitHub review follows the new stack shape. If branch inference is ambiguous, pass --plan explicitly.

If local state drifts from repo reality, use repair-state to snapshot the stale state file, rebuild clean state from current repo facts, and print the repaired fields before resuming delivery.

Optional Telegram Notifications

The orchestrator can emit best-effort Telegram notifications for milestone events such as:

Notifications are optional and advisory. They must never block orchestrator progress if delivery to Telegram fails.

Enable them by setting both env vars in your repo’s .env file (or your shell environment):

# .env — Telegram notifications for Son of Anton delivery milestones
TELEGRAM_BOT_TOKEN=your-bot-token-here
TELEGRAM_CHAT_ID=your-chat-id-here

The orchestrator reads these via process.env at startup. If either is absent or empty, the notifier returns { kind: 'noop', enabled: false } and all notification calls are skipped — no errors, no warnings, no blocked progress.

To get a bot token: create a bot via @BotFather on Telegram. To get your chat ID: send a message to your bot and call https://api.telegram.org/bot<TOKEN>/getUpdates — the chat.id field in the response is your TELEGRAM_CHAT_ID.

When those env vars are absent, the notifier stays disabled and the orchestrator behaves normally.

Review Artifact Location

Fetched review output is written under:

Generated handoff artifacts are written under:

State is written under:

State file and primary checkout (multi-worktree)

For active ticket continuation, start writes the authoritative bounded continuation set into the started ticket worktree. That means the started worktree is the local source of truth for continuing that ticket.

The orchestrator also writes state.json in the repo directory where you run deliver (the current working directory). If you use one ticket worktree per ticket and a separate primary checkout for closeout-stack or other commands, that checkout’s delivery tree does not update automatically.

Fetched review artifacts under reviews/ and generated handoff artifacts under handoffs/ are still written under the same path relative to the worktree where each command ran. Across a stacked phase, full history is therefore often spread across multiple ticket worktrees — the final worktree is not guaranteed to contain every reviews/<ticket>-*.json or handoffs/<ticket>-handoff.md produced earlier.

Recommendation:

Example (adjust paths and plan key; final-wt is the worktree that completed the last ticket):

mkdir -p /path/to/primary-clone/docs/product/delivery/<plan-key>/reviews \
         /path/to/primary-clone/.agents/delivery/<plan-key>/handoffs

cp /path/to/final-wt/.agents/delivery/<plan-key>/state.json \
   /path/to/primary-clone/.agents/delivery/<plan-key>/state.json

for wt in /path/to/phase-wt-01 /path/to/phase-wt-02 /path/to/phase-wt-NN; do
  cp -R "$wt/docs/product/delivery/<plan-key>/reviews/"* \
        /path/to/primary-clone/docs/product/delivery/<plan-key>/reviews/ 2>/dev/null || true
  cp -R "$wt/.agents/delivery/<plan-key>/handoffs/"* \
        /path/to/primary-clone/.agents/delivery/<plan-key>/handoffs/ 2>/dev/null || true
done

Stance: Treat each started ticket worktree as authoritative for continuing its active ticket; treat the primary checkout as the aggregate mirror for full-phase history and closeout. Active-ticket continuation should not require scavenging across older worktrees, but aggregate reviews/ and handoffs/ still must be reconciled across all worktrees, not only copied from the latest one.

PR Body Maintenance

PR descriptions are maintained as delivery metadata, not one-shot text.

This matters because the repo squash-merges PRs onto closeoutBranch, so the PR body needs to mention prudent ai-cr follow-up work before the stack moves on.