diff --git a/CHANGELOG.md b/CHANGELOG.md
index 915844f..54da8ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,10 @@ Please choose versions by [Semantic Versioning](http://semver.org/).
* MINOR version when you add functionality in a backwards-compatible manner, and
* PATCH version when you make backwards-compatible bug fixes.
+## Unreleased
+
+- feat: `vault-cli goal set` and `topic set` accept the full canonical task phase set (`todo`, `planning`, `execution`, `ai_review`, `human_review`, `done`), and an invalid phase now exits non-zero with the validator's message listing the accepted values, rendered from the enum collection so the list cannot drift. `in_progress` stays a task-only alias and is still rejected.
+
## v0.174.1
- fix: `session-close` prints its open-goal status lines from a new `session-close-goals.sh` resolver and requires the closer to reproduce them, so a close can no longer drop the line silently. Measured 2026-10-20: one real close ran the phase's own lookup, received `status=in_progress`, and emitted no goal line at all.
diff --git a/integration/cli_test.go b/integration/cli_test.go
index 5a6733a..df5af94 100644
--- a/integration/cli_test.go
+++ b/integration/cli_test.go
@@ -4821,6 +4821,56 @@ page_type: goal
Expect(showPhaseField(configPath, "topic", "Phase Target")).To(Equal("execution"))
})
+ It("topic set accepts ai_review and writes it to the page on disk", func() {
+ vaultPath, configPath, cleanup := createTempVaultWithTopicPages(
+ "Topics",
+ map[string]string{
+ "Phase Target": "---\nstatus: in_progress\n---\n# Phase Target\n",
+ },
+ nil,
+ )
+ defer cleanup()
+
+ cmd := exec.Command(
+ binPath, "--config", configPath, "--vault", "test",
+ "topic", "set", "Phase Target", "phase", "ai_review",
+ )
+ session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
+ Expect(err).NotTo(HaveOccurred())
+ Eventually(session).Should(gexec.Exit(0))
+
+ content, err := os.ReadFile(filepath.Join(vaultPath, "Topics", "Phase Target.md"))
+ Expect(err).NotTo(HaveOccurred())
+ Expect(string(content)).To(ContainSubstring("phase: ai_review"))
+
+ Expect(showPhaseField(configPath, "topic", "Phase Target")).To(Equal("ai_review"))
+ })
+
+ It("topic set accepts human_review and writes it to the page on disk", func() {
+ vaultPath, configPath, cleanup := createTempVaultWithTopicPages(
+ "Topics",
+ map[string]string{
+ "Phase Target": "---\nstatus: in_progress\n---\n# Phase Target\n",
+ },
+ nil,
+ )
+ defer cleanup()
+
+ cmd := exec.Command(
+ binPath, "--config", configPath, "--vault", "test",
+ "topic", "set", "Phase Target", "phase", "human_review",
+ )
+ session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
+ Expect(err).NotTo(HaveOccurred())
+ Eventually(session).Should(gexec.Exit(0))
+
+ content, err := os.ReadFile(filepath.Join(vaultPath, "Topics", "Phase Target.md"))
+ Expect(err).NotTo(HaveOccurred())
+ Expect(string(content)).To(ContainSubstring("phase: human_review"))
+
+ Expect(showPhaseField(configPath, "topic", "Phase Target")).To(Equal("human_review"))
+ })
+
It("topic set refuses a non-canonical phase with the validator's wording and leaves the page byte-identical", func() {
vaultPath, configPath, cleanup := createTempVaultWithTopicPages(
"Topics",
@@ -4849,7 +4899,51 @@ page_type: goal
Expect(err).NotTo(HaveOccurred())
Eventually(session).Should(gexec.Exit(1))
- Expect(string(session.Err.Contents())).To(ContainSubstring("unknown topic phase 'bogus'"))
+ stderr := string(session.Err.Contents())
+ Expect(stderr).To(ContainSubstring("unknown topic phase 'bogus'"))
+ for _, valid := range []string{
+ "todo", "planning", "execution", "ai_review", "human_review", "done",
+ } {
+ Expect(stderr).To(ContainSubstring(valid))
+ }
+ Expect(sha256OfFile(topicPath)).To(Equal(before))
+ })
+
+ It("topic set refuses in_progress as a phase and names the six accepted values", func() {
+ vaultPath, configPath, cleanup := createTempVaultWithTopicPages(
+ "Topics",
+ map[string]string{
+ "Phase Target": "---\nstatus: in_progress\n---\n# Phase Target\n",
+ },
+ nil,
+ )
+ defer cleanup()
+
+ sha256OfFile := func(path string) string {
+ data, err := os.ReadFile(path)
+ Expect(err).NotTo(HaveOccurred())
+ sum := sha256.Sum256(data)
+ return fmt.Sprintf("%x", sum)
+ }
+
+ topicPath := filepath.Join(vaultPath, "Topics", "Phase Target.md")
+ before := sha256OfFile(topicPath)
+
+ cmd := exec.Command(
+ binPath, "--config", configPath, "--vault", "test",
+ "topic", "set", "Phase Target", "phase", "in_progress",
+ )
+ session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
+ Expect(err).NotTo(HaveOccurred())
+ Eventually(session).Should(gexec.Exit(1))
+
+ stderr := string(session.Err.Contents())
+ Expect(stderr).To(ContainSubstring("unknown topic phase 'in_progress'"))
+ for _, valid := range []string{
+ "todo", "planning", "execution", "ai_review", "human_review", "done",
+ } {
+ Expect(stderr).To(ContainSubstring(valid))
+ }
Expect(sha256OfFile(topicPath)).To(Equal(before))
})
@@ -5406,4 +5500,158 @@ page_type: goal
})
})
+ Describe("vault-cli goal command family", func() {
+ It("goal set accepts ai_review and writes it to the page on disk", func() {
+ vaultPath, configPath, cleanup := createTempVaultWithTopicPages(
+ "Topics",
+ nil,
+ map[string]string{
+ "Phase Target": "---\nstatus: in_progress\n---\n# Phase Target\n",
+ },
+ )
+ defer cleanup()
+
+ cmd := exec.Command(
+ binPath, "--config", configPath, "--vault", "test",
+ "goal", "set", "Phase Target", "phase", "ai_review",
+ )
+ session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
+ Expect(err).NotTo(HaveOccurred())
+ Eventually(session).Should(gexec.Exit(0))
+
+ content, err := os.ReadFile(filepath.Join(vaultPath, "Goals", "Phase Target.md"))
+ Expect(err).NotTo(HaveOccurred())
+ Expect(string(content)).To(ContainSubstring("phase: ai_review"))
+
+ Expect(showPhaseField(configPath, "goal", "Phase Target")).To(Equal("ai_review"))
+ })
+
+ It("goal set accepts human_review and writes it to the page on disk", func() {
+ vaultPath, configPath, cleanup := createTempVaultWithTopicPages(
+ "Topics",
+ nil,
+ map[string]string{
+ "Phase Target": "---\nstatus: in_progress\n---\n# Phase Target\n",
+ },
+ )
+ defer cleanup()
+
+ cmd := exec.Command(
+ binPath, "--config", configPath, "--vault", "test",
+ "goal", "set", "Phase Target", "phase", "human_review",
+ )
+ session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
+ Expect(err).NotTo(HaveOccurred())
+ Eventually(session).Should(gexec.Exit(0))
+
+ content, err := os.ReadFile(filepath.Join(vaultPath, "Goals", "Phase Target.md"))
+ Expect(err).NotTo(HaveOccurred())
+ Expect(string(content)).To(ContainSubstring("phase: human_review"))
+
+ Expect(showPhaseField(configPath, "goal", "Phase Target")).To(Equal("human_review"))
+ })
+
+ It("goal set refuses a non-canonical phase with the six accepted values and leaves the page byte-identical", func() {
+ vaultPath, configPath, cleanup := createTempVaultWithTopicPages(
+ "Topics",
+ nil,
+ map[string]string{
+ "Phase Target": "---\nstatus: in_progress\n---\n# Phase Target\n",
+ },
+ )
+ defer cleanup()
+
+ sha256OfFile := func(path string) string {
+ data, err := os.ReadFile(path)
+ Expect(err).NotTo(HaveOccurred())
+ sum := sha256.Sum256(data)
+ return fmt.Sprintf("%x", sum)
+ }
+
+ goalPath := filepath.Join(vaultPath, "Goals", "Phase Target.md")
+ before := sha256OfFile(goalPath)
+
+ cmd := exec.Command(
+ binPath, "--config", configPath, "--vault", "test",
+ "goal", "set", "Phase Target", "phase", "bogus",
+ )
+ session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
+ Expect(err).NotTo(HaveOccurred())
+ Eventually(session).Should(gexec.Exit(1))
+
+ stderr := string(session.Err.Contents())
+ Expect(stderr).To(ContainSubstring("unknown goal phase 'bogus'"))
+ for _, valid := range []string{
+ "todo", "planning", "execution", "ai_review", "human_review", "done",
+ } {
+ Expect(stderr).To(ContainSubstring(valid))
+ }
+ Expect(sha256OfFile(goalPath)).To(Equal(before))
+ })
+
+ It("goal set refuses in_progress as a phase and names the six accepted values", func() {
+ vaultPath, configPath, cleanup := createTempVaultWithTopicPages(
+ "Topics",
+ nil,
+ map[string]string{
+ "Phase Target": "---\nstatus: in_progress\n---\n# Phase Target\n",
+ },
+ )
+ defer cleanup()
+
+ sha256OfFile := func(path string) string {
+ data, err := os.ReadFile(path)
+ Expect(err).NotTo(HaveOccurred())
+ sum := sha256.Sum256(data)
+ return fmt.Sprintf("%x", sum)
+ }
+
+ goalPath := filepath.Join(vaultPath, "Goals", "Phase Target.md")
+ before := sha256OfFile(goalPath)
+
+ cmd := exec.Command(
+ binPath, "--config", configPath, "--vault", "test",
+ "goal", "set", "Phase Target", "phase", "in_progress",
+ )
+ session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
+ Expect(err).NotTo(HaveOccurred())
+ Eventually(session).Should(gexec.Exit(1))
+
+ stderr := string(session.Err.Contents())
+ Expect(stderr).To(ContainSubstring("unknown goal phase 'in_progress'"))
+ for _, valid := range []string{
+ "todo", "planning", "execution", "ai_review", "human_review", "done",
+ } {
+ Expect(stderr).To(ContainSubstring(valid))
+ }
+ Expect(sha256OfFile(goalPath)).To(Equal(before))
+ })
+
+ It("goal get reads a hand-typed in_progress phase back unchanged", func() {
+ vaultPath, configPath, cleanup := createTempVaultWithTopicPages(
+ "Topics",
+ nil,
+ map[string]string{
+ "Legacy Phase": "---\nstatus: in_progress\nphase: in_progress\n---\n# Legacy Phase\n",
+ },
+ )
+ defer cleanup()
+
+ Expect(showPhaseField(configPath, "goal", "Legacy Phase")).To(Equal("in_progress"))
+
+ cmd := exec.Command(
+ binPath, "--config", configPath, "--vault", "test",
+ "goal", "get", "Legacy Phase", "phase",
+ )
+ session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
+ Expect(err).NotTo(HaveOccurred())
+ Eventually(session).Should(gexec.Exit(0))
+ Expect(string(session.Out.Contents())).To(Equal("in_progress\n"))
+
+ content, err := os.ReadFile(filepath.Join(vaultPath, "Goals", "Legacy Phase.md"))
+ Expect(err).NotTo(HaveOccurred())
+ Expect(string(content)).To(ContainSubstring("phase: in_progress"))
+ })
+ })
+
})
diff --git a/pkg/domain/goal_frontmatter.go b/pkg/domain/goal_frontmatter.go
index 4d258b6..bf299c4 100644
--- a/pkg/domain/goal_frontmatter.go
+++ b/pkg/domain/goal_frontmatter.go
@@ -379,7 +379,7 @@ func (f *GoalFrontmatter) setPhaseField(ctx context.Context, value string) error
}
phase := GoalPhase(value)
if err := phase.Validate(ctx); err != nil {
- return errors.Wrapf(ctx, validation.Error, "unknown goal phase '%s'", value)
+ return err
}
f.SetPhase(&phase)
return nil
diff --git a/pkg/domain/goal_frontmatter_test.go b/pkg/domain/goal_frontmatter_test.go
index 4dc729c..786317b 100644
--- a/pkg/domain/goal_frontmatter_test.go
+++ b/pkg/domain/goal_frontmatter_test.go
@@ -424,6 +424,8 @@ var _ = Describe("GoalFrontmatter", func() {
Entry("todo", "todo"),
Entry("planning", "planning"),
Entry("execution", "execution"),
+ Entry("ai_review", "ai_review"),
+ Entry("human_review", "human_review"),
Entry("done", "done"),
)
diff --git a/pkg/domain/goal_phase.go b/pkg/domain/goal_phase.go
index 4e88a48..e91db06 100644
--- a/pkg/domain/goal_phase.go
+++ b/pkg/domain/goal_phase.go
@@ -2,10 +2,11 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package domain
+package domain //nolint:dupl // Goal and topic phases share the same enum shape but are distinct types
import (
"context"
+ "strings"
"github.com/bborbe/collection"
"github.com/bborbe/errors"
@@ -22,6 +23,10 @@ const (
GoalPhasePlanning GoalPhase = "planning"
// GoalPhaseExecution means active work is underway.
GoalPhaseExecution GoalPhase = "execution"
+ // GoalPhaseAIReview means automated checks are running.
+ GoalPhaseAIReview GoalPhase = "ai_review"
+ // GoalPhaseHumanReview means manual review is required.
+ GoalPhaseHumanReview GoalPhase = "human_review"
// GoalPhaseDone means the goal is ready to close.
GoalPhaseDone GoalPhase = "done"
)
@@ -31,6 +36,8 @@ var AvailableGoalPhases = GoalPhases{
GoalPhaseTodo,
GoalPhasePlanning,
GoalPhaseExecution,
+ GoalPhaseAIReview,
+ GoalPhaseHumanReview,
GoalPhaseDone,
}
@@ -47,10 +54,20 @@ func (g GoalPhase) String() string {
return string(g)
}
+// String returns the comma-separated list of phases in declaration order.
+func (g GoalPhases) String() string {
+ parts := make([]string, 0, len(g))
+ for _, p := range g {
+ parts = append(parts, string(p))
+ }
+ return strings.Join(parts, ", ")
+}
+
// Validate returns an error if the phase is not a valid canonical value.
func (g GoalPhase) Validate(ctx context.Context) error {
if !AvailableGoalPhases.Contains(g) {
- return errors.Wrapf(ctx, validation.Error, "unknown goal phase '%s'", g)
+ return errors.Wrapf(ctx, validation.Error,
+ "unknown goal phase '%s' (valid: %s)", g, AvailableGoalPhases)
}
return nil
}
diff --git a/pkg/domain/goal_phase_test.go b/pkg/domain/goal_phase_test.go
index 58f0117..fda3df0 100644
--- a/pkg/domain/goal_phase_test.go
+++ b/pkg/domain/goal_phase_test.go
@@ -6,6 +6,7 @@ package domain_test
import (
"context"
+ "strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@@ -28,6 +29,8 @@ var _ = Describe("GoalPhase", func() {
Entry("todo", domain.GoalPhaseTodo),
Entry("planning", domain.GoalPhasePlanning),
Entry("execution", domain.GoalPhaseExecution),
+ Entry("ai_review", domain.GoalPhaseAIReview),
+ Entry("human_review", domain.GoalPhaseHumanReview),
Entry("done", domain.GoalPhaseDone),
)
@@ -52,6 +55,17 @@ var _ = Describe("GoalPhase", func() {
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("unknown goal phase"))
})
+
+ It("names the value and lists the accepted set", func() {
+ err := domain.GoalPhase("bogus").Validate(ctx)
+ Expect(err).NotTo(BeNil())
+ Expect(err.Error()).To(ContainSubstring("bogus"))
+ parts := make([]string, 0, len(domain.AvailableGoalPhases))
+ for _, p := range domain.AvailableGoalPhases {
+ parts = append(parts, string(p))
+ }
+ Expect(err.Error()).To(ContainSubstring(strings.Join(parts, ", ")))
+ })
})
})
@@ -83,6 +97,8 @@ var _ = Describe("GoalPhase", func() {
Expect(domain.AvailableGoalPhases.Contains(domain.GoalPhaseTodo)).To(BeTrue())
Expect(domain.AvailableGoalPhases.Contains(domain.GoalPhasePlanning)).To(BeTrue())
Expect(domain.AvailableGoalPhases.Contains(domain.GoalPhaseExecution)).To(BeTrue())
+ Expect(domain.AvailableGoalPhases.Contains(domain.GoalPhaseAIReview)).To(BeTrue())
+ Expect(domain.AvailableGoalPhases.Contains(domain.GoalPhaseHumanReview)).To(BeTrue())
Expect(domain.AvailableGoalPhases.Contains(domain.GoalPhaseDone)).To(BeTrue())
})
@@ -94,4 +110,23 @@ var _ = Describe("GoalPhase", func() {
).To(BeFalse())
})
})
+
+ Describe("AvailableGoalPhases", func() {
+ It("matches the canonical task phase set element for element", func() {
+ Expect(domain.AvailableGoalPhases).To(HaveLen(6))
+ Expect(domain.AvailableTaskPhases).To(HaveLen(6))
+ for i := range domain.AvailableGoalPhases {
+ Expect(string(domain.AvailableGoalPhases[i])).
+ To(Equal(string(domain.AvailableTaskPhases[i])))
+ }
+ })
+
+ It("renders the accepted set comma-separated in declaration order", func() {
+ parts := make([]string, 0, len(domain.AvailableTaskPhases))
+ for _, p := range domain.AvailableTaskPhases {
+ parts = append(parts, string(p))
+ }
+ Expect(domain.AvailableGoalPhases.String()).To(Equal(strings.Join(parts, ", ")))
+ })
+ })
})
diff --git a/pkg/domain/topic_frontmatter_test.go b/pkg/domain/topic_frontmatter_test.go
index c58b7ff..548788b 100644
--- a/pkg/domain/topic_frontmatter_test.go
+++ b/pkg/domain/topic_frontmatter_test.go
@@ -109,6 +109,8 @@ var _ = Describe("TopicFrontmatter", func() {
Entry("todo", "todo"),
Entry("planning", "planning"),
Entry("execution", "execution"),
+ Entry("ai_review", "ai_review"),
+ Entry("human_review", "human_review"),
Entry("done", "done"),
)
diff --git a/pkg/domain/topic_phase.go b/pkg/domain/topic_phase.go
index eb16bbf..09a5723 100644
--- a/pkg/domain/topic_phase.go
+++ b/pkg/domain/topic_phase.go
@@ -2,10 +2,11 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package domain
+package domain //nolint:dupl // Goal and topic phases share the same enum shape but are distinct types
import (
"context"
+ "strings"
"github.com/bborbe/collection"
"github.com/bborbe/errors"
@@ -22,6 +23,10 @@ const (
TopicPhasePlanning TopicPhase = "planning"
// TopicPhaseExecution means active work is underway.
TopicPhaseExecution TopicPhase = "execution"
+ // TopicPhaseAIReview means automated checks are running.
+ TopicPhaseAIReview TopicPhase = "ai_review"
+ // TopicPhaseHumanReview means manual review is required.
+ TopicPhaseHumanReview TopicPhase = "human_review"
// TopicPhaseDone means the topic is ready to close.
TopicPhaseDone TopicPhase = "done"
)
@@ -31,6 +36,8 @@ var AvailableTopicPhases = TopicPhases{
TopicPhaseTodo,
TopicPhasePlanning,
TopicPhaseExecution,
+ TopicPhaseAIReview,
+ TopicPhaseHumanReview,
TopicPhaseDone,
}
@@ -47,10 +54,20 @@ func (t TopicPhase) String() string {
return string(t)
}
+// String returns the comma-separated list of phases in declaration order.
+func (t TopicPhases) String() string {
+ parts := make([]string, 0, len(t))
+ for _, p := range t {
+ parts = append(parts, string(p))
+ }
+ return strings.Join(parts, ", ")
+}
+
// Validate returns an error if the phase is not a valid canonical value.
func (t TopicPhase) Validate(ctx context.Context) error {
if !AvailableTopicPhases.Contains(t) {
- return errors.Wrapf(ctx, validation.Error, "unknown topic phase '%s'", t)
+ return errors.Wrapf(ctx, validation.Error,
+ "unknown topic phase '%s' (valid: %s)", t, AvailableTopicPhases)
}
return nil
}
diff --git a/pkg/domain/topic_phase_test.go b/pkg/domain/topic_phase_test.go
index 6dfee89..4088aa2 100644
--- a/pkg/domain/topic_phase_test.go
+++ b/pkg/domain/topic_phase_test.go
@@ -6,6 +6,7 @@ package domain_test
import (
"context"
+ "strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@@ -28,6 +29,8 @@ var _ = Describe("TopicPhase", func() {
Entry("todo", domain.TopicPhaseTodo),
Entry("planning", domain.TopicPhasePlanning),
Entry("execution", domain.TopicPhaseExecution),
+ Entry("ai_review", domain.TopicPhaseAIReview),
+ Entry("human_review", domain.TopicPhaseHumanReview),
Entry("done", domain.TopicPhaseDone),
)
@@ -53,11 +56,15 @@ var _ = Describe("TopicPhase", func() {
Expect(err.Error()).To(ContainSubstring("unknown topic phase"))
})
- It("returns an error for the task-only phase ai_review", func() {
- phase := domain.TopicPhase("ai_review")
- err := phase.Validate(ctx)
+ It("names the value and lists the accepted set", func() {
+ err := domain.TopicPhase("bogus").Validate(ctx)
Expect(err).NotTo(BeNil())
- Expect(err.Error()).To(ContainSubstring("unknown topic phase"))
+ Expect(err.Error()).To(ContainSubstring("bogus"))
+ parts := make([]string, 0, len(domain.AvailableTopicPhases))
+ for _, p := range domain.AvailableTopicPhases {
+ parts = append(parts, string(p))
+ }
+ Expect(err.Error()).To(ContainSubstring(strings.Join(parts, ", ")))
})
})
})
@@ -90,6 +97,8 @@ var _ = Describe("TopicPhase", func() {
Expect(domain.AvailableTopicPhases.Contains(domain.TopicPhaseTodo)).To(BeTrue())
Expect(domain.AvailableTopicPhases.Contains(domain.TopicPhasePlanning)).To(BeTrue())
Expect(domain.AvailableTopicPhases.Contains(domain.TopicPhaseExecution)).To(BeTrue())
+ Expect(domain.AvailableTopicPhases.Contains(domain.TopicPhaseAIReview)).To(BeTrue())
+ Expect(domain.AvailableTopicPhases.Contains(domain.TopicPhaseHumanReview)).To(BeTrue())
Expect(domain.AvailableTopicPhases.Contains(domain.TopicPhaseDone)).To(BeTrue())
})
@@ -101,4 +110,23 @@ var _ = Describe("TopicPhase", func() {
).To(BeFalse())
})
})
+
+ Describe("AvailableTopicPhases", func() {
+ It("matches the canonical task phase set element for element", func() {
+ Expect(domain.AvailableTopicPhases).To(HaveLen(6))
+ Expect(domain.AvailableTaskPhases).To(HaveLen(6))
+ for i := range domain.AvailableTopicPhases {
+ Expect(string(domain.AvailableTopicPhases[i])).
+ To(Equal(string(domain.AvailableTaskPhases[i])))
+ }
+ })
+
+ It("renders the accepted set comma-separated in declaration order", func() {
+ parts := make([]string, 0, len(domain.AvailableTaskPhases))
+ for _, p := range domain.AvailableTaskPhases {
+ parts = append(parts, string(p))
+ }
+ Expect(domain.AvailableTopicPhases.String()).To(Equal(strings.Join(parts, ", ")))
+ })
+ })
})
diff --git a/pkg/ops/frontmatter_entity_test.go b/pkg/ops/frontmatter_entity_test.go
index 4a8c312..9f359e1 100644
--- a/pkg/ops/frontmatter_entity_test.go
+++ b/pkg/ops/frontmatter_entity_test.go
@@ -1176,6 +1176,8 @@ var _ = Describe("NewTopicSetOperation", func() {
Entry("todo", "todo"),
Entry("planning", "planning"),
Entry("execution", "execution"),
+ Entry("ai_review", "ai_review"),
+ Entry("human_review", "human_review"),
Entry("done", "done"),
)
})
diff --git a/prompts/completed/261-spec-062-phase-enum.md b/prompts/completed/261-spec-062-phase-enum.md
new file mode 100644
index 0000000..86c5d93
--- /dev/null
+++ b/prompts/completed/261-spec-062-phase-enum.md
@@ -0,0 +1,97 @@
+---
+status: completed
+spec: [062-goal-topic-phase-set]
+summary: Widened goal/topic phase enums to the six canonical task phases and derived the valid-value list in validator errors from the enum collections
+execution_id: vault-cli-goal-topic-phase-set-exec-261-spec-062-phase-enum
+dark-factory-version: v0.198.7
+created: "2026-10-20T10:50:00Z"
+queued: "2026-10-20T10:51:07Z"
+started: "2026-10-20T10:52:53Z"
+completed: "2026-10-20T10:57:59Z"
+---
+
+# Widen the goal and topic phase enums and derive the valid-value list in the error (spec 062, prompt 1 of 2)
+
+
+- A goal or topic accepts exactly the six canonical task phases (`todo`, `planning`, `execution`, `ai_review`, `human_review`, `done`).
+- An invalid phase produces an error that names the value and lists the accepted set.
+- The accepted set is rendered from the enum collection itself, so it cannot drift when a phase is added.
+- `vault-cli goal set … phase bogus` now prints the validator's message instead of a locally rebuilt one.
+- `in_progress` stays rejected by goal and topic validation.
+- Unit tests lock the enum equality, the valid/invalid values, and the derived message.
+
+
+
+A goal or topic accepts the same six phases a task does, and an invalid phase is refused with a message that lists every accepted value, derived from the enum itself so the list cannot drift.
+
+
+
+Read `CLAUDE.md` for project conventions and `docs/development-patterns.md` for the entity layout.
+Read these files before editing:
+- `pkg/domain/task_phase.go` — the canonical six-value set (`AvailableTaskPhases`); `in_progress` is an alias, not a member.
+- `pkg/domain/goal_phase.go` and `pkg/domain/topic_phase.go` — the two four-value enums to widen.
+- `pkg/domain/goal_frontmatter.go` (`setPhaseField`, ~line 371) and `pkg/domain/topic_frontmatter.go` (`setPhaseField`, ~line 82).
+- `specs/completed/052-topic-phase-field.md` — the constraint that the canonical set is defined once and the error is the validator's own wording.
+
+
+
+1. In `pkg/domain/goal_phase.go`, add the two constants after `GoalPhaseExecution` and before `GoalPhaseDone`, keeping the existing doc-comment style:
+ ```go
+ // GoalPhaseAIReview means automated checks are running.
+ GoalPhaseAIReview GoalPhase = "ai_review"
+ // GoalPhaseHumanReview means manual review is required.
+ GoalPhaseHumanReview GoalPhase = "human_review"
+ ```
+ Add both to `AvailableGoalPhases` in declaration order: `todo, planning, execution, ai_review, human_review, done`.
+
+2. In `pkg/domain/topic_phase.go`, add the same two constants (`TopicPhaseAIReview`, `TopicPhaseHumanReview`) and add both to `AvailableTopicPhases` in the same order.
+
+3. Add a `String() string` method on the **plural** collection types `GoalPhases` and `TopicPhases` that renders the values comma-separated with a space (`todo, planning, execution, ai_review, human_review, done`), in declaration order. Use `strings.Join` over a preallocated slice — `parts := make([]string, 0, len(g))`, then `parts = append(parts, string(p))` in a `range` loop (the `prealloc` linter flags an unpreallocated append slice); add the `strings` import where missing. Do not rename or change the existing singular `String()` methods.
+
+4. Change each `Validate` error to include the accepted set, formatted from the collection:
+ ```go
+ return errors.Wrapf(ctx, validation.Error,
+ "unknown goal phase '%s' (valid: %s)", g, AvailableGoalPhases)
+ ```
+ and the topic equivalent. Keep the membership check `if !AvailableXs.Contains(v)` — no switch.
+
+5. In `pkg/domain/goal_frontmatter.go` `setPhaseField`, replace
+ ```go
+ return errors.Wrapf(ctx, validation.Error, "unknown goal phase '%s'", value)
+ ```
+ with
+ ```go
+ return err
+ ```
+ so the validator's message (including the valid set) reaches the CLI. Keep the empty-value branch that clears the key. The `errors` and `validation` imports stay — both are used elsewhere in the file.
+
+6. Leave `pkg/domain/topic_frontmatter.go` `setPhaseField` unchanged: it already returns `errors.Wrap(ctx, err, "invalid topic phase")`, which now carries the valid set because the validator does.
+
+7. Update the unit tests:
+ - `pkg/domain/goal_phase_test.go`: add `ai_review` and `human_review` entries to the valid `DescribeTable`; add `ai_review` and `human_review` `Contains` assertions to the existing `AvailableGoalPhases.Contains` block; add a test asserting `AvailableGoalPhases` equals `AvailableTaskPhases` element-for-element (length 6, same order) by comparing `string(AvailableGoalPhases[i])` to `string(AvailableTaskPhases[i])` — a direct Gomega `Equal` compares distinct types (`GoalPhases` vs `TaskPhases`) and fails even when the values match; add a test asserting `AvailableGoalPhases.String()` equals `todo, planning, execution, ai_review, human_review, done`; add a test asserting `GoalPhase("bogus").Validate(ctx)`'s error contains the string built by iterating `AvailableGoalPhases` and joining with `", "`. Keep the `in_progress` rejection test.
+ - `pkg/domain/topic_phase_test.go`: the same additions (`DescribeTable` entries, `Contains` assertions, element-wise equality against `AvailableTaskPhases`, the `String()` contract, the derived-message test). **Invert** the existing `It("returns an error for the task-only phase ai_review")` — `ai_review` is now valid, so move it into the valid table and delete that rejection case. Keep the `in_progress` rejection test.
+ - `pkg/domain/goal_frontmatter_test.go`: add `ai_review` and `human_review` entries to the `SetField phase` / "accepts canonical values" table.
+ - `pkg/domain/topic_frontmatter_test.go`: the same table additions.
+ - `pkg/ops/frontmatter_entity_test.go`: add `ai_review` and `human_review` entries to the topic "setting each canonical phase value" `DescribeTableSubtree`.
+
+8. Before finishing, re-run the `` block and walk each requirement above against the diff.
+
+
+
+- Do NOT commit — dark-factory handles git.
+- Do NOT change task phase validation, statuses, lint rules, or the `show` / `get` / `list` read paths.
+- Do NOT accept `in_progress` in either goal or topic validation — it stays an alias-only value.
+- Do NOT duplicate the six-value list as a literal anywhere in `pkg/domain/`; the message must format the collection.
+- Existing tests must still pass after the `ai_review` rejection case is inverted.
+- Follow the enum pattern already implemented in `pkg/domain/task_phase.go` (constants → `AvailableXs` collection → `Contains` → `Validate`); the generic guide is `~/.claude-yolo/plugins/marketplaces/coding/docs/go-enum-type-pattern.md`.
+
+
+
+Run `make precommit` — must pass.
+
+Then:
+- `go test ./pkg/domain/... ./pkg/ops/...` — must pass.
+- `! grep -rq 'todo, planning' pkg/domain/` — must succeed (no inline literal list in the domain package).
+- `! grep -q 'unknown goal phase' pkg/domain/goal_frontmatter.go` — must succeed (the local rebuild is gone).
+- `grep -n 'ai_review' pkg/domain/goal_phase.go pkg/domain/topic_phase.go` — must return matches in both files, including the constant declaration and the `Available` collection entry.
+
diff --git a/prompts/completed/262-spec-062-integration-changelog.md b/prompts/completed/262-spec-062-integration-changelog.md
new file mode 100644
index 0000000..47c4eeb
--- /dev/null
+++ b/prompts/completed/262-spec-062-integration-changelog.md
@@ -0,0 +1,74 @@
+---
+status: completed
+spec: [062-goal-topic-phase-set]
+summary: Added end-to-end integration coverage for the widened goal/topic phase enums (ai_review, human_review accepted; bogus and in_progress rejected with the six valid values listed) and an Unreleased CHANGELOG bullet.
+execution_id: vault-cli-goal-topic-phase-set-exec-262-spec-062-integration-changelog
+dark-factory-version: v0.198.7
+created: "2026-10-20T10:52:00Z"
+queued: "2026-10-20T10:51:07Z"
+started: "2026-10-20T10:58:00Z"
+completed: "2026-10-20T11:00:33Z"
+---
+
+# Lock the widened goal/topic phase set end-to-end and record it (spec 062, prompt 2 of 2)
+
+
+- Integration specs prove `goal set` and `topic set` accept `ai_review` and `human_review` and write them to disk.
+- Integration specs prove `goal set … phase bogus` and `topic set … phase bogus` exit 1 and print the six valid values.
+- The invalid-phase probes leave the page byte-identical.
+- `in_progress` stays rejected by both verbs.
+- The changelog records the widened enum and the listed-valid-values error under an unreleased heading.
+
+
+
+Add end-to-end CLI coverage for the widened goal and topic phase enums and a `## Unreleased` CHANGELOG bullet, so the behavior is locked at the command boundary rather than only at the domain layer.
+
+
+
+Read `CLAUDE.md` and `docs/development-patterns.md` first, and `coding/docs/go-testing-guide.md` for the Ginkgo/Gomega integration-test conventions.
+Read these files before editing:
+- `integration/cli_test.go` — helpers `createTempVaultWithTopicPages` (~line 188, creates both `Goals/` and `Topics/`), `showPhaseField` (~line 283, reads `.fields.phase` from ` show --output json`), and the existing topic phase block (~line 4799-4900) whose `bogus` test already asserts exit 1 and the stderr wording.
+- `CHANGELOG.md` — the preamble ends before the newest `## vX.Y.Z`; `scripts/check-changelog.sh` requires `## Unreleased` to sit below the preamble and above the newest release section.
+- `specs/in-progress/062-goal-topic-phase-set.md` — Acceptance Criteria 2-5 and 8.
+
+
+
+1. In `integration/cli_test.go`, add a new nested `Describe("vault-cli goal command family", func() { ... })` immediately after the existing `Describe("vault-cli topic command family", ...)` block (starts ~line 4652; its phase tests begin ~line 4799), and put the goal phase `It`s inside it. Use `createTempVaultWithTopicPages("Topics", nil, map[string]string{...})` so a `Goals/` page exists. Cover:
+ - `goal set phase ai_review` exits 0, the file contains `phase: ai_review`, and `showPhaseField(configPath, "goal", )` returns `ai_review`.
+ - `goal set phase human_review` exits 0, the file contains `phase: human_review`, and `showPhaseField` returns `human_review`.
+ - `goal set phase bogus` exits 1; stderr contains `unknown goal phase 'bogus'` and the six values `todo, planning, execution, ai_review, human_review, done`; the file's sha256 is unchanged before and after.
+ - `goal set phase in_progress` exits 1, and stderr contains `unknown goal phase 'in_progress'` plus the six accepted values.
+ - A page hand-typed to `phase: in_progress` reads back unchanged: build the goal fixture with `phase: in_progress` and assert `showPhaseField(configPath, "goal", )` returns `in_progress` and `goal get phase` prints it — the read path validates nothing (spec Failure Modes row 2).
+
+2. In the same file, extend the existing topic phase block:
+ - Add `topic set phase ai_review` and `phase human_review` success cases (exit 0, `phase: ai_review` / `phase: human_review` on disk, `showPhaseField(configPath, "topic", )` returns the value).
+ - Extend the existing `topic set … phase bogus` assertion so stderr also contains all six values, not only `unknown topic phase 'bogus'`.
+ - Add `topic set phase in_progress` exits 1, and stderr contains `unknown topic phase 'in_progress'` plus the six accepted values.
+
+3. Use the existing test idioms: `exec.Command(binPath, "--config", configPath, "--vault", "test", ...)` + `gexec.Start` + `Eventually(session).Should(gexec.Exit(N))`; read files with `os.ReadFile(path)` and the `//#nosec G304 -- test file` comment where the existing tests carry it. Compute the sha256 helper locally in the test (as the existing topic `bogus` test does) rather than adding a package-level helper.
+
+4. In `CHANGELOG.md`, insert a `## Unreleased` section directly below the preamble (`All notable changes…` and the `* MAJOR / MINOR / PATCH` lines) and above the newest `## vX.Y.Z`, with one bullet:
+ ```markdown
+ - feat: `vault-cli goal set` and `topic set` accept the full task phase set (`todo`, `planning`, `execution`, `ai_review`, `human_review`, `done`), and an invalid phase now exits non-zero with a message listing the accepted values, rendered from the enum.
+ ```
+
+5. Before finishing, re-run the `` block and walk each requirement against the diff.
+
+
+
+- Do NOT commit — dark-factory handles git.
+- Do NOT change production code in this prompt; it is tests plus the CHANGELOG only. If a spec fails because prompt 1's behavior is missing, stop and report rather than editing `pkg/`.
+- Keep `## Unreleased` below the preamble — never between the `# Changelog` title and the preamble.
+- Do not add a new scenario file; the spec's scenario coverage is NO new scenario.
+- Existing integration tests must still pass.
+
+
+
+Run `make precommit` — must pass.
+
+Then:
+- `go test ./integration/...` — must pass.
+- `sed -n '/^## Unreleased/,/^## v/p' CHANGELOG.md | grep -q ai_review` — must succeed.
+- `grep -n "unknown goal phase 'bogus'" integration/cli_test.go` — must return at least one match.
+- `grep -n 'human_review' integration/cli_test.go` — must return matches for both goal and topic cases.
+
diff --git a/specs/in-progress/062-goal-topic-phase-set.md b/specs/in-progress/062-goal-topic-phase-set.md
new file mode 100644
index 0000000..47b0159
--- /dev/null
+++ b/specs/in-progress/062-goal-topic-phase-set.md
@@ -0,0 +1,115 @@
+---
+status: verifying
+approved: "2026-10-20T10:46:01Z"
+generating: "2026-10-20T10:51:08Z"
+prompted: "2026-10-20T10:51:08Z"
+verifying: "2026-10-20T11:00:34Z"
+branch: dark-factory/goal-topic-phase-set
+---
+
+## Summary
+
+- Goal and topic phase validation currently accepts four values (`todo`, `planning`, `execution`, `done`) and rejects the two review phases a task accepts (`ai_review`, `human_review`).
+- The rejection message names only the invalid value; it never lists the accepted set, so a caller cannot learn the valid values from the error.
+- This spec makes both enums equal the six canonical task phases and derives the "valid values" list in the error from the enum itself, so the message can never drift from the set.
+- It reverses the four-value decision recorded in `specs/completed/024-goal-phase-field.md` and `specs/completed/052-topic-phase-field.md`, both of which said extending the enum is a separate spec — this is that spec.
+
+## Problem
+
+`vault-cli goal set phase ai_review` and the topic equivalent fail, while `vault-cli task set phase ai_review` succeeds. The parent goal `Topics and Goals Run on the Same Phase Lifecycle and Manager Tree as Tasks` requires goals and topics to accept exactly the task phase set, so the manager lifecycle can advance a goal or topic through the same review stages a task uses. Today the mismatch blocks that lifecycle and the error does not tell the caller what values are accepted: `goal set phase bogus` prints `unknown goal phase 'bogus': validation error`, exit 1.
+
+## Goal
+
+A goal or topic accepts exactly the six canonical task phases (`todo`, `planning`, `execution`, `ai_review`, `human_review`, `done`), and an invalid phase exits non-zero with a message that lists all six values, rendered from the same enum the validator checks.
+
+## Assumptions
+
+- The six-value set is `AvailableTaskPhases` in declaration order.
+- `in_progress` stays a task-only read alias (`NormalizeTaskPhase` → `execution`), not a canonical value; goal/topic validation keeps rejecting it.
+- The "extending the enum is a separate spec" deferral in `specs/completed/024-goal-phase-field.md` and `052-topic-phase-field.md` is discharged by this spec.
+- Topic `setPhaseField` already wraps the validator's error; goal `setPhaseField` does not and must change.
+- `detectStatusPhaseMismatch` already treats `ai_review` and `human_review` as active phases, so no lint change is needed.
+- `pkg/domain/topic_phase_test.go` currently asserts "returns an error for the task-only phase `ai_review`"; that case is inverted by this change, and the valid-value `DescribeTable` lists in `goal_phase_test.go` and `topic_phase_test.go` gain `ai_review` and `human_review`.
+
+## Non-goals
+
+- Accepting `in_progress` as a goal/topic alias.
+- The `goal approve` / `topic approve` verbs and the `todo → planning` set-path guard (owned by the vault task `Goals and Topics Carry a Phase Field Gating Manager Start`).
+- Manager-start gating on the phase field.
+- Backfilling `phase` on existing goal or topic pages.
+- Reconciling the vault's `Goal Lifecycle Guide`, which restates the four-value decision — handled as a follow-up after release, since the spec cannot edit the vault.
+
+## Acceptance Criteria
+
+- [ ] `AvailableGoalPhases` and `AvailableTopicPhases` each equal `AvailableTaskPhases` value-for-value, in declaration order. Evidence: a table assertion in `pkg/domain/goal_phase_test.go` and `topic_phase_test.go` compares each collection to `AvailableTaskPhases` element-by-element and asserts length 6; `make test` exits 0.
+- [ ] `vault-cli goal set phase ai_review` and `phase human_review` exit 0 and write the value to disk. Evidence: integration test in `integration/cli_test.go` against a temp vault — exit 0, the file contains `phase: ai_review` (resp. `phase: human_review`), and `goal show --output json` returns the same value at `.fields.phase`.
+- [ ] `vault-cli topic set phase ai_review` and `phase human_review` exit 0 and write the value to disk. Evidence: the same integration shape against a temp topic — exit 0, `phase: ai_review` on disk, `topic show --output json` returns it.
+- [ ] `vault-cli goal set phase bogus` exits 1 and stderr contains `unknown goal phase 'bogus'` followed by all six values `todo, planning, execution, ai_review, human_review, done`. Evidence: integration test asserts exit 1 and the stderr substring; the page is byte-identical (sha256) before and after.
+- [ ] `vault-cli topic set phase bogus` exits 1 and stderr contains `unknown topic phase 'bogus'` followed by the same six values. Evidence: integration test asserts exit 1 and the stderr substring; the page is byte-identical before and after.
+- [ ] `in_progress` stays rejected by goal and topic validation. Evidence: unit tests assert `GoalPhase("in_progress").Validate` and `TopicPhase("in_progress").Validate` return an error, and integration `goal set … phase in_progress` / `topic set … phase in_progress` exit 1.
+- [ ] The valid-value list is derived from the enum, not a second literal list. Evidence: `! grep -rq 'todo, planning' pkg/domain/` succeeds (no inline literal anywhere in the domain package); `! grep -q 'unknown goal phase' pkg/domain/goal_frontmatter.go` succeeds (the local rebuild is gone, not merely supplemented); and a unit test asserts `GoalPhase.Validate(ctx)`'s error contains a list built by iterating `AvailableGoalPhases` (topic equivalent likewise), so the test fails if the enum grows and the message does not.
+- [ ] `CHANGELOG.md` carries one `## Unreleased` bullet describing the widened enum and the listed-valid-values error. Evidence: `sed -n '/^## Unreleased/,/^## v/p' CHANGELOG.md | grep -q ai_review` succeeds.
+
+## Verification
+
+Scenario coverage: **NO new scenario** — domain unit tests plus integration tests against a temp vault reach the behavior; no Docker, cluster, or external system is involved.
+
+### Container-executable (runs inside the YOLO container at prompt time)
+
+- `ROOTDIR=/workspace make precommit` — format, generate, test, lint, vet, vulncheck, license, changelog structure all pass
+- `go test ./pkg/domain/... ./integration/...` — the enum, frontmatter and CLI integration specs pass
+- `! grep -rq 'todo, planning' pkg/domain/` — no inline valid-value literal in the domain package
+- `! grep -q 'unknown goal phase' pkg/domain/goal_frontmatter.go` — the goal setter no longer rebuilds the message
+- `sed -n '/^## Unreleased/,/^## v/p' CHANGELOG.md | grep -q ai_review` — the new bullet sits under `## Unreleased`
+
+### Operator-executable (runs on the host after PR merge)
+
+- `gh pr view --repo bborbe/vault-cli --json state,mergeCommit` — merged
+- `git tag --contains ` — lists the release tag `vX.Y.Z`
+- `make install` after the release gate in `docs/releasing-vault-cli.md`, then `$(command -v vault-cli) --version` — prints that same `vX.Y.Z`
+- `vault-cli goal set phase ai_review` and `phase bogus` — exit 0 and exit 1 respectively, the latter naming all six values
+
+## Desired Behavior
+
+1. Goal and topic phase validation accepts exactly the six canonical task phases (`todo`, `planning`, `execution`, `ai_review`, `human_review`, `done`); `in_progress` stays rejected.
+2. An invalid phase produces an error naming the value and the accepted set, and the set is rendered by iterating `AvailableGoalPhases` / `AvailableTopicPhases` — no second literal list of phase strings exists in the domain package.
+3. `vault-cli goal set` and `vault-cli topic set` surface that validator error, so the CLI prints the accepted set for both verbs (the goal setter stops discarding the validator's error; the topic setter keeps wrapping it).
+4. A goal or topic carrying `phase: ai_review` / `phase: human_review` is written to disk and read back unchanged by `show` / `get`.
+
+## Constraints
+
+- The canonical set is defined once per phase type, as `AvailableGoalPhases` / `AvailableTopicPhases`; the error text must derive from it. A duplicated literal list is the defect this spec exists to avoid. The two collections stay distinct typed collections for type safety; their equality to `AvailableTaskPhases` is enforced by test rather than by aliasing.
+- `Validate` stays a membership check against the collection (`if !AvailableXs.Contains(v)`), per the enum pattern in `coding/docs/go-enum-type-pattern.md`.
+- The error keeps the `unknown goal phase ''` / `unknown topic phase ''` substring the existing tests and the measured probe rely on (a substring, not a prefix: the goal path now wraps the validator error).
+- `in_progress` stays an alias-only value: `AvailableTaskPhases` excludes it and goal/topic validation must not add it.
+- No behavior change for tasks, statuses, lint rules, or the `show` / `get` / `list` read paths. In particular, a goal or topic at `phase: ai_review` / `phase: human_review` with `status: in_progress` stays free of `STATUS_PHASE_MISMATCH` — `detectStatusPhaseMismatch` already treats both as active phases, so this is a regression guard, not new work.
+- `CHANGELOG.md` gains `## Unreleased` above the newest `## vX.Y.Z`, below the preamble.
+
+## Failure Modes
+
+| Trigger | Expected behavior | Recovery |
+|---|---|---|
+| A caller passes `in_progress` to `goal set` / `topic set` | Exit 1 with `unknown goal phase 'in_progress' (valid: …)` | Caller uses `execution`, the canonical value |
+| A legacy page hand-typed to `phase: in_progress` is read | `show` / `get` return the raw value unchanged — the read path does no validation | A write re-validates and rejects until the page is corrected to `execution` |
+| Two concurrent `goal set` writes on the same page | Last writer wins; each write is whole-file | Re-read and re-apply the intended value |
+| The process is killed during a write | The page can be left truncated or partial (a whole-file rewrite is not atomic) | Restore the page from git, then re-apply the intended value |
+| A future edit adds a phase to the task enum but not to the goal/topic enums | The enum-equality test fails in `make test` | Add the value to both enums; the error text updates automatically because it derives from the collection |
+
+## Security / Abuse Cases
+
+N/A. The only new input is a phase string on the existing `goal set` / `topic set` verbs; it is validated against a closed enum before any write, and the target page is resolved through the existing vault path, so no path traversal is introduced. No network, no secrets, no new files.
+
+## Suggested Decomposition
+
+Prompts should be generated in this order — each row is a single prompt with a clear scope.
+
+| # | Prompt focus | Covers DBs | Covers ACs | Depends on |
+|---|---|---|---|---|
+| 1 | Widen both phase enums; derive the valid-value list in `Validate`; make both `setPhaseField` surface the validator's error; unit tests for enum equality, valid/invalid values, the `in_progress` rejection, and the derived message | 1, 2, 3 | 1, 6, 7 | — |
+| 2 | Integration specs for `goal set` / `topic set` with `ai_review` / `human_review` and `bogus` (exit code, stderr list, byte-identical page) plus the `## Unreleased` CHANGELOG bullet | 3, 4 | 2, 3, 4, 5, 8 | prompt 1 |
+
+Rationale: prompt 1 establishes the enum and the message wording the integration specs assert on; prompt 2 can only pass once prompt 1 has landed.
+
+## Do-Nothing Option
+
+Goals and topics keep rejecting `ai_review` / `human_review`, so a goal or topic cannot enter the review stages the manager lifecycle uses, and callers who pass an invalid phase get no hint of the accepted values. The parent goal's SC1 stays unmet and every future caller re-derives the four-value set by trial and error.