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.
Files changed (72) hide show
  1. package/AGENTS.md +10 -94
  2. package/CLAUDE.md +7 -160
  3. package/LICENSE +21 -0
  4. package/README.md +65 -110
  5. package/bin/devlyn.js +95 -116
  6. package/bin/instruction-templates.json +43 -2
  7. package/bin/instructions.js +29 -8
  8. package/config/skills/_shared/adapters/claude.md +8 -85
  9. package/config/skills/_shared/adapters/codex.md +14 -34
  10. package/config/skills/_shared/adapters/omp.md +1 -11
  11. package/config/skills/_shared/codex-monitored.sh +4 -54
  12. package/config/skills/_shared/engine-doctor.sh +18 -58
  13. package/config/skills/_shared/engine-preflight.md +15 -47
  14. package/config/skills/_shared/expected-contract.py +242 -0
  15. package/config/skills/_shared/expected.schema.json +10 -95
  16. package/config/skills/_shared/invocation-receipt.py +26 -757
  17. package/config/skills/_shared/platform-support.py +37 -28
  18. package/config/skills/_shared/role-config.py +100 -421
  19. package/config/skills/_shared/runtime-principles.md +17 -93
  20. package/config/skills/_shared/task-complete.py +490 -285
  21. package/config/skills/_shared/task-completion.md +235 -0
  22. package/config/skills/devlyn-engines/SKILL.md +9 -11
  23. package/config/skills/devlyn-ideate/SKILL.md +57 -107
  24. package/config/skills/devlyn-ideate/references/elicitation.md +18 -99
  25. package/config/skills/devlyn-ideate/references/loop.md +110 -0
  26. package/config/skills/devlyn-ideate/references/package-format.md +128 -0
  27. package/config/skills/devlyn-ideate/scripts/acceptance.py +795 -0
  28. package/config/skills/devlyn-ideate/scripts/queue.py +1780 -0
  29. package/package.json +2 -3
  30. package/config/commit-conventions.md +0 -28
  31. package/config/skills/_shared/adapters/README.md +0 -74
  32. package/config/skills/_shared/adapters/grok.md +0 -100
  33. package/config/skills/_shared/archive_run.py +0 -969
  34. package/config/skills/_shared/codex-config.md +0 -81
  35. package/config/skills/_shared/collect-codex-findings.py +0 -505
  36. package/config/skills/_shared/finish-gate.py +0 -521
  37. package/config/skills/_shared/grok-anchor-guard.py +0 -78
  38. package/config/skills/_shared/judge-output-parser.py +0 -200
  39. package/config/skills/_shared/judge-role-evidence.py +0 -320
  40. package/config/skills/_shared/phase-prompt-render.py +0 -183
  41. package/config/skills/_shared/process-evidence.py +0 -926
  42. package/config/skills/_shared/resolve-bootstrap.py +0 -1410
  43. package/config/skills/_shared/resolve-stop-hook.py +0 -359
  44. package/config/skills/_shared/run-bounded.py +0 -58
  45. package/config/skills/_shared/spec-verify-check.py +0 -5083
  46. package/config/skills/_shared/state-phase-write.py +0 -5989
  47. package/config/skills/_shared/terminal-claim-check.py +0 -777
  48. package/config/skills/_shared/verify-merge-findings.py +0 -3528
  49. package/config/skills/devlyn-design-ui/SKILL.md +0 -650
  50. package/config/skills/devlyn-ideate/references/from-spec-mode.md +0 -61
  51. package/config/skills/devlyn-ideate/references/project-mode.md +0 -80
  52. package/config/skills/devlyn-ideate/references/spec-template.md +0 -121
  53. package/config/skills/devlyn-ideate/references/templates/decision.md +0 -48
  54. package/config/skills/devlyn-ideate/references/templates/roadmap.md +0 -50
  55. package/config/skills/devlyn-ideate/references/templates/vision.md +0 -44
  56. package/config/skills/devlyn-queue/SKILL.md +0 -66
  57. package/config/skills/devlyn-queue/scripts/append.py +0 -94
  58. package/config/skills/devlyn-resolve/SKILL.md +0 -379
  59. package/config/skills/devlyn-resolve/references/free-form-mode.md +0 -71
  60. package/config/skills/devlyn-resolve/references/outer-loop.md +0 -23
  61. package/config/skills/devlyn-resolve/references/phases/build-gate.md +0 -75
  62. package/config/skills/devlyn-resolve/references/phases/cleanup.md +0 -33
  63. package/config/skills/devlyn-resolve/references/phases/implement.md +0 -44
  64. package/config/skills/devlyn-resolve/references/phases/plan.md +0 -39
  65. package/config/skills/devlyn-resolve/references/phases/probe-derive.md +0 -261
  66. package/config/skills/devlyn-resolve/references/phases/surface-close.md +0 -34
  67. package/config/skills/devlyn-resolve/references/phases/verify.md +0 -310
  68. package/config/skills/devlyn-resolve/references/state-schema.md +0 -152
  69. package/config/skills/devlyn-resolve/references/task-completion.md +0 -160
  70. package/config/templates/prompt-templates.md +0 -71
  71. package/config/templates/template-feature.spec.md +0 -255
  72. package/config/templates/template-product-spec.md +0 -680
@@ -1,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())