devlyn-cli 3.1.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +6 -1
  2. package/bin/devlyn.js +69 -23
  3. package/config/skills/_shared/adapters/claude.md +3 -2
  4. package/config/skills/_shared/adapters/codex.md +1 -1
  5. package/config/skills/_shared/archive_run.py +5 -3
  6. package/config/skills/_shared/codex-config.md +13 -8
  7. package/config/skills/_shared/codex-monitored.sh +12 -19
  8. package/config/skills/_shared/collect-codex-findings.py +1 -0
  9. package/config/skills/_shared/engine-preflight.md +2 -0
  10. package/config/skills/_shared/finish-gate.py +4 -1
  11. package/config/skills/_shared/invocation-receipt.py +194 -5
  12. package/config/skills/_shared/judge-role-evidence.py +33 -6
  13. package/config/skills/_shared/phase-prompt-render.py +2 -0
  14. package/config/skills/_shared/platform-support.py +370 -0
  15. package/config/skills/_shared/process-evidence.py +4 -2
  16. package/config/skills/_shared/resolve-bootstrap.py +102 -59
  17. package/config/skills/_shared/resolve-stop-hook.py +3 -1
  18. package/config/skills/_shared/role-config.py +7 -5
  19. package/config/skills/_shared/run-bounded.py +35 -21
  20. package/config/skills/_shared/spec-verify-check.py +274 -157
  21. package/config/skills/_shared/state-phase-write.py +26 -9
  22. package/config/skills/_shared/task-complete.py +87 -83
  23. package/config/skills/_shared/verify-merge-findings.py +9 -8
  24. package/config/skills/devlyn:resolve/SKILL.md +10 -7
  25. package/config/skills/devlyn:resolve/references/phases/verify.md +5 -3
  26. package/config/skills/devlyn:resolve/references/task-completion.md +1 -1
  27. package/package.json +1 -1
  28. package/scripts/lint-skills.sh +6 -5
@@ -252,7 +252,7 @@ def safe_goal_file(cwd: pathlib.Path, raw_path: str) -> bytes:
252
252
 
253
253
 
254
254
  def run_checked(command: list[str], cwd: pathlib.Path) -> None:
255
- proc = subprocess.run(command, cwd=cwd, capture_output=True, text=True)
255
+ proc = subprocess.run(command, cwd=cwd, capture_output=True, text=True, encoding="utf-8")
256
256
  if proc.returncode != 0:
257
257
  detail = (proc.stderr or proc.stdout).strip() or "command failed"
258
258
  block("BLOCKED:invalid-flags", detail)
@@ -320,6 +320,7 @@ def base_branch(cwd: pathlib.Path) -> str | None:
320
320
  cwd=cwd,
321
321
  capture_output=True,
322
322
  text=True,
323
+ encoding="utf-8",
323
324
  )
324
325
  if proc.returncode == 1 and not proc.stdout.strip() and not proc.stderr.strip():
325
326
  return None
@@ -363,24 +364,18 @@ def require_clean_tracked_baseline(cwd: pathlib.Path) -> None:
363
364
  @contextlib.contextmanager
364
365
  def admission_lock(cwd: pathlib.Path):
365
366
  lock_path = pathlib.Path(git_text(cwd, "rev-parse", "--absolute-git-dir")) / "devlyn-bootstrap.lock"
366
- try:
367
- import fcntl
368
- handle = lock_path.open("a+b")
369
- except (ImportError, OSError) as exc:
370
- block("BLOCKED:bootstrap-lock-unavailable", f"{cwd}: {lock_path}: {exc}")
371
- # Closing releases flock on every exit; keep the inode for the next caller.
372
- with handle:
367
+ with contextlib.ExitStack() as stack:
373
368
  try:
374
- fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
369
+ stack.enter_context(runpy.run_path(pathlib.Path(__file__).with_name("platform-support.py"))["file_lock"](lock_path))
375
370
  except BlockingIOError:
376
371
  block(
377
372
  "BLOCKED:bootstrap-contended",
378
373
  f"Bootstrap admission is occupied at {cwd}. Retry from this root after admission finishes.",
379
374
  )
380
- except (OSError, AttributeError) as exc:
375
+ except (ImportError, OSError, AttributeError) as exc:
381
376
  block("BLOCKED:bootstrap-lock-unavailable", f"{cwd}: {lock_path}: {exc}")
382
377
  for path in (cwd / ".devlyn", cwd / ".devlyn" / "runs"):
383
- if path.is_symlink() or (path.exists() and not path.is_dir()):
378
+ if path.is_symlink() or path.resolve() != path or (path.exists() and not path.is_dir()):
384
379
  block("BLOCKED:devlyn-path-redirect", f"{cwd}: require an unredirected directory at {path}")
385
380
  yield
386
381
 
@@ -502,6 +497,9 @@ def pathname_self_test() -> None:
502
497
  env = dict(os.environ, GIT_CONFIG_GLOBAL=os.devnull, GIT_CONFIG_NOSYSTEM="1")
503
498
  with tempfile.TemporaryDirectory() as tmp:
504
499
  for name in ("plain", "trailing ", "trailing\t", "trailing\n", "trailing\r\n"):
500
+ if os.name == "nt" and name != "plain":
501
+ print(f"SKIP POSIX-only pathname {name!r}: native Windows rejects/normalizes this name")
502
+ continue
505
503
  repo = pathlib.Path(tmp).resolve() / name
506
504
  repo.mkdir()
507
505
 
@@ -515,13 +513,13 @@ def pathname_self_test() -> None:
515
513
  for option, path in (("--show-toplevel", repo), ("--absolute-git-dir", repo / ".git")):
516
514
  raw = git("rev-parse", option)
517
515
  print(f"pathname {name!r} {option}: {raw!r}", flush=True)
518
- assert raw == os.fsencode(path) + b"\n", raw
516
+ assert raw == os.fsencode(path.as_posix() if os.name == "nt" else path) + b"\n", raw
519
517
  command = [sys.executable, str(script), "pathname", "probe"]
520
518
  proc = subprocess.run(command, cwd=repo, env=env, capture_output=True, timeout=15)
521
519
  result = strict_json(proc.stdout)
522
520
  assert proc.returncode == 0 and result["ok"] and not proc.stderr, (name, proc, result)
523
521
  for option, path in (("--show-toplevel", repo), ("--absolute-git-dir", repo / ".git")):
524
- assert git_text(repo, "rev-parse", option) == str(path)
522
+ assert git_text(repo, "rev-parse", option) == (path.as_posix() if os.name == "nt" else str(path))
525
523
  lock = repo / ".git" / "devlyn-bootstrap.lock"
526
524
  inode = lock.stat().st_ino
527
525
  devlyn = repo / ".devlyn"
@@ -565,23 +563,34 @@ def admission_self_test() -> None:
565
563
  git(repo, "init", "-q")
566
564
  git(repo, "config", "user.email", "test@example.com")
567
565
  git(repo, "config", "user.name", "Test")
568
- (repo / "app.py").write_text("print('base')\n")
566
+ (repo / "app.py").write_text("print('base')\n", encoding="utf-8")
569
567
  git(repo, "add", "app.py")
570
568
  git(repo, "commit", "-qm", "base")
571
569
  inputs(repo)
572
570
 
573
571
  def snapshot(path):
574
- if not path.exists() and not path.is_symlink():
572
+ try:
573
+ pending = [(path, path.lstat())]
574
+ except FileNotFoundError:
575
575
  return {}
576
- return {
577
- str(p.relative_to(path)): (str(p.readlink()) if p.is_symlink()
578
- else None if p.is_dir() else p.read_bytes())
579
- for p in (path, *path.rglob("*"))
580
- }
576
+ result = {}
577
+ while pending:
578
+ p, metadata = pending.pop()
579
+ key = str(p.relative_to(path))
580
+ if stat.S_ISLNK(metadata.st_mode) or (
581
+ os.name == "nt" and metadata.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT
582
+ ):
583
+ result[key] = str(p.readlink())
584
+ elif stat.S_ISDIR(metadata.st_mode):
585
+ result[key] = None
586
+ pending.extend((child, child.lstat()) for child in p.iterdir())
587
+ else:
588
+ result[key] = p.read_bytes()
589
+ return result
581
590
 
582
591
  def cli(repo, argv, extra_env=None):
583
592
  proc = subprocess.run([sys.executable, str(script), *argv], cwd=repo,
584
- env=extra_env or env, capture_output=True, text=True, timeout=15)
593
+ env=extra_env or env, capture_output=True, text=True, timeout=15, encoding="utf-8")
585
594
  assert proc.stderr == "", (proc.returncode, proc.stdout, proc.stderr)
586
595
  result = strict_json(proc.stdout)
587
596
  assert proc.returncode == (0 if result["ok"] else 1), result
@@ -628,7 +637,13 @@ if action.startswith('temp:'):
628
637
  return replace(path, target)
629
638
  pathlib.Path.replace = held_replace
630
639
  signals.with_suffix('.ready').touch()
631
- if int(barrier) >= 0:
640
+ if barrier.startswith('file:'):
641
+ deadline = time.monotonic() + 20
642
+ while not pathlib.Path(barrier[5:]).exists():
643
+ if time.monotonic() > deadline:
644
+ raise TimeoutError('native admission barrier was not released')
645
+ time.sleep(0.01)
646
+ elif int(barrier) >= 0:
632
647
  os.read(int(barrier), 1)
633
648
  os.close(int(barrier))
634
649
  try:
@@ -646,7 +661,7 @@ print(json.dumps(result))
646
661
  first = cli(repo, modes[0])
647
662
  devlyn = repo / ".devlyn"
648
663
  state_path = devlyn / "pipeline.state.json"
649
- initial = strict_json(state_path.read_text())
664
+ initial = strict_json(state_path.read_text(encoding="utf-8"))
650
665
  (devlyn / "engines.json").write_bytes(b'{"executor":"codex"}\n')
651
666
  (devlyn / "config.json").write_bytes(b'{"setting":true}\n')
652
667
  (devlyn / "unrelated.data").write_bytes(b"keep\n")
@@ -671,7 +686,7 @@ print(json.dumps(result))
671
686
  refusal(repo, modes[0], "BLOCKED:prior-run-unfinished")
672
687
  for verdict in ("PASS", "PASS_WITH_ISSUES", "NEEDS_WORK", "BLOCKED"):
673
688
  for timestamp in ("2024-02-29T23:59:59Z", "2024-02-29T23:59:59.123Z"):
674
- state = strict_json(state_path.read_text())
689
+ state = strict_json(state_path.read_text(encoding="utf-8"))
675
690
  state["phases"]["final_report"] = {"verdict": verdict, "completed_at": timestamp}
676
691
  state_path.write_bytes(json_bytes(state))
677
692
  raw = state_path.read_bytes()
@@ -706,7 +721,13 @@ print(json.dumps(result))
706
721
  identity = root / "identity"
707
722
  init(identity)
708
723
  alias = root / "alias"
709
- alias.symlink_to(identity, target_is_directory=True)
724
+ def directory_link(link, target):
725
+ if os.name == "nt":
726
+ subprocess.run(["cmd.exe", "/d", "/c", "mklink", "/J", str(link), str(target)],
727
+ check=True, capture_output=True)
728
+ else:
729
+ link.symlink_to(target, target_is_directory=True)
730
+ directory_link(alias, identity)
710
731
  sub = identity / "sub"
711
732
  sub.mkdir()
712
733
  detail = refusal(sub, ["--goal-file", "relative.txt"], "BLOCKED:worktree-root-required")["detail"]
@@ -731,20 +752,26 @@ print(json.dumps(result))
731
752
  (target / "keep").write_bytes(b"untouched")
732
753
  link = identity / relative
733
754
  link.parent.mkdir(exist_ok=True)
734
- link.symlink_to(target, target_is_directory=True)
755
+ directory_link(link, target)
735
756
  before = snapshot(target)
757
+ assert before == ({} if dangling else {".": None, "keep": b"untouched"})
758
+ expected = {".": str(link.readlink())} if relative == ".devlyn" else {
759
+ ".": None, "runs": str(link.readlink()),
760
+ }
761
+ assert snapshot(identity / ".devlyn") == expected
736
762
  result = refusal(identity, modes[0], "BLOCKED:devlyn-path-redirect")
737
- assert str(link) in result["detail"] and link.is_symlink()
763
+ assert str(link) in result["detail"] and link.resolve() == target and (os.name == "nt" or link.is_symlink())
738
764
  assert snapshot(target) == before
739
- link.unlink()
765
+ link.rmdir() if os.name == "nt" else link.unlink()
740
766
  print("PASS admission identity: physical-root guidance, Git redirects including empty, live/dangling directory links")
741
767
 
742
768
  def launch(repo, signals, action, argv, barrier=-1):
743
769
  return subprocess.Popen(
744
770
  [sys.executable, "-c", child, str(script), str(repo), str(signals),
745
771
  action, str(barrier), json.dumps(argv)],
746
- cwd=repo, env=env, pass_fds=(() if barrier < 0 else (barrier,)),
772
+ cwd=repo, env=env, **({} if os.name == "nt" else {"pass_fds": (() if barrier < 0 else (barrier,))}),
747
773
  stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
774
+ encoding="utf-8",
748
775
  )
749
776
 
750
777
  def receipt(proc):
@@ -762,14 +789,22 @@ print(json.dumps(result))
762
789
  other = ["goal", "B"] if i == 0 else [s.replace("spec.md", "spec-b.md").replace(
763
790
  "external.patch", "external-b.patch") for s in argv]
764
791
  signals = [root / f"race-{i}-{label}" for label in ("A", "B")]
765
- rfd, wfd = os.pipe()
792
+ if os.name == "nt":
793
+ barrier_file = root / f"race-{i}.barrier"
794
+ rfd = "file:" + str(barrier_file)
795
+ else:
796
+ rfd, wfd = os.pipe()
766
797
  procs = []
767
798
  try:
768
799
  procs = [launch(race, signal, "hold", args, rfd)
769
800
  for signal, args in zip(signals, (argv, other))]
770
- os.close(rfd)
801
+ if os.name != "nt":
802
+ os.close(rfd)
771
803
  wait_for(lambda: all(s.with_suffix(".ready").exists() for s in signals))
772
- os.close(wfd)
804
+ if os.name == "nt":
805
+ barrier_file.touch()
806
+ else:
807
+ os.close(wfd)
773
808
  wait_for(lambda: any(s.with_suffix(".inside").exists() for s in signals)
774
809
  and any(p.poll() is not None for p in procs))
775
810
  holders = [j for j, s in enumerate(signals) if s.with_suffix(".inside").exists()]
@@ -840,9 +875,10 @@ print(json.dumps(result))
840
875
  assert not (failure / ".devlyn").exists()
841
876
  failure_lock = failure / ".git/devlyn-bootstrap.lock"
842
877
  failure_inode = failure_lock.stat().st_ino
843
- import fcntl
844
- for replacement in (patch.dict(sys.modules, {"fcntl": None}),
845
- patch.object(fcntl, "flock", side_effect=OSError("unsupported locking"))):
878
+ module = __import__("msvcrt" if os.name == "nt" else "fcntl")
879
+ operation = "locking" if os.name == "nt" else "flock"
880
+ for replacement in (patch.dict(sys.modules, {module.__name__: None}),
881
+ patch.object(module, operation, side_effect=OSError("unsupported locking"))):
846
882
  with replacement:
847
883
  try:
848
884
  bootstrap(modes[0], failure, shared)
@@ -874,7 +910,7 @@ print(json.dumps(result))
874
910
  inode = (dead / ".git/devlyn-bootstrap.lock").stat().st_ino
875
911
  proc.kill()
876
912
  stdout, stderr = proc.communicate(timeout=15)
877
- assert proc.returncode == -9 and stdout == "" and stderr == "", (stdout, stderr)
913
+ assert proc.returncode == (1 if os.name == "nt" else -9) and stdout == "" and stderr == "", (proc.returncode, stdout, stderr)
878
914
  finally:
879
915
  if proc.poll() is None:
880
916
  proc.kill()
@@ -904,7 +940,7 @@ def self_test() -> int:
904
940
  subprocess.run(["git", "init", "-q"], cwd=path, check=True)
905
941
  subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=path, check=True)
906
942
  subprocess.run(["git", "config", "user.name", "Test"], cwd=path, check=True)
907
- (path / "app.py").write_text("print('base')\n")
943
+ (path / "app.py").write_text("print('base')\n", encoding="utf-8")
908
944
  subprocess.run(["git", "add", "app.py"], cwd=path, check=True)
909
945
  subprocess.run(["git", "commit", "-qm", "base"], cwd=path, check=True)
910
946
 
@@ -917,7 +953,7 @@ def self_test() -> int:
917
953
 
918
954
  def complete_prior(path: pathlib.Path) -> None:
919
955
  state_path = path / ".devlyn" / "pipeline.state.json"
920
- state = strict_json(state_path.read_text())
956
+ state = strict_json(state_path.read_text(encoding="utf-8"))
921
957
  state["phases"]["final_report"] = {
922
958
  "verdict": "PASS", "completed_at": "2026-09-06T00:00:00Z",
923
959
  }
@@ -940,8 +976,8 @@ def self_test() -> int:
940
976
  role_raw = b'{"roles":{"worker":{"engine":"codex","model":"gpt-6-astra"}}}'
941
977
  role_file.write_bytes(role_raw)
942
978
  bootstrap(["--role-config", str(role_file), "--no-pair", "fix app.py"], role_work, script_shared)
943
- staged_role = strict_json((role_work / ".devlyn/pipeline.state.json").read_text())
944
- role_file.write_text("{}")
979
+ staged_role = strict_json((role_work / ".devlyn/pipeline.state.json").read_text(encoding="utf-8"))
980
+ role_file.write_text("{}", encoding="utf-8")
945
981
  assert staged_role["role_config_input"]["sha256"] == sha256(role_raw)
946
982
  assert staged_role["role_config_input"]["value"] == strict_json(role_raw.decode())
947
983
  assert staged_role["role_no_pair"] is True
@@ -952,7 +988,7 @@ def self_test() -> int:
952
988
  prior_session = os.environ.pop(session_key, None)
953
989
  result = bootstrap(["fix", "app.py", "failing", "test"], work, script_shared)
954
990
  state_path = work / ".devlyn" / "pipeline.state.json"
955
- state = strict_json(state_path.read_text())
991
+ state = strict_json(state_path.read_text(encoding="utf-8"))
956
992
  expected = {
957
993
  "version": "3.0",
958
994
  "run_id": state["run_id"],
@@ -994,7 +1030,10 @@ def self_test() -> int:
994
1030
  "phases": {name: None for name in PHASE_NAMES},
995
1031
  "verify": {"coverage_failed": False, "pair_trigger": None},
996
1032
  }
997
- schema = (script_shared.parent / "devlyn:resolve" / "references" / "state-schema.md").read_text()
1033
+ schema_dirs = [script_shared.parent / name for name in ("devlyn:resolve", "devlyn\uF03Aresolve")
1034
+ if (script_shared.parent / name).is_dir()]
1035
+ assert len(schema_dirs) == 1, schema_dirs
1036
+ schema = (schema_dirs[0] / "references" / "state-schema.md").read_text(encoding="utf-8")
998
1037
  assert '"version": "3.0"' in schema and all(f'"{name}"' in schema for name in PHASE_NAMES)
999
1038
  assert state_path.read_bytes() == json_bytes(expected)
1000
1039
  assert result["state_sha256"] == sha256(json_bytes(expected))
@@ -1007,7 +1046,7 @@ def self_test() -> int:
1007
1046
  init_repo(detached)
1008
1047
  subprocess.run(["git", "checkout", "--detach", "-q"], cwd=detached, check=True)
1009
1048
  detached_result = bootstrap(["fix", "app.py"], detached, script_shared)
1010
- detached_state = strict_json((detached / ".devlyn" / "pipeline.state.json").read_text())
1049
+ detached_state = strict_json((detached / ".devlyn" / "pipeline.state.json").read_text(encoding="utf-8"))
1011
1050
  assert detached_result["ok"] is True
1012
1051
  assert detached_state["base_ref"]["branch"] is None
1013
1052
  assert detached_state["base_ref"]["sha"] == git_text(detached, "rev-parse", "HEAD")
@@ -1016,7 +1055,7 @@ def self_test() -> int:
1016
1055
  complete_prior(work)
1017
1056
  os.environ[session_key] = "session-self-test"
1018
1057
  bootstrap(["--engine", "raw-engine", "--pair-verify", "fix", "app.py"], work, script_shared)
1019
- stamped = strict_json(state_path.read_text())
1058
+ stamped = strict_json(state_path.read_text(encoding="utf-8"))
1020
1059
  assert stamped["session_id"] == "session-self-test"
1021
1060
  assert stamped["engine"] == "raw-engine" and stamped["engine_source"] == "flag"
1022
1061
  assert stamped["pair_verify"] is True
@@ -1063,6 +1102,7 @@ def self_test() -> int:
1063
1102
  cli_blocked = subprocess.run(
1064
1103
  [sys.executable, str(pathlib.Path(__file__).resolve()), "--pair-verify", "--no-pair"],
1065
1104
  cwd=blocked_work, capture_output=True, text=True,
1105
+ encoding="utf-8",
1066
1106
  )
1067
1107
  assert cli_blocked.returncode == 1 and cli_blocked.stderr == ""
1068
1108
  assert strict_json(cli_blocked.stdout)["blocked"] == "BLOCKED:invalid-flags"
@@ -1070,7 +1110,7 @@ def self_test() -> int:
1070
1110
  print("PASS bootstrap self-test failure receipt: machine-only JSON, zero partial state")
1071
1111
 
1072
1112
  goal_file = blocked_work / "goal.txt"
1073
- goal_file.write_text(" \n")
1113
+ goal_file.write_text(" \n", encoding="utf-8")
1074
1114
  try:
1075
1115
  bootstrap(["--goal-file", "goal.txt"], blocked_work, script_shared)
1076
1116
  except BootstrapBlocked as exc:
@@ -1135,7 +1175,8 @@ def self_test() -> int:
1135
1175
  spec_path = spec_dir / "spec.md"
1136
1176
  spec_path.write_text(
1137
1177
  "---\ncomplexity: medium\n---\n# Spec\n\n<!-- devlyn:verification -->\n"
1138
- "## Verification\n\n```json\n{\"verification_commands\":[{\"cmd\":\"printf ok\",\"stdout_contains\":[\"ok\"]}]}\n```\n"
1178
+ "## Verification\n\n```json\n{\"verification_commands\":[{\"cmd\":\"printf ok\",\"stdout_contains\":[\"ok\"]}]}\n```\n",
1179
+ encoding="utf-8",
1139
1180
  )
1140
1181
  spec_raw = spec_path.read_bytes()
1141
1182
  external_patch = work / ".devlyn" / "external-diff.patch"
@@ -1143,7 +1184,7 @@ def self_test() -> int:
1143
1184
  complete_prior(work)
1144
1185
  spec_result = bootstrap(["--spec", str(spec_path.relative_to(work))], work, script_shared)
1145
1186
  assert not external_patch.exists()
1146
- staged = strict_json((work / ".devlyn" / "spec-verify.json").read_text())
1187
+ staged = strict_json((work / ".devlyn" / "spec-verify.json").read_text(encoding="utf-8"))
1147
1188
  assert staged["verification_commands"][0]["cmd"] == "printf ok"
1148
1189
  assert spec_result["source"]["spec_sha256"] == sha256(spec_raw)
1149
1190
  external_patch.write_bytes(b"stale free-form patch\n")
@@ -1152,14 +1193,14 @@ def self_test() -> int:
1152
1193
  assert not external_patch.exists()
1153
1194
  (spec_dir / "spec.expected.json").write_text(json.dumps({
1154
1195
  "verification_commands": [{"cmd": "printf expected", "stdout_contains": ["expected"]}],
1155
- }) + "\n")
1196
+ }) + "\n", encoding="utf-8")
1156
1197
  complete_prior(work)
1157
1198
  bootstrap(["--spec", str(spec_path.relative_to(work))], work, script_shared)
1158
- staged = strict_json((work / ".devlyn" / "spec-verify.json").read_text())
1199
+ staged = strict_json((work / ".devlyn" / "spec-verify.json").read_text(encoding="utf-8"))
1159
1200
  assert staged["verification_commands"][0]["cmd"] == "printf expected"
1160
1201
  patch_raw = b"diff --git a/app.py b/app.py\nexact external bytes\x00\n"
1161
1202
  (work / "external.patch").write_bytes(patch_raw)
1162
- (work / "app.py").write_text("print('dirty verify-only input')\n")
1203
+ (work / "app.py").write_text("print('dirty verify-only input')\n", encoding="utf-8")
1163
1204
  complete_prior(work)
1164
1205
  verify_result = bootstrap([
1165
1206
  "--verify-only", "external.patch", "--spec", str(spec_path.relative_to(work)),
@@ -1173,7 +1214,7 @@ def self_test() -> int:
1173
1214
  init_repo(dirty_work)
1174
1215
  bootstrap(["clean", "baseline"], dirty_work, script_shared)
1175
1216
  before_dirty = snapshot(dirty_work / ".devlyn")
1176
- (dirty_work / "app.py").write_text("print('unstaged')\n")
1217
+ (dirty_work / "app.py").write_text("print('unstaged')\n", encoding="utf-8")
1177
1218
  for label in ("unstaged", "staged"):
1178
1219
  if label == "staged":
1179
1220
  subprocess.run(["git", "add", "app.py"], cwd=dirty_work, check=True)
@@ -1192,16 +1233,16 @@ def self_test() -> int:
1192
1233
  init_repo(devlyn_work)
1193
1234
  (devlyn_work / ".devlyn").mkdir()
1194
1235
  owner_file = devlyn_work / ".devlyn" / "owner.txt"
1195
- owner_file.write_text("base\n")
1236
+ owner_file.write_text("base\n", encoding="utf-8")
1196
1237
  subprocess.run(["git", "add", ".devlyn/owner.txt"], cwd=devlyn_work, check=True)
1197
1238
  subprocess.run(["git", "commit", "-qm", "track devlyn owner"], cwd=devlyn_work, check=True)
1198
- owner_file.write_text("dirty allowed\n")
1239
+ owner_file.write_text("dirty allowed\n", encoding="utf-8")
1199
1240
  assert bootstrap(["devlyn", "allowed"], devlyn_work, script_shared)["ok"] is True
1200
- assert owner_file.read_text() == "dirty allowed\n"
1241
+ assert owner_file.read_text(encoding="utf-8") == "dirty allowed\n"
1201
1242
 
1202
1243
  untracked_work = root / "untracked-repo"
1203
1244
  init_repo(untracked_work)
1204
- (untracked_work / "untracked.txt").write_text("allowed\n")
1245
+ (untracked_work / "untracked.txt").write_text("allowed\n", encoding="utf-8")
1205
1246
  assert bootstrap(["untracked", "allowed"], untracked_work, script_shared)["ok"] is True
1206
1247
  print("PASS bootstrap self-test honest baseline: dirty owner blocked; .devlyn/untracked allowed")
1207
1248
 
@@ -1248,11 +1289,11 @@ def self_test() -> int:
1248
1289
  assert (prior_archive / name).is_file(), name
1249
1290
  if name not in {"pipeline.state.json", "goal.raw.txt"}:
1250
1291
  assert not (prior_devlyn / name).exists(), name
1251
- assert strict_json((prior_devlyn / "pipeline.state.json").read_text())["run_id"] == replacement["run_id"]
1292
+ assert strict_json((prior_devlyn / "pipeline.state.json").read_text(encoding="utf-8"))["run_id"] == replacement["run_id"]
1252
1293
  assert (prior_archive / "goal.raw.txt").read_bytes() == b"prior run"
1253
1294
  assert (prior_devlyn / "goal.raw.txt").read_bytes() == b"replacement run"
1254
- assert (prior_devlyn / "engines.json").read_text() == '{"executor":"codex"}\n'
1255
- assert (prior_devlyn / "unrelated.data").read_text() == "preserve\n"
1295
+ assert (prior_devlyn / "engines.json").read_text(encoding="utf-8") == '{"executor":"codex"}\n'
1296
+ assert (prior_devlyn / "unrelated.data").read_text(encoding="utf-8") == "preserve\n"
1256
1297
 
1257
1298
  for label, state_raw in (("missing", None), ("malformed", b"{\n")):
1258
1299
  unauthenticated = root / f"unauthenticated-{label}"
@@ -1276,7 +1317,8 @@ def self_test() -> int:
1276
1317
  init_repo(malformed_work)
1277
1318
  bad_spec = malformed_work / "spec.md"
1278
1319
  bad_spec.write_text(
1279
- "# Spec\n\n<!-- devlyn:verification -->\n## Verification\n\n```json\n{broken\n```\n"
1320
+ "# Spec\n\n<!-- devlyn:verification -->\n## Verification\n\n```json\n{broken\n```\n",
1321
+ encoding="utf-8",
1280
1322
  )
1281
1323
  try:
1282
1324
  bootstrap(["--spec", "spec.md"], malformed_work, script_shared)
@@ -1319,4 +1361,5 @@ def main() -> int:
1319
1361
 
1320
1362
 
1321
1363
  if __name__ == "__main__":
1364
+ runpy.run_path(str(pathlib.Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
1322
1365
  raise SystemExit(main())
@@ -3,6 +3,7 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ import runpy
6
7
  import fnmatch
7
8
  import hashlib
8
9
  import importlib.util
@@ -281,7 +282,7 @@ def self_test() -> int:
281
282
  assert hashlib.sha256(block_state.read_bytes()).hexdigest() == before_sha
282
283
  assert len(receipts) == 1
283
284
  assert any(fnmatch.fnmatch(receipts[0].name, pattern) for pattern in archive_module.PER_RUN_PATTERNS)
284
- assert json.loads(receipts[0].read_text())["stop_hook_active"] is True
285
+ assert json.loads(receipts[0].read_text(encoding="utf-8"))["stop_hook_active"] is True
285
286
 
286
287
  absent = scratch / "control-a-absent"
287
288
  absent.mkdir()
@@ -354,4 +355,5 @@ def main(argv: list[str] | None = None) -> int:
354
355
 
355
356
 
356
357
  if __name__ == "__main__":
358
+ runpy.run_path(str(pathlib.Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
357
359
  raise SystemExit(main())
@@ -2,6 +2,7 @@
2
2
  """Resolve explicit devlyn roles; no model dispatch or lifecycle changes."""
3
3
  from __future__ import annotations
4
4
 
5
+ import runpy
5
6
  import argparse
6
7
  import hashlib
7
8
  import json
@@ -58,7 +59,7 @@ def adapter(engine, judge=False, shared=SHARED):
58
59
  path = shared / "adapters" / f"{engine}.md"
59
60
  if not path.is_file():
60
61
  fail(f"no adapter for {engine}")
61
- text = path.read_text()
62
+ text = path.read_text(encoding="utf-8")
62
63
  field = "pair_judge" if judge else "executor"
63
64
  if re.search(rf"^{field}: no\s*$", text, re.M):
64
65
  fail(f"{engine} is ineligible for {'judge' if judge else 'worker'}")
@@ -238,7 +239,7 @@ def native_version(engine):
238
239
  if binary is None:
239
240
  fail(f"install/authenticate {engine} and retry", f"{engine}-unavailable")
240
241
  try:
241
- proc = subprocess.run([binary, "--version"], capture_output=True, text=True, timeout=20)
242
+ proc = subprocess.run(runpy.run_path(Path(__file__).with_name("platform-support.py"))["native_argv"]([binary, "--version"]), capture_output=True, text=True, timeout=20, encoding="utf-8")
242
243
  except (OSError, subprocess.SubprocessError) as exc:
243
244
  fail(f"{engine} --version failed: {exc}; install/authenticate the native CLI and retry", f"{engine}-unavailable")
244
245
  if proc.returncode != 0:
@@ -402,7 +403,7 @@ def self_test():
402
403
  for role, arguments in (("pair_judge", ["--value", '{"engine":"claude"}']),
403
404
  ("worker", ["--value", "{"]), ("worker", [])):
404
405
  proc = subprocess.run([sys.executable, str(Path(__file__).resolve()), "--workdir", str(work),
405
- "--set-role", role, *arguments], capture_output=True, text=True)
406
+ "--set-role", role, *arguments], capture_output=True, text=True, encoding="utf-8")
406
407
  assert proc.returncode == 1 and "BLOCKED:invalid-engine-config" in proc.stderr, proc.stderr
407
408
  assert "Traceback" not in proc.stderr and path.read_bytes() == before
408
409
  path.write_bytes(encoded({"executor": "claude", "pair_judge_priority": ["codex"]}))
@@ -420,7 +421,7 @@ def self_test():
420
421
  assert status["roles"]["primary_judge"]["availability"] == "CLI-unavailable"
421
422
  before = path.read_bytes()
422
423
  proc = subprocess.run([sys.executable, str(Path(__file__).resolve()), "--workdir", str(work)],
423
- capture_output=True, text=True, env={**os.environ, "PATH": str(work / "no-binaries")})
424
+ capture_output=True, text=True, env={**os.environ, "PATH": str(work / "no-binaries")}, encoding="utf-8")
424
425
  assert proc.returncode == 0 and loads(proc.stdout)["roles"]["pair_judge"]["engine"] is None, proc.stderr
425
426
  assert path.read_bytes() == before
426
427
  for engine in (None, ""):
@@ -470,7 +471,7 @@ def self_test():
470
471
  raise AssertionError("unknown judge effort support was assumed")
471
472
  ineligible = work / "adapters"
472
473
  ineligible.mkdir()
473
- (ineligible / "claude.md").write_text("pair_judge: no\n")
474
+ (ineligible / "claude.md").write_text("pair_judge: no\n", encoding="utf-8")
474
475
  try:
475
476
  options(future, "primary_judge", version=future_version, shared=work)
476
477
  except ValueError as exc:
@@ -549,4 +550,5 @@ def main():
549
550
 
550
551
 
551
552
  if __name__ == "__main__":
553
+ runpy.run_path(str(Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
552
554
  raise SystemExit(main())
@@ -1,7 +1,12 @@
1
1
  #!/usr/bin/env python3
2
2
  from __future__ import annotations
3
3
 
4
- import os, signal, subprocess, sys
4
+ from pathlib import Path
5
+ import runpy
6
+ import subprocess
7
+ import sys
8
+
9
+ PLATFORM = runpy.run_path(Path(__file__).with_name("platform-support.py"))
5
10
 
6
11
 
7
12
  def fail(message: str) -> int:
@@ -11,34 +16,43 @@ def fail(message: str) -> int:
11
16
 
12
17
  def main(argv: list[str]) -> int:
13
18
  if len(argv) < 4:
14
- return fail("usage: run-bounded.py <seconds> -- <cmd> [args...]")
19
+ return fail("usage: run-bounded.py <seconds> [--stdin-file <path> [--record-transport]] -- <cmd> [args...]")
15
20
  try:
16
21
  seconds = int(argv[1])
17
22
  except ValueError:
18
23
  return fail("seconds must be a positive integer")
19
24
  if seconds <= 0:
20
25
  return fail("seconds must be a positive integer")
21
- if argv[2] != "--":
26
+ index, prompt, record_transport = 2, None, False
27
+ if argv[index] == "--stdin-file":
28
+ if len(argv) < 6:
29
+ return fail("--stdin-file requires a path and command")
30
+ prompt, index = argv[index + 1], index + 2
31
+ if argv[index] == "--record-transport":
32
+ record_transport, index = True, index + 1
33
+ if argv[index] != "--" or not argv[index + 1:]:
22
34
  return fail("expected -- before command")
23
-
24
- child = subprocess.Popen(argv[3:], stdin=subprocess.DEVNULL, start_new_session=True)
35
+ stream = None
25
36
  try:
26
- return child.wait(timeout=seconds)
27
- except subprocess.TimeoutExpired:
28
- try:
29
- os.killpg(child.pid, signal.SIGTERM)
30
- except ProcessLookupError:
31
- return 124
32
- try:
33
- child.wait(timeout=5)
34
- except subprocess.TimeoutExpired:
35
- try:
36
- os.killpg(child.pid, signal.SIGKILL)
37
- except ProcessLookupError:
38
- pass
39
- child.wait()
40
- return 124
37
+ command = argv[index + 1:]
38
+ actual = PLATFORM["native_argv"](command)
39
+ if record_transport:
40
+ transport = runpy.run_path(Path(__file__).with_name("invocation-receipt.py"))
41
+ stream, carrier, record = transport["prepare_transport"](prompt, command, actual, seconds)
42
+ transport["write_transport"](carrier, record)
43
+ elif prompt is not None:
44
+ stream = PLATFORM["open_stdin"](prompt)
45
+ code = PLATFORM["run_process"](actual, stream if stream is not None else subprocess.DEVNULL, seconds)
46
+ if record_transport:
47
+ transport["finish_transport"](carrier, code)
48
+ return code
49
+ except (OSError, ValueError) as exc:
50
+ return fail(str(exc))
51
+ finally:
52
+ if stream is not None:
53
+ stream.close()
41
54
 
42
55
 
43
56
  if __name__ == "__main__":
44
- raise SystemExit(main(sys.argv))
57
+ PLATFORM["configure_utf8"]()
58
+ raise SystemExit(PLATFORM["system_exit_code"](main(sys.argv)))