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.

Each gate has a name, a command, a required flag, an optional timeout (seconds; 10 minutes when absent; both multiplied by the daemon's FOUNDRY_GATE_TIMEOUT_SCALE), and an optional fix_command — an in-place command that mechanically repairs the gate's failure. A gate looks like:

{
  "name": "format",
  "command": "cargo fmt --check",
  "required": true,
  "fix_command": "cargo fmt"
}

Only declare a fix_command for gates whose failures are safely auto-fixable — formatters and lint autofixers (cargo fmt, ruff format, biome check --write, a clang-format target). Never for tests, typecheck, build, or security gates, where a "fix" would mask a real problem.

2. Preflight Gates

Run Preflight Gates executes every gate against the unmodified codebase.

When a gate fails and declares a fix_command, the runner runs the fix once and re-checks; a passing re-check resolves the gate (recorded with fix_applied: true on the result), and the in-place changes left in the working tree are committed by the downstream Commit and Push step. This is what lets formatter/lint gates self-heal: without it, a required formatting gate would abort the run before the maintain step that reformats the code — a deadlock where the failing gate blocks its own fix.

If a required gate fails and cannot self-heal (no fix_command, or the fix didn't resolve it), required_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 (or can be brought to one mechanically).

Gate output and full logs

Each gate result carries an output field with a bounded view of what the command printed. The view holds at most 200 lines per command run. Stdout and stderr are tailed separately and share that budget. When both streams are longer than 100 lines, each keeps its last 100 lines. When one stream needs fewer lines, the other stream gets the lines it does not use. A --- stderr --- line separates the two tails when both streams printed something. So a tool that writes thousands of compile lines to stderr cannot push the failing test name on stdout out of the view. A failed self-heal attempt adds the fix or re-check view after an --- autofix attempted, gate still failing --- or --- autofix command failed --- line.

When a gate fails, Foundry also writes the complete stdout and stderr of each command the gate ran. This includes the fix and re-check runs when self-heal was attempted. The files go under ~/.foundry/evidence/gate-logs/, in one directory per gate run. The directory name starts with a UTC timestamp, so the names sort by time (for example 20261016T221500Z-<uuid>/). The files are named for the gate and the run:

coverage.stdout.log
coverage.stderr.log
format.fix.stdout.log
format.recheck.stderr.log

The result's logs field lists the absolute paths. Prompts and reports that show a failed gate to an agent or to you include these paths, and foundry validate prints them under the failed gate. A passing gate writes no files and has no logs field. A stream larger than 8 MiB keeps its last 8 MiB, after a one-line note that gives the original size.

Foundry prunes this directory after each write. It deletes run directories older than 14 days. Then, if the directory is still larger than 1 GiB, it deletes the oldest run directories until the total is 1 GiB or less. The prune only removes directories inside evidence/gate-logs/ and never touches other evidence. If a log cannot be written, Foundry logs a warning and the gate result is unchanged, with an empty logs field.

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.

The plan agent is also asked to emit a machine-readable JSON block at the end of its response declaring whether correction is actually needed:

{ "correctionNeeded": true, "reason": "<one sentence>" }

Setting correctionNeeded to false signals that the agent examined the codebase and found the original assessment inaccurate — the codebase already satisfies the principle and no changes are warranted. This becomes a legitimate no-op (see §7). The flag defaults to true on any parse failure (fail-closed).

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 dry_run throttle, this block returns a simulated success without modifying any files.

Legitimate no-ops — When correctionNeeded is false in the PlanCompleted payload, a clean working tree after execution is treated as a success rather than a flake. The downstream gate verification runs normally and, assuming gates pass, the iteration completes successfully. This mirrors hone's busy-work-containment semantics: an agent that correctly concludes "nothing to do" should not be penalised with retries.

Silent no-op guard — When correctionNeeded is true (the default), an agent that exits successfully but makes no meaningful file changes is overridden to success: false, triggering the retry loop. This prevents agents that silently skip their work from consuming a passing iteration slot.

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

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-5Deep analysis and planning
Codingclaude-sonnet-5Code 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

Agent profile (~/.foundry/agents.json)

Blocks ask for a model tier (deep, balanced, fast) and a reasoning effort (minimal, low, medium, high, max). They never name a concrete model. ~/.foundry/agents.json maps these values to concrete values for each provider. Set FOUNDRY_AGENT_CONFIG_PATH to use a different file.

  • models maps a tier to the model id that the provider CLI gets.
  • effort maps an effort level to the provider CLI's reasoning-effort token.
  • effort_caps (optional) sets the maximum effort for a tier.

The daemon writes the full default seed on first start. On each restart, it adds the provider, tier, and effort keys that are missing. It does not change your model or token edits.

Per-tier effort caps

Several blocks ask for high effort on the balanced tier. Use a cap to limit effort for each model tier, not only for each requested level:

{
  "version": 1,
  "providers": {
    "codex": {
      "models": { "deep": "gpt-6-astra", "balanced": "gpt-5.6-sol" },
      "effort_caps": { "deep": "high", "balanced": "medium" }
    },
    "claude": {
      "effort_caps": { "deep": "high" }
    }
  }
}

Foundry applies the cap before it maps the effort to a CLI token:

  • If the requested effort is more than the tier's cap, Foundry uses the cap. With the example above, a balanced request for high runs at medium.
  • If the requested effort is less than the cap, Foundry does not change it.
  • If a tier has no cap, Foundry does not change the requested effort.

The default seed has no caps. The seed merge does not add, remove, or change caps. Caps are your policy.

The AgentSessionStarted event records both values. effort is the level the block requested. effective_effort is the level the session used after the cap. Compare the two fields to see where a cap applied.

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
correction_neededPlan completed through executionfalse when plan agent concluded no changes are warranted; defaults to true
correction_reasonPlan completed through executionOne-sentence explanation when correction_needed is false