Skip to content

Commit 098e639

Browse files
authored
Merge pull request #69 from mataeil/feat/prefix-and-cloud-recipe
feat(v1.6.1): namespacing callout + verified cloud routine recipe
2 parents 1daaf62 + cf39561 commit 098e639

8 files changed

Lines changed: 145 additions & 4 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
},
1717
"homepage": "https://github.com/mataeil/OODA-loop",
1818
"tags": ["ooda-loop", "autonomous-agent", "operations", "side-projects"],
19-
"version": "1.6.0"
19+
"version": "1.6.1"
2020
}
2121
]
2222
}

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ooda-loop",
33
"displayName": "OODA-loop",
4-
"version": "1.6.0",
4+
"version": "1.6.1",
55
"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.",
66
"author": {
77
"name": "Taeil Ma",

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@ independently. Bump there signals migration work for downstream projects.
88

99
---
1010

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

1328
**The Claude-Code-native release.** OODA-loop is Claude-Code-exclusive, so it now

README.ko.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ git clone https://github.com/mataeil/OODA-loop.git ~/.ooda-loop
6666
~/.ooda-loop/install.sh
6767
```
6868

69+
> **명령어 표기 (한 번만 읽기).** 이 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`로 내 설치가 노출하는 정확한 이름 확인 가능.)
70+
6971
**프로젝트에 설정:**
7072

7173
```bash

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ git clone https://github.com/mataeil/OODA-loop.git ~/.ooda-loop
6666
~/.ooda-loop/install.sh
6767
```
6868

69+
> **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.)
70+
6971
**Set up your project:**
7072

7173
```bash

TESTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ is *not* yet covered.
1010

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

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

2020
| Tier | What | When |
2121
|------|------|------|

docs/claude-code-integration.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,42 @@ versioned*, not gitignored — see issue #31). For cloud routines:
105105
On a local machine (`/loop` or a Desktop scheduled task) the working directory is
106106
reused, so state persists on disk *and* in git — no extra steps.
107107

108+
> **This is verified, not aspirational.** `tests/e2e/scenarios/test_cloud_state.py`
109+
> proves it with real git: run 1 commits + pushes its state in one clone; run 2,
110+
> a *separate fresh clone* of the same remote, reads run 1's `cycle_count`,
111+
> continues to cycle 2 (no reset), and accumulates the Outcome Record across both
112+
> — exactly the fresh-clone path a cloud routine takes.
113+
114+
### Cloud routine recipe (copy-paste)
115+
116+
```text
117+
1. Make state push to a branch the next run reads. In config.json keep
118+
agent/state/ TRACKED (not gitignored — #31). Decide the state branch:
119+
• simplest: let the routine push to your default branch (main), OR
120+
• isolate: dedicate `ooda/state` and run the routine against it.
121+
122+
2. Create the routine (Claude Code, in the repo):
123+
/schedule
124+
cadence: every 4 hours (cloud minimum is 1h)
125+
repo: your project
126+
branch: main (or ooda/state)
127+
prompt: (below)
128+
129+
3. Routine prompt — re-anchors mission, runs one cycle, persists state:
130+
Read config.json (the mission) and agent/state/ (prior cycles).
131+
Run /ooda-loop:evolve for exactly one OODA cycle.
132+
Ensure Step 6-D committed agent/state/**; then push to this branch so the
133+
next run inherits it. If agent/safety/HALT exists, stop without acting.
134+
135+
4. Verify after a few runs:
136+
/ooda-status --scorecard → cycle_count climbs, Loop Value Score trends
137+
```
138+
139+
Notes: cloud runs are autonomous (no permission prompts) — keep `enable_auto_merge`
140+
OFF unless you've opted in, and the HALT hook (below) still applies because plugin
141+
hooks run in the cloud. Use the bare `/evolve` only if the routine repo uses the
142+
symlink install; for a plugin install use `/ooda-loop:evolve`.
143+
108144
---
109145

110146
## The HALT kill-switch, enforced by a hook
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""E2E: cloud-routine state persistence across FRESH CLONES.
2+
3+
A `/schedule` cloud routine clones the default branch fresh every run with no
4+
local state carried between runs. OODA-loop's claim is that committing
5+
agent/state/ each cycle (Step 6-D) makes the loop's memory survive that. This
6+
test proves the mechanism with REAL git: run 1 in one clone commits + pushes its
7+
state; run 2 in a SEPARATE fresh clone of the same remote must SEE run 1's state
8+
(cycle_count continuity, decision_log carried) — exactly what a cloud routine
9+
relies on. If this passed only by reusing a working dir it would be meaningless,
10+
so each "run" is a distinct clone of a bare remote.
11+
"""
12+
import subprocess
13+
import sys
14+
import tempfile
15+
import unittest
16+
from pathlib import Path
17+
18+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
19+
from driver.engine import Engine, read_json # noqa: E402
20+
from driver.sandbox import make_project # noqa: E402
21+
22+
23+
def git(*a, cwd):
24+
return subprocess.run(["git", *a], cwd=cwd, capture_output=True, text=True, check=True)
25+
26+
27+
class CloudStatePersistence(unittest.TestCase):
28+
def setUp(self):
29+
self.tmp = tempfile.TemporaryDirectory()
30+
self.root = Path(self.tmp.name)
31+
self.remote = self.root / "remote.git"
32+
git("init", "--bare", "-b", "main", str(self.remote), cwd=self.root)
33+
34+
def tearDown(self):
35+
self.tmp.cleanup()
36+
37+
def _clone(self, name):
38+
d = self.root / name
39+
git("clone", str(self.remote), str(d), cwd=self.root)
40+
git("config", "user.email", "e2e@ooda.local", cwd=d)
41+
git("config", "user.name", "OODA E2E", cwd=d)
42+
return d
43+
44+
def test_state_survives_fresh_clone(self):
45+
# --- Run 1: a fresh clone, seed the project, run a cycle, push state ---
46+
run1 = self._clone("run1")
47+
make_project(run1, safety={"min_cycle_interval_minutes": 0, "halt_file": "agent/safety/HALT",
48+
"lock_timeout_minutes": 30, "max_silent_failures": 99})
49+
git("add", "config.json", "agent", cwd=run1)
50+
git("commit", "-q", "-m", "seed", cwd=run1)
51+
git("push", "-q", "origin", "main", cwd=run1)
52+
53+
e1 = Engine(run1)
54+
e1.run_cycle("2026-08-01T06:00:00", {"selected_domain": "test_coverage",
55+
"selected_skill": "/check-tests",
56+
"result": "success", "had_output": True})
57+
self.assertEqual(e1.state()["cycle_count"], 1)
58+
# 6-D committed agent/state; push it (what a cloud routine does at cycle end)
59+
git("add", "agent/state", cwd=run1)
60+
# commit may be a no-op if engine already committed; tolerate
61+
subprocess.run(["git", "commit", "-q", "-m", "cycle 1 state"], cwd=run1, capture_output=True)
62+
git("push", "-q", "origin", "main", cwd=run1)
63+
64+
# --- Run 2: a SEPARATE fresh clone (a new cloud run) ---
65+
run2 = self._clone("run2")
66+
self.assertFalse((run2 / "run1").exists()) # genuinely separate working dir
67+
st2 = read_json(run2 / "agent" / "state" / "evolve" / "state.json")
68+
self.assertEqual(st2["cycle_count"], 1,
69+
"run 2's fresh clone must SEE run 1's committed cycle_count")
70+
self.assertEqual(len(st2["decision_log"]), 1, "decision_log carried across the clone")
71+
self.assertEqual(st2["decision_log"][-1]["selected_domain"], "test_coverage")
72+
73+
# run 2 continues the loop from the persisted state → cycle 2, not 1 again
74+
e2 = Engine(run2)
75+
e2.run_cycle("2026-08-01T12:00:00", {"selected_domain": "backlog",
76+
"selected_skill": "/plan-backlog",
77+
"result": "success", "had_output": True})
78+
self.assertEqual(e2.state()["cycle_count"], 2,
79+
"the loop accumulates across fresh-clone runs (no reset)")
80+
# outcomes.json (the measurement memory) also carried + grew
81+
outc = read_json(run2 / "agent" / "state" / "evolve" / "outcomes.json")["entries"]
82+
self.assertEqual(len(outc), 2, "Outcome Record accumulated across the two cloud runs")
83+
84+
85+
if __name__ == "__main__":
86+
unittest.main()

0 commit comments

Comments
 (0)