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:
@@ -404,6 +404,12 @@ func initTownBeads(townPath string) error {
|
|||||||
fmt.Printf(" %s Could not update routes.jsonl: %v\n", style.Dim.Render("⚠"), err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ func createAutoConvoy(beadID, beadTitle string) (string, error) {
|
|||||||
|
|
||||||
townBeads := filepath.Join(townRoot, ".beads")
|
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())
|
convoyID := fmt.Sprintf("hq-cv-%s", slingGenerateShortID())
|
||||||
|
|
||||||
// Create convoy with title "Work: <issue-title>"
|
// Create convoy with title "Work: <issue-title>"
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ func (c *RoutesCheck) Run(ctx *CheckContext) *CheckResult {
|
|||||||
|
|
||||||
var details []string
|
var details []string
|
||||||
var missingTownRoute bool
|
var missingTownRoute bool
|
||||||
|
var missingConvoyRoute bool
|
||||||
|
|
||||||
// Check town root route exists (hq- -> .)
|
// Check town root route exists (hq- -> .)
|
||||||
if _, hasTownRoute := routeByPrefix["hq-"]; !hasTownRoute {
|
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")
|
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
|
// Load rigs registry
|
||||||
rigsPath := filepath.Join(ctx.TownRoot, "mayor", "rigs.json")
|
rigsPath := filepath.Join(ctx.TownRoot, "mayor", "rigs.json")
|
||||||
rigsConfig, err := config.LoadRigsConfig(rigsPath)
|
rigsConfig, err := config.LoadRigsConfig(rigsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// No rigs config - check for missing town route and validate existing routes
|
// No rigs config - check for missing town/convoy routes and validate existing routes
|
||||||
if missingTownRoute {
|
if missingTownRoute || missingConvoyRoute {
|
||||||
return &CheckResult{
|
return &CheckResult{
|
||||||
Name: c.Name(),
|
Name: c.Name(),
|
||||||
Status: StatusWarning,
|
Status: StatusWarning,
|
||||||
Message: "Town root route is missing",
|
Message: "Required town routes are missing",
|
||||||
Details: details,
|
Details: details,
|
||||||
FixHint: "Run 'gt doctor --fix' to add missing routes",
|
FixHint: "Run 'gt doctor --fix' to add missing routes",
|
||||||
}
|
}
|
||||||
@@ -155,13 +162,16 @@ func (c *RoutesCheck) Run(ctx *CheckContext) *CheckResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine result
|
// Determine result
|
||||||
if missingTownRoute || len(missingRigs) > 0 || len(invalidRoutes) > 0 {
|
if missingTownRoute || missingConvoyRoute || len(missingRigs) > 0 || len(invalidRoutes) > 0 {
|
||||||
status := StatusWarning
|
status := StatusWarning
|
||||||
var messageParts []string
|
var messageParts []string
|
||||||
|
|
||||||
if missingTownRoute {
|
if missingTownRoute {
|
||||||
messageParts = append(messageParts, "town root route missing")
|
messageParts = append(messageParts, "town root route missing")
|
||||||
}
|
}
|
||||||
|
if missingConvoyRoute {
|
||||||
|
messageParts = append(messageParts, "convoy route missing")
|
||||||
|
}
|
||||||
if len(missingRigs) > 0 {
|
if len(missingRigs) > 0 {
|
||||||
messageParts = append(messageParts, fmt.Sprintf("%d rig(s) missing routes", len(missingRigs)))
|
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
|
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
|
// Load rigs registry
|
||||||
rigsPath := filepath.Join(ctx.TownRoot, "mayor", "rigs.json")
|
rigsPath := filepath.Join(ctx.TownRoot, "mayor", "rigs.json")
|
||||||
rigsConfig, err := config.LoadRigsConfig(rigsPath)
|
rigsConfig, err := config.LoadRigsConfig(rigsPath)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func TestRoutesCheck_MissingTownRoute(t *testing.T) {
|
|||||||
t.Fatal(err)
|
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")
|
routesPath := filepath.Join(beadsDir, "routes.jsonl")
|
||||||
routesContent := `{"prefix": "gt-", "path": "gastown/mayor/rig"}
|
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)
|
t.Errorf("expected StatusWarning, got %v: %s", result.Status, result.Message)
|
||||||
}
|
}
|
||||||
// When no rigs.json exists, the message comes from the early return path
|
// When no rigs.json exists, the message comes from the early return path
|
||||||
if result.Message != "Town root route is missing" {
|
if result.Message != "Required town routes are missing" {
|
||||||
t.Errorf("expected 'Town root route is missing', got %s", result.Message)
|
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)
|
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")
|
routesPath := filepath.Join(beadsDir, "routes.jsonl")
|
||||||
routesContent := `{"prefix": "hq-", "path": "."}
|
routesContent := `{"prefix": "hq-", "path": "."}
|
||||||
|
{"prefix": "hq-cv-", "path": "."}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(routesPath, []byte(routesContent), 0644); err != nil {
|
if err := os.WriteFile(routesPath, []byte(routesContent), 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -103,7 +104,7 @@ func TestRoutesCheck_FixRestoresTownRoute(t *testing.T) {
|
|||||||
t.Fatalf("Fix failed: %v", err)
|
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)
|
content, err := os.ReadFile(routesPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to read routes.jsonl: %v", err)
|
t.Fatalf("Failed to read routes.jsonl: %v", err)
|
||||||
@@ -115,6 +116,7 @@ func TestRoutesCheck_FixRestoresTownRoute(t *testing.T) {
|
|||||||
|
|
||||||
contentStr := string(content)
|
contentStr := string(content)
|
||||||
if contentStr != `{"prefix":"hq-","path":"."}
|
if contentStr != `{"prefix":"hq-","path":"."}
|
||||||
|
{"prefix":"hq-cv-","path":"."}
|
||||||
` {
|
` {
|
||||||
t.Errorf("unexpected routes.jsonl content: %s", contentStr)
|
t.Errorf("unexpected routes.jsonl content: %s", contentStr)
|
||||||
}
|
}
|
||||||
@@ -141,7 +143,7 @@ func TestRoutesCheck_FixRestoresTownRoute(t *testing.T) {
|
|||||||
t.Fatal(err)
|
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")
|
routesPath := filepath.Join(beadsDir, "routes.jsonl")
|
||||||
routesContent := `{"prefix": "my-", "path": "myrig/mayor/rig"}
|
routesContent := `{"prefix": "my-", "path": "myrig/mayor/rig"}
|
||||||
`
|
`
|
||||||
@@ -162,16 +164,17 @@ func TestRoutesCheck_FixRestoresTownRoute(t *testing.T) {
|
|||||||
t.Fatalf("Fix failed: %v", err)
|
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)
|
content, err := os.ReadFile(routesPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to read routes.jsonl: %v", err)
|
t.Fatalf("Failed to read routes.jsonl: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
contentStr := string(content)
|
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"}
|
if contentStr != `{"prefix":"my-","path":"myrig/mayor/rig"}
|
||||||
{"prefix":"hq-","path":"."}
|
{"prefix":"hq-","path":"."}
|
||||||
|
{"prefix":"hq-cv-","path":"."}
|
||||||
` {
|
` {
|
||||||
t.Errorf("unexpected routes.jsonl content: %s", contentStr)
|
t.Errorf("unexpected routes.jsonl content: %s", contentStr)
|
||||||
}
|
}
|
||||||
@@ -186,9 +189,10 @@ func TestRoutesCheck_FixRestoresTownRoute(t *testing.T) {
|
|||||||
t.Fatal(err)
|
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")
|
routesPath := filepath.Join(beadsDir, "routes.jsonl")
|
||||||
originalContent := `{"prefix": "hq-", "path": "."}
|
originalContent := `{"prefix": "hq-", "path": "."}
|
||||||
|
{"prefix": "hq-cv-", "path": "."}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(routesPath, []byte(originalContent), 0644); err != nil {
|
if err := os.WriteFile(routesPath, []byte(originalContent), 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -246,12 +250,12 @@ func TestRoutesCheck_CorruptedRoutesJsonl(t *testing.T) {
|
|||||||
result := check.Run(ctx)
|
result := check.Run(ctx)
|
||||||
|
|
||||||
// Corrupted/malformed lines are skipped, resulting in empty routes
|
// 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 {
|
if result.Status != StatusWarning {
|
||||||
t.Errorf("expected StatusWarning, got %v: %s", result.Status, result.Message)
|
t.Errorf("expected StatusWarning, got %v: %s", result.Status, result.Message)
|
||||||
}
|
}
|
||||||
if result.Message != "Town root route is missing" {
|
if result.Message != "Required town routes are missing" {
|
||||||
t.Errorf("expected 'Town root route is missing', got %s", result.Message)
|
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)
|
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)
|
content, err := os.ReadFile(routesPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to read routes.jsonl: %v", err)
|
t.Fatalf("Failed to read routes.jsonl: %v", err)
|
||||||
@@ -291,6 +295,7 @@ func TestRoutesCheck_CorruptedRoutesJsonl(t *testing.T) {
|
|||||||
|
|
||||||
contentStr := string(content)
|
contentStr := string(content)
|
||||||
if contentStr != `{"prefix":"hq-","path":"."}
|
if contentStr != `{"prefix":"hq-","path":"."}
|
||||||
|
{"prefix":"hq-cv-","path":"."}
|
||||||
` {
|
` {
|
||||||
t.Errorf("unexpected routes.jsonl content after fix: %s", contentStr)
|
t.Errorf("unexpected routes.jsonl content after fix: %s", contentStr)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user