Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ Each command gives the same steps and the same checks to every host.
- **The checks are gates.** A script fails when the spec misses a section, or when an acceptance
criterion stays unchecked. On Claude Code a hook blocks the turn. On another host the skill
stops the agent from reporting the work as done.
- **Claude Code does the independent work in parallel.** `/sf:spec` and `/sf:document` run as
workflow scripts. Every other host does the same steps in order.
- **Claude Code runs workflow steps.** `/sf:spec` and `/sf:document` run as workflow scripts.
Every other host does the same steps in order.

The spec stays on your machine. [Team Workflow](docs/team-workflow.md) shows how a team shares it
through the ticket.
Expand All @@ -53,6 +53,10 @@ The table shows the typical range for each phase. The example is a small CLI too
| implement | 30k–120k | 5k–30k |
| document | 20k–60k | 3k–10k |

By default, `/sf:spec` uses one schema-checked research call and one synthesis call. Set the
workflow input `parallelResearch: true` to use separate scope, criteria, and risk calls in
parallel.

Your token counts change with the size of the codebase, the complexity of the feature, and the number of iterations. For current prices, see [Claude pricing](https://www.anthropic.com/pricing).

## Command Reference
Expand All @@ -67,7 +71,7 @@ Your token counts change with the size of the codebase, the complexity of the fe

| Host | Install | What runs |
|------|---------|-----------|
| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) — reads `~/.claude/skills` | `claude plugin install sf@spec-first` | The three commands, parallel subagents, and validation hooks |
| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) — reads `~/.claude/skills` | `claude plugin install sf@spec-first` | The three commands, workflow subagents, and validation hooks |
| Hosts that read `.agents/skills`: pi, opencode, Codex CLI, GitHub Copilot CLI, Gemini CLI, Cursor, Zed, Amp, Goose, Crush, Kilo Code, Warp, Factory Droid, OpenHands | `./scripts/install.sh` | The three commands in order. The skills call the validation scripts, so the same checks run. |
| Hosts that read their own directory: Cline (`~/.cline/skills`), Qwen Code (`~/.qwen/skills`), iFlow CLI (`~/.iflow/skills`) | `./scripts/install.sh --dir <that directory>` | Same as above |
| Any other host with a skills directory | `./scripts/install.sh --dir <dir>` | Same as above |
Expand Down
31 changes: 19 additions & 12 deletions docs/technical-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
## Overview

- 3 commands. Each one is a skill that states the steps and the gates for one agent to run in order.
- On Claude Code, `/sf:spec` and `/sf:document` run as workflow scripts, which start 4 and 6
agents. `/sf:implement` runs the built-in Explore subagent, then `implement-minimal`.
- On Claude Code, `/sf:spec` starts 2 agents by default and `/sf:document` starts 6 agents.
`parallelResearch: true` makes `/sf:spec` start 4 agents. `/sf:implement` runs the built-in
Explore subagent, then `implement-minimal`.
- `agents/` registers 1 subagent. The workflow scripts hold the other prompts, so those agents
cost no session context.
- Steps that write research output write it to `.sf/research/`. Git ignores this directory. The
Expand All @@ -16,7 +17,8 @@ The `skills/` directory defines the commands. Each `SKILL.md` gives the steps an

The steps below are the Claude Code path.

- `/sf:spec [REQUIREMENTS]` runs the `sf-spec` workflow. The workflow researches scope, criteria and risks in parallel, then writes `spec.md`.
- `/sf:spec [REQUIREMENTS]` runs the `sf-spec` workflow. By default, the workflow uses one
schema-checked research call for scope, criteria, and risks, then writes `spec.md`.
- `/sf:implement [SPEC_OR_PATH]` runs the Explore subagent to find patterns. Then it runs `implement-minimal` to write the code.
- `/sf:document [PATHS]` runs the `sf-document` workflow. The workflow analyzes the change, drafts the documents, then integrates them into `docs/`.

Expand All @@ -41,17 +43,19 @@ needs no registration, so every prompt the workflows own lives in the script ins

| Agent | Model | Effort |
| --- | --- | --- |
| `scope`, `criteria`, `risks` in `sf-spec` | caller/host policy | `low` |
| `research-combined` in `sf-spec` (default) | caller/host policy | `low` |
| `scope`, `criteria`, `risks` in `sf-spec` (`parallelResearch: true`) | caller/host policy | `low` |
| `artifacts`, `implementation`, `inventory` in `sf-document` | caller/host policy | `low` |
| `synthesize-spec` in `sf-spec` | the caller's | the caller's |
| `technical-docs`, `user-docs`, `integrate-docs` in `sf-document` | the caller's | the caller's |
| `implement-minimal` | the caller's | the caller's |

The six extraction agents produce short structured output from text they are given at low effort.
They inherit the caller's or host's model policy and do not require Haiku access. The others make
judgment calls about what to build and what to write. They inherit the caller's tier, because that
tier is the user's choice for the session. A named tier would downgrade a user on Opus and raise
the bill for a user on Haiku.
The default `research-combined` agent and the three analysis agents produce short structured
output from text they are given at low effort. With `parallelResearch: true`, `sf-spec` uses the
three focused structured research agents instead. They inherit the caller's or host's model
policy and do not require Haiku access. The others make judgment calls about what to build and
what to write. They inherit the caller's tier, because that tier is the user's choice for the
session. A named tier would downgrade a user on Opus and raise the bill for a user on Haiku.

`effort` accepts `low`, `medium`, `high`, `xhigh`, `max`, or an integer.

Expand Down Expand Up @@ -82,12 +86,15 @@ the `sf-` prefix for that reason.
{
"requirements": "string — what to specify",
"specPath": "string — where to write spec.md",
"templatePath": "string — the spec-template.md to follow"
"templatePath": "string — the spec-template.md to follow",
"parallelResearch": "boolean — false by default; true uses three parallel research calls"
}
```

Returns `{ specPath, criteria, risks }`, where the last two are counts. The three research
agents return validated objects, so this path writes no files under `research/`.
Returns `{ specPath, criteria, risks }`, where the last two are counts. By default, one
schema-checked research result contains the existing nested scope, criteria, and risks objects.
When `parallelResearch` is `true`, the three separate research agents return those same objects.
This path writes no files under `research/`.

### sf-document

Expand Down
12 changes: 8 additions & 4 deletions skills/spec/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: spec
description: Create specifications through parallel analysis
description: Create specifications from requirements
disable-model-invocation: true
argument-hint: "[REQUIREMENTS]"
---
Expand Down Expand Up @@ -59,8 +59,10 @@ Say: "Spec written to `{output path}`. Run `/sf:implement` to build it."

## On Claude Code

Claude Code runs steps 1 to 3 as the `sf-spec` workflow — in parallel, schema-checked, and
without the research files.
Claude Code runs one schema-checked combined research call by default, then synthesis. The
combined result supplies scope, criteria, and risks without research files. Set
`parallelResearch: true` in the workflow input to run steps 1 to 3 as three parallel research
calls.

Context arrives for free:
- Branch: !`git branch --show-current 2>/dev/null`
Expand All @@ -71,5 +73,7 @@ Context arrives for free:
- Use the **AskUserQuestion** tool for both questions above, so execution pauses for the answer.
- Bash: `bash ${CLAUDE_PLUGIN_ROOT}/scripts/spec-dir.sh $MODE ${CLAUDE_PROJECT_DIR:+$CLAUDE_PROJECT_DIR/.sf}`
- Workflow tool, `name: "sf-spec"`. Pass `args` as a JSON object, never as a string:
`{"requirements": "$ARGUMENTS", "specPath": "<the spec.md path above>", "templatePath": "${CLAUDE_SKILL_DIR}/spec-template.md"}`
`{"requirements": "$ARGUMENTS", "specPath": "<the spec.md path above>", "templatePath": "${CLAUDE_SKILL_DIR}/spec-template.md"}`.
`parallelResearch` is a boolean and defaults to `false`; set it to `true` for three parallel
research calls.
- Gate: `bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate-spec.sh ${CLAUDE_PROJECT_DIR:+$CLAUDE_PROJECT_DIR/.sf}`
30 changes: 22 additions & 8 deletions tests/integration/workflow-paths.bats
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ load "$PROJECT_ROOT/tests/helpers/assertions.bash"
HARNESS="$PROJECT_ROOT/tests/workflow-harness.mjs"
export HARNESS

# Every research agent returns a valid result, so only the args handling decides the outcome.
SPEC_AGENTS='{"scope":{"include":["a flag"],"exclude":["a config file"]},"criteria":{"criteria":["the flag works"]},"risks":{"risks":["none"]},"synthesize-spec":"written"}'
# The default combined research agent returns the existing nested result shapes.
SPEC_AGENTS='{"research-combined":{"scope":{"include":["a flag"],"exclude":["a config file"]},"criteria":{"criteria":["the flag works"]},"risks":{"risks":["none"]}},"synthesize-spec":"written"}'
export SPEC_AGENTS

# The opt-in path retains the three separate research agent results.
PARALLEL_SPEC_AGENTS='{"scope":{"include":["a flag"],"exclude":["a config file"]},"criteria":{"criteria":["the flag works"]},"risks":{"risks":["none"]},"synthesize-spec":"written"}'
export PARALLEL_SPEC_AGENTS

# The three analysis results document.js needs before it drafts anything.
DOC_ANALYSIS='"artifacts":{"requirements":["a flag"],"outcomes":["the flag works"]},"implementation":{"files":["src/index.js"],"interfaces":["greet()"]},"inventory":{"docs":[{"path":"README.md","topic":"overview"}]}'
export DOC_ANALYSIS
Expand All @@ -37,19 +41,28 @@ run_workflow() {
run node "$HARNESS" "$PROJECT_ROOT/workflows/$workflow" "$TEST_DIR/fixture.json"
}

@test "sf-spec runs every agent when args is an object" {
@test "sf-spec runs combined research by default when args is an object" {
run_workflow spec.js '{"args":{"requirements":"add a flag","specPath":"docs/spec.md","templatePath":"tpl.md"},"agents":'"$SPEC_AGENTS"'}'

[ "$status" -eq 0 ]
assert_output_contains '"specPath":"docs/spec.md"'
assert_output_contains '"agents":["scope","criteria","risks","synthesize-spec"]'
assert_output_contains '"agents":["research-combined","synthesize-spec"]'
assert_output_contains '"phases":["Research","Synthesis"]'
assert_output_contains '"agentOptions":'
assert_output_contains '"label":"research-combined","phase":"Research","schema":'
assert_output_contains '"effort":"low"},{"label":"synthesize-spec","phase":"Synthesis"'
refute_output_contains '"model":'
}

@test "sf-spec runs separate research agents when parallel research is enabled" {
run_workflow spec.js '{"args":{"requirements":"add a flag","specPath":"docs/spec.md","templatePath":"tpl.md","parallelResearch":true},"agents":'"$PARALLEL_SPEC_AGENTS"'}'

[ "$status" -eq 0 ]
assert_output_contains '"agents":["scope","criteria","risks","synthesize-spec"]'
assert_output_contains '"label":"scope","phase":"Research","schema":'
assert_output_contains '"effort":"low"},{"label":"criteria","phase":"Research","schema":'
assert_output_contains '"effort":"low"},{"label":"risks","phase":"Research","schema":'
assert_output_contains '"effort":"low"},{"label":"synthesize-spec","phase":"Synthesis"'
refute_output_contains '"model":'
}

@test "sf-spec treats a JSON-string args the same as an object" {
Expand All @@ -72,12 +85,13 @@ run_workflow() {
assert_output_contains '"agents":[]'
}

@test "sf-spec skips synthesis when a research agent returns nothing" {
# A dead subagent resolves to null, so the fixture omits the risks label.
run_workflow spec.js '{"args":{"requirements":"add a flag","specPath":"docs/spec.md","templatePath":"tpl.md"},"agents":{"scope":{"include":["a flag"],"exclude":[]},"criteria":{"criteria":["it works"]}}}'
@test "sf-spec skips synthesis when combined research returns nothing" {
# A dead subagent resolves to null, so the fixture omits the combined research label.
run_workflow spec.js '{"args":{"requirements":"add a flag","specPath":"docs/spec.md","templatePath":"tpl.md"},"agents":{"synthesize-spec":"written"}}'

[ "$status" -eq 0 ]
assert_output_contains 'Research incomplete'
assert_output_contains '"agents":["research-combined"]'
refute_output_contains 'synthesize-spec'
}

Expand Down
71 changes: 49 additions & 22 deletions workflows/spec.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export const meta = {
name: 'sf-spec',
description: 'Spec First: research scope, criteria and risks in parallel, then write spec.md',
description: 'Spec First: research scope, criteria and risks, then write spec.md',
whenToUse: 'The /sf:spec skill calls this. Do not call it directly.',
phases: [
{ title: 'Research', detail: 'scope, criteria and risks at the same time' },
{ title: 'Research', detail: 'structured scope, criteria and risks research' },
{ title: 'Synthesis', detail: 'merge the research into spec.md' },
],
}
Expand Down Expand Up @@ -35,38 +35,65 @@ const RISKS = {
},
}

const RESEARCH = {
type: 'object',
required: ['scope', 'criteria', 'risks'],
properties: {
scope: SCOPE,
criteria: CRITERIA,
risks: RISKS,
},
}

// The caller is a model, so args may arrive as a JSON string instead of an object.
const input = typeof args === 'string' ? JSON.parse(args) : (args ?? {})
const requirements = input.requirements
const specPath = input.specPath
const templatePath = input.templatePath
const parallelResearch = input.parallelResearch === true

if (!requirements || !specPath || !templatePath) {
return { error: 'sf-spec needs requirements, specPath and templatePath in args.' }
}

phase('Research')

const [scope, criteria, risks] = await parallel([
() => agent(
`Define the narrowest viable scope for these requirements.\n\nRequirements: ${requirements}\n\n` +
`${RULES}\nExclude everything that is not needed now. Challenge feature creep. If a ` +
`requirement is unclear, exclude it.`,
{ label: 'scope', phase: 'Research', schema: SCOPE, effort: 'low' },
),
() => agent(
`Write the simplest testable pass/fail conditions for these requirements.\n\n` +
`Requirements: ${requirements}\n\n${RULES}\nNo enterprise metrics unless the requirements ` +
`ask for them. "It works" is better than "it is optimal".`,
{ label: 'criteria', phase: 'Research', schema: CRITERIA, effort: 'low' },
),
() => agent(
`Identify the blockers for these requirements.\n\nRequirements: ${requirements}\n\n` +
`${RULES}\nBlockers only, not every possible risk. Challenge assumptions. Essential edge ` +
`cases only.`,
{ label: 'risks', phase: 'Research', schema: RISKS, effort: 'low' },
),
])
let scope
let criteria
let risks

if (parallelResearch) {
[scope, criteria, risks] = await parallel([
() => agent(
`Define the narrowest viable scope for these requirements.\n\nRequirements: ${requirements}\n\n` +
`${RULES}\nExclude everything that is not needed now. Challenge feature creep. If a ` +
`requirement is unclear, exclude it.`,
{ label: 'scope', phase: 'Research', schema: SCOPE, effort: 'low' },
),
() => agent(
`Write the simplest testable pass/fail conditions for these requirements.\n\n` +
`Requirements: ${requirements}\n\n${RULES}\nNo enterprise metrics unless the requirements ` +
`ask for them. "It works" is better than "it is optimal".`,
{ label: 'criteria', phase: 'Research', schema: CRITERIA, effort: 'low' },
),
() => agent(
`Identify the blockers for these requirements.\n\nRequirements: ${requirements}\n\n` +
`${RULES}\nBlockers only, not every possible risk. Challenge assumptions. Essential edge ` +
`cases only.`,
{ label: 'risks', phase: 'Research', schema: RISKS, effort: 'low' },
),
])
} else {
const research = await agent(
`Define the narrowest viable scope, simplest testable pass/fail conditions, and blockers for ` +
`these requirements.\n\nRequirements: ${requirements}\n\n${RULES}\nExclude everything that ` +
`is not needed now. Challenge feature creep and assumptions. If a requirement is unclear, ` +
`exclude it. Blockers only, not every possible risk. Essential edge cases only. No enterprise ` +
`metrics unless the requirements ask for them. "It works" is better than "it is optimal".`,
{ label: 'research-combined', phase: 'Research', schema: RESEARCH, effort: 'low' },
);
({ scope, criteria, risks } = research ?? {})
}

if (!scope || !criteria || !risks) {
return { error: 'Research incomplete. Rerun /sf:spec.' }
Expand Down