Cross-project projection
Cross-project orchestration is a first-class run shape, not a single-project run with extra metadata. The cross runner owns a multi-project plan, fans out into per-project sub-pipelines, then validates the integration contract across the whole change before writing one terminal status.
run_cross_pipeline(task, projects, profile) -> project_cross_profile(profile) # split into global + project steps -> CROSS-PLAN -> CROSS-VALIDATE-PLAN # global steps -> extract subtasks by project alias -> per alias: write implementation_handoff.{json,md} # JSON canonical run_pipeline(project_steps, plan_source="cross") -> contract_check # per-alias gate, runner-owned -> cross_final_acceptance # system release gate, runner-owned -> run.end (status = done | failed)That shape, as a real run — one feature across an api and a web project:
the cross plan fans out into per-project sub-pipelines, the runner-owned
contract check and cross final acceptance validate the whole change, and both
projects are delivered under one terminal status:
Recorded from a mock pipeline (—mock) — the cross-run
anatomy is exactly what a real run prints. A real cross run takes tens of
minutes per project.
This page is the design view. For the operator view — when to use the mode and how to start it — read Cross-project mode.
A first-class run shape
Section titled “A first-class run shape”The run.start event is a discriminated union on run_kind:
run_kind | Required payload |
|---|---|
single_project | task, project, profile; optional parent_run_id, project_alias |
cross_project | task, projects[], cross_mode, profile, plan_source; optional projected_profile |
This avoids lossy fields such as project=null or “project OR projects”.
A consumer can validate the payload without guessing which run shape it is
reading, and the cross topology stays visible in the event stream instead of
being flattened into single-project events plus annotations.
Profile projection
Section titled “Profile projection”The operator supplies one profile; there is no separate sub-profile flag. The
cross runner loads it and splits it into global_steps and project_steps
using a per-step cross: { scope, handler? } annotation:
| Scope | Effect |
|---|---|
global | Runs once at the cross level. Optional handler selects the cross-level function (cross_plan, cross_validate_plan). |
project | Runs inside each child sub-pipeline. |
both | Appears in both lists. |
skip | Omitted entirely in cross mode. |
A global handler preserves the step’s semantic phase name, so loop predicates
like until: validate_plan.approved still match. Children run an in-memory
Profile built from project_steps — the same profile language drives both
levels.
Projection carries coherence rules. LoopStep inner steps must agree on
scope; mixed loops are an error. Project-scoped implement or
repair_changes requires at least one global plan / validate_plan step to
produce a handoff, so the task profile is rejected for cross mode. Profiles
without cross metadata stay valid for mono runs; the cross runner rejects
them with an actionable error.
Runner-owned terminal gates
Section titled “Runner-owned terminal gates”contract_check and cross_final_acceptance are not profile steps. The
cross runner appends them after all project pipelines finish, and mono runs
never invoke them. Keeping them out of child profiles means no projection or
profile edit can opt a cross run out of its system boundary.
contract_check runs per alias. Each verdict is recorded in
session.phases.contract_check[alias] using the same typed JSON contract as
every other reviewer phase — verdict, short_summary, findings,
rendered (the generated markdown), and raw_response (the model’s JSON for
re-validation). Malformed structured output is downgraded to REJECTED with a
parse_error field: the contract is the gate, not prose heuristics.
cross_final_acceptance is the system release gate — one terminal step that
answers “can the coordinated multi-repo change ship as one system?”, distinct
from per-alias contract matching. It runs in two paths:
- Precondition path (no agent call) — synthesises a
REJECTEDrelease verdict when any upstream signal already blocks ship. - Agent path — preconditions pass, so the cross reviewer is invoked with
the cross plan, per-project release verdicts, and contract-check results,
parsed via
parse_release.
Each precondition violation becomes a named release blocker:
| Blocker id prefix | Cause |
|---|---|
CFA_MISSING_CHILD_<alias> | Child sub-pipeline crashed or never produced a sub-session entry. |
CFA_MISSING_RELEASE_<alias> | Child sub-session has no final_acceptance entry or release fields. |
CFA_CHILD_REJECTED_<alias> | Per-project final_acceptance.ship_ready == false. |
CFA_CONTRACT_REJECTED_<alias> | contract_check[alias].verdict == "REJECTED". |
CFA_PARSE_ERROR_<alias> | Parse error in upstream final_acceptance or contract_check. |
The runner sets session.status in exactly one place, after the gate records
its phase entry. contract_check does not write terminal status in full cross
runs; it produces results the gate consumes through preconditions.
Handoff artifacts
Section titled “Handoff artifacts”After cross planning approves, the runner writes per-child
implementation_handoff.{json,md} under <run_dir>/<alias>/. The JSON is
the source of truth: write_handoff validates the typed Handoff and
returns the JSON path, which is passed to the child run_pipeline via
handoff_path. The .md is a derived audit view rendered from the same typed
object — not the data channel. The child loads and re-validates the JSON, then
renders the prompt body from the typed object.
v1 handoffs carry full_cross_plan_markdown, project_subtask,
cross_validation_summary, and sibling_aliases. The source project_path
is retained in the JSON for audit only and is never rendered into the runtime
body; the authoritative path is the child worktree from its own context block.
A handoff is required exactly when the projected profile contains implement
or repair_changes — review-only profiles run without one.
Child-run linkage
Section titled “Child-run linkage”Each child run’s run.start carries two extra fields:
parent_run_id— the parent cross run’s directory name;project_alias— the alias from the parent’sprojects[]manifest.
Both are optional but paired: the schema validator rejects half-set payloads.
With them, an MCP or evidence consumer can rebuild the full parent → children
→ phases timeline from events.jsonl alone, without reading meta.json or
inferring filesystem layout.
Per-alias verdicts
Section titled “Per-alias verdicts”Each participating repo carries its own decisions: a per-project
final_acceptance release verdict (ship_ready) from its sub-pipeline, and a
per-alias contract_check verdict at the cross level. The system gate
consumes both, so a rejection in one repo surfaces as a named CFA_* blocker
for that alias rather than an undifferentiated cross failure — and the
coordinated change ships only when the gate approves the whole set.
Deep reference
Section titled “Deep reference”The canonical engineering doc lives with the code: