* test(#4378): regression tests for collision-free seed ids * fix(#4378): mint collision-free SEED-YYMMDD-xxx ids, not a shared count plant-seed derived the next seed id from 'ls .planning/seeds/SEED-*.md | wc -l'. .planning/seeds/ is shared but each worktree only sees what has merged, so two workstreams planting before either merges computed the same id and git merged both files silently. The id is now the local date plus a 3-char random base36 suffix -- the shape .planning/quick/ already uses -- computed from knowledge one worktree has alone, with a same-day regen guard. deriveSeedIdentity learns the new canonical grammar alongside legacy SEED-NNN (whose parsing never changes), the --enrich parser and the filename-prefix fallback keep the full new-format id, and the docs that state the filename shape move to it. The prefix fallback previously truncated any non-pure-numeric id at 'SEED-<digits>' -- the same one-id-two-answers ambiguity the issue reports, reproduced one level down. * fix(#4378): harden seed id generation per adversarial review - parse-idea: anchor the --enrich extractor to the flag and capture the complete id, uppercase-tolerant; a leftmost 'SEED-[0-9]+' truncated an uppercase or malformed suffix to its date and enriched an arbitrary same-day seed via head -1. Ambiguous and unmatched targets now fail closed instead. - generate-seed-id: tolerate the expected SIGPIPE under pipefail, abort loudly when the suffix cannot be drawn (an empty suffix would collapse every seed's id to the bare date), and run the same-day regen guard as a find existence test (the 'ls <glob>' shape trips the #3409 drift guard and degenerates under a stray nullglob). - deriveSeedIdentity: document the theoretical legacy/new grammar ambiguity (6-digit counter + 3-char base36 slug, no frontmatter). - changeset: state the residual same-day collision bound instead of implying zero. Emitted-Drift-Ack-Growth: plant-seed.md — the counting step became hardened date+random generation with explicit failure modes; growth is the failure handling, not duplicated logic * fix(#4378): address standards and spec review findings - tests: move the allow-test-rule marker to its suppression site (the file-header placement was inert per CONTRIBUTING site-scoping); add width-boundary coverage (5/7-digit dates, 2/4-char suffixes pin the documented branch behavior); add a writer-to-reader parity property that parses the mint widths out of the shipped workflow so the two grammar owners cannot drift; cover uppercase ids end-to-end in the reader. - plant-seed.md: draw/retry restructured as one loop with a loud terminal failure; SEED_SUFX renamed SEED_SUFFIX; regen guard drops the redundant head -1; the ambiguity error no longer advises an impossible 'complete id' for duplicate legacy ids. - commands.cts: refresh the cmdListSeeds comment still describing SEED-NNN as the only canonical form. - changeset: drop the audit claim the spec axis showed to be an overstatement (audit's id display is filename-derived, pre-existing). - remove a stray untracked artifact file swept into the tree. * test(#4378): correct boundary expectations to the module's real branch behavior The first matrix run on the boundary tests caught my hand-trace of the regex branches, not a module defect: the slug regex's alternation backtracks to the legacy branch whenever the canonical branch cannot complete (so the slug is the remainder after the legacy numeric prefix), and the 7-digit case fails the canonical branch at its 7th digit before the dash. Pin the verified values. * docs(#4378): backfill changeset PR number * fix(#4378): audit seed identity uses the canonical grammar Review of this PR found the audit surface publishing a fused filename stem (SEED-081-region for SEED-081-region.md) where list-seeds reports the canonical id -- one id, two answers across surfaces, the same ambiguity class the issue files. scanSeeds now derives identity through the SAME deriveSeedIdentity the list-seeds gate uses (frontmatter id, then filename id-prefix, then stem), and audit-open acknowledge resolves --seed-id by scanning for the derived identity, falling back to the literal stem so callers scripted against pre-canonical output keep working. Roll-in per the fix-inline rule: found during this PR's review, same seed-identity seam. RED probe: pre-fix audit published seed_id SEED-081-region-becomes / slug 081-region-becomes for a legacy seeded file; post-fix SEED-081 / region-becomes, matching list-seeds. * test(#4378): probe timeout uses the class norm after windows-lane timeout The windows conformance shard failed its bounded sh -c probes at the local 5000ms bound (cold sh.exe spawn under shard load) while the identical code passed this PR's two earlier windows waves. The probe now uses PROBE_TIMEOUT_MS from the class-norm module instead of a local override, per the helpers/timeouts.cjs convention. --------- Co-authored-by: sim <sim@local>
12 KiB
Seeds beat deferred items because they:
- Preserve WHY the idea matters (not just WHAT)
- 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.First, check for an enrich flag:
if echo "$ARGUMENTS" | grep -qE '\-\-enrich[[:space:]]+SEED-[0-9]+(-[a-zA-Z0-9]{3})?'; then
# Anchor on the flag and capture the COMPLETE id — uppercase-tolerant, since
# the docs display SEED-YYMMDD-XXX. A leftmost `SEED-[0-9]+` would truncate
# an uppercase or malformed suffix to its date and enrich an arbitrary
# same-day seed (#4378 review).
ENRICH_MATCH=$(echo "$ARGUMENTS" | grep -oE '\-\-enrich[[:space:]]+SEED-[0-9]+(-[a-zA-Z0-9]{3})?' | head -1)
ENRICH_TARGET=${ENRICH_MATCH##*[[:space:]]}
SEED_FILE=$(ls .planning/seeds/${ENRICH_TARGET}-*.md 2>/dev/null) || true
if [ -z "$SEED_FILE" ]; then
echo "ERROR: no seed file matches '$ENRICH_TARGET' in .planning/seeds/." >&2
exit 1
fi
if [ "$(printf '%s\n' "$SEED_FILE" | grep -c .)" -gt 1 ]; then
echo "ERROR: '$ENRICH_TARGET' matches multiple seed files — duplicate legacy ids cannot be disambiguated by a longer id, so rename the duplicates; for new-format ids re-run with the complete id:" >&2
printf '%s\n' "$SEED_FILE" >&2
exit 1
fi
# 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="<user response>"
fi
fi
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.
Generate slug from idea summary.
Write `.planning/seeds/{SEED_ID}-{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--enrichscope: default is"unknown"— the user can update it via--enrich
---
id: {SEED_ID}
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_ID}: {$IDEA}
## Why This Matters
_To be filled in. Run `/gsd:capture --seed --enrich {SEED_ID}` 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_ID}` to estimate effort.
## Breadcrumbs
_No breadcrumbs collected yet._
## Notes
_Captured via one-shot seed capture. Enrich with trigger, why, and scope at your convenience._
Extract one or two key terms from $IDEA (the most distinctive noun or phrase) and store as $KEYWORD.
# 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
# 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.
If response_language is set: All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations — MUST be presented in {response_language}. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated.
"{$IDEA}" File: .planning/seeds/{SEED_ID}-{slug}.md
Trigger and scope are set to defaults. Run /gsd:capture --seed --enrich {SEED_ID}
to add trigger conditions, rationale, and scope estimate at your convenience.
This seed will surface automatically when you run /gsd:new-milestone.
</step>
<step name="enrich-seed">
**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, `parse-idea` has already failed closed (no seed matches the id), so this step is never reached with an unresolved target.
- 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, Antigravity, 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')",
options: [] // freeform
)
Store as $TRIGGER.
AskUserQuestion(
header: "Why",
question: "Why does this matter? What problem does it solve or what opportunity does it create?",
options: []
)
Store as $WHY.
AskUserQuestion(
header: "Scope",
question: "How big is this? (rough estimate)",
options: [
{ label: "Small", description: "A few hours — could be a quick task" },
{ label: "Medium", description: "A phase or two — needs planning" },
{ label: "Large", description: "A full milestone — significant effort" }
]
)
Store as $SCOPE.
Update the seed file's frontmatter and sections with the gathered values:
- Set
trigger_when: {$TRIGGER} - Set
scope: {$SCOPE} - Fill in
## Why This Matterswith{$WHY} - Fill in
## When to Surfacetrigger detail - Fill in
## Scope Estimateelaboration
Commit the update:
gsd_run query commit "docs: enrich seed ${SEED_ID} — trigger + why + scope" --files "$SEED_FILE"
Confirm:
✅ Seed enriched: ${SEED_ID}
Trigger: {$TRIGGER}
Scope: {$SCOPE}
<success_criteria>
- 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 file path
- Optional --enrich path available for adding trigger, why, scope post-capture </success_criteria>