loopy-loop 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. loopy_loop/__init__.py +1 -0
  2. loopy_loop/cli.py +293 -0
  3. loopy_loop/config.py +491 -0
  4. loopy_loop/coordinator_app.py +706 -0
  5. loopy_loop/harness_runner.py +195 -0
  6. loopy_loop/models.py +163 -0
  7. loopy_loop/scheduler.py +232 -0
  8. loopy_loop/sessions.py +313 -0
  9. loopy_loop/state_store.py +110 -0
  10. loopy_loop/templates/inner_outer_eval/.gitignore +4 -0
  11. loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_reviewer/config.yaml +11 -0
  12. loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_reviewer/prompt.txt +97 -0
  13. loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_runner/config.yaml +12 -0
  14. loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_runner/prompt.txt +86 -0
  15. loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/inner/config.yaml +8 -0
  16. loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/inner/prompt.txt +223 -0
  17. loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/outer/config.yaml +8 -0
  18. loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/outer/prompt.txt +333 -0
  19. loopy_loop/templates/inner_outer_eval/loopy_loop_config.yaml +45 -0
  20. loopy_loop/templates/inner_outer_eval/loopy_loop_goal.txt +5 -0
  21. loopy_loop/templates/pm_planner_dispatcher/.gitignore +1 -0
  22. loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/config.yaml +8 -0
  23. loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/prompt.txt +78 -0
  24. loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/config.yaml +9 -0
  25. loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/prompt.txt +99 -0
  26. loopy_loop/templates/pm_planner_dispatcher/loopy_loop_config.yaml +24 -0
  27. loopy_loop/templates/pm_planner_dispatcher/loopy_loop_goal.txt +1 -0
  28. loopy_loop/worker.py +265 -0
  29. loopy_loop-0.2.0.dist-info/METADATA +408 -0
  30. loopy_loop-0.2.0.dist-info/RECORD +33 -0
  31. loopy_loop-0.2.0.dist-info/WHEEL +4 -0
  32. loopy_loop-0.2.0.dist-info/entry_points.txt +3 -0
  33. loopy_loop-0.2.0.dist-info/licenses/LICENSE +201 -0
loopy_loop/sessions.py ADDED
@@ -0,0 +1,313 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ import uuid
6
+
7
+ from loopy_loop.config import LOOPY_DIRNAME
8
+ from loopy_loop.models import utc_now
9
+
10
+ SESSIONS_DIRNAME = "sessions"
11
+ ITERATIONS_DIRNAME = "iterations"
12
+ CHILDREN_DIRNAME = "children"
13
+ CHILD_REQUESTS_DIRNAME = "child_requests"
14
+ SESSION_METADATA_FILENAME = "session.json"
15
+ STATE_FILENAME = "state.json"
16
+ CHILDREN_FILENAME = "children.json"
17
+ PARENT_FILENAME = "parent.json"
18
+ GOAL_FILENAME = "goal.md"
19
+ EVENTS_FILENAME = "events.jsonl"
20
+ PROJECT_STATE_DIRNAME = "project_state"
21
+ EVAL_CHECKS_DIRNAME = "eval_checks"
22
+ HARNESS_OUTPUTS_DIRNAME = "harness_outputs"
23
+ UPDATES_FROM_USER_FILENAME = "updates_from_user.md"
24
+ FINISHED_FILENAME = "finished.md"
25
+ PROMPT_FILENAME = "prompt.txt"
26
+ RESULT_FILENAME = "result.json"
27
+ RESULT_TEXT_FILENAME = "result_text.txt"
28
+ HARNESS_RUN_ID_FILENAME = "harness_run_id.txt"
29
+ PENDING_FINISHED_REQUEST_FILENAME = "pending_finished_request.json"
30
+ CONTROL_FILENAME = "control.json"
31
+ GOAL_CHECK_FILENAME = "goal_check.json"
32
+
33
+
34
+ def create_session_id(*, goal_hash: str) -> str:
35
+ stamp = utc_now().strftime("%Y%m%d_%H%M%S")
36
+ unique = uuid.uuid4().hex[:8]
37
+ return f"{stamp}_{goal_hash}_{unique}"
38
+
39
+
40
+ def create_session_dir(
41
+ *,
42
+ repo_root: Path,
43
+ session_id: str,
44
+ goal_hash: str,
45
+ goal: str = "",
46
+ workflow_set: str,
47
+ parent_session_id: str | None = None,
48
+ ) -> Path:
49
+ created_at = utc_now().isoformat().replace("+00:00", "Z")
50
+ if parent_session_id is None:
51
+ session_dir = sessions_root_path(repo_root=repo_root) / session_id
52
+ else:
53
+ session_dir = (
54
+ session_dir_path(repo_root=repo_root, session_id=parent_session_id)
55
+ / CHILDREN_DIRNAME
56
+ / session_id
57
+ )
58
+ session_dir.mkdir(parents=True, exist_ok=True)
59
+ metadata_path = session_dir / SESSION_METADATA_FILENAME
60
+ if not metadata_path.exists():
61
+ payload = {
62
+ "session_id": session_id,
63
+ "goal_hash": goal_hash,
64
+ "workflow_set": workflow_set,
65
+ "parent_session_id": parent_session_id,
66
+ "created_at": created_at,
67
+ }
68
+ metadata_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
69
+ if parent_session_id is not None:
70
+ parent_path = session_dir / PARENT_FILENAME
71
+ if not parent_path.exists():
72
+ payload = {
73
+ "schema_version": 1,
74
+ "parent_session_id": parent_session_id,
75
+ "parent_relative_path": "../..",
76
+ "created_at": created_at,
77
+ }
78
+ parent_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
79
+ goal_path = session_dir / GOAL_FILENAME
80
+ if goal and not goal_path.exists():
81
+ goal_path.write_text(goal.rstrip() + "\n", encoding="utf-8")
82
+ children = session_dir / CHILDREN_FILENAME
83
+ if not children.exists():
84
+ payload = {"schema_version": 1, "children": []}
85
+ children.write_text(json.dumps(payload, indent=2), encoding="utf-8")
86
+ child_requests_dir_path(repo_root=repo_root, session_id=session_id).mkdir(
87
+ parents=True, exist_ok=True
88
+ )
89
+ events_path = session_dir / EVENTS_FILENAME
90
+ if not events_path.exists():
91
+ events_path.write_text("", encoding="utf-8")
92
+ control_path(repo_root=repo_root, session_id=session_id)
93
+ updates_path = updates_from_user_path(repo_root=repo_root, session_id=session_id)
94
+ if not updates_path.exists():
95
+ updates_path.write_text("", encoding="utf-8")
96
+ project_state_dir_path(repo_root=repo_root, session_id=session_id).mkdir(
97
+ parents=True, exist_ok=True
98
+ )
99
+ finished = finished_path(repo_root=repo_root, session_id=session_id)
100
+ if not finished.exists():
101
+ finished.write_text("# Finished Work\n", encoding="utf-8")
102
+ eval_checks_dir_path(repo_root=repo_root, session_id=session_id).mkdir(
103
+ parents=True, exist_ok=True
104
+ )
105
+ harness_outputs_dir_path(repo_root=repo_root, session_id=session_id).mkdir(
106
+ parents=True, exist_ok=True
107
+ )
108
+ iterations_dir_path(repo_root=repo_root, session_id=session_id).mkdir(
109
+ parents=True, exist_ok=True
110
+ )
111
+ return session_dir
112
+
113
+
114
+ def sessions_root_path(*, repo_root: Path) -> Path:
115
+ return repo_root / LOOPY_DIRNAME / SESSIONS_DIRNAME
116
+
117
+
118
+ def session_dir_path(*, repo_root: Path, session_id: str) -> Path:
119
+ root = sessions_root_path(repo_root=repo_root)
120
+ direct = root / session_id
121
+ if direct.exists():
122
+ return direct
123
+ if root.exists():
124
+ for candidate in sorted(root.rglob(session_id)):
125
+ if candidate.is_dir() and candidate.name == session_id:
126
+ return candidate
127
+ return direct
128
+
129
+
130
+ def state_path(*, repo_root: Path, session_id: str) -> Path:
131
+ return session_dir_path(repo_root=repo_root, session_id=session_id) / STATE_FILENAME
132
+
133
+
134
+ def children_path(*, repo_root: Path, session_id: str) -> Path:
135
+ return (
136
+ session_dir_path(repo_root=repo_root, session_id=session_id) / CHILDREN_FILENAME
137
+ )
138
+
139
+
140
+ def parent_path(*, repo_root: Path, session_id: str) -> Path:
141
+ return (
142
+ session_dir_path(repo_root=repo_root, session_id=session_id) / PARENT_FILENAME
143
+ )
144
+
145
+
146
+ def child_requests_dir_path(*, repo_root: Path, session_id: str) -> Path:
147
+ return (
148
+ session_dir_path(repo_root=repo_root, session_id=session_id)
149
+ / CHILD_REQUESTS_DIRNAME
150
+ )
151
+
152
+
153
+ def child_sessions_dir_path(*, repo_root: Path, session_id: str) -> Path:
154
+ return (
155
+ session_dir_path(repo_root=repo_root, session_id=session_id) / CHILDREN_DIRNAME
156
+ )
157
+
158
+
159
+ def session_goal_path(*, repo_root: Path, session_id: str) -> Path:
160
+ return session_dir_path(repo_root=repo_root, session_id=session_id) / GOAL_FILENAME
161
+
162
+
163
+ def latest_top_level_state_path(*, repo_root: Path) -> Path | None:
164
+ root = sessions_root_path(repo_root=repo_root)
165
+ if not root.exists():
166
+ return None
167
+ candidates = [
168
+ path / STATE_FILENAME
169
+ for path in root.iterdir()
170
+ if path.is_dir() and (path / STATE_FILENAME).exists()
171
+ ]
172
+ return sorted(candidates)[-1] if candidates else None
173
+
174
+
175
+ def latest_state_path(*, repo_root: Path) -> Path | None:
176
+ root = sessions_root_path(repo_root=repo_root)
177
+ if not root.exists():
178
+ return None
179
+ candidates = sorted(root.rglob(STATE_FILENAME))
180
+ return candidates[-1] if candidates else None
181
+
182
+
183
+ def iterations_dir_path(*, repo_root: Path, session_id: str) -> Path:
184
+ return (
185
+ session_dir_path(repo_root=repo_root, session_id=session_id)
186
+ / ITERATIONS_DIRNAME
187
+ )
188
+
189
+
190
+ def project_state_dir_path(*, repo_root: Path, session_id: str) -> Path:
191
+ return (
192
+ session_dir_path(repo_root=repo_root, session_id=session_id)
193
+ / PROJECT_STATE_DIRNAME
194
+ )
195
+
196
+
197
+ def eval_checks_dir_path(*, repo_root: Path, session_id: str) -> Path:
198
+ return (
199
+ session_dir_path(repo_root=repo_root, session_id=session_id)
200
+ / EVAL_CHECKS_DIRNAME
201
+ )
202
+
203
+
204
+ def harness_outputs_dir_path(*, repo_root: Path, session_id: str) -> Path:
205
+ return (
206
+ session_dir_path(repo_root=repo_root, session_id=session_id)
207
+ / HARNESS_OUTPUTS_DIRNAME
208
+ )
209
+
210
+
211
+ def updates_from_user_path(*, repo_root: Path, session_id: str) -> Path:
212
+ return (
213
+ session_dir_path(repo_root=repo_root, session_id=session_id)
214
+ / UPDATES_FROM_USER_FILENAME
215
+ )
216
+
217
+
218
+ def finished_path(*, repo_root: Path, session_id: str) -> Path:
219
+ return (
220
+ project_state_dir_path(repo_root=repo_root, session_id=session_id)
221
+ / FINISHED_FILENAME
222
+ )
223
+
224
+
225
+ def iteration_dir_name(*, iteration: int, workflow_id: str) -> str:
226
+ return f"{iteration:04d}_{workflow_id}"
227
+
228
+
229
+ def iteration_dir_path(
230
+ *, repo_root: Path, session_id: str, iteration: int, workflow_id: str
231
+ ) -> Path:
232
+ return iterations_dir_path(repo_root=repo_root, session_id=session_id) / (
233
+ iteration_dir_name(iteration=iteration, workflow_id=workflow_id)
234
+ )
235
+
236
+
237
+ def iteration_harness_output_root(
238
+ *, repo_root: Path, session_id: str, iteration: int, workflow_id: str
239
+ ) -> Path:
240
+ return harness_outputs_dir_path(
241
+ repo_root=repo_root, session_id=session_id
242
+ ) / iteration_dir_name(iteration=iteration, workflow_id=workflow_id)
243
+
244
+
245
+ def ensure_iteration_dir(
246
+ *, repo_root: Path, session_id: str, iteration: int, workflow_id: str
247
+ ) -> Path:
248
+ iteration_dir = iteration_dir_path(
249
+ repo_root=repo_root,
250
+ session_id=session_id,
251
+ iteration=iteration,
252
+ workflow_id=workflow_id,
253
+ )
254
+ iteration_dir.mkdir(parents=True, exist_ok=True)
255
+ return iteration_dir
256
+
257
+
258
+ def pending_finished_request_path(
259
+ *, repo_root: Path, session_id: str, iteration: int, workflow_id: str
260
+ ) -> Path:
261
+ return (
262
+ iteration_dir_path(
263
+ repo_root=repo_root,
264
+ session_id=session_id,
265
+ iteration=iteration,
266
+ workflow_id=workflow_id,
267
+ )
268
+ / PENDING_FINISHED_REQUEST_FILENAME
269
+ )
270
+
271
+
272
+ def result_path(
273
+ *, repo_root: Path, session_id: str, iteration: int, workflow_id: str
274
+ ) -> Path:
275
+ return (
276
+ iteration_dir_path(
277
+ repo_root=repo_root,
278
+ session_id=session_id,
279
+ iteration=iteration,
280
+ workflow_id=workflow_id,
281
+ )
282
+ / RESULT_FILENAME
283
+ )
284
+
285
+
286
+ def control_path(*, repo_root: Path, session_id: str) -> Path:
287
+ path = (
288
+ session_dir_path(repo_root=repo_root, session_id=session_id) / CONTROL_FILENAME
289
+ )
290
+ if not path.exists():
291
+ path.parent.mkdir(parents=True, exist_ok=True)
292
+ payload = {
293
+ "state": "running",
294
+ "reason": "session active",
295
+ "stop_reason": None,
296
+ "schema_version": 1,
297
+ }
298
+ path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
299
+ return path
300
+
301
+
302
+ def goal_check_path(
303
+ *, repo_root: Path, session_id: str, iteration: int, workflow_id: str = "goal_check"
304
+ ) -> Path:
305
+ return (
306
+ ensure_iteration_dir(
307
+ repo_root=repo_root,
308
+ session_id=session_id,
309
+ iteration=iteration,
310
+ workflow_id=workflow_id,
311
+ )
312
+ / GOAL_CHECK_FILENAME
313
+ )
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ import os
5
+ from pathlib import Path
6
+ from typing import TypeVar
7
+
8
+ from filelock import FileLock
9
+
10
+ from loopy_loop.config import LOOPY_DIRNAME
11
+ from loopy_loop.models import DEFAULT_LOCK_TIMEOUT_SECONDS
12
+ from loopy_loop.models import LoopState
13
+ from loopy_loop.models import utc_now
14
+ from loopy_loop.sessions import latest_top_level_state_path
15
+ from loopy_loop.sessions import state_path as session_state_path
16
+
17
+ STATE_FILENAME = "state.json"
18
+ LOCK_FILENAME = "state.json.lock"
19
+ TEMP_FILENAME = "state.json.tmp"
20
+ ARCHIVE_PREFIX = "state.json.archive_"
21
+ TERMINAL_STATUSES = {"stopped", "goal_met", "failed", "max_turns"}
22
+
23
+ T = TypeVar("T")
24
+
25
+
26
+ class StateStore:
27
+ def __init__(
28
+ self,
29
+ *,
30
+ repo_root: Path,
31
+ state_path: Path | None = None,
32
+ lock_timeout_seconds: float = DEFAULT_LOCK_TIMEOUT_SECONDS,
33
+ ) -> None:
34
+ self.repo_root = repo_root
35
+ self.loopy_dir = repo_root / LOOPY_DIRNAME
36
+ self.state_path = state_path
37
+ self.lock_timeout_seconds = lock_timeout_seconds
38
+
39
+ def read_state(self) -> LoopState | None:
40
+ with self._lock():
41
+ return self._read_state_unlocked()
42
+
43
+ def write_state(self, *, state: LoopState) -> LoopState:
44
+ with self._lock():
45
+ self._write_state_unlocked(state=state)
46
+ return state
47
+
48
+ def mutate(self, mutator: Callable[[LoopState | None], tuple[LoopState, T]]) -> T:
49
+ with self._lock():
50
+ current = self._read_state_unlocked()
51
+ next_state, result = mutator(current)
52
+ self._write_state_unlocked(state=next_state)
53
+ return result
54
+
55
+ def archive_state(self) -> Path | None:
56
+ with self._lock():
57
+ current = self._read_state_unlocked()
58
+ if current is None or self.state_path is None:
59
+ return None
60
+ archive_path = self._archive_path()
61
+ os.replace(self.state_path, archive_path)
62
+ return archive_path
63
+
64
+ def clear_state(self) -> None:
65
+ with self._lock():
66
+ if self.state_path is not None and self.state_path.exists():
67
+ self.state_path.unlink()
68
+
69
+ def is_terminal_state(self, *, state: LoopState) -> bool:
70
+ return state.status in TERMINAL_STATUSES
71
+
72
+ def _lock(self) -> FileLock:
73
+ self.loopy_dir.mkdir(parents=True, exist_ok=True)
74
+ lock_path = self._effective_state_path()
75
+ if lock_path is None:
76
+ lock_path = self.loopy_dir / LOCK_FILENAME
77
+ else:
78
+ lock_path = lock_path.with_name(LOCK_FILENAME)
79
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
80
+ return FileLock(str(lock_path), timeout=self.lock_timeout_seconds)
81
+
82
+ def _read_state_unlocked(self) -> LoopState | None:
83
+ state_path = self._effective_state_path()
84
+ if state_path is None or not state_path.exists():
85
+ return None
86
+ self.state_path = state_path
87
+ raw = state_path.read_text(encoding="utf-8")
88
+ return LoopState.model_validate_json(raw)
89
+
90
+ def _write_state_unlocked(self, *, state: LoopState) -> None:
91
+ if self.state_path is None:
92
+ self.state_path = session_state_path(
93
+ repo_root=self.repo_root, session_id=state.active_session_id
94
+ )
95
+ self.state_path.parent.mkdir(parents=True, exist_ok=True)
96
+ temp_path = self.state_path.with_name(TEMP_FILENAME)
97
+ payload = state.model_dump_json(indent=2)
98
+ temp_path.write_text(payload, encoding="utf-8")
99
+ os.replace(temp_path, self.state_path)
100
+
101
+ def _archive_path(self) -> Path:
102
+ if self.state_path is None:
103
+ raise RuntimeError("Cannot archive without a state path")
104
+ stamp = utc_now().strftime("%Y%m%dT%H%M%SZ")
105
+ return self.state_path.with_name(f"{ARCHIVE_PREFIX}{stamp}.json")
106
+
107
+ def _effective_state_path(self) -> Path | None:
108
+ if self.state_path is not None:
109
+ return self.state_path
110
+ return latest_top_level_state_path(repo_root=self.repo_root)
@@ -0,0 +1,4 @@
1
+ .loopy_loop/sessions/
2
+ .loopy_loop/state.json
3
+ .loopy_loop/state.json.lock
4
+ .loopy_loop/state.json.archive_*.json
@@ -0,0 +1,11 @@
1
+ enabled: true
2
+ priority: 100
3
+ run_every: 1
4
+ must_follow: null
5
+ not_before_iteration: 0
6
+ run_on_start: true
7
+ run_after_successes:
8
+ workflow_id: inner
9
+ every: 10
10
+ description: |
11
+ Create or refresh high-level eval-banana checks for the active session.
@@ -0,0 +1,97 @@
1
+ You are the eval reviewer for this loopy-loop session.
2
+
3
+ Your job is to create and maintain high-level eval-banana scenarios that can
4
+ decide whether the configured goal and completion criteria have been met.
5
+ Keep the scenarios general enough to allow different implementation choices.
6
+
7
+ Inputs available in the rendered assignment:
8
+ - Goal, completion criteria, and stop criteria
9
+ - Session directory
10
+ - Session project_state directory
11
+ - Session eval_checks directory
12
+ - Session finished ledger path
13
+ - Session harness outputs directory
14
+ - Iteration directory
15
+ - Iteration harness output root
16
+
17
+ Goal source of truth:
18
+ - Treat the rendered Goal input and loopy_loop_goal.txt as canonical.
19
+ - Do not infer or restate the goal from project_state files.
20
+ - Use project_state only to understand progress, decisions, memory, and eval
21
+ history.
22
+
23
+ State contract:
24
+ - Create the session project_state directory if it does not exist.
25
+ - Create the session eval_checks directory if it does not exist.
26
+ - Ensure project_state/README.md explains:
27
+ - loopy_loop_goal.txt is the goal source of truth
28
+ - memory.md is essential durable facts only
29
+ - current_state.md is live status and latest eval headline only
30
+ - eval_results.md owns eval check inventory, run history, and report links
31
+ - finished.md is outer-owned accepted completions only
32
+ - Ensure project_state/memory.md exists and stays limited to essential durable
33
+ facts future iterations must keep in working memory. It is not iteration
34
+ history and not decision rationale.
35
+ - Read and update project_state/current_state.md and project_state/eval_results.md.
36
+ - Read project_state/memory.md when creating or refreshing checks.
37
+ - Do not mark implementation tasks complete. That belongs to outer after review.
38
+
39
+ Eval state split:
40
+ - Keep eval check inventory, commands, validation result, run result, report
41
+ paths, and missing/weak checks in project_state/eval_results.md.
42
+ - Keep project_state/current_state.md limited to eval readiness headline and
43
+ next action.
44
+ - Do not copy full eval reports into current_state.md.
45
+
46
+ PR and merge guardrail:
47
+ - Do not create branches, open PRs, or merge PRs. Eval workflows only create or
48
+ refresh eval checks and eval state.
49
+
50
+ Eval-banana contract:
51
+ - Check files are YAML files.
52
+ - Use schema_version: 1.
53
+ - Use unique check ids.
54
+ - Only create harness_judge checks. Use them to describe the desired outcomes.
55
+ These can be kept high level.
56
+ - Do not create deterministic checks. Deterministic checks are forbidden for
57
+ this workflow, even for objective file, structure, command, or data
58
+ assertions.
59
+ - target_paths are resolved from the repo root.
60
+ - Session-local checks should evaluate the repo by running eval-banana from the
61
+ repo root with --cwd . and --check-dir <session eval_checks directory>.
62
+ - The eval runner will run only this session's checks with:
63
+ eval-banana run --cwd . --check-dir <session eval_checks directory> --output-dir <session directory>/eval_results
64
+
65
+ What to do:
66
+ 1. Read .agents/skills/eval-banana/SKILL.md if present. If it is absent, use
67
+ the eval-banana contract above.
68
+ 2. Read the session project_state files if they exist, including memory.md.
69
+ 3. Inspect the repo enough to understand the current implementation shape.
70
+ 4. Create or refresh YAML harness_judge checks under the session eval_checks
71
+ directory.
72
+ 5. Make the harness_judge checks high-level and outcome-focused. Avoid brittle
73
+ assertions about implementation details unless the completion criteria
74
+ require them.
75
+ 6. Run eval-banana validate with --check-dir for the session eval_checks
76
+ directory when eval-banana is available.
77
+ 7. Update project_state/eval_results.md with:
78
+ - active checks
79
+ - which goal condition each check covers
80
+ - last validation result
81
+ - last run result, if one exists
82
+ - known weak or missing checks
83
+ 8. Update project_state/current_state.md with the current eval readiness state.
84
+ 9. Update project_state/memory.md only if eval work discovers an essential
85
+ durable fact future iterations must remember.
86
+ 10. Write any supporting reports under the iteration harness output root when useful.
87
+
88
+ Good check coverage should answer:
89
+ - Does the repo contain a runnable implementation for the goal?
90
+ - Are the configured completion criteria observably satisfied?
91
+ - Are there clear local run/test instructions where relevant?
92
+ - Are important user-facing workflows covered at a high level?
93
+
94
+ If eval-banana is unavailable, still write the YAML checks and record that
95
+ validation could not be run.
96
+
97
+ Use Codex.
@@ -0,0 +1,12 @@
1
+ enabled: true
2
+ priority: 90
3
+ run_every: 1
4
+ must_follow: eval_reviewer
5
+ not_before_iteration: 10
6
+ run_after_successes:
7
+ workflow_id: inner
8
+ every: 10
9
+ emits_goal_check: true
10
+ description: |
11
+ Run the session eval-banana checks and write goal_check.json. The loop stops
12
+ only when all checks pass.
@@ -0,0 +1,86 @@
1
+ You are the eval runner for this loopy-loop session.
2
+
3
+ Your job is to run the session eval-banana checks and write the goal_check.json
4
+ file requested in the rendered assignment.
5
+
6
+ Inputs available in the rendered assignment:
7
+ - Goal, completion criteria, and stop criteria
8
+ - Session directory
9
+ - Session project_state directory
10
+ - Session eval_checks directory
11
+ - Session finished ledger path
12
+ - Session harness outputs directory
13
+ - Iteration directory
14
+ - Iteration harness output root
15
+ - Session control path
16
+ - goal_check.json output path
17
+
18
+ Goal source of truth:
19
+ - Treat the rendered Goal input and loopy_loop_goal.txt as canonical.
20
+ - Do not infer or restate the goal from project_state files.
21
+ - Use project_state only to understand progress, memory, and eval history.
22
+
23
+ Eval state split:
24
+ - project_state/eval_results.md owns eval commands, validation status, run ids,
25
+ report paths, score, pass/fail history, and failed-check details.
26
+ - project_state/current_state.md should contain only the latest eval headline
27
+ and the next action.
28
+ - Do not copy full eval reports into current_state.md.
29
+
30
+ PR and merge guardrail:
31
+ - Do not create branches, open PRs, or merge PRs. Eval workflows only run evals,
32
+ update eval state, and write goal_check.json.
33
+
34
+ What to do:
35
+ 1. Read project_state/memory.md, project_state/current_state.md, and
36
+ project_state/eval_results.md if they exist.
37
+ 2. Confirm the session eval_checks directory exists and contains YAML checks.
38
+ 3. If eval-banana is available, create <session directory>/eval_results and run
39
+ from the repo root. Pass --cwd . so session-local checks evaluate repo-root
40
+ paths:
41
+ eval-banana validate --cwd . --check-dir <session eval_checks directory>
42
+ eval-banana run --cwd . --check-dir <session eval_checks directory> --output-dir <session directory>/eval_results
43
+ 4. Read the eval-banana console output and the latest report.json/report.md
44
+ under <session directory>/eval_results/<run_id>/ when available.
45
+ 5. Update project_state/eval_results.md as a curated eval index with:
46
+ - iteration number
47
+ - commands run
48
+ - validation status
49
+ - run id and report path when available
50
+ - score and pass/fail status
51
+ - failed checks with concise reasons
52
+ - next eval-facing action
53
+ 6. Update project_state/current_state.md with a short latest-eval summary:
54
+ goal_met, pass/fail, run id/report path, and what should happen next.
55
+ 7. Update project_state/memory.md only if the eval result changes an essential
56
+ durable fact future iterations must remember.
57
+ 8. Do not update project_state/finished.md. Do not mark implementation work
58
+ complete.
59
+ 9. Write supporting eval logs or reports under the iteration harness output root
60
+ when useful.
61
+ 10. Write goal_check.json to the exact path from the rendered assignment.
62
+ 11. If and only if goal_check.json has goal_met true, update the rendered
63
+ Session control path to stop the loop with stop_reason goal_met.
64
+
65
+ goal_check.json schema:
66
+ {
67
+ "goal_met": false,
68
+ "reason": "brief explanation",
69
+ "schema_version": 1
70
+ }
71
+
72
+ Set goal_met to true only when:
73
+ - eval-banana validation succeeds,
74
+ - eval-banana run succeeds,
75
+ - all scenarios pass.
76
+
77
+ If eval-banana is unavailable, checks are missing, validation fails, or any
78
+ scenario fails, set goal_met to false and explain why in the reason.
79
+
80
+ Session control stop schema:
81
+ {
82
+ "state": "stopped",
83
+ "reason": "eval-banana validation and run passed; all scenarios passed",
84
+ "stop_reason": "goal_met",
85
+ "schema_version": 1
86
+ }
@@ -0,0 +1,8 @@
1
+ enabled: true
2
+ priority: 20
3
+ run_every: 1
4
+ must_follow: outer
5
+ not_before_iteration: 1
6
+ description: |
7
+ Implement the first available leaf task from the session plan, run relevant
8
+ checks, and mark the leaf task as waiting for outer-loop review.