"Invoke Specfuse validation on drafted specs, interpret the output into actionable failure feedback (via a common-error interpretation table), and own the drafting → validating → planning state transitions that hand the initiative to the PM agent. Use when the human requests validation during or after a drafting session; emits spec_validated and feature_state_changed events with idempotence guards, and never self-validates, fixes specs, or transitions on a failed run."
Install
npx skillscat add specfuse/authoring/spec-validation Install via the SkillsCat registry.
Spec validation — v1.1
Reframe note (Model B; docs/naming-convention.md). The unit being validated is an
initiative (INIT-YYYY-NNNN). Thedrafting → validating → planningtransitions are on the
initiative; thevalidating → planninghandoff hands the initiative to the PM agent, whosefeature-decompositionskill (reframed fromtask-decomposition) decomposes it into afeature_graph. Map feature→initiative /FEAT-→INIT-for the orchestrator-unit references
below; generic "spec"/product wording is unchanged.
Invokes Specfuse validation on drafted specs, interprets the output, presents actionable feedback on failures, and owns the drafting → validating → planning state transitions that hand the initiative off to the PM agent. This is the gating skill between the specs agent's conversational work and the PM agent's structured planning work: when this skill emits feature_state_changed(validating → planning), the PM agent's feature-decomposition skill is the next actor (it decomposes the initiative into features).
When this file and the specs agent role config disagree, the role config wins and this file is wrong. Raise an escalation rather than reconciling silently.
Trigger
The human says something like "run validation on INIT-2026-NNNN" or "validate the specs" during a drafting session (first-pass validation) or after fixing issues surfaced by a prior run (re-validation). The trigger is conversational — there is no structured-event trigger.
Precondition. A valid initiative registry entry must exist at /features/INIT-YYYY-NNNN.md with state: drafting (first-pass) or state: validating (re-validation after a failed pass). The ## Related specs section must contain at least one spec file link — validation against zero files is a configuration error, not a pass. If the feature is in any state other than drafting or validating, the skill does not proceed — it informs the human and suggests the appropriate entry point.
Substrate precondition (authoring #26, repointed in #55). Before Step 1, and before any write, resolve every artifact this skill depends on.
Prerequisite: specfuse init . has been run in this repo. Core provisions
the methodology substrate into .specfuse/methodology/, so the contracts below
resolve from the repo you are working in. A sibling ../orchestrator/ checkout
is not involved and is not a fallback — that path is the dependency inversion
#26 exists to remove.
Resolving from .specfuse/methodology/ once init has run:
schemas/event.schema.json— event envelope.schemas/events/spec_validated.schema.json— per-type payload.rules/correlation-ids.md,rules/never-touch.md,rules/role-switch-hygiene.md,rules/verification-discipline.md.
Available as core CLI commands, not as files to read — specfuse init
installs the specfuse package that provides them:
specfuse validate-event --file <path>— validates the envelope AND the
per-type payload, includingfeature_state_changedandspec_validated.
Verified against core 0.12.1 in a repo with onlyspecfuse initrun: no
orchestrator checkout,INIT-correlation IDs accepted.specfuse validate-frontmatter --file <path>— validates registry-entry
frontmatter againstfeature-frontmatter.schema.json. An initiative entry
needsfeature_graphpresent; that is the schema's discriminator, not a
planning field this skill has to invent.
Use these rather than looking for scripts/validate-event.py orscripts/validate-frontmatter.py. Those paths are how the orchestrator
resolved the validators from its own checkout; the CLI is how every other plane
reaches the same schemas.
Still shipped from nowhere the authoring plane can reach:
shared/rules/escalation-protocol.md,shared/rules/verify-before-report.md.feature-frontmatter.schema.jsonas a readable file. Validation against
it works (above); reading it to inspect its shape does not, because the
ownership manifest categorises it as an execution-plane schema —specfuse/orchestrator#87. This skill only validates, so it is not blocked.
In core but held back from provisioning, so still unreachable today:
- The state vocabulary. Core carries the shared lifecycle spine — the
drafting → validating → planning → …states and their transition owners — inmethodology/glossary.md§"Lifecycle states", which is canonical and is what
theshared/rules/state-vocabulary.mdcitations in this file need. Core's
provisioner lays downrules/andschemas/only and holds the prose back
because the loop scaffold ships a divergedglossary.md; that editorial
decision isspecfuse/specfuse#137. Repointing these citations is a
one-line change here once it lands.
If anything above is unresolvable, STOP and report:
This skill requires substrate the authoring plugin does not ship. If
.specfuse/methodology/is absent, runspecfuse init .and retry. If what is
missing isescalation-protocol.md,verify-before-report.mdor the state
vocabulary, core does not ship it to this plane yet — see authoring #26 / #55.
If aspecfuse validate-*command is missing, the core package is not
installed, whichspecfuse initalso fixes. No spec was validated, no event was emitted, and
no state was transitioned.
Do not improvise a replacement, skip the validation step, or read the artifacts
out of a sibling ../orchestrator/ checkout.
Stopping here costs a session. Stopping partway through costs a half-written
artifact that looks finished — which is the failure this check exists to prevent.
Inputs
The skill reads, in order:
- The initiative registry entry at
/features/INIT-YYYY-NNNN.md— its frontmatter (correlation_id,state,involved_repos) and its## Related specsbody section (which lists the spec file paths to validate). - This skill file and the specs agent role config — reloaded per
.specfuse/methodology/rules/role-switch-hygiene.md. - The feature's event log at
/events/INIT-YYYY-NNNN.jsonl— read to determine whether priorspec_validatedandfeature_state_changedevents exist for this feature (required for idempotence guards in Steps 3 and 8). - Each spec file listed in
## Related specs— read and passed to the Specfuse validator.
Procedure
Step 1 — Read the initiative registry and confirm preconditions
Read /features/INIT-YYYY-NNNN.md. Extract the state from frontmatter and the spec file paths from the ## Related specs section.
Extracting spec file paths. The ## Related specs section contains markdown list items linking to spec files. Each item follows the convention:
- `product/specs/<feature-slug>.yaml` — description...
- `product/features/INIT-YYYY-NNNN.md` — description...Extract the path from the backtick-enclosed segment of each list item. The paths are relative to the product specs repo root.
Precondition checks:
| Condition | Action |
|---|---|
state is drafting |
First-pass validation. Proceed to Step 2. |
state is validating |
Re-validation. Skip Step 2 (the drafting → validating transition has already occurred). Proceed to Step 3. |
state is any other value |
Do not proceed. Inform the human: "Feature INIT-YYYY-NNNN is in state <state> — validation requires drafting or validating. If you need to re-draft, use the spec-drafting skill. If the feature is already in planning or beyond, the PM agent has taken over." |
## Related specs has zero file links |
Do not proceed. Inform the human: "No spec files found in the initiative registry's Related specs section. Use the spec-drafting skill to create at least one spec file before running validation." |
Step 2 — Emit drafting → validating transition (first-pass only)
This step runs only when the feature's current state is drafting. On re-validation (state is already validating), this step is skipped — no duplicate state-transition event is emitted.
2a. Update the initiative registry frontmatter: change state: drafting to state: validating.
2b. Construct the feature_state_changed event:
| Field | Value |
|---|---|
timestamp |
date -u +%Y-%m-%dT%H:%M:%SZ — captured at emission time |
correlation_id |
INIT-YYYY-NNNN |
event_type |
feature_state_changed |
source |
specs |
source_version |
The literal "n/a" — the specs agent is a session-driven external actor across the plane seam, not a versioned orchestrator role |
payload.from_state |
drafting |
payload.to_state |
validating |
payload.trigger |
validation_requested |
2c. Write the event to /tmp/event.json. Validate through scripts/validate-event.py:
python3 scripts/validate-event.py --file /tmp/event.jsonExit 0: append to the event log using the safe append pattern:
printf '%s\n' "$(cat /tmp/event.json)" >> events/INIT-YYYY-NNNN.jsonlExit 1 or 2: diagnose, correct, re-validate. Three consecutive failures → escalate with spinning_detected.
Step 3 — Invoke the Specfuse validator
For each spec file path extracted in Step 1, invoke the Specfuse validator:
specfuse validate <spec-file-path>The validator is invoked once per spec file. Capture:
- The exit code (0 = pass, non-zero = at least one error).
- The structured output (JSON array of error objects, each with
file,line,message,severityfields). - The validator version (
specfuse --version), captured once at the start of the validation run — not per-file.
Collecting results. Aggregate the per-file results into a single validation result:
pass:trueif every file's exit code is 0;falseif any file produces a non-zero exit code.spec_files_checked: the ordered list of paths that were validated.errors: the union of all error objects across all files (empty whenpassistrue).validator_version: the captured version string.
Step 4 — Emit the spec_validated event
Construct the spec_validated event with the aggregated results:
| Field | Value |
|---|---|
timestamp |
date -u +%Y-%m-%dT%H:%M:%SZ — captured at emission time |
correlation_id |
INIT-YYYY-NNNN |
event_type |
spec_validated |
source |
specs |
source_version |
The literal "n/a" — the specs agent is a session-driven external actor across the plane seam, not a versioned orchestrator role |
payload.feature_correlation_id |
INIT-YYYY-NNNN |
payload.pass |
true or false |
payload.spec_files_checked |
Array of paths from Step 3 |
payload.errors |
Array of error objects from Step 3 (empty on pass) |
payload.validator_version |
Captured version string |
Write to /tmp/event.json, validate, and append:
python3 scripts/validate-event.py --file /tmp/event.json
# Exit 0 → append
printf '%s\n' "$(cat /tmp/event.json)" >> events/INIT-YYYY-NNNN.jsonlThree consecutive validation failures → spinning_detected escalation.
Step 5 — Branch on validation result
If pass is false: proceed to Step 6 (present failure feedback). The feature stays in validating.
If pass is true: proceed to Step 7 (present success summary), then Step 8 (emit validating → planning transition).
Step 6 — Present actionable failure feedback
The skill does not dump raw validator output. For each error in the errors array, the skill:
- Looks up the error in the interpretation table below.
- If a match is found: presents the interpreted guidance — the specific file and line, what went wrong in plain language, and a concrete suggestion for fixing it.
- If no match is found: presents the raw error with a caveat: "This error is not in my interpretation table — please review the raw validator output below."
Presentation format:
Validation failed for INIT-YYYY-NNNN — N error(s) across M file(s).
──────────────────────────────────────────────────────────────────
File: product/specs/<feature-slug>.yaml
Line: 42
Error: <plain-language description of what went wrong>
Fix: <concrete remediation step>
Raw: <original validator message, for reference>
──────────────────────────────────────────────────────────────────
File: product/specs/<feature-slug>.yaml
Line: 78
...
──────────────────────────────────────────────────────────────────
The feature remains in `validating`. Fix the issues above and ask me
to re-validate when ready.After presenting the feedback, the skill's work for this run is complete. The human works with the spec-drafting skill to fix the issues, then triggers re-validation by returning to this skill.
Step 7 — Present success summary
On a clean pass:
Validation passed for INIT-YYYY-NNNN — N file(s) checked, 0 errors.
Files validated:
- product/specs/<feature-slug>.yaml ✓
- product/features/INIT-YYYY-NNNN.md ✓
Proceeding to hand off to the PM agent for task decomposition.Step 8 — Emit validating → planning transition (clean pass only)
This step runs only on a clean validation pass (pass is true).
8a. Idempotence guard. Read the event log at /events/INIT-YYYY-NNNN.jsonl. Scan for a prior feature_state_changed event where payload.from_state is validating and payload.to_state is planning. If found, the transition has already been emitted (e.g., the human ran validation again after a prior clean pass without any intervening changes) — skip the rest of this step and report:
Feature INIT-YYYY-NNNN has already transitioned to `planning`.
The PM agent can pick up the feature for task decomposition.8b. Update the initiative registry frontmatter: change state: validating to state: planning.
8c. Construct the feature_state_changed event:
| Field | Value |
|---|---|
timestamp |
date -u +%Y-%m-%dT%H:%M:%SZ — captured at emission time |
correlation_id |
INIT-YYYY-NNNN |
event_type |
feature_state_changed |
source |
specs |
source_version |
The literal "n/a" — the specs agent is a session-driven external actor across the plane seam, not a versioned orchestrator role |
payload.from_state |
validating |
payload.to_state |
planning |
payload.trigger |
validation_passed |
8d. Write to /tmp/event.json, validate, and append:
python3 scripts/validate-event.py --file /tmp/event.json
# Exit 0 → append
printf '%s\n' "$(cat /tmp/event.json)" >> events/INIT-YYYY-NNNN.jsonl8e. Report completion:
Feature INIT-YYYY-NNNN is ready for PM planning.
State: validating → planning
Trigger: validation_passed
PM handoff: the PM agent's task-decomposition skill can now pick up
this feature.This is the last action the specs agent takes on this feature's forward path. After the handoff, the PM agent is the next actor.
Step 9 — Verify
Per verify-before-report.md:
- Re-read
/features/INIT-YYYY-NNNN.mdand confirm thestatefield matches the expected value (validatingon failure,planningon success). - Re-read
/events/INIT-YYYY-NNNN.jsonland round-trip the most recent event(s) throughvalidate-event.pywith exit 0. - If the
validating → planningtransition was emitted: confirm no written path is innever-touch.md.
Three consecutive re-read failures → spinning_detected escalation per escalation-protocol.md.
State transition discipline
The skill owns two feature-level transitions. Both are emitted as feature_state_changed events with structured payloads per feature_state_changed.schema.json.
drafting → validating
- When: the human requests validation and the feature is currently in
drafting. - Timing: emitted before the validator runs (Step 2), so that the event log records the intent to validate even if the validator invocation fails.
- Trigger:
validation_requested. - Idempotence: if the feature is already in
validating(re-validation), the transition is skipped entirely — no duplicate event is emitted. Thevalidatingstate means "validation in progress or awaiting fix + re-validation." - Registry update:
state: drafting→state: validatingin the feature's frontmatter.
validating → planning
- When: all spec files pass validation with zero errors.
- Timing: emitted after the
spec_validated(pass: true)event (Step 8), as the last action of the skill. - Trigger:
validation_passed. - Idempotence: guarded by scanning the event log for a prior
feature_state_changedevent withfrom_state: validatingandto_state: planning. If found, the transition is skipped. This prevents duplicate handoffs if the human runs validation again on unchanged, already-validated specs. - Registry update:
state: validating→state: planningin the feature's frontmatter. - PM handoff: this transition is what releases the PM agent's task-decomposition skill. Once emitted, the PM agent can pick up the feature. On a failed validation, the feature stays in
validating— the skill does not transition back todraftingon failure, because that would require the human to re-trigger thedrafting → validatingtransition unnecessarily.
Validation output structure
Every validation run (pass or fail) produces a spec_validated event. The per-type payload schema is spec_validated.schema.json, with fields:
| Field | Type | Description |
|---|---|---|
feature_correlation_id |
string |
INIT-YYYY-NNNN — duplicated from envelope for payload self-containment |
pass |
boolean |
true if all files pass; false if any file has errors |
spec_files_checked |
string[] |
Paths checked, relative to product specs repo root |
errors |
object[] |
Structured error objects (empty on pass) |
errors[].file |
string |
Spec file path that produced the error |
errors[].line |
integer |
Line number (0 if not reported) |
errors[].message |
string |
Validator's raw error message |
errors[].severity |
string |
error (blocks) or warning (advisory) |
validator_version |
string |
Specfuse validator version used |
The event log is append-only. A re-validation after a fix produces a new spec_validated event — prior failures are preserved for audit. Consumers that need the current validation status read the most recent spec_validated event for the feature.
Interpretation table for common Specfuse validation errors
The skill translates raw validator errors into actionable remediation. For each error encountered during a validation run, the skill looks up the error's message pattern in this table and presents the corresponding plain-language explanation and fix.
| Error pattern | Plain-language explanation | Remediation |
|---|---|---|
missing required property: operationId |
An API operation is missing its operationId field. Every OpenAPI operation must have a unique operationId — it is the stable identifier that downstream skills (QA, component) use to reference specific endpoints. |
Add an operationId field to the operation. Use a camelCase name that describes the action: e.g., operationId: listWidgets for GET /widgets. |
missing required property: summary |
An API operation is missing its summary field. While not always schema-required, the Specfuse validator enforces summary for spec readability. |
Add a summary field with a brief description of what the operation does: e.g., summary: List all widgets. |
duplicate operationId: <id> |
Two or more operations share the same operationId. Operation IDs must be unique across the entire spec document. |
Rename one of the duplicate operations. Use a more specific name that distinguishes the two: e.g., listWidgets vs. listArchivedWidgets. |
invalid \$ref: <path> |
A $ref JSON reference points to a schema or component that does not exist in the document. |
Check the $ref path for typos. Verify the target schema exists under components/schemas/ (or the appropriate section). Common mistakes: wrong casing, missing the leading #/. |
missing required property: info |
The spec document is missing the top-level info object, which is required by the OpenAPI specification. |
Add an info object with at least title and version fields at the top level of the spec. |
missing required property: paths |
The spec document has no paths object. At least one path is required for the validator to consider the spec meaningful. |
Add a paths object containing at least one path definition with at least one operation. |
invalid response status code: <code> |
A response is declared with an HTTP status code that is not a valid three-digit code or a valid range pattern (2XX, 3XX, etc.). |
Correct the status code. Use standard HTTP status codes: 200, 201, 204, 400, 404, 409, 500. Range patterns: 2XX, 3XX, 4XX, 5XX. |
path parameter .* not found in path template |
A parameter declared with in: path does not appear as a {placeholder} in the URL path template. |
Either add {<param_name>} to the path string, or change the parameter's in value to query or header if it is not a path segment. |
schema type mismatch |
A schema declares a type that conflicts with the surrounding context or a $ref target's type. |
Review the schema definition. Common cause: a $ref points to an object schema but the enclosing context expects an array (or vice versa). Wrap with type: array + items: {$ref: ...} if an array of objects is intended. |
YAML syntax error at line <N> |
The spec file is not valid YAML. The parser cannot proceed beyond the indicated line. | Open the file and check line N for common YAML mistakes: incorrect indentation, unquoted special characters (:, #, @), missing colons after keys, tabs mixed with spaces. |
JSON syntax error |
The spec file is not valid JSON. | Run the file through a JSON linter. Common causes: trailing commas, unquoted keys, single-quoted strings (JSON requires double quotes). |
unknown spec format |
The validator cannot determine whether the file is OpenAPI, AsyncAPI, or Arazzo. | Ensure the file has the correct top-level discriminator: openapi: "3.x.x" for OpenAPI, asyncapi: "2.x.x" or asyncapi: "3.x.x" for AsyncAPI, arazzo: "1.0" for Arazzo. |
missing channel or missing channels |
An AsyncAPI document is missing the channels section. |
Add a channels object with at least one channel definition. Each channel must have a name and at least one operation (publish/subscribe). |
invalid sourceDescription reference |
An Arazzo workflow references a sourceDescription that does not exist in the document's sourceDescriptions array. |
Check the sourceDescriptions array at the top level of the Arazzo document. Add the missing entry, ensuring the name matches what the workflow step references and the url points to a valid OpenAPI or AsyncAPI document. |
must NOT have additional properties on an x-* block |
A vendor extension carries a key the repo's Spectral ruleset does not list. Read this as a ruleset gap before a spec error: the guard is a closed schema over a vocabulary the generator owns, so a key the generator added and the ruleset never learned about produces exactly this message, pointing at the spec. | Check the key against Vendor_Extensions.md first. If the handbook documents it, the ruleset is behind — run ./scripts/specfuse/check-extension-vocabulary.py to confirm, then add the key to the guard's schema in the same change that adopts it. Only if the handbook does not document it is this a spec typo. |
acceptance criterion .* not testable |
A feature narrative's acceptance criterion does not map to a testable behavior — it is too vague, describes multiple behaviors, or lacks an observable outcome. | Rewrite the criterion to describe a single, observable behavior. Each criterion should answer: "What input triggers this behavior? What observable outcome does it produce?" See the spec-drafting skill's §"Writing acceptance criteria that the QA agent can consume" for examples. |
Errors not in the table. For any error whose message does not match a pattern in the table above, the skill presents the raw error with:
This error is not in my interpretation table — please review the raw
validator output below.
File: <file>
Line: <line>
Raw: <original message>
Severity: <severity>
If this error recurs across features, consider filing an issue to
expand the interpretation table.Closed-schema extension guards must be verified against their owner
A repo that lints vendor extensions with additionalProperties: false has
closed a schema over a vocabulary it does not own. The generator owns that
vocabulary and adds to it on its own release cadence. This is not a warning-level
exposure: when the generator ships a new key, the first spec that declares it
fails lint with an additionalProperties error, so the generator feature cannot
be adopted at all, and the failure names the spec instead of the ruleset.
The skill treats an automated agreement check as a precondition for trusting a
clean lint. Before reporting a Spectral pass as evidence, confirm the repo has
one:
./scripts/specfuse/check-extension-vocabulary.pyexists and is invoked by the
Spectral runner (the kit wires it intovalidate-spectral.sh).- Its most recent run did not skip. A skip is loud but still a skip — with no
cached generator jar, nothing was verified. CI should pass--require-jar.
If the repo has no such check, say so in the validation summary and treat the
Spectral result as unverified for extension shape. Do not accept "the ruleset
is updated by hand whenever a feature adds a key" as a substitute. That is the
practice this exists to replace: it failed three times on one extension —x-entity.domain shipped across 78 entities and sat broken for months,x-entity.concurrency blocked a rollout until someone patched the ruleset, andx-entity.delete was heading for the same wall. Each was caught by a human
noticing, which is not a control.
Every closed guard, not just the famous one. x-entity is where this keeps
being discovered, but the exposure belongs to the closed schema, not the
extension: x-value-object, the async and Arazzo rulesets, and any project-local<token>-*-shape rule have it identically. The check discovers guards
structurally — any rule whose then closes a schema — so a guard added next
month is covered without editing anything.
One ruleset, one runner. A second copy of a hand-maintained ruleset drifts
silently, and the copy CI does not run is the one that rots — a real instance had
diverged so far that the platform running it rejected every entity in the tree
while CI stayed green on the other copy. Ruleset files no script names are
reported for this reason.
Idempotence under re-validation
The skill handles two modes — first-pass and re-validation — and must be idempotent: running validation twice on unchanged specs produces the same result without duplicate state-transition events.
State transitions are guarded, not events. The event log is append-only: every validation run produces a new spec_validated event, even if the result is identical to a prior run. This is correct — the append-only log records that validation was invoked, not just its outcome. State transitions, however, are not append-only: each transition happens at most once per feature per direction.
Guards:
drafting → validatingguard (Step 2). Before emitting, check the registry'sstatefield. If it is alreadyvalidating, the transition has been emitted in a prior run — skip Step 2 entirely. This covers the re-validation case: the human fixed issues and is asking for another pass without needing to re-enterdrafting.validating → planningguard (Step 8a). Before emitting, scan the event log for a priorfeature_state_changedevent withpayload.from_state: "validating"andpayload.to_state: "planning"on this feature's correlation ID. If found, the handoff has already occurred — skip the transition. This covers the case where the human runs validation a second time after a prior clean pass.
Prior failures are preserved. A re-validation after a fix produces a new spec_validated event. The event log therefore contains the full history: prior failed runs followed by the successful run. No events are deleted or overwritten.
Worked example
A realistic validation session for a feature with two spec files, demonstrating both failure and success paths, the interpretation of errors, and the state transitions.
Context. The human has drafted specs for INIT-2026-0042 (Inventory API bookmarks). The initiative registry at /features/INIT-2026-0042.md has state: drafting and the following ## Related specs section:
## Related specs
- `product/features/INIT-2026-0042.md` — feature narrative with acceptance
criteria (AC-1 through AC-5).
- `product/specs/inventory-api-bookmarks.yaml` — OpenAPI spec for the
bookmarks endpoints.First validation run — failure
Human: "Run validation on INIT-2026-0042."
Step 1 — Precondition check:
The skill reads /features/INIT-2026-0042.md. State is drafting. Two spec files found in ## Related specs. Preconditions pass.
Step 2 — Emit drafting → validating:
# Update registry frontmatter
# state: drafting → state: validating
# Capture timestamp and version
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
SOURCE_VERSION="n/a" # session-driven external actor; not a versioned orchestrator role
# Construct event
cat > /tmp/event.json << EOF
{"timestamp":"${TIMESTAMP}","correlation_id":"INIT-2026-0042","event_type":"feature_state_changed","source":"specs","source_version":"${SOURCE_VERSION}","payload":{"from_state":"drafting","to_state":"validating","trigger":"validation_requested"}}
EOF
# Validate and append
python3 scripts/validate-event.py --file /tmp/event.json
# Exit 0
printf '%s\n' "$(cat /tmp/event.json)" >> events/INIT-2026-0042.jsonlStep 3 — Invoke Specfuse validator:
VALIDATOR_VERSION=$(specfuse --version)
# → "1.0.0"
specfuse validate product/features/INIT-2026-0042.md
# Exit 0 — narrative spec passes
specfuse validate product/specs/inventory-api-bookmarks.yaml
# Exit 1 — one errorValidator output for inventory-api-bookmarks.yaml:
[
{
"file": "product/specs/inventory-api-bookmarks.yaml",
"line": 18,
"message": "missing required property: operationId",
"severity": "error"
}
]Aggregated result: pass: false, 1 error across 2 files.
Step 4 — Emit spec_validated event:
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
SOURCE_VERSION="n/a" # session-driven external actor; not a versioned orchestrator role
cat > /tmp/event.json << EOF
{"timestamp":"${TIMESTAMP}","correlation_id":"INIT-2026-0042","event_type":"spec_validated","source":"specs","source_version":"${SOURCE_VERSION}","payload":{"feature_correlation_id":"INIT-2026-0042","pass":false,"spec_files_checked":["product/features/INIT-2026-0042.md","product/specs/inventory-api-bookmarks.yaml"],"errors":[{"file":"product/specs/inventory-api-bookmarks.yaml","line":18,"message":"missing required property: operationId","severity":"error"}],"validator_version":"1.0.0"}}
EOF
python3 scripts/validate-event.py --file /tmp/event.json
# Exit 0
printf '%s\n' "$(cat /tmp/event.json)" >> events/INIT-2026-0042.jsonlStep 5 — Branch: pass is false → proceed to Step 6.
Step 6 — Actionable failure feedback:
The skill looks up missing required property: operationId in the interpretation table. Match found. Presents:
Validation failed for INIT-2026-0042 — 1 error across 2 files.
──────────────────────────────────────────────────────────────────
File: product/specs/inventory-api-bookmarks.yaml
Line: 18
Error: An API operation is missing its `operationId` field. Every
OpenAPI operation must have a unique `operationId` — it is
the stable identifier that downstream skills (QA, component)
use to reference specific endpoints.
Fix: Add an `operationId` field to the operation at line 18.
Use a camelCase name that describes the action: e.g.,
`operationId: listBookmarks` for the GET /bookmarks
endpoint.
Raw: missing required property: operationId
──────────────────────────────────────────────────────────────────
The feature remains in `validating`. Fix the issue above and ask me
to re-validate when ready.Verification (Step 9): registry state is validating ✓. Event log has two entries (feature_state_changed + spec_validated), both validate ✓.
Re-validation after fix — success
The human opens the OpenAPI spec and adds operationId: listBookmarks to the GET /users/{user_id}/bookmarks operation at line 18.
Human: "I've added the operationId. Re-validate."
Step 1 — Precondition check:
Registry state is validating. Two spec files in ## Related specs. Preconditions pass.
Step 2 — Skipped. State is validating, not drafting. No drafting → validating transition is emitted. Idempotence: the transition from the first run is preserved.
Step 3 — Invoke Specfuse validator:
VALIDATOR_VERSION=$(specfuse --version)
# → "1.0.0"
specfuse validate product/features/INIT-2026-0042.md
# Exit 0
specfuse validate product/specs/inventory-api-bookmarks.yaml
# Exit 0Aggregated result: pass: true, 0 errors across 2 files.
Step 4 — Emit spec_validated event:
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
SOURCE_VERSION="n/a" # session-driven external actor; not a versioned orchestrator role
cat > /tmp/event.json << EOF
{"timestamp":"${TIMESTAMP}","correlation_id":"INIT-2026-0042","event_type":"spec_validated","source":"specs","source_version":"${SOURCE_VERSION}","payload":{"feature_correlation_id":"INIT-2026-0042","pass":true,"spec_files_checked":["product/features/INIT-2026-0042.md","product/specs/inventory-api-bookmarks.yaml"],"errors":[],"validator_version":"1.0.0"}}
EOF
python3 scripts/validate-event.py --file /tmp/event.json
# Exit 0
printf '%s\n' "$(cat /tmp/event.json)" >> events/INIT-2026-0042.jsonlStep 5 — Branch: pass is true → proceed to Step 7, then Step 8.
Step 7 — Success summary:
Validation passed for INIT-2026-0042 — 2 files checked, 0 errors.
Files validated:
- product/features/INIT-2026-0042.md ✓
- product/specs/inventory-api-bookmarks.yaml ✓
Proceeding to hand off to the PM agent for task decomposition.Step 8 — Emit validating → planning:
8a. Idempotence guard: scan event log for feature_state_changed(validating → planning) on INIT-2026-0042. Not found — this is the first clean pass. Proceed.
8b. Update registry frontmatter: state: validating → state: planning.
8c–8d. Construct, validate, and append the event:
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
SOURCE_VERSION="n/a" # session-driven external actor; not a versioned orchestrator role
cat > /tmp/event.json << EOF
{"timestamp":"${TIMESTAMP}","correlation_id":"INIT-2026-0042","event_type":"feature_state_changed","source":"specs","source_version":"${SOURCE_VERSION}","payload":{"from_state":"validating","to_state":"planning","trigger":"validation_passed"}}
EOF
python3 scripts/validate-event.py --file /tmp/event.json
# Exit 0
printf '%s\n' "$(cat /tmp/event.json)" >> events/INIT-2026-0042.jsonl8e. Report:
Feature INIT-2026-0042 is ready for PM planning.
State: validating → planning
Trigger: validation_passed
PM handoff: the PM agent's task-decomposition skill can now pick up
this feature.Verification (Step 9): registry state is planning ✓. Event log has four entries (feature_state_changed(drafting→validating), spec_validated(pass:false), spec_validated(pass:true), feature_state_changed(validating→planning)), all validate ✓. No duplicate state transitions ✓.
Event log after the worked example
The event log at /events/INIT-2026-0042.jsonl contains four entries (plus the feature_created from intake):
| # | event_type |
Key payload fields |
|---|---|---|
| 1 | feature_created |
(from intake — pre-existing) |
| 2 | feature_state_changed |
from_state: drafting, to_state: validating, trigger: validation_requested |
| 3 | spec_validated |
pass: false, 1 error (missing operationId) |
| 4 | spec_validated |
pass: true, 0 errors |
| 5 | feature_state_changed |
from_state: validating, to_state: planning, trigger: validation_passed |
Confirming the acceptance criteria:
- (a) First validation emits
drafting → validating(entry 2) +spec_validated(pass: false)(entry 3). ✓ - (b) Re-validation emits
spec_validated(pass: true)(entry 4) +validating → planning(entry 5). ✓ - (c) No duplicate state transitions: one
drafting → validating, onevalidating → planning. ✓ - (d) The
validating → planningtransition is the last action — no further specs-agent work after the handoff. ✓
Artifacts produced
| Artifact | Path | Validated against |
|---|---|---|
| Initiative registry entry (state update) | /features/INIT-YYYY-NNNN.md |
feature-frontmatter.schema.json via validate-frontmatter.py |
feature_state_changed event(s) |
/events/INIT-YYYY-NNNN.jsonl |
event.schema.json + feature_state_changed.schema.json via validate-event.py |
spec_validated event |
/events/INIT-YYYY-NNNN.jsonl |
event.schema.json + spec_validated.schema.json via validate-event.py |
Schemas consumed
.specfuse/methodology/schemas/event.schema.json— event envelope validation.shared/schemas/events/feature_state_changed.schema.json— per-type payload validation for state transition events..specfuse/methodology/schemas/events/spec_validated.schema.json— per-type payload validation for validation result events (authored in this WU).shared/schemas/feature-frontmatter.schema.json— frontmatter validation after registry state updates.
Rules absorbed
.specfuse/methodology/rules/correlation-ids.md— feature-level ID used in event envelopes and payloads.verification-discipline.md(corespecfusemethodology) — the four-step cycle: state intent, act, verify, report.shared/rules/verify-before-report.md(orchestrator plane) — the surface-specific report shape built on top of it, plus §"Event-emission operational discipline" (timestamps at emission time, canonical--file /tmp/event.jsoninvocation, JSONL single-line requirement, safe append pattern) and the corrective-cycle limit. Not a renamed copy of the core rule — the emission half exists only here, so it is a real cross-plane dependency, not a reference update.specfuse/specfuse#119closed as completed without placing this content anywhere the authoring plane can reach, and its own closing comment still lists "authoring-surface expression of the report step" as an open decision — so this citation currently has no owner upstream. See authoring #26 / #55..specfuse/methodology/rules/never-touch.md— path prohibition check on every write.shared/rules/state-vocabulary.md—validatingandplanningstates, transition legality.shared/rules/escalation-protocol.md—spinning_detectedescalation after three consecutive validation failures..specfuse/methodology/rules/role-switch-hygiene.md— re-read shared rules unconditionally at the start of every task.
Anti-patterns
- Self-validating. The skill runs the Specfuse validator — it does not judge whether the spec "looks correct." "The spec looks valid to me" is not validation; it must be
specfuse validate <file>with exit 0. - Emitting
validating → planningon a failed run. The transition is gated onpass: true. A falsevalidating → planningtransition releases task decomposition on an unvalidated spec — the single worst outcome this skill can produce. - Emitting duplicate state transitions. Running validation twice on unchanged specs must not produce two
drafting → validatingevents or twovalidating → planningevents. The guards in Steps 2 and 8a prevent this. - Transitioning back to
draftingon failure. On a failed validation, the feature stays invalidating. Thevalidatingstate means "validation in progress or awaiting fix + re-validation." Transitioning back todraftingwould force the human to re-trigger thedrafting → validatingtransition unnecessarily. - Dumping raw validator output without interpretation. The skill's value is translating machine output into human-actionable guidance. Presenting raw JSON errors without context, specific file locations, and concrete fix suggestions fails the human.
- Skipping the
spec_validatedevent on failure. Thespec_validatedevent is emitted on every run, pass or fail. The append-only log records the full validation history. Skipping the event on failure hides the failure from audit. - Eye-caching
validator_version. It is captured at execution time viaspecfuse --version; a stale string produces a misleading audit trail.source_versionis not read from anywhere — it is the constant"n/a"for this role (see the event-field tables above). - Appending events before validation. Events must pass
validate-event.pywith exit 0 before append. An invalid event in the log corrupts the audit trail. - Modifying spec files. The spec-validation skill does not fix specs. It reports what is wrong. The spec-drafting skill is the tool for fixing spec content.
- Performing work after the PM handoff. The
validating → planningtransition is the last action. After emitting it, the specs agent has no further forward-path work on this feature. Any follow-up (spec-issue triage on routed issues) is a separate entry point, not a continuation of the validation session.
What this skill does not do
- It does not draft or fix specs. That is the spec-drafting skill's concern. This skill reports what is wrong; the human and the spec-drafting skill fix it.
- It does not create the task graph or open issues. That is the PM agent's territory — specifically
task-decomposition/SKILL.md, which is triggered by theplanningstate this skill produces. - It does not author test plans. That is the QA agent's concern via
qa-authoring/SKILL.md. - It does not write to
/product/. The validation skill reads spec files; it does not modify them. - It does not write to
/business/. That subtree is off-limits pernever-touch.md§4. - It does not modify
event.schema.json's enum. Thespec_validatedtype already exists in the Phase 1 baseline. This WU adds only the per-type payload schema. - It does not modify the PM agent's configuration or any downstream skill. The handoff is clean: emit the event, update the state, stop.
References
/docs/orchestrator-architecture.md§6.1 (feature state machine), §6.3 (transition ownership)./agents/specs/CLAUDE.md— the specs agent role config that orchestrates this skill./agents/specs/skills/spec-drafting/SKILL.md— the preceding skill that produces the spec files this skill validates; also the skill the human returns to when fixing validation failures./agents/specs/skills/initiative-intake/SKILL.md— the intake skill that creates the initiative registry entry this skill reads and updates./agents/pm/skills/task-decomposition/SKILL.md— the downstream skill that consumes theplanningstate this skill produces; the PM handoff target..specfuse/methodology/schemas/event.schema.json— event envelope schema./shared/schemas/events/feature_state_changed.schema.json— per-type schema for state transitions..specfuse/methodology/schemas/events/spec_validated.schema.json— per-type schema for validation results (authored in this WU)./shared/schemas/feature-frontmatter.schema.json— frontmatter validation for registry state updates.verification-discipline.md(corespecfusemethodology) — the four-step cycle: state intent, act, verify, report.shared/rules/verify-before-report.md(orchestrator plane) — the surface-specific report shape built on top of it, plus §"Event-emission operational discipline" (timestamps at emission time, canonical--file /tmp/event.jsoninvocation, JSONL single-line requirement, safe append pattern) and the corrective-cycle limit. Not a renamed copy of the core rule — the emission half exists only here, so it is a real cross-plane dependency, not a reference update.specfuse/specfuse#119closed as completed without placing this content anywhere the authoring plane can reach, and its own closing comment still lists "authoring-surface expression of the report step" as an open decision — so this citation currently has no owner upstream. See authoring #26 / #55..specfuse/methodology/rules/never-touch.md— path prohibition./shared/rules/state-vocabulary.md— feature state machine states and transitions./shared/rules/escalation-protocol.md—spinning_detectedescalation./docs/walkthroughs/phase-3/retrospective.md— WU 3.10 idempotence guard pattern.