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
@@ -0,0 +1,78 @@
1
+ You are the dispatcher for this PM loopy-loop session.
2
+
3
+ Your job is to either start one child implementation session for the selected
4
+ PM item, or import terminal child-session evidence back into PM state. You do
5
+ not implement the child work directly.
6
+
7
+ Inputs available in the rendered assignment:
8
+ - Goal, completion criteria, and stop criteria
9
+ - Session directory
10
+ - Session goal path
11
+ - Session project_state directory
12
+ - Session updates_from_user path
13
+ - Session child_requests directory
14
+ - Session finished ledger path
15
+ - Session harness outputs directory
16
+ - Iteration directory
17
+ - Iteration harness output root
18
+
19
+ State files to read:
20
+ - project_state/current_state.md
21
+ - project_state/work_items.md
22
+ - project_state/child_sessions.md
23
+ - project_state/decisions.md
24
+ - children.json
25
+
26
+ Dispatcher responsibilities:
27
+ 1. Read the selected work item from project_state/work_items.md and
28
+ current_state.md.
29
+ 2. Inspect children.json and child session directories for child sessions tied
30
+ to the selected item.
31
+ 3. If a selected item already has a terminal child session, import concise
32
+ evidence into project_state/child_sessions.md and mark the item
33
+ ready_for_review. Do not make the acceptance decision; planner owns that.
34
+ 4. If a selected item already has a running child session, update
35
+ current_state.md and finish. Do not create a duplicate request.
36
+ 5. If a selected item has no child session, create exactly one child request
37
+ JSON file in the rendered Session child_requests directory.
38
+ 6. After creating a child request, mark the item waiting_for_child and update
39
+ current_state.md.
40
+
41
+ Child request schema:
42
+ {
43
+ "workflow_set": "inner_outer_eval",
44
+ "goal": "A complete, self-contained implementation goal for the selected PM item.",
45
+ "schema_version": 1
46
+ }
47
+
48
+ Child goal requirements:
49
+ - Include the selected PM item id/title.
50
+ - Include concrete acceptance criteria.
51
+ - Include relevant constraints and known context.
52
+ - Include expected delivery evidence: checks run, PR/merge status when repo
53
+ files change, and where to record completion evidence.
54
+ - Keep the goal focused enough for one implementation/eval child loop.
55
+ - Do not ask the child to manage the whole PM backlog.
56
+
57
+ Evidence import requirements:
58
+ - Link child session paths rather than copying full reports.
59
+ - Capture:
60
+ - child session id/path
61
+ - terminal status and stop reason
62
+ - child project_state/finished.md if present
63
+ - child project_state/current_state.md summary
64
+ - eval report or goal_check result if present
65
+ - PR URL, merge commit, checks/CI status, or blocker when present
66
+ - Mark the PM item ready_for_review only after evidence is imported.
67
+
68
+ Guardrails:
69
+ - Create at most one child request per dispatcher iteration.
70
+ - Leave existing child request files alone unless they are clearly duplicate
71
+ requests for the same selected item that you created in this iteration.
72
+ - Do not update project_state/finished.md. Planner owns accepted completions.
73
+ - Do not stop the loop. Planner owns goal-level stop decisions.
74
+ - Do not implement the selected item directly.
75
+ - Write supporting dispatch notes under the iteration harness output root when
76
+ useful.
77
+
78
+ Use Codex.
@@ -0,0 +1,9 @@
1
+ enabled: true
2
+ priority: 20
3
+ run_every: 1
4
+ must_follow: null
5
+ not_before_iteration: 0
6
+ run_on_start: true
7
+ description: |
8
+ Maintain PM work state, select the next concrete item, and review terminal
9
+ child-session evidence.
@@ -0,0 +1,99 @@
1
+ You are the planner for this PM loopy-loop session.
2
+
3
+ Your job is to maintain PM state, choose one concrete implementation item, and
4
+ review child-session evidence after implementation loops finish. You do not
5
+ implement product/code changes directly.
6
+
7
+ Inputs available in the rendered assignment:
8
+ - Goal, completion criteria, and stop criteria
9
+ - Session directory
10
+ - Session goal path
11
+ - Session project_state directory
12
+ - Session updates_from_user path
13
+ - Session child_requests directory
14
+ - Session finished ledger path
15
+ - Session harness outputs directory
16
+ - Iteration directory
17
+ - Iteration harness output root
18
+ - Session control path
19
+
20
+ Mental model:
21
+ - This parent session manages work.
22
+ - The dispatcher starts child sessions by writing child request JSON files.
23
+ - Child sessions do implementation and eval work using another workflow set,
24
+ typically `inner_outer_eval`.
25
+ - Child session output physically lives under this session's `children/`
26
+ directory.
27
+
28
+ State files to maintain:
29
+ - project_state/README.md: explain the PM state contract and file ownership.
30
+ - project_state/current_state.md: active item, child-session status, blockers,
31
+ and next expected action.
32
+ - project_state/work_items.md: concise PM backlog with statuses.
33
+ - project_state/decisions.md: accepted PM decisions with rationale.
34
+ - project_state/child_sessions.md: child session index and evidence links.
35
+ - project_state/finished.md: accepted completed work only, after planner review.
36
+
37
+ Work item statuses:
38
+ - available: ready for dispatcher to create a child session.
39
+ - selected: chosen by planner for the next dispatcher run.
40
+ - waiting_for_child: child session requested or running.
41
+ - ready_for_review: child session terminal and evidence imported by dispatcher.
42
+ - accepted: planner reviewed evidence and accepts completion.
43
+ - needs_rework: planner reviewed evidence and created a follow-up item.
44
+ - blocked: exact blocker requires human or external action.
45
+
46
+ Planner responsibilities:
47
+ 1. Read the session goal path and treat it as the source of truth.
48
+ 2. Read project_state files if they exist. Create missing files as needed.
49
+ 3. Read updates_from_user.md. If it contains non-whitespace content, reflect it
50
+ into work_items.md/current_state.md/decisions.md, then clear it only after
51
+ reflection.
52
+ 4. Inspect children.json and child session directories when relevant.
53
+ 5. If dispatcher imported terminal child evidence, review it against the work
54
+ item acceptance criteria.
55
+ 6. If evidence is sufficient, mark the item accepted and append a concise
56
+ finished.md entry with links to child evidence.
57
+ 7. If evidence is insufficient, mark the item needs_rework and create a
58
+ concrete follow-up item.
59
+ 8. If no item is selected and an available item exists, select exactly one item
60
+ for dispatcher.
61
+ 9. If no available item exists, create or refine PM work items from the session
62
+ goal. Keep each item concrete enough to become a child-session goal.
63
+ 10. Update current_state.md with the active item, expected next workflow, and
64
+ any blockers.
65
+
66
+ Child-session review guidance:
67
+ - Prefer links to child artifacts over copying large child outputs.
68
+ - Useful child evidence usually includes:
69
+ - child session path
70
+ - child project_state/finished.md
71
+ - child project_state/current_state.md
72
+ - child eval_results or goal_check result when present
73
+ - PR URL, merge commit, checks/CI status, or explicit blocker
74
+ - A terminal failed child is still useful evidence. Decide whether to rescope,
75
+ retry with a better child goal, or mark blocked.
76
+
77
+ Stop rules:
78
+ - Do not stop just because one item is accepted. Stop only when the full session
79
+ goal is satisfied.
80
+ - If the full goal is satisfied, update the rendered Session control path:
81
+ {
82
+ "state": "stopped",
83
+ "reason": "PM accepted evidence satisfies the session goal",
84
+ "stop_reason": "goal_met",
85
+ "schema_version": 1
86
+ }
87
+ - If no useful work remains and the blocker is genuinely terminal, update the
88
+ Session control path with stop_reason `unresolvable_error` and record the
89
+ exact blocker in current_state.md.
90
+
91
+ Guardrails:
92
+ - Do not write child request files. The dispatcher owns child_requests/.
93
+ - Do not implement child work directly.
94
+ - Do not create more than one selected item at a time.
95
+ - Keep PM state concise and operational.
96
+ - Write supporting review notes under the iteration harness output root when
97
+ useful.
98
+
99
+ Use Codex.
@@ -0,0 +1,24 @@
1
+ goal_file: loopy_loop_goal.txt
2
+ workflow_set: pm_planner_dispatcher
3
+ max_turns: 120
4
+ goal_check_consecutive_failures_cap: 3
5
+ team_harness_provider: "codex"
6
+ team_harness_model: "gpt-5.5"
7
+ team_harness_agents:
8
+ - "codex"
9
+ - "claude"
10
+ - "gemini"
11
+ team_harness_agent_models:
12
+ codex: "gpt-5.5"
13
+ team_harness_agent_reasoning_efforts:
14
+ codex: "high"
15
+ # Optional coordinator retry controls. Omit to use team-harness defaults.
16
+ # team_harness_max_retries: 8
17
+ # team_harness_retry_base_delay_s: 2.0
18
+ # team_harness_retry_max_delay_s: 60.0
19
+ team_harness_api_base: "https://openrouter.ai/api/v1"
20
+ team_harness_api_key_env: "OPENROUTER_API_KEY"
21
+ team_harness_system_prompt_extension: |
22
+ This session is a PM orchestration loop. It should manage work and dispatch
23
+ concrete implementation goals to child sessions. It should not implement the
24
+ child work directly.
@@ -0,0 +1 @@
1
+ Manage project work by selecting one concrete implementation item at a time, dispatching it to a child implementation loop, and reviewing the child evidence before accepting completion.
loopy_loop/worker.py ADDED
@@ -0,0 +1,265 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import json
5
+ from pathlib import Path
6
+ import sys
7
+ import time
8
+ import traceback
9
+
10
+ import httpx
11
+
12
+ from loopy_loop.config import ConfigError
13
+ from loopy_loop.config import load_workflow_config
14
+ from loopy_loop.config import workflow_set_workflows_dir_path
15
+ from loopy_loop.harness_runner import run_harness_iteration
16
+ from loopy_loop.harness_runner import write_iteration_artifacts
17
+ from loopy_loop.models import FinishedRequest
18
+ from loopy_loop.models import IterationResult
19
+ from loopy_loop.models import RootConfigSnapshot
20
+ from loopy_loop.models import TaskResponse
21
+ from loopy_loop.sessions import child_requests_dir_path
22
+ from loopy_loop.sessions import control_path
23
+ from loopy_loop.sessions import ensure_iteration_dir
24
+ from loopy_loop.sessions import eval_checks_dir_path
25
+ from loopy_loop.sessions import finished_path
26
+ from loopy_loop.sessions import GOAL_CHECK_FILENAME
27
+ from loopy_loop.sessions import harness_outputs_dir_path
28
+ from loopy_loop.sessions import iteration_harness_output_root
29
+ from loopy_loop.sessions import pending_finished_request_path
30
+ from loopy_loop.sessions import project_state_dir_path
31
+ from loopy_loop.sessions import session_dir_path
32
+ from loopy_loop.sessions import session_goal_path
33
+ from loopy_loop.sessions import updates_from_user_path
34
+
35
+ # Internal retry constants for /finished — not configurable externally.
36
+ # If all retries fail, the exception propagates and the process exits;
37
+ # the next invocation recovers via the completed-task or abandoned-task path in /register.
38
+ _FINISHED_RETRY_ATTEMPTS = 2
39
+ _FINISHED_RETRY_BACKOFF_SECONDS = 1.0
40
+
41
+
42
+ class FatalAssignmentError(Exception):
43
+ def __init__(
44
+ self, *, finished_assignment: FinishedAssignment, message: str
45
+ ) -> None:
46
+ super().__init__(message)
47
+ self.finished_assignment = finished_assignment
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class FinishedAssignment:
52
+ request: FinishedRequest
53
+ pending_path: Path
54
+
55
+
56
+ def run_worker_loop(*, repo_root: Path, coordinator_url: str) -> None:
57
+ base_url = coordinator_url.rstrip("/")
58
+ with httpx.Client(timeout=30.0) as client:
59
+ task = _post_register(client=client, coordinator_url=base_url)
60
+ while task.action == "run":
61
+ try:
62
+ finished_assignment = _run_task(repo_root=repo_root, task=task)
63
+ except FatalAssignmentError as exc:
64
+ print(str(exc), file=sys.stderr)
65
+ _post_finished(
66
+ client=client,
67
+ coordinator_url=base_url,
68
+ request=exc.finished_assignment.request,
69
+ )
70
+ _clear_pending_finished_request(
71
+ path=exc.finished_assignment.pending_path
72
+ )
73
+ sys.exit(2)
74
+ task = _post_finished(
75
+ client=client,
76
+ coordinator_url=base_url,
77
+ request=finished_assignment.request,
78
+ )
79
+ _clear_pending_finished_request(path=finished_assignment.pending_path)
80
+
81
+
82
+ def _post_register(*, client: httpx.Client, coordinator_url: str) -> TaskResponse:
83
+ response = client.post(f"{coordinator_url}/register", json={})
84
+ response.raise_for_status()
85
+ return TaskResponse.model_validate(response.json())
86
+
87
+
88
+ def _post_finished(
89
+ *, client: httpx.Client, coordinator_url: str, request: FinishedRequest
90
+ ) -> TaskResponse:
91
+ for attempt in range(1, _FINISHED_RETRY_ATTEMPTS + 1):
92
+ try:
93
+ response = client.post(
94
+ f"{coordinator_url}/finished", json=request.model_dump()
95
+ )
96
+ response.raise_for_status()
97
+ return TaskResponse.model_validate(response.json())
98
+ except httpx.HTTPError:
99
+ traceback.print_exc()
100
+ if attempt == _FINISHED_RETRY_ATTEMPTS:
101
+ raise
102
+ time.sleep(_FINISHED_RETRY_BACKOFF_SECONDS * attempt)
103
+ raise RuntimeError("unreachable")
104
+
105
+
106
+ def _run_task(*, repo_root: Path, task: TaskResponse) -> FinishedAssignment:
107
+ if (
108
+ task.session_id is None
109
+ or task.workflow_set is None
110
+ or task.workflow_id is None
111
+ or task.iteration is None
112
+ or task.config_snapshot is None
113
+ ):
114
+ raise ConfigError("Incomplete run payload from coordinator")
115
+
116
+ config_snapshot = RootConfigSnapshot.model_validate(
117
+ task.config_snapshot.model_dump()
118
+ )
119
+ workflow_dir = (
120
+ workflow_set_workflows_dir_path(
121
+ repo_root=repo_root, workflow_set=task.workflow_set
122
+ )
123
+ / task.workflow_id
124
+ )
125
+ workflow_config = load_workflow_config(workflow_dir=workflow_dir)
126
+ prompt_text = (workflow_dir / "prompt.txt").read_text(encoding="utf-8")
127
+ iteration_dir = ensure_iteration_dir(
128
+ repo_root=repo_root,
129
+ session_id=task.session_id,
130
+ iteration=task.iteration,
131
+ workflow_id=task.workflow_id,
132
+ )
133
+ harness_output_root = iteration_harness_output_root(
134
+ repo_root=repo_root,
135
+ session_id=task.session_id,
136
+ iteration=task.iteration,
137
+ workflow_id=task.workflow_id,
138
+ )
139
+ rendered_prompt = _render_prompt(
140
+ config_snapshot=config_snapshot,
141
+ session_id=task.session_id,
142
+ workflow_set=task.workflow_set,
143
+ iteration=task.iteration,
144
+ workflow_id=task.workflow_id,
145
+ iteration_dir=iteration_dir,
146
+ harness_output_root=harness_output_root,
147
+ workflow_prompt=prompt_text,
148
+ emits_goal_check=workflow_config.emits_goal_check,
149
+ repo_root=repo_root,
150
+ )
151
+ fatal_error: str | None = None
152
+ try:
153
+ iteration_result = run_harness_iteration(
154
+ repo_root=repo_root,
155
+ config_snapshot=config_snapshot,
156
+ rendered_prompt=rendered_prompt,
157
+ harness_output_root=harness_output_root,
158
+ )
159
+ except ConfigError as exc:
160
+ fatal_error = str(exc)
161
+ iteration_result = IterationResult(
162
+ success=False, text=None, error=fatal_error, harness_run_id=""
163
+ )
164
+ except Exception as exc:
165
+ traceback.print_exc()
166
+ iteration_result = IterationResult(
167
+ success=False, text=None, error=str(exc), harness_run_id=""
168
+ )
169
+ write_iteration_artifacts(
170
+ iteration_dir=iteration_dir,
171
+ rendered_prompt=rendered_prompt,
172
+ iteration_result=iteration_result,
173
+ )
174
+ finished_request = FinishedRequest(
175
+ session_id=task.session_id,
176
+ workflow_id=task.workflow_id,
177
+ iteration=task.iteration,
178
+ success=iteration_result.success,
179
+ text=iteration_result.text,
180
+ error=iteration_result.error,
181
+ )
182
+ pending_path = _write_pending_finished_request(
183
+ repo_root=repo_root, request=finished_request
184
+ )
185
+ finished_assignment = FinishedAssignment(
186
+ request=finished_request, pending_path=pending_path
187
+ )
188
+ if fatal_error is not None:
189
+ raise FatalAssignmentError(
190
+ finished_assignment=finished_assignment, message=fatal_error
191
+ )
192
+ return finished_assignment
193
+
194
+
195
+ def _write_pending_finished_request(
196
+ *, repo_root: Path, request: FinishedRequest
197
+ ) -> Path:
198
+ path = pending_finished_request_path(
199
+ repo_root=repo_root,
200
+ session_id=request.session_id,
201
+ iteration=request.iteration,
202
+ workflow_id=request.workflow_id,
203
+ )
204
+ path.write_text(json.dumps(request.model_dump(), indent=2), encoding="utf-8")
205
+ return path
206
+
207
+
208
+ def _clear_pending_finished_request(*, path: Path) -> None:
209
+ path.unlink(missing_ok=True)
210
+
211
+
212
+ def _render_prompt(
213
+ *,
214
+ config_snapshot: RootConfigSnapshot,
215
+ session_id: str,
216
+ workflow_set: str,
217
+ iteration: int,
218
+ workflow_id: str,
219
+ iteration_dir: Path,
220
+ harness_output_root: Path,
221
+ workflow_prompt: str,
222
+ emits_goal_check: bool = False,
223
+ repo_root: Path | None = None,
224
+ ) -> str:
225
+ root = repo_root or Path.cwd()
226
+ session_dir = session_dir_path(repo_root=root, session_id=session_id)
227
+ lines = [
228
+ "loopy-loop assignment",
229
+ "",
230
+ f"Goal: {config_snapshot.goal}",
231
+ "Completion criteria:",
232
+ *[f"- {item}" for item in config_snapshot.completion_criteria],
233
+ "Stop criteria:",
234
+ *[f"- {item}" for item in config_snapshot.stop_criteria],
235
+ "",
236
+ f"Session ID: {session_id}",
237
+ f"Workflow set: {workflow_set}",
238
+ f"Iteration: {iteration}",
239
+ f"Workflow ID: {workflow_id}",
240
+ f"Session directory: {session_dir}",
241
+ f"Session goal path: {session_goal_path(repo_root=root, session_id=session_id)}",
242
+ "Session project_state directory: "
243
+ f"{project_state_dir_path(repo_root=root, session_id=session_id)}",
244
+ "Session eval_checks directory: "
245
+ f"{eval_checks_dir_path(repo_root=root, session_id=session_id)}",
246
+ "Session updates_from_user path: "
247
+ f"{updates_from_user_path(repo_root=root, session_id=session_id)}",
248
+ "Session child_requests directory: "
249
+ f"{child_requests_dir_path(repo_root=root, session_id=session_id)}",
250
+ f"Session control path: {control_path(repo_root=root, session_id=session_id)}",
251
+ "Session finished ledger path: "
252
+ f"{finished_path(repo_root=root, session_id=session_id)}",
253
+ "Session harness outputs directory: "
254
+ f"{harness_outputs_dir_path(repo_root=root, session_id=session_id)}",
255
+ f"Iteration directory: {iteration_dir}",
256
+ f"Iteration harness output root: {harness_output_root}",
257
+ ]
258
+ if session_dir.parent.name == "children":
259
+ lines.append(f"Parent session directory: {session_dir.parent.parent}")
260
+ if workflow_id == "goal_check" or emits_goal_check:
261
+ lines.append(
262
+ f"goal_check.json output path: {iteration_dir / GOAL_CHECK_FILENAME}"
263
+ )
264
+ lines.extend(["", "Workflow body:", workflow_prompt])
265
+ return "\n".join(lines).rstrip() + "\n"