devlyn-cli 3.1.0 → 3.1.2
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.
- package/README.md +6 -1
- package/bin/devlyn.js +69 -23
- package/config/skills/_shared/adapters/claude.md +3 -2
- package/config/skills/_shared/adapters/codex.md +1 -1
- package/config/skills/_shared/archive_run.py +5 -3
- package/config/skills/_shared/codex-config.md +13 -8
- package/config/skills/_shared/codex-monitored.sh +12 -19
- package/config/skills/_shared/collect-codex-findings.py +1 -0
- package/config/skills/_shared/engine-preflight.md +2 -0
- package/config/skills/_shared/finish-gate.py +4 -1
- package/config/skills/_shared/invocation-receipt.py +194 -5
- package/config/skills/_shared/judge-role-evidence.py +33 -6
- package/config/skills/_shared/phase-prompt-render.py +2 -0
- package/config/skills/_shared/platform-support.py +370 -0
- package/config/skills/_shared/process-evidence.py +20 -3
- package/config/skills/_shared/resolve-bootstrap.py +131 -63
- package/config/skills/_shared/resolve-stop-hook.py +3 -1
- package/config/skills/_shared/role-config.py +7 -5
- package/config/skills/_shared/run-bounded.py +35 -21
- package/config/skills/_shared/spec-verify-check.py +393 -167
- package/config/skills/_shared/state-phase-write.py +26 -9
- package/config/skills/_shared/task-complete.py +337 -97
- package/config/skills/_shared/verify-merge-findings.py +9 -8
- package/config/skills/devlyn:ideate/references/spec-template.md +1 -1
- package/config/skills/devlyn:resolve/SKILL.md +10 -7
- package/config/skills/devlyn:resolve/references/free-form-mode.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/verify.md +14 -10
- package/config/skills/devlyn:resolve/references/state-schema.md +1 -1
- package/config/skills/devlyn:resolve/references/task-completion.md +28 -1
- package/package.json +1 -1
- package/scripts/lint-skills.sh +6 -5
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
"""Create and validate run-owned Codex invocation receipts."""
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import runpy
|
|
6
|
+
|
|
5
7
|
import argparse
|
|
6
8
|
import hashlib
|
|
7
9
|
import json
|
|
@@ -11,6 +13,7 @@ import re
|
|
|
11
13
|
import signal
|
|
12
14
|
import shutil
|
|
13
15
|
import subprocess
|
|
16
|
+
import sys
|
|
14
17
|
import tempfile
|
|
15
18
|
import time
|
|
16
19
|
|
|
@@ -21,6 +24,7 @@ SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
|
21
24
|
SAFE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
|
22
25
|
DANGEROUS_FLAGS = {"--dangerously-bypass-approvals-and-sandbox", "--yolo"}
|
|
23
26
|
NETWORK_ACCESS_CONFIG = "sandbox_workspace_write.network_access"
|
|
27
|
+
PLATFORM = runpy.run_path(pathlib.Path(__file__).with_name("platform-support.py"))
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
class ReceiptError(ValueError):
|
|
@@ -121,6 +125,95 @@ def sandbox_network_access(argv: list[str], phase: str) -> bool:
|
|
|
121
125
|
return enabled
|
|
122
126
|
|
|
123
127
|
|
|
128
|
+
def file_prompt_args(argv):
|
|
129
|
+
"""File mode accepts one explicit stdin prompt and no positional competitors."""
|
|
130
|
+
values = {"-C", "--cd", "-s", "--sandbox", "-m", "--model", "-c", "--config",
|
|
131
|
+
"-o", "--output-last-message", "--output-schema", "--color", "--enable",
|
|
132
|
+
"--disable", "--add-dir"}
|
|
133
|
+
flags = {"--json", "--ignore-user-config", "--ignore-rules", "--ephemeral",
|
|
134
|
+
"--skip-git-repo-check", "--full-auto"}
|
|
135
|
+
positional = []
|
|
136
|
+
index = 0
|
|
137
|
+
while index < len(argv):
|
|
138
|
+
token = argv[index]
|
|
139
|
+
if token in values:
|
|
140
|
+
if index + 1 >= len(argv):
|
|
141
|
+
raise ReceiptError(f"{token} requires a value")
|
|
142
|
+
index += 2
|
|
143
|
+
elif any(token.startswith(key + "=") for key in values) or token in flags:
|
|
144
|
+
index += 1
|
|
145
|
+
elif token == "--":
|
|
146
|
+
positional.extend(argv[index + 1:])
|
|
147
|
+
break
|
|
148
|
+
elif token == "-" or not token.startswith("-"):
|
|
149
|
+
positional.append(token)
|
|
150
|
+
index += 1
|
|
151
|
+
else:
|
|
152
|
+
raise ReceiptError(f"unsupported Codex file-transport option: {token}")
|
|
153
|
+
if positional != ["-"]:
|
|
154
|
+
raise ReceiptError("file transport requires the sole prompt argument '-'; competing prompts are forbidden")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def prepare_transport(prompt_path, command, argv, seconds, *, isolated=False):
|
|
158
|
+
path = pathlib.Path(prompt_path).resolve(strict=True)
|
|
159
|
+
with PLATFORM["open_stdin"](path) as source:
|
|
160
|
+
raw = source.read()
|
|
161
|
+
record = {"schema_version": 1, "transport": "stdin-file",
|
|
162
|
+
"prompt": {"path": str(path), "sha256": sha256(raw), "bytes": len(raw)},
|
|
163
|
+
"command": command, "argv": argv, "timeout_sec": seconds, "isolated": isolated,
|
|
164
|
+
"status": "started", "exit_code": None}
|
|
165
|
+
carrier = path.with_name(path.name + ".transport.json")
|
|
166
|
+
if carrier.exists():
|
|
167
|
+
raise ReceiptError(f"prompt transport already exists: {carrier}")
|
|
168
|
+
stream = tempfile.TemporaryFile("w+b")
|
|
169
|
+
try:
|
|
170
|
+
stream.write(raw)
|
|
171
|
+
stream.seek(0)
|
|
172
|
+
except BaseException:
|
|
173
|
+
stream.close()
|
|
174
|
+
raise
|
|
175
|
+
return stream, carrier, record
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def write_transport(path, record):
|
|
179
|
+
with path.open("xb") as stream:
|
|
180
|
+
stream.write((json.dumps(record, sort_keys=True) + "\n").encode("utf-8"))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def finish_transport(path, exit_code):
|
|
184
|
+
record = read_receipt(path)
|
|
185
|
+
if record.get("status") != "started":
|
|
186
|
+
raise ReceiptError("prompt transport is not open")
|
|
187
|
+
record.update(status="completed", exit_code=exit_code)
|
|
188
|
+
atomic_write(path, record)
|
|
189
|
+
|
|
190
|
+
|
|
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
|
+
|
|
124
217
|
def start_receipt(
|
|
125
218
|
work: pathlib.Path,
|
|
126
219
|
receipt_path: pathlib.Path,
|
|
@@ -130,6 +223,7 @@ def start_receipt(
|
|
|
130
223
|
prompt_path: str,
|
|
131
224
|
session_path: str,
|
|
132
225
|
argv: list[str],
|
|
226
|
+
*, transport: dict | None = None,
|
|
133
227
|
) -> None:
|
|
134
228
|
if receipt_path.exists():
|
|
135
229
|
raise ReceiptError(f"invocation receipt already exists: {receipt_path}")
|
|
@@ -205,7 +299,12 @@ def start_receipt(
|
|
|
205
299
|
prompt_argument = prompt_raw.decode("utf-8").rstrip("\n")
|
|
206
300
|
except UnicodeError as exc:
|
|
207
301
|
raise ReceiptError("invocation prompt is not UTF-8") from exc
|
|
208
|
-
if
|
|
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:
|
|
209
308
|
raise ReceiptError("Codex prompt argument does not match the canonical prompt file")
|
|
210
309
|
|
|
211
310
|
atomic_write(receipt_file, {
|
|
@@ -222,6 +321,7 @@ def start_receipt(
|
|
|
222
321
|
"argv_sha256": sha256(json.dumps(argv, separators=(",", ":")).encode("utf-8")),
|
|
223
322
|
"status": "started",
|
|
224
323
|
"exit_code": None,
|
|
324
|
+
**({"transport": {"path": prompt_relative + ".transport.json", "sha256": None}} if transport is not None else {}),
|
|
225
325
|
})
|
|
226
326
|
|
|
227
327
|
|
|
@@ -252,6 +352,9 @@ def finish_receipt(work: pathlib.Path, receipt_path: pathlib.Path, exit_code: in
|
|
|
252
352
|
}
|
|
253
353
|
receipt["status"] = "completed"
|
|
254
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())
|
|
255
358
|
atomic_write(receipt_path, receipt)
|
|
256
359
|
|
|
257
360
|
|
|
@@ -267,7 +370,7 @@ def validate_receipt_artifacts(
|
|
|
267
370
|
work, str(receipt_path), "invocation receipt", require=True,
|
|
268
371
|
)
|
|
269
372
|
receipt = read_receipt(receipt_file)
|
|
270
|
-
if set(receipt) != {
|
|
373
|
+
if set(receipt) - {"transport"} != {
|
|
271
374
|
"schema_version", "run_id", "phase", "round", "engine", "model",
|
|
272
375
|
"sandbox", "sandbox_network_access", "prompt", "session", "argv_sha256",
|
|
273
376
|
"status", "exit_code",
|
|
@@ -309,6 +412,22 @@ def validate_receipt_artifacts(
|
|
|
309
412
|
or sha256(prompt_file.read_bytes()) != prompt["sha256"]
|
|
310
413
|
):
|
|
311
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")
|
|
312
431
|
session = receipt["session"]
|
|
313
432
|
if not isinstance(session, dict) or set(session) != {"path", "sha256", "bytes"}:
|
|
314
433
|
raise ReceiptError("invocation receipt session binding is invalid")
|
|
@@ -387,6 +506,9 @@ def validate_receipt(
|
|
|
387
506
|
|
|
388
507
|
|
|
389
508
|
def monitor_descendant_regression() -> None:
|
|
509
|
+
if os.name == "nt":
|
|
510
|
+
print("SKIP POSIX shell process-group fixture; test-windows-portability.py exercises native Node/Python trees")
|
|
511
|
+
return
|
|
390
512
|
with tempfile.TemporaryDirectory() as raw_tmp:
|
|
391
513
|
work = pathlib.Path(raw_tmp)
|
|
392
514
|
fixture_bin = work / "bin"
|
|
@@ -428,7 +550,7 @@ def monitor_descendant_regression() -> None:
|
|
|
428
550
|
key.startswith("DEVLYN_INVOCATION_")
|
|
429
551
|
or key.startswith("DEVLYN_WATCHDOG_TEST_")
|
|
430
552
|
or key.startswith("CODEX_MONITORED_")
|
|
431
|
-
or key in {"CODEX_BLOCKED", "CODEX_REAL_BIN"}
|
|
553
|
+
or key in {"CODEX_BLOCKED", "CODEX_REAL_BIN", "DEVLYN_CODEX_PROMPT_FILE"}
|
|
432
554
|
):
|
|
433
555
|
env.pop(key)
|
|
434
556
|
env.update({
|
|
@@ -879,6 +1001,9 @@ def self_test() -> int:
|
|
|
879
1001
|
model="gpt-wrapper", prompt_sha256=sha256(glued_prompt.read_bytes()),
|
|
880
1002
|
session_path=glued_session,
|
|
881
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
|
|
882
1007
|
fake_codex = work / "fake-codex"
|
|
883
1008
|
fake_codex.write_text(
|
|
884
1009
|
"#!/usr/bin/env bash\n"
|
|
@@ -903,6 +1028,7 @@ def self_test() -> int:
|
|
|
903
1028
|
assert b"forbidden Codex sandbox" in widened.stderr
|
|
904
1029
|
assert widened.stdout == b""
|
|
905
1030
|
env = os.environ.copy()
|
|
1031
|
+
env.pop("DEVLYN_CODEX_PROMPT_FILE", None)
|
|
906
1032
|
env.update({
|
|
907
1033
|
"CODEX_BIN": str(fake_codex),
|
|
908
1034
|
"CODEX_MONITORED_HEARTBEAT": "1",
|
|
@@ -952,6 +1078,7 @@ def self_test() -> int:
|
|
|
952
1078
|
wrapped_plan_session = devlyn / "plan.worker-session.1.jsonl"
|
|
953
1079
|
wrapped_plan_receipt = devlyn / "plan.invocation.1.json"
|
|
954
1080
|
plan_env = os.environ.copy()
|
|
1081
|
+
plan_env.pop("DEVLYN_CODEX_PROMPT_FILE", None)
|
|
955
1082
|
plan_env.update({
|
|
956
1083
|
"CODEX_BIN": str(fake_codex),
|
|
957
1084
|
"CODEX_MONITORED_HEARTBEAT": "1",
|
|
@@ -995,6 +1122,56 @@ def self_test() -> int:
|
|
|
995
1122
|
return 0
|
|
996
1123
|
|
|
997
1124
|
|
|
1125
|
+
def complete_dispatch(exit_code):
|
|
1126
|
+
prompt = os.environ.get("DEVLYN_CODEX_PROMPT_FILE")
|
|
1127
|
+
if prompt:
|
|
1128
|
+
path = pathlib.Path(prompt).resolve()
|
|
1129
|
+
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
|
+
|
|
1135
|
+
|
|
1136
|
+
def dispatch_codex(args):
|
|
1137
|
+
argv = args.argv[1:] if args.argv[:1] == ["--"] else args.argv
|
|
1138
|
+
command = [args.binary, "exec", *argv]
|
|
1139
|
+
actual = PLATFORM["native_argv"](command)
|
|
1140
|
+
prompt = os.environ.get("DEVLYN_CODEX_PROMPT_FILE")
|
|
1141
|
+
if prompt == "":
|
|
1142
|
+
raise ReceiptError("DEVLYN_CODEX_PROMPT_FILE must name a readable prompt file")
|
|
1143
|
+
stream, carrier, transport = None, None, None
|
|
1144
|
+
try:
|
|
1145
|
+
if prompt:
|
|
1146
|
+
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)
|
|
1159
|
+
if transport is not None:
|
|
1160
|
+
write_transport(carrier, transport)
|
|
1161
|
+
if os.name != "nt":
|
|
1162
|
+
if stream is not None:
|
|
1163
|
+
os.dup2(stream.fileno(), 0)
|
|
1164
|
+
os.execvp(actual[0], actual)
|
|
1165
|
+
code = PLATFORM["run_process"](actual, stream if stream is not None else subprocess.DEVNULL,
|
|
1166
|
+
args.timeout, heartbeat=args.heartbeat)
|
|
1167
|
+
complete_dispatch(code)
|
|
1168
|
+
print(f"[codex-monitored] codex exited: code={code}", file=sys.stderr)
|
|
1169
|
+
return code
|
|
1170
|
+
finally:
|
|
1171
|
+
if stream is not None:
|
|
1172
|
+
stream.close()
|
|
1173
|
+
|
|
1174
|
+
|
|
998
1175
|
def main() -> int:
|
|
999
1176
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
1000
1177
|
parser.add_argument("--self-test", action="store_true")
|
|
@@ -1012,11 +1189,22 @@ def main() -> int:
|
|
|
1012
1189
|
finish.add_argument("--workdir", required=True)
|
|
1013
1190
|
finish.add_argument("--receipt", required=True)
|
|
1014
1191
|
finish.add_argument("--exit-code", type=int, required=True)
|
|
1192
|
+
dispatch = subparsers.add_parser("dispatch")
|
|
1193
|
+
dispatch.add_argument("--binary", required=True)
|
|
1194
|
+
dispatch.add_argument("--timeout", type=int, required=True)
|
|
1195
|
+
dispatch.add_argument("--heartbeat", type=int, required=True)
|
|
1196
|
+
dispatch.add_argument("argv", nargs=argparse.REMAINDER)
|
|
1197
|
+
completed = subparsers.add_parser("complete-dispatch")
|
|
1198
|
+
completed.add_argument("--exit-code", type=int, required=True)
|
|
1015
1199
|
args = parser.parse_args()
|
|
1016
1200
|
if args.self_test:
|
|
1017
1201
|
return self_test()
|
|
1018
1202
|
try:
|
|
1019
|
-
if args.action == "
|
|
1203
|
+
if args.action == "dispatch":
|
|
1204
|
+
return dispatch_codex(args)
|
|
1205
|
+
elif args.action == "complete-dispatch":
|
|
1206
|
+
complete_dispatch(args.exit_code)
|
|
1207
|
+
elif args.action == "start":
|
|
1020
1208
|
argv = args.argv[1:] if args.argv[:1] == ["--"] else args.argv
|
|
1021
1209
|
start_receipt(
|
|
1022
1210
|
pathlib.Path(args.workdir).resolve(), pathlib.Path(args.receipt), args.run_id,
|
|
@@ -1038,4 +1226,5 @@ def main() -> int:
|
|
|
1038
1226
|
|
|
1039
1227
|
|
|
1040
1228
|
if __name__ == "__main__":
|
|
1041
|
-
|
|
1229
|
+
runpy.run_path(str(pathlib.Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
|
|
1230
|
+
raise SystemExit(PLATFORM["system_exit_code"](main()))
|
|
@@ -88,7 +88,29 @@ def describe(devlyn, state, role, exit_code):
|
|
|
88
88
|
else:
|
|
89
89
|
stderr = raw.decode()
|
|
90
90
|
require(isinstance(argv, list) and all(isinstance(x, str) for x in argv), "argv must be an array of strings")
|
|
91
|
-
|
|
91
|
+
file_mode = ((devlyn / (stem + ".prompt.transport.json")).exists()
|
|
92
|
+
or ("--stdin-file" in argv if engine == "claude" else "-" in argv))
|
|
93
|
+
transport = None
|
|
94
|
+
if file_mode:
|
|
95
|
+
artifacts["transport"], _ = seal(devlyn, stem + ".prompt.transport.json")
|
|
96
|
+
transport = runpy.run_path(Path(__file__).with_name("invocation-receipt.py"))["validate_transport"](
|
|
97
|
+
devlyn / (stem + ".prompt.transport.json"), prompt.encode("utf-8"))
|
|
98
|
+
require(transport["exit_code"] == exit_code and transport["timeout_sec"] == 600, "file transport exit/budget mismatch")
|
|
99
|
+
if engine == "claude":
|
|
100
|
+
index = next((i for i, arg in enumerate(argv) if Path(arg).name == "run-bounded.py"), -1)
|
|
101
|
+
require(index >= 0 and argv[index + 1:index + 3] == ["600", "--stdin-file"]
|
|
102
|
+
and len(argv) > index + 6 and argv[index + 4:index + 6] == ["--record-transport", "--"], "invalid bounded file transport")
|
|
103
|
+
require(Path(argv[index + 3]).resolve() == devlyn / (stem + ".prompt"), "bounded prompt path mismatch")
|
|
104
|
+
require(transport["command"] == argv[index + 6:] and prompt not in transport["command"], "bounded actual argv mismatch")
|
|
105
|
+
require("-p" in transport["command"] or "--print" in transport["command"], "Claude print mode missing")
|
|
106
|
+
else:
|
|
107
|
+
index = next((i for i, arg in enumerate(argv) if Path(arg).name == "codex-monitored.sh"), -1)
|
|
108
|
+
require(index >= 0, "monitored file transport missing")
|
|
109
|
+
isolation = ["--ignore-user-config", "--ignore-rules", "--ephemeral", "--disable", "codex_hooks", "--disable", "hooks"]
|
|
110
|
+
require(transport["isolated"] and transport["command"][1:] == ["exec", *isolation, *argv[index + 1:]], "actual isolated argv mismatch")
|
|
111
|
+
runpy.run_path(Path(__file__).with_name("invocation-receipt.py"))["file_prompt_args"](transport["command"][2:])
|
|
112
|
+
else:
|
|
113
|
+
require(prompt in argv, "canonical prompt is not an exact dispatched argument")
|
|
92
114
|
diagnostics = stderr.partition("\nuser\n")[0] if engine == "codex" else stderr
|
|
93
115
|
require(not re.search(r"(?im)^.*(?:model|effort).*\b(?:ignor\w*|clamp\w*|unsupported|not supported)\b", diagnostics), "native diagnostic rejected or ignored an explicit option")
|
|
94
116
|
requested_model, requested_effort = entry.get("model_requested"), entry.get("effort_requested")
|
|
@@ -100,7 +122,7 @@ def describe(devlyn, state, role, exit_code):
|
|
|
100
122
|
("--allowedTools", "Read,Grep,Glob"), ("--setting-sources", "project"), ("--output-format", "json")):
|
|
101
123
|
require(option(argv, flag) == expected, f"Claude {flag} differs from read-only contract")
|
|
102
124
|
require("--strict-mcp-config" in argv and loads(option(argv, "--mcp-config") or "null") == {"mcpServers": {}}, "Claude MCP isolation missing")
|
|
103
|
-
require(any(Path(arg).name == "run-bounded.py" and argv[i + 1:i + 3] == ["600", "--"] for i, arg in enumerate(argv)), "Claude 600s bound missing")
|
|
125
|
+
require(transport is not None or any(Path(arg).name == "run-bounded.py" and argv[i + 1:i + 3] == ["600", "--"] for i, arg in enumerate(argv)), "Claude 600s bound missing")
|
|
104
126
|
if requested_effort:
|
|
105
127
|
require(option(argv, "--effort") == requested_effort, "requested effort differs from argv")
|
|
106
128
|
artifacts["raw"], raw = seal(devlyn, stem + ".output.json")
|
|
@@ -208,14 +230,18 @@ def self_test():
|
|
|
208
230
|
receipt, _ = describe(devlyn, state, "pair_judge", 0)
|
|
209
231
|
assert receipt["model_observed"] == "gpt-6-astra"
|
|
210
232
|
alias = work / "logical-cwd"
|
|
211
|
-
|
|
233
|
+
if sys.platform == "win32":
|
|
234
|
+
import subprocess
|
|
235
|
+
subprocess.run(["cmd.exe", "/d", "/c", "mklink", "/J", str(alias), str(work)], check=True, capture_output=True)
|
|
236
|
+
else:
|
|
237
|
+
alias.symlink_to(work, target_is_directory=True)
|
|
212
238
|
alias_argv = [str(alias) if arg == str(work) else arg for arg in argv]
|
|
213
239
|
alias_header = header.replace(f"workdir: {work}\n", f"workdir: {alias}\n")
|
|
214
240
|
(devlyn / "codex-judge.r0.argv.json").write_bytes(encoded(alias_argv))
|
|
215
|
-
(devlyn / "codex-judge.r0.stderr").write_text(alias_header)
|
|
241
|
+
(devlyn / "codex-judge.r0.stderr").write_text(alias_header, encoding="utf-8")
|
|
216
242
|
receipt, _ = describe(devlyn, state, "pair_judge", 0)
|
|
217
243
|
assert receipt["artifacts"]["argv"]["sha256"] == digest(encoded(alias_argv))
|
|
218
|
-
assert (devlyn / "codex-judge.r0.stderr").read_text() == alias_header
|
|
244
|
+
assert (devlyn / "codex-judge.r0.stderr").read_text(encoding="utf-8") == alias_header
|
|
219
245
|
elsewhere = work / "elsewhere"; elsewhere.mkdir()
|
|
220
246
|
(devlyn / "codex-judge.r0.argv.json").write_bytes(encoded([str(elsewhere) if arg == str(work) else arg for arg in argv]))
|
|
221
247
|
try:
|
|
@@ -226,7 +252,7 @@ def self_test():
|
|
|
226
252
|
raise AssertionError("different physical worktree accepted")
|
|
227
253
|
(devlyn / "codex-judge.r0.argv.json").write_bytes(encoded(argv))
|
|
228
254
|
for bad in ("Warning: model effort unsupported and ignored\n" + header, "\nuser\n" + header, header.replace("\nmodel:", "\nmodel: gpt-5.6-sol\nmodel:"), header.replace("sandbox: read-only", "sandbox: workspace-write"), header.replace("model: gpt-6-astra", "model: gpt-5.6-sol"), header.replace("[codex-monitored] isolated=1\n", ""), header.replace("timeout=600s", "timeout=300s")):
|
|
229
|
-
(devlyn / "codex-judge.r0.stderr").write_text(bad)
|
|
255
|
+
(devlyn / "codex-judge.r0.stderr").write_text(bad, encoding="utf-8")
|
|
230
256
|
try:
|
|
231
257
|
describe(devlyn, state, "pair_judge", 0)
|
|
232
258
|
except ValueError:
|
|
@@ -270,4 +296,5 @@ def main():
|
|
|
270
296
|
|
|
271
297
|
|
|
272
298
|
if __name__ == "__main__":
|
|
299
|
+
runpy.run_path(str(Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
|
|
273
300
|
raise SystemExit(main())
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""Render exact phase-prompt bytes and print their SHA-256 digest."""
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import runpy
|
|
5
6
|
import argparse
|
|
6
7
|
import hashlib
|
|
7
8
|
import os
|
|
@@ -178,4 +179,5 @@ def main() -> int:
|
|
|
178
179
|
|
|
179
180
|
|
|
180
181
|
if __name__ == "__main__":
|
|
182
|
+
runpy.run_path(str(pathlib.Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
|
|
181
183
|
raise SystemExit(main())
|