ARCHITECTURE · v0.3.8.67
Autonomy & stop controls
Source: docs/AUTONOMY.md — versioned with the code.
Execution framework note (docs/ANT_EXECUTION.md): autonomous missions use the same expanded role catalog as operator missions — the Director/Strategist plan against
AntRegistry.ExecutableRoleIds, which includes gated specialists only when their rollout gates are open. Planner eligibility ≠ UI visibility (visible-only and deterministic-service roles are never planned). Failure handoffs (tester→medic→repair) pass the bounded HandoffGate (depth/budget/dedupe). Post-mission archival runs only on terminal outcomes; partial or unverified completions never reinforce positively. Capability enforcement at tool dispatch applies identically during autonomy.
Status: Phase 0–5 IMPLEMENTED — the autonomy roadmap is complete. For the current ordered build order across the whole project, see docs/PLAN.md — the canonical roadmap. Rails, Director loop, Strategist, Concurrency, Learning loop, and now Phase 5 gated auto-apply. The colony can now run autonomously: the Director works the objective backlog — up to
autonomy_concurrencymissions at once, sized down live by the ResourceGovernor under host/backend pressure — under budgets and the kill switch, with writes queued for human review. Mission goals are LLM-generated per objective (with a deterministic charter-as-goal fallback), deduped against recent mission history, and the colony can enqueue its own follow-up objectives within depth/rate caps. Scheduling is strict priority with anti-starvation aging plus an outcome-driven learning bias; objectives that stop producing value or loop are auto-paused for review. Target: ANTHILL v1.9.x.
Phase 5 — what landed (gated auto-apply)
The Director can now apply a coder patch without human review — the answer to the approval queue filling up with fixes nobody has time to click through. It is the highest-risk capability in the system (autonomous writes to disk), so it is fail-closed and multiply gated, and the whole safety story is apply → verify → keep-or-rollback.
AutoApplyPolicy(Anthill.Core/Autonomy/AutoApplyPolicy.cs): the pure, side-effect-free eligibility gate. A patch is a candidate only when all hold:autonomy_autoapply_enabledis on; the change isadd/modify(never delete/rename); the file path matches at least one operator glob inautonomy_autoapply_paths(an empty allowlist means nothing is eligible, so the feature is inert until deliberately widened); and the change is withinautonomy_autoapply_max_lines. Glob supports**(subtree),*(within a segment),?.AutoApplyRunner(Anthill.Api/AutoApplyRunner.cs): runs on the Director thread after a successful mission's outcome is recorded. Filters the mission's proposals through the policy, applies the eligible ones to disk (each with a pre-apply backup via the existingapply_patchtool), then runs the verify step — built-indotnet build && dotnet test, or an operatorautonomy_autoapply_verify_cmd— in the workspace root, timeout-bounded (autonomy_autoapply_verify_timeout, default 900s). Green ⇒ the changes stay, the matching approval requests are markedconsumed(so they leave the queue), and — ifautonomy_autoapply_git_commitis on — a localgit add+commit(never pushed). Red or timeout ⇒ every applied patch is rolled back (modify → restore backup, add → delete the file), markedfailed, and logged.- Write-gate dependency: auto-apply also requires
patch_application_enabled+file_writing_enabled; if they're off it logsautonomy_autoapply_skippedand does nothing. - Full audit trail:
autonomy_autoapply_started/_applied/_verified/_reverted/_rolled_back/_ineligible/_skippedevents, all replayable, and the applied/reverted patches show up in the mission report's "tangible changes" with their final status. - Config (all clamped/whitelisted; forced off in every safety profile):
autonomy_autoapply_enabled(false),autonomy_autoapply_paths([]),autonomy_autoapply_max_lines(40),autonomy_autoapply_verify_cmd(""),autonomy_autoapply_verify_timeout(900),autonomy_autoapply_git_commit(false). All editable in Configuration → Security → Autonomous Auto-Apply. - Tests:
AutoApplyPolicyTests— eligibility matrix, glob semantics, size cap, change-type, disabled and empty-allowlist denial.
Operational note: the verify build blocks the Director thread (deliberately — no new launches
mid-verify), so keep autonomy_concurrency and the verify command in mind on a busy box.
Deploying auto-apply for real self-modification. On the hardened LXC/systemd install the ANTHILL
source tree (/opt/anthill/src) is read-only to the service (ProtectSystem=strict +
ReadWritePaths=…/.anthill), so auto-apply can only write inside the workspace — it cannot modify
the running source. That's a safe default. To actually let the Director ship changes to a codebase:
point agent_workspace_dir at a writable checkout the anthill service user owns (add it to
the unit's ReadWritePaths, or keep it under …/.anthill/), set autonomy_autoapply_paths to the
paths within it you trust, and turn on autonomy_autoapply_git_commit so verified changes land as
local commits. If the workspace root isn't writable when auto-apply runs, the Director logs a single
autonomy_autoapply_skipped (reason: workspace_readonly) rather than failing patch-by-patch.
Phase 4 — what landed
ObjectiveLearning(Anthill.Core/Autonomy/ObjectiveLearning.cs): three pure functions over the per-objective success EMA.UpdateEmafolds each run's mission success score intoobjectives.success_ema(α =autonomy_score_ema_alpha, default 0.3; unscored runs count as 0; always recorded even when learning is off).PriorityBiasmaps the EMA linearly to ±autonomy_priority_bias_maxeffective-priority points at selection time — read-time only, stored priorities never drift, new objectives (null EMA) are unbiased.EvaluateRetirementdecides stale (run_count ≥ autonomy_retire_min_runsand EMA <autonomy_retire_score_threshold) and looping (lastautonomy_loop_windowgenerated goals all ≥autonomy_dedupe_similaritykeyword overlap — same containment metric as Strategist dedup, so a charter-fallback spiral is caught as identical goals run after run).- Retirement = auto-pause +
objective_retiredevent (codestale_low_success/looping_goals, with reason, EMA, run count) — mirrors the failure circuit breaker; a human reviews and resumes from the Autonomy page. Checks run on the director thread after each outcome, so they never race the objective's bookkeeping. Only Active objectives are considered. - Schema v11 (additive):
objectives.success_ema REAL,EnsureColumns+ migration 11. - Config knobs (clamped, settings-whitelisted):
autonomy_learning_enabled(default true — false restores exact Phase 3 behavior),autonomy_priority_bias_max(2),autonomy_score_ema_alpha(0.3),autonomy_retire_min_runs(5),autonomy_retire_score_threshold(0.25),autonomy_loop_window(4, 0 = off). - Observability: Score (EMA) column in
/objectives+ the backlog table;success_emaonautonomy_mission_finishedevents;learning_enabledin/autonomy/status. - Tests:
LearningTests— EMA math and persistence, bias bounds/linearity, EMA-driven selection ordering, and every retirement branch (stale, looping, disabled, non-active).
Phase 3 — what landed
ResourceGovernor(Anthill.Core/Autonomy/ResourceGovernor.cs): sizes effective concurrency each Director cycle, starting from the configuredautonomy_concurrencycap and only ever lowering it. Three cheap signals: normalized CPU load (1-min loadavg per core, soft ≥1.25 halves / hard ≥2.0 clamps to 1), available-memory fraction (soft ≤20% halves / hard ≤10% clamps to 1), and an Ollama latency probe (GET /api/version, cached 15s — unreachable clamps to 1, ≥2.5s halves). Failure posture: unreachable backend clamps (missions would fail anyway); an unreadable host signal is skipped, failing open to the configured cap (e.g. non-Linux hosts without /proc). Full VRAM tracking is deferred to a later hardware-aware scheduler phase. Signal readers are injectable — seeGovernorTests.- Concurrent Director loop:
ColonyDirectornow launches without blocking, tracks in-flight missions, and reaps outcomes as jobs finish. All launching/reaping stays on the single director thread, soBudgetGuardandStrategistcalls remain sequential by construction; budgets are re-checked before every individual launch within a cycle. Kill switch / stop now drains: no new launches, in-flight missions finish and are recorded, then the thread exits. - Scheduling — strict priority + aging (
SqliteMemory.NextReadyObjectives): concurrency slots are filled with the highest-effective-priority distinct ready objectives; an objective never has two missions in flight at once (which also keeps its run-outcome bookkeeping serial). Effective priority = stored priority + 1 perautonomy_aging_minuteswaited since last run (or creation); ties break toward the longest-queued. Aging is computed at read time — stored priorities never drift.autonomy_aging_minutes = 0disables aging (pure strict priority). - Mission-id integrity:
Queen.RunMissiongained anonMissionCreatedcallback andApiJobRegistrystampsjob.MissionIdfrom it the moment the mission row exists — concurrent workers can no longer read another mission's id off the sharedQueen.LastMissionId(which remains, last-writer-wins, for the single-mission CLI path). Job workers are sized tomax(api_job_workers, autonomy_concurrency)at boot so autonomous missions actually get slots. - Config knobs:
autonomy_concurrency(default 1, clamped 1–8) andautonomy_aging_minutes(default 30, clamped 0–10080). Both editable from the Settings UI. - Observability:
/autonomy/statusaddsconcurrency_configured/concurrency_effective,governor_code/governor_reason/governor_signals,aging_minutes, and anin_flightlist (objective, run id, mission id, job status, started_at).autonomy_mission_startedevents record the in-flight count, effective concurrency, and governor code; the Autonomy page shows a Concurrency KPI plus live In-flight / Governor rows. - Tests:
GovernorTests(all clamp paths, fail-open vs. fail-safe, tightest-constraint-wins), multi-slot selection + aging tests inAutonomyTests, and an offline two-slot Director run inDirectorTestsasserting both outcomes are recorded with distinct mission ids.
Phase 2 — what landed
Strategist(Anthill.Core/Autonomy/Strategist.cs): turns an objective + recentautonomy_runshistory + top pheromone trails into a concrete mission goal via the newstrategistmodel-router role. Always computes the deterministic charter-as-goal fallback first; if the router is unset, the call errors, or the response isn't parseable JSON, it returns the fallback — never blocks or throws (StrategistResult.Sourceis"strategist"or"fallback"so the choice is auditable).- Dedup: rejects a generated goal that's a near-duplicate of a recent completed/partial run
for the same objective, using
TextUtil.ExtractKeywordscontainment-ratio overlap againstListAutonomyRuns(objectiveId, limit: 10), thresholdautonomy_dedupe_similarity(default 0.8). A rejected goal falls back to the deterministic charter goal. - Follow-up objectives: the Strategist can propose follow-up objectives in its JSON response;
ColonyDirectoronly saves them after a successful mission, capped byautonomy_max_followups_per_run(default 1) andautonomy_max_objective_depth(default 3, walked via the newSqliteMemory.ObjectiveDepth). Follow-ups inheritParentObjectiveIdand run atPriority - 1so they don't outrank their parent's siblings. - Config knobs (all fail-closed, safe defaults):
autonomy_dedupe_similarity(0–1, clamped),autonomy_max_followups_per_run,autonomy_max_objective_depth. ColonyDirector:RunObjectiveOncenow callsStrategist.GenerateGoalinstead of a static charter-as-goal builder; logsgoal_sourceandstrategist_noteson theautonomy_mission_startedevent, andfollow_ups_createdonautonomy_mission_finished. Writes are still queue-only — the Strategist only chooses what mission to run next, never applies patches.- UI: new admin-only "Autonomy" page — Director status/start/stop/kill-switch card, an objectives table (add/pause-resume/reprioritize/delete), and a recent-runs table showing the goal source (strategist vs. fallback) per run.
- Tests: offline
StrategistTestscover the no-router fallback path (never blocks/throws) andObjectiveDepthwalking/edge cases. The LLM-driven generation/dedup paths aren't testable without a live provider — covered by manual verification once a provider key is configured.
Phase 1 — what landed
ColonyDirector(Anthill.Api/ColonyDirector.cs): the supervisor loop — budget + kill-switch check →NextReadyObjective→ submit a mission through the shared job worker → wait → record the outcome (AutonomyRun+RecordObjectiveRunOutcome) → idle backoff. One mission at a time (concurrency is Phase 3). Charter is used directly as the goal.- Queue-for-review writes: the Director only launches missions; it never approves or applies
patches. Proposals accumulate in the normal
/approvalsqueue. --autonomousflag: starts the Director at boot (still gated byautonomy_enabled).- Control plane endpoints:
GET /autonomy/status,POST /autonomy/start,POST /autonomy/stop,GET /autonomy/runsGET/POST /objectives,GET/PATCH/DELETE /objectives/{id}
- Events:
autonomy_started/stopped/idle/mission_started/mission_finished/erroron thesystem_apichannel, so the loop is fully visible in the existing event log/UI. - Tests: 3 Director integration tests (offline loop runs an objective end-to-end, start refusal when disabled, halt on kill switch) + the Phase 0 suite. Verified live over HTTP.
LLM-driven goal synthesis and mission de-dup landed in Phase 2 (see above).
Intent fidelity (v1.8.15.2). Live testing showed the Strategist could drift — rewriting a
one-shot charter like "create docs/x.md" into an unrelated goal ("train a model on docs"). Two
guards now keep the operator's intent intact: an objective with max_runs == 1 bypasses the LLM
and uses its charter verbatim (explicit one-shot tasks are never reinterpreted, Source = "charter_verbatim"), and the Strategist prompt for standing objectives now requires the goal to
directly accomplish the charter (execute it as written on the first run; only take the next
incremental step once a prior run already accomplished it) and to almost never invent follow-ups.
A structural autonomy_max_backlog cap stops the Strategist enqueuing new follow-ups once the open
backlog (pending + active) is full, bounding sprawl regardless of model behavior.
Phase 0 — what landed
- Schema v8 (
objectives,autonomy_runstables + indexes, migration ledger entry 8). - Domain models:
Objective(+ObjectiveStatusenum),AutonomyRun. - Backlog store (
SqliteMemory.Autonomy.cs): save/get/list objectives,NextReadyObjective(priority desc, oldest-first, skips paused/done/budget-exhausted), priority/status setters,RecordObjectiveRunOutcome(run-count, last-run stamp, consecutive-failure circuit breaker, Done/Paused/Active transitions), audit-trail writes +CountAutonomyRunsSince. - Kill switch (
Autonomy/AutonomyControl.cs): durable.anthill/STOPsentinel and in-process flag;Stop/Resume/IsStopped. No auto-clear. - Budget guard (
Autonomy/BudgetGuard.cs): denies when autonomy is disabled, the kill switch is engaged, or the hourly/daily mission budget is reached. Reads counts from the audit trail so budgets survive restarts. - Config knobs (all fail-closed):
autonomy_enabled(default false, forced off by every safety profile),autonomy_poll_seconds,autonomy_max_missions_per_hour,autonomy_max_missions_per_day,autonomy_max_consecutive_failures. - Tests: 9 autonomy tests (backlog ordering, breaker, kill switch, budget denials).
- Incidental fix surfaced by fresh-DB testing: system/sentinel missions are now seeded so the self-test passes 15/15 on an empty database (was failing on a clean install).
There is no execution loop yet — nothing consumes the backlog. That is Phase 1.
1. Goal
Let ANTHILL run continuously (24/7), working a user-maintained backlog of objectives: generating its own concrete missions, executing them with the existing colony, learning from the outcomes, and queuing any file changes for human review — all under hard budgets and an always-available kill switch.
2. Locked decisions
| Decision | Choice | Implication |
|---|---|---|
| Unattended write policy | Queue for human review first. Build auto-apply later as a gated phase, but keep it disabled until the loop is proven stable. | The autonomous Director can propose patches but never writes to disk on its own. Approvals accumulate in the existing approval queue for a human to /approve + /apply. |
| Objective model | Backlog / priority queue. The colony can enqueue follow-up objectives it discovers. | New persistent objective store; Director pulls the highest-priority ready objective each cycle. |
| Deliverable | This doc + iterative refinement. | — |
3. Core principle: the Director sits above missions
Queen.RunMission(goal) stays one-shot and synchronous (Queen.cs).
Autonomy is a new long-lived supervisor — the Colony Director — that loops:
┌─────────────────────────────────────────────┐
│ Colony Director │
│ (long-lived loop; one per process) │
│ │
kill switch ──┤ 1. check budgets + kill switch │
│ 2. pull next ready objective (backlog) │
│ 3. Strategist: objective + memory → goal │
│ 4. dedupe goal against mission history │
│ 5. Queen.RunMission(goal) ◄── unchanged │
│ 6. record outcome, update pheromones │
│ 7. enqueue discovered follow-ups │
│ 8. idle backoff, then repeat │
└─────────────────────────────────────────────┘
Everything under step 5 is the existing, tested system. Per-mission caps
(MaxMissionSeconds, scheduler, approval gate, spec-ingestion) keep working unchanged.
4. New components
4.1 ObjectiveStore (backlog)
Persistent, priority-ordered queue of objectives.
- Fields:
id,title,charter(the standing goal text),priority(int),status(pending/active/paused/done/failed),created_at,last_run_at,run_count,max_runs(0 = unlimited),parent_objective_id(for discovered follow-ups),metadata. - API:
POST /objectives,GET /objectives,PATCH /objectives/{id}(pause/resume/reprioritize),DELETE /objectives/{id}. - Backed by a new
objectivestable (schema migration → SchemaVersion 8).
4.2 Strategist
Turns one objective + pheromone memory + recent mission history into the next concrete
mission goal. Reuses ModelRouter with a new strategist role route.
- Input: objective charter, last N mission summaries for this objective, top pheromone trails.
- Output: a single concrete goal string + optional list of follow-up objectives to enqueue.
- Dedup: reject a generated goal that is too similar to a recent completed mission
(reuse
TextUtil.ExtractKeywordsoverlap, same approach as source scoring).
4.3 Director loop
The supervisor service. One instance per process. Responsibilities:
- Budget + kill-switch checks before every mission.
- Pull next ready objective (respects
priority,status,max_runs,paused). - Call Strategist →
Queen.RunMission→ record anautonomy_runrow. - Idle backoff when the backlog is empty or budgets are exhausted (sleep, then re-check).
- Graceful shutdown on kill switch / SIGINT.
4.4 ResourceGovernor (Phase 3) — LANDED
Sizes concurrency to host load, memory headroom, and Ollama responsiveness (see "Phase 3 — what landed" above). VRAM-level tracking is deferred to a later hardware-aware scheduler phase.
5. Safety model for unattended operation
Autonomy multiplies blast radius, so rails come first (Phase 0), before the loop exists.
- Kill switch: a sentinel file (
.anthill/STOP) and an API endpoint (POST /autonomy/stop). Checked before every mission and on a timer mid-mission. Presence = immediate drain + halt. - Budgets (config, hard caps):
max_missions_per_day,max_missions_per_hour,max_consecutive_failures(circuit breaker → auto-pause objective), optional wall-clockdaily_runtime_seconds. - Write policy = queue only: Director runs with patch application gated off. Proposals
pile up as approval requests; a human reviews via the existing
/approvals→/approve→/applyflow. The Director never calls/apply. - Audit trail: every autonomous decision (objective chosen, goal generated, mission id,
outcome) is an event + an
autonomy_runsrow, fully replayable. - Default off: autonomy only starts with an explicit
--autonomousflag orPOST /autonomy/start; never by default.
6. Data model additions
objectives(see 4.1).autonomy_runs:id,objective_id,mission_id,generated_goal,started_at,finished_at,mission_status,success_score,follow_ups_created,notes.- Schema migration bumps
SchemaVersion7 → 8 inAnthillRuntime+SqliteMemory.Schema.
7. Config additions (config.json)
"autonomy_enabled": false, // master switch; CLI --autonomous also required
"autonomy_poll_seconds": 30, // idle backoff between cycles
"autonomy_max_missions_per_hour": 6,
"autonomy_max_missions_per_day": 60,
"autonomy_max_consecutive_failures": 3, // circuit breaker per objective
"autonomy_dedupe_similarity": 0.8, // reject near-duplicate generated goals
"autonomy_max_followups_per_run": 1, // cap self-enqueued follow-up objectives per mission
"autonomy_max_objective_depth": 3, // cap parent-chain depth for follow-up objectives
"autonomy_max_backlog": 40, // stop enqueuing follow-ups when pending+active hits this; 0 = no cap
"autonomy_concurrency": 1, // Phase 3: >1 enables concurrent missions (governor can lower it)
"autonomy_aging_minutes": 30, // Phase 3: anti-starvation aging; 0 = pure strict priority
"autonomy_learning_enabled": true, // Phase 4: outcome bias + retirement; false = pure Phase 3
"autonomy_priority_bias_max": 2, // Phase 4: max ± effective-priority points from the success EMA
"autonomy_score_ema_alpha": 0.3, // Phase 4: EMA weight of the newest run's score
"autonomy_retire_min_runs": 5, // Phase 4: runs required before stale retirement may trigger
"autonomy_retire_score_threshold": 0.25, // Phase 4: EMA below this (with enough runs) = stale
"autonomy_loop_window": 4, // Phase 4: near-identical goals in a row = looping; 0 = off
"autonomy_autoapply_enabled": false, // Phase 5: apply allowlisted patches that verify green, no review
"autonomy_autoapply_paths": [], // Phase 5: workspace globs a patch must match; [] = nothing eligible
"autonomy_autoapply_max_lines": 40, // Phase 5: max changed lines per auto-applied patch
"autonomy_autoapply_verify_cmd": "", // Phase 5: verify command; "" = dotnet build && dotnet test
"autonomy_autoapply_verify_timeout": 900, // Phase 5: verify hard timeout (seconds)
"autonomy_autoapply_git_commit": false // Phase 5: also git-commit verified changes locally
All default to the safe/off values.
8. Phased plan
| Phase | Deliverable | Key files |
|---|---|---|
| 0 — Rails ✅ | DONE. Kill switch, budgets, objectives + autonomy_runs tables, config knobs, schema v8. No loop yet. |
AnthillConfig, AnthillRuntime, SqliteMemory.Schema, SqliteMemory.Autonomy, Autonomy/ namespace |
| 1 — Loop (MVP) ✅ | DONE. Director runs one objective at a time, queue-for-review writes only, --autonomous flag, /autonomy/{start,stop,status,runs} + /objectives CRUD. This is the milestone that must be stable before anything else. |
Anthill.Api/ColonyDirector.cs, ApiHost.cs, Program.cs |
| 2 — Self-generated missions ✅ | DONE. Strategist role + dedup + follow-up enqueue (depth/rate capped). | Autonomy/Strategist.cs, ModelRouter route, ColonyDirector.cs |
| 3 — Concurrency ✅ | DONE. ResourceGovernor (load/memory/backend-probe sizing), concurrent Director loop with drain-on-stop, strict priority + aging scheduling, autonomy_concurrency/autonomy_aging_minutes knobs. VRAM-aware scaling deferred to a later hardware-aware phase. |
Autonomy/ResourceGovernor.cs, ColonyDirector.cs, SqliteMemory.Autonomy, ApiHost |
| 4 — Learning loop ✅ | DONE. Success-EMA per objective biases selection (read-time, bounded); stale/looping objectives auto-pause with objective_retired events. |
Autonomy/ObjectiveLearning.cs, SqliteMemory.Autonomy, ColonyDirector.cs |
| 5 — Auto-apply (gated, OFF) ✅ | DONE. Strict allowlist auto-approve+apply — path globs, size cap, add/modify only, must build+test green afterward, auto-rollback on red. Fail-closed OFF, inert with an empty allowlist, forced off in every safety profile. | Autonomy/AutoApplyPolicy.cs, Anthill.Api/AutoApplyRunner.cs, Queen.Views (apply/rollback), ColonyDirector.cs |
Implementation order was strictly 0 → 1 → (stabilize) → 2 → 3 → 4 → 5 — all phases now shipped.
9. UI / observability
- New "Autonomy" panel: current objective, generated goal, live budget counters,
next-run countdown, big STOP button (calls
/autonomy/stop). - Backlog editor: add/reprioritize/pause objectives.
- Reuse the existing event stream + colony canvas for the running mission.
10. Open questions
- Auto-apply criteria (Phase 5): exact allowlist, and "must build + test green" needs a sandboxed build runner — design separately.
Follow-up explosion control: resolved in Phase 2 —autonomy_max_followups_per_runandautonomy_max_objective_depthcap rate/depth of self-enqueued objectives.Multi-objective fairness: resolved in Phase 3 — strict priority with anti-starvation aging (autonomy_aging_minutes); queued time breaks ties. Round-robin/weighted rejected as less predictable/auditable.- VRAM-aware scaling: deferred from Phase 3 — the governor uses load/memory/backend-probe signals; explicit VRAM budgeting needs a configured GPU capacity (Ollama doesn't report total VRAM) and belongs to a later hardware-aware scheduler phase.
- Mission de-dup window: Phase 2 compares against the last 10 runs per objective
(
ListAutonomyRuns(..., limit: 10)); revisit if a time-based window is preferred instead.