start
Initialize SDD pipeline on current branch or create a working branch. Flexible branch handling. Fetches Jira, sets up .specwork state and source. Does not write the spec — run /sdd:spec next to draft
Files3
View on GitHubSKILL.mdskills/start/SKILL.md
GitHubStart
Initialize the SDD pipeline on the current branch or create a new working branch.
/sdd:start PROJ-123
/sdd:start "fix duplicate leads when applicationId is null"
Runs /init to bootstrap the project's CLAUDE.md only when none exists, then pre-flight checks, fetches Jira through direct REST access when configured, creates or reuses the working branch, initializes .specwork/, captures the source, and writes the compact machine-readable state. It does not write the spec — drafting spec.md (and running triage on it) is /sdd:spec's job. After /sdd:start, run /sdd:spec.
Execution
| Step | Action |
|---|---|
| 0 | Existing-pipeline check — classify whatever is in .specwork/ (active here / merged leftovers / another branch's in-flight work) and stop with the matching guidance instead of re-initializing (see Step 0 below) |
| 0.5 | Required-input guard — if no pipeline is active and no ticket/description was given, stop with no writes (see Step 0.5 below) |
| 0.6 | If the project has no CLAUDE.md (repo root or .claude/), run /init to bootstrap it before branching; skip otherwise (see Step 0.6 below) |
| 1 | Detect current branch; apply flexible pre-flight checks |
| 2 | Classify input: Jira ticket or free-text |
| 3 | If the input is a Jira key: fetch via direct REST (curl + ${CLAUDE_PLUGIN_ROOT}/lib/jira.sh) — hard-stop with no artifacts if Jira is unconfigured or unreachable (see Step 3 below). Free-text input skips Jira entirely. |
| 4 | Suggest and confirm branch handling |
| 5 | Create a new feature branch or keep the current branch |
| 6 | Initialize .specwork/ folders: _spec/, _state/, _progress/ (working execution memory) |
| 7 | Load persistent rules from ${CLAUDE_PLUGIN_ROOT}/AGENTS.md and ./.claude/service-rules.md in the active project (if present) |
| 8 | Run python3 ${CLAUDE_PLUGIN_ROOT}/lib/start.py to generate rules.json, cache, state.json, and source.md in one call (it also gitignores .specwork/) |
| 9 | Tell the user to run /sdd:spec to draft spec.md from the captured source and run triage |
Step 0 — Existing-Pipeline Check
Before initializing anything, check whether a pipeline is already active here:
python3 ${CLAUDE_PLUGIN_ROOT}/lib/gates.py precheck
Non-zero (no pipeline state on disk at all): proceed with the steps below.
Exit code 0 (something exists in
.specwork/): do not re-initialize and do not wipe anything. Find out what exists before writing any message —.specwork/is gitignored, so it survivesgit checkoutand can easily belong to a different branch, or to work that already shipped:CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)" python3 ${CLAUDE_PLUGIN_ROOT}/lib/gates.py pipeline-inventory "$CURRENT_BRANCH"This prints one JSON object with
pipelines,orphans(pipelines this branch does not own) andclosable(orphans whose work already landed —merge_statusofmergedorbranch-gone). Never eyeball branch names to make this call; branch on the JSON. Exactly one of the three cases below applies.
Whichever case you land in, stop with no writes: /sdd:start never silently
reuses or deletes an existing .specwork/. Starting over is an explicit
/sdd:close.
Case A — this branch owns an active pipeline
Some entry has is_current: true. The user is mid-pipeline and should continue,
park, or close it. Use that entry's slug as <ACTIVE_SLUG>:
✗ There is an active pipeline for <ACTIVE_SLUG> (branch <CURRENT_BRANCH>).
/sdd:start will not start new work on top of it.
• Continue it: run /sdd:spec (draft/refine) or /sdd:state (next step)
• Park it: run /sdd:pause to stash <ACTIVE_SLUG> and resume later
• Close it: run /sdd:close (then /sdd:start for the new work)
If $ARGUMENTS clearly points at different work (a ticket key that does not
equal <ACTIVE_SLUG>, or any free-text description), lead with that mismatch so
the user understands why nothing new was started. When $ARGUMENTS is empty or
matches, the same message applies.
Case B — orphan leftovers whose work already landed
No entry is is_current, and closable is non-empty. This is the common
footgun: a feature was merged and /sdd:close was never run, so dead state sits
on disk and blocks unrelated new work. Do not offer /sdd:spec here — there
is nothing worth continuing. Name each closable entry with its reason:
✗ /sdd:start did not start anything — .specwork/ still holds a finished pipeline.
• <slug> (branch <branch>) — already merged into <base_branch>
• <slug> (branch <branch>) — branch no longer exists locally
That work already landed. Clear the leftovers, then start again:
/sdd:close # re-checks MR state, then wipes .specwork/
/sdd:start <your input>
Render one bullet per closable entry: merge_status: "merged" → "already merged
into <base_branch>"; merge_status: "branch-gone" → "branch no longer exists
locally". For a branch-gone entry, add this line — a branch rename that skipped
/sdd:resync is indistinguishable from a merged-and-deleted branch, and
/sdd:close would throw away live work:
If <branch> was renamed rather than merged, run /sdd:resync instead of /sdd:close.
merge_status is derived from git alone (is the branch an ancestor of its base?),
so it is a warning, not proof — /sdd:close still verifies the real MR state
via glab before deleting anything. Never run /sdd:close on the user's behalf
here; deleting a pipeline is their call.
Case C — orphan leftovers still in flight
No entry is is_current and closable is empty: the pipelines on disk belong to
another branch and have not landed (open, or unknown when the merge state
could not be determined). Closing them would destroy unmerged work, so point at
the owning branch instead. Use the first orphan as <slug> / <branch>:
✗ .specwork/ belongs to '<branch>' (slug '<slug>'), not '<CURRENT_BRANCH>'.
/sdd:start will not start new work on top of another branch's pipeline.
• Resume it: switch to '<branch>' and run /sdd:state
• Park it: switch to '<branch>' and run /sdd:pause, then come back here
• Renamed? run /sdd:resync if '<branch>' is this branch under an old name
Step 0.5 — Required Input Guard (HARD STOP)
/sdd:start needs a ticket key or a free-text description to start a pipeline.
With no input there is nothing to base a branch name, source, or spec on, so
do not create a branch and do not initialize .specwork/.
When Step 0 found no active pipeline and $ARGUMENTS is empty (after
trimming whitespace), stop immediately with no writes and tell the user:
✗ Nothing to start. /sdd:start needs a ticket key or a description.
/sdd:start PROJ-123
/sdd:start "fix duplicate leads when applicationId is null"
This guard only applies when there is no active pipeline — if Step 0 already found one, it stopped there and you never reach this point.
Step 0.6 — Bootstrap CLAUDE.md with /init (only when missing)
CLAUDE.md is the project's agent-memory file — the model's persistent context
throughout the pipeline. Run /init to generate it only when the project does
not already have one. /init scans the whole project and can be slow, so skip
it whenever a CLAUDE.md is already present.
First detect it. Check the repo root and the project .claude/ directory —
some projects keep CLAUDE.md under .claude/ rather than at the root:
if test -f CLAUDE.md || test -f .claude/CLAUDE.md; then echo FOUND; else echo MISSING; fi
- FOUND — skip
/initentirely; the model context is already in place. - MISSING — run
/initto generateCLAUDE.md.
When you do run it:
- Run
/initbefore branch creation so the generatedCLAUDE.mdis carried onto the new feature branch. /initis idempotent and will not overwrite an existing agent-memory file.- No pipeline artifact depends on the output of
/init— it is purely for model-context bootstrapping.
Branch Detection & Pre-flight Logic
Detect current branch and apply flexible pre-flight checks:
On any starting branch:
- Detect the current branch and use its current HEAD as the default base
- Offer a suggested working-branch name derived from the ticket or free-text input
- Allow creating a new branch from the current branch so the new branch inherits the parent branch's commits and changes
- Allow staying on the current branch when that is the better choice
If on development, develop, main, or master:
Require a clean working tree before creating a new branch, checking with agent-memory files exempted:
python3 ${CLAUDE_PLUGIN_ROOT}/lib/worktree.py dirty-files --exclude-agent-filesAbort only if that command prints anything: "Working tree has changes. Commit or stash first."
Agent-memory files (
AGENTS.md,CLAUDE.md,GEMINI.md) dropped by/initor proactive models are legitimate, version-controllable context — not feature WIP — so they must not block/sdd:start. Creating the new branch carries them onto it, where they get committed with the rest of the work. They block only when other, real changes are also present.
If staying on the current branch:
- Reuse the current branch as the working branch
- Continue with slug extraction from the active branch name
In all cases:
- If Step 0 found an active pipeline, you already stopped there — do not reach this point with an existing
.specwork/. - Never auto-delete an existing
.specwork/. Starting over is an explicit/sdd:close, not a side effect of/sdd:start.
Branch Confirmation Prompt
Always ask how to handle the working branch using a single AskUserQuestion call with two
explicit options plus the built-in "Other" field for custom names. Never issue a second follow-up
question.
Use this question format:
"Current branch: <current> | Suggested: <suggested>
Which branch should be used for this pipeline?"
Options to present:
| Label | Description |
|---|---|
<suggested> (Recommended) |
Create <suggested> from <current> HEAD |
Keep <current> |
Stay on the current branch and initialize .specwork/ here |
The tool automatically appends an "Other" free-text field — that is the custom-name path. Do not add a third predefined option for custom names; rely on "Other" instead.
Handling each answer:
- Suggested branch selected: create the suggested branch from current HEAD, switch to it, then continue to artifact generation.
- Keep current selected: stay on the current branch and continue to artifact generation.
- Other (custom text): treat the typed value as the branch name. Create that branch from
current HEAD, switch to it, then continue to artifact generation. Validate the typed name is
a valid git branch name before running
git checkout -b; if invalid, surface the error and stop.
Slug derivation (ALWAYS after branch is confirmed): The slug passed to start.py must
be derived from the final chosen branch name, not from the original description or ticket.
Strip the leading type prefix (feature/, bugfix/, hotfix/, release/) and use the
remainder as the slug. Examples: feature/test2 → test2;
feature/fix-duplicate-leads → fix-duplicate-leads; feature/PROJ-123 → PROJ-123.
This guarantees that artifact file names always match the branch name.
Suggested names follow feature/<TICKET> or feature/<slugified-description>.
Example: on feature/IR-40, starting /sdd:start IR-45 suggests feature/IR-45 so the new
branch inherits the parent's commits.
start.py Artifact Generator
After reading the rule sources, generate all boilerplate artifacts with a single call:
python3 ${CLAUDE_PLUGIN_ROOT}/lib/start.py \
--slug "<slug>" \
--ticket "<TICKET>" \
--input-type jira \
--branch "<branch>" \
--base-branch "<base>" \
--scaffold-rules-template ${CLAUDE_PLUGIN_ROOT}/templates/service-rules.md \
--rules \
${CLAUDE_PLUGIN_ROOT}/AGENTS.md \
.claude/service-rules.md \
.claude/rules \
$([ "${SDD_NON_INTERACTIVE:-0}" = "1" ] && echo "--non-interactive")
Free-text input — persist the description into source.md. When the input
is free text (not a Jira key), do not let source.md fall back to a
placeholder: the description is the only material /sdd:spec has to draft from,
so it must survive on disk (this is what makes the context survive a /sdd:auto
run that later stops at the Open Questions gate). Pass the verbatim text via
--source-body — no temp file needed:
python3 ${CLAUDE_PLUGIN_ROOT}/lib/start.py \
--slug "<slug>" \
--ticket "none" \
--input-type freetext \
--branch "<branch>" \
--base-branch "<base>" \
--source-body "<full free-text description verbatim>" \
--scaffold-rules-template ${CLAUDE_PLUGIN_ROOT}/templates/service-rules.md \
--rules \
${CLAUDE_PLUGIN_ROOT}/AGENTS.md \
.claude/service-rules.md \
.claude/rules \
$([ "${SDD_NON_INTERACTIVE:-0}" = "1" ] && echo "--non-interactive")
The Jira path doesn't need this — jira_write_issue_markdown (see below) writes
the fetched issue straight into source.md.
The script creates: state.json, rules.json, implementation-cache.json,
source.md (the --source-body contents, or a placeholder when omitted),
and an empty spec.md scaffold — a placeholder only. /sdd:start never writes
spec content; /sdd:spec drafts the canonical spec downstream skills consume. It
prints a JSON summary with the paths to each artifact and source_has_body (true when
the body was captured).
It also ensures .specwork/ is gitignored (transient pipeline state must never
be committed): it appends .specwork/ to .gitignore if missing, creating the
file when absent. The check is idempotent. If .specwork/ is already tracked
from a prior bad setup, the summary's gitignore.tracked is true and the
script prints a warning with the exact git rm -r --cached .specwork/ fix — it
never untracks files automatically.
Jira Access Without MCP
/sdd:start should use direct Jira REST access and must not depend on an MCP server.
Supported environment variables:
JIRA_BASE_URLorJIRA_URL- Jira Cloud:
JIRA_USER+JIRA_TOKEN - Jira Server/Data Center PAT:
JIRA_TOKEN+JIRA_AUTH_MODE=bearer
Preferred fetch pattern:
source ${CLAUDE_PLUGIN_ROOT}/lib/jira.sh
if jira_is_ticket_key "$TICKET" && jira_is_configured; then
jira_write_issue_markdown "$TICKET" ".specwork/_spec/<slug>-source.md"
# Preserve any supplementary free text typed alongside the ticket
# (e.g. /sdd:start PROJ-123 "watch out: only the publisher, not the consumer").
# That note is usually the highest-value, least-recoverable context — never drop it.
if [ -n "$DESC" ]; then
printf '\n## Additional context (from /sdd:start)\n\n%s\n' "$DESC" \
>> ".specwork/_spec/<slug>-source.md"
fi
fi
Here $TICKET is the leading ticket key and $DESC is any free text typed after
it. When the user passes a ticket plus a note, the note must end up in
source.md so /sdd:spec drafts from it.
When the input is a Jira ticket key, Jira access is required — do NOT fall back silently.
Jira not configured (missing
JIRA_BASE_URL/JIRA_TOKEN): stop immediately, do not create any.specwork/artifacts, and tell the user:✗ Jira is not configured. Set the following environment variables and retry: export JIRA_BASE_URL=https://<your-domain>.atlassian.net export JIRA_USER=your@email.com export JIRA_TOKEN=<personal-access-token> Run /sdd:start again once Jira is configured.Jira configured but the call fails (curl error, HTTP error, bad credentials): stop immediately, do not create any
.specwork/artifacts, and tell the user the specific error returned.
The fallback to free-text (using the ticket id as a title placeholder) is only acceptable when the input was free text to begin with — never when a Jira key was the input.
Artifacts written by /sdd:start
/sdd:start writes three state artifacts plus a source placeholder. Their schemas and example post-run output are documented in ${CLAUDE_PLUGIN_ROOT}/skills/start/REFERENCE.md:
- State file schema —
.specwork/_state/<slug>-state.jsonstructure and field semantics - Implementation cache file schema —
.specwork/_state/<slug>-implementation-cache.jsonshape - Rules file schema —
.specwork/_state/<slug>-rules.jsonshape (consumed by downstream skills instead of re-reading markdown rules) - Output examples — canonical post-
/sdd:startoutputs (new branch from development, dependent branch, current branch kept)
The spec template (templates/spec.md) and spec-writing rules belong to /sdd:spec, which drafts .specwork/_spec/<slug>-spec.md. Read the relevant section before writing each artifact.
Project-level scaffolding (handled by start.py, not by this skill in prose):
- Service rules can live either as a single
./.claude/service-rules.md(legacy) or as per-topic files under./.claude/rules/*.md(preferred — each file is one domain, auto-loaded by Claude Code).start.pyreads both:.claude/rulesis passed as a--rulessource and expands to every*.mdinside it. - The "scaffold only when neither exists" decision is enforced in code: the
--scaffold-rules-templateflag (already wired into the invocation above) makesstart.pycreate./.claude/rules/service-rules.mdfrom the template only when there is no./.claude/service-rules.mdand no./.claude/rules/*.md. Existing rules are never overwritten. The JSON summary reports the outcome underrules_scaffold.
Next Step
/sdd:start only bootstraps state — the spec does not exist yet. The next step is
always /sdd:spec:
/sdd:spec # draft spec.md from the captured source, then run triage
/sdd:spec owns the spec, the Open Questions summary, and triage classification
(it reads ## Summary + ## Behavior, which only exist once the spec is
drafted). After /sdd:spec, triage's recommended path leads into /sdd:plan /
/sdd:implement.
Related Skills
spec— drafts the spec from the source captured here; run it nextplan— optional discovery + plan step (recommended for medium/large features)implement— consumes the spec + rules (and the plan if it exists)close— clears.specwork/artifacts including_state/