Spec-driven development on OpenSpec, with mechanical spec-as-source enforcement: a custom 'spec-as-source' OpenSpec schema adds file-ownership (targets) and test-verification ([@test]) metadata to every capability spec, three scripts (link check, ownership check, manifest build) keep code and specs from drifting apart, plus requirement-gathering, spec-writer, work-review, and a session-handoff skill with a proactive context-warning hook.
68
85%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Five stages, in order. The order is the whole design: exclusion, then phase, then similarity. Similarity is the last criterion, never the first.
Every command below runs in one of two places. Getting this wrong makes the router non-deterministic — the same prompt routes differently depending on where you happened to be.
| Directory | What runs there | |
|---|---|---|
$ROUTER_HOME | the repo holding the skills (~/spec-as-source unless SKILL_SOURCE_ROOT says otherwise) | Stage 0 only: regenerating the table |
$PROJECT | the project the user is asking about — the current working directory | Stage 2: every probe, without exception |
State both in the trace. When they are the same directory, say so; when they differ, a probe result only means something once you know which tree it looked at.
Runs in $ROUTER_HOME, not in the project:
ROUTER_HOME="${SKILL_SOURCE_ROOT:-$HOME/spec-as-source}"
python3 "$ROUTER_HOME/skills/skill-router/build_router.py"Regeneration is deterministic: unchanged skills produce a byte-identical file, so a no-op run costs nothing. If the table did change, say so before deciding — never decide from a stale table.
Then read routing-table.md section 1 (the index). Sections 2-4 are read when
needed: section 4 when two candidates must be compared, sections 2-3 when a
divergence or a dangling name is relevant to the decision.
Evaluated before any similarity computation.
Read the authoritative exclusions at their source and cite them in the trace by file and section heading — the one citation form this document uses, at every stage that asks for one (see Stage 5). Do not copy the list into this file: a copy that diverges is a silent bug.
~/.claude/CLAUDE.md — section "NON si applicano"rules/prompt-loop.md — section "Trivial-work exclusions"If excluded: answer the request directly and declare it as an exclusion. "Excluded because the work is trivial" and "no skill matched" are different outcomes and must never be reported as the same thing — the first says the process does not apply, the second says the process applies and we could not find its skill.
A heavy process applied to trivial work is not rigour, it is friction, and friction teaches the operator to bypass the process.
If admitted: go to Stage 2.
The phase has two independent sources, answering different questions:
$PROJECT on disk: the blocking probes
(2A) and the change-state probes (2D).Both are always evaluated. Intent is not a fallback for when the disk is silent: an open change does not stop the user from asking for something else. If the two disagree, they become competing candidates and go to the ambiguity branch of Stage 3 — the router asks, it does not pick.
Only the blocking probes (2A) override the request outright, and only because missing infrastructure makes every other answer impossible.
Every phase in the table must be reachable in practice, not merely on paper. A rule that exists here but can never fire — because another rule always pre-empts it — leaves its skills exactly as unreachable as if it were absent. That is a defect in this document, not a skill to be quietly ignored.
Infrastructure that must exist before any other answer is possible. All run in
$PROJECT, in order; the first that trips settles the phase outright —
2C and 2D are not evaluated, and the request's wording does not get a vote.
Record the probe, the exact command, and what it actually printed.
The Trips when column is explicit for each probe, because "fails" is not one condition: some probes trip on a non-zero exit, others on an empty result, others on a count above zero.
| # | Command (in $PROJECT) | Trips when | ⇒ phase | Destination |
|---|---|---|---|---|
| P0 | test -f scripts/verify.sh && test -d openspec/schemas | exit ≠ 0 | 0-setup | spec-as-source-setup |
| P1 | test -f openspec/PLAN.md | exit ≠ 0 | 1-plan | plan-mode |
| P2 | bash scripts/check-plan-gate.sh | exit ≠ 0 | 1-plan | plan-mode, reason verbatim |
Before routing to openspec-propose, always, in $PROJECT:
bash scripts/check-plan-gate.sh --change <name>Non-zero ⇒ do not route to openspec-propose. Redirect to plan-mode and
quote the script's reason verbatim: NO-ENTRY, NOT-APPROVED,
HASH-MISMATCH or STALE. Do not paraphrase it — the reason names the fix.
If the intended change name is not yet known, ask. Do not invent one to get past the gate.
NO-ENTRY on work the user clearly wants is not a formality: it means the work
is not in the plan. Adding an entry is the fix, and it is the user's call, not
yours.
Always evaluated, unless a blocking probe in 2A tripped. These phases are properties of the request, not of the filesystem — so read the request, and say in the trace which rule matched.
Evaluating 2C even when 2D has settled a phase is the point: a repo almost always
has an open change, and gating intent behind "no probe tripped" would make
maint, 3-explore and 2-intake unreachable in every real project.
Take them in order; the first that matches settles the phase.
| # | The request is… | ⇒ phase | Destination |
|---|---|---|---|
| I0 | about the repo's own infrastructure rather than a feature — CI out of step with the tests, or rebuilding targets from their specs to prove the specs suffice | maint | spec-ci-sync / spec-rebuild |
| I1 | exploratory: thinking through an idea, comparing approaches, understanding existing code, with no decision to implement yet | 3-explore | openspec-explore |
| I2 | new non-trivial development work whose contract is not yet fixed — vague scope, unstated constraints, or simply not yet run through refinement | 2-intake | prompt-loop (which absorbs requirement-gathering's interview) |
| I3 | new development work with a clear, already-refined contract | 4-propose | openspec-propose (via Stage 2B) |
| I4 | a plain continuation of work already under way — "continua", "vai avanti", "finisci" | (defers to 2D) | whatever 2D settled |
I2 before I3 is not a preference: ~/.claude/CLAUDE.md puts requirement
gathering first in the mandatory workflow, and rules/prompt-loop.md requires
the loop to run before openspec-propose for non-trivial work. A router that
jumps straight to proposing skips the step whose whole purpose is to stop you
from proposing the wrong thing.
I4 exists so that "continua" is an explicit match rather than the absence of one: it defers to whatever 2D found, and no competition arises.
Where the project is in the life cycle of a named active change. Runs in
$PROJECT, in order; the first that trips settles the disk-side phase.
The Trips when column is explicit for each probe, because "fails" is not one condition: some trip on a non-zero exit, others on empty output, others on a count above zero.
| # | Command (in $PROJECT) | Trips when | ⇒ phase | Destination |
|---|---|---|---|---|
| P3 | ls -d openspec/changes/*/ 2>/dev/null | grep -v archive | output is empty | — | no block: go to 2C |
| P4 | openspec instructions apply --change <n> --json | .state is "blocked" | 4-propose or 5-spec, by the rule below | openspec-propose / spec-writer |
| P5 | grep -c '^- \[ \]' openspec/changes/<n>/tasks.md | count > 0 | 6-apply | openspec-apply-change / spec-loop |
| P6 | bash scripts/verify.sh | exit ≠ 0, or never run in this session | 7-verify | spec-verify |
| P7 | (judgement, not a command) no work-review done for this change in this session | true | 8-review | work-review |
| P8 | while IFS= read -r req; do grep -Fqx "$req" openspec/specs/<cap>/spec.md || exit 1; done < <(grep '^### Requirement:' openspec/changes/<n>/specs/<cap>/spec.md) | exit ≠ 0 | 9-close | openspec-sync-specs → openspec-archive-change; otherwise archive |
P4 chooses by the missing artifact, and the rule is mechanical:
test -f openspec/changes/<n>/proposal.md # absent ⇒ 4-propose
test -f openspec/changes/<n>/specs/<cap>/spec.md # proposal present, this absent ⇒ 5-specA missing proposal.md means the change was never proposed; a proposal without
its capability spec means it was proposed and never specified. "4-propose or
5-spec" without this rule leaves the choice to judgement, which is the thing
the probe exists to remove.
When several changes are active, ask which one rather than picking the first. If
P3's output is empty there is no active change, and the disk side settles on
4-propose (via Stage 2B).
P7 is a judgement, and it is labelled as one. No artifact on disk records that a review happened. Do not invent a command for it, and do not report it as if it had run: say in the trace that it was judged, and on what basis. P6's second condition ("never run in this session") is a judgement too, for the same reason.
maint
while a change is mid-flight — which is neither upstream nor downstream of
anything.When the request textually matches a skill downstream of the settled phase, do not route there. Route upstream, to the missing phase, and state which probe tripped and what it printed.
"implementa il login" in a project with no
openspec/does not route toopenspec-apply-change. It routes tospec-as-source-setup, because P0 tripped, and the trace says so.
The mirror case is real too: when the request points upstream of the settled phase — asking to propose something new while a different change is mid-flight — the phase does not override the request. Treat the two as competing candidates and use the ambiguity branch of Stage 3: ask which one the user means.
This is not an edge case. It is the reason the router exists.
Routing upstream answers a different question from the one asked:
spec-as-source-setup does not implement the login. Say so explicitly, and say
what comes next — that once the upstream skill completes, the router re-runs
from Stage 0 with the original request. A redirect that leaves the user holding
scaffolding and no path back to their actual request has done half a job.
Candidates are only:
phase equals the settled phase — whichever source settled it:
a blocking probe in 2A, an intent rule in 2C, or a change-state probe in 2D.
All three are sources of a settled phase, and leaving one out would strand its
skills as surely as having no rule at all — 2D settles 6-apply, the single
most common phase in a live repo, plusany (handoff, handoff-skill) when the prompt actually
concerns them, plusWithin that set, and only there, compare the prompt against the description
field — section 4 of the table, verbatim. Not the skill name, not the folder
name, not the body.
A skill whose description matches best but whose precondition is unmet is discarded, and the trace says why it was discarded. That is the point of computing similarity last.
modeinline — read the chosen SKILL.md in full (plus its references/ and
templates/ as it instructs) and follow it in this conversation. The
interaction with the user stays in the main thread.
subagent — launch a fresh-context subagent with: the absolute path of
the SKILL.md, the instruction to read and follow it, the user's request, and
the Stage 2 trace. Do not paste the SKILL.md into the parent context. Report
the subagent's result.
self — unreachable by construction: the router is not its own
destination.
Two prohibitions:
inline skill to a subagent. Their input is the
current conversation, which a fresh context cannot see. A handoff run in a
subagent produces a well-formed, empty snapshot — a failure that does not
announce itself.Before a destructive skill (spec-rebuild deletes every targets: file and
rebuilds it): ask for explicit confirmation first, and show what will be
deleted.
Every decision is reported in this shape. A decision that cannot be explained cannot be verified, and one that cannot be verified cannot be corrected.
After a selected skill completes, announce the next expected step from
SEQUENCE.md and stop. Do not invoke that step automatically: the sequence is
orientation only, while disk probes and request intent decide the next actual
routing outcome on a new invocation.
Dirs : $PROJECT=/path/to/project $ROUTER_HOME=~/spec-as-source
Gate : admitted — source ~/.claude/CLAUDE.md "NON si applicano"
Phase : 6-apply — probe P5: `grep -c '^- \[ \]' …/tasks.md` → 7
Plan gate : ok — check-plan-gate.sh → 0 (E03 approved, hash valid)
Candidates : openspec-apply-change, spec-loop
Discarded : spec-verify (phase 7-verify — P5 tripped first)
Chosen : openspec-apply-change — mode subagent
Match on : "Use when the user wants to start implementing, continue
implementation, or work through tasks."Cite sources by section heading, not by line number: a hard-coded line number is the same silent-drift bug Stage 1 warns about two paragraphs earlier, and it has already happened in this file.
The three outcomes carry different phase lines, and none of them may be faked:
not evaluated — Stage 1 short-circuited. Do not invent a probe
result to fill the shape, and do not run Stage 2 just to have something to
write there.These hold at every stage.
routing-table.md. If the right action is covered by no skill, say so openly
instead of naming a plausible one. Do not infer a skill's existence from its
name.rules/ is routable: they are
always-active constraints on the router itself. If asked to "apply rule X",
explain that rules are not activated, they hold..tessl-plugin
rules
skills
handoff
handoff-skill
openspec-apply-change
openspec-archive-change
openspec-explore
openspec-propose
openspec-sync-specs
plan-judge
plan-mode
prompt-loop
requirement-gathering
spec-as-source-setup
templates
openspec-schema
spec-as-source
templates
spec-ci-sync
spec-loop
spec-rebuild
spec-verify
spec-writer
work-review