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
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.
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
An issue declaring Blocked by: #N for an open/unmerged N is discovered but not dispatched, and is visibly distinguishable (Slack/label) from a safety-gate skip.
Closing/merging N automatically promotes the dependent issue on the next cycle without an operator re-running anything.
A cross-repo Blocked by: owner/repo#N resolves correctly and does not collide with a same-numbered issue in a different repo.
A dependency cycle is detected and reported, not silently deadlocked.
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)
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 insrc/— the only "blocked" Factory currently understands isAgentLifecycleSignal.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-readytoday. 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:
BatchTrackeralready has a queue/admit/promote lifecycle (dispatch-batch-admission,dispatch-queue-promotionin.agentworkforce/features/manifest.yaml) — dependency checking is a second admission predicate alongside capacity, not a new subsystem.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 — aBlocked by: #N/Related: #Nline 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-nativeinsrc/orchestrator/factory.ts), extract its declared dependencies:Blocked by: #123, #124line 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.blocks/blocked byrelations 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#123means "in this same repo" unless qualified asowner/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-dependencylabel/comment, following the existingfactory:in-progress/factory:human-reviewlabel 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, persrc/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-drivendispatch-queue-promotion, and should reuse that promotion path rather than adding a second one.Acceptance criteria
Blocked by: #Nfor an open/unmerged N is discovered but not dispatched, and is visibly distinguishable (Slack/label) from a safety-gate skip.Blocked by: owner/repo#Nresolves correctly and does not collide with a same-numbered issue in a different repo.Notes for the implementer
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.npm test(vitest); extendsrc/orchestrator/batch-tracker.test.tsandsrc/orchestrator/factory.test.ts.Related: #128, #129, #130, #131, #132 (this repo's own current
factory-readybacklog is the fixture this feature needs to sequence correctly)