okstra 0.160.0 → 0.162.0

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.
@@ -0,0 +1,595 @@
1
+ """cmux command helpers for okstra-owned worker surfaces.
2
+
3
+ The tmux sibling of this module is `tmux.py`. cmux is a GUI app, so there is no
4
+ detached-server equivalent here — every surface okstra creates lands in the
5
+ workspace the user is already looking at.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import shlex
12
+ import shutil
13
+ import socket
14
+ import subprocess
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Any, Sequence
18
+
19
+ PING_OK = "PONG"
20
+
21
+ # cmux answers over a local unix socket, so a probe that has not returned in a
22
+ # few seconds means the app is wedged or gone — not that it is being slow.
23
+ PROBE_TIMEOUT_SECONDS = 5
24
+
25
+ # A login shell sources the user's whole rc chain, which can be slow on a
26
+ # developer machine, so this gets far more room than a socket probe.
27
+ LOGIN_SHELL_TIMEOUT_SECONDS = 15
28
+
29
+ # cmux plants a per-surface directory of CLI shims on PATH; every entry under it
30
+ # re-enters cmux's own agent wrapper instead of the real CLI.
31
+ SHIM_DIR_MARKER = "cmux-cli-shims"
32
+
33
+ # Workers fill two columns and then grow downward. A third column would cost
34
+ # width, and width is the dimension a horizontal split cannot give back.
35
+ GRID_COLUMNS = 2
36
+
37
+ # Measured against a Claude Code worker: 67 columns renders losslessly, 33 drops
38
+ # content off the right edge. Below this floor okstra stacks a tab instead of
39
+ # splitting, because an unreadable pane costs the lead its view of that worker.
40
+ WORKER_MIN_COLUMNS = 60
41
+
42
+ # What the lead keeps for itself once workers arrive.
43
+ LEAD_TARGET_COLUMNS = 80
44
+
45
+ # Sidebar entries are keyed by source so tools do not overwrite each other's.
46
+ SIDEBAR_SOURCE = "okstra"
47
+
48
+ # Why a run that prepare recorded as cmux can no longer see cmux. Kept apart
49
+ # because they call for opposite responses: a sanitized environment or a denied
50
+ # socket means a sandbox stands in the way and the fallback is doomed with it,
51
+ # while a quit app leaves the worker CLIs perfectly able to run.
52
+ LOST_NOTHING = ""
53
+ LOST_ENVIRONMENT = "environment"
54
+ LOST_DENIED = "denied"
55
+ LOST_GONE = "gone"
56
+
57
+ # Verdicts from `socket_reachability`. `denied` is the one that matters: it
58
+ # means a sandbox stands between this process and cmux, and the same sandbox
59
+ # hides the worker CLIs' own config, so falling back is already doomed.
60
+ SOCKET_OK = "ok"
61
+ SOCKET_DENIED = "denied"
62
+ SOCKET_MISSING = "missing"
63
+ SOCKET_UNREACHABLE = "unreachable"
64
+
65
+ SOCKET_PROBE_TIMEOUT_SECONDS = 2
66
+
67
+
68
+ def cmux_cli_path() -> str:
69
+ """Absolute path to the cmux CLI, or "" when cmux is not installed.
70
+
71
+ The bundled CLI wins over PATH: cmux documents the /usr/local/bin/cmux
72
+ symlink as a manual step, so a working install may leave PATH untouched.
73
+ """
74
+ bundled = os.environ.get("CMUX_BUNDLED_CLI_PATH", "")
75
+ if bundled and os.access(bundled, os.X_OK):
76
+ return bundled
77
+ return shutil.which("cmux") or ""
78
+
79
+
80
+ def run_cmux(
81
+ args: Sequence[str], *, timeout: int = PROBE_TIMEOUT_SECONDS
82
+ ) -> subprocess.CompletedProcess[str]:
83
+ cli = cmux_cli_path()
84
+ if not cli:
85
+ raise FileNotFoundError(
86
+ "cmux CLI not found: CMUX_BUNDLED_CLI_PATH unset and no cmux on PATH"
87
+ )
88
+ return subprocess.run(
89
+ [cli, *args],
90
+ capture_output=True,
91
+ text=True,
92
+ timeout=timeout,
93
+ check=False,
94
+ )
95
+
96
+
97
+ def cmux_available() -> bool:
98
+ """True only when okstra can drive cmux *and* knows where the lead sits.
99
+
100
+ Being able to drive cmux is not sufficient. Workers attach to the lead's
101
+ workspace, so a run whose lead location is unresolvable would open panes on
102
+ a screen nobody is watching; degrading to the blocking wrapper is the better
103
+ failure. Nested tmux is exactly that case — see `resolve_lead_workspace`.
104
+ """
105
+ if not cmux_cli_path():
106
+ return False
107
+ # Cheapest discriminator first: no workspace in the environment means this
108
+ # is not a cmux-hosted session, and the probes below would only confirm that
109
+ # the app happens to be installed.
110
+ if not _lead_workspace_env():
111
+ return False
112
+ if not _ping_answers():
113
+ return False
114
+ return bool(resolve_lead_workspace())
115
+
116
+
117
+ def resolve_lead_workspace() -> str:
118
+ """The lead's workspace UUID, or "" when it cannot be resolved.
119
+
120
+ The UUID is read from the environment rather than from `identify`, whose
121
+ refs are positional and shift as surfaces open and close. `identify` is
122
+ consulted only to prove the environment is current: a tmux server freezes
123
+ the CMUX_* block it was launched with, so those variables can outlive the
124
+ surface they name.
125
+ """
126
+ workspace = _lead_workspace_env()
127
+ if not workspace:
128
+ return ""
129
+ if not identify_caller():
130
+ return ""
131
+ return workspace
132
+
133
+
134
+ def _lead_workspace_env() -> str:
135
+ return os.environ.get("CMUX_WORKSPACE_ID", "").strip()
136
+
137
+
138
+ @dataclass(frozen=True)
139
+ class PaneGeometry:
140
+ """One pane as `pane.list` reports it.
141
+
142
+ `columns` and `rows` come straight from cmux rather than being derived from
143
+ the container frame, so a display with a different cell size needs no
144
+ conversion here.
145
+ """
146
+
147
+ pane_id: str
148
+ surface_ids: tuple[str, ...]
149
+ columns: int
150
+ rows: int
151
+ x: int
152
+ y: int
153
+ cell_width_points: int
154
+ ref: str = ""
155
+ selected_surface_id: str = ""
156
+
157
+
158
+ @dataclass(frozen=True)
159
+ class Placement:
160
+ """Where the next worker goes: split `pane_id`, or stack a tab into it."""
161
+
162
+ pane_id: str
163
+ direction: str
164
+ stack_as_tab: bool
165
+
166
+
167
+ def plan_worker_placement(
168
+ panes: Sequence[PaneGeometry], *, lead_pane_id: str, min_columns: int
169
+ ) -> Placement:
170
+ """Pick the next worker slot from the workspace's current geometry.
171
+
172
+ Stateless by design: okstra records surface UUIDs, never a layout, so a
173
+ resumed or crashed run cannot carry a layout model that no longer matches
174
+ the screen. Every dispatch re-reads the panes and derives the next slot.
175
+ """
176
+ workers = [pane for pane in panes if pane.pane_id != lead_pane_id]
177
+ if not workers:
178
+ return Placement(pane_id=lead_pane_id, direction="right", stack_as_tab=False)
179
+
180
+ columns = _panes_by_column(workers)
181
+ if len(columns) < GRID_COLUMNS:
182
+ return _widen_the_grid(workers, min_columns=min_columns)
183
+ return _extend_the_shortest_column(columns)
184
+
185
+
186
+ def lead_shrink_points(lead: PaneGeometry, *, target_columns: int) -> int:
187
+ """How far to push the lead's right border, in the points `pane.resize` takes.
188
+
189
+ The API's `amount` is points, not cells — measured at this pane's own
190
+ `cell_width_points`, so passing a column count shrinks by an eighth of the
191
+ intent on a typical display.
192
+ """
193
+ surplus = lead.columns - target_columns
194
+ if surplus <= 0:
195
+ return 0
196
+ return surplus * lead.cell_width_points
197
+
198
+
199
+ def _widen_the_grid(
200
+ workers: Sequence[PaneGeometry], *, min_columns: int
201
+ ) -> Placement:
202
+ rightmost = max(workers, key=lambda pane: pane.x)
203
+ if rightmost.columns // GRID_COLUMNS >= min_columns:
204
+ return Placement(pane_id=rightmost.pane_id, direction="right", stack_as_tab=False)
205
+ roomiest = min(workers, key=lambda pane: (len(pane.surface_ids), pane.x))
206
+ return Placement(pane_id=roomiest.pane_id, direction="", stack_as_tab=True)
207
+
208
+
209
+ def _extend_the_shortest_column(
210
+ columns: dict[int, list[PaneGeometry]]
211
+ ) -> Placement:
212
+ shortest = min(columns.values(), key=lambda group: (len(group), group[0].x))
213
+ bottom = max(shortest, key=lambda pane: pane.y)
214
+ return Placement(pane_id=bottom.pane_id, direction="down", stack_as_tab=False)
215
+
216
+
217
+ def _panes_by_column(
218
+ workers: Sequence[PaneGeometry],
219
+ ) -> dict[int, list[PaneGeometry]]:
220
+ columns: dict[int, list[PaneGeometry]] = {}
221
+ for pane in workers:
222
+ columns.setdefault(pane.x, []).append(pane)
223
+ return columns
224
+
225
+
226
+ def shim_free_login_path() -> str:
227
+ """The user's login PATH with cmux's per-surface CLI shims removed.
228
+
229
+ A cmux pane execs its command through `login … bash --noprofile --norc`, so
230
+ none of the user's shell rc runs and PATH is cmux's own short list. What that
231
+ list does hold is a shim directory bound to the new surface, where `claude`
232
+ and `codex` are wrappers into cmux's agent lifecycle rather than the real
233
+ CLIs — and where a provider cmux does not know has no entry at all, which is
234
+ a plain exit 127 inside the worker wrapper.
235
+
236
+ Restoring the login shell's PATH and dropping the shim entries gives the
237
+ wrapper what it would see in an ordinary terminal, which is what its own
238
+ `command -v <cli>` check expects. Nothing here is provider-specific, so
239
+ adding a provider does not touch this path.
240
+ """
241
+ entries = _login_shell_path().split(os.pathsep)
242
+ return os.pathsep.join(
243
+ entry for entry in entries if entry and SHIM_DIR_MARKER not in entry
244
+ )
245
+
246
+
247
+ def worker_command_line(
248
+ *, cwd: Path, argv: Sequence[str], path_value: str
249
+ ) -> str:
250
+ """The single shell line a cmux pane execs to run one worker.
251
+
252
+ Every part is shell-quoted. PATH entries routinely contain spaces — macOS
253
+ ships `/Applications/VMware Fusion.app/Contents/Public` on any machine with
254
+ VMware — and an unquoted assignment stops at the first space, then hands the
255
+ remainder to the shell as a command name.
256
+
257
+ The `cd` is part of the command because neither `new-split` nor
258
+ `respawn-pane` accepts a working directory the way `tmux split-window -c`
259
+ does.
260
+ """
261
+ return (
262
+ f"cd {shlex.quote(str(cwd))} && "
263
+ f"PATH={shlex.quote(path_value)} exec {shlex.join(argv)}"
264
+ )
265
+
266
+
267
+ def list_panes(workspace: str) -> list[PaneGeometry]:
268
+ payload = rpc("pane.list", {"workspace_id": workspace})
269
+ return [_pane_geometry(entry) for entry in payload.get("panes", [])]
270
+
271
+
272
+ def spawn_worker_surface(
273
+ *, workspace: str, cwd: Path, command: Sequence[str], title: str
274
+ ) -> str:
275
+ """Start one worker beside the lead and return its surface UUID.
276
+
277
+ The UUID is what okstra records and later closes by. Positional refs cannot
278
+ serve that purpose: cmux renumbers them as surfaces open and close, so a
279
+ close by ref can land on a pane okstra never created.
280
+ """
281
+ panes = list_panes(workspace)
282
+ lead = _lead_pane(panes)
283
+ placement = plan_worker_placement(
284
+ panes, lead_pane_id=lead.pane_id, min_columns=WORKER_MIN_COLUMNS
285
+ )
286
+ target = _pane_by_id(panes, placement.pane_id)
287
+ surface_uuid = _open_worker_surface(workspace, placement, target)
288
+ run_cmux(["rename-tab", "--surface", surface_uuid, "--title", title])
289
+ _exec_worker(surface_uuid, cwd=cwd, command=command)
290
+ _shrink_lead_pane(workspace)
291
+ return surface_uuid
292
+
293
+
294
+ def close_surface(surface_uuid: str) -> None:
295
+ """Close an okstra-created surface, killing whatever still runs inside it."""
296
+ try:
297
+ run_cmux(["close-surface", "--surface", surface_uuid])
298
+ except (OSError, subprocess.SubprocessError):
299
+ return
300
+
301
+
302
+ def capture_surface(surface_uuid: str, *, last_lines: int = 200) -> str:
303
+ """What the worker's screen shows — for the lead to look at, never to parse.
304
+
305
+ cmux hands back a rendered grid: wrapped to the pane's width with a finite
306
+ history, so a path or a count read off it can be silently truncated.
307
+ """
308
+ try:
309
+ result = run_cmux(
310
+ [
311
+ "capture-pane",
312
+ "--surface",
313
+ surface_uuid,
314
+ "--scrollback",
315
+ "--lines",
316
+ str(last_lines),
317
+ ]
318
+ )
319
+ except (OSError, subprocess.SubprocessError):
320
+ return ""
321
+ return result.stdout if result.returncode == 0 else ""
322
+
323
+
324
+ def socket_reachability() -> str:
325
+ """Why this process can or cannot reach cmux, decided at syscall level.
326
+
327
+ Separating a sandbox from a closed app without matching on cmux's error
328
+ text: a denied connect raises PermissionError, an absent socket raises
329
+ FileNotFoundError. The distinction decides whether degrading is worth
330
+ attempting at all.
331
+ """
332
+ path = os.environ.get("CMUX_SOCKET_PATH", "").strip()
333
+ if not path:
334
+ return SOCKET_MISSING
335
+ probe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
336
+ try:
337
+ probe.settimeout(SOCKET_PROBE_TIMEOUT_SECONDS)
338
+ probe.connect(path)
339
+ return SOCKET_OK
340
+ except PermissionError:
341
+ return SOCKET_DENIED
342
+ except FileNotFoundError:
343
+ return SOCKET_MISSING
344
+ except OSError:
345
+ return SOCKET_UNREACHABLE
346
+ finally:
347
+ probe.close()
348
+
349
+
350
+ def unreachable_reason() -> str:
351
+ """Why cmux cannot be reached from here, or "" when it can.
352
+
353
+ `environment` is the case a socket probe alone cannot see: a sandbox that
354
+ sanitizes the environment leaves no CMUX_* variables at all, so cmux looks
355
+ identical to a machine that never had it — except this run's manifest says
356
+ prepare reached it minutes ago.
357
+ """
358
+ if not _lead_workspace_env():
359
+ return LOST_ENVIRONMENT
360
+ reachability = socket_reachability()
361
+ if reachability == SOCKET_DENIED:
362
+ return LOST_DENIED
363
+ if reachability != SOCKET_OK:
364
+ return LOST_GONE
365
+ return LOST_NOTHING if resolve_lead_workspace() else LOST_GONE
366
+
367
+
368
+ def sidebar_log(workspace: str, message: str, *, level: str = "info") -> None:
369
+ """Append one line to the workspace sidebar's log."""
370
+ _sidebar_call(
371
+ workspace,
372
+ ["log", "--workspace", workspace, "--level", level,
373
+ "--source", SIDEBAR_SOURCE, "--", message],
374
+ )
375
+
376
+
377
+ def sidebar_notify(workspace: str, *, title: str, body: str) -> None:
378
+ """Raise a notification, which badges the tab even from another workspace."""
379
+ _sidebar_call(
380
+ workspace,
381
+ ["notify", "--workspace", workspace, "--title", title, "--body", body],
382
+ )
383
+
384
+
385
+ def sidebar_progress(workspace: str, done: int, total: int, *, label: str) -> None:
386
+ """Show `done`/`total` as the sidebar's progress bar.
387
+
388
+ An empty roster reports nothing rather than zero percent: no workers is not
389
+ the same claim as no progress.
390
+ """
391
+ if total <= 0:
392
+ return
393
+ fraction = min(1.0, done / total)
394
+ _sidebar_call(
395
+ workspace,
396
+ ["set-progress", str(round(fraction, 2)), "--workspace", workspace,
397
+ "--label", label],
398
+ )
399
+
400
+
401
+ def _sidebar_call(workspace: str, args: Sequence[str]) -> None:
402
+ """Best-effort sidebar write.
403
+
404
+ The sidebar is a view of the run, not part of it, so a wedged or closed cmux
405
+ must never take a dispatch down. The workspace is always explicit: letting
406
+ cmux choose would drop one run's noise into whichever workspace happens to
407
+ be focused, which on a machine running two okstra tasks is the other one.
408
+ """
409
+ if not workspace:
410
+ return
411
+ try:
412
+ run_cmux(args)
413
+ except (OSError, subprocess.SubprocessError):
414
+ return
415
+
416
+
417
+ def rpc(method: str, params: dict[str, Any]) -> dict[str, Any]:
418
+ """Call a cmux rpc method.
419
+
420
+ The rpc surface is used rather than the same-named CLI verbs wherever both
421
+ exist, because the CLI swallows failures — `resize-pane` reports success on
422
+ a rejected resize, while `pane.resize` returns the reason.
423
+ """
424
+ result = run_cmux(["rpc", method, json.dumps(params)])
425
+ if result.returncode != 0:
426
+ raise RuntimeError(result.stderr.strip() or f"cmux {method} failed")
427
+ try:
428
+ payload = json.loads(result.stdout)
429
+ except ValueError as exc:
430
+ raise RuntimeError(f"cmux {method} returned non-JSON output") from exc
431
+ return payload if isinstance(payload, dict) else {}
432
+
433
+
434
+ def _open_worker_surface(
435
+ workspace: str, placement: Placement, target: PaneGeometry
436
+ ) -> str:
437
+ before = _surface_ids(workspace)
438
+ _create_surface(workspace, placement, target)
439
+ new_ids = _surface_ids(workspace) - before
440
+ if len(new_ids) != 1:
441
+ raise RuntimeError(
442
+ f"cmux opened {len(new_ids)} surfaces where exactly one was expected"
443
+ )
444
+ return new_ids.pop()
445
+
446
+
447
+ def _surface_ids(workspace: str) -> set[str]:
448
+ """Every surface UUID in the workspace.
449
+
450
+ The new surface is identified by diffing this set rather than by translating
451
+ the `OK surface:N` echo, because `list-pane-surfaces` reports only the
452
+ focused pane unless given a `--pane`, and the new pane is not focused.
453
+ """
454
+ return {
455
+ surface_id
456
+ for pane in list_panes(workspace)
457
+ for surface_id in pane.surface_ids
458
+ }
459
+
460
+
461
+ def _create_surface(
462
+ workspace: str, placement: Placement, target: PaneGeometry
463
+ ) -> None:
464
+ if placement.stack_as_tab:
465
+ created = run_cmux(
466
+ ["new-surface", "--workspace", workspace, "--pane", target.pane_id]
467
+ )
468
+ else:
469
+ created = run_cmux(
470
+ [
471
+ "new-split",
472
+ placement.direction,
473
+ "--workspace",
474
+ workspace,
475
+ "--surface",
476
+ target.selected_surface_id or target.surface_ids[0],
477
+ ]
478
+ )
479
+ if created.returncode != 0:
480
+ raise RuntimeError(created.stderr.strip() or "cmux could not open a pane")
481
+
482
+
483
+ def _exec_worker(surface_uuid: str, *, cwd: Path, command: Sequence[str]) -> None:
484
+ line = worker_command_line(
485
+ cwd=cwd, argv=command, path_value=shim_free_login_path()
486
+ )
487
+ started = run_cmux(["respawn-pane", "--surface", surface_uuid, "--command", line])
488
+ if started.returncode != 0:
489
+ raise RuntimeError(started.stderr.strip() or "cmux could not start the worker")
490
+
491
+
492
+ def _shrink_lead_pane(workspace: str) -> None:
493
+ """Give the lead's width to the workers by pushing its right border left.
494
+
495
+ The lead cannot shrink itself: `pane.resize` moves the named pane's border,
496
+ so asking the leftmost pane to move `left` fails with no adjacent border and
497
+ `right` widens it. The neighbour on its right carries the request instead.
498
+ """
499
+ panes = list_panes(workspace)
500
+ lead = _lead_pane(panes)
501
+ amount = lead_shrink_points(lead, target_columns=LEAD_TARGET_COLUMNS)
502
+ if amount <= 0:
503
+ return
504
+ neighbours = [pane for pane in panes if pane.x > lead.x]
505
+ if not neighbours:
506
+ return
507
+ rpc(
508
+ "pane.resize",
509
+ {
510
+ "workspace_id": workspace,
511
+ "pane_id": min(neighbours, key=lambda pane: pane.x).pane_id,
512
+ "direction": "left",
513
+ "amount": amount,
514
+ },
515
+ )
516
+
517
+
518
+ def _lead_pane(panes: Sequence[PaneGeometry]) -> PaneGeometry:
519
+ caller_pane_ref = identify_caller().get("pane_ref", "")
520
+ for pane in panes:
521
+ if caller_pane_ref and pane.ref == caller_pane_ref:
522
+ return pane
523
+ raise RuntimeError("cmux could not locate the lead's pane in its workspace")
524
+
525
+
526
+ def _pane_by_id(panes: Sequence[PaneGeometry], pane_id: str) -> PaneGeometry:
527
+ for pane in panes:
528
+ if pane.pane_id == pane_id:
529
+ return pane
530
+ raise RuntimeError(f"cmux pane {pane_id} disappeared while placing a worker")
531
+
532
+
533
+ def _pane_geometry(entry: dict[str, Any]) -> PaneGeometry:
534
+ frame = entry.get("pixel_frame") or {}
535
+ return PaneGeometry(
536
+ pane_id=str(entry.get("id", "")),
537
+ surface_ids=tuple(str(s) for s in entry.get("surface_ids") or ()),
538
+ columns=int(entry.get("columns", 0)),
539
+ rows=int(entry.get("rows", 0)),
540
+ x=int(frame.get("x", 0)),
541
+ y=int(frame.get("y", 0)),
542
+ cell_width_points=int(entry.get("cell_width_points", 0)),
543
+ ref=str(entry.get("ref", "")),
544
+ selected_surface_id=str(entry.get("selected_surface_id", "")),
545
+ )
546
+
547
+
548
+ def _login_shell_path() -> str:
549
+ """PATH as the user's own login shell resolves it.
550
+
551
+ Falls back to the inherited PATH: a worker started with a shim-filtered
552
+ inherited PATH is still better than one started with cmux's bare list.
553
+ """
554
+ shell = os.environ.get("SHELL", "") or "/bin/sh"
555
+ try:
556
+ result = subprocess.run(
557
+ [shell, "-lc", 'printf %s "$PATH"'],
558
+ capture_output=True,
559
+ text=True,
560
+ timeout=LOGIN_SHELL_TIMEOUT_SECONDS,
561
+ check=False,
562
+ )
563
+ except (OSError, subprocess.SubprocessError):
564
+ return os.environ.get("PATH", "")
565
+ if result.returncode != 0 or not result.stdout.strip():
566
+ return os.environ.get("PATH", "")
567
+ return result.stdout.strip()
568
+
569
+
570
+ def identify_caller() -> dict[str, Any]:
571
+ """The caller's ref bundle, or {} when cmux cannot resolve it.
572
+
573
+ `identify` exits 0 even for an id it cannot resolve, so the exit code says
574
+ nothing; a null `caller` is the only signal that the lookup failed.
575
+ """
576
+ try:
577
+ result = run_cmux(["identify"])
578
+ except (OSError, subprocess.SubprocessError):
579
+ return {}
580
+ if result.returncode != 0:
581
+ return {}
582
+ try:
583
+ payload = json.loads(result.stdout)
584
+ except ValueError:
585
+ return {}
586
+ caller = payload.get("caller")
587
+ return caller if isinstance(caller, dict) else {}
588
+
589
+
590
+ def _ping_answers() -> bool:
591
+ try:
592
+ result = run_cmux(["ping"])
593
+ except (OSError, subprocess.SubprocessError):
594
+ return False
595
+ return result.returncode == 0 and result.stdout.strip() == PING_OK
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  import argparse
10
10
  import json
11
- import os
12
11
  import subprocess
13
12
  import sys
14
13
  from dataclasses import dataclass
@@ -762,18 +761,7 @@ def _run_cli_wrapper_worker(
762
761
  plan: DispatchPlan,
763
762
  worker: WorkerJob,
764
763
  ) -> subprocess.CompletedProcess[str]:
765
- env = {
766
- **os.environ,
767
- "OKSTRA_WORKER_ID": worker.worker_id,
768
- "OKSTRA_WORKER_RESULT_PATH": str(worker.result_path),
769
- "OKSTRA_WORKER_AUDIT_PATH": str(worker.worker_result_path),
770
- "OKSTRA_RUN_MANIFEST_PATH": str(plan.manifest_path),
771
- }
772
- if worker.worker_id == REPORT_WRITER_WORKER_ID:
773
- env["OKSTRA_REPORT_WRITER_MARKDOWN_PATH"] = str(
774
- _final_report_markdown_path(worker.result_path)
775
- )
776
- return subprocess.run(worker.command, cwd=plan.project_root, env=env, text=True)
764
+ return subprocess.run(worker.command, cwd=plan.project_root, text=True)
777
765
 
778
766
 
779
767
  def _post_process_report_writer_result(