piocloop 0.1.1__tar.gz → 0.1.2__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.
- {piocloop-0.1.1 → piocloop-0.1.2}/PKG-INFO +1 -1
- {piocloop-0.1.1 → piocloop-0.1.2}/pyproject.toml +1 -1
- {piocloop-0.1.1 → piocloop-0.1.2}/src/piocloop/loop.py +18 -3
- {piocloop-0.1.1 → piocloop-0.1.2}/tests/test_loop.py +53 -3
- {piocloop-0.1.1 → piocloop-0.1.2}/.github/workflows/publish.yml +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/.gitignore +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/ANALYSIS.md +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/DESIGN.md +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/LICENSE +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/PLAN.md +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/README.md +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/skills/README.md +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/skills/piocloop-setup/SKILL.md +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/skills/piocloop-setup/assets/PLAN.template.md +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/skills/piocloop-setup/assets/loop-prompt.template.md +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/skills/piocloop-setup/references/PLAN-FORMAT.md +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/skills/piocloop-setup/references/PROMPT-AUTHORING.md +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/src/piocloop/__init__.py +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/src/piocloop/__main__.py +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/src/piocloop/cli.py +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/src/piocloop/pi_client.py +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/src/piocloop/pi_events.py +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/src/piocloop/plan_parser.py +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/src/piocloop/tui.py +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/tests/fake_pi.py +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/tests/test_pi_client.py +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/tests/test_pi_events.py +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/tests/test_plan_parser.py +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/tests/test_skill_assets.py +0 -0
- {piocloop-0.1.1 → piocloop-0.1.2}/tests/test_tui.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: piocloop
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: TUI loop harness that orchestrates the PI coding agent to execute tasks from a PLAN.md file iteratively
|
|
5
5
|
Project-URL: Homepage, https://github.com/rbarzic/piocloop
|
|
6
6
|
Project-URL: Repository, https://github.com/rbarzic/piocloop
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "piocloop"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "TUI loop harness that orchestrates the PI coding agent to execute tasks from a PLAN.md file iteratively"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -183,7 +183,18 @@ class LoopEngine:
|
|
|
183
183
|
self.ui.log_line("warn", f"Restarting pi (respawn {self.respawns}/{self.config.max_respawns})")
|
|
184
184
|
await self.shutdown()
|
|
185
185
|
self._mapper = EventMapper(self.config.plan_file)
|
|
186
|
-
|
|
186
|
+
try:
|
|
187
|
+
await self._spawn_client()
|
|
188
|
+
except (PiError, OSError) as exc:
|
|
189
|
+
# shutdown() deliberately clears self.client before starting the
|
|
190
|
+
# replacement. If readiness fails, keep that invariant explicit
|
|
191
|
+
# and turn the failure into an iteration error rather than letting
|
|
192
|
+
# the next retry reach _run_iteration with no client.
|
|
193
|
+
self.client = None
|
|
194
|
+
self.ui.log_line("error", f"Could not restart pi: {exc}")
|
|
195
|
+
if isinstance(exc, PiError):
|
|
196
|
+
raise
|
|
197
|
+
raise PiError(f"Could not restart pi: {exc}") from exc
|
|
187
198
|
|
|
188
199
|
# ------------------------------------------------------------------
|
|
189
200
|
# Events
|
|
@@ -274,6 +285,8 @@ class LoopEngine:
|
|
|
274
285
|
continue
|
|
275
286
|
|
|
276
287
|
try:
|
|
288
|
+
if self.client is None:
|
|
289
|
+
raise PiExited("pi client is unavailable")
|
|
277
290
|
await self._run_iteration()
|
|
278
291
|
except (PiError, PiExited, PiTimeout, OSError) as exc:
|
|
279
292
|
if self._stop_requested:
|
|
@@ -315,7 +328,8 @@ class LoopEngine:
|
|
|
315
328
|
self.ui.log_line("info", "Loop stopped")
|
|
316
329
|
|
|
317
330
|
async def _run_iteration(self) -> None:
|
|
318
|
-
|
|
331
|
+
if self.client is None:
|
|
332
|
+
raise PiExited("pi client is unavailable")
|
|
319
333
|
self._settled.clear()
|
|
320
334
|
self._seen_agent_start = False
|
|
321
335
|
|
|
@@ -350,7 +364,8 @@ class LoopEngine:
|
|
|
350
364
|
|
|
351
365
|
async def _await_settle(self) -> None:
|
|
352
366
|
"""Bounded wait for `agent_settled`. Cannot hang (DESIGN §4.2)."""
|
|
353
|
-
|
|
367
|
+
if self.client is None:
|
|
368
|
+
raise PiExited("pi client is unavailable")
|
|
354
369
|
cfg = self.config
|
|
355
370
|
deadline = time.monotonic() + cfg.iteration_timeout
|
|
356
371
|
idle_polls = 0
|
|
@@ -24,6 +24,7 @@ from piocloop.loop import (
|
|
|
24
24
|
LoopConfig,
|
|
25
25
|
LoopEngine,
|
|
26
26
|
)
|
|
27
|
+
from piocloop.pi_client import PiTimeout
|
|
27
28
|
|
|
28
29
|
FAKE_PI = str(Path(__file__).parent / "fake_pi.py")
|
|
29
30
|
|
|
@@ -92,6 +93,26 @@ async def wait_for_state(engine: LoopEngine, states: set[str], timeout: float =
|
|
|
92
93
|
pytest.fail(f"engine stuck in {engine.state!r}, never reached {states}")
|
|
93
94
|
|
|
94
95
|
|
|
96
|
+
class RespawnReadinessFailureClient:
|
|
97
|
+
"""Minimal client sequence: first session fails, replacement never readies."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, replacement: bool) -> None:
|
|
100
|
+
self.replacement = replacement
|
|
101
|
+
self.events: asyncio.Queue[dict] = asyncio.Queue()
|
|
102
|
+
self.exited = asyncio.Event()
|
|
103
|
+
|
|
104
|
+
async def start(self) -> None:
|
|
105
|
+
if self.replacement:
|
|
106
|
+
raise PiTimeout("no response to 'get_state' within deadline")
|
|
107
|
+
|
|
108
|
+
async def close(self) -> None:
|
|
109
|
+
self.exited.set()
|
|
110
|
+
|
|
111
|
+
async def new_session(self) -> dict:
|
|
112
|
+
raise PiTimeout("no response to 'new_session' within deadline")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
95
116
|
class EngineHarness:
|
|
96
117
|
"""Boot an engine, run it in the background, always clean it up."""
|
|
97
118
|
|
|
@@ -114,9 +135,38 @@ class EngineHarness:
|
|
|
114
135
|
await self.engine.shutdown()
|
|
115
136
|
|
|
116
137
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
138
|
+
async def test_failed_respawn_readiness_is_a_controlled_retry_then_stop(workspace):
|
|
139
|
+
"""A replacement that never becomes ready must not hit the client assert."""
|
|
140
|
+
created = 0
|
|
141
|
+
|
|
142
|
+
def factory() -> RespawnReadinessFailureClient:
|
|
143
|
+
nonlocal created
|
|
144
|
+
created += 1
|
|
145
|
+
return RespawnReadinessFailureClient(replacement=created > 1)
|
|
146
|
+
|
|
147
|
+
config = make_config(
|
|
148
|
+
workspace,
|
|
149
|
+
"normal",
|
|
150
|
+
max_respawns=1,
|
|
151
|
+
max_error_streak=2,
|
|
152
|
+
error_backoff_base=1.0,
|
|
153
|
+
max_stalls=0,
|
|
154
|
+
)
|
|
155
|
+
engine = LoopEngine(config, ui=RecordingUI(), client_factory=factory)
|
|
156
|
+
assert await engine.boot()
|
|
157
|
+
task = asyncio.create_task(engine.run(auto_start=True))
|
|
158
|
+
try:
|
|
159
|
+
await wait_for_state(engine, {STATE_ERROR}, timeout=5.0)
|
|
160
|
+
assert engine.client is None
|
|
161
|
+
assert engine.error_streak == 2
|
|
162
|
+
finally:
|
|
163
|
+
engine.request_stop()
|
|
164
|
+
await asyncio.wait_for(task, timeout=5.0)
|
|
165
|
+
await engine.shutdown()
|
|
166
|
+
|
|
167
|
+
assert engine.state == STATE_ERROR
|
|
168
|
+
|
|
169
|
+
|
|
120
170
|
|
|
121
171
|
|
|
122
172
|
async def test_plan_runs_to_completion(workspace):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|