diff --git a/.changeset/sturdy-sloths-hum.md b/.changeset/sturdy-sloths-hum.md new file mode 100644 index 000000000..733fd85c8 --- /dev/null +++ b/.changeset/sturdy-sloths-hum.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3250 +--- +**`/gsd-capture --seed ` is one-shot again** — Trigger / Why / Scope are optional inputs with sensible defaults instead of a mandatory pre-capture questionnaire. Users capturing a stream of ideas no longer get blocked between writes. diff --git a/get-shit-done/workflows/plant-seed.md b/get-shit-done/workflows/plant-seed.md index 41e5850d6..bed8e51ee 100644 --- a/get-shit-done/workflows/plant-seed.md +++ b/get-shit-done/workflows/plant-seed.md @@ -8,34 +8,162 @@ Seeds beat deferred items because they: - Define WHEN to surface (trigger conditions, not manual scanning) - Track breadcrumbs (code references, related decisions) - Auto-present at the right time via new-milestone scan + +**One-shot capture**: the seed file is written immediately from the idea text alone. +Trigger / Why / Scope are optional enrichment — they can be provided now or added +later. The file is never gated behind questions. - + Parse `$ARGUMENTS` for the idea summary. -If empty, ask: -``` -What's the idea? (one sentence) +First, check for an enrich flag: + +```bash +if echo "$ARGUMENTS" | grep -qE '\-\-enrich[[:space:]]+SEED-[0-9]+'; then + ENRICH_TARGET=$(echo "$ARGUMENTS" | grep -oE 'SEED-[0-9]+') + SEED_FILE=$(ls .planning/seeds/${ENRICH_TARGET}-*.md 2>/dev/null | head -1) + # Skip to enrich-seed step — do not prompt for $IDEA +else + if [ -n "$ARGUMENTS" ]; then + IDEA="$ARGUMENTS" + else + # Ask only when no arguments at all + # What's the idea? (one sentence) + IDEA="" + fi +fi ``` -Store as `$IDEA`. +If `$ENRICH_TARGET` is set, skip straight to the `enrich-seed` step. Do not set `$IDEA` and do not run `create-seed-dir`, `generate-seed-id`, `write-seed`, `collect-breadcrumbs`, `commit-seed`, or `confirm`. + +If `$ARGUMENTS` is non-empty and contains no `--enrich` flag, treat the full value as `$IDEA` (no prompt). + +Only prompt for the idea when `$ARGUMENTS` is empty and no enrich target is present. Store the response as `$IDEA`. - + ```bash mkdir -p .planning/seeds ``` - -Ask focused questions to build a complete seed: + +```bash +# Find next seed number +EXISTING=$( (ls .planning/seeds/SEED-*.md 2>/dev/null || true) | wc -l ) +NEXT=$((EXISTING + 1)) +PADDED=$(printf "%03d" $NEXT) +``` + +Generate slug from idea summary. + + + +Write `.planning/seeds/SEED-{PADDED}-{slug}.md` immediately with sensible defaults: + +- `trigger_when`: default is `"when relevant"` — the seed will surface during any + new-milestone scan; the user can narrow it later via `--enrich` +- `scope`: default is `"unknown"` — the user can update it via `--enrich` + +```markdown +--- +id: SEED-{PADDED} +status: dormant +planted: {ISO date} +planted_during: {current milestone/phase from STATE.md, or "unknown" if not in a GSD project} +trigger_when: when relevant +scope: unknown +--- + +# SEED-{PADDED}: {$IDEA} + +## Why This Matters + +_To be filled in. Run `/gsd-capture --seed --enrich SEED-{PADDED}` to add context._ + +## When to Surface + +**Trigger:** when relevant + +This seed will surface during `/gsd-new-milestone` when the milestone scope matches. + +## Scope Estimate + +**Unknown** — run `/gsd-capture --seed --enrich SEED-{PADDED}` to estimate effort. + +## Breadcrumbs + +_No breadcrumbs collected yet._ + +## Notes + +_Captured via one-shot seed capture. Enrich with trigger, why, and scope at your convenience._ +``` + + + +After writing the file, search the codebase for relevant references: + +Extract one or two key terms from `$IDEA` (the most distinctive noun or phrase) and store as `$KEYWORD`. + +```bash +# Derive a single keyword for breadcrumb search. +# Lower-case, strip punctuation, take the first token longer than 2 chars. +KEYWORD=$(printf '%s' "$IDEA" \ + | tr '[:upper:]' '[:lower:]' \ + | tr -cs 'a-z0-9' '\n' \ + | awk 'length > 2 {print; exit}') +KEYWORD="${KEYWORD:-seed}" # fallback to literal "seed" if extraction yields nothing +``` + +```bash +# Find files related to the idea keywords ($KEYWORD derived from $IDEA) +grep -rl "$KEYWORD" --include="*.ts" --include="*.js" --include="*.md" . 2>/dev/null | head -10 +``` + +Also check: +- Current STATE.md for related decisions +- ROADMAP.md for related phases +- todos/ for related captured ideas + +If any breadcrumbs are found, update the Breadcrumbs section of the seed file. +Store relevant file paths as `$BREADCRUMBS`. + + + +```bash +gsd-sdk query commit "docs: plant seed — {$IDEA}" --files .planning/seeds/SEED-{PADDED}-{slug}.md +``` + + + +```text +✅ Seed planted: SEED-{PADDED} + +"{$IDEA}" +File: .planning/seeds/SEED-{PADDED}-{slug}.md + +Trigger and scope are set to defaults. Run `/gsd-capture --seed --enrich SEED-{PADDED}` +to add trigger conditions, rationale, and scope estimate at your convenience. + +This seed will surface automatically when you run /gsd-new-milestone. +``` + + + +**Optional enrichment — only run this step when `--enrich` flag is present.** + +If `--enrich` flag is in `$ARGUMENTS`: +- `$ENRICH_TARGET` and `$SEED_FILE` are already set by `parse-idea`. Derive `$SEED_ID` from `$ENRICH_TARGET` (e.g. `SEED_ID="$ENRICH_TARGET"`). If `$SEED_FILE` is empty, fall back to the most-recently modified file in `.planning/seeds/` and set `$SEED_ID` from its filename. +- Ask focused questions to build a complete seed: **Text mode (`workflow.text_mode: true` in config or `--text` flag):** Set `TEXT_MODE=true` if `--text` is present in `$ARGUMENTS` OR `text_mode` from init JSON is `true`. When TEXT_MODE is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Gemini CLI, etc.) where `AskUserQuestion` is not available. -``` +```text AskUserQuestion( header: "Trigger", question: "When should this idea surface? (e.g., 'when we add user accounts', 'next major version', 'when performance becomes a priority')", @@ -45,7 +173,7 @@ AskUserQuestion( Store as `$TRIGGER`. -``` +```text AskUserQuestion( header: "Why", question: "Why does this matter? What problem does it solve or what opportunity does it create?", @@ -55,7 +183,7 @@ AskUserQuestion( Store as `$WHY`. -``` +```text AskUserQuestion( header: "Scope", question: "How big is this? (rough estimate)", @@ -68,105 +196,34 @@ AskUserQuestion( ``` Store as `$SCOPE`. - - -Search the codebase for relevant references: +Update the seed file's frontmatter and sections with the gathered values: +- Set `trigger_when: {$TRIGGER}` +- Set `scope: {$SCOPE}` +- Fill in `## Why This Matters` with `{$WHY}` +- Fill in `## When to Surface` trigger detail +- Fill in `## Scope Estimate` elaboration +Commit the update: ```bash -# Find files related to the idea keywords -grep -rl "$KEYWORD" --include="*.ts" --include="*.js" --include="*.md" . 2>/dev/null | head -10 +gsd-sdk query commit "docs: enrich seed ${SEED_ID} — trigger + why + scope" --files "$SEED_FILE" ``` -Also check: -- Current STATE.md for related decisions -- ROADMAP.md for related phases -- todos/ for related captured ideas - -Store relevant file paths as `$BREADCRUMBS`. - - - -```bash -# Find next seed number -EXISTING=$( (ls .planning/seeds/SEED-*.md 2>/dev/null || true) | wc -l ) -NEXT=$((EXISTING + 1)) -PADDED=$(printf "%03d" $NEXT) -``` - -Generate slug from idea summary. - - - -Write `.planning/seeds/SEED-{PADDED}-{slug}.md`: - -```markdown ---- -id: SEED-{PADDED} -status: dormant -planted: {ISO date} -planted_during: {current milestone/phase from STATE.md} -trigger_when: {$TRIGGER} -scope: {$SCOPE} ---- - -# SEED-{PADDED}: {$IDEA} - -## Why This Matters - -{$WHY} - -## When to Surface - -**Trigger:** {$TRIGGER} - -This seed should be presented during `/gsd-new-milestone` when the milestone -scope matches any of these conditions: -- {trigger condition 1} -- {trigger condition 2} - -## Scope Estimate - -**{$SCOPE}** — {elaboration based on scope choice} - -## Breadcrumbs - -Related code and decisions found in the current codebase: - -{list of $BREADCRUMBS with file paths} - -## Notes - -{any additional context from the current session} -``` - - - -```bash -gsd-sdk query commit "docs: plant seed — {$IDEA}" --files .planning/seeds/SEED-{PADDED}-{slug}.md -``` - - - -``` -✅ Seed planted: SEED-{PADDED} - -"{$IDEA}" +Confirm: +```text +✅ Seed enriched: ${SEED_ID} Trigger: {$TRIGGER} Scope: {$SCOPE} -File: .planning/seeds/SEED-{PADDED}-{slug}.md - -This seed will surface automatically when you run /gsd-new-milestone -and the milestone scope matches the trigger condition. ``` -- [ ] Seed file created in .planning/seeds/ -- [ ] Frontmatter includes status, trigger, scope -- [ ] Breadcrumbs collected from codebase +- [ ] Seed file created in .planning/seeds/ in one step, no questions required +- [ ] Frontmatter includes status, trigger_when (default: "when relevant"), scope (default: "unknown") +- [ ] File is written BEFORE any optional enrichment questions are asked - [ ] Committed to git -- [ ] User shown confirmation with trigger info +- [ ] User shown confirmation with file path +- [ ] Optional --enrich path available for adding trigger, why, scope post-capture diff --git a/tests/bug-3236-capture-seed-one-shot.test.cjs b/tests/bug-3236-capture-seed-one-shot.test.cjs new file mode 100644 index 000000000..a8310e626 --- /dev/null +++ b/tests/bug-3236-capture-seed-one-shot.test.cjs @@ -0,0 +1,165 @@ +// allow-test-rule: source-text-is-the-product — workflow and command .md files +// ARE what the runtime loads; asserting their existence and behavioral content +// tests the deployed skill surface contract, not implementation internals. + +'use strict'; + +// Regression tests for bug #3236. +// +// The `plant-seed.md` workflow gained a mandatory Trigger / Why / Scope +// questionnaire (gather_context step) that blocks before the seed file is +// written. Users capturing a stream of ideas lose flow because the AI must +// receive three answers before a single write happens. +// +// Fix: the seed file must be written FIRST (one-shot), with sensible defaults +// for Trigger / Why / Scope. The enrichment questions must be optional and must +// come AFTER the file is written, not before. +// +// Behavioral contract tested here: +// 1. The `write-seed` step exists and comes BEFORE any AskUserQuestion for +// Trigger / Why / Scope enrichment. +// 2. The workflow provides sensible defaults for trigger_when and scope when +// the user supplies only the idea summary. +// 3. The AskUserQuestion calls for Trigger / Why / Scope still exist (optional +// enrichment path preserved) but are gated after the file is written. + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const ROOT = path.join(__dirname, '..'); +const PLANT_SEED = path.join(ROOT, 'get-shit-done', 'workflows', 'plant-seed.md'); + +// ── helpers ─────────────────────────────────────────────────────────────────── + +function readPlantSeed() { + try { + return fs.readFileSync(PLANT_SEED, 'utf8'); + } catch (err) { + throw new Error('get-shit-done/workflows/plant-seed.md not found: ' + err.message); + } +} + +/** + * Extract step names in document order from workflow XML. + */ +function extractStepNames(src) { + const names = []; + const re = / { + test('plant-seed.md exists', () => { + assert.ok( + fs.existsSync(PLANT_SEED), + 'get-shit-done/workflows/plant-seed.md does not exist', + ); + }); + + test('write-seed step exists (hyphenated name per CONTEXT.md rule)', () => { + const src = readPlantSeed(); + const steps = extractStepNames(src); + assert.ok( + steps.includes('write-seed'), + 'plant-seed.md must contain a (hyphens, not underscores); found: ' + JSON.stringify(steps), + ); + }); + + test('write-seed step appears before Trigger AskUserQuestion', () => { + const src = readPlantSeed(); + const writeOff = stepOffset(src, 'write-seed'); + assert.ok(writeOff !== -1, 'write-seed step must exist'); + const triggerOff = askQuestionOffset(src, 'Trigger'); + if (triggerOff === -1) return; // fully optional — no question at all is fine + assert.ok( + writeOff < triggerOff, + 'write-seed (offset ' + writeOff + ') must precede Trigger question (offset ' + triggerOff + ')', + ); + }); + + test('write-seed step appears before Why AskUserQuestion', () => { + const src = readPlantSeed(); + const writeOff = stepOffset(src, 'write-seed'); + assert.ok(writeOff !== -1, 'write-seed step must exist'); + const whyOff = askQuestionOffset(src, 'Why'); + if (whyOff === -1) return; + assert.ok( + writeOff < whyOff, + 'write-seed (offset ' + writeOff + ') must precede Why question (offset ' + whyOff + ')', + ); + }); + + test('write-seed step appears before Scope AskUserQuestion', () => { + const src = readPlantSeed(); + const writeOff = stepOffset(src, 'write-seed'); + assert.ok(writeOff !== -1, 'write-seed step must exist'); + const scopeOff = askQuestionOffset(src, 'Scope'); + if (scopeOff === -1) return; + assert.ok( + writeOff < scopeOff, + 'write-seed (offset ' + writeOff + ') must precede Scope question (offset ' + scopeOff + ')', + ); + }); + + test('workflow documents a default value for trigger_when', () => { + const src = readPlantSeed(); + assert.ok( + /trigger_when.*default|default.*trigger|when relevant|when scope matches|unspecified/i.test(src), + 'plant-seed.md must document a default for trigger_when when the user provides no Trigger', + ); + }); + + test('workflow documents a default value for scope', () => { + const src = readPlantSeed(); + assert.ok( + /scope.*default|default.*scope|scope.*unknown|unknown|scope.*unspecified|unspecified/i.test(src), + 'plant-seed.md must document a default for scope when the user provides no Scope', + ); + }); + + test('write-seed step precedes enrich/gather step in step order', () => { + const src = readPlantSeed(); + const steps = extractStepNames(src); + const writeIdx = steps.findIndex((s) => s === 'write-seed' || s === 'write_seed'); + const gatherIdx = steps.findIndex( + (s) => s === 'gather-context' || s === 'gather_context' || s === 'enrich-seed' || s === 'enrich_seed', + ); + if (gatherIdx === -1 || writeIdx === -1) return; // no gather step — one-shot only path, fine + assert.ok( + writeIdx < gatherIdx, + 'write-seed (step index ' + writeIdx + ') must appear before gather/enrich (index ' + gatherIdx + ') — capture first, enrich later', + ); + }); + + test('optional enrichment path is preserved', () => { + const src = readPlantSeed(); + const hasEnrichStep = /enrich|gather.context|gather_context/i.test(src); + const hasTriggerQuestion = src.includes('header: "Trigger"'); + assert.ok( + hasEnrichStep || hasTriggerQuestion, + 'plant-seed.md must preserve an optional enrichment path (enrich step or Trigger/Why/Scope AskUserQuestion)', + ); + }); +});