fix(sling): register hq-cv- prefix for convoy beads (#475)

Instead of changing the convoy ID format, register the hq-cv- prefix
as a valid route pointing to town beads. This preserves the semantic
meaning of convoy IDs (hq-cv-xxxxx) while fixing the prefix mismatch.

Changes:
- Register hq-cv- prefix during gt install
- Add doctor check and fix for missing convoy route
- Update routes_check tests for both hq- and hq-cv- routes

Fixes: gt-4nmfh

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Keith Wyatt
2026-01-13 13:19:15 -08:00
committed by GitHub
parent 503e66ba8d
commit f42ec42268
4 changed files with 48 additions and 18 deletions

View File

@@ -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
}

View File

@@ -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: <issue-title>"

View File

@@ -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)

View File

@@ -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)
}