CtrlK
BlogDocsLog inGet started
Tessl Logo

spec-driven-development/spec-as-source

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

Quality

85%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

ROUTER.mdskills/skill-router/

ROUTER — the decision procedure

Five stages, in order. The order is the whole design: exclusion, then phase, then similarity. Similarity is the last criterion, never the first.

Two directories, and they are not the same one

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.

DirectoryWhat runs there
$ROUTER_HOMEthe repo holding the skills (~/spec-as-source unless SKILL_SOURCE_ROOT says otherwise)Stage 0 only: regenerating the table
$PROJECTthe project the user is asking about — the current working directoryStage 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.


Stage 0 — Refresh the table

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.


Stage 1 — Applicability gate

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.


Stage 2 — Phase

The phase has two independent sources, answering different questions:

  • Where the project is — read from $PROJECT on disk: the blocking probes (2A) and the change-state probes (2D).
  • What the request is about — read from the request itself (2C). Refining an unclear contract, exploring an idea, repository maintenance: none of these is a state of a filesystem, and no probe can detect them.

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.

Stage 2A — Blocking probes

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⇒ phaseDestination
P0test -f scripts/verify.sh && test -d openspec/schemasexit ≠ 00-setupspec-as-source-setup
P1test -f openspec/PLAN.mdexit ≠ 01-planplan-mode
P2bash scripts/check-plan-gate.shexit ≠ 01-planplan-mode, reason verbatim

Stage 2B — The plan gate, before proposing

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.

Stage 2C — Intent

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…⇒ phaseDestination
I0about 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 sufficemaintspec-ci-sync / spec-rebuild
I1exploratory: thinking through an idea, comparing approaches, understanding existing code, with no decision to implement yet3-exploreopenspec-explore
I2new non-trivial development work whose contract is not yet fixed — vague scope, unstated constraints, or simply not yet run through refinement2-intakeprompt-loop (which absorbs requirement-gathering's interview)
I3new development work with a clear, already-refined contract4-proposeopenspec-propose (via Stage 2B)
I4a 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.

Stage 2D — Change state, on disk

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⇒ phaseDestination
P3ls -d openspec/changes/*/ 2>/dev/null | grep -v archiveoutput is emptyno block: go to 2C
P4openspec instructions apply --change <n> --json.state is "blocked"4-propose or 5-spec, by the rule belowopenspec-propose / spec-writer
P5grep -c '^- \[ \]' openspec/changes/<n>/tasks.mdcount > 06-applyopenspec-apply-change / spec-loop
P6bash scripts/verify.shexit ≠ 0, or never run in this session7-verifyspec-verify
P7(judgement, not a command) no work-review done for this change in this sessiontrue8-reviewwork-review
P8while 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 ≠ 09-closeopenspec-sync-specsopenspec-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-spec

A 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.

Reconciling 2C and 2D

  • Same phase → settled, no competition.
  • 2C matched I4 ("continua") → 2D wins.
  • Different phases → both skills become candidates in Stage 3 and go to its ambiguity branch. Say in the trace what each source found. This covers the case where the request is orthogonal to the life cycle — asking for maint while a change is mid-flight — which is neither upstream nor downstream of anything.
  • 2D found nothing and 2C matched → 2C settles it.

The core rule

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 to openspec-apply-change. It routes to spec-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.

After an upstream redirect

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.

Stage 3 — Candidates and similarity

Candidates are only:

  • rows whose 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, plus
  • rows with phase any (handoff, handoff-skill) when the prompt actually concerns them, plus
  • the competing candidate from the mirror case: when the request points upstream of the settled phase, the skill it points at joins the set rather than being silently dropped, and the two go to the ambiguity branch below.

Within 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.

  • One clear winner → Stage 4.
  • Two or more comparable, or none convincingstop and ask. List the candidates with the description fragment that makes each plausible, and let the user choose. Never take the first, the shortest, or the topmost.

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.


Stage 4 — Execute according to mode

inline — 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:

  • Never invoke an unregistered skill through a skill-invocation tool. Only the router is registered; the rest are activated by reading their file.
  • Never delegate an 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.


Stage 5 — Trace

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 the selected skill

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:

  • Routed — the phase line names the probe that tripped, or the intent rule that matched, and what it produced.
  • Redirected upstream — same, plus what the user actually asked for and what happens after the upstream skill completes.
  • Excluded — Stage 1 short-circuits, so no probe ever runs. The phase line reads 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.

Standing prohibitions

These hold at every stage.

  • No invented destinations. Route only to rows present in 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.
  • No dangling destinations. Names listed in section 3 of the table exist nowhere on disk. If the user asks for one by name, say it does not exist — citing where it is referenced from — and do not route to it.
  • Rules are not destinations. Nothing under 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.
  • Never route to the router.

skills

README.md

tile.json