From cf39561a3427a460cc7d5f4ce042e2d8e73031c3 Mon Sep 17 00:00:00 2001 From: Taeil Ma Date: Sun, 14 Jun 2026 20:42:28 +0900 Subject: [PATCH] feat(v1.6.1): plugin-namespacing callout + verified cloud routine recipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README/README.ko: a one-time command-naming callout at first command exposure — bare /evolve (git/symlink install) vs /ooda-loop:evolve (plugin install) — instead of mechanically prefixing 99 occurrences. - docs/claude-code-integration.md: copy-paste cloud routine recipe (/schedule + routine prompt + git state-branch flow). - tests/e2e/scenarios/test_cloud_state.py: PROVES cloud state persistence with real git — run 1 commits/pushes state, run 2 (separate fresh clone) reads it, cycle_count continues 1→2, Outcome Record accumulates. The fresh-clone path a routine takes, verified deterministically. Docker E2E 22→23. - plugin/marketplace 1.6.0→1.6.1; TESTING 47/0 · E2E 23/23. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 15 +++++ README.ko.md | 2 + README.md | 2 + TESTING.md | 4 +- docs/claude-code-integration.md | 36 +++++++++++ tests/e2e/scenarios/test_cloud_state.py | 86 +++++++++++++++++++++++++ 8 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 tests/e2e/scenarios/test_cloud_state.py diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 4289fa0..de0c153 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index ddddc8c..3046bdb 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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", diff --git a/CHANGELOG.md b/CHANGELOG.md index 26460b7..e0f86d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.ko.md b/README.ko.md index e1a217c..0ffaabd 100644 --- a/README.ko.md +++ b/README.ko.md @@ -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 diff --git a/README.md b/README.md index 08704a7..ebda2b3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/TESTING.md b/TESTING.md index 0c5dd4d..52ad629 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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 | |------|------|------| diff --git a/docs/claude-code-integration.md b/docs/claude-code-integration.md index 0c78963..3bbf801 100644 --- a/docs/claude-code-integration.md +++ b/docs/claude-code-integration.md @@ -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 diff --git a/tests/e2e/scenarios/test_cloud_state.py b/tests/e2e/scenarios/test_cloud_state.py new file mode 100644 index 0000000..44cf1d0 --- /dev/null +++ b/tests/e2e/scenarios/test_cloud_state.py @@ -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()