gRPC API

The Foundry service is defined in proto/foundry.proto.

Service: Foundry

Emit(EmitRequest) → EmitResponse

Fire an event into the system. The engine spawns processing as a background task and returns the event ID immediately. Use Trace to check for completion, Status to see in-flight workflows, or Watch for real-time event streaming.

Request:

FieldTypeDescription
event_typestringEvent type name
projectstringTarget project
throttleThrottle enumTHROTTLE_FULL, THROTTLE_AUDIT_ONLY, THROTTLE_DRY_RUN
payload_jsonstringOptional JSON payload

Response:

FieldTypeDescription
event_idstringDeterministic ID of the created event
workflow_idstringID of the triggered workflow (if any)

Status(StatusRequest) → StatusResponse

Query active workflow states.

Request:

FieldTypeDescription
workflow_idstringSpecific workflow (empty for all active)

Response:

FieldTypeDescription
workflowsrepeated WorkflowStatusActive workflow states

Watch(WatchRequest) → stream WatchResponse

Server-side streaming of live events as they are processed by the engine. Optionally filtered by project name.

Request:

FieldTypeDescription
projectstringProject name to filter by; empty string for all projects

Response (stream):

FieldTypeDescription
event_idstringEvent identifier
event_typestringEvent type name
projectstringTarget project
payload_jsonstringEvent payload as JSON

RegistryAdd(RegistryAddRequest) → RegistryAddResponse

Add a project to the daemon's in-memory registry and persist the change to registry.json. The daemon is the single source of truth for registry state.

Request:

FieldTypeDescription
namestringUnique project name
pathstringAbsolute path on the local filesystem
stackstringTechnology stack: rust, python, typescript, elixir, cpp, swift, kotlin
agentstringAI agent name
repostringGitHub repo slug (owner/repo)
branchstringDefault branch (empty → main)
iterateboolEnable iterate action
maintainboolEnable maintain action
pushboolEnable push action
auditboolEnable audit action
releaseboolEnable release action
install_commandstringShell command for local install (mutually exclusive with install_brew)
install_brewstringHomebrew formula for local install (mutually exclusive with install_command)
notesstringHuman-readable notes (empty → none)
timeout_secsuint64Per-project timeout (0 → use default 3600 s)
update_policystringpatch, minor or major (empty → not set; maintenance behaves as minor)
installs_skillstringtrue, false, or a skill-install command (empty → not set; blank → INVALID_ARGUMENT)

Response:

FieldTypeDescription
projectProjectThe newly created project entry

Errors: ALREADY_EXISTS if the name is already in the registry; INVALID_ARGUMENT for an unknown stack, an unknown update policy, or conflicting install fields; INTERNAL with the stable message failed to persist registry state when saving fails.

CLI: foundry registry add ... routes through this RPC by default and therefore requires a reachable daemon. Pass --offline to bypass the daemon and mutate the registry file directly. Without --offline, an unreachable daemon returns an error and leaves the client-side registry file unchanged. The online path mutates daemon-owned state only and does not create FOUNDRY_REGISTRY_PATH. If daemon persistence fails, the RPC returns INTERNAL with a stable failed to persist registry state message and leaves both the daemon's in-memory registry and its on-disk registry bytes unchanged.

RegistryList(RegistryListRequest) → RegistryListResponse

List the daemon-owned registry inventory from the in-memory state held by foundryd. The online CLI path renders this response directly and does not read FOUNDRY_REGISTRY_PATH.

Request:

This message has no fields.

Response:

FieldTypeDescription
projectsrepeated ProjectEvery project currently loaded in the daemon-owned registry

Each Project carries the full registry data required by online clients: name, path, stack, agent, repo, branch, skip, action flags, install config, notes, timeout, installs-skill state, audit exceptions, and update_policy (empty when not set).

Errors: None at the RPC layer; the daemon answers from already-loaded registry state.

CLI: foundry registry list routes through this RPC by default and therefore requires a reachable daemon. Pass --offline to read the registry file directly. Without --offline, an unreachable daemon returns an error and leaves the client-side registry file unchanged. The online path renders the RPC response directly and does not create FOUNDRY_REGISTRY_PATH; if that path is absent, the online path leaves it absent. foundry registry init is not part of this RPC surface and remains an offline-only recovery command.

RegistryShow(RegistryShowRequest) → RegistryShowResponse

Retrieve one exact-name project from the daemon-owned registry state. The match is exact and does not perform prefix or substring lookup.

Request:

FieldTypeDescription
namestringExact project name to retrieve

Response:

FieldTypeDescription
projectProjectThe full daemon-owned project record for that exact name

Errors: NOT_FOUND if no exact-name project exists.

CLI: foundry registry show <name> routes through this RPC by default and therefore requires a reachable daemon. Pass --offline to read the registry file directly. Without --offline, an unreachable daemon returns an error and leaves the client-side registry file unchanged. The online path renders the RPC response directly and does not create FOUNDRY_REGISTRY_PATH; if that path is absent, the online path leaves it absent.

RegistryRemove(RegistryRemoveRequest) → RegistryRemoveResponse

Remove a project from the registry by name.

Request:

FieldTypeDescription
namestringProject name to remove

Errors: NOT_FOUND if no project with that name exists; INTERNAL with the stable message failed to persist registry state when saving fails.

CLI: foundry registry remove <name> routes through this RPC by default and therefore requires a reachable daemon. Pass --offline to bypass the daemon and mutate the registry file directly. Without --offline, an unreachable daemon returns an error and leaves the client-side registry file unchanged. The online path mutates daemon-owned state only and does not create FOUNDRY_REGISTRY_PATH. If daemon persistence fails, the RPC returns INTERNAL with a stable failed to persist registry state message and leaves both the daemon's in-memory registry and its on-disk registry bytes unchanged.

RegistryEdit(RegistryEditRequest) → RegistryEditResponse

Apply partial edits to an existing project. Only fields that are non-empty / non-zero are applied. Use clear_* booleans to explicitly clear optional fields (e.g. clear_skip = true to un-skip a project).

Request:

FieldTypeDescription
namestringProject to edit (required)
pathstringNew path (empty → no change)
stackstringNew stack (empty → no change)
agentstringNew agent (empty → no change)
repostringNew repo slug (empty → no change)
branchstringNew branch (empty → no change)
skipstringNew skip reason; non-empty sets it; empty → no change unless clear_skip
clear_skipboolRemove the skip flag
iterateboolSet iterate to true (use clear_iterate to set false)
clear_iterateboolSet iterate to false
maintainboolSet maintain to true
clear_maintainboolSet maintain to false
pushboolSet push to true
clear_pushboolSet push to false
auditboolSet audit to true
clear_auditboolSet audit to false
releaseboolSet release to true
clear_releaseboolSet release to false
install_commandstringSet a shell-command install
install_brewstringSet a Homebrew formula install
clear_installboolRemove the install config
notesstringSet notes (empty string + clear_notes = false → no change)
clear_notesboolRemove notes
timeout_secsuint64Set timeout (0 → no change unless clear_timeout)
clear_timeoutboolRevert timeout to the daemon default
update_policystringSet patch, minor or major (empty → no change)
installs_skillstringSet true, false, or a skill-install command (empty → no change; blank → INVALID_ARGUMENT)

Response:

FieldTypeDescription
projectProjectThe updated project entry

Errors: NOT_FOUND; INVALID_ARGUMENT for conflicting install fields or unknown stack; INTERNAL with the stable message failed to persist registry state when saving fails.

CLI: foundry registry edit <name> ... routes through this RPC by default and therefore requires a reachable daemon. Pass --offline to bypass the daemon and mutate the registry file directly. Without --offline, an unreachable daemon returns an error and leaves the client-side registry file unchanged. The online path mutates daemon-owned state only and does not create FOUNDRY_REGISTRY_PATH. If daemon persistence fails, the RPC returns INTERNAL with a stable failed to persist registry state message and leaves both the daemon's in-memory registry and its on-disk registry bytes unchanged.

SentinelList(SentinelListRequest) → SentinelListResponse

List every daemon-owned sentinel from the in-memory scheduler control-plane state held by foundryd.

Request:

This message has no fields.

Response:

FieldTypeDescription
sentinelsrepeated SentinelEvery daemon-owned sentinel entry in scheduler evaluation order

Each Sentinel carries the exact scheduler contract the CLI needs to render: name, cron, emit_event_type, emit_project, emit_throttle, emit_payload_json, and enabled.

Errors: None at the RPC layer; the daemon answers from already-loaded sentinel state.

CLI: foundry sentinel list routes through this RPC by default and therefore requires a reachable daemon. Pass --offline to read the sentinel file directly. Without --offline, an unreachable daemon returns an error and leaves the client-side sentinel file unchanged. The online path renders the RPC response directly and does not create FOUNDRY_SENTINELS_PATH; if that path is absent, the online path leaves it absent.

SentinelShow(SentinelShowRequest) → SentinelShowResponse

Retrieve one exact-name sentinel from the daemon-owned scheduler control-plane state. The name match is exact and does not perform prefix or substring lookup.

Request:

FieldTypeDescription
namestringExact sentinel name to retrieve

Response:

FieldTypeDescription
sentinelSentinelThe full daemon-owned sentinel record for that name

Errors: NOT_FOUND if no exact-name sentinel exists.

CLI: foundry sentinel show <name> routes through this RPC by default and therefore requires a reachable daemon. Pass --offline to read the sentinel file directly. Without --offline, an unreachable daemon returns an error and leaves the client-side sentinel file unchanged. The online path renders the RPC response directly and does not create FOUNDRY_SENTINELS_PATH; if that path is absent, the online path leaves it absent.

SentinelEnable(SentinelEnableRequest) → SentinelEnableResponse

Mark one daemon-owned sentinel as enabled, persist the updated sentinel store, and wake the in-process scheduler so the next firing is recomputed immediately.

Request:

FieldTypeDescription
namestringExact sentinel name to enable

Response:

FieldTypeDescription
sentinelSentinelThe committed daemon-owned sentinel record after enable

Errors: NOT_FOUND if no exact-name sentinel exists; INTERNAL if the daemon cannot persist the sentinel store.

CLI: foundry sentinel enable <name> routes through this RPC by default and therefore requires a reachable daemon. Pass --offline to bypass the daemon and mutate the sentinel file directly. Without --offline, an unreachable daemon returns an error and leaves the client-side sentinel file unchanged. The online path mutates daemon-owned state only and does not create FOUNDRY_SENTINELS_PATH. If daemon persistence fails, the RPC returns INTERNAL, leaves the daemon-owned in-memory sentinel store unchanged, does not wake the scheduler, and leaves the on-disk sentinel bytes unchanged.

SentinelDisable(SentinelDisableRequest) → SentinelDisableResponse

Mark one daemon-owned sentinel as disabled, persist the updated sentinel store, and wake the in-process scheduler so any pending firing is cancelled immediately.

Request:

FieldTypeDescription
namestringExact sentinel name to disable

Response:

FieldTypeDescription
sentinelSentinelThe committed daemon-owned sentinel record after disable

Errors: NOT_FOUND if no exact-name sentinel exists; INTERNAL if the daemon cannot persist the sentinel store.

CLI: foundry sentinel disable <name> routes through this RPC by default and therefore requires a reachable daemon. Pass --offline to bypass the daemon and mutate the sentinel file directly. Without --offline, an unreachable daemon returns an error and leaves the client-side sentinel file unchanged. The online path mutates daemon-owned state only and does not create FOUNDRY_SENTINELS_PATH. If daemon persistence fails, the RPC returns INTERNAL, leaves the daemon-owned in-memory sentinel store unchanged, does not wake the scheduler, and leaves the on-disk sentinel bytes unchanged.

AddCampaign(AddCampaignRequest) → AddCampaignResponse

Add one campaign definition to the daemon-owned campaign store. The daemon parses the JSON definition, validates the referenced project and context paths against daemon-owned registry state, acquires the exclusive campaign-store lock, and persists the new definition atomically.

Request:

FieldTypeDescription
definition_jsonstringFull campaign definition JSON exactly as accepted by foundry campaign add

Response:

FieldTypeDescription
campaignCampaignDetailFull durable definition that was persisted

Errors: INVALID_ARGUMENT when the JSON is invalid or the definition is structurally invalid; FAILED_PRECONDITION when the definition references an unknown registered project or invalid context artifact; ALREADY_EXISTS when a campaign with the same name already exists; INTERNAL when the store cannot be saved. On INTERNAL, the daemon leaves the on-disk campaign store unchanged.

CLI: foundry campaign add <definition.json> routes through this RPC by default. The online CLI renders the returned CampaignDetail directly and does not re-read FOUNDRY_CAMPAIGNS_PATH. Pass --offline only when you intentionally need direct-file recovery while the daemon is stopped.

ListCampaigns(ListCampaignsRequest) → ListCampaignsResponse

List the durable campaign inventory from the daemon's configured campaign store. This is a read-only query: it loads the store at request time, returns records in deterministic campaign-name order, and exposes summary/status fields only.

Request:

FieldTypeDescription
projectstringOptional exact project-name filter; empty string returns all campaigns

Response:

FieldTypeDescription
campaignsrepeated CampaignDurable inventory records sorted by campaign name

An escalated campaign carries open_escalation, read the same way as GetCampaign reads it.

Errors: FAILED_PRECONDITION when the campaign store is malformed; INTERNAL when the campaign store is unreadable, or when an escalated campaign needs the event-log fallback and the event log cannot be read. Missing or empty stores return an empty list.

PauseCampaign(PauseCampaignRequest) → PauseCampaignResponse

Pause a campaign. The operation is idempotent on the status field — pausing an already-paused campaign is not an error. The daemon holds an exclusive lock on the campaign store for the duration of the write, so a concurrent advance formation cannot interleave with a pause.

Any pending_run_result that was recorded before the pause is explicitly preserved: the operation never clears or overwrites it. The result remains available for the next manual advance after a subsequent resume.

Request:

FieldTypeDescription
namestringExact campaign name to pause

Response:

FieldTypeDescription
campaignCampaignDetailFull campaign detail reflecting the state after the pause is applied

Errors: NOT_FOUND when no campaign with the given name exists; FAILED_PRECONDITION when the campaign store is malformed; INTERNAL when the campaign store is unreadable or when persistence fails. On save failure, the daemon leaves the persisted campaign store unchanged.

CLI: foundry campaign pause <name> routes through this RPC when the daemon is reachable. Pass --offline to bypass the daemon and mutate the store file directly (useful when foundryd is not running). Without --offline, an unreachable daemon is an error and the client-side campaigns path is left untouched.

ResumeCampaign(ResumeCampaignRequest) → ResumeCampaignResponse

Resume a paused or escalated campaign, optionally extending its cycle budget. The daemon holds an exclusive lock on the campaign store for the duration of the write.

pending_run_result is explicitly preserved: the operation never clears or overwrites it. The result remains available for the next manual advance after resume.

Accepted statuses: paused and escalated. Budget-only escalations (where the engine stopped because the cycle limit was reached) may be resumed with this RPC without recording an owner-decision entry. Use DecideCampaign when the escalation contains a human judgment question that requires a policy record.

Exhausted budget guard: when add_cycles == 0 and cycles_completed >= max_cycles, the RPC returns FAILED_PRECONDITION. Pass a positive add_cycles to explicitly authorize more work; the engine never silently reactivates an exhausted campaign.

Request:

FieldTypeDescription
namestringExact campaign name to resume
add_cyclesuint64Additional cycles to add to max_cycles before resuming (0 = no extension)

Response:

FieldTypeDescription
campaignCampaignDetailFull campaign detail reflecting the state after the resume is applied

Errors: NOT_FOUND when no campaign with the given name exists; FAILED_PRECONDITION when the campaign is not paused or escalated, lacks authorized_by, the budget is exhausted and add_cycles == 0, add_cycles would overflow max_cycles, or the campaign store is malformed; INTERNAL when the campaign store is unreadable or persistence fails. On save failure, the daemon leaves the persisted campaign store unchanged.

CLI: foundry campaign resume <name> routes through this RPC when the daemon is reachable. Pass --offline to bypass the daemon and mutate the store file directly (useful when foundryd is not running). Without --offline, an unreachable daemon is an error and the client-side campaigns path is left untouched. The rendered output is built from the ResumeCampaignResponse.campaign detail — the CLI never re-reads the store file on the online path.

DecideCampaign(DecideCampaignRequest) → DecideCampaignResponse

Record a binding owner decision on an active, paused or escalated campaign. The daemon holds the exclusive campaign-store lock for the full mutation and appends one durable owner decision record. Every later formation prompt carries it as binding policy.

The persisted owner decision carries the decision text, the campaign's current authorized_by identity, and the daemon timestamp. Existing counters and any stored pending_run_result are preserved. No event is emitted; the response carries the resulting status.

Active or paused: the decision is recorded and nothing else changes. The status, max_cycles and open_escalation stay as they were. A spent budget does not block it. A positive add_cycles returns FAILED_PRECONDITION and records nothing: budget extension through this RPC answers an escalation, and a paused campaign takes more cycles through ResumeCampaign. A decision recorded while the campaign forms makes that formation form again from the changed record.

Escalated: the decision answers the escalation. The daemon adds add_cycles to max_cycles (checked addition), clears open_escalation and sets active, all in one transition. When cycles_completed >= max_cycles, the request must carry a positive add_cycles; with add_cycles == 0 the RPC returns FAILED_PRECONDITION and records nothing, because an active campaign with no cycle left would escalate on budget at its next advance, and ResumeCampaign does not accept an active campaign.

Retries: on an active or paused campaign, a decision whose trimmed text and owner equal the latest recorded decision returns FAILED_PRECONDITION ("campaign '' already records this owner decision as its latest; nothing to apply") and records nothing. A successful escalated decide leaves the campaign active, so its retry meets this check before the add_cycles check: the decision is not recorded twice and add_cycles is not applied twice. An escalated campaign accepts the same words, because it stopped on a new question after that decision.

Request:

FieldTypeDescription
namestringExact campaign name to update
decisionstringNon-empty owner decision text to record
add_cyclesuint64Cycles to add to max_cycles with the decision (0 = none; escalated campaigns only; required on a spent escalated budget)

Response:

FieldTypeDescription
campaignCampaignDetailFull campaign detail reflecting the state after the decision is applied

Errors: NOT_FOUND when no campaign with the given name exists; INVALID_ARGUMENT when decision is empty after trimming; FAILED_PRECONDITION when the campaign is staged, completed or cancelled, lacks authorized_by, repeats its latest decision, is active or paused and add_cycles > 0, is escalated with a spent budget and add_cycles == 0, add_cycles would overflow max_cycles, or the campaign store is malformed; INTERNAL when the campaign store is unreadable or persistence fails. On save failure, the daemon leaves the persisted campaign store unchanged.

CLI: foundry campaign decide <name> --decision "<text>" [--add-cycles <n>] routes through this RPC by default and therefore requires a reachable daemon. Pass --offline to bypass the daemon and mutate the store file directly. Without --offline, an unreachable daemon returns an error and leaves the client-side campaigns path unchanged.

CompleteCampaign(CompleteCampaignRequest) → CompleteCampaignResponse

Mark an authorized campaign complete from outside the formation loop. The request requires a non-empty reason and an existing authorized_by owner. The daemon stores the reason as an append-only owner record, clears any pending run result, changes the status to completed, and emits the normal CampaignCompleted event for terminal observers. Calling it on an already completed campaign is idempotent.

Request:

FieldTypeDescription
namestringExact campaign name to complete
reasonstringEvidence-backed owner reason for external completion

Response:

FieldTypeDescription
campaignCampaignDetailFull campaign detail reflecting the state after completion is applied

Errors: INVALID_ARGUMENT for a blank reason, NOT_FOUND for an unknown campaign, and FAILED_PRECONDITION when the campaign has no authorizing owner or the campaign store is malformed; INTERNAL when the campaign store is unreadable or persistence fails. On save failure, the daemon leaves the persisted campaign store unchanged and does not emit CampaignCompleted.

CLI: foundry campaign complete <name> --reason "<text>" routes through this RPC by default. The online CLI renders the returned CampaignDetail directly, never re-reads FOUNDRY_CAMPAIGNS_PATH, and leaves the client-side path untouched if the daemon is unreachable. Pass --offline only for direct-file recovery while the daemon is stopped.

CancelCampaign(CancelCampaignRequest) → CancelCampaignResponse

Stop a campaign permanently before its done evidence was met. Distinct from CompleteCampaign because completed is an evidence claim and cancellation records the opposite. The daemon changes the status to cancelled, clears any pending run result, stores the reason as an append-only owner record when the campaign has an owner, and emits CampaignCancelled for terminal observers.

Unlike CompleteCampaign, this does not require authorized_by — an unauthorized campaign has no other reachable terminal state.

With terminate_now, the daemon aborts the in-flight workflow before touching the store. Aborting it kills the running agent process. A formation holds the campaign store lock only in its short read and write sections, never while its agent runs; if the abort lands inside one, dropping the task releases the lock. The abort is awaited, so the subsequent store acquisition does not wait on the aborted task. discard_work then selects how the orphaned worktree is disposed of by the DisposeCampaignWork block.

Request:

FieldTypeDescription
namestringExact campaign name to cancel
reasonstringOwner reason for abandoning the mission
terminate_nowboolAbort the in-flight workflow, killing the running agent process
discard_workboolThrow the terminated cycle's uncommitted work away

Response:

FieldTypeDescription
campaignCampaignDetailFull campaign detail reflecting the state after cancellation
event_idstringThe emitted CampaignCancelled event; empty when the call was a no-op

Errors: INVALID_ARGUMENT for a blank reason, NOT_FOUND for an unknown campaign, FAILED_PRECONDITION when the campaign is already completed or the store is malformed, and INTERNAL when the store is unreadable or persistence fails. Cancelling an already-cancelled campaign is idempotent: it returns the current detail with an empty event_id and emits no second event.

CLI: foundry campaign cancel <name> --reason "<text>" [--now] [--discard-work] routes through this RPC by default. --discard-work requires --now. --offline uses a graceful-only direct-file path that emits no event; --offline --now is refused rather than downgraded.

AdvanceCampaign(AdvanceCampaignRequest) → AdvanceCampaignResponse

Dispatch one manual campaign-advance workflow for an active or staged campaign. Under the exclusive campaign-store lock the daemon validates the current status, checks for campaign work already in flight, emits CampaignAdvanceRequested through spawn_workflow (which records the workflow in the tracker before it returns), then releases the lock and returns. A formation never holds that lock while its agent runs, so the call returns promptly whatever other campaigns are doing.

An advance never duplicates work in flight. When the campaign already has, in the work-item ledger, an item submitted, queued, held or running (or still holding its slot) that is one of its cycles (source campaign:<name>) or an advance of it (a CampaignAdvanceRequested root naming it), or a tracked advance or cycle workflow not yet admitted to the ledger, the daemon dispatches nothing and returns in_flight instead of an event_id. Because the check and the dispatch happen under the same lock, two simultaneous advances for one campaign dispatch exactly one workflow. The automatic advance after a cycle completes is not an RPC and is unaffected.

Request:

FieldTypeDescription
namestringExact campaign name to advance

Response:

FieldTypeDescription
campaignCampaignDetailCurrent campaign detail at dispatch time
event_idstringRoot event ID of the dispatched CampaignAdvanceRequested workflow; empty with in_flight
in_flightCampaignWorkInFlightSet when the request was a no-op because campaign work was already in flight

CampaignWorkInFlight carries kind (advance or cycle), work_item_id (empty for an accepted advance not yet in the ledger), state (submitted, queued, held or running) and the root event_id.

Errors: NOT_FOUND when the name is absent; FAILED_PRECONDITION when the campaign is paused, escalated, completed or cancelled, or the work-item ledger is malformed; INTERNAL when the campaign store or the ledger is unreadable. Rejected advances and no-op advances do not mutate either store.

CLI: foundry campaign advance <name> routes through this RPC by default. The online CLI prints the returned root event_id, watches the daemon-owned workflow stream for that advance's own trace only, stops at its campaign_advance_completed (a decision, or stood_down when the campaign was stopped first; or its work item's settlement when the advance failed without one), and renders the daemon trace for that event. For a no-op it prints one line naming the in-flight work and the command to inspect it, and exits 0 without watching. It does not re-read FOUNDRY_CAMPAIGNS_PATH.

GetCampaign(GetCampaignRequest) → GetCampaignResponse

Retrieve the complete durable definition of one campaign by exact name. Unlike ListCampaigns, this RPC returns the full detail record including intent_refs, context_paths, done_evidence (with the Gate/Review type distinction preserved), escalation rules, and all runtime status fields.

Request:

FieldTypeDescription
namestringExact campaign name to look up

Response:

FieldTypeDescription
campaignCampaignDetailFull durable definition of the campaign

For an escalated campaign, open_escalation says why it waits on its owner. The daemon reads it from the campaign record, which formation writes when it escalates the campaign. A campaign that escalated before the record existed has none: the daemon then recovers the reason from the newest campaign_escalated event naming the campaign (every monthly file under FOUNDRY_EVENTS_DIR) and sets from_event_log. A recovered record is never written back to the store.

Errors: NOT_FOUND when no campaign with the given name exists in the store (even when other campaigns are present); FAILED_PRECONDITION when the campaign store is malformed; INTERNAL when the campaign store is unreadable, or when the event-log fallback cannot read the event log.

CLI: foundry campaign show <name> routes through this RPC by default and renders the returned CampaignDetail directly, without re-reading FOUNDRY_CAMPAIGNS_PATH.

ListWorkItems(ListWorkItemsRequest) → ListWorkItemsResponse

List records from the daemon-owned work-item ledger (FOUNDRY_WORK_ITEMS_PATH). The store is loaded from disk on every call and nothing is cached between requests, so a caller always sees the ledger as it stands. This RPC never writes and never takes the ledger write gate.

Request:

FieldTypeDescription
projectstringExact-match project filter. Empty means every project; alpha does not match alpha-2
statestringExact-match state filter, given as the item state's serialized tag. Empty means every state

Response:

FieldTypeDescription
itemsrepeated WorkItemThe selected records, in order

Ordering is deterministic and independent of the store's contents order and insertion order:

  1. running items, by started_at ascending
  2. submitted, queued and held items, by submitted_at ascending
  3. Open items (preserved, needs_decision, failed), by settled_at descending
  4. Terminal items (landed, cancelled), by settled_at descending

Ties within a group break by id ascending. A state filter returns the matching items in the same relative order as the unfiltered list.

Errors: INVALID_ARGUMENT when state is a non-empty tag that is not one of submitted, queued, held, running, landed, preserved, needs_decision, failed, cancelled; FAILED_PRECONDITION when the ledger contains malformed JSON; INTERNAL when the ledger path is unreadable. A missing ledger file, or one holding {"version":1,"items":[]}, is an empty list rather than an error.

CLI: online callers render this response directly and never read the client-side ledger file; if FOUNDRY_WORK_ITEMS_PATH is absent, the online path leaves it absent.

GetWorkItem(GetWorkItemRequest) → GetWorkItemResponse

Retrieve one work-item ledger record by exact id. The store is loaded from disk on every call and nothing is cached between requests. This RPC never writes and never takes the ledger write gate.

Request:

FieldTypeDescription
idstringExact work-item id to look up

Response:

FieldTypeDescription
itemWorkItemThe full durable ledger record

Errors: NOT_FOUND when no item with that id exists, including when the ledger holds other items and when the ledger file is missing or empty; FAILED_PRECONDITION when the ledger contains malformed JSON; INTERNAL when the ledger path is unreadable.

CLI: online callers render this response directly and never read the client-side ledger file; if FOUNDRY_WORK_ITEMS_PATH is absent, the online path leaves it absent.

CloseWorkItem and CancelWorkItem

Both return an item: WorkItem after owner-directed settlement to cancelled. CloseWorkItemRequest carries id, a nonblank reason, and nonblank operator_origin; it accepts only preserved, needs_decision, failed. CancelWorkItemRequest carries id and nonblank operator_origin; it accepts only submitted, queued, held and uses cancelled by operator as the reason.

Validation returns INVALID_ARGUMENT, unknown exact ids NOT_FOUND, refused states and malformed ledgers FAILED_PRECONDITION, and I/O faults INTERNAL. The daemon reloads and atomically replaces the ledger under the shared write gate before emitting work_item_cancelled on Watch and in the durable event log. Failed saves emit nothing. Neither RPC dispatches, aborts a workflow nor disposes of work. The original identity, origin, objective, trace and disposition remain intact. operator_action records command, operator origin, previous state, previous reason and optional previous settlement timestamp. Earlier lifecycle events remain unchanged. CLI mutations reject --offline.

HoldWorkItem and ReleaseWorkItem

Both carry id and nonblank operator_origin and return an item: WorkItem. HoldWorkItem accepts only a queued item and moves it to held with the reason held by operator; the scheduler ignores it until released. ReleaseWorkItem accepts a held item, or one in needs_decision because a dependency settled some way other than landed, and returns it to queued: the settled dependencies are dropped from depends_on, and the recorded reason is the scheduler's own verdict for the item as it now stands. Validation, error codes, write-gate discipline and operator_action follow CloseWorkItem; the emitted event is work_item_held or work_item_released, on Watch and in the durable log, after the ledger is saved. Neither dispatches or aborts work.

GetPacing(GetPacingRequest) → GetPacingResponse

Return the pacing stage as it stands: the host running cap, how many items run, the paused lanes, every running item and every waiting item with its reason. Loaded from the ledger and the pacing files on every call; never writes and never takes the ledger write gate. Running items are listed oldest start first; waiting items in the scheduler's priority order, then held items.

Response: pacing: PacingStatus.

PausePacing and ResumePacing

Both carry lanes (lane tags interactive, campaign, maintenance, or all; empty means every lane) and nonblank operator_origin, and return pacing: PacingStatus after the change. PausePacing stops new starts in the named lanes; running items are untouched. ResumePacing allows starts again and the next scheduler tick starts the next eligible item. The pause state is saved to FOUNDRY_PACING_STATE_PATH through an atomic rename and is in force after a restart. An unknown lane is INVALID_ARGUMENT; a failed save is INTERNAL and emits nothing. Each emits pacing_paused or pacing_resumed for the project system, durable and on Watch, even when the named lanes were already in that state.

ListWorkItemEvents(ListWorkItemEventsRequest) → ListWorkItemEventsResponse

List one work item's work_item_* events (work_item_submitted, work_item_started, work_item_settled, work_item_cancelled, work_item_held, work_item_released) from the durable event log under FOUNDRY_EVENTS_DIR (one YYYY-MM.jsonl file per month). Additive to GetWorkItem: a caller that never sends this request sees exactly the GetWorkItem and ListWorkItems behaviour it saw before. The ledger and the event log are loaded from disk on every call and nothing is cached between requests. This RPC never writes and never takes the ledger write gate.

Request:

FieldTypeDescription
idstringExact work-item id whose events to list

Response:

FieldTypeDescription
eventsrepeated WorkItemEventThe item's events, in chronological order

Selection and order: events are selected by exact payload item_id — never by trace id and never by project, since the items of one maintenance run share both — from every monthly file regardless of age, and returned by occurred_at ascending, ties in log order (file name, then line).

Errors: NOT_FOUND when no item with that id is in the ledger; FAILED_PRECONDITION when the ledger contains malformed JSON; INTERNAL when the ledger path is unreadable, or when an existing events directory or log file cannot be read — never an empty list. A known item with no events, or a missing events directory, is an empty list. A malformed line in the log (not JSON, or two events glued onto one line) is skipped with a warning in the daemon log, and the item's well-formed events on other lines are still returned.

CLI: foundry queue show <id> renders this after GetWorkItem. Online callers never read the client-side ledger or events files; if FOUNDRY_WORK_ITEMS_PATH or FOUNDRY_EVENTS_DIR is absent, the online path leaves it absent.

History(HistoryRequest) → HistoryResponse

List durable trace history from the daemon-owned trace store.

Request:

FieldTypeDescription
datestringExact day in YYYY-MM-DD format, or empty to request recent history
projectstringExact project filter, or empty for all projects
recent_daysuint32Number of recent days to return when date is empty; online CLI uses 7

Response:

FieldTypeDescription
daysrepeated HistoryDayMatching days, newest day first

Each HistoryDay carries a date plus repeated HistoryTrace. HistoryTrace contains event_id, event_type, project, success, total_duration_ms, and trace_id.

Completed traces are persisted to disk under ~/.foundry/traces/YYYY-MM-DD/ and survive daemon restarts. The daemon reads that store directly when serving history, preserving durable retention semantics even after in-memory cache entries expire.

CLI: foundry history routes through this RPC by default, renders the daemon response directly, and does not read or create a client-side FOUNDRY_TRACES_DIR unless --offline is explicit.

Trace(TraceRequest) → TraceResponse

Retrieve the trace of a completed event chain. Returns all events produced during processing and a record of each block execution.

Request:

FieldTypeDescription
event_idstringRoot event ID to look up

Response:

FieldTypeDescription
foundboolWhether a trace was found for the given event ID
eventsrepeated TraceEventAll events in the chain
block_executionsrepeated TraceBlockExecutionRecord of each block execution

Completed traces are persisted to disk under ~/.foundry/traces/YYYY-MM-DD/ and survive daemon restarts. The Trace RPC checks the in-memory store first (for recently completed chains) and falls back to disk for older traces.

CLI: foundry trace <event-id> routes through this RPC by default. When no trace is found, the CLI prints No trace found for <event-id> (expired or unknown).

Span(SpanRequest) → SpanResponse

Retrieve every event and block execution that belongs to one span.

Request:

FieldTypeDescription
span_idstringExact span ID to look up

Response:

FieldTypeDescription
foundboolWhether a span was found for the given span ID
eventsrepeated TraceEventEvents whose span_id matches the requested span
block_executionsrepeated TraceBlockExecutionBlocks whose own span or parent span matches
trace_idstringOwning trace ID, including for block-only spans
total_duration_msuint64Sum of returned block durations

Span is an in-memory lookup keyed by the daemon's span index. It is intended for live drill-down and status filtering rather than durable offline browsing. If the span is unknown, the response is found = false with empty collections and an empty trace_id.

Messages

WorkflowStatus

FieldTypeDescription
workflow_idstringWorkflow identifier
workflow_typestringWorkflow type name
projectstringTarget project
statestringpending, running, completed, failed
started_atstringISO 8601 timestamp
completed_atstringISO 8601 timestamp (empty if running)
task_blocksrepeated TaskBlockStatusPer-block status

TaskBlockStatus

FieldTypeDescription
namestringBlock name
statestringpending, running, completed, skipped, failed
started_atstringISO 8601 timestamp
completed_atstringISO 8601 timestamp
throttledboolTrue if emission was suppressed by throttle

TraceEvent

FieldTypeDescription
event_idstringDeterministic event identifier
event_typestringEvent type name
projectstringTarget project
occurred_atstringISO 8601 timestamp
throttleThrottle enumThrottle level for this event

WorkItem

One durable work-item ledger record, as returned by ListWorkItems and GetWorkItem. The fields only a settled item has are proto3 optional, so a caller can tell "not recorded" from a recorded empty string or false.

FieldTypeDescription
idstringStable identity minted at submission (wi_ plus 24 hex characters)
projectstringRegistry project name
objectivestringThe task description or campaign objective the work serves
kindstringtask, campaign_cycle, maintenance, major_upgrade, release, or remediation
lanestringinteractive, campaign, or maintenance
originstringOpaque submitter text; Foundry never interprets it
submitted_atstringISO 8601 timestamp the item entered the ledger
started_atoptional stringISO 8601 timestamp an agent started on it; absent when it never started
settled_atoptional stringISO 8601 timestamp it settled; absent while unsettled
statestringsubmitted, queued, held, running, landed, preserved, needs_decision, failed, or cancelled
reasonstringWhy it is in that state, in one line; for a queued item, the scheduler's current verdict
trace_idoptional stringThe workflow trace the item belongs to; absent when it carries none
verdictoptional stringSettlement: the reviewer's typed verdict tag
landed_commitoptional stringSettlement: the trunk commit the work landed as; absent unless it landed
preservation_refoptional stringSettlement: the durable ref (branch or bundle:<path>) holding unlanded work
worktreeoptional stringSettlement: the isolated worktree the work ran in
worktree_removedoptional boolSettlement: whether that worktree was gone by settlement time; absent when the item records no worktree
depends_onrepeated stringPacing: the items this one waits on; it starts only once every one has settled landed. Empty when none
not_beforeoptional stringPacing: the earliest time the scheduler may start it (ISO 8601); absent when unset

WorkItem.operator_action is an optional WorkItemOperatorAction message with command (close, cancel, resume, hold or release), origin, previous_state, previous_reason and optional previous_settled_at. Absent on items without an owner action.

ResumeWorkItemRequest also carries additive depends_on (repeated string; each must be in the ledger) and not_before (ISO 8601 string; empty means at once).

PacingStatus and PacingItem

FieldTypeDescription
max_runninguint64The host running cap from pacing.json (default 2)
runninguint64How many items run on this host now
paused_lanesrepeated stringThe paused lanes, as lane tags, in lane order
running_itemsrepeated PacingItemEvery running item, oldest start first
waiting_itemsrepeated PacingItemQueued items in the scheduler's priority order, then held

A PacingItem carries id, project, repository (the key the per-repository rule uses), kind, lane, state, reason and since (started_at for a running item, submitted_at for a waiting one).

WorkItemEvent

One work_item_* lifecycle event read back from the durable event log, as returned by ListWorkItemEvents.

FieldTypeDescription
idstringThe event's own id
event_typestringwork_item_submitted, work_item_started, work_item_settled, work_item_cancelled, work_item_held, or work_item_released
occurred_atstringISO 8601 timestamp the event occurred
statestringThe item's state as of this event (same vocabulary as WorkItem.state)
reasonstringWhy it was in that state, in one line
trace_idoptional stringThe workflow trace the event was emitted on; absent when it carries none

Campaign

Summary-only wire form returned by ListCampaigns. Intentionally omits intent_refs, context_paths, done_evidence, and escalation — use GetCampaign to retrieve those fields.

FieldTypeDescription
namestringCampaign name
projectstringRegistered project name
missionstringCampaign mission statement
statusstringDurable status: staged, active, paused, escalated, or completed
cycles_completeduint64Number of dispatched task cycles
cycles_landeduint64Number of task results whose work reached trunk
max_cyclesuint64Configured campaign cycle budget
authorized_bystringOwner authorization identity, or empty when absent
agent_providerstringPreferred agent provider, or empty when absent
last_run_event_idstringMost recent campaign-run event ID, or empty when absent
open_escalationCampaignEscalationWhy the campaign waits on its owner; set only while escalated, absent when no reason was ever recorded

CampaignEscalation

Why an escalated campaign waits on its owner.

FieldTypeDescription
reasonstringThe reason formation recorded, as the campaign_escalated event carries it
escalated_atstringRFC 3339 time of the escalation
cycles_completeduint64Dispatched cycles at the escalation
cycles_landeduint64Cycles whose work had landed at the escalation
questionoptional stringThe task agent's question, when a task stopped with blocked_on_decision
choicesrepeated stringThe choices the task agent offered with its question
from_event_logboolThe record was recovered from the event log; the escalation predates the record

CampaignDetail

Full durable definition of a campaign, as returned by GetCampaign. Carries all fields in Campaign plus the definition-time fields that the summary form omits.

FieldTypeDescription
namestringCampaign name
projectstringRegistered project name
missionstringCampaign mission statement
statusstringDurable status: staged, active, paused, escalated, or completed
cycles_completeduint64Number of dispatched task cycles
cycles_landeduint64Number of task results whose work reached trunk
max_cyclesuint64Configured campaign cycle budget
authorized_bystringOwner authorization identity, or empty when absent
agent_providerstringPreferred agent provider, or empty when absent
last_run_event_idstringMost recent campaign-run event ID, or empty when absent
intent_refsrepeated stringIntent reference labels that anchor the mission
context_pathsrepeated stringPaths to context documents for the campaign
done_evidencerepeated DoneEvidenceCompletion criteria with Gate/Review type distinction
escalationrepeated stringHuman-readable escalation instructions
owner_decisionsrepeated OwnerDecisionAppend-only owner policy decisions recorded after escalations
open_escalationCampaignEscalationWhy the campaign waits on its owner; see Campaign.open_escalation

OwnerDecision

One recorded owner decision attached to a campaign after an escalation. These records are append-only and are threaded back into future campaign formation prompts as binding context.

FieldTypeDescription
decisionstringOwner-authored decision text
authorized_bystringOwner identity copied from the campaign at record time
decided_atstringRFC 3339 timestamp recorded by the daemon

DoneEvidence

One completion-evidence item. The kind field distinguishes the two variants.

Gate variant (kind = "gate"):

FieldTypeDescription
kindstring"gate"
commandstringShell command to evaluate
requiredboolWhether this gate is required for completion
statementstringEmpty for Gate items
artifactsrepeated stringRepository-relative paths that must exist before the gate can pass

Review variant (kind = "review"):

FieldTypeDescription
kindstring"review"
commandstringEmpty for Review items
requiredboolfalse for Review items
statementstringHuman-readable completion statement
artifactsrepeated stringEmpty for Review items

TraceBlockExecution

FieldTypeDescription
block_namestringName of the block that executed
trigger_event_idstringEvent ID that triggered this block
successboolWhether the block succeeded
summarystringHuman-readable summary of the result
emitted_event_idsrepeated stringIDs of events emitted by this block
duration_msuint64Wall-clock milliseconds for this block execution (including retries)
raw_outputstringCombined stdout+stderr from any shell command run by this block
exit_codeint32Exit code from any shell command run by this block
trigger_payload_jsonstringJSON payload of the event that triggered this block
emitted_payload_jsonsrepeated stringJSON payloads of events emitted by this block
audit_artifactsrepeated stringPaths to audit artefact files produced by this block

Throttle (enum)

ValueNumberDescription
THROTTLE_FULL0All blocks emit
THROTTLE_AUDIT_ONLY1Observers emit, mutators suppress
THROTTLE_DRY_RUN2Read-only, no side effects