loopy-loop 0.3.0__tar.gz → 0.4.0__tar.gz

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 (114) hide show
  1. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/CHANGELOG.md +55 -0
  2. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/PKG-INFO +3 -2
  3. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/README.md +2 -1
  4. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/docs/http-contract.md +15 -1
  5. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/docs/session-layout.md +20 -0
  6. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/pyproject.toml +1 -1
  7. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/cli.py +24 -0
  8. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/coordinator_app.py +412 -131
  9. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/harness_runner.py +13 -9
  10. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/models.py +21 -0
  11. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/recovery.py +2 -3
  12. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/sessions.py +28 -0
  13. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_reviewer/prompt.txt +4 -2
  14. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_runner/prompt.txt +11 -1
  15. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/inner/prompt.txt +3 -1
  16. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/outer/prompt.txt +15 -4
  17. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/prompt.txt +8 -0
  18. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/pm_planner_dispatcher/loopy_loop_config.yaml +7 -4
  19. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/worker.py +8 -2
  20. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_cli.py +105 -0
  21. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_coordinator_app.py +7 -0
  22. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_idempotent_finished.py +3 -0
  23. loopy_loop-0.4.0/src/tests/test_session_stack_recovery.py +824 -0
  24. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/docs/cli-reference/page.mdx +1 -1
  25. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/docs/http-contract/page.mdx +12 -0
  26. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/docs/session-layout/page.mdx +9 -0
  27. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/.eval-banana/config.toml +0 -0
  28. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/.github/workflows/ci.yml +0 -0
  29. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/.github/workflows/docs-deploy.yml +0 -0
  30. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/.github/workflows/release.yml +0 -0
  31. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/.gitignore +0 -0
  32. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/.team-harness/config.toml +0 -0
  33. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/.team-harness/coordinator_system_message.md +0 -0
  34. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/.team-harness/worker_footer.md +0 -0
  35. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/.team-harness/worker_suffix.md +0 -0
  36. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/AGENTS.md +0 -0
  37. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/CLAUDE.md +0 -0
  38. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/LICENSE +0 -0
  39. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/Makefile +0 -0
  40. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/design/analysis/codex_parallel_analysis.md +0 -0
  41. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/design/analysis/loopy-loop-state-and-roadmap.md +0 -0
  42. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/design/analysis/synthesis.md +0 -0
  43. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/design/decisions.md +0 -0
  44. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/design/designs/documentation-site.md +0 -0
  45. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/design/designs/improvement-proposals.md +0 -0
  46. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/design/designs/success-semantics-and-evaluation.md +0 -0
  47. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/docs/images/logo.png +0 -0
  48. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/skills/loopy-loop/SKILL.md +0 -0
  49. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/skills-lock.json +0 -0
  50. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/__init__.py +0 -0
  51. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/config.py +0 -0
  52. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/scheduler.py +0 -0
  53. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/state_store.py +0 -0
  54. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/inner_outer_eval/.gitignore +0 -0
  55. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_reviewer/config.yaml +0 -0
  56. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_runner/config.yaml +0 -0
  57. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/inner/config.yaml +0 -0
  58. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/outer/config.yaml +0 -0
  59. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/inner_outer_eval/loopy_loop_config.yaml +0 -0
  60. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/inner_outer_eval/loopy_loop_goal.txt +0 -0
  61. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/pm_planner_dispatcher/.gitignore +0 -0
  62. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/config.yaml +0 -0
  63. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/prompt.txt +0 -0
  64. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/config.yaml +0 -0
  65. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/templates/pm_planner_dispatcher/loopy_loop_goal.txt +0 -0
  66. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/loopy_loop/worker_identity.py +0 -0
  67. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/__init__.py +0 -0
  68. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/conftest.py +0 -0
  69. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_api_base_normalization.py +0 -0
  70. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_config.py +0 -0
  71. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_examples.py +0 -0
  72. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_fresh_run_archive.py +0 -0
  73. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_goal_check_gate.py +0 -0
  74. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_goal_hash_derivation.py +0 -0
  75. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_harness_runner.py +0 -0
  76. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_must_follow_success.py +0 -0
  77. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_scheduler.py +0 -0
  78. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_sessions.py +0 -0
  79. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_state_store.py +0 -0
  80. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_worker.py +0 -0
  81. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/src/tests/test_worker_liveness_recovery.py +0 -0
  82. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/uv.lock +0 -0
  83. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/.gitignore +0 -0
  84. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/README.md +0 -0
  85. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/mdx-components.tsx +0 -0
  86. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/next.config.ts +0 -0
  87. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/package-lock.json +0 -0
  88. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/package.json +0 -0
  89. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/postcss.config.mjs +0 -0
  90. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/public/.nojekyll +0 -0
  91. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/public/CNAME +0 -0
  92. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/docs/child-sessions/page.mdx +0 -0
  93. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/docs/concepts/page.mdx +0 -0
  94. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/docs/configuration/page.mdx +0 -0
  95. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/docs/evaluation/page.mdx +0 -0
  96. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/docs/getting-started/page.mdx +0 -0
  97. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/docs/layout.tsx +0 -0
  98. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/docs/page.mdx +0 -0
  99. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/docs/success-and-control/page.mdx +0 -0
  100. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/docs/troubleshooting/page.mdx +0 -0
  101. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/docs/workflows/page.mdx +0 -0
  102. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/globals.css +0 -0
  103. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/layout.tsx +0 -0
  104. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/app/page.tsx +0 -0
  105. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/components/docs/DocsPageNavigation.tsx +0 -0
  106. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/components/docs/DocsSidebar.tsx +0 -0
  107. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/components/docs/DocsTableOfContents.tsx +0 -0
  108. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/components/docs/MobileDocsSidebar.tsx +0 -0
  109. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/components/search/SearchCommand.tsx +0 -0
  110. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/components/site/SiteHeader.tsx +0 -0
  111. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/lib/docs/navigation.ts +0 -0
  112. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/lib/search/pagefind.ts +0 -0
  113. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/src/lib/utils.ts +0 -0
  114. {loopy_loop-0.3.0 → loopy_loop-0.4.0}/website/tsconfig.json +0 -0
@@ -1,5 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 (breaking)
4
+
5
+ **Breaking API interaction — completions must echo the dispatched
6
+ `attempt_id`.** Every dispatched task now carries an `attempt_id` that the
7
+ worker must echo on `/finished`; a completion without the live task's attempt
8
+ is treated as stale (this is what fences superseded work). A 0.3.0 worker
9
+ therefore cannot complete tasks against a 0.4.0 coordinator — upgrade workers
10
+ and coordinator together (they normally ship in the same install).
11
+
12
+ - **Durable session-stack recovery (P0.1).** While a child session runs, the
13
+ parent's `state.json` records `active_child_session_id`; on `--resume` the
14
+ coordinator walks the pointer chain to the deepest live session instead of
15
+ silently reopening the parent and orphaning the running child. Terminal
16
+ children found at startup are finalized (children.json completed, pointer
17
+ cleared) and their parent resumed. Every interrupted-dispatch crash window
18
+ reconciles deterministically: dangling pointers are cleared, a fully
19
+ created child whose parent commit never landed is adopted, and leftover
20
+ request files never dispatch twice (children.json records the originating
21
+ `request_file`). Invalid child requests are terminally rejected
22
+ (`*.json.rejected`) instead of being re-read forever.
23
+ - **Attempt ids.** Every dispatched task carries a unique `attempt_id`
24
+ (also on the wire in `TaskResponse`, echoed in `FinishedRequest`); a late
25
+ `/finished` from a superseded attempt of the same coordinates is treated as
26
+ stale rather than recorded as the current result.
27
+ - Iteration artifacts (`result.json`, `result_text.txt`, `prompt.txt`,
28
+ `harness_run_id.txt`, `pending_finished_request.json`), `children.json`,
29
+ and `salvage.json` are all written atomically (unique temp + rename) — a
30
+ crash can never leave a truncated recovery artifact.
31
+ - Internal: the three duplicated dispatch blocks in the coordinator collapsed
32
+ into one `_advance()` step (stop checks → child dispatch → next workflow →
33
+ stamped task), so they can no longer drift apart. `_advance()` also enforces
34
+ the suspended-parent invariant: a parent with a live child can never acquire
35
+ its own task (a duplicate `/finished` retry gets the child's live task
36
+ instead), and a coordinator-level transition lock serializes cross-store
37
+ handoffs.
38
+ - Review hardening (adversarial Codex review of the above): request-file
39
+ tombstones apply only to RUNNING child records (a completed child's
40
+ filename is reusable for new work); the children.json record lands BEFORE
41
+ the child state so an interrupted dispatch is always discoverable
42
+ (`failed_dispatch` + exactly-once redispatch); startup reconciles every
43
+ running-projected record (terminal children finalize even without a
44
+ pointer); the first child task carries an attempt id; attempt checks are
45
+ strict whenever the live task has one (including `result.json` provenance —
46
+ a stale artifact can no longer complete a new attempt); semantically
47
+ unusable child requests (unknown workflow set, no eligible workflow) are
48
+ terminally rejected instead of wedging every completion; packaged prompts
49
+ instruct atomic control/goal_check publication; the crash model (process
50
+ crash, no fsync) is documented.
51
+ - **The `pm_planner_dispatcher` template is executable from a clean init
52
+ (P0.4).** `loopy init --template pm_planner_dispatcher` now also ships the
53
+ `inner_outer_eval` child workflow set its dispatcher spawns — previously a
54
+ clean init could not execute a single child session. The child set is
55
+ sourced from the `inner_outer_eval` template itself, so the two copies can
56
+ never drift apart.
57
+
3
58
  ## 0.3.0 (breaking)
4
59
 
5
60
  **Breaking API change — `/register` requires the worker's process identity.**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loopy-loop
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Run long-running AI agent workflows inside your repository.
5
5
  Project-URL: Homepage, https://github.com/writeitai/loopy-loop
6
6
  Project-URL: Repository, https://github.com/writeitai/loopy-loop
@@ -484,7 +484,8 @@ loopy init [--template default|inner_outer_eval|pm_planner_dispatcher]
484
484
  Scaffolds loopy-loop files. The default template creates only the reserved
485
485
  `goal_check` workflow. `inner_outer_eval` creates the recommended outer/inner/eval
486
486
  workflow set. `pm_planner_dispatcher` creates planner/dispatcher workflows for
487
- child-session orchestration.
487
+ child-session orchestration — and also ships the `inner_outer_eval` child set
488
+ its dispatcher spawns, so a clean init is executable end to end.
488
489
 
489
490
  ```bash
490
491
  loopy coordinator --host 0.0.0.0 --port 8080 [--resume] [--workflow-set NAME] [--goal-file PATH]
@@ -444,7 +444,8 @@ loopy init [--template default|inner_outer_eval|pm_planner_dispatcher]
444
444
  Scaffolds loopy-loop files. The default template creates only the reserved
445
445
  `goal_check` workflow. `inner_outer_eval` creates the recommended outer/inner/eval
446
446
  workflow set. `pm_planner_dispatcher` creates planner/dispatcher workflows for
447
- child-session orchestration.
447
+ child-session orchestration — and also ships the `inner_outer_eval` child set
448
+ its dispatcher spawns, so a clean init is executable end to end.
448
449
 
449
450
  ```bash
450
451
  loopy coordinator --host 0.0.0.0 --port 8080 [--resume] [--workflow-set NAME] [--goal-file PATH]
@@ -33,6 +33,7 @@ Run response:
33
33
  "workflow_id": "planner",
34
34
  "session_id": "20260419_143022_71393ee22450_ab12cd34",
35
35
  "iteration": 3,
36
+ "attempt_id": "a1b2c3d4e5f6",
36
37
  "config_snapshot": {
37
38
  "goal": "Ship a minimal working landing page",
38
39
  "goal_hash": "71393ee22450",
@@ -115,6 +116,13 @@ Rules:
115
116
  escape hatch is to kill that process and register again — the 409 message
116
117
  names its pid.
117
118
  - If the loop is in a terminal state, `/register` immediately returns `action=stop`.
119
+ - On `--resume`, the coordinator walks the durable parent→child session
120
+ pointers to the deepest live session: a running child continues where it
121
+ was (previously a restart silently reopened the parent and orphaned the
122
+ child), a child found terminal is finalized and its parent resumed, and
123
+ each interrupted-dispatch crash window reconciles deterministically
124
+ (dangling pointers cleared, fully-created-but-unpointed children adopted,
125
+ leftover request files never dispatched twice).
118
126
 
119
127
  ## POST /finished
120
128
 
@@ -128,6 +136,7 @@ Request:
128
136
  "success": true,
129
137
  "text": "done",
130
138
  "error": null,
139
+ "attempt_id": "a1b2c3d4e5f6",
131
140
  "worker": {
132
141
  "hostname": "buildbox",
133
142
  "pid": 4242,
@@ -143,7 +152,12 @@ Response: same shape as `/register` response (`action` is either `"run"` or `"st
143
152
 
144
153
  Rules:
145
154
 
146
- - If `session_id` + `workflow_id` + `iteration` does not match `current_task`,
155
+ - Every dispatched task carries a unique `attempt_id`; the worker echoes it on
156
+ `/finished`. A call whose attempt id differs from the live task's — even
157
+ with matching session/workflow/iteration — is stale: it belongs to a
158
+ superseded attempt whose work was already recovered or abandoned.
159
+ - If `session_id` + `workflow_id` + `iteration` (+ `attempt_id`, when both
160
+ sides carry one) does not match `current_task`,
147
161
  the call is treated as stale: state is not mutated and `current_task` is not
148
162
  changed. The current task's run response is returned only when the caller's
149
163
  identity matches the task's recorded owner (or either identity is unknown —
@@ -165,6 +165,26 @@ eval-banana run \
165
165
  - If the file is missing but `result.json` exists for the active task, the
166
166
  coordinator can reconstruct the finished request from `result.json`
167
167
 
168
+ `children.json` (parent sessions)
169
+
170
+ - Index of child sessions dispatched by this session; each record carries the
171
+ child `session_id`, `workflow_set`, `status`, timestamps, `stop_reason`, and
172
+ the originating `request_file` name (which makes the dispatch scan
173
+ idempotent: a request whose filename already appears here is never
174
+ dispatched twice, even if a crash left the file behind)
175
+ - The parent's `state.json` additionally records `active_child_session_id`
176
+ while a child runs — the durable session-stack pointer a restarted
177
+ coordinator follows to resume the child instead of orphaning it
178
+
179
+ Atomicity and crash model: the coordinator- and worker-owned artifacts above
180
+ are written atomically (same-directory temp file + rename), so a **process
181
+ crash** never leaves them truncated — readers see either the previous or the
182
+ new complete file. This does not extend to power loss / kernel crashes (no
183
+ fsync), and it cannot be enforced for **workflow-written** signals
184
+ (`control.json`, `goal_check.json`, child requests): the packaged prompts
185
+ instruct agents to publish those via temp-file + rename, but a torn write by a
186
+ non-compliant agent is read as invalid output.
187
+
168
188
  `salvage.json`
169
189
 
170
190
  - Written into the interrupted iteration's directory during crash recovery,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "loopy-loop"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Run long-running AI agent workflows inside your repository."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -46,6 +46,20 @@ PACKAGED_TEMPLATE_FILES_BY_NAME = {
46
46
  ".loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/prompt.txt",
47
47
  ],
48
48
  }
49
+ # Files a template ships FROM ANOTHER template's directory. The
50
+ # pm_planner_dispatcher dispatcher spawns child sessions running the
51
+ # inner_outer_eval workflow set, so a clean `loopy init` must ship that set
52
+ # too — sourced from the inner_outer_eval template itself so the two copies
53
+ # can never drift apart.
54
+ PACKAGED_TEMPLATE_EXTRA_SOURCES: dict[str, list[tuple[str, str]]] = {
55
+ PM_PLANNER_DISPATCHER_TEMPLATE_NAME: [
56
+ (INNER_OUTER_EVAL_TEMPLATE_NAME, relative_path)
57
+ for relative_path in PACKAGED_TEMPLATE_FILES_BY_NAME[
58
+ INNER_OUTER_EVAL_TEMPLATE_NAME
59
+ ]
60
+ if relative_path.startswith(".loopy_loop/workflow_sets/")
61
+ ]
62
+ }
49
63
  PACKAGED_TEMPLATE_NAMES = list(PACKAGED_TEMPLATE_FILES_BY_NAME)
50
64
  GITIGNORE_LINES = [".loopy_loop/sessions/"]
51
65
  ROOT_CONFIG_TEMPLATE = f"""goal_file: "{DEFAULT_GOAL_FILENAME}"
@@ -176,6 +190,16 @@ def _init_packaged_template(*, repo_root: Path, template_name: str) -> list[str]
176
190
  repo_root=repo_root,
177
191
  )
178
192
  )
193
+ for source_template, relative_path in PACKAGED_TEMPLATE_EXTRA_SOURCES.get(
194
+ template_name, []
195
+ ):
196
+ created.extend(
197
+ _copy_template_file_if_missing(
198
+ source_root=files("loopy_loop").joinpath("templates", source_template),
199
+ relative_path=relative_path,
200
+ repo_root=repo_root,
201
+ )
202
+ )
179
203
  return created
180
204
 
181
205