Purpose of this page: answer the three questions every new engineer asks — how a task is picked, how it runs, and what actually runs in parallel — with one flowchart you can keep in your head.
Flow

Scheduling & execution

There is no scheduler component. Selection is a pull model: readiness promotes tasks into a pool, and workers claim from it. This page is the one flow to know.

Short answers

How does a task get selected?

A task becomes ready when every non-waived dependency is done or partial. Workers list nodes with state=ready and claim one. Ties break by ULID, bytewise.

How does a task get executed?

The claim opens one SQLite transaction: leases, run tree, first attempt, state transition, events. The worker then executes in the objective's clone and heartbeats to hold its lease.

How does parallel execution happen?

Across objectives. Each objective owns one clone and holds one lease, so its tasks run one at a time. Different objectives run on different workers at the same time.

The most important flow

Step 1 Plan import write path
every node starts pending objectives · tasks · dependency edges
a completing dependency re-evaluates its dependents
Step 2 Readiness promotion same transaction as the completion
pending → ready when all deps are done/partial or waived ready → pending demotion on regression
the ready pool grows
Step 3 Worker polls the pool read path over HTTP
list nodes filtered state=ready order: topological walk · ULID tie-break
the worker picks one and claims it
Step 4 Claim — one transaction the heart of the flow
sweep expired leases completeness check on ancestors lease-hierarchy refusal: self · parent · sibling · descendant acquire objective lease + task lease (fenced) open or adopt objective run · open task run · attempt №1 ready → running, ancestors cascade append lease.claimed + node.running
lease, fence, run id, attempt id return to the worker
Step 5 Execute in the clone outside the daemon
one clone per objective, shared by its tasks the worker's agent loop does the work, outside the daemon process heartbeat every ttl ÷ 3 renews both leases, fence never changes
report · release · or failure
Step 6 Close and settle write path
attempts close: released · cancelled · rejected running → done or partial rejection loops to a new attempt, limit 3 → blocked release puts the task back to ready
completion re-runs readiness in the same transaction
Step 7 Readiness ripple back to step 3
newly unblocked dependents promote to ready the pool refills without any central scheduler
step 7 feeds step 3, until the graph drains

Where the parallelism actually is

Worker A

objective-01 · clone A
task → task → task, strictly serial.
The objective lease is the serialization point.

Worker B

objective-02 · clone B
task → task → task, strictly serial.
Runs at the same time as Worker A.

Why a sibling claim is refused

A second claim on the same objective collides with the live objective lease (relation: parent) or a sibling's lease. Refusal names the holder, its fence, and its expiry.

  1. One live lease per subject. The fence counter never resets, so a stale holder cannot write.
  2. One active run per node, enforced by a partial index.
  3. One clone per objective — the reason tasks of an objective cannot run side by side.
  4. An expired lease does not heal itself. Recovery is explicit: status reports stale, then abandon or startup recovery acts.

Where each step lives in the code

StepModule
2 · promotionsrc/domain/readiness.ts · applied by src/services/readiness/dependency.ts
3 · orderingsrc/domain/task-order.ts — topological walk, ULID tie-break
4 · claimsrc/commands/node/claim-node.ts
4 · leasessrc/domain/lease-hierarchy.ts · SqliteLease
4 · run treeSqliteExecution — run · attempt rows
5 · heartbeatsrc/commands/node/heartbeat-node.ts — renew at fence
6 · settlesrc/commands/node/release-node.ts · outcome commands
recoverystartup-recovery commands · unblock · abandon