---
name: jj-openspec
description: |
  Background an OpenSpec workflow verb in its own jj workspace via the jj-
  concurrent orchestrator. Maps each verb to a shape (implementing vs
  authoring) with the right reconcile tail; for apply, can fan one change out
  across concurrent workers (one workspace per separable tasks.md group) or
  run a multi-change pipeline over a set; relay drafts a proposal, halts at a
  human go/no-go gate, then applies on go. Triggers: /jj-openspec <verb>
  [change], "apply <change> on a jj workspace", "run the multi-change
  pipeline", "/jj-openspec relay <idea>". Requires a jj repo (ideally
  colocated), the jj-concurrent plugin, an openspec/ directory, and the opsx
  skills available in-session.
metadata:
  version: "0.2.0"
  author: outfitter-style
---

# jj-openspec — OpenSpec verbs on jj workspaces

A thin binding: resolve the OpenSpec-specific parameters, then hand off to the
`jj-delegate` skill (jj-concurrent plugin) with `/opsx:<verb> <args>` as the
workload. This skill owns nothing about jj/workspace choreography (that is
jj-delegate's) and nothing about OpenSpec artifact rules (those are the opsx
skills'). If either changes, this binding should not need to.

## The verb → shape map

OpenSpec verbs share inference (change-name resolution, bookmark naming, issue
tracker) and differ only in **shape** — what the worker produces and how you
reconcile it:

| Verb(s) | Shape | What the worker produces | Reconcile tail |
|---------|-------|--------------------------|----------------|
| `apply` | **Implementing** | code changes + ticked tasks.md | integrate → `/opsx:verify` **(GATE)** → on green: `/opsx:archive` then issue-tracker update → `/jj-pr` push/PR; on non-green: **stop** before trunk-advance/PR and report |
| `propose` / `new` / `ff` | **Authoring** | the change artifacts under `openspec/changes/<name>/` (proposal/design/specs/tasks) | surface artifacts for review; bookmark only; **no verify, no archive, no merge-to-main** (nothing is implemented yet) |
| `relay` | **Composite** (authoring-leg → human gate → implementing-leg) | first the authoring artifacts, then — only on a human *go* — the implementing code + ticked tasks.md | the **authoring** tail (validate + surface) at the gate, then on go the **implementing** tail (integrate → verify gate → archive → push/PR); see §6 |
| `explore` | **Interactive** | (a thinking partner — file output only when asked) | NOT a default background candidate. Only background as "autonomous exploration → a written findings doc" when the user explicitly asks. Otherwise run it inline, not via a worker. |

`relay` is **not a new shape** — it is a *composition* of the two existing
shapes. Each leg dispatches the same shape the corresponding single-shape verb
already uses: the first leg is the authoring shape exactly as `propose`/`new`/`ff`
run it, and the second leg is the implementing shape exactly as `apply` runs it.
The relay owns **only** the ordering of the two legs and the human go/no-go gate
between them; it adds no shape-internal behaviour. Full flow in §6.

The **implementing (`apply`)** shape supports two **distributions**:

- **single-worker** (default and fallback) — one worker for the whole change.
- **per-group fan-out** (optimization) — one concurrent worker per *separable*
  `tasks.md` group, all reconciled into **one** change branch.

Fan-out is opt-in **by data**, default-safe **by policy**: it is attempted only
when separability detection (§A) finds ≥2 qualifying separable groups; otherwise
apply transparently runs the single-worker distribution. Fan-out never changes
the final reconciled result relative to single-worker — it only changes how the
work is distributed. Only the `apply` shape fans out; `propose`/`new`/`ff` and
`explore` are always single-worker / inline.

The `apply` shape also supports a third concurrency **axis** — the
**multi-change pipeline** — when it is supplied a *set* of changes rather than
one. This is the **across-changes axis**: one whole-change apply-shape worker per
change, dispatched as concurrent siblings, each on its OWN change's proposal
revision and bookmark, then integrated as independent landings or a stitched
stack. It is orthogonal to per-group fan-out (the **within-a-change axis**, §A)
— see §B and §P. Like fan-out, the pipeline is opt-in by data and default-safe
by policy: it engages only when the supplied set resolves to ≥2 distinct
apply-ready changes; otherwise apply falls back to the single-change path with
no behaviour change.

## 1. Resolve the verb + change

- **verb** — first token of the arguments (`apply`, `propose`, `new`, `ff`,
  `explore`). If absent, infer from context; if still unclear, ask.
- **change name** — in order: explicit argument → the change under discussion
  → for implementing verbs, `openspec list --json` filtered to changes with
  unticked tasks (use it if exactly one); else ask.
- For **apply**: validate the change exists and is apply-ready
  (`openspec status --change "<name>" --json`). If apply-required artifacts are
  missing, stop and tell the user to author the change first (a `propose` run).
- For **propose/new/ff**: the change may not exist yet — that is the point.

## 2. Derive the bookmark

- **Implementing**: `feat/<short-slug>` from the change name.
- **Authoring**: `change/<short-slug>` (signals it is a proposal, not an
  implementation, so reviewers and the merge step treat it accordingly).

## 3. Base revision — what the worker's workspace must contain

This is where the verbs differ in provisioning (and where jj removes the git
seed-commit ceremony — see jj-delegate §3):

- **apply**: the change's `openspec/changes/<name>/` artifacts must already
  exist on the base revision. They are usually already present and snapshotted
  in `@` (jj has no untracked limbo). Base the worker on the curated revision
  that holds them: pass `jj-delegate` a base-rev of `@` (after confirming `@`
  is clean) or a dedicated change-rev. If the artifacts are NOT yet committed
  anywhere clean, describe the current change first (`jj describe -m
  "docs(openspec): <name> change artifacts"`) so there is a named revision to
  base on — this is the jj-light analog of the old seed commit (a describe, not
  an add+commit dance).
- **propose/new/ff**: base on trunk; the worker CREATES the artifacts in its
  workspace.

## 3.5 Pre-flight health check (gate before hand-off)

After parameters are resolved (§1–§3) and **before** any hand-off to
`jj-delegate` (§4) or fan-out detection (§A), the binding runs a pre-flight
**health check** in the orchestrator's primary workspace. Its job: catch a
malformed *existing* change before a workspace is provisioned, and surface real
errors that the opsx CLI otherwise buries in harmless stderr noise. The check
**provisions nothing, mutates no jj state, moves no bookmark, and pushes
nothing** — it only reads/validates artifacts and runs CLI calls
non-interactively (no `-i`, no editor, `--no-pager` where applicable).

### Step 1 — Decide the shape (gate vs no-op)

Key off whether the resolved change directory **already exists** at
`openspec/changes/<change>/` on the base revision:

- **Existing-change verbs** (`apply`, `verify`, `archive`, `continue`, and
  `ff` when the directory **exists**) → **hard gate**: run the validation below;
  a non-zero filtered result aborts dispatch.
- **New-change verbs** (`propose`, `new`, and `ff` when the directory does
  **not** exist) → **no-op**: there is nothing to validate yet. Record
  "health check skipped (new change)" and proceed straight to §4. Do **not**
  run `openspec validate` against a non-existent change (that would surface a
  spurious "change not found" blocker on the very verbs whose job is to create
  it).

`ff` is the ambiguous case: the directory's existence — not the verb name —
decides gate-vs-skip.

### Step 2 — Validate through the noise-filtering wrapper

For a gated verb, run `openspec validate <change>` **through the wrapper**
(below) and read the *filtered* output and the *preserved* exit code.

**CLI preflight.** Before validating, confirm the `openspec` CLI is on PATH
(e.g. `command -v openspec`). If it is **absent**, do not attempt validation:
emit a distinct **"openspec CLI not found"** blocker (its own failure class,
never reported as a malformed change) and abort dispatch.

**The noise-filtering wrapper.** Wrap every `openspec`/opsx CLI invocation so
that:

- stderr lines matching the explicit allow-list (below) are **dropped** from
  the surfaced output;
- every stderr line **not** on the allow-list passes through **unchanged**
  (fail open — a novel warning is shown, never silently swallowed);
- the process **exit code is preserved verbatim** — filtering never alters it,
  so a real non-zero failure is never masked even when its only stderr lines
  happen to be on the allow-list.

The **allow-list** is an explicit, named set of patterns anchored to the
specific known-harmless opsx schema-config phrases — specific enough that a
genuine error line is never matched:

| Name | Anchored phrase (substring/regex, case-sensitive on the literal) |
|------|------------------------------------------------------------------|
| `tasks-rules-not-array` | `Rules for 'tasks' must be an array` |
| `unknown-artifact-id-in-rules` | `Unknown artifact ID in rules` |

This is the one documented place the allow-list lives; the filter fails **open**
(unknown line shown) rather than **closed**, so allow-list drift produces
visible noise rather than hidden errors. A concrete shell realisation ships
with this skill at [`scripts/opsx_filtered.sh`](scripts/opsx_filtered.sh) —
source it, then `opsx_filtered openspec validate <change>`. (The agent may
instead apply the same allow-list/exit-code discipline inline — the table above
is the source of truth, not the script.)

### Step 3 — Act on the result

- **Filtered exit code zero** → health check **passes**. Proceed to §A / §4 and
  hand the verb invocation to `jj-delegate` **unchanged**.
- **Filtered exit code non-zero** → **abort dispatch** and emit a single
  operator-facing **blocker**, distinct in form from a worker-execution failure
  so the operator can tell "malformed change" from "worker errored". The blocker
  contains:
  1. the **change name**;
  2. the **noise-filtered validation output** (the genuine errors, with the
     known-harmless lines already stripped);
  3. the instruction: **"fix the change artifacts and re-dispatch"**.

  No workspace is provisioned, no bookmark moves, nothing is pushed — the
  binding stops here and reports the blocker.

## A. Separability detection (apply only — picks the distribution)

Before dispatch, the `apply` shape decides **single-worker vs fan-out** by
analysing the change's `tasks.md`. This is the only OpenSpec-aware decomposition
the binding owns; everything physical stays jj-delegate's. The procedure is
cheap, conservative, and biased toward the safe single-worker fallback.

1. **Partition into task groups.** Read the change's `tasks.md` and split it into
   groups by its structure — each top-level `##` heading / numbered section
   (`## 1. …`, `## 2. …`) is one group, with its `- [ ]` task lines as members.
   If `tasks.md` has no group structure (a flat list, no `##`/numbered sections),
   treat the whole file as a single group ⇒ no fan-out.
2. **Derive each group's file area.** From the group's task lines, collect every
   target path the tasks reference: explicit file/dir mentions (e.g.
   `src/api/foo.ts`, `docs/**`, a `path/like/this`) **plus** the file areas of
   the spec deltas that group implements (the `specs/<capability>/…` the tasks
   point at). Record the union as the group's *file area*. If a group's tasks
   name no derivable path at all, record its area as **undeterminable**.
3. **Separability test.** Two groups are **separable** only when BOTH hold:
   - their file areas are **disjoint** (no path/glob intersection), AND
   - there is **no stated cross-group ordering dependency** (neither group's
     prose says it depends on / follows / requires the other's completion).
   An **undeterminable** area is never disjoint from anything ⇒ any group with an
   undeterminable area is **non-separable**. Overlapping areas ⇒ non-separable.
   A stated ordering dependency ⇒ non-separable. Fail safe toward single-worker
   whenever the test is unsure.
4. **Compute the separable-group set + minimum-benefit gate.** Collect the groups
   that are mutually separable (pairwise-disjoint, dependency-free, determinable).
   Apply the gate: fan out only when the separable set has **≥2 groups of
   non-trivial size** (skip near-empty / single-task groups not worth a whole
   workspace). If the gate is **met** → mark the change for **fan-out** with that
   group set. If **not met** → mark the change for **single-worker**.

This detection is policy, not a spec requirement: the gate (group count / size)
is a tunable knob, and any ambiguity resolves to single-worker.

## B. Change-set supply and resolution (apply only — picks the pipeline)

§A is the **within-a-change** axis. This section is the **across-changes** axis:
when `apply` is handed a *set* of changes, the binding resolves that set to a
concrete confirmed list and decides **single-change vs pipeline distribution**.
This is the only across-changes decomposition the binding owns; everything
physical stays jj-delegate's.

1. **Accept the target as a list or a selector.** The apply target may be
   supplied two ways:
   - an **explicit list** of change names (`add-export add-import tidy-logs`), or
   - a **selector** — a glob/query over `openspec/changes/` plus OpenSpec status
     (e.g. all changes under a prefix, or `openspec list --json` filtered to
     apply-ready). A bare single change name is the degenerate one-element list.
2. **Resolve a selector to a concrete list.** Expand the selector against
   `openspec/changes/` and `openspec list --json`, then **de-duplicate** to a
   list of existing change names. Never dispatch lazily from an unresolved
   selector — resolve first so the fan-out width is visible.
3. **Filter to apply-ready members.** For each resolved name, run
   `openspec status --change "<name>" --json` and keep only changes that exist
   and whose `applyRequires` artifacts are complete. **Exclude** any
   non-existent or unready member, and record an **explicit note** naming what
   was dropped and why (missing change / unready artifacts). An excluded member
   is never dispatched.
4. **Confirm the concrete set before dispatch.** Present the final resolved,
   de-duplicated, apply-ready set and its **fan-out width** (how many concurrent
   workspaces will be provisioned) for confirmation before any worker spawns.
5. **Distribution gate.** If the confirmed set has **≥2 distinct apply-ready
   changes** → mark for the **pipeline** (§4c, §P). Otherwise (one change, an
   empty set after exclusions, or only duplicates/overlaps reducing to one) →
   **single-change fallback** (§4a, the existing path), no pipeline overhead.

The pipeline (across-changes) and per-group fan-out (within-a-change) are fully
orthogonal: a confirmed pipeline member is itself a normal `apply` invocation,
so it MAY internally fan out per §A with no coupling — neither policy needs to
know about the other.

## 4. Hand off to jj-delegate (distribution-aware)

Invoke the `jj-delegate` skill. The shape from §1 and the distribution from §A
decide whether you hand off **one** workload or **several concurrent siblings**.

This step runs **only after the pre-flight health check (§3.5) has passed or
been skipped** — a gated, malformed change never reaches hand-off; it stops at
the §3.5 blocker with no workspace provisioned. The check adds this gate to the
existing dispatch path; it does **not** change how verbs map to shapes (§1) or
how `jj-delegate` choreographs workspaces.

### 4a. Single-worker distribution (default / fallback — all verbs)

This is the existing hand-off, **unchanged**:

- **workload (skill form)**: `/opsx:<verb> <change-or-args>` — the worker
  invokes the opsx skill (fallback skill names: `opsx:apply` →
  `openspec-apply-change`, `opsx:propose` → `openspec-propose`, etc.). For
  `apply`, the dispatch brief tells the worker to work through `tasks.md` via
  the skill (which owns task selection + checkbox discipline), never hand-edit
  artifacts outside it, and commit the ticked `tasks.md` alongside the
  implementation so progress travels with the workspace.
- **bookmark**: from §2.
- **base-rev**: from §3.

jj-delegate runs the confirm → provision → dispatch (background by default) →
reconcile lifecycle. Every non-fan-out case lands here: one group, overlapping
areas, an ordering dependency, an undeterminable area, an ungrouped `tasks.md`,
or any verb other than `apply`.

### 4b. Per-group fan-out distribution (apply, gate met from §A)

When §A marks the change for fan-out, hand jj-delegate **one group-scoped
workload per separable group**, as **concurrent siblings** all based on the
change's proposal revision (the §3 apply base-rev). For each separable group N:

- **workload (group-scoped)**: `/opsx:apply <change>` with an explicit
  instruction to **implement ONLY group N's tasks and mark ONLY group N's
  checkboxes** in `tasks.md`, leaving all other groups' tasks and checkboxes
  untouched, and to stay within group N's recorded file area. (No `--group`
  flag exists on `/opsx:apply` today; the scoping lives in the instruction
  string — see the binding's design Open Questions for a possible future flag.)
- **bookmark / base-rev**: the same change bookmark (§2) and the same proposal
  base-rev (§3) for every sibling — they are siblings on one branch, not
  separate branches.

Hand all group-scoped workloads to `jj-delegate` as concurrent siblings and let
it own **every** provisioning, dispatch, and integration choreography
(`jj workspace add -r <base-rev>` per group, background dispatch, never-halting
integration). The binding adds no jj/workspace mechanics; it only supplies the
per-group workload strings and the reconcile policy in §5.

### 4c. Multi-change pipeline distribution (apply, gate met from §B)

When §B confirms a set of ≥2 distinct apply-ready changes, hand jj-delegate
**one whole-change apply-shape workload per change**, as **concurrent siblings**.
Unlike per-group fan-out (§4b), each pipeline worker is a *separate change* with
its OWN bookmark and OWN base revision — they are not siblings on one branch.

For each change C in the confirmed set:

- **workload (whole-change)**: `/opsx:apply C` for that single change only — the
  standard single-change apply brief from §4a, unchanged. The worker works
  through C's `tasks.md` via the opsx skill, marks C's checkboxes, and touches
  only C's file area. No worker is scoped across changes.
- **bookmark**: C's own implementing bookmark `feat/<short-slug-of-C>` (§2),
  resolved per change.
- **base-rev**: C's OWN proposal revision (§3 apply base-rev resolved *for C*),
  so C's `openspec/changes/C/` artifacts are present in its workspace. Different
  changes may live on different revisions; basing per-change is the only correct
  seed.

A pipeline member MAY itself meet §A's fan-out gate; if so, that one change
fans out internally into its own per-group siblings on its own `feat/<slug-of-C>`
branch — orthogonal to and nested under the pipeline, with no coupling.

Hand all whole-change workloads to `jj-delegate` as concurrent siblings and let
it own every provisioning, dispatch, and never-halting integration choreography
(`jj workspace add -r <C's base-rev>` per change, background dispatch). The
binding adds no jj/workspace mechanics; it only resolves each change's
parameters and supplies the pipeline reconcile policy in §P.

The single-change hand-off (§4a) is preserved **unchanged** as the fallback path
for any set that reduces below two apply-ready changes (§B step 5).

## 5. Reconcile tail (after jj-delegate integrates the worker's commits)

**Implementing (`apply`)**, in the primary workspace. The verify GATE and the
auto-archive-on-green step run **exactly once over the reconciled change
branch**, identically for both distributions — the only difference is how many
workers feed into it. Verify is a **gate**, not advisory: trunk-advance, push,
and PR are CONDITIONAL on a green verify. All of verify, archive, the
conditional trunk-advance, and push/PR run in the orchestrator (primary
workspace) — never in a worker (workers never verify, archive, or push).

**Reconcile-into-one-change (fan-out only — first):**

1. Integrate each group worker's commits, as they report, into **ONE** change
   branch for the change (stack / merge the sibling changes onto one another) —
   never separate per-group branches. This is the same branch the single-worker
   path would have produced.
2. Disjoint areas reconcile without conflict. On any overlapping content, rely
   on jj's **never-halting** integration: the integration still succeeds (exit
   0) and jj records the overlap as a **first-class conflict** on the change.
   Resolve it deliberately by **editing the conflict markers in the file**
   before proceeding — **never** the interactive `jj resolve`.
3. `tasks.md` is shared across groups, but each worker marked ONLY its own
   group's checkboxes, so the checkbox hunks are **disjoint lines** and reconcile
   cleanly. If a true overlap appears in `tasks.md` (or anywhere), resolve it by
   editing markers as in step 2 before continuing.
4. Only once every group's commits are reconciled onto the one change branch
   (every group's tasks present and ticked) do you run the verify/push tail
   below — once, over the combined result.

(Single-worker distribution skips the reconcile-into-one step: jj-delegate has
already integrated the lone worker onto the change branch.)

**Verify GATE → archive → push/PR tail (both distributions):**

This tail runs ONCE, over the fully reconciled change branch (every group's
commits integrated, every task ticked), regardless of single-worker vs fan-out.

1. **Verify GATE.** Run `/opsx:verify <change>` against the integrated result
   (check out / log the worker's commits; jj makes them visible without a
   checkout dance). Branch on its outcome — and treat only an **unambiguous
   green** as pass (a flaky/inconclusive result fails safe toward NOT pushing):

   - **Non-green (fail or inconclusive) ⇒ STOP the tail here.** Perform **no**
     `/opsx:archive`, **no** trunk-advance, **no** `jj git push`, and open
     **no** PR — no trunk mutation, no ref mutation. Hold the integrated change
     un-pushed in place so a human or a follow-up apply can inspect and fix it.
     Surface the verify failure as orchestrator data: which scenarios / tasks
     verify flagged, on which change/bookmark. Report and stop.
   - **Green ⇒ continue** to step 2.

2. **Auto-archive on green.** Before any push/PR, run `/opsx:archive <change>`
   in the primary workspace. This syncs the change's delta specs into the
   canonical `openspec/specs/` and moves `openspec/changes/<name>/` to
   `openspec/changes/archive/`, closing the OpenSpec lifecycle as part of the
   successful apply. Capture the canonical-spec sync **and** the change-directory
   move on the **same** reconciled change branch that will advance to trunk / the
   PR — so the PR/trunk state reflects the *closed* lifecycle, reviewed and
   merged in one shot. If archive's delta-sync overlaps canonical specs already
   edited on trunk, jj records a **first-class conflict**; resolve it by
   **editing the conflict markers in the file** (never the interactive
   `jj resolve`), consistent with the integration contract in the
   reconcile-into-one step above.

3. If the project links changes to an issue tracker (e.g. a Linear umbrella
   issue per change), update it with the push/PR link and status.

4. Run the push-and-PR step [`/jj-pr <bookmark>`](../../../jj-concurrent/skills/jj-pr/SKILL.md):
   it pushes the bookmark (`jj git push -b <bookmark>`, handling one-time
   `jj bookmark track`) and creates-or-updates the GitHub PR via `gh`, sourcing
   the body from this change's `proposal.md` (now under
   `openspec/changes/archive/<name>/`). Report: change, bookmark, PR, verify
   outcome (green), that the change was archived, and any remaining unticked
   tasks.

**Authoring (`propose`/`new`/`ff`)**, in the primary workspace:

1. Run `openspec validate <change>` on the drafted artifacts.
2. Surface them for review — either push the `change/<slug>` bookmark and open
   a draft PR, or simply report the new `openspec/changes/<name>/` tree for the
   user to inspect. Do NOT merge to main, do NOT verify, and do NOT archive
   (there is no implementation yet — the verify GATE and auto-archive are the
   `apply` shape's only). `explore` is likewise untouched: it runs inline with
   no gate and no archive.
3. Report: change name, the artifacts created, validate outcome, and the
   natural next step (`/jj-openspec apply <name>` once the proposal is agreed).

## 6. Relay flow (`relay` only — author → gate → apply, one command)

`/jj-openspec relay <idea>` is the single entry point for the most common real
workflow: *draft this, let me look, then build it.* It chains the **existing**
authoring and implementing shapes with a **mandatory human go/no-go gate**
between them. The relay owns only the composition (which leg runs when) and the
gate (the pause between legs): it adds **no** new jj/workspace choreography (that
stays jj-delegate's) and **no** new OpenSpec artifact rules (those stay the opsx
skills'). Each leg dispatches its shape *unchanged* — the relay touches neither.

### 6a. First leg — authoring

Dispatch the authoring shape **exactly as `/jj-openspec propose` does** (§4a with
the authoring tail of §5): a worker runs `/opsx:propose` (or `new`/`ff`) on its
own revision, drafts the change artifacts under `openspec/changes/<name>/`,
validates, and surfaces them — **no verify, no merge** (nothing is implemented
yet). The relay does not reimplement authoring; it composes that shape.

The authoring leg's report **must carry the proposal revision** — the
change-id / revision the artifacts were drafted on — so the gate can thread it
to the implementing leg without re-prompting the human. (Read it from the
worker's structured report field, falling back to `jj log` for the drafted
change if the report omits it.)

### 6b. The human go/no-go gate (hard halt — never auto-advance)

After the authoring leg reports, the binding **halts and surfaces the drafted
artifacts** (the §5 authoring surface + its `openspec validate` result) for an
explicit human go/no-go. It **SHALL NOT auto-advance** to the implementing leg:
nothing is built without a sign-off. This is the whole point of the workflow.

- **Go** → proceed to the implementing leg (§6c).
- **No-go** → end the relay. Dispatch **no** implementing leg. Leave the proposal
  revision **intact** for the human to revise or discard — the relay itself
  **deletes nothing**. (A no-go is not a discard.)
- **Never decided** → the relay simply stays parked at the gate, proposal
  revision intact; nothing is built and nothing is lost. No timeout / auto-advance
  exists (a non-goal).

### 6c. Second leg — implementing, seeded from the proposal revision

On **go**, dispatch the implementing shape **exactly as `/jj-openspec apply
<change>` does** (§4 distribution + the §5 verify → push/PR tail): integrate →
`/opsx:verify` → issue-tracker update → `/jj-pr`.

The implementing leg is **based on the approved proposal revision** via the
binding's *existing* apply-worker-base rule (§3 apply: seed-intent — the worker's
workspace starts from the revision that carries the drafted artifacts, with **no
seed commit**). The relay reuses that rule; it introduces no separate seeding
mechanism. The base revision is **resolved from the authoring leg's reported
result** (§6a), not re-supplied by the human at the gate — removing exactly the
re-derive-the-base-by-hand friction this flow exists to eliminate. (If the human
revised the draft after the authoring leg reported, they re-run the relay or a
standalone `apply` against the new revision; the relay does not silently
re-resolve a moving target.)

### 6d. Relay is additive — existing verbs unchanged

`relay` is added **alongside** `propose`/`new`/`ff`/`apply`/`explore`; it changes
none of them. Each existing verb keeps its single mapped shape exactly as before,
with **no gate and no composition** — they have no gate, and `relay` adds none to
them. No existing verb's mapping, reconcile tail, or (absent) gate changes. The
relay is an additional trigger, not a modification of the existing single-shape
triggers.

## P. Pipeline reconcile (across-changes — after §4c dispatch)

The pipeline (§4c) adds a thin reconciliation layer **on top of** the per-change
reconcile tails of §5. It never collapses two changes' results: each change runs
its OWN §5 verify → push/PR tail over its OWN reconciled result. The pipeline
layer only orders the landings/stack and emits a single operator-level summary.

**Per-change reconcile + verify (unchanged, once per change):**

For each pipeline member C, as its worker(s) report, jj-delegate integrates C's
commits onto C's own `feat/<slug-of-C>` branch (and, if C fanned out internally
per §A, the §5 reconcile-into-one-change step runs for C first). Then C's §5
verify → push/PR tail runs **over C's own result only**. Never run one change's
verify over another change's result, and never merge two changes' verify
outcomes.

**Integration ordering / stacking (pipeline-level):**

1. **Independent landings (default).** With **no declared inter-change
   dependency** in the set, each completed change is integrated onto trunk
   **independently**, in whatever order its worker reports — no ordering is
   imposed between members. A failed/blocked member does NOT block the others.
2. **Stitched stack (declared dependencies only).** When inter-change
   dependencies are **declared** within the set (a change depends on another in
   the set — taken as DECLARED, e.g. an explicit pipeline argument; absence
   means independent), compute a **topological order** of those dependencies and
   integrate the changes as a **stitched stack** in that order (the depended-on
   change below its dependent). For a stitched stack, the stacked PRs are opened
   via `/jj-stacked-pr` (parent-based bases) rather than independent `/jj-pr`s.
3. **Cycle aborts.** If the declared dependencies form a **cycle**, abort
   stacking with an **explicit error** — never choose an arbitrary order. Report
   the cycle and stop the stacking decision (independent members can still land).
4. **Overlap is a first-class conflict.** All integration rides jj-delegate's
   **never-halting** contract. If two members unexpectedly touch the same lines
   at integration, the integration still succeeds (exit 0) and jj records the
   overlap as a **first-class conflict**; resolve it deliberately by **editing
   the conflict markers in the file** — **never** the interactive `jj resolve` —
   *before* the affected change's §5 verify tail runs.

**Pipeline summary (emitted once):**

After every member's own verify tail, emit a **single pipeline-level summary**
listing each change's outcome: **landed** (independent, on trunk), **stacked**
(in the stitched stack, with its stack position), **conflicted** (overlap
resolved at integration), or **failed** (worker reported a blocker / verify
failed). A failed or blocked member leaves its workspace **intact for
inspection** (per jj-delegate teardown rules) and is reported as failed in the
summary; it does not block independent members. Report per change: change name,
bookmark, PR (or stack position), verify outcome, and any remaining unticked
tasks.

## Variants

- **Background** is the default (the session stays free; another `/jj-openspec`
  or `/jj-delegate` can run a second independent slice concurrently).
- **Foreground** ("and wait") — passes through to jj-delegate's foreground mode.
- **Relay** (author → gate → apply, one command): `/jj-openspec relay <idea>`
  drafts a proposal, halts at a human go/no-go gate, then on go applies it —
  the apply leg bases on the proposal revision automatically. See §6.

## Worked example: two disjoint groups fanned out into one branch

A change `add-export` has this `tasks.md`:

```
## 1. Export API
- [ ] 1.1 Add `src/api/export.ts` endpoint
- [ ] 1.2 Wire `src/api/router.ts` to the export endpoint

## 2. Export docs
- [ ] 2.1 Document the export endpoint in `docs/export.md`
- [ ] 2.2 Add a usage example to `docs/examples/export.md`
```

`/jj-openspec apply add-export` resolves apply → implementing, derives bookmark
`feat/add-export`, and bases on the proposal revision. §A finds Group 1
(`src/api/**`) and Group 2 (`docs/**`) disjoint, independent, and non-trivial ⇒
**fan-out**: two concurrent `jj-delegate` siblings on the same bookmark/base,
each running `/opsx:apply add-export` scoped to ONLY its own group's tasks and
ticks. At reconcile (§5) the disjoint edits — including the disjoint `tasks.md`
tick hunks — merge cleanly onto the one `feat/add-export` branch, and the
verify → archive → push/PR tail runs **once** over the combined result,
identical to what a single worker would have produced. (Overlapping areas or a
stated ordering dependency would have routed to the single-worker path
instead.)

Three more worked examples — the verify GATE (green vs failing), a set of
independent changes landed concurrently, and a declared-dependency stitched
stack — live in [references/examples.md](references/examples.md).

## Two orthogonal concurrency axes (pipeline vs fan-out)

| Axis | Skill / section | Unit of work | Bookmarks | Reconcile |
|------|-----------------|--------------|-----------|-----------|
| **Across-changes** (pipeline) | this binding §B/§4c/§P | one whole change per worker | one per change | independent landings or stitched stack + pipeline summary |
| **Within-a-change** (fan-out) | `openspec-apply-fan-out` §A/§4b/§5 | one tasks.md group per worker | one shared change bookmark | reconcile-into-one-change branch |

The pipeline operates BETWEEN changes (one worker per whole change, distinct
bookmarks/revisions); fan-out operates INSIDE one change (several workers, one
shared bookmark). They compose without coupling: a pipeline member MAY itself
fan out internally, and neither policy needs to know about the other. (Full
rationale: root `DESIGN.md` §Concurrency axes.)

## Fallback guarantee (identical end result)

Both axes are **always** an optimization, never a correctness requirement:
fan-out falls back to the single-worker distribution (§4a) and the pipeline to
the single-change apply whenever their gates are unmet, and when detection is
unsure it resolves to the fallback. Whichever distribution runs, each change
ends with the **same** completed tasks and the **same** verify → push/PR tail —
the axes change only how work is distributed, never any change's outcome.
(Full rationale: root `DESIGN.md` §Concurrency axes.)
