From b8a5ee162b9ce9fc22c248407258b00293483767 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 29 Dec 2025 21:04:28 -0800 Subject: [PATCH] feat: Add tracks relation type for convoy tracking (bd-3roq) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds non-blocking tracks dependency type for convoy to issue relationships: - Non-blocking: does not affect ready work calculation - Cross-prefix capable: convoys in hq-* can track issues in gt-*, bd-* - Reverse lookup: bd dep list --direction=up -t tracks Also adds bd dep list command with direction and type filtering for querying dependencies/dependents. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .beads/issues.jsonl | 4 +- cmd/bd/dep.go | 139 +++++++++++++++++++++++++++++- internal/storage/memory/memory.go | 43 +++++++++ internal/storage/storage.go | 2 + internal/types/types.go | 5 +- 5 files changed, 189 insertions(+), 4 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 97d9b1fa..0488334c 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -73,7 +73,7 @@ {"id":"bd-3bsz","title":"gt mail send: support reading message body from stdin","description":"Currently gt mail send -m requires the message as a command-line argument, which causes shell escaping issues with backticks, quotes, and special characters.\n\nAdd support for reading message body from stdin:\n- gt mail send addr -s 'Subject' --stdin # Read body from stdin\n- echo 'body' | gt mail send addr -s 'Subject' -m - # Convention: -m - means stdin\n\nThis would allow:\ncat \u003c\u003c'EOF' | gt mail send addr -s 'Subject' --stdin\nMessage with `backticks` and 'quotes' safely\nEOF\n\nWithout this, agents struggle to send handoff messages containing code snippets.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-23T03:21:39.496208-08:00","updated_at":"2025-12-23T12:19:44.443554-08:00","closed_at":"2025-12-23T12:19:44.443554-08:00"} {"id":"bd-3ggb","title":"Rebuild local binary","description":"Build and verify: go build -o bd ./cmd/bd \u0026\u0026 ./bd version","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-18T22:43:03.101428-08:00","updated_at":"2025-12-24T16:25:30.089869-08:00","dependencies":[{"issue_id":"bd-3ggb","depends_on_id":"bd-qqc","type":"parent-child","created_at":"2025-12-18T22:43:16.748289-08:00","created_by":"daemon"},{"issue_id":"bd-3ggb","depends_on_id":"bd-4y4g","type":"blocks","created_at":"2025-12-18T22:43:20.950376-08:00","created_by":"daemon"}],"deleted_at":"2025-12-24T16:25:30.089869-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"} {"id":"bd-3jcw","title":"activity.go: Missing test coverage","description":"The new activity.go command (from bd-xo1o.3) has no test coverage. At minimum, tests should cover:\n- parseDurationString() for various formats (5m, 1h, 2d, invalid)\n- filterEvents() for --mol and --type filtering\n- formatEvent() and getEventDisplay() for all mutation types\n\nDiscovered during code review of bd-xo1o implementation.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-23T04:06:15.563579-08:00","updated_at":"2025-12-23T04:14:56.150151-08:00","closed_at":"2025-12-23T04:14:56.150151-08:00"} -{"id":"bd-3roq","title":"Add 'tracks' relation type for convoy tracking","description":"Add a new relation type 'tracks' for convoy → issue relationships.\n\nUnlike 'depends_on':\n- Non-blocking (tracked issue doesn't block convoy)\n- Cross-prefix capable (convoy in hq-* tracks issues in gt-*, bd-*)\n- Supports reverse lookup ('what convoys track this issue?')\n\nUsed by convoys to track issues across project chains without creating dependencies.\n\nRelated: hq-7h8jx (Convoy System epic in town beads)","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-29T18:47:00.581639-08:00","created_by":"mayor","updated_at":"2025-12-29T18:47:00.581639-08:00"} +{"id":"bd-3roq","title":"Add 'tracks' relation type for convoy tracking","description":"Add a new relation type 'tracks' for convoy → issue relationships.\n\nUnlike 'depends_on':\n- Non-blocking (tracked issue doesn't block convoy)\n- Cross-prefix capable (convoy in hq-* tracks issues in gt-*, bd-*)\n- Supports reverse lookup ('what convoys track this issue?')\n\nUsed by convoys to track issues across project chains without creating dependencies.\n\nRelated: hq-7h8jx (Convoy System epic in town beads)","status":"in_progress","priority":1,"issue_type":"task","created_at":"2025-12-29T18:47:00.581639-08:00","created_by":"mayor","updated_at":"2025-12-29T20:58:37.388899-08:00"} {"id":"bd-3sz0","title":"Auto-repair stale merge driver configs with invalid placeholders","description":"Old bd versions (\u003c0.24.0) installed merge driver with invalid placeholders %L %R instead of %A %B. Add detection to bd doctor --fix: check if git config merge.beads.driver contains %L or %R, auto-repair to 'bd merge %A %O %A %B'. One-time migration for users who initialized with old versions.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-21T23:16:10.762808-08:00","updated_at":"2025-12-23T23:48:18.11858-08:00","closed_at":"2025-12-23T23:48:18.11858-08:00","dependencies":[{"issue_id":"bd-3sz0","depends_on_id":"bd-tbz3","type":"parent-child","created_at":"2025-11-21T23:16:10.763612-08:00","created_by":"daemon"}]} {"id":"bd-3u8m","title":"Create bd admin parent command and nest cleanup/compact/reset","description":"## Task\nCreate new `bd admin` parent command and move:\n- `bd cleanup` → `bd admin cleanup`\n- `bd compact` → `bd admin compact`\n- `bd reset` → `bd admin reset`\n\n## Implementation\n\n### 1. Create admin.go\nNew file with parent command:\n```go\nvar adminCmd = \u0026cobra.Command{\n Use: \"admin\",\n Short: \"Administrative commands for database maintenance\",\n Long: `Administrative commands for beads database maintenance.\n\nThese commands are for advanced users and should be used carefully:\n cleanup Delete closed issues and prune expired tombstones\n compact Compact old closed issues to save space\n reset Remove all beads data and configuration\n\nFor routine operations, prefer 'bd doctor --fix'.`,\n}\n\nfunc init() {\n rootCmd.AddCommand(adminCmd)\n adminCmd.AddCommand(cleanupCmd)\n adminCmd.AddCommand(compactCmd)\n adminCmd.AddCommand(resetCmd)\n}\n```\n\n### 2. Update cleanup.go, compact.go, reset.go\n- Remove `rootCmd.AddCommand()` from each init()\n- Keep all existing functionality\n\n### 3. Create hidden aliases for backwards compatibility\nTop-level hidden commands that forward to admin subcommands.\n\n### 4. Update docs (major updates)\n- docs/CLI_REFERENCE.md - cleanup, compact references\n- docs/QUICKSTART.md - compact, cleanup references\n- docs/FAQ.md - compact references\n- docs/TROUBLESHOOTING.md - compact references\n- docs/DELETIONS.md - compact reference\n- docs/CONFIG.md - compact reference\n- skills/beads/SKILL.md - compact reference\n- commands/compact.md - update all examples\n- examples/compaction/README.md - update examples\n\n## Files to create\n- cmd/bd/admin.go\n\n## Files to modify\n- cmd/bd/cleanup.go\n- cmd/bd/compact.go\n- cmd/bd/reset.go\n- cmd/bd/main.go (aliases)\n- Multiple docs (see list above)\n","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-27T15:10:41.836341-08:00","created_by":"mayor","updated_at":"2025-12-27T16:07:30.707323-08:00","closed_at":"2025-12-27T16:07:30.707323-08:00"} {"id":"bd-3uje","title":"Test issue for pin --for","description":"Testing the pin --for flag","status":"tombstone","priority":3,"issue_type":"task","created_at":"2025-12-22T02:53:43.075522-08:00","updated_at":"2025-12-22T02:54:07.973855-08:00","deleted_at":"2025-12-22T02:54:07.973855-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"} @@ -422,7 +422,7 @@ {"id":"bd-jdz3","title":"Add pager support to bd list","description":"Add pager support following gh cli conventions:\n\nFlags:\n- --no-pager: disable pager for this command\n\nEnvironment variables:\n- BD_PAGER / PAGER: pager program (default: less)\n- BD_NO_PAGER: disable pager globally\n\nBehavior:\n- Auto-enable pager when output exceeds terminal height\n- Respect LESS env var for pager options\n- Disable pager when stdout is not a TTY (pipes/scripts)","notes":"## Implementation Plan\n\n### Dependencies\n```go\nimport \"github.com/muesli/termenv\" // or golang.org/x/term\n```\n\n### Code Changes\n\n1. **Add pager helper** (internal/ui/pager.go):\n```go\nfunc ToPager(content string) error {\n // Check BD_NO_PAGER or --no-pager\n if os.Getenv(\"BD_NO_PAGER\") \\!= \"\" {\n fmt.Print(content)\n return nil\n }\n \n // Get pager command\n pager := os.Getenv(\"BD_PAGER\")\n if pager == \"\" {\n pager = os.Getenv(\"PAGER\")\n }\n if pager == \"\" {\n pager = \"less\"\n }\n \n // Check if content exceeds terminal height\n // If not, just print directly\n \n // Pipe to pager\n cmd := exec.Command(pager)\n cmd.Stdin = strings.NewReader(content)\n cmd.Stdout = os.Stdout\n cmd.Stderr = os.Stderr\n return cmd.Run()\n}\n```\n\n2. **Add --no-pager flag** (cmd/bd/list.go init):\n```go\nlistCmd.Flags().Bool(\"no-pager\", false, \"Disable pager output\")\n```\n\n3. **Use pager in list output** (end of Run):\n```go\nif \\!noPager \u0026\u0026 isTerminal(os.Stdout) {\n ui.ToPager(output.String())\n} else {\n fmt.Print(output.String())\n}\n```\n\n### Environment Variables\n- `BD_PAGER`: pager program (overrides PAGER)\n- `BD_NO_PAGER`: set to any value to disable\n- `PAGER`: fallback pager\n- `LESS`: passed through for less options\n\n### Testing\n- `bd list` pipes to pager when output \u003e terminal height\n- `bd list --no-pager` prints directly\n- `BD_NO_PAGER=1 bd list` prints directly\n- `bd list | cat` auto-disables pager (not a TTY)","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-29T15:25:09.109258-08:00","created_by":"stevey","updated_at":"2025-12-29T15:26:49.025186-08:00"} {"id":"bd-jgxi","title":"Auto-migrate database on CLI version bump","description":"When CLI is upgraded (e.g., 0.24.0 → 0.24.1), database version becomes stale. Add auto-migration in PersistentPreRun or daemon startup. Check dbVersion != CLIVersion and run bd migrate automatically. Fixes recurring UX issue where bd doctor shows version mismatch after every CLI upgrade.","status":"tombstone","priority":0,"issue_type":"feature","created_at":"2025-11-21T23:16:09.004619-08:00","updated_at":"2025-12-25T01:21:01.952723-08:00","dependencies":[{"issue_id":"bd-jgxi","depends_on_id":"bd-tbz3","type":"parent-child","created_at":"2025-11-21T23:16:09.005513-08:00","created_by":"daemon"}],"deleted_at":"2025-12-25T01:21:01.952723-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"feature"} {"id":"bd-jke6","title":"Add covering index (label, issue_id) for label queries","description":"GetIssuesByLabel joins labels table but requires table lookup after using idx_labels_label.\n\n**Query (labels.go:165):**\n```sql\nSELECT ... FROM issues i\nJOIN labels l ON i.id = l.issue_id\nWHERE l.label = ?\n```\n\n**Problem:** Current idx_labels_label index doesn't cover issue_id, requiring row lookup.\n\n**Solution:** Add migration:\n```sql\nCREATE INDEX IF NOT EXISTS idx_labels_label_issue ON labels(label, issue_id);\n```\n\nThis is a covering index - query can be satisfied entirely from the index without touching the labels table rows.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-22T22:58:51.485354-08:00","updated_at":"2025-12-22T23:15:13.839904-08:00","closed_at":"2025-12-22T23:15:13.839904-08:00","dependencies":[{"issue_id":"bd-jke6","depends_on_id":"bd-h0we","type":"discovered-from","created_at":"2025-12-22T22:58:51.485984-08:00","created_by":"daemon"}]} -{"id":"bd-jsk7","title":"Agent beads: structured labels for filtering","description":"## Agent Beads Need Queryable Labels\n\nCurrently agent beads have role_type/rig in description text, not as labels. This breaks @group resolution in gt mail.\n\n## Current State\n```json\n{\n \"id\": \"gt-gastown-witness\",\n \"issue_type\": \"agent\",\n \"description\": \"...\\\\nrole_type: witness\\\\nrig: gastown\\\\n...\"\n}\n```\n\nCannot query: `bd list --type=agent --label=role_type:witness` returns nothing.\n\n## Required\nAgent bead creation should add labels:\n- `role_type:\u003ctype\u003e` (witness, refinery, crew, polecat, dog, mayor, deacon)\n- `rig:\u003crig\u003e` (gastown, beads, or \"town\" for town-level)\n\n## Where to Fix\ngt polecat/crew/agent creation commands should add labels.\n\n## Acceptance\n- New agent beads created with role_type/rig labels\n- @group patterns work in gt mail router","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-29T20:49:13.444793-08:00","created_by":"gastown/crew/joe","updated_at":"2025-12-29T20:49:13.444793-08:00"} +{"id":"bd-jsk7","title":"Agent beads: structured labels for filtering","description":"## Agent Beads Need Queryable Labels\n\nCurrently agent beads have role_type/rig in description text, not as labels. This breaks @group resolution in gt mail.\n\n## Current State\n```json\n{\n \"id\": \"gt-gastown-witness\",\n \"issue_type\": \"agent\",\n \"description\": \"...\\\\nrole_type: witness\\\\nrig: gastown\\\\n...\"\n}\n```\n\nCannot query: `bd list --type=agent --label=role_type:witness` returns nothing.\n\n## Required\nAgent bead creation should add labels:\n- `role_type:\u003ctype\u003e` (witness, refinery, crew, polecat, dog, mayor, deacon)\n- `rig:\u003crig\u003e` (gastown, beads, or \"town\" for town-level)\n\n## Where to Fix\ngt polecat/crew/agent creation commands should add labels.\n\n## Acceptance\n- New agent beads created with role_type/rig labels\n- @group patterns work in gt mail router","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-29T20:49:13.444793-08:00","created_by":"gastown/crew/joe","updated_at":"2025-12-29T20:58:04.67866-08:00","closed_at":"2025-12-29T20:58:04.67866-08:00","close_reason":"Duplicate of bd-g7eq"} {"id":"bd-jv4w","title":"Phase 1.2: Separate bdt executable - Initial structure","description":"Create minimal bdt command structure completely separate from bd. Must not share code, config, or database.\n\n## Subtasks\n1. Create cmd/bdt/ directory with main.go\n2. Implement bdt version, help, and status commands\n3. Configure separate database location: $HOME/.bdt/ (not $HOME/.beads/)\n4. Create separate issues file: issues.toon (not issues.jsonl)\n5. Update build system:\n - Makefile: Add bdt target\n - .goreleaser.yml: Add bdt binary config\n\n## Files to Create\n- cmd/bdt/main.go - Entry point\n- cmd/bdt/version.go - Version handling\n- cmd/bdt/help.go - Help text (separate from bd)\n\n## Success Criteria\n- `make build` produces both `bd` and `bdt` executables\n- `bdt version` shows distinct version output from bd\n- `bdt --help` shows distinct help text\n- bdt uses $HOME/.bdt/ directory (verify with `bdt info`)\n- bd and bdt completely isolated (no shared imports beyond stdlib)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-19T11:48:34.866877282-07:00","updated_at":"2025-12-19T12:59:11.389296015-07:00","closed_at":"2025-12-19T12:59:11.389296015-07:00"} {"id":"bd-jvu","title":"Add bd update --parent flag to change issue parent","description":"Allow changing an issue's parent with bd update --parent \u003cnew-parent-id\u003e. Useful for reorganizing tasks under different epics or moving issues between hierarchies. Should update the parent-child dependency relationship.","status":"tombstone","priority":3,"issue_type":"feature","created_at":"2025-12-17T22:24:07.274485-08:00","updated_at":"2025-12-25T01:21:01.952723-08:00","deleted_at":"2025-12-25T01:21:01.952723-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"feature"} {"id":"bd-kblo","title":"bd prime should mention when beads is redirected","description":"## Problem\n\nAgents running in a redirected clone don't know they're sharing beads with other clones. This can cause confusion when molecules or issues seem to 'appear' or 'disappear'.\n\n## Proposed Solution\n\nWhen `bd prime` runs and detects a redirect, include it in the output:\n\n```\nBeads: /Users/stevey/gt/beads/mayor/rig/.beads\n (redirected from crew/dave - you share issues with other clones)\n```\n\n## Why\n\nVisibility over magic. If agents can see the redirect, they can reason about it.\n\n## Related\n\n- bd where command (shows this on demand)\n- gt redirect following (ensures gt matches bd behavior)","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-12-27T21:15:55.026907-08:00","created_by":"beads/crew/dave","updated_at":"2025-12-27T21:33:33.765635-08:00","closed_at":"2025-12-27T21:33:33.765635-08:00"} diff --git a/cmd/bd/dep.go b/cmd/bd/dep.go index 3fb1bd1b..fe8c26ad 100644 --- a/cmd/bd/dep.go +++ b/cmd/bd/dep.go @@ -221,6 +221,138 @@ Examples: }, } +var depListCmd = &cobra.Command{ + Use: "list [issue-id]", + Short: "List dependencies or dependents of an issue", + Long: `List dependencies or dependents of an issue with optional type filtering. + +By default shows dependencies (what this issue depends on). Use --direction to control: + - down: Show dependencies (what this issue depends on) - default + - up: Show dependents (what depends on this issue) + +Use --type to filter by dependency type (e.g., tracks, blocks, parent-child). + +Examples: + bd dep list gt-abc # Show what gt-abc depends on + bd dep list gt-abc --direction=up # Show what depends on gt-abc + bd dep list gt-abc --direction=up -t tracks # Show what tracks gt-abc (convoy tracking)`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + ctx := rootCtx + + // Resolve partial ID first + var fullID string + if daemonClient != nil { + resolveArgs := &rpc.ResolveIDArgs{ID: args[0]} + resp, err := daemonClient.ResolveID(resolveArgs) + if err != nil { + FatalErrorRespectJSON("resolving issue ID %s: %v", args[0], err) + } + if err := json.Unmarshal(resp.Data, &fullID); err != nil { + FatalErrorRespectJSON("unmarshaling resolved ID: %v", err) + } + } else { + var err error + fullID, err = utils.ResolvePartialID(ctx, store, args[0]) + if err != nil { + FatalErrorRespectJSON("resolving %s: %v", args[0], err) + } + } + + // If daemon is running but doesn't support this command, use direct storage + if daemonClient != nil && store == nil { + var err error + store, err = sqlite.New(rootCtx, dbPath) + if err != nil { + FatalErrorRespectJSON("failed to open database: %v", err) + } + defer func() { _ = store.Close() }() + } + + direction, _ := cmd.Flags().GetString("direction") + typeFilter, _ := cmd.Flags().GetString("type") + + if direction == "" { + direction = "down" + } + + var issues []*types.IssueWithDependencyMetadata + var err error + + if direction == "up" { + issues, err = store.GetDependentsWithMetadata(ctx, fullID) + } else { + issues, err = store.GetDependenciesWithMetadata(ctx, fullID) + } + if err != nil { + FatalErrorRespectJSON("%v", err) + } + + // Apply type filter if specified + if typeFilter != "" { + var filtered []*types.IssueWithDependencyMetadata + for _, iss := range issues { + if string(iss.DependencyType) == typeFilter { + filtered = append(filtered, iss) + } + } + issues = filtered + } + + if jsonOutput { + if issues == nil { + issues = []*types.IssueWithDependencyMetadata{} + } + outputJSON(issues) + return + } + + if len(issues) == 0 { + if typeFilter != "" { + if direction == "up" { + fmt.Printf("\nNo issues depend on %s with type '%s'\n", fullID, typeFilter) + } else { + fmt.Printf("\n%s has no dependencies of type '%s'\n", fullID, typeFilter) + } + } else { + if direction == "up" { + fmt.Printf("\nNo issues depend on %s\n", fullID) + } else { + fmt.Printf("\n%s has no dependencies\n", fullID) + } + } + return + } + + if direction == "up" { + fmt.Printf("\n%s Issues that depend on %s:\n\n", ui.RenderAccent("📋"), fullID) + } else { + fmt.Printf("\n%s %s depends on:\n\n", ui.RenderAccent("📋"), fullID) + } + + for _, iss := range issues { + // Color the ID based on status + var idStr string + switch iss.Status { + case types.StatusOpen: + idStr = ui.StatusOpenStyle.Render(iss.ID) + case types.StatusInProgress: + idStr = ui.StatusInProgressStyle.Render(iss.ID) + case types.StatusBlocked: + idStr = ui.StatusBlockedStyle.Render(iss.ID) + case types.StatusClosed: + idStr = ui.StatusClosedStyle.Render(iss.ID) + default: + idStr = iss.ID + } + + fmt.Printf(" %s: %s [P%d] (%s) via %s\n", + idStr, iss.Title, iss.Priority, iss.Status, iss.DependencyType) + } + fmt.Println() + }, +} + var depRemoveCmd = &cobra.Command{ Use: "remove [issue-id] [depends-on-id]", Aliases: []string{"rm"}, @@ -842,7 +974,7 @@ func ParseExternalRef(ref string) (project, capability string) { } func init() { - depAddCmd.Flags().StringP("type", "t", "blocks", "Dependency type (blocks|related|parent-child|discovered-from)") + depAddCmd.Flags().StringP("type", "t", "blocks", "Dependency type (blocks|tracks|related|parent-child|discovered-from)") // Note: --json flag is defined as a persistent flag in main.go, not here // Note: --json flag is defined as a persistent flag in main.go, not here @@ -853,12 +985,17 @@ func init() { depTreeCmd.Flags().String("direction", "", "Tree direction: 'down' (dependencies), 'up' (dependents), or 'both'") depTreeCmd.Flags().String("status", "", "Filter to only show issues with this status (open, in_progress, blocked, deferred, closed)") depTreeCmd.Flags().String("format", "", "Output format: 'mermaid' for Mermaid.js flowchart") + depTreeCmd.Flags().StringP("type", "t", "", "Filter to only show dependencies of this type (e.g., tracks, blocks, parent-child)") // Note: --json flag is defined as a persistent flag in main.go, not here // Note: --json flag is defined as a persistent flag in main.go, not here + depListCmd.Flags().String("direction", "down", "Direction: 'down' (dependencies), 'up' (dependents)") + depListCmd.Flags().StringP("type", "t", "", "Filter by dependency type (e.g., tracks, blocks, parent-child)") + depCmd.AddCommand(depAddCmd) depCmd.AddCommand(depRemoveCmd) + depCmd.AddCommand(depListCmd) depCmd.AddCommand(depTreeCmd) depCmd.AddCommand(depCyclesCmd) rootCmd.AddCommand(depCmd) diff --git a/internal/storage/memory/memory.go b/internal/storage/memory/memory.go index 35c76217..89e52ee0 100644 --- a/internal/storage/memory/memory.go +++ b/internal/storage/memory/memory.go @@ -695,6 +695,49 @@ func (m *MemoryStorage) GetDependents(ctx context.Context, issueID string) ([]*t return results, nil } +// GetDependenciesWithMetadata gets issues that this issue depends on, with dependency type +func (m *MemoryStorage) GetDependenciesWithMetadata(ctx context.Context, issueID string) ([]*types.IssueWithDependencyMetadata, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + var results []*types.IssueWithDependencyMetadata + for _, dep := range m.dependencies[issueID] { + if issue, exists := m.issues[dep.DependsOnID]; exists { + issueCopy := *issue + results = append(results, &types.IssueWithDependencyMetadata{ + Issue: issueCopy, + DependencyType: dep.Type, + }) + } + } + + return results, nil +} + +// GetDependentsWithMetadata gets issues that depend on this issue, with dependency type +func (m *MemoryStorage) GetDependentsWithMetadata(ctx context.Context, issueID string) ([]*types.IssueWithDependencyMetadata, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + var results []*types.IssueWithDependencyMetadata + for id, deps := range m.dependencies { + for _, dep := range deps { + if dep.DependsOnID == issueID { + if issue, exists := m.issues[id]; exists { + issueCopy := *issue + results = append(results, &types.IssueWithDependencyMetadata{ + Issue: issueCopy, + DependencyType: dep.Type, + }) + } + break + } + } + } + + return results, nil +} + // GetDependencyCounts returns dependency and dependent counts for multiple issues func (m *MemoryStorage) GetDependencyCounts(ctx context.Context, issueIDs []string) (map[string]*types.DependencyCounts, error) { m.mu.RLock() diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 8ac122d7..aa25d864 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -92,6 +92,8 @@ type Storage interface { RemoveDependency(ctx context.Context, issueID, dependsOnID string, actor string) error GetDependencies(ctx context.Context, issueID string) ([]*types.Issue, error) GetDependents(ctx context.Context, issueID string) ([]*types.Issue, error) + GetDependenciesWithMetadata(ctx context.Context, issueID string) ([]*types.IssueWithDependencyMetadata, error) + GetDependentsWithMetadata(ctx context.Context, issueID string) ([]*types.IssueWithDependencyMetadata, error) GetDependencyRecords(ctx context.Context, issueID string) ([]*types.Dependency, error) GetAllDependencyRecords(ctx context.Context) (map[string][]*types.Dependency, error) GetDependencyCounts(ctx context.Context, issueIDs []string) (map[string]*types.DependencyCounts, error) diff --git a/internal/types/types.go b/internal/types/types.go index 75e12524..502a34c3 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -519,6 +519,9 @@ const ( DepAuthoredBy DependencyType = "authored-by" // Creator relationship DepAssignedTo DependencyType = "assigned-to" // Assignment relationship DepApprovedBy DependencyType = "approved-by" // Approval relationship + + // Convoy tracking (non-blocking cross-project references) + DepTracks DependencyType = "tracks" // Convoy → issue tracking (non-blocking) ) // IsValid checks if the dependency type value is valid. @@ -534,7 +537,7 @@ func (d DependencyType) IsWellKnown() bool { switch d { case DepBlocks, DepParentChild, DepConditionalBlocks, DepWaitsFor, DepRelated, DepDiscoveredFrom, DepRepliesTo, DepRelatesTo, DepDuplicates, DepSupersedes, - DepAuthoredBy, DepAssignedTo, DepApprovedBy: + DepAuthoredBy, DepAssignedTo, DepApprovedBy, DepTracks: return true } return false