# Evolve

> OODA Meta-Orchestrator. Observes all domain states, orients by learning from past outcomes, decides the highest-priority action, and executes it. Run with /evolve or /loop 4h /evolve.

- **Type:** Skill
- **Install:** `agentstack add skill-mataeil-ooda-loop-evolve`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [mataeil](https://agentstack.voostack.com/s/mataeil)
- **Installs:** 0
- **Category:** [Cloud & Infrastructure](https://agentstack.voostack.com/c/cloud-infrastructure)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [mataeil](https://github.com/mataeil)
- **Source:** https://github.com/mataeil/OODA-loop/tree/main/skills/evolve

## Install

```sh
agentstack add skill-mataeil-ooda-loop-evolve
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# evolve: OODA Meta-Orchestrator -- Autonomous Decision Engine

evolve is the brain of OODA-loop. It sits above every domain skill and runs
one full OODA loop per cycle: Observe the world, Orient by analyzing patterns
and updating beliefs, Decide which domain needs attention most, then Act by
invoking the winning skill.

This is NOT a round-robin scheduler. The Orient step differentiates evolve from
a cron job -- each cycle it reviews PR outcomes, updates confidence, applies
memos, detects urgent signals, and adjusts its world model. Over time it learns
which domains produce value and which waste cycles.

Based on John Boyd's OODA loop with an added Reflect step that extracts skill
gaps, writes memos, and feeds the next Orient -- creating a double-loop learning
system. All behavior is driven by `config.json`. No domain names, skills, or
routing tables are hardcoded.

---

## Safety Rules

These rules are absolute. Violating any is a bug.

1. **HALT File** -- Before ANY work, check `config.safety.halt_file`. If it exists, print reason and stop immediately.
2. **Protected Paths** -- PRs touching `config.safety.protected_paths` MUST be Risk Tier 3 (human review). No auto-merge ever.
3. **PR Limit** -- Max `config.safety.max_prs_per_cycle` PRs per cycle (default 1).
4. **State-Only Direct Write** -- evolve writes only to `agent/state/evolve/*`. All other changes require branch + PR.
5. **Skill Allowlist** -- Only invoke skills in `config.safety.skill_allowlist`. Unlisted skill = safety violation, skip to Reflect.
6. **PR Size Limits** -- Respect `config.safety.max_files_per_pr` (20) and `config.safety.max_lines_per_pr` (500). Exceeding = Level 3.

---

## File Structure

```
agent/state/evolve/
  state.json          -- cycle_count, last_cycle, decision_log (max 20)
  confidence.json     -- per-domain confidence scores (0.1-1.0)
  goals.json          -- user-defined goals with progress tracking
  skill_gaps.json     -- detected missing capabilities with frequency
  memos.json          -- cross-cycle notes and score_adjustments
  action_queue.json   -- RICE-scored pending/in_progress/completed actions
  metrics.json        -- permanent counters (cycles, PRs, costs)
  cost_ledger.json    -- daily API cost tracking (resets at 00:00 UTC)
  episodes.json       -- weekly summaries (tier 2 memory)
  principles.json     -- permanent learnings (tier 3 memory)
  CHANGELOG.md        -- cycle activity log (most recent 50)
```

Domain skills write their own state files. evolve reads but never writes them.

---

## Step 0: Safety Checks

### 0-Pre: Config Validation

```
if config.json does not exist:
  Print "[FATAL] config.json not found. Run /ooda-setup to create it."
  EXIT immediately.
if config.json is not valid JSON:
  Print "[FATAL] config.json parse error: {error_message}. Fix syntax and retry."
  EXIT immediately.
```

### 0-A: HALT Check

```
if file exists at config.safety.halt_file:
  Print "[HALT] Agent stopped. Reason: {file_content}"
  Print "Remove to resume: rm {config.safety.halt_file}"
  EXIT immediately.
```

### 0-B: Concurrent Execution Lock

```
lock_file = agent/state/evolve/.lock
if lock_file exists:
  age_minutes = now - lock_file.started_at
  if age_minutes = {lock_timeout_minutes}m). Removing and recovering."
  Delete lock_file.
  -- Fall through to 0-C, which performs the crash recovery for the cycle
  -- that left this lock behind.
Create lock_file with content: {"pid": current, "started_at": "ISO 8601"}
```

The lock file is deleted at the end of Step 6. **Every early-exit path
(min-cycle-interval skip in 0-D, HALT re-check in 4-A, HALT during execution,
dry-run, min-score skip, confidence gate with no fallback) MUST also delete the
lock file before exiting.** If evolve crashes mid-run, the lock persists and
blocks live invocations only until `lock_timeout_minutes` elapses — then the
next invocation removes it, runs 0-C crash recovery, and proceeds. Unattended
operation therefore self-heals from crashes; manual `rm` is never required
(though always allowed).

### 0-C: Crash Recovery

```
if state.json.cycle_in_progress == true:
  -- The crashed cycle never reached Step 6, so it has NO decision_log entry:
  -- decision_log[-1] is the last COMPLETED cycle, and the crashed one is
  -- cycle_count + 1. Name them correctly in the diagnostics (surfaced by the
  -- v1.3.0 live soak run — the old message blamed the wrong cycle number).
  crashed_cycle = state.cycle_count + 1
  last = state.json.decision_log[-1] (if exists — the last completed cycle)
  Print "[WARN] Cycle #{crashed_cycle} did not complete (crash/kill detected)."
  Print "  Last completed: #{last.cycle} — domain {last.selected_domain}, skill {last.selected_skill}, at {last.timestamp}"
  Print "  Resetting cycle_in_progress. Starting fresh cycle."
  Set cycle_in_progress = false, write state.json.
  Add memo: { type: "crash_recovery", cycle: crashed_cycle, last_completed: last.cycle }
  Continue with fresh cycle (do NOT resume old one).
```

### 0-D: Min Cycle Interval

```
elapsed = (now - state.json.last_cycle) in minutes.
if elapsed = config.active_context.refresh_interval_hours:
        Schedule a chain-trigger refresh via config.active_context.refresh_skill
        (recorded as a memo, consumed by Step 4-B's chain logic)
  except (file missing or malformed):
    Print "[Observe] active_context not loadable: {error}. Proceeding without context."
    active_context_blob = null
else:
  active_context_blob = null
```

The resolved `active_context_blob` is passed to every invoked skill in Step 4-B
as a context variable (opaque blob; skills interpret domain-specifically).

```
for each domain_name, domain_config in config.domains:
  if domain_name in disabled_by_season:
    log "[{domain}] Disabled by season mode '{mode_name}'. Skipping."
    skip, do not score
  if domain_config.status == "disabled": skip entirely, do not score
  if domain_config.status == "available":
    log "[{domain}] Not yet configured. Skipping."
    skip, do not score
  if domain_config.status == "active": proceed normally
  # Legacy: if no status field, treat as "active" (backward compat)

  # Season mode weight override (v1.2.0): replace the static weight in-memory
  if domain_name in weight_overrides:
    domain_config.weight = weight_overrides[domain_name]

  # Legacy: "enabled" field is superseded by "status" but still honored as fallback
  if not domain_config.enabled: skip
  file = domain_config.state_file
  if file missing:
    mark as "never_run", hours_since_last = config.scoring.hours_if_never_run
  else:
    read JSON. Calculate hours_since_last from data.last_run to now.
    (Legacy: if `last_run` is missing, also check `last_check` — older scan-health
    versions used this key. Treat either as the last-run timestamp.)
    Extract: status, run_count, alerts.

  # Lens pre-init (v1.2.0): initialize the lens file here, before reading, so
  # first cycles and domains with custom observe skills (which may not call the
  # Step 5-E init helper) always have a valid lens file on disk. Production
  # deployments observed 152 cycles with zero lens.json files created because
  # the only init path was inside Step 5-E's observe-skill loop.
  lens_path = agent/state/{domain_name}/lens.json
  if lens_path does not exist:
    mkdir -p agent/state/{domain_name}/
    Write initial lens:
    {
      "schema_version": "1.0.0",
      "domain": "{domain_name}",
      "last_updated": now (ISO 8601),
      "focus_items": [],
      "learned_thresholds": [],
      "discovered_signals": [],
      "evidence": [],
      "deprecated_items": []
    }
    Print "[Observe] Initialized lens for {domain_name}."

  Also read lens file: agent/state/{domain_name}/lens.json
  If lens exists and is valid JSON:
    Extract focus_items where confidence >= 0.6
    Extract learned_thresholds where confidence >= 0.6
    Extract discovered_signals
    Store as domain_config.lens for use in Step 4 skill invocation
  If lens missing or corrupt: proceed without lens (base behavior)
```

Also read evolve self-state: state.json, confidence.json, memos.json,
goals.json, action_queue.json, skill_gaps.json, cost_ledger.json.

If `cost_ledger.json` is MISSING (fresh install / first run), create with initial structure:
`{"schema_version": "1.0.0", "date": "", "entries": [], "total_estimated_usd": 0.0}`

If it EXISTS but is CORRUPT (unparseable JSON): **fail closed** — back it up to
`cost_ledger.json.corrupt`, create the HALT file ("cost ledger corrupt — today's
spend unknown; refusing to run without cost accounting"), and EXIT (delete the
lock first). Recreating a corrupt ledger as $0.00 would silently erase today's
spend and defeat the daily cap.

**Daily reset**: Compare `cost_ledger.json.date` to current UTC date (`YYYY-MM-DD`).
If different: reset `total_estimated_usd` to 0.0, clear `entries`, update `date` to today.
Cost accounting always uses UTC regardless of `config.project.timezone`.

If `config.implementation.enabled`: read action_queue.json for pending_count,
oldest_pending_hours, highest_rice.

### 1-B: GitHub Status

Run these commands. If `gh` fails, log warning and continue with empty data.

```bash
gh pr list --state open --json number,title,labels,createdAt,headRefName
gh pr list --state merged --limit 5 --json number,title,mergedAt,headRefName
gh pr list --state closed --limit 5 --json number,title,closedAt,headRefName
gh issue list --state open --json number,title,labels
```

Store as: open_prs, merged_prs, closed_prs, open_issues.

### 1-C: External Signals

Read `agent/state/external/*.json` if any files exist. Include contents in
observations under "external_signals". Missing directory = skip silently.

### 1-D: Observation Structuring

Assemble in-memory observation object (NOT written to file):

```
observation = {
  timestamp, domains (1-A), github (1-B),
  implementation { pending_count, oldest_pending_hours, highest_rice },
  external_signals (1-C),
  evolve_state { cycle_count, decision_log_length, active_goals, pending_actions }
}
```

Print: `[Observe] Scanned {N} domains. {M} open PRs, {K} merged since last cycle.`

---

## Step 2: Orient

### 2-A: Pattern Analysis

Read last 5 entries from state.json.decision_log. Detect:
- **Consecutive same domain**: count recent consecutive selections of same domain.
- **Execution-result correlation**: match decision_log PR numbers to merged/closed PRs.
- **Repeated failures**: count entries with result "error"/"failed" for same domain. If >= 2, flag as infrastructure_issue.
- **Futile loop**: derive the consecutive-futile count by scanning `decision_log` backwards from the newest entry (no separately persisted counter — the log IS the source of truth, so the count survives restarts): consecutive entries with the same `selected_domain`, result "success", and no actionable output recorded (no actions extracted, no alerts generated, no PR — e.g., plan-backlog returning "no_remote" repeatedly). If >= 3 consecutive futile cycles for the same domain, add a memo penalty of -10.0 to that domain and log `[Orient] Futile loop detected: {domain} produced no output for {N} consecutive cycles. Penalizing.` This prevents the staleness score from endlessly selecting an unproductive domain.
  - **Scope**: penalty applies only to the specific futile domain, not globally.
  - **Lifetime**: written as `score_adjustments[domain] = -10.0` in memos.json. Consumed (deleted) after one application in Step 3-A scoring. Does not persist across multiple cycles.
  - **Recovery**: domain recovers automatically by producing a non-empty observation in any subsequent cycle (the consecutive futile counter resets to 0).

Store patterns for Steps 2-E and 5-C.

### 2-A2: Saturation Circuit Breaker

Track `consecutive_observe_only_cycles` in state.json. A cycle is "observe-only"
if it produced result "success" but: no PRs created, no actions extracted, no
new alerts generated, and no confidence changes occurred.

This check runs in Orient, BEFORE the current cycle has acted — so it evaluates
the **previous completed cycle** (the newest `decision_log` entry and what Step 6
recorded for it), never the in-flight one. If the counter field is missing from
state.json (fresh install, pre-v1.3 state), initialize it to 0 — a missing field
must not silently disable the circuit breaker.

```
-- Evaluate the PREVIOUS completed cycle's actionable output (from its
-- decision_log entry / recorded outcome — current cycle hasn't acted yet)
if state.consecutive_observe_only_cycles is undefined:
  state.consecutive_observe_only_cycles = 0
prev = decision_log[-1]  (if none: skip 2-A2 entirely — nothing to evaluate)
has_output = (prev created a PR OR extracted actions OR generated new alerts
              OR changed confidence)

if has_output:
  state.consecutive_observe_only_cycles = 0
else:
  state.consecutive_observe_only_cycles += 1
  N = state.consecutive_observe_only_cycles

  warn_threshold = config.saturation.warn_threshold      -- default 5
  boost_threshold = config.saturation.boost_threshold    -- default 10
  halt_threshold = config.saturation.halt_threshold      -- default 15

  if N == warn_threshold:
    Add memo: { type: "saturation_warning", message: "Observation saturation: {N} cycles without actionable output. Consider enabling implementation or reviewing domain configuration." }
    Print "[Orient] ⚠ Saturation warning: {N} consecutive observe-only cycles."

  if N == boost_threshold:
    -- Boost pending actions and implementation domain to break out of observation loop
    for each item in action_queue.pending:
      item.effective_rice += config.saturation.implementation_boost  -- default 5.0
    Print "[Orient] ⚠ Saturation boost applied: action queue +{boost}, implementation domain boosted."

  if N >= halt_threshold AND config.saturation.auto_halt (default true):
    Create HALT file: "Observation saturation: {N} cycles without actionable output. Human review needed. Delete this file to resume."
    Print "[Orient] 🛑 Saturation halt: {N} cycles. HALT file created. Review required."
```

### 2-B: Confidence Update

`confidence.json` exists in two shapes in the wild and BOTH must be read
correctly (surfaced by the 2026-06 framework-repo dogfood run, where live state
was nested while fixtures were flat):
- flat (fixtures, early projects): `{ "domain_name": 0.7, ... }`
- nested (live deployments): `{ "domains": { "domain_name": { "score": 0.7,
  "last_updated": ..., "recent_outcomes": [...] } } }`

Read rule: if a top-level `domains` key holds objects, the per-domain value is
`domains[name].score`; otherwise the top-level value itself. Write rule:
preserve the file's existing shape (update `score` in place for nested; the
bare float for flat) — never silently convert a project's state file.

```
for each PR in merged_prs:
  match PR.headRefName to domain via config.domains.*.branch_prefix
  if PR.number already in decision_log: skip (prevent double-counting)
  confidence[domain] += config.confidence.merge_boost  (cap at config.confidence.max)
  Print "[Orient] PR #{n} merged -> {domain} confidence +{boost}"

for each PR in closed_prs (not merged):
  match branch to domain via branch_prefix
  if already logged: skip
  confidence[domain] -= config.confidence.reject_penalty  (floor at config.confidence.min)
  Print "[Orient] PR #{n} rejected -> {domain} confidence -{penalty}"

for domains not yet in confidence.json:
  set to config.confidence.initial (default 0.7)
```

If config.implementation.enabled: apply same logic to implementation PRs.

### 2-B1: Observation-Based Micro-Adjustments

When `config.confidence.observation_micro_adjustments` is true (default) AND
`progressive_complexity.current_level  0 or alerts found):
    confidence[domain] += 0.02   (cap at config.confidence.max)
    Print "[Orient] {domain} produced findings -> confidence +0.02"
  else if skill produced alerts (domain state has severity warning or critical):
    confidence[domain] +=

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [mataeil](https://github.com/mataeil)
- **Source:** [mataeil/OODA-loop](https://github.com/mataeil/OODA-loop)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-mataeil-ooda-loop-evolve
- Seller: https://agentstack.voostack.com/s/mataeil
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
