Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"homepage": "https://github.com/mataeil/OODA-loop",
"tags": ["ooda-loop", "autonomous-agent", "operations", "side-projects"],
"version": "1.6.0"
"version": "1.6.1"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ooda-loop",
"displayName": "OODA-loop",
"version": "1.6.0",
"version": "1.6.1",
"description": "An autonomous operations layer for your live side project. It watches, re-orients from which PRs you merge and reject, and opens small revertible PRs — bounded by a HALT file, protected paths, and a hard cost cap. Built on Boyd's OODA loop. You stay in command.",
"author": {
"name": "Taeil Ma",
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ independently. Bump there signals migration work for downstream projects.

---

## [v1.6.1] — 2026-06-14

### Added / clarified — plugin namespacing + cloud routine recipe
- **Command-naming callout** at the top of README / README.ko: the docs use the
bare `/evolve` form (git/symlink install); plugin installs prefix with
`ooda-loop:` (`/ooda-loop:evolve`). Stated once at first command exposure
rather than noising every line.
- **Cloud routine recipe** in docs/claude-code-integration.md — the exact
`/schedule` setup + routine prompt + the git state-branch flow.
- **Cloud state persistence is now verified** (not just documented):
tests/e2e/scenarios/test_cloud_state.py proves with real git that state
committed in one clone is read by a separate FRESH clone (cycle_count
continuity, Outcome Record accumulation) — the fresh-clone path a cloud
routine takes. Docker E2E 22 → 23.

## [v1.6.0] — 2026-06-14

**The Claude-Code-native release.** OODA-loop is Claude-Code-exclusive, so it now
Expand Down
2 changes: 2 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ git clone https://github.com/mataeil/OODA-loop.git ~/.ooda-loop
~/.ooda-loop/install.sh
```

> **명령어 표기 (한 번만 읽기).** 이 README는 **bare 형태**(`/evolve`, `/ooda-setup`, `/ooda-status`)로 씁니다 — **git/symlink 설치(방법 B)**가 노출하는 형태입니다. **플러그인(방법 A)**으로 설치했다면 Claude Code가 모든 스킬에 네임스페이스를 붙이므로 **`ooda-loop:` 를 접두**하세요: `/ooda-loop:evolve`, `/ooda-loop:ooda-setup`, `/loop 4h /ooda-loop:evolve`. 스킬·동작은 동일, 접두사만 다릅니다. (`/help`로 내 설치가 노출하는 정확한 이름 확인 가능.)

**프로젝트에 설정:**

```bash
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ git clone https://github.com/mataeil/OODA-loop.git ~/.ooda-loop
~/.ooda-loop/install.sh
```

> **Command naming (read once).** This README writes the **bare** form — `/evolve`, `/ooda-setup`, `/ooda-status`. That's what the **git/symlink install (Option B)** exposes. If you installed the **plugin (Option A)**, Claude Code namespaces every skill — **prefix it with `ooda-loop:`**: `/ooda-loop:evolve`, `/ooda-loop:ooda-setup`, `/loop 4h /ooda-loop:evolve`. Same skills, same behaviour — just the prefix. (Run `/help` to see the exact names your install exposes.)

**Set up your project:**

```bash
Expand Down
4 changes: 2 additions & 2 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ is *not* yet covered.

```bash
python3 tests/verify.py # Tier 0: static fixture walkthrough (38 checks)
tests/e2e/run.sh # Tier 1: isolated Docker E2E (22 rail scenarios)
tests/e2e/run.sh # Tier 1: isolated Docker E2E (23 rail scenarios)
tests/e2e/run.sh --local # …same suite without Docker
```

Both tiers run in CI on every push and pull request
(`.github/workflows/e2e.yml`). Current status: **Tier 0: 47/0 · Tier 1: 22/22.**
(`.github/workflows/e2e.yml`). Current status: **Tier 0: 47/0 · Tier 1: 23/23.**

| Tier | What | When |
|------|------|------|
Expand Down
36 changes: 36 additions & 0 deletions docs/claude-code-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,42 @@ versioned*, not gitignored — see issue #31). For cloud routines:
On a local machine (`/loop` or a Desktop scheduled task) the working directory is
reused, so state persists on disk *and* in git — no extra steps.

> **This is verified, not aspirational.** `tests/e2e/scenarios/test_cloud_state.py`
> proves it with real git: run 1 commits + pushes its state in one clone; run 2,
> a *separate fresh clone* of the same remote, reads run 1's `cycle_count`,
> continues to cycle 2 (no reset), and accumulates the Outcome Record across both
> — exactly the fresh-clone path a cloud routine takes.

### Cloud routine recipe (copy-paste)

```text
1. Make state push to a branch the next run reads. In config.json keep
agent/state/ TRACKED (not gitignored — #31). Decide the state branch:
• simplest: let the routine push to your default branch (main), OR
• isolate: dedicate `ooda/state` and run the routine against it.

2. Create the routine (Claude Code, in the repo):
/schedule
cadence: every 4 hours (cloud minimum is 1h)
repo: your project
branch: main (or ooda/state)
prompt: (below)

3. Routine prompt — re-anchors mission, runs one cycle, persists state:
Read config.json (the mission) and agent/state/ (prior cycles).
Run /ooda-loop:evolve for exactly one OODA cycle.
Ensure Step 6-D committed agent/state/**; then push to this branch so the
next run inherits it. If agent/safety/HALT exists, stop without acting.

4. Verify after a few runs:
/ooda-status --scorecard → cycle_count climbs, Loop Value Score trends
```

Notes: cloud runs are autonomous (no permission prompts) — keep `enable_auto_merge`
OFF unless you've opted in, and the HALT hook (below) still applies because plugin
hooks run in the cloud. Use the bare `/evolve` only if the routine repo uses the
symlink install; for a plugin install use `/ooda-loop:evolve`.

---

## The HALT kill-switch, enforced by a hook
Expand Down
86 changes: 86 additions & 0 deletions tests/e2e/scenarios/test_cloud_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""E2E: cloud-routine state persistence across FRESH CLONES.

A `/schedule` cloud routine clones the default branch fresh every run with no
local state carried between runs. OODA-loop's claim is that committing
agent/state/ each cycle (Step 6-D) makes the loop's memory survive that. This
test proves the mechanism with REAL git: run 1 in one clone commits + pushes its
state; run 2 in a SEPARATE fresh clone of the same remote must SEE run 1's state
(cycle_count continuity, decision_log carried) — exactly what a cloud routine
relies on. If this passed only by reusing a working dir it would be meaningless,
so each "run" is a distinct clone of a bare remote.
"""
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from driver.engine import Engine, read_json # noqa: E402
from driver.sandbox import make_project # noqa: E402


def git(*a, cwd):
return subprocess.run(["git", *a], cwd=cwd, capture_output=True, text=True, check=True)


class CloudStatePersistence(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
self.remote = self.root / "remote.git"
git("init", "--bare", "-b", "main", str(self.remote), cwd=self.root)

def tearDown(self):
self.tmp.cleanup()

def _clone(self, name):
d = self.root / name
git("clone", str(self.remote), str(d), cwd=self.root)
git("config", "user.email", "e2e@ooda.local", cwd=d)
git("config", "user.name", "OODA E2E", cwd=d)
return d

def test_state_survives_fresh_clone(self):
# --- Run 1: a fresh clone, seed the project, run a cycle, push state ---
run1 = self._clone("run1")
make_project(run1, safety={"min_cycle_interval_minutes": 0, "halt_file": "agent/safety/HALT",
"lock_timeout_minutes": 30, "max_silent_failures": 99})
git("add", "config.json", "agent", cwd=run1)
git("commit", "-q", "-m", "seed", cwd=run1)
git("push", "-q", "origin", "main", cwd=run1)

e1 = Engine(run1)
e1.run_cycle("2026-08-01T06:00:00", {"selected_domain": "test_coverage",
"selected_skill": "/check-tests",
"result": "success", "had_output": True})
self.assertEqual(e1.state()["cycle_count"], 1)
# 6-D committed agent/state; push it (what a cloud routine does at cycle end)
git("add", "agent/state", cwd=run1)
# commit may be a no-op if engine already committed; tolerate
subprocess.run(["git", "commit", "-q", "-m", "cycle 1 state"], cwd=run1, capture_output=True)
git("push", "-q", "origin", "main", cwd=run1)

# --- Run 2: a SEPARATE fresh clone (a new cloud run) ---
run2 = self._clone("run2")
self.assertFalse((run2 / "run1").exists()) # genuinely separate working dir
st2 = read_json(run2 / "agent" / "state" / "evolve" / "state.json")
self.assertEqual(st2["cycle_count"], 1,
"run 2's fresh clone must SEE run 1's committed cycle_count")
self.assertEqual(len(st2["decision_log"]), 1, "decision_log carried across the clone")
self.assertEqual(st2["decision_log"][-1]["selected_domain"], "test_coverage")

# run 2 continues the loop from the persisted state → cycle 2, not 1 again
e2 = Engine(run2)
e2.run_cycle("2026-08-01T12:00:00", {"selected_domain": "backlog",
"selected_skill": "/plan-backlog",
"result": "success", "had_output": True})
self.assertEqual(e2.state()["cycle_count"], 2,
"the loop accumulates across fresh-clone runs (no reset)")
# outcomes.json (the measurement memory) also carried + grew
outc = read_json(run2 / "agent" / "state" / "evolve" / "outcomes.json")["entries"]
self.assertEqual(len(outc), 2, "Outcome Record accumulated across the two cloud runs")


if __name__ == "__main__":
unittest.main()
Loading