feat: add WaitsFor parsing and mol bond command (gt-odfr, gt-isje)
WaitsFor parsing: - Add WaitsFor []string field to MoleculeStep struct - Parse WaitsFor lines in molecule descriptions - Enables fanout gate semantics (e.g., WaitsFor: all-children) - Case-insensitive parsing like Needs/Tier mol bond command: - Add gt mol bond for dynamic child molecule creation - Supports --parent, --ref, and --var flags - Enables Christmas Ornament pattern for parallel child execution - Creates child issue with expanded template variables - Instantiates proto steps under the bonded child 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -343,6 +343,11 @@ func TestWitnessPatrolMolecule(t *testing.T) {
|
||||
t.Errorf("aggregate should need survey-workers, got %v", steps[4].Needs)
|
||||
}
|
||||
|
||||
// aggregate should have WaitsFor: all-children
|
||||
if len(steps[4].WaitsFor) != 1 || steps[4].WaitsFor[0] != "all-children" {
|
||||
t.Errorf("aggregate should WaitsFor all-children, got %v", steps[4].WaitsFor)
|
||||
}
|
||||
|
||||
// burn-or-loop needs context-check
|
||||
if len(steps[8].Needs) != 1 || steps[8].Needs[0] != "context-check" {
|
||||
t.Errorf("burn-or-loop should need context-check, got %v", steps[8].Needs)
|
||||
|
||||
@@ -13,6 +13,7 @@ type MoleculeStep struct {
|
||||
Title string // Step title (first non-empty line or ref)
|
||||
Instructions string // Prose instructions for this step
|
||||
Needs []string // Step refs this step depends on
|
||||
WaitsFor []string // Dynamic wait conditions (e.g., "all-children")
|
||||
Tier string // Optional tier hint: haiku, sonnet, opus
|
||||
}
|
||||
|
||||
@@ -25,6 +26,10 @@ var needsLineRegex = regexp.MustCompile(`(?i)^Needs:\s*(.+)$`)
|
||||
// tierLineRegex matches "Tier: haiku|sonnet|opus" lines.
|
||||
var tierLineRegex = regexp.MustCompile(`(?i)^Tier:\s*(haiku|sonnet|opus)\s*$`)
|
||||
|
||||
// waitsForLineRegex matches "WaitsFor: condition1, condition2, ..." lines.
|
||||
// Common conditions: "all-children" (fanout gate for dynamically bonded children)
|
||||
var waitsForLineRegex = regexp.MustCompile(`(?i)^WaitsFor:\s*(.+)$`)
|
||||
|
||||
// templateVarRegex matches {{variable}} placeholders.
|
||||
var templateVarRegex = regexp.MustCompile(`\{\{(\w+)\}\}`)
|
||||
|
||||
@@ -77,6 +82,18 @@ func ParseMoleculeSteps(description string) ([]MoleculeStep, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for WaitsFor: line
|
||||
if matches := waitsForLineRegex.FindStringSubmatch(trimmed); matches != nil {
|
||||
conditions := strings.Split(matches[1], ",")
|
||||
for _, cond := range conditions {
|
||||
cond = strings.TrimSpace(cond)
|
||||
if cond != "" {
|
||||
currentStep.WaitsFor = append(currentStep.WaitsFor, cond)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Regular instruction line
|
||||
instructionLines = append(instructionLines, line)
|
||||
}
|
||||
|
||||
@@ -143,16 +143,56 @@ Tier: opus`
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMoleculeSteps_WithWaitsFor(t *testing.T) {
|
||||
desc := `## Step: survey
|
||||
Discover work items.
|
||||
|
||||
## Step: aggregate
|
||||
Collect results from dynamically bonded children.
|
||||
WaitsFor: all-children
|
||||
Needs: survey
|
||||
|
||||
## Step: finish
|
||||
Wrap up.
|
||||
WaitsFor: all-children, external-signal
|
||||
Needs: aggregate`
|
||||
|
||||
steps, err := ParseMoleculeSteps(desc)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(steps) != 3 {
|
||||
t.Fatalf("expected 3 steps, got %d", len(steps))
|
||||
}
|
||||
|
||||
// survey has no WaitsFor
|
||||
if len(steps[0].WaitsFor) != 0 {
|
||||
t.Errorf("step[0].WaitsFor = %v, want empty", steps[0].WaitsFor)
|
||||
}
|
||||
|
||||
// aggregate waits for all-children
|
||||
if !reflect.DeepEqual(steps[1].WaitsFor, []string{"all-children"}) {
|
||||
t.Errorf("step[1].WaitsFor = %v, want [all-children]", steps[1].WaitsFor)
|
||||
}
|
||||
|
||||
// finish waits for multiple conditions
|
||||
if !reflect.DeepEqual(steps[2].WaitsFor, []string{"all-children", "external-signal"}) {
|
||||
t.Errorf("step[2].WaitsFor = %v, want [all-children, external-signal]", steps[2].WaitsFor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMoleculeSteps_CaseInsensitive(t *testing.T) {
|
||||
desc := `## STEP: Design
|
||||
Plan the work.
|
||||
NEEDS: nothing
|
||||
TIER: SONNET
|
||||
WAITSFOR: All-Children
|
||||
|
||||
## step: implement
|
||||
Write code.
|
||||
needs: Design
|
||||
tier: Haiku`
|
||||
tier: Haiku
|
||||
waitsfor: some-condition`
|
||||
|
||||
steps, err := ParseMoleculeSteps(desc)
|
||||
if err != nil {
|
||||
@@ -169,6 +209,10 @@ tier: Haiku`
|
||||
if steps[0].Tier != "sonnet" {
|
||||
t.Errorf("step[0].Tier = %q, want sonnet", steps[0].Tier)
|
||||
}
|
||||
// WaitsFor values preserve case
|
||||
if !reflect.DeepEqual(steps[0].WaitsFor, []string{"All-Children"}) {
|
||||
t.Errorf("step[0].WaitsFor = %v, want [All-Children]", steps[0].WaitsFor)
|
||||
}
|
||||
|
||||
if steps[1].Ref != "implement" {
|
||||
t.Errorf("step[1].Ref = %q, want implement", steps[1].Ref)
|
||||
@@ -176,6 +220,9 @@ tier: Haiku`
|
||||
if steps[1].Tier != "haiku" {
|
||||
t.Errorf("step[1].Tier = %q, want haiku", steps[1].Tier)
|
||||
}
|
||||
if !reflect.DeepEqual(steps[1].WaitsFor, []string{"some-condition"}) {
|
||||
t.Errorf("step[1].WaitsFor = %v, want [some-condition]", steps[1].WaitsFor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMoleculeSteps_EngineerInBox(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user