The Work Queue

For a daily walkthrough, read A morning check.

Foundry runs engineering work continuously — one-shot tasks, campaign cycles, nightly maintenance, releases, remediations. The work-item ledger is the durable record of that work as units, so you can answer "what is running, and what still needs me?" without reading raw events, worktrees on disk and remote branches.

foundry queue is the operator's view of that ledger. The overview, show and open read it; close and cancel let an owner discharge specific items; hold, release and foundry pacing steer what starts next (see Pacing).

What a work item is

A work item is one unit of work Foundry dispatched. It is recorded at the root of its chain — the event that set the work in motion — so a dispatch that stops early is still in the ledger rather than invisible.

Each item carries a stable id (wi_ followed by 24 hex characters), the project it belongs to, the objective it serves, its kind, its lane, an opaque origin string Foundry never interprets, a typed source saying what dispatched it, the timestamps of its life, its state, a one-line reason for that state, the workflow trace_id it runs under, and — once it settles — its settlement fields.

The ledger lives in a single JSON file, ~/.foundry/work-items.json (override with FOUNDRY_WORK_ITEMS_PATH). It is owned by foundryd and authoritative: every mutation loads the file, applies the change, and saves it through a same-directory temp-file rename.

The seven kinds

KindWhat it isAdmitted rootSettlement
taskA one-shot taskTask ExecutionRequestedTyped TaskRunCompleted
campaign_cycleOne campaign cycleTask ExecutionRequestedTyped TaskRunCompleted
major_upgradeA nightly major upgradeTask ExecutionRequestedTyped TaskRunCompleted
maintenanceA project maintenance runProjectRunStarted, ProjectMaintenanceRequestedWhole project traversal
releaseA releaseReleaseRequestedRelease, pipeline and installation traversal
remediationPipeline or vulnerability workPipeline and vulnerability rootsWhole remediation traversal
workflowOther engineering work, including agentsIteration, validation, formation, digest, summary and scan rootsWhole traversal

Task settlement retains its typed verdict. Its execution slot stays occupied until task cleanup finishes. Other kinds settle after the whole traversal. A release reached during remediation uses that root's existing ownership.

The three lanes

LaneWho submitted the work
interactiveA person at the CLI
campaignA campaign derived it
maintenanceA scheduled maintenance run dispatched it

Origin: how the work reached Foundry

An item's origin says how the work arrived. For work an automation dispatched that is the lane itself:

Dispatched byOrigin
foundry taskfoundry task
A campaign cyclecampaign <name> cycle <n>
The nightly majors lanenightly majors lane

For work a person asked for by hand, the useful extra fact is who asked and from where. foundry task and foundry campaign advance therefore record the CLI client's hostname, and accept an optional --origin <text> note that is recorded verbatim beside it:

foundry task acme "Add a --quiet flag" --origin "asked by Stacey in standup"
foundry campaign advance tidy-cli --origin "kicked off by hand after the fix"

The recorded origins then read:

foundry task (host workbench: asked by Stacey in standup)
campaign tidy-cli cycle 4 (host workbench: kicked off by hand after the fix)

The operator context is appended to the dispatch origin rather than replacing it, so a campaign cycle still says which campaign and cycle it is. If the hostname cannot be read, the origin states host unknown host rather than going silent — "the lookup failed" is a more honest record than "no operator was involved".

Origin is opaque. Nothing parses, validates, filters or groups on it; foundry queue only displays it, and no dispatch is ever rejected, delayed or altered because of what its origin says — an empty --origin is accepted and simply adds nothing. Cycles dispatched by the automatic post-result advance carry no operator context, because no operator issued them.

Source: what dispatched the work

Origin is text for a person. source is the typed answer to the same question, recorded at submission and never changed, so the ledger alone can answer "everything this campaign dispatched" or "what did the nightly start" without reading the event log to recover a campaign from a trace.

A source is a kind from a closed set, the one ref that names the dispatcher, and, for a campaign, the cycle number:

KindrefRecorded when
campaignthe campaign namea campaign advance dispatches a cycle; cycle is the cycle number, whoever asked for the advance
sentinelthe sentinel namea sentinel-fired chain dispatches an item: the nightly's per-project maintenance runs, its majors-lane upgrades, a remediation or release it reaches
operatorthe hostname of the CLI that askedfoundry task, foundry iterate, foundry run, foundry release, foundry pipeline, and any item a chain they start reaches
work_itemthe parent item idan item created from another item: a queue resume child or a nightly continuation (its resumes link is unchanged)

The kinds are a closed enum; adding one is a code change, never a free string. The source travels on the event envelope: whatever emits a root event names it there, and every event below the root inherits it verbatim (see Tracing), which is how a per-project run three hops below the nightly's root still knows which sentinel fired it. A release that follows remediation retains the root's source and execution slot.

An item recorded before the source existed has none, and so does one whose root named none (a raw foundry emit). Every reader treats that as not recorded, exactly as worktree_removed is handled: queue show prints no Source: line, --json emits no source key, and a row shows -. The free-text origin is untouched by any of this; the two sit side by side.

To list one source's items:

foundry queue --source campaign:bedrock-gated-trials-v1   # every cycle of that campaign
foundry queue --source sentinel:nightly-maintenance       # what the nightly started
foundry queue open --source operator:workbench            # open work asked for from that host

The filter is an exact match on kind and ref, applied by the daemon's ListWorkItems (and by the same selection offline); an unknown kind, or a kind with no ref, is refused. --source goes before or after open. foundry campaign show <name> lists the campaign's cycles through this same filter. The filter also selects escalated campaigns: campaign:<name> shows that campaign's escalation, and every other kind shows none.

The states

An item moves through unsettled states, then settles. Settled does not mean finished: three of the settled states still hold an obligation for a person, which is why the ledger says an item settles rather than completes.

StateMeaningGroup
submittedAdmitted to the ledger, not yet queuedQueued
queuedWaiting for the scheduler to start it; reason says why it waitsQueued
heldTaken out of the scheduler's hands by queue hold until releasedQueued
runningAn agent is working on itRunning
preservedWork is held on a durable ref for a later cycleOpen
needs_decisionWork stopped on a question only a person can answerOpen
failedWork stopped on a faultOpen
landedThe work reached trunkTerminal
cancelledAn operator stopped the workTerminal

landed and cancelled are terminal — nothing more is owed. preserved, needs_decision and failed are open — settled, but still carrying something for you to discharge. foundry queue open is that set, plus every escalated campaign.

Escalated campaigns wait on you too

An escalated campaign has stopped until its owner decides, resumes, completes or cancels it. That is as much "waiting on you" as a preserved item, but it is campaign state, not a unit of executed work, so the ledger never records it as an item. On 2026-10-11 two of four escalated campaigns had no open item at all, because they escalated after their final cycle landed, and queue open did not show them.

So foundry queue open, and the open group of the foundry queue overview, list every escalated campaign after the open items, one row each:

Open — needs a person
  wi_ccc3  beta  major_upgrade  maintenance  sentinel:nightly-maintenance  preserved  2026-09-29T03:10:00+00:00  gates red after the bump
  campaign  mojentic-ex-v2  mojentic-ex  escalated  2026-10-11T03:12:40+00:00  campaign cycle budget exhausted (6)
            next: foundry campaign show mojentic-ex-v2 (decide, resume, complete, cancel)

The row carries the campaign, its project, when it escalated and why (see What an escalated campaign shows its owner), then the command that prints the full reason and the owner's command lines. The reason reads (reason not recorded) when neither the campaign nor the event log has one.

Every escalated campaign is listed, including one that also has an open cycle item. The two are not duplicates: the item is the work and its evidence (a preserved ref, a failure), the campaign row is the decision and the commands that answer it. The row names the item, as (open cycle wi_…), so they read as one obligation. Hiding the campaign row when an item exists would hide the reason and the commands again, which is the defect this fixes; hiding the item would make the ledger view incomplete.

Every kind enters queued and starts only when the shared scheduler permits. Its reason names what it waits on, or says ready. The submitted state exists in the model, but normal admission records the queued state directly.

Queued and held items survive a daemon restart as they are: the scheduler reads them back off the ledger and picks up where it left off.

Daemon restarts settle running items as failed

A running item means "an agent is working on it", and after a daemon restart no agent is. On every start foundryd settles each item still running as failed, with the reason daemon restarted. This is deliberate: reporting long-dead work as running is worse than reporting it as failed, and a failed item is visible in foundry queue open where you can decide what to do with it.

The six lifecycle events

The ledger emits six events. They are the owner-specified exception to Foundry's *Started/*Completed pairing rule.

EventMeaning
work_item_submittedA unit of work entered the ledger; for paced work, state is queued and reason says why it waits
work_item_startedAn agent started on a ledger item
work_item_settledA ledger item reached a settled state, with its disposition
work_item_cancelledAn operator, or the item's own campaign, stopped a ledger item
work_item_heldAn operator took a queued item out of the scheduler's hands
work_item_releasedAn operator returned a held or dependency-blocked item to the queue

work_item_started pairs with work_item_settled rather than a work_item_completed, because an item does not complete — it settles, into a state that may still hold an obligation.

work_item_cancelled is emitted by foundry queue close, foundry queue cancel, and foundry campaign cancel <name> --reason … --now, which stops the in-flight cycle outright and cancels the campaign's queued cycles with it. It is also emitted when a campaign moves past an open cycle (see "Campaign cycles the campaign has moved past" below). It carries the item's id, project, kind, lane, state, reason and origin, plus the settlement fields, and for the aborted cycle it rides the aborted cycle's trace rather than the cancellation's — so it sits with the rest of the events about that unit of work. Like the other five it reaches foundry watch and the durable JSONL event log.

Commands

foundry queue [--source <kind>:<ref>] [--json] [--offline]
foundry queue show <id> [--json] [--offline]
foundry queue open [--source <kind>:<ref>] [--json] [--offline]
foundry queue hold <id> [--origin <text>]
foundry queue release <id> [--origin <text>]

foundry queue

Prints four groups on one screen, in this order:

  1. Running — items an agent is working on, by start time.
  2. Queued — items in submitted, queued or held, by submission time.
  3. Open — needs a person — preserved, needs_decision and failed, newest settlement first.
  4. Settled (last 20) — the newest 20 terminal (landed, cancelled) items. Older terminal items are omitted; the terminal group grows forever, so the overview shows only its newest page. The cap applies to this group alone.

Each line shows the item's id, project, kind, lane, source (- when none was recorded), state, the timestamp that placed it in its group, and the one-line reason:

Running
  wi_aaa1  alpha  task  interactive  operator:workbench  running  2026-09-30T01:00:05+00:00  running

Queued
  (none)

Open — needs a person
  wi_ccc3  beta  major_upgrade  maintenance  sentinel:nightly-maintenance  preserved  2026-09-29T03:10:00+00:00  gates red after the bump

Settled (last 20)
  wi_ddd4  alpha  release  maintenance  -  landed  2026-09-28T02:40:00+00:00  released v1.2.0

The group order and the order within each group come from the daemon's ListWorkItems response. The CLI groups by state; it never re-sorts. With --source <kind>:<ref> the daemon returns only that source's items, in the same order, and the groups show those.

foundry queue show <id>

Prints one item's full durable record, one field per line, including every settlement field, then a blank line and the item's own work_item_* events. An optional field the ledger never recorded produces no line at all, so you never have to tell a recorded empty string from an unset field. worktree_removed is the sharpest case: a recorded false prints Worktree removed: no, while "no worktree recorded" prints nothing. Source: follows the same rule: an item recorded before the source existed prints no Source: line.

Id:               wi_ccc3
Project:          beta
Objective:        bump serde to 2.0
Kind:             major_upgrade
Lane:             maintenance
Origin:           nightly majors lane
Source:           sentinel:nightly-maintenance
State:            preserved
Reason:           gates red after the bump; work held on a branch
Submitted:        2026-09-29T02:00:00+00:00
Started:          2026-09-29T02:00:01+00:00
Settled:          2026-09-29T03:10:00+00:00
Trace:            abcdef0123456789abcdef0123456789
Verdict:          remainder
Preservation ref: foundry/majors/serde
Worktree:         /home/you/.foundry/worktrees/beta/x
Worktree removed: no

Events:
  2026-09-29T02:00:00+00:00  work_item_submitted  submitted       evt_1a2b…  submitted
  2026-09-29T02:00:01+00:00  work_item_started    running         evt_3c4d…  agent started
  2026-09-29T03:10:00+00:00  work_item_settled    preserved       evt_5e6f…  gates red after the bump; work held on a branch

Each event line shows when it occurred, its type, the state it left the item in, the event id and the one-line reason. The events are read from the durable event log (FOUNDRY_EVENTS_DIR, one YYYY-MM.jsonl file per month) through the daemon's ListWorkItemEvents RPC, and are:

  • selected by the item's id in the event payload — never by trace or project. Multiple independently queued roots can share a trace and project; each shows only its own events.
  • read from every monthly file, however old, so an item from last year still shows its history rather than a false "no events".
  • in chronological order — oldest first, ties in the order they were logged.

An item with no events in the log (or no event log at all) prints (no events) under the heading rather than nothing. A line in the log that is not valid JSON — or that holds two events run together — is skipped with a warning in the daemon log; the item's other events are still shown. A fault reading the log itself is an error, never an empty history.

foundry queue open

Prints only the open group — the work that still needs a person, then the escalated campaigns. This is the command to run when you want the shortest possible answer to "what is waiting on me?"

--json

All three commands take --json. The JSON form renders from the same fetched data as the human form, so the two can never disagree about which items the daemon returned.

  • foundry queue --json and foundry queue open --json emit one object with two arrays: items, the ledger records, and escalated_campaigns, one entry per escalated campaign the view lists. Through 0.44.4 both emitted a bare array of items; a script that read .[] now reads .items[]. A campaign entry carries name, project, status, cycles_completed, cycles_landed, max_cycles, open_escalation (absent when no reason was recorded), open_cycles (the ids of its open items, absent when none) and owner_options (control, command, effect for each). The two arrays are separate because a campaign escalation is not a ledger record: a consumer that counts items never counts a campaign.
  • foundry queue show <id> --json emits a single JSON object: the record's keys, unchanged, plus an events array. Each event carries id, event_type, occurred_at, state, reason and, when it has one, trace_id; an item with no events has "events": [].

Optional fields are absent when unset rather than emitted as null, "" or false, so the JSON round-trips the same facts the record carries:

{
  "items": [
  {
    "id": "wi_ccc3",
    "project": "beta",
    "objective": "bump serde to 2.0",
    "kind": "major_upgrade",
    "lane": "maintenance",
    "origin": "nightly majors lane",
    "source": { "kind": "sentinel", "ref": "nightly-maintenance" },
    "submitted_at": "2026-09-29T02:00:00+00:00",
    "started_at": "2026-09-29T02:00:01+00:00",
    "settled_at": "2026-09-29T03:10:00+00:00",
    "state": "preserved",
    "reason": "gates red after the bump; work held on a branch",
    "trace_id": "abcdef0123456789abcdef0123456789",
    "verdict": "remainder",
    "preservation_ref": "foundry/majors/serde",
    "worktree": "/home/you/.foundry/worktrees/beta/x",
    "worktree_removed": false
  }
  ],
  "escalated_campaigns": []
}

Note what is not there: landed_commit. The item did not land, so the field was never recorded, so it is absent. A source has the same keys the ledger writes (kind, ref, and cycle for a campaign); an item that records none has no source key. The items array is the fetched list unchanged — neither grouped nor capped at 20 — so a consumer can apply its own rules.

Online versus offline

CommandDaemon required?Notes
foundry queue [--source <kind>:<ref>]Yes (or --offline)Renders ListWorkItems; --source is its exact-match source filter. The open group adds the escalated campaigns from ListCampaigns
foundry queue show <id>Yes (or --offline)Renders GetWorkItem, then ListWorkItemEvents
foundry queue open [--source <kind>:<ref>]Yes (or --offline)Renders ListWorkItems, open group only, then the escalated campaigns from ListCampaigns

Online is the default and is daemon-authoritative. All three commands render the daemon's response directly and never read, create or mutate the client-side ledger or events files: if FOUNDRY_WORK_ITEMS_PATH or FOUNDRY_EVENTS_DIR is absent, the online path leaves it absent, and an existing file is left byte-for-byte untouched. If foundryd is not listening, the command fails with a stable actionable error naming the matching --offline recovery command. There is no silent fallback.

A NOT_FOUND from GetWorkItem surfaces as an id-not-found error and a non-zero exit, not as an empty record.

--offline is explicit recovery for when the daemon is stopped and you intentionally want direct file access. It reads FOUNDRY_WORK_ITEMS_PATH (and, for show, FOUNDRY_EVENTS_DIR) directly and never contacts the daemon, applying the same grouping order the ListWorkItems contract documents and the same event selection and order ListWorkItemEvents documents — --offline differs from the online path only in transport, never in reading order.

  • A missing ledger file renders four empty groups and exits zero. Absence is the normal starting state, not a fault, and a read never creates the file.
  • A malformed ledger file is an error: the command exits non-zero and names the parse failure.
  • --source applies the same exact-match selection to the file the daemon applies to its store, so a filtered --offline read lists the same items.
  • The overview and open read escalated campaigns from FOUNDRY_CAMPAIGNS_PATH, and recover a pre-record escalation reason from FOUNDRY_EVENTS_DIR, the same way ListCampaigns does. A missing campaign store has no campaigns, and a read never creates it.

Close or cancel an item

foundry queue close wi_0123456789abcdef01234567 --reason "Reviewed; no further work required" --origin "owner review"
foundry queue cancel wi_0123456789abcdef01234567 --origin "withdrawn request"

close discharges an open obligation in preserved, needs_decision or failed; it requires a nonblank --reason. cancel stops a submitted, queued or held item and records the reason cancelled by operator, without requiring a reason argument. Both settle exactly the requested id as cancelled.

Both require the daemon, reject --offline, and never fall back to local writes. They record this CLI's hostname and optional --origin in operator_action, separately from the original submission origin. The action retains the previous state, reason and settlement timestamp. Submission identity, objective, trace and known disposition remain intact, including preserved refs and worktree-removal observations. No agent starts, running workflow stops, or preserved work is disposed of by either command.

The daemon reloads the ledger under its shared write gate and saves by atomic replacement before emitting work_item_cancelled to Watch and the durable log. Earlier event bytes stay untouched. Unknown ids return NOT_FOUND, blank input returns INVALID_ARGUMENT, and other states return FAILED_PRECONDITION. Malformed ledgers also return FAILED_PRECONDITION; read or save failures return INTERNAL and a failed save emits no cancellation. Concurrent requests for one item can succeed only once.

Follow up on open work

foundry task <project> "Fix the review findings on wi_X's branch" --continues wi_0123456789abcdef01234567

A follow-up task often finishes work that another item left open: it builds on that item's branch, fixes its review findings, and lands. foundry task <project> "<description>" --continues <id> (repeatable) records that lineage. Each id must name an item in the ledger (otherwise INVALID_ARGUMENT, as for --after) that belongs to the same project and is open, that is preserved, needs_decision or failed (otherwise FAILED_PRECONDITION). A refused dispatch admits and runs nothing.

--continues is lineage only. It does not change the task's base or its objective, and it does not make the task wait: combine it with --after for that. Use foundry queue resume instead to re-run a preserved item's own objective from its preserved branch.

The new item records the ids in continues, shown by foundry queue show in human and JSON output, by the queue overview as (continues <id>, …), and in its lifecycle payloads beside resumes. When the new item settles landed with a landing commit, each continued item that is still open settles landed with landed_commit set and the reason superseded by <commit> via <new item id>. A work_item_settled event records it on that item's own trace, and the guarded branch cleanup runs as for any landed item. The decision is taken on the ledger as loaded under the shared write gate, so an item the owner already closed or cancelled stays as it is. A follow-up that does not land, including a complete that delivered nothing, leaves the continued items open and unchanged.

Resume preserved work

foundry queue resume wi_0123456789abcdef01234567 --origin "finish preserved work"
foundry queue resume wi_0123456789abcdef01234567 --after wi_89abcdef0123456789abcdef --not-before 30m

Owner-directed preserved-work continuation uses foundry queue resume <id> [--origin <text>] [--after <id>]... [--not-before <RFC3339|duration>]. It requires a live daemon and refuses --offline. Only a preserved item with a usable preservation ref and a registered project can be resumed. Foundry records a new task with the original objective, queued for the pacing scheduler, that starts from the preserved local branch, remote ref or bundle:<path> through the existing continuation path. When the preserved work does not contain current trunk, Foundry rebases it onto freshly fetched trunk before the agent starts (the preservation ref itself is not rewritten). If that rebase conflicts, the child is refused before any agent session: it settles failed with land_blocked: continuation_conflict and a reason that names the conflicting files, and the original stays preserved. --after and --not-before carry the same meaning as on foundry task. The new record and its lifecycle payloads expose resumes, the exact original id. Queue reads show this link in human and JSON output. Submission identity and the parent's prior evidence remain intact; the child records the hostname and optional origin in its resume operator action.

The original obligation stays preserved until the linked task actually lands. Then Foundry records its landing commit and appends work_item_settled for the original. Failed, blocked, preserved and no-landing results leave it open. An original cancelled by its owner stays cancelled even if its child lands. Unknown ids return NOT_FOUND, blank inputs INVALID_ARGUMENT, ineligible states or unusable evidence FAILED_PRECONDITION, and persistence failures INTERNAL. A rejected admission dispatches no execution and invokes no agent.

Admission first saves a child in failed with reason resume admission incomplete; execution not dispatched, no started_at, a settled_at, and the exact resumes and operator context. Only after both work_item_submitted and work_item_started roots have been appended successfully does Foundry save that child as running and dispatch it. If the initial ledger save fails, no child or lifecycle event is created. A later lifecycle or final ledger-save failure leaves the staged failed child; the parent's obligation and unrelated records remain unchanged. The failed child has no preservation disposition; continue using the preserved parent's id. An interruption during admission also leaves this non-running record.

Watch publishes each admission lifecycle root only after its write succeeds. A rejected admission may retain a successfully written submitted event, or both roots if the final ledger save fails. These are evidence of an admission attempt, not proof of execution: consult the ledger and RPC result. Earlier event bytes, including any partial failed append, remain intact; Foundry never truncates or rewrites history to undo admission. Ordinary engine roots, block outputs, scatter/gather, progress, close/cancel and restart settlement retain their existing best-effort event-persistence behaviour.

When an otherwise eligible nightly major upgrade matches a preserved ledger item by exact registered project, package and target version, the majors lane resumes that obligation from its preservation ref and original objective. Its child records kind: major_upgrade, lane: maintenance, origin nightly majors lane and the exact resumes id; automation records no operator action. The parent retains its submission identity and evidence. Only an actual child landing settles the preserved parent landed; other outcomes leave it open, and owner cancellation remains terminal. A selected resume admission failure is reported and never falls back to a fresh task.

Continuations count against the existing nightly caps and run sequentially with fresh upgrades in the existing order. Update policies, holds, security eligibility, successful maintenance and in-flight suppression still apply. Reviews, dry runs and interrupted-cycle summaries dispatch nothing. Without a matching preserved ledger item, the existing fresh-task and history-suppression rules apply. Owner queue resume remains an interactive, asynchronous task.

After a task actually lands, Foundry also checks that registered project's other open ledger items (preserved, needs_decision or failed) that recorded a preservation ref against the registered trunk branch, using the registered checkout path. Three proofs, tried in this order, can settle such an item landed:

  • Ancestry: the preserved commit is reachable from trunk.
  • Patch equivalence: git cherry matches every one of a nonempty set of preserved commits to a patch on trunk.
  • Content containment: git merge-tree --write-tree merges the preserved commit into trunk without a conflict, and the merged tree is trunk's own tree. Trunk then already holds every change the branch makes relative to their merge base. This is how work that a later cycle squashed, or rebased with the same conflict resolution, is recognised when its patches no longer match.

Its landed_commit is the verified trunk commit and its reason is exactly superseded by <commit>, whichever proof succeeded; the structured log names the proof. The appended work_item_settled event carries that item's exact id, project and original trace. Objective, submission origin, resumes and prior preservation evidence remain intact.

A needs_decision or failed run keeps a preservation ref even when it made no commits. Its branch then still points at the trunk commit it started from, which every later trunk contains. Such an item settles only when its preserved tip was committed after the item started, compared at whole seconds; otherwise it stays open with a diagnostic, because its obligation is the question or the fault, not work that can land. An open item without a preservation ref, such as one blocked on a dependency before it ran, is not checked. Content containment changes no ref, index or worktree; it writes the merged tree as unreferenced loose objects, which garbage collection removes. It needs Git 2.38 or later. Once an item settles landed, its recorded task branch locally and preservation branch locally and on origin are eligible for best-effort deletion. Each ref needs fresh proof by one of the three checks above against the registered trunk. Unowned refs, bundles, branches checked out in any worktree, and refs also owned by unlanded items in any registered project sharing the repository are kept. Ownership compares exact refs across repository slugs and shared Git common directories, including separate clones and linked worktrees. Deletion uses the proved commit as a guard against concurrent ref changes. The disposition and settlement event retain each ref name, observed commit, deletion result and failure reason. A failed deletion leaves the item landed. Preservation evidence for unlanded work is never deleted. Cleanup decides eligibility under the ledger write gate, releases it for all Git operations, then reloads under the gate to record outcomes. Concurrent ledger updates are retained; a changed state or ref ownership is never overwritten by stale cleanup evidence.

This check runs only after actual task landing. Failed, blocked, preserved and no-landing task results do not trigger it. It checks only open items with a preservation ref in the same registered project; owner-cancelled items remain cancelled. A repeated terminal does not append another settlement. The ledger is reloaded under the shared write gate before saving, so concurrent owner actions and unrelated updates are retained. A failed save emits no settlement.

Missing refs or objects, failed Git commands, preserved changes that trunk does not hold (unmatched patches whose merge into trunk conflicts or would still change trunk's tree), and ambiguous evidence leave the item open with an explicit supersession diagnostic in the settlement block's result and structured log. When patch equivalence and content containment both fail, the diagnostic gives each reason. Empty Git output is never proof. Local branch refs and commit ids are checked directly. Branch names are also checked by exact name on origin when configured, without fetching; conflicting local and remote heads remain unresolved. A remote-only head must already have locally available objects. A bundle must advertise exactly one head and that commit must already be locally available. Patch comparison and content containment require complete, shared history; patch comparison rejects merge commits that git cherry cannot account for. Queue show and JSON expose the persisted commit and reason through the daemon's read path. Scheduled invocation and the reporting reconciler are not delivered by this landing-triggered check.

What queue does not do

queue close, queue cancel, queue hold and queue release do not stop running work. To stop an in-flight campaign cycle use foundry campaign cancel <name> --reason … --now.

Pacing

Pacing is the stage between admission and execution. Every engineering workflow enters the same durable queue before any block runs. This includes manual and nightly maintenance, tasks, campaign formation and cycles, iteration, validation, releases, remediation, supply-chain scans, and agents that write digests. Raw emitted events pass through the same boundary.

The nightly records all project roots before it waits for their results. Queued or held maintenance remains outstanding in its cycle's gather. A restart rejoins these roots. Cancellation produces a failed project completion, so the cycle reports what stopped instead of reporting success.

One admitted root owns its downstream workflow through release installation and task cleanup. Those continuations use the existing slot. Independent tasks and campaign advances enter the queue before their parent releases its slot; they cannot deadlock by waiting inside that parent.

System digests and maintenance summaries consume host capacity without locking project repositories. They share a system resource lock so concurrent digests cannot overwrite shared output files or watermarks. A supply-chain scan with auto-fix enabled reserves every active repository it can change. The current scanner handles those projects sequentially and writes one aggregate digest. This conservative reservation can wait behind a busy repository; it never skips that repository. With auto-fix disabled, the scan consumes only a system slot.

Dry runs use the same queue because observers can run agents or checks. Mutators remain simulated, and the settlement reason says dry-run completed; mutations simulated. Lightweight status, work reconciliation and operator controls remain immediate. Submission and start lifecycle writes must succeed before execution begins. The scheduler validates the complete persisted root; a payload with an item id cannot grant execution ownership.

The rules

The scheduler evaluates every queued item on each tick, in this order, and starts the first items the rules let through:

  1. One mutating item per repository. A repository is the registered GitHub slug, or the registered checkout path when there is no slug, so two projects on one slug share a repository. An admitted workflow occupies it until all its side effects finish.
  2. Host running cap. At most max_running items (default 2) run on the host at once, whatever their lane.
  3. not_before has passed.
  4. Every depends_on item has settled landed.
  5. The item's lane is not paused.

Candidates are considered interactive lane first, then oldest submission first. A start earlier in a tick counts against the repository and host rules for the items after it.

An item can reserve the repositories of other projects. A supply-chain scan with auto-fix waits until all its reserved repositories are free. An initiative assessment reserves its repositories only if they are free.

For an initiative assessment, rule 1 applies to its own repository only. When the other rules let it start, it holds all reserved repositories if all are free. If one of them is busy, it starts with its own repository only. Its work item then shows no reserved repositories.

A dependency that can never land is not worth waiting for. When a dependency settles any way other than landed, or names an id that is not in the ledger, the dependent settles needs_decision with the reason waits on <id>, which settled <state> (or which is not in the ledger). foundry queue release <id> returns it to the queue: the settled dependencies are dropped from depends_on and the item runs regardless of them.

The reasons

A queued item's reason is the scheduler's current verdict, refreshed on every tick and visible on foundry queue rows, queue show, pacing show and in --json:

ReasonMeaning
repository busy: wi_…That running item occupies the repository
host at capacity N/NThe host running cap is reached
not before <time>Its not_before has not passed
waits on wi_…That item has not yet settled landed
lane pausedIts lane is paused
readyNothing holds it; the next tick starts it
held by operatorqueue hold took it out of the scheduler's hands

Scheduling a task

foundry task <project> "<description>" --after wi_a --after wi_b --not-before 2h
foundry queue resume wi_c --after wi_a --not-before 2026-10-11T09:00:00Z

--after <id> (repeatable) adds a dependency; every named id must already be in the ledger or the dispatch is refused with INVALID_ARGUMENT. --not-before takes an RFC 3339 timestamp or a duration from now: 90s, 30m, 2h, 1d. foundry task waits for its own run exactly as before; the wait now includes the time the item spends queued.

Hold and release

foundry queue hold wi_0123456789abcdef01234567 --origin "wait for the review"
foundry queue release wi_0123456789abcdef01234567

hold takes a queued item out of the scheduler's hands; it sits held with the reason held by operator until released. release returns a held item, or one in needs_decision over a dependency, to queued with the scheduler's current verdict as its reason. Both require the daemon, reject --offline, record this CLI's hostname and optional --origin in operator_action, and emit work_item_held / work_item_released on Watch and in the durable log. queue cancel also accepts a held item.

Pausing lanes

foundry pacing show [--json] [--offline]
foundry pacing pause [--lane interactive|campaign|maintenance|all] [--origin <text>]
foundry pacing resume [--lane interactive|campaign|maintenance|all] [--origin <text>]
foundry pacing drain [--timeout <duration>] [--origin <text>]

pacing show prints the limits in force, every running item with the repository it occupies, every waiting item with its reason in the scheduler's priority order, and the paused lanes. A waiting item that reserves repositories only if free shows them as (and <repositories> if free). --offline reads the ledger and the pacing files directly when the daemon is stopped.

pacing pause stops new starts in a lane (default all); running items are untouched. pacing resume allows starts again, and the next tick starts the next eligible item. Both emit pacing_paused / pacing_resumed for the project system, durable and on Watch. The pause state is persisted through an atomic rename and is in force after a daemon restart.

pacing drain pauses every lane, then waits until nothing is running, printing each item as it settles, and exits 0 when the daemon is idle. With --timeout it gives up after that long and exits 1 naming what still runs. It does not stop the daemon and does not resume the lanes afterwards: run pacing resume when you are ready for work to start again.

The two files

FileOwnerShape
~/.foundry/pacing.json (FOUNDRY_PACING_PATH)You{"max_running": 2}; a missing file means the defaults
~/.foundry/pacing-state.json (FOUNDRY_PACING_STATE_PATH)foundryd{"version": 1, "paused": ["campaign"]}; written by pause and resume

A malformed pacing.json falls back to the defaults with a warning. A malformed state file fails closed: every lane is treated as paused until the file is repaired or rewritten by a pacing resume.

What pacing does not do

Per-provider caps, starts-per-hour limits, provider-health gating and dashboard controls are out of scope. pacing drain does not refuse a daemon stop; it only tells you when a stop would interrupt nothing.

What campaign cancel --now does to the cycle's item

A --now cancellation kills the in-flight cycle, so the TaskRunCompleted that normally settles its item never arrives. Foundry therefore settles that item itself:

  • The running item carrying the aborted run's trace settles cancelled, with your --reason text as its reason and the settlement time recorded. It moves out of Running and into the Settled (last 20) group; cancelled is terminal, so it never shows up under queue open.
  • Its disposition records the cycle's worktree and whether that worktree is gone, observed after disposal ran — plus the branch (or bundle: path) the work was preserved on. With --discard-work there is no preservation ref, because the work was thrown away. A cycle that had built no worktree records no disposition at all.
  • Correlation is by trace alone, exactly as a normal settlement: if no running item carries the aborted run's trace, nothing is settled. Foundry never falls back to "the project's newest running item", because that would report an unrelated concurrent run as cancelled.
  • A graceful cancel (no --now) does not stop the in-flight cycle: it finishes on its own and then settles cancelled, as described in the next section. A --now cancel with nothing in flight, and cancelling an already-cancelled campaign, both change no item either.

The ledger is bookkeeping beside the cancellation, never a precondition for it: if the ledger cannot be read or written, the campaign is still cancelled and the CampaignCancelled event is still emitted — the fault is logged, and the item is left for the restart sweep.

Campaign cycles the campaign has moved past

A campaign forms each cycle from the previous cycle's preserved ref, or from trunk. An earlier cycle left preserved, needs_decision or failed is therefore not owed by a person: the campaign carried it, and the campaign's own escalation is what reaches a person. Foundry settles such cycles itself, as cancelled, without an operator_action, keeping their preservation ref and worktree fields:

  • When the scheduler starts a later cycle of the same campaign, every open cycle with a lower cycle number settles with the reason carried forward by campaign <name> cycle <n> (<item id>). This happens before the new cycle's agent runs. The newest cycle submitted carries; one that was itself cancelled carries nothing. An escalated (needs_decision) cycle is the live question, so it stays open until the campaign advances again or ends.
  • When the campaign completes (formation done or foundry campaign complete) or is cancelled, its remaining open cycles settle with the reason campaign <name> completed or campaign <name> cancelled, followed by : <reason> when one was recorded.
  • A graceful cancel lets the in-flight cycle finish after CampaignCancelled. When that cycle settles open, its own work_item_settled finds the campaign already cancelled in the campaign store, and the cycle settles cancelled then. It is not stranded open.
  • The campaign's formation runs (campaign_advance_requested), which durable admission records as workflow items with the campaign's source, follow the same rules. An open formation run settles with the carried-forward reason when a cycle submitted after it starts, and with the terminal reason when the campaign completes or is cancelled. A formation run never carries other items.

Each settlement emits work_item_cancelled on that item's own trace. Only campaign_cycle items whose typed source names the campaign and a cycle, and workflow items whose typed source names the campaign, are touched. An item recorded before the typed source existed, another campaign's item, a non-campaign item and a settled item never change. The decision is taken on the ledger as loaded under the shared write gate, in the same pass that writes it, so an owner action that landed first wins. The Settle Carried Campaign Work block runs on every campaign cycle's and formation run's work_item_submitted, work_item_started and work_item_settled, and on campaign_completed and campaign_cancelled; each trigger decides from the current ledger, so a repeated or reordered trigger changes nothing more. Durable admission persists and broadcasts work_item_submitted without routing it to blocks, so in the daemon the carry happens at work_item_started.

Work reconciliation

The work-reconciler sentinel runs at 30 */3 * * *, or use foundry queue reconcile against the daemon to run it now and print that invocation’s report. It reports exact inventory identities and conservatively settles verified preserved obligations without deleting their evidence. Orphan, broken and unresolved findings, including inspection errors, reach the ops anomaly path even below the normal pressure threshold. See the work reconciler guide for proof and failure semantics.

Worktree housekeeping after validation

Successful validation runs Cleanup Branches, including during nightly maintenance and foundry validate. It prunes Git metadata for directories that no longer exist. An existing secondary worktree is removed only when it is under that project's Foundry worktree root (FOUNDRY_WORKTREES_DIR, default ~/.foundry/worktrees), has no submitted, queued or running work-item owner or active campaign cycle, has no uncommitted files (including ignored files), and has no commits absent from every remote-tracking ref. Cleanup uses ordinary git worktree remove, without force.

Every retained worktree is named in the validation housekeeping summary and structured log, with its reason: outside Foundry ownership, owned by live work, or holding unpreserved work. A failed safety check also retains the worktree and reports the error. Cleanup reloads the daemon-owned ledger for each deletion and releases its write gate and campaign store lock before every Git await. Ownership is checked again immediately before each individual removal, after inspecting worktree contents. A task is admitted on ExecutionRequested, before workspace creation on PlanCompleted; the worktree path is recorded after git worktree add. Until that path is known, the live dispatch conservatively protects every candidate for its project. Creation refuses an existing path, so a later admission cannot adopt the existing worktree that cleanup has selected for removal.

Merged branches still use git branch -d. Branches used by live work or recorded preservation refs are retained. This housekeeping does not dispose of the workspace of a task undergoing review; task finalization remains responsible for its own workspace after landing or preservation.

Initiative work

An initiative groups campaigns under a shared outcome. Its cycles carry source: initiative:<name> while retaining the member's campaign and campaign_cycle in their task payload. Initiative steering and standalone dispatches use the same source filter. Their typed root and member identities distinguish them from campaign cycles.

foundry queue --source initiative:shared-contract
foundry queue open --source initiative:shared-contract

The open queue lists an escalated initiative once and suppresses its member campaign escalation rows. Open ledger items retain their own rows. foundry initiative show <name> lists each shared question, its applicable members and the command that answers it.

A resumed initiative member receives the current brief path and content hash, stage providers and bounded findings. Its diagnosis remains available until work that saw the finding lands.