devlyn-cli 4.0.1 → 4.2.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.
Files changed (72) hide show
  1. package/AGENTS.md +11 -95
  2. package/CLAUDE.md +7 -160
  3. package/LICENSE +21 -0
  4. package/README.md +83 -114
  5. package/bin/devlyn.js +330 -472
  6. package/bin/instruction-templates.json +120 -2
  7. package/bin/instructions.js +35 -8
  8. package/config/skills/_shared/adapters/claude.md +8 -85
  9. package/config/skills/_shared/adapters/codex.md +14 -34
  10. package/config/skills/_shared/adapters/omp.md +1 -11
  11. package/config/skills/_shared/codex-monitored.sh +4 -54
  12. package/config/skills/_shared/engine-doctor.sh +18 -58
  13. package/config/skills/_shared/engine-preflight.md +15 -47
  14. package/config/skills/_shared/expected-contract.py +242 -0
  15. package/config/skills/_shared/expected.schema.json +10 -95
  16. package/config/skills/_shared/invocation-receipt.py +26 -757
  17. package/config/skills/_shared/platform-support.py +37 -28
  18. package/config/skills/_shared/role-config.py +100 -421
  19. package/config/skills/_shared/runtime-principles.md +17 -93
  20. package/config/skills/_shared/task-complete.py +490 -285
  21. package/config/skills/_shared/task-completion.md +235 -0
  22. package/config/skills/devlyn-engines/SKILL.md +9 -11
  23. package/config/skills/devlyn-ideate/SKILL.md +57 -107
  24. package/config/skills/devlyn-ideate/references/elicitation.md +18 -99
  25. package/config/skills/devlyn-ideate/references/loop.md +110 -0
  26. package/config/skills/devlyn-ideate/references/package-format.md +128 -0
  27. package/config/skills/devlyn-ideate/scripts/acceptance.py +795 -0
  28. package/config/skills/devlyn-ideate/scripts/queue.py +1780 -0
  29. package/package.json +2 -3
  30. package/config/commit-conventions.md +0 -28
  31. package/config/skills/_shared/adapters/README.md +0 -74
  32. package/config/skills/_shared/adapters/grok.md +0 -100
  33. package/config/skills/_shared/archive_run.py +0 -969
  34. package/config/skills/_shared/codex-config.md +0 -81
  35. package/config/skills/_shared/collect-codex-findings.py +0 -505
  36. package/config/skills/_shared/finish-gate.py +0 -521
  37. package/config/skills/_shared/grok-anchor-guard.py +0 -78
  38. package/config/skills/_shared/judge-output-parser.py +0 -200
  39. package/config/skills/_shared/judge-role-evidence.py +0 -320
  40. package/config/skills/_shared/phase-prompt-render.py +0 -183
  41. package/config/skills/_shared/process-evidence.py +0 -926
  42. package/config/skills/_shared/resolve-bootstrap.py +0 -1410
  43. package/config/skills/_shared/resolve-stop-hook.py +0 -359
  44. package/config/skills/_shared/run-bounded.py +0 -58
  45. package/config/skills/_shared/spec-verify-check.py +0 -5083
  46. package/config/skills/_shared/state-phase-write.py +0 -5989
  47. package/config/skills/_shared/terminal-claim-check.py +0 -777
  48. package/config/skills/_shared/verify-merge-findings.py +0 -3528
  49. package/config/skills/devlyn-design-ui/SKILL.md +0 -650
  50. package/config/skills/devlyn-ideate/references/from-spec-mode.md +0 -61
  51. package/config/skills/devlyn-ideate/references/project-mode.md +0 -80
  52. package/config/skills/devlyn-ideate/references/spec-template.md +0 -121
  53. package/config/skills/devlyn-ideate/references/templates/decision.md +0 -48
  54. package/config/skills/devlyn-ideate/references/templates/roadmap.md +0 -50
  55. package/config/skills/devlyn-ideate/references/templates/vision.md +0 -44
  56. package/config/skills/devlyn-queue/SKILL.md +0 -66
  57. package/config/skills/devlyn-queue/scripts/append.py +0 -94
  58. package/config/skills/devlyn-resolve/SKILL.md +0 -379
  59. package/config/skills/devlyn-resolve/references/free-form-mode.md +0 -71
  60. package/config/skills/devlyn-resolve/references/outer-loop.md +0 -23
  61. package/config/skills/devlyn-resolve/references/phases/build-gate.md +0 -75
  62. package/config/skills/devlyn-resolve/references/phases/cleanup.md +0 -33
  63. package/config/skills/devlyn-resolve/references/phases/implement.md +0 -44
  64. package/config/skills/devlyn-resolve/references/phases/plan.md +0 -39
  65. package/config/skills/devlyn-resolve/references/phases/probe-derive.md +0 -261
  66. package/config/skills/devlyn-resolve/references/phases/surface-close.md +0 -34
  67. package/config/skills/devlyn-resolve/references/phases/verify.md +0 -310
  68. package/config/skills/devlyn-resolve/references/state-schema.md +0 -152
  69. package/config/skills/devlyn-resolve/references/task-completion.md +0 -160
  70. package/config/templates/prompt-templates.md +0 -71
  71. package/config/templates/template-feature.spec.md +0 -255
  72. package/config/templates/template-product-spec.md +0 -680
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Create and validate run-owned Codex invocation receipts."""
2
+ """Dispatch `codex exec` for codex-monitored.sh: exact argv, an optional sealed prompt file, native process ownership."""
3
3
  from __future__ import annotations
4
4
 
5
5
  import runpy
@@ -18,16 +18,10 @@ import tempfile
18
18
  import time
19
19
 
20
20
 
21
- SCHEMA_VERSION = "2.0"
22
- PHASES = {"plan", "implement", "build_gate", "cleanup"}
23
- SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
24
- SAFE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
25
- DANGEROUS_FLAGS = {"--dangerously-bypass-approvals-and-sandbox", "--yolo"}
26
- NETWORK_ACCESS_CONFIG = "sandbox_workspace_write.network_access"
27
21
  PLATFORM = runpy.run_path(pathlib.Path(__file__).with_name("platform-support.py"))
28
22
 
29
23
 
30
- class ReceiptError(ValueError):
24
+ class TransportError(ValueError):
31
25
  pass
32
26
 
33
27
 
@@ -69,62 +63,6 @@ def atomic_write(path: pathlib.Path, value: object) -> None:
69
63
  raise
70
64
 
71
65
 
72
- def relative_file(work: pathlib.Path, raw_path: str, label: str, *, require: bool) -> tuple[pathlib.Path, str]:
73
- path = pathlib.Path(raw_path)
74
- candidate = path if path.is_absolute() else work / path
75
- try:
76
- if require and candidate.is_symlink():
77
- raise ReceiptError(f"{label} must not be a symlink: {raw_path}")
78
- resolved = candidate.resolve(strict=require)
79
- relative = resolved.relative_to(work.resolve()).as_posix()
80
- except (OSError, ValueError) as exc:
81
- raise ReceiptError(f"{label} is missing or escapes the worktree: {raw_path}") from exc
82
- if require and not resolved.is_file():
83
- raise ReceiptError(f"{label} is not a regular file: {raw_path}")
84
- return resolved, relative
85
-
86
-
87
- def option_values(argv: list[str], short: str, long: str) -> list[str]:
88
- values = []
89
- index = 0
90
- while index < len(argv):
91
- token = argv[index]
92
- if token in {short, long}:
93
- if index + 1 >= len(argv):
94
- raise ReceiptError(f"{token} requires a value")
95
- values.append(argv[index + 1])
96
- index += 2
97
- continue
98
- if token.startswith(long + "="):
99
- values.append(token.split("=", 1)[1])
100
- elif token.startswith(short + "="):
101
- values.append(token.split("=", 1)[1])
102
- index += 1
103
- return values
104
-
105
-
106
- def option_value(argv: list[str], short: str, long: str) -> str | None:
107
- values = option_values(argv, short, long)
108
- if len(values) > 1:
109
- raise ReceiptError(f"duplicate {long} option")
110
- return values[0] if values else None
111
-
112
-
113
- def sandbox_network_access(argv: list[str], phase: str) -> bool:
114
- enabled = phase == "build_gate"
115
- expected = f"{NETWORK_ACCESS_CONFIG}={'true' if enabled else 'false'}"
116
- related = [
117
- raw for raw in option_values(argv, "-c", "--config")
118
- if "sandbox_workspace_write" in raw or "network_access" in raw
119
- ]
120
- if related != [expected]:
121
- raise ReceiptError(
122
- f"Codex {phase} requires exactly one -c {expected}; "
123
- "alternate, table, missing, and duplicate network overrides are forbidden"
124
- )
125
- return enabled
126
-
127
-
128
66
  def file_prompt_args(argv):
129
67
  """File mode accepts one explicit stdin prompt and no positional competitors."""
130
68
  values = {"-C", "--cd", "-s", "--sandbox", "-m", "--model", "-c", "--config",
@@ -138,7 +76,7 @@ def file_prompt_args(argv):
138
76
  token = argv[index]
139
77
  if token in values:
140
78
  if index + 1 >= len(argv):
141
- raise ReceiptError(f"{token} requires a value")
79
+ raise TransportError(f"{token} requires a value")
142
80
  index += 2
143
81
  elif any(token.startswith(key + "=") for key in values) or token in flags:
144
82
  index += 1
@@ -149,22 +87,22 @@ def file_prompt_args(argv):
149
87
  positional.append(token)
150
88
  index += 1
151
89
  else:
152
- raise ReceiptError(f"unsupported Codex file-transport option: {token}")
90
+ raise TransportError(f"unsupported Codex file-transport option: {token}")
153
91
  if positional != ["-"]:
154
- raise ReceiptError("file transport requires the sole prompt argument '-'; competing prompts are forbidden")
92
+ raise TransportError("file transport requires the sole prompt argument '-'; competing prompts are forbidden")
155
93
 
156
94
 
157
- def prepare_transport(prompt_path, command, argv, seconds, *, isolated=False):
95
+ def prepare_transport(prompt_path, command, argv, seconds):
158
96
  path = pathlib.Path(prompt_path).resolve(strict=True)
159
97
  with PLATFORM["open_stdin"](path) as source:
160
98
  raw = source.read()
161
99
  record = {"schema_version": 1, "transport": "stdin-file",
162
100
  "prompt": {"path": str(path), "sha256": sha256(raw), "bytes": len(raw)},
163
- "command": command, "argv": argv, "timeout_sec": seconds, "isolated": isolated,
101
+ "command": command, "argv": argv, "timeout_sec": seconds,
164
102
  "status": "started", "exit_code": None}
165
103
  carrier = path.with_name(path.name + ".transport.json")
166
104
  if carrier.exists():
167
- raise ReceiptError(f"prompt transport already exists: {carrier}")
105
+ raise TransportError(f"prompt transport already exists: {carrier}")
168
106
  stream = tempfile.TemporaryFile("w+b")
169
107
  try:
170
108
  stream.write(raw)
@@ -181,330 +119,23 @@ def write_transport(path, record):
181
119
 
182
120
 
183
121
  def finish_transport(path, exit_code):
184
- record = read_receipt(path)
122
+ record = read_transport(path)
185
123
  if record.get("status") != "started":
186
- raise ReceiptError("prompt transport is not open")
124
+ raise TransportError("prompt transport is not open")
187
125
  record.update(status="completed", exit_code=exit_code)
188
126
  atomic_write(path, record)
189
127
 
190
128
 
191
- def validate_transport(path, prompt_raw):
192
- record = read_receipt(path)
193
- if set(record) != {"schema_version", "transport", "prompt", "command", "argv", "timeout_sec", "isolated", "status", "exit_code"}:
194
- raise ReceiptError("invalid prompt transport shape")
195
- prompt = record["prompt"]
196
- if not isinstance(prompt, dict) or set(prompt) != {"path", "sha256", "bytes"}:
197
- raise ReceiptError("invalid delivered prompt binding")
198
- if (record["schema_version"] != 1 or record["transport"] != "stdin-file"
199
- or record["status"] != "completed" or type(record["exit_code"]) is not int
200
- or type(record["timeout_sec"]) is not int or record["timeout_sec"] < 0
201
- or type(record["isolated"]) is not bool
202
- or prompt["sha256"] != sha256(prompt_raw) or type(prompt["bytes"]) is not int
203
- or prompt["bytes"] != len(prompt_raw)):
204
- raise ReceiptError("delivered prompt/transport mismatch")
205
- original = pathlib.Path(prompt["path"])
206
- # Custody may relocate this sealed bundle; rehash its local prompt, not a live source tree.
207
- if (not original.is_absolute() or path.name != original.name + ".transport.json"
208
- or path.with_name(original.name).read_bytes() != prompt_raw):
209
- raise ReceiptError("transport prompt path/bytes mismatch")
210
- command, actual = record["command"], record["argv"]
211
- if not all(isinstance(args, list) and args and all(isinstance(arg, str) for arg in args)
212
- for args in (command, actual)) or len(actual) < len(command) or actual[len(actual) - len(command) + 1:] != command[1:]:
213
- raise ReceiptError("transport actual argv mismatch")
214
- return record
215
-
216
-
217
- def start_receipt(
218
- work: pathlib.Path,
219
- receipt_path: pathlib.Path,
220
- run_id: str,
221
- phase: str,
222
- round_: int,
223
- prompt_path: str,
224
- session_path: str,
225
- argv: list[str],
226
- *, transport: dict | None = None,
227
- ) -> None:
228
- if receipt_path.exists():
229
- raise ReceiptError(f"invocation receipt already exists: {receipt_path}")
230
- if SAFE_ID_RE.fullmatch(run_id) is None:
231
- raise ReceiptError("run id is invalid")
232
- if phase not in PHASES:
233
- raise ReceiptError(f"unsupported invocation phase: {phase}")
234
- if round_ < 0:
235
- raise ReceiptError("round must be non-negative")
236
- evidence_manifest = (
237
- work / ".devlyn" / "process-evidence" / run_id / phase
238
- / f"round-{round_}" / "manifest.json"
239
- )
240
- if evidence_manifest.is_file():
241
- manifest = loads_strict_json(evidence_manifest.read_text(encoding="utf-8"))
242
- entries = manifest.get("entries") if isinstance(manifest, dict) else None
243
- if not isinstance(entries, list):
244
- raise ReceiptError("existing process-evidence manifest is malformed")
245
- if any(
246
- isinstance(entry, dict)
247
- and isinstance(entry.get("classification"), dict)
248
- and entry["classification"].get("kind") == "capability_denied"
249
- for entry in entries
250
- ):
251
- raise ReceiptError(
252
- "capability-denied phase/round cannot launch another Codex invocation"
253
- )
254
- forbidden = sorted(DANGEROUS_FLAGS.intersection(argv))
255
- if forbidden:
256
- raise ReceiptError(f"forbidden Codex bypass flag: {forbidden[0]}")
257
- if "--json" not in argv[:-1]:
258
- raise ReceiptError("receipt-bound Codex invocation requires --json before the prompt")
259
- model = option_value(argv, "-m", "--model")
260
- sandbox = option_value(argv, "-s", "--sandbox")
261
- invocation_workdir = option_value(argv, "-C", "--cd")
262
- if not model:
263
- raise ReceiptError("Codex invocation requires an explicit model")
264
- if not sandbox:
265
- raise ReceiptError("Codex invocation requires an explicit sandbox")
266
- if sandbox != "workspace-write":
267
- raise ReceiptError(
268
- f"Codex {phase} sandbox must remain workspace-write, got {sandbox}"
269
- )
270
- network_access = sandbox_network_access(argv, phase)
271
- if not invocation_workdir:
272
- raise ReceiptError("Codex invocation requires an explicit workdir")
273
- try:
274
- if pathlib.Path(invocation_workdir).resolve(strict=True) != work.resolve():
275
- raise ReceiptError("Codex invocation workdir does not match receipt workdir")
276
- except OSError as exc:
277
- raise ReceiptError("Codex invocation workdir is missing") from exc
278
-
279
- prompt_file, prompt_relative = relative_file(work, prompt_path, "invocation prompt", require=True)
280
- session_file, session_relative = relative_file(work, session_path, "worker session", require=False)
281
- receipt_file, receipt_relative = relative_file(work, str(receipt_path), "invocation receipt", require=False)
282
- expected_receipt = f".devlyn/{phase}.invocation.{round_}.json"
283
- expected_session = f".devlyn/{phase}.worker-session.{round_}.jsonl"
284
- expected_prompt = f".devlyn/{phase}.prompt.{round_}"
285
- if receipt_relative != expected_receipt:
286
- raise ReceiptError(
287
- f"invocation receipt path expected {expected_receipt}, got {receipt_relative}"
288
- )
289
- if session_relative != expected_session:
290
- raise ReceiptError(
291
- f"worker session path expected {expected_session}, got {session_relative}"
292
- )
293
- if prompt_relative != expected_prompt:
294
- raise ReceiptError(
295
- f"invocation prompt path expected {expected_prompt}, got {prompt_relative}"
296
- )
297
- prompt_raw = prompt_file.read_bytes()
298
- try:
299
- prompt_argument = prompt_raw.decode("utf-8").rstrip("\n")
300
- except UnicodeError as exc:
301
- raise ReceiptError("invocation prompt is not UTF-8") from exc
302
- if transport is not None:
303
- file_prompt_args(argv)
304
- if (transport["prompt"] != {"path": str(prompt_file), "sha256": sha256(prompt_raw), "bytes": len(prompt_raw)}
305
- or transport["command"][1:] != ["exec", *argv]):
306
- raise ReceiptError("delivered prompt file/bytes or actual argv does not match the canonical invocation")
307
- elif not argv or argv[-1] == "-" or argv[-1] != prompt_argument:
308
- raise ReceiptError("Codex prompt argument does not match the canonical prompt file")
309
-
310
- atomic_write(receipt_file, {
311
- "schema_version": SCHEMA_VERSION,
312
- "run_id": run_id,
313
- "phase": phase,
314
- "round": round_,
315
- "engine": "codex",
316
- "model": model,
317
- "sandbox": sandbox,
318
- "sandbox_network_access": network_access,
319
- "prompt": {"path": prompt_relative, "sha256": sha256(prompt_raw)},
320
- "session": {"path": session_relative, "sha256": None, "bytes": None},
321
- "argv_sha256": sha256(json.dumps(argv, separators=(",", ":")).encode("utf-8")),
322
- "status": "started",
323
- "exit_code": None,
324
- **({"transport": {"path": prompt_relative + ".transport.json", "sha256": None}} if transport is not None else {}),
325
- })
326
-
327
-
328
- def read_receipt(path: pathlib.Path) -> dict:
129
+ def read_transport(path: pathlib.Path) -> dict:
329
130
  try:
330
131
  value = loads_strict_json(path.read_text(encoding="utf-8"))
331
132
  except (OSError, UnicodeError, ValueError) as exc:
332
- raise ReceiptError(f"invocation receipt is invalid: {exc}") from exc
133
+ raise TransportError(f"prompt transport is invalid: {exc}") from exc
333
134
  if not isinstance(value, dict):
334
- raise ReceiptError("invocation receipt must contain an object")
135
+ raise TransportError("prompt transport must contain an object")
335
136
  return value
336
137
 
337
138
 
338
- def finish_receipt(work: pathlib.Path, receipt_path: pathlib.Path, exit_code: int) -> None:
339
- receipt = read_receipt(receipt_path)
340
- if receipt.get("status") != "started" or receipt.get("exit_code") is not None:
341
- raise ReceiptError("invocation receipt is not open")
342
- session = receipt.get("session")
343
- session_path = session.get("path") if isinstance(session, dict) else None
344
- if not isinstance(session_path, str):
345
- raise ReceiptError("invocation receipt session path is invalid")
346
- session_file, _ = relative_file(work, session_path, "worker session", require=True)
347
- raw = session_file.read_bytes()
348
- receipt["session"] = {
349
- "path": session_path,
350
- "sha256": sha256(raw),
351
- "bytes": len(raw),
352
- }
353
- receipt["status"] = "completed"
354
- receipt["exit_code"] = exit_code
355
- if "transport" in receipt:
356
- carrier, _ = relative_file(work, receipt["transport"]["path"], "prompt transport", require=True)
357
- receipt["transport"]["sha256"] = sha256(carrier.read_bytes())
358
- atomic_write(receipt_path, receipt)
359
-
360
-
361
- def validate_receipt_artifacts(
362
- work: pathlib.Path,
363
- receipt_path: pathlib.Path,
364
- *,
365
- run_id: str,
366
- phase: str,
367
- ) -> tuple[dict, pathlib.Path, pathlib.Path, pathlib.Path]:
368
- """Validate self-contained receipt identity and every referenced byte carrier."""
369
- receipt_file, receipt_relative = relative_file(
370
- work, str(receipt_path), "invocation receipt", require=True,
371
- )
372
- receipt = read_receipt(receipt_file)
373
- if set(receipt) - {"transport"} != {
374
- "schema_version", "run_id", "phase", "round", "engine", "model",
375
- "sandbox", "sandbox_network_access", "prompt", "session", "argv_sha256",
376
- "status", "exit_code",
377
- }:
378
- raise ReceiptError("invocation receipt has an invalid shape")
379
- round_ = receipt.get("round")
380
- if isinstance(round_, bool) or not isinstance(round_, int) or round_ < 0:
381
- raise ReceiptError("invocation receipt round is invalid")
382
- expected_path = f".devlyn/{phase}.invocation.{round_}.json"
383
- if receipt_relative != expected_path:
384
- raise ReceiptError(f"invocation receipt path expected {expected_path}")
385
- expected_identity = (SCHEMA_VERSION, run_id, phase, round_, "codex", "completed")
386
- actual_identity = (
387
- receipt["schema_version"], receipt["run_id"], receipt["phase"], receipt["round"],
388
- receipt["engine"], receipt["status"],
389
- )
390
- if actual_identity != expected_identity:
391
- raise ReceiptError("invocation receipt identity/status mismatch")
392
- if not isinstance(receipt["model"], str) or not receipt["model"]:
393
- raise ReceiptError("invocation receipt model is invalid")
394
- if receipt["sandbox"] != "workspace-write":
395
- raise ReceiptError("invocation receipt sandbox must be workspace-write")
396
- expected_network_access = phase == "build_gate"
397
- if receipt["sandbox_network_access"] is not expected_network_access:
398
- raise ReceiptError("invocation receipt sandbox network-access capability mismatch")
399
- if not isinstance(receipt["exit_code"], int) or isinstance(receipt["exit_code"], bool):
400
- raise ReceiptError("invocation receipt exit_code is invalid")
401
- if not isinstance(receipt["argv_sha256"], str) or SHA256_RE.fullmatch(receipt["argv_sha256"]) is None:
402
- raise ReceiptError("invocation receipt argv digest is invalid")
403
- prompt = receipt["prompt"]
404
- if not isinstance(prompt, dict) or set(prompt) != {"path", "sha256"}:
405
- raise ReceiptError("invocation receipt prompt binding is invalid")
406
- prompt_file, _ = relative_file(work, prompt["path"], "invocation prompt", require=True)
407
- if prompt["path"] != f".devlyn/{phase}.prompt.{round_}":
408
- raise ReceiptError("invocation receipt prompt path mismatch")
409
- if (
410
- not isinstance(prompt["sha256"], str)
411
- or SHA256_RE.fullmatch(prompt["sha256"]) is None
412
- or sha256(prompt_file.read_bytes()) != prompt["sha256"]
413
- ):
414
- raise ReceiptError("invocation prompt digest mismatch")
415
- if "transport" in receipt:
416
- binding = receipt["transport"]
417
- if not isinstance(binding, dict) or set(binding) != {"path", "sha256"} or binding["path"] != prompt["path"] + ".transport.json":
418
- raise ReceiptError("invocation transport binding is invalid")
419
- carrier, _ = relative_file(work, binding["path"], "prompt transport", require=True)
420
- if sha256(carrier.read_bytes()) != binding["sha256"]:
421
- raise ReceiptError("invocation transport digest mismatch")
422
- transport = validate_transport(carrier, prompt_file.read_bytes())
423
- argv = transport["command"][2:]
424
- file_prompt_args(argv)
425
- if (transport["command"][1:2] != ["exec"] or transport["exit_code"] != receipt["exit_code"]
426
- or sha256(json.dumps(argv, separators=(",", ":")).encode("utf-8")) != receipt["argv_sha256"]
427
- or option_value(argv, "-m", "--model") != receipt["model"]
428
- or option_value(argv, "-s", "--sandbox") != receipt["sandbox"]
429
- or sandbox_network_access(argv, phase) != receipt["sandbox_network_access"]):
430
- raise ReceiptError("invocation actual argv/capability mismatch")
431
- session = receipt["session"]
432
- if not isinstance(session, dict) or set(session) != {"path", "sha256", "bytes"}:
433
- raise ReceiptError("invocation receipt session binding is invalid")
434
- if not isinstance(session["path"], str):
435
- raise ReceiptError("invocation receipt session path is invalid")
436
- session_file, relative_session = relative_file(
437
- work, session["path"], "worker session", require=True,
438
- )
439
- if relative_session != f".devlyn/{phase}.worker-session.{round_}.jsonl":
440
- raise ReceiptError("invocation receipt session path mismatch")
441
- raw = session_file.read_bytes()
442
- if (
443
- not isinstance(session["sha256"], str)
444
- or SHA256_RE.fullmatch(session["sha256"]) is None
445
- or isinstance(session["bytes"], bool)
446
- or not isinstance(session["bytes"], int)
447
- or session["bytes"] < 0
448
- or session["sha256"] != sha256(raw)
449
- or session["bytes"] != len(raw)
450
- ):
451
- raise ReceiptError("invocation worker-session digest mismatch")
452
- return receipt, receipt_file, prompt_file, session_file
453
-
454
-
455
- def validate_receipt(
456
- work: pathlib.Path,
457
- receipt_path: pathlib.Path,
458
- *,
459
- run_id: str,
460
- phase: str,
461
- round_: int,
462
- model: str,
463
- prompt_sha256: str,
464
- session_path: pathlib.Path,
465
- ) -> dict:
466
- receipt, receipt_file, _prompt_file, receipt_session = validate_receipt_artifacts(
467
- work, receipt_path, run_id=run_id, phase=phase,
468
- )
469
- if receipt["round"] != round_ or receipt["model"] != model:
470
- raise ReceiptError("invocation receipt round/model mismatch")
471
- if receipt["prompt"]["sha256"] != prompt_sha256:
472
- raise ReceiptError("invocation prompt digest mismatch")
473
- supplied_session, _ = relative_file(
474
- work, str(session_path), "worker session", require=True,
475
- )
476
- if supplied_session != receipt_session:
477
- raise ReceiptError("invocation receipt session path mismatch")
478
- raw = receipt_session.read_bytes()
479
- if sha256(raw) != receipt["session"]["sha256"]:
480
- raise ReceiptError("invocation worker-session digest mismatch")
481
- for number, line in enumerate(raw.decode("utf-8").split("\n"), start=1):
482
- if not line.strip():
483
- continue
484
- try:
485
- event = loads_strict_json(line)
486
- except ValueError as exc:
487
- raise ReceiptError(f"invalid worker-session JSONL at line {number}: {exc}") from exc
488
- if not isinstance(event, dict):
489
- raise ReceiptError(f"worker-session event at line {number} must be an object")
490
- item = event.get("item")
491
- if (event.get("type") == "item.completed" and isinstance(item, dict)
492
- and item.get("type") == "error" and isinstance(item.get("message"), str)
493
- and item["message"].startswith("model rerouted: ")):
494
- raise ReceiptError(
495
- f"Codex reported model reroute at worker-session line {number}; "
496
- "requested model was not preserved; inspect the retained session"
497
- )
498
- return {
499
- "path": receipt_file.relative_to(work.resolve()).as_posix(),
500
- "sha256": sha256(receipt_file.read_bytes()),
501
- "sandbox": receipt["sandbox"],
502
- "sandbox_network_access": receipt["sandbox_network_access"],
503
- "argv_sha256": receipt["argv_sha256"],
504
- "exit_code": receipt["exit_code"],
505
- }
506
-
507
-
508
139
  def monitor_descendant_regression() -> None:
509
140
  if os.name == "nt":
510
141
  print("SKIP POSIX shell process-group fixture; test-windows-portability.py exercises native Node/Python trees")
@@ -547,8 +178,7 @@ def monitor_descendant_regression() -> None:
547
178
  env = os.environ.copy()
548
179
  for key in tuple(env):
549
180
  if (
550
- key.startswith("DEVLYN_INVOCATION_")
551
- or key.startswith("DEVLYN_WATCHDOG_TEST_")
181
+ key.startswith("DEVLYN_WATCHDOG_TEST_")
552
182
  or key.startswith("CODEX_MONITORED_")
553
183
  or key in {"CODEX_BLOCKED", "CODEX_REAL_BIN", "DEVLYN_CODEX_PROMPT_FILE"}
554
184
  ):
@@ -783,342 +413,23 @@ def monitor_descendant_regression() -> None:
783
413
 
784
414
  def self_test() -> int:
785
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
786
419
  with tempfile.TemporaryDirectory() as raw_tmp:
787
420
  work = pathlib.Path(raw_tmp)
788
- devlyn = work / ".devlyn"
789
- devlyn.mkdir()
790
- prompt = devlyn / "implement.prompt.0"
791
- prompt.write_text("do the task\n", encoding="utf-8")
792
- session = devlyn / "implement.worker-session.0.jsonl"
793
- session.write_text('{"type":"thread.started"}\n', encoding="utf-8")
794
- receipt = devlyn / "implement.invocation.0.json"
795
- argv = [
796
- "--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-test",
797
- "-c", "sandbox_workspace_write.network_access=false", "do the task",
798
- ]
799
- start_receipt(work, receipt, "rs-receipt", "implement", 0, str(prompt), str(session), argv)
800
- finish_receipt(work, receipt, 0)
801
- bound = validate_receipt(
802
- work, receipt, run_id="rs-receipt", phase="implement", round_=0,
803
- model="gpt-test", prompt_sha256=sha256(prompt.read_bytes()), session_path=session,
804
- )
805
- assert bound["sandbox"] == "workspace-write" and bound["exit_code"] == 0
806
- original_session = session.read_bytes()
807
- original_receipt = receipt.read_bytes()
808
- reroute = "model rerouted: gpt-test -> another-model (Policy)"
809
- cases = [
810
- ({"type": "item.completed", "item": {"type": "error", "message": reroute}}, True),
811
- ({"type": "item.completed", "item": {"type": "agent_message", "text": reroute}}, False),
812
- ({"type": "item.completed", "item": {"type": "command_execution", "aggregated_output": reroute}}, False),
813
- ({"type": "item.completed", "item": {"type": "error", "message": "recoverable tool error"}}, False),
814
- ]
815
- cases.append(({"type": "item.completed", "item": {
816
- "type": "agent_message", "text": "ordinary \u2028 \u2029 \u0085 message",
817
- }}, False))
818
- for event, blocked in cases:
819
- raw = original_session + (json.dumps(event, ensure_ascii=False) + '\n{"type":"turn.completed"}\n').encode()
820
- session.write_bytes(raw)
821
- receipt.unlink()
822
- start_receipt(work, receipt, "rs-receipt", "implement", 0, str(prompt), str(session), argv)
823
- finish_receipt(work, receipt, 0)
824
- sealed = receipt.read_bytes()
825
- try:
826
- validate_receipt(work, receipt, run_id="rs-receipt", phase="implement", round_=0,
827
- model="gpt-test", prompt_sha256=sha256(prompt.read_bytes()), session_path=session)
828
- except ReceiptError as exc:
829
- assert blocked and "reported model reroute" in str(exc), str(exc)
830
- else:
831
- assert not blocked, "native reroute followed by success was accepted"
832
- validate_receipt_artifacts(work, receipt, run_id="rs-receipt", phase="implement")
833
- assert receipt.read_bytes() == sealed and session.read_bytes() == raw
834
- for malformed in (b'{broken\n', b'null\n', b'{"type":"item.completed","type":"ignored"}\n'):
835
- session.write_bytes(malformed)
836
- receipt.unlink()
837
- start_receipt(work, receipt, "rs-receipt", "implement", 0, str(prompt), str(session), argv)
838
- finish_receipt(work, receipt, 0)
839
- try:
840
- validate_receipt(work, receipt, run_id="rs-receipt", phase="implement", round_=0,
841
- model="gpt-test", prompt_sha256=sha256(prompt.read_bytes()), session_path=session)
842
- except ReceiptError:
843
- pass
844
- else:
845
- raise AssertionError("malformed worker event was accepted")
846
- validate_receipt_artifacts(work, receipt, run_id="rs-receipt", phase="implement")
847
- session.write_bytes(original_session)
848
- receipt.write_bytes(original_receipt)
849
- plan_prompt = devlyn / "plan.prompt.0"
850
- plan_prompt.write_text("plan exactly\n", encoding="utf-8")
851
- plan_session = devlyn / "plan.worker-session.0.jsonl"
852
- plan_session.write_text('{"type":"thread.started"}\n', encoding="utf-8")
853
- plan_receipt = devlyn / "plan.invocation.0.json"
854
- plan_argv = [
855
- "--json", "-C", str(work), "-s", "workspace-write",
856
- "-m", "gpt-plan", "-c",
857
- "sandbox_workspace_write.network_access=false", "plan exactly",
858
- ]
859
- start_receipt(
860
- work, plan_receipt, "rs-receipt", "plan", 0,
861
- str(plan_prompt), str(plan_session), plan_argv,
862
- )
863
- finish_receipt(work, plan_receipt, 0)
864
- plan_bound = validate_receipt(
865
- work, plan_receipt, run_id="rs-receipt", phase="plan", round_=0,
866
- model="gpt-plan", prompt_sha256=sha256(plan_prompt.read_bytes()),
867
- session_path=plan_session,
868
- )
869
- assert plan_bound["sandbox"] == "workspace-write"
870
- try:
871
- loads_strict_json('{"run_id":"a","run_id":"b"}')
872
- except ValueError as exc:
873
- assert "duplicate JSON key" in str(exc)
874
- else:
875
- raise AssertionError("duplicate invocation receipt key was accepted")
876
- try:
877
- start_receipt(
878
- work, devlyn / "cleanup.invocation.0.json", "rs-receipt", "cleanup", 0,
879
- str(prompt), str(devlyn / "cleanup.worker-session.0.jsonl"),
880
- ["--json", "--dangerously-bypass-approvals-and-sandbox", "-s", "danger-full-access",
881
- "-m", "gpt-test", "do the task"],
882
- )
883
- except ReceiptError as exc:
884
- assert "forbidden Codex bypass flag" in str(exc)
885
- else:
886
- raise AssertionError("Codex bypass flag was accepted")
887
- cleanup_prompt = devlyn / "cleanup.prompt.0"
888
- cleanup_prompt.write_text("do the task\n", encoding="utf-8")
889
- try:
890
- start_receipt(
891
- work, devlyn / "cleanup.invocation.0.json", "rs-receipt", "cleanup", 0,
892
- str(cleanup_prompt), str(devlyn / "cleanup.worker-session.0.jsonl"),
893
- ["--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-test",
894
- "do the task"],
895
- )
896
- except ReceiptError as exc:
897
- assert "requires exactly one -c sandbox_workspace_write.network_access=false" in str(exc)
898
- else:
899
- raise AssertionError("Codex cleanup accepted implicit network capability")
900
- try:
901
- start_receipt(
902
- work, devlyn / "cleanup.invocation.0.json", "rs-receipt", "cleanup", 0,
903
- str(cleanup_prompt), str(devlyn / "cleanup.worker-session.0.jsonl"),
904
- ["--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-test",
905
- "-c", "sandbox_workspace_write.network_access=true", "do the task"],
906
- )
907
- except ReceiptError as exc:
908
- assert "requires exactly one -c sandbox_workspace_write.network_access=false" in str(exc)
909
- else:
910
- raise AssertionError("Codex cleanup accepted enabled network capability")
911
- try:
912
- start_receipt(
913
- work, devlyn / "cleanup.invocation.0.json", "rs-receipt", "cleanup", 0,
914
- str(cleanup_prompt), str(devlyn / "cleanup.worker-session.0.jsonl"),
915
- ["--json", "-s", "danger-full-access", "-m", "gpt-test", "do the task"],
916
- )
917
- except ReceiptError as exc:
918
- assert "must remain workspace-write" in str(exc)
919
- else:
920
- raise AssertionError("widened Codex sandbox was accepted")
921
- try:
922
- start_receipt(
923
- work, devlyn / "cleanup.invocation.0.json", "rs-receipt", "cleanup", 0,
924
- str(cleanup_prompt), str(devlyn / "cleanup.worker-session.0.jsonl"),
925
- ["--json", "-C", str(devlyn), "-s", "workspace-write", "-m", "gpt-test",
926
- "-c", "sandbox_workspace_write.network_access=false", "do the task"],
927
- )
928
- except ReceiptError as exc:
929
- assert "workdir does not match" in str(exc)
930
- else:
931
- raise AssertionError("Codex invocation accepted a different working directory")
932
- session.write_text('{"type":"thread.started","mutated":true}\n', encoding="utf-8")
933
- try:
934
- validate_receipt(
935
- work, receipt, run_id="rs-receipt", phase="implement", round_=0,
936
- model="gpt-test", prompt_sha256=sha256(prompt.read_bytes()), session_path=session,
937
- )
938
- except ReceiptError as exc:
939
- assert "worker-session digest mismatch" in str(exc)
940
- else:
941
- raise AssertionError("mutated worker session was accepted")
942
-
943
- build_prompt = devlyn / "build_gate.prompt.0"
944
- build_prompt.write_text("verify the task\n", encoding="utf-8")
945
- build_session = devlyn / "build_gate.worker-session.0.jsonl"
946
- build_receipt = devlyn / "build_gate.invocation.0.json"
947
- try:
948
- start_receipt(
949
- work, build_receipt, "rs-wrapper", "build_gate", 0,
950
- str(build_prompt), str(build_session),
951
- ["--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-wrapper",
952
- "verify the task"],
953
- )
954
- except ReceiptError as exc:
955
- assert "requires exactly one -c sandbox_workspace_write.network_access=true" in str(exc)
956
- else:
957
- raise AssertionError("Codex build_gate accepted missing network capability")
958
- try:
959
- start_receipt(
960
- work, build_receipt, "rs-wrapper", "build_gate", 0,
961
- str(build_prompt), str(build_session),
962
- ["--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-wrapper",
963
- "-c", "sandbox_workspace_write.network_access=true",
964
- "-c", "sandbox_workspace_write.network_access=false",
965
- "verify the task"],
966
- )
967
- except ReceiptError as exc:
968
- assert "duplicate network overrides are forbidden" in str(exc)
969
- else:
970
- raise AssertionError("Codex build_gate accepted duplicate network capabilities")
971
- for alternate in (
972
- "sandbox_workspace_write={network_access=true}",
973
- '"sandbox_workspace_write".network_access=true',
974
- ):
975
- try:
976
- start_receipt(
977
- work, build_receipt, "rs-wrapper", "build_gate", 0,
978
- str(build_prompt), str(build_session),
979
- ["--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-wrapper",
980
- "-c", "sandbox_workspace_write.network_access=true",
981
- "--config=" + alternate, "verify the task"],
982
- )
983
- except ReceiptError as exc:
984
- assert "alternate, table, missing, and duplicate" in str(exc)
985
- else:
986
- raise AssertionError(f"Codex build_gate accepted alternate override: {alternate}")
987
- glued_prompt = devlyn / "build_gate.prompt.1"
988
- glued_prompt.write_text("verify glued config\n", encoding="utf-8")
989
- glued_session = devlyn / "build_gate.worker-session.1.jsonl"
990
- glued_session.write_text('{"type":"thread.started"}\n', encoding="utf-8")
991
- glued_receipt = devlyn / "build_gate.invocation.1.json"
992
- start_receipt(
993
- work, glued_receipt, "rs-glued", "build_gate", 1,
994
- str(glued_prompt), str(glued_session),
995
- ["--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-wrapper",
996
- "-c=sandbox_workspace_write.network_access=true", "verify glued config"],
997
- )
998
- finish_receipt(work, glued_receipt, 0)
999
- assert validate_receipt(
1000
- work, glued_receipt, run_id="rs-glued", phase="build_gate", round_=1,
1001
- model="gpt-wrapper", prompt_sha256=sha256(glued_prompt.read_bytes()),
1002
- session_path=glued_session,
1003
- )["sandbox_network_access"] is True
1004
- if os.name == "nt":
1005
- print("SKIP POSIX executable-shell wrapper fixtures; native npm worker/judge transport is covered by test-windows-portability.py")
1006
- return 0
1007
421
  fake_codex = work / "fake-codex"
1008
- fake_codex.write_text(
1009
- "#!/usr/bin/env bash\n"
1010
- "touch native-started\n"
1011
- "printf '%s\\n' '{\"type\":\"thread.started\"}'\n",
1012
- encoding="utf-8",
1013
- )
422
+ fake_codex.write_text("#!/usr/bin/env bash\ntouch native-started\n", encoding="utf-8")
1014
423
  fake_codex.chmod(0o755)
1015
424
  wrapper = pathlib.Path(__file__).with_name("codex-monitored.sh")
1016
425
  widened = subprocess.run(
1017
- [
1018
- "bash", str(wrapper), "-C", str(work), "-s", "danger-full-access",
1019
- "-m", "gpt-wrapper", "verify the task",
1020
- ],
1021
- cwd=work,
1022
- env={**os.environ, "CODEX_BIN": str(fake_codex)},
1023
- stdout=subprocess.PIPE,
1024
- stderr=subprocess.PIPE,
1025
- check=False,
1026
- )
1027
- assert widened.returncode == 64
1028
- assert b"forbidden Codex sandbox" in widened.stderr
1029
- assert widened.stdout == b""
1030
- env = os.environ.copy()
1031
- env.pop("DEVLYN_CODEX_PROMPT_FILE", None)
1032
- env.update({
1033
- "CODEX_BIN": str(fake_codex),
1034
- "CODEX_MONITORED_HEARTBEAT": "1",
1035
- "DEVLYN_INVOCATION_RUN_ID": "rs-wrapper",
1036
- "DEVLYN_INVOCATION_PHASE": "build_gate",
1037
- "DEVLYN_INVOCATION_ROUND": "0",
1038
- "DEVLYN_INVOCATION_WORKDIR": str(work),
1039
- "DEVLYN_INVOCATION_PROMPT_FILE": str(build_prompt),
1040
- "DEVLYN_INVOCATION_SESSION_FILE": str(build_session),
1041
- "DEVLYN_INVOCATION_RECEIPT": str(build_receipt),
1042
- })
1043
- with build_session.open("wb") as stdout:
1044
- missing_json = subprocess.run(
1045
- ["bash", str(wrapper), "-C", str(work), "-s", "workspace-write",
1046
- "-m", "gpt-wrapper", "-c",
1047
- "sandbox_workspace_write.network_access=true", "verify the task"],
1048
- cwd=work, env=env, stdout=stdout, stderr=subprocess.PIPE,
1049
- check=False,
1050
- )
1051
- assert missing_json.returncode == 64 and b"requires --json" in missing_json.stderr, missing_json.stderr
1052
- assert build_session.read_bytes() == b"" and not (work / "native-started").exists()
1053
- assert not build_receipt.exists()
1054
- with build_session.open("wb") as stdout:
1055
- wrapped = subprocess.run(
1056
- [
1057
- "bash", str(wrapper), "--json", "-C", str(work), "-s", "workspace-write",
1058
- "-m", "gpt-wrapper", "-c",
1059
- "sandbox_workspace_write.network_access=true", "verify the task",
1060
- ],
1061
- cwd=work,
1062
- env=env,
1063
- stdout=stdout,
1064
- stderr=subprocess.PIPE,
1065
- check=False,
1066
- )
1067
- assert wrapped.returncode == 0, wrapped.stderr.decode("utf-8", errors="replace")
1068
- wrapper_bound = validate_receipt(
1069
- work, build_receipt, run_id="rs-wrapper", phase="build_gate", round_=0,
1070
- model="gpt-wrapper", prompt_sha256=sha256(build_prompt.read_bytes()),
1071
- session_path=build_session,
1072
- )
1073
- assert wrapper_bound["exit_code"] == 0
1074
- assert wrapper_bound["sandbox_network_access"] is True
1075
-
1076
- wrapped_plan_prompt = devlyn / "plan.prompt.1"
1077
- wrapped_plan_prompt.write_text("plan through wrapper\n", encoding="utf-8")
1078
- wrapped_plan_session = devlyn / "plan.worker-session.1.jsonl"
1079
- wrapped_plan_receipt = devlyn / "plan.invocation.1.json"
1080
- plan_env = os.environ.copy()
1081
- plan_env.pop("DEVLYN_CODEX_PROMPT_FILE", None)
1082
- plan_env.update({
1083
- "CODEX_BIN": str(fake_codex),
1084
- "CODEX_MONITORED_HEARTBEAT": "1",
1085
- "DEVLYN_INVOCATION_RUN_ID": "rs-plan-wrapper",
1086
- "DEVLYN_INVOCATION_PHASE": "plan",
1087
- "DEVLYN_INVOCATION_ROUND": "1",
1088
- "DEVLYN_INVOCATION_WORKDIR": str(work),
1089
- "DEVLYN_INVOCATION_PROMPT_FILE": str(wrapped_plan_prompt),
1090
- "DEVLYN_INVOCATION_SESSION_FILE": str(wrapped_plan_session),
1091
- "DEVLYN_INVOCATION_RECEIPT": str(wrapped_plan_receipt),
1092
- })
1093
- with wrapped_plan_session.open("wb") as stdout:
1094
- wrapped_plan = subprocess.run(
1095
- [
1096
- "bash", str(wrapper), "--json", "-C", str(work),
1097
- "-s", "workspace-write", "-m", "gpt-plan-wrapper",
1098
- "-c", "sandbox_workspace_write.network_access=false",
1099
- "plan through wrapper",
1100
- ],
1101
- cwd=work,
1102
- env=plan_env,
1103
- stdout=stdout,
1104
- stderr=subprocess.PIPE,
1105
- check=False,
1106
- )
1107
- assert wrapped_plan.returncode == 0, wrapped_plan.stderr.decode(
1108
- "utf-8", errors="replace",
1109
- )
1110
- plan_wrapper_bound = validate_receipt(
1111
- work, wrapped_plan_receipt, run_id="rs-plan-wrapper", phase="plan",
1112
- round_=1, model="gpt-plan-wrapper",
1113
- prompt_sha256=sha256(wrapped_plan_prompt.read_bytes()),
1114
- session_path=wrapped_plan_session,
1115
- )
1116
- assert plan_wrapper_bound["exit_code"] == 0
1117
- print(
1118
- "PASS invocation receipt identity, prompt/session digest, bypass guard, "
1119
- "phase-scoped BUILD_GATE network capability, and monitored-wrapper "
1120
- "integration including PLAN"
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,
1121
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")
1122
433
  return 0
1123
434
 
1124
435
 
@@ -1127,10 +438,6 @@ def complete_dispatch(exit_code):
1127
438
  if prompt:
1128
439
  path = pathlib.Path(prompt).resolve()
1129
440
  finish_transport(path.with_name(path.name + ".transport.json"), exit_code)
1130
- if os.environ.get("DEVLYN_INVOCATION_RECEIPT"):
1131
- work = pathlib.Path(os.environ["DEVLYN_INVOCATION_WORKDIR"]).resolve()
1132
- path = pathlib.Path(os.environ["DEVLYN_INVOCATION_RECEIPT"])
1133
- finish_receipt(work, path if path.is_absolute() else work / path, exit_code)
1134
441
 
1135
442
 
1136
443
  def dispatch_codex(args):
@@ -1139,23 +446,12 @@ def dispatch_codex(args):
1139
446
  actual = PLATFORM["native_argv"](command)
1140
447
  prompt = os.environ.get("DEVLYN_CODEX_PROMPT_FILE")
1141
448
  if prompt == "":
1142
- raise ReceiptError("DEVLYN_CODEX_PROMPT_FILE must name a readable prompt file")
449
+ raise TransportError("DEVLYN_CODEX_PROMPT_FILE must name a readable prompt file")
1143
450
  stream, carrier, transport = None, None, None
1144
451
  try:
1145
452
  if prompt:
1146
453
  file_prompt_args(argv)
1147
- stream, carrier, transport = prepare_transport(
1148
- prompt, command, actual, args.timeout,
1149
- isolated=bool(os.environ.get("CODEX_MONITORED_ISOLATED")),
1150
- )
1151
- if os.environ.get("DEVLYN_INVOCATION_RECEIPT"):
1152
- env = os.environ
1153
- work = pathlib.Path(env["DEVLYN_INVOCATION_WORKDIR"]).resolve()
1154
- path = pathlib.Path(env["DEVLYN_INVOCATION_RECEIPT"])
1155
- start_receipt(work, path if path.is_absolute() else work / path,
1156
- env["DEVLYN_INVOCATION_RUN_ID"], env["DEVLYN_INVOCATION_PHASE"],
1157
- int(env["DEVLYN_INVOCATION_ROUND"]), env["DEVLYN_INVOCATION_PROMPT_FILE"],
1158
- env["DEVLYN_INVOCATION_SESSION_FILE"], argv, transport=transport)
454
+ stream, carrier, transport = prepare_transport(prompt, command, actual, args.timeout)
1159
455
  if transport is not None:
1160
456
  write_transport(carrier, transport)
1161
457
  if os.name != "nt":
@@ -1176,19 +472,6 @@ def main() -> int:
1176
472
  parser = argparse.ArgumentParser(description=__doc__)
1177
473
  parser.add_argument("--self-test", action="store_true")
1178
474
  subparsers = parser.add_subparsers(dest="action")
1179
- start = subparsers.add_parser("start")
1180
- start.add_argument("--workdir", required=True)
1181
- start.add_argument("--receipt", required=True)
1182
- start.add_argument("--run-id", required=True)
1183
- start.add_argument("--phase", choices=sorted(PHASES), required=True)
1184
- start.add_argument("--round", type=int, required=True)
1185
- start.add_argument("--prompt-file", required=True)
1186
- start.add_argument("--session-file", required=True)
1187
- start.add_argument("argv", nargs=argparse.REMAINDER)
1188
- finish = subparsers.add_parser("finish")
1189
- finish.add_argument("--workdir", required=True)
1190
- finish.add_argument("--receipt", required=True)
1191
- finish.add_argument("--exit-code", type=int, required=True)
1192
475
  dispatch = subparsers.add_parser("dispatch")
1193
476
  dispatch.add_argument("--binary", required=True)
1194
477
  dispatch.add_argument("--timeout", type=int, required=True)
@@ -1204,20 +487,6 @@ def main() -> int:
1204
487
  return dispatch_codex(args)
1205
488
  elif args.action == "complete-dispatch":
1206
489
  complete_dispatch(args.exit_code)
1207
- elif args.action == "start":
1208
- argv = args.argv[1:] if args.argv[:1] == ["--"] else args.argv
1209
- start_receipt(
1210
- pathlib.Path(args.workdir).resolve(), pathlib.Path(args.receipt), args.run_id,
1211
- args.phase, args.round, args.prompt_file, args.session_file, argv,
1212
- )
1213
- elif args.action == "finish":
1214
- work = pathlib.Path(args.workdir).resolve()
1215
- receipt_path = pathlib.Path(args.receipt)
1216
- if not receipt_path.is_absolute():
1217
- receipt_path = work / receipt_path
1218
- finish_receipt(
1219
- work, receipt_path, args.exit_code,
1220
- )
1221
490
  else:
1222
491
  parser.error("an action is required")
1223
492
  except (OSError, UnicodeError, ValueError) as exc: