You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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:
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.
GET /api/dependencies/{issue}/ready -- returns {ready: bool, blocking: [{id, title, state}]}. Ready when all deps are in DONE state.
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/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
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
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
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)
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)
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
Create dashboard router -- dependencies.py with 3 GET endpoints, register in app.py
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
Problem
Dependency drift: The standalone
docs/roadmap.htmlis 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## Dependenciessection 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.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.
Standalone roadmap is disconnected:
docs/roadmap.htmlis a static file manually regenerated via/create-roadmapand/update-roadmapcommands. It's not connected to the supervisor/progression engine that could actually use the dependency information.Solution
Build a dependency graph engine that parses
## Dependenciessections 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:DependencyGraphclass:build(issues: list[Task])-- parse## Dependenciessections, extract#NNNrefs, construct DAGget_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 blockingget_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#NNNfrom## DependenciessectionAdapter 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 depsGET /api/dependencies/{issue}/chain-- upstream + downstream chainDependency parsing format (already standardized)
Parser extracts
#NNNrefs. Text after--is stored as reason. Issues without a## Dependenciessection have no dependencies (always ready).Roadmap transition plan
docs/roadmap.htmlretired when Supervisor Daemon issue ships the visual dashboard graph/create-roadmapand/update-roadmapcommands retired at the same time/api/dependenciesbecomes the single source of truth for task orderingDependencies
None (independent foundation).
Acceptance Criteria
DependencyGraph.build()correctly parses## Dependenciesfrom issue bodiesget_ready_tasks()only returns issues whose deps are all closed + mergedget_blocked_tasks()reports which specific dep is blocking each issueget_parallel_groups()correctly identifies simultaneously workable issuesvalidate()detects cycles and missing issue references_parse_dependencies()handles edge cases: no deps section, empty section, malformed refs, self-referentialPart 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.pywith Kahn's algorithm topological sort and cycle detection. The dependency graph engine should reuse that algorithmic pattern but operate onTaskobjects from the adapter layer rather than workflow command nodes. Thesova/supervisor/module does not exist onmainyet (only in a worktree for #291), so this issue will create the initial module structure. TheTaskdataclass insova/adapters/base.pyhas abodyfield containing the raw issue body, which is where## Dependenciessections 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 initsova/supervisor/dependency_graph.py(create): CoreDependencyGraphclass with DAG construction, readiness checks, chain traversal, parallel group detection, and validation. Contains_parse_dependencies(body: str) -> list[DependencyRef]for extracting#NNNrefs with optional reason text from## Dependenciessectionssova/adapters/base.py(modify): Addget_task_dependencies(task_id: str) -> list[int]toTaskAdapterABC -- thin method that callsget_task()then_parse_dependencies()on the body. Keep the parser in the supervisor module; the adapter method is a convenience delegationsova/adapters/github.py(modify): Implementget_task_dependencies()inGitHubAdaptersova/adapters/jira.py(modify): Implementget_task_dependencies()inJiraAdapter(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 resultssova/dashboard/app.py(modify): Registerdependencies.routerin_register_api_routers()tests/test_dependency_graph.py(create): Unit tests for parser, DAG construction, readiness, cycles, chains, parallel groups, validationPattern 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. TheDependencyGraphclass 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, followsova/dashboard/services/lifecycle_service.py(async functions taking session parameter, returning dicts).The
PlannedTask.dependencies: list[str]insova/roles/planner.py:40shows 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:GET /api/dependencies-- returns{nodes: [{id, title, state, labels, milestone}], edges: [{source, target, reason}]}for all open issues. Query param?milestone=Phase+7to filter.GET /api/dependencies/{issue}/ready-- returns{ready: bool, blocking: [{id, title, state}]}. Ready when all deps are in DONE state.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--Taskdataclass,TaskAdapterABC: extend withget_task_dependencies()sova/adapters/github.py--GitHubAdapter.list_tasks(): fetches issues with body field (line 78: includesbodyin 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 adapterEdge Cases
validate()must surface the cycle with involved issue numbers## Dependenciessection containing#293in issue feat(supervisor): task dependency graph engine #293. Parser must filterdep == self_issue_number#999referenced but issue doesn't exist or is in a different repo.validate()flags these;get_ready_tasks()treats missing deps as blocking (fail-closed)## Dependenciesheading with no list items -- same as no dependencies#prefix -- not parsed, only#\d+format is recognizedget_ready_tasks()must check state is DONE, not verify PR merge status (adapter state already handles this via label-based transitions)## Dependenciessections: take the first one (defensive), log a warningget_task())Suggested Approach
sova/supervisor/dependency_graph.py-- implement_parse_dependencies(body, self_issue=None) -> list[DependencyRef]with regex to find## Dependenciessection and extract#(\d+)refs with optional reason text. AddDependencyRefdataclass withissue_number: intandreason: strDependencyGraphclass -- constructor takeslist[Task], builds adjacency dict and reverse-adjacency dict. Implementget_ready_tasks()(filter where all deps havestate == 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)get_task_dependencies()toTaskAdapterbase class, implement inGitHubAdapterandJiraAdapter(both delegate to_parse_dependencies()after fetching the task body)dependency_service.pywithget_dependency_graph(adapter, filters),check_readiness(adapter, issue_number),get_chain(adapter, issue_number). Each fetches tasks, builds graph, returns serialized dictdependencies.pywith 3 GET endpoints, register inapp.pytests/test_dependency_graph.pycovering: 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