Foundry

Foundry is an event-driven workflow engine for engineering automation. It replaces imperative shell scripts with composable task blocks connected by events, with throttle controlling how far each event ripples through the system.

Foundry runs as a daemon (foundryd) with a CLI controller (foundry) communicating over gRPC. Any emitter — a scheduled job, a webhook, a manual command — can fire an event and trigger the same downstream workflow.

How We Got Here

Foundry has been evolving for roughly four months. It started as a patchwork of shell scripts and launchd jobs — nightly maintenance automation that grew into a linear pipeline: iterate projects, audit for vulnerabilities, cut releases, install locally. Each step waited for all previous steps to complete. Projects couldn't run in parallel. An audit couldn't start until every project finished updating. A vulnerability discovered at 2pm had to wait for the 2am maintenance window.

As the scripts accumulated, the concepts started to clarify. Events, task blocks, throttle control, self-filtering — these patterns kept emerging from the scripts, so we started extracting them into something more intentional. Foundry is the result: a strongly typed Rust framework for building event-driven engineering workflows, replacing the fragile shell glue with compiler-checked event flows and composable task blocks.

Foundry decouples the work from the scheduling. The same task blocks that run during nightly maintenance can be triggered individually, at any time, with throttle controlling how deep the ripple goes.

Where We Are Today

Foundry has two layers, both defined in Rust:

  • Event library — the vocabulary of immutable facts that flow through the system. Each event type is a variant of the EventType enum with a well-defined payload structure.
  • Block library — the catalogue of reusable task blocks. Each block implements the TaskBlock trait, declaring which events it sinks on, what work it performs, and what events it emits.

All blocks are registered into a single engine at startup. Workflows are not declared anywhere — they emerge from the sink/emit relationships between blocks. The engine routes by event type; blocks self-filter by inspecting payload fields. This means a different entry event (or a different payload) activates a different subset of blocks, producing a different workflow — all from the same block library.

See Workflow Formations for the formations that exist today and the possibilities the current block library opens up.

Where We're Headed

Events and blocks will remain Rust-defined — they benefit from strong type safety and compile-time guarantees. The composition layer is what we intend to extract: allowing teams to declare which blocks participate in a formation and how events flow between them through configuration rather than code, enabling situational customisation without recompilation.

Key Ideas

  • Events are immutable facts — something happened. They carry a payload but have no opinion about what should happen next.
  • Task blocks are reusable units of work. Each block sinks on specific event types, does work, and emits new events.
  • Throttle sits on a task block's output side. It controls whether downstream events are emitted: full (everything propagates), audit_only (observers emit, mutators suppress), or dry_run (read-only, no side effects).
  • Workflows are compositions of task blocks wired together by events. They emerge from the emitter/sink relationships — not from a central orchestrator.

Charter

Mission

Provide a reliable, observable, event-driven engine for automating engineering workflows — from vulnerability remediation to dependency maintenance to release management.

Principles

  1. Events over orchestration. Work is triggered by events, not by position in a script. Any emitter can start any workflow.

  2. Composable task blocks. Small, reusable units of work. The same "Cut Release" block serves the vulnerability workflow and the maintenance workflow.

  3. Throttle controls depth. Every invocation declares how far the ripple should go. Audit without releasing. Release without installing. The same workflow, different throttle.

  4. Observability is paramount. Every event, every task block execution, every throttle decision is logged and traceable. If it happened, you can see it.

  5. Correctness through types. Rust's type system enforces exhaustive event handling, valid throttle states, and safe concurrency. Malformed events are compiler errors, not runtime surprises.

Scope

Foundry automates engineering workflows for a registered project portfolio:

  • Vulnerability detection and remediation
  • Dependency maintenance (iterate, maintain, commit, push)
  • Release management (tag, build, distribute)
  • Local tool installation
  • Release pipeline observation

It does not replace the existing evt-cli event logging system. Foundry emits events into the same JSONL intake files, coexisting with the current tooling. Over time, evt-cli may be rewritten in Rust to share Foundry's event type crate.

Non-Goals

  • General-purpose workflow engine (this serves specific engineering needs)
  • CI/CD replacement (Foundry orchestrates local work and observes pipelines)
  • Real-time monitoring dashboard (use Grafana, ops-visualizer, or similar)

Concepts

Events

An event records that something happened. It is an immutable fact appended to the event log. Events carry:

  • id — deterministic hash of content (same input always produces same ID)
  • event_type — what happened (e.g., vulnerability_detected)
  • project — which project this relates to
  • throttle — propagated through the chain, controls downstream behaviour
  • payload — event-type-specific data as JSON
  • occurred_at / recorded_at — timestamps

Events have no opinion about what should happen next. That's the job of task blocks.

Task Blocks

A task block is a unit of work. It has:

  • Name — human-readable identifier
  • KindObserver (reads/scans) or Mutator (writes/deploys)
  • Sinks — which event types trigger this block
  • Work — what it does when triggered
  • Emits — which events it produces on completion

Task blocks are registered with the engine at startup. When an event arrives, the engine finds all blocks that sink on that event type and executes them.

Observer vs Mutator

The distinction matters for throttle behaviour:

KindExamplesThrottle behaviour
ObserverAudit tag, audit main, validate projectAlways executes, always emits
MutatorCut release, install locally, commit+pushExecution and emission controlled by throttle

Throttle

The throttle sits on a task block's output side. After a block completes its work, it checks the throttle before emitting downstream events.

LevelObserversMutators
fullExecute + emitExecute + emit
audit_onlyExecute + emitExecute + suppress emission
dry_runExecute + emitSkip execution + suppress emission

The throttle is set when an event is first emitted (e.g., via CLI) and propagated through the entire chain. This means a single command controls how deep the ripple goes.

Workflows

A workflow is not a first-class object in Foundry. It emerges from the emitter/sink wiring between task blocks. When you emit vulnerability_detected, the chain of task blocks that fire in response is the vulnerability remediation workflow.

This means workflows are composable by nature — adding a new task block that sinks on an existing event type automatically extends every workflow that produces that event.

Engine

The engine is the runtime that:

  1. Receives an event
  2. Finds task blocks that sink on that event type
  3. Checks throttle to decide whether to execute and/or emit
  4. Executes matching blocks
  5. Collects emitted events and feeds them back into step 2
  6. Continues until no more events are produced (the chain is complete)

The engine processes events depth-first within a single invocation.

Event Model

Foundry's event model defines the vocabulary of immutable facts that flow through the system. Events carry payloads describing what occurred but have no opinion about what should happen next — task blocks make those decisions.

Authoritative Source

The canonical event type definitions live in foundry-core/src/event.rs (EventType enum). The implementation roadmap for completing all task blocks and workflows is in IMPLEMENTATION_PLAN.md at the project root.

Event Categories

Run Lifecycle

EventEmitterPurpose
maintenance_run_startedOrchestrator / manualBegins a per-project maintenance chain
maintenance_run_completedOrchestrator (fan-in)All projects finished; carries aggregate results

Per-Project: Iterate / Maintain

EventEmitterPurpose
project_validation_completedValidateProjectPre-flight check (dir, branch, gates)
project_iteration_completedRouteGateResultOne structural improvement attempted
project_maintenance_completedRouteGateResultDependencies updated, gates verified
project_changes_committedCommitAndPushGit commit created
project_changes_pushedCommitAndPushPushed to remote

Per-Project: Release Audit

EventEmitterPurpose
release_tag_auditedAuditReleaseTagLatest tag scanned for vulnerabilities
main_branch_auditedAuditMainBranchMain branch checked for same vulnerability
release_requestedAudit chainIntent to cut a patch release
release_completedCutReleaseTag pushed

Vulnerability Remediation

EventEmitterPurpose
vulnerability_detectedExternal / nightly auditEntry point for remediation workflow
remediation_startedRemediateVulnerabilityFix attempt underway
remediation_completedRemediateVulnerabilityFix attempt finished (success or failure)

Distribution Pipeline

EventEmitterPurpose
release_pipeline_completedWatchPipelineGitHub Actions finished building and publishing
local_install_completedInstallLocallyTool reinstalled on local machine

Event Structure

Every event has:

  • id — Deterministic SHA256 hash of (type + project + occurred_at + payload)
  • event_type — One of the EventType enum variants
  • project — Which project this event relates to
  • occurred_at / recorded_at — When it happened vs. when it was logged
  • throttle — Propagated through the chain to control downstream behaviour
  • payload — Event-type-specific JSON data

Payload Conventions

Downstream blocks read payload fields to make routing decisions (self-filtering). The engine routes by event type only — it cannot inspect payloads.

FieldUsed ByValues
vulnerableAuditMainBranchtrue/false — whether the tag has known CVEs
dirtyRemediateVulnerability, CutReleasetrue/false — whether main still has the vulnerability
cveAll vulnerability blocksCVE identifier string
statusDownstream blocks"ok"/"error" — validation and completion status
has_changesCommitAndPushWhether there are uncommitted changes to persist

Task Block Library

Task blocks are the reusable processing units of Foundry. Each block is defined once and can participate in multiple workflows.

Implementing the TaskBlock Trait

Every task block implements foundry_core::task_block::TaskBlock:

#![allow(unused)]
fn main() {
pub trait TaskBlock: Send + Sync {
    fn name(&self) -> &'static str;
    fn kind(&self) -> BlockKind;
    fn sinks_on(&self) -> &[EventType];
    fn execute(
        &self,
        trigger: &Event,
    ) -> Pin<Box<dyn Future<Output = anyhow::Result<TaskBlockResult>> + Send + '_>>;

    // Optional — defaults to no retries
    fn retry_policy(&self) -> RetryPolicy {
        RetryPolicy::default()
    }
}
}

The trait provides default implementations for should_emit() and should_execute() based on kind() and the throttle level. Override retry_policy() to enable automatic retry of transient failures.

See the Writing Task Blocks guide for step-by-step instructions and a full example including RetryPolicy.

Current Blocks

Hello-World (validates engine mechanics)

BlockKindSinks OnEmits
Compose GreetingObservergreet_requestedgreeting_composed
Deliver GreetingMutatorgreeting_composedgreeting_delivered

Vulnerability Remediation

These blocks form two paths through the vulnerability remediation workflow. Both Remediate Vulnerability and Cut Release sink on main_branch_audited and self-filter based on the dirty flag in the payload — only one path fires per event.

Audit Release Tag also sinks on project_changes_pushed to perform a post-push re-audit, confirming the fix is clean before anything downstream acts.

BlockKindSinks OnEmitsSelf-filters
Scan DependenciesObserverscan_requestedvulnerability_detected
Audit Release TagObservervulnerability_detected, project_changes_pushedrelease_tag_auditedSkips post-push when project not in registry
Audit Main BranchObserverrelease_tag_auditedmain_branch_auditedSkips when vulnerable=false
Remediate VulnerabilityMutatormain_branch_auditedremediation_completedOnly when dirty=true
Commit and PushMutatorremediation_completed, project_iteration_completed, project_maintenance_completedproject_changes_committed, project_changes_pushedSkips when tree is clean or changes=false
Cut ReleaseMutatormain_branch_auditedrelease_completedOnly when dirty=false
Watch PipelineMutatorrelease_completedrelease_pipeline_completed
Install LocallyMutatorproject_changes_pushed, release_pipeline_completedlocal_install_completed

Maintenance

The maintenance workflow uses an explicit routing Observer (Route Project Workflow) to delineate which sub-workflow runs. This keeps each downstream block focused on a single responsibility.

BlockKindSinks OnEmitsSelf-filters
Validate ProjectObservermaintenance_run_startedproject_validation_completedSkips projects not in active registry
Route Project WorkflowObserverproject_validation_completediteration_requested or maintenance_requestedStops when status != "ok" or no actions enabled
Commit and PushMutatorproject_iteration_completed, project_maintenance_completedproject_changes_committed, project_changes_pushedSkips when tree is clean
Audit Release TagObserverproject_changes_pushedrelease_tag_auditedSkips when project not in registry

The actions.maintain flag is forwarded inside the iteration_requested payload so that the gate routing can chain directly to maintenance_requested after a successful iteration without re-querying the project configuration.

Gate Orchestration

These blocks provide native gate resolution, execution, and routing for iterate, maintain, and validation workflows.

BlockKindSinks OnEmitsSelf-filters
Resolve GatesObserveriteration_requested, maintenance_requested, validation_requestedgate_resolution_completed
Run Preflight GatesObservergate_resolution_completedpreflight_completedSkips maintain workflow
Run Verify GatesObserverexecution_completedgate_verification_completed
Route Gate ResultObservergate_verification_completedproject_iteration_completed / project_maintenance_completed / retry_requestedRoutes based on pass/fail and retry count
Route Validation ResultObserverpreflight_completedvalidation_completedOnly handles validate workflow

Iterate Workflow

These blocks form the native iterate chain, running inside the gate orchestration lifecycle.

BlockKindSinks OnEmitsSelf-filters
Check CharterObserverpreflight_completedcharter_validatedOnly handles iterate workflow
Assess ProjectMutatorcharter_validatedproject_assessed
Triage AssessmentMutatorproject_assessedassessment_triaged
Create PlanMutatorassessment_triagedplan_created
Execute PlanMutatorplan_createdexecution_completed

Maintain Workflow

BlockKindSinks OnEmitsSelf-filters
Execute MaintainMutatorgate_resolution_completedexecution_completedOnly handles maintain workflow
Retry ExecutionMutatorretry_requestedexecution_completed
Summarize ResultObserverproject_iteration_completed, project_maintenance_completedgenerate_summary

Validation Workflow

A dedicated read-only workflow for checking project gate health. No Mutator blocks are involved — validation never modifies code.

validation_requested
  → Resolve Gates → gate_resolution_completed
    → Run Preflight Gates → preflight_completed
      → Route Validation Result → validation_completed

Vulnerability Workflow Chain

flowchart TD
    A[scan_requested] --> B([Scan Dependencies])
    B --> C[vulnerability_detected]
    C --> D([Audit Release Tag])
    D --> E[release_tag_audited]
    E --> F([Audit Main Branch])
    F --> G[main_branch_audited]
    G --> H{dirty?}
    H -->|dirty=true| I([Remediate Vulnerability])
    I --> J[remediation_completed]
    J --> K([Commit and Push])
    K --> L[project_changes_committed]
    K --> M[project_changes_pushed]
    M --> N([Audit Release Tag post-push])
    N --> O[release_tag_audited]
    M --> P([Install Locally])
    P --> Q[local_install_completed]
    H -->|dirty=false| R([Cut Release])
    R --> S[release_completed]
    S --> T([Watch Pipeline])
    T --> U[release_pipeline_completed]
    U --> V([Install Locally])
    V --> W[local_install_completed]

Maintenance Workflow Chain

flowchart TD
    A([maintenance_run_started]) --> B[[Validate Project]]
    B --> C([project_validation_completed])
    C --> D[[Route Project Workflow]]
    D -->|iterate=true| E([iteration_requested])
    D -->|iterate=false, maintain=true| F([maintenance_requested])
    D -->|no actions| G([end — no automation])
    E --> H[[Resolve Gates]]
    H --> I[[Run Preflight Gates]]
    I --> J[[Check Charter]]
    J --> K[[Assess Project]]
    K --> L[[Triage Assessment]]
    L --> M[[Create Plan]]
    M --> N[[Execute Plan]]
    N --> O[[Run Verify Gates]]
    O --> P[[Route Gate Result]]
    P -->|pass| Q([project_iteration_completed])
    P -->|fail, retries left| R[[Retry Execution]]
    Q -->|maintain=true| F
    F --> S[[Resolve Gates]]
    S --> T[[Execute Maintain]]
    T --> U[[Run Verify Gates]]
    U --> V[[Route Gate Result]]
    V -->|pass| W([project_maintenance_completed])
    V -->|fail, retries left| X[[Retry Execution]]

Gateway Pattern

Every block that executes an external process (a shell command or an audit tool) receives its I/O capability through a gateway trait rather than calling the implementation module directly. Two gateway traits live in gateway.rs:

  • ShellGateway — wraps crate::shell::run for arbitrary command execution.
  • ScannerGateway — wraps crate::scanner::run_audit for vulnerability scanning.

Production blocks hold an Arc<dyn ShellGateway> (and/or Arc<dyn ScannerGateway>) which is initialised to the real implementation in new(). A #[cfg(test)] constructor accepts a fake instead, enabling hermetic unit tests for every block.

This separation means:

  • The happy path and every failure/edge-case branch can be tested without spawning real processes.
  • shell.rs and scanner.rs stay untouched; the gateway is a thin adapter.
  • No async_trait macro is required — the return type uses an explicit Pin<Box<dyn Future + Send + '_>> (the same pattern as TaskBlock::execute).

See Testing with Fakes for usage examples.

RetryPolicy

Blocks can declare automatic retry behaviour by overriding retry_policy():

#![allow(unused)]
fn main() {
use std::time::Duration;
use foundry_core::task_block::RetryPolicy;

fn retry_policy(&self) -> RetryPolicy {
    RetryPolicy {
        max_retries: 3,
        backoff: Duration::from_secs(5),
    }
}
}

max_retries: 0 (the default) means the block runs exactly once. With max_retries: N, the engine retries up to N times after any failure (either a returned Err or a TaskBlockResult { success: false, .. }), sleeping backoff between each attempt. The final result (success or failure) is what the engine records in the BlockExecution trace.

Crate Structure

Foundry is organised as a Cargo workspace with three crates:

foundry/
├── Cargo.toml              # Workspace root
├── proto/foundry.proto     # gRPC service definition
├── crates/
│   ├── foundry-core/       # Shared types (library)
│   ├── foundryd/           # Daemon (binary)
│   └── foundry-cli/        # CLI controller (binary)
└── book/                   # This documentation

foundry-core

Shared types used by both the daemon and CLI:

  • event.rsEvent struct, EventType enum, deterministic ID generation
  • throttle.rsThrottle enum (Full, AuditOnly, DryRun)
  • task_block.rsTaskBlock trait, BlockKind, TaskBlockResult, RetryPolicy
  • registry.rsRegistry, ProjectEntry, ActionFlags, Stack, InstallConfig
  • trace.rsTraceIndex, BlockExecution, ProcessResult — the structured types used to persist and display execution traces. Moved here from foundryd so the CLI can deserialise on-disk traces without depending on the daemon crate.

This crate has no async runtime dependency. It defines the vocabulary that the rest of the system speaks.

foundryd

The daemon process. Listens on gRPC (127.0.0.1:50051 by default) and runs the workflow engine.

Core engine

  • engine.rs — event router: matches events to task blocks, executes them with retry logic, propagates emitted events respecting the throttle level. Exposes BlockExecution and ProcessResult for structured telemetry.
  • service.rs — gRPC service implementation (Emit, Status, Watch, Trace)

Daemon support modules

  • orchestrator.rs — coordinates per-project maintenance runs with concurrency control. Dispatches MaintenanceRunStarted per project, enforces max_concurrent via a semaphore, and prevents double-running via an active project set with a drop-guard cleanup.
  • event_writer.rs — appends every event to monthly JSONL files (YYYY-MM.jsonl) inside ~/.foundry/events/ (or FOUNDRY_EVENTS_DIR). Crash-safe: each write opens, flushes, and closes the file. A Mutex serializes concurrent writes.
  • trace_store.rs — in-memory store of recent ProcessResult chains, keyed by root event ID. Used for fast Trace RPC lookups of workflows still in progress or recently completed.
  • trace_writer.rs — persists completed ProcessResult objects to disk as pretty-printed JSON files under ~/.foundry/traces/YYYY-MM-DD/{event_id}.json. Traces written here survive daemon restarts indefinitely and are read by foundry history and foundry trace when the in-memory store has no match.
  • workflow_tracker.rs — tracks workflows that are currently being processed by background tasks. Thread-safe via RwLock. Each Emit RPC inserts an ActiveWorkflow entry on start; a RAII WorkflowGuard removes it on completion or panic. The Status RPC reads this tracker to show live in-flight workflows.
  • shell.rs — async shell runner used by block implementations. Runs an external command with configurable timeout (default 5 min), captures stdout and stderr, and returns a CommandResult.
  • scanner.rs — vulnerability scanner abstraction. Dispatches to the stack-appropriate tool (cargo audit, npm audit, pip-audit, mix deps.audit) and normalizes output into a Vec<Vulnerability>.
  • gateway.rs — I/O abstraction layer for task blocks. Defines ShellGateway and ScannerGateway traits with ProcessShellGateway and ProcessScannerGateway production implementations. Also provides FakeShellGateway and FakeScannerGateway test doubles (available under #[cfg(test)] only) that record invocations and return pre-configured results, enabling hermetic unit testing of every block without spawning real processes.
  • summary.rs — renders a MaintenanceRunSummary as a Markdown report (project table with success/failure/skipped, a failures section, and timing statistics).

Task block implementations (blocks/)

  • validate.rsValidateProject: pre-flight checks before a maintenance run
  • resolve_gates.rsResolveGates: reads .hone-gates.json and emits gate definitions
  • run_preflight_gates.rsRunPreflightGates: runs gates on unmodified codebase
  • run_verify_gates.rsRunVerifyGates: runs gates after code changes
  • route_gate_result.rsRouteGateResult: routes pass/fail to completion or retry
  • route_validation_result.rsRouteValidationResult: routes validation-only results
  • check_charter.rsCheckCharter: validates project charter before iteration
  • assess_project.rsAssessProject: AI-driven project assessment
  • triage_assessment.rsTriageAssessment: prioritises assessment findings
  • create_plan.rsCreatePlan: generates an execution plan from triaged findings
  • execute_plan.rsExecutePlan: executes the generated plan
  • execute_maintain.rsExecuteMaintain: runs maintenance tasks
  • retry_execution.rsRetryExecution: retries failed executions with context
  • summarize_result.rsSummarizeResult: generates workflow summary and traces
  • git_ops.rsCommitAndPush: stages, commits, and optionally pushes changes
  • audit.rsAuditReleaseTag, AuditMainBranch: vulnerability scanning
  • release.rsCutRelease, WatchPipeline: tagging and CI monitoring
  • install.rsInstallLocally: reinstalls the project locally after a fix
  • remediate.rsRemediateVulnerability: invokes the AI agent to fix a CVE
  • scan.rsScanDependencies: scans for known vulnerabilities
  • greet.rsComposeGreeting, DeliverGreeting: hello-world engine validation

foundry-cli

The CLI controller. Connects to foundryd over gRPC.

  • main.rsclap-based argument parsing; subcommands: emit, status, watch, trace, run, history, registry
  • commands.rs — async implementations of each subcommand via tonic gRPC client; also contains the history command which reads on-disk traces directly from ~/.foundry/traces/ without a daemon connection
  • registry_commands.rs — pure I/O implementations of the registry subcommands (init, list, show, add, remove, edit); reads and writes ~/.foundry/registry.json using foundry_core::registry types

proto/foundry.proto

The gRPC contract between CLI and daemon:

  • Emit — fire an event with type, project, throttle, and optional JSON payload
  • Status — query active workflow states (all or by workflow ID)
  • Watch — server-side streaming of live events, filterable by project
  • Trace — retrieve the full event chain and block execution records for a completed workflow

Getting Started

Prerequisites

  • Rust 1.85+ (via rust-toolchain.toml)
  • protoc (Protocol Buffers compiler): brew install protobuf

Build

cd path/to/foundry
cargo build --release

This produces two binaries:

  • target/release/foundryd — the daemon
  • target/release/foundry — the CLI controller

Install Locally

Install both binaries to ~/.cargo/bin/ using the convenience script:

./install.sh

Or install each crate individually:

cargo install --path crates/foundryd
cargo install --path crates/foundry-cli

Re-run after making changes to pick up the latest version.

Start the Daemon

foundryd

You'll see the registered task blocks and the listening address:

INFO foundryd::engine: registered task block block="Compose Greeting" sinks=[GreetRequested]
INFO foundryd::engine: registered task block block="Deliver Greeting" sinks=[GreetingComposed]
INFO foundryd::engine: registered task block block="Audit Release Tag" sinks=[VulnerabilityDetected]
INFO foundryd::engine: registered task block block="Audit Main Branch" sinks=[ReleaseTagAudited]
INFO foundryd::engine: registered task block block="Remediate Vulnerability" sinks=[MainBranchAudited]
INFO foundryd::engine: registered task block block="Commit and Push" sinks=[RemediationCompleted]
INFO foundryd::engine: registered task block block="Cut Release" sinks=[MainBranchAudited]
INFO foundryd::engine: registered task block block="Watch Pipeline" sinks=[ReleaseCompleted]
INFO foundryd::engine: registered task block block="Install Locally" sinks=[ProjectChangesPushed, ReleasePipelineCompleted]
INFO foundryd: foundryd listening on 127.0.0.1:50051

Send Your First Event

In another terminal:

foundry emit greet_requested \
  --project hello \
  --payload '{"name": "World"}'

In the daemon logs you'll see the event chain:

INFO foundryd::service: processing event event_type=greet_requested project=hello throttle=full
INFO foundryd::engine: executing task block block="Compose Greeting" ...
INFO foundryd::blocks::greet: composed greeting greeting=Hello, World!
INFO foundryd::engine: executing task block block="Deliver Greeting" ...
INFO foundryd::blocks::greet: delivering greeting greeting=Hello, World!
INFO foundryd::service: event chain complete total_events=3

Three events in the chain: greet_requestedgreeting_composedgreeting_delivered.

Try Throttle Control

# Observers run, mutators suppress emission
foundry emit greet_requested \
  --project hello \
  --throttle audit_only \
  --payload '{"name": "World"}'

Now you'll see only 2 events — greeting_delivered is suppressed because Deliver Greeting is a Mutator and the throttle is audit_only.

Quality Gates

cargo test                                              # Run all tests
cargo clippy --all-targets --all-features -- -D warnings # Lint
cargo fmt --check                                       # Format check
cargo deny check                                        # License + advisory audit

Emitting Events

From the CLI

foundry emit <event_type> --project <project> [--throttle <level>] [--payload <json>]

Arguments

ArgumentRequiredDescription
event_typeYesEvent type name (e.g., greet_requested, vulnerability_detected)
--projectYesTarget project name
--throttleNofull (default), audit_only, or dry_run
--payloadNoJSON string with event-specific data
--addrNoDaemon address (default: http://127.0.0.1:50051)

Examples

# Simple event, default throttle
foundry emit greet_requested --project hello

# With payload
foundry emit greet_requested --project hello --payload '{"name": "Stacey"}'

# Audit only — observe without mutating
foundry emit vulnerability_detected \
  --project my-tool \
  --throttle audit_only \
  --payload '{"cve": "CVE-2026-1234", "severity": "high"}'

# Dry run — no side effects at all
foundry emit maintenance_run_started \
  --project evt-cli \
  --throttle dry_run

What Happens When You Emit

  1. The CLI sends the event to foundryd via gRPC
  2. The engine creates an Event with a deterministic ID
  3. The engine finds all task blocks that sink on the event type
  4. For each matching block:
    • Check throttle: should this block execute? Should it emit?
    • Execute the block's work
    • Collect emitted events
  5. Feed emitted events back into step 3 (the chain continues)
  6. Return the initial event ID to the CLI

The chain continues until no more events are produced.

Inspecting a Completed Chain

After emitting an event, use foundry trace with the returned event ID to see the full propagation tree and what each block did:

$ foundry emit greet_requested --project hello --payload '{"name": "Stacey"}'
Event emitted: evt_47fcb603e1b18c8435b8cc3b

$ foundry trace evt_47fcb603e1b18c8435b8cc3b
greet_requested (evt_47fcb603e1b18c8435b8cc3b) project=hello
  → ComposeGreeting: ok — composed greeting for Stacey
    greeting_composed (evt_a1b2c3d4e5f6) project=hello
      → DeliverGreeting: ok — delivered greeting: Hello, Stacey!
        greeting_delivered (evt_f6e5d4c3b2a1) project=hello

Pass --verbose to also show trigger payloads, emitted payloads, raw shell output, and paths to any audit artefacts produced.

Persistent Trace Storage

Every completed event chain is written to disk under ~/.foundry/traces/YYYY-MM-DD/{event_id}.json (overridable via FOUNDRY_TRACES_DIR). Traces persist indefinitely across daemon restarts — there is no expiry TTL on disk.

To browse past traces, use foundry history:

# Show the last 7 days
foundry history

# Show a specific date
foundry history 2026-03-22

# Filter by project
foundry history --project my-tool

You can retrieve the full trace for any on-disk event with foundry trace, even if the daemon has been restarted since the event was processed.

The Project Registry

The registry is Foundry's source of truth for which projects exist on your machine and what automation applies to each one. Without a populated registry, the daemon starts successfully but skips all project-specific work.

Where the Registry Lives

By default: ~/.foundry/registry.json

Override the path with the environment variable:

export FOUNDRY_REGISTRY_PATH=/path/to/my-registry.json

The daemon reads the registry on startup. If the file is missing it logs a warning and continues with an empty registry (no projects will be processed).

Registry Format (v2)

{
  "version": 2,
  "projects": [
    {
      "name": "my-tool",
      "path": "/Users/alice/projects/my-tool",
      "stack": "rust",
      "agent": "claude",
      "repo": "alice/my-tool",
      "branch": "main",
      "skip": false,
      "actions": {
        "iterate": true,
        "maintain": true,
        "push": true,
        "audit": true,
        "release": false
      },
      "install": {
        "command": "cargo install --path ."
      }
    }
  ]
}

Top-level fields

FieldTypeDescription
versionnumberMust be 2
projectsarrayList of ProjectEntry objects

ProjectEntry fields

FieldRequiredTypeDescription
nameYesstringUnique human-readable identifier used in events and logs
pathYesstringAbsolute path to the project on your local filesystem
stackYesstringTechnology stack — see Stack values
agentYesstringAI agent name used for automation (e.g. "claude")
repoYesstringGitHub repository slug (owner/repo) used by Watch Pipeline
branchYesstringDefault branch (e.g. "main") — validation checks out this branch
skipNostring or nullAbsent or null means not skipped; a non-empty string value is the skip reason. Accepts true (treated as "skipped") and false/null for backwards compatibility
actionsNoobjectWhich automation steps are enabled; all default to false
installNoobjectHow to reinstall locally after automation — see InstallConfig
notesNostringHuman-readable notes about the project (informational only)
timeout_secsNonumberTimeout in seconds for long-running commands. Defaults to 3600 (60 minutes) when absent

Stack values

The stack field tells Foundry which audit tool to use and how to run stack-specific commands.

ValueAudit toolNotes
"rust"cargo audit --jsonRequires cargo-audit to be installed
"typescript"npm audit --jsonExit code 1 = vulnerabilities found (not a tool failure)
"python"pip-audit --format=jsonRequires pip-audit to be installed
"elixir"mix deps.audit --format=json
"cpp"Placeholder for C++ projects; audit tooling not yet wired

ActionFlags

The actions object controls which steps run during a maintenance run. All flags default to false when the actions key is absent.

FlagWhat it enables
iterateRuns the iterate workflow (assess, plan, execute) after project validation passes
maintainRuns the maintain workflow — either after iterate completes (when both are enabled) or directly after validation (when only maintain is enabled)
pushPushes commits to the remote via git push after a commit is made
audit(Reserved for future use — currently informational only)
release(Reserved for future use — currently informational only)

InstallConfig

The install field configures how the project is reinstalled locally after automation completes. Exactly one variant is used per entry:

Command — runs an arbitrary shell command in the project directory:

"install": { "command": "cargo install --path ." }

Brew — installs via a Homebrew formula:

"install": { "brew": "my-formula" }

Minimal Project Entry

Only the six required fields are needed. All optional fields default to safe values (no actions enabled, no install step, not skipped):

{
  "version": 2,
  "projects": [
    {
      "name": "minimal-project",
      "path": "/Users/alice/projects/minimal-project",
      "stack": "rust",
      "agent": "claude",
      "repo": "alice/minimal-project",
      "branch": "main"
    }
  ]
}

Excluding a Project Temporarily

Set skip to a string reason to pause automation without removing the entry:

{
  "name": "on-hold",
  "path": "/Users/alice/projects/on-hold",
  "stack": "typescript",
  "agent": "claude",
  "repo": "alice/on-hold",
  "branch": "main",
  "skip": "Waiting for CI to stabilise"
}

The value of skip is the human-readable reason displayed in foundry registry show. Absent, null, false, or an empty string all mean "not skipped". For backwards compatibility, true is treated as the reason string "skipped".

The Validate Project block silently acknowledges skipped projects (emits project_validation_completed with status: "skipped") so the engine trace remains complete.

Managing the Registry with the CLI

Rather than editing registry.json by hand, use the foundry registry subcommands. All commands respect FOUNDRY_REGISTRY_PATH for non-default locations.

# Create an empty registry
foundry registry init

# List all projects
foundry registry list

# Inspect one project
foundry registry show my-tool

# Add a new project
foundry registry add \
  --name my-tool \
  --path /Users/alice/projects/my-tool \
  --stack rust \
  --agent claude \
  --repo alice/my-tool \
  --iterate --maintain --push \
  --install-command "cargo install --path ." \
  --notes "Main CLI toolchain"

# Edit an existing project
foundry registry edit my-tool --timeout-secs 3600

# Skip a project temporarily
foundry registry edit my-tool --skip "Waiting for CI to stabilise"

# Clear a skip (resume automation)
foundry registry edit my-tool --skip ""

# Remove a project
foundry registry remove my-tool

See CLI Commands — foundry registry for the full option reference.

Multiple Projects

A single registry file can declare any number of projects. They are processed concurrently during a maintenance run (up to max_concurrent at a time, which defaults to the number of active projects unless the orchestrator is configured otherwise):

{
  "version": 2,
  "projects": [
    {
      "name": "api-server",
      "path": "/Users/alice/projects/api-server",
      "stack": "rust",
      "agent": "claude",
      "repo": "alice/api-server",
      "branch": "main",
      "actions": { "iterate": true, "maintain": true, "push": true }
    },
    {
      "name": "frontend",
      "path": "/Users/alice/projects/frontend",
      "stack": "typescript",
      "agent": "claude",
      "repo": "alice/frontend",
      "branch": "main",
      "actions": { "maintain": true, "push": true }
    }
  ]
}

Workflow Formations

A workflow formation is not a first-class object in Foundry. It is the logical result of which blocks sink on which events — a chain that emerges when you emit a particular entry event. All blocks live in one engine. The formation that fires depends entirely on the entry event and the payload values that flow through it.

This page documents the formations that exist today and explores how the current block library could be recombined for different purposes.

The Block Library at a Glance

Every block declares its sinks (what triggers it), its emits (what it produces), and its kind (Observer or Mutator). The engine does the rest.

Shared Infrastructure

These blocks appear in multiple formations:

BlockKindSinks OnEmits
Resolve GatesObservercharter_check_completed, maintenance_requested, validation_requestedgate_resolution_completed
Run Preflight GatesObservergate_resolution_completedpreflight_completed
Run Verify GatesObserverexecution_completedgate_verification_completed
Route Gate ResultObservergate_verification_completedproject_iteration_completed / project_maintenance_completed / retry_requested
Retry ExecutionMutatorretry_requestedexecution_completed
Summarize ResultObserverproject_iteration_completed, project_maintenance_completedsummarize_completed
Commit and PushMutatorremediation_completed, project_iteration_completed, project_maintenance_completedproject_changes_committed, project_changes_pushed

Iteration Blocks

BlockKindSinks OnEmits
Check CharterObserveriteration_requestedcharter_check_completed
Assess ProjectObserverpreflight_completedassessment_completed
Triage AssessmentObserverassessment_triagedtriage_completed
Create PlanObservertriage_completedplan_completed
Execute PlanMutatorplan_completedexecution_completed

Maintenance Blocks

BlockKindSinks OnEmits
Execute MaintainMutatorgate_resolution_completedexecution_completed

Vulnerability Blocks

BlockKindSinks OnEmits
Scan DependenciesObserverscan_requestedvulnerability_detected (per CVE)
Audit Release TagObservervulnerability_detected, project_changes_pushedrelease_tag_audited
Audit Main BranchObserverrelease_tag_auditedmain_branch_audited
Remediate VulnerabilityMutatormain_branch_auditedremediation_completed
Cut ReleaseMutatormain_branch_auditedrelease_completed
Watch PipelineMutatorrelease_completedrelease_pipeline_completed
Install LocallyMutatorproject_changes_pushed, release_pipeline_completedlocal_install_completed

Prompt Workflow Blocks

BlockKindSinks OnEmits
Direct PromptObserverpreflight_completed (workflow=prompt)plan_completed

Strategic Loop Blocks

BlockKindSinks OnEmits
Strategic AssessorObserveriteration_requested (strategic=true)strategic_assessment_completed
Strategic Loop ControllerObserverstrategic_assessment_completed, inner_iteration_completediteration_requested (loop) / project_iteration_completed (done)

Orchestration Blocks

BlockKindSinks OnEmits
Validate ProjectObservermaintenance_run_startedproject_validation_completed
Route Project WorkflowObserverproject_validation_completediteration_requested / maintenance_requested

Current Formations

These are the formations that fire today, depending on which entry event you emit.

The Full Nightly Run

Entry event: maintenance_run_started

This is the broadest formation. It validates the project, routes to iteration and/or maintenance based on the project's registry flags, and chains the two sub-workflows together when both are enabled.

flowchart TD
    A([maintenance_run_started]) --> B[[Validate Project]]
    B --> C[[Route Project Workflow]]
    C -->|iterate=true| D([iteration_requested])
    C -->|maintain=true| E([maintenance_requested])
    D --> F[Iterate Formation]
    F -->|success + maintain=true| E
    E --> G[Maintain Formation]

Iterate Formation

Entry event: iteration_requested

The full assessment-to-execution pipeline with gate verification and bounded retry.

flowchart TD
    A([iteration_requested]) --> B[[Check Charter]]
    B -->|passed| C[[Resolve Gates]]
    C --> D[[Run Preflight Gates]]
    D -->|passed| E[[Assess Project]]
    E --> F[[Triage Assessment]]
    F -->|accepted| G[[Create Plan]]
    G --> H[[Execute Plan]]
    H --> I[[Run Verify Gates]]
    I --> J[[Route Gate Result]]
    J -->|pass| K([project_iteration_completed])
    J -->|fail, retries left| L[[Retry Execution]]
    L --> I
    K --> M[[Summarize Result]]
    K --> N[[Commit and Push]]

Prompt Formation

Entry event: prompt_execution_requested

A streamlined formation that executes an arbitrary user-provided prompt with gate verification. It skips assessment, triage, and plan creation — the user's prompt IS the plan.

flowchart TD
    A([prompt_execution_requested]) --> B[[Check Charter]]
    B -->|passed| C[[Resolve Gates]]
    C --> D[[Run Preflight Gates]]
    D -->|passed| E[[Direct Prompt]]
    E --> F[[Execute Plan]]
    F --> G[[Run Verify Gates]]
    G --> H[[Route Gate Result]]
    H -->|pass| I([project_iteration_completed])
    H -->|fail, retries left| J[[Retry Execution]]
    J --> G
    I --> K[[Summarize Result]]
    I --> L[[Commit and Push]]

Usage:

foundry emit prompt_execution_requested my-project \
  --payload '{"prompt": "Pick the highest priority interaction from et and implement it."}'

Strategic Iterate Formation

Entry event: iteration_requested with strategic: true

A nested loop that wraps the iterate formation. The strategic assessor identifies multiple areas for improvement, then the loop controller enters the inner iterate formation for each area. After each inner iteration completes, an AI assessment decides whether to continue. Changes are committed per iteration.

flowchart TD
    A([iteration_requested<br/>strategic=true]) --> B[[Strategic Assessor]]
    B --> C([strategic_assessment_completed])
    C --> D[[Strategic Loop Controller]]
    D --> E([iteration_requested<br/>with loop_context])
    E --> F[Iterate Formation<br/>inner loop]
    F --> G([inner_iteration_completed])
    G --> H[[Commit and Push]]
    G --> D
    D -->|continue| E
    D -->|done| I([project_iteration_completed])
    I --> J[[Summarize Result]]
    I --> K[[Commit and Push]]

The inner iterate formation runs exactly as documented above, with one difference: Route Gate Result emits inner_iteration_completed instead of project_iteration_completed when loop_context is present in the payload. This allows the strategic loop controller to intercept the completion and decide whether to continue.

Terminal blocks (Summarize Result and Commit and Push) self-filter on loop_context — they skip intermediate completions and only fire on the final project_iteration_completed emitted by the strategic loop controller (which strips loop_context before emitting it).

Maintain Formation

Entry event: maintenance_requested

Dependency updates and general maintenance. Preflight gates are skipped (the codebase may be in a pre-maintenance state), but verification gates run after execution.

flowchart TD
    A([maintenance_requested]) --> B[[Resolve Gates]]
    B --> C[[Run Preflight Gates]]
    C -->|skipped| D[[Execute Maintain]]
    D --> E[[Run Verify Gates]]
    E --> F[[Route Gate Result]]
    F -->|pass| G([project_maintenance_completed])
    F -->|fail, retries left| H[[Retry Execution]]
    H --> E
    G --> I[[Summarize Result]]
    G --> J[[Commit and Push]]

Vulnerability Remediation Formation

Entry event: vulnerability_detected

Two paths through the same blocks, governed by the dirty payload flag.

flowchart TD
    A([vulnerability_detected]) --> B[[Audit Release Tag]]
    B --> C[[Audit Main Branch]]
    C --> D{dirty?}
    D -->|true| E[[Remediate Vulnerability]]
    E --> F[[Commit and Push]]
    F --> G[[Install Locally]]
    D -->|false| H[[Cut Release]]
    H --> I[[Watch Pipeline]]
    I --> J[[Install Locally]]

Scan Formation

Entry event: scan_requested

A broader entry point that discovers vulnerabilities and feeds them into the remediation formation. Scan Dependencies emits one vulnerability_detected event per CVE found, so a single scan can trigger multiple parallel remediation chains.

flowchart TD
    A([scan_requested]) --> B[[Scan Dependencies]]
    B -->|per CVE| C([vulnerability_detected])
    C --> D[Remediation Formation]

Validation Formation

Entry event: validation_requested

A read-only health check. No Mutator blocks fire — it just resolves gates, runs them, and reports the results.

flowchart TD
    A([validation_requested]) --> B[[Resolve Gates]]
    B --> C[[Run Preflight Gates]]
    C --> D[[Route Validation Result]]
    D --> E([validation_completed])

Possible Formations

The block library already supports formations that aren't part of the nightly run. Because the engine routes by event type and blocks self-filter on payload, you can trigger these directly.

Iterate Without Maintenance

Emit iteration_requested with actions.maintain=false. The iterate formation runs, and on success Route Gate Result emits project_iteration_completed without chaining to maintenance_requested.

foundry emit iteration_requested my-project \
  --payload '{"actions":{"iterate":true,"maintain":false}}'

This is useful when you want to improve code quality without touching dependencies — a focused structural improvement pass.

Maintenance Without Iteration

Emit maintenance_requested directly. The maintain formation runs on its own, skipping assessment, triage, and planning entirely.

foundry emit maintenance_requested my-project

This is a pure dependency update pass — update libraries, run gates, commit if they pass.

Scan Without Remediation

Emit scan_requested with audit_only throttle. Scan Dependencies and the audit blocks run (they are Observers), but Remediate Vulnerability and Cut Release (Mutators) suppress their output.

foundry emit scan_requested my-project --throttle audit_only

This tells you what vulnerabilities exist and whether main is dirty, without making any changes.

Remediation Without Scanning

Emit vulnerability_detected directly with the CVE details. This skips the scan entirely and jumps straight into the audit-and-fix chain.

foundry emit vulnerability_detected my-project \
  --payload '{"cve":"CVE-2026-1234","vulnerable":true,"dirty":true}'

This is how you would handle a vulnerability reported through a channel other than Foundry's scanner — a security advisory, a colleague's finding, or a CI notification.

Post-Push Audit

The Audit Release Tag block also sinks on project_changes_pushed. This means that after an iterate or maintain formation commits and pushes, the release tag is automatically re-audited. If the push introduced a vulnerability (or resolved one), the audit chain picks it up without a separate scan.

Strategic Iteration

Emit iteration_requested with strategic: true to enter the nested loop. The strategic assessor analyses the codebase holistically and the loop controller runs multiple inner iterate cycles until the AI determines the codebase has plateaued.

foundry emit iteration_requested my-project \
  --payload '{"strategic":true,"max_iterations":5}'

The max_iterations field caps the loop to prevent runaway iterations. Each inner cycle commits its changes independently.

Gate Check Only

Emit validation_requested to run all gates without modifying anything. This is the lightest formation — it tells you whether the project is healthy right now.

foundry emit validation_requested my-project

Designing New Formations

The current block library is a toolkit. The formations above are the ones we use today, but the same blocks can participate in formations we haven't built yet. A few principles guide what's possible:

  1. Entry events define scope. The deeper into a chain you emit, the narrower the formation. Emitting maintenance_run_started runs everything; emitting plan_completed skips assessment entirely and just executes a plan you provide.

  2. Payload values steer routing. Blocks self-filter on payload fields like dirty, accepted, workflow, and actions. Changing a payload value changes which blocks fire without changing any code.

  3. Throttle controls depth. The same formation behaves differently under full, audit_only, and dry_run. This gives you three versions of every formation for free.

  4. Shared blocks multiply formations. Commit and Push sinks on three different event types. Install Locally sinks on two. Every block that participates in multiple formations is a junction point where chains can converge or diverge.

Vulnerability Remediation Workflow

The vulnerability remediation workflow is Foundry's primary use case. It replaces a linear shell script pipeline with an event-driven chain of task blocks that branches based on the state of the codebase.

The Two Paths

When a vulnerability is detected, the chain branches based on whether the main branch still contains the vulnerability:

flowchart TD
    A[vulnerability_detected] --> B([Audit Release Tag])
    B --> C[release_tag_audited]
    C --> D([Audit Main Branch])
    D --> E[main_branch_audited]
    E --> F{dirty?}
    F -->|"dirty=true (vulnerability present on main)"| G([Remediate Vulnerability])
    G --> H[remediation_completed]
    H --> I([Commit and Push])
    I --> J[project_changes_committed]
    I --> K[project_changes_pushed]
    K --> L([Install Locally])
    L --> M[local_install_completed]
    F -->|"dirty=false (main already fixed, no release cut yet)"| N([Cut Release])
    N --> O[release_completed]
    O --> P([Watch Pipeline])
    P --> Q[release_pipeline_completed]
    Q --> R([Install Locally])
    R --> S[local_install_completed]

Dirty path — the vulnerability exists on main. Foundry remediates it (e.g., dependency update), commits, pushes, and reinstalls locally.

Clean path — main is already fixed (perhaps by a developer), but no release has been cut. Foundry tags a patch release, watches the CI pipeline, and reinstalls locally.

If the release tag is not vulnerable at all, the chain stops after release_tag_auditedAudit Main Branch self-filters and emits nothing.

Self-Filtering

The engine routes events by type only — it cannot inspect payloads. When both Remediate Vulnerability and Cut Release sink on main_branch_audited, both blocks receive every main_branch_audited event. Each block checks the dirty flag in the payload and returns an empty result if the condition doesn't match. This ensures only one path fires.

Running the Workflow

Full run (default)

All blocks execute and emit. The complete chain runs to local_install_completed:

foundry emit vulnerability_detected \
  --project my-tool \
  --payload '{"cve": "CVE-2026-1234", "vulnerable": true, "dirty": true}'

Then inspect the trace:

foundry trace <event_id>
vulnerability_detected (evt_...) project=my-tool
  → Audit Release Tag: ok — Release tag audited: CVE-2026-1234 vulnerable=true
    release_tag_audited (evt_...) project=my-tool
      → Audit Main Branch: ok — Main branch audited: CVE-2026-1234 dirty=true
        main_branch_audited (evt_...) project=my-tool
          → Remediate Vulnerability: ok — Remediated CVE-2026-1234
            remediation_completed (evt_...) project=my-tool
              → Commit and Push: ok — Committed and pushed fix for CVE-2026-1234
                project_changes_committed (evt_...) project=my-tool
                project_changes_pushed (evt_...) project=my-tool
                  → Install Locally: ok — Installed locally
                    local_install_completed (evt_...) project=my-tool
          → Cut Release: ok — Skipped: main branch is dirty

Notice that Cut Release still appears in the trace — it received the event but self-filtered and returned an empty result.

Audit only

Observers run and emit, mutators run but suppress downstream events:

foundry emit vulnerability_detected \
  --project my-tool \
  --throttle audit_only \
  --payload '{"cve": "CVE-2026-1234", "vulnerable": true, "dirty": true}'

This tells you what would happen without actually remediating, committing, or releasing. The audit blocks produce their findings, but the chain stops at main_branch_audited.

Dry run

Only observers execute. Mutators are skipped entirely:

foundry emit vulnerability_detected \
  --project my-tool \
  --throttle dry_run \
  --payload '{"cve": "CVE-2026-1234", "vulnerable": true, "dirty": true}'

The chain produces vulnerability_detectedrelease_tag_auditedmain_branch_audited and stops. No remediation, no commits, no releases.

Clean Path Example

When the main branch is already fixed but no release has been cut:

foundry emit vulnerability_detected \
  --project my-tool \
  --payload '{"cve": "CVE-2026-5678", "vulnerable": true, "dirty": false}'

The trace shows the release path instead:

vulnerability_detected (evt_...) project=my-tool
  → Audit Release Tag: ok — ...
    release_tag_audited (evt_...) project=my-tool
      → Audit Main Branch: ok — Main branch audited: CVE-2026-5678 dirty=false
        main_branch_audited (evt_...) project=my-tool
          → Remediate Vulnerability: ok — Skipped: main branch is clean
          → Cut Release: ok — Cut patch release for CVE-2026-5678
            release_completed (evt_...) project=my-tool
              → Watch Pipeline: ok — Release pipeline completed successfully
                release_pipeline_completed (evt_...) project=my-tool
                  → Install Locally: ok — Installed locally
                    local_install_completed (evt_...) project=my-tool

Not Vulnerable

If the release tag has no vulnerability, the chain stops early:

foundry emit vulnerability_detected \
  --project my-tool \
  --payload '{"cve": "CVE-2026-9999", "vulnerable": false}'

Only two events: vulnerability_detectedrelease_tag_audited. The Audit Main Branch block sees vulnerable=false and emits nothing.

Payload Fields

FieldUsed ByValuesDefault
cveAll blocksCVE identifier string"unknown"
vulnerableAudit Release Tag, Audit Main Branchtrue / falsetrue
dirtyAudit Main Branch, Remediate, Cut Releasetrue / falsetrue

Real Implementations

All blocks in the vulnerability remediation workflow perform real work:

  • Audit Release Tag — shells out to the stack-appropriate tool (cargo audit --json, npm audit --json, pip-audit --format=json, mix deps.audit) via the ScannerGateway abstraction
  • Audit Main Branch — checks out the main branch and runs the same scan
  • Remediate Vulnerability — invokes the configured AI agent (e.g. via the claude CLI) to apply a fix
  • Commit and Push — runs git add, git commit, and optionally git push
  • Cut Release — creates and pushes a git tag using the AI agent
  • Watch Pipeline — polls the GitHub Actions API until the release workflow completes or times out
  • Install Locally — runs the project's configured install command (e.g. cargo install --path .) or Homebrew formula after a successful release

Iteration Workflow

The iteration workflow is Foundry's code quality improvement engine. It identifies the most-violated engineering principle in a project, creates a targeted fix plan, executes it, and verifies the result against quality gates — with automatic retries on failure.

The Chain

flowchart TD
    A([iteration_requested]) --> B[[Resolve Gates]]
    B --> C([gate_resolution_completed])
    C --> D[[Run Preflight Gates]]
    D --> E([preflight_completed])
    E --> F[[Check Charter]]
    F --> G([charter_validated])
    G --> H[[Assess Project]]
    H --> I([project_assessed])
    I --> J[[Triage Assessment]]
    J --> K([assessment_triaged])
    K --> L[[Create Plan]]
    L --> M([plan_created])
    M --> N[[Execute Plan]]
    N --> O([execution_completed])
    O --> P[[Run Verify Gates]]
    P --> Q([gate_verification_completed])
    Q --> R[[Route Gate Result]]
    R -->|pass| S([project_iteration_completed])
    R -->|fail, retries left| T[[Retry Execution]]
    T --> O
    R -->|fail, retries exhausted| U([project_iteration_completed — failure])
    S -->|maintain=true| V([maintenance_requested])

Phase by Phase

1. Gate Resolution

Resolve Gates reads .hone-gates.json from the project directory and emits the gate definitions with workflow: "iterate". The actions object from the trigger event is forwarded through the chain so that gate routing can chain into the maintenance workflow afterwards.

2. Preflight Gates

Run Preflight Gates executes every gate against the unmodified codebase. If any required gate fails, all_passed is false and the chain stops — Assess Project self-filters on preflight success.

This establishes a baseline: the iterate workflow only attempts improvements when the project is already in a passing state.

3. Charter Validation

Check Charter verifies that the project has intent documentation (e.g. CHARTER.md). If the charter is missing, passed is false and the chain stops at this point — there is no assessment without context.

4. Assessment

Assess Project invokes an AI agent with reasoning capability and read-only access to analyse the codebase. It identifies the single most-violated engineering principle, returning a severity score (1–10), the principle name, category, and a detailed assessment.

A second quick agent call generates a kebab-case audit filename for traceability.

5. Triage

Triage Assessment uses a quick agent call to decide whether the finding is worth acting on. It rejects issues with severity below 4 or findings that amount to busy-work rather than substantive improvement. On agent failure, triage defaults to accepted — it is better to attempt a fix than to silently skip.

If triage rejects the finding, Create Plan self-filters on accepted=false and the chain stops cleanly.

6. Plan Creation

Create Plan invokes an AI agent with reasoning capability and read-only access to produce a step-by-step correction plan. The plan is concrete — it names exact files and functions — minimal, and testable. Gate definitions are forwarded so the execution phase knows what must pass.

7. Execution

Execute Plan is the only Mutator in the assessment-to-execution pipeline. It invokes an AI agent with coding capability and full filesystem access. The agent receives the plan plus gate context (the gates it must satisfy) and applies the changes.

Under audit_only or dry_run throttle, this block returns a simulated success without modifying any files.

8. Gate Verification

Run Verify Gates re-reads .hone-gates.json from disk (a fresh read, not cached from phase 1) and runs every gate against the modified codebase. The retry_count from the payload tracks which attempt this is.

9. Routing and Retry

Route Gate Result makes the terminal decision:

ConditionAction
All required gates passEmit project_iteration_completed (success)
Required gates fail, retry count < 3Emit retry_requested with failure context
Required gates fail, retry count ≥ 3Emit project_iteration_completed (failure)

When retrying, Retry Execution receives the failure context (which gates failed and their output) and invokes a coding agent to fix only the issues causing those failures. The result loops back to Run Verify Gates for another round.

The maximum is 3 retries (4 total attempts: 1 initial + 3 retries).

10. Chaining to Maintenance

On success, if actions.maintain=true was forwarded through the chain, Route Gate Result also emits maintenance_requested. This triggers the Maintenance Workflow without re-querying the project configuration.

Self-Filtering

Several blocks use self-filtering to stop the chain gracefully without errors:

  • Assess Project — skips when workflow != "iterate" or preflight failed
  • Check Charter — skips when workflow != "iterate"
  • Create Plan — skips when accepted != true (triage rejected)
  • Summarize Result — skips when success != true

The engine routes by event type only and cannot inspect payloads. Each block checks the relevant payload fields and returns an empty result when the condition does not match.

Running the Workflow

Direct trigger

To run the iterate workflow for a single project:

foundry emit iteration_requested my-project \
  --payload '{"actions":{"iterate":true,"maintain":false}}'

With maintenance chaining

To iterate and then run maintenance:

foundry emit iteration_requested my-project \
  --payload '{"actions":{"iterate":true,"maintain":true}}'

Via the maintenance run

The full maintenance lifecycle triggers iteration automatically when iterate=true in the project's registry entry:

foundry emit maintenance_run_started my-project

Audit only

Observers run and emit, but mutators suppress downstream events:

foundry emit iteration_requested my-project \
  --throttle audit_only \
  --payload '{"actions":{"iterate":true,"maintain":false}}'

This runs the assessment and triage phases but stops at execution — useful for seeing what would be improved without modifying any files.

Dry run

Only observers execute. Mutators are skipped entirely:

foundry emit iteration_requested my-project \
  --throttle dry_run \
  --payload '{"actions":{"iterate":true,"maintain":false}}'

Agent Capabilities

Foundry delegates AI work to the Claude CLI, mapping each block's capability hint to a concrete model:

CapabilityModelUse Case
Reasoningclaude-opus-4-6Deep analysis and planning
Codingclaude-sonnet-4-6Code generation and modification
Quickclaude-haiku-4-5-20251001Fast, lightweight decisions

Access levels control which CLI tools the agent may use:

AccessAllowed Tools
Read-onlyRead, Glob, Grep, WebFetch, WebSearch
FullAll tools (no restrictions)

Each phase in the iterate workflow maps to a specific capability and access level:

PhaseCapabilityModelAccessPurpose
AssessmentReasoningOpusRead-onlyDeep analysis of codebase
Audit namingQuickHaikuRead-onlyGenerate kebab-case filename
TriageQuickHaikuRead-onlyAccept/reject decision
Plan creationReasoningOpusRead-onlyStep-by-step correction plan
ExecutionCodingSonnetFullApply code changes
RetryCodingSonnetFullFix gate failures
SummarisationQuickHaikuRead-onlyGenerate headline and summary

All agent invocations use the --print flag (non-interactive output) and --dangerously-skip-permissions (unattended execution). Blocks that reference a project agent file pass it via --agent. Timeouts are set per-project from the registry entry, except Triage and Summarisation which use a fixed 120-second timeout for their lightweight Quick calls.

Payload Fields

FieldCarried ByPurpose
actionsAll events{iterate, maintain} flags for workflow routing
gatesGates resolved through plan creationGate definitions for execution context
audit_nameAssessment through plan creationKebab-case audit filename for traceability
severityAssessment through plan creationViolation severity (1–10)
principleAssessment through plan creationName of the violated principle
categoryAssessment through plan creationCategory of the violation
assessmentAssessment through plan creationDetailed assessment text
retry_countExecution through gate routingCurrent retry attempt (0-based)
failure_contextRetry requestedGate output from failed verification

Strategic Iteration

Strategic iteration wraps the standard iterate workflow in an outer loop. Instead of finding and fixing one issue, it assesses the project holistically, identifies multiple areas for improvement, and works through them one at a time — committing changes after each cycle and re-evaluating whether further work is warranted.

This is a two-layer nested loop:

  1. Outer loop (strategic) — AI identifies areas to improve, decides when to stop
  2. Inner loop (operational) — the existing iterate formation: assess, triage, plan, execute, verify gates, retry

Prerequisites

Before running a strategic iteration you need the same setup as a standard iterate workflow:

  • A project registered in the Foundry registry (foundry registry add)
  • A CHARTER.md (or equivalent intent documentation) in the project root
  • A .hone-gates.json defining quality gates
  • The foundryd daemon running

If you can successfully run foundry iterate my-project, you are ready for strategic iteration.

Step 1: Start the Daemon

foundryd

Verify the strategic blocks are registered in the startup logs:

INFO foundryd::engine: registered task block block="Strategic Assessor" sinks=[IterationRequested]
INFO foundryd::engine: registered task block block="Strategic Loop Controller" sinks=[StrategicAssessmentCompleted, InnerIterationCompleted]

Step 2: Dry Run First

Before committing to a full strategic run, use dry_run throttle to see the shape of the event chain without modifying any files:

foundry emit iteration_requested my-project \
  --throttle dry_run \
  --payload '{"strategic": true, "max_iterations": 3}'

Watch the event stream in another terminal:

foundry watch --project my-project

You should see events flowing through the strategic assessment, into the inner iterate chain, and back to the strategic controller. Mutator blocks (Execute Plan, Commit and Push) emit simulated success events under dry_run.

Step 3: Run a Strategic Iteration

foundry emit iteration_requested my-project \
  --payload '{"strategic": true, "max_iterations": 5}'

What happens

flowchart TD
    A([iteration_requested<br/>strategic=true]) --> B[[Strategic Assessor]]
    B -->|AI holistic assessment| C([strategic_assessment_completed])
    C --> D[[Strategic Loop Controller]]
    D -->|pick first area| E([iteration_requested<br/>with loop_context])
    E --> F[Inner Iterate Formation]
    F --> G([inner_iteration_completed])
    G --> H[[Commit and Push<br/>per-iteration commit]]
    G --> D
    D -->|AI: continue?| I{More work?}
    I -->|yes| E
    I -->|no| J([project_iteration_completed])
    J --> K[[Summarize Result]]
    J --> L[[Commit and Push]]
  1. Strategic Assessor analyses the codebase and produces a ranked list of improvement areas (e.g., "test coverage in auth module", "inconsistent error handling in API layer").

  2. Strategic Loop Controller picks the top area and enters the inner iterate formation by emitting iteration_requested with a loop_context payload.

  3. The inner iterate formation runs exactly as described in the Iteration Workflow guide — charter check, gate resolution, preflight, assessment, triage, plan, execute, verify, retry. The only difference is that Route Gate Result emits inner_iteration_completed instead of project_iteration_completed.

  4. Commit and Push fires on inner_iteration_completed, creating a per-iteration commit so each improvement is isolated in git history.

  5. Strategic Loop Controller receives inner_iteration_completed and asks an AI agent: "Is there more meaningful work to do, or has the codebase reached a plateau?" Based on the response and the iteration cap, it either re-enters the inner loop or completes.

  6. When the loop completes, the controller emits project_iteration_completed without loop_context, which triggers Summarize Result and a final Commit and Push.

Step 4: Monitor Progress

Watch the live event stream:

foundry watch --project my-project

Key events to look for:

EventMeaning
strategic_assessment_completedAI identified areas; check areas in payload
iteration_requested (with loop_context)Inner loop entered for an area
inner_iteration_completedOne cycle done; check success
project_changes_committedPer-iteration commit created
project_iteration_completedStrategic loop finished
summarize_completedFinal summary generated

Step 5: Review the Trace

After completion, inspect the full event chain:

foundry trace <event-id>

Use the event ID from the original iteration_requested event. The trace shows every block that executed, what it emitted, and how long it took — across all iterations of the loop.

Payload Reference

Entry payload

FieldTypeDefaultPurpose
strategicboolfalseMust be true to activate strategic mode
max_iterationsinteger5Maximum number of inner loop cycles
strategic_promptstring(built-in)Custom directive for the assessment and continue checks (see below)
actionsobjectOptional {maintain: true} to chain maintenance after the loop completes

loop_context (internal)

The loop_context object is managed by the strategic blocks. You do not set it manually — it is created by the Strategic Assessor and threaded through the chain automatically.

{
  "loop_context": {
    "strategic": {
      "iteration": 2,
      "max": 5,
      "total_areas": 3,
      "current_area": {"area": "test coverage", "severity": 8}
    }
  }
}

All blocks in the iterate chain forward loop_context transparently. Terminal blocks (Summarize Result, Commit and Push) skip when they see it on a completion event — they only fire on the final completion that the Strategic Loop Controller emits without loop_context.

Controlling the Loop

Iteration cap

The max_iterations field is the hard stop. Even if the AI wants to continue, the loop completes when this count is reached. Start with a low number (2–3) and increase once you are comfortable with the results.

Custom prompt

By default the Strategic Assessor analyses broad code quality and the continue check looks for remaining violations. You can replace both directives with a strategic_prompt to steer the loop toward a specific goal:

foundry emit iteration_requested my-project \
  --payload '{
    "strategic": true,
    "max_iterations": 5,
    "strategic_prompt": "Pick the highest priority interaction from et and implement it."
  }'

The prompt you supply is used in two places:

  1. Assessment — the Strategic Assessor wraps your prompt in a request to produce the areas JSON. The AI reads the codebase with your directive as context and returns a ranked list of work items.
  2. Continue check — after each inner iteration, the Strategic Loop Controller asks the AI the same prompt again to decide whether the loop should continue. The AI responds with {"continue": true/false}.

The prompt is stored inside loop_context.strategic.prompt so it survives the full inner chain and is available on every re-entry.

Some examples:

GoalPrompt
Product work from a backlog"Pick the highest priority interaction from et and implement it."
Focused refactoring"Find the module with the worst test coverage and add tests."
Dependency modernisation"Identify deprecated dependencies and upgrade them one at a time."
Security hardening"Find the most critical OWASP Top 10 risk in this codebase and fix it."

AI continue assessment

After each inner iteration, the Strategic Loop Controller asks an AI agent whether further improvement is warranted. When no strategic_prompt is set, the agent considers:

  • Are there remaining violations with severity >= 4?
  • Would another iteration produce meaningful improvement?
  • Has the codebase reached a diminishing-returns plateau?

When strategic_prompt is set, your prompt replaces this default reasoning — the AI uses your directive to decide whether more work remains.

If the inner iteration failed (gates did not pass after retries), the loop stops immediately rather than attempting further iterations on a broken codebase.

Throttle interaction

ThrottleStrategic AssessorInner IterateCommits
fullRunsRuns (modifies files)Real commits
audit_onlyRunsRuns, but mutator events suppressedNo commits
dry_runRunsSimulated successNo commits

Chaining with Maintenance

To run a strategic iteration and then chain into maintenance:

foundry emit iteration_requested my-project \
  --payload '{"strategic": true, "max_iterations": 3, "actions": {"maintain": true}}'

Maintenance chaining is suppressed during the inner loop (to prevent maintenance from running after every cycle). It only fires after the strategic loop completes — the final project_iteration_completed event triggers Route Gate Result's normal chaining logic.

Example: Full Session

# Terminal 1: start daemon
foundryd

# Terminal 2: watch events
foundry watch --project my-api

# Terminal 3: trigger strategic iteration
foundry emit iteration_requested my-api \
  --payload '{"strategic": true, "max_iterations": 3}'

# After completion, review the trace
foundry trace evt_abc123def456

Expected event flow for a 2-iteration run:

iteration_requested (strategic=true)
  strategic_assessment_completed (areas: [test coverage, error handling])
    iteration_requested (loop_context: iteration=1)
      charter_check_completed
      gate_resolution_completed
      preflight_completed
      assessment_completed
      triage_completed
      plan_completed
      execution_completed
      gate_verification_completed
      inner_iteration_completed (success=true)
        project_changes_committed
    iteration_requested (loop_context: iteration=2)
      charter_check_completed
      ...
      inner_iteration_completed (success=true)
        project_changes_committed
    project_iteration_completed (no loop_context — loop done)
      summarize_completed
      project_changes_committed

Agent Capabilities

Strategic iteration adds two blocks with their own agent invocations on top of the standard iterate chain:

PhaseCapabilityModelAccessPurpose
Strategic AssessorReasoningclaude-opus-4-6Read-onlyHolistic codebase analysis producing ranked improvement areas
Continue checkQuickclaude-haiku-4-5-20251001Read-onlyDecide whether the loop should continue after each cycle

The inner iterate formation uses the same model assignments described in the Iteration Workflow guide.

When a strategic_prompt is set, both the assessment and continue-check prompts incorporate it — the same custom directive steers area selection and termination decisions.

Comparison with Standard Iterate

AspectStandard IterateStrategic Iterate
Entry payload{} or {actions: ...}{strategic: true, max_iterations: N}
ScopeOne issue per runMultiple issues per run
CommitsOne at the endOne per inner cycle + one final
Continue logicNone (single pass)AI re-assessment after each cycle
Inner retryUp to 3 retries on gate failureSame (unchanged)
Maintenance chainingAfter single completionAfter loop completion only

Maintenance Workflow

The maintenance workflow runs iterate and maintain automation against each registered project, committing and pushing any changes they produce. It is designed to be triggered nightly via launchd or manually via foundry emit.

How It Works

Each project goes through its own independent chain. The chain is driven entirely by events — no mutable state is shared between projects.

Per-Project Chain

flowchart TD
    A([maintenance_run_started]) --> B[[Validate Project]]
    B --> C([project_validation_completed])
    C --> D[[Route Project Workflow]]
    D -->|iterate=true| E([iteration_requested])
    D -->|iterate=false, maintain=true| F([maintenance_requested])
    D -->|no actions enabled| G([end])
    E --> H[[Resolve Gates]]
    H --> I[[Run Preflight Gates]]
    I --> J[[Check Charter]]
    J --> K[[Assess Project]]
    K --> L[[Triage Assessment]]
    L --> M[[Create Plan]]
    M --> N[[Execute Plan]]
    N --> O[[Run Verify Gates]]
    O --> P[[Route Gate Result]]
    P -->|pass| Q([project_iteration_completed])
    P -->|fail, retries left| R[[Retry Execution]]
    Q -->|maintain=true| F
    F --> S[[Resolve Gates]]
    S --> T[[Execute Maintain]]
    T --> U[[Run Verify Gates]]
    U --> V[[Route Gate Result]]
    V -->|pass| W([project_maintenance_completed])
    V -->|fail, retries left| X[[Retry Execution]]

Routing Logic

Route Project Workflow reads the actions flags forwarded in the project_validation_completed payload and makes a single decision:

ConditionEmits
status != "ok"nothing — chain stops
actions.iterate = trueiteration_requested
actions.iterate = false, actions.maintain = truemaintenance_requested
both falsenothing — no automation enabled

When iterate = true, the actions.maintain flag is forwarded inside the iteration_requested payload. After a successful iteration, the gate routing emits maintenance_requested automatically when that flag is true, so the maintain sub-workflow starts without an extra routing step.

Triggering a Maintenance Run

To run maintenance for a single project:

foundry emit project_validation_completed my-project \
  --payload '{"status":"ok","actions":{"iterate":true,"maintain":true}}'

To trigger the full nightly cycle:

foundry emit maintenance_run_started my-project

Throttle Behaviour

ThrottleEffect
fullAll blocks execute and emit events
audit_onlyObservers emit; mutators suppress output
dry_runObservers emit; mutators are skipped entirely

Under dry_run, only iteration_requested or maintenance_requested are emitted (by the Observer router). No execution blocks run.

Agent Capabilities

The maintenance workflow uses a single agent invocation in Execute Maintain:

PhaseCapabilityModelAccessPurpose
Execute MaintainCodingclaude-sonnet-4-6FullUpdate dependencies, fix vulnerabilities, resolve gate failures

Gate definitions are passed as context so the agent knows what must pass after its changes. If the project has an agent file registered, it is supplied via --agent.

See the Iteration Workflow for the full model-to-capability mapping and CLI parameters used across all agent invocations.

Throttle Control

Throttle controls how far an event ripples through the task block chain. It's set at invocation time and propagated through every event in the chain.

Levels

LevelObserversMutatorsUse case
fullExecute + emitExecute + emitAutomated runs, nightly maintenance
audit_onlyExecute + emitExecute + suppress emission"Just check" — audit without releasing
dry_runExecute + emitSkip execution entirelyPreview what would happen

How It Works

The throttle is a property of the event, not the task block. When a block emits downstream events, those events carry the same throttle as the triggering event. This means the throttle decision is made once (at invocation) and respected throughout the chain.

foundry emit vulnerability_detected --project my-tool --throttle audit_only

  vulnerability_detected (throttle: audit_only)
    → Audit Main Branch (Observer) → executes, emits main_branch_audited
      → Cut Release (Mutator) → executes, but SUPPRESSES release_completed
        (chain stops here — no further propagation)

Observer vs Mutator

The key design question for every task block: is it an Observer or a Mutator?

  • Observer: reads state, runs scans, checks conditions. Never changes the world. Always runs, always emits, regardless of throttle.
  • Mutator: writes files, pushes commits, cuts releases, installs tools. Changes the world. Throttle controls whether it runs and emits.

At audit_only, a Mutator still executes (so you see what it would do in the logs) but suppresses emission (so downstream blocks don't fire). At dry_run, Mutators don't execute at all.

CLI Usage

# Default: full
foundry emit greet_requested --project hello

# Explicit throttle
foundry emit greet_requested --project hello --throttle audit_only
foundry emit greet_requested --project hello --throttle dry_run

Writing Task Blocks

To add a new task block to Foundry:

  1. Implement the TaskBlock trait
  2. Register it with the engine in main.rs

The TaskBlock Trait

#![allow(unused)]
fn main() {
use std::pin::Pin;
use foundry_core::event::{Event, EventType};
use foundry_core::task_block::{BlockKind, TaskBlock, TaskBlockResult};

pub struct MyBlock;

impl TaskBlock for MyBlock {
    fn name(&self) -> &'static str {
        "My Block"
    }

    fn kind(&self) -> BlockKind {
        BlockKind::Observer  // or BlockKind::Mutator
    }

    fn sinks_on(&self) -> &[EventType] {
        &[EventType::GreetRequested]  // which events trigger this block
    }

    fn execute(
        &self,
        trigger: &Event,
    ) -> Pin<Box<dyn std::future::Future<Output = anyhow::Result<TaskBlockResult>> + Send + '_>>
    {
        let project = trigger.project.clone();
        let throttle = trigger.throttle;

        Box::pin(async move {
            // Do your work here...

            Ok(TaskBlockResult {
                events: vec![
                    Event::new(
                        EventType::GreetingComposed,
                        project,
                        throttle,
                        serde_json::json!({"result": "done"}),
                    ),
                ],
                success: true,
                summary: "Did the thing".to_string(),
                raw_output: None,
                exit_code: None,
                audit_artifacts: vec![],
            })
        })
    }
}
}

Key Points

  • Propagate throttle: always pass trigger.throttle to emitted events
  • Clone what you need: extract data from trigger before the async move block
  • Return events: the engine handles routing them to downstream blocks
  • Observer vs Mutator: choose based on whether your block has side effects

TaskBlockResult Fields

FieldTypeDescription
eventsVec<Event>Events to emit downstream (subject to throttle)
successboolWhether the block's work succeeded
summaryStringHuman-readable one-line summary shown in traces
raw_outputOption<String>Combined stdout+stderr from any shell command — shown in foundry trace --verbose
exit_codeOption<i32>Exit code from any shell command — useful for observability
audit_artifactsVec<String>Paths to files produced by this block (e.g. audit logs). Listed under artifacts: in verbose trace output

Blocks that do not run external processes should set raw_output: None, exit_code: None, and audit_artifacts: vec![]. Blocks that shell out should populate raw_output with the combined output and exit_code with the process exit code so that traces provide full observability without needing to reproduce the command.

Registering

In foundryd/src/main.rs:

#![allow(unused)]
fn main() {
let mut engine = engine::Engine::new();
engine.register(Box::new(blocks::MyBlock));
}

RetryPolicy

Override retry_policy() to enable automatic retry of transient failures. The default is zero retries (execute exactly once).

#![allow(unused)]
fn main() {
use std::time::Duration;
use foundry_core::task_block::RetryPolicy;

fn retry_policy(&self) -> RetryPolicy {
    RetryPolicy {
        max_retries: 3,
        backoff: Duration::from_secs(5),
    }
}
}

With max_retries: N, the engine tries the block up to N + 1 times total (1 initial attempt plus up to N retries), sleeping backoff between each attempt. Both Err results and TaskBlockResult { success: false, .. } trigger a retry. The final attempt's outcome is what appears in the BlockExecution trace.

Use retries for operations that may fail transiently (network calls, shell commands that occasionally time out). Do not use retries for operations that are expected to fail deterministically (e.g. self-filtering by payload).

Gateway Pattern

Task blocks that execute external processes (shell commands, audit tools) receive those capabilities through gateway traits rather than calling the implementation directly. This isolates I/O at the block boundary and makes every block fully testable without spawning real processes.

The ShellGateway Trait

#![allow(unused)]
fn main() {
pub trait ShellGateway: Send + Sync {
    fn run<'a>(
        &'a self,
        working_dir: &'a Path,
        command: &'a str,
        args: &'a [&'a str],
        env: Option<&'a [(String, String)]>,
        timeout: Option<Duration>,
    ) -> Pin<Box<dyn Future<Output = anyhow::Result<CommandResult>> + Send + 'a>>;
}
}

In production, ProcessShellGateway delegates to crate::shell::run. Blocks accept the gateway through their constructor:

#![allow(unused)]
fn main() {
pub struct MyBlock {
    registry: Arc<Registry>,
    shell: Arc<dyn ShellGateway>,
}

impl MyBlock {
    pub fn new(registry: Arc<Registry>) -> Self {
        Self {
            registry,
            shell: Arc::new(ProcessShellGateway),
        }
    }

    #[cfg(test)]
    fn with_shell(registry: Arc<Registry>, shell: Arc<dyn ShellGateway>) -> Self {
        Self { registry, shell }
    }
}
}

Testing with Fakes

gateway::fakes (available only under #[cfg(test)]) provides pre-built fakes:

#![allow(unused)]
fn main() {
use crate::gateway::fakes::{FakeShellGateway, FakeScannerGateway};
use crate::shell::CommandResult;

// Always return a successful, empty result.
let shell = FakeShellGateway::success();

// Always return a failure with the given stderr.
let shell = FakeShellGateway::failure("not installed");

// Return a fixed result every time.
let shell = FakeShellGateway::always(CommandResult { ... });

// Return results in sequence (last one repeats).
let shell = FakeShellGateway::sequence(vec![first_result, second_result]);

// Inspect recorded invocations after the fact.
let invocations = shell.invocations();
assert_eq!(invocations[0].command, "git");
}

For scanner-based blocks:

#![allow(unused)]
fn main() {
let scanner = FakeScannerGateway::clean();
let scanner = FakeScannerGateway::with_vulnerabilities(vec![...]);
let scanner = FakeScannerGateway::with_error("cargo audit not installed");
}

This pattern allows testing every code path — including failure modes and edge cases — without any real I/O:

#![allow(unused)]
fn main() {
#[tokio::test]
async fn detached_head_recovery_succeeds() {
    let dir = tempfile::tempdir().expect("tempdir");
    let registry = make_registry(/* ... */);

    // First call returns "HEAD" (detached); second call (checkout) succeeds.
    let shell = FakeShellGateway::sequence(vec![
        CommandResult { stdout: "HEAD\n".into(), exit_code: 0, success: true, .. },
        CommandResult { stdout: String::new(), exit_code: 0, success: true, .. },
    ]);
    let block = ValidateProject::with_shell(registry, shell);

    let result = block.execute(&trigger).await.unwrap();
    assert_eq!(result.events[0].payload["status"], "ok");
}
}

File Organisation

Place block implementations in foundryd/src/blocks/:

blocks/
├── mod.rs              # pub use declarations
├── greet.rs            # hello-world blocks (ComposeGreeting, DeliverGreeting)
├── validate.rs         # ValidateProject
├── resolve_gates.rs    # ResolveGates
├── run_preflight_gates.rs  # RunPreflightGates
├── run_verify_gates.rs     # RunVerifyGates
├── route_gate_result.rs    # RouteGateResult
├── route_validation_result.rs # RouteValidationResult
├── check_charter.rs    # CheckCharter
├── assess_project.rs   # AssessProject
├── triage_assessment.rs # TriageAssessment
├── create_plan.rs      # CreatePlan
├── execute_plan.rs     # ExecutePlan
├── execute_maintain.rs # ExecuteMaintain
├── retry_execution.rs  # RetryExecution
├── summarize_result.rs # SummarizeResult
├── git_ops.rs          # CommitAndPush
├── audit.rs            # AuditReleaseTag, AuditMainBranch
├── release.rs          # CutRelease, WatchPipeline
├── install.rs          # InstallLocally
├── remediate.rs        # RemediateVulnerability
└── scan.rs             # ScanDependencies

CLI Commands

The foundry CLI communicates with a running foundryd daemon over gRPC.

Global Options

OptionDefaultDescription
--addr <url>http://127.0.0.1:50051Daemon address

foundry emit

Emit an event into the system. May trigger a workflow chain.

foundry emit <event_type> --project <project> [--throttle <level>] [--payload <json>] [--wait]
ArgumentRequiredDescription
event_typeYesEvent type name (positional)
--projectYesTarget project
--throttleNofull, audit_only, or dry_run (default: full)
--payloadNoJSON string with event-specific data
--waitNoBlock until processing completes, then display the trace

By default, emit returns immediately after the daemon accepts the event. Use --wait to block until the full event chain finishes and then display the trace output (equivalent to running foundry trace after completion).

Output (default):

Event emitted: evt_47fcb603e1b18c8435b8cc3b

Output (with --wait):

Event emitted: evt_47fcb603e1b18c8435b8cc3b
Waiting for processing to complete...
greet_requested (evt_47fcb603e1b18c8435b8cc3b) project=hello
  → Compose Greeting (1ms): ok — composed greeting for Stacey
    greeting_composed (evt_a1b2c3d4e5f6) project=hello
      → Deliver Greeting (0ms): ok — delivered greeting: Hello, Stacey!
---
Total: 2ms (blocks: 1ms)

foundry status

Show status of active workflows. Queries the daemon for workflows that are currently being processed in the background. The daemon tracks these via an in-memory WorkflowTracker that is populated when each Emit request spawns a background task and cleared on completion.

foundry status [workflow_id]

Without an argument, shows all active workflows. With a workflow ID, shows details for that specific workflow.

Output example:

evt_47fcb603e1b18c8435b8cc3b [iteration_requested] foundry — running

If no workflows are currently running:

No active workflows.

foundry watch

Stream live events as they are emitted in real time.

foundry watch [--project <project>]
OptionRequiredDescription
--projectNoFilter by project name; omit to see all projects

Server-side streaming — stays open until interrupted (Ctrl-C). Each line shows the event type, event ID, project, and payload (when non-empty).

Output example:

maintenance_run_started evt_abc project=my-tool
project_validation_completed evt_def project=my-tool
  payload: {"status":"ok","has_gates":true}
project_iteration_completed evt_ghi project=my-tool

foundry run

Trigger a maintenance run for all active projects or a single named project.

foundry run [--project <project>] [--throttle <level>]
OptionRequiredDescription
--projectNoLimit run to a single project by name; omit to run all projects
--throttleNofull, audit_only, or dry_run (default: full)

foundry run emits a maintenance_run_started event which triggers the maintenance workflow chain: validate → iterate (if enabled) → maintain (if enabled) → commit and push → post-push audit.

The command streams progress events in real time and exits automatically when the daemon broadcasts a maintenance_run_completed event at the end of the processing chain. This differs from foundry watch, which streams indefinitely.

When --project is omitted, the project name sent to the daemon is "system", which causes all active (non-skipped) projects to be processed.

Output:

Triggered maintenance run for my-tool
Event: evt_47fcb603e1b18c8435b8cc3b

[my-tool] maintenance_run_started
[my-tool] project_validation_completed (ok)
[my-tool] maintenance_run_completed (ok)

Use foundry trace <event_id> to inspect the full trace after the run completes.

foundry validate

Validate quality gates for one or more projects without running iterate or maintain workflows. This is a read-only operation — no code changes are made.

foundry validate <project>...
foundry validate --all
ArgumentRequiredDescription
projectYes (unless --all)One or more project names (positional)
--allNoValidate all active projects in the registry

For each project, emits a validation_requested event which triggers: Resolve GatesRun Preflight GatesRoute Validation Resultvalidation_completed. No Mutator blocks are involved, so throttle level is irrelevant.

Output example:

Validating mojentic-ts...
  mojentic-ts: PASS
    lint: ok (required)
    format: ok (required)
    test: ok (required)
    build: ok (required)
    security: ok (optional)
validation_requested (evt_007572156d627d7b1211d76f) project=mojentic-ts
  → Resolve Gates (0ms): ok — mojentic-ts: resolved 5 gates for validate workflow
    gate_resolution_completed (evt_92531a666649d6464e569dc2) project=mojentic-ts
      → Run Preflight Gates (6931ms): ok — mojentic-ts: preflight gates passed
        preflight_completed (evt_08b0f626599a23ee8c648a8c) project=mojentic-ts
          → Route Validation Result (3ms): ok — mojentic-ts: validation passed
            validation_completed (evt_e60a246dfa9072414890fa24) project=mojentic-ts
---

Exits with code 0 if all projects pass, non-zero if any required gate fails. Optional gate failures are reported but do not affect the exit code.

foundry trace

View the trace of a completed event chain.

foundry trace <event_id> [--verbose]
ArgumentRequiredDescription
event_idYesRoot event ID returned by foundry emit (positional)
--verboseNoShow trigger payloads, emitted payloads, raw shell output, and audit artifact paths

Displays the full event propagation tree with block execution results. Traces are stored on disk indefinitely under ~/.foundry/traces/YYYY-MM-DD/ and survive daemon restarts.

Output (default):

greet_requested (evt_47fcb603e1b18c8435b8cc3b) project=hello
  → ComposeGreeting: ok — composed greeting for Stacey
    greeting_composed (evt_a1b2c3d4e5f6) project=hello
      → DeliverGreeting: ok — delivered greeting: Hello, Stacey!
        greeting_delivered (evt_f6e5d4c3b2a1) project=hello
---
Total: 2ms (blocks: 1ms)

Output (with --verbose):

greet_requested (evt_47fcb603e1b18c8435b8cc3b) project=hello
  → ComposeGreeting (1ms): ok — composed greeting for Stacey
    trigger: {"name":"Stacey"}
    emitted[0]: {"greeting":"Hello, Stacey!"}
    greeting_composed (evt_a1b2c3d4e5f6) project=hello
      → DeliverGreeting (0ms): ok — delivered greeting: Hello, Stacey!
---
Total: 2ms (blocks: 1ms)

If the trace is unknown:

No trace found for evt_unknown (expired or unknown).

foundry history

Browse completed traces stored on disk.

foundry history [<date>] [--project <project>]
ArgumentRequiredDescription
dateNoDate in YYYY-MM-DD format; omit to show the last 7 days
--projectNoFilter results by project name

Traces are read from ~/.foundry/traces/ (or FOUNDRY_TRACES_DIR). Each row shows the event ID, success status, duration, event type, and project. Dates with no traces are omitted.

Output example:

2026-03-22
┌──────────────────────────────┬────────┬──────────┬──────────────────────────┬───────────┐
│ Event ID                     │ Status │ Duration │ Type                     │ Project   │
╞══════════════════════════════╪════════╪══════════╪══════════════════════════╪═══════════╡
│ evt_47fcb603e1b18c8435b8cc3b │ ok     │ 312ms    │ maintenance_run_started  │ my-tool   │
│ evt_a1b2c3d4e5f6789012345678 │ ok     │ 48ms     │ greet_requested          │ hello     │
└──────────────────────────────┴────────┴──────────┴──────────────────────────┴───────────┘

If no traces are found:

No traces found in the last 7 days.

foundry registry

Manage the project registry without editing the JSON file directly.

foundry registry <subcommand>

foundry registry init

Create an empty registry file at the default path (~/.foundry/registry.json). Does nothing if the file already exists.

foundry registry init

foundry registry list

List all projects in the registry as a table.

foundry registry list

Output example:

┌──────────┬────────────┬──────┬──────────────────────────┬───────┐
│ Name     │ Stack      │ Skip │ Actions                  │ Skill │
╞══════════╪════════════╪══════╪══════════════════════════╪═══════╡
│ my-tool  │ rust       │ no   │ iterate, maintain, push  │ auto  │
│ frontend │ typescript │ yes  │ maintain, push           │       │
└──────────┴────────────┴──────┴──────────────────────────┴───────┘

The Skill column shows auto (default derived command), cmd (custom command), off (explicitly disabled), or blank (not configured).

foundry registry show <name>

Show all details for a single project.

foundry registry show my-tool

Output example:

Name:      my-tool
Path:      /Users/alice/projects/my-tool
Stack:     rust
Agent:     claude
Repo:      alice/my-tool
Branch:    main
Skip:      no
Actions:   iterate, maintain, push
Install:   brew: my-tool
Installs skill: yes (default -- runs my-tool init --global --force)
Timeout:   3600s (default)

foundry registry add

Add a new project to the registry. If the registry file does not exist, it is created automatically.

foundry registry add \
  --name my-tool \
  --path /Users/alice/projects/my-tool \
  --stack rust \
  --agent claude \
  --repo alice/my-tool \
  --branch main \
  [--iterate] [--maintain] [--push] [--audit] [--release] \
  [--install-command "cargo install --path ."] \
  [--install-brew my-formula] \
  [--notes "Human-readable notes about the project"] \
  [--timeout-secs 3600]
OptionRequiredDescription
--nameYesUnique project name
--pathYesAbsolute path to the project
--stackYesTechnology stack: rust, python, typescript, elixir, cpp
--agentYesAI agent name (e.g. claude)
--repoYesGitHub slug (owner/repo)
--branchNoDefault branch (default: main)
--iterateNoEnable iterate action
--maintainNoEnable maintain action
--pushNoEnable push action
--auditNoEnable audit action
--releaseNoEnable release action
--install-commandNoShell command to run for local install
--install-brewNoHomebrew formula name
--notesNoHuman-readable notes
--timeout-secsNoCommand timeout in seconds (default: 1800)

foundry registry remove <name>

Remove a project from the registry. Errors if the project is not found.

foundry registry remove my-tool

foundry registry edit <name>

Update settings for an existing project. Only the fields you pass are changed; all others are left as-is.

foundry registry edit my-tool \
  --skip "Waiting for CI to stabilise" \
  --timeout-secs 3600
OptionDescription
--pathUpdate the project path
--stackUpdate the technology stack
--agentUpdate the agent name
--repoUpdate the GitHub slug
--branchUpdate the default branch
--skipSet a skip reason (pass empty string "" to clear the skip)
--iterateSet iterate action (true/false)
--maintainSet maintain action
--pushSet push action
--auditSet audit action
--releaseSet release action
--install-commandSet install command
--install-brewSet Homebrew formula
--notesSet notes (pass empty string "" to clear)
--timeout-secsSet command timeout in seconds

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

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.

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

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

Event Types

All event types are defined in foundry-core/src/event.rs as the EventType enum. The string representation uses snake_case.

Every event carries these common fields:

FieldTypeDescription
idstringDeterministic SHA-256 derived ID, prefixed evt_
event_typestringSnake-case event type name
projectstringProject this event relates to
occurred_atRFC 3339 timestampWhen the event happened
recorded_atRFC 3339 timestampWhen the event was logged
throttlestringfull, audit_only, or dry_run
payloadJSON objectEvent-type-specific fields (see below)

Hello-World (engine validation)

TypeDescription
greet_requestedRequest to compose and deliver a greeting
greeting_composedGreeting message has been composed
greeting_deliveredGreeting has been delivered (side effect)

greet_requested payload

FieldTypeDescription
namestringName to greet

greeting_composed payload

FieldTypeDescription
greetingstringComposed greeting text

Vulnerability Remediation

TypeDescription
scan_requestedRequest to scan a project for known vulnerabilities
vulnerability_detectedA vulnerability was found (or injected externally)
release_tag_auditedLatest release tag scanned for the vulnerability
main_branch_auditedMain branch checked for the same vulnerability
remediation_startedAutomated fix attempt initiated
remediation_completedFix attempt finished (success or failure)

vulnerability_detected payload

FieldTypeDescription
cvestringCVE or advisory ID (e.g. "CVE-2026-1234")
vulnerableboolWhether the project is affected
dirtybool (optional)Whether the main branch still contains the vulnerability

release_tag_audited payload

FieldTypeDescription
cvestringCVE from the scan or forwarded from the trigger
vulnerableboolWhether the release tag is affected
dirtybool (optional)Forwarded from the upstream trigger for downstream routing

main_branch_audited payload

FieldTypeDescription
cvestringCVE identifier
dirtybooltrue if the vulnerability is still present on main

remediation_completed payload

FieldTypeDescription
cvestringCVE that was remediated
successboolWhether the fix was applied successfully

Release Lifecycle

TypeDescription
release_requestedDecision made to cut a patch release
release_completedRelease tag created and pushed
release_pipeline_completedGitHub Actions build/publish workflow finished

release_completed payload

FieldTypeDescription
cvestringCVE that prompted the release
releasestringRelease type (e.g. "patch")
new_tagstring or nullSemver tag extracted from Claude CLI output
successboolWhether the Claude CLI invocation succeeded

release_pipeline_completed payload

FieldTypeDescription
statusstring"success" or "failure"
conclusionstring (optional)GitHub Actions conclusion label

Project Lifecycle

TypeDescription
project_validation_completedPre-flight checks for a maintenance run
project_iteration_completedIterate workflow finished
project_maintenance_completedMaintain workflow finished
project_changes_committedGit commit created
project_changes_pushedChanges pushed to remote

project_validation_completed payload

FieldTypeDescription
statusstring"ok", "error", or "skipped"
reasonstring (optional)Human-readable explanation when status is not "ok"
has_gatesbool (optional)Whether .hone-gates.json is present (only on "ok")

project_iteration_completed payload

FieldTypeDescription
projectstringProject name
workflowstring"iterate"
successboolWhether the iterate workflow succeeded
summarystringHuman-readable summary of the result
changesbool (optional)Whether code changes were made

project_maintenance_completed payload

FieldTypeDescription
projectstringProject name
workflowstring"maintain"
successboolWhether the maintain workflow succeeded
summarystringHuman-readable summary of the result
changesbool (optional)Whether code changes were made

project_changes_committed payload

FieldTypeDescription
cvestringCVE or "unknown" (from remediation path)
messagestringGit commit message used

project_changes_pushed payload

FieldTypeDescription
cvestringCVE or "unknown" (from remediation path)

Local Install

TypeDescription
local_install_completedLocal tool reinstallation finished

Maintenance Workflow

TypePayloadDescription
iteration_requested{ project }Triggers the iterate sub-workflow for a validated project
maintenance_requested{ project }Triggers the maintain sub-workflow for a validated project

Gate Orchestration

TypeDescription
gate_resolution_completedGate definitions loaded from .hone-gates.json
preflight_completedGates passed/failed on unmodified codebase
execution_completedCode changes applied (emitted by future execution blocks)
gate_verification_completedGates passed/failed after execution
retry_requestedGate failure triggers bounded retry

gate_resolution_completed payload

FieldTypeDescription
projectstringProject name
workflowstring"iterate", "maintain", or "validate"
gatesarrayGate definitions (name, command, required, timeout_secs)
actionsobject (optional)Forwarded actions from the trigger event

preflight_completed payload

FieldTypeDescription
projectstringProject name
workflowstringWorkflow that triggered the preflight
all_passedboolWhether every gate passed
required_passedboolWhether all required gates passed
resultsarrayPer-gate results (name, passed, required, output, exit_code)

gate_verification_completed payload

FieldTypeDescription
projectstringProject name
workflowstringOriginating workflow
all_passedboolWhether every gate passed
required_passedboolWhether all required gates passed
retry_countnumberCurrent retry count (0 on first attempt)
resultsarrayPer-gate results

retry_requested payload

FieldTypeDescription
projectstringProject name
workflowstringOriginating workflow
retry_countnumberIncremented retry count
failure_contextstringGate output from the failed verification
actionsobject (optional)Forwarded actions

Validation

TypeDescription
validation_requestedRequest to validate a project's gate health
validation_completedTerminal event with per-gate pass/fail results

validation_requested payload

FieldTypeDescription
projectstringProject name

validation_completed payload

FieldTypeDescription
projectstringProject name
successboolWhether all required gates passed
resultsarrayPer-gate results (name, passed, required, output snippet)

Maintenance Run Lifecycle

TypeDescription
maintenance_run_startedA maintenance run was triggered for a project
maintenance_run_completedAll projects processed, summary available

maintenance_run_started payload

FieldTypeDescription
projectstringProject name this run covers

maintenance_run_completed payload

FieldTypeDescription
totalnumberTotal number of projects processed
succeedednumberProjects that completed successfully
failednumberProjects that encountered an error
skippednumberProjects that were skipped (already active or skip=true)
projectsarrayPer-project result objects (name, status, duration_secs)

Release Tag Audit

TypeDescription
release_tag_auditedLatest release tag scanned (see payload above)