← back to Cli Printing Press
fix(cli): gate ship plans on agent readiness (#1638)
ed17d302df9dafd7b3bfc3ded8099ce1525ec77d · 2026-05-18 17:13:34 -0700 · Trevin Chow
* fix(cli): gate ship plans on agent readiness
* docs(cli): document readiness ship gate learning
* fix(cli): clean readiness finding rendering
* fix(cli): normalize readiness table findings
* fix(cli): clarify readiness verdict holds
---------
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Files touched
M docs/PIPELINE.mdA docs/solutions/logic-errors/ship-plan-agent-readiness-gate-2026-05-18.mdM internal/pipeline/planner.goM internal/pipeline/planner_artifacts_test.goM internal/pipeline/seeds.go
Diff
commit ed17d302df9dafd7b3bfc3ded8099ce1525ec77d
Author: Trevin Chow <trevin@trevinchow.com>
Date: Mon May 18 17:13:34 2026 -0700
fix(cli): gate ship plans on agent readiness (#1638)
* fix(cli): gate ship plans on agent readiness
* docs(cli): document readiness ship gate learning
* fix(cli): clean readiness finding rendering
* fix(cli): normalize readiness table findings
* fix(cli): clarify readiness verdict holds
---------
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
---
docs/PIPELINE.md | 5 +-
.../ship-plan-agent-readiness-gate-2026-05-18.md | 68 ++++++
internal/pipeline/planner.go | 230 +++++++++++++++++++--
internal/pipeline/planner_artifacts_test.go | 217 +++++++++++++++++++
internal/pipeline/seeds.go | 6 +-
5 files changed, 509 insertions(+), 17 deletions(-)
diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md
index e96e8813..a4ed1150 100644
--- a/docs/PIPELINE.md
+++ b/docs/PIPELINE.md
@@ -192,6 +192,7 @@ Outputs:
- Reviewer scorecard across seven principles and three severities
- Fix implementation log (which fixes were applied, which were skipped or reverted)
- Phase verdict: `Pass`, `Warn`, or `Degrade`
+- `pipeline/agent-readiness.md` with a parseable `Phase verdict: Pass|Warn|Degrade` line and remaining Blocker/Friction findings as bullets or table rows
Gates:
- `Pass` - zero Blockers and zero Frictions
@@ -199,7 +200,7 @@ Gates:
- `Degrade` - Blockers remain; phase fails
Artifacts:
-- Reviewer scorecard document in the pipeline directory
+- `pipeline/agent-readiness.md`
- Fix log in the pipeline directory
### 8. comparative
@@ -241,6 +242,7 @@ Purpose: package the generated CLI output and produce the final handoff report.
Inputs:
- Review score and `review.md` from the review phase
+- Agent-readiness verdict from `agent-readiness.md`
- Working CLI binary ready for handoff
Outputs:
@@ -250,6 +252,7 @@ Outputs:
Gates:
- All prior phases are `completed`
+- `agent-readiness.md` reports `Pass`, unless a maintainer records an explicit override in the handoff
- Output directory contains a valid CLI source tree and compiled binary
Artifacts:
diff --git a/docs/solutions/logic-errors/ship-plan-agent-readiness-gate-2026-05-18.md b/docs/solutions/logic-errors/ship-plan-agent-readiness-gate-2026-05-18.md
new file mode 100644
index 00000000..d53f935f
--- /dev/null
+++ b/docs/solutions/logic-errors/ship-plan-agent-readiness-gate-2026-05-18.md
@@ -0,0 +1,68 @@
+---
+title: "Ship plan agent-readiness gate"
+date: 2026-05-18
+category: logic-errors
+module: internal/pipeline
+problem_type: logic_error
+component: tooling
+symptoms:
+ - "PhaseShip could emit SHIP when the scorecard passed even though agent-readiness reported Degrade"
+ - "Agent-readiness findings appeared in pipeline artifacts but did not affect the generated ship decision"
+root_cause: logic_error
+resolution_type: code_fix
+severity: high
+tags:
+ - pipeline
+ - agent-readiness
+ - ship-gate
+ - artifact-parsing
+ - scorecard
+---
+
+# Ship plan agent-readiness gate
+
+## Problem
+
+The managed pipeline's ship plan treated the scorecard threshold as the only load-bearing ship gate. A run could produce `pipeline/agent-readiness.md` with a non-Pass verdict and still generate a ship plan that said `SHIP` when the scorecard cleared 65%.
+
+## Symptoms
+
+- PhaseShip emitted `SHIP` for a high-scoring CLI even when agent-readiness reported `Degrade`.
+- Readiness Blocker and Friction findings were visible in artifacts, but the ship decision ignored them.
+- Missing readiness evidence was effectively neutral instead of blocking ship.
+
+## What Didn't Work
+
+- Reading only the scorecard in `GenerateNextPlan` kept quality and readiness as independent reports, but only quality became a gate.
+- Loose markdown parsing would have created false positives from prose mentioning "non-Pass" or "Degrade" without declaring a verdict.
+- Falling back to `proofs/agent-readiness.md` would have let stale or non-canonical proof data satisfy the gate when the canonical `pipeline/agent-readiness.md` was missing.
+
+## Solution
+
+Make PhaseShip compose quality and agent-readiness as independent gates:
+
+- Load readiness only for `PhaseShip`, and only from `state.PipelineDir()/agent-readiness.md`.
+- Parse a narrow verdict contract: `Phase verdict: Pass|Warn|Degrade` or `Verdict: Pass|Warn|Degrade`.
+- Treat missing, malformed, `Warn`, and `Degrade` readiness reports as `HOLD`.
+- Keep scorecard and readiness independent in the rendered plan so operators can see which axis failed.
+- Update the agent-readiness seed and `docs/PIPELINE.md` to require the exact artifact the ship gate consumes.
+
+## Why This Works
+
+The root cause was not the readiness reviewer itself. The failure was at the artifact contract boundary: the ship plan did not consume the readiness phase's canonical output. Loading the canonical artifact and parsing only its explicit verdict line makes readiness load-bearing without turning plan generation into a hard error path.
+
+Keeping missing or malformed readiness as a rendered `HOLD` also preserves the reusable planning contract. `GenerateNextPlan` can still produce a useful handoff document, but the document cannot silently recommend shipping without the required evidence.
+
+## Prevention
+
+- When a later phase gates on an earlier phase's artifact, update the producer seed, consumer parser, docs, and regression tests in the same change.
+- Do not treat absent evidence as neutral in ship gates. Missing scorecard or readiness data should render an explicit `HOLD`.
+- Add false-positive parser tests for prose that mentions verdict words without declaring the verdict.
+- Add stale-location tests when a canonical runstate path has tempting fallback locations.
+
+## Related Issues
+
+- GitHub issue #1354
+- `docs/solutions/logic-errors/scorecard-accuracy-broadened-pattern-matching-2026-03-27.md`
+- `docs/solutions/logic-errors/scorer-dogfood-composed-header-auth-and-example-continuations-2026-05-05.md`
+- `docs/solutions/best-practices/checkout-scoped-printing-press-output-layout-2026-03-28.md`
diff --git a/internal/pipeline/planner.go b/internal/pipeline/planner.go
index e0b49f93..a776237f 100644
--- a/internal/pipeline/planner.go
+++ b/internal/pipeline/planner.go
@@ -2,17 +2,35 @@ package pipeline
import (
"fmt"
+ "os"
+ "path/filepath"
"strings"
"time"
)
// PlanContext aggregates outputs from completed phases for dynamic plan generation.
type PlanContext struct {
- SeedData SeedData
- Research *ResearchResult
- Dogfood *DogfoodReport
- Scorecard *Scorecard
- Learnings *LearningsDB
+ SeedData SeedData
+ Research *ResearchResult
+ Dogfood *DogfoodReport
+ Scorecard *Scorecard
+ Learnings *LearningsDB
+ Readiness *AgentReadinessReport
+ ReadinessErr error
+}
+
+type AgentReadinessVerdict string
+
+const (
+ AgentReadinessPass AgentReadinessVerdict = "Pass"
+ AgentReadinessWarn AgentReadinessVerdict = "Warn"
+ AgentReadinessDegrade AgentReadinessVerdict = "Degrade"
+)
+
+type AgentReadinessReport struct {
+ Path string
+ Verdict AgentReadinessVerdict
+ Findings []string
}
// GenerateNextPlan writes a dynamic plan for the next phase, informed by
@@ -47,6 +65,7 @@ func GenerateNextPlan(state *PipelineState, nextPhase string) (string, error) {
case PhaseComparative:
return generateComparativePlan(ctx)
case PhaseShip:
+ ctx.Readiness, ctx.ReadinessErr = loadAgentReadinessForPlanState(state)
return generateShipPlan(ctx)
default:
// Preflight, Research, AgentReadiness use static seeds
@@ -212,16 +231,7 @@ func generateShipPlan(ctx PlanContext) (string, error) {
writePipelineContext(&b, ctx.SeedData)
- // Dynamic: ship/hold decision based on scores
- if ctx.Scorecard != nil {
- b.WriteString("## Ship Decision\n\n")
- if ctx.Scorecard.Steinberger.Percentage >= 65 {
- fmt.Fprintf(&b, "**SHIP** - Quality score %d%% (grade %s) meets threshold.\n\n", ctx.Scorecard.Steinberger.Percentage, ctx.Scorecard.OverallGrade)
- } else {
- fmt.Fprintf(&b, "**HOLD** - Quality score %d%% (grade %s) is below 65%% threshold.\n", ctx.Scorecard.Steinberger.Percentage, ctx.Scorecard.OverallGrade)
- b.WriteString("Fix the gaps identified in the scorecard before shipping.\n\n")
- }
- }
+ writeShipDecision(&b, ctx)
b.WriteString("## What This Phase Must Produce\n\n")
fmt.Fprintf(&b, "- Git repository initialized in %s\n", ctx.SeedData.OutputDir)
@@ -231,6 +241,47 @@ func generateShipPlan(ctx PlanContext) (string, error) {
return b.String(), nil
}
+func writeShipDecision(b *strings.Builder, ctx PlanContext) {
+ b.WriteString("## Ship Decision\n\n")
+
+ ship := true
+ switch {
+ case ctx.Scorecard == nil:
+ b.WriteString("- Quality: HOLD - missing scorecard.json; run the review phase before shipping.\n")
+ ship = false
+ case ctx.Scorecard.Steinberger.Percentage >= 65:
+ fmt.Fprintf(b, "- Quality: PASS - score %d%% (grade %s) meets threshold.\n", ctx.Scorecard.Steinberger.Percentage, ctx.Scorecard.OverallGrade)
+ default:
+ fmt.Fprintf(b, "- Quality: HOLD - score %d%% (grade %s) is below 65%% threshold.\n", ctx.Scorecard.Steinberger.Percentage, ctx.Scorecard.OverallGrade)
+ ship = false
+ }
+
+ switch {
+ case ctx.Readiness == nil:
+ if ctx.ReadinessErr != nil && !os.IsNotExist(ctx.ReadinessErr) {
+ fmt.Fprintf(b, "- Agent readiness: HOLD - %s.\n", ctx.ReadinessErr)
+ } else {
+ b.WriteString("- Agent readiness: HOLD - missing pipeline/agent-readiness.md; run the agent-readiness phase before shipping.\n")
+ }
+ ship = false
+ case ctx.Readiness.Verdict == AgentReadinessPass:
+ fmt.Fprintf(b, "- Agent readiness: PASS - %s reports Pass.\n", ctx.Readiness.Path)
+ default:
+ fmt.Fprintf(b, "- Agent readiness: HOLD - %s reports %s.\n", ctx.Readiness.Path, ctx.Readiness.Verdict)
+ for _, finding := range ctx.Readiness.Findings {
+ fmt.Fprintf(b, " - %s\n", finding)
+ }
+ b.WriteString(" - To ship anyway, record an explicit maintainer override and cite the readiness findings in the handoff report.\n")
+ ship = false
+ }
+
+ if ship {
+ b.WriteString("\n**SHIP** - Quality and agent-readiness gates both pass.\n\n")
+ return
+ }
+ b.WriteString("\n**HOLD** - Do not package or promote the CLI until the failed gate is resolved or explicitly overridden.\n\n")
+}
+
// Helper functions
func writePlanHeader(b *strings.Builder, apiName, phase, title string) {
@@ -280,3 +331,152 @@ func loadScorecardForPlanState(state *PipelineState) (*Scorecard, error) {
}
return nil, fmt.Errorf("scorecard not found")
}
+
+func loadAgentReadinessForPlanState(state *PipelineState) (*AgentReadinessReport, error) {
+ return LoadAgentReadinessReport(state.PipelineDir())
+}
+
+func LoadAgentReadinessReport(dir string) (*AgentReadinessReport, error) {
+ path := filepath.Join(dir, "agent-readiness.md")
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+
+ report := &AgentReadinessReport{Path: path}
+ for line := range strings.SplitSeq(string(data), "\n") {
+ trimmed := strings.TrimSpace(strings.Trim(line, "|"))
+ if trimmed == "" {
+ continue
+ }
+ if report.Verdict == "" {
+ report.Verdict = parseAgentReadinessVerdict(trimmed)
+ }
+ if finding, ok := normalizeAgentReadinessFinding(line); ok {
+ report.Findings = append(report.Findings, finding)
+ }
+ }
+ if report.Verdict == "" {
+ return nil, fmt.Errorf("agent-readiness verdict not found in %s", path)
+ }
+ return report, nil
+}
+
+func parseAgentReadinessVerdict(line string) AgentReadinessVerdict {
+ clean := strings.Trim(strings.TrimSpace(line), "*` ")
+ clean = strings.ReplaceAll(clean, "**", "")
+ lower := strings.ToLower(clean)
+ for _, prefix := range []string{"phase verdict:", "verdict:", "phase verdict -", "verdict -"} {
+ if !strings.HasPrefix(lower, prefix) {
+ continue
+ }
+ rest := strings.TrimSpace(clean[len(prefix):])
+ if rest == "" {
+ return ""
+ }
+ token := strings.Trim(strings.Fields(rest)[0], "*`.,;:()[]{} ")
+ switch strings.ToLower(token) {
+ case "pass":
+ return AgentReadinessPass
+ case "warn":
+ return AgentReadinessWarn
+ case "degrade":
+ return AgentReadinessDegrade
+ default:
+ return ""
+ }
+ }
+ return ""
+}
+
+func normalizeAgentReadinessFinding(line string) (string, bool) {
+ trimmed := strings.TrimSpace(line)
+ lower := strings.ToLower(trimmed)
+ switch {
+ case strings.HasPrefix(lower, "- ") || strings.HasPrefix(lower, "* "):
+ finding := strings.TrimSpace(trimmed[2:])
+ return finding, finding != ""
+ case strings.HasPrefix(trimmed, "|"):
+ return normalizeAgentReadinessTableFinding(readinessTableCells(trimmed))
+ default:
+ return "", false
+ }
+}
+
+func readinessTableCells(line string) []string {
+ parts := strings.Split(strings.Trim(line, "|"), "|")
+ cells := make([]string, 0, len(parts))
+ for _, part := range parts {
+ cell := strings.TrimSpace(part)
+ if cell != "" {
+ cells = append(cells, cell)
+ }
+ }
+ return cells
+}
+
+func normalizeAgentReadinessTableFinding(cells []string) (string, bool) {
+ if len(cells) < 2 || isAgentReadinessTableSeparator(cells) || isAgentReadinessTableHeader(cells) {
+ return "", false
+ }
+ for i, cell := range cells {
+ lower := strings.ToLower(cell)
+ for _, severity := range []string{"blocker", "friction"} {
+ if strings.HasPrefix(lower, severity+":") {
+ return cell, true
+ }
+ if lower == severity {
+ rest := nonHeaderTableCells(cells[i+1:])
+ if len(rest) == 0 {
+ return "", false
+ }
+ return capitalizeASCII(severity) + ": " + strings.Join(rest, " - "), true
+ }
+ }
+ }
+ return "", false
+}
+
+func isAgentReadinessTableSeparator(cells []string) bool {
+ for _, cell := range cells {
+ if strings.Trim(cell, "-: ") != "" {
+ return false
+ }
+ }
+ return true
+}
+
+func isAgentReadinessTableHeader(cells []string) bool {
+ for _, cell := range cells {
+ if !isAgentReadinessHeaderCell(cell) {
+ return false
+ }
+ }
+ return true
+}
+
+func isAgentReadinessHeaderCell(cell string) bool {
+ switch strings.ToLower(strings.TrimSpace(cell)) {
+ case "severity", "finding", "description", "tool", "evidence", "impact", "command", "notes", "blocker", "friction":
+ return true
+ default:
+ return false
+ }
+}
+
+func nonHeaderTableCells(cells []string) []string {
+ var out []string
+ for _, cell := range cells {
+ if !isAgentReadinessHeaderCell(cell) {
+ out = append(out, cell)
+ }
+ }
+ return out
+}
+
+func capitalizeASCII(s string) string {
+ if s == "" {
+ return ""
+ }
+ return strings.ToUpper(s[:1]) + s[1:]
+}
diff --git a/internal/pipeline/planner_artifacts_test.go b/internal/pipeline/planner_artifacts_test.go
index 2ba727b2..9253cd47 100644
--- a/internal/pipeline/planner_artifacts_test.go
+++ b/internal/pipeline/planner_artifacts_test.go
@@ -50,3 +50,220 @@ func TestGenerateNextPlanLoadsArtifactsFromRunstateDirs(t *testing.T) {
assert.Contains(t, comparativePlan, "Overall: 72% (B)")
assert.Contains(t, comparativePlan, "example gap")
}
+
+func TestGenerateShipPlanHoldsOnAgentReadinessDegrade(t *testing.T) {
+ setPressTestEnv(t)
+
+ state := NewStateWithRun("sample", filepath.Join(t.TempDir(), "sample-pp-cli"), "run-123", "test-scope")
+ require.NoError(t, state.Save())
+ scorecard := &Scorecard{
+ APIName: "sample",
+ Steinberger: SteinerScore{
+ Percentage: 80,
+ },
+ OverallGrade: "A",
+ }
+ require.NoError(t, writeScorecardJSON(scorecard, state.ProofsDir()))
+ require.NoError(t, os.WriteFile(filepath.Join(state.PipelineDir(), "agent-readiness.md"), []byte(`## Agent Readiness
+
+Phase verdict: Degrade
+
+- Blocker: mutating commands can run under verify mode
+- Friction: help omits automation examples
+`), 0o644))
+
+ shipPlan, err := GenerateNextPlan(state, PhaseShip)
+ require.NoError(t, err)
+ assert.Contains(t, shipPlan, "Quality: PASS")
+ assert.Contains(t, shipPlan, "Agent readiness: HOLD")
+ assert.Contains(t, shipPlan, "reports Degrade")
+ assert.Contains(t, shipPlan, "Blocker: mutating commands can run under verify mode")
+ assert.Contains(t, shipPlan, " - Blocker: mutating commands can run under verify mode")
+ assert.NotContains(t, shipPlan, " - - Blocker:")
+ assert.Contains(t, shipPlan, "**HOLD**")
+ assert.NotContains(t, shipPlan, "**SHIP** - Quality and agent-readiness gates both pass.")
+}
+
+func TestGenerateShipPlanRendersTableAgentReadinessFindings(t *testing.T) {
+ setPressTestEnv(t)
+
+ state := NewStateWithRun("sample", filepath.Join(t.TempDir(), "sample-pp-cli"), "run-123", "test-scope")
+ require.NoError(t, state.Save())
+ scorecard := &Scorecard{
+ APIName: "sample",
+ Steinberger: SteinerScore{
+ Percentage: 80,
+ },
+ OverallGrade: "A",
+ }
+ require.NoError(t, writeScorecardJSON(scorecard, state.ProofsDir()))
+ require.NoError(t, os.WriteFile(filepath.Join(state.PipelineDir(), "agent-readiness.md"), []byte(`## Agent Readiness
+
+Phase verdict: Degrade
+
+| Blocker | Description |
+|---|---|
+| Blocker | mutating commands can run under verify mode |
+`), 0o644))
+
+ shipPlan, err := GenerateNextPlan(state, PhaseShip)
+ require.NoError(t, err)
+ assert.Contains(t, shipPlan, " - Blocker: mutating commands can run under verify mode")
+ assert.NotContains(t, shipPlan, "| Blocker | Description |")
+ assert.NotContains(t, shipPlan, " - |")
+ assert.Contains(t, shipPlan, "**HOLD**")
+}
+
+func TestGenerateShipPlanShipsWhenScorecardAndReadinessPass(t *testing.T) {
+ setPressTestEnv(t)
+
+ state := NewStateWithRun("sample", filepath.Join(t.TempDir(), "sample-pp-cli"), "run-123", "test-scope")
+ require.NoError(t, state.Save())
+ scorecard := &Scorecard{
+ APIName: "sample",
+ Steinberger: SteinerScore{
+ Percentage: 80,
+ },
+ OverallGrade: "A",
+ }
+ require.NoError(t, writeScorecardJSON(scorecard, state.ProofsDir()))
+ require.NoError(t, os.WriteFile(filepath.Join(state.PipelineDir(), "agent-readiness.md"), []byte(`## Agent Readiness
+
+Phase verdict: Pass
+`), 0o644))
+
+ shipPlan, err := GenerateNextPlan(state, PhaseShip)
+ require.NoError(t, err)
+ assert.Contains(t, shipPlan, "Quality: PASS")
+ assert.Contains(t, shipPlan, "Agent readiness: PASS")
+ assert.Contains(t, shipPlan, "**SHIP** - Quality and agent-readiness gates both pass.")
+ assert.NotContains(t, shipPlan, "**HOLD**")
+}
+
+func TestGenerateShipPlanHoldsWhenAgentReadinessMissing(t *testing.T) {
+ setPressTestEnv(t)
+
+ state := NewStateWithRun("sample", filepath.Join(t.TempDir(), "sample-pp-cli"), "run-123", "test-scope")
+ require.NoError(t, state.Save())
+ scorecard := &Scorecard{
+ APIName: "sample",
+ Steinberger: SteinerScore{
+ Percentage: 80,
+ },
+ OverallGrade: "A",
+ }
+ require.NoError(t, writeScorecardJSON(scorecard, state.ProofsDir()))
+
+ shipPlan, err := GenerateNextPlan(state, PhaseShip)
+ require.NoError(t, err)
+ assert.Contains(t, shipPlan, "Quality: PASS")
+ assert.Contains(t, shipPlan, "Agent readiness: HOLD - missing pipeline/agent-readiness.md")
+ assert.Contains(t, shipPlan, "**HOLD**")
+ assert.NotContains(t, shipPlan, "**SHIP** - Quality and agent-readiness gates both pass.")
+}
+
+func TestGenerateShipPlanIgnoresReadinessOutsidePipelineDir(t *testing.T) {
+ setPressTestEnv(t)
+
+ state := NewStateWithRun("sample", filepath.Join(t.TempDir(), "sample-pp-cli"), "run-123", "test-scope")
+ require.NoError(t, state.Save())
+ scorecard := &Scorecard{
+ APIName: "sample",
+ Steinberger: SteinerScore{
+ Percentage: 80,
+ },
+ OverallGrade: "A",
+ }
+ require.NoError(t, writeScorecardJSON(scorecard, state.ProofsDir()))
+ require.NoError(t, os.WriteFile(filepath.Join(state.ProofsDir(), "agent-readiness.md"), []byte(`## Agent Readiness
+
+Phase verdict: Pass
+`), 0o644))
+
+ shipPlan, err := GenerateNextPlan(state, PhaseShip)
+ require.NoError(t, err)
+ assert.Contains(t, shipPlan, "Agent readiness: HOLD - missing pipeline/agent-readiness.md")
+ assert.Contains(t, shipPlan, "**HOLD**")
+ assert.NotContains(t, shipPlan, "**SHIP** - Quality and agent-readiness gates both pass.")
+}
+
+func TestGenerateShipPlanHoldsWhenScorecardMissing(t *testing.T) {
+ setPressTestEnv(t)
+
+ state := NewStateWithRun("sample", filepath.Join(t.TempDir(), "sample-pp-cli"), "run-123", "test-scope")
+ require.NoError(t, state.Save())
+ require.NoError(t, os.WriteFile(filepath.Join(state.PipelineDir(), "agent-readiness.md"), []byte(`## Agent Readiness
+
+Phase verdict: Pass
+`), 0o644))
+
+ shipPlan, err := GenerateNextPlan(state, PhaseShip)
+ require.NoError(t, err)
+ assert.Contains(t, shipPlan, "Quality: HOLD - missing scorecard.json")
+ assert.Contains(t, shipPlan, "Agent readiness: PASS")
+ assert.Contains(t, shipPlan, "**HOLD**")
+ assert.NotContains(t, shipPlan, "**SHIP** - Quality and agent-readiness gates both pass.")
+}
+
+func TestGenerateShipPlanHoldsWhenAgentReadinessVerdictMissing(t *testing.T) {
+ setPressTestEnv(t)
+
+ state := NewStateWithRun("sample", filepath.Join(t.TempDir(), "sample-pp-cli"), "run-123", "test-scope")
+ require.NoError(t, state.Save())
+ scorecard := &Scorecard{
+ APIName: "sample",
+ Steinberger: SteinerScore{
+ Percentage: 80,
+ },
+ OverallGrade: "A",
+ }
+ require.NoError(t, writeScorecardJSON(scorecard, state.ProofsDir()))
+ require.NoError(t, os.WriteFile(filepath.Join(state.PipelineDir(), "agent-readiness.md"), []byte(`## Agent Readiness
+
+A non-Pass verdict such as Degrade must block ship, but this prose is not the verdict line.
+`), 0o644))
+
+ shipPlan, err := GenerateNextPlan(state, PhaseShip)
+ require.NoError(t, err)
+ assert.Contains(t, shipPlan, "Agent readiness: HOLD - agent-readiness verdict not found")
+ assert.Contains(t, shipPlan, "**HOLD**")
+ assert.NotContains(t, shipPlan, "**SHIP** - Quality and agent-readiness gates both pass.")
+}
+
+func TestParseAgentReadinessVerdictRequiresExplicitVerdictLine(t *testing.T) {
+ assert.Equal(t, AgentReadinessDegrade, parseAgentReadinessVerdict("Phase verdict: **Degrade**"))
+ assert.Equal(t, AgentReadinessWarn, parseAgentReadinessVerdict("**Verdict:** Warn"))
+ assert.Empty(t, parseAgentReadinessVerdict("A non-Pass verdict such as Degrade must block ship."))
+}
+
+func TestLoadAgentReadinessReportCollectsTableFindings(t *testing.T) {
+ dir := t.TempDir()
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "agent-readiness.md"), []byte(`## Agent Readiness
+
+Phase verdict: Degrade
+
+| Severity | Finding |
+|---|---|
+| Blocker | mutating commands can run under verify mode |
+`), 0o644))
+
+ report, err := LoadAgentReadinessReport(dir)
+ require.NoError(t, err)
+ assert.Equal(t, AgentReadinessDegrade, report.Verdict)
+ assert.Equal(t, []string{"Blocker: mutating commands can run under verify mode"}, report.Findings)
+}
+
+func TestLoadAgentReadinessReportCollectsNonKeywordBulletFindings(t *testing.T) {
+ dir := t.TempDir()
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "agent-readiness.md"), []byte(`## Agent Readiness
+
+Phase verdict: Degrade
+
+- Fail: command crashes with --json flag
+`), 0o644))
+
+ report, err := LoadAgentReadinessReport(dir)
+ require.NoError(t, err)
+ assert.Equal(t, AgentReadinessDegrade, report.Verdict)
+ assert.Equal(t, []string{"Fail: command crashes with --json flag"}, report.Findings)
+}
diff --git a/internal/pipeline/seeds.go b/internal/pipeline/seeds.go
index 905705b3..853c061c 100644
--- a/internal/pipeline/seeds.go
+++ b/internal/pipeline/seeds.go
@@ -330,7 +330,8 @@ and implement its fixes in a severity-gated loop (max 2 passes) until no Blocker
- 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)
+- {{.PipelineDir}}/agent-readiness.md with a parseable Phase verdict: Pass|Warn|Degrade line
+- Remaining Blocker and Friction findings listed as bullets or table rows in agent-readiness.md
## Prior Phase Outputs
@@ -342,6 +343,7 @@ and implement its fixes in a severity-gated loop (max 2 passes) until no Blocker
- 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)
+- Ship gate parser: internal/pipeline/planner.go LoadAgentReadinessReport
- If the run started in codex mode, preserve that mode here: reviewer runs in Claude, but each accepted fix patch is delegated to Codex and then verified in Claude
`,
PhaseShip: `---
@@ -372,12 +374,14 @@ Package the generated CLI output and produce the final handoff report for humans
## Prior Phase Outputs
- Review score and review.md from the review phase
+- Agent-readiness verdict from agent-readiness.md
- Working CLI binary ready for packaging and handoff
## Codebase Pointers
- Output CLI tree in {{.OutputDir}}
- Review artifacts in {{.PipelineDir}}
+- Agent-readiness report in {{.PipelineDir}}/agent-readiness.md
- Morning report format from SKILL.md Workflow 4 Step 6
`,
}
← 7eee7dfc fix(cli): harden browser-sniff PII placeholders (#1639)
·
back to Cli Printing Press
·
feat(cli): add cliutil.LooksLikeJWT shared validator with le b5b1dd59 →