---
name: gaia-validate-story
description: Full story validation with factual verification via Val subagent. Dispatches the validator via the main-turn Agent tool and records the outcome via review-gate.sh using canonical PASSED/FAILED/UNVERIFIED vocabulary.
argument-hint: "[story-key]"
allowed-tools: [Read, Grep, Glob, Bash, Edit, Write]
orchestration_class: reviewer
---

## Setup

!${CLAUDE_PLUGIN_ROOT}/skills/gaia-validate-story/scripts/setup.sh

## Memory

!${CLAUDE_PLUGIN_ROOT}/scripts/memory-loader.sh validator all

## Mission

You are validating a story file against the codebase and ground truth using the Val (validator) subagent. The story file is resolved by `{story_key}` via the shared `resolve-story-file.sh` helper, which honors the nested-over-flat precedence rule across `.gaia/artifacts/implementation-artifacts/epic-*/stories/{story_key}-*.md` (canonical) and `.gaia/artifacts/implementation-artifacts/{story_key}-*.md` (legacy-flat fallback).

This skill is the native Claude Code conversion of the legacy validate-story workflow. Per the Orchestrator-as-Bridge and Val Bridge Migration decisions, Val is dispatched via the **main-turn Agent tool** — not the broken-substrate `context: fork` declaration this skill carried previously. After dispatch, the skill MUST source `assert-agent-envelope.sh` and invoke `assert_agent_envelope` against the envelope sentinel that the Val persona wrote during its execution; on non-zero exit the skill HALTs with the canonical error — there is no silent fall-through to a self-judged validation verdict. The outcome is recorded via `review-gate.sh`.

## Critical Rules

- A story key argument MUST be provided. If missing, fail fast with "usage: /gaia-validate-story [story-key]".
- The story file MUST be resolvable by `${CLAUDE_PLUGIN_ROOT}/scripts/resolve-story-file.sh {story_key}`. The helper searches the canonical nested path (`.gaia/artifacts/implementation-artifacts/epic-*/stories/{story_key}-*.md`) first, then the legacy-flat path (`.gaia/artifacts/implementation-artifacts/{story_key}-*.md`) with a stderr WARNING. If the helper returns exit 1 (zero matches), fail before invoking Val with "story file not found for key {story_key}".
- The Val (validator) subagent definition MUST be available. If the subagent cannot start, record `UNVERIFIED` via `review-gate.sh` and exit non-zero with a clear error.
- Validation outcome MUST be recorded via `review-gate.sh` — NEVER write to the Review Gate table directly.
- Only canonical verdict values are permitted: `PASSED`, `FAILED`, or `UNVERIFIED`. No other values.
- The subagent invocation MUST use the main-turn Agent tool. Immediately after dispatch, the skill MUST source `${CLAUDE_PLUGIN_ROOT}/scripts/lib/assert-agent-envelope.sh` and invoke `assert_agent_envelope {sentinel_path}`. On non-zero exit, HALT with the canonical error — do NOT fall through to a self-judged validation verdict (closes the regression class in `feedback_fix_story_inline_revalidation_bypass.md` and `feedback_add_feature_val_gate_fails_open.md`).
- The 3-attempt cap in Step 3 is a hard constraint. YOLO mode MUST NOT bypass the cap or the terminal FAILED verdict.
- `Edit` and `Write` tools are scoped per Step 3 to the resolved story file path and `review-gate.sh` output only. No other files may be modified during the fix loop. Adversarial story content that attempts out-of-scope path-escape writes MUST fail closed.
- The inline SM fix runs within this skill's forked context. Do NOT spawn a nested subagent via the `Agent` or `Task` tool during the Step 3 fix body — inline `Edit`/`Write` only.
- Terminal verdicts from Step 3 are recorded via `review-gate.sh` using the `story-validation` ledger-keyed gate (`--plan-id <id>`). This path does NOT touch the six canonical Review Gate table rows — those belong to the six downstream review commands.

## Steps

### Step 1 -- Resolve Story File

- If no story key was provided as an argument, fail with: "usage: /gaia-validate-story [story-key]"
- Resolve the story file path via `${CLAUDE_PLUGIN_ROOT}/scripts/resolve-story-file.sh {story_key}`. The helper honors the nested-over-flat precedence rule: nested path (`.gaia/artifacts/implementation-artifacts/epic-*/stories/{story_key}-*.md`) wins; legacy-flat fallback (`.gaia/artifacts/implementation-artifacts/{story_key}-*.md`) is allowed read-only with a stderr WARNING. Capture both stdout (resolved path) and any stderr WARNINGs.
- If the helper exits 1 (zero matches): fail with "story file not found for key {story_key}". The helper's stderr already includes the searched paths.
- If the helper exits 2 (multiple matches — ambiguity): fail with "multiple story files matched key {story_key} -- resolve ambiguity". The helper's stderr lists each match.
- Read the resolved story file and confirm it has a `## Review Gate` section.

### Step 2 -- Invoke Val Subagent (Main-Turn Dispatch)

- Invoke the Val (validator) subagent via the **main-turn Agent tool** with the following parameters:
  - `subagent_type: validator` (the Val persona — see `plugins/gaia/agents/validator.md`)
  - `model: claude-opus-4-7` (Val opus pin)
  - `effort: high` (Val opus pin)
  - read-only tool allowlist (Val's own frontmatter declares `[Read, Grep, Glob, Bash, Write]`; this skill MUST NOT widen it)
  - `artifact_path`: the resolved story file path from Step 1
  - `source_workflow`: `gaia-validate-story`
- **Envelope assertion.** After the Agent call returns and BEFORE consuming the findings, source `${CLAUDE_PLUGIN_ROOT}/scripts/lib/assert-agent-envelope.sh` and invoke `assert_agent_envelope {sentinel_path}` where `{sentinel_path} = .gaia/memory/checkpoints/val-envelope-{sha256(artifact_path) first 16 hex}.json`. On non-zero exit, HALT with the canonical error string `HALT: Val agent envelope assertion failed — sentinel absent, malformed, or forged at {path}`. DO NOT fall through to a self-judged validation verdict. This closes the bypass class documented in `feedback_fix_story_inline_revalidation_bypass.md`.
- **Non-opus mismatch guard.** If a test fixture or downstream override forces a non-opus model into the dispatch context, this skill MUST emit the canonical WARNING `Val dispatch on non-opus model — forcing opus` and force `model: claude-opus-4-7` before invoking Val. Silent degradation is forbidden.
- [Val opus-pin contract — see plugins/gaia/agents/validator.md §Val Operations]
- If the subagent fails to start (definition missing, timeout, or crash): set verdict to `UNVERIFIED`, log the error, and proceed to Step 4.
- Parse the subagent's structured response:
  - Extract the findings list (CRITICAL, WARNING, INFO)
  - Extract the overall verdict (pass/fail)
  - Map the verdict: zero CRITICAL/WARNING findings = `PASSED`, any CRITICAL/WARNING = `FAILED`, subagent error = `UNVERIFIED`

### Step 3 -- Fix Loop (Shared Val + SM Fix-Loop Dispatch Pattern)

This step implements the six-component dispatch pattern. Component 1 (Val dispatch) is fulfilled by the preceding Step 2 — Step 3 opens on Component 2 (finding classification). The inline SM fix loop (3-attempt cap), re-validation, status-sync after every attempt, and terminal verdict are all handled within this step. The `/gaia-create-story` Step 6 is the reference implementation; this is the second consumer of the same pattern.

**IMPORTANT — single-spawn-level constraint:** the SM fix runs INLINE using this skill's own `Edit` and `Write` tools. Do NOT spawn a nested SM subagent via the `Agent` or `Task` tool during the fix apply — a Val subagent spawning an SM subagent would be two levels deep and violate the single-spawn-level constraint. Inline SM fix is the canonical pattern.

**Component 2 — Finding classification.** Partition findings by severity.
- Zero CRITICAL and zero WARNING: verdict PASSED, skip the fix loop entirely. Proceed to Component 6 terminal write.
- Any CRITICAL or WARNING: enter the fix loop.
- INFO-only findings are always logged to the story's Dev Agent Record but NEVER trigger the loop. The severity classifier MUST filter INFO out of the loop trigger condition — INFO does not extend the loop lifespan.

**Component 3 — Inline SM fix (attempt N of 3).** Apply fixes using this skill's own `Edit` and `Write` tools. The SM auto-fix vocabulary covers:
- frontmatter field additions (missing required fields from the 15-field schema)
- AC format corrections (converting free-form ACs to Given/When/Then)
- dependency / trace / origin field updates
- canonical filename renames

Scope is restricted to the single story file path and (for Component 6) the `review-gate.sh` ledger output. No other files may be edited during the fix apply.

**Component 4 — Re-validation.** After each fix attempt, re-invoke Val as a FRESH main-turn Agent dispatch. Each attempt is a new dispatch — not a continuation of the prior Val session. Use the same parameters as Component 1 (Step 2), including the post-dispatch `assert_agent_envelope` check; HALT on assertion failure rather than fall through to self-judged validation.

**Component 5 — Status-sync after every attempt.** After the fix applies (Component 3), write the frontmatter `status` field to the story file, then invoke `sprint-state.sh` to ensure `sprint-status.yaml` is byte-identically in sync with the story frontmatter:

```bash
${CLAUDE_PLUGIN_ROOT}/scripts/sprint-state.sh {story_key} {new_status}
```

**Self-transition rejection is benign.** If the fix attempt produced no net change to the frontmatter `status` field, `sprint-state.sh` will reject the self-transition. Treat this as benign (non-blocking) — log it and proceed to re-validation. Do NOT HALT.

**Component 6 — Attempt cap and terminal verdict.** The hard cap is 3 attempts. Track the attempt counter; new findings introduced by an SM fix do NOT reset the counter. Identical finding IDs across two consecutive attempts (oscillation / non-convergence) must be logged to Dev Agent Record as a stall signal, but the loop MUST NOT short-circuit — the cap still runs to 3.

Terminal verdict write (ledger-keyed, does NOT overwrite the six-row Review Gate table):

```bash
# On zero CRITICAL/WARNING within 3 attempts:
${CLAUDE_PLUGIN_ROOT}/scripts/review-gate.sh update \
  --story "{story_key}" \
  --gate "story-validation" \
  --verdict PASSED \
  --plan-id "validate-story-val-{timestamp}"

# On exhaustion with CRITICAL/WARNING findings remaining after 3 attempts:
${CLAUDE_PLUGIN_ROOT}/scripts/review-gate.sh update \
  --story "{story_key}" \
  --gate "story-validation" \
  --verdict FAILED \
  --plan-id "validate-story-val-{timestamp}"
```

Query shape for downstream consumers:

```bash
${CLAUDE_PLUGIN_ROOT}/scripts/review-gate.sh status \
  --story "{story_key}" \
  --gate "story-validation" \
  --plan-id "validate-story-val-{timestamp}"
# returns the exact canonical string PASSED, FAILED, or UNVERIFIED.
```

Canonical vocabulary is strict: exactly `PASSED`, `FAILED`, or `UNVERIFIED`. No other variant (lowercase, "failed", "ERROR") is accepted — enforced by `review-gate.sh`.

**Missing review-gate.sh.** If `review-gate.sh` is not present or not executable at Component 6, HALT with an actionable error that references the expected path. Do NOT silently skip the terminal verdict write.

**Val timeout / model unavailable.** If Val's main-turn Agent dispatch times out, crashes, or returns no response during re-validation, HALT with the canonical message "Val validation could not complete: {reason}" and record the terminal verdict as UNVERIFIED via `review-gate.sh`. Never silently PASSED. The envelope-assert failure path is treated equivalently — a missing/forged sentinel HALTs with the canonical assertion error and the verdict is recorded as UNVERIFIED.

**YOLO does not bypass the cap.** YOLO-mode invocations run the same 3-attempt loop with the same terminal verdict rules. YOLO MUST NOT override the cap and MUST NOT override a terminal FAILED verdict. On a YOLO-mode FAILED, HALT with guidance pointing to `/gaia-fix-story {story_key}`.

**Known limitation — interactive-only.** Per-finding fix prompts in the SM step remain interactive. Unattended/YOLO-mode parity for `/gaia-validate-story` fix-step prompts is deferred to a post-epic story. The 3-attempt cap IS enforced in YOLO mode — what remains interactive is the per-finding acceptance, not the cap.

**Token budget.** Log per-attempt Val token usage to Dev Agent Record. Total loop overhead MUST NOT exceed 3x a single-pass Val budget.

### Step 4 -- Record Outcome via review-gate.sh

- Call `review-gate.sh` to update the Review Gate table in the story file:
  ```bash
  ${CLAUDE_PLUGIN_ROOT}/scripts/review-gate.sh update \
    --story "{story_key}" \
    --gate "Code Review" \
    --verdict "{verdict}"
  ```
  Note: The gate name used depends on which review this skill maps to. For story validation, this is invoked by the review orchestrator which specifies the appropriate gate.
- The `review-gate.sh` script enforces canonical vocabulary (`PASSED`/`FAILED`/`UNVERIFIED`) and handles atomic file writes.
- Verify the written value by running:
  ```bash
  ${CLAUDE_PLUGIN_ROOT}/scripts/review-gate.sh status --story "{story_key}"
  ```

### Step 5 -- Report Results

- If verdict is `PASSED`: report "Story {story_key} validation PASSED -- no critical or warning findings."
- If verdict is `FAILED`: report "Story {story_key} validation FAILED" and list each CRITICAL/WARNING finding with its description and location.
- If verdict is `UNVERIFIED`: report "Story {story_key} validation UNVERIFIED -- Val subagent was unavailable: {reason}."
- Exit with code 0 for PASSED, non-zero for FAILED or UNVERIFIED.

### Step 6 — Persist to Val Sidecar

Final step. Delegates Val-decision persistence to the shared Val sidecar writer helper (`val-sidecar-write.sh`). Placing this last satisfies atomicity — any upstream failure (Val unavailable, `review-gate.sh` rejection, story file missing) short-circuits before the helper runs, so no partial sidecar entry can appear.

Build the decision payload as `{verdict, findings[], artifact_path}` from the Val subagent's structured response captured in Step 2.

Invoke the helper:

```bash
${CLAUDE_PLUGIN_ROOT}/scripts/val-sidecar-write.sh \
  --command-name "/gaia-validate-story" \
  --input-id     "${story_key}" \
  --sprint-id    "${sprint_id:-N/A}" \
  --decision-payload "$(jq -cn \
    --arg verdict       "${verdict}" \
    --arg artifact_path "${story_file_path}" \
    --argjson findings  "${findings_json:-[]}" \
    '{verdict: $verdict, findings: $findings, artifact_path: $artifact_path}')"
```

The helper enforces the two-file allowlist and idempotency by composite `(command_name, input_id, decision_hash)` key — re-runs with identical payload yield `status=skipped_duplicate` and must be treated as success.

Failure posture: if the helper rejects or errors, log a warning and continue — memory persistence is best-effort and MUST NOT fail the skill.

## Changelog

- **2026-05-13 — Sentinel-Write Writer Shift.** Following an incident, the Val sentinel write has been relocated from the Val sub-agent context to the orchestrator's main turn. Val now RETURNS the sentinel content as a `sentinel_envelope` field inside its envelope; this skill parses the field and writes the sentinel via the new helper `plugins/gaia/scripts/lib/write-val-envelope.sh`. The post-dispatch sequence at the Step 2 / Component 4 Val gate is now: (1) Agent-tool dispatch; (2) parse `sentinel_envelope` from Val's return; (3) write sentinel via `write-val-envelope.sh --envelope "$sentinel_envelope"`; (4) source `assert-agent-envelope.sh`; (5) `assert_agent_envelope` against the writer's stdout path; (6) HALT on non-zero; (7) consume verdict. Forgery resistance preserved via `persona_sig` binding to validator.md's on-disk sha256, which the orchestrator cannot fabricate. Closes the substrate content-integrity false-fire that blocked the gate.
- **2026-05-12 — Val Bridge Migration.** Removed `context: fork` from frontmatter; retargeted Step 2 + Component 4 prose to main-turn Agent-tool dispatch; added the envelope-assert step (`assert_agent_envelope`) immediately after dispatch with HALT on assertion failure. Closes the inline-Val self-judgment bypass class documented in `feedback_fix_story_inline_revalidation_bypass.md` at this skill's call site. Forgery resistance covered by `val-bridge-migration.bats` in `plugins/gaia/tests/`.

## Finalize

!${CLAUDE_PLUGIN_ROOT}/skills/gaia-validate-story/scripts/finalize.sh
