# Wayfinder resolution pipeline — research/specification tasks without the code
# implementation pipeline (no task-pipeline / /sp:dev-run recursion).
#
# Reliability (aligned with task-pipeline / ADR-043):
#   - Soft precheck (task check + show) → failed via transitions (status file, always exit 0);
#     the show capture doubles as the investigate input bundle (0769: the separate collect
#     state re-ran the same CLI reads with no write in between — deleted)
#   - investigate→verify passes only on a non-empty answer capture (fail-closed both ways)
#   - verify is an independent fresh-session observe-only reviewer over the standard verdict
#     contract (`spur task verdict --from-answer` + proof digest bracket + verify-verdict
#     artifact) — 0769 retires the ad-hoc PASS-word file and the >5-line/>60-word padding
#     metrics; evidence citations must still be truthful (grep -n anchors)
#   - HITL approve routing is exhaustive (yes/no/cancel)
#   - record captures `task record` → `task update done --no-lifecycle` into a status file;
#     record → done is fail-closed on the captured result + persisted status readback — an
#     exit 0 never converts a denied record into success (0769)
#   - Free-form agent inputs are intentional: no pure-slash research surface yet

"$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
version: "1"
kind: state-machine
name: wayfinder-resolution
description: "Reusable resolution pipeline for research/specification tasks without invoking the code implementation pipeline"
iterationBound: 20
initialState: precheck
terminalStates:
  - done
  - failed
  - cancelled
failureStates:
  - failed
  - cancelled
vars:
  wbs: "0000"
  profile: "standard"
  spurBin: "spur"
  agent: "auto"
  stepTimeoutMs: "1800000"
  approval: "required"
  resolutionMode: "research"
  evidencePolicy: "spur-cli-first"
  __hitlAnswer: ""
  # R8 (0366): injected by WorkflowAppService.run(); run-scopes every capture.
  __runId: ""
  # Resolved per-run by the verify state's taskpath shell step; folded into the proof
  # digest via taskFile (docs/tasks* is excluded from the digest's git-tree half).
  taskSpecPath: ""
  # Proof-state bracket (docs-pipeline precedent): canonical capture at verify entry,
  # live re-capture after the verifier; the verify → record guard compares them.
  proofDigest: ""
  proofDigestNow: ""

states:
  - id: precheck
    description: >
      Soft precheck: task check + show, status file, always exit 0 so failures
      route to `failed` via transitions (not a raw lifecycle abort). The `task show`
      capture is reused as the investigate input bundle — no separate collect state
      (0769: same reads, no intervening write, no re-run).
    onEnter:
      - kind: shell
        options:
          # (e) 0825: 8 lines — task check + show capture the investigate input bundle in the
          # same soft probe; show must stay verbatim (it IS the bundle downstream reads).
          command: >-
            mkdir -p .spur/run; $spurBin task check $wbs; c=$?; $spurBin task show $wbs --json > .spur/run/$__runId-wayfinder-input.json && [ "$c" -eq 0 ] && printf 'PASS\n' > .spur/run/$__runId-wayfinder-precheck.status || printf 'FAIL\n' > .spur/run/$__runId-wayfinder-precheck.status; exit 0
      - kind: note
        options:
          message: "Wayfinder resolution started for task ${vars.wbs} (mode=${vars.resolutionMode}, evidence=${vars.evidencePolicy}); no code implementation pipeline is permitted."

  - id: investigate
    description: "Resolve the task's research/specification requirements and author evidence-backed sections through Spur CLI."
    onEnter:
      - kind: agent.run
        options:
          agent: ${vars.agent}
          capture: true
          # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
          role: planner
          mode: text
          # (e) 0825: bounded reference-reading prompt (260 chars) — the authoring contract
          # lives in the skill reference sp:wayfinder references/pipeline-resolution.md.
          input: >-
            Resolve wayfinder task ${vars.wbs} per the sp:wayfinder skill (references/pipeline-resolution.md). Mode ${vars.resolutionMode}; evidence policy ${vars.evidencePolicy}; bundle .spur/run/${vars.__runId}-wayfinder-input.json. Finish with a short summary and stop.
          answerFile: .spur/run/${vars.__runId}-wayfinder-answer.md
          expectFile: .spur/run/${vars.__runId}-wayfinder-answer.md
          timeoutMs: ${vars.stepTimeoutMs}

  - id: verify
    description: >
      Independent, observe-only measured verification (0769): a fresh-session reviewer runs
      /sp:dev-verify --fix none, the standard `spur task verdict` contract derives ONE verdict,
      and a proof-input digest bracket fails closed on verifier-time mutation. Retires the
      ad-hoc resolution-verdict PASS-word file. The verifier never repairs and never edits
      task sections.
    onEnter:
      - kind: shell
        options:
          command: "mkdir -p .spur/run && rm -f .spur/run/$__runId-wayfinder-verify-answer.txt .spur/run/$wbs-verdict.json"
      # Fail-closed task-path lookup (0760 R1 pattern): the resolved path folds into the
      # proof digest via taskFile; a silent miss would degrade the proof to tree-only.
      - kind: shell
        options:
          # (e) 0825: 6 lines — fail-closed lookup feeds every downstream artifact name;
          # folding a silent miss into the digest would fake tree coverage.
          command: >-
            set -e;
            task_path="$($spurBin task path $wbs --json | jq -r '.path // .filePath // empty')";
            if [ -z "$task_path" ]; then echo "wayfinder-resolution: task path did not resolve for wbs $wbs" >&2; exit 1; fi;
            printf '%s' "$task_path" > ".spur/run/$__runId-wayfinder-taskpath.txt"
      - kind: file.read.into-var
        options:
          path: .spur/run/${vars.__runId}-wayfinder-taskpath.txt
          var: taskSpecPath
      - kind: proof.fingerprint
        options:
          var: proofDigest
          taskFile: ${vars.taskSpecPath}
          input: /sp:dev-verify ${vars.wbs} --auto --fix none --focus all
          timeoutMs: ${vars.stepTimeoutMs}
      - kind: agent.run
        options:
          agent: ${vars.agent}
          capture: true
          # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
          role: reviewer
          mode: text
          # Observe-only + fresh session (ADR-071 / 0710 R2 precedent): the verifier
          # certifies, it never repairs and never inherits the investigate session.
          freshSession: true
          input: /sp:dev-verify ${vars.wbs} --auto --fix none --focus all
          answerFile: .spur/run/${vars.__runId}-wayfinder-verify-answer.txt
          expectFile: .spur/run/${vars.__runId}-wayfinder-verify-answer.txt
          timeoutMs: ${vars.stepTimeoutMs}
      - kind: shell
        options:
          command: "$spurBin task verdict $wbs --from-answer .spur/run/$__runId-wayfinder-verify-answer.txt"
      - kind: shell
        options:
          # (e) 0825: 6 lines — stamp shell freezes the proof digest + capture point into the
          # verdict before it is registered; the closing guard compares against this stamp.
          command: >-
            V=".spur/run/$wbs-verdict.json";
            if [ -f "$V" ] && [ -n "$proofDigest" ]; then
              jq --arg d "$proofDigest" --arg r "$__runId" '. + {proof: {digest: $d, capturePoint: "wayfinder-verify-entry", runId: $r}}' "$V" > "$V.tmp" && mv "$V.tmp" "$V";
            fi;
            exit 0
      - kind: run.artifact
        options:
          path: .spur/run/${vars.wbs}-verdict.json
          artifactKind: verify-verdict
      - kind: proof.fingerprint
        options:
          var: proofDigestNow
          taskFile: ${vars.taskSpecPath}
          input: /sp:dev-verify ${vars.wbs} --auto --fix none --focus all
          timeoutMs: ${vars.stepTimeoutMs}

  - id: approve
    description: "Human review gate unless the caller explicitly selects auto approval."
    pause: true
    onEnter:
      - kind: hitl.confirm
        options:
          prompt: "Approve research resolution for task ${vars.wbs}?"

  - id: record
    description: >
      Record the resolution AFTER measured verification (0769). The `task record` verb
      backfills Solution from the diff and moves the task to testing; `task update done
      --no-lifecycle` then completes it (this pipeline's own verify already produced the
      standard verdict evidence). Both results are captured to a run-scoped status file and
      the persisted status is read back — a denied record or a task that never reached done
      routes to `failed` via the guards below and is never converted to success by exit 0.
    onEnter:
      - kind: shell
        options:
          command: >-
            if $spurBin task record $wbs --solution-from-diff --transition testing &&
               $spurBin task update $wbs done --no-lifecycle; then
              printf 'PASS\n' > ".spur/run/$__runId-wayfinder-final.status";
            else
              printf 'FAIL\n' > ".spur/run/$__runId-wayfinder-final.status";
            fi;
            $spurBin task show $wbs --json | jq -r '.status // empty' > ".spur/run/$__runId-wayfinder-status.txt"

  - id: done
    description: "Research task resolved and ready to feed the implementation backlog."
    onEnter:
      - kind: note
        options:
          message: "Wayfinder research task ${vars.wbs} resolved; implementation remains deferred to the synthesized build backlog."

  - id: failed
    description: "Research resolution failed a gate; no completion transition is attempted."

  - id: cancelled
    description: "Operator cancelled the research resolution."

transitions:
  - from: precheck
    to: investigate
    description: "Precheck PASS — resolve the research task (input bundle captured at precheck)."
    guard:
      kind: shell
      options:
        command: 'test "$(cat .spur/run/$__runId-wayfinder-precheck.status 2>/dev/null)" = PASS'
  - from: precheck
    to: failed
    description: "Precheck FAIL — stop before delegation."
    guard:
      kind: always
  - from: investigate
    to: verify
    description: "Research resolution authored — verify evidence and traceability. Guard enforces a non-empty answer capture (fail-closed both ways)."
    guard:
      kind: shell
      options:
        command: 'test -s .spur/run/$__runId-wayfinder-answer.md'
  - from: investigate
    to: failed
    description: "Empty answer capture — investigate produced nothing to verify; stop rather than certify hollow evidence."
    guard:
      kind: shell
      options:
        command: '! test -s .spur/run/$__runId-wayfinder-answer.md'
  - from: verify
    to: record
    description: "Auto profile — record only on a measured PASS verdict with the proof bracket intact (fail-closed to the approve gate otherwise)."
    guard:
      kind: shell
      options:
        # (e) 0825: 5-line guard — approval + digest-bracket + verdict PASS must all hold
        # before record; dropping any operand would let a stale proof auto-record.
        command: >-
          test "$approval" = auto &&
          test -n "$proofDigest" &&
          test "$proofDigestNow" = "$proofDigest" &&
          test -f ".spur/run/$wbs-verdict.json" &&
          test "$(jq -r '.verdict // empty' .spur/run/$wbs-verdict.json 2>/dev/null)" = PASS
  - from: verify
    to: approve
    description: "Verification completed — require explicit review approval."
    guard:
      kind: always
  - from: approve
    to: record
    description: "Operator approved the research resolution."
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = yes'
  - from: approve
    to: cancelled
    description: "Operator cancelled the research resolution."
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = cancel'
  - from: approve
    to: failed
    description: "Operator rejected the research resolution."
    guard:
      kind: shell
      options:
        command: 'test "$__hitlAnswer" = no'
  - from: record
    to: done
    description: "Captured record result PASS and the persisted status readback is actually done."
    guard:
      kind: shell
      options:
        command: 'test "$(cat .spur/run/$__runId-wayfinder-final.status 2>/dev/null)" = PASS && test "$(cat .spur/run/$__runId-wayfinder-status.txt 2>/dev/null)" = done'
  - from: record
    to: failed
    description: "Recording failed, was denied, or the task never reached done — stop without claiming completion."
    guard:
      kind: always
