devlyn-cli 4.3.0 → 4.3.3

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 (62) hide show
  1. package/bin/skill-history.json +3 -0
  2. package/config/skills/_shared/harness-store.py +2 -1
  3. package/config/skills/_shared/task-completion.md +6 -3
  4. package/config/skills/_shared/warm-build-dirs.py +97 -2
  5. package/config/skills/devlyn-ideate/scripts/queue.py +10 -1
  6. package/harness/AGENTS.md +37 -0
  7. package/harness/CLAUDE.md +37 -0
  8. package/native/claude/.claude-plugin/plugin.json +6 -0
  9. package/native/claude/skills/_shared/.ruff.toml +2 -0
  10. package/native/claude/skills/_shared/adapters/claude.md +15 -0
  11. package/native/claude/skills/_shared/adapters/codex.md +14 -0
  12. package/native/claude/skills/_shared/adapters/omp.md +6 -0
  13. package/native/claude/skills/_shared/codex-monitored.sh +263 -0
  14. package/native/claude/skills/_shared/engine-doctor.sh +70 -0
  15. package/native/claude/skills/_shared/engine-preflight.md +29 -0
  16. package/native/claude/skills/_shared/expected-contract.py +242 -0
  17. package/native/claude/skills/_shared/expected.schema.json +120 -0
  18. package/native/claude/skills/_shared/harness-store.py +202 -0
  19. package/native/claude/skills/_shared/invocation-receipt.py +499 -0
  20. package/native/claude/skills/_shared/platform-support.py +395 -0
  21. package/native/claude/skills/_shared/role-config.py +247 -0
  22. package/native/claude/skills/_shared/runtime-principles.md +35 -0
  23. package/native/claude/skills/_shared/task-complete.py +2865 -0
  24. package/native/claude/skills/_shared/task-completion.md +249 -0
  25. package/native/claude/skills/_shared/warm-build-dirs.py +1065 -0
  26. package/native/claude/skills/devlyn-engines/SKILL.md +63 -0
  27. package/native/claude/skills/devlyn-ideate/SKILL.md +132 -0
  28. package/native/claude/skills/devlyn-ideate/references/elicitation.md +29 -0
  29. package/native/claude/skills/devlyn-ideate/references/loop.md +123 -0
  30. package/native/claude/skills/devlyn-ideate/references/package-format.md +128 -0
  31. package/native/claude/skills/devlyn-ideate/references/plan-add.md +12 -0
  32. package/native/claude/skills/devlyn-ideate/scripts/acceptance.py +882 -0
  33. package/native/claude/skills/devlyn-ideate/scripts/queue.py +2191 -0
  34. package/native/codex/.agents/plugins/marketplace.json +12 -0
  35. package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +6 -0
  36. package/native/codex/plugins/devlyn/skills/_shared/.ruff.toml +2 -0
  37. package/native/codex/plugins/devlyn/skills/_shared/adapters/claude.md +15 -0
  38. package/native/codex/plugins/devlyn/skills/_shared/adapters/codex.md +14 -0
  39. package/native/codex/plugins/devlyn/skills/_shared/adapters/omp.md +6 -0
  40. package/native/codex/plugins/devlyn/skills/_shared/codex-monitored.sh +263 -0
  41. package/native/codex/plugins/devlyn/skills/_shared/engine-doctor.sh +70 -0
  42. package/native/codex/plugins/devlyn/skills/_shared/engine-preflight.md +29 -0
  43. package/native/codex/plugins/devlyn/skills/_shared/expected-contract.py +242 -0
  44. package/native/codex/plugins/devlyn/skills/_shared/expected.schema.json +120 -0
  45. package/native/codex/plugins/devlyn/skills/_shared/harness-store.py +202 -0
  46. package/native/codex/plugins/devlyn/skills/_shared/invocation-receipt.py +499 -0
  47. package/native/codex/plugins/devlyn/skills/_shared/platform-support.py +395 -0
  48. package/native/codex/plugins/devlyn/skills/_shared/role-config.py +247 -0
  49. package/native/codex/plugins/devlyn/skills/_shared/runtime-principles.md +35 -0
  50. package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +2865 -0
  51. package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +249 -0
  52. package/native/codex/plugins/devlyn/skills/_shared/warm-build-dirs.py +1065 -0
  53. package/native/codex/plugins/devlyn/skills/devlyn-engines/SKILL.md +63 -0
  54. package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +132 -0
  55. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/elicitation.md +29 -0
  56. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +123 -0
  57. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/package-format.md +128 -0
  58. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/plan-add.md +12 -0
  59. package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py +882 -0
  60. package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +2191 -0
  61. package/package.json +1 -1
  62. package/release.json +1630 -0
@@ -0,0 +1,499 @@
1
+ #!/usr/bin/env python3
2
+ """Dispatch `codex exec` for codex-monitored.sh: exact argv, an optional sealed prompt file, native process ownership."""
3
+ from __future__ import annotations
4
+
5
+ import runpy
6
+
7
+ import argparse
8
+ import hashlib
9
+ import json
10
+ import os
11
+ import pathlib
12
+ import re
13
+ import signal
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+ import tempfile
18
+ import time
19
+
20
+
21
+ PLATFORM = runpy.run_path(pathlib.Path(__file__).with_name("platform-support.py"))
22
+
23
+
24
+ class TransportError(ValueError):
25
+ pass
26
+
27
+
28
+ def reject_constant(token: str) -> None:
29
+ raise ValueError(f"invalid JSON numeric constant: {token}")
30
+
31
+
32
+ def reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict:
33
+ result = {}
34
+ for key, value in pairs:
35
+ if key in result:
36
+ raise ValueError(f"duplicate JSON key: {key}")
37
+ result[key] = value
38
+ return result
39
+
40
+
41
+ def loads_strict_json(text: str):
42
+ return json.loads(
43
+ text,
44
+ parse_constant=reject_constant,
45
+ object_pairs_hook=reject_duplicate_keys,
46
+ )
47
+
48
+
49
+ def sha256(raw: bytes) -> str:
50
+ return hashlib.sha256(raw).hexdigest()
51
+
52
+
53
+ def atomic_write(path: pathlib.Path, value: object) -> None:
54
+ path.parent.mkdir(parents=True, exist_ok=True)
55
+ raw = (json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8")
56
+ fd, temporary = tempfile.mkstemp(dir=path.parent, prefix=path.name + ".tmp.")
57
+ try:
58
+ with os.fdopen(fd, "wb") as handle:
59
+ handle.write(raw)
60
+ pathlib.Path(temporary).replace(path)
61
+ except BaseException:
62
+ pathlib.Path(temporary).unlink(missing_ok=True)
63
+ raise
64
+
65
+
66
+ def file_prompt_args(argv):
67
+ """File mode accepts one explicit stdin prompt and no positional competitors."""
68
+ values = {"-C", "--cd", "-s", "--sandbox", "-m", "--model", "-c", "--config",
69
+ "-o", "--output-last-message", "--output-schema", "--color", "--enable",
70
+ "--disable", "--add-dir"}
71
+ flags = {"--json", "--ignore-user-config", "--ignore-rules", "--ephemeral",
72
+ "--skip-git-repo-check", "--full-auto"}
73
+ positional = []
74
+ index = 0
75
+ while index < len(argv):
76
+ token = argv[index]
77
+ if token in values:
78
+ if index + 1 >= len(argv):
79
+ raise TransportError(f"{token} requires a value")
80
+ index += 2
81
+ elif any(token.startswith(key + "=") for key in values) or token in flags:
82
+ index += 1
83
+ elif token == "--":
84
+ positional.extend(argv[index + 1:])
85
+ break
86
+ elif token == "-" or not token.startswith("-"):
87
+ positional.append(token)
88
+ index += 1
89
+ else:
90
+ raise TransportError(f"unsupported Codex file-transport option: {token}")
91
+ if positional != ["-"]:
92
+ raise TransportError("file transport requires the sole prompt argument '-'; competing prompts are forbidden")
93
+
94
+
95
+ def prepare_transport(prompt_path, command, argv, seconds):
96
+ path = pathlib.Path(prompt_path).resolve(strict=True)
97
+ with PLATFORM["open_stdin"](path) as source:
98
+ raw = source.read()
99
+ record = {"schema_version": 1, "transport": "stdin-file",
100
+ "prompt": {"path": str(path), "sha256": sha256(raw), "bytes": len(raw)},
101
+ "command": command, "argv": argv, "timeout_sec": seconds,
102
+ "status": "started", "exit_code": None}
103
+ carrier = path.with_name(path.name + ".transport.json")
104
+ if carrier.exists():
105
+ raise TransportError(f"prompt transport already exists: {carrier}")
106
+ stream = tempfile.TemporaryFile("w+b")
107
+ try:
108
+ stream.write(raw)
109
+ stream.seek(0)
110
+ except BaseException:
111
+ stream.close()
112
+ raise
113
+ return stream, carrier, record
114
+
115
+
116
+ def write_transport(path, record):
117
+ with path.open("xb") as stream:
118
+ stream.write((json.dumps(record, sort_keys=True) + "\n").encode("utf-8"))
119
+
120
+
121
+ def finish_transport(path, exit_code):
122
+ record = read_transport(path)
123
+ if record.get("status") != "started":
124
+ raise TransportError("prompt transport is not open")
125
+ record.update(status="completed", exit_code=exit_code)
126
+ atomic_write(path, record)
127
+
128
+
129
+ def read_transport(path: pathlib.Path) -> dict:
130
+ try:
131
+ value = loads_strict_json(path.read_text(encoding="utf-8"))
132
+ except (OSError, UnicodeError, ValueError) as exc:
133
+ raise TransportError(f"prompt transport is invalid: {exc}") from exc
134
+ if not isinstance(value, dict):
135
+ raise TransportError("prompt transport must contain an object")
136
+ return value
137
+
138
+
139
+ def monitor_descendant_regression() -> None:
140
+ if os.name == "nt":
141
+ print("SKIP POSIX shell process-group fixture; test-windows-portability.py exercises native Node/Python trees")
142
+ return
143
+ with tempfile.TemporaryDirectory() as raw_tmp:
144
+ work = pathlib.Path(raw_tmp)
145
+ fixture_bin = work / "bin"
146
+ fixture_bin.mkdir()
147
+ release_marker = work / "release"
148
+ codex_pid_marker = work / "codex.pid"
149
+ sleep_marker = work / "sleep.tsv"
150
+ real_bash = shutil.which("bash")
151
+ real_sleep = shutil.which("sleep")
152
+ if real_bash is None or real_sleep is None:
153
+ raise AssertionError("monitor regression requires bash and sleep")
154
+ real_bash = str(pathlib.Path(real_bash).resolve(strict=True))
155
+ real_sleep = str(pathlib.Path(real_sleep).resolve(strict=True))
156
+
157
+ fake_codex = work / "fake-codex"
158
+ fake_codex.write_text(
159
+ f"#!{real_bash}\n"
160
+ "printf '%s\\n' \"$$\" > \"$DEVLYN_WATCHDOG_TEST_CODEX_PID\"\n"
161
+ "while [ ! -e \"$DEVLYN_WATCHDOG_TEST_RELEASE\" ]; do\n"
162
+ " \"$DEVLYN_WATCHDOG_TEST_REAL_SLEEP\" 0.01\n"
163
+ "done\n",
164
+ encoding="utf-8",
165
+ )
166
+ fake_codex.chmod(0o755)
167
+ sleep_shim = fixture_bin / "sleep"
168
+ sleep_shim.write_text(
169
+ f"#!{real_bash}\n"
170
+ "printf '%s\\t%s\\n' \"$$\" \"${1-}\" >> "
171
+ "\"$DEVLYN_WATCHDOG_TEST_SLEEP_MARKER\"\n"
172
+ "exec \"$DEVLYN_WATCHDOG_TEST_REAL_SLEEP\" \"$@\"\n",
173
+ encoding="utf-8",
174
+ )
175
+ sleep_shim.chmod(0o755)
176
+
177
+ wrapper = pathlib.Path(__file__).resolve().with_name("codex-monitored.sh")
178
+ env = os.environ.copy()
179
+ for key in tuple(env):
180
+ if (
181
+ key.startswith("DEVLYN_WATCHDOG_TEST_")
182
+ or key.startswith("CODEX_MONITORED_")
183
+ or key in {"CODEX_BLOCKED", "CODEX_REAL_BIN", "DEVLYN_CODEX_PROMPT_FILE"}
184
+ ):
185
+ env.pop(key)
186
+ env.update({
187
+ "CODEX_BIN": str(fake_codex),
188
+ "CODEX_MONITORED_HEARTBEAT": "1",
189
+ "CODEX_MONITORED_TIMEOUT_SEC": "30",
190
+ "DEVLYN_WATCHDOG_TEST_CODEX_PID": str(codex_pid_marker),
191
+ "DEVLYN_WATCHDOG_TEST_RELEASE": str(release_marker),
192
+ "DEVLYN_WATCHDOG_TEST_SLEEP_MARKER": str(sleep_marker),
193
+ "DEVLYN_WATCHDOG_TEST_REAL_SLEEP": real_sleep,
194
+ "PATH": str(fixture_bin) + os.pathsep + env.get("PATH", ""),
195
+ })
196
+
197
+ wrapped: subprocess.Popen | None = None
198
+ fixture_groups: dict[int, set[int]] = {}
199
+ wrapper_pgid: int | None = None
200
+
201
+ def record_owned_process(pid: int, session_id: int) -> int | None:
202
+ try:
203
+ process_session = os.getsid(pid)
204
+ process_group = os.getpgid(pid)
205
+ except ProcessLookupError:
206
+ return None
207
+ except PermissionError as exc:
208
+ raise AssertionError(f"cannot inspect fixture pid {pid}: {exc}") from exc
209
+ if process_session != session_id:
210
+ raise AssertionError(
211
+ f"fixture pid {pid} escaped session {session_id}: {process_session}"
212
+ )
213
+ fixture_groups.setdefault(process_group, set()).update({pid, process_group})
214
+ return process_group
215
+
216
+ def read_announced_groups(session_id: int) -> tuple[
217
+ tuple[int, int] | None, dict[str, tuple[int, int]]
218
+ ]:
219
+ codex_info = None
220
+ if codex_pid_marker.is_file():
221
+ raw_pid = codex_pid_marker.read_text(encoding="utf-8")
222
+ if raw_pid.endswith("\n") and re.fullmatch(r"[1-9][0-9]*\n", raw_pid):
223
+ codex_pid = int(raw_pid.strip())
224
+ codex_pgid = record_owned_process(codex_pid, session_id)
225
+ if codex_pgid is not None:
226
+ codex_info = (codex_pid, codex_pgid)
227
+ elif raw_pid.endswith("\n"):
228
+ raise AssertionError(f"invalid fake Codex PID marker: {raw_pid!r}")
229
+
230
+ sleeps: dict[str, tuple[int, int]] = {}
231
+ if sleep_marker.is_file():
232
+ raw_sleeps = sleep_marker.read_text(encoding="utf-8")
233
+ complete = raw_sleeps if raw_sleeps.endswith("\n") else raw_sleeps.rsplit("\n", 1)[0]
234
+ for line in complete.splitlines():
235
+ match = re.fullmatch(r"([1-9][0-9]*)\t([^\t]+)", line)
236
+ if match is None:
237
+ raise AssertionError(f"invalid monitor sleep marker: {line!r}")
238
+ sleep_pid = int(match.group(1))
239
+ sleep_pgid = record_owned_process(sleep_pid, session_id)
240
+ if sleep_pgid is not None:
241
+ sleeps[match.group(2)] = (sleep_pid, sleep_pgid)
242
+ return codex_info, sleeps
243
+
244
+ def owned_group_alive(pgid: int, session_id: int) -> bool:
245
+ for pid in fixture_groups.get(pgid, ()):
246
+ try:
247
+ if os.getsid(pid) == session_id and os.getpgid(pid) == pgid:
248
+ return True
249
+ except ProcessLookupError:
250
+ continue
251
+ except PermissionError as exc:
252
+ raise AssertionError(
253
+ f"cannot inspect fixture process group {pgid}: {exc}"
254
+ ) from exc
255
+ return False
256
+
257
+ def cleanup_fixture(session_id: int) -> list[str]:
258
+ failures: list[str] = []
259
+ if wrapped is None:
260
+ return failures
261
+ try:
262
+ read_announced_groups(session_id)
263
+ except (OSError, UnicodeError, AssertionError) as exc:
264
+ failures.append(f"announcement discovery failed: {exc}")
265
+
266
+ def live_groups() -> list[int]:
267
+ result = []
268
+ for pgid in sorted(fixture_groups):
269
+ if pgid == os.getpgrp():
270
+ failures.append(f"refused to signal current process group {pgid}")
271
+ continue
272
+ try:
273
+ if owned_group_alive(pgid, session_id):
274
+ result.append(pgid)
275
+ except AssertionError as exc:
276
+ failures.append(str(exc))
277
+ return result
278
+
279
+ for pgid in live_groups():
280
+ try:
281
+ os.killpg(pgid, signal.SIGTERM)
282
+ except ProcessLookupError:
283
+ pass
284
+ except PermissionError as exc:
285
+ failures.append(f"TERM fixture process group {pgid} failed: {exc}")
286
+
287
+ term_deadline = time.monotonic() + 1.0
288
+ survivors = live_groups()
289
+ while survivors and time.monotonic() < term_deadline:
290
+ time.sleep(0.01)
291
+ survivors = live_groups()
292
+ for pgid in survivors:
293
+ try:
294
+ os.killpg(pgid, signal.SIGKILL)
295
+ except ProcessLookupError:
296
+ pass
297
+ except PermissionError as exc:
298
+ failures.append(f"KILL fixture process group {pgid} failed: {exc}")
299
+
300
+ kill_deadline = time.monotonic() + 1.0
301
+ survivors = live_groups()
302
+ while survivors and time.monotonic() < kill_deadline:
303
+ time.sleep(0.01)
304
+ survivors = live_groups()
305
+ if survivors:
306
+ failures.append(f"fixture process groups survived cleanup: {survivors}")
307
+
308
+ try:
309
+ wrapped.wait(timeout=1)
310
+ except subprocess.TimeoutExpired:
311
+ failures.append("wrapper wait exceeded cleanup deadline")
312
+ try:
313
+ wrapped.kill()
314
+ except ProcessLookupError:
315
+ pass
316
+ except PermissionError as exc:
317
+ failures.append(f"direct wrapper KILL failed: {exc}")
318
+ try:
319
+ wrapped.wait(timeout=1)
320
+ except subprocess.TimeoutExpired:
321
+ failures.append("wrapper remained alive after direct KILL")
322
+
323
+ try:
324
+ wrapped.communicate(timeout=1)
325
+ except subprocess.TimeoutExpired:
326
+ failures.append("captured stderr did not reach EOF during cleanup")
327
+ if wrapped.stderr is not None:
328
+ wrapped.stderr.close()
329
+ return failures
330
+
331
+ try:
332
+ wrapped = subprocess.Popen(
333
+ [
334
+ real_bash, str(wrapper), "-C", str(work), "-s", "workspace-write",
335
+ "-m", "gpt-wrapper", "verify monitor cleanup",
336
+ ],
337
+ cwd=work,
338
+ env=env,
339
+ stdout=subprocess.DEVNULL,
340
+ stderr=subprocess.PIPE,
341
+ start_new_session=True,
342
+ )
343
+ session_id = wrapped.pid
344
+ wrapper_pgid = record_owned_process(wrapped.pid, session_id)
345
+ if wrapper_pgid != wrapped.pid:
346
+ raise AssertionError(
347
+ f"wrapper did not own its fixture session/process group: {wrapper_pgid}"
348
+ )
349
+
350
+ setup_deadline = time.monotonic() + 5.0
351
+ ready = None
352
+ while time.monotonic() < setup_deadline:
353
+ codex_info, sleeps = read_announced_groups(session_id)
354
+ heartbeat = sleeps.get("1")
355
+ watchdog = sleeps.get("30")
356
+ if codex_info is not None and heartbeat is not None and watchdog is not None:
357
+ groups = {
358
+ wrapper_pgid,
359
+ codex_info[1],
360
+ heartbeat[1],
361
+ watchdog[1],
362
+ }
363
+ if len(groups) != 4:
364
+ raise AssertionError(
365
+ "wrapper, Codex, heartbeat, and watchdog did not own distinct groups: "
366
+ f"{sorted(groups)}"
367
+ )
368
+ if all(owned_group_alive(pgid, session_id) for pgid in groups):
369
+ ready = (codex_info, heartbeat, watchdog)
370
+ break
371
+ if wrapped.poll() is not None:
372
+ raise AssertionError(
373
+ f"wrapper exited during monitor setup with code {wrapped.returncode}"
374
+ )
375
+ time.sleep(0.01)
376
+ if ready is None:
377
+ sleep_detail = sleep_marker.read_text(encoding="utf-8") if sleep_marker.exists() else ""
378
+ raise AssertionError(
379
+ "monitor setup did not announce live Codex/heartbeat=1/watchdog=30 "
380
+ f"within 5 seconds: {sleep_detail!r}"
381
+ )
382
+
383
+ release_marker.write_bytes(b"release\n")
384
+ try:
385
+ _stdout, stderr = wrapped.communicate(timeout=5)
386
+ except subprocess.TimeoutExpired as exc:
387
+ raise AssertionError(
388
+ "wrapper completion or captured stderr EOF exceeded 5 seconds "
389
+ f"after release (returncode={wrapped.poll()})"
390
+ ) from exc
391
+ if wrapped.returncode != 0:
392
+ raise AssertionError(stderr.decode("utf-8", errors="replace"))
393
+ read_announced_groups(session_id)
394
+ survivors = [
395
+ pgid
396
+ for pgid in sorted(fixture_groups)
397
+ if owned_group_alive(pgid, session_id)
398
+ ]
399
+ if survivors:
400
+ raise AssertionError(
401
+ f"wrapper returned while fixture process groups remained alive: {survivors}"
402
+ )
403
+ except BaseException as exc:
404
+ session_id = wrapped.pid if wrapped is not None else -1
405
+ cleanup_failures = cleanup_fixture(session_id)
406
+ if cleanup_failures:
407
+ raise AssertionError(
408
+ f"monitor regression failed ({exc}); cleanup failed: "
409
+ + "; ".join(cleanup_failures)
410
+ ) from exc
411
+ raise
412
+
413
+
414
+ def self_test() -> int:
415
+ monitor_descendant_regression()
416
+ if os.name == "nt":
417
+ print("SKIP POSIX executable-shell wrapper fixture; native npm transport is covered by test-windows-portability.py")
418
+ return 0
419
+ with tempfile.TemporaryDirectory() as raw_tmp:
420
+ work = pathlib.Path(raw_tmp)
421
+ fake_codex = work / "fake-codex"
422
+ fake_codex.write_text("#!/usr/bin/env bash\ntouch native-started\n", encoding="utf-8")
423
+ fake_codex.chmod(0o755)
424
+ wrapper = pathlib.Path(__file__).with_name("codex-monitored.sh")
425
+ widened = subprocess.run(
426
+ ["bash", str(wrapper), "-C", str(work), "-s", "danger-full-access", "-m", "gpt-wrapper", "verify the task"],
427
+ cwd=work, env={**os.environ, "CODEX_BIN": str(fake_codex)},
428
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False,
429
+ )
430
+ assert widened.returncode == 64 and b"forbidden Codex sandbox" in widened.stderr, widened.stderr
431
+ assert widened.stdout == b"" and not (work / "native-started").exists()
432
+ print("PASS monitored-wrapper descendant cleanup and authority-widening refusal")
433
+ return 0
434
+
435
+
436
+ def complete_dispatch(exit_code):
437
+ prompt = os.environ.get("DEVLYN_CODEX_PROMPT_FILE")
438
+ if prompt:
439
+ path = pathlib.Path(prompt).resolve()
440
+ finish_transport(path.with_name(path.name + ".transport.json"), exit_code)
441
+
442
+
443
+ def dispatch_codex(args):
444
+ argv = args.argv[1:] if args.argv[:1] == ["--"] else args.argv
445
+ command = [args.binary, "exec", *argv]
446
+ actual = PLATFORM["native_argv"](command)
447
+ prompt = os.environ.get("DEVLYN_CODEX_PROMPT_FILE")
448
+ if prompt == "":
449
+ raise TransportError("DEVLYN_CODEX_PROMPT_FILE must name a readable prompt file")
450
+ stream, carrier, transport = None, None, None
451
+ try:
452
+ if prompt:
453
+ file_prompt_args(argv)
454
+ stream, carrier, transport = prepare_transport(prompt, command, actual, args.timeout)
455
+ if transport is not None:
456
+ write_transport(carrier, transport)
457
+ if os.name != "nt":
458
+ if stream is not None:
459
+ os.dup2(stream.fileno(), 0)
460
+ os.execvp(actual[0], actual)
461
+ code = PLATFORM["run_process"](actual, stream if stream is not None else subprocess.DEVNULL,
462
+ args.timeout, heartbeat=args.heartbeat)
463
+ complete_dispatch(code)
464
+ print(f"[codex-monitored] codex exited: code={code}", file=sys.stderr)
465
+ return code
466
+ finally:
467
+ if stream is not None:
468
+ stream.close()
469
+
470
+
471
+ def main() -> int:
472
+ parser = argparse.ArgumentParser(description=__doc__)
473
+ parser.add_argument("--self-test", action="store_true")
474
+ subparsers = parser.add_subparsers(dest="action")
475
+ dispatch = subparsers.add_parser("dispatch")
476
+ dispatch.add_argument("--binary", required=True)
477
+ dispatch.add_argument("--timeout", type=int, required=True)
478
+ dispatch.add_argument("--heartbeat", type=int, required=True)
479
+ dispatch.add_argument("argv", nargs=argparse.REMAINDER)
480
+ completed = subparsers.add_parser("complete-dispatch")
481
+ completed.add_argument("--exit-code", type=int, required=True)
482
+ args = parser.parse_args()
483
+ if args.self_test:
484
+ return self_test()
485
+ try:
486
+ if args.action == "dispatch":
487
+ return dispatch_codex(args)
488
+ elif args.action == "complete-dispatch":
489
+ complete_dispatch(args.exit_code)
490
+ else:
491
+ parser.error("an action is required")
492
+ except (OSError, UnicodeError, ValueError) as exc:
493
+ parser.error(str(exc))
494
+ return 0
495
+
496
+
497
+ if __name__ == "__main__":
498
+ runpy.run_path(str(pathlib.Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
499
+ raise SystemExit(PLATFORM["system_exit_code"](main()))