Skip to content

Dependency-aware dispatch ordering: let Factory sequence and reconcile issues that block each other #133

Description

@khaliqgant

Summary

Factory discovers and dispatches "Ready for Agent"/labeled issues independently of one another — BatchTracker (src/orchestrator/batch-tracker.ts) admits by capacity only (start()/queue()/complete()), and nothing in triage or dispatch reads whether one issue is blocked by another. Confirmed: no existing "blocked by"/"depends on"/dependency concept anywhere in src/ — the only "blocked" Factory currently understands is AgentLifecycleSignal.kind: 'blocked' (an agent mid-task needing a human answer), which is unrelated to issue-to-issue ordering.

This matters concretely right now: #128 (feature-map generation), #131 (consumes #128's manifest), and #132 (drift-checks #128/#131) are three issues in a strict prerequisite chain, all carrying factory-ready today. If Factory's discovery cycle picks up all five in one sweep, nothing stops it from dispatching #131 and #132 in parallel with #128 — the implementer would be reviewing/checking a manifest schema that doesn't exist yet. Right now the only thing preventing that is a human reading the "Notes for the implementer" prose and holding issues back manually, which doesn't scale and isn't what "opt-in, safety-gated dispatch" is supposed to mean.

Background

Two relevant existing mechanisms this should build on rather than duplicate:

  • BatchTracker already has a queue/admit/promote lifecycle (dispatch-batch-admission, dispatch-queue-promotion in .agentworkforce/features/manifest.yaml) — dependency checking is a second admission predicate alongside capacity, not a new subsystem.
  • Both providers Factory already discovers from have native relationship concepts to read instead of inventing a third format: GitHub issues support a "blocked by"/"blocks" relation surfaced via its API; Linear has native issue relations (blocks/blocked by/related). Factory's own convention in practice today (see Per-repo feature/test map: lazy, incremental bootstrap generation for customer repos #128-CI drift-check for the per-repo feature/test map (catch stale locations and coverage rot) #132) is prose — a Blocked by: #N / Related: #N line in the issue body — which is a reasonable, zero-new-API-surface v1 fallback precisely because it's already what gets written by hand.

Proposed changes

A. Dependency extraction at discovery time

When an issue is pulled (discovery-linear-ready/discovery-github-native in src/orchestrator/factory.ts), extract its declared dependencies:

  • v1: parse a structured Blocked by: #123, #124 line from the issue body (GitHub) or description (Linear) — same shape already used by hand in this repo's own issues. Be strict about the format so this doesn't silently misparse prose that happens to contain a # and a number.
  • v2 (follow-up, not required here): read GitHub's native issue-relation API and Linear's native blocks/blocked by relations directly, falling back to the text convention when absent.

B. Composite cross-repo identity

A dependency reference must resolve to the same composite repo+number identity issueKey() already uses for dispatch/registry/PR-cache isolation (dispatch-composite-issue-identity) — a bare #123 means "in this same repo" unless qualified as owner/repo#123, and must not collide with an unrelated issue #123 in a different repo.

C. Admission gating in BatchTracker

Extend BatchTracker.start()/queue() (src/orchestrator/batch-tracker.ts) with a second admission check: an issue with unresolved dependencies (any declared blocker not yet closed/merged) is parked, not queued-for-capacity — these are different reasons for not dispatching and must be distinguishable (see D).

D. Visible, distinct parked state

Today "pulled but not dispatched" already has one meaning (outside the safety-gate scope, per the README's own caveat) — a dependency park must not look identical to that from an operator's point of view, or every "why didn't this dispatch" question becomes ambiguous. Post a writeback (Slack thread and/or a factory:blocked-on-dependency label/comment, following the existing factory:in-progress/factory:human-review label pattern) naming the specific unresolved blocker(s).

E. Cycle detection

A declares blocked-by B, B declares blocked-by A (directly or transitively) must be detected and refused rather than silently deadlocking both issues forever — fail closed and surface it as an operator-visible error, consistent with the safety-gate philosophy of "fail closed, never guess."

F. Reconciliation on blocker completion

When a blocking issue reaches its terminal state (pr-done-terminal/GitHub issue close, per src/orchestrator/factory.ts), re-evaluate every parked issue that named it as a blocker and promote it if now fully unblocked — this is the dependency-driven analogue of the existing capacity-driven dispatch-queue-promotion, and should reuse that promotion path rather than adding a second one.

Acceptance criteria

Notes for the implementer

  • This is an admission-time gate on top of the existing BatchTracker, not a replacement for it — capacity queuing and dependency parking are two independent reasons to hold an issue, and both should be able to apply to the same issue simultaneously.
  • v1 (C-F) should ship against the text-convention parser (A, v1) — don't block this issue on building native GitHub/Linear relation-API integration; that's an explicit, separable v2.
  • Run npm test (vitest); extend src/orchestrator/batch-tracker.test.ts and src/orchestrator/factory.test.ts.

Related: #128, #129, #130, #131, #132 (this repo's own current factory-ready backlog is the fixture this feature needs to sequence correctly)

Metadata

Metadata

Assignees

No one assigned

    Labels

    factory-readyApproved for the Factory to pick up and implementfactory:in-progressFactory agents are working on this issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions