Skip to content

feat(supervisor): task dependency graph engine #293

Description

@xsovad06

Problem

  1. Dependency drift: The standalone docs/roadmap.html is hand-maintained and drifts from actual issue dependencies. Example: feat(knowledge): add memory relationship graph for connected retrieval #225 appeared parallel to feat(knowledge): add semantic memory search via embeddings #224 in the roadmap visualization, but feat(knowledge): add memory relationship graph for connected retrieval #225's ## Dependencies section lists feat(knowledge): add semantic memory search via embeddings #224 as a hard dependency. The researcher started on feat(knowledge): add memory relationship graph for connected retrieval #225 before feat(knowledge): add semantic memory search via embeddings #224 was merged, discovered the dependency at spec time, and wasted a cycle.

  2. No readiness check: Nothing prevents spawning a researcher/developer on an issue whose dependencies aren't merged to main yet. The researcher discovers mid-work that prerequisite code doesn't exist.

  3. Standalone roadmap is disconnected: docs/roadmap.html is a static file manually regenerated via /create-roadmap and /update-roadmap commands. It's not connected to the supervisor/progression engine that could actually use the dependency information.

Solution

Build a dependency graph engine that parses ## Dependencies sections from issue bodies, constructs a DAG, and provides readiness checks. The visual roadmap moves permanently into the supervisor dashboard (in the Supervisor Daemon issue) and the standalone file plus its generation commands are retired.

Components

  • New module sova/supervisor/dependency_graph.py:

    • DependencyGraph class:
      • build(issues: list[Task]) -- parse ## Dependencies sections, extract #NNN refs, construct DAG
      • get_ready_tasks() -> list[Task] -- issues whose ALL dependencies are DONE (closed + PR merged to main)
      • get_blocked_tasks() -> list[tuple[Task, list[BlockingDep]]] -- issues with unmet deps and which dep is blocking
      • get_task_chain(issue_number) -> Chain -- full dependency chain (upstream ancestors + downstream dependents)
      • get_parallel_groups() -> list[list[Task]] -- issues at same DAG depth with no cross-deps (safe for simultaneous work)
      • validate() -> list[GraphError] -- detect cycles, missing referenced issues, stale deps
    • _parse_dependencies(body: str) -> list[int] -- extract #NNN from ## Dependencies section
  • Adapter extension: TaskAdapter.get_task_dependencies(issue_number) -> list[int] -- delegates to body parsing. Jira adapter would parse equivalent format.

  • API endpoints:

    • GET /api/dependencies -- full graph as JSON (nodes with state/labels + edges)
    • GET /api/dependencies/{issue}/ready -- readiness check with blocking deps
    • GET /api/dependencies/{issue}/chain -- upstream + downstream chain

Dependency parsing format (already standardized)

## Dependencies

- #224 (semantic search) -- required for embedding-based retrieval
- #225 (memory graph) -- optional but enhances results

Parser extracts #NNN refs. Text after -- is stored as reason. Issues without a ## Dependencies section have no dependencies (always ready).

Roadmap transition plan

  • docs/roadmap.html retired when Supervisor Daemon issue ships the visual dashboard graph
  • /create-roadmap and /update-roadmap commands retired at the same time
  • /api/dependencies becomes the single source of truth for task ordering

Dependencies

None (independent foundation).

Acceptance Criteria

  • DependencyGraph.build() correctly parses ## Dependencies from issue bodies
  • get_ready_tasks() only returns issues whose deps are all closed + merged
  • get_blocked_tasks() reports which specific dep is blocking each issue
  • get_parallel_groups() correctly identifies simultaneously workable issues
  • validate() detects cycles and missing issue references
  • API returns graph as JSON suitable for D3.js visualization
  • _parse_dependencies() handles edge cases: no deps section, empty section, malformed refs, self-referential
  • Adapter extension works for GitHub; Jira adapter can implement same interface
  • Tests cover DAG construction, readiness, cycle detection, chain traversal, parsing edge cases

Part of

Chain L: Supervisor Agent (Phase 7)

Research

Issue: feat(supervisor): task dependency graph engine
Complexity: moderate (5-7 files, ~150-250 lines)

The codebase already has a mature DAG engine in sova/core/dag.py with Kahn's algorithm topological sort and cycle detection. The dependency graph engine should reuse that algorithmic pattern but operate on Task objects from the adapter layer rather than workflow command nodes. The sova/supervisor/ module does not exist on main yet (only in a worktree for #291), so this issue will create the initial module structure. The Task dataclass in sova/adapters/base.py has a body field containing the raw issue body, which is where ## Dependencies sections live -- no adapter changes are needed to access the data, only a parser to extract structured dependency information from it.

Affected Files

  • sova/supervisor/__init__.py (create): New package init
  • sova/supervisor/dependency_graph.py (create): Core DependencyGraph class with DAG construction, readiness checks, chain traversal, parallel group detection, and validation. Contains _parse_dependencies(body: str) -> list[DependencyRef] for extracting #NNN refs with optional reason text from ## Dependencies sections
  • sova/adapters/base.py (modify): Add get_task_dependencies(task_id: str) -> list[int] to TaskAdapter ABC -- thin method that calls get_task() then _parse_dependencies() on the body. Keep the parser in the supervisor module; the adapter method is a convenience delegation
  • sova/adapters/github.py (modify): Implement get_task_dependencies() in GitHubAdapter
  • sova/adapters/jira.py (modify): Implement get_task_dependencies() in JiraAdapter (same body parsing, different task fetch)
  • sova/dashboard/routers/dependencies.py (create): New API router with 3 endpoints (GET /api/dependencies, GET /api/dependencies/{issue}/ready, GET /api/dependencies/{issue}/chain)
  • sova/dashboard/services/dependency_service.py (create): Service layer -- fetches tasks via adapter, builds graph, returns serialized results
  • sova/dashboard/app.py (modify): Register dependencies.router in _register_api_routers()
  • tests/test_dependency_graph.py (create): Unit tests for parser, DAG construction, readiness, cycles, chains, parallel groups, validation

Pattern Reference

Follow the DAG implementation in sova/core/dag.py:validate_dag() (line 228) and _topological_sort() (line 298) for cycle detection via Kahn's algorithm and graph validation. The DependencyGraph class should use the same adjacency-list + in-degree representation.

For the dashboard router, follow sova/dashboard/routers/lifecycle.py (simple CRUD-style GET endpoints with validation). For the service layer, follow sova/dashboard/services/lifecycle_service.py (async functions taking session parameter, returning dicts).

The PlannedTask.dependencies: list[str] in sova/roles/planner.py:40 shows the existing convention for representing dependencies as ["#123"] strings.

Data Model Changes

None required. The dependency graph is computed on-the-fly from issue bodies (via adapter get_task() calls). No new DB tables or migrations. The graph is ephemeral -- rebuilt on each API call from current issue state. Caching can be added later if performance requires it.

API Changes

Three new endpoints under /api/dependencies:

  1. GET /api/dependencies -- returns {nodes: [{id, title, state, labels, milestone}], edges: [{source, target, reason}]} for all open issues. Query param ?milestone=Phase+7 to filter.
  2. GET /api/dependencies/{issue}/ready -- returns {ready: bool, blocking: [{id, title, state}]}. Ready when all deps are in DONE state.
  3. GET /api/dependencies/{issue}/chain -- returns {upstream: [{id, title, state}], downstream: [{id, title, state}]} for the full ancestor/descendant chain.

Dependencies

  • sova/core/dag.py -- _topological_sort(), validate_dag(): reuse Kahn's algorithm pattern (don't import directly -- the existing DAG operates on command nodes, not tasks; reimplement for task-typed graph)
  • sova/adapters/base.py -- Task dataclass, TaskAdapter ABC: extend with get_task_dependencies()
  • sova/adapters/github.py -- GitHubAdapter.list_tasks(): fetches issues with body field (line 78: includes body in JSON fields)
  • sova/dashboard/routers/lifecycle.py -- Router pattern reference (prefix, tags, Pydantic models)
  • sova/config/loader.py -- create_adapter() factory: used by the service layer to get the configured adapter

Edge Cases

  • Circular dependencies: issue A depends on B, B depends on A. Kahn's algorithm naturally detects this -- validate() must surface the cycle with involved issue numbers
  • Self-referential dependency: ## Dependencies section containing #293 in issue feat(supervisor): task dependency graph engine #293. Parser must filter dep == self_issue_number
  • Missing referenced issues: #999 referenced but issue doesn't exist or is in a different repo. validate() flags these; get_ready_tasks() treats missing deps as blocking (fail-closed)
  • No Dependencies section: treated as zero dependencies -- issue is always ready
  • Empty Dependencies section: ## Dependencies heading with no list items -- same as no dependencies
  • Malformed refs: text like "depends on issue 224" without # prefix -- not parsed, only #\d+ format is recognized
  • Closed but not merged: issue closed without a PR merge (e.g., "won't fix"). get_ready_tasks() must check state is DONE, not verify PR merge status (adapter state already handles this via label-based transitions)
  • Multiple ## Dependencies sections: take the first one (defensive), log a warning
  • Dependency on issues outside the filtered set: when filtering by milestone, a dep on an issue in a different milestone must still be resolved (fetch it individually via get_task())

Suggested Approach

  1. Create sova/supervisor/dependency_graph.py -- implement _parse_dependencies(body, self_issue=None) -> list[DependencyRef] with regex to find ## Dependencies section and extract #(\d+) refs with optional reason text. Add DependencyRef dataclass with issue_number: int and reason: str
  2. Build DependencyGraph class -- constructor takes list[Task], builds adjacency dict and reverse-adjacency dict. Implement get_ready_tasks() (filter where all deps have state == DONE), get_blocked_tasks() (complement with blocking dep details), validate() (Kahn's sort for cycle detection + missing ref check), get_task_chain() (BFS upstream via reverse-adjacency + BFS downstream via forward-adjacency), get_parallel_groups() (group by DAG depth from topological sort)
  3. Extend adapter ABC -- add get_task_dependencies() to TaskAdapter base class, implement in GitHubAdapter and JiraAdapter (both delegate to _parse_dependencies() after fetching the task body)
  4. Create dashboard service -- dependency_service.py with get_dependency_graph(adapter, filters), check_readiness(adapter, issue_number), get_chain(adapter, issue_number). Each fetches tasks, builds graph, returns serialized dict
  5. Create dashboard router -- dependencies.py with 3 GET endpoints, register in app.py
  6. Write tests -- tests/test_dependency_graph.py covering: parser edge cases (no section, empty, malformed, self-ref, multiple sections), DAG construction from mock tasks, readiness checks, cycle detection, chain traversal, parallel groups, validation errors

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions