Skip to content

task dependency graph (#104)#139

Open
atomikpanda wants to merge 20 commits into
mainfrom
feat/task-dependency-graph
Open

task dependency graph (#104)#139
atomikpanda wants to merge 20 commits into
mainfrom
feat/task-dependency-graph

Conversation

@atomikpanda
Copy link
Copy Markdown
Owner

Summary

First-class task-to-task dependency edges. task B depends_on task A is now expressed in workspace state (state.yaml), and the rest of mship reads it:

  • New verb group: mship depends add/remove/list (--graph) to manage edges on existing tasks.
  • mship spawn --depends-on a,b,... declares upstream task(s) at creation. Unknown upstream and cycles are rejected loudly at write time.
  • mship finish refuses to ship a downstream task whose upstream isn't merged. --bypass-deps overrides.
  • mship close --cascade / --detach-downstream handle the downstream case. Non-TTY refuses without an explicit flag; TTY prompts.
  • mship status exposes the graph at .resolved_task.dependencies (upstream / downstream / blocked / blocked_by).
  • mship dispatch includes a ## Dependencies section in the subagent prompt body, with per-edge ready/not-ready state.
  • mship reconcile grows a dependency_stale state — surfaced when a downstream that's otherwise in-sync has an upstream that merged after the downstream was created (i.e. needs rebase).

Closes #104.

Design and plan

  • Spec: docs/superpowers/specs/2026-05-14-task-dependency-graph-design.md
  • Plan: docs/superpowers/plans/2026-05-14-task-dependency-graph.md

Key design decisions

  • Single edge type (hard). No soft / advisory edges in v1 — mship journal already handles "informed by task-a" relationships.
  • Slug-only endpoints. External-PR dependencies are a v2 cross-workspace concern.
  • Fan-in is AND — all upstream must be merged. No OR / alternative-groups.
  • Cycle detection at write time with explicit cycle-path error.
  • task_graph.py is a new module distinct from graph.py (which models repo topology). Mixing the two semantically-different graphs in one module would confuse readers.
  • Backward compatible. Task.depends_on is additive, defaults to []. Existing state.yaml files load unchanged.

What changed on the wire

  • mship status envelope grows one additive key under resolved_task: dependencies (upstream/downstream/blocked/blocked_by).
  • mship reconcile UpstreamState enum gains one variant: dependency_stale.

Both are additive. No consumer breakage.

Test plan

  • Unit tests for task_graph.py — cycle detection (self, 2-node, 3-node, diamond), transitive upstream/downstream, readiness queries.
  • CLI tests for mship depends add/remove/list — happy path, unknown upstream, self-edge, cycle, idempotent add, missing edge on remove, task-scoped list, --graph DAG output.
  • CLI tests for spawn --depends-on — persists edges, errors loudly on unknown upstream.
  • CLI tests for finish — blocked by unready upstream; --bypass-deps clears.
  • CLI tests for close — non-TTY refuses with downstream; --cascade removes from state; --detach-downstream clears edges.
  • CLI tests for statusresolved_task.dependencies block with correct shape.
  • CLI tests for dispatch## Dependencies section emitted with readiness signal.
  • Tests for reconcile.dependency_stale — only overrides in_sync decisions, only when upstream merged after downstream created.
  • End-to-end integration test: spawn a → spawn b --depends-on a → status shows blocked → finish blocks → finish --bypass-deps clears.

Full test suite: 1235 passed, 0 failures.

Known follow-ups (intentionally deferred)

  • status.resolved_task.dependencies.ready is always false for unmerged upstreams (uses an empty reconcile decisions dict to avoid a network round-trip on the hot path). A future change could read the existing ReconcileCache to populate readiness without a fetch.
  • --cascade removes downstream tasks from state but does NOT tear down their worktrees — left to mship prune. Documented in the flag's help text.
  • --cascade doesn't clean reconcile cache entries for removed downstream tasks. Harmless (cache is TTL-bound).
  • No GitHub-level "Depends on #N" sync. Mship's graph is mship's; no PR-body integration.
  • No automatic rebasing when an upstream merges. reconcile.dependency_stale surfaces the need; user runs the rebase.

Out of scope (v2 concerns)

Multi-agent orchestration, cross-workspace edges, critical-path scheduling, parallel-execution coordination. The graph being a first-class primitive in v1 is the seam these features will sit on top of without speculative design.

Closes #104

First-class depends_on edges between tasks, persisted in state.yaml.
Design covers state model, mship depends verb group, integration with
spawn/finish/close/status/dispatch/reconcile, cycle detection, and the
hard/soft edge model. All 8 design questions from the issue resolved.
Simpler model: single edge type with hard semantics. Tracking-without-
enforcement is what mship journal already covers. Soft edges become an
additive future change if a use case surfaces.
14 tasks, TDD-first. Covers state model, task_graph.py utilities,
mship depends verb group, integration with spawn/finish/close/status/
dispatch/reconcile, docs, and an end-to-end integration test.

Divergence from spec: new code lives in src/mship/core/task_graph.py
(not graph.py, which already models repo dependency topology).
…ecks (#104)

Adds --depends-on option to mship spawn. Validates upstream slugs exist,
runs cycle detection, and persists DependencyEdge list on the new task.
…verride (#104)

Add a dependency-readiness gate to `mship finish` that checks each
depends_on upstream via is_ready() before any push. Names the offending
upstreams in the error. --bypass-deps skips the gate. Module-level
_dependency_decisions() helper enables monkeypatching in tests.
)

Non-TTY close refuses when downstream tasks depend on the closing task;
TTY prompts for cascade/detach/abort. --cascade removes downstream from
state; --detach-downstream clears inbound edges. --force bypasses all.
Adds test_dependency_flow covering: spawn task-a, spawn task-b --depends-on a,
status shows blocked==True/blocked_by==[a], finish refuses with upstream message,
finish --bypass-deps clears the gate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

First-class task dependency graph (blocks / depends-on) — design issue, marinating

1 participant