diff --git a/internal/cmd/install.go b/internal/cmd/install.go index 53b2121f..97f66ff3 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -404,6 +404,12 @@ func initTownBeads(townPath string) error { fmt.Printf(" %s Could not update routes.jsonl: %v\n", style.Dim.Render("⚠"), err) } + // Register hq-cv- prefix for convoy beads (auto-created by gt sling). + // Convoys use hq-cv-* IDs for visual distinction from other town beads. + if err := beads.AppendRoute(townPath, beads.Route{Prefix: "hq-cv-", Path: "."}); err != nil { + fmt.Printf(" %s Could not register convoy prefix: %v\n", style.Dim.Render("⚠"), err) + } + return nil } diff --git a/internal/cmd/sling_convoy.go b/internal/cmd/sling_convoy.go index d4bc23e9..06355ade 100644 --- a/internal/cmd/sling_convoy.go +++ b/internal/cmd/sling_convoy.go @@ -65,7 +65,8 @@ func createAutoConvoy(beadID, beadTitle string) (string, error) { townBeads := filepath.Join(townRoot, ".beads") - // Generate convoy ID with cv- prefix + // Generate convoy ID with hq-cv- prefix for visual distinction + // The hq-cv- prefix is registered in routes during gt install convoyID := fmt.Sprintf("hq-cv-%s", slingGenerateShortID()) // Create convoy with title "Work: " diff --git a/internal/doctor/routes_check.go b/internal/doctor/routes_check.go index 1c2a5d49..b9385ef4 100644 --- a/internal/doctor/routes_check.go +++ b/internal/doctor/routes_check.go @@ -75,6 +75,7 @@ func (c *RoutesCheck) Run(ctx *CheckContext) *CheckResult { var details []string var missingTownRoute bool + var missingConvoyRoute bool // Check town root route exists (hq- -> .) if _, hasTownRoute := routeByPrefix["hq-"]; !hasTownRoute { @@ -82,16 +83,22 @@ func (c *RoutesCheck) Run(ctx *CheckContext) *CheckResult { details = append(details, "Town root route (hq- -> .) is missing") } + // Check convoy route exists (hq-cv- -> .) + if _, hasConvoyRoute := routeByPrefix["hq-cv-"]; !hasConvoyRoute { + missingConvoyRoute = true + details = append(details, "Convoy route (hq-cv- -> .) is missing") + } + // Load rigs registry rigsPath := filepath.Join(ctx.TownRoot, "mayor", "rigs.json") rigsConfig, err := config.LoadRigsConfig(rigsPath) if err != nil { - // No rigs config - check for missing town route and validate existing routes - if missingTownRoute { + // No rigs config - check for missing town/convoy routes and validate existing routes + if missingTownRoute || missingConvoyRoute { return &CheckResult{ Name: c.Name(), Status: StatusWarning, - Message: "Town root route is missing", + Message: "Required town routes are missing", Details: details, FixHint: "Run 'gt doctor --fix' to add missing routes", } @@ -155,13 +162,16 @@ func (c *RoutesCheck) Run(ctx *CheckContext) *CheckResult { } // Determine result - if missingTownRoute || len(missingRigs) > 0 || len(invalidRoutes) > 0 { + if missingTownRoute || missingConvoyRoute || len(missingRigs) > 0 || len(invalidRoutes) > 0 { status := StatusWarning var messageParts []string if missingTownRoute { messageParts = append(messageParts, "town root route missing") } + if missingConvoyRoute { + messageParts = append(messageParts, "convoy route missing") + } if len(missingRigs) > 0 { messageParts = append(messageParts, fmt.Sprintf("%d rig(s) missing routes", len(missingRigs))) } @@ -249,6 +259,14 @@ func (c *RoutesCheck) Fix(ctx *CheckContext) error { modified = true } + // Ensure convoy route exists (hq-cv- -> .) + // Convoys use hq-cv-* IDs for visual distinction from other town beads + if !routeMap["hq-cv-"] { + routes = append(routes, beads.Route{Prefix: "hq-cv-", Path: "."}) + routeMap["hq-cv-"] = true + modified = true + } + // Load rigs registry rigsPath := filepath.Join(ctx.TownRoot, "mayor", "rigs.json") rigsConfig, err := config.LoadRigsConfig(rigsPath) diff --git a/internal/doctor/routes_check_test.go b/internal/doctor/routes_check_test.go index 43692929..8bc238c9 100644 --- a/internal/doctor/routes_check_test.go +++ b/internal/doctor/routes_check_test.go @@ -16,7 +16,7 @@ func TestRoutesCheck_MissingTownRoute(t *testing.T) { t.Fatal(err) } - // Create routes.jsonl with only a rig route (no hq- route) + // Create routes.jsonl with only a rig route (no hq- or hq-cv- routes) routesPath := filepath.Join(beadsDir, "routes.jsonl") routesContent := `{"prefix": "gt-", "path": "gastown/mayor/rig"} ` @@ -37,8 +37,8 @@ func TestRoutesCheck_MissingTownRoute(t *testing.T) { t.Errorf("expected StatusWarning, got %v: %s", result.Status, result.Message) } // When no rigs.json exists, the message comes from the early return path - if result.Message != "Town root route is missing" { - t.Errorf("expected 'Town root route is missing', got %s", result.Message) + if result.Message != "Required town routes are missing" { + t.Errorf("expected 'Required town routes are missing', got %s", result.Message) } }) @@ -51,9 +51,10 @@ func TestRoutesCheck_MissingTownRoute(t *testing.T) { t.Fatal(err) } - // Create routes.jsonl with hq- route + // Create routes.jsonl with both hq- and hq-cv- routes routesPath := filepath.Join(beadsDir, "routes.jsonl") routesContent := `{"prefix": "hq-", "path": "."} +{"prefix": "hq-cv-", "path": "."} ` if err := os.WriteFile(routesPath, []byte(routesContent), 0644); err != nil { t.Fatal(err) @@ -103,7 +104,7 @@ func TestRoutesCheck_FixRestoresTownRoute(t *testing.T) { t.Fatalf("Fix failed: %v", err) } - // Verify routes.jsonl now contains hq- route + // Verify routes.jsonl now contains both hq- and hq-cv- routes content, err := os.ReadFile(routesPath) if err != nil { t.Fatalf("Failed to read routes.jsonl: %v", err) @@ -115,6 +116,7 @@ func TestRoutesCheck_FixRestoresTownRoute(t *testing.T) { contentStr := string(content) if contentStr != `{"prefix":"hq-","path":"."} +{"prefix":"hq-cv-","path":"."} ` { t.Errorf("unexpected routes.jsonl content: %s", contentStr) } @@ -141,7 +143,7 @@ func TestRoutesCheck_FixRestoresTownRoute(t *testing.T) { t.Fatal(err) } - // Create routes.jsonl with only a rig route (no hq- route) + // Create routes.jsonl with only a rig route (no hq- or hq-cv- routes) routesPath := filepath.Join(beadsDir, "routes.jsonl") routesContent := `{"prefix": "my-", "path": "myrig/mayor/rig"} ` @@ -162,16 +164,17 @@ func TestRoutesCheck_FixRestoresTownRoute(t *testing.T) { t.Fatalf("Fix failed: %v", err) } - // Verify routes.jsonl now contains both routes + // Verify routes.jsonl now contains all routes content, err := os.ReadFile(routesPath) if err != nil { t.Fatalf("Failed to read routes.jsonl: %v", err) } contentStr := string(content) - // Should have both the original rig route and the new hq- route + // Should have the original rig route plus both hq- and hq-cv- routes if contentStr != `{"prefix":"my-","path":"myrig/mayor/rig"} {"prefix":"hq-","path":"."} +{"prefix":"hq-cv-","path":"."} ` { t.Errorf("unexpected routes.jsonl content: %s", contentStr) } @@ -186,9 +189,10 @@ func TestRoutesCheck_FixRestoresTownRoute(t *testing.T) { t.Fatal(err) } - // Create routes.jsonl with hq- route already present + // Create routes.jsonl with both hq- and hq-cv- routes already present routesPath := filepath.Join(beadsDir, "routes.jsonl") originalContent := `{"prefix": "hq-", "path": "."} +{"prefix": "hq-cv-", "path": "."} ` if err := os.WriteFile(routesPath, []byte(originalContent), 0644); err != nil { t.Fatal(err) @@ -246,12 +250,12 @@ func TestRoutesCheck_CorruptedRoutesJsonl(t *testing.T) { result := check.Run(ctx) // Corrupted/malformed lines are skipped, resulting in empty routes - // This triggers the "Town root route is missing" warning + // This triggers the "Required town routes are missing" warning if result.Status != StatusWarning { t.Errorf("expected StatusWarning, got %v: %s", result.Status, result.Message) } - if result.Message != "Town root route is missing" { - t.Errorf("expected 'Town root route is missing', got %s", result.Message) + if result.Message != "Required town routes are missing" { + t.Errorf("expected 'Required town routes are missing', got %s", result.Message) } }) @@ -283,7 +287,7 @@ func TestRoutesCheck_CorruptedRoutesJsonl(t *testing.T) { t.Fatalf("Fix failed: %v", err) } - // Verify routes.jsonl now contains hq- route + // Verify routes.jsonl now contains both hq- and hq-cv- routes content, err := os.ReadFile(routesPath) if err != nil { t.Fatalf("Failed to read routes.jsonl: %v", err) @@ -291,6 +295,7 @@ func TestRoutesCheck_CorruptedRoutesJsonl(t *testing.T) { contentStr := string(content) if contentStr != `{"prefix":"hq-","path":"."} +{"prefix":"hq-cv-","path":"."} ` { t.Errorf("unexpected routes.jsonl content after fix: %s", contentStr) }