Independently verify one built EPIC agent-side — the deep gate of the execute pipeline (implement → validate → launch). Re-runs everything mechanically through a per-tool runner family (java, .net, node, frontend, sql, lint, sonar): the epic''s tests, blast-radius-scoped regression, code-level security and quality scans — then compares results against the slice''s quality-lens gates AND the product profile''s benchmarks (green is the entry, the floor is the bar). Ends in a binary stamped verd...
Installs into .claude/skills of the current project.
Are you the author of Validate?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/kapilvirenahuja-validate)
---
name: validate
position: none
description: 'Independently verify one built EPIC agent-side — the deep gate of the execute pipeline (implement → validate → launch). Re-runs everything mechanically through a per-tool runner family (java, .net, node, frontend, sql, lint, sonar): the epic''s tests, blast-radius-scoped regression, code-level security and quality scans — then compares results against the slice''s quality-lens gates AND the product profile''s benchmarks (green is the entry, the floor is the bar). Ends in a binary stamped verdict: `validated` (the precondition /launch requires) or `fix_required` (blocks /launch; the fix report names each failure by check and location so /implement fixes exactly that, nothing more). Implement ↔ validate is expected to loop — scope narrows per round, 3 rejections halt to a human. Finds, never fixes; every finding mechanically cited. The /validate command in the ProductOS command model. Use when an implemented epic awaits validation.'
user-invocable: true
---
# validate
Take one epic /implement built to "specs passing, awaiting validation" and verify it
**independently, agent-side** — the deep gate of the execute pipeline. Agents are good at
fixing things; this play's whole value is **finding what is not working with total
clarity**. Implement ↔ validate is expected to run a few rounds; the loop is affordable
because every finding is mechanically cited (the failing check, the rule that fired,
where) and the re-verification scope narrows per round
(`architecture/regression-by-blast-radius`).
**Green is the entry, never the verdict** (`technology/validation-floor-profile-benchmarks`):
captured results are compared against the slice's quality-lens gates and the **product
profile's benchmarks** — all tests passing with coverage below the profile's floor is a
reject. An unmeasured bar is not a passed bar.
The mechanics run through a **per-tool runner family** (the platform-adapter pattern):
`run_checks.py` orchestrates; `runners/runner_{java,dotnet,node,frontend,sql,lint,sonar}.py`
each ensure their tool, run it, and emit one normalized result record. The play may
compile, run, or deploy the product **only to test it** — deploying for human acceptance
is /launch's. Security is **code-level only**: static analysis, lint, dependency scans —
never penetration-style probing.
The verdict is binary and **stamped on the epic** — the one durable model write:
`validated` unblocks /launch; `fix_required` blocks it and re-admits /implement with the
fix report as its exact work list (lightweight re-entry: fix what's named, no re-plan).
**Pipeline position: none.** /validate is the MIDDLE play of the execute pipeline
(implement → validate → launch): it expects to run on the epic branch /implement already
started (one issue per epic), injects no head and no close, stops after the verdict is
stamped and reported, and leaves the branch as-is — /launch carries the close chain after
human sign-off. (#434, decisions 20 + 24)
## Compiled From
This play was compiled from the validate ICE (`reference/ice.md`) by play-editor
(#466 Batch B, Level 3 rollout per ADR 025; #467 Batch D turned the Step 4 plan-review
gate off and elevated its machine precondition). Intent defines constraints (C1–C15) and
failure conditions (F1–F15); the expectation defines success scenarios (S1–S5), a Done
means (D1–D4, baked to `stop-condition.yaml`), and one recovery entry per failure
condition. This play is SINGLE-PASS per run: no internal loop, no iteration cap — the
implement ↔ validate fix loop lives ACROSS runs; the Done means is proven once, at the
gated close.
To modify this play, update `reference/ice.md` and recompile with play-editor.
Do NOT edit this file manually — it is a compiled artifact.
## Role
You are the orchestrator. You own the workflow and the step order. You delegate the two
pieces of judgment — planning the checks (which tools for which stacks, KB-grounded) and
judging the captured results into cited findings — to the `quality-auditor` agent via JSON
contracts over files on disk, and you run every mechanical part (eligibility + round
resolution, scope resolution, check execution, gates + benchmarks, the verdict, the stamp,
the report, the final self-check) through bundled scripts. You never edit product code,
never run a check inline that a runner owns, never hand-stamp a verdict, and never write
any model file except through `scripts/stamp_epic.py`.
**Forbidden:** editing or "quick-fixing" product code (validate finds, /implement fixes);
hand-authoring a verdict or a finding without its captured citation; running tools outside
the runner family; trusting implement's self-reported results instead of re-running;
probing live systems for security; full-suite regression when a scope file bounds it;
touching the epic record beyond the surgical stamp; starting a fourth round past the cap;
closing COMPLETED without the stop-condition verdict held (C14/F14 — a verdict stamped
either way counts as done, an unstamped run does not).
**Agent boundaries:**
| Agent | Domain | Skills it invokes | Phases |
|-------|--------|-------------------|--------|
| `quality-auditor` | Plan the checks (stack detection + KB-grounded tooling) and judge the captured results into mechanically cited findings | `plan-validation-checks`, `judge-validation-results` | Plan, Judge |
| `project-orchestrator` | Post the rendered report to the epic's tracked issue | `manage-issue` | Report |
| `repo-orchestrator` | Evidence self-commit per the close standard | `commit-change` (evidence scope) | Close |
`quality-auditor` is the single **domain agent** (1 of the ≤5 budget); the other two are
utility. The judge skill carries no Bash — it cannot run mechanics (C4).
## Pre-flight
| Check | Constraint | Action on Failure |
|-------|-----------|-------------------|
| Resolve config + tokens (`.garura/core/config.yaml`) | — | Hard halt |
| On the epic's issue branch (issue resolved from branch) | C10-position | Hard halt |
| Epic eligible: `in_delivery` + issue_ref; implement's done verdict holds (pieces done, gates pass, steelman PASS) | C1 | Hard halt (REC1) |
| Loop cap not reached (rejections < 3) | C10 | Hard halt → escalation record (REC10) |
```
python3 scripts/preflight.py --play validate --config .garura/core/config.yaml \
--branch "$(git branch --show-current)" --porcelain-file <captured>
python3 scripts/check_ready_validate.py --product-base <product_base> --epic <epic_id> \
--plan {stm_base}{issue}/plan.yaml \
--gates {stm_base}{issue}/gates-results.yaml \
--verdict {stm_base}{issue}/verdict.yaml \
--status-dir {stm_base}{issue}/validate/status --max-rounds 3
```
`preflight.py` returns config facts (`stm_base`, `product_base`, `evidence_record`, the
issue from the branch). The epic resolves by id to its entry in the spine `epics` index
(`{product_base}/product-os/_spine.yaml`); its grounding lives in the epic.md the entry
points to. `check_ready_validate.py` is the
eligibility gate AND round resolver: it emits `round` (prior rejections + 1) and
`prior_report` (the last fix report, the round-N scope source). At the cap it sets
`escalate: true` — present the round history to the human and stop; never start the round
(C10/F10). Also capture the two live reads to files now: `git status --porcelain >
{working}/porcelain-before.txt` (SE-12 compares after the run).
`{working}` is the issue's STM context dir (`{stm_base}{issue}/context/`) — the baked
stop-condition manifest reads the run's artifacts there, relative to the STM root.
Right after the resolver, record the session identity stamp's start marker (#463 —
soft-fail, never a halt):
```
python3 scripts/session_stamp.py --phase start \
--marker "{stm_base}{issue}/status/session-stamp-validate.json" \
--cwd "$(pwd)" --branch "$(git branch --show-current)"
```
**Resume check:** if `{stm_base}{issue}/validate/status/round-<n>-progress.json` exists,
resume — skip completed steps, reset any in-progress step to pending.
## Task DAG
Create ALL tasks immediately after pre-flight — before any domain work.
The play owns this DAG; agents must not edit its top-level tasks.
```
[T1] Resolve blast scope blockedBy: []
[T2] Plan checks (KB-grounded) blockedBy: [T1]
[T3] Grounding check (KB) blockedBy: [T2]
[T4] Checkpoint (skippable) blockedBy: [T3]
[T5] Run checks (runner family) blockedBy: [T4]
[T6] Gates + benchmarks blockedBy: [T5]
[T7] Judge findings blockedBy: [T6]
[T8] Compute verdict blockedBy: [T7]
[T9] Stamp epic blockedBy: [T8]
[T10] Render + post report blockedBy: [T9]
[T11] Verify the run blockedBy: [T10]
[T12] Scenario Validation blockedBy: [T11]
[T13] Close blockedBy: [T12]
```
Mark each task in-progress before its step and completed right after its eval passes.
No runtime reordering. On resume, skip completed and reset in-progress to pending.
## Workflow
### Phase: Scope
**Step 1 — Resolve blast scope** · Owner: play · Depends on: pre-flight
```
python3 scripts/resolve_blast_scope.py \
--reports-dir {stm_base}{issue}/piece-reports \
--round <round> [--prior-report <prior_report>] \
--out {working}/scope.json
```
Round 1 scopes from implement's piece reports (`files_modified` — the build's recorded
claims); round N+1 from the FIX's piece reports plus what the prior report's findings
named. The script can only read claims or fail loud — it cannot invent scope.
**SE-2 (F8/C6):** `resolve_blast_scope.py` exits 0; `scope.json.source` is
`implement-claims` (round 1) or `fix-claims+prior-report` (later rounds); its `files` are
non-empty and every entry traces to a piece report or the prior report.
### Phase: Plan
**Step 2 — Plan checks (KB-grounded)** · Owner: `quality-auditor` · Depends on: Step 1
The agent gathers the epic, the slice's quality + architecture lenses, the profile, the
scope, and the repo, and invokes `plan-validation-checks`:
{
"task": "author the check manifest for this epic: detect the stack(s) (arch lens + repo manifests), resolve each to KB-grounded tooling (build, tests, scoped regression per scope.json, lint/static analysis, dependency audit, the scanners the bars demand), map every quality-lens gate (quality.md "## Gates" table) and measure-lens metric (measure.md "## Metrics" table) to a concrete check; code-level security only; round > 1 plans only what the prior report and narrowed scope require; uncovered tool choices become KB-learning-gap proposals, never silent picks. ALSO map the epic's declared surface.type (surface-contract.md) to its ONE required runnable check and add it to the manifest tagged `surface_check: <surface.type>` — a real browser check that opens each must_open artifact (web_dashboard), an HTTP/API call to the declared endpoint (server_api), a run of human_run_target (cli), or the library/service's own tests (library/service_read_model). The plan is INCOMPLETE for a required (user-facing) surface unless it carries that surface_check entry — never omit it for lack of a browser/API probe; the absence of a probe fails the surface, it does not waive it.",
"inputs": { "epic_file": "<epic_file>",
"quality_lens": "<lens_dir>/quality.md",
"arch_lens": "<lens_dir>/architecture.md",
"measure_lens": "<lens_dir>/measure.md",
"profile_path": "<product_base>/product-os/_spine.yaml",
"scope_file": "{working}/scope.json",
"repo_root": "<repo_root>",
"round": "<round>",
"kb_search": "<skills>/kb-search/scripts/kb_search.py",
"kb_root": "<knowledge_dir>" },
"outputs": { "out_dir": "{working}/plan/",
"manifest": "{working}/plan/checks.yaml",
"choices": "{working}/plan/check-choices.yaml",
"proposals": "{working}/plan/proposals/" }
}
**SE-4 (F9/C5):** every `checks.yaml` entry's `runner` is one of the bundled runner
classes (java, dotnet, node, frontend, sql, lint, sonar) and no command targets a remote
system — the plan is code-level/local by construction; a probe-class check cannot execute
because no runner exists for it.
**SE-14 (F13/C13):** the plan is complete against the epic's surface — `checks.yaml`
carries exactly one entry tagged `surface_check: <epic.surface.type>` for the declared
type (for a user-facing type web_dashboard/server_api/cli that entry is the required
runnable check: a browser open of each `must_open`, an HTTP call to the endpoint, or a run
of `human_run_target`; for library/service_read_model it is the own-tests entry). A
required surface with no `surface_check` entry is an INCOMPLETE plan — the check is not
waived for lack of a browser/API probe; loop back to Step 2 to add it before running.
**Step 3 — Grounding check (KB)** · Owner: play · Depends on: Step 2
```
python3 scripts/check_kb_grounding.py --manifest {working}/plan/check-choices.yaml \
--proposals-dir {working}/plan/proposals --kb-root <knowledge_dir>
```
**SE-3 (F12/C12):** exits 0 — every tool pick and scoping decision carries a KB learning
id that resolves or a proposal file that exists; none is invented. On GAP, apply REC12.
### Phase: Checkpoint (skippable)
**Step 4 — Review the plan** · Owner: play · Depends on: Step 3 · **Checkpoint (class: standard)**
Resolve the checkpoint's gate FIRST, per `standards/rules/gate-config.md` (first match
wins: `gates.plays.validate` → `gates.classes.standard` → `gates.default`; absent ⇒ on).
Under #467 Batch D `gates.plays.validate` is **off** — the human plan-review is redundant
because the manifest is fully DERIVED (Step 2 detects the stacks, Step 3 grounds every
tool choice in the KB, and any ungroundable pick already halted as a KB-learning-gap at
Step 3, C12/F12). The gate governs ONLY the human review; that grounding wall is the
machine's and is never gated (C15).
- **off** — do NOT wait. The precondition that replaces the human review is the machine
wall: `check-choices.yaml` grounded clean at Step 3 — every check traces to a
stack-detection result plus a KB-grounded tool choice, no ungrounded pick. Record
`gate skipped by config (gates.plays.validate)` as a Checkpoint Decisions row in the
evidence and proceed. If any tool choice is ungrounded and was NOT recorded as a
KB-learning-gap, do NOT proceed — apply REC15: halt, run the KB search, record the gap,
re-derive, and proceed only when the manifest is fully grounded (this halt is the
machine wall, never gated).
- **on** — present the check plan in plain language: the stacks detected, the checks per
bar, the regression scope and its source, any KB gaps or lens-vs-repo mismatches from
`notes`. **Skip rule:** when `check-choices.yaml` grounded clean with zero proposals and
the plan's `notes` are empty, skip — post the summary and continue. Otherwise wait for a
typed response.
**SE-16 (F15/C15):** every entry in `checks.yaml` traces to a stack-detection result plus
a KB basis (its `check-choices.yaml` learning id resolves) OR to a recorded
KB-learning-gap proposal under `{working}/plan/proposals/` — the off-path precondition
holds; a proceed on an ungrounded, unrecorded choice is F15 (REC15). The Step 4 gate
resolution (fired, skipped, or its config-skip row) is recorded in the evidence either
way — never silent.
### Phase: Execute
**Step 5 — Run checks (runner family)** · Owner: play · Depends on: Step 4
```
python3 scripts/run_checks.py --manifest {working}/plan/checks.yaml \
--results-dir {working}/results [--only <prior-findings' check ids, round > 1>]
```
The orchestrator dispatches each check to its runner as a **concurrent read-only fan-out**
(`standards/rules/concurrent-fanout.md`): the checks run in a bounded worker pool and JOIN
before the summary is built. The three safety conditions hold — each check only READS the
code (SE-12 proves the tracked-file set is byte-unchanged), each writes only its own
`<check_id>.json`, and no check consumes another's output; results are collected in
manifest order, so `summary.json` is identical to a serial run. Runners ensure their tool
(install when the manifest says how), run it, and emit normalized records. A tool that
cannot run is `status: error` — recorded, never skipped.
**SE-5 (F5/C4, C7):** every result in `{working}/results/` was emitted by a runner (each
carries `raw_log_path`; `summary.json` counts them); no check result was authored by an
agent or inline prose; everything that ran, ran to TEST — the manifest carries no
acceptance-deploy entry (deploying for human acceptance is /launch's, C7). **SE-12 (F4/C2):** re-capture `git status --porcelain >
{working}/porcelain-after.txt`; the tracked-file set matches `porcelain-before.txt` —
validate edited no product code (build outputs are untracked; any tracked diff is REC4).
**Step 6 — Gates + benchmarks** · Owner: play · Depends on: Step 5
```
python3 scripts/check_gates.py --quality-lens <lens_dir>/quality.md \
--measure-lens <lens_dir>/measure.md \
--results-dir {working}/results --out {working}/gates-map.json \
--product-base <product_base> --epic <epic_id>
```
**SE-11 (F3-partial/C11):** every quality-lens gate (read from `quality.md`'s "## Gates"
table, by dimension) maps to a captured result and every measure-lens metric (read from
`measure.md`'s "## Metrics" table) is captured and, where its Target cell yields a
comparator + number, at/above its floor (or at/below its ceiling) — or it is a FINDING in
`gates-map.json` with citation + location (gate-unmeasured, benchmark-below-floor). The
floor is the bar; exit 1 with findings is a valid (failing) outcome, not an error.
**SE-14 (F13/C13) — verdict half:** `check_gates.py` (with `--product-base --epic`) requires a
captured result tagged `surface_check: <epic.surface_type>` with status pass; a required
surface the run did not measure, or a captured surface result whose type does not match the
declared `surface.type`, is a finding (`surface-unmeasured` / `surface-mismatch` /
`surface-failed`) in `gates-map.json` with citation + location — so the verdict step
(Step 8) computes `fix_required`, never `validated`, on an unmeasured required surface. A
browser/API/CLI promise cannot pass on code-level checks alone.
### Phase: Judge
**Step 7 — Judge findings** · Owner: `quality-auditor` · Depends on: Step 6
The agent reads ONLY the captured artifacts and invokes `judge-validation-results`:
{
"task": "compose the findings from the captured results: extract the exact failing assertion/rule/file:line from each failed check's raw log, deduplicate root causes across tools, flag gamed-looking patterns (collapsed test counts, fresh suppressions) — every finding cites captured output and a location; no speculation, nothing run, nothing fixed",
"inputs": { "results_dir": "{working}/results",
"gates_map": "{working}/gates-map.json",
"scope_file": "{working}/scope.json" },
"outputs": { "out_findings": "{working}/findings.yaml" }
}
**SE-6 (F2/C3):** every entry in `findings.yaml` carries a `citation` quoting captured
output and a `location` (file:line or the raw log path) — enforced again mechanically at
Step 8, which refuses uncited findings.
### Phase: Verdict
**Step 8 — Compute verdict** · Owner: play · Depends on: Step 7
```
python3 scripts/compute_verdict.py --summary {working}/results/summary.json \
--gates-map {working}/gates-map.json --findings {working}/findings.yaml \
--round <round> --epic-id <epic_id> --out {working}/verdict.json
```
**SE-7 (F3/C8):** the verdict is computed mechanically — `validated` only when every
check passed AND zero gate/benchmark findings AND zero judge findings; the script refuses
uncited findings (ok=false) rather than weakening the verdict. Record the round:
copy `verdict.json` → `{stm_base}{issue}/validate/status/verdict-round-<round>.json`
(with `report` set to the report path once rendered) — the next round's gate reads these.
**Step 9 — Stamp epic** · Owner: play · Depends on: Step 8
Snapshot first, then the one durable model write:
```
cp <product_base>/product-os/_spine.yaml {working}/spine-before.yaml
python3 scripts/stamp_epic.py --product-base <product_base> --epic <epic_id> \
--verdict-file {working}/verdict.json --record {working}/verdict.json
```
On a successful stamp, `--record` marks the verdict artifact `stamped: true` in place —
the value the stop-condition gate reads at close (#464, Done means D4; a verdict stamped
either way counts as done, an unstamped run does not — C14). Then re-copy
`verdict.json` → `{stm_base}{issue}/validate/status/verdict-round-<round>.json` so the
durable round record carries the stamp.
On a `validated` verdict the stamp also sets `surface_verified: true` — the surface-parity
gate (C13) passed, so the epic records that its required surface was measured and matched
(surface-contract.md); /next reads this to distinguish a surface that shipped from surface
debt. On `fix_required`, `surface_verified` is left untouched.
**SE-8 (F6/C8):** `stamp_epic.py` exits 0 and the epic's status now equals the verdict —
`fix_required` actually blocks /launch and re-admits /implement; `validated` actually
unblocks it. **SE-9 (F11/C8):** Step 11 proves against the snapshot that only `status`,
`metadata.version`, and (on a pass) `surface_verified: true` changed.
### Phase: Report
**Step 10 — Render + post report** · Owner: play, then `project-orchestrator` · Depends on: Step 9
```
python3 scripts/render_fix_report.py --verdict {working}/verdict.json --round <round> \
--out-yaml {working}/report.yaml --out-md {working}/report.md
```
Then dispatch `project-orchestrator` to post `report.md` to the epic's tracked issue via
`manage-issue` (comment) — the report is implement's work list and must survive a lost
session, like implement's published plan. Update the round record's `report` field with
the `report.yaml` path.
**SE-10 (F7/C9):** `render_fix_report.py` exits 0 (it refuses uncited entries); on
`fix_required` every rendered entry names the check id, the citation, and the location;
the issue carries the posted comment.
### Phase: Verify
**Step 11 — Verify the run** · Owner: play · Depends on: Step 10
```
python3 scripts/check_validate.py --verdict {working}/verdict.json \
--summary {working}/results/summary.json --gates-map {working}/gates-map.json \
--spine-before {working}/spine-before.yaml --product-base <product_base> --epic <epic_id> \
--report-yaml {working}/report.yaml --report-md {working}/report.md
```
**SE-1 (F1/C1):** holistic re-assert — the run only got here through the eligibility
gate (pre-flight exit 0 recorded in the round progress record). **SE-13 (F10/C10):** the
round number in `verdict.json` is ≤ 3 and, when this round was a third rejection, the
close presents the escalation record (all three rounds' findings + fixes) instead of
inviting a fourth. `check_validate.py` exits 0: the verdict matches the recomputed
evidence (F3), the stamp matches the verdict (F6), the epic write was surgical (F11),
and a failed run carries a complete, cited report (F7).
### Phase: Scenario Validation
**Step 12 — Scenario evals** · Owner: play · Depends on: Step 11
- **SCE-1 (S1 — delivery lead):** on a clean run, the epic file's status is `validated`,
`verdict.json` cites every captured check (counts match `summary.json`), and its
findings list is empty.
- **SCE-2 (S2 — implementer):** on a failing run, the epic status is `fix_required`,
every `report.yaml` finding carries check id + location, and on the re-run round the
stamp flips to `validated` with total rounds ≤ 3.
- **SCE-3 (S3 — security auditor):** scan results exist under `{working}/results/` with
rule ids in their findings' citations; no check ran outside the runner family; the
grounding check exits 0 with zero ungrounded tool choices.
- **SCE-4 (S4 — product owner):** the regression checks' commands trace to `scope.json`,
whose `source` is recorded claims; each regression failure in the findings carries its
check id.
- **SCE-5 (S5 — human):** at three rejections the next invocation's eligibility gate
exits with `escalate: true`, no round-4 artifacts exist, and the escalation record
lists each round's findings and fixes.
### Phase: Evidence & Close
**Step 13 — Close** · Owner: play · Depends on: Step 12
Run the Standard Play Close. /validate is **project-scoped** (the epic's issue). The close
is GATED (the stop-condition step below, #464): the verdict decides COMPLETED vs HALTED.
**SE-15 (F14/C14):** the stop-condition verdict read held before COMPLETED was stamped —
D1–D4 all held; a run whose checks ran but whose verdict never stamped closes HALTED
(`stop_condition_unmet`), never COMPLETED. A stamped `fix_required` counts as done — the
gate binds the RUN's completeness, not the epic's quality.
```bash
# --- Standard Play Close (canonical; see standards/rules/play-close.md) ---
# Path tokens resolved at pre-flight (resolve here if not already):
# ltm_project_target = yq '.ltm.project-target' .garura/core/config.yaml
# evidence_base, slug:
# project-scoped play : evidence_base="${stm_base}${issue}/evidence/validate/" ; slug="#${issue}"
# product-scoped play : evidence_base="${product_base}_evidence/validate/" ; slug="${product_slug}"
evidence_template=$(cat "${ltm_project_target}standards/templates/evidence-file.md")
delivery_template=$(cat "${ltm_project_target}standards/templates/delivery-report.md")
ts=$(date -u +%Y%m%d-%H%M%S)
evidence_dest="${evidence_base}${ts}.md"
mkdir -p "$(dirname "$evidence_dest")"
# Session identity stamp (#463) — close phase; start phase ran at pre-flight
session_stamp=$(python3 scripts/session_stamp.py --phase close \
--marker "${stm_base}${issue}/status/session-stamp-validate.json")
# Stop-condition gate (#464) — Step C0: this play carries a baked manifest, so the
# gate is LIVE. Single-pass play: the Done means is proven here, once.
python3 scripts/check_stop_condition.py \
--manifest "<play-dir>/stop-condition.yaml" \
--base "${stm_base}${issue}/" \
--out "${stm_base}${issue}/status/stop-condition-validate.yaml"
sc_exit=$? # 0 held · 1 unmet · 2 error
```
**Step C0 — bind the verdict.** `sc_exit == 0` (held) permits `status: COMPLETED`.
Anything else closes `HALTED` with `exit_reason: stop_condition_unmet` and the evidence's
Stop Condition section names every unmet clause. An unevaluable verdict is never a pass.
The bind is about the RUN, not the epic: a `fix_required` verdict, stamped, still closes
COMPLETED — an unstamped run does not (C14).
**Step C1 — Write evidence file.** Gated by the resolved `evidence.record` flag (global +
per-play `evidence.plays.validate`; first match wins, absent ⇒ record). When false, skip
the write and record `evidence skipped (record=false)` in the report's pointer line.
Otherwise fill the `evidence-file.md` slots (play `validate`, run_id `validate-${ts}`,
the issue, started_at/completed_at, status per C0, exit_reason; artifacts produced:
scope.json, checks.yaml + check-choices.yaml, results/ + summary.json, gates-map.json,
findings.yaml, verdict.json, the round record, report.yaml/.md, the stamp diff, the
stop-condition verdict; step and scenario eval results SE-1…SE-15 / SCE-1…SCE-5; the
Step 4 checkpoint decision or skip (or its config-skip row); the session identity stamp
fields from $session_stamp (#463): session_id, ledger_file, ledger_start_offset,
ledger_end_offset (null when unresolved — never blocks the close); and stop_condition
per C0 with the Stop Condition section filled) and write to `$evidence_dest`. Do NOT
hand-author the body. Then dispatch `repo-orchestrator` for the
evidence self-commit when evidence was recorded (the WORK is never committed by this
play — the branch is /launch's to close).
**Step C2 — Render delivery report.** Also render the **Next** line: resolve this play in `standards/rules/pipeline-next.md` and emit `**Next:** /<command> — <why>. Or run /next to see all recommended actions.` (only /next pointer, or omit, when the mapped command is null), per `play-close.md`. Fill the `delivery-report.md` slots and output the
report: `## validate Round <round> — #${issue}` (the verdict, plainly: validated → "/launch
is next"; fix_required → "the fix report is on the issue — run /implement to fix exactly
what it names"; escalation → the three-round history), the Run Summary table, the Pipeline
Steps table from the task DAG, the Artifacts Produced table, Next Steps, and a pointer to
`$evidence_dest`. Always emitted; never gated.
```bash
# --- end Standard Play Close ---
```
## Scenario Validation
| Scenario | Persona | Eval |
|----------|---------|------|
| S1 — happy path | delivery lead | SCE-1 |
| S2 — the fix loop | implementer | SCE-2 |
| S3 — security pass | security auditor | SCE-3 |
| S4 — nothing shipped broke | product owner | SCE-4 |
| S5 — escalation | human | SCE-5 |
## Recovery
| For | Trigger | Direction | Handoff |
|-----|---------|-----------|---------|
| F1 | implement's done verdict missing or incomplete at pre-flight | halt; the epic must finish /implement first | human |
| F2 | a finding has no mechanical citation | rebuild the finding from captured results; re-run its check if needed; drop what cannot be cited | autonomous |
| F3 | the verdict contradicts captured evidence | recompute the verdict mechanically from the result files — never hand-stamp | autonomous |
| F4 | validate edited product code | revert the edit; record it as a finding routed to implement | autonomous |
| F5 | an agent ran mechanics or judged without captured results | re-dispatch with the script doing the work; the agent re-judges over its output | autonomous |
| F6 | a failed run carries no `fix_required` stamp | apply the stamp via the surgical writer before close | autonomous |
| F7 | a fix report too vague to act on | regenerate entries with check id + location from captured results | autonomous |
| F8 | blast scope not drawn from recorded change claims | rebuild the scope from the recorded claims; re-run regression on it | autonomous |
| F9 | probing beyond code-level | discard those results; re-run the security pass code-level only | autonomous |
| F10 | the cap reached without escalation | stop the loop; produce the escalation record with round history | human |
| F11 | the epic record touched beyond the stamp | restore the record and re-apply only the surgical stamp | autonomous |
| F12 | an ungrounded scan choice with no gap recorded | run the KB search; if still uncovered, record the gap proposal, then proceed | autonomous |
| F13 | the required surface check for the epic's `surface.type` was not run, or the captured surface result does not match the declared type | stamp `fix_required` naming the unmeasured/mismatched surface (the `surface.type` and the `must_open`/`human_run_target` it owed), and re-admit /implement to restore it so the next round can measure it | autonomous |
| F14 | the close would stamp COMPLETED without the stop-condition verdict held | evaluate the stop condition, surface the unmet clauses, and close HALTED (`stop_condition_unmet`) until they are fixed — an unevaluable verdict is never a pass | autonomous |
| F15 | with the plan-review gate off, the manifest reached the check-execution step carrying an ungrounded tool choice that was neither KB-matched nor recorded as a KB-learning-gap | halt before the checks run; run the KB search for the uncovered choice, record the KB-learning-gap proposal, re-derive the manifest, and proceed only when every check is fully grounded | autonomous |
## Pause and Resume
Steps run top to bottom. On entry, resolve config, capture the live reads, run the
eligibility + round gate, check the round progress marker, skip completed steps, reset any
in-progress step to pending, and continue. The epic snapshot is captured at Step 9 (the
gated stamp step) and preserved on resume, so the surgical comparison always diffs against
true pre-stamp state. A fresh start with no marker runs everything and creates the marker
at Step 1. Each round is its own progress marker; a resumed round never re-counts itself.
## Compilation Metadata
| Field | Value |
|-------|-------|
| fingerprint | sha256:01bbfe25700849b61f01cc3d0ee6390e2578db62c704560ea22831e01cea3ac8 (of `reference/ice.md`) |
| compiled_by | play-editor (#466 Batch B; #467 Batch D — Step 4 gate off + C15/F15/REC15/SE-16) |
| pipeline_position | none |
| position_exception | middle of the execute pipeline (implement → validate → launch) — runs on the epic branch /implement started; the close chain belongs to /launch after human sign-off (#434, decisions 20 + 24) |
| structural_constraints | C2-part (finder/fixer split — no code-edit step exists), C4 (judge skill carries no Bash; mechanics only via runners), C7 (no acceptance-deploy step — run-to-test only), C9 (remediation only via the posted report; no builder dispatch), C10-position (execute middle — no head, no close) |
| workflow_structure | A (Step 4 checkpoint gate off under #467 Batch D — the human plan-review is replaced by the KB-grounding wall, C15; still skippable when on and the plan grounds clean); single-pass — no loop, no cap; gated close |
| stop_condition | stop-condition.yaml (D1–D4), gate live at Step C0 |
| domain_agents | 1 (quality-auditor) |
| utility_agents | 2 (project-orchestrator, repo-orchestrator) |
| skills_used | plan-validation-checks, judge-validation-results, kb-search, manage-issue |
| standards_consumed | standards/rules/gate-config.md (Step 4 switch); standards/rules/play-close.md; standards/rules/pipeline-next.md |
| scripts | 13 + 8 runners (preflight.py, check_ready_validate.py, resolve_blast_scope.py, run_checks.py, check_gates.py, check_kb_grounding.py, compute_verdict.py, stamp_epic.py, render_fix_report.py, check_validate.py, session_stamp.py — session identity stamp, check_stop_condition.py — Done-means gate, runners/_runner_common.py + 7 runner_*.py) |
| step_evals | 16 (SE-1…SE-16; every failure condition covered; SE-14 covers the surface contract F13/C13 at the plan and verdict halves; SE-15 covers the gated close F14/C14; SE-16 covers the gate-off manifest-grounding precondition F15/C15) |
| scenario_evals | 5 (SCE-1…SCE-5) |
| recovery_entries | 15 (one per failure condition; 13 autonomous / 2 human) |
## Direct-edit deviation note (#434, spine + grounding model)
Non-intent edit: the epic moved from a per-epic `epic.yaml` file to an entry in the spine
`epics` index (`product-os/_spine.yaml`) plus an `epic.md` grounding doc. The mechanism
scripts were retargeted: `check_ready_validate.py` reads the epic entry by id from the spine;
`check_gates.py` reads the epic's `surface_type` from the spine entry (was `epic.yaml`
`surface.type`); `stamp_epic.py` makes the in_delivery → validated/fix_required flip (plus
`surface_verified` on a pass) as a surgical write to the spine epics entry (`--product-base
--epic`); the surgical-write proof now diffs the epic entry between a pre-stamp spine snapshot
(`--spine-before`) and the live spine (`check_validate.py`). Prose invocations updated to
match. No constraint/failure/scenario/eval text changed — the verdict logic, the surface gate,
and the surgical-write guarantee are identical; only the storage moved. `reference/ice.md` and
the fingerprint are unchanged.
**Direct-edit deviation note (#434, validate-readers-spine-migration):** `check_gates.py`'s
gate + benchmark DEFINITIONS now read the lens GROUNDING DOCS, not YAML: quality gates parse
`quality.md`'s "## Gates" table (gate id = dimension slug; captured check_id matches the slug),
and benchmarks parse `measure.md`'s "## Metrics" table (each metric is presence-required, and
where its Target cell yields a comparator + number it is also compared as a floor/ceiling). The
old standalone `profile.yaml` benchmark source is retired — the structured floor/ceiling numbers
it carried now live as prose targets in the measure lens, so a tolerant comparator parse stands
in (prose-only targets are presence-checked). CLI: `--quality-lens` now takes `quality.md`,
`--profile` is replaced by `--measure-lens <measure.md>`; the plan-validation-checks contract
points at the `.md` lenses + the spine. The gate LOGIC (every declared bar maps to a captured
result; an unmeasured bar is not a passed bar) is identical — only the definition source moved
from YAML to the linted, eval-gated grounding docs. `reference/ice.md` and the fingerprint are
unchanged.
**Direct-edit deviation note (#468 Stage 5, concurrent fan-out):** `run_checks.py` now runs
its per-tool check runners as a concurrent read-only fan-out — a bounded worker pool that
joins before the summary is built (`standards/rules/concurrent-fanout.md`) — instead of a
serial loop. The three safety conditions hold (each check reads only the code, writes only
its own `<check_id>.json`, and depends on no other), and results are collected in manifest
order, so `summary.json` is byte-identical to a serial run; a `--max-parallel 1` flag forces
the old serial behavior. Execution-timing change only: no constraint/failure/scenario/eval
touched, the finder-not-fixer guarantee (SE-12) is unchanged, and `reference/ice.md` and the
fingerprint stand. A duplicate-`check_id` guard was added because concurrent runners must not
race on the same result file.