diff --git a/README.md b/README.md index fb95e7b..ab456d6 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ target repo/ ```bash loopy init loopy init --template inner_outer_eval +loopy init --template pm_planner_dispatcher loopy coordinator --host 127.0.0.1 --port 8080 loopy worker --coordinator http://127.0.0.1:8080 loopy status @@ -69,14 +70,18 @@ loopy stop Use `loopy init --template inner_outer_eval` to scaffold the packaged workflow set for outer planning, inner implementation, eval review, and eval running. +Use `loopy init --template pm_planner_dispatcher` to scaffold the packaged PM +workflow set for planner/dispatcher orchestration that starts child sessions. ## CLI Reference -`loopy init [--template default|inner_outer_eval]` +`loopy init [--template default|inner_outer_eval|pm_planner_dispatcher]` - Scaffolds the root config and reserved `goal_check` workflow. - `--template inner_outer_eval` scaffolds the packaged inner/outer/eval workflow set instead: `outer`, `inner`, `eval_reviewer`, and `eval_runner`. +- `--template pm_planner_dispatcher` scaffolds the packaged PM orchestration + workflow set: `planner` and `dispatcher`. - Does not overwrite existing workflow files. `loopy coordinator --host 0.0.0.0 --port 8080 [--resume] [--workflow-set NAME] [--goal-file PATH]` @@ -210,6 +215,14 @@ The coordinator creates the child session under the parent session's the requested workflow set, and resumes the parent session after the child reaches a terminal state. v1 is depth-first and single-child-at-a-time. +The packaged `pm_planner_dispatcher` workflow set uses this contract as a +generic parent loop: + +- `planner` maintains PM state, selects one work item, and reviews terminal + child-session evidence. +- `dispatcher` writes one child request for the selected work item or imports + terminal child evidence back into PM state. + Cadence example: ```yaml diff --git a/src/loopy_loop/cli.py b/src/loopy_loop/cli.py index ad98ed9..e86a217 100644 --- a/src/loopy_loop/cli.py +++ b/src/loopy_loop/cli.py @@ -20,6 +20,7 @@ DEFAULT_TEMPLATE_NAME = "default" MAIN_WORKFLOW_SET_NAME = "main" INNER_OUTER_EVAL_TEMPLATE_NAME = "inner_outer_eval" +PM_PLANNER_DISPATCHER_TEMPLATE_NAME = "pm_planner_dispatcher" PACKAGED_TEMPLATE_FILES_BY_NAME = { INNER_OUTER_EVAL_TEMPLATE_NAME: [ ".gitignore", @@ -33,7 +34,16 @@ ".loopy_loop/workflow_sets/inner_outer_eval/workflows/inner/prompt.txt", ".loopy_loop/workflow_sets/inner_outer_eval/workflows/outer/config.yaml", ".loopy_loop/workflow_sets/inner_outer_eval/workflows/outer/prompt.txt", - ] + ], + PM_PLANNER_DISPATCHER_TEMPLATE_NAME: [ + ".gitignore", + ROOT_CONFIG_FILENAME, + DEFAULT_GOAL_FILENAME, + ".loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/config.yaml", + ".loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/prompt.txt", + ".loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/config.yaml", + ".loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/prompt.txt", + ], } PACKAGED_TEMPLATE_NAMES = list(PACKAGED_TEMPLATE_FILES_BY_NAME) GITIGNORE_LINES = [".loopy_loop/sessions/"] diff --git a/src/loopy_loop/templates/pm_planner_dispatcher/.gitignore b/src/loopy_loop/templates/pm_planner_dispatcher/.gitignore new file mode 100644 index 0000000..18c3748 --- /dev/null +++ b/src/loopy_loop/templates/pm_planner_dispatcher/.gitignore @@ -0,0 +1 @@ +.loopy_loop/sessions/ diff --git a/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/config.yaml b/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/config.yaml new file mode 100644 index 0000000..b9dfc0b --- /dev/null +++ b/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/config.yaml @@ -0,0 +1,8 @@ +enabled: true +priority: 10 +run_every: 1 +must_follow: planner +not_before_iteration: 1 +description: | + Create one child-session request for the selected PM item or import terminal + child-session evidence back into PM state. diff --git a/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/prompt.txt b/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/prompt.txt new file mode 100644 index 0000000..5dbfd51 --- /dev/null +++ b/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/prompt.txt @@ -0,0 +1,78 @@ +You are the dispatcher for this PM loopy-loop session. + +Your job is to either start one child implementation session for the selected +PM item, or import terminal child-session evidence back into PM state. You do +not implement the child work directly. + +Inputs available in the rendered assignment: +- Goal, completion criteria, and stop criteria +- Session directory +- Session goal path +- Session project_state directory +- Session updates_from_user path +- Session child_requests directory +- Session finished ledger path +- Session harness outputs directory +- Iteration directory +- Iteration harness output root + +State files to read: +- project_state/current_state.md +- project_state/work_items.md +- project_state/child_sessions.md +- project_state/decisions.md +- children.json + +Dispatcher responsibilities: +1. Read the selected work item from project_state/work_items.md and + current_state.md. +2. Inspect children.json and child session directories for child sessions tied + to the selected item. +3. If a selected item already has a terminal child session, import concise + evidence into project_state/child_sessions.md and mark the item + ready_for_review. Do not make the acceptance decision; planner owns that. +4. If a selected item already has a running child session, update + current_state.md and finish. Do not create a duplicate request. +5. If a selected item has no child session, create exactly one child request + JSON file in the rendered Session child_requests directory. +6. After creating a child request, mark the item waiting_for_child and update + current_state.md. + +Child request schema: +{ + "workflow_set": "inner_outer_eval", + "goal": "A complete, self-contained implementation goal for the selected PM item.", + "schema_version": 1 +} + +Child goal requirements: +- Include the selected PM item id/title. +- Include concrete acceptance criteria. +- Include relevant constraints and known context. +- Include expected delivery evidence: checks run, PR/merge status when repo + files change, and where to record completion evidence. +- Keep the goal focused enough for one implementation/eval child loop. +- Do not ask the child to manage the whole PM backlog. + +Evidence import requirements: +- Link child session paths rather than copying full reports. +- Capture: + - child session id/path + - terminal status and stop reason + - child project_state/finished.md if present + - child project_state/current_state.md summary + - eval report or goal_check result if present + - PR URL, merge commit, checks/CI status, or blocker when present +- Mark the PM item ready_for_review only after evidence is imported. + +Guardrails: +- Create at most one child request per dispatcher iteration. +- Leave existing child request files alone unless they are clearly duplicate + requests for the same selected item that you created in this iteration. +- Do not update project_state/finished.md. Planner owns accepted completions. +- Do not stop the loop. Planner owns goal-level stop decisions. +- Do not implement the selected item directly. +- Write supporting dispatch notes under the iteration harness output root when + useful. + +Use Codex. diff --git a/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/config.yaml b/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/config.yaml new file mode 100644 index 0000000..0b1436f --- /dev/null +++ b/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/config.yaml @@ -0,0 +1,9 @@ +enabled: true +priority: 20 +run_every: 1 +must_follow: null +not_before_iteration: 0 +run_on_start: true +description: | + Maintain PM work state, select the next concrete item, and review terminal + child-session evidence. diff --git a/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/prompt.txt b/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/prompt.txt new file mode 100644 index 0000000..e60d8d0 --- /dev/null +++ b/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/prompt.txt @@ -0,0 +1,99 @@ +You are the planner for this PM loopy-loop session. + +Your job is to maintain PM state, choose one concrete implementation item, and +review child-session evidence after implementation loops finish. You do not +implement product/code changes directly. + +Inputs available in the rendered assignment: +- Goal, completion criteria, and stop criteria +- Session directory +- Session goal path +- Session project_state directory +- Session updates_from_user path +- Session child_requests directory +- Session finished ledger path +- Session harness outputs directory +- Iteration directory +- Iteration harness output root +- Session control path + +Mental model: +- This parent session manages work. +- The dispatcher starts child sessions by writing child request JSON files. +- Child sessions do implementation and eval work using another workflow set, + typically `inner_outer_eval`. +- Child session output physically lives under this session's `children/` + directory. + +State files to maintain: +- project_state/README.md: explain the PM state contract and file ownership. +- project_state/current_state.md: active item, child-session status, blockers, + and next expected action. +- project_state/work_items.md: concise PM backlog with statuses. +- project_state/decisions.md: accepted PM decisions with rationale. +- project_state/child_sessions.md: child session index and evidence links. +- project_state/finished.md: accepted completed work only, after planner review. + +Work item statuses: +- available: ready for dispatcher to create a child session. +- selected: chosen by planner for the next dispatcher run. +- waiting_for_child: child session requested or running. +- ready_for_review: child session terminal and evidence imported by dispatcher. +- accepted: planner reviewed evidence and accepts completion. +- needs_rework: planner reviewed evidence and created a follow-up item. +- blocked: exact blocker requires human or external action. + +Planner responsibilities: +1. Read the session goal path and treat it as the source of truth. +2. Read project_state files if they exist. Create missing files as needed. +3. Read updates_from_user.md. If it contains non-whitespace content, reflect it + into work_items.md/current_state.md/decisions.md, then clear it only after + reflection. +4. Inspect children.json and child session directories when relevant. +5. If dispatcher imported terminal child evidence, review it against the work + item acceptance criteria. +6. If evidence is sufficient, mark the item accepted and append a concise + finished.md entry with links to child evidence. +7. If evidence is insufficient, mark the item needs_rework and create a + concrete follow-up item. +8. If no item is selected and an available item exists, select exactly one item + for dispatcher. +9. If no available item exists, create or refine PM work items from the session + goal. Keep each item concrete enough to become a child-session goal. +10. Update current_state.md with the active item, expected next workflow, and + any blockers. + +Child-session review guidance: +- Prefer links to child artifacts over copying large child outputs. +- Useful child evidence usually includes: + - child session path + - child project_state/finished.md + - child project_state/current_state.md + - child eval_results or goal_check result when present + - PR URL, merge commit, checks/CI status, or explicit blocker +- A terminal failed child is still useful evidence. Decide whether to rescope, + retry with a better child goal, or mark blocked. + +Stop rules: +- Do not stop just because one item is accepted. Stop only when the full session + goal is satisfied. +- If the full goal is satisfied, update the rendered Session control path: + { + "state": "stopped", + "reason": "PM accepted evidence satisfies the session goal", + "stop_reason": "goal_met", + "schema_version": 1 + } +- If no useful work remains and the blocker is genuinely terminal, update the + Session control path with stop_reason `unresolvable_error` and record the + exact blocker in current_state.md. + +Guardrails: +- Do not write child request files. The dispatcher owns child_requests/. +- Do not implement child work directly. +- Do not create more than one selected item at a time. +- Keep PM state concise and operational. +- Write supporting review notes under the iteration harness output root when + useful. + +Use Codex. diff --git a/src/loopy_loop/templates/pm_planner_dispatcher/loopy_loop_config.yaml b/src/loopy_loop/templates/pm_planner_dispatcher/loopy_loop_config.yaml new file mode 100644 index 0000000..b596678 --- /dev/null +++ b/src/loopy_loop/templates/pm_planner_dispatcher/loopy_loop_config.yaml @@ -0,0 +1,24 @@ +goal_file: loopy_loop_goal.txt +workflow_set: pm_planner_dispatcher +max_turns: 120 +goal_check_consecutive_failures_cap: 3 +team_harness_provider: "codex" +team_harness_model: "gpt-5.5" +team_harness_agents: + - "codex" + - "claude" + - "gemini" +team_harness_agent_models: + codex: "gpt-5.5" +team_harness_agent_reasoning_efforts: + codex: "high" +# Optional coordinator retry controls. Omit to use team-harness defaults. +# team_harness_max_retries: 8 +# team_harness_retry_base_delay_s: 2.0 +# team_harness_retry_max_delay_s: 60.0 +team_harness_api_base: "https://openrouter.ai/api/v1" +team_harness_api_key_env: "OPENROUTER_API_KEY" +team_harness_system_prompt_extension: | + This session is a PM orchestration loop. It should manage work and dispatch + concrete implementation goals to child sessions. It should not implement the + child work directly. diff --git a/src/loopy_loop/templates/pm_planner_dispatcher/loopy_loop_goal.txt b/src/loopy_loop/templates/pm_planner_dispatcher/loopy_loop_goal.txt new file mode 100644 index 0000000..5a72cfc --- /dev/null +++ b/src/loopy_loop/templates/pm_planner_dispatcher/loopy_loop_goal.txt @@ -0,0 +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. diff --git a/src/tests/test_cli.py b/src/tests/test_cli.py index eb958e0..eec2ad1 100644 --- a/src/tests/test_cli.py +++ b/src/tests/test_cli.py @@ -150,6 +150,31 @@ def test_init_inner_outer_eval_template_preserves_existing_files( assert gitignore_lines.count(line) == 1 +def test_init_pm_planner_dispatcher_template_scaffolds_expected_files( + repo_root: Any, monkeypatch: Any +) -> None: + monkeypatch.chdir(repo_root) + runner = CliRunner() + + result = runner.invoke(main, ["init", "--template", "pm_planner_dispatcher"]) + + assert result.exit_code == 0 + assert repo_root.joinpath("loopy_loop_config.yaml").exists() + assert repo_root.joinpath("loopy_loop_goal.txt").exists() + assert repo_root.joinpath( + ".loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/prompt.txt" + ).exists() + assert repo_root.joinpath( + ".loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/prompt.txt" + ).exists() + assert "workflow_set: pm_planner_dispatcher" in repo_root.joinpath( + "loopy_loop_config.yaml" + ).read_text(encoding="utf-8") + assert "Child request schema:" in repo_root.joinpath( + ".loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/prompt.txt" + ).read_text(encoding="utf-8") + + def test_init_rejects_unknown_template(repo_root: Any, monkeypatch: Any) -> None: monkeypatch.chdir(repo_root) runner = CliRunner() diff --git a/src/tests/test_examples.py b/src/tests/test_examples.py index 4b4c29a..bfd716f 100644 --- a/src/tests/test_examples.py +++ b/src/tests/test_examples.py @@ -22,3 +22,21 @@ def test_inner_outer_eval_template_preflight() -> None: "outer", ] assert preflight.root_config.team_harness_provider == "codex" + + +def test_pm_planner_dispatcher_template_preflight() -> None: + repo_root = ( + Path(__file__).resolve().parents[1] + / "loopy_loop" + / "templates" + / "pm_planner_dispatcher" + ) + + preflight = run_preflight(repo_root=repo_root) + + assert [workflow.id for workflow in preflight.workflows] == [ + "dispatcher", + "planner", + ] + assert preflight.workflow_set == "pm_planner_dispatcher" + assert preflight.root_config.team_harness_provider == "codex"