678 lines
19 KiB
Go
678 lines
19 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/gastown/internal/formula"
|
|
"github.com/steveyegge/gastown/internal/runtime"
|
|
"github.com/steveyegge/gastown/internal/style"
|
|
"github.com/steveyegge/gastown/internal/workspace"
|
|
)
|
|
|
|
// Synthesis command flags
|
|
var (
|
|
synthesisRig string
|
|
synthesisDryRun bool
|
|
synthesisForce bool
|
|
synthesisReviewID string
|
|
)
|
|
|
|
var synthesisCmd = &cobra.Command{
|
|
Use: "synthesis",
|
|
Aliases: []string{"synth"},
|
|
GroupID: GroupWork,
|
|
Short: "Manage convoy synthesis steps",
|
|
RunE: requireSubcommand,
|
|
Long: `Manage synthesis steps for convoy formulas.
|
|
|
|
Synthesis is the final step in a convoy workflow that combines outputs
|
|
from all parallel legs into a unified deliverable.
|
|
|
|
Commands:
|
|
start Start synthesis for a convoy (checks all legs complete)
|
|
status Show synthesis readiness and leg outputs
|
|
close Close convoy after synthesis complete
|
|
|
|
Examples:
|
|
gt synthesis status hq-cv-abc # Check if ready for synthesis
|
|
gt synthesis start hq-cv-abc # Start synthesis step
|
|
gt synthesis close hq-cv-abc # Close convoy after synthesis`,
|
|
}
|
|
|
|
var synthesisStartCmd = &cobra.Command{
|
|
Use: "start <convoy-id>",
|
|
Short: "Start synthesis for a convoy",
|
|
Long: `Start the synthesis step for a convoy.
|
|
|
|
This command:
|
|
1. Verifies all legs are complete
|
|
2. Collects outputs from all legs
|
|
3. Creates a synthesis bead with combined context
|
|
4. Slings the synthesis to a polecat
|
|
|
|
Options:
|
|
--rig=NAME Target rig for synthesis polecat (default: current)
|
|
--review-id=ID Override review ID for output paths
|
|
--force Start synthesis even if some legs incomplete
|
|
--dry-run Show what would happen without executing`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runSynthesisStart,
|
|
}
|
|
|
|
var synthesisStatusCmd = &cobra.Command{
|
|
Use: "status <convoy-id>",
|
|
Short: "Show synthesis readiness",
|
|
Long: `Show whether a convoy is ready for synthesis.
|
|
|
|
Displays:
|
|
- Convoy metadata
|
|
- Leg completion status
|
|
- Available leg outputs
|
|
- Formula synthesis configuration`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runSynthesisStatus,
|
|
}
|
|
|
|
var synthesisCloseCmd = &cobra.Command{
|
|
Use: "close <convoy-id>",
|
|
Short: "Close convoy after synthesis",
|
|
Long: `Close a convoy after synthesis is complete.
|
|
|
|
This marks the convoy as complete and triggers any configured notifications.`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runSynthesisClose,
|
|
}
|
|
|
|
func init() {
|
|
// Start flags
|
|
synthesisStartCmd.Flags().StringVar(&synthesisRig, "rig", "", "Target rig for synthesis polecat")
|
|
synthesisStartCmd.Flags().BoolVar(&synthesisDryRun, "dry-run", false, "Preview execution")
|
|
synthesisStartCmd.Flags().BoolVar(&synthesisForce, "force", false, "Start even if legs incomplete")
|
|
synthesisStartCmd.Flags().StringVar(&synthesisReviewID, "review-id", "", "Override review ID")
|
|
|
|
// Add subcommands
|
|
synthesisCmd.AddCommand(synthesisStartCmd)
|
|
synthesisCmd.AddCommand(synthesisStatusCmd)
|
|
synthesisCmd.AddCommand(synthesisCloseCmd)
|
|
|
|
rootCmd.AddCommand(synthesisCmd)
|
|
}
|
|
|
|
// LegOutput represents collected output from a convoy leg.
|
|
type LegOutput struct {
|
|
LegID string `json:"leg_id"`
|
|
Title string `json:"title"`
|
|
Status string `json:"status"`
|
|
FilePath string `json:"file_path,omitempty"`
|
|
Content string `json:"content,omitempty"`
|
|
HasFile bool `json:"has_file"`
|
|
}
|
|
|
|
// ConvoyMeta holds metadata about a convoy including its formula.
|
|
type ConvoyMeta struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Status string `json:"status"`
|
|
Formula string `json:"formula,omitempty"` // Formula name
|
|
FormulaPath string `json:"formula_path,omitempty"` // Path to formula file
|
|
ReviewID string `json:"review_id,omitempty"` // Review ID for output paths
|
|
LegIssues []string `json:"leg_issues,omitempty"` // Tracked leg issue IDs
|
|
}
|
|
|
|
// runSynthesisStart implements gt synthesis start.
|
|
func runSynthesisStart(cmd *cobra.Command, args []string) error {
|
|
convoyID := args[0]
|
|
|
|
// Get convoy metadata
|
|
meta, err := getConvoyMeta(convoyID)
|
|
if err != nil {
|
|
return fmt.Errorf("getting convoy metadata: %w", err)
|
|
}
|
|
|
|
fmt.Printf("%s Checking synthesis readiness for %s...\n", style.Bold.Render("🔬"), convoyID)
|
|
|
|
// Load formula if specified
|
|
var f *formula.Formula
|
|
if meta.FormulaPath != "" {
|
|
f, err = formula.ParseFile(meta.FormulaPath)
|
|
if err != nil {
|
|
return fmt.Errorf("loading formula: %w", err)
|
|
}
|
|
} else if meta.Formula != "" {
|
|
// Try to find formula by name
|
|
formulaPath, findErr := findFormula(meta.Formula)
|
|
if findErr == nil {
|
|
f, err = formula.ParseFile(formulaPath)
|
|
if err != nil {
|
|
return fmt.Errorf("loading formula: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check leg completion status
|
|
legOutputs, allComplete, err := collectLegOutputs(meta, f)
|
|
if err != nil {
|
|
return fmt.Errorf("collecting leg outputs: %w", err)
|
|
}
|
|
|
|
// Report status
|
|
completedCount := 0
|
|
for _, leg := range legOutputs {
|
|
if leg.Status == "closed" {
|
|
completedCount++
|
|
}
|
|
}
|
|
fmt.Printf(" Legs: %d/%d complete\n", completedCount, len(legOutputs))
|
|
|
|
if !allComplete && !synthesisForce {
|
|
fmt.Printf("\n%s Not all legs complete. Use --force to proceed anyway.\n",
|
|
style.Warning.Render("⚠"))
|
|
fmt.Printf("\nIncomplete legs:\n")
|
|
for _, leg := range legOutputs {
|
|
if leg.Status != "closed" {
|
|
fmt.Printf(" ○ %s: %s [%s]\n", leg.LegID, leg.Title, leg.Status)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Determine review ID
|
|
reviewID := synthesisReviewID
|
|
if reviewID == "" {
|
|
reviewID = meta.ReviewID
|
|
}
|
|
if reviewID == "" {
|
|
// Extract from convoy ID
|
|
reviewID = strings.TrimPrefix(convoyID, "hq-cv-")
|
|
}
|
|
|
|
// Determine target rig
|
|
targetRig := synthesisRig
|
|
if targetRig == "" {
|
|
townRoot, err := workspace.FindFromCwdOrError()
|
|
if err == nil {
|
|
rigName, _, rigErr := findCurrentRig(townRoot)
|
|
if rigErr == nil && rigName != "" {
|
|
targetRig = rigName
|
|
}
|
|
}
|
|
if targetRig == "" {
|
|
targetRig = "gastown"
|
|
}
|
|
}
|
|
|
|
if synthesisDryRun {
|
|
fmt.Printf("\n%s Would start synthesis:\n", style.Dim.Render("[dry-run]"))
|
|
fmt.Printf(" Convoy: %s\n", convoyID)
|
|
fmt.Printf(" Review ID: %s\n", reviewID)
|
|
fmt.Printf(" Target: %s\n", targetRig)
|
|
fmt.Printf(" Legs: %d outputs collected\n", len(legOutputs))
|
|
if f != nil && f.Synthesis != nil {
|
|
fmt.Printf(" Synthesis: %s\n", f.Synthesis.Title)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Create synthesis bead
|
|
synthesisID, err := createSynthesisBead(convoyID, meta, f, legOutputs, reviewID)
|
|
if err != nil {
|
|
return fmt.Errorf("creating synthesis bead: %w", err)
|
|
}
|
|
fmt.Printf("%s Created synthesis bead: %s\n", style.Bold.Render("✓"), synthesisID)
|
|
|
|
// Sling to target rig
|
|
fmt.Printf(" Slinging to %s...\n", targetRig)
|
|
if err := slingSynthesis(synthesisID, targetRig); err != nil {
|
|
return fmt.Errorf("slinging synthesis: %w", err)
|
|
}
|
|
|
|
fmt.Printf("%s Synthesis started\n", style.Bold.Render("✓"))
|
|
fmt.Printf(" Monitor: gt convoy status %s\n", convoyID)
|
|
|
|
return nil
|
|
}
|
|
|
|
// runSynthesisStatus implements gt synthesis status.
|
|
func runSynthesisStatus(cmd *cobra.Command, args []string) error {
|
|
convoyID := args[0]
|
|
|
|
meta, err := getConvoyMeta(convoyID)
|
|
if err != nil {
|
|
return fmt.Errorf("getting convoy metadata: %w", err)
|
|
}
|
|
|
|
// Load formula if available
|
|
var f *formula.Formula
|
|
if meta.FormulaPath != "" {
|
|
f, _ = formula.ParseFile(meta.FormulaPath)
|
|
} else if meta.Formula != "" {
|
|
if path, err := findFormula(meta.Formula); err == nil {
|
|
f, _ = formula.ParseFile(path)
|
|
}
|
|
}
|
|
|
|
// Collect leg outputs
|
|
legOutputs, allComplete, err := collectLegOutputs(meta, f)
|
|
if err != nil {
|
|
return fmt.Errorf("collecting leg outputs: %w", err)
|
|
}
|
|
|
|
// Display status
|
|
fmt.Printf("🚚 %s %s\n\n", style.Bold.Render(convoyID+":"), meta.Title)
|
|
fmt.Printf(" Status: %s\n", formatConvoyStatus(meta.Status))
|
|
|
|
if meta.Formula != "" {
|
|
fmt.Printf(" Formula: %s\n", meta.Formula)
|
|
}
|
|
|
|
fmt.Printf("\n %s\n", style.Bold.Render("Legs:"))
|
|
for _, leg := range legOutputs {
|
|
status := "○"
|
|
if leg.Status == "closed" {
|
|
status = "✓"
|
|
}
|
|
fileStatus := ""
|
|
if leg.HasFile {
|
|
fileStatus = style.Dim.Render(" (output: ✓)")
|
|
}
|
|
fmt.Printf(" %s %s: %s [%s]%s\n", status, leg.LegID, leg.Title, leg.Status, fileStatus)
|
|
}
|
|
|
|
// Synthesis readiness
|
|
fmt.Printf("\n %s\n", style.Bold.Render("Synthesis:"))
|
|
if allComplete {
|
|
fmt.Printf(" %s Ready - all legs complete\n", style.Success.Render("✓"))
|
|
fmt.Printf(" Run: gt synthesis start %s\n", convoyID)
|
|
} else {
|
|
completedCount := 0
|
|
for _, leg := range legOutputs {
|
|
if leg.Status == "closed" {
|
|
completedCount++
|
|
}
|
|
}
|
|
fmt.Printf(" %s Waiting - %d/%d legs complete\n",
|
|
style.Warning.Render("○"), completedCount, len(legOutputs))
|
|
}
|
|
|
|
if f != nil && f.Synthesis != nil {
|
|
fmt.Printf("\n %s\n", style.Bold.Render("Synthesis Config:"))
|
|
fmt.Printf(" Title: %s\n", f.Synthesis.Title)
|
|
if f.Output != nil && f.Output.Synthesis != "" {
|
|
fmt.Printf(" Output: %s\n", f.Output.Synthesis)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// runSynthesisClose implements gt synthesis close.
|
|
func runSynthesisClose(cmd *cobra.Command, args []string) error {
|
|
convoyID := args[0]
|
|
|
|
townBeads, err := getTownBeadsDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Close the convoy
|
|
closeArgs := []string{"close", convoyID, "--reason=synthesis complete"}
|
|
if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
|
|
closeArgs = append(closeArgs, "--session="+sessionID)
|
|
}
|
|
closeCmd := exec.Command("bd", closeArgs...)
|
|
closeCmd.Dir = townBeads
|
|
closeCmd.Stderr = os.Stderr
|
|
|
|
if err := closeCmd.Run(); err != nil {
|
|
return fmt.Errorf("closing convoy: %w", err)
|
|
}
|
|
|
|
fmt.Printf("%s Convoy closed: %s\n", style.Bold.Render("✓"), convoyID)
|
|
|
|
// TODO: Trigger notification if configured
|
|
// Parse description for "Notify: <address>" and send mail
|
|
|
|
return nil
|
|
}
|
|
|
|
// getConvoyMeta retrieves convoy metadata from beads.
|
|
func getConvoyMeta(convoyID string) (*ConvoyMeta, error) {
|
|
townBeads, err := getTownBeadsDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
showCmd := exec.Command("bd", "show", convoyID, "--json")
|
|
showCmd.Dir = townBeads
|
|
var stdout bytes.Buffer
|
|
showCmd.Stdout = &stdout
|
|
|
|
if err := showCmd.Run(); err != nil {
|
|
return nil, fmt.Errorf("convoy '%s' not found", convoyID)
|
|
}
|
|
|
|
var convoys []struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Status string `json:"status"`
|
|
Description string `json:"description"`
|
|
Type string `json:"issue_type"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &convoys); err != nil {
|
|
return nil, fmt.Errorf("parsing convoy data: %w", err)
|
|
}
|
|
|
|
if len(convoys) == 0 || convoys[0].Type != "convoy" {
|
|
return nil, fmt.Errorf("'%s' is not a convoy", convoyID)
|
|
}
|
|
|
|
convoy := convoys[0]
|
|
|
|
// Parse formula and review ID from description
|
|
meta := &ConvoyMeta{
|
|
ID: convoy.ID,
|
|
Title: convoy.Title,
|
|
Status: convoy.Status,
|
|
}
|
|
|
|
// Look for structured fields in description
|
|
for _, line := range strings.Split(convoy.Description, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if colonIdx := strings.Index(line, ":"); colonIdx != -1 {
|
|
key := strings.ToLower(strings.TrimSpace(line[:colonIdx]))
|
|
value := strings.TrimSpace(line[colonIdx+1:])
|
|
switch key {
|
|
case "formula":
|
|
meta.Formula = value
|
|
case "formula_path", "formula-path":
|
|
meta.FormulaPath = value
|
|
case "review_id", "review-id":
|
|
meta.ReviewID = value
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get tracked leg issues
|
|
tracked := getTrackedIssues(townBeads, convoyID)
|
|
for _, t := range tracked {
|
|
meta.LegIssues = append(meta.LegIssues, t.ID)
|
|
}
|
|
|
|
return meta, nil
|
|
}
|
|
|
|
// collectLegOutputs gathers outputs from all convoy legs.
|
|
func collectLegOutputs(meta *ConvoyMeta, f *formula.Formula) ([]LegOutput, bool, error) { //nolint:unparam // error return kept for future use
|
|
var outputs []LegOutput
|
|
allComplete := true
|
|
|
|
// If we have tracked issues, use those as legs
|
|
if len(meta.LegIssues) > 0 {
|
|
for _, issueID := range meta.LegIssues {
|
|
details := getIssueDetails(issueID)
|
|
output := LegOutput{
|
|
LegID: issueID,
|
|
Title: "(unknown)",
|
|
}
|
|
if details != nil {
|
|
output.Title = details.Title
|
|
output.Status = details.Status
|
|
}
|
|
if output.Status != "closed" {
|
|
allComplete = false
|
|
}
|
|
outputs = append(outputs, output)
|
|
}
|
|
}
|
|
|
|
// If we have a formula, also try to find output files
|
|
if f != nil && f.Output != nil && meta.ReviewID != "" {
|
|
for _, leg := range f.Legs {
|
|
// Expand output path template
|
|
outputPath := expandOutputPath(f.Output.Directory, f.Output.LegPattern,
|
|
meta.ReviewID, leg.ID)
|
|
|
|
// Check if file exists and read content
|
|
if content, err := os.ReadFile(outputPath); err == nil {
|
|
// Find or create leg output entry
|
|
found := false
|
|
for i := range outputs {
|
|
if outputs[i].LegID == leg.ID {
|
|
outputs[i].FilePath = outputPath
|
|
outputs[i].Content = string(content)
|
|
outputs[i].HasFile = true
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
outputs = append(outputs, LegOutput{
|
|
LegID: leg.ID,
|
|
Title: leg.Title,
|
|
Status: "closed", // If file exists, assume complete
|
|
FilePath: outputPath,
|
|
Content: string(content),
|
|
HasFile: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return outputs, allComplete, nil
|
|
}
|
|
|
|
// expandOutputPath expands template variables in output paths.
|
|
// Supports: {{review_id}}, {{leg.id}}
|
|
func expandOutputPath(directory, pattern, reviewID, legID string) string {
|
|
// Expand directory
|
|
dir := strings.ReplaceAll(directory, "{{review_id}}", reviewID)
|
|
|
|
// Expand pattern
|
|
file := strings.ReplaceAll(pattern, "{{leg.id}}", legID)
|
|
|
|
return filepath.Join(dir, file)
|
|
}
|
|
|
|
// createSynthesisBead creates a bead for the synthesis step.
|
|
func createSynthesisBead(convoyID string, meta *ConvoyMeta, f *formula.Formula,
|
|
legOutputs []LegOutput, reviewID string) (string, error) {
|
|
|
|
// Build synthesis title
|
|
title := "Synthesis: " + meta.Title
|
|
if f != nil && f.Synthesis != nil && f.Synthesis.Title != "" {
|
|
title = f.Synthesis.Title + ": " + meta.Title
|
|
}
|
|
|
|
// Build synthesis description with leg outputs
|
|
var desc strings.Builder
|
|
desc.WriteString(fmt.Sprintf("convoy: %s\n", convoyID))
|
|
desc.WriteString(fmt.Sprintf("review_id: %s\n", reviewID))
|
|
desc.WriteString("\n")
|
|
|
|
// Add synthesis instructions from formula
|
|
if f != nil && f.Synthesis != nil && f.Synthesis.Description != "" {
|
|
desc.WriteString("## Instructions\n\n")
|
|
desc.WriteString(f.Synthesis.Description)
|
|
desc.WriteString("\n\n")
|
|
}
|
|
|
|
// Add collected leg outputs
|
|
desc.WriteString("## Leg Outputs\n\n")
|
|
for _, leg := range legOutputs {
|
|
desc.WriteString(fmt.Sprintf("### %s: %s\n\n", leg.LegID, leg.Title))
|
|
if leg.Content != "" {
|
|
desc.WriteString(leg.Content)
|
|
desc.WriteString("\n\n")
|
|
} else if leg.FilePath != "" {
|
|
desc.WriteString(fmt.Sprintf("Output file: %s\n\n", leg.FilePath))
|
|
} else {
|
|
desc.WriteString("(no output available)\n\n")
|
|
}
|
|
}
|
|
|
|
// Add output path if configured
|
|
if f != nil && f.Output != nil && f.Output.Synthesis != "" {
|
|
outputPath := strings.ReplaceAll(f.Output.Directory, "{{review_id}}", reviewID)
|
|
outputPath = filepath.Join(outputPath, f.Output.Synthesis)
|
|
desc.WriteString(fmt.Sprintf("\n## Output\n\nWrite synthesis to: %s\n", outputPath))
|
|
}
|
|
|
|
// Create the bead
|
|
createArgs := []string{
|
|
"create",
|
|
"--type=task",
|
|
"--title=" + title,
|
|
"--description=" + desc.String(),
|
|
"--json",
|
|
}
|
|
|
|
townBeads, err := getTownBeadsDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
createCmd := exec.Command("bd", createArgs...)
|
|
createCmd.Dir = townBeads
|
|
var stdout bytes.Buffer
|
|
createCmd.Stdout = &stdout
|
|
createCmd.Stderr = os.Stderr
|
|
|
|
if err := createCmd.Run(); err != nil {
|
|
return "", fmt.Errorf("creating synthesis bead: %w", err)
|
|
}
|
|
|
|
// Parse created bead ID
|
|
var result struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
|
|
// Try to extract ID from non-JSON output
|
|
out := strings.TrimSpace(stdout.String())
|
|
if strings.HasPrefix(out, "hq-") || strings.HasPrefix(out, "gt-") {
|
|
return out, nil
|
|
}
|
|
return "", fmt.Errorf("parsing created bead: %w", err)
|
|
}
|
|
|
|
// Add tracking relation: convoy tracks synthesis
|
|
depArgs := []string{"dep", "add", convoyID, result.ID, "--type=tracks"}
|
|
depCmd := exec.Command("bd", depArgs...)
|
|
depCmd.Dir = townBeads
|
|
_ = depCmd.Run() // Non-fatal if this fails
|
|
|
|
return result.ID, nil
|
|
}
|
|
|
|
// slingSynthesis slings the synthesis bead to a rig.
|
|
func slingSynthesis(beadID, targetRig string) error {
|
|
slingArgs := []string{"sling", beadID, targetRig}
|
|
slingCmd := exec.Command("gt", slingArgs...)
|
|
slingCmd.Stdout = os.Stdout
|
|
slingCmd.Stderr = os.Stderr
|
|
|
|
return slingCmd.Run()
|
|
}
|
|
|
|
// findFormula searches for a formula file by name.
|
|
func findFormula(name string) (string, error) {
|
|
// Search paths
|
|
searchPaths := []string{
|
|
".beads/formulas",
|
|
}
|
|
|
|
// Add home directory formulas
|
|
if home, err := os.UserHomeDir(); err == nil {
|
|
searchPaths = append(searchPaths, filepath.Join(home, ".beads", "formulas"))
|
|
}
|
|
|
|
// Add GT_ROOT formulas if set
|
|
if gtRoot := os.Getenv("GT_ROOT"); gtRoot != "" {
|
|
searchPaths = append(searchPaths, filepath.Join(gtRoot, ".beads", "formulas"))
|
|
}
|
|
|
|
// Try each search path
|
|
for _, searchPath := range searchPaths {
|
|
// Try with .formula.toml extension
|
|
path := filepath.Join(searchPath, name+".formula.toml")
|
|
if _, err := os.Stat(path); err == nil {
|
|
return path, nil
|
|
}
|
|
|
|
// Try with .formula.json extension
|
|
path = filepath.Join(searchPath, name+".formula.json")
|
|
if _, err := os.Stat(path); err == nil {
|
|
return path, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("formula '%s' not found", name)
|
|
}
|
|
|
|
// CheckSynthesisReady checks if a convoy is ready for synthesis.
|
|
// Returns true if all tracked legs are complete.
|
|
func CheckSynthesisReady(convoyID string) (bool, error) {
|
|
meta, err := getConvoyMeta(convoyID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
_, allComplete, err := collectLegOutputs(meta, nil)
|
|
return allComplete, err
|
|
}
|
|
|
|
// TriggerSynthesisIfReady checks convoy status and starts synthesis if ready.
|
|
// This can be called by the witness when a leg completes.
|
|
func TriggerSynthesisIfReady(convoyID, targetRig string) error {
|
|
ready, err := CheckSynthesisReady(convoyID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !ready {
|
|
return nil // Not ready yet
|
|
}
|
|
|
|
// Synthesis is ready - start it
|
|
fmt.Printf("%s All legs complete, starting synthesis...\n", style.Bold.Render("🔬"))
|
|
|
|
meta, err := getConvoyMeta(convoyID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load formula if available
|
|
var f *formula.Formula
|
|
if meta.FormulaPath != "" {
|
|
f, _ = formula.ParseFile(meta.FormulaPath)
|
|
} else if meta.Formula != "" {
|
|
if path, err := findFormula(meta.Formula); err == nil {
|
|
f, _ = formula.ParseFile(path)
|
|
}
|
|
}
|
|
|
|
legOutputs, _, _ := collectLegOutputs(meta, f)
|
|
reviewID := meta.ReviewID
|
|
if reviewID == "" {
|
|
reviewID = strings.TrimPrefix(convoyID, "hq-cv-")
|
|
}
|
|
|
|
synthesisID, err := createSynthesisBead(convoyID, meta, f, legOutputs, reviewID)
|
|
if err != nil {
|
|
return fmt.Errorf("creating synthesis bead: %w", err)
|
|
}
|
|
|
|
if err := slingSynthesis(synthesisID, targetRig); err != nil {
|
|
return fmt.Errorf("slinging synthesis: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|