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
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  import argparse
10
10
  import contextlib
11
- import fcntl
12
11
  import functools
13
12
  import hashlib
14
13
  import json
@@ -35,7 +34,7 @@ def require(condition, message):
35
34
 
36
35
 
37
36
  def command(argv, *, cwd=None, ok=(0,)):
38
- result = subprocess.run(argv, cwd=cwd, capture_output=True, text=True)
37
+ result = subprocess.run(argv, cwd=cwd, capture_output=True, text=True, encoding="utf-8")
39
38
  require(result.returncode in ok, f"{shlex.join(argv[:4])}: {result.stderr.strip() or result.stdout.strip()}")
40
39
  return result.stdout.strip()
41
40
 
@@ -51,23 +50,24 @@ def shared(name):
51
50
 
52
51
  def read_json(path):
53
52
  require(stat.S_ISREG(path.lstat().st_mode), f"JSON must be a nonsymlink regular file: {path}")
54
- return shared("archive_run")["loads_strict_json"](path.read_text())
53
+ return shared("archive_run")["loads_strict_json"](path.read_text(encoding="utf-8"))
55
54
 
56
55
 
57
56
  def atomic_json(path, value):
58
57
  fd, temporary = tempfile.mkstemp(dir=path.parent, prefix=".receipt-")
59
58
  try:
60
- with os.fdopen(fd, "w") as stream:
59
+ with os.fdopen(fd, "w", encoding="utf-8") as stream:
61
60
  json.dump(value, stream, sort_keys=True, indent=2)
62
61
  stream.write("\n")
63
62
  stream.flush()
64
63
  os.fsync(stream.fileno())
65
64
  os.replace(temporary, path)
66
- directory = os.open(path.parent, os.O_RDONLY)
67
- try:
68
- os.fsync(directory)
69
- finally:
70
- os.close(directory)
65
+ if os.name != "nt":
66
+ directory = os.open(path.parent, os.O_RDONLY)
67
+ try:
68
+ os.fsync(directory)
69
+ finally:
70
+ os.close(directory)
71
71
  finally:
72
72
  Path(temporary).unlink(missing_ok=True)
73
73
 
@@ -167,7 +167,7 @@ def repository_from_url(url):
167
167
 
168
168
 
169
169
  def policy(receipt, override):
170
- config = subprocess.run(["git", "--git-dir", receipt["common_gitdir"], "config", "--local", "--get-all", "devlyn.completionMode"], capture_output=True, text=True)
170
+ config = subprocess.run(["git", "--git-dir", receipt["common_gitdir"], "config", "--local", "--get-all", "devlyn.completionMode"], capture_output=True, text=True, encoding="utf-8")
171
171
  require(config.returncode in {0, 1}, "cannot read local completion policy: " + config.stderr.strip())
172
172
  require(config.returncode == 1 or config.stdout in {"auto\n", "pr\n"}, "invalid local devlyn.completionMode; use auto|pr")
173
173
  value = config.stdout.strip() if config.returncode == 0 else None
@@ -412,7 +412,7 @@ def stopped_writers(work, *, linked):
412
412
  # The owner assertion covers its actual children; an OS observation catches
413
413
  # other currently open files/cwds, not future writers or a universal lease.
414
414
  if sys.platform == "darwin":
415
- result = subprocess.run(["lsof", "-Fpn", "+D", str(work)], capture_output=True, text=True)
415
+ result = subprocess.run(["lsof", "-Fpn", "+D", str(work)], capture_output=True, text=True, encoding="utf-8")
416
416
  require(result.returncode in {0, 1} and not result.stderr.strip(), "writer observation unavailable; retain tree and inspect writers")
417
417
  pid = None
418
418
  for line in result.stdout.splitlines():
@@ -508,8 +508,11 @@ def cleanup(receipt, path, pr, writers_stopped):
508
508
  def locked_receipt(path):
509
509
  require(path.name == "receipt.json" and path.parent.parent.name == "devlyn-completion" and path == path.resolve(), "receipt must be its original external nonsymlink path")
510
510
  require(not (path.parent / "lock").is_symlink(), "receipt lock must not be a symlink")
511
- with (path.parent / "lock").open("a+") as lock:
512
- fcntl.flock(lock, fcntl.LOCK_EX)
511
+ with contextlib.ExitStack() as stack:
512
+ try:
513
+ stack.enter_context(shared("platform-support")["file_lock"](path.parent / "lock", blocking=True))
514
+ except (ImportError, OSError, AttributeError) as exc:
515
+ raise CompletionError(f"receipt lock unavailable: {path}: {exc}") from exc
513
516
  receipt = read_json(path)
514
517
  require(path == Path(receipt["common_gitdir"]) / "devlyn-completion" / receipt["id"] / "receipt.json", "receipt/common Gitdir binding mismatch")
515
518
  require(receipt["id"] == hashlib.sha256(receipt["branch"].encode()).hexdigest()[:24] and receipt["branch"] not in {receipt["base"], "main", "master"}, "receipt branch ownership changed")
@@ -639,10 +642,10 @@ def self_test(names=None):
639
642
  FAKE_GH = r'''#!/usr/bin/env python3
640
643
  import json, os, pathlib, subprocess, sys
641
644
  p = pathlib.Path(os.environ['FIXTURE_GH'])
642
- d = json.loads(p.read_text()); a = sys.argv[1:]
643
- def save(): p.write_text(json.dumps(d))
645
+ d = json.loads(p.read_text(encoding="utf-8")); a = sys.argv[1:]
646
+ def save(): p.write_text(json.dumps(d), encoding="utf-8")
644
647
  def remote(ref):
645
- r = subprocess.run([os.environ['REAL_GIT'], '--git-dir', d['bare'], 'rev-parse', '--verify', ref], capture_output=True, text=True)
648
+ r = subprocess.run([os.environ['REAL_GIT'], '--git-dir', d['bare'], 'rev-parse', '--verify', ref], capture_output=True, text=True, encoding="utf-8")
646
649
  return r.stdout.strip() if r.returncode == 0 else None
647
650
  if a[:2] == ['repo', 'view']:
648
651
  assert a[2:] == ['github.com/test/project', '--json', 'nameWithOwner,url,defaultBranchRef,mergeCommitAllowed'], 'unsupported gh repo view arguments: '+str(a)
@@ -673,8 +676,8 @@ elif a[:2] == ['pr', 'merge']:
673
676
  else:
674
677
  g = [os.environ['REAL_GIT'],'--git-dir',d['bare']]
675
678
  base = remote('refs/heads/main')
676
- tree = subprocess.check_output(g+['rev-parse',sha+'^{tree}'],text=True).strip()
677
- merge = subprocess.check_output(g+['commit-tree',tree,'-p',base,'-p',sha,'-m','merge fixture'],text=True).strip()
679
+ tree = subprocess.check_output(g+['rev-parse',sha+'^{tree}'],text=True, encoding="utf-8").strip()
680
+ merge = subprocess.check_output(g+['commit-tree',tree,'-p',base,'-p',sha,'-m','merge fixture'],text=True, encoding="utf-8").strip()
678
681
  subprocess.check_call(g+['update-ref','refs/heads/main',merge,base])
679
682
  d['pr'].update(state='MERGED',mergedAt='now',mergeCommit={'oid':merge})
680
683
  save()
@@ -686,22 +689,22 @@ else:
686
689
  GIT_WRAPPER = r'''#!/usr/bin/env python3
687
690
  import json, os, pathlib, subprocess, sys
688
691
  a = sys.argv[1:]
689
- p = pathlib.Path(os.environ['FIXTURE_GH']); d = json.loads(p.read_text())
692
+ p = pathlib.Path(os.environ['FIXTURE_GH']); d = json.loads(p.read_text(encoding="utf-8"))
690
693
  if 'push' in a and any(x.startswith('--force-with-lease=') for x in a) and d.pop('remote_delete_race',False):
691
694
  subprocess.check_call([os.environ['REAL_GIT'],'--git-dir',d['bare'],'update-ref','refs/heads/'+d['pr']['headRefName'],d['race_sha']])
692
- p.write_text(json.dumps(d))
695
+ p.write_text(json.dumps(d), encoding="utf-8")
693
696
  for operation in ('push', 'fetch', 'ls-remote'):
694
697
  if operation in a and 'origin' in a:
695
698
  prefix = a[:a.index(operation)]
696
- url = subprocess.check_output([os.environ['REAL_GIT'], *prefix, 'remote', 'get-url', *(['--push'] if operation == 'push' else []), '--all', 'origin'], text=True).strip()
699
+ url = subprocess.check_output([os.environ['REAL_GIT'], *prefix, 'remote', 'get-url', *(['--push'] if operation == 'push' else []), '--all', 'origin'], text=True, encoding="utf-8").strip()
697
700
  if url in {'https://github.com/test/project.git', 'git@github.com:test/project.git', 'ssh://git@github.com/test/project.git'}:
698
701
  a[a.index('origin')] = d['bare']
699
702
  break
700
703
  r = subprocess.run([os.environ['REAL_GIT'],*a])
701
704
  event = 'delete' if 'push' in a and any(x.startswith(':refs/') for x in a) else 'push' if 'push' in a else 'remove' if 'worktree' in a and 'remove' in a else None
702
705
  if r.returncode == 0 and event:
703
- d = json.loads(p.read_text()); d[event+'s'] = d.get(event+'s',0)+1
704
- fail = d.pop('interrupt_'+event,False); p.write_text(json.dumps(d))
706
+ d = json.loads(p.read_text(encoding="utf-8")); d[event+'s'] = d.get(event+'s',0)+1
707
+ fail = d.pop('interrupt_'+event,False); p.write_text(json.dumps(d), encoding="utf-8")
705
708
  if fail: sys.exit(1)
706
709
  sys.exit(r.returncode)
707
710
  '''
@@ -717,7 +720,7 @@ class CompletionTests(unittest.TestCase):
717
720
  self.bin = self.root / "bin"
718
721
  self.bin.mkdir()
719
722
  self.data = self.root / "gh.json"
720
- self.data.write_text(json.dumps({"bare": str(self.bare)}))
723
+ self.data.write_text(json.dumps({"bare": str(self.bare)}), encoding="utf-8")
721
724
  self.env = {**os.environ, "GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": str(self.root / "gitconfig"),
722
725
  "GIT_AUTHOR_NAME": "Fixture", "GIT_AUTHOR_EMAIL": "fixture@example.invalid",
723
726
  "GIT_COMMITTER_NAME": "Fixture", "GIT_COMMITTER_EMAIL": "fixture@example.invalid",
@@ -728,13 +731,13 @@ class CompletionTests(unittest.TestCase):
728
731
  del self.env[key]
729
732
  for name, body in (("gh", FAKE_GH), ("git", GIT_WRAPPER)):
730
733
  path = self.bin / name
731
- path.write_text(body)
734
+ path.write_text(body, encoding="utf-8")
732
735
  path.chmod(0o755)
733
736
  self.env["PATH"] = str(self.bin) + os.pathsep + os.environ["PATH"]
734
737
  self.run_cmd(["git", "init", "--bare", "--initial-branch=main", str(self.bare)])
735
738
  self.run_cmd(["git", "init", "--initial-branch=main", str(self.work)])
736
- (self.work / ".gitignore").write_text(".devlyn/\nignored/\n")
737
- (self.work / "product").write_text("baseline\n")
739
+ (self.work / ".gitignore").write_text(".devlyn/\nignored/\n", encoding="utf-8")
740
+ (self.work / "product").write_text("baseline\n", encoding="utf-8")
738
741
  self.g("add", ".")
739
742
  self.g("commit", "-m", "baseline")
740
743
  self.g("remote", "add", "origin", "https://github.com/test/project.git")
@@ -742,7 +745,7 @@ class CompletionTests(unittest.TestCase):
742
745
  self.configure(pushs=0)
743
746
 
744
747
  def run_cmd(self, args, *, cwd=None, success=True):
745
- r = subprocess.run(args, cwd=cwd or self.root, env=self.env, capture_output=True, text=True)
748
+ r = subprocess.run(args, cwd=cwd or self.root, env=self.env, capture_output=True, text=True, encoding="utf-8")
746
749
  if success:
747
750
  self.assertEqual(r.returncode, 0, r.stderr + r.stdout)
748
751
  return r
@@ -751,8 +754,8 @@ class CompletionTests(unittest.TestCase):
751
754
  return self.run_cmd(["git", "-C", str(work or self.work), *args]).stdout.strip()
752
755
 
753
756
  def configure(self, **values):
754
- d = json.loads(self.data.read_text()); d.update(values)
755
- self.data.write_text(json.dumps(d))
757
+ d = json.loads(self.data.read_text(encoding="utf-8")); d.update(values)
758
+ self.data.write_text(json.dumps(d), encoding="utf-8")
756
759
 
757
760
  def cli(self, *args, success=True, cwd=None):
758
761
  r = self.run_cmd([sys.executable, str(Path(__file__).resolve()), *map(str, args)], success=success, cwd=cwd)
@@ -769,47 +772,47 @@ class CompletionTests(unittest.TestCase):
769
772
 
770
773
  def accept(self, pipeline=False, queue=False, spec_expected=None):
771
774
  if spec_expected is not None:
772
- (self.task / "spec.md").write_text("# Fixture\nProduct contains accepted bytes.\n")
773
- (self.task / "spec.expected.json").write_text(json.dumps(spec_expected))
775
+ (self.task / "spec.md").write_text("# Fixture\nProduct contains accepted bytes.\n", encoding="utf-8")
776
+ (self.task / "spec.expected.json").write_text(json.dumps(spec_expected), encoding="utf-8")
774
777
  self.g("add", "spec.md", "spec.expected.json", work=self.task)
775
- (self.task / "product").write_text("accepted\n")
778
+ (self.task / "product").write_text("accepted\n", encoding="utf-8")
776
779
  self.g("add", "product", work=self.task)
777
780
  self.g("commit", "-m", "scoped task", work=self.task)
778
781
  self.sha = self.g("rev-parse", "HEAD", work=self.task)
779
782
  evidence = self.task / ".devlyn"
780
783
  evidence.mkdir(exist_ok=True)
781
- (evidence / "checks.txt").write_text("actual fixture check: accepted bytes\n")
784
+ (evidence / "checks.txt").write_text("actual fixture check: accepted bytes\n", encoding="utf-8")
782
785
  a = {"kind": "direct", "task": "fixture", "source_sha": self.sha,
783
786
  "checks": [{"command": "fixture byte assertion", "evidence": ".devlyn/checks.txt"}]}
784
- self.assertEqual((self.task / "product").read_text(), "accepted\n")
787
+ self.assertEqual((self.task / "product").read_text(encoding="utf-8"), "accepted\n")
785
788
  if pipeline:
786
789
  a = {"kind": "pipeline", "task": "fixture", "source_sha": self.sha, "run_id": "fixture-run"}
787
790
  archive = evidence / "runs" / a["run_id"]
788
791
  archive.mkdir(parents=True)
789
792
  self.archive = archive
790
793
  report = "<!-- devlyn:final-report run_id=fixture-run -->\nFixture completed.\n"
791
- (archive / "final-report.md").write_text(report)
794
+ (archive / "final-report.md").write_text(report, encoding="utf-8")
792
795
  phases = {n: {"started_at": "2026-09-10T00:00:00Z", "completed_at": "2026-09-10T00:00:01Z", "verdict": "PASS"}
793
796
  for n in ("plan", "implement", "build_gate", "cleanup", "verify", "final_report")}
794
797
  phases["cleanup"]["post_sha"] = self.sha
795
798
  phases["final_report"].update(output_sha256=hashlib.sha256(report.encode()).hexdigest(), artifacts={"log_file": ".devlyn/final-report.md"})
796
799
  criteria = "# Fixture acceptance\nProduct contains accepted bytes.\n"
797
- (archive / "criteria.generated.md").write_text(criteria)
800
+ (archive / "criteria.generated.md").write_text(criteria, encoding="utf-8")
798
801
  source = {"type": "generated", "criteria_path": ".devlyn/criteria.generated.md", "criteria_sha256": hashlib.sha256(criteria.encode()).hexdigest()}
799
802
  self.state = {"run_id": a["run_id"], "mode": "free-form", "source": source, "phases": phases, "process_evidence": None}
800
803
  if spec_expected is not None:
801
804
  self.state["mode"] = "spec"
802
805
  self.state["source"] = {"type": "spec", "spec_path": "spec.md", "spec_sha256": hashlib.sha256((self.task / "spec.md").read_bytes()).hexdigest()}
803
- (archive / "pipeline.state.json").write_text(json.dumps(self.state))
804
- (archive / "finish-gate.summary.json").write_text(json.dumps({"mode": self.state["mode"], "exit": 0, "offenders": 0, "checked": 1}))
805
- (archive / "verify-merge.summary.json").write_text(json.dumps({"verdict": "PASS"}))
806
+ (archive / "pipeline.state.json").write_text(json.dumps(self.state), encoding="utf-8")
807
+ (archive / "finish-gate.summary.json").write_text(json.dumps({"mode": self.state["mode"], "exit": 0, "offenders": 0, "checked": 1}), encoding="utf-8")
808
+ (archive / "verify-merge.summary.json").write_text(json.dumps({"verdict": "PASS"}), encoding="utf-8")
806
809
  if queue:
807
- (self.task / "queue.md").write_text("- [x] fixture\n")
810
+ (self.task / "queue.md").write_text("- [x] fixture\n", encoding="utf-8")
808
811
  self.g("add", "queue.md", work=self.task)
809
812
  self.g("commit", "-m", "terminal queue", work=self.task)
810
813
  a["queue"] = {"commit": self.g("rev-parse", "HEAD", work=self.task), "file": "queue.md"}
811
814
  self.acceptance = evidence / "acceptance.json"
812
- self.acceptance.write_text(json.dumps(a))
815
+ self.acceptance.write_text(json.dumps(a), encoding="utf-8")
813
816
 
814
817
  def complete(self, *args, success=True, cwd=None, acceptance=True):
815
818
  command_args = ["complete", "--receipt", self.receipt]
@@ -843,11 +846,11 @@ class CompletionTests(unittest.TestCase):
843
846
  alias = "git@github.com:test/project.git"
844
847
  self.g("config", "url." + alias + ".insteadOf", intended)
845
848
  self.allocate(); self.accept()
846
- self.assertEqual(json.loads(self.receipt.read_text())["remote_url"], {"literal": intended, "fetch": alias, "push": alias})
849
+ self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["remote_url"], {"literal": intended, "fetch": alias, "push": alias})
847
850
  result, _ = self.complete("--mode", "pr")
848
851
  self.assertEqual(result["status"], "PR")
849
852
  self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
850
- self.assertEqual(json.loads(self.data.read_text())["pushs"], 1)
853
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8"))["pushs"], 1)
851
854
 
852
855
  def test_remote_rewrite_after_allocation_is_rejected(self):
853
856
  self.allocate(); self.accept()
@@ -868,9 +871,9 @@ class CompletionTests(unittest.TestCase):
868
871
  destination = "https://github.com/test/other.git"
869
872
  self.g("config", "url." + destination + ".insteadOf", "https://github.com/test/project.git")
870
873
  # Reproduce a receipt the old literal-only allocation check accepted.
871
- receipt = json.loads(self.receipt.read_text())
874
+ receipt = json.loads(self.receipt.read_text(encoding="utf-8"))
872
875
  receipt["remote_url"].update(fetch=destination, push=destination)
873
- self.receipt.write_text(json.dumps(receipt))
876
+ self.receipt.write_text(json.dumps(receipt), encoding="utf-8")
874
877
  local_refs = self.g("show-ref")
875
878
  remote_refs = self.run_cmd(["git", "--git-dir", str(self.bare), "show-ref"]).stdout
876
879
  server = self.data.read_bytes()
@@ -891,17 +894,17 @@ class CompletionTests(unittest.TestCase):
891
894
  for _ in range(2):
892
895
  result, _ = self.complete("--mode", "auto")
893
896
  self.assertEqual(result["status"], "PENDING")
894
- d = json.loads(self.data.read_text())
897
+ d = json.loads(self.data.read_text(encoding="utf-8"))
895
898
  self.assertEqual((d["creates"], d["merges"], d["pushs"]), (1, 1, 1))
896
899
 
897
900
  def test_in_place_retains_ignored_data(self):
898
901
  self.allocate(); self.accept()
899
902
  (self.task / "ignored").mkdir()
900
- (self.task / "ignored" / "user.txt").write_text("retain me")
903
+ (self.task / "ignored" / "user.txt").write_text("retain me", encoding="utf-8")
901
904
  result, _ = self.complete("--writers-stopped")
902
905
  self.assertEqual(result["status"], "COMPLETE")
903
906
  self.assertEqual(self.g("branch", "--show-current"), "main")
904
- self.assertEqual((self.task / "ignored/user.txt").read_text(), "retain me")
907
+ self.assertEqual((self.task / "ignored/user.txt").read_text(encoding="utf-8"), "retain me")
905
908
  self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
906
909
  self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
907
910
  again, _ = self.complete("--writers-stopped", acceptance=False)
@@ -915,7 +918,7 @@ class CompletionTests(unittest.TestCase):
915
918
  result, r = self.complete("--writers-stopped", success=False)
916
919
  self.assertNotEqual(r.returncode, 0)
917
920
  self.assertFalse(self.task.exists())
918
- saved = json.loads(self.receipt.read_text())
921
+ saved = json.loads(self.receipt.read_text(encoding="utf-8"))
919
922
  recovered = self.receipt.parent / "custody" / ".devlyn/runs/fixture-run/final-report.md"
920
923
  self.assertEqual(hashlib.sha256(recovered.read_bytes()).hexdigest(), saved["files"][".devlyn/runs/fixture-run/final-report.md"]["sha256"])
921
924
  result, _ = self.complete("--writers-stopped", acceptance=False)
@@ -933,30 +936,30 @@ class CompletionTests(unittest.TestCase):
933
936
  if case == "unfinished": state["phases"]["verify"]["completed_at"] = None
934
937
  if case == "digest": state["phases"]["final_report"]["output_sha256"] = "0"*64
935
938
  if case == "source": state["phases"]["cleanup"]["post_sha"] = self.g("rev-parse", "main")
936
- (self.archive / "pipeline.state.json").write_text(json.dumps(state))
939
+ (self.archive / "pipeline.state.json").write_text(json.dumps(state), encoding="utf-8")
937
940
  with self.subTest(case=case):
938
941
  _, r = self.complete(success=False)
939
942
  self.assertNotEqual(r.returncode, 0)
940
943
  self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
941
- (self.archive / "pipeline.state.json").write_text(json.dumps(original))
944
+ (self.archive / "pipeline.state.json").write_text(json.dumps(original), encoding="utf-8")
942
945
  for value in ("typo", "", " auto"):
943
946
  self.g("config", "--local", "devlyn.completionMode", value)
944
947
  _, r = self.complete("--mode", "pr", success=False)
945
948
  self.assertNotEqual(r.returncode, 0)
946
949
  result, _ = self.complete("--local-only")
947
950
  self.assertEqual(result["status"], "LOCAL_ONLY")
948
- self.assertEqual(json.loads(self.data.read_text()).get("creates", 0), 0)
951
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("creates", 0), 0)
949
952
 
950
953
  def test_queue_only_and_unverified_descendant(self):
951
954
  self.allocate(); self.accept(pipeline=True, queue=True)
952
955
  result, _ = self.complete("--mode", "pr")
953
956
  self.assertEqual(result["status"], "PR")
954
- (self.task / "product").write_text("unverified")
957
+ (self.task / "product").write_text("unverified", encoding="utf-8")
955
958
  self.g("add", "product", work=self.task)
956
959
  self.g("commit", "-m", "unverified", work=self.task)
957
960
  _, r = self.complete(success=False)
958
961
  self.assertNotEqual(r.returncode, 0)
959
- self.assertEqual(json.loads(self.data.read_text()).get("merges", 0), 0)
962
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("merges", 0), 0)
960
963
 
961
964
  def test_interrupted_external_effects(self):
962
965
  self.allocate(); self.accept()
@@ -966,7 +969,7 @@ class CompletionTests(unittest.TestCase):
966
969
  self.assertNotEqual(r.returncode, 0)
967
970
  result, _ = self.complete("--writers-stopped")
968
971
  self.assertEqual(result["status"], "COMPLETE")
969
- d = json.loads(self.data.read_text())
972
+ d = json.loads(self.data.read_text(encoding="utf-8"))
970
973
  self.assertEqual((d["pushs"], d["creates"], d["merges"]), (1, 1, 1))
971
974
 
972
975
  def test_retains_unknown_dirty_locked_and_active_tree(self):
@@ -974,7 +977,7 @@ class CompletionTests(unittest.TestCase):
974
977
  for case in ("ignored", "dirty", "untracked", "locked", "cwd", "no-yield"):
975
978
  path = self.task / ("ignored/unknown" if case == "ignored" else "product" if case == "dirty" else "unknown")
976
979
  if case in {"ignored", "dirty", "untracked"}:
977
- path.parent.mkdir(exist_ok=True); path.write_text("do not delete")
980
+ path.parent.mkdir(exist_ok=True); path.write_text("do not delete", encoding="utf-8")
978
981
  if case == "locked": self.g("worktree", "lock", str(self.task))
979
982
  with self.subTest(case=case):
980
983
  _, r = self.complete(*([] if case == "no-yield" else ["--writers-stopped"]), cwd=self.task if case == "cwd" else None, success=False)
@@ -982,7 +985,7 @@ class CompletionTests(unittest.TestCase):
982
985
  self.assertTrue(self.task.exists())
983
986
  self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
984
987
  if case in {"ignored", "untracked"}: path.unlink()
985
- if case == "dirty": path.write_text("accepted\n")
988
+ if case == "dirty": path.write_text("accepted\n", encoding="utf-8")
986
989
  if case == "locked": self.g("worktree", "unlock", str(self.task))
987
990
 
988
991
  def test_no_adoption_and_remote_head_race(self):
@@ -993,34 +996,34 @@ class CompletionTests(unittest.TestCase):
993
996
  self.run_cmd(["git", "--git-dir", str(self.bare), "update-ref", "refs/heads/task/fixture", self.g("rev-parse", "main")])
994
997
  _, r = self.complete(success=False)
995
998
  self.assertNotEqual(r.returncode, 0)
996
- self.assertEqual(json.loads(self.data.read_text()).get("merges",0), 0)
999
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("merges",0), 0)
997
1000
 
998
1001
  def test_concurrent_completions(self):
999
1002
  self.allocate(); self.accept(); self.configure(pending=True)
1000
1003
  args = [sys.executable, str(Path(__file__).resolve()), "complete", "--receipt", str(self.receipt), "--acceptance", str(self.acceptance)]
1001
- procs = [subprocess.Popen(args, cwd=self.root, env=self.env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) for _ in range(2)]
1004
+ procs = [subprocess.Popen(args, cwd=self.root, env=self.env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8") for _ in range(2)]
1002
1005
  for proc in procs:
1003
1006
  out, err = proc.communicate(timeout=30)
1004
1007
  self.assertEqual(proc.returncode, 0, out+err)
1005
- d = json.loads(self.data.read_text())
1008
+ d = json.loads(self.data.read_text(encoding="utf-8"))
1006
1009
  self.assertEqual((d["pushs"], d["creates"], d["merges"]), (1,1,1))
1007
1010
 
1008
1011
  def test_custody_failure_and_evidence_tamper(self):
1009
1012
  self.allocate(linked=True); self.accept()
1010
1013
  bad = self.receipt.parent / "custody/.devlyn/checks.txt"
1011
1014
  bad.parent.mkdir(parents=True)
1012
- bad.write_text("partial interrupted copy")
1015
+ bad.write_text("partial interrupted copy", encoding="utf-8")
1013
1016
  _, r = self.complete("--writers-stopped", success=False)
1014
1017
  self.assertNotEqual(r.returncode, 0)
1015
1018
  self.assertTrue(self.task.exists())
1016
- self.assertEqual((self.task / ".devlyn/checks.txt").read_text(), "actual fixture check: accepted bytes\n")
1019
+ self.assertEqual((self.task / ".devlyn/checks.txt").read_text(encoding="utf-8"), "actual fixture check: accepted bytes\n")
1017
1020
  self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
1018
1021
  bad.unlink() # Owner repairs only the identified corrupt fixture copy.
1019
1022
  self.complete("--mode", "pr")
1020
- (self.task / ".devlyn/checks.txt").write_text("altered after acceptance")
1023
+ (self.task / ".devlyn/checks.txt").write_text("altered after acceptance", encoding="utf-8")
1021
1024
  _, r = self.complete("--mode", "auto", success=False)
1022
1025
  self.assertNotEqual(r.returncode, 0)
1023
- self.assertEqual(json.loads(self.data.read_text()).get("merges",0), 0)
1026
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("merges",0), 0)
1024
1027
 
1025
1028
  def test_actual_foreign_writer_and_registration(self):
1026
1029
  self.allocate(linked=True); self.accept()
@@ -1053,7 +1056,7 @@ class CompletionTests(unittest.TestCase):
1053
1056
  self.configure(merge_allowed=False)
1054
1057
  _, r = self.complete(success=False)
1055
1058
  self.assertNotEqual(r.returncode, 0)
1056
- self.assertEqual(json.loads(self.data.read_text()).get("pushs",0), 0)
1059
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("pushs",0), 0)
1057
1060
  self.complete("--mode", "pr")
1058
1061
  result, _ = self.complete()
1059
1062
  self.assertEqual(result["status"], "PR")
@@ -1068,15 +1071,15 @@ class CompletionTests(unittest.TestCase):
1068
1071
 
1069
1072
  def test_queue_commit_cannot_hide_product_changes(self):
1070
1073
  self.allocate(); self.accept(pipeline=True, queue=True)
1071
- (self.task / "product").write_text("unverified extra\n")
1074
+ (self.task / "product").write_text("unverified extra\n", encoding="utf-8")
1072
1075
  self.g("add", "product", work=self.task)
1073
1076
  self.g("commit", "--amend", "--no-edit", work=self.task)
1074
- a = json.loads(self.acceptance.read_text())
1077
+ a = json.loads(self.acceptance.read_text(encoding="utf-8"))
1075
1078
  a["queue"]["commit"] = self.g("rev-parse", "HEAD", work=self.task)
1076
- self.acceptance.write_text(json.dumps(a))
1079
+ self.acceptance.write_text(json.dumps(a), encoding="utf-8")
1077
1080
  _, r = self.complete(success=False)
1078
1081
  self.assertNotEqual(r.returncode, 0)
1079
- self.assertEqual(json.loads(self.data.read_text()).get("pushs",0), 0)
1082
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("pushs",0), 0)
1080
1083
 
1081
1084
  def test_pipeline_sealed_process_evidence(self):
1082
1085
  self.allocate(); self.accept(pipeline=True)
@@ -1088,15 +1091,15 @@ class CompletionTests(unittest.TestCase):
1088
1091
  results = {"commands": evidence_module["bound_carrier_summary_commands"](self.task, carrier), "process_evidence": carrier}
1089
1092
  self.state["phases"]["verify"]["round"] = 0
1090
1093
  self.state["process_evidence"] = [carrier]
1091
- (self.archive / "pipeline.state.json").write_text(json.dumps(self.state))
1092
- (self.archive / "spec-verify.results.json").write_text(json.dumps(results))
1094
+ (self.archive / "pipeline.state.json").write_text(json.dumps(self.state), encoding="utf-8")
1095
+ (self.archive / "spec-verify.results.json").write_text(json.dumps(results), encoding="utf-8")
1093
1096
  shutil.move(str(self.task / ".devlyn/process-evidence"), str(self.archive / "process-evidence"))
1094
1097
  stdout = self.archive / carrier["streams"][0]["stdout"]["path"].removeprefix(".devlyn/")
1095
1098
  before = stdout.read_bytes()
1096
- stdout.write_text("tampered")
1099
+ stdout.write_text("tampered", encoding="utf-8")
1097
1100
  _, r = self.complete(success=False)
1098
1101
  self.assertNotEqual(r.returncode, 0)
1099
- self.assertEqual(json.loads(self.data.read_text()).get("pushs",0), 0)
1102
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("pushs",0), 0)
1100
1103
  stdout.write_bytes(before)
1101
1104
  result, _ = self.complete("--mode", "pr")
1102
1105
  self.assertEqual(result["status"], "PR")
@@ -1105,7 +1108,7 @@ class CompletionTests(unittest.TestCase):
1105
1108
  self.allocate(); self.accept()
1106
1109
  user_file = self.task / "ignored/user.txt"
1107
1110
  user_file.parent.mkdir()
1108
- user_file.write_text("retained user data")
1111
+ user_file.write_text("retained user data", encoding="utf-8")
1109
1112
  self.configure(interrupt_delete=True)
1110
1113
  _, r = self.complete("--writers-stopped", success=False)
1111
1114
  self.assertNotEqual(r.returncode, 0)
@@ -1113,13 +1116,13 @@ class CompletionTests(unittest.TestCase):
1113
1116
  other = self.root / "advance"
1114
1117
  self.run_cmd(["git", "clone", str(self.bare), str(other)])
1115
1118
  (other / "ignored").mkdir()
1116
- (other / "ignored/user.txt").write_text("new upstream tracked file")
1119
+ (other / "ignored/user.txt").write_text("new upstream tracked file", encoding="utf-8")
1117
1120
  self.g("add", "-f", "ignored/user.txt", work=other)
1118
1121
  self.g("commit", "-m", "upstream collision", work=other)
1119
1122
  self.g("push", "origin", "main", work=other)
1120
1123
  _, r = self.complete("--writers-stopped", acceptance=False, success=False)
1121
1124
  self.assertNotEqual(r.returncode, 0)
1122
- self.assertEqual(user_file.read_text(), "retained user data")
1125
+ self.assertEqual(user_file.read_text(encoding="utf-8"), "retained user data")
1123
1126
  self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
1124
1127
 
1125
1128
  def test_replaced_tree_and_unsafe_custody(self):
@@ -1138,21 +1141,21 @@ class CompletionTests(unittest.TestCase):
1138
1141
  self.assertNotEqual(r.returncode, 0)
1139
1142
  self.assertIn("replaced", json.loads(r.stdout)["reason"])
1140
1143
  self.assertTrue(self.task.exists())
1141
- self.assertEqual(json.loads(self.data.read_text()).get("pushs",0), 0)
1144
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("pushs",0), 0)
1142
1145
 
1143
1146
  def test_spec_source_and_report_binding(self):
1144
1147
  self.allocate(); self.accept(pipeline=True, spec_expected={"verification_commands": []})
1145
1148
  report = self.archive / "final-report.md"
1146
1149
  original = report.read_bytes()
1147
- report.write_text("<!-- devlyn:final-report run_id=another-run -->\nBody\n")
1150
+ report.write_text("<!-- devlyn:final-report run_id=another-run -->\nBody\n", encoding="utf-8")
1148
1151
  self.state["phases"]["final_report"]["output_sha256"] = hashlib.sha256(report.read_bytes()).hexdigest()
1149
- (self.archive / "pipeline.state.json").write_text(json.dumps(self.state))
1152
+ (self.archive / "pipeline.state.json").write_text(json.dumps(self.state), encoding="utf-8")
1150
1153
  _, r = self.complete(success=False)
1151
1154
  self.assertNotEqual(r.returncode, 0)
1152
- self.assertEqual(json.loads(self.data.read_text()).get("pushs",0), 0)
1155
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("pushs",0), 0)
1153
1156
  report.write_bytes(original)
1154
1157
  self.state["phases"]["final_report"]["output_sha256"] = hashlib.sha256(original).hexdigest()
1155
- (self.archive / "pipeline.state.json").write_text(json.dumps(self.state))
1158
+ (self.archive / "pipeline.state.json").write_text(json.dumps(self.state), encoding="utf-8")
1156
1159
  result, _ = self.complete("--mode", "pr")
1157
1160
  self.assertEqual(result["status"], "PR")
1158
1161
 
@@ -1162,8 +1165,9 @@ class CompletionTests(unittest.TestCase):
1162
1165
  _, r = self.complete(success=False)
1163
1166
  self.assertNotEqual(r.returncode, 0)
1164
1167
  self.assertIn("implement process evidence", json.loads(r.stdout)["reason"])
1165
- self.assertEqual(json.loads(self.data.read_text()).get("pushs",0), 0)
1168
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("pushs",0), 0)
1166
1169
 
1167
1170
 
1168
1171
  if __name__ == "__main__":
1172
+ runpy.run_path(str(Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
1169
1173
  sys.exit(main())
@@ -474,7 +474,7 @@ def read_findings(devlyn: pathlib.Path) -> tuple[list[dict[str, Any]], dict[str,
474
474
  )
475
475
  if (devlyn / "pipeline.state.json").is_file():
476
476
  try:
477
- state = loads_strict_json((devlyn / "pipeline.state.json").read_text())
477
+ state = loads_strict_json((devlyn / "pipeline.state.json").read_text(encoding="utf-8"))
478
478
  required = JUDGE_ROLE_EVIDENCE["required_roles"](state)
479
479
  if rank(source_verdicts.get("mechanical")) >= 2:
480
480
  required = []
@@ -1663,21 +1663,21 @@ def self_test() -> int:
1663
1663
  for malformed in (None, {}, {"roles": {}}):
1664
1664
  bad_state = loads_strict_json(original_state)
1665
1665
  bad_state["role_resolution"] = malformed
1666
- (devlyn / "pipeline.state.json").write_text(json.dumps(bad_state))
1666
+ (devlyn / "pipeline.state.json").write_text(json.dumps(bad_state), encoding="utf-8")
1667
1667
  result = subprocess.run([sys.executable, str(pathlib.Path(__file__).resolve()),
1668
- "--devlyn-dir", str(devlyn), "--write-state"], capture_output=True, text=True)
1668
+ "--devlyn-dir", str(devlyn), "--write-state"], capture_output=True, text=True, encoding="utf-8")
1669
1669
  assert result.returncode == 0 and "Traceback" not in result.stderr, result.stderr
1670
- persisted = loads_strict_json((devlyn / "pipeline.state.json").read_text())
1670
+ persisted = loads_strict_json((devlyn / "pipeline.state.json").read_text(encoding="utf-8"))
1671
1671
  assert persisted["phases"]["verify"]["verdict"] == "BLOCKED"
1672
- assert "verify-role-resolution-invalid" in (devlyn / "verify-merged.findings.jsonl").read_text()
1672
+ assert "verify-role-resolution-invalid" in (devlyn / "verify-merged.findings.jsonl").read_text(encoding="utf-8")
1673
1673
  (devlyn / "pipeline.state.json").write_bytes(original_state)
1674
1674
  different_primary = {"engine": "codex", "phases": {"verify": {"engine": "claude"}}}
1675
- (devlyn / "pipeline.state.json").write_text(json.dumps(different_primary))
1676
- (devlyn / "verify.primary.timeout.json").write_text(json.dumps({"engine": "claude", "budget_seconds": 600}))
1675
+ (devlyn / "pipeline.state.json").write_text(json.dumps(different_primary), encoding="utf-8")
1676
+ (devlyn / "verify.primary.timeout.json").write_text(json.dumps({"engine": "claude", "budget_seconds": 600}), encoding="utf-8")
1677
1677
  marker, violation = read_primary_timeout_marker(devlyn)
1678
1678
  assert marker is not None and violation is None
1679
1679
  different_primary["phases"]["verify"]["engine"] = None
1680
- (devlyn / "pipeline.state.json").write_text(json.dumps(different_primary))
1680
+ (devlyn / "pipeline.state.json").write_text(json.dumps(different_primary), encoding="utf-8")
1681
1681
  assert read_primary_timeout_marker(devlyn)[1] is not None
1682
1682
  (devlyn / "verify.primary.timeout.json").unlink()
1683
1683
  (devlyn / "pipeline.state.json").write_bytes(original_state)
@@ -3433,4 +3433,5 @@ def main() -> int:
3433
3433
 
3434
3434
 
3435
3435
  if __name__ == "__main__":
3436
+ runpy.run_path(str(pathlib.Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
3436
3437
  raise SystemExit(main())