Skip to content

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 DFS
  • server/orchestrator/dispatcher.ts — dispatch filtering logic
  • app/components/IssueDetailSidebar.vue — frontend display

Data Model

Database Schema:

  • Table: issueDependencies
  • Structure: issueIddependsOnId (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):

typescript
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:

json
{ "depends_on_id": "target-issue-id" }

Validation Steps (in order):

  1. Verify source issue exists and belongs to the route project
  2. Verify target issue exists (any project — cross-project allowed)
  3. Check: issue cannot depend on itself
  4. Check: dependency would create a circular chain (DFS cycle detection)
  5. Check: duplicate dependency does not exist
  6. 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:

json
{ "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:

  1. Fetch source issue by identifier
  2. Query all dependency edges where issueId matches
  3. For each edge, fetch the target issue + its project record
  4. Return issue with dependencies array containing:
    • id, identifier, title, status
    • project context (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):

typescript
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 done status unblocks dependents. Cancelled, blocked, review, etc. still block.

Query Strategy (N+1 avoidance):

  1. Single query: fetch all dependencies for candidate issues
  2. Single query: fetch all target issue statuses
  3. 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) uses dep.project?.slug ?? currentSlug

Dependencies are created/removed only via MCP tools (agents) or API — not exposed in the issue detail UI form.


Key Components

FileResponsibility
server/database/schema.ts:40-46Table definition with unique constraint
app/utils/issueTypes.ts:95-100Frontend type definition
server/api/.../dependencies.post.tsCreate endpoint with cycle detection
server/api/.../dependencies.delete.tsDelete endpoint
server/api/.../by-identifier/[identifier].get.tsFetch + enrichment
server/utils/dependencyCycle.tsCycle detection via DFS
server/orchestrator/dispatchPriority.tsDependency resolution helpers
server/orchestrator/dispatcher.ts:35-81Dispatch filtering logic
app/components/IssueDetailSidebar.vueFrontend display
tests/integration/dependencyChain.test.tsIntegration tests

Design Decisions

Unidirectional + Explicit Edges

Dependencies stored as issueIddependsOnId. 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