devlyn-cli 4.1.0 → 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.
- package/AGENTS.md +10 -94
- package/CLAUDE.md +7 -160
- package/LICENSE +21 -0
- package/README.md +65 -110
- package/bin/devlyn.js +95 -116
- package/bin/instruction-templates.json +43 -2
- package/bin/instructions.js +29 -8
- 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/package.json +2 -3
- 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,1410 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Deterministic PHASE-0 bootstrap for /devlyn-resolve."""
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import contextlib
|
|
6
|
-
import datetime
|
|
7
|
-
import hashlib
|
|
8
|
-
import importlib.util
|
|
9
|
-
import json
|
|
10
|
-
import os
|
|
11
|
-
import pathlib
|
|
12
|
-
import runpy
|
|
13
|
-
import secrets
|
|
14
|
-
import stat
|
|
15
|
-
import subprocess
|
|
16
|
-
import sys
|
|
17
|
-
import tempfile
|
|
18
|
-
|
|
19
|
-
sys.dont_write_bytecode = True
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
VALUE_FLAGS = {
|
|
23
|
-
"--max-rounds", "--engine", "--spec", "--verify-only", "--goal-file", "--bypass", "--role-config",
|
|
24
|
-
}
|
|
25
|
-
BOOL_FLAGS = {"--pair-verify", "--no-pair", "--risk-probes", "--no-risk-probes", "--perf"}
|
|
26
|
-
SINGLE_VALUE_FLAGS = VALUE_FLAGS - {"--bypass"}
|
|
27
|
-
VALID_BYPASSES = {"build-gate", "cleanup"}
|
|
28
|
-
PHASE_NAMES = (
|
|
29
|
-
"plan", "probe_derive", "implement", "surface_close",
|
|
30
|
-
"build_gate", "cleanup", "verify", "final_report",
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class BootstrapBlocked(Exception):
|
|
35
|
-
def __init__(self, reason: str, detail: str):
|
|
36
|
-
super().__init__(detail)
|
|
37
|
-
self.reason = reason
|
|
38
|
-
self.detail = detail
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def block(reason: str, detail: str) -> None:
|
|
42
|
-
raise BootstrapBlocked(reason, detail)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def reject_json_constant(token: str) -> None:
|
|
46
|
-
raise ValueError(f"invalid JSON numeric constant: {token}")
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict:
|
|
50
|
-
result = {}
|
|
51
|
-
for key, value in pairs:
|
|
52
|
-
if key in result:
|
|
53
|
-
raise ValueError(f"duplicate JSON key: {key}")
|
|
54
|
-
result[key] = value
|
|
55
|
-
return result
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def strict_json(text: str):
|
|
59
|
-
return json.loads(
|
|
60
|
-
text,
|
|
61
|
-
parse_constant=reject_json_constant,
|
|
62
|
-
object_pairs_hook=reject_duplicate_keys,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def json_bytes(value: object) -> bytes:
|
|
67
|
-
return (json.dumps(value, indent=2, sort_keys=True) + "\n").encode()
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def atomic_write(path: pathlib.Path, raw: bytes) -> None:
|
|
71
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
-
fd, name = tempfile.mkstemp(dir=path.parent, prefix=path.name + ".tmp.")
|
|
73
|
-
try:
|
|
74
|
-
with open(fd, "wb") as handle:
|
|
75
|
-
handle.write(raw)
|
|
76
|
-
pathlib.Path(name).replace(path)
|
|
77
|
-
except BaseException:
|
|
78
|
-
pathlib.Path(name).unlink(missing_ok=True)
|
|
79
|
-
raise
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def atomic_write_batch(
|
|
83
|
-
outputs: dict[pathlib.Path, bytes | None],
|
|
84
|
-
writer=atomic_write,
|
|
85
|
-
) -> None:
|
|
86
|
-
originals = {
|
|
87
|
-
path: (path.exists(), path.read_bytes() if path.exists() else None)
|
|
88
|
-
for path in outputs
|
|
89
|
-
}
|
|
90
|
-
parent_existed = {path.parent: path.parent.exists() for path in outputs}
|
|
91
|
-
try:
|
|
92
|
-
for path, raw in outputs.items():
|
|
93
|
-
if raw is None:
|
|
94
|
-
path.unlink(missing_ok=True)
|
|
95
|
-
else:
|
|
96
|
-
writer(path, raw)
|
|
97
|
-
except BaseException:
|
|
98
|
-
for path, (existed, raw) in reversed(originals.items()):
|
|
99
|
-
if existed:
|
|
100
|
-
assert raw is not None
|
|
101
|
-
atomic_write(path, raw)
|
|
102
|
-
else:
|
|
103
|
-
path.unlink(missing_ok=True)
|
|
104
|
-
for parent, existed in parent_existed.items():
|
|
105
|
-
if not existed and parent.exists():
|
|
106
|
-
parent.rmdir()
|
|
107
|
-
raise
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def sha256(raw: bytes) -> str:
|
|
111
|
-
return hashlib.sha256(raw).hexdigest()
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def parse_flags(argv: list[str]) -> dict:
|
|
115
|
-
values: dict[str, str | list[str]] = {"--bypass": []}
|
|
116
|
-
switches: set[str] = set()
|
|
117
|
-
positional: list[str] = []
|
|
118
|
-
seen_single: set[str] = set()
|
|
119
|
-
i = 0
|
|
120
|
-
positional_only = False
|
|
121
|
-
while i < len(argv):
|
|
122
|
-
token = argv[i]
|
|
123
|
-
if positional_only:
|
|
124
|
-
positional.append(token)
|
|
125
|
-
i += 1
|
|
126
|
-
continue
|
|
127
|
-
if token == "--":
|
|
128
|
-
positional_only = True
|
|
129
|
-
i += 1
|
|
130
|
-
continue
|
|
131
|
-
if token in BOOL_FLAGS:
|
|
132
|
-
switches.add(token)
|
|
133
|
-
i += 1
|
|
134
|
-
continue
|
|
135
|
-
if token in VALUE_FLAGS:
|
|
136
|
-
if token in SINGLE_VALUE_FLAGS and token in seen_single:
|
|
137
|
-
block("BLOCKED:invalid-flags", f"{token} may be passed only once")
|
|
138
|
-
if i + 1 >= len(argv) or argv[i + 1].startswith("--"):
|
|
139
|
-
block("BLOCKED:invalid-flags", f"{token} requires a value")
|
|
140
|
-
value = argv[i + 1]
|
|
141
|
-
if token == "--bypass":
|
|
142
|
-
assert isinstance(values[token], list)
|
|
143
|
-
values[token].append(value)
|
|
144
|
-
else:
|
|
145
|
-
values[token] = value
|
|
146
|
-
seen_single.add(token)
|
|
147
|
-
i += 2
|
|
148
|
-
continue
|
|
149
|
-
if token.startswith("-"):
|
|
150
|
-
block("BLOCKED:invalid-flags", f"unknown flag: {token}")
|
|
151
|
-
positional.append(token)
|
|
152
|
-
i += 1
|
|
153
|
-
|
|
154
|
-
if "--pair-verify" in switches and "--no-pair" in switches:
|
|
155
|
-
block("BLOCKED:invalid-flags", "--pair-verify and --no-pair are mutually exclusive")
|
|
156
|
-
if "--goal-file" in values and any(flag in values for flag in ("--spec", "--verify-only")):
|
|
157
|
-
block("BLOCKED:invalid-flags", "--goal-file is mutually exclusive with --spec/--verify-only")
|
|
158
|
-
if "--goal-file" in values and positional:
|
|
159
|
-
block("BLOCKED:invalid-flags", "--goal-file is mutually exclusive with an inline goal")
|
|
160
|
-
if "--verify-only" in values and "--spec" not in values:
|
|
161
|
-
block("BLOCKED:invalid-flags", "--verify-only requires --spec")
|
|
162
|
-
if "--spec" in values and positional:
|
|
163
|
-
block("BLOCKED:invalid-flags", "--spec is mutually exclusive with an inline goal")
|
|
164
|
-
|
|
165
|
-
max_rounds_raw = values.get("--max-rounds", "4")
|
|
166
|
-
try:
|
|
167
|
-
max_rounds = int(str(max_rounds_raw))
|
|
168
|
-
except ValueError:
|
|
169
|
-
block("BLOCKED:invalid-flags", "--max-rounds must be a positive integer")
|
|
170
|
-
if max_rounds < 1:
|
|
171
|
-
block("BLOCKED:invalid-flags", "--max-rounds must be a positive integer")
|
|
172
|
-
|
|
173
|
-
bypasses: list[str] = []
|
|
174
|
-
for group in values["--bypass"]:
|
|
175
|
-
for phase in group.split(","):
|
|
176
|
-
if phase not in VALID_BYPASSES:
|
|
177
|
-
block("BLOCKED:invalid-flags", f"invalid --bypass phase: {phase or '<empty>'}")
|
|
178
|
-
if phase not in bypasses:
|
|
179
|
-
bypasses.append(phase)
|
|
180
|
-
|
|
181
|
-
mode = "verify-only" if "--verify-only" in values else "spec" if "--spec" in values else "free-form"
|
|
182
|
-
return {
|
|
183
|
-
"mode": mode,
|
|
184
|
-
"max_rounds": max_rounds,
|
|
185
|
-
"engine": values.get("--engine"),
|
|
186
|
-
"spec": values.get("--spec"),
|
|
187
|
-
"verify_ref": values.get("--verify-only"),
|
|
188
|
-
"goal_file": values.get("--goal-file"),
|
|
189
|
-
"role_config": values.get("--role-config"),
|
|
190
|
-
"no_pair": "--no-pair" in switches,
|
|
191
|
-
"inline_goal": " ".join(positional),
|
|
192
|
-
"pair_verify": "--pair-verify" in switches,
|
|
193
|
-
"bypasses": bypasses,
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def validate_shared_dir(shared_dir: pathlib.Path) -> None:
|
|
198
|
-
for name in ("spec-verify-check.py", "archive_run.py", "process-evidence.py"):
|
|
199
|
-
required = shared_dir / name
|
|
200
|
-
if not required.is_file():
|
|
201
|
-
block("BLOCKED:shared-dir-unresolved", str(required))
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def archive_prior_run(devlyn: pathlib.Path, shared_dir: pathlib.Path) -> None:
|
|
205
|
-
archive = runpy.run_path(shared_dir / "archive_run.py")
|
|
206
|
-
if not archive["has_owned_artifacts"](devlyn):
|
|
207
|
-
return
|
|
208
|
-
try:
|
|
209
|
-
state = archive["read_state"](devlyn)
|
|
210
|
-
except (archive["ArchiveError"], OSError, UnicodeError, ValueError) as exc:
|
|
211
|
-
block("BLOCKED:prior-run-ownership-unverified", str(exc))
|
|
212
|
-
run_id = state["run_id"]
|
|
213
|
-
if not archive["is_completed"](state) or archive["has_bootstrap_residue"](devlyn):
|
|
214
|
-
block(
|
|
215
|
-
"BLOCKED:prior-run-unfinished",
|
|
216
|
-
f"Run {run_id} at {devlyn.parent} is unfinished or indeterminate. "
|
|
217
|
-
"Continue through its owning session or start in a distinct worktree.",
|
|
218
|
-
)
|
|
219
|
-
try:
|
|
220
|
-
archive["move_artifacts"](devlyn, devlyn / "runs" / run_id)
|
|
221
|
-
archive["prune"](devlyn / "runs", keep=10)
|
|
222
|
-
except (archive["ArchiveError"], OSError, UnicodeError, ValueError) as exc:
|
|
223
|
-
block("BLOCKED:prior-run-archive-failed", str(exc))
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def safe_goal_file(cwd: pathlib.Path, raw_path: str) -> bytes:
|
|
227
|
-
supplied = pathlib.Path(raw_path)
|
|
228
|
-
if supplied.is_absolute() or ".." in supplied.parts:
|
|
229
|
-
block("BLOCKED:goal-file-invalid-path", raw_path)
|
|
230
|
-
target = cwd / supplied
|
|
231
|
-
try:
|
|
232
|
-
resolved = target.resolve(strict=True)
|
|
233
|
-
resolved.relative_to(cwd.resolve())
|
|
234
|
-
except (OSError, ValueError):
|
|
235
|
-
if target.exists():
|
|
236
|
-
block("BLOCKED:goal-file-invalid-path", raw_path)
|
|
237
|
-
block("BLOCKED:goal-file-unreadable", raw_path)
|
|
238
|
-
try:
|
|
239
|
-
metadata = resolved.stat()
|
|
240
|
-
except OSError:
|
|
241
|
-
block("BLOCKED:goal-file-unreadable", raw_path)
|
|
242
|
-
if not stat.S_ISREG(metadata.st_mode) or metadata.st_mode & 0o444 == 0:
|
|
243
|
-
block("BLOCKED:goal-file-unreadable", raw_path)
|
|
244
|
-
try:
|
|
245
|
-
raw = resolved.read_bytes()
|
|
246
|
-
text = raw.decode("utf-8")
|
|
247
|
-
except (OSError, UnicodeError):
|
|
248
|
-
block("BLOCKED:goal-file-unreadable", raw_path)
|
|
249
|
-
if not text.strip():
|
|
250
|
-
block("BLOCKED:goal-file-empty", raw_path)
|
|
251
|
-
return raw
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def run_checked(command: list[str], cwd: pathlib.Path) -> None:
|
|
255
|
-
proc = subprocess.run(command, cwd=cwd, capture_output=True, text=True, encoding="utf-8")
|
|
256
|
-
if proc.returncode != 0:
|
|
257
|
-
detail = (proc.stderr or proc.stdout).strip() or "command failed"
|
|
258
|
-
block("BLOCKED:invalid-flags", detail)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
def load_spec_helper(shared_dir: pathlib.Path):
|
|
262
|
-
path = shared_dir / "spec-verify-check.py"
|
|
263
|
-
spec = importlib.util.spec_from_file_location("devlyn_spec_verify_check", path)
|
|
264
|
-
if spec is None or spec.loader is None:
|
|
265
|
-
block("BLOCKED:shared-dir-unresolved", str(path))
|
|
266
|
-
module = importlib.util.module_from_spec(spec)
|
|
267
|
-
spec.loader.exec_module(module)
|
|
268
|
-
return module
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def init_spec_source(
|
|
272
|
-
cwd: pathlib.Path, staging_dir: pathlib.Path, shared_dir: pathlib.Path, raw_path: str,
|
|
273
|
-
) -> tuple[dict, bytes | None]:
|
|
274
|
-
path = pathlib.Path(raw_path)
|
|
275
|
-
path = path if path.is_absolute() else cwd / path
|
|
276
|
-
if not path.is_file():
|
|
277
|
-
block("BLOCKED:invalid-flags", f"spec not found: {raw_path}")
|
|
278
|
-
try:
|
|
279
|
-
raw = path.read_bytes()
|
|
280
|
-
raw.decode("utf-8")
|
|
281
|
-
except (OSError, UnicodeError) as exc:
|
|
282
|
-
block("BLOCKED:invalid-flags", f"spec unreadable: {raw_path}: {exc}")
|
|
283
|
-
helper = shared_dir / "spec-verify-check.py"
|
|
284
|
-
expected = path.with_name("spec.expected.json")
|
|
285
|
-
module = load_spec_helper(shared_dir)
|
|
286
|
-
if expected.is_file():
|
|
287
|
-
found, _staged, error, _expected_path, _data = module.stage_from_expected(path, staging_dir)
|
|
288
|
-
if not found or error:
|
|
289
|
-
block("BLOCKED:invalid-flags", error or f"expected contract not found: {expected}")
|
|
290
|
-
else:
|
|
291
|
-
run_checked([sys.executable, str(helper), "--check", str(path)], cwd)
|
|
292
|
-
_found, _staged, error = module.stage_from_source(path, staging_dir)
|
|
293
|
-
if error:
|
|
294
|
-
block("BLOCKED:invalid-flags", error)
|
|
295
|
-
staged_path = staging_dir / "spec-verify.json"
|
|
296
|
-
return ({
|
|
297
|
-
"type": "spec",
|
|
298
|
-
"spec_path": raw_path,
|
|
299
|
-
"spec_sha256": sha256(raw),
|
|
300
|
-
"criteria_path": None,
|
|
301
|
-
"criteria_sha256": None,
|
|
302
|
-
}, staged_path.read_bytes() if staged_path.is_file() else None)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def git_text(cwd: pathlib.Path, *args: str) -> str:
|
|
306
|
-
proc = subprocess.run(["git", *args], cwd=cwd, capture_output=True)
|
|
307
|
-
text = os.fsdecode(proc.stdout.removesuffix(b"\n"))
|
|
308
|
-
if proc.returncode != 0 or not text:
|
|
309
|
-
detail = os.fsdecode(proc.stderr or proc.stdout).strip() or "git command failed"
|
|
310
|
-
block("BLOCKED:invalid-flags", detail)
|
|
311
|
-
return text
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
def base_branch(cwd: pathlib.Path) -> str | None:
|
|
315
|
-
proc = subprocess.run(
|
|
316
|
-
["git", "symbolic-ref", "--short", "-q", "HEAD"],
|
|
317
|
-
cwd=cwd,
|
|
318
|
-
capture_output=True,
|
|
319
|
-
text=True,
|
|
320
|
-
encoding="utf-8",
|
|
321
|
-
)
|
|
322
|
-
if proc.returncode == 1 and not proc.stdout.strip() and not proc.stderr.strip():
|
|
323
|
-
return None
|
|
324
|
-
if proc.returncode != 0 or not proc.stdout.strip():
|
|
325
|
-
detail = (proc.stderr or proc.stdout).strip() or "git symbolic-ref failed"
|
|
326
|
-
block("BLOCKED:invalid-flags", detail)
|
|
327
|
-
return proc.stdout.strip()
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def capture_external_diff(cwd: pathlib.Path, ref: str) -> bytes:
|
|
331
|
-
supplied = pathlib.Path(ref)
|
|
332
|
-
source = supplied if supplied.is_absolute() else cwd / supplied
|
|
333
|
-
if source.is_file():
|
|
334
|
-
raw = source.read_bytes()
|
|
335
|
-
else:
|
|
336
|
-
proc = subprocess.run(["git", "diff", "--binary", "--no-ext-diff", "--src-prefix=a/", "--dst-prefix=b/", ref], cwd=cwd, capture_output=True)
|
|
337
|
-
if proc.returncode != 0:
|
|
338
|
-
block("BLOCKED:invalid-flags", os.fsdecode(proc.stderr or proc.stdout).strip())
|
|
339
|
-
raw = proc.stdout
|
|
340
|
-
return raw
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
def require_clean_tracked_baseline(cwd: pathlib.Path) -> None:
|
|
344
|
-
changed: list[bytes] = []
|
|
345
|
-
for args in (("diff",), ("diff", "--cached")):
|
|
346
|
-
proc = subprocess.run(
|
|
347
|
-
["git", *args, "--no-renames", "--name-only", "-z"],
|
|
348
|
-
cwd=cwd,
|
|
349
|
-
capture_output=True,
|
|
350
|
-
)
|
|
351
|
-
if proc.returncode != 0:
|
|
352
|
-
block("BLOCKED:invalid-flags", os.fsdecode(proc.stderr or proc.stdout).strip())
|
|
353
|
-
changed.extend(path for path in proc.stdout.split(b"\0") if path)
|
|
354
|
-
if any(path != b".devlyn" and not path.startswith(b".devlyn/") for path in changed):
|
|
355
|
-
block(
|
|
356
|
-
"BLOCKED:worktree-dirty",
|
|
357
|
-
"Commit or stash tracked changes outside .devlyn before starting a full resolve.",
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
@contextlib.contextmanager
|
|
362
|
-
def admission_lock(cwd: pathlib.Path):
|
|
363
|
-
lock_path = pathlib.Path(git_text(cwd, "rev-parse", "--absolute-git-dir")) / "devlyn-bootstrap.lock"
|
|
364
|
-
with contextlib.ExitStack() as stack:
|
|
365
|
-
try:
|
|
366
|
-
stack.enter_context(runpy.run_path(pathlib.Path(__file__).with_name("platform-support.py"))["file_lock"](lock_path))
|
|
367
|
-
except BlockingIOError:
|
|
368
|
-
block(
|
|
369
|
-
"BLOCKED:bootstrap-contended",
|
|
370
|
-
f"Bootstrap admission is occupied at {cwd}. Retry from this root after admission finishes.",
|
|
371
|
-
)
|
|
372
|
-
except (ImportError, OSError, AttributeError) as exc:
|
|
373
|
-
block("BLOCKED:bootstrap-lock-unavailable", f"{cwd}: {lock_path}: {exc}")
|
|
374
|
-
for path in (cwd / ".devlyn", cwd / ".devlyn" / "runs"):
|
|
375
|
-
if path.is_symlink() or path.resolve() != path or (path.exists() and not path.is_dir()):
|
|
376
|
-
block("BLOCKED:devlyn-path-redirect", f"{cwd}: require an unredirected directory at {path}")
|
|
377
|
-
yield
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
def bootstrap(
|
|
381
|
-
argv: list[str],
|
|
382
|
-
cwd: pathlib.Path,
|
|
383
|
-
shared_dir: pathlib.Path,
|
|
384
|
-
*,
|
|
385
|
-
default_engine: str = "claude",
|
|
386
|
-
writer=atomic_write,
|
|
387
|
-
) -> dict:
|
|
388
|
-
cwd = cwd.resolve()
|
|
389
|
-
validate_shared_dir(shared_dir)
|
|
390
|
-
parsed = parse_flags(argv)
|
|
391
|
-
for name in ("GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR"):
|
|
392
|
-
if name in os.environ:
|
|
393
|
-
block("BLOCKED:git-env-redirect", f"{cwd}: inherited {name} redirects Git; unset it before retrying.")
|
|
394
|
-
root = pathlib.Path(git_text(cwd, "rev-parse", "--show-toplevel")).resolve()
|
|
395
|
-
if cwd != root:
|
|
396
|
-
block("BLOCKED:worktree-root-required", f"{cwd} is not the worktree root {root}. Retry from {root}.")
|
|
397
|
-
if parsed["mode"] != "verify-only":
|
|
398
|
-
require_clean_tracked_baseline(cwd)
|
|
399
|
-
with admission_lock(cwd):
|
|
400
|
-
devlyn = cwd / ".devlyn"
|
|
401
|
-
outputs: dict[pathlib.Path, bytes | None] = {devlyn / "external-diff.patch": None}
|
|
402
|
-
if parsed["mode"] == "free-form":
|
|
403
|
-
raw_goal = (
|
|
404
|
-
safe_goal_file(cwd, parsed["goal_file"])
|
|
405
|
-
if parsed["goal_file"] is not None
|
|
406
|
-
else parsed["inline_goal"].encode("utf-8")
|
|
407
|
-
)
|
|
408
|
-
outputs[devlyn / "goal.raw.txt"] = raw_goal
|
|
409
|
-
source = {
|
|
410
|
-
"type": "generated",
|
|
411
|
-
"spec_path": None,
|
|
412
|
-
"spec_sha256": None,
|
|
413
|
-
"goal_path": ".devlyn/goal.raw.txt",
|
|
414
|
-
"goal_sha256": sha256(raw_goal),
|
|
415
|
-
"criteria_path": ".devlyn/criteria.generated.md",
|
|
416
|
-
"criteria_sha256": None,
|
|
417
|
-
}
|
|
418
|
-
else:
|
|
419
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
420
|
-
source, staged_spec = init_spec_source(
|
|
421
|
-
cwd, pathlib.Path(tmp), shared_dir, parsed["spec"],
|
|
422
|
-
)
|
|
423
|
-
outputs[devlyn / "spec-verify.json"] = staged_spec
|
|
424
|
-
if parsed["mode"] == "verify-only":
|
|
425
|
-
outputs[devlyn / "external-diff.patch"] = capture_external_diff(cwd, parsed["verify_ref"])
|
|
426
|
-
|
|
427
|
-
role_input = None
|
|
428
|
-
if parsed["role_config"] is not None:
|
|
429
|
-
role_module = runpy.run_path(pathlib.Path(__file__).with_name("role-config.py"))
|
|
430
|
-
requested_path = pathlib.Path(parsed["role_config"])
|
|
431
|
-
if not requested_path.is_absolute():
|
|
432
|
-
requested_path = cwd / requested_path
|
|
433
|
-
try:
|
|
434
|
-
value, pin = role_module["read_config"](requested_path, run=True)
|
|
435
|
-
except ValueError as exc:
|
|
436
|
-
block("BLOCKED:invalid-engine-config", str(exc))
|
|
437
|
-
role_input = {**pin, "value": value}
|
|
438
|
-
|
|
439
|
-
engine = parsed["engine"] or default_engine
|
|
440
|
-
engine_source = "flag" if parsed["engine"] is not None else "default"
|
|
441
|
-
now = datetime.datetime.now(datetime.timezone.utc)
|
|
442
|
-
started_at = now.strftime("%Y-%m-%dT%H:%M:%S.") + f"{now.microsecond // 1000:03d}Z"
|
|
443
|
-
run_id = now.strftime("rs-%Y%m%dT%H%M%SZ-") + secrets.token_hex(6)
|
|
444
|
-
state = {
|
|
445
|
-
"version": "3.0",
|
|
446
|
-
"role_config_input": role_input,
|
|
447
|
-
"role_no_pair": parsed["no_pair"],
|
|
448
|
-
"run_id": run_id,
|
|
449
|
-
"started_at": started_at,
|
|
450
|
-
"session_id": os.environ.get("CLAUDE_CODE_SESSION_ID"),
|
|
451
|
-
"engine": engine,
|
|
452
|
-
"engine_source": engine_source,
|
|
453
|
-
"mode": parsed["mode"],
|
|
454
|
-
"pair_verify": parsed["pair_verify"],
|
|
455
|
-
"complexity": None,
|
|
456
|
-
"risk_profile": {
|
|
457
|
-
"high_risk": False,
|
|
458
|
-
"reasons": [],
|
|
459
|
-
"risk_probes_enabled": False,
|
|
460
|
-
"risk_probes_explicit": False,
|
|
461
|
-
"pair_default_enabled": True,
|
|
462
|
-
},
|
|
463
|
-
"risk_probes_digest": None,
|
|
464
|
-
"process_evidence": None,
|
|
465
|
-
"base_ref": {
|
|
466
|
-
"branch": base_branch(cwd),
|
|
467
|
-
"sha": git_text(cwd, "rev-parse", "HEAD"),
|
|
468
|
-
},
|
|
469
|
-
"rounds": {"max_rounds": parsed["max_rounds"], "global": 0},
|
|
470
|
-
"bypasses": parsed["bypasses"],
|
|
471
|
-
"implement_passed_sha": None,
|
|
472
|
-
"source": source,
|
|
473
|
-
"criteria": [],
|
|
474
|
-
"phases": {name: None for name in PHASE_NAMES},
|
|
475
|
-
"verify": {"coverage_failed": False, "pair_trigger": None},
|
|
476
|
-
}
|
|
477
|
-
state_raw = json_bytes(state)
|
|
478
|
-
outputs[devlyn / "pipeline.state.json"] = state_raw
|
|
479
|
-
archive_prior_run(devlyn, shared_dir)
|
|
480
|
-
atomic_write_batch(outputs, writer)
|
|
481
|
-
return {
|
|
482
|
-
"ok": True,
|
|
483
|
-
"run_id": run_id,
|
|
484
|
-
"mode": parsed["mode"],
|
|
485
|
-
"source": source,
|
|
486
|
-
"state_path": ".devlyn/pipeline.state.json",
|
|
487
|
-
"state_sha256": sha256(state_raw),
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
def pathname_self_test() -> None:
|
|
493
|
-
script = pathlib.Path(__file__).resolve()
|
|
494
|
-
env = dict(os.environ, GIT_CONFIG_GLOBAL=os.devnull, GIT_CONFIG_NOSYSTEM="1")
|
|
495
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
496
|
-
for name in ("plain", "trailing ", "trailing\t", "trailing\n", "trailing\r\n"):
|
|
497
|
-
if os.name == "nt" and name != "plain":
|
|
498
|
-
print(f"SKIP POSIX-only pathname {name!r}: native Windows rejects/normalizes this name")
|
|
499
|
-
continue
|
|
500
|
-
repo = pathlib.Path(tmp).resolve() / name
|
|
501
|
-
repo.mkdir()
|
|
502
|
-
|
|
503
|
-
def git(*args):
|
|
504
|
-
return subprocess.run(["git", *args], cwd=repo, env=env,
|
|
505
|
-
check=True, capture_output=True).stdout
|
|
506
|
-
|
|
507
|
-
git("init", "-q")
|
|
508
|
-
git("-c", "user.name=Test", "-c", "user.email=test@example.com",
|
|
509
|
-
"commit", "--allow-empty", "-qm", "base")
|
|
510
|
-
for option, path in (("--show-toplevel", repo), ("--absolute-git-dir", repo / ".git")):
|
|
511
|
-
raw = git("rev-parse", option)
|
|
512
|
-
print(f"pathname {name!r} {option}: {raw!r}", flush=True)
|
|
513
|
-
assert raw == os.fsencode(path.as_posix() if os.name == "nt" else path) + b"\n", raw
|
|
514
|
-
command = [sys.executable, str(script), "pathname", "probe"]
|
|
515
|
-
proc = subprocess.run(command, cwd=repo, env=env, capture_output=True, timeout=15)
|
|
516
|
-
result = strict_json(proc.stdout)
|
|
517
|
-
assert proc.returncode == 0 and result["ok"] and not proc.stderr, (name, proc, result)
|
|
518
|
-
for option, path in (("--show-toplevel", repo), ("--absolute-git-dir", repo / ".git")):
|
|
519
|
-
assert git_text(repo, "rev-parse", option) == (path.as_posix() if os.name == "nt" else str(path))
|
|
520
|
-
lock = repo / ".git" / "devlyn-bootstrap.lock"
|
|
521
|
-
inode = lock.stat().st_ino
|
|
522
|
-
devlyn = repo / ".devlyn"
|
|
523
|
-
before = {p.relative_to(devlyn): p.read_bytes() for p in devlyn.rglob("*") if p.is_file()}
|
|
524
|
-
assert (devlyn / "goal.raw.txt").read_bytes() == b"pathname probe"
|
|
525
|
-
proc = subprocess.run(command, cwd=repo, env=env, capture_output=True, timeout=15)
|
|
526
|
-
refused = strict_json(proc.stdout)
|
|
527
|
-
assert proc.returncode == 1 and not proc.stderr, (name, proc)
|
|
528
|
-
assert refused["blocked"] == "BLOCKED:prior-run-unfinished", refused
|
|
529
|
-
assert str(repo) in refused["detail"] and result["run_id"] in refused["detail"]
|
|
530
|
-
assert {p.relative_to(devlyn): p.read_bytes() for p in devlyn.rglob("*") if p.is_file()} == before
|
|
531
|
-
assert not (devlyn / "runs").exists() and lock.stat().st_ino == inode
|
|
532
|
-
print(f"PASS admission pathname {name!r}: exact root/Gitdir, stable lock, unfinished refusal")
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
def admission_self_test() -> None:
|
|
536
|
-
import time
|
|
537
|
-
from unittest.mock import patch
|
|
538
|
-
|
|
539
|
-
shared = pathlib.Path(__file__).resolve().parent
|
|
540
|
-
script = shared / "resolve-bootstrap.py"
|
|
541
|
-
archive = runpy.run_path(shared / "archive_run.py")
|
|
542
|
-
env = dict(os.environ, GIT_CONFIG_GLOBAL=os.devnull, GIT_CONFIG_NOSYSTEM="1",
|
|
543
|
-
CLAUDE_CODE_SESSION_ID="same-admission-session")
|
|
544
|
-
spec_raw = (
|
|
545
|
-
'# Spec\n\n<!-- devlyn:verification -->\n## Verification\n\n```json\n'
|
|
546
|
-
'{"verification_commands":[{"cmd":"printf ok","stdout_contains":["ok"]}]}\n```\n'
|
|
547
|
-
).encode()
|
|
548
|
-
modes = (["goal", "A"], ["--spec", "spec.md"],
|
|
549
|
-
["--verify-only", "external.patch", "--spec", "spec.md"])
|
|
550
|
-
|
|
551
|
-
def git(repo, *args):
|
|
552
|
-
return subprocess.run(["git", *args], cwd=repo, env=env, check=True, capture_output=True).stdout
|
|
553
|
-
|
|
554
|
-
def inputs(repo):
|
|
555
|
-
(repo / "spec.md").write_bytes(spec_raw)
|
|
556
|
-
(repo / "external.patch").write_bytes(b"external patch A\x00\n")
|
|
557
|
-
|
|
558
|
-
def init(repo):
|
|
559
|
-
repo.mkdir()
|
|
560
|
-
git(repo, "init", "-q")
|
|
561
|
-
git(repo, "config", "user.email", "test@example.com")
|
|
562
|
-
git(repo, "config", "user.name", "Test")
|
|
563
|
-
(repo / "app.py").write_text("print('base')\n", encoding="utf-8")
|
|
564
|
-
git(repo, "add", "app.py")
|
|
565
|
-
git(repo, "commit", "-qm", "base")
|
|
566
|
-
inputs(repo)
|
|
567
|
-
|
|
568
|
-
def snapshot(path):
|
|
569
|
-
try:
|
|
570
|
-
pending = [(path, path.lstat())]
|
|
571
|
-
except FileNotFoundError:
|
|
572
|
-
return {}
|
|
573
|
-
result = {}
|
|
574
|
-
while pending:
|
|
575
|
-
p, metadata = pending.pop()
|
|
576
|
-
key = str(p.relative_to(path))
|
|
577
|
-
if stat.S_ISLNK(metadata.st_mode) or (
|
|
578
|
-
os.name == "nt" and metadata.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT
|
|
579
|
-
):
|
|
580
|
-
result[key] = str(p.readlink())
|
|
581
|
-
elif stat.S_ISDIR(metadata.st_mode):
|
|
582
|
-
result[key] = None
|
|
583
|
-
pending.extend((child, child.lstat()) for child in p.iterdir())
|
|
584
|
-
else:
|
|
585
|
-
result[key] = p.read_bytes()
|
|
586
|
-
return result
|
|
587
|
-
|
|
588
|
-
def cli(repo, argv, extra_env=None):
|
|
589
|
-
proc = subprocess.run([sys.executable, str(script), *argv], cwd=repo,
|
|
590
|
-
env=extra_env or env, capture_output=True, text=True, timeout=15, encoding="utf-8")
|
|
591
|
-
assert proc.stderr == "", (proc.returncode, proc.stdout, proc.stderr)
|
|
592
|
-
result = strict_json(proc.stdout)
|
|
593
|
-
assert proc.returncode == (0 if result["ok"] else 1), result
|
|
594
|
-
return result
|
|
595
|
-
|
|
596
|
-
def refusal(repo, argv, reason, extra_env=None):
|
|
597
|
-
before = snapshot(repo / ".devlyn")
|
|
598
|
-
result = cli(repo, argv, extra_env)
|
|
599
|
-
assert result.get("blocked") == reason, result
|
|
600
|
-
assert snapshot(repo / ".devlyn") == before, result
|
|
601
|
-
return result
|
|
602
|
-
|
|
603
|
-
def wait_for(predicate):
|
|
604
|
-
deadline = time.monotonic() + 15
|
|
605
|
-
while not predicate():
|
|
606
|
-
assert time.monotonic() < deadline, "admission child synchronization timed out"
|
|
607
|
-
time.sleep(0.01)
|
|
608
|
-
|
|
609
|
-
# Test-only seam around the real bootstrap and atomic writer. Both children
|
|
610
|
-
# reach the pipe barrier before flock; only the winner reaches the writer.
|
|
611
|
-
child = r"""
|
|
612
|
-
import json, os, pathlib, runpy, sys, time
|
|
613
|
-
script, repo, signals, action, barrier, argv = sys.argv[1:]
|
|
614
|
-
m = runpy.run_path(script)
|
|
615
|
-
signals = pathlib.Path(signals)
|
|
616
|
-
def hold():
|
|
617
|
-
signals.with_suffix('.inside').touch()
|
|
618
|
-
deadline = time.monotonic() + 20
|
|
619
|
-
while not signals.with_suffix('.release').exists():
|
|
620
|
-
if time.monotonic() > deadline:
|
|
621
|
-
raise TimeoutError('held admission was not released')
|
|
622
|
-
time.sleep(0.01)
|
|
623
|
-
def writer(path, raw):
|
|
624
|
-
if action in ('hold', 'before'):
|
|
625
|
-
hold()
|
|
626
|
-
m['atomic_write'](path, raw)
|
|
627
|
-
if action == 'canonical':
|
|
628
|
-
hold()
|
|
629
|
-
if action.startswith('temp:'):
|
|
630
|
-
replace = pathlib.Path.replace
|
|
631
|
-
def held_replace(path, target):
|
|
632
|
-
if path.name.startswith(action[5:] + '.tmp.'):
|
|
633
|
-
hold()
|
|
634
|
-
return replace(path, target)
|
|
635
|
-
pathlib.Path.replace = held_replace
|
|
636
|
-
signals.with_suffix('.ready').touch()
|
|
637
|
-
if barrier.startswith('file:'):
|
|
638
|
-
deadline = time.monotonic() + 20
|
|
639
|
-
while not pathlib.Path(barrier[5:]).exists():
|
|
640
|
-
if time.monotonic() > deadline:
|
|
641
|
-
raise TimeoutError('native admission barrier was not released')
|
|
642
|
-
time.sleep(0.01)
|
|
643
|
-
elif int(barrier) >= 0:
|
|
644
|
-
os.read(int(barrier), 1)
|
|
645
|
-
os.close(int(barrier))
|
|
646
|
-
try:
|
|
647
|
-
result = m['bootstrap'](json.loads(argv), pathlib.Path(repo), pathlib.Path(script).parent, writer=writer)
|
|
648
|
-
except m['BootstrapBlocked'] as exc:
|
|
649
|
-
print(json.dumps({'ok': False, 'blocked': exc.reason, 'detail': exc.detail}))
|
|
650
|
-
sys.exit(1)
|
|
651
|
-
print(json.dumps(result))
|
|
652
|
-
"""
|
|
653
|
-
|
|
654
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
655
|
-
root = pathlib.Path(tmp).resolve()
|
|
656
|
-
repo = root / "state"
|
|
657
|
-
init(repo)
|
|
658
|
-
first = cli(repo, modes[0])
|
|
659
|
-
devlyn = repo / ".devlyn"
|
|
660
|
-
state_path = devlyn / "pipeline.state.json"
|
|
661
|
-
initial = strict_json(state_path.read_text(encoding="utf-8"))
|
|
662
|
-
(devlyn / "engines.json").write_bytes(b'{"executor":"codex"}\n')
|
|
663
|
-
(devlyn / "config.json").write_bytes(b'{"setting":true}\n')
|
|
664
|
-
(devlyn / "unrelated.data").write_bytes(b"keep\n")
|
|
665
|
-
old_archive = devlyn / "runs" / "unfinished-old" / "pipeline.state.json"
|
|
666
|
-
old_archive.parent.mkdir(parents=True)
|
|
667
|
-
old_archive.write_bytes(b'{"run_id":"unfinished-old","phases":{}}\n')
|
|
668
|
-
for argv in modes:
|
|
669
|
-
result = refusal(repo, argv, "BLOCKED:prior-run-unfinished")
|
|
670
|
-
assert first["run_id"] in result["detail"] and str(repo) in result["detail"]
|
|
671
|
-
invalid = [None, [], "PASS", {}, {"verdict": "PASS"},
|
|
672
|
-
{"completed_at": "2026-09-06T00:00:00Z"}]
|
|
673
|
-
invalid += [{"verdict": v, "completed_at": "2026-09-06T00:00:00Z"}
|
|
674
|
-
for v in (None, [], {}, "UNKNOWN", "FAIL")]
|
|
675
|
-
invalid += [{"verdict": "PASS", "completed_at": t} for t in (
|
|
676
|
-
None, [], "", "2026-09-06T00:00:00", "2026-02-29T00:00:00Z",
|
|
677
|
-
"2026-09-06T24:00:00Z", "2026-09-06T00:00:00.1Z",
|
|
678
|
-
"2026-09-06T00:00:00+00:00",
|
|
679
|
-
)]
|
|
680
|
-
for final in invalid:
|
|
681
|
-
state = dict(initial, phases={"final_report": final, "verify": {"verdict": "PASS"}})
|
|
682
|
-
state_path.write_bytes(json_bytes(state))
|
|
683
|
-
refusal(repo, modes[0], "BLOCKED:prior-run-unfinished")
|
|
684
|
-
for verdict in ("PASS", "PASS_WITH_ISSUES", "NEEDS_WORK", "BLOCKED"):
|
|
685
|
-
for timestamp in ("2024-02-29T23:59:59Z", "2024-02-29T23:59:59.123Z"):
|
|
686
|
-
state = strict_json(state_path.read_text(encoding="utf-8"))
|
|
687
|
-
state["phases"]["final_report"] = {"verdict": verdict, "completed_at": timestamp}
|
|
688
|
-
state_path.write_bytes(json_bytes(state))
|
|
689
|
-
raw = state_path.read_bytes()
|
|
690
|
-
goal = (devlyn / "goal.raw.txt").read_bytes()
|
|
691
|
-
assert cli(repo, modes[0])["ok"]
|
|
692
|
-
archived = devlyn / "runs" / state["run_id"]
|
|
693
|
-
assert (archived / "pipeline.state.json").read_bytes() == raw
|
|
694
|
-
assert (archived / "goal.raw.txt").read_bytes() == goal
|
|
695
|
-
assert (devlyn / "engines.json").read_bytes() == b'{"executor":"codex"}\n'
|
|
696
|
-
assert (devlyn / "config.json").read_bytes() == b'{"setting":true}\n'
|
|
697
|
-
assert (devlyn / "unrelated.data").read_bytes() == b"keep\n"
|
|
698
|
-
assert old_archive.read_bytes() == b'{"run_id":"unfinished-old","phases":{}}\n'
|
|
699
|
-
for raw in (None, b"{", b"null", b'{"run_id":[]}', b'{"run_id":"../escape"}'):
|
|
700
|
-
if raw is None:
|
|
701
|
-
state_path.unlink()
|
|
702
|
-
else:
|
|
703
|
-
state_path.write_bytes(raw)
|
|
704
|
-
refusal(repo, modes[0], "BLOCKED:prior-run-ownership-unverified")
|
|
705
|
-
# Residue also blocks when the canonical report otherwise looks completed.
|
|
706
|
-
for pattern in archive["BOOTSTRAP_TEMP_PATTERNS"]:
|
|
707
|
-
state = dict(initial, phases={"final_report": {
|
|
708
|
-
"verdict": "PASS", "completed_at": "2026-09-06T00:00:00Z",
|
|
709
|
-
}})
|
|
710
|
-
state_path.write_bytes(json_bytes(state))
|
|
711
|
-
residue = devlyn / pattern.replace("*", "interrupted")
|
|
712
|
-
residue.write_bytes(b"partial")
|
|
713
|
-
for argv in modes:
|
|
714
|
-
refusal(repo, argv, "BLOCKED:prior-run-unfinished")
|
|
715
|
-
residue.unlink()
|
|
716
|
-
print("PASS admission state: same-session/all-mode byte stability, 8 completed forms, ownership/residue refusal")
|
|
717
|
-
|
|
718
|
-
identity = root / "identity"
|
|
719
|
-
init(identity)
|
|
720
|
-
alias = root / "alias"
|
|
721
|
-
def directory_link(link, target):
|
|
722
|
-
if os.name == "nt":
|
|
723
|
-
subprocess.run(["cmd.exe", "/d", "/c", "mklink", "/J", str(link), str(target)],
|
|
724
|
-
check=True, capture_output=True)
|
|
725
|
-
else:
|
|
726
|
-
link.symlink_to(target, target_is_directory=True)
|
|
727
|
-
directory_link(alias, identity)
|
|
728
|
-
sub = identity / "sub"
|
|
729
|
-
sub.mkdir()
|
|
730
|
-
detail = refusal(sub, ["--goal-file", "relative.txt"], "BLOCKED:worktree-root-required")["detail"]
|
|
731
|
-
assert str(identity) in detail and "Retry" in detail
|
|
732
|
-
assert not (identity / ".devlyn").exists()
|
|
733
|
-
external = root / "external"
|
|
734
|
-
init(external)
|
|
735
|
-
external_before = snapshot(external)
|
|
736
|
-
for key in ("GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR"):
|
|
737
|
-
for value in ("", str(external / ".git")):
|
|
738
|
-
for argv in modes:
|
|
739
|
-
result = refusal(identity, argv, "BLOCKED:git-env-redirect", dict(env, **{key: value}))
|
|
740
|
-
assert key in result["detail"] and str(identity) in result["detail"]
|
|
741
|
-
assert snapshot(external) == external_before
|
|
742
|
-
# Even a non-repository location must reject Git redirection before Git.
|
|
743
|
-
assert cli(root, modes[0], dict(env, GIT_DIR=""))["blocked"] == "BLOCKED:git-env-redirect"
|
|
744
|
-
for relative in (".devlyn", ".devlyn/runs"):
|
|
745
|
-
for dangling in (False, True):
|
|
746
|
-
target = root / ("missing-target" if dangling else "redirect-target")
|
|
747
|
-
if not dangling:
|
|
748
|
-
target.mkdir(exist_ok=True)
|
|
749
|
-
(target / "keep").write_bytes(b"untouched")
|
|
750
|
-
link = identity / relative
|
|
751
|
-
link.parent.mkdir(exist_ok=True)
|
|
752
|
-
directory_link(link, target)
|
|
753
|
-
before = snapshot(target)
|
|
754
|
-
assert before == ({} if dangling else {".": None, "keep": b"untouched"})
|
|
755
|
-
expected = {".": str(link.readlink())} if relative == ".devlyn" else {
|
|
756
|
-
".": None, "runs": str(link.readlink()),
|
|
757
|
-
}
|
|
758
|
-
assert snapshot(identity / ".devlyn") == expected
|
|
759
|
-
result = refusal(identity, modes[0], "BLOCKED:devlyn-path-redirect")
|
|
760
|
-
assert str(link) in result["detail"] and link.resolve() == target and (os.name == "nt" or link.is_symlink())
|
|
761
|
-
assert snapshot(target) == before
|
|
762
|
-
link.rmdir() if os.name == "nt" else link.unlink()
|
|
763
|
-
print("PASS admission identity: physical-root guidance, Git redirects including empty, live/dangling directory links")
|
|
764
|
-
|
|
765
|
-
def launch(repo, signals, action, argv, barrier=-1):
|
|
766
|
-
return subprocess.Popen(
|
|
767
|
-
[sys.executable, "-c", child, str(script), str(repo), str(signals),
|
|
768
|
-
action, str(barrier), json.dumps(argv)],
|
|
769
|
-
cwd=repo, env=env, **({} if os.name == "nt" else {"pass_fds": (() if barrier < 0 else (barrier,))}),
|
|
770
|
-
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
|
771
|
-
encoding="utf-8",
|
|
772
|
-
)
|
|
773
|
-
|
|
774
|
-
def receipt(proc):
|
|
775
|
-
stdout, stderr = proc.communicate(timeout=15)
|
|
776
|
-
assert stderr == "", (proc.returncode, stdout, stderr)
|
|
777
|
-
result = strict_json(stdout)
|
|
778
|
-
assert proc.returncode == (0 if result["ok"] else 1), result
|
|
779
|
-
return result
|
|
780
|
-
|
|
781
|
-
for i, argv in enumerate(modes):
|
|
782
|
-
race = root / f"race-{i}"
|
|
783
|
-
init(race)
|
|
784
|
-
(race / "spec-b.md").write_bytes(spec_raw.replace(b"# Spec", b"# Other spec"))
|
|
785
|
-
(race / "external-b.patch").write_bytes(b"external patch B\n")
|
|
786
|
-
other = ["goal", "B"] if i == 0 else [s.replace("spec.md", "spec-b.md").replace(
|
|
787
|
-
"external.patch", "external-b.patch") for s in argv]
|
|
788
|
-
signals = [root / f"race-{i}-{label}" for label in ("A", "B")]
|
|
789
|
-
if os.name == "nt":
|
|
790
|
-
barrier_file = root / f"race-{i}.barrier"
|
|
791
|
-
rfd = "file:" + str(barrier_file)
|
|
792
|
-
else:
|
|
793
|
-
rfd, wfd = os.pipe()
|
|
794
|
-
procs = []
|
|
795
|
-
try:
|
|
796
|
-
procs = [launch(race, signal, "hold", args, rfd)
|
|
797
|
-
for signal, args in zip(signals, (argv, other))]
|
|
798
|
-
if os.name != "nt":
|
|
799
|
-
os.close(rfd)
|
|
800
|
-
wait_for(lambda: all(s.with_suffix(".ready").exists() for s in signals))
|
|
801
|
-
if os.name == "nt":
|
|
802
|
-
barrier_file.touch()
|
|
803
|
-
else:
|
|
804
|
-
os.close(wfd)
|
|
805
|
-
wait_for(lambda: any(s.with_suffix(".inside").exists() for s in signals)
|
|
806
|
-
and any(p.poll() is not None for p in procs))
|
|
807
|
-
holders = [j for j, s in enumerate(signals) if s.with_suffix(".inside").exists()]
|
|
808
|
-
assert len(holders) == 1, holders
|
|
809
|
-
winner = holders[0]
|
|
810
|
-
loser = receipt(procs[1 - winner])
|
|
811
|
-
assert loser.get("blocked") == "BLOCKED:bootstrap-contended", loser
|
|
812
|
-
assert str(race) in loser["detail"] and "run_id" not in loser
|
|
813
|
-
assert snapshot(race / ".devlyn") == {}, "loser mutated owned/archive state"
|
|
814
|
-
signals[winner].with_suffix(".release").touch()
|
|
815
|
-
won = receipt(procs[winner])
|
|
816
|
-
assert won["ok"]
|
|
817
|
-
finally:
|
|
818
|
-
for proc in procs:
|
|
819
|
-
if proc.poll() is None:
|
|
820
|
-
proc.kill()
|
|
821
|
-
proc.communicate()
|
|
822
|
-
state_raw = (race / ".devlyn/pipeline.state.json").read_bytes()
|
|
823
|
-
state = strict_json(state_raw.decode())
|
|
824
|
-
assert state["run_id"] == won["run_id"] and sha256(state_raw) == won["state_sha256"]
|
|
825
|
-
expected_files = {"pipeline.state.json"}
|
|
826
|
-
if i == 0:
|
|
827
|
-
goal = (race / ".devlyn/goal.raw.txt").read_bytes()
|
|
828
|
-
assert goal == (b"goal A" if winner == 0 else b"goal B")
|
|
829
|
-
assert sha256(goal) == state["source"]["goal_sha256"]
|
|
830
|
-
expected_files.add("goal.raw.txt")
|
|
831
|
-
else:
|
|
832
|
-
source_path = "spec.md" if winner == 0 else "spec-b.md"
|
|
833
|
-
assert state["source"]["spec_path"] == source_path
|
|
834
|
-
assert state["source"]["spec_sha256"] == sha256((race / source_path).read_bytes())
|
|
835
|
-
expected_files.add("spec-verify.json")
|
|
836
|
-
if i == 2:
|
|
837
|
-
patch_path = "external.patch" if winner == 0 else "external-b.patch"
|
|
838
|
-
assert (race / ".devlyn/external-diff.patch").read_bytes() == (race / patch_path).read_bytes()
|
|
839
|
-
expected_files.add("external-diff.patch")
|
|
840
|
-
assert {p.name for p in (race / ".devlyn").iterdir()} == expected_files
|
|
841
|
-
for attempt in modes:
|
|
842
|
-
refusal(race, attempt, "BLOCKED:prior-run-unfinished")
|
|
843
|
-
print("PASS admission processes: pre-lock synchronized pair in all 3 modes, one winner, zero loser delta, unmixed outputs")
|
|
844
|
-
|
|
845
|
-
linked = root / "linked"
|
|
846
|
-
git(identity, "worktree", "add", "-qb", "independent", str(linked))
|
|
847
|
-
inputs(linked)
|
|
848
|
-
lock_path = pathlib.Path(git(identity, "rev-parse", "--absolute-git-dir").decode().strip()) / "devlyn-bootstrap.lock"
|
|
849
|
-
with admission_lock(identity):
|
|
850
|
-
inode = lock_path.stat().st_ino
|
|
851
|
-
assert cli(linked, modes[0])["ok"]
|
|
852
|
-
refusal(alias, modes[0], "BLOCKED:bootstrap-contended")
|
|
853
|
-
assert cli(alias, modes[0])["ok"]
|
|
854
|
-
assert lock_path.stat().st_ino == inode
|
|
855
|
-
assert pathlib.Path(git(linked, "rev-parse", "--absolute-git-dir").decode().strip()) != lock_path.parent
|
|
856
|
-
collision = subprocess.run(["git", "worktree", "add", str(root / "collision"), "independent"],
|
|
857
|
-
cwd=identity, env=env, capture_output=True)
|
|
858
|
-
assert collision.returncode != 0 and b"already" in collision.stderr
|
|
859
|
-
assert not (root / "collision").exists()
|
|
860
|
-
print("PASS admission worktrees: linked root initializes under other's lock; aliases exclude; branch collision retained")
|
|
861
|
-
|
|
862
|
-
failure = root / "failure"
|
|
863
|
-
init(failure)
|
|
864
|
-
def fail_writer(path, raw):
|
|
865
|
-
raise OSError("before-first-write")
|
|
866
|
-
try:
|
|
867
|
-
bootstrap(modes[0], failure, shared, writer=fail_writer)
|
|
868
|
-
except OSError as exc:
|
|
869
|
-
assert str(exc) == "before-first-write"
|
|
870
|
-
else:
|
|
871
|
-
raise AssertionError("writer exception was hidden")
|
|
872
|
-
assert not (failure / ".devlyn").exists()
|
|
873
|
-
failure_lock = failure / ".git/devlyn-bootstrap.lock"
|
|
874
|
-
failure_inode = failure_lock.stat().st_ino
|
|
875
|
-
module = __import__("msvcrt" if os.name == "nt" else "fcntl")
|
|
876
|
-
operation = "locking" if os.name == "nt" else "flock"
|
|
877
|
-
for replacement in (patch.dict(sys.modules, {module.__name__: None}),
|
|
878
|
-
patch.object(module, operation, side_effect=OSError("unsupported locking"))):
|
|
879
|
-
with replacement:
|
|
880
|
-
try:
|
|
881
|
-
bootstrap(modes[0], failure, shared)
|
|
882
|
-
except BootstrapBlocked as exc:
|
|
883
|
-
assert exc.reason == "BLOCKED:bootstrap-lock-unavailable" and str(failure) in exc.detail
|
|
884
|
-
else:
|
|
885
|
-
raise AssertionError("unavailable locking was accepted")
|
|
886
|
-
assert not (failure / ".devlyn").exists()
|
|
887
|
-
assert failure_lock.stat().st_ino == failure_inode
|
|
888
|
-
denied = root / "lock-open-failure"
|
|
889
|
-
init(denied)
|
|
890
|
-
(denied / ".git/devlyn-bootstrap.lock").mkdir()
|
|
891
|
-
refusal(denied, modes[0], "BLOCKED:bootstrap-lock-unavailable")
|
|
892
|
-
assert cli(failure, modes[0])["ok"] and failure_lock.stat().st_ino == failure_inode
|
|
893
|
-
print("PASS admission failures: before-write exception/retry; explicit import/open/acquisition refusal; persistent inode")
|
|
894
|
-
|
|
895
|
-
deaths = [("before", modes[0], None), ("canonical", modes[0], "goal.raw.txt")]
|
|
896
|
-
deaths += [("temp:" + name, modes[mode], name + ".tmp.*") for name, mode in (
|
|
897
|
-
("goal.raw.txt", 0), ("spec-verify.json", 1), ("external-diff.patch", 2), ("pipeline.state.json", 0),
|
|
898
|
-
)]
|
|
899
|
-
for i, (action, argv, family) in enumerate(deaths):
|
|
900
|
-
dead = root / f"death-{i}"
|
|
901
|
-
init(dead)
|
|
902
|
-
signal = root / f"death-{i}"
|
|
903
|
-
proc = launch(dead, signal, action, argv)
|
|
904
|
-
try:
|
|
905
|
-
wait_for(lambda: signal.with_suffix(".inside").exists() or proc.poll() is not None)
|
|
906
|
-
assert signal.with_suffix(".inside").exists(), proc.communicate()
|
|
907
|
-
inode = (dead / ".git/devlyn-bootstrap.lock").stat().st_ino
|
|
908
|
-
proc.kill()
|
|
909
|
-
stdout, stderr = proc.communicate(timeout=15)
|
|
910
|
-
assert proc.returncode == (1 if os.name == "nt" else -9) and stdout == "" and stderr == "", (proc.returncode, stdout, stderr)
|
|
911
|
-
finally:
|
|
912
|
-
if proc.poll() is None:
|
|
913
|
-
proc.kill()
|
|
914
|
-
proc.communicate()
|
|
915
|
-
if family is None:
|
|
916
|
-
assert not (dead / ".devlyn").exists()
|
|
917
|
-
assert cli(dead, modes[1])["ok"]
|
|
918
|
-
else:
|
|
919
|
-
assert len(list((dead / ".devlyn").glob(family))) == 1
|
|
920
|
-
for attempt in modes:
|
|
921
|
-
refusal(dead, attempt, "BLOCKED:prior-run-ownership-unverified")
|
|
922
|
-
assert (dead / ".git/devlyn-bootstrap.lock").stat().st_ino == inode
|
|
923
|
-
print("PASS admission death: SIGKILL before write retries; canonical and all 4 real atomic temp families preserved across modes")
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
def self_test() -> int:
|
|
927
|
-
script_shared = pathlib.Path(__file__).resolve().parent
|
|
928
|
-
try:
|
|
929
|
-
strict_json('{"run_id":"a","run_id":"b"}')
|
|
930
|
-
except ValueError as exc:
|
|
931
|
-
assert "duplicate JSON key" in str(exc)
|
|
932
|
-
else:
|
|
933
|
-
raise AssertionError("duplicate bootstrap state key was accepted")
|
|
934
|
-
|
|
935
|
-
def init_repo(path: pathlib.Path) -> None:
|
|
936
|
-
path.mkdir()
|
|
937
|
-
subprocess.run(["git", "init", "-q"], cwd=path, check=True)
|
|
938
|
-
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=path, check=True)
|
|
939
|
-
subprocess.run(["git", "config", "user.name", "Test"], cwd=path, check=True)
|
|
940
|
-
(path / "app.py").write_text("print('base')\n", encoding="utf-8")
|
|
941
|
-
subprocess.run(["git", "add", "app.py"], cwd=path, check=True)
|
|
942
|
-
subprocess.run(["git", "commit", "-qm", "base"], cwd=path, check=True)
|
|
943
|
-
|
|
944
|
-
def snapshot(path: pathlib.Path) -> dict[str, bytes]:
|
|
945
|
-
return {
|
|
946
|
-
str(file.relative_to(path)): file.read_bytes()
|
|
947
|
-
for file in path.rglob("*")
|
|
948
|
-
if file.is_file()
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
def complete_prior(path: pathlib.Path) -> None:
|
|
952
|
-
state_path = path / ".devlyn" / "pipeline.state.json"
|
|
953
|
-
state = strict_json(state_path.read_text(encoding="utf-8"))
|
|
954
|
-
state["phases"]["final_report"] = {
|
|
955
|
-
"verdict": "PASS", "completed_at": "2026-09-06T00:00:00Z",
|
|
956
|
-
}
|
|
957
|
-
state_path.write_bytes(json_bytes(state))
|
|
958
|
-
|
|
959
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
960
|
-
root = pathlib.Path(tmp)
|
|
961
|
-
role_work = root / "role-repo"
|
|
962
|
-
init_repo(role_work)
|
|
963
|
-
role_file = root / "roles.json"
|
|
964
|
-
for raw in (b"{", b'{"executor":"codex"}', b'{"roles":null}'):
|
|
965
|
-
role_file.write_bytes(raw)
|
|
966
|
-
try:
|
|
967
|
-
bootstrap(["--role-config", str(role_file), "fix app.py"], role_work, script_shared)
|
|
968
|
-
except BootstrapBlocked as exc:
|
|
969
|
-
assert exc.reason == "BLOCKED:invalid-engine-config"
|
|
970
|
-
else:
|
|
971
|
-
raise AssertionError("invalid per-run roles admitted")
|
|
972
|
-
assert not (role_work / ".devlyn/pipeline.state.json").exists()
|
|
973
|
-
role_raw = b'{"roles":{"worker":{"engine":"codex","model":"gpt-6-astra"}}}'
|
|
974
|
-
role_file.write_bytes(role_raw)
|
|
975
|
-
bootstrap(["--role-config", str(role_file), "--no-pair", "fix app.py"], role_work, script_shared)
|
|
976
|
-
staged_role = strict_json((role_work / ".devlyn/pipeline.state.json").read_text(encoding="utf-8"))
|
|
977
|
-
role_file.write_text("{}", encoding="utf-8")
|
|
978
|
-
assert staged_role["role_config_input"]["sha256"] == sha256(role_raw)
|
|
979
|
-
assert staged_role["role_config_input"]["value"] == strict_json(role_raw.decode())
|
|
980
|
-
assert staged_role["role_no_pair"] is True
|
|
981
|
-
work = root / "repo"
|
|
982
|
-
init_repo(work)
|
|
983
|
-
session_key = "CLAUDE_CODE_SESSION_ID"
|
|
984
|
-
session_present = session_key in os.environ
|
|
985
|
-
prior_session = os.environ.pop(session_key, None)
|
|
986
|
-
result = bootstrap(["fix", "app.py", "failing", "test"], work, script_shared)
|
|
987
|
-
state_path = work / ".devlyn" / "pipeline.state.json"
|
|
988
|
-
state = strict_json(state_path.read_text(encoding="utf-8"))
|
|
989
|
-
expected = {
|
|
990
|
-
"version": "3.0",
|
|
991
|
-
"run_id": state["run_id"],
|
|
992
|
-
"started_at": state["started_at"],
|
|
993
|
-
"session_id": None,
|
|
994
|
-
"engine": "claude",
|
|
995
|
-
"engine_source": "default",
|
|
996
|
-
"mode": "free-form",
|
|
997
|
-
"role_config_input": None,
|
|
998
|
-
"role_no_pair": False,
|
|
999
|
-
"pair_verify": False,
|
|
1000
|
-
"complexity": None,
|
|
1001
|
-
"risk_profile": {
|
|
1002
|
-
"high_risk": False,
|
|
1003
|
-
"reasons": [],
|
|
1004
|
-
"risk_probes_enabled": False,
|
|
1005
|
-
"risk_probes_explicit": False,
|
|
1006
|
-
"pair_default_enabled": True,
|
|
1007
|
-
},
|
|
1008
|
-
"risk_probes_digest": None,
|
|
1009
|
-
"process_evidence": None,
|
|
1010
|
-
"base_ref": {
|
|
1011
|
-
"branch": base_branch(work),
|
|
1012
|
-
"sha": git_text(work, "rev-parse", "HEAD"),
|
|
1013
|
-
},
|
|
1014
|
-
"rounds": {"max_rounds": 4, "global": 0},
|
|
1015
|
-
"bypasses": [],
|
|
1016
|
-
"implement_passed_sha": None,
|
|
1017
|
-
"source": {
|
|
1018
|
-
"type": "generated",
|
|
1019
|
-
"spec_path": None,
|
|
1020
|
-
"spec_sha256": None,
|
|
1021
|
-
"goal_path": ".devlyn/goal.raw.txt",
|
|
1022
|
-
"goal_sha256": sha256(b"fix app.py failing test"),
|
|
1023
|
-
"criteria_path": ".devlyn/criteria.generated.md",
|
|
1024
|
-
"criteria_sha256": None,
|
|
1025
|
-
},
|
|
1026
|
-
"criteria": [],
|
|
1027
|
-
"phases": {name: None for name in PHASE_NAMES},
|
|
1028
|
-
"verify": {"coverage_failed": False, "pair_trigger": None},
|
|
1029
|
-
}
|
|
1030
|
-
schema_dirs = [script_shared.parent / name for name in ("devlyn-resolve",)
|
|
1031
|
-
if (script_shared.parent / name).is_dir()]
|
|
1032
|
-
assert len(schema_dirs) == 1, schema_dirs
|
|
1033
|
-
schema = (schema_dirs[0] / "references" / "state-schema.md").read_text(encoding="utf-8")
|
|
1034
|
-
assert '"version": "3.0"' in schema and all(f'"{name}"' in schema for name in PHASE_NAMES)
|
|
1035
|
-
assert state_path.read_bytes() == json_bytes(expected)
|
|
1036
|
-
assert result["state_sha256"] == sha256(json_bytes(expected))
|
|
1037
|
-
assert (work / ".devlyn" / "goal.raw.txt").read_bytes() == b"fix app.py failing test"
|
|
1038
|
-
assert not (work / ".devlyn" / "untracked.baseline").exists()
|
|
1039
|
-
assert set(result) == {"ok", "run_id", "mode", "source", "state_path", "state_sha256"}
|
|
1040
|
-
print("PASS bootstrap self-test state-init byte contract: schema v3.0 exact, slim result")
|
|
1041
|
-
|
|
1042
|
-
detached = root / "detached-repo"
|
|
1043
|
-
init_repo(detached)
|
|
1044
|
-
subprocess.run(["git", "checkout", "--detach", "-q"], cwd=detached, check=True)
|
|
1045
|
-
detached_result = bootstrap(["fix", "app.py"], detached, script_shared)
|
|
1046
|
-
detached_state = strict_json((detached / ".devlyn" / "pipeline.state.json").read_text(encoding="utf-8"))
|
|
1047
|
-
assert detached_result["ok"] is True
|
|
1048
|
-
assert detached_state["base_ref"]["branch"] is None
|
|
1049
|
-
assert detached_state["base_ref"]["sha"] == git_text(detached, "rev-parse", "HEAD")
|
|
1050
|
-
print("PASS bootstrap self-test detached HEAD: branch null with exact HEAD sha")
|
|
1051
|
-
|
|
1052
|
-
complete_prior(work)
|
|
1053
|
-
os.environ[session_key] = "session-self-test"
|
|
1054
|
-
bootstrap(["--engine", "raw-engine", "--pair-verify", "fix", "app.py"], work, script_shared)
|
|
1055
|
-
stamped = strict_json(state_path.read_text(encoding="utf-8"))
|
|
1056
|
-
assert stamped["session_id"] == "session-self-test"
|
|
1057
|
-
assert stamped["engine"] == "raw-engine" and stamped["engine_source"] == "flag"
|
|
1058
|
-
assert stamped["pair_verify"] is True
|
|
1059
|
-
if session_present:
|
|
1060
|
-
assert prior_session is not None
|
|
1061
|
-
os.environ[session_key] = prior_session
|
|
1062
|
-
else:
|
|
1063
|
-
os.environ.pop(session_key)
|
|
1064
|
-
print("PASS bootstrap self-test session stamp: null-safe and env-present; engine flag passthrough")
|
|
1065
|
-
|
|
1066
|
-
invalid_flag_cases = [
|
|
1067
|
-
["--role-config"],
|
|
1068
|
-
["--role-config", "a", "--role-config", "b", "fix app.py"],
|
|
1069
|
-
["--pair-verify", "--no-pair", "fix", "app.py"],
|
|
1070
|
-
["--goal-file", "goal.txt", "--spec", "spec.md"],
|
|
1071
|
-
["--goal-file", "goal.txt", "--verify-only", "patch", "--spec", "spec.md"],
|
|
1072
|
-
["--goal-file", "goal.txt", "fix", "app.py"],
|
|
1073
|
-
["--goal-file"],
|
|
1074
|
-
["--goal-file", "a", "--goal-file", "b"],
|
|
1075
|
-
["--verify-only", "patch"],
|
|
1076
|
-
["--spec", "spec.md", "fix", "app.py"],
|
|
1077
|
-
["--spec", "a", "--spec", "b"],
|
|
1078
|
-
["--verify-only", "a", "--spec", "s", "--verify-only", "b"],
|
|
1079
|
-
["--engine"],
|
|
1080
|
-
["--engine", "claude", "--engine", "codex"],
|
|
1081
|
-
["--max-rounds", "0", "fix", "app.py"],
|
|
1082
|
-
["--max-rounds", "x", "fix", "app.py"],
|
|
1083
|
-
["--max-rounds"],
|
|
1084
|
-
["--bypass", "plan", "fix", "app.py"],
|
|
1085
|
-
["--bypass"],
|
|
1086
|
-
["--unknown", "fix", "app.py"],
|
|
1087
|
-
]
|
|
1088
|
-
for case in invalid_flag_cases:
|
|
1089
|
-
try:
|
|
1090
|
-
parse_flags(case)
|
|
1091
|
-
except BootstrapBlocked as exc:
|
|
1092
|
-
assert exc.reason == "BLOCKED:invalid-flags", (case, exc.reason)
|
|
1093
|
-
else:
|
|
1094
|
-
raise AssertionError(f"invalid flag combination accepted: {case}")
|
|
1095
|
-
print(f"PASS bootstrap self-test BLOCKED:invalid-flags: {len(invalid_flag_cases)} combinations")
|
|
1096
|
-
|
|
1097
|
-
blocked_work = root / "blocked-repo"
|
|
1098
|
-
init_repo(blocked_work)
|
|
1099
|
-
cli_blocked = subprocess.run(
|
|
1100
|
-
[sys.executable, str(pathlib.Path(__file__).resolve()), "--pair-verify", "--no-pair"],
|
|
1101
|
-
cwd=blocked_work, capture_output=True, text=True,
|
|
1102
|
-
encoding="utf-8",
|
|
1103
|
-
)
|
|
1104
|
-
assert cli_blocked.returncode == 1 and cli_blocked.stderr == ""
|
|
1105
|
-
assert strict_json(cli_blocked.stdout)["blocked"] == "BLOCKED:invalid-flags"
|
|
1106
|
-
assert not (blocked_work / ".devlyn").exists()
|
|
1107
|
-
print("PASS bootstrap self-test failure receipt: machine-only JSON, zero partial state")
|
|
1108
|
-
|
|
1109
|
-
goal_file = blocked_work / "goal.txt"
|
|
1110
|
-
goal_file.write_text(" \n", encoding="utf-8")
|
|
1111
|
-
try:
|
|
1112
|
-
bootstrap(["--goal-file", "goal.txt"], blocked_work, script_shared)
|
|
1113
|
-
except BootstrapBlocked as exc:
|
|
1114
|
-
assert exc.reason == "BLOCKED:goal-file-empty"
|
|
1115
|
-
else:
|
|
1116
|
-
raise AssertionError("empty goal file accepted")
|
|
1117
|
-
try:
|
|
1118
|
-
bootstrap(["--goal-file", "missing.txt"], blocked_work, script_shared)
|
|
1119
|
-
except BootstrapBlocked as exc:
|
|
1120
|
-
assert exc.reason == "BLOCKED:goal-file-unreadable"
|
|
1121
|
-
else:
|
|
1122
|
-
raise AssertionError("missing goal file accepted")
|
|
1123
|
-
try:
|
|
1124
|
-
bootstrap(["--goal-file", "../outside.txt"], blocked_work, script_shared)
|
|
1125
|
-
except BootstrapBlocked as exc:
|
|
1126
|
-
assert exc.reason == "BLOCKED:goal-file-invalid-path"
|
|
1127
|
-
else:
|
|
1128
|
-
raise AssertionError("escaping goal path accepted")
|
|
1129
|
-
assert not (blocked_work / ".devlyn").exists()
|
|
1130
|
-
print("PASS bootstrap self-test goal-file BLOCKED paths: empty/unreadable/invalid-path, zero partial state")
|
|
1131
|
-
|
|
1132
|
-
goal_raw = "fix app.py\r\npreserve café bytes\n".encode()
|
|
1133
|
-
(work / "goal.txt").write_bytes(goal_raw)
|
|
1134
|
-
complete_prior(work)
|
|
1135
|
-
goal_result = bootstrap(["--goal-file", "goal.txt"], work, script_shared)
|
|
1136
|
-
assert goal_result["source"]["goal_sha256"] == sha256(goal_raw)
|
|
1137
|
-
assert (work / ".devlyn" / "goal.raw.txt").read_bytes() == goal_raw
|
|
1138
|
-
print("PASS bootstrap self-test goal source: exact bytes and sha256")
|
|
1139
|
-
|
|
1140
|
-
atomic_work = root / "atomic-repo"
|
|
1141
|
-
init_repo(atomic_work)
|
|
1142
|
-
write_count = 0
|
|
1143
|
-
|
|
1144
|
-
def fail_second_write(path: pathlib.Path, raw: bytes) -> None:
|
|
1145
|
-
nonlocal write_count
|
|
1146
|
-
write_count += 1
|
|
1147
|
-
if write_count == 2:
|
|
1148
|
-
raise OSError("forced second-write failure")
|
|
1149
|
-
atomic_write(path, raw)
|
|
1150
|
-
|
|
1151
|
-
try:
|
|
1152
|
-
bootstrap(["fix", "app.py"], atomic_work, script_shared, writer=fail_second_write)
|
|
1153
|
-
except OSError as exc:
|
|
1154
|
-
assert str(exc) == "forced second-write failure"
|
|
1155
|
-
else:
|
|
1156
|
-
raise AssertionError("forced atomic batch failure did not fail")
|
|
1157
|
-
assert not (atomic_work / ".devlyn").exists()
|
|
1158
|
-
print("PASS bootstrap self-test atomic batch rollback: zero partial filesystem state")
|
|
1159
|
-
|
|
1160
|
-
previous_state = state_path.read_bytes()
|
|
1161
|
-
try:
|
|
1162
|
-
bootstrap(["fix", "app.py"], work, root / "missing-shared")
|
|
1163
|
-
except BootstrapBlocked as exc:
|
|
1164
|
-
assert exc.reason == "BLOCKED:shared-dir-unresolved"
|
|
1165
|
-
else:
|
|
1166
|
-
raise AssertionError("missing shared directory accepted")
|
|
1167
|
-
assert state_path.read_bytes() == previous_state
|
|
1168
|
-
print("PASS bootstrap self-test BLOCKED:shared-dir-unresolved: existing state byte-stable")
|
|
1169
|
-
|
|
1170
|
-
spec_dir = work / "docs" / "specs" / "sample"
|
|
1171
|
-
spec_dir.mkdir(parents=True)
|
|
1172
|
-
spec_path = spec_dir / "spec.md"
|
|
1173
|
-
spec_path.write_text(
|
|
1174
|
-
"---\ncomplexity: medium\n---\n# Spec\n\n<!-- devlyn:verification -->\n"
|
|
1175
|
-
"## Verification\n\n```json\n{\"verification_commands\":[{\"cmd\":\"printf ok\",\"stdout_contains\":[\"ok\"]}]}\n```\n",
|
|
1176
|
-
encoding="utf-8",
|
|
1177
|
-
)
|
|
1178
|
-
spec_raw = spec_path.read_bytes()
|
|
1179
|
-
external_patch = work / ".devlyn" / "external-diff.patch"
|
|
1180
|
-
external_patch.write_bytes(b"stale spec patch\n")
|
|
1181
|
-
complete_prior(work)
|
|
1182
|
-
spec_result = bootstrap(["--spec", str(spec_path.relative_to(work))], work, script_shared)
|
|
1183
|
-
assert not external_patch.exists()
|
|
1184
|
-
staged = strict_json((work / ".devlyn" / "spec-verify.json").read_text(encoding="utf-8"))
|
|
1185
|
-
assert staged["verification_commands"][0]["cmd"] == "printf ok"
|
|
1186
|
-
assert spec_result["source"]["spec_sha256"] == sha256(spec_raw)
|
|
1187
|
-
pure_spec = spec_dir / "design.md"
|
|
1188
|
-
pure_spec.write_text(
|
|
1189
|
-
"# Design\n\n<!-- devlyn:verification -->\n## Verification\n\n"
|
|
1190
|
-
'```json\n{"pure_design":true,"verification_commands":[]}\n```\n',
|
|
1191
|
-
encoding="utf-8",
|
|
1192
|
-
)
|
|
1193
|
-
complete_prior(work)
|
|
1194
|
-
pure_result = bootstrap(["--spec", str(pure_spec.relative_to(work))], work, script_shared)
|
|
1195
|
-
assert pure_result["source"]["spec_sha256"] == sha256(pure_spec.read_bytes())
|
|
1196
|
-
assert not (work / ".devlyn/spec-verify.json").exists()
|
|
1197
|
-
external_patch.write_bytes(b"stale free-form patch\n")
|
|
1198
|
-
complete_prior(work)
|
|
1199
|
-
bootstrap(["fresh", "goal"], work, script_shared)
|
|
1200
|
-
assert not external_patch.exists()
|
|
1201
|
-
(spec_dir / "spec.expected.json").write_text(json.dumps({
|
|
1202
|
-
"verification_commands": [{"cmd": "printf expected", "stdout_contains": ["expected"]}],
|
|
1203
|
-
}) + "\n", encoding="utf-8")
|
|
1204
|
-
complete_prior(work)
|
|
1205
|
-
bootstrap(["--spec", str(spec_path.relative_to(work))], work, script_shared)
|
|
1206
|
-
staged = strict_json((work / ".devlyn" / "spec-verify.json").read_text(encoding="utf-8"))
|
|
1207
|
-
assert staged["verification_commands"][0]["cmd"] == "printf expected"
|
|
1208
|
-
patch_raw = b"diff --git a/app.py b/app.py\nexact external bytes\x00\n"
|
|
1209
|
-
(work / "external.patch").write_bytes(patch_raw)
|
|
1210
|
-
(work / "app.py").write_text("print('dirty verify-only input')\n", encoding="utf-8")
|
|
1211
|
-
complete_prior(work)
|
|
1212
|
-
verify_result = bootstrap([
|
|
1213
|
-
"--verify-only", "external.patch", "--spec", str(spec_path.relative_to(work)),
|
|
1214
|
-
], work, script_shared)
|
|
1215
|
-
assert verify_result["mode"] == "verify-only"
|
|
1216
|
-
assert external_patch.read_bytes() == patch_raw
|
|
1217
|
-
subprocess.run(["git", "restore", "app.py"], cwd=work, check=True)
|
|
1218
|
-
# Ambient no-prefix config must not change the reader's a/b contract.
|
|
1219
|
-
(work / "app.py").write_text("print('prefix control')\n", encoding="utf-8")
|
|
1220
|
-
settings = ["diff.noprefix"] + (["diff.external"] if os.name != "nt" else [])
|
|
1221
|
-
for setting in settings:
|
|
1222
|
-
subprocess.run(["git", "config", setting, "true"], cwd=work, check=True)
|
|
1223
|
-
try:
|
|
1224
|
-
assert b"diff --git a/app.py b/app.py\n" in capture_external_diff(work, "HEAD")
|
|
1225
|
-
finally:
|
|
1226
|
-
subprocess.run(["git", "config", "--unset", setting], cwd=work, check=True)
|
|
1227
|
-
subprocess.run(["git", "restore", "app.py"], cwd=work, check=True)
|
|
1228
|
-
print("PASS bootstrap self-test patch lifecycle: full-mode removal + dirty verify-only exact capture")
|
|
1229
|
-
|
|
1230
|
-
for valid_named in (True, False):
|
|
1231
|
-
named_work = root / f"named-spec-{valid_named}"
|
|
1232
|
-
init_repo(named_work)
|
|
1233
|
-
named_spec = named_work / "X.md"
|
|
1234
|
-
named_spec.write_text(
|
|
1235
|
-
"---\ncomplexity: " + ("medium" if valid_named else "hihg") + "\n---\n# Named spec\n",
|
|
1236
|
-
encoding="utf-8",
|
|
1237
|
-
)
|
|
1238
|
-
(named_work / "spec.md").write_text(
|
|
1239
|
-
"---\ncomplexity: " + ("hihg" if valid_named else "medium") + "\n---\n# Unrelated spec\n",
|
|
1240
|
-
encoding="utf-8",
|
|
1241
|
-
)
|
|
1242
|
-
(named_work / "spec.expected.json").write_text(json.dumps({
|
|
1243
|
-
"verification_commands": [{"cmd": "printf named-contract"}],
|
|
1244
|
-
}), encoding="utf-8")
|
|
1245
|
-
try:
|
|
1246
|
-
result = bootstrap(["--spec", "X.md"], named_work, script_shared)
|
|
1247
|
-
except BootstrapBlocked as exc:
|
|
1248
|
-
assert not valid_named and exc.reason == "BLOCKED:invalid-flags", exc
|
|
1249
|
-
assert "frontmatter complexity" in exc.detail, exc.detail
|
|
1250
|
-
assert not (named_work / ".devlyn").exists()
|
|
1251
|
-
else:
|
|
1252
|
-
assert valid_named, "invalid named spec was accepted via unrelated spec.md"
|
|
1253
|
-
assert result["source"]["spec_path"] == "X.md"
|
|
1254
|
-
assert result["source"]["spec_sha256"] == sha256(named_spec.read_bytes())
|
|
1255
|
-
staged = strict_json((named_work / ".devlyn/spec-verify.json").read_text(encoding="utf-8"))
|
|
1256
|
-
assert staged["verification_commands"] == [{"cmd": "printf named-contract"}]
|
|
1257
|
-
|
|
1258
|
-
dirty_work = root / "dirty-repo"
|
|
1259
|
-
init_repo(dirty_work)
|
|
1260
|
-
bootstrap(["clean", "baseline"], dirty_work, script_shared)
|
|
1261
|
-
before_dirty = snapshot(dirty_work / ".devlyn")
|
|
1262
|
-
(dirty_work / "app.py").write_text("print('unstaged')\n", encoding="utf-8")
|
|
1263
|
-
for label in ("unstaged", "staged"):
|
|
1264
|
-
if label == "staged":
|
|
1265
|
-
subprocess.run(["git", "add", "app.py"], cwd=dirty_work, check=True)
|
|
1266
|
-
try:
|
|
1267
|
-
bootstrap(["blocked", label], dirty_work, script_shared)
|
|
1268
|
-
except BootstrapBlocked as exc:
|
|
1269
|
-
assert exc.reason == "BLOCKED:worktree-dirty"
|
|
1270
|
-
assert "commit or stash" in exc.detail.lower()
|
|
1271
|
-
else:
|
|
1272
|
-
raise AssertionError(f"{label} tracked owner change accepted")
|
|
1273
|
-
assert snapshot(dirty_work / ".devlyn") == before_dirty
|
|
1274
|
-
subprocess.run(["git", "restore", "--staged", "app.py"], cwd=dirty_work, check=True)
|
|
1275
|
-
subprocess.run(["git", "restore", "app.py"], cwd=dirty_work, check=True)
|
|
1276
|
-
|
|
1277
|
-
devlyn_work = root / "devlyn-dirty-repo"
|
|
1278
|
-
init_repo(devlyn_work)
|
|
1279
|
-
(devlyn_work / ".devlyn").mkdir()
|
|
1280
|
-
owner_file = devlyn_work / ".devlyn" / "owner.txt"
|
|
1281
|
-
owner_file.write_text("base\n", encoding="utf-8")
|
|
1282
|
-
subprocess.run(["git", "add", ".devlyn/owner.txt"], cwd=devlyn_work, check=True)
|
|
1283
|
-
subprocess.run(["git", "commit", "-qm", "track devlyn owner"], cwd=devlyn_work, check=True)
|
|
1284
|
-
owner_file.write_text("dirty allowed\n", encoding="utf-8")
|
|
1285
|
-
assert bootstrap(["devlyn", "allowed"], devlyn_work, script_shared)["ok"] is True
|
|
1286
|
-
assert owner_file.read_text(encoding="utf-8") == "dirty allowed\n"
|
|
1287
|
-
|
|
1288
|
-
untracked_work = root / "untracked-repo"
|
|
1289
|
-
init_repo(untracked_work)
|
|
1290
|
-
(untracked_work / "untracked.txt").write_text("allowed\n", encoding="utf-8")
|
|
1291
|
-
assert bootstrap(["untracked", "allowed"], untracked_work, script_shared)["ok"] is True
|
|
1292
|
-
print("PASS bootstrap self-test honest baseline: dirty owner blocked; .devlyn/untracked allowed")
|
|
1293
|
-
|
|
1294
|
-
prior_work = root / "prior-run-repo"
|
|
1295
|
-
init_repo(prior_work)
|
|
1296
|
-
prior = bootstrap(["prior", "run"], prior_work, script_shared)
|
|
1297
|
-
prior_devlyn = prior_work / ".devlyn"
|
|
1298
|
-
for name in (
|
|
1299
|
-
"implement.task-context",
|
|
1300
|
-
"implement.prompt",
|
|
1301
|
-
"implement.stdout",
|
|
1302
|
-
"implement.stderr",
|
|
1303
|
-
"implement.events.jsonl",
|
|
1304
|
-
"implement.retry.1.stdout",
|
|
1305
|
-
"verify.primary.timeout.json",
|
|
1306
|
-
):
|
|
1307
|
-
(prior_devlyn / name).write_text(f"prior {name}\n", encoding="utf-8")
|
|
1308
|
-
(prior_devlyn / "engines.json").write_text('{"executor":"codex"}\n', encoding="utf-8")
|
|
1309
|
-
(prior_devlyn / "unrelated.data").write_text("preserve\n", encoding="utf-8")
|
|
1310
|
-
before = snapshot(prior_devlyn)
|
|
1311
|
-
try:
|
|
1312
|
-
bootstrap(["replacement", "run"], prior_work, script_shared)
|
|
1313
|
-
except BootstrapBlocked as exc:
|
|
1314
|
-
assert exc.reason == "BLOCKED:prior-run-unfinished"
|
|
1315
|
-
assert prior["run_id"] in exc.detail and str(prior_work.resolve()) in exc.detail
|
|
1316
|
-
else:
|
|
1317
|
-
raise AssertionError("unfinished prior run was replaced")
|
|
1318
|
-
assert snapshot(prior_devlyn) == before
|
|
1319
|
-
complete_prior(prior_work)
|
|
1320
|
-
replacement = bootstrap(["replacement", "run"], prior_work, script_shared)
|
|
1321
|
-
prior_archive = prior_devlyn / "runs" / prior["run_id"]
|
|
1322
|
-
assert replacement["run_id"] != prior["run_id"]
|
|
1323
|
-
for name in (
|
|
1324
|
-
"pipeline.state.json",
|
|
1325
|
-
"goal.raw.txt",
|
|
1326
|
-
"implement.task-context",
|
|
1327
|
-
"implement.prompt",
|
|
1328
|
-
"implement.stdout",
|
|
1329
|
-
"implement.stderr",
|
|
1330
|
-
"implement.events.jsonl",
|
|
1331
|
-
"implement.retry.1.stdout",
|
|
1332
|
-
"verify.primary.timeout.json",
|
|
1333
|
-
):
|
|
1334
|
-
assert (prior_archive / name).is_file(), name
|
|
1335
|
-
if name not in {"pipeline.state.json", "goal.raw.txt"}:
|
|
1336
|
-
assert not (prior_devlyn / name).exists(), name
|
|
1337
|
-
assert strict_json((prior_devlyn / "pipeline.state.json").read_text(encoding="utf-8"))["run_id"] == replacement["run_id"]
|
|
1338
|
-
assert (prior_archive / "goal.raw.txt").read_bytes() == b"prior run"
|
|
1339
|
-
assert (prior_devlyn / "goal.raw.txt").read_bytes() == b"replacement run"
|
|
1340
|
-
assert (prior_devlyn / "engines.json").read_text(encoding="utf-8") == '{"executor":"codex"}\n'
|
|
1341
|
-
assert (prior_devlyn / "unrelated.data").read_text(encoding="utf-8") == "preserve\n"
|
|
1342
|
-
|
|
1343
|
-
for label, state_raw in (("missing", None), ("malformed", b"{\n")):
|
|
1344
|
-
unauthenticated = root / f"unauthenticated-{label}"
|
|
1345
|
-
init_repo(unauthenticated)
|
|
1346
|
-
unauthenticated_devlyn = unauthenticated / ".devlyn"
|
|
1347
|
-
unauthenticated_devlyn.mkdir()
|
|
1348
|
-
(unauthenticated_devlyn / "implement.stdout").write_bytes(b"owned bytes\n")
|
|
1349
|
-
if state_raw is not None:
|
|
1350
|
-
(unauthenticated_devlyn / "pipeline.state.json").write_bytes(state_raw)
|
|
1351
|
-
before = snapshot(unauthenticated_devlyn)
|
|
1352
|
-
try:
|
|
1353
|
-
bootstrap(["must", "block"], unauthenticated, script_shared)
|
|
1354
|
-
except BootstrapBlocked as exc:
|
|
1355
|
-
assert exc.reason == "BLOCKED:prior-run-ownership-unverified", exc.reason
|
|
1356
|
-
else:
|
|
1357
|
-
raise AssertionError(f"{label} prior-run ownership was accepted")
|
|
1358
|
-
assert snapshot(unauthenticated_devlyn) == before
|
|
1359
|
-
print("PASS bootstrap self-test prior-run ownership: unfinished refused, completed archived; unauthenticated bytes stable")
|
|
1360
|
-
|
|
1361
|
-
malformed_work = root / "malformed-spec-repo"
|
|
1362
|
-
init_repo(malformed_work)
|
|
1363
|
-
bad_spec = malformed_work / "spec.md"
|
|
1364
|
-
bad_spec.write_text(
|
|
1365
|
-
"# Spec\n\n<!-- devlyn:verification -->\n## Verification\n\n```json\n{broken\n```\n",
|
|
1366
|
-
encoding="utf-8",
|
|
1367
|
-
)
|
|
1368
|
-
try:
|
|
1369
|
-
bootstrap(["--spec", "spec.md"], malformed_work, script_shared)
|
|
1370
|
-
except BootstrapBlocked as exc:
|
|
1371
|
-
assert exc.reason == "BLOCKED:invalid-flags"
|
|
1372
|
-
else:
|
|
1373
|
-
raise AssertionError("malformed spec accepted")
|
|
1374
|
-
assert not (malformed_work / ".devlyn").exists()
|
|
1375
|
-
print("PASS bootstrap self-test spec validation failure: zero partial filesystem state")
|
|
1376
|
-
|
|
1377
|
-
pathname_self_test()
|
|
1378
|
-
admission_self_test()
|
|
1379
|
-
return 0
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
def main() -> int:
|
|
1383
|
-
if sys.argv[1:] == ["--self-test"]:
|
|
1384
|
-
return self_test()
|
|
1385
|
-
try:
|
|
1386
|
-
result = bootstrap(
|
|
1387
|
-
sys.argv[1:], pathlib.Path.cwd(), pathlib.Path(__file__).resolve().parent,
|
|
1388
|
-
default_engine=os.environ.get("DEVLYN_DEFAULT_ENGINE", "claude"),
|
|
1389
|
-
)
|
|
1390
|
-
except BootstrapBlocked as exc:
|
|
1391
|
-
sys.stdout.write(json.dumps({
|
|
1392
|
-
"ok": False,
|
|
1393
|
-
"blocked": exc.reason,
|
|
1394
|
-
"detail": exc.detail,
|
|
1395
|
-
}, sort_keys=True) + "\n")
|
|
1396
|
-
return 1
|
|
1397
|
-
except Exception as exc:
|
|
1398
|
-
sys.stdout.write(json.dumps({
|
|
1399
|
-
"ok": False,
|
|
1400
|
-
"blocked": "BLOCKED:init-failed",
|
|
1401
|
-
"detail": str(exc) or type(exc).__name__,
|
|
1402
|
-
}, sort_keys=True) + "\n")
|
|
1403
|
-
return 1
|
|
1404
|
-
sys.stdout.write(json.dumps(result, sort_keys=True) + "\n")
|
|
1405
|
-
return 0
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
if __name__ == "__main__":
|
|
1409
|
-
runpy.run_path(str(pathlib.Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
|
|
1410
|
-
raise SystemExit(main())
|