devlyn-cli 4.1.0 → 4.2.1
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/AGENTS.md +10 -94
- package/CLAUDE.md +7 -160
- package/LICENSE +21 -0
- package/README.md +68 -111
- package/bin/devlyn.js +265 -259
- package/bin/instruction-templates.json +43 -2
- package/bin/instructions.js +29 -8
- package/bin/skill-history.json +140 -0
- package/bin/skill-ownership.js +67 -0
- package/config/skills/_shared/adapters/claude.md +8 -85
- package/config/skills/_shared/adapters/codex.md +14 -34
- package/config/skills/_shared/adapters/omp.md +1 -11
- package/config/skills/_shared/codex-monitored.sh +4 -54
- package/config/skills/_shared/engine-doctor.sh +18 -58
- package/config/skills/_shared/engine-preflight.md +15 -47
- package/config/skills/_shared/expected-contract.py +242 -0
- package/config/skills/_shared/expected.schema.json +10 -95
- package/config/skills/_shared/invocation-receipt.py +26 -757
- package/config/skills/_shared/platform-support.py +37 -28
- package/config/skills/_shared/role-config.py +100 -421
- package/config/skills/_shared/runtime-principles.md +17 -93
- package/config/skills/_shared/task-complete.py +490 -285
- package/config/skills/_shared/task-completion.md +235 -0
- package/config/skills/devlyn-engines/SKILL.md +9 -11
- package/config/skills/devlyn-ideate/SKILL.md +57 -107
- package/config/skills/devlyn-ideate/references/elicitation.md +18 -99
- package/config/skills/devlyn-ideate/references/loop.md +110 -0
- package/config/skills/devlyn-ideate/references/package-format.md +128 -0
- package/config/skills/devlyn-ideate/scripts/acceptance.py +795 -0
- package/config/skills/devlyn-ideate/scripts/queue.py +1780 -0
- package/node_modules/@inquirer/ansi/LICENSE +22 -0
- package/node_modules/@inquirer/ansi/README.md +121 -0
- package/node_modules/@inquirer/ansi/dist/index.d.ts +14 -0
- package/node_modules/@inquirer/ansi/dist/index.js +21 -0
- package/node_modules/@inquirer/ansi/package.json +79 -0
- package/node_modules/@inquirer/checkbox/LICENSE +22 -0
- package/node_modules/@inquirer/checkbox/README.md +265 -0
- package/node_modules/@inquirer/checkbox/dist/index.d.ts +67 -0
- package/node_modules/@inquirer/checkbox/dist/index.js +210 -0
- package/node_modules/@inquirer/checkbox/package.json +93 -0
- package/node_modules/@inquirer/core/LICENSE +22 -0
- package/node_modules/@inquirer/core/README.md +424 -0
- package/node_modules/@inquirer/core/dist/index.d.ts +13 -0
- package/node_modules/@inquirer/core/dist/index.js +12 -0
- package/node_modules/@inquirer/core/dist/lib/Separator.d.ts +10 -0
- package/node_modules/@inquirer/core/dist/lib/Separator.js +21 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p1.d.ts +9 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p1.js +4 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p2.d.ts +9 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p2.js +3 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p3.d.ts +9 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p3.js +5 -0
- package/node_modules/@inquirer/core/dist/lib/create-prompt.d.ts +11 -0
- package/node_modules/@inquirer/core/dist/lib/create-prompt.js +185 -0
- package/node_modules/@inquirer/core/dist/lib/errors.d.ts +20 -0
- package/node_modules/@inquirer/core/dist/lib/errors.js +21 -0
- package/node_modules/@inquirer/core/dist/lib/hook-engine.d.ts +17 -0
- package/node_modules/@inquirer/core/dist/lib/hook-engine.js +109 -0
- package/node_modules/@inquirer/core/dist/lib/key.d.ts +15 -0
- package/node_modules/@inquirer/core/dist/lib/key.js +34 -0
- package/node_modules/@inquirer/core/dist/lib/make-theme.d.ts +3 -0
- package/node_modules/@inquirer/core/dist/lib/make-theme.js +33 -0
- package/node_modules/@inquirer/core/dist/lib/pagination/use-pagination.d.ts +15 -0
- package/node_modules/@inquirer/core/dist/lib/pagination/use-pagination.js +121 -0
- package/node_modules/@inquirer/core/dist/lib/screen-manager.d.ts +14 -0
- package/node_modules/@inquirer/core/dist/lib/screen-manager.js +84 -0
- package/node_modules/@inquirer/core/dist/lib/theme.d.ts +166 -0
- package/node_modules/@inquirer/core/dist/lib/theme.js +29 -0
- package/node_modules/@inquirer/core/dist/lib/use-effect.d.ts +2 -0
- package/node_modules/@inquirer/core/dist/lib/use-effect.js +11 -0
- package/node_modules/@inquirer/core/dist/lib/use-keypress.d.ts +3 -0
- package/node_modules/@inquirer/core/dist/lib/use-keypress.js +20 -0
- package/node_modules/@inquirer/core/dist/lib/use-memo.d.ts +1 -0
- package/node_modules/@inquirer/core/dist/lib/use-memo.js +14 -0
- package/node_modules/@inquirer/core/dist/lib/use-prefix.d.ts +5 -0
- package/node_modules/@inquirer/core/dist/lib/use-prefix.js +35 -0
- package/node_modules/@inquirer/core/dist/lib/use-ref.d.ts +6 -0
- package/node_modules/@inquirer/core/dist/lib/use-ref.js +4 -0
- package/node_modules/@inquirer/core/dist/lib/use-state.d.ts +7 -0
- package/node_modules/@inquirer/core/dist/lib/use-state.js +28 -0
- package/node_modules/@inquirer/core/dist/lib/utils.d.ts +13 -0
- package/node_modules/@inquirer/core/dist/lib/utils.js +25 -0
- package/node_modules/@inquirer/core/package.json +98 -0
- package/node_modules/@inquirer/figures/LICENSE +22 -0
- package/node_modules/@inquirer/figures/dist/index.d.ts +275 -0
- package/node_modules/@inquirer/figures/dist/index.js +315 -0
- package/node_modules/@inquirer/figures/package.json +81 -0
- package/node_modules/@inquirer/select/LICENSE +22 -0
- package/node_modules/@inquirer/select/README.md +258 -0
- package/node_modules/@inquirer/select/dist/index.d.ts +42 -0
- package/node_modules/@inquirer/select/dist/index.js +191 -0
- package/node_modules/@inquirer/select/package.json +93 -0
- package/node_modules/@inquirer/type/LICENSE +22 -0
- package/node_modules/@inquirer/type/dist/index.d.ts +2 -0
- package/node_modules/@inquirer/type/dist/index.js +2 -0
- package/node_modules/@inquirer/type/dist/inquirer.d.ts +35 -0
- package/node_modules/@inquirer/type/dist/inquirer.js +1 -0
- package/node_modules/@inquirer/type/dist/utils.d.ts +45 -0
- package/node_modules/@inquirer/type/dist/utils.js +16 -0
- package/node_modules/@inquirer/type/package.json +88 -0
- package/node_modules/cli-width/LICENSE +13 -0
- package/node_modules/cli-width/README.md +71 -0
- package/node_modules/cli-width/index.d.ts +13 -0
- package/node_modules/cli-width/index.js +49 -0
- package/node_modules/cli-width/package.json +40 -0
- package/node_modules/fast-string-truncated-width/dist/index.d.ts +4 -0
- package/node_modules/fast-string-truncated-width/dist/index.js +111 -0
- package/node_modules/fast-string-truncated-width/dist/types.d.ts +19 -0
- package/node_modules/fast-string-truncated-width/dist/types.js +2 -0
- package/node_modules/fast-string-truncated-width/dist/utils.d.ts +4 -0
- package/node_modules/fast-string-truncated-width/dist/utils.js +20 -0
- package/node_modules/fast-string-truncated-width/license +21 -0
- package/node_modules/fast-string-truncated-width/package.json +35 -0
- package/node_modules/fast-string-truncated-width/readme.md +59 -0
- package/node_modules/fast-string-width/dist/index.d.ts +4 -0
- package/node_modules/fast-string-width/dist/index.js +14 -0
- package/node_modules/fast-string-width/license +21 -0
- package/node_modules/fast-string-width/package.json +34 -0
- package/node_modules/fast-string-width/readme.md +42 -0
- package/node_modules/fast-wrap-ansi/LICENSE +23 -0
- package/node_modules/fast-wrap-ansi/README.md +26 -0
- package/node_modules/fast-wrap-ansi/lib/main.d.ts +6 -0
- package/node_modules/fast-wrap-ansi/lib/main.js +218 -0
- package/node_modules/fast-wrap-ansi/package.json +51 -0
- package/node_modules/mute-stream/LICENSE +15 -0
- package/node_modules/mute-stream/README.md +68 -0
- package/node_modules/mute-stream/lib/index.js +142 -0
- package/node_modules/mute-stream/package.json +54 -0
- package/node_modules/signal-exit/LICENSE.txt +16 -0
- package/node_modules/signal-exit/README.md +74 -0
- package/node_modules/signal-exit/dist/cjs/browser.d.ts +12 -0
- package/node_modules/signal-exit/dist/cjs/browser.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/cjs/browser.js +10 -0
- package/node_modules/signal-exit/dist/cjs/browser.js.map +1 -0
- package/node_modules/signal-exit/dist/cjs/index.d.ts +48 -0
- package/node_modules/signal-exit/dist/cjs/index.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/cjs/index.js +279 -0
- package/node_modules/signal-exit/dist/cjs/index.js.map +1 -0
- package/node_modules/signal-exit/dist/cjs/package.json +3 -0
- package/node_modules/signal-exit/dist/cjs/signals.d.ts +29 -0
- package/node_modules/signal-exit/dist/cjs/signals.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/cjs/signals.js +42 -0
- package/node_modules/signal-exit/dist/cjs/signals.js.map +1 -0
- package/node_modules/signal-exit/dist/mjs/browser.d.ts +12 -0
- package/node_modules/signal-exit/dist/mjs/browser.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/mjs/browser.js +4 -0
- package/node_modules/signal-exit/dist/mjs/browser.js.map +1 -0
- package/node_modules/signal-exit/dist/mjs/index.d.ts +48 -0
- package/node_modules/signal-exit/dist/mjs/index.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/mjs/index.js +275 -0
- package/node_modules/signal-exit/dist/mjs/index.js.map +1 -0
- package/node_modules/signal-exit/dist/mjs/package.json +3 -0
- package/node_modules/signal-exit/dist/mjs/signals.d.ts +29 -0
- package/node_modules/signal-exit/dist/mjs/signals.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/mjs/signals.js +39 -0
- package/node_modules/signal-exit/dist/mjs/signals.js.map +1 -0
- package/node_modules/signal-exit/package.json +106 -0
- package/package.json +17 -4
- package/config/commit-conventions.md +0 -28
- package/config/skills/_shared/adapters/README.md +0 -74
- package/config/skills/_shared/adapters/grok.md +0 -100
- package/config/skills/_shared/archive_run.py +0 -969
- package/config/skills/_shared/codex-config.md +0 -81
- package/config/skills/_shared/collect-codex-findings.py +0 -505
- package/config/skills/_shared/finish-gate.py +0 -521
- package/config/skills/_shared/grok-anchor-guard.py +0 -78
- package/config/skills/_shared/judge-output-parser.py +0 -200
- package/config/skills/_shared/judge-role-evidence.py +0 -320
- package/config/skills/_shared/phase-prompt-render.py +0 -183
- package/config/skills/_shared/process-evidence.py +0 -926
- package/config/skills/_shared/resolve-bootstrap.py +0 -1410
- package/config/skills/_shared/resolve-stop-hook.py +0 -359
- package/config/skills/_shared/run-bounded.py +0 -58
- package/config/skills/_shared/spec-verify-check.py +0 -5083
- package/config/skills/_shared/state-phase-write.py +0 -5989
- package/config/skills/_shared/terminal-claim-check.py +0 -777
- package/config/skills/_shared/verify-merge-findings.py +0 -3528
- package/config/skills/devlyn-design-ui/SKILL.md +0 -650
- package/config/skills/devlyn-ideate/references/from-spec-mode.md +0 -61
- package/config/skills/devlyn-ideate/references/project-mode.md +0 -80
- package/config/skills/devlyn-ideate/references/spec-template.md +0 -121
- package/config/skills/devlyn-ideate/references/templates/decision.md +0 -48
- package/config/skills/devlyn-ideate/references/templates/roadmap.md +0 -50
- package/config/skills/devlyn-ideate/references/templates/vision.md +0 -44
- package/config/skills/devlyn-queue/SKILL.md +0 -66
- package/config/skills/devlyn-queue/scripts/append.py +0 -94
- package/config/skills/devlyn-resolve/SKILL.md +0 -379
- package/config/skills/devlyn-resolve/references/free-form-mode.md +0 -71
- package/config/skills/devlyn-resolve/references/outer-loop.md +0 -23
- package/config/skills/devlyn-resolve/references/phases/build-gate.md +0 -75
- package/config/skills/devlyn-resolve/references/phases/cleanup.md +0 -33
- package/config/skills/devlyn-resolve/references/phases/implement.md +0 -44
- package/config/skills/devlyn-resolve/references/phases/plan.md +0 -39
- package/config/skills/devlyn-resolve/references/phases/probe-derive.md +0 -261
- package/config/skills/devlyn-resolve/references/phases/surface-close.md +0 -34
- package/config/skills/devlyn-resolve/references/phases/verify.md +0 -310
- package/config/skills/devlyn-resolve/references/state-schema.md +0 -152
- package/config/skills/devlyn-resolve/references/task-completion.md +0 -160
- package/config/templates/prompt-templates.md +0 -71
- package/config/templates/template-feature.spec.md +0 -255
- package/config/templates/template-product-spec.md +0 -680
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Parse the one permitted pair-JUDGE stdout emission contract."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import json
|
|
7
|
-
import pathlib
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
FINDING_SEVERITIES = {"CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"}
|
|
12
|
-
VERDICTS = {"PASS", "PASS_WITH_ISSUES", "FAIL", "NEEDS_WORK", "BLOCKED"}
|
|
13
|
-
ENVELOPE_KEYS = {"text", "stopReason", "sessionId", "requestId"}
|
|
14
|
-
IGNORABLE_FENCES = {"```", "```json", "```jsonl"}
|
|
15
|
-
NARRATIVE_PREAMBLE_BYTES = frozenset(
|
|
16
|
-
b"\t\n\r !\"$%&'()*+,-./0123456789:;<=>?@"
|
|
17
|
-
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz|}~"
|
|
18
|
-
) | frozenset(range(0x80, 0x100))
|
|
19
|
-
STREAM_RECORD_TYPES = {"system", "assistant", "user", "result"}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def reject_json_constant(token: str) -> None:
|
|
23
|
-
raise ValueError(f"invalid JSON numeric constant: {token}")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict:
|
|
27
|
-
result = {}
|
|
28
|
-
for key, value in pairs:
|
|
29
|
-
if key in result:
|
|
30
|
-
raise ValueError(f"duplicate JSON key: {key}")
|
|
31
|
-
result[key] = value
|
|
32
|
-
return result
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def loads_strict_json(text: str) -> Any:
|
|
36
|
-
return json.loads(
|
|
37
|
-
text,
|
|
38
|
-
parse_constant=reject_json_constant,
|
|
39
|
-
object_pairs_hook=reject_duplicate_keys,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def collect_text(text: str, source: pathlib.Path) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
|
|
44
|
-
findings: list[dict[str, Any]] = []
|
|
45
|
-
summary: dict[str, Any] | None = None
|
|
46
|
-
for line_no, line in enumerate(text.splitlines(), 1):
|
|
47
|
-
raw = line.strip()
|
|
48
|
-
if not raw or raw in IGNORABLE_FENCES:
|
|
49
|
-
continue
|
|
50
|
-
if raw.startswith("# SUMMARY "):
|
|
51
|
-
if summary is not None:
|
|
52
|
-
raise SystemExit(f"error: record after terminal verdict at {source}:{line_no}")
|
|
53
|
-
try:
|
|
54
|
-
item = loads_strict_json(raw.removeprefix("# SUMMARY ").strip())
|
|
55
|
-
except ValueError as exc:
|
|
56
|
-
raise SystemExit(f"error: invalid SUMMARY JSON at {source}:{line_no}: {exc}")
|
|
57
|
-
if not isinstance(item, dict) or item.get("verdict") not in VERDICTS:
|
|
58
|
-
raise SystemExit(f"error: verdict has unknown value at {source}:{line_no}")
|
|
59
|
-
summary = item
|
|
60
|
-
continue
|
|
61
|
-
if summary is not None:
|
|
62
|
-
raise SystemExit(f"error: record after terminal verdict at {source}:{line_no}")
|
|
63
|
-
if raw in VERDICTS:
|
|
64
|
-
summary = {"verdict": raw}
|
|
65
|
-
continue
|
|
66
|
-
try:
|
|
67
|
-
item = loads_strict_json(raw)
|
|
68
|
-
except ValueError as exc:
|
|
69
|
-
raise SystemExit(f"error: invalid JSONL at {source}:{line_no}: {exc}")
|
|
70
|
-
if not isinstance(item, dict):
|
|
71
|
-
raise SystemExit(f"error: JSONL item is not an object at {source}:{line_no}")
|
|
72
|
-
severity = str(item.get("severity") or "").upper()
|
|
73
|
-
if severity not in FINDING_SEVERITIES:
|
|
74
|
-
raise SystemExit(f"error: finding missing valid severity at {source}:{line_no}")
|
|
75
|
-
findings.append(item)
|
|
76
|
-
return findings, summary
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def recover_envelope_text(text: str, source: pathlib.Path) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
|
|
80
|
-
encoded = text.encode("utf-8")
|
|
81
|
-
for cut, byte in enumerate(encoded[:-1], 1):
|
|
82
|
-
if byte not in NARRATIVE_PREAMBLE_BYTES:
|
|
83
|
-
break
|
|
84
|
-
try:
|
|
85
|
-
findings, summary = collect_text(encoded[cut:].decode("utf-8"), source)
|
|
86
|
-
except (UnicodeDecodeError, SystemExit):
|
|
87
|
-
continue
|
|
88
|
-
if summary is not None and summary.get("verdict") == "NEEDS_WORK" and findings:
|
|
89
|
-
return findings, summary
|
|
90
|
-
raise SystemExit("error: no admissible envelope recovery")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def collect_message_stream(
|
|
94
|
-
text: str, source: pathlib.Path
|
|
95
|
-
) -> tuple[list[dict[str, Any]], dict[str, Any] | None] | None:
|
|
96
|
-
"""Bind adjudication to a whole-message NDJSON stream's terminal assistant turn.
|
|
97
|
-
|
|
98
|
-
Returns None when the first record does not declare that stream. Once it does,
|
|
99
|
-
this returns or rejects: tool and narration turns are never forwarded, and no
|
|
100
|
-
envelope recovery is attempted.
|
|
101
|
-
"""
|
|
102
|
-
lines = [(no, raw) for no, line in enumerate(text.splitlines(), 1) if (raw := line.strip())]
|
|
103
|
-
if not lines:
|
|
104
|
-
return None
|
|
105
|
-
try:
|
|
106
|
-
first = loads_strict_json(lines[0][1])
|
|
107
|
-
except ValueError:
|
|
108
|
-
return None
|
|
109
|
-
if (
|
|
110
|
-
not isinstance(first, dict)
|
|
111
|
-
or first.get("type") != "system"
|
|
112
|
-
or str(first.get("severity") or "").upper() in FINDING_SEVERITIES
|
|
113
|
-
):
|
|
114
|
-
return None
|
|
115
|
-
if first.get("subtype") != "init":
|
|
116
|
-
raise SystemExit(f"error: stream must open with system/init at {source}:{lines[0][0]}")
|
|
117
|
-
records = [first]
|
|
118
|
-
for line_no, raw in lines[1:]:
|
|
119
|
-
try:
|
|
120
|
-
item = loads_strict_json(raw)
|
|
121
|
-
except ValueError as exc:
|
|
122
|
-
raise SystemExit(f"error: invalid stream NDJSON at {source}:{line_no}: {exc}")
|
|
123
|
-
if not isinstance(item, dict) or item.get("type") not in STREAM_RECORD_TYPES:
|
|
124
|
-
raise SystemExit(f"error: unknown stream record at {source}:{line_no}")
|
|
125
|
-
if item["type"] == "system":
|
|
126
|
-
raise SystemExit(f"error: stream has a second system record at {source}:{line_no}")
|
|
127
|
-
records.append(item)
|
|
128
|
-
|
|
129
|
-
results = [item for item in records if item["type"] == "result"]
|
|
130
|
-
if len(results) != 1 or results[0] is not records[-1]:
|
|
131
|
-
raise SystemExit(f"error: stream needs exactly one terminal result at {source}")
|
|
132
|
-
result = results[0]
|
|
133
|
-
if (
|
|
134
|
-
result.get("subtype") != "success"
|
|
135
|
-
or result.get("is_error") is not False
|
|
136
|
-
or result.get("stop_reason") != "end_turn"
|
|
137
|
-
):
|
|
138
|
-
raise SystemExit(f"error: stream result is not a successful end_turn at {source}")
|
|
139
|
-
|
|
140
|
-
assistants = [item for item in records if item["type"] == "assistant"]
|
|
141
|
-
terminals = [
|
|
142
|
-
item
|
|
143
|
-
for item in assistants
|
|
144
|
-
if isinstance(item.get("message"), dict) and item["message"].get("stop_reason") == "end_turn"
|
|
145
|
-
]
|
|
146
|
-
if (
|
|
147
|
-
len(terminals) != 1
|
|
148
|
-
or terminals[0] is not assistants[-1]
|
|
149
|
-
or terminals[0] is not records[-2]
|
|
150
|
-
):
|
|
151
|
-
raise SystemExit(f"error: stream needs exactly one final end_turn assistant message at {source}")
|
|
152
|
-
|
|
153
|
-
session = first.get("session_id")
|
|
154
|
-
if not isinstance(session, str) or any(
|
|
155
|
-
item.get("session_id") != session for item in (terminals[0], result)
|
|
156
|
-
):
|
|
157
|
-
raise SystemExit(f"error: stream session identity does not agree at {source}")
|
|
158
|
-
|
|
159
|
-
blocks = terminals[0]["message"].get("content")
|
|
160
|
-
if not isinstance(blocks, list):
|
|
161
|
-
raise SystemExit(f"error: stream terminal message has no content blocks at {source}")
|
|
162
|
-
parts: list[str] = []
|
|
163
|
-
for block in blocks:
|
|
164
|
-
if not isinstance(block, dict):
|
|
165
|
-
raise SystemExit(f"error: stream content block is not an object at {source}")
|
|
166
|
-
if block.get("type") == "text":
|
|
167
|
-
if not isinstance(block.get("text"), str):
|
|
168
|
-
raise SystemExit(f"error: stream text block is not a string at {source}")
|
|
169
|
-
parts.append(block["text"])
|
|
170
|
-
message_text = "".join(parts)
|
|
171
|
-
if result.get("result") != message_text:
|
|
172
|
-
raise SystemExit(f"error: stream result text does not match the terminal message at {source}")
|
|
173
|
-
return collect_text(message_text, source)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def collect_stdout(stdout_path: pathlib.Path) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
|
|
177
|
-
stdout_text = stdout_path.read_text(encoding="utf-8")
|
|
178
|
-
try:
|
|
179
|
-
candidate = loads_strict_json(stdout_text)
|
|
180
|
-
except ValueError:
|
|
181
|
-
candidate = None
|
|
182
|
-
if isinstance(candidate, dict) and ENVELOPE_KEYS.issubset(candidate) and "severity" not in candidate:
|
|
183
|
-
if candidate["stopReason"] != "EndTurn":
|
|
184
|
-
raise SystemExit(
|
|
185
|
-
f"error: envelope stopReason must be 'EndTurn' at {stdout_path}; "
|
|
186
|
-
f"got {candidate['stopReason']!r}"
|
|
187
|
-
)
|
|
188
|
-
if not isinstance(candidate["text"], str):
|
|
189
|
-
raise SystemExit(f"error: envelope text must be a string at {stdout_path}")
|
|
190
|
-
try:
|
|
191
|
-
return collect_text(candidate["text"], stdout_path)
|
|
192
|
-
except SystemExit as exc:
|
|
193
|
-
try:
|
|
194
|
-
return recover_envelope_text(candidate["text"], stdout_path)
|
|
195
|
-
except SystemExit:
|
|
196
|
-
raise SystemExit(f"error: envelope text rejected: {exc}") from None
|
|
197
|
-
stream = collect_message_stream(stdout_text, stdout_path)
|
|
198
|
-
if stream is not None:
|
|
199
|
-
return stream
|
|
200
|
-
return collect_text(stdout_text, stdout_path)
|
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Authenticate explicit read-only judge transport; never accept mutation receipts."""
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import argparse
|
|
6
|
-
import json
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
import re
|
|
9
|
-
import runpy
|
|
10
|
-
import sys
|
|
11
|
-
import tempfile
|
|
12
|
-
|
|
13
|
-
sys.dont_write_bytecode = True
|
|
14
|
-
ROLE = runpy.run_path(Path(__file__).with_name("role-config.py"))
|
|
15
|
-
loads, encoded, digest = (ROLE[key] for key in ("loads", "encoded", "digest"))
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def require(ok, detail):
|
|
19
|
-
if not ok:
|
|
20
|
-
raise ValueError("BLOCKED:judge-role-evidence: " + detail)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def option(argv, *names):
|
|
24
|
-
values = []
|
|
25
|
-
for index, item in enumerate(argv):
|
|
26
|
-
for name in names:
|
|
27
|
-
if item == name:
|
|
28
|
-
require(index + 1 < len(argv), f"missing {name}")
|
|
29
|
-
values.append(argv[index + 1])
|
|
30
|
-
elif item.startswith(name + "="):
|
|
31
|
-
values.append(item[len(name) + 1:])
|
|
32
|
-
require(len(values) <= 1, f"duplicate {names[0]}")
|
|
33
|
-
return values[0] if values else None
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def seal(devlyn, name):
|
|
37
|
-
path = devlyn / name
|
|
38
|
-
require(path.parent == devlyn and not path.is_symlink() and path.is_file(), f"not a regular canonical artifact: {name}")
|
|
39
|
-
raw = path.read_bytes()
|
|
40
|
-
return {"path": ".devlyn/" + name, "sha256": digest(raw), "bytes": len(raw)}, raw
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def claude_result(raw, exit_code):
|
|
44
|
-
value = loads(raw)
|
|
45
|
-
require(type(exit_code) is int and exit_code == 0 and isinstance(value, dict), "unsuccessful Claude execution")
|
|
46
|
-
require(value.get("type") == "result" and value.get("subtype") == "success"
|
|
47
|
-
and value.get("is_error") is False and value.get("stop_reason") == "end_turn"
|
|
48
|
-
and isinstance(value.get("session_id"), str) and bool(value["session_id"].strip())
|
|
49
|
-
and isinstance(value.get("result"), str), "unsuccessful or malformed Claude terminal envelope")
|
|
50
|
-
model = runpy.run_path(Path(__file__).with_name("state-phase-write.py"))["select_claude_primary_model"](value)
|
|
51
|
-
return value["result"].encode(), model, value["session_id"]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def codex_header(stderr):
|
|
55
|
-
prefix, separator, _ = stderr.partition("\nuser\n")
|
|
56
|
-
require(bool(separator), "native header must precede the echoed user prompt")
|
|
57
|
-
starts = list(re.finditer(r"^OpenAI Codex v[^\n]+\n--------\n", prefix, re.M))
|
|
58
|
-
require(len(starts) == 1, "missing or conflicting first native Codex header")
|
|
59
|
-
header = prefix[starts[0].end():]
|
|
60
|
-
require(header.endswith("\n--------"), "malformed native Codex header boundary")
|
|
61
|
-
fields = {"cli_version": "codex-cli " + starts[0].group(0).splitlines()[0].removeprefix("OpenAI Codex v")}
|
|
62
|
-
for key in ("model", "workdir", "sandbox", "session id", "reasoning effort"):
|
|
63
|
-
values = re.findall(r"^" + re.escape(key) + r": (.+)$", header, re.M)
|
|
64
|
-
require(len(values) == 1 and bool(values[0].strip()), f"missing/conflicting native {key}")
|
|
65
|
-
fields[key] = values[0]
|
|
66
|
-
return fields
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def describe(devlyn, state, role, exit_code):
|
|
70
|
-
require(type(exit_code) is int and exit_code == 0, "judge process did not complete successfully")
|
|
71
|
-
resolution = ROLE["snapshot"](state)
|
|
72
|
-
require(resolution is not None and role in ("primary_judge", "pair_judge"), "missing frozen judge selection")
|
|
73
|
-
entry = resolution["roles"][role]
|
|
74
|
-
engine = entry["engine"]
|
|
75
|
-
require(engine in {"codex", "claude"}, "this judge channel has no explicit-field evidence contract")
|
|
76
|
-
require(not entry.get("skipped_reason"), "cannot authenticate a skipped judge")
|
|
77
|
-
verify = state.get("phases", {}).get("verify", {})
|
|
78
|
-
require(isinstance(verify, dict) and type(verify.get("round")) is int, "missing VERIFY round")
|
|
79
|
-
require(ROLE["primary_engine"](state) == resolution["roles"]["primary_judge"]["engine"], "primary differs from frozen selection")
|
|
80
|
-
stem = engine + "-judge.r" + str(verify["round"])
|
|
81
|
-
artifacts = {}
|
|
82
|
-
for key, suffix in (("argv", ".argv.json"), ("prompt", ".prompt"), ("stderr", ".stderr")):
|
|
83
|
-
artifacts[key], raw = seal(devlyn, stem + suffix)
|
|
84
|
-
if key == "argv":
|
|
85
|
-
argv = loads(raw)
|
|
86
|
-
elif key == "prompt":
|
|
87
|
-
prompt = raw.decode()
|
|
88
|
-
else:
|
|
89
|
-
stderr = raw.decode()
|
|
90
|
-
require(isinstance(argv, list) and all(isinstance(x, str) for x in argv), "argv must be an array of strings")
|
|
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")).resolve(), "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")
|
|
114
|
-
diagnostics = stderr.partition("\nuser\n")[0] if engine == "codex" else stderr
|
|
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")
|
|
116
|
-
requested_model, requested_effort = entry.get("model_requested"), entry.get("effort_requested")
|
|
117
|
-
supplied_model = option(argv, "-m", "--model")
|
|
118
|
-
if requested_model is not None:
|
|
119
|
-
require(supplied_model == requested_model, "requested model differs from argv")
|
|
120
|
-
if engine == "claude":
|
|
121
|
-
for flag, expected in (("--permission-mode", "dontAsk"), ("--tools", "Read,Grep,Glob"),
|
|
122
|
-
("--allowedTools", "Read,Grep,Glob"), ("--setting-sources", "project"), ("--output-format", "json")):
|
|
123
|
-
require(option(argv, flag) == expected, f"Claude {flag} differs from read-only contract")
|
|
124
|
-
require("--strict-mcp-config" in argv and loads(option(argv, "--mcp-config") or "null") == {"mcpServers": {}}, "Claude MCP isolation 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")
|
|
126
|
-
if requested_effort:
|
|
127
|
-
require(option(argv, "--effort") == requested_effort, "requested effort differs from argv")
|
|
128
|
-
artifacts["raw"], raw = seal(devlyn, stem + ".output.json")
|
|
129
|
-
derived, observed, session = claude_result(raw, exit_code)
|
|
130
|
-
effort = None
|
|
131
|
-
basis = "native-Claude-result-modelUsage"
|
|
132
|
-
else:
|
|
133
|
-
require(option(argv, "-s", "--sandbox") == "read-only", "judge sandbox is not read-only")
|
|
134
|
-
cwd = option(argv, "-C", "--cd")
|
|
135
|
-
require(isinstance(cwd, str) and Path(cwd).is_absolute() and Path(cwd).resolve() == devlyn.parent.resolve(), "judge cwd differs from actual worktree")
|
|
136
|
-
require(not any(arg in argv for arg in ("--dangerously-bypass-approvals-and-sandbox", "--yolo", "--full-auto")), "judge bypass is forbidden")
|
|
137
|
-
flags = [argv[i + 1] for i, item in enumerate(argv[:-1]) if item in {"-c", "--config"}]
|
|
138
|
-
efforts = [flag.split("=", 1)[1].strip('"') for flag in flags if flag.startswith("model_reasoning_effort=")]
|
|
139
|
-
require(len(efforts) == 1 and (not requested_effort or efforts[0] == requested_effort), "missing/conflicting requested effort")
|
|
140
|
-
require("[codex-monitored] isolated=1\n" in diagnostics, "missing actual isolation marker")
|
|
141
|
-
require(re.search(r"^\[codex-monitored\] start: .* timeout=600s ", diagnostics, re.M) is not None, "missing actual 600s bound")
|
|
142
|
-
header = codex_header(stderr)
|
|
143
|
-
require(Path(header["workdir"]).is_absolute() and Path(header["workdir"]).resolve() == devlyn.parent.resolve()
|
|
144
|
-
and header["sandbox"] == "read-only", "observed workdir/sandbox mismatch")
|
|
145
|
-
observed, effort, session = header["model"], header["reasoning effort"], header["session id"]
|
|
146
|
-
require(effort == efforts[0], "native effort differs from dispatched effort")
|
|
147
|
-
artifacts["raw"], derived = seal(devlyn, stem + ".stdout")
|
|
148
|
-
basis = "native-Codex-configuration-header"
|
|
149
|
-
require(requested_model is None or observed == requested_model, "observed model differs from explicit requested model")
|
|
150
|
-
artifacts["stdout"] = {"path": ".devlyn/" + stem + ".stdout", "sha256": digest(derived), "bytes": len(derived)}
|
|
151
|
-
record = {"schema": 1, "run_id": state.get("run_id"), "round": verify["round"], "role": role,
|
|
152
|
-
"engine": engine, "model_requested": requested_model, "effort_requested": requested_effort,
|
|
153
|
-
"model_observed": observed, "effort_observed": effort, "session_id": session,
|
|
154
|
-
"identity_basis": basis, "exit_code": exit_code, "resolution_sha256": resolution["sha256"], "artifacts": artifacts}
|
|
155
|
-
return record, derived
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def authenticate(devlyn, state, role):
|
|
159
|
-
resolution = ROLE["snapshot"](state)
|
|
160
|
-
engine = resolution["roles"][role]["engine"]
|
|
161
|
-
stem = engine + "-judge.r" + str(state["phases"]["verify"]["round"])
|
|
162
|
-
binding, raw = seal(devlyn, stem + ".role-evidence.json")
|
|
163
|
-
document = loads(raw)
|
|
164
|
-
actual, derived = describe(devlyn, state, role, document.get("exit_code"))
|
|
165
|
-
require(actual == document, "retained role evidence differs from current raw/selection")
|
|
166
|
-
_, stdout = seal(devlyn, stem + ".stdout")
|
|
167
|
-
require(stdout == derived, "derived stdout differs from retained native result")
|
|
168
|
-
_, canonical = seal(devlyn, engine + "-judge.stdout")
|
|
169
|
-
require(canonical == derived, "canonical collector stdout differs from retained native result")
|
|
170
|
-
return {**binding, "model_observed": actual["model_observed"], "effort_observed": actual["effort_observed"],
|
|
171
|
-
"identity_basis": actual["identity_basis"], "artifacts": list(actual["artifacts"].values())}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def required_roles(state):
|
|
175
|
-
resolution = ROLE["snapshot"](state)
|
|
176
|
-
if resolution is None:
|
|
177
|
-
return []
|
|
178
|
-
return [role for role in ("primary_judge", "pair_judge")
|
|
179
|
-
if not resolution["roles"][role].get("skipped_reason")
|
|
180
|
-
and any(resolution["roles"][role].get(key) is not None for key in ("model_requested", "effort_requested"))]
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def self_test():
|
|
184
|
-
with tempfile.TemporaryDirectory() as temp:
|
|
185
|
-
work = Path(temp).resolve(); devlyn = work / ".devlyn"; devlyn.mkdir()
|
|
186
|
-
config = {"roles": {"primary_judge": {"engine": "claude", "model": "fixture-claude-model", "effort": "high"},
|
|
187
|
-
"pair_judge": {"engine": "codex", "model": "gpt-6-astra", "effort": "high"}}}
|
|
188
|
-
(devlyn / "engines.json").write_bytes(encoded(config))
|
|
189
|
-
selection = ROLE["resolve"](work, "codex", available=lambda e: True)
|
|
190
|
-
state = {"run_id": "fixture", "role_resolution": selection, "engine": "codex", "phases": {"verify": {"engine": "claude", "round": 0}}}
|
|
191
|
-
envelope = {"type": "result", "subtype": "success", "is_error": False, "stop_reason": "end_turn",
|
|
192
|
-
"session_id": "fixture-claude", "result": "PASS", "modelUsage": {"fixture-claude-model": {}}}
|
|
193
|
-
argv = ["python3", "run-bounded.py", "600", "--", "claude", "-p", "review", "--model", "fixture-claude-model", "--effort", "high", "--permission-mode", "dontAsk", "--tools", "Read,Grep,Glob", "--allowedTools", "Read,Grep,Glob", "--setting-sources", "project", "--output-format", "json", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}']
|
|
194
|
-
for suffix, raw in (("argv.json", encoded(argv)), ("prompt", b"review"), ("stderr", b""), ("output.json", encoded(envelope))):
|
|
195
|
-
(devlyn / ("claude-judge.r0." + suffix)).write_bytes(raw)
|
|
196
|
-
receipt, text = describe(devlyn, state, "primary_judge", 0)
|
|
197
|
-
assert text == b"PASS" and receipt["effort_observed"] is None
|
|
198
|
-
(devlyn / "claude-judge.stdout").write_bytes(text)
|
|
199
|
-
(devlyn / "claude-judge.r0.stdout").write_bytes(text)
|
|
200
|
-
(devlyn / "claude-judge.r0.role-evidence.json").write_bytes(encoded(receipt))
|
|
201
|
-
authenticate(devlyn, state, "primary_judge")
|
|
202
|
-
file_command = [argv[4], argv[5], *argv[7:]]
|
|
203
|
-
file_argv = ["python3", "run-bounded.py", "600", "--stdin-file",
|
|
204
|
-
str(devlyn / "claude-judge.r0.prompt"), "--record-transport", "--", *file_command]
|
|
205
|
-
(devlyn / "claude-judge.r0.argv.json").write_bytes(encoded(file_argv))
|
|
206
|
-
carrier = devlyn / "claude-judge.r0.prompt.transport.json"
|
|
207
|
-
carrier.write_bytes(encoded({"schema_version": 1, "transport": "stdin-file",
|
|
208
|
-
"prompt": {"path": str(devlyn / "claude-judge.r0.prompt"), "sha256": digest(b"review"), "bytes": 6},
|
|
209
|
-
"command": file_command, "argv": file_command, "timeout_sec": 600,
|
|
210
|
-
"isolated": False, "status": "completed", "exit_code": 0}))
|
|
211
|
-
describe(devlyn, state, "primary_judge", 0)
|
|
212
|
-
file_argv[4] = str(work / "wrong.prompt")
|
|
213
|
-
(devlyn / "claude-judge.r0.argv.json").write_bytes(encoded(file_argv))
|
|
214
|
-
try:
|
|
215
|
-
describe(devlyn, state, "primary_judge", 0)
|
|
216
|
-
except ValueError as exc:
|
|
217
|
-
assert "bounded prompt path mismatch" in str(exc)
|
|
218
|
-
else:
|
|
219
|
-
raise AssertionError("wrong file-transport prompt accepted")
|
|
220
|
-
carrier.unlink()
|
|
221
|
-
(devlyn / "claude-judge.r0.argv.json").write_bytes(encoded(argv))
|
|
222
|
-
for field, value in (("subtype", "error_max_turns"), ("stop_reason", "tool_use"), ("is_error", True), ("session_id", ""), ("result", None)):
|
|
223
|
-
bad = {**envelope, field: value}
|
|
224
|
-
try:
|
|
225
|
-
claude_result(encoded(bad), 0)
|
|
226
|
-
except ValueError:
|
|
227
|
-
pass
|
|
228
|
-
else:
|
|
229
|
-
raise AssertionError(field)
|
|
230
|
-
for status in (1, 124, True):
|
|
231
|
-
try:
|
|
232
|
-
describe(devlyn, state, "primary_judge", status)
|
|
233
|
-
except ValueError:
|
|
234
|
-
pass
|
|
235
|
-
else:
|
|
236
|
-
raise AssertionError("failed exit accepted")
|
|
237
|
-
(devlyn / "claude-judge.stdout").write_bytes(b"forged PASS")
|
|
238
|
-
try:
|
|
239
|
-
authenticate(devlyn, state, "primary_judge")
|
|
240
|
-
except ValueError:
|
|
241
|
-
pass
|
|
242
|
-
else:
|
|
243
|
-
raise AssertionError("derived tampering accepted")
|
|
244
|
-
prompt = "review unsupported model/effort warning handling\n"
|
|
245
|
-
argv = ["bash", "codex-monitored.sh", "-C", str(work), "-s", "read-only", "-m", "gpt-6-astra", "-c", "model_reasoning_effort=high", prompt]
|
|
246
|
-
header = f"[codex-monitored] start: ts=fixture timeout=600s bin=codex\n[codex-monitored] isolated=1\nOpenAI Codex v0.153.4\n--------\nworkdir: {work}\nmodel: gpt-6-astra\nsandbox: read-only\nreasoning effort: high\nsession id: fixture-codex\n--------\nuser\nreview\n"
|
|
247
|
-
header = header.replace("\nuser\nreview\n", "\nuser\n" + prompt)
|
|
248
|
-
for suffix, raw in (("argv.json", encoded(argv)), ("prompt", prompt.encode()), ("stderr", header.encode()), ("stdout", b"PASS\n")):
|
|
249
|
-
(devlyn / ("codex-judge.r0." + suffix)).write_bytes(raw)
|
|
250
|
-
receipt, _ = describe(devlyn, state, "pair_judge", 0)
|
|
251
|
-
assert receipt["model_observed"] == "gpt-6-astra"
|
|
252
|
-
alias = work / "logical-cwd"
|
|
253
|
-
if sys.platform == "win32":
|
|
254
|
-
import subprocess
|
|
255
|
-
subprocess.run(["cmd.exe", "/d", "/c", "mklink", "/J", str(alias), str(work)], check=True, capture_output=True)
|
|
256
|
-
else:
|
|
257
|
-
alias.symlink_to(work, target_is_directory=True)
|
|
258
|
-
alias_argv = [str(alias) if arg == str(work) else arg for arg in argv]
|
|
259
|
-
alias_header = header.replace(f"workdir: {work}\n", f"workdir: {alias}\n")
|
|
260
|
-
(devlyn / "codex-judge.r0.argv.json").write_bytes(encoded(alias_argv))
|
|
261
|
-
(devlyn / "codex-judge.r0.stderr").write_text(alias_header, encoding="utf-8")
|
|
262
|
-
receipt, _ = describe(devlyn, state, "pair_judge", 0)
|
|
263
|
-
assert receipt["artifacts"]["argv"]["sha256"] == digest(encoded(alias_argv))
|
|
264
|
-
assert (devlyn / "codex-judge.r0.stderr").read_text(encoding="utf-8") == alias_header
|
|
265
|
-
elsewhere = work / "elsewhere"; elsewhere.mkdir()
|
|
266
|
-
(devlyn / "codex-judge.r0.argv.json").write_bytes(encoded([str(elsewhere) if arg == str(work) else arg for arg in argv]))
|
|
267
|
-
try:
|
|
268
|
-
describe(devlyn, state, "pair_judge", 0)
|
|
269
|
-
except ValueError:
|
|
270
|
-
pass
|
|
271
|
-
else:
|
|
272
|
-
raise AssertionError("different physical worktree accepted")
|
|
273
|
-
(devlyn / "codex-judge.r0.argv.json").write_bytes(encoded(argv))
|
|
274
|
-
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")):
|
|
275
|
-
(devlyn / "codex-judge.r0.stderr").write_text(bad, encoding="utf-8")
|
|
276
|
-
try:
|
|
277
|
-
describe(devlyn, state, "pair_judge", 0)
|
|
278
|
-
except ValueError:
|
|
279
|
-
pass
|
|
280
|
-
else:
|
|
281
|
-
raise AssertionError("invalid native header accepted")
|
|
282
|
-
print("PASS judge-role-evidence self-test: success/error/timeout, raw derivation, tampering and native header identity")
|
|
283
|
-
return 0
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def main():
|
|
287
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
288
|
-
parser.add_argument("--self-test", action="store_true")
|
|
289
|
-
parser.add_argument("--devlyn-dir", type=Path, default=Path(".devlyn"))
|
|
290
|
-
parser.add_argument("--role", choices=("primary_judge", "pair_judge"))
|
|
291
|
-
parser.add_argument("--exit-code", type=int)
|
|
292
|
-
args = parser.parse_args()
|
|
293
|
-
if args.self_test:
|
|
294
|
-
return self_test()
|
|
295
|
-
if args.role is None or args.exit_code is None:
|
|
296
|
-
parser.error("--role and --exit-code are required")
|
|
297
|
-
try:
|
|
298
|
-
devlyn = args.devlyn_dir.resolve()
|
|
299
|
-
state = loads((devlyn / "pipeline.state.json").read_bytes())
|
|
300
|
-
record, derived = describe(devlyn, state, args.role, args.exit_code)
|
|
301
|
-
stem = record["engine"] + "-judge.r" + str(record["round"])
|
|
302
|
-
target = devlyn / (stem + ".role-evidence.json")
|
|
303
|
-
require(not target.exists(), "role evidence already exists for this round")
|
|
304
|
-
stdout = devlyn / (record["engine"] + "-judge.stdout")
|
|
305
|
-
require(not stdout.exists(), "canonical stdout already exists")
|
|
306
|
-
if record["engine"] == "claude":
|
|
307
|
-
require(not (devlyn / (stem + ".stdout")).exists(), "derived stdout already exists")
|
|
308
|
-
(devlyn / (stem + ".stdout")).write_bytes(derived)
|
|
309
|
-
stdout.write_bytes(derived)
|
|
310
|
-
target.write_bytes(encoded(record))
|
|
311
|
-
print(json.dumps(authenticate(devlyn, state, args.role), sort_keys=True))
|
|
312
|
-
return 0
|
|
313
|
-
except (OSError, ValueError, TypeError, KeyError) as exc:
|
|
314
|
-
print(str(exc), file=sys.stderr)
|
|
315
|
-
return 1
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if __name__ == "__main__":
|
|
319
|
-
runpy.run_path(str(Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
|
|
320
|
-
raise SystemExit(main())
|