Skip to content

Commit 3e29f56

Browse files
committed
feat: add task_header_patterns config option with preset/regex support
adds task_header_patterns config option (comma-separated list of preset names or raw Go regexes) that controls which headers the plan parser recognizes as task sections. supports built-in presets (default, openspec) and arbitrary raw regexes for custom plan formats. - preset registry in pkg/plan/presets.go with DefaultHeaderPatterns(), ResolveHeaderPattern(), ResolveHeaderPatterns(), PresetDescription() - ParsePlan/ParsePlanFile accept []*regexp.Regexp; nil/empty falls back to DefaultHeaderPatterns() - {{TASK_HEADER_PATTERNS}} template variable expands to human-readable descriptions of configured patterns in task.txt - raw regexes validated at config load time via regexp.Compile so users get a clear error instead of a silent fallback to defaults - web dashboard plan panel updated to pass patterns from config - docs and embedded config template updated
1 parent fb35726 commit 3e29f56

31 files changed

Lines changed: 2296 additions & 198 deletions

.golangci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ linters:
141141
- noctx
142142
- usestdlibvars
143143
path: _test\.go$
144+
- linters:
145+
- gosec
146+
text: "G704: SSRF via taint analysis"
147+
path: _test\.go$
144148
- linters:
145149
- errcheck
146150
text: "Error return value of .*.Close.*is not checked"

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ GOOS=windows GOARCH=amd64 go build ./...
291291
- `session_timeout` config option: per-session timeout for claude (e.g., "30m", "1h"). Kills hanging sessions and continues to next iteration. CLI flag `--session-timeout` takes precedence. Disabled by default
292292
- `idle_timeout` config option: kills claude sessions when no output for specified duration (e.g., "5m"). Resets on each output line, only fires when session goes silent. CLI flag `--idle-timeout` takes precedence. Disabled by default
293293
- `move_plan_on_completion` config option: controls whether completed plans move to `docs/plans/completed/` on success. Default `true`. Disable for workflows that manage plan lifecycle externally (spec-driven tooling with separate archive steps)
294+
- `task_header_patterns` config option: comma-separated list of preset names (`default`, `openspec`) or raw Go regexes controlling which headers the plan parser recognizes as task sections. Capture group 1 = task id (required), capture group 2 = title (optional). Default: `default` (matches `### Task N: title` and `### Iteration N: title`). Use `openspec` or a raw regex for spec-driven workflows that use different header shapes. Resolved via `plan.ResolveHeaderPatterns` in `pkg/plan/presets.go`. Key files: `pkg/plan/presets.go` (preset registry, `ResolveHeaderPattern`, `ResolveHeaderPatterns`, `DefaultHeaderPatterns`, `PresetDescription`); `pkg/plan/parse.go` (`ParsePlan(content string, patterns []*regexp.Regexp)`, `ParsePlanFile(path string, patterns []*regexp.Regexp)` — callers resolve `[]string` config values via `plan.ResolveHeaderPatterns` before passing; `nil`/empty slice falls back to `DefaultHeaderPatterns()`)
294295

295296
### Local Project Config (.ralphex/)
296297

@@ -372,6 +373,7 @@ Implementation:
372373
- `{{DEFAULT_BRANCH}}` - detected default branch (main, master, origin/main, etc.), overridable via `--base-ref` CLI flag or `default_branch` config option
373374
- `{{DIFF_INSTRUCTION}}` - git diff command for current iteration (first: `git diff main...HEAD`, subsequent: `git diff`)
374375
- `{{PREVIOUS_REVIEW_CONTEXT}}` - previous review context block for external review iterations (empty on first iteration, formatted context on subsequent)
376+
- `{{TASK_HEADER_PATTERNS}}` - human-readable descriptions of configured `task_header_patterns` (preset descriptions or raw regex patterns, quoted, `or`-joined; used in `task.txt`)
375377
- `{{agent:name}}` - expands to Task tool instructions for the named agent
376378

377379
Variables are also expanded inside agent content, so custom agents can use `{{DEFAULT_BRANCH}}` etc.

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,21 @@ Set `move_plan_on_completion = false` in `~/.config/ralphex/config` or `.ralphex
172172

173173
**When to disable:** workflows that manage plan file lifecycle externally (e.g. spec-driven tooling where the plan lives inside a bundle that a separate archive step consumes) should opt out so ralphex doesn't fight the external tool's file layout.
174174

175+
### Plan Header Patterns (optional)
176+
177+
By default, ralphex recognizes `### Task N: ...` and `### Iteration N: ...` as task section headers. Set `task_header_patterns` to a comma-separated list of preset names or raw Go regexes to support alternative plan formats (e.g. OpenSpec-style `## 1. Phase Name`). Each entry is either a known preset (`default`, `openspec`) or a raw Go regexp where capture group 1 is the task id and capture group 2 is the optional title.
178+
179+
```ini
180+
# in ~/.config/ralphex/config or .ralphex/config
181+
# use built-in openspec preset
182+
task_header_patterns = openspec
183+
184+
# or a raw regex
185+
task_header_patterns = ^## (\d+)\.\s*(.*)$
186+
```
187+
188+
**When to use:** spec-driven workflows (OpenSpec etc.) whose `tasks.md` uses different header conventions than the ralphex defaults. Leaving the option unset preserves today's behavior.
189+
175190
### Review-Only Mode
176191

177192
Review-only mode (`--review`) runs the full review pipeline (Phase 2 → Phase 3 → Phase 4) on changes already present on the current branch. This is useful when changes were made outside ralphex — via Claude Code's built-in plan mode, manual edits, other AI agents, or any other workflow.
@@ -681,6 +696,7 @@ Custom prompt files support variable expansion. All variables use the `{{VARIABL
681696
| `{{PROGRESS_FILE}}` | Path to the progress log file | `.ralphex/progress/progress-feature.txt` |
682697
| `{{GOAL}}` | Human-readable goal description | `implementation of plan at docs/plans/feature.md` |
683698
| `{{DEFAULT_BRANCH}}` | Default branch name (overridable via `--base-ref` or `default_branch` config) | `main`, `master`, `origin/main` |
699+
| `{{TASK_HEADER_PATTERNS}}` | Human-readable descriptions of configured task header patterns (preset descriptions or raw regexes, quoted, `or`-joined; used in `task.txt`) | `'### Task N: title or ### Iteration N: title'` |
684700
| `{{agent:name}}` | Expands to Task tool instructions for the named agent | (see below) |
685701

686702
**Agent references:**
@@ -831,6 +847,7 @@ Use `--config-dir` or `RALPHEX_CONFIG_DIR` to override the global config locatio
831847
| `task_retry_count` | Task retry attempts | `1` |
832848
| `finalize_enabled` | Enable finalize step after reviews | `false` |
833849
| `move_plan_on_completion` | Move completed plan file into `docs/plans/completed/` on success (disable for external plan-lifecycle workflows) | `true` |
850+
| `task_header_patterns` | Comma-separated preset names (`default`, `openspec`) or raw Go regexes the plan parser uses to recognize task sections. Capture group 1 = task id, group 2 = title (optional) | `default` |
834851
| `use_worktree` | Run each plan in an isolated git worktree (full and tasks-only modes only) | `false` |
835852
| `plans_dir` | Plans directory | `docs/plans` |
836853
| `default_branch` | Override auto-detected default branch for review diffs | auto-detect |

cmd/ralphex/main.go

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -399,11 +399,16 @@ func setupProgressLogger(o opts, req executePlanRequest, branch string) (progres
399399
baseLog = req.ProgressLog
400400
} else {
401401
var err error
402+
var taskHeaderPatterns []string
403+
if req.Config != nil {
404+
taskHeaderPatterns = req.Config.TaskHeaderPatterns
405+
}
402406
baseLog, err = progress.NewLogger(progress.Config{
403-
PlanFile: req.PlanFile,
404-
Mode: string(req.Mode),
405-
Branch: branch,
406-
NoColor: o.NoColor,
407+
PlanFile: req.PlanFile,
408+
Mode: string(req.Mode),
409+
Branch: branch,
410+
TaskHeaderPatterns: taskHeaderPatterns,
411+
NoColor: o.NoColor,
407412
}, req.Colors, holder)
408413
if err != nil {
409414
return progressLogResult{}, fmt.Errorf("create progress logger: %w", err)
@@ -514,14 +519,15 @@ func executePlan(ctx context.Context, o opts, req executePlanRequest) error {
514519
var runnerLog processor.Logger = plr.baseLog
515520
if o.Serve {
516521
dashboard := web.NewDashboard(web.DashboardConfig{
517-
BaseLog: plr.baseLog,
518-
Port: o.Port,
519-
Host: o.Host,
520-
PlanFile: req.PlanFile,
521-
Branch: branch,
522-
WatchDirs: o.Watch,
523-
ConfigWatchDirs: req.Config.WatchDirs,
524-
Colors: req.Colors,
522+
BaseLog: plr.baseLog,
523+
Port: o.Port,
524+
Host: o.Host,
525+
PlanFile: req.PlanFile,
526+
Branch: branch,
527+
WatchDirs: o.Watch,
528+
ConfigWatchDirs: req.Config.WatchDirs,
529+
Colors: req.Colors,
530+
TaskHeaderPatterns: req.Config.TaskHeaderPatterns,
525531
}, plr.holder)
526532
var dashErr error
527533
runnerLog, dashErr = dashboard.Start(ctx)
@@ -646,10 +652,11 @@ func runWithWorktree(ctx context.Context, o opts, req executePlanRequest) (err e
646652
holder := &status.PhaseHolder{}
647653
branch := plan.ExtractBranchName(req.PlanFile)
648654
baseLog, err := progress.NewLogger(progress.Config{
649-
PlanFile: req.PlanFile,
650-
Mode: string(req.Mode),
651-
Branch: branch,
652-
NoColor: o.NoColor,
655+
PlanFile: req.PlanFile,
656+
Mode: string(req.Mode),
657+
Branch: branch,
658+
TaskHeaderPatterns: req.Config.TaskHeaderPatterns,
659+
NoColor: o.NoColor,
653660
}, req.Colors, holder)
654661
if err != nil {
655662
return fmt.Errorf("create progress logger: %w", err)
@@ -767,9 +774,10 @@ func isWatchOnlyMode(o opts, configWatchDirs []string) bool {
767774
func runWatchOnly(ctx context.Context, o opts, cfg *config.Config, colors *progress.Colors) error {
768775
dirs := web.ResolveWatchDirs(o.Watch, cfg.WatchDirs)
769776
dashboard := web.NewDashboard(web.DashboardConfig{
770-
Port: o.Port,
771-
Host: o.Host,
772-
Colors: colors,
777+
Port: o.Port,
778+
Host: o.Host,
779+
Colors: colors,
780+
TaskHeaderPatterns: cfg.TaskHeaderPatterns,
773781
}, nil)
774782
if watchErr := dashboard.RunWatchOnly(ctx, dirs); watchErr != nil {
775783
return fmt.Errorf("run watch-only mode: %w", watchErr)
@@ -934,10 +942,11 @@ func runPlanMode(ctx context.Context, o opts, req executePlanRequest, selector *
934942

935943
// create progress logger for plan mode
936944
baseLog, err := progress.NewLogger(progress.Config{
937-
PlanDescription: o.PlanDescription,
938-
Mode: string(processor.ModePlan),
939-
Branch: branch,
940-
NoColor: o.NoColor,
945+
PlanDescription: o.PlanDescription,
946+
Mode: string(processor.ModePlan),
947+
Branch: branch,
948+
TaskHeaderPatterns: req.Config.TaskHeaderPatterns,
949+
NoColor: o.NoColor,
941950
}, req.Colors, holder)
942951
if err != nil {
943952
return fmt.Errorf("create progress logger: %w", err)

0 commit comments

Comments
 (0)