Issue Dependencies
Models task ordering and blocking relationships across issues. Issues can declare dependencies on other issues (within the same project or across projects), and the orchestrator respects these dependencies during dispatch — an issue is only dispatchable when all its dependencies have been completed.
Why it matters:
- Allows work to be structured hierarchically (foundation → service → API layers)
- Prevents parallel dispatch of dependent tasks
- Enables blocking/unblocking workflows across projects
- Supports cross-project task coordination
Key source files:
server/utils/dependencyCycle.ts— cycle detection via DFSserver/orchestrator/dispatcher.ts— dispatch filtering logicapp/components/IssueDetailSidebar.vue— frontend display
Data Model
Database Schema:
- Table:
issueDependencies - Structure:
issueId→dependsOnId(many-to-many, directional) - Constraints:
- Unique on (issueId, dependsOnId) — prevents duplicate dependencies
- Foreign keys cascade on delete — removing an issue removes its dependencies
- No self-dependencies allowed (enforced at API)
- No circular dependencies allowed (enforced at API via cycle detection)
Frontend Types (app/utils/issueTypes.ts):
export interface IssueDependency {
readonly id: string
readonly identifier: string
readonly title: string
readonly status: IssueStatus
}Dependencies are included in IssueDetail as a read-only array. Project context is enriched by the API for cross-project visualization.
API Flow
Creating a Dependency
Endpoint: POST /api/projects/[projectId]/issues/[issueId]/dependencies
Request Body:
{ "depends_on_id": "target-issue-id" }Validation Steps (in order):
- Verify source issue exists and belongs to the route project
- Verify target issue exists (any project — cross-project allowed)
- Check: issue cannot depend on itself
- Check: dependency would create a circular chain (DFS cycle detection)
- Check: duplicate dependency does not exist
- If all pass: insert into issueDependencies
Cycle Detection (server/utils/dependencyCycle.ts):
- Loads all existing edges, builds adjacency map
- DFS from target issue following all outbound edges
- If DFS finds source issue, a cycle would be created
- Returns human-readable cycle path (e.g., "A → B → C → A")
Error Responses:
- 404: Source or target issue not found
- 400: Self-dependency or missing depends_on_id
- 409: Circular dependency detected OR duplicate already exists
Removing a Dependency
Endpoint: DELETE /api/projects/[projectId]/issues/[issueId]/dependencies
Request Body:
{ "depends_on_id": "target-issue-id" }Simple delete via unique constraint; returns { deleted: true }.
Fetching Issue with Dependencies
Endpoint: GET /api/projects/[projectId]/issues/by-identifier/[identifier]
Enrichment Process:
- Fetch source issue by identifier
- Query all dependency edges where issueId matches
- For each edge, fetch the target issue + its project record
- Return issue with
dependenciesarray containing:id,identifier,title,statusprojectcontext (id, slug, name, prefix) for cross-project badges
This enrichment happens on every fetch (no caching), ensuring fresh UI state.
Dispatcher Integration
Dependencies are evaluated in getDispatchableIssues() (server/orchestrator/dispatcher.ts):
const dispatchable = candidates.filter(issue =>
areDependenciesResolved(issue.id, depsByIssueId, depStatusMap),
)Dependency Resolution Check (areDependenciesResolved()):
- Input: issue ID, dependency map, status map
- Logic: if issue has no dependencies → true; else all dependencies must have status:
done - Key: Only
donestatus unblocks dependents. Cancelled, blocked, review, etc. still block.
Query Strategy (N+1 avoidance):
- Single query: fetch all dependencies for candidate issues
- Single query: fetch all target issue statuses
- Filter in-memory: check each candidate's deps against status map
This batching is critical because the dispatcher runs every 5s across potentially hundreds of issues.
UI Display
Component: IssueDetailSidebar.vue
Rendering:
- Displays dependencies as a list with issue identifier + title
- Cross-project dependencies marked with badge showing project name
- Each dependency is a clickable link to its detail page
- Route computation:
depRoute(dep)usesdep.project?.slug ?? currentSlug
Dependencies are created/removed only via MCP tools (agents) or API — not exposed in the issue detail UI form.
Key Components
| File | Responsibility |
|---|---|
server/database/schema.ts:40-46 | Table definition with unique constraint |
app/utils/issueTypes.ts:95-100 | Frontend type definition |
server/api/.../dependencies.post.ts | Create endpoint with cycle detection |
server/api/.../dependencies.delete.ts | Delete endpoint |
server/api/.../by-identifier/[identifier].get.ts | Fetch + enrichment |
server/utils/dependencyCycle.ts | Cycle detection via DFS |
server/orchestrator/dispatchPriority.ts | Dependency resolution helpers |
server/orchestrator/dispatcher.ts:35-81 | Dispatch filtering logic |
app/components/IssueDetailSidebar.vue | Frontend display |
tests/integration/dependencyChain.test.ts | Integration tests |
Design Decisions
Unidirectional + Explicit Edges
Dependencies stored as issueId → dependsOnId. No implicit reverse relationships. Why: Clearer semantics. Queries are simple. Cost of traversing both directions is minimal.
Cycle Detection at Write Time
Cycles prevented on creation, not detected at dispatch time. Why: Fail fast (user gets immediate feedback). DFS is cheap for typical 10-50 node graphs.
DFS Over BFS
DFS chosen for cycle detection to construct cycle path for error message. Why: Path reconstruction is useful for debugging ("A → B → C → A"). BFS would require path tracking anyway.
Only done Unblocks
Dependencies only resolve when target status is done. Not when cancelled or blocked. Why: Semantic clarity. Cancelled should not unblock (task was abandoned). If a dependency is blocked, the issue depending on it should also be blocked.
Batch Queries in Dispatcher
All candidate dependencies fetched in 1 query, all status checks in 1 query. Why: Called every 5s. Prevents N+1 explosion. Scales from 10 to 1000 issues in same time.
Cross-Project Dependencies Allowed
No validation that source and target are in same project. Why: Supports workspace-level coordination. Common pattern: infrastructure repo blocking UI repo.
Known Gaps
- Circular dependency reporting: Returns simple formatted path; could enhance with visual diagram
- Dependency metrics: No dashboard showing dependency depth, blocking ratio, or critical path
- Dependency history: No audit trail of when dependencies changed
- UI for creating dependencies: Only MCP tools and API can create them
- Cross-workspace dependencies: All dependencies must be in same SQLite DB; multi-repo workspace would need federation