[object Object]

← back to Cli Printing Press

feat(pipeline): add PhaseAgentReadiness and plugin dependency

bdc9a90f563c1a89d37977bafd4a91a016dfe2e9 · 2026-03-27 19:32:35 -0700 · Trevin Chow

- Add PhaseAgentReadiness constant between PhaseReview and PhaseComparative
- Switch PlanPath derivation from index-based to name-based format
- Bump state version to 2 with migration that:
  - Backfills agent-readiness phase with Status+PlanStatus completed
  - Updates all PlanPaths from %02d-name to name-only format
  - Fixes latent bug where backfilled phases had no PlanStatus
- Add seed template for agent-readiness phase
- Update planner.go comment to note AgentReadiness uses static seed
- Create .claude/settings.json with compound-engineering plugin dependency
- Add tests for phase ordering, migration v1→v2, name-based PlanPath

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit bdc9a90f563c1a89d37977bafd4a91a016dfe2e9
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Fri Mar 27 19:32:35 2026 -0700

    feat(pipeline): add PhaseAgentReadiness and plugin dependency
    
    - Add PhaseAgentReadiness constant between PhaseReview and PhaseComparative
    - Switch PlanPath derivation from index-based to name-based format
    - Bump state version to 2 with migration that:
      - Backfills agent-readiness phase with Status+PlanStatus completed
      - Updates all PlanPaths from %02d-name to name-only format
      - Fixes latent bug where backfilled phases had no PlanStatus
    - Add seed template for agent-readiness phase
    - Update planner.go comment to note AgentReadiness uses static seed
    - Create .claude/settings.json with compound-engineering plugin dependency
    - Add tests for phase ordering, migration v1→v2, name-based PlanPath
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 .claude/settings.json           |  5 +++
 internal/pipeline/planner.go    |  2 +-
 internal/pipeline/seeds.go      | 38 +++++++++++++++++++
 internal/pipeline/state.go      | 40 ++++++++++++--------
 internal/pipeline/state_test.go | 84 +++++++++++++++++++++++++++++++++++++++++
 5 files changed, 153 insertions(+), 16 deletions(-)

diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 00000000..eca411a1
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,5 @@
+{
+  "enabledPlugins": {
+    "compound-engineering@every-marketplace": true
+  }
+}
diff --git a/internal/pipeline/planner.go b/internal/pipeline/planner.go
index 54aa4cd5..167f07f8 100644
--- a/internal/pipeline/planner.go
+++ b/internal/pipeline/planner.go
@@ -47,7 +47,7 @@ func GenerateNextPlan(state *PipelineState, nextPhase string) (string, error) {
 	case PhaseShip:
 		return generateShipPlan(ctx)
 	default:
-		// Preflight, Research use static seeds
+		// Preflight, Research, AgentReadiness use static seeds
 		return RenderSeed(nextPhase, ctx.SeedData)
 	}
 }
diff --git a/internal/pipeline/seeds.go b/internal/pipeline/seeds.go
index 42b51dc5..12e0c3d2 100644
--- a/internal/pipeline/seeds.go
+++ b/internal/pipeline/seeds.go
@@ -300,6 +300,44 @@ Evaluate the generated CLI with static scoring and dogfooding evidence that dete
 - Review scoring rules defined by the pipeline review plan for this phase
 - Dogfood model uses three tiers: Tier 1 no credentials, Tier 2 read-only, Tier 3 sandbox write
 - Generated CLI binary and help surfaces in {{.OutputDir}}
+`,
+	PhaseAgentReadiness: `---
+title: "{{.APIName}} CLI Pipeline - Agent Readiness Review"
+type: feat
+status: seed
+pipeline_phase: agent-readiness
+pipeline_api: {{.APIName}}
+date: {{now}}
+---
+
+# Phase Goal
+
+Run the compound-engineering:cli-agent-readiness-reviewer agent on the generated {{.APIName}} CLI
+and implement its fixes in a severity-gated loop (max 2 passes) until no Blockers or Frictions remain.
+
+## Context
+
+- Pipeline directory: {{.PipelineDir}}
+- Output directory: {{.OutputDir}}
+- Spec URL: {{.SpecURL}}
+- Spec source: {{.SpecSource}}
+
+## What This Phase Must Produce
+
+- Agent readiness reviewer scorecard (7 principles x severity)
+- Fix implementation log (which fixes were applied, which were skipped/reverted)
+- Phase verdict: Pass (zero Blockers and Frictions), Warn (Frictions remain), or Degrade (Blockers remain)
+
+## Prior Phase Outputs
+
+- Runtime verification results from Phase 4.8 (pass rate, data pipeline status)
+- Working CLI binary in {{.OutputDir}}
+
+## Codebase Pointers
+
+- Reviewer agent: compound-engineering:cli-agent-readiness-reviewer (external plugin)
+- Plugin dependency declared in .claude/settings.json
+- Phase 4.8 analog: SKILL.md Phase 4.8 (Runtime Verification)
 `,
 	PhaseShip: `---
 title: "{{.APIName}} CLI Pipeline - Phase 5: Ship"
diff --git a/internal/pipeline/state.go b/internal/pipeline/state.go
index aec39709..7c5c5b0a 100644
--- a/internal/pipeline/state.go
+++ b/internal/pipeline/state.go
@@ -10,14 +10,15 @@ import (
 
 // Phase names in execution order.
 const (
-	PhasePreflight   = "preflight"
-	PhaseResearch    = "research"
-	PhaseScaffold    = "scaffold"
-	PhaseEnrich      = "enrich"
-	PhaseRegenerate  = "regenerate"
-	PhaseReview      = "review"
-	PhaseComparative = "comparative"
-	PhaseShip        = "ship"
+	PhasePreflight      = "preflight"
+	PhaseResearch       = "research"
+	PhaseScaffold       = "scaffold"
+	PhaseEnrich         = "enrich"
+	PhaseRegenerate     = "regenerate"
+	PhaseReview         = "review"
+	PhaseAgentReadiness = "agent-readiness"
+	PhaseComparative    = "comparative"
+	PhaseShip           = "ship"
 )
 
 // PhaseOrder defines execution order.
@@ -28,6 +29,7 @@ var PhaseOrder = []string{
 	PhaseEnrich,
 	PhaseRegenerate,
 	PhaseReview,
+	PhaseAgentReadiness,
 	PhaseComparative,
 	PhaseShip,
 }
@@ -59,7 +61,7 @@ type PipelineState struct {
 	DogfoodTier    int                   `json:"dogfood_tier,omitempty"`            // max tier to run (1-3, default 1)
 }
 
-const currentStateVersion = 1
+const currentStateVersion = 2
 
 // PhaseState tracks a single phase.
 type PhaseState struct {
@@ -81,10 +83,10 @@ func StatePath(apiName string) string {
 // NewState creates a fresh pipeline state.
 func NewState(apiName, outputDir string) *PipelineState {
 	phases := make(map[string]PhaseState, len(PhaseOrder))
-	for i, name := range PhaseOrder {
+	for _, name := range PhaseOrder {
 		phases[name] = PhaseState{
 			Status:   StatusPending,
-			PlanPath: filepath.Join(PipelineDir(apiName), fmt.Sprintf("%02d-%s-plan.md", i, name)),
+			PlanPath: filepath.Join(PipelineDir(apiName), fmt.Sprintf("%s-plan.md", name)),
 		}
 	}
 	state := &PipelineState{
@@ -122,14 +124,22 @@ func LoadState(apiName string) (*PipelineState, error) {
 	if err := json.Unmarshal(data, &s); err != nil {
 		return nil, fmt.Errorf("parsing state: %w", err)
 	}
-	// Migrate: add any missing phases from PhaseOrder (e.g., research, comparative).
+	// Migrate: add missing phases and update PlanPath from index-based to name-based format.
 	if s.Version < currentStateVersion {
-		for i, name := range PhaseOrder {
+		for _, name := range PhaseOrder {
 			if _, ok := s.Phases[name]; !ok {
+				// Backfill missing phases as completed (both Status and PlanStatus)
+				// so NextPhase() doesn't treat them as pending.
 				s.Phases[name] = PhaseState{
-					Status:   StatusCompleted,
-					PlanPath: filepath.Join(PipelineDir(apiName), fmt.Sprintf("%02d-%s-plan.md", i, name)),
+					Status:     StatusCompleted,
+					PlanStatus: PlanStatusCompleted,
+					PlanPath:   filepath.Join(PipelineDir(apiName), fmt.Sprintf("%s-plan.md", name)),
 				}
+			} else {
+				// Migrate existing phases from index-based to name-based PlanPath.
+				p := s.Phases[name]
+				p.PlanPath = filepath.Join(PipelineDir(apiName), fmt.Sprintf("%s-plan.md", name))
+				s.Phases[name] = p
 			}
 		}
 		s.Version = currentStateVersion
diff --git a/internal/pipeline/state_test.go b/internal/pipeline/state_test.go
index c132b17e..b4ee6abf 100644
--- a/internal/pipeline/state_test.go
+++ b/internal/pipeline/state_test.go
@@ -118,6 +118,90 @@ func TestDefaultOutputDir(t *testing.T) {
 	}
 }
 
+func TestPhaseAgentReadinessInPhaseOrder(t *testing.T) {
+	// PhaseAgentReadiness must be between PhaseReview and PhaseComparative.
+	var reviewIdx, agentIdx, compIdx int
+	for i, name := range PhaseOrder {
+		switch name {
+		case PhaseReview:
+			reviewIdx = i
+		case PhaseAgentReadiness:
+			agentIdx = i
+		case PhaseComparative:
+			compIdx = i
+		}
+	}
+	assert.Greater(t, agentIdx, reviewIdx, "agent-readiness must come after review")
+	assert.Less(t, agentIdx, compIdx, "agent-readiness must come before comparative")
+}
+
+func TestNextPhaseReturnsAgentReadiness(t *testing.T) {
+	s := NewState("ar-test", "/tmp/test")
+	// Complete through PhaseReview.
+	for _, name := range PhaseOrder {
+		if name == PhaseAgentReadiness {
+			break
+		}
+		s.Complete(name)
+	}
+	assert.Equal(t, PhaseAgentReadiness, s.NextPhase())
+}
+
+func TestNewStatePlanPathNameBased(t *testing.T) {
+	s := NewState("path-test", "/tmp/test")
+	for _, name := range PhaseOrder {
+		expected := "docs/plans/path-test-pipeline/" + name + "-plan.md"
+		assert.Equal(t, expected, s.Phases[name].PlanPath, "PlanPath for %s", name)
+	}
+}
+
+func TestLoadStateMigratesV1ToV2(t *testing.T) {
+	apiName := "migrate-v1-test"
+	dir := PipelineDir(apiName)
+	require.NoError(t, os.MkdirAll(dir, 0o755))
+	defer os.RemoveAll(dir)
+
+	// Simulate a v1 state file without agent-readiness phase and with index-based paths.
+	v1State := PipelineState{
+		Version:   1,
+		APIName:   apiName,
+		OutputDir: "/tmp/migrate-cli",
+		Phases: map[string]PhaseState{
+			PhasePreflight:   {Status: StatusCompleted, PlanStatus: PlanStatusCompleted, PlanPath: dir + "/00-preflight-plan.md"},
+			PhaseResearch:    {Status: StatusCompleted, PlanStatus: PlanStatusCompleted, PlanPath: dir + "/01-research-plan.md"},
+			PhaseScaffold:    {Status: StatusCompleted, PlanStatus: PlanStatusCompleted, PlanPath: dir + "/02-scaffold-plan.md"},
+			PhaseEnrich:      {Status: StatusCompleted, PlanStatus: PlanStatusCompleted, PlanPath: dir + "/03-enrich-plan.md"},
+			PhaseRegenerate:  {Status: StatusCompleted, PlanStatus: PlanStatusCompleted, PlanPath: dir + "/04-regenerate-plan.md"},
+			PhaseReview:      {Status: StatusCompleted, PlanStatus: PlanStatusCompleted, PlanPath: dir + "/05-review-plan.md"},
+			PhaseComparative: {Status: StatusPending, PlanPath: dir + "/06-comparative-plan.md"},
+			PhaseShip:        {Status: StatusPending, PlanPath: dir + "/07-ship-plan.md"},
+		},
+	}
+	data, err := json.MarshalIndent(v1State, "", "  ")
+	require.NoError(t, err)
+	require.NoError(t, os.WriteFile(StatePath(apiName), data, 0o644))
+
+	loaded, err := LoadState(apiName)
+	require.NoError(t, err)
+
+	// Version bumped.
+	assert.Equal(t, 2, loaded.Version)
+
+	// New phase was backfilled as completed.
+	ar := loaded.Phases[PhaseAgentReadiness]
+	assert.Equal(t, StatusCompleted, ar.Status)
+	assert.Equal(t, PlanStatusCompleted, ar.PlanStatus)
+
+	// All phases have name-based PlanPaths.
+	for _, name := range PhaseOrder {
+		expected := dir + "/" + name + "-plan.md"
+		assert.Equal(t, expected, loaded.Phases[name].PlanPath, "migrated PlanPath for %s", name)
+	}
+
+	// Existing phase statuses preserved (comparative was pending).
+	assert.Equal(t, StatusPending, loaded.Phases[PhaseComparative].Status)
+}
+
 func TestPhaseStateJSONIncludesPlanStatus(t *testing.T) {
 	state := PhaseState{
 		Status:     StatusPlanned,

← e49ec256 docs: add implementation plan for Phase 4.9 agent readiness  ·  back to Cli Printing Press  ·  feat(skill): add Phase 4.9 agent readiness review loop to SK 5a6c8095 →