piocloop 0.1.0__tar.gz → 0.1.1__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 (30) hide show
  1. {piocloop-0.1.0 → piocloop-0.1.1}/.gitignore +2 -0
  2. {piocloop-0.1.0 → piocloop-0.1.1}/PKG-INFO +1 -1
  3. {piocloop-0.1.0 → piocloop-0.1.1}/pyproject.toml +1 -1
  4. {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/cli.py +4 -1
  5. {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/loop.py +14 -1
  6. {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/pi_client.py +17 -1
  7. {piocloop-0.1.0 → piocloop-0.1.1}/tests/fake_pi.py +5 -1
  8. {piocloop-0.1.0 → piocloop-0.1.1}/tests/test_pi_client.py +6 -0
  9. {piocloop-0.1.0 → piocloop-0.1.1}/.github/workflows/publish.yml +0 -0
  10. {piocloop-0.1.0 → piocloop-0.1.1}/ANALYSIS.md +0 -0
  11. {piocloop-0.1.0 → piocloop-0.1.1}/DESIGN.md +0 -0
  12. {piocloop-0.1.0 → piocloop-0.1.1}/LICENSE +0 -0
  13. {piocloop-0.1.0 → piocloop-0.1.1}/PLAN.md +0 -0
  14. {piocloop-0.1.0 → piocloop-0.1.1}/README.md +0 -0
  15. {piocloop-0.1.0 → piocloop-0.1.1}/skills/README.md +0 -0
  16. {piocloop-0.1.0 → piocloop-0.1.1}/skills/piocloop-setup/SKILL.md +0 -0
  17. {piocloop-0.1.0 → piocloop-0.1.1}/skills/piocloop-setup/assets/PLAN.template.md +0 -0
  18. {piocloop-0.1.0 → piocloop-0.1.1}/skills/piocloop-setup/assets/loop-prompt.template.md +0 -0
  19. {piocloop-0.1.0 → piocloop-0.1.1}/skills/piocloop-setup/references/PLAN-FORMAT.md +0 -0
  20. {piocloop-0.1.0 → piocloop-0.1.1}/skills/piocloop-setup/references/PROMPT-AUTHORING.md +0 -0
  21. {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/__init__.py +0 -0
  22. {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/__main__.py +0 -0
  23. {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/pi_events.py +0 -0
  24. {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/plan_parser.py +0 -0
  25. {piocloop-0.1.0 → piocloop-0.1.1}/src/piocloop/tui.py +0 -0
  26. {piocloop-0.1.0 → piocloop-0.1.1}/tests/test_loop.py +0 -0
  27. {piocloop-0.1.0 → piocloop-0.1.1}/tests/test_pi_events.py +0 -0
  28. {piocloop-0.1.0 → piocloop-0.1.1}/tests/test_plan_parser.py +0 -0
  29. {piocloop-0.1.0 → piocloop-0.1.1}/tests/test_skill_assets.py +0 -0
  30. {piocloop-0.1.0 → piocloop-0.1.1}/tests/test_tui.py +0 -0
@@ -49,3 +49,5 @@ Thumbs.db
49
49
  # Project-specific
50
50
  *.log
51
51
  .pi/
52
+ .env.local
53
+ .local.env
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: piocloop
3
- Version: 0.1.0
3
+ Version: 0.1.1
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.0"
7
+ version = "0.1.1"
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" }
@@ -101,7 +101,10 @@ def run(
101
101
  help="How to answer blocking extension dialogs: cancel|allow|deny",
102
102
  ),
103
103
  session_dir: Optional[Path] = typer.Option(None, "--session-dir", help="Passed through to pi"),
104
- no_session: bool = typer.Option(False, "--no-session", help="Do not persist pi sessions"),
104
+ no_session: bool = typer.Option(
105
+ True, "--no-session/--persist-sessions",
106
+ help="Use an ephemeral pi session (default); --persist-sessions enables session files",
107
+ ),
105
108
  tools: Optional[str] = typer.Option(None, "--tools", help="Comma-separated tool allowlist"),
106
109
  exclude_tools: Optional[str] = typer.Option(
107
110
  None, "--exclude-tools", help="Comma-separated tool denylist"
@@ -319,7 +319,20 @@ class LoopEngine:
319
319
  self._settled.clear()
320
320
  self._seen_agent_start = False
321
321
 
322
- await self.client.new_session()
322
+ try:
323
+ await self.client.new_session()
324
+ except (PiTimeout, PiExited) as exc:
325
+ # A session-switch timeout is not a recoverable iteration error:
326
+ # pi can have accepted the switch while losing its response, so
327
+ # retrying on the same process is unsafe and used to produce the
328
+ # characteristic three consecutive ``new_session`` failures.
329
+ # Replace the process, with start() performing a readiness
330
+ # handshake before the next iteration reaches this point.
331
+ self.ui.log_line("warn", f"new_session failed; restarting pi: {exc}")
332
+ if self.respawns >= self.config.max_respawns:
333
+ raise
334
+ await self._respawn_client()
335
+ raise
323
336
  self.iteration += 1
324
337
  self.iter_start_time = time.monotonic()
325
338
  self.ui.log_line("start", f"Iteration {self.iteration}")
@@ -137,7 +137,17 @@ class PiClient:
137
137
  # Lifecycle
138
138
  # ------------------------------------------------------------------
139
139
 
140
- async def start(self) -> None:
140
+ async def start(self, *, readiness_timeout: float = 60.0) -> None:
141
+ """Start pi and wait until its RPC session is usable.
142
+
143
+ ``create_subprocess_exec`` only means that the OS process exists. Pi
144
+ still has to load its model catalogue, session and extensions before
145
+ it starts consuming RPC commands. Sending ``new_session`` during
146
+ that window is particularly fragile with persistent sessions: the
147
+ command can sit unhandled until the caller's deadline expires. A
148
+ bounded get_state handshake makes readiness explicit and also gives
149
+ respawned clients the same startup guarantee as the first client.
150
+ """
141
151
  if self._proc is not None:
142
152
  raise PiError("PiClient already started")
143
153
  try:
@@ -158,6 +168,12 @@ class PiClient:
158
168
  asyncio.create_task(self._read_stdout(), name="pi-stdout"),
159
169
  asyncio.create_task(self._read_stderr(), name="pi-stderr"),
160
170
  ]
171
+ try:
172
+ # Do not let callers race startup with the first session switch.
173
+ await self.request({"type": "get_state"}, timeout=readiness_timeout)
174
+ except Exception:
175
+ await self.close()
176
+ raise
161
177
 
162
178
  async def close(self, timeout: float = 5.0) -> None:
163
179
  """Shut the process down. Idempotent, and safe to call after a crash."""
@@ -17,6 +17,7 @@ import time
17
17
  # fallback cannot mistake a stuck run for a finished one.
18
18
  _PRETEND_STREAMING = {"busy", "wedged"}
19
19
  _streaming = False
20
+ _ready = False
20
21
 
21
22
 
22
23
  def work_the_plan(message: str) -> None:
@@ -86,7 +87,7 @@ def settle() -> None:
86
87
 
87
88
 
88
89
  def main() -> int:
89
- global _streaming
90
+ global _streaming, _ready
90
91
  scenario = sys.argv[1] if len(sys.argv) > 1 else "normal"
91
92
 
92
93
  while True:
@@ -116,11 +117,14 @@ def main() -> int:
116
117
  continue
117
118
 
118
119
  if ctype == "get_state":
120
+ _ready = True
119
121
  ok("get_state", rid,
120
122
  {"isStreaming": _streaming, "sessionId": "fake", "messageCount": 0})
121
123
  continue
122
124
 
123
125
  if ctype == "new_session":
126
+ if scenario == "readiness" and not _ready:
127
+ continue
124
128
  ok("new_session", rid, {"cancelled": False})
125
129
  continue
126
130
 
@@ -126,6 +126,12 @@ async def test_new_session_and_abort():
126
126
  await collect_until(client, "agent_settled")
127
127
 
128
128
 
129
+ async def test_start_waits_for_rpc_readiness_before_new_session():
130
+ """Startup must complete before the first session replacement is sent."""
131
+ async with PiClient(fake_argv("readiness")) as client:
132
+ assert await client.new_session() == {"cancelled": False}
133
+
134
+
129
135
  async def test_concurrent_requests_are_correlated():
130
136
  """Interleaved commands must not cross-resolve each other's futures."""
131
137
  async with PiClient(fake_argv("normal")) as client:
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