go-dispatcher-spec-decomposition
The bug (GAP-007): DecomposeSpec only recognized ## Phase / ## Feature H2 headings as task sections, but the repo's own specs use # Helix Feature N H1 titles whose H2 sections (## Overview, ## Requirements, ## Acceptance Criteria) contain neither keyword — so helix dispatcher list-tasks --spec specs/*.md found zero sections and errored on every spec.
The fix (in decompose.go) adds one case-insensitive H1 prefix matcher and one switch branch; existing H2 detection and the ErrNoSections path are untouched:
var (
// h2Section matches existing "## Phase N" / "## Feature X" H2 headings.
h2Section = regexp.MustCompile(`(?i)^##\s+.*(?:phase|feature).*$`)
// h1Feature matches the repo's "# Helix Feature N" H1 title convention
// (case-insensitive, e.g. "# HELIX FEATURE 1 — Persistent Job Queue").
h1Feature = regexp.MustCompile(`(?i)^#\s+helix\s+feature\b`)
)
func DecomposeSpec(markdown string) ([]Task, error) {
var tasks []Task
var cur *Task
start := func(heading string) {
if cur != nil {
cur.Body = strings.TrimSpace(cur.Body)
tasks = append(tasks, *cur)
}
cur = &Task{Title: heading, Priority: len(tasks) + 1}
}
for _, line := range strings.Split(markdown, "\n") {
trimmed := strings.TrimSpace(line)
switch {
case h1Feature.MatchString(trimmed):
start(strings.TrimSpace(trimmed[1:])) // drop leading '#'
case h2Section.MatchString(trimmed):
start(strings.TrimSpace(trimmed[2:])) // drop leading '##'
default:
if cur != nil {
cur.Body += line + "\n"
}
}
}
if cur != nil {
cur.Body = strings.TrimSpace(cur.Body)
tasks = append(tasks, *cur)
}
if len(tasks) == 0 {
return nil, ErrNoSections
}
return tasks, nil
}
Semantics: each # Helix Feature… H1 (matched case-insensitively, word-boundary so # Helix Features doesn't match) starts one task; priority is the 1-based document order; H1 and H2 markers can be mixed and interleave in order; content before the first marker is skipped; original heading case is preserved in Task.Title.
Repo: ~/helix-dispatcher — decompose.go (fix, 68 lines incl. docs), cmd/helix/main.go (CLI), specs/feature-1.md … feature-6.md (the 6 repo-convention specs), decompose_test.go (3 existing-behavior tests + 1 new test).
**New test** `TestDecomposeSpecHelixFeatureH1` (1 new test function; existing tests untouched) covers: `# HELIX FEATURE 1` all-caps matching, title casing preserved, priority-by-order (1, 2), body split between sections, mixed `# Helix Feature` H1 + `## Phase` H2 order preservation, and a live loop asserting all 6 `specs/*.md` decompose to exactly 1 task each. ``` go test -race -count=1 ./... → ok helix 1.013s PASS TestDecomposeSpecH2Phase / TestDecomposeSpecH2Feature / TestDecomposeSpecNoSections PASS TestDecomposeSpecHelixFeatureH1 ``` **Live CLI run of the originally failing command** — all 6 feature specs now decompose (previously: `helix: … spec contains no task sections` per file): ``` $ helix dispatcher list-tasks --spec specs/*.md specs/feature-1.md: 1 task(s) [1] Helix Feature 1 — Persistent Job Queue specs/feature-2.md: 1 task(s) [1] Helix Feature 2 — Retry with Backoff specs/feature-3.md: 1 task(s) [1] Helix Feature 3 — Priority Scheduling specs/feature-4.md: 1 task(s) [1] Helix Feature 4 — Cancellation API specs/feature-5.md: 1 task(s) [1] Helix Feature 5 — Observability Metrics specs/feature-6.md: 1 task(s) [1] Helix Feature 6 — Plugin Sandbox ``` **Edge cases tested:** - No-section doc → `ErrNoSections`, `tasks == nil`, CLI exits 1 with `helix: no-sections.md: spec contains no task sections` (error path preserved). - `# HELIX FEATURE` (all caps, no number) → matches, exit 0. - `# Helix Features` (plural) → correctly NOT a marker, exit 1 (word boundary). - Quoted glob `--spec "specs/*.md"` → expanded by CLI, works. - Existing `## Phase 1 — Design` / `## Feature: Retry` H2 decomposition unchanged.
{"model": "deepseek-v4-flash", "problem_class": "go-dispatcher-spec-decomposition", "result": "passed", "tests": 4}The bug (GAP-007): DecomposeSpec only recognized ## Phase / ## Feature H2 headings as task sections, but the repo's own specs use # Helix Feature N H1 titles whose H2 sections (## Overview, ## Requirements, ## Acceptance Criteria) contain neither keyword — so helix dispatcher list-tasks --spec specs/*.md found zero sections and errored on every spec.
The fix (in decompose.go) adds one case-insensitive H1 prefix matcher and one switch branch; existing H2 detection and the ErrNoSections path are untouched:
var (
// h2Section matches existing "## Phase N" / "## Feature X" H2 headings.
h2Section = regexp.MustCompile(`(?i)^##\s+.*(?:phase|feature).*$`)
// h1Feature matches the repo's "# Helix Feature N" H1 title convention
// (case-insensitive, e.g. "# HELIX FEATURE 1 — Persistent Job Queue").
h1Feature = regexp.MustCompile(`(?i)^#\s+helix\s+feature\b`)
)
func DecomposeSpec(markdown string) ([]Task, error) {
var tasks []Task
var cur *Task
start := func(heading string) {
if cur != nil {
cur.Body = strings.TrimSpace(cur.Body)
tasks = append(tasks, *cur)
}
cur = &Task{Title: heading, Priority: len(tasks) + 1}
}
for _, line := range strings.Split(markdown, "\n") {
trimmed := strings.TrimSpace(line)
switch {
case h1Feature.MatchString(trimmed):
start(strings.TrimSpace(trimmed[1:])) // drop leading '#'
case h2Section.MatchString(trimmed):
start(strings.TrimSpace(trimmed[2:])) // drop leading '##'
default:
if cur != nil {
cur.Body += line + "\n"
}
}
}
if cur != nil {
cur.Body = strings.TrimSpace(cur.Body)
tasks = append(tasks, *cur)
}
if len(tasks) == 0 {
return nil, ErrNoSections
}
return tasks, nil
}
Semantics: each # Helix Feature… H1 (matched case-insensitively, word-boundary so # Helix Features doesn't match) starts one task; priority is the 1-based document order; H1 and H2 markers can be mixed and interleave in order; content before the first marker is skipped; original heading case is preserved in Task.Title.
Repo: ~/helix-dispatcher — decompose.go (fix, 68 lines incl. docs), cmd/helix/main.go (CLI), specs/feature-1.md … feature-6.md (the 6 repo-convention specs), decompose_test.go (3 existing-behavior tests + 1 new test).
**New test** `TestDecomposeSpecHelixFeatureH1` (1 new test function; existing tests untouched) covers: `# HELIX FEATURE 1` all-caps matching, title casing preserved, priority-by-order (1, 2), body split between sections, mixed `# Helix Feature` H1 + `## Phase` H2 order preservation, and a live loop asserting all 6 `specs/*.md` decompose to exactly 1 task each. ``` go test -race -count=1 ./... → ok helix 1.013s PASS TestDecomposeSpecH2Phase / TestDecomposeSpecH2Feature / TestDecomposeSpecNoSections PASS TestDecomposeSpecHelixFeatureH1 ``` **Live CLI run of the originally failing command** — all 6 feature specs now decompose (previously: `helix: … spec contains no task sections` per file): ``` $ helix dispatcher list-tasks --spec specs/*.md specs/feature-1.md: 1 task(s) [1] Helix Feature 1 — Persistent Job Queue specs/feature-2.md: 1 task(s) [1] Helix Feature 2 — Retry with Backoff specs/feature-3.md: 1 task(s) [1] Helix Feature 3 — Priority Scheduling specs/feature-4.md: 1 task(s) [1] Helix Feature 4 — Cancellation API specs/feature-5.md: 1 task(s) [1] Helix Feature 5 — Observability Metrics specs/feature-6.md: 1 task(s) [1] Helix Feature 6 — Plugin Sandbox ``` **Edge cases tested:** - No-section doc → `ErrNoSections`, `tasks == nil`, CLI exits 1 with `helix: no-sections.md: spec contains no task sections` (error path preserved). - `# HELIX FEATURE` (all caps, no number) → matches, exit 0. - `# Helix Features` (plural) → correctly NOT a marker, exit 1 (word boundary). - Quoted glob `--spec "specs/*.md"` → expanded by CLI, works. - Existing `## Phase 1 — Design` / `## Feature: Retry` H2 decomposition unchanged.
{"model": "deepseek-v4-flash", "problem_class": "go-dispatcher-spec-decomposition", "result": "passed", "tests": 4}