Merge pull request #238 from joshuavial/fix/sling-on-wisp-json-parse
fix(sling): parse bd mol wisp --json new_epic_id
This commit is contained in:
@@ -23,6 +23,39 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type wispCreateJSON struct {
|
||||||
|
NewEpicID string `json:"new_epic_id"`
|
||||||
|
RootID string `json:"root_id"`
|
||||||
|
ResultID string `json:"result_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWispIDFromJSON(jsonOutput []byte) (string, error) {
|
||||||
|
var result wispCreateJSON
|
||||||
|
if err := json.Unmarshal(jsonOutput, &result); err != nil {
|
||||||
|
return "", fmt.Errorf("parsing wisp JSON: %w (output: %s)", err, trimJSONForError(jsonOutput))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case result.NewEpicID != "":
|
||||||
|
return result.NewEpicID, nil
|
||||||
|
case result.RootID != "":
|
||||||
|
return result.RootID, nil
|
||||||
|
case result.ResultID != "":
|
||||||
|
return result.ResultID, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("wisp JSON missing id field (expected one of new_epic_id, root_id, result_id); output: %s", trimJSONForError(jsonOutput))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimJSONForError(jsonOutput []byte) string {
|
||||||
|
s := strings.TrimSpace(string(jsonOutput))
|
||||||
|
const maxLen = 500
|
||||||
|
if len(s) > maxLen {
|
||||||
|
return s[:maxLen] + "..."
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
var slingCmd = &cobra.Command{
|
var slingCmd = &cobra.Command{
|
||||||
Use: "sling <bead-or-formula> [target]",
|
Use: "sling <bead-or-formula> [target]",
|
||||||
GroupID: GroupWork,
|
GroupID: GroupWork,
|
||||||
@@ -372,13 +405,10 @@ func runSling(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse wisp output to get the root ID
|
// Parse wisp output to get the root ID
|
||||||
var wispResult struct {
|
wispRootID, err := parseWispIDFromJSON(wispOut)
|
||||||
RootID string `json:"root_id"`
|
if err != nil {
|
||||||
}
|
|
||||||
if err := json.Unmarshal(wispOut, &wispResult); err != nil {
|
|
||||||
return fmt.Errorf("parsing wisp output: %w", err)
|
return fmt.Errorf("parsing wisp output: %w", err)
|
||||||
}
|
}
|
||||||
wispRootID := wispResult.RootID
|
|
||||||
fmt.Printf("%s Formula wisp created: %s\n", style.Bold.Render("✓"), wispRootID)
|
fmt.Printf("%s Formula wisp created: %s\n", style.Bold.Render("✓"), wispRootID)
|
||||||
|
|
||||||
// Step 3: Bond wisp to original bead (creates compound)
|
// Step 3: Bond wisp to original bead (creates compound)
|
||||||
@@ -931,21 +961,17 @@ func runSlingFormula(args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse wisp output to get the root ID
|
// Parse wisp output to get the root ID
|
||||||
var wispResult struct {
|
wispRootID, err := parseWispIDFromJSON(wispOut)
|
||||||
RootID string `json:"root_id"`
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("parsing wisp output: %w", err)
|
||||||
if err := json.Unmarshal(wispOut, &wispResult); err != nil {
|
|
||||||
// Fallback: use formula name as identifier, but warn user
|
|
||||||
fmt.Printf("%s Could not parse wisp output, using formula name as ID\n", style.Dim.Render("Warning:"))
|
|
||||||
wispResult.RootID = formulaName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Wisp created: %s\n", style.Bold.Render("✓"), wispResult.RootID)
|
fmt.Printf("%s Wisp created: %s\n", style.Bold.Render("✓"), wispRootID)
|
||||||
|
|
||||||
// Step 3: Hook the wisp bead using bd update.
|
// Step 3: Hook the wisp bead using bd update.
|
||||||
// See: https://github.com/steveyegge/gastown/issues/148
|
// See: https://github.com/steveyegge/gastown/issues/148
|
||||||
hookCmd := exec.Command("bd", "--no-daemon", "update", wispResult.RootID, "--status=hooked", "--assignee="+targetAgent)
|
hookCmd := exec.Command("bd", "--no-daemon", "update", wispRootID, "--status=hooked", "--assignee="+targetAgent)
|
||||||
hookCmd.Dir = beads.ResolveHookDir(townRoot, wispResult.RootID, "")
|
hookCmd.Dir = beads.ResolveHookDir(townRoot, wispRootID, "")
|
||||||
hookCmd.Stderr = os.Stderr
|
hookCmd.Stderr = os.Stderr
|
||||||
if err := hookCmd.Run(); err != nil {
|
if err := hookCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("hooking wisp bead: %w", err)
|
return fmt.Errorf("hooking wisp bead: %w", err)
|
||||||
@@ -954,23 +980,23 @@ func runSlingFormula(args []string) error {
|
|||||||
|
|
||||||
// Log sling event to activity feed (formula slinging)
|
// Log sling event to activity feed (formula slinging)
|
||||||
actor := detectActor()
|
actor := detectActor()
|
||||||
payload := events.SlingPayload(wispResult.RootID, targetAgent)
|
payload := events.SlingPayload(wispRootID, targetAgent)
|
||||||
payload["formula"] = formulaName
|
payload["formula"] = formulaName
|
||||||
_ = events.LogFeed(events.TypeSling, actor, payload)
|
_ = events.LogFeed(events.TypeSling, actor, payload)
|
||||||
|
|
||||||
// Update agent bead's hook_bead field (ZFC: agents track their current work)
|
// Update agent bead's hook_bead field (ZFC: agents track their current work)
|
||||||
// Note: formula slinging uses town root as workDir (no polecat-specific path)
|
// Note: formula slinging uses town root as workDir (no polecat-specific path)
|
||||||
updateAgentHookBead(targetAgent, wispResult.RootID, "", townBeadsDir)
|
updateAgentHookBead(targetAgent, wispRootID, "", townBeadsDir)
|
||||||
|
|
||||||
// Store dispatcher in bead description (enables completion notification to dispatcher)
|
// Store dispatcher in bead description (enables completion notification to dispatcher)
|
||||||
if err := storeDispatcherInBead(wispResult.RootID, actor); err != nil {
|
if err := storeDispatcherInBead(wispRootID, actor); err != nil {
|
||||||
// Warn but don't fail - polecat will still complete work
|
// Warn but don't fail - polecat will still complete work
|
||||||
fmt.Printf("%s Could not store dispatcher in bead: %v\n", style.Dim.Render("Warning:"), err)
|
fmt.Printf("%s Could not store dispatcher in bead: %v\n", style.Dim.Render("Warning:"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store args in wisp bead if provided (no-tmux mode: beads as data plane)
|
// Store args in wisp bead if provided (no-tmux mode: beads as data plane)
|
||||||
if slingArgs != "" {
|
if slingArgs != "" {
|
||||||
if err := storeArgsInBead(wispResult.RootID, slingArgs); err != nil {
|
if err := storeArgsInBead(wispRootID, slingArgs); err != nil {
|
||||||
fmt.Printf("%s Could not store args in bead: %v\n", style.Dim.Render("Warning:"), err)
|
fmt.Printf("%s Could not store args in bead: %v\n", style.Dim.Render("Warning:"), err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s Args stored in bead (durable)\n", style.Bold.Render("✓"))
|
fmt.Printf("%s Args stored in bead (durable)\n", style.Bold.Render("✓"))
|
||||||
|
|||||||
@@ -2,6 +2,58 @@ package cmd
|
|||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseWispIDFromJSON(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
json string
|
||||||
|
wantID string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "new_epic_id",
|
||||||
|
json: `{"new_epic_id":"gt-wisp-abc","created":7,"phase":"vapor"}`,
|
||||||
|
wantID: "gt-wisp-abc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "root_id legacy",
|
||||||
|
json: `{"root_id":"gt-wisp-legacy"}`,
|
||||||
|
wantID: "gt-wisp-legacy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "result_id forward compat",
|
||||||
|
json: `{"result_id":"gt-wisp-result"}`,
|
||||||
|
wantID: "gt-wisp-result",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "precedence prefers new_epic_id",
|
||||||
|
json: `{"root_id":"gt-wisp-legacy","new_epic_id":"gt-wisp-new"}`,
|
||||||
|
wantID: "gt-wisp-new",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing id keys",
|
||||||
|
json: `{"created":7,"phase":"vapor"}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid JSON",
|
||||||
|
json: `{"new_epic_id":`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotID, err := parseWispIDFromJSON([]byte(tt.json))
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Fatalf("parseWispIDFromJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if gotID != tt.wantID {
|
||||||
|
t.Fatalf("parseWispIDFromJSON() id = %q, want %q", gotID, tt.wantID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFormatTrackBeadID(t *testing.T) {
|
func TestFormatTrackBeadID(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user