← back to Cli Printing Press
refactor(pipeline): use stable-numbered PlanPath instead of name-only
630cd0b5e13f9795bc4497d739047a68e59226fc · 2026-03-27 20:02:21 -0700 · Trevin Chow
Switch from name-only PlanPath (preflight-plan.md) to stable-numbered
format with gaps (00-preflight-plan.md, 55-agent-readiness-plan.md).
Preserves visual scan order in directory listings while allowing future
phase insertions without renaming existing files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
M internal/pipeline/state.goM internal/pipeline/state_test.go
Diff
commit 630cd0b5e13f9795bc4497d739047a68e59226fc
Author: Trevin Chow <trevin@trevinchow.com>
Date: Fri Mar 27 20:02:21 2026 -0700
refactor(pipeline): use stable-numbered PlanPath instead of name-only
Switch from name-only PlanPath (preflight-plan.md) to stable-numbered
format with gaps (00-preflight-plan.md, 55-agent-readiness-plan.md).
Preserves visual scan order in directory listings while allowing future
phase insertions without renaming existing files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
internal/pipeline/state.go | 40 ++++++++++++++++++++++++++++++----------
internal/pipeline/state_test.go | 12 ++++++++----
2 files changed, 38 insertions(+), 14 deletions(-)
diff --git a/internal/pipeline/state.go b/internal/pipeline/state.go
index 07904feb..97212be1 100644
--- a/internal/pipeline/state.go
+++ b/internal/pipeline/state.go
@@ -34,6 +34,26 @@ var PhaseOrder = []string{
PhaseShip,
}
+// phaseNumber assigns a stable prefix for plan filenames. Numbers use
+// gaps (0, 10, 20 …) so future phases can be inserted without renaming
+// existing files.
+var phaseNumber = map[string]int{
+ PhasePreflight: 0,
+ PhaseResearch: 10,
+ PhaseScaffold: 20,
+ PhaseEnrich: 30,
+ PhaseRegenerate: 40,
+ PhaseReview: 50,
+ PhaseAgentReadiness: 55,
+ PhaseComparative: 60,
+ PhaseShip: 70,
+}
+
+// PlanFilename returns the stable plan filename for a phase.
+func PlanFilename(phase string) string {
+ return fmt.Sprintf("%02d-%s-plan.md", phaseNumber[phase], phase)
+}
+
const (
StatusPending = "pending"
StatusPlanned = "planned" // plan.md exists but not yet executed
@@ -86,7 +106,7 @@ func NewState(apiName, outputDir string) *PipelineState {
for _, name := range PhaseOrder {
phases[name] = PhaseState{
Status: StatusPending,
- PlanPath: filepath.Join(PipelineDir(apiName), fmt.Sprintf("%s-plan.md", name)),
+ PlanPath: filepath.Join(PipelineDir(apiName), PlanFilename(name)),
}
}
state := &PipelineState{
@@ -124,7 +144,8 @@ func LoadState(apiName string) (*PipelineState, error) {
if err := json.Unmarshal(data, &s); err != nil {
return nil, fmt.Errorf("parsing state: %w", err)
}
- // Migrate: add missing phases and update PlanPath from index-based to name-based format.
+ // Migrate: add missing phases, update PlanPath to stable-numbered format,
+ // and backfill PlanStatus for completed phases that predate the field.
if s.Version < currentStateVersion {
for _, name := range PhaseOrder {
if _, ok := s.Phases[name]; !ok {
@@ -133,14 +154,13 @@ func LoadState(apiName string) (*PipelineState, error) {
s.Phases[name] = PhaseState{
Status: StatusCompleted,
PlanStatus: PlanStatusCompleted,
- PlanPath: filepath.Join(PipelineDir(apiName), fmt.Sprintf("%s-plan.md", name)),
+ PlanPath: filepath.Join(PipelineDir(apiName), PlanFilename(name)),
}
} else {
- // Migrate existing phases: update PlanPath from index-based to
- // name-based format, and backfill PlanStatus for completed phases
- // that predate the PlanStatus field.
+ // Migrate existing phases to stable-numbered PlanPath and
+ // backfill PlanStatus for completed phases that predate the field.
p := s.Phases[name]
- p.PlanPath = filepath.Join(PipelineDir(apiName), fmt.Sprintf("%s-plan.md", name))
+ p.PlanPath = filepath.Join(PipelineDir(apiName), PlanFilename(name))
if p.Status == StatusCompleted && p.PlanStatus == "" {
p.PlanStatus = PlanStatusCompleted
}
@@ -149,9 +169,9 @@ func LoadState(apiName string) (*PipelineState, error) {
}
s.Version = currentStateVersion
// Persist the migration so it doesn't re-run on every load.
- // Note: plan files on disk keep their old index-based names.
- // Completed phases' plans are not re-read; pending phases get
- // new seeds written at the new paths by Init.
+ // Note: plan files on disk keep their old sequential-index names
+ // (00-, 01-, …). Completed phases' plans are not re-read; pending
+ // phases get new seeds written at the stable-numbered paths by Init.
if err := s.Save(); err != nil {
return nil, fmt.Errorf("saving migrated state: %w", err)
}
diff --git a/internal/pipeline/state_test.go b/internal/pipeline/state_test.go
index 31651e75..3d7709c3 100644
--- a/internal/pipeline/state_test.go
+++ b/internal/pipeline/state_test.go
@@ -147,12 +147,16 @@ func TestNextPhaseReturnsAgentReadiness(t *testing.T) {
assert.Equal(t, PhaseAgentReadiness, s.NextPhase())
}
-func TestNewStatePlanPathNameBased(t *testing.T) {
+func TestNewStatePlanPathStableNumbered(t *testing.T) {
s := NewState("path-test", "/tmp/test")
for _, name := range PhaseOrder {
- expected := "docs/plans/path-test-pipeline/" + name + "-plan.md"
+ expected := "docs/plans/path-test-pipeline/" + PlanFilename(name)
assert.Equal(t, expected, s.Phases[name].PlanPath, "PlanPath for %s", name)
}
+ // Spot-check a few to verify the numbering scheme.
+ assert.Contains(t, s.Phases[PhasePreflight].PlanPath, "00-preflight-plan.md")
+ assert.Contains(t, s.Phases[PhaseAgentReadiness].PlanPath, "55-agent-readiness-plan.md")
+ assert.Contains(t, s.Phases[PhaseShip].PlanPath, "70-ship-plan.md")
}
func TestLoadStateMigratesV1ToV2(t *testing.T) {
@@ -193,9 +197,9 @@ func TestLoadStateMigratesV1ToV2(t *testing.T) {
assert.Equal(t, StatusCompleted, ar.Status)
assert.Equal(t, PlanStatusCompleted, ar.PlanStatus)
- // All phases have name-based PlanPaths.
+ // All phases have stable-numbered PlanPaths.
for _, name := range PhaseOrder {
- expected := dir + "/" + name + "-plan.md"
+ expected := dir + "/" + PlanFilename(name)
assert.Equal(t, expected, loaded.Phases[name].PlanPath, "migrated PlanPath for %s", name)
}
← 0d4e711c fix(cli): propagate ReadDir error in explicitOutput collisio
·
back to Cli Printing Press
·
docs: update specs to reflect stable-numbered PlanPath appro fe6a3634 →