# Idea-to-feature pipeline — unified entry from a vague idea to a feature, AC, and task batch
# (design §idea-pipeline).
#
# Orchestration is configuration (ADR-022 / §3.2): this is YAML over the existing
# dual-workflow engine — zero new engine code. The pipeline STOPS at handoff — tasks are
# created but NOT executed. Use task-pipeline.yaml or feature-dev.yaml for execution.
# No state in this pipeline inlines another pipeline's state graph (no-nesting principle,
# design §System Principles 4).
#
# Shape: start -> discovery -> idea-eval -> feature-create -> ac-generate -> feature-check
#          -> system-design (conditional: needs_design signal)
#          -> design-approval (taste HITL gate; not auto-clicked by --auto)
#          -> decompose -> batch-create -> handoff-finalize -> handoff
#        (idea-eval rejection -> cancelled)
#        (feature-check failure routes back to ac-generate; batch-create failure to decompose).
#
# Evidence instead of ceremony (task 0769): the feature check is MEASURED exactly once at
# each author/revise boundary — `idea-ac-check` at the end of ac-generate, `idea-design-check`
# at the end of system-design — by a deterministic `command.gate` writing a run-scoped
# PASS/FAIL result file. Transition guards only CONSUME the recorded result (run-scoped path,
# no CLI re-run per guard); a gate re-runs only after a relevant write (revision pass) or on
# the next boundary.
#
# Vars (passed as a JSON object via `--vars`):
#   idea     — the idea text (required), e.g. --vars '{"idea":"add a --dry-run flag to dev-wrap"}'
#   profile  — set --vars '{"profile":"auto"}' to skip objective HITL gates (feature-check, batch-create)
#   design   — "auto" (default, signal-driven) or "skip" (--skip-design). No force path.
#              Unified: skip also means omit per-task Design in batch-create (scaffold only;
#              refine fills later). Default: decompose must author batch item `design`.
#   design_approved — "true" when taste pre-cleared (CLI --approve-taste or alias --design-approved);
#              lets profile=auto route around the design-approval taste gate (default "false")
#   idea_approved — "true" when taste pre-cleared (CLI --approve-taste or alias --idea-approved);
#              lets profile=auto route around the idea-eval taste gate (default "false")
#   CLI --approve-taste sets both design_approved and idea_approved to true.
#   spurBin  — PATH-independent spur invocation (overridden by CLI at run start)
#   agent    — agent for agent.run steps (default: auto → `agent.default` in config)
#
# Reliability (aligned with task-pipeline / ADR-043):
#   - Soft agent doctor at start → failed via transitions (not raw lifecycle abort)
#   - expectFile on feature-create / ac-generate / decompose artifacts (no silent no-ops)
#   - Caped AC and batch-create retry loops with fail-closed escalation
#   - HITL taste gates (idea-eval, design-approval) exhaustive yes/no/cancel
#   - Free-form skill pointers remain only where no pure slash surface exists yet
#     (discovery/sys-architecture/decompose internals — residual until dedicated commands)
#
# Seeded by `spur init`; adapt the agent.run inputs to your project's command set.

"$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
version: "2"
kind: state-machine
name: idea-pipeline
description: "Idea to feature + AC + task batch: discovery, idea-eval, feature-create, ac-generate, feature-check, system-design, decompose, batch-create, handoff"
# Worst legal auto path with advertised caps: start/discovery/idea-eval/feature/ac plus 2 AC
# retries, design, one design rejection, decompose plus 2 batch retries, handoff ≈24 transitions.
# Keep a small cushion so the explicit retry caps fail first, not the engine bound.
iterationBound: 25
initialState: start
terminalStates:
  - handoff
  - cancelled
  - failed
failureStates:
  - failed
  - cancelled
vars:
  idea: ""
  profile: "standard"
  design: "auto"
  design_approved: "false"
  idea_approved: "false"
  featureId: ""
  __hitlAnswer: ""
  spurBin: "spur"
  # R8 (0366): injected by WorkflowAppService.run(); stamps discovery artifact provenance.
  __runId: ""
  agent: "auto"
  planningAgent: ""
  stepTimeoutMs: "1800000"

states:
  - id: start
    description: >
      Pipeline start. The idea text arrives as vars.idea. The pipeline stops at
      handoff — no task execution. Soft agent doctor (status file) so doctor red
      routes to `failed` via transitions instead of a raw lifecycle abort.
    onEnter:
      - kind: note
        options:
          message: "Idea pipeline start for idea: ${vars.idea}"
      - kind: doctor.probe
        options:
          resultFile: ".spur/run/${vars.__runId}-idea-precheck-doctor.status"
          spurBin: "${vars.spurBin}"
          agent: "${vars.agent}"
          role: planner
          resolvedAgentVar: planningAgent
          # B7 R1 (0894): also pin the role once — __executor.planner feeds stage
          # dispatch so no later idea stage re-walks the doctor ladder.
          roles:
            planner: "${vars.agent}"
      - kind: shell
        options:
          command: >-
            mkdir -p .spur/run &&
            printf '%s\n' "$idea" > ".spur/run/$__runId-idea-input.md" &&
            awk 'NF' ".spur/run/$__runId-idea-input.md" | grep -q . ||
            printf 'FAIL\n' > ".spur/run/$__runId-idea-precheck-doctor.status"

  - id: discovery
    description: >
      Dispatch sp:brainstorm to explore the idea, generate approaches with trade-offs,
      and record a design summary. The brainstorm ALWAYS records a design summary
      ("nothing is too simple" pattern). The brainstorm also emits a needs_design
      boolean signal written to .spur/run/${vars.__runId}-idea-needs-design.json — this determines
      whether the system-design state runs. As its terminal artifact for the idea path,
      brainstorm also emits the idea-evaluation report to .spur/run/${vars.__runId}-idea-eval-report.md
      (template: the `sp:spur-dev` skill's `idea-evaluation` reference — named by skill, not by
      repo path, because `spur init` never scaffolds `plugins/sp/` into a seeded project).
      The operator's verbatim idea argument is persisted at .spur/run/${vars.__runId}-idea-input.md
      (written by start; the authoritative ask every model-bearing stage reads).
      expectFile fails a silent no-op discovery (no eval report).
    onEnter:
      - kind: agent.run
        options:
          agent: ${vars.planningAgent}
          input: "The operator's ask of record is .spur/run/${vars.__runId}-idea-input.md — idea persisted verbatim at start; authoritative, read it first (${vars.idea} is a convenience echo). Run sp:brainstorm. The skill owns the approach-generation, design summary, and `needs_design` signal criteria; emit .spur/run/${vars.__runId}-idea-needs-design.json ({\"needs_design\": true|false}) plus the design summary per its `Design Approval Gate` / `The needs_design signal` sections. Also emit the idea-evaluation report to .spur/run/${vars.__runId}-idea-eval-report.md per the `sp:spur-dev` skill's `idea-evaluation` reference (urgency/necessity 0–5, premises, pros/cons, alternatives, enhanced idea, recommendation, plus mandatory `## Requirement inventory`: numbered I<n> items quoting/paraphrasing idea-input lines; `[unclear: ...]` marks ambiguity, `[deferred: <reason>]` marks out-of-scope). End with this exact footer: `---\\nrun_id: ${vars.__runId}\\ngenerated_at: <RFC3339>\\n---` (omit if run_id is empty)."
          # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
          role: planner
          # B7 R6 (0894): planner stages declare fresh — the role default is stated
          # explicitly so the definition is self-documenting.
          session: fresh
          expectFile: .spur/run/${vars.__runId}-idea-eval-report.md
          timeoutMs: ${vars.stepTimeoutMs}

  - id: idea-eval
    description: >
      HITL taste gate. Discovery has produced the idea-evaluation report
      (.spur/run/${vars.__runId}-idea-eval-report.md) with urgency/necessity scores, premises,
      pros/cons, and a recommendation. The operator reviews and approves or
      rejects. This gate is NOT auto-clicked by --auto (taste gate, like
      design-approval). Only routes around when vars.idea_approved=true
      (explicit prior approval).
    pause: true
    onEnter:
      - kind: hitl.confirm
        options:
          prompt: "Review the idea evaluation report (.spur/run/${vars.__runId}-idea-eval-report.md). Approve to create a feature, or reject to cancel? (Reject creates no feature.)"

  - id: feature-create
    description: >
      Create a feature via spur feature create, or select an existing feature id.
      The agent writes the feature id to .spur/run/${vars.__runId}-idea-feature-id.txt and body-only
      intent artifacts .spur/run/${vars.__runId}-idea-goal.md / -idea-scope.md. Goal carries
      concise intent only; Scope carries explicit in/out boundaries — task breakdowns and
      checklists never enter Goal. Shell actions then persist both sections through
      `spur feature update --section Goal|Scope --from-file ...`; a missing/empty artifact
      stops the state. Prefer the enhanced idea from .spur/run/${vars.__runId}-idea-eval-report.md as
      context; do not overwrite vars.idea. The operator's verbatim ask of record is
      .spur/run/${vars.__runId}-idea-input.md (persisted at start; authoritative over any
      paraphrase).
    onEnter:
      - kind: agent.run
        options:
          agent: ${vars.planningAgent}
          input: 'Create a feature for the idea. The operator''s ask of record is .spur/run/${vars.__runId}-idea-input.md — the idea argument persisted verbatim at start; treat it as the authoritative ask. Read .spur/run/${vars.__runId}-idea-eval-report.md if present for the enhanced idea and scores. Use ''spur feature create "<name>" --json'' to create it; with --json the feature id is returned under the .ref.id envelope (n), not a top-level .id — read it from there. Write the feature id to .spur/run/${vars.__runId}-idea-feature-id.txt. If an existing feature is appropriate, use its id instead. Also write two body-only intent artifacts: .spur/run/${vars.__runId}-idea-goal.md with concise Goal intent only (a short statement of what the feature achieves; never task breakdowns, checklists, or how-to steps), and .spur/run/${vars.__runId}-idea-scope.md with explicit in-scope and out-of-scope boundary bullets.'
          # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
          role: planner
          # B7 R6 (0894): declared fresh policy (planner default).
          session: fresh
          expectFile: .spur/run/${vars.__runId}-idea-feature-id.txt
          timeoutMs: ${vars.stepTimeoutMs}
      - kind: file.read.into-var
        options:
          path: .spur/run/${vars.__runId}-idea-feature-id.txt
          var: featureId
      - kind: shell
        options:
          command: >-
            mkdir -p .spur/run &&
            test -s .spur/run/$__runId-idea-goal.md &&
            $spurBin feature update "$featureId" --section Goal --from-file .spur/run/$__runId-idea-goal.md
      - kind: shell
        options:
          command: >-
            mkdir -p .spur/run &&
            test -s .spur/run/$__runId-idea-scope.md &&
            $spurBin feature update "$featureId" --section Scope --from-file .spur/run/$__runId-idea-scope.md

  - id: ac-generate
    description: >
      Generate acceptance criteria per ac-style-guide.md. Write AC scenarios to the
      feature file via `spur feature update --section "Acceptance Criteria"
      --from-file`. The agent authors R-numbered Gherkin scenarios tied to the
      design summary into a captured file; the shell step verifies the file is
      non-empty, writes it through the CLI, and writes a completion sentinel
      (.spur/run/${vars.__runId}-idea-ac-done.txt). AC quality is MEASURED once at this
      boundary by the deterministic `idea-ac-check` command.gate below (task 0769): a failing
      check records FAIL and routes through the capped retry loop via guards that consume the
      recorded result — it never fails the run (the engine's default onError policy is `fail`,
      so an in-action check failure would kill the run before the retry edges are evaluated).
      Requirement coverage is MEASURED alongside it by the soft idea-coverage-check shell
      (0887 R4): the recorded coverage status conjuncts into the profile=auto ac-generate
      guards below so uncovered inventory items route through the same capped retry loop.
    onEnter:
      - kind: shell
        options:
          command: "mkdir -p .spur/run && count=$(cat .spur/run/$__runId-idea-ac-retry-count 2>/dev/null || echo 0); echo $((count + 1)) > .spur/run/$__runId-idea-ac-retry-count; rm -f .spur/run/$__runId-idea-ac-content.md .spur/run/$__runId-idea-ac-done.txt"
      - kind: agent.run
        options:
          agent: ${vars.planningAgent}
          input: "Generate acceptance criteria for feature ${vars.featureId}. The operator's ask of record is .spur/run/${vars.__runId}-idea-input.md — idea persisted verbatim at start; authoritative. Read the feature file and author R-numbered BDD Gherkin scenarios per ac-style-guide.md. Tie every scenario to the `## Requirement inventory` of .spur/run/${vars.__runId}-idea-eval-report.md: under each `Scenario:` heading add a comment `# covers: I1, I3` listing covered ids — every non-`[deferred: ...]` item needs at least one covering scenario. Two `spur task check` rules bind AC text: (1) AC bullets copy scenario titles verbatim — the title is the byte-identical identity key of its bullet; (2) gate-language words (HITL, approval/approved, merged/merge event, content-gate, GATED, capstone standalone) are forbidden in titles, bodies, and enum values (L4.gate-language) — rephrase around them. Output only the Acceptance Criteria section body (gherkin fence included), no commentary, no heading."
          # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
          role: planner
          # B7 R6 (0894): declared fresh policy (planner default).
          session: fresh
          answerFile: .spur/run/${vars.__runId}-idea-ac-content.md
          expectFile: .spur/run/${vars.__runId}-idea-ac-content.md
          timeoutMs: ${vars.stepTimeoutMs}
      # Empty capture is a RETRYABLE outcome, not a run abort. `answerFile` always
      # creates the file (so `expectFile` above cannot catch an empty answer), and the
      # old `test -s … && …` chain exited 1 on empty content — killing the run under
      # the default `fail` policy before the capped retry edges were ever evaluated,
      # contradicting this state's own contract. Empty now falls through with exit 0
      # so the recorded FAIL routes it back here (cap: 3). A failing CLI *write* still
      # exits non-zero and aborts — that one is not retryable.
      - kind: shell
        options:
          command: >-
            if test -s .spur/run/$__runId-idea-ac-content.md; then
              $spurBin feature update "$featureId" --section "Acceptance Criteria" --from-file .spur/run/$__runId-idea-ac-content.md && date -u +%Y-%m-%dT%H:%M:%SZ > .spur/run/$__runId-idea-ac-done.txt;
            else
              exit 0;
            fi
      # 0769: ONE measured check at the AC author/revise boundary. `softFail: true` keeps a
      # FAIL routable (retry loop / failed) instead of aborting; sibling guards consume the
      # recorded result from the run-scoped status file and never re-run the CLI.
      - kind: command.gate
        options:
          id: idea-ac-check
          executable: "${vars.spurBin}"
          args: ["feature", "check", "${vars.featureId}"]
          resultFile: .spur/run/${vars.__runId}-idea-ac-check.status
          softFail: true
          timeoutMs: 120000
      # 0887 R4: requirement-inventory ↔ AC coverage, measured once at the same author/revise
      # boundary. Soft shell (exit 0 always): the checker writes the PASS/FAIL status itself and
      # the guards below consume the recorded result — never re-run the checker (0769 pattern).
      # Repo-checkout path first, then the superskill-staged twin (handoff-finalize resolution
      # shape); neither present fails closed to FAIL so readiness degrades visibly instead of
      # silently skipping coverage.
      - kind: shell
        options:
          command: >-
            mkdir -p .spur/run &&
            S=plugins/sp/scripts/idea-coverage-check.ts &&
            if [ ! -f "$S" ]; then S="$(superskill script path sp idea-coverage-check.ts 2>/dev/null)"; fi &&
            if [ -n "$S" ] && [ -f "$S" ]; then
              bun "$S" --run-id "$__runId" --report ".spur/run/$__runId-idea-eval-report.md" --ac ".spur/run/$__runId-idea-ac-content.md" || printf 'FAIL run=%s checker exited nonzero (bun missing or checker crash)\n' "$__runId" > ".spur/run/$__runId-idea-coverage.reason";
            else
              printf 'FAIL run=%s checker not found — run superskill install sp\n' "$__runId" | tee ".spur/run/$__runId-idea-coverage.reason" >&2;
              printf 'FAIL\n' > ".spur/run/$__runId-idea-coverage.status";
            fi

  - id: feature-check
    description: >
      HITL gate over the recorded `idea-ac-check` result (measured at the ac-generate
      boundary — this gate never re-runs the CLI). Deliberately NOT --strict: before
      decompose/batch-create a feature has scenarios but zero linked tasks, which
      emits L4.orphan-scenarios at severity `warning`; --strict would elevate that
      to a failure, making the success edges unreachable and looping ac-generate
      into `failed`. Malformed AC still blocks via the error-severity L3 BDD
      checks. Strict coverage belongs at the shippable gate, not here. The recorded
      result is objective (schema/AC validation) and auto-routable — under profile=auto
      the transition guards route directly from ac-generate to the appropriate next
      state, so this state is only entered in interactive mode. On failure, the retry
      cap routes back to ac-generate (≤3 retries) or escalates to failed.
      Requirement coverage (.spur/run/${vars.__runId}-idea-coverage.status, 0887 R4) is part of
      the recorded results this gate surfaces: a FAIL there means some inventory items have no
      covering scenario — answer no to route back to ac-generate for revision if needed.
    pause: true
    onEnter:
      - kind: hitl.confirm
        options:
          prompt: "Feature check for ${vars.featureId}. Review the AC and confirm to proceed? Requirement coverage status: $(cat .spur/run/${vars.__runId}-idea-coverage.status 2>/dev/null || echo unknown) — reason: $(cat .spur/run/${vars.__runId}-idea-coverage.reason 2>/dev/null || echo n/a) (.spur/run/${vars.__runId}-idea-coverage.status[.reason]). (Failures route back to ac-generate for revision, capped at 3 retries.)"

  - id: system-design
    description: >
      Dispatch sp:sys-architecture to produce the system design. This state runs
      only when needs_design=true (design=auto signal path). The agent creates ADR
      entries, architecture updates, and design satellites through constitution rules.
      A run-scoped design-review artifact (.spur/run/${vars.__runId}-idea-design-review.md)
      with fixed headings `## Proposed design`, `## Operator feedback`, and `## Reconciliation`
      carries operator rejection feedback back into this state: on the first pass the agent
      fills `## Proposed design`; on retry (operator feedback present) it revises the design,
      records the reconciliation, and updates invalidated Acceptance Criteria through
      `spur feature update` before design can exit. Design quality is then MEASURED once at
      this boundary by the deterministic `idea-design-check` command.gate (task 0769). The
      design-approval state follows (taste gate).
    onEnter:
      - kind: shell
        options:
          command: >-
            mkdir -p .spur/run &&
            REVIEW=".spur/run/$__runId-idea-design-review.md" &&
            test -f "$REVIEW" || printf '## Proposed design\n\n## Operator feedback\n\n## Reconciliation\n' > "$REVIEW"
      - kind: agent.run
        options:
          agent: ${vars.planningAgent}
          input: 'The operator''s ask of record is .spur/run/${vars.__runId}-idea-input.md (idea persisted verbatim at start; authoritative). Run sp:sys-architecture for feature ${vars.featureId}. Read the brainstorm artifact, feature AC, and .spur/run/${vars.__runId}-idea-design-review.md. Produce ADR entries, architecture updates, and design satellites (docs/design/<slug>.md) per the constitution edit rules; never write task or feature corpus files directly. Design-review contract (fixed headings `## Proposed design`, `## Operator feedback`, `## Reconciliation`): first pass — write the proposed summary under `## Proposed design`, leave `## Operator feedback` empty; retry after operator feedback — revise the design/ADR artifacts, document changes under `## Reconciliation`, and when feedback invalidates an Acceptance Criteria scenario write the revised AC section body to a file, persist via `$spurBin feature update "$featureId" --section "Acceptance Criteria" --from-file <file>`.'
          # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
          role: planner
          # B7 R6 (0894): declared fresh policy (planner default).
          session: fresh
          expectFile: .spur/run/${vars.__runId}-idea-design-review.md
          timeoutMs: ${vars.stepTimeoutMs}
      # expectFile proves existence only, and the onEnter skeleton pre-creates the file — so an
      # agent no-op would still pass it (0515 P3-2). Fail closed: the `## Proposed design` section
      # must carry non-whitespace content before design can exit to approval/decompose.
      - kind: shell
        options:
          command: >-
            REVIEW=".spur/run/$__runId-idea-design-review.md" &&
            awk '/^## Proposed design/{f=1; next} /^## Operator feedback/{f=0} f' "$REVIEW" | grep -q '[^[:space:]]'
      # 0769: ONE measured check at the design author/revise boundary — after this pass's
      # design writes (including AC reconciliation). softFail keeps FAIL routable; sibling
      # guards consume the recorded result and never re-run the CLI.
      - kind: command.gate
        options:
          id: idea-design-check
          executable: "${vars.spurBin}"
          args: ["feature", "check", "${vars.featureId}"]
          resultFile: .spur/run/${vars.__runId}-idea-design-check.status
          softFail: true
          timeoutMs: 120000

  - id: design-approval
    description: >
      HITL taste gate. The operator reviews the system design and approves or
      rejects it. This gate is NOT auto-clicked by --auto (Auto-Decision Principle
      #5: taste-decision -> surface to human). Only routes around when
      vars.design_approved=true (explicit prior approval).

      R7 (0433): a headless `no` persisted by DefaultHitlResponder would loop
      design-approval -> system-design -> design-approval, burning unbounded
      agent passes. The onEnter shell increments a reject counter; the `no`
      edge to system-design is capped at 1 revise, after which the run
      terminates as `failed` naming this gate.

      Rejection must carry operator feedback: before answering `no`, the operator
      records the concrete issue(s) in the run-scoped design-review artifact
      (.spur/run/${vars.__runId}-idea-design-review.md) under `## Operator feedback` —
      the revision pass reads it to reconcile invalidated AC before re-approval.
    pause: true
    onEnter:
      - kind: shell
        options:
          command: "mkdir -p .spur/run && count=$(cat .spur/run/$__runId-idea-design-reject-count 2>/dev/null || echo 0); echo $((count + 1)) > .spur/run/$__runId-idea-design-reject-count"
      - kind: hitl.confirm
        options:
          prompt: "Review the system design for feature ${vars.featureId}. Approve to proceed to decomposition? To reject, first record your feedback in .spur/run/${vars.__runId}-idea-design-review.md under the `## Operator feedback` heading, then answer no."

  - id: decompose
    description: >
      Dispatch sp:spec-decomposition with the brainstorm artifact, feature AC, and
      design doc as input. The agent produces a task-batch JSON file at
      .spur/run/${vars.__runId}-idea-task-batch.json, validated against task-batch.schema.json,
      plus the private task-order sidecar .spur/run/${vars.__runId}-idea-task-order.json
      (a JSON array of { name, depends_on_names[] }; `[]` when no ordering exists).
      A post-agent shell action validates the sidecar shape before decomposition
      can exit — missing or ambiguous title-to-WBS resolution fails before handoff.
    onEnter:
      - kind: shell
        options:
          command: "mkdir -p .spur/run && count=$(cat .spur/run/$__runId-idea-decompose-retry-count 2>/dev/null || echo 0); echo $((count + 1)) > .spur/run/$__runId-idea-decompose-retry-count; rm -f .spur/run/$__runId-idea-task-batch.json .spur/run/$__runId-idea-task-order.json .spur/run/$__runId-idea-batch-create.done .spur/run/$__runId-idea-batch-create.failed .spur/run/$__runId-idea-batch-create-result.json .spur/run/$__runId-idea-handoff.md .spur/run/$__runId-idea-ready.json"
      - kind: agent.run
        options:
          agent: ${vars.planningAgent}
          input: "The operator's ask of record is .spur/run/${vars.__runId}-idea-input.md (the idea argument persisted verbatim at start; authoritative). Run sp:spec-decomposition for feature ${vars.featureId} per skill references/decomposition.md § Idea-pipeline emission: sizing first, then the batch JSON at .spur/run/${vars.__runId}-idea-task-batch.json and the private task-order sidecar at .spur/run/${vars.__runId}-idea-task-order.json."
          # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
          role: planner
          # B7 R6 (0894): declared fresh policy (planner default).
          session: fresh
          expectFile: .spur/run/${vars.__runId}-idea-task-batch.json
          timeoutMs: ${vars.stepTimeoutMs}
      # R1 (0518): the task-order sidecar is the ordering contract for handoff-finalize.
      # Validate it fails closed: sidecar must be an array, batch names unique, sidecar names
      # unique, every sidecar name/dependency must refer to exactly one batch name, and — the
      # converse (F2, 0518 verify) — every batch name must appear in the sidecar, so a partial
      # sidecar can never silently skip `task deps` for an unlisted item (`[]` is valid).
      # (warn) stays inline by design: one jq predicate over two run-scoped files; splitting
      # it into a script would hide the exact fail-closed contract this guard pins (0824).
      - kind: shell
        options:
          command: >-
            BATCH=".spur/run/$__runId-idea-task-batch.json" &&
            ORDER=".spur/run/$__runId-idea-task-order.json" &&
            jq -e --slurpfile b "$BATCH" '
              (type == "array") and
              (($b[0] | map(.name) | length) == ($b[0] | map(.name) | unique | length)) and
              ((map(.name) | length) == (map(.name) | unique | length)) and
              ((map(.name) - ($b[0] | map(.name))) | length == 0) and
              ((($b[0] | map(.name)) - map(.name)) | length == 0) and
              (((map(.depends_on_names // []) | flatten | unique) - ($b[0] | map(.name))) | length == 0)
            ' "$ORDER" >/dev/null

  - id: batch-create
    description: >
      HITL gate: runs spur task batch-create with the batch JSON from the
      decompose state. The check is objective (schema validation) and
      auto-routable — under profile=auto the transition guards route directly
      from decompose to the appropriate next state, so this state is only
      entered in interactive mode. On failure, the retry cap routes back to
      decompose (≤3 retries) or escalates to failed.
    pause: true
    onEnter:
      - kind: hitl.confirm
        options:
          prompt: "Batch-create for feature ${vars.featureId}. Review the task batch and confirm to proceed? (Failures route back to decompose for revision, capped at 3 retries.)"

  - id: batch-create-run
    description: >
      Executes task batch creation exactly once per decomposition attempt. The
      side effect lives in onEnter and records a sentinel; transition guards only
      inspect sentinel/retry state. The `--json` result is captured atomically in
      .spur/run/${vars.__runId}-idea-batch-create-result.json (temp file + mv); the done
      sentinel is written only after the JSON parses and `created == (.wbs | length)`,
      so handoff-finalize can zip batch names to the returned WBS list. Any failure
      (CLI error or malformed result) writes the failed sentinel, preserving the
      existing retry behavior.
    onEnter:
      # (e) idempotent batch creation: sentinel + temp/mv atomicity stay inline (0824);
      # the jq verdict check keeps the done sentinel truthful so handoff-finalize can
      # zip batch names to the returned WBS list.
      - kind: shell
        options:
          command: >-
            P=".spur/run/$__runId-idea-batch-create" &&
            test ! -f "$P.done" || exit 0 &&
            rm -f "$P.failed" "$P-result.json" "$P-result.json.tmp" &&
            if $spurBin task batch-create --file .spur/run/$__runId-idea-task-batch.json --skip-ready --json > "$P-result.json.tmp" &&
              jq -e ".created == (.wbs | length)" "$P-result.json.tmp" >/dev/null 2>&1; then
              mv "$P-result.json.tmp" "$P-result.json" &&
              date -u +%Y-%m-%dT%H:%M:%SZ > "$P.done";
            else
              rm -f "$P-result.json.tmp" &&
              date -u +%Y-%m-%dT%H:%M:%SZ > "$P.failed";
            fi

  - id: ready-prepare
    description: >
      Ready-by-default preparation (0788): the planning owner applies the
      ready-refinement checklist to each created task (requirements, design,
      plan, ac, decisions, dependencies, premises) so the deterministic task
      check passes, then writes the run-scoped ready evidence sidecar
      (.spur/run/${vars.__runId}-idea-ready.json) with one row per task: wbs,
      status (ready|failed|skipped), planningDigest, and seven check rows.
      Handoff-finalize verifies presence, digest and checklist evidence before
      recommending auto runall; missing or failing evidence never fails the
      run — it degrades the recommendation to ready-depth refineall.
    onEnter:
      - kind: agent.run
        options:
          agent: ${vars.planningAgent}
          # Declared Layer-1 role (0538 R2), same planner executor as decompose.
          role: planner
          # B7 R6 (0894): declared fresh policy (planner default).
          session: fresh
          timeoutMs: ${vars.stepTimeoutMs}
          # (warn) non-slash pointer: the 0788 checklist is per-checkout (digest via the
          # project's own computePlanningDigest), so the bounded prompt pins the skill
          # reference instead of a command; artifacts are gated by answerFile/expectFile.
          input: >-
            The operator's ask of record is .spur/run/${vars.__runId}-idea-input.md (the idea
            argument persisted verbatim at start; authoritative). Run the ready-prepare stage
            for feature ${vars.featureId} per sp:spur-dev
            references/planning-workflow.md § Step 5.6 (Ready preparation): read
            .spur/run/${vars.__runId}-idea-batch-create-result.json and write
            .spur/run/${vars.__runId}-idea-ready.json.
          answerFile: .spur/run/${vars.__runId}-ready-prepare-answer.txt
          expectFile: .spur/run/${vars.__runId}-ready-prepare-answer.txt
      # Fail-closed shape validation (mirrors the order-sidecar guard). Absence is
      # normalized to an empty sidecar: finalize and the seeded fallback then degrade
      # the recommendation to refineall instead of failing the run. A PRESENT but
      # malformed sidecar fails the run here — it must not masquerade as evidence.
      # (warn) stays inline by design: single jq shape predicate over the run-scoped
      # sidecar; the failed/skipped rows are what degrade the recommendation (0824).
      - kind: shell
        options:
          command: >-
            READY=".spur/run/$__runId-idea-ready.json" &&
            if ! test -f "$READY"; then printf '{"runId":"%s","depth":"ready","tasks":[]}\n' "$__runId" > "$READY"; fi &&
            jq -e 'type == "object" and (.tasks | type == "array") and (all(.tasks[]; (.wbs | type == "string") and (.status == "ready" or .status == "failed" or .status == "skipped") and (.planningDigest | type == "string") and (.checks | type == "array") and (all(.checks[]; (.id | type == "string") and (.pass | type == "boolean") and (.evidence | type == "string")))))' "$READY" >/dev/null

  - id: handoff-finalize
    description: >
      Post-create finalization (0518): zip batch item names to the created WBS
      values from the captured batch-create result (equal-length/unique-name
      checks), apply every non-empty depends_on_names list through
      `spur task deps`, refresh the feature roster, check every created task, and
      write the run-scoped handoff report
      (.spur/run/${vars.__runId}-idea-handoff.md) with exactly one next command.
      A mapping or CLI error fails the run before terminal handoff; an unready
      task is a successful planning outcome recorded as a refineall recommendation.
    onEnter:
      # (d) idea-handoff owns finalization (0824): the bundled finalizeIdeaHandoff
      # capability runs from the monorepo checkout first; seeded projects (no
      # packages/, no plugins/) resolve the registered idea-handoff.mjs twin under
      # bare node; neither present fails closed (exit 1) instead of silently
      # skipping finalization. The zip/deps/refresh/check/report contract itself is
      # pinned by finalizeIdeaHandoff unit tests, not by this file.
      - kind: shell
        options:
          command: >-
            if [ -f packages/app/src/workflow/idea-handoff-cli.ts ]; then
              bun packages/app/src/workflow/idea-handoff-cli.ts;
            elif H="$(superskill script path sp idea-handoff.mjs 2>/dev/null)" && [ -f "$H" ]; then
              node "$H";
            else
              echo "idea handoff failed closed — idea-handoff script not found — run 'superskill install sp'" >&2;
              exit 1;
            fi
  - id: handoff
    description: >
      Terminal — idea pipeline complete. Ordering applied, roster refreshed, and
      the handoff report written at .spur/run/${vars.__runId}-idea-handoff.md:
      feature id, task WBS list, per-task readiness, and exactly one recommended
      next command (ready-depth refineall when any task is unready, else auto
      runall). No task execution — the pipeline stops here.
    onEnter:
      - kind: note
        options:
          message: "Idea pipeline handoff. Feature: ${vars.featureId}. See .spur/run/${vars.__runId}-idea-handoff.md for the created task list and recommended next command."
      - kind: run.artifact
        options:
          path: .spur/run/${vars.__runId}-idea-handoff.md
          artifactKind: idea-handoff

  - id: cancelled
    description: Terminal — pipeline cancelled by operator or error.

  - id: failed
    description: >
      Terminal — pipeline failed after exhausting retry caps on cyclic edges
      (feature-check → ac-generate or batch-create → decompose). The error
      message in the run trace identifies which edge exceeded the cap.

transitions:
  # ── start -> discovery ──
  - from: start
    to: discovery
    description: Agent doctor PASS — begin discovery.
    guard:
      kind: shell
      options:
        command: 'test "$(cat .spur/run/$__runId-idea-precheck-doctor.status 2>/dev/null)" = PASS'
  - from: start
    to: failed
    description: Agent doctor FAIL — stop before discovery.
    guard:
      kind: always

  # ── discovery: auto-skip (profile=auto + idea_approved) OR enter idea-eval taste gate ──
  # Declaration order: auto-skip FIRST (same pattern as system-design → design-approval).
  # Without this edge, discovery always enters idea-eval (pause: true), rendering the prompt
  # and persisting an avoidable pause before the in-state auto-skip guard can fire (0366 R4/R5).
  - from: discovery
    to: feature-create
    description: profile=auto AND idea_approved=true — skip idea-eval taste gate entirely.
    guard:
      kind: shell
      options:
        command: 'test "$profile" = auto && test "$idea_approved" = true'
  - from: discovery
    to: idea-eval
    description: Discovery complete — gate on idea evaluation taste decision.
    guard:
      kind: always

  # ── idea-eval: auto-skip (profile=auto + idea_approved) OR approve/reject HITL ──
  # Declaration order: auto-skip FIRST (same pattern as system-design → design-approval).
  - from: idea-eval
    to: feature-create
    description: profile=auto AND idea_approved=true — skip idea-eval taste gate.
    guard:
      kind: shell
      options:
        command: 'test "$profile" = auto && test "$idea_approved" = true'
  - from: idea-eval
    to: feature-create
    description: Idea evaluation approved — create or select a feature.
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = yes'
  - from: idea-eval
    to: cancelled
    description: Idea evaluation rejected or cancelled — no feature created.
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = no || test "$__hitlAnswer" = cancel'

  # ── feature-create -> ac-generate ──
  - from: feature-create
    to: ac-generate
    description: Feature created — generate acceptance criteria.
    guard:
      kind: always

  # ── ac-generate: auto-skip (profile=auto) OR enter feature-check HITL gate (interactive) ──
  # Declaration order: auto-skip guards tried FIRST (same pattern as task-pipeline review→verify
  # and design-gen→handoff). Under profile=auto, the recorded `idea-ac-check` result and the
  # recorded requirement-coverage status (0887 R4) route directly to the appropriate next
  # state — guards never re-run the CLI or the checker (task 0769). Under interactive, the
  # always fallback enters the feature-check state whose onEnter hitl.confirm pauses for
  # operator confirmation (coverage is surfaced in that prompt; the operator's answer governs).
  #
  # Auto-skip 1: pass + coverage + design route → system-design (design=auto + needs_design != false)
  - from: ac-generate
    to: system-design
    description: "profile=auto, check passed, requirements covered, design route — run system design."
    # (warn) 4 commands: one persistent multi-assignment line captures both statuses; `test -a` folds the
    # profile/ac and design/needs pairs (same operand semantics); guards never re-run the CLI (0769).
    guard:
      kind: shell
      options:
        command: >-
          ac_status="$(cat .spur/run/$__runId-idea-ac-check.status 2>/dev/null)" cov_status="$(cat .spur/run/$__runId-idea-coverage.status 2>/dev/null)";
          test "$profile" = auto -a "$ac_status" = PASS && test "$cov_status" = PASS && test "$design" = auto -a "$(jq -r .needs_design .spur/run/$__runId-idea-needs-design.json 2>/dev/null)" != false
  # Auto-skip 2: pass + coverage + skip-design route → decompose
  - from: ac-generate
    to: decompose
    description: "profile=auto, check passed, requirements covered, skip-design route — go directly to decompose."
    # (warn) 4 test segments: profile + inlined ac/coverage reads + the OR'd design=skip/(design=auto AND
    # needs_design=false) pair folded into one `test` (`-a` binds tighter than `-o`), keeping one captured
    # signal file authoritative for both routes (0769).
    guard:
      kind: shell
      options:
        command: 'test "$profile" = auto && test "$(cat .spur/run/$__runId-idea-ac-check.status 2>/dev/null)" = PASS && test "$(cat .spur/run/$__runId-idea-coverage.status 2>/dev/null)" = PASS && test "$design" = skip -o "$design" = auto -a "$(jq -r .needs_design .spur/run/$__runId-idea-needs-design.json 2>/dev/null)" = false'
  # Auto-skip 3: check or coverage failed, retry < 3 → loop back to ac-generate (self-loop)
  - from: ac-generate
    to: ac-generate
    description: "profile=auto, check or coverage failed, retry cap not reached — re-run ac-generate."
    # (warn) 5 commands: one persistent multi-assignment line captures both statuses and the retry count
    # (with its 0 fallback); the failed-check pair stays verbatim and the profile gate folds into the retry
    # test via `test -a`; the retry loop is the smallest honest formulation of cap<3 routing (0769).
    guard:
      kind: shell
      options:
        command: >-
          ac_status="$(cat .spur/run/$__runId-idea-ac-check.status 2>/dev/null)" cov_status="$(cat .spur/run/$__runId-idea-coverage.status 2>/dev/null)" retry="$(cat .spur/run/$__runId-idea-ac-retry-count 2>/dev/null || echo 0)";
          { test "$ac_status" != PASS || test "$cov_status" != PASS; } && test "$profile" = auto -a "$retry" -lt 3
  # Auto-skip 4: check or coverage failed, retry cap reached → escalate to failed
  - from: ac-generate
    to: failed
    description: "profile=auto, check or coverage failed after 3 retries — escalate to failed."
    # (warn) 5 commands: mirror of the retry guard with cap>=3; keeping escalation and retry as one
    # test-chain pair makes the cap boundary auditable in the diff (0769).
    guard:
      kind: shell
      options:
        command: >-
          ac_status="$(cat .spur/run/$__runId-idea-ac-check.status 2>/dev/null)" cov_status="$(cat .spur/run/$__runId-idea-coverage.status 2>/dev/null)" retry="$(cat .spur/run/$__runId-idea-ac-retry-count 2>/dev/null || echo 0)";
          { test "$ac_status" != PASS || test "$cov_status" != PASS; } && test "$profile" = auto -a "$retry" -ge 3
  # Interactive fallback: enter feature-check HITL gate
  - from: ac-generate
    to: feature-check
    description: Interactive — enter feature-check HITL gate for operator confirmation.
    guard:
      kind: always

  # ── feature-check: pass + design route -> system-design, pass + skip route -> decompose, fail -> ac-generate ──
  # Declaration order matters: system-design route tried first, then skip route, then fail.
  - from: feature-check
    to: system-design
    description: Feature check passed, design route — run system design.
    # (warn) 5 commands (named status capture + 4 conditions): HITL answer + captured status + design route; same shape as the
    # ac-generate auto-skips, reading the same captured signal files (0769).
    guard:
      kind: shell
      options:
        command: >-
          ac_status="$(cat .spur/run/$__runId-idea-ac-check.status 2>/dev/null)";
          test "$__hitlAnswer" = yes && test "$ac_status" = PASS && test "$design" = auto && test "$(jq -r .needs_design .spur/run/$__runId-idea-needs-design.json 2>/dev/null)" != false
  - from: feature-check
    to: decompose
    description: Feature check passed, skip-design route — go directly to decompose.
    # (warn) 5 test segments: same as the feature-check design route plus the OR'd
    # design=auto/needs_design=false pair (0769).
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = yes && test "$(cat .spur/run/$__runId-idea-ac-check.status 2>/dev/null)" = PASS && (test "$design" = skip || (test "$design" = auto && test "$(jq -r .needs_design .spur/run/$__runId-idea-needs-design.json 2>/dev/null)" = false))'
  - from: feature-check
    to: ac-generate
    description: "Feature check failed — revise AC (retry cap: 3)."
    # (warn) 5 commands (named status capture + 4 conditions): HITL reject/failed-status OR-pair + captured retry count; the
    # interactive retry mirror of the ac-generate cap<3 guard (0769).
    guard:
      kind: shell
      options:
        command: >-
          ac_status="$(cat .spur/run/$__runId-idea-ac-check.status 2>/dev/null)";
          (test "$__hitlAnswer" = no || test "$ac_status" != PASS) && test "$(cat .spur/run/$__runId-idea-ac-retry-count 2>/dev/null || echo 0)" -lt 3
  - from: feature-check
    to: failed
    description: Feature check failed after 3 retries — escalate to failed.
    # (warn) 5 commands (named status capture + 4 conditions): mirror of the revise guard with cap>=3 (0769).
    guard:
      kind: shell
      options:
        command: >-
          ac_status="$(cat .spur/run/$__runId-idea-ac-check.status 2>/dev/null)";
          (test "$__hitlAnswer" = no || test "$ac_status" != PASS) && test "$(cat .spur/run/$__runId-idea-ac-retry-count 2>/dev/null || echo 0)" -ge 3
  - from: feature-check
    to: cancelled
    description: Operator cancelled the feature-check gate.
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = cancel'

  # ── system-design: -> design-approval (normal) OR -> decompose (auto + prior approval) ──
  # Declaration order: auto-skip guard tried FIRST (like task-pipeline's review->verify pattern).
  - from: system-design
    to: decompose
    description: profile=auto AND design_approved=true AND recorded design check passes — skip design approval gate.
    guard:
      kind: shell
      options:
        command: 'test "$profile" = auto && test "$design_approved" = true && test "$(cat .spur/run/$__runId-idea-design-check.status 2>/dev/null)" = PASS'
  - from: system-design
    to: design-approval
    description: System design done — gate on design approval.
    guard:
      kind: always

  # ── design-approval -> decompose (after operator approval), AC-failure -> feature-check ──
  - from: design-approval
    to: decompose
    description: Design approved and recorded design check passes — proceed to decomposition.
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = yes && test "$(cat .spur/run/$__runId-idea-design-check.status 2>/dev/null)" = PASS'
  - from: design-approval
    to: feature-check
    description: "Design approved but recorded design check fails — route back through the AC gate (revise AC, retry cap: 3)."
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = yes && test "$(cat .spur/run/$__runId-idea-design-check.status 2>/dev/null)" != PASS'
  - from: design-approval
    to: system-design
    description: "Design rejected - revise system design (cap: 1 revise)."
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = no && test "$(cat .spur/run/$__runId-idea-design-reject-count 2>/dev/null || echo 0)" -le 1'
  - from: design-approval
    to: failed
    description: "Design rejected after 1 revise - design-approval gate exhausted (R7 0433)."
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = no && test "$(cat .spur/run/$__runId-idea-design-reject-count 2>/dev/null || echo 0)" -gt 1'
  - from: design-approval
    to: cancelled
    description: Operator cancelled design approval.
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = cancel'

  # ── decompose: auto-skip (profile=auto) OR enter batch-create HITL gate (interactive) ──
  - from: decompose
    to: batch-create-run
    description: "profile=auto — execute batch-create side effect."
    guard:
      kind: shell
      options:
        command: 'test "$profile" = auto'
  # Interactive fallback: enter batch-create HITL gate
  - from: decompose
    to: batch-create
    description: Interactive — enter batch-create HITL gate for operator confirmation.
    guard:
      kind: always

  - from: batch-create
    to: batch-create-run
    description: Operator approved batch creation — execute side effect.
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = yes'
  - from: batch-create
    to: decompose
    description: Operator requested decomposition revisions.
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = no'
  - from: batch-create
    to: cancelled
    description: Operator cancelled batch creation.
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = cancel'

  # ── batch-create-run: success -> ready-prepare, failure -> decompose (retry cap: 3), failure+cap -> failed ──
  - from: batch-create-run
    to: ready-prepare
    description: Batch created and result captured — apply ready-by-default preparation, then finalize the handoff.
    guard:
      kind: shell
      options:
        command: "test -f .spur/run/$__runId-idea-batch-create.done"
  # ── ready-prepare: -> handoff-finalize (always; evidence absence degrades to refineall, never fails the run) ──
  - from: ready-prepare
    to: handoff-finalize
    description: Ready evidence sidecar written (or normalized empty) — finalize the handoff report.
    guard:
      kind: always
  - from: batch-create-run
    to: decompose
    description: "Batch-create failed — revise decomposition (retry cap: 3)."
    guard:
      kind: shell
      options:
        command: 'test -f .spur/run/$__runId-idea-batch-create.failed && test "$(cat .spur/run/$__runId-idea-decompose-retry-count 2>/dev/null || echo 0)" -lt 3'
  - from: batch-create-run
    to: failed
    description: Batch-create failed after 3 retries — escalate to failed.
    guard:
      kind: shell
      options:
        command: 'test -f .spur/run/$__runId-idea-batch-create.failed && test "$(cat .spur/run/$__runId-idea-decompose-retry-count 2>/dev/null || echo 0)" -ge 3'

  # ── handoff-finalize: -> handoff (terminal) ──
  - from: handoff-finalize
    to: handoff
    description: Ordering applied, roster refreshed, handoff report written — terminal handoff.
    guard:
      kind: always
