Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ validate: build vet test
bin/$(BIN) ledger validate >/tmp/prophet-ledger-validate.json
bin/$(BIN) ledger records >/tmp/prophet-ledger-records.json
bin/$(BIN) agent registry list >/tmp/prophet-agent-registry-list.json
bin/$(BIN) spine list >/tmp/prophet-spine-list.json
bin/$(BIN) spine gate --help >/tmp/prophet-spine-gate-help.txt

verify: fmt validate

Expand Down
1 change: 1 addition & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func NewRootCommand() *cobra.Command {
newModelCmd(),
newGuardrailCmd(),
newLedgerCmd(),
newSpineCmd(),
newAgentSuiteCmd(),
newPlaceholderCmd("ask", "Agent assist: explain or inspect without mutating state"),
newPlaceholderCmd("plan", "Agent assist: generate a plan over deterministic tools"),
Expand Down
220 changes: 220 additions & 0 deletions internal/cmd/spine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package cmd

import (
"bytes"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/spf13/cobra"
)

type spineGate struct {
Repo string
Steps [][]string // each step is a command + args
}

var spineGates = []spineGate{
{
Repo: "prophet-mesh",
Steps: [][]string{{"make", "validate"}, {"make", "test"}},
},
{
Repo: "agent-registry",
Steps: [][]string{{"make", "validate"}, {"make", "test"}},
},
{
Repo: "model-router",
Steps: [][]string{{"make", "validate"}, {"make", "test"}},
},
{
Repo: "agentplane",
Steps: [][]string{{"make", "validate"}, {"make", "test"}},
},
{
Repo: "memory-mesh",
Steps: [][]string{{"make", "validate-prophet-mesh-scope-mirror"}},
},
}

func newSpineCmd() *cobra.Command {
cmd := &cobra.Command{Use: "spine", Short: "Prophet Mesh private-preview spine gates"}

cmd.AddCommand(newSpineValidateCmd())
cmd.AddCommand(newSpineGateCmd())
cmd.AddCommand(newSpineListCmd())

return cmd
}

func newSpineValidateCmd() *cobra.Command {
var repo string
c := &cobra.Command{
Use: "validate",
Short: "Run all spine gate checks (or a single repo gate with --repo)",
RunE: func(cmd *cobra.Command, args []string) error {
if repo != "" {
return runSpineGate(repo)
}
return runAllSpineGates()
},
}
c.Flags().StringVar(&repo, "repo", "", "run gate for a single repo only")
return c
}

func newSpineGateCmd() *cobra.Command {
return &cobra.Command{
Use: "gate <repo>",
Short: "Run the spine gate for a specific repo",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runSpineGate(args[0])
},
}
}

func newSpineListCmd() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List spine repos and their gate steps",
RunE: func(cmd *cobra.Command, args []string) error {
gates := make([]map[string]any, 0, len(spineGates))
for _, g := range spineGates {
steps := make([]string, 0, len(g.Steps))
for _, s := range g.Steps {
steps = append(steps, strings.Join(s, " "))
}
gates = append(gates, map[string]any{
"repo": g.Repo,
"steps": steps,
})
}
return emit(map[string]any{
"command": "prophet spine list",
"status": "ok",
"gates": gates,
})
},
}
}

func runAllSpineGates() error {
results := make([]map[string]any, 0, len(spineGates))
allPassed := true

for _, g := range spineGates {
result := runGate(g)
results = append(results, result)
if result["status"] != "ok" {
allPassed = false
}
}

status := "ok"
if !allPassed {
status = "failed"
}

return emit(map[string]any{
"command": "prophet spine validate",
"status": status,
"gates": results,
})
Comment on lines +120 to +124

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Return a non-zero error when validation fails

When any repo gate fails, allPassed is set to false and the JSON status becomes failed, but this path still returns only emit(...); if the output is written successfully, Cobra sees a nil error and prophet spine validate exits 0. In any CI or release script that uses this validation gate, failed make validate/make test checks would be treated as success, so the command needs to return an error after emitting a failed result.

Useful? React with 👍 / 👎.

}

func runSpineGate(repo string) error {
for _, g := range spineGates {
if g.Repo == repo {
result := runGate(g)
result["command"] = "prophet spine gate " + repo
return emit(result)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fail the single-repo gate on failed steps

For a known repo, runGate can return status: "failed" when one of its make steps fails, but runSpineGate still returns nil here as long as the JSON output succeeds. That means prophet spine gate prophet-mesh exits 0 despite a failed gate, which defeats callers that rely on this command to block on a single repo's validation result.

Useful? React with 👍 / 👎.

}
}
return emit(map[string]any{
"command": "prophet spine gate " + repo,
"status": "error",
"reason": "unknown spine repo: " + repo,
"known": spineRepoNames(),
})
}

func runGate(g spineGate) map[string]any {
repoDir := localRepoRoot(g.Repo)
stepResults := make([]map[string]any, 0, len(g.Steps))
gateStatus := "ok"

for _, step := range g.Steps {
sr := runStep(repoDir, step)
stepResults = append(stepResults, sr)
if sr["status"] != "ok" {
gateStatus = "failed"
}
}

return map[string]any{
"repo": g.Repo,
"status": gateStatus,
"steps": stepResults,
}
}

func runStep(dir string, args []string) map[string]any {
if len(args) == 0 {
return map[string]any{"status": "error", "reason": "empty step"}
}

binary, err := exec.LookPath(args[0])
if err != nil {
// try resolving relative to dir (e.g. make)
binary = args[0]
}

c := exec.Command(binary, args[1:]...)
c.Dir = dir

var stdout, stderr bytes.Buffer
c.Stdout = &stdout
c.Stderr = &stderr

start := time.Now()
runErr := c.Run()
elapsed := time.Since(start).Milliseconds()

step := strings.Join(args, " ")
if runErr != nil {
return map[string]any{
"step": step,
"status": "failed",
"stdout": strings.TrimSpace(stdout.String()),
"stderr": strings.TrimSpace(stderr.String()),
"error": runErr.Error(),
"elapsed_ms": elapsed,
}
}

result := map[string]any{
"step": step,
"status": "ok",
"elapsed_ms": elapsed,
}
if out := strings.TrimSpace(stdout.String()); out != "" {
result["stdout"] = out
}
return result
}

func spineRepoNames() []string {
names := make([]string, 0, len(spineGates))
for _, g := range spineGates {
names = append(names, g.Repo)
}
return names
}

// spineGateDir resolves a spine repo to its absolute local path.
// Exported for use by Makefile validate target.
func spineGateDir(repo string) string {
return filepath.Join(localRepoRoot(repo), ".")
}
Loading