Install
$ agentstack add skill-mataeil-ooda-loop-evolve ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →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.
- HALT File -- Before ANY work, check
config.safety.halt_file. If it exists, print reason and stop immediately. - Protected Paths -- PRs touching
config.safety.protected_pathsMUST be Risk Tier 3 (human review). No auto-merge ever. - PR Limit -- Max
config.safety.max_prs_per_cyclePRs per cycle (default 1). - State-Only Direct Write -- evolve writes only to
agent/state/evolve/*. All other changes require branch + PR. - Skill Allowlist -- Only invoke skills in
config.safety.skill_allowlist. Unlisted skill = safety violation, skip to Reflect. - PR Size Limits -- Respect
config.safety.max_files_per_pr(20) andconfig.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, actionqueue.json, skillgaps.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 actionqueue.json for pendingcount, oldestpendinghours, highest_rice.
1-B: GitHub Status
Run these commands. If gh fails, log warning and continue with empty data.
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: openprs, mergedprs, closedprs, openissues.
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_logbackwards from the newest entry (no separately persisted counter — the log IS the source of truth, so the count survives restarts): consecutive entries with the sameselected_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.0in 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,
"lastupdated": ..., "recentoutcomes": [...] } } }`
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 `progressivecomplexity.currentlevel 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
- Source: mataeil/OODA-loop
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.