Versions
Sep 23, 2026Claude CodeCurrent
Task dependencies and ready work
Specification for #76 (from #92). It decides what "ready" means, how blockers behave and what a claim checks. Blockers (#93) and the ready queue with strict claims (#94) are built; the web controls (#95) and agent onboarding (#96) follow. It builds on the relation model in #287 and the task rules in TaskService.
Ready work is a recommendation and an eligibility check, never a scheduler: nothing starts, reassigns or changes status on its own.
Concepts
| Concept | Meaning | Affects readiness |
|---|---|---|
| Parent / subtask | Decomposition. Siblings are unordered | Yes: a parent with open subtasks is a container, not work; blockers and a manual block of an ancestor apply to its descendants |
blocked_by |
The source task cannot start before the target task is done | Yes |
Manual blocked status |
Waiting on something outside the graph (a person, a decision, an external system), with a reason | Yes |
related, references, implements |
Information only (#287) | No |
discovered_from |
The source task was found while working on the target item (task or memory) | No |
A blocked_by edge never changes a status. The status field keeps its five values (open, in_progress, blocked, done, canceled); "waiting on #10" is derived from the graph and shown next to the status, it is not stored.
Blockers
- Stored as a
ProjectItemRelationof kindblocked_by, source = the waiting task, target = the prerequisite. Read in both directions: #11 shows "Waits for #10", #10 shows "Blocks #11". - Both endpoints are tasks of the same project. Memories, pages and other projects are rejected ("Blockers link tasks of one project"). Cross-project blockers are deferred.
- Created with
item_action relate(relation: blocked_by) and removed withunrelate, like other explicit relations: no item version is needed and neither task's version changes. Each change is recorded in the waiting task's history, so a removed blocker is auditable. - Adding one needs write access to the project. It needs no access beyond that: both tasks are in the same project.
- Rejected: a self-link; an edge that closes a cycle (direct or transitive); an edge between a task and its own ancestor or descendant. A subtask waiting for its parent would deadlock, because a parent cannot close before its subtasks; a parent waiting for its subtask is already implied. Adding an existing edge or removing a missing one succeeds and changes nothing.
- An edge may point at a closed prerequisite (it is satisfied at once when that one is done) but not start from a closed task.
When a blocker is satisfied
| Prerequisite | Satisfied | What the waiting task explains |
|---|---|---|
done |
Yes | nothing |
open, in_progress, blocked |
No | "Waits for #10 (in progress)" |
canceled |
No | "#10 was canceled: remove this blocker or replace it" |
| archived and not done | No | "#10 was archived before it was done: remove this blocker or replace it" |
| privacy-restricted or otherwise hidden | No | "Waits for a task you cannot see" (never its number or title) |
| deleted | the edge is deleted with it | nothing |
A canceled prerequisite is never silently counted as success: someone decides to remove the blocker, replace it with another task, or reopen the prerequisite.
Reopening a done prerequisite makes its waiting tasks unready again. A waiting task that is already in_progress keeps its status and owner.
Ready
A task is ready for a caller when all of these hold:
- Its status is
open. - It is unassigned, or assigned to the caller.
- Every
blocked_byprerequisite of the task and of each ancestor is satisfied. - No ancestor has the manual status
blocked. - It has no active subtasks (a parent whose subtasks are all closed is ready again, for example for a wrap-up).
- It is neither archived nor privacy-restricted.
in_progress work is never "ready". It is reported separately as the caller's work in progress (assigned to the caller) and as taken (assigned to others).
Order: priority (high, normal, low), then oldest first (FIFO by creation), then number. A task assigned to the caller does not jump the queue.
Truth table
"Blockers" covers the task's own blockers and those of its ancestors; "leaf" means no active subtasks. The state column is what TaskService.Readiness returns.
| Status | Blockers | Ancestor manually blocked | Assignee | Leaf | Ready for caller | State |
|---|---|---|---|---|---|---|
| open | satisfied | no | none | yes | yes | ready |
| open | satisfied | no | caller | yes | yes | ready |
| open | any | any | someone else | any | no | taken |
| open | unsatisfied | any | none or caller | any | no | waiting |
| open | satisfied | yes | none or caller | any | no | waiting ("Parent #P is blocked") |
| open | satisfied | no | none or caller | no | no | container |
| in_progress | any | any | caller | any | no | in_progress (yours) |
| in_progress | any | any | someone else | any | no | taken |
| blocked | any | any | any | any | no | blocked |
| done / canceled | any | any | any | any | no | closed |
Claim
A claim is the eligibility check. In one transaction, under the project's task lock, it:
- rejects a closed task (existing);
- succeeds without a change when the task is already the caller's and
in_progress, whatever version was sent, so a retry after a lost answer is harmless; - rejects a changed version (existing);
- rejects a task assigned to someone else: "#12 is already taken by Codex" (new: before, a claim silently took a task over);
- rejects a manually
blockedtask: unblock it first; - rejects a task that waits for its own or an ancestor's prerequisite, with the same reasons
item_getshows; - assigns the caller and sets
in_progress.
A parent with open subtasks may be claimed (decided while building #94): claiming an epic is how a person or agent takes ownership of a whole piece of work. Containers are only left out of the ready queue.
There is no override flag. Starting blocked work anyway means removing the blocker or unblocking first, which is explicit and recorded. Reassigning someone else's task stays an explicit assignee change (item_store), which already needs write access and appears in history. No claim, completion or blocker change ever starts, assigns or changes the status of another task.
Concurrency
Blocker mutations and claims take the same project-scoped lock (RelationService.LockTasks: sp_getapplock on tasks:{projectId} in SQL Server; SQLite serialises writers anyway). The cycle check, the ancestry check and the readiness check read their data under that lock, so:
- two people or agents claiming the same task: one succeeds, the other gets "already taken" (
ClaimConcurrencyTestson SQL Server); - A adds #11 → #10 while B adds #10 → #11: the second sees the first edge and is rejected as a cycle (
BlockerConcurrencyTests); - a blocker added while a claim runs: the claim either sees it and fails, or completes first; the blocker then applies to an already started task, which keeps its status and shows "waiting".
Surfaces
item_list(kind: task, filter: {ready: true})returns the caller's ready queue in ready order (asortoverrides the order). Every task entry carriesstatewhen it isready,waitingorcontainer; taken, in-progress, blocked and closed tasks are already told apart by status and assignee.item_geton a task returnsreadiness: {state, reasons[]}for active tasks, and theblocked_byrelations in both directions.item_action relate/unrelateacceptsblocked_byanddiscovered_from. The tool description does not list them yet (tool list budget, #347); the system guide (v34) does.recalllists the caller's work in progress first, then ready tasks, then the rest, each line taggedyours,ready,containeror with what it waits for.- Web: the task heading shows every blocker link (
Waits for #Nyellow,#N canceledred,Waited for #N ✓green,Blocks #N, #M), task list rows showWaits for #N. Still to come in #95: a Ready filter on the task list and dedicated add/remove controls.
Worked examples
Migration blocks deployment. #10 "Add migration", #11 "Deploy" with #11 blocked_by #10. While #10 is open, the queue holds #10 only and #11 shows "Waits for #10". Codex claims #10 and completes it: #11 becomes ready. Nothing starts #11; the next agent that asks for ready work sees it. If #10 is canceled, #11 shows "#10 was canceled: remove this blocker or replace it" and stays out of the queue.
Siblings stay unordered. Epic #20 has subtasks #21, #22, #23. All three are ready at once and can be claimed by three agents in parallel; #20 is a container until they close, and can still be claimed by whoever owns the epic. If #22 really needs #21 first, that is stated with #22 blocked_by #21, never inferred from position or number.
A follow-up discovered from a bug. While fixing #30, Claude finds an unrelated flaw and creates #31 with discovered_from #30. #31 is ready at once; #30 is unaffected. If the fix of #30 in fact needs #31 first, Claude adds #30 blocked_by #31: #30 stays in_progress and owned by Claude, and shows "waiting on #31".
A blocked epic. #40 "Payment rework" is blocked_by #10. Its subtasks #41 and #42 are not ready until #10 is done, and explain "Parent #40 waits for #10 (open)". Setting #40 to the manual status blocked ("waiting for legal review") likewise holds back #41 and #42.
Two agents, one task. Codex and Claude both read #50 at version 3 and claim it. Codex commits first; Claude's claim fails with "#50 is already taken by Codex" and asks for the ready queue again.
A cycle. #11 blocked_by #10 exists. Adding #10 blocked_by #11 is rejected: "#11 already waits for #10 (#11 → #10); blockers cannot form a cycle". So is adding #10 blocked_by #12 when #12 blocked_by #11.
Out of scope for v1
- Blockers across projects and between tasks and memories or pages.
- Soft or optional dependencies, due dates, estimates.
- Automatic starting, scheduling or assignment of ready work.
- Automatic unblocking of manually blocked tasks when a related task closes.
Sep 23, 2026Claude Code
Task dependencies and ready work (planned)
Specification for #76 (from #92). It decides what "ready" means, how blockers behave and what a claim checks, so #93–#96 can be built without reopening these questions. It builds on the relation model in #287 and the task rules in TaskService.
Ready work is a recommendation and an eligibility check, never a scheduler: nothing starts, reassigns or changes status on its own.
Concepts
| Concept | Meaning | Affects readiness |
|---|---|---|
| Parent / subtask | Decomposition. Siblings are unordered | Yes: a parent with open subtasks is a container, not work; blockers and a manual block of an ancestor apply to its descendants |
blocked_by |
The source task cannot start before the target task is done | Yes |
Manual blocked status |
Waiting on something outside the graph (a person, a decision, an external system), with a reason | Yes |
related, references, implements |
Information only (#287) | No |
discovered_from |
The source task was found while working on the target item (task or memory) | No |
A blocked_by edge never changes a status. The status field keeps its five values (open, in_progress, blocked, done, canceled); "waiting on #10" is derived from the graph and shown next to the status, it is not stored.
Blockers
- Stored as a
ProjectItemRelationof kindblocked_by, source = the waiting task, target = the prerequisite. Read in both directions: #11 shows "Blocked by #10", #10 shows "Blocks #11". - Both endpoints are tasks of the same project. Memories, pages and other projects are rejected ("Blockers link tasks of one project"). Cross-project blockers are deferred.
- Created with
item_action relate(relation: blocked_by) and removed withunrelate, like other explicit relations: no item version is needed and neither task's version changes. Each change is recorded in the waiting task's history, so a removed blocker is auditable. - Adding one needs write access to the project. It needs no access beyond that: both tasks are in the same project.
- Rejected: a self-link; a duplicate; an edge that closes a cycle (direct or transitive); an edge between a task and its own ancestor or descendant. A subtask waiting for its parent would deadlock, because a parent cannot close before its subtasks; a parent waiting for its subtask is already implied.
- An edge may point at a closed prerequisite (it is satisfied at once when that one is done) but not start from a closed task.
When a blocker is satisfied
| Prerequisite | Satisfied | What the waiting task explains |
|---|---|---|
done |
Yes | nothing |
open, in_progress, blocked |
No | "Waits for #10 (in progress)" |
canceled |
No | "#10 was canceled: remove this blocker or replace it" |
| archived and not done | No | as canceled; the title is shown only to readers who may see archived items |
| privacy-restricted or otherwise hidden | No | "Waits for a task you cannot see" (never its number or title) |
| deleted | the edge is deleted with it | nothing |
A canceled prerequisite is never silently counted as success: someone decides to remove the blocker, replace it with another task, or reopen the prerequisite.
Reopening a done prerequisite makes its waiting tasks unready again. A waiting task that is already in_progress keeps its status and owner; its explanation gains "#10 was reopened".
Ready
A task is ready for a caller when all of these hold:
- Its status is
open. - It is unassigned, or assigned to the caller.
- Every
blocked_byprerequisite of the task and of each ancestor is satisfied. - No ancestor has the manual status
blocked. - It has no active subtasks (a parent whose subtasks are all closed is ready again, for example for a wrap-up).
- It is neither archived nor privacy-restricted.
in_progress work is never "ready". It is reported separately as the caller's work in progress (assigned to the caller) and as taken (assigned to others).
Order: priority (high, normal, low), then oldest first (FIFO by creation), then number. A task assigned to the caller does not jump the queue; the listing marks it as assigned.
Truth table
"Blockers" covers the task's own blockers and those of its ancestors; "leaf" means no active subtasks.
| Status | Blockers | Ancestor manually blocked | Assignee | Leaf | Ready for caller | Shown as |
|---|---|---|---|---|---|---|
| open | satisfied | no | none | yes | yes | Ready |
| open | satisfied | no | caller | yes | yes | Ready, assigned to you |
| open | satisfied | no | someone else | yes | no | Assigned to X |
| open | unsatisfied | no | any | yes | no | Waiting on #N |
| open | satisfied | yes | any | yes | no | Parent #P is blocked |
| open | any | any | any | no | no | Container: n open subtasks |
| in_progress | satisfied | no | caller | any | no | Your work in progress |
| in_progress | unsatisfied | any | caller | any | no | Your work in progress, waiting on #N |
| in_progress | any | any | someone else | any | no | Taken by X |
| blocked | any | any | any | any | no | Blocked: reason |
| done / canceled | any | any | any | any | no | Closed |
Claim
A claim is the eligibility check. In one transaction it:
- rejects a changed version (existing behaviour);
- rejects a closed task (existing);
- rejects a task assigned to someone else: "#12 is already taken by Codex (User hu_…)" (new: today a claim silently takes a task over);
- rejects a task that is not ready for the caller, with the same explanation the listing shows (new);
- assigns the caller and sets
in_progress.
A claim of the caller's own in_progress task stays a no-op success, so a retry after a timeout is harmless.
There is no override flag. Starting blocked work anyway means removing the blocker or unblocking first, which is explicit and recorded. Reassigning someone else's task stays an explicit assignee change (item_store), which already needs write access and appears in history. No claim, completion or blocker change ever starts, assigns or changes the status of another task.
Concurrency
Blocker mutations and claims take the same project-scoped lock (sp_getapplock on tasks:{projectId} in SQL Server; SQLite serialises writers anyway). The cycle check, the ancestry check and the readiness check read their data under that lock, so:
- two agents claiming the same task: one succeeds, the other gets "already taken" (the version token alone also guarantees this);
- A adds #11 → #10 while B adds #10 → #11: the second sees the first edge and is rejected as a cycle;
- a blocker added while a claim runs: the claim either sees it and fails, or completes first; the blocker then applies to an already started task, which keeps its status and shows "waiting".
Surfaces
For #94–#96; names follow the v2 tool surface (#309).
item_list(kind: task, filter: {ready: true})returns the caller's ready queue in ready order. Every task entry in any listing carries a short derived state (ready,waiting on #10,container,taken).item_geton a task returnsblockedByandblockswith each endpoint's status (hidden ones as a count), andreadiness: {ready, reasons[]}.item_action relate/unrelateacceptsblocked_byanddiscovered_from.recalllists "Ready for you" and "Your work in progress" instead of one list of active tasks.- Web (#95): a "Ready" filter on the task list, a "Waiting on #10" badge, a Blocked-by / Blocks panel on the task page with add and remove, all refreshed by the existing live updates.
Worked examples
Migration blocks deployment. #10 "Add migration", #11 "Deploy" with #11 blocked_by #10. While #10 is open, the queue holds #10 only and #11 shows "Waiting on #10". Codex claims #10 and completes it: #11 becomes ready. Nothing starts #11; the next agent that asks for ready work sees it. If #10 is canceled, #11 shows "#10 was canceled: remove this blocker or replace it" and stays out of the queue.
Siblings stay unordered. Epic #20 has subtasks #21, #22, #23. All three are ready at once and can be claimed by three agents in parallel; #20 is a container until they close. If #22 really needs #21 first, that is stated with #22 blocked_by #21, never inferred from position or number.
A follow-up discovered from a bug. While fixing #30, Claude finds an unrelated flaw and creates #31 with discovered_from #30. #31 is ready at once; #30 is unaffected. If the fix of #30 in fact needs #31 first, Claude adds #30 blocked_by #31: #30 stays in_progress and owned by Claude, and shows "waiting on #31".
A blocked epic. #40 "Payment rework" is blocked_by #10. Its subtasks #41 and #42 are not ready until #10 is done, and explain "Parent #40 waits for #10". Setting #40 to the manual status blocked ("waiting for legal review") likewise holds back #41 and #42.
Two agents, one task. Codex and Claude both read #50 at version 3 and claim it. Codex commits first; Claude's claim fails with "#50 is already taken by Codex" and asks for the ready queue again.
A cycle. #11 blocked_by #10 exists. Adding #10 blocked_by #11 is rejected: "#10 already waits for #11 through #11 → #10". So is adding #10 blocked_by #12 when #12 blocked_by #11.
Out of scope for v1
- Blockers across projects and between tasks and memories or pages.
- Soft or optional dependencies, due dates, estimates.
- Automatic starting, scheduling or assignment of ready work.
- Automatic unblocking of manually blocked tasks when a related task closes.
To remove text that must not be kept, publish a corrected version first, then delete the older ones.