feat(synthesis): Add synthesis step for convoy formulas (gt-v5s0j)
Implements the synthesis step that combines leg outputs into a final deliverable for convoy workflows: - `gt synthesis start <convoy-id>` - Start synthesis after verifying all legs are complete, collecting outputs, and slinging to polecat - `gt synthesis status <convoy-id>` - Show synthesis readiness and leg completion status - `gt synthesis close <convoy-id>` - Close convoy after synthesis Key features: - Collects leg outputs from formula-defined paths (e.g., findings.md) - Creates synthesis bead with combined context from all leg outputs - Integrates with formula.toml synthesis configuration - Provides TriggerSynthesisIfReady() for automated synthesis trigger - Adds SynthesisFields struct for tracking synthesis state in beads 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -344,6 +344,87 @@ func SetMRFields(issue *Issue, fields *MRFields) string {
|
||||
return formatted + "\n\n" + strings.Join(otherLines, "\n")
|
||||
}
|
||||
|
||||
// SynthesisFields holds structured fields for synthesis beads.
|
||||
// These fields track the synthesis step in a convoy workflow.
|
||||
type SynthesisFields struct {
|
||||
ConvoyID string `json:"convoy_id"` // Parent convoy ID
|
||||
ReviewID string `json:"review_id"` // Review ID for output paths
|
||||
OutputPath string `json:"output_path"` // Path to synthesis output file
|
||||
Formula string `json:"formula"` // Formula name (if from formula)
|
||||
}
|
||||
|
||||
// ParseSynthesisFields extracts synthesis fields from an issue's description.
|
||||
// Fields are expected as "key: value" lines. Returns nil if no fields found.
|
||||
func ParseSynthesisFields(issue *Issue) *SynthesisFields {
|
||||
if issue == nil || issue.Description == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
fields := &SynthesisFields{}
|
||||
hasFields := false
|
||||
|
||||
for _, line := range strings.Split(issue.Description, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
colonIdx := strings.Index(line, ":")
|
||||
if colonIdx == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(line[:colonIdx])
|
||||
value := strings.TrimSpace(line[colonIdx+1:])
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch strings.ToLower(key) {
|
||||
case "convoy", "convoy_id", "convoy-id":
|
||||
fields.ConvoyID = value
|
||||
hasFields = true
|
||||
case "review_id", "review-id", "reviewid":
|
||||
fields.ReviewID = value
|
||||
hasFields = true
|
||||
case "output_path", "output-path", "outputpath":
|
||||
fields.OutputPath = value
|
||||
hasFields = true
|
||||
case "formula":
|
||||
fields.Formula = value
|
||||
hasFields = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasFields {
|
||||
return nil
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// FormatSynthesisFields formats SynthesisFields as a string for issue description.
|
||||
func FormatSynthesisFields(fields *SynthesisFields) string {
|
||||
if fields == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var lines []string
|
||||
if fields.ConvoyID != "" {
|
||||
lines = append(lines, "convoy: "+fields.ConvoyID)
|
||||
}
|
||||
if fields.ReviewID != "" {
|
||||
lines = append(lines, "review_id: "+fields.ReviewID)
|
||||
}
|
||||
if fields.OutputPath != "" {
|
||||
lines = append(lines, "output_path: "+fields.OutputPath)
|
||||
}
|
||||
if fields.Formula != "" {
|
||||
lines = append(lines, "formula: "+fields.Formula)
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// RoleConfig holds structured lifecycle configuration for role beads.
|
||||
// These fields are stored as "key: value" lines in the role bead description.
|
||||
// This enables agents to self-register their lifecycle configuration,
|
||||
|
||||
Reference in New Issue
Block a user