devlyn-cli 4.3.17 → 4.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/bin/skill-history.json +9 -0
- package/config/skills/_shared/adapters/claude.md +15 -0
- package/config/skills/_shared/adapters/codex.md +10 -0
- package/config/skills/_shared/invocation-receipt.py +179 -9
- package/config/skills/_shared/platform-support.py +102 -1
- package/config/skills/_shared/unattended-tools.py +206 -0
- package/config/skills/devlyn-ideate/SKILL.md +1 -1
- package/config/skills/devlyn-ideate/scripts/acceptance.py +307 -1
- package/config/skills/devlyn-ideate/scripts/queue.py +24 -11
- package/native/claude/.claude-plugin/plugin.json +1 -1
- package/native/claude/skills/_shared/adapters/claude.md +15 -0
- package/native/claude/skills/_shared/adapters/codex.md +10 -0
- package/native/claude/skills/_shared/invocation-receipt.py +179 -9
- package/native/claude/skills/_shared/platform-support.py +102 -1
- package/native/claude/skills/_shared/unattended-tools.py +206 -0
- package/native/claude/skills/devlyn-ideate/SKILL.md +1 -1
- package/native/claude/skills/devlyn-ideate/scripts/acceptance.py +307 -1
- package/native/claude/skills/devlyn-ideate/scripts/queue.py +24 -11
- package/native/codex/.agents/plugins/marketplace.json +1 -1
- package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +1 -1
- package/native/codex/plugins/devlyn/skills/_shared/adapters/claude.md +15 -0
- package/native/codex/plugins/devlyn/skills/_shared/adapters/codex.md +10 -0
- package/native/codex/plugins/devlyn/skills/_shared/invocation-receipt.py +179 -9
- package/native/codex/plugins/devlyn/skills/_shared/platform-support.py +102 -1
- package/native/codex/plugins/devlyn/skills/_shared/unattended-tools.py +206 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +1 -1
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py +307 -1
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +24 -11
- package/package.json +1 -1
- package/release.json +44 -29
|
@@ -17,6 +17,7 @@ import hashlib
|
|
|
17
17
|
import json
|
|
18
18
|
import os
|
|
19
19
|
from pathlib import Path
|
|
20
|
+
import platform
|
|
20
21
|
import re
|
|
21
22
|
import runpy
|
|
22
23
|
import subprocess
|
|
@@ -454,7 +455,17 @@ def derive(packet, submission, result):
|
|
|
454
455
|
|
|
455
456
|
|
|
456
457
|
class AcceptanceTests(unittest.TestCase):
|
|
458
|
+
def macho_header(self):
|
|
459
|
+
cpu = {"arm64": 0x0100000c, "x86_64": 0x01000007}[platform.machine()]
|
|
460
|
+
return bytes.fromhex("cffaedfe") + cpu.to_bytes(4, "little") + bytes(24)
|
|
461
|
+
|
|
457
462
|
def setUp(self):
|
|
463
|
+
if sys.platform != "darwin" and os.name != "nt":
|
|
464
|
+
# Darwin argv tests emulate an unquarantined script; Linux has a different xattr namespace.
|
|
465
|
+
import errno
|
|
466
|
+
from unittest import mock
|
|
467
|
+
self.enterContext(mock.patch.object(errno, "ENOATTR", errno.ENODATA, create=True))
|
|
468
|
+
self.enterContext(mock.patch.object(os, "getxattr", side_effect=OSError(errno.ENOATTR, "attribute absent")))
|
|
458
469
|
self.temp = tempfile.TemporaryDirectory(prefix="devlyn-acceptance ")
|
|
459
470
|
self.addCleanup(self.temp.cleanup)
|
|
460
471
|
self.root = Path(self.temp.name).resolve()
|
|
@@ -665,6 +676,300 @@ class AcceptanceTests(unittest.TestCase):
|
|
|
665
676
|
self.assertNotIn("reused_from", result["commands"][0])
|
|
666
677
|
self.assertEqual(((self.work / ".devlyn/count").read_text(), result["verdict"]), ("2", "ACCEPTED"))
|
|
667
678
|
|
|
679
|
+
@unittest.skipIf(os.name == "nt", "requires POSIX executable permissions and paths")
|
|
680
|
+
def test_darwin_runs_shebang_programs_through_their_interpreter(self):
|
|
681
|
+
"""Prediction: Darwin's 512-byte shebang rules preserve argv and candidate spelling; unsafe rewrites
|
|
682
|
+
preserve the original command and CPython's PATH search without a shell fallback."""
|
|
683
|
+
from types import SimpleNamespace
|
|
684
|
+
from unittest import mock
|
|
685
|
+
resolve = shared("platform-support")["native_argv"]
|
|
686
|
+
with tempfile.TemporaryDirectory(prefix="devlyn-shebang-") as temporary, \
|
|
687
|
+
mock.patch.object(platform, "machine", return_value="arm64"), \
|
|
688
|
+
mock.patch.dict(resolve.__globals__, {"sys": SimpleNamespace(platform="darwin")}):
|
|
689
|
+
root = Path(temporary).resolve()
|
|
690
|
+
interpreter = root / "interpreter"
|
|
691
|
+
interpreter.write_bytes(self.macho_header()); interpreter.chmod(0o755)
|
|
692
|
+
p = os.fsencode(interpreter)
|
|
693
|
+
script = root / "s"
|
|
694
|
+
cases = [
|
|
695
|
+
("1", b"#!" + p + b"\n", []),
|
|
696
|
+
("2", b"#! " + p + b" a b\tc \n", ["a", "b", "c"]),
|
|
697
|
+
("3", b"#!" + p + b" a#b c\n", ["a"]),
|
|
698
|
+
("4", b"#!" + p + b" a # c\n", ["a"]),
|
|
699
|
+
("5", b"#!" + p + b" #", []),
|
|
700
|
+
("6", b"#!" + p + b" a\r\n", ["a\r"]),
|
|
701
|
+
("7", b"#!" + p + b"\r\n", None),
|
|
702
|
+
("8a", b"#!" + p, None), ("8b", b"#!" + p + b" a", None),
|
|
703
|
+
("9a", b"#! \n", None), ("9b", b"#! #x\n", None),
|
|
704
|
+
("10", b"#!" + p + b" a\0b c\n", None),
|
|
705
|
+
("11", b"#!interpreter\n", None),
|
|
706
|
+
("14", b"#!/usr/bin/env P a b\n", ["/usr/bin/env", "P", "a", "b"]),
|
|
707
|
+
("15a", b"#!" + p + b" " * (509 - len(p)) + b"\n", []),
|
|
708
|
+
("15b", b"#!" + p + b" " * (510 - len(p)) + b"\n", None),
|
|
709
|
+
("17", b"plain text\n", None),
|
|
710
|
+
("blank-limit", b"#!" + b" " * 510 + b"\n", None),
|
|
711
|
+
]
|
|
712
|
+
for vector, header, arguments in cases:
|
|
713
|
+
with self.subTest(vector=vector):
|
|
714
|
+
if vector == "14" and sys.platform != "darwin":
|
|
715
|
+
self.skipTest("/usr/bin/env is a Mach-O image only on macOS")
|
|
716
|
+
script.write_bytes(header); script.chmod(0o755)
|
|
717
|
+
expected = [str(script), "X"] if arguments is None else [str(interpreter), *arguments, str(script), "X"]
|
|
718
|
+
if vector == "14":
|
|
719
|
+
expected = [*arguments, str(script), "X"]
|
|
720
|
+
self.assertEqual(resolve([str(script), "X"]), expected)
|
|
721
|
+
|
|
722
|
+
script.write_bytes(b"#!" + p + b"\n")
|
|
723
|
+
for mode, body in ((0o644, b"binary"), (0o755, b"#!/bin/sh\n"), (0o755, b"")):
|
|
724
|
+
with self.subTest(vector="12/13", mode=mode, body=body):
|
|
725
|
+
interpreter.write_bytes(body); interpreter.chmod(mode)
|
|
726
|
+
self.assertEqual(resolve([str(script), "X"]), [str(script), "X"])
|
|
727
|
+
interpreter.write_bytes(self.macho_header()); interpreter.chmod(0o755)
|
|
728
|
+
for mode in (0o4755, 0o2755, 0o644):
|
|
729
|
+
with self.subTest(vector="18/17", mode=mode):
|
|
730
|
+
script.chmod(mode)
|
|
731
|
+
if script.stat().st_mode & 0o7777 != mode:
|
|
732
|
+
self.skipTest("filesystem strips set-id bits")
|
|
733
|
+
self.assertEqual(resolve([str(script), "X"]), [str(script), "X"])
|
|
734
|
+
script.chmod(0o755)
|
|
735
|
+
folder = root / "D"; folder.mkdir()
|
|
736
|
+
link = root / "link"; link.symlink_to(script)
|
|
737
|
+
for candidate in (str(root) + "/D/../x//s", str(link), "./s"):
|
|
738
|
+
with self.subTest(vector="16/23", candidate=candidate):
|
|
739
|
+
(root / "x").mkdir(exist_ok=True)
|
|
740
|
+
(root / "x/s").write_bytes(script.read_bytes()); (root / "x/s").chmod(0o755)
|
|
741
|
+
self.assertEqual(resolve([candidate, "X"], cwd=root), [str(interpreter), candidate, "X"])
|
|
742
|
+
|
|
743
|
+
a, b = root / "a", root / "b"
|
|
744
|
+
a.mkdir(); b.mkdir()
|
|
745
|
+
second = b / "s"; second.write_bytes(script.read_bytes()); second.chmod(0o755)
|
|
746
|
+
first = a / "s"; first.mkdir()
|
|
747
|
+
with mock.patch.dict(os.environ, {"PATH": f"{a}:{b}"}):
|
|
748
|
+
self.assertEqual(resolve(["s", "X"], cwd=root), [str(interpreter), str(second), "X"])
|
|
749
|
+
first.rmdir(); first.write_bytes(script.read_bytes()); first.chmod(0o644)
|
|
750
|
+
self.assertEqual(resolve(["s", "X"], cwd=root), [str(interpreter), str(second), "X"])
|
|
751
|
+
first.write_bytes(b"#!/missing-interpreter\n"); first.chmod(0o755)
|
|
752
|
+
self.assertEqual(resolve(["s", "X"], cwd=root), ["s", "X"]) # 19: CPython must retry b/s.
|
|
753
|
+
for path, candidate in ((f"missing:{b}", str(second)), (f"missing::{b}", "s"), (f":{b}", "s"),
|
|
754
|
+
(f"b:{b}", "b/s")):
|
|
755
|
+
with self.subTest(vector="21", path=path), mock.patch.dict(os.environ, {"PATH": path}):
|
|
756
|
+
self.assertEqual(resolve(["s", "X"], cwd=root), [str(interpreter), candidate, "X"])
|
|
757
|
+
for entry in ("/" + "x" * 1021, "/" + "é" * 511):
|
|
758
|
+
with self.subTest(vector="22", entry=entry), mock.patch.dict(os.environ, {"PATH": f"{entry}:{b}"}):
|
|
759
|
+
self.assertEqual(resolve(["s", "X"], cwd=root), ["s", "X"])
|
|
760
|
+
with mock.patch.dict(os.environ, {"PATH": str(b)}), mock.patch.object(os, "getcwd", return_value=str(root)):
|
|
761
|
+
self.assertEqual(resolve(["./s", "X"]), [str(interpreter), "./s", "X"])
|
|
762
|
+
with mock.patch.object(os, "getcwd", side_effect=OSError("cwd unavailable")):
|
|
763
|
+
self.assertEqual(resolve(["s", "X"]), ["s", "X"])
|
|
764
|
+
|
|
765
|
+
@unittest.skipIf(os.name == "nt", "requires POSIX executable permissions and paths")
|
|
766
|
+
def test_darwin_interpreter_eligibility(self):
|
|
767
|
+
"""Prediction: only executable, regular, non-set-id Mach-O interpreters with a native CPU and subtype rewrite;
|
|
768
|
+
capability bits are ignored, arm64e is accepted, and x86_64h and unknown subtypes are declined."""
|
|
769
|
+
import shutil
|
|
770
|
+
import stat
|
|
771
|
+
from types import SimpleNamespace
|
|
772
|
+
from unittest import mock
|
|
773
|
+
helper = shared("platform-support")
|
|
774
|
+
resolve = helper["native_argv"]
|
|
775
|
+
prefix = helper["darwin_script_argv"]
|
|
776
|
+
with tempfile.TemporaryDirectory(prefix="devlyn-interpreter-") as temporary, \
|
|
777
|
+
mock.patch.object(platform, "machine", return_value="arm64"), \
|
|
778
|
+
mock.patch.dict(resolve.__globals__, {"sys": SimpleNamespace(platform="darwin")}):
|
|
779
|
+
root = Path(temporary).resolve()
|
|
780
|
+
interpreter = root / "interpreter"
|
|
781
|
+
script = root / "s"
|
|
782
|
+
script.write_bytes(b"#!" + os.fsencode(interpreter) + b"\n"); script.chmod(0o755)
|
|
783
|
+
interpreter.mkdir()
|
|
784
|
+
self.assertIsNone(prefix(str(script), str(root)))
|
|
785
|
+
interpreter.rmdir()
|
|
786
|
+
for body in (b"plain text\n", b"", b"#!/bin/sh\n", b"\xcf\xfa\xed", b"\x7fELF"):
|
|
787
|
+
with self.subTest(body=body):
|
|
788
|
+
interpreter.write_bytes(body); interpreter.chmod(0o755)
|
|
789
|
+
self.assertIsNone(prefix(str(script), str(root)))
|
|
790
|
+
self.assertEqual(resolve([str(script), "X"]), [str(script), "X"])
|
|
791
|
+
for machine, cpu in (("arm64", 0x0100000c), ("x86_64", 0x01000007)):
|
|
792
|
+
for magic in ("feedface", "feedfacf", "cefaedfe", "cffaedfe",
|
|
793
|
+
"cafebabe", "bebafeca", "cafebabf", "bfbafeca"):
|
|
794
|
+
order = "little" if magic in ("cefaedfe", "cffaedfe", "bebafeca", "bfbafeca") else "big"
|
|
795
|
+
fat = magic in ("cafebabe", "bebafeca", "cafebabf", "bfbafeca")
|
|
796
|
+
if fat:
|
|
797
|
+
size = 32 if magic in ("cafebabf", "bfbafeca") else 20
|
|
798
|
+
else:
|
|
799
|
+
size = 32 if magic in ("feedfacf", "cffaedfe") else 28
|
|
800
|
+
for image_cpu, subtype, accepted in ((0x01000007, 0, False), (0x01000007, 3, True), (0x01000007, 8, False),
|
|
801
|
+
(0x0100000c, 0, True), (0x0100000c, 1, True),
|
|
802
|
+
(0x0100000c, 0x80000002, True), (0x0100000c, 9, False)):
|
|
803
|
+
image = bytes.fromhex(magic)
|
|
804
|
+
if fat:
|
|
805
|
+
image += (1).to_bytes(4, order)
|
|
806
|
+
image += image_cpu.to_bytes(4, order) + subtype.to_bytes(4, order) + bytes(size - (8 if fat else 12))
|
|
807
|
+
for truncated in (False, True):
|
|
808
|
+
with self.subTest(machine=machine, magic=magic, cpu=image_cpu, subtype=subtype, truncated=truncated), \
|
|
809
|
+
mock.patch.object(platform, "machine", return_value=machine):
|
|
810
|
+
interpreter.write_bytes(image[:-1] if truncated else image); interpreter.chmod(0o755)
|
|
811
|
+
compatible = cpu == image_cpu and accepted
|
|
812
|
+
expected = [str(interpreter), str(script)] if compatible and not truncated else None
|
|
813
|
+
self.assertEqual(prefix(str(script), str(root)), expected)
|
|
814
|
+
self.assertEqual(resolve([str(script), "X"]), [*expected, "X"] if expected else [str(script), "X"])
|
|
815
|
+
if fat:
|
|
816
|
+
# A later native slice must also be found in a universal image.
|
|
817
|
+
universal = bytes.fromhex(magic) + (2).to_bytes(4, order)
|
|
818
|
+
universal += b"".join(value.to_bytes(4, order) + subtype.to_bytes(4, order) + bytes(size - 8)
|
|
819
|
+
for value, subtype in ((0x01000007, 3), (0x0100000c, 0)))
|
|
820
|
+
with mock.patch.object(platform, "machine", return_value=machine):
|
|
821
|
+
interpreter.write_bytes(universal)
|
|
822
|
+
self.assertEqual(prefix(str(script), str(root)), [str(interpreter), str(script)])
|
|
823
|
+
interpreter.write_bytes(self.macho_header())
|
|
824
|
+
interpreter.chmod(0o644)
|
|
825
|
+
self.assertIsNone(prefix(str(script), str(root)))
|
|
826
|
+
link = root / "link"; link.symlink_to(interpreter)
|
|
827
|
+
for bit in (stat.S_ISUID, stat.S_ISGID):
|
|
828
|
+
with self.subTest(set_id=oct(bit)):
|
|
829
|
+
interpreter.chmod(0o755 | bit)
|
|
830
|
+
if not interpreter.stat().st_mode & bit:
|
|
831
|
+
self.skipTest(f"filesystem or sandbox strips interpreter set-id bit {oct(bit)}")
|
|
832
|
+
for target in (interpreter, link):
|
|
833
|
+
script.write_bytes(b"#!" + os.fsencode(target) + b"\n")
|
|
834
|
+
self.assertIsNone(prefix(str(script), str(root)))
|
|
835
|
+
self.assertEqual(resolve([str(script), "X"]), [str(script), "X"])
|
|
836
|
+
for kind in ("copy", "symlink"):
|
|
837
|
+
with self.subTest(shell=kind):
|
|
838
|
+
if sys.platform != "darwin":
|
|
839
|
+
self.skipTest("/bin/sh is a Mach-O image only on macOS")
|
|
840
|
+
shell = root / kind
|
|
841
|
+
if kind == "copy":
|
|
842
|
+
shutil.copyfile("/bin/sh", shell); shell.chmod(0o755)
|
|
843
|
+
else:
|
|
844
|
+
shell.symlink_to("/bin/sh")
|
|
845
|
+
script.write_bytes(b"#!" + os.fsencode(shell) + b"\n")
|
|
846
|
+
self.assertEqual(prefix(str(script), str(root)), [str(shell), str(script)])
|
|
847
|
+
self.assertEqual(resolve([str(script), "X"]), [str(shell), str(script), "X"])
|
|
848
|
+
|
|
849
|
+
@unittest.skipIf(os.name == "nt", "requires POSIX executable permissions and paths")
|
|
850
|
+
def test_darwin_relative_candidates_and_path_errors(self):
|
|
851
|
+
"""Prediction: a cwd join exceeding PATH_MAX still rewrites; directory search errors stop PATH inspection."""
|
|
852
|
+
import contextlib
|
|
853
|
+
import errno
|
|
854
|
+
from types import SimpleNamespace
|
|
855
|
+
from unittest import mock
|
|
856
|
+
helper = shared("platform-support")
|
|
857
|
+
resolve = helper["native_argv"]
|
|
858
|
+
prefix = helper["darwin_script_argv"]
|
|
859
|
+
with tempfile.TemporaryDirectory(prefix="devlyn-relative-") as temporary, \
|
|
860
|
+
mock.patch.object(platform, "machine", return_value="arm64"), \
|
|
861
|
+
mock.patch.dict(resolve.__globals__, {"sys": SimpleNamespace(platform="darwin")}), \
|
|
862
|
+
contextlib.ExitStack() as handles:
|
|
863
|
+
root = Path(temporary).resolve()
|
|
864
|
+
interpreter = root / "interpreter"
|
|
865
|
+
interpreter.write_bytes(self.macho_header()); interpreter.chmod(0o755)
|
|
866
|
+
body = b"#!" + os.fsencode(interpreter) + b"\n"
|
|
867
|
+
directory = "d" * 100
|
|
868
|
+
candidate = directory + "/s"
|
|
869
|
+
cwd = str(root)
|
|
870
|
+
fd = os.open(cwd, os.O_RDONLY | os.O_DIRECTORY)
|
|
871
|
+
handles.callback(os.close, fd)
|
|
872
|
+
for _ in range(10):
|
|
873
|
+
os.mkdir(directory, dir_fd=fd)
|
|
874
|
+
child_fd = os.open(directory, os.O_RDONLY | os.O_DIRECTORY, dir_fd=fd)
|
|
875
|
+
handles.callback(os.close, child_fd)
|
|
876
|
+
with os.fdopen(os.open("s", os.O_WRONLY | os.O_CREAT, 0o755, dir_fd=child_fd), "wb") as stream:
|
|
877
|
+
stream.write(body)
|
|
878
|
+
with mock.patch.dict(os.environ, {"PATH": directory}):
|
|
879
|
+
self.assertEqual(prefix("s", cwd), [str(interpreter), candidate])
|
|
880
|
+
self.assertEqual(resolve(["s", "X"], cwd=cwd), [str(interpreter), candidate, "X"])
|
|
881
|
+
self.assertEqual(prefix("./" + candidate, cwd), [str(interpreter), "./" + candidate])
|
|
882
|
+
if len(os.fsencode(os.path.join(cwd, candidate))) > 1024:
|
|
883
|
+
break
|
|
884
|
+
cwd = os.path.join(cwd, directory)
|
|
885
|
+
fd = child_fd
|
|
886
|
+
else:
|
|
887
|
+
self.fail("fixture must exceed Darwin PATH_MAX")
|
|
888
|
+
if sys.platform == "darwin":
|
|
889
|
+
with self.assertRaises(OSError) as caught:
|
|
890
|
+
os.stat(os.path.join(cwd, candidate))
|
|
891
|
+
self.assertEqual(caught.exception.errno, errno.ENAMETOOLONG)
|
|
892
|
+
|
|
893
|
+
blocked = root / "blocked"; blocked.mkdir()
|
|
894
|
+
first = blocked / "s"; first.write_bytes(body); first.chmod(0o755)
|
|
895
|
+
second = root / "s"; second.write_bytes(body); second.chmod(0o755)
|
|
896
|
+
with mock.patch.dict(os.environ, {"PATH": f"{blocked}:{root}"}):
|
|
897
|
+
self.assertEqual(prefix("s", str(root)), [str(interpreter), str(first)])
|
|
898
|
+
blocked.chmod(0o600)
|
|
899
|
+
try:
|
|
900
|
+
if os.geteuid() == 0:
|
|
901
|
+
self.skipTest("root bypasses directory search permission")
|
|
902
|
+
with self.assertRaises(PermissionError):
|
|
903
|
+
first.stat()
|
|
904
|
+
self.assertIsNone(prefix("s", str(root)))
|
|
905
|
+
self.assertEqual(resolve(["s", "X"], cwd=root), ["s", "X"])
|
|
906
|
+
finally:
|
|
907
|
+
blocked.chmod(0o700)
|
|
908
|
+
# ENAMETOOLONG is not an absent candidate, even if a later PATH entry works.
|
|
909
|
+
with mock.patch.dict(os.environ, {"PATH": f"{'x' * 256}:{root}"}):
|
|
910
|
+
self.assertIsNone(prefix("s", str(root)))
|
|
911
|
+
|
|
912
|
+
@unittest.skipUnless(sys.platform == "darwin", "requires macOS quarantine attributes")
|
|
913
|
+
def test_darwin_script_quarantine(self):
|
|
914
|
+
"""Prediction: quarantine presence (including empty values and symlink targets) and xattr errors decline."""
|
|
915
|
+
import errno
|
|
916
|
+
from types import SimpleNamespace
|
|
917
|
+
from unittest import mock
|
|
918
|
+
helper = shared("platform-support")
|
|
919
|
+
resolve = helper["native_argv"]
|
|
920
|
+
prefix = helper["darwin_script_argv"]
|
|
921
|
+
with tempfile.TemporaryDirectory(prefix="devlyn-quarantine-") as temporary, \
|
|
922
|
+
mock.patch.object(platform, "machine", return_value="arm64"), \
|
|
923
|
+
mock.patch.dict(resolve.__globals__, {"sys": SimpleNamespace(platform="darwin")}):
|
|
924
|
+
root = Path(temporary).resolve()
|
|
925
|
+
interpreter = root / "interpreter"
|
|
926
|
+
interpreter.write_bytes(self.macho_header()); interpreter.chmod(0o755)
|
|
927
|
+
script = root / "s"
|
|
928
|
+
script.write_bytes(b"#!" + os.fsencode(interpreter) + b"\n"); script.chmod(0o755)
|
|
929
|
+
link = root / "link"; link.symlink_to(script)
|
|
930
|
+
for value in (None, "0083;00000000;Test;", ""):
|
|
931
|
+
if value is not None:
|
|
932
|
+
if hasattr(os, "setxattr"):
|
|
933
|
+
os.setxattr(script, "com.apple.quarantine", value.encode())
|
|
934
|
+
else:
|
|
935
|
+
subprocess.run(["/usr/bin/xattr", "-w", "com.apple.quarantine", value, str(script)], check=True)
|
|
936
|
+
for target in (script, link):
|
|
937
|
+
with self.subTest(value=value, target=target.name):
|
|
938
|
+
expected = [str(interpreter), str(target)] if value is None else None
|
|
939
|
+
self.assertEqual(prefix(str(target), str(root)), expected)
|
|
940
|
+
self.assertEqual(resolve([str(target), "X"]), [*expected, "X"] if expected else [str(target), "X"])
|
|
941
|
+
# Exercise the os.getxattr branch as well, including fail-closed errors.
|
|
942
|
+
for error in (errno.ENOATTR, errno.EACCES, errno.ENOTSUP, errno.EIO):
|
|
943
|
+
with self.subTest(error=error), \
|
|
944
|
+
mock.patch.object(os, "getxattr", create=True, side_effect=OSError(error, os.strerror(error))):
|
|
945
|
+
expected = [str(interpreter), str(script)] if error == errno.ENOATTR else None
|
|
946
|
+
self.assertEqual(prefix(str(script), str(root)), expected)
|
|
947
|
+
with mock.patch.object(os, "getxattr", create=True, return_value=b""):
|
|
948
|
+
self.assertIsNone(prefix(str(script), str(root)))
|
|
949
|
+
|
|
950
|
+
@unittest.skipIf(os.name == "nt", "requires POSIX executable permissions and paths")
|
|
951
|
+
def test_darwin_path_unset_uses_cpython_default(self):
|
|
952
|
+
"""Prediction: unset PATH searches os.get_exec_path() in order, including os.defpath."""
|
|
953
|
+
from types import SimpleNamespace
|
|
954
|
+
from unittest import mock
|
|
955
|
+
resolve = shared("platform-support")["native_argv"]
|
|
956
|
+
with tempfile.TemporaryDirectory(prefix="devlyn-default-path-") as temporary, \
|
|
957
|
+
mock.patch.object(platform, "machine", return_value="arm64"), \
|
|
958
|
+
mock.patch.dict(resolve.__globals__, {"sys": SimpleNamespace(platform="darwin")}), \
|
|
959
|
+
mock.patch.dict(os.environ):
|
|
960
|
+
os.environ.pop("PATH", None)
|
|
961
|
+
root = Path(temporary).resolve()
|
|
962
|
+
interpreter = root / "interpreter"
|
|
963
|
+
interpreter.write_bytes(self.macho_header()); interpreter.chmod(0o755)
|
|
964
|
+
for name in ("a", "b"):
|
|
965
|
+
folder = root / name; folder.mkdir()
|
|
966
|
+
script = folder / "s"
|
|
967
|
+
script.write_bytes(b"#!" + os.fsencode(interpreter) + b"\n"); script.chmod(0o755)
|
|
968
|
+
for default in (f"{root}/a:{root}/b", f"{root}/b:{root}/a"):
|
|
969
|
+
with self.subTest(default=default), mock.patch.object(os, "defpath", default):
|
|
970
|
+
candidate = os.path.join(os.get_exec_path()[0], "s")
|
|
971
|
+
self.assertEqual(resolve(["s", "X"], cwd=root), [str(interpreter), candidate, "X"])
|
|
972
|
+
|
|
668
973
|
def test_windows_runs_node_command_shims_without_cmd(self):
|
|
669
974
|
"""Windows resolution mocked: verification argv such as ["npm", "test"] runs the Node and script its command
|
|
670
975
|
shim names, as cmd.exe would resolve it; any other batch file is refused with an actionable message."""
|
|
@@ -703,7 +1008,8 @@ class AcceptanceTests(unittest.TestCase):
|
|
|
703
1008
|
def npm_prefix(argv, cwd=None, **kwargs): # what node npm-prefix.js prints, recorded with the directory it ran in
|
|
704
1009
|
lookups.append((argv, cwd))
|
|
705
1010
|
return SimpleNamespace(stdout=f"{prefix}\n")
|
|
706
|
-
windows = {"os": SimpleNamespace(name="nt"), "
|
|
1011
|
+
windows = {"os": SimpleNamespace(name="nt"), "sys": SimpleNamespace(platform="win32"),
|
|
1012
|
+
"shutil": SimpleNamespace(which=lambda name: str(found[name]) if name in found else None),
|
|
707
1013
|
"subprocess": SimpleNamespace(run=npm_prefix)}
|
|
708
1014
|
with mock.patch.dict(platform["native_argv"].__globals__, windows):
|
|
709
1015
|
resolve = platform["native_argv"]
|
|
@@ -1243,17 +1243,30 @@ def ensure_submission(identity, packet_path, packet, executor):
|
|
|
1243
1243
|
output = Path(packet["evidence_dir"])
|
|
1244
1244
|
output.mkdir(parents=True, exist_ok=True)
|
|
1245
1245
|
record(log, "start")
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1246
|
+
with contextlib.ExitStack() as stack:
|
|
1247
|
+
try:
|
|
1248
|
+
env = None
|
|
1249
|
+
claude_names = {"claude", "claude.exe", "claude.cmd", "claude.bat"}
|
|
1250
|
+
if Path(argv[0]).name.lower() in claude_names:
|
|
1251
|
+
argv, env = stack.enter_context(shared("unattended-tools")["claude_launch"](argv, worktree))
|
|
1252
|
+
else:
|
|
1253
|
+
paths = [Path(part.replace("\\", "/")) for part in argv]
|
|
1254
|
+
node_wrapper = any(path.name.lower() in {"node", "node.exe"} for path in paths)
|
|
1255
|
+
for part, path in zip(argv, paths):
|
|
1256
|
+
if path.name.lower() in claude_names or (path.name.lower() == "cli.js" and
|
|
1257
|
+
(node_wrapper or any("claude" in parent.lower() for parent in path.parts[:-1]))):
|
|
1258
|
+
raise ValueError(f"unattended tools: wrapped Claude or ambiguous Node CLI executor {argv[0]!r} via {part!r} is unsupported; "
|
|
1259
|
+
"invoke claude directly as argv[0]")
|
|
1260
|
+
# Files, never a pipe: a wrapper such as codex-monitored.sh refuses a piped stdout; stdin is empty, never the driver's.
|
|
1261
|
+
with (output / "executor.stdout").open("ab") as stdout, (output / "executor.stderr").open("ab") as stderr:
|
|
1262
|
+
child = subprocess.Popen(shared("platform-support")["native_argv"](argv, cwd=worktree), cwd=worktree, stdin=subprocess.DEVNULL,
|
|
1263
|
+
stdout=stdout, stderr=stderr, env=env)
|
|
1264
|
+
except (OSError, ValueError) as exc:
|
|
1265
|
+
record(log, f"not started: {one_line(str(exc))}")
|
|
1266
|
+
raise LoopError(f"executor could not start: {exc}; restore the configured executable, permissions or task workspace; "
|
|
1267
|
+
"the next drain retries") from exc
|
|
1268
|
+
progress(identity, f"executing; output in {output}")
|
|
1269
|
+
code = child.wait()
|
|
1257
1270
|
record(log, f"exit {code} (pid {child.pid})")
|
|
1258
1271
|
if not submission.exists():
|
|
1259
1272
|
raise Waiting(f"executor exited {code} without a submission (see {output / 'executor.stderr'}); the next drain runs it again")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.20",
|
|
4
4
|
"description": "Loop & harness engineering for AI coding agents (Claude Code, Codex, and any AGENTS.md-driven agent) — core engineering principles for CLAUDE.md/AGENTS.md and a loop designer that splits one intent into tasks agents drain hands-free",
|
|
5
5
|
"homepage": "https://github.com/fysoul17/devlyn-cli#readme",
|
|
6
6
|
"bin": {
|
package/release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "devlyn-cli",
|
|
4
|
-
"version": "4.3.
|
|
4
|
+
"version": "4.3.20",
|
|
5
5
|
"principles": {
|
|
6
6
|
"defaults": "config/skills/_shared/runtime-principles.md",
|
|
7
7
|
"memory": {
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
"path": "README.md",
|
|
117
|
-
"sha256": "
|
|
117
|
+
"sha256": "44858e5ef12f218490a59fc29e21528c6f0c2a530f37a98c701b8a97b743831a",
|
|
118
118
|
"executable": false
|
|
119
119
|
},
|
|
120
120
|
{
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
},
|
|
165
165
|
{
|
|
166
166
|
"path": "bin/skill-history.json",
|
|
167
|
-
"sha256": "
|
|
167
|
+
"sha256": "15f993a6060c0ee8fc7a11e568ca02db1e458244eaff2621cb18c16142b60903",
|
|
168
168
|
"executable": false
|
|
169
169
|
},
|
|
170
170
|
{
|
|
@@ -179,12 +179,12 @@
|
|
|
179
179
|
},
|
|
180
180
|
{
|
|
181
181
|
"path": "config/skills/_shared/adapters/claude.md",
|
|
182
|
-
"sha256": "
|
|
182
|
+
"sha256": "8e9d6b99001c195c6608c8da117424d131ca3929adc738ee5f2bbdd14a47cc12",
|
|
183
183
|
"executable": false
|
|
184
184
|
},
|
|
185
185
|
{
|
|
186
186
|
"path": "config/skills/_shared/adapters/codex.md",
|
|
187
|
-
"sha256": "
|
|
187
|
+
"sha256": "8fa200290caa7dd7b8d578157de3b233503d50ec1338d210279eb0ee7fa30e16",
|
|
188
188
|
"executable": false
|
|
189
189
|
},
|
|
190
190
|
{
|
|
@@ -224,12 +224,12 @@
|
|
|
224
224
|
},
|
|
225
225
|
{
|
|
226
226
|
"path": "config/skills/_shared/invocation-receipt.py",
|
|
227
|
-
"sha256": "
|
|
227
|
+
"sha256": "1ce372b84faf3f0afdd987b76f87c07e630c2ff2f9fc2e49654573d78a463855",
|
|
228
228
|
"executable": false
|
|
229
229
|
},
|
|
230
230
|
{
|
|
231
231
|
"path": "config/skills/_shared/platform-support.py",
|
|
232
|
-
"sha256": "
|
|
232
|
+
"sha256": "b68da0ef8279513b48ad6597a92b022145b1257e15348ad6a32d29fbbc121a64",
|
|
233
233
|
"executable": false
|
|
234
234
|
},
|
|
235
235
|
{
|
|
@@ -252,6 +252,11 @@
|
|
|
252
252
|
"sha256": "3cd317e94402fd6d8693f91e1d3d70c6928ef0dce18f355829de6c21847305c8",
|
|
253
253
|
"executable": false
|
|
254
254
|
},
|
|
255
|
+
{
|
|
256
|
+
"path": "config/skills/_shared/unattended-tools.py",
|
|
257
|
+
"sha256": "aac9d8cc8216ad1ece32e06df926c2d7b3099f59f43ff23bb98516efa16e8c58",
|
|
258
|
+
"executable": false
|
|
259
|
+
},
|
|
255
260
|
{
|
|
256
261
|
"path": "config/skills/_shared/warm-build-dirs.py",
|
|
257
262
|
"sha256": "ee9ccbc94b614ff5ae04142bca415a4d7d6a4f569066b494ffcf3d9c612051d7",
|
|
@@ -264,7 +269,7 @@
|
|
|
264
269
|
},
|
|
265
270
|
{
|
|
266
271
|
"path": "config/skills/devlyn-ideate/SKILL.md",
|
|
267
|
-
"sha256": "
|
|
272
|
+
"sha256": "d91c9e08c32d8b3c8bfa6a6e50fdb351bd2e53aace579362f1f325ff163d4937",
|
|
268
273
|
"executable": false
|
|
269
274
|
},
|
|
270
275
|
{
|
|
@@ -289,12 +294,12 @@
|
|
|
289
294
|
},
|
|
290
295
|
{
|
|
291
296
|
"path": "config/skills/devlyn-ideate/scripts/acceptance.py",
|
|
292
|
-
"sha256": "
|
|
297
|
+
"sha256": "f25f30356e1fee4aa3a437a5724b298eb08f0079ac1fb7f797ff6b1e87427189",
|
|
293
298
|
"executable": false
|
|
294
299
|
},
|
|
295
300
|
{
|
|
296
301
|
"path": "config/skills/devlyn-ideate/scripts/queue.py",
|
|
297
|
-
"sha256": "
|
|
302
|
+
"sha256": "07fa3c9222a980e17bc8793d67b702e25ae72d2485a3d17e855ed423ac29c95a",
|
|
298
303
|
"executable": false
|
|
299
304
|
},
|
|
300
305
|
{
|
|
@@ -309,7 +314,7 @@
|
|
|
309
314
|
},
|
|
310
315
|
{
|
|
311
316
|
"path": "native/claude/.claude-plugin/plugin.json",
|
|
312
|
-
"sha256": "
|
|
317
|
+
"sha256": "7c1fcb961b0323a0ae712b100e42e277d90e4d13f52ed93a7ac8cfe8198cef6a",
|
|
313
318
|
"executable": false
|
|
314
319
|
},
|
|
315
320
|
{
|
|
@@ -319,12 +324,12 @@
|
|
|
319
324
|
},
|
|
320
325
|
{
|
|
321
326
|
"path": "native/claude/skills/_shared/adapters/claude.md",
|
|
322
|
-
"sha256": "
|
|
327
|
+
"sha256": "8e9d6b99001c195c6608c8da117424d131ca3929adc738ee5f2bbdd14a47cc12",
|
|
323
328
|
"executable": false
|
|
324
329
|
},
|
|
325
330
|
{
|
|
326
331
|
"path": "native/claude/skills/_shared/adapters/codex.md",
|
|
327
|
-
"sha256": "
|
|
332
|
+
"sha256": "8fa200290caa7dd7b8d578157de3b233503d50ec1338d210279eb0ee7fa30e16",
|
|
328
333
|
"executable": false
|
|
329
334
|
},
|
|
330
335
|
{
|
|
@@ -364,12 +369,12 @@
|
|
|
364
369
|
},
|
|
365
370
|
{
|
|
366
371
|
"path": "native/claude/skills/_shared/invocation-receipt.py",
|
|
367
|
-
"sha256": "
|
|
372
|
+
"sha256": "1ce372b84faf3f0afdd987b76f87c07e630c2ff2f9fc2e49654573d78a463855",
|
|
368
373
|
"executable": false
|
|
369
374
|
},
|
|
370
375
|
{
|
|
371
376
|
"path": "native/claude/skills/_shared/platform-support.py",
|
|
372
|
-
"sha256": "
|
|
377
|
+
"sha256": "b68da0ef8279513b48ad6597a92b022145b1257e15348ad6a32d29fbbc121a64",
|
|
373
378
|
"executable": false
|
|
374
379
|
},
|
|
375
380
|
{
|
|
@@ -392,6 +397,11 @@
|
|
|
392
397
|
"sha256": "3cd317e94402fd6d8693f91e1d3d70c6928ef0dce18f355829de6c21847305c8",
|
|
393
398
|
"executable": false
|
|
394
399
|
},
|
|
400
|
+
{
|
|
401
|
+
"path": "native/claude/skills/_shared/unattended-tools.py",
|
|
402
|
+
"sha256": "aac9d8cc8216ad1ece32e06df926c2d7b3099f59f43ff23bb98516efa16e8c58",
|
|
403
|
+
"executable": false
|
|
404
|
+
},
|
|
395
405
|
{
|
|
396
406
|
"path": "native/claude/skills/_shared/warm-build-dirs.py",
|
|
397
407
|
"sha256": "ee9ccbc94b614ff5ae04142bca415a4d7d6a4f569066b494ffcf3d9c612051d7",
|
|
@@ -404,7 +414,7 @@
|
|
|
404
414
|
},
|
|
405
415
|
{
|
|
406
416
|
"path": "native/claude/skills/devlyn-ideate/SKILL.md",
|
|
407
|
-
"sha256": "
|
|
417
|
+
"sha256": "d91c9e08c32d8b3c8bfa6a6e50fdb351bd2e53aace579362f1f325ff163d4937",
|
|
408
418
|
"executable": false
|
|
409
419
|
},
|
|
410
420
|
{
|
|
@@ -429,22 +439,22 @@
|
|
|
429
439
|
},
|
|
430
440
|
{
|
|
431
441
|
"path": "native/claude/skills/devlyn-ideate/scripts/acceptance.py",
|
|
432
|
-
"sha256": "
|
|
442
|
+
"sha256": "f25f30356e1fee4aa3a437a5724b298eb08f0079ac1fb7f797ff6b1e87427189",
|
|
433
443
|
"executable": false
|
|
434
444
|
},
|
|
435
445
|
{
|
|
436
446
|
"path": "native/claude/skills/devlyn-ideate/scripts/queue.py",
|
|
437
|
-
"sha256": "
|
|
447
|
+
"sha256": "07fa3c9222a980e17bc8793d67b702e25ae72d2485a3d17e855ed423ac29c95a",
|
|
438
448
|
"executable": false
|
|
439
449
|
},
|
|
440
450
|
{
|
|
441
451
|
"path": "native/codex/.agents/plugins/marketplace.json",
|
|
442
|
-
"sha256": "
|
|
452
|
+
"sha256": "af71ae92fbc23e126a7d1cfc6341d642c22ee999c1972dbf337c4c6e0f623cb8",
|
|
443
453
|
"executable": false
|
|
444
454
|
},
|
|
445
455
|
{
|
|
446
456
|
"path": "native/codex/plugins/devlyn/.codex-plugin/plugin.json",
|
|
447
|
-
"sha256": "
|
|
457
|
+
"sha256": "7c1fcb961b0323a0ae712b100e42e277d90e4d13f52ed93a7ac8cfe8198cef6a",
|
|
448
458
|
"executable": false
|
|
449
459
|
},
|
|
450
460
|
{
|
|
@@ -454,12 +464,12 @@
|
|
|
454
464
|
},
|
|
455
465
|
{
|
|
456
466
|
"path": "native/codex/plugins/devlyn/skills/_shared/adapters/claude.md",
|
|
457
|
-
"sha256": "
|
|
467
|
+
"sha256": "8e9d6b99001c195c6608c8da117424d131ca3929adc738ee5f2bbdd14a47cc12",
|
|
458
468
|
"executable": false
|
|
459
469
|
},
|
|
460
470
|
{
|
|
461
471
|
"path": "native/codex/plugins/devlyn/skills/_shared/adapters/codex.md",
|
|
462
|
-
"sha256": "
|
|
472
|
+
"sha256": "8fa200290caa7dd7b8d578157de3b233503d50ec1338d210279eb0ee7fa30e16",
|
|
463
473
|
"executable": false
|
|
464
474
|
},
|
|
465
475
|
{
|
|
@@ -499,12 +509,12 @@
|
|
|
499
509
|
},
|
|
500
510
|
{
|
|
501
511
|
"path": "native/codex/plugins/devlyn/skills/_shared/invocation-receipt.py",
|
|
502
|
-
"sha256": "
|
|
512
|
+
"sha256": "1ce372b84faf3f0afdd987b76f87c07e630c2ff2f9fc2e49654573d78a463855",
|
|
503
513
|
"executable": false
|
|
504
514
|
},
|
|
505
515
|
{
|
|
506
516
|
"path": "native/codex/plugins/devlyn/skills/_shared/platform-support.py",
|
|
507
|
-
"sha256": "
|
|
517
|
+
"sha256": "b68da0ef8279513b48ad6597a92b022145b1257e15348ad6a32d29fbbc121a64",
|
|
508
518
|
"executable": false
|
|
509
519
|
},
|
|
510
520
|
{
|
|
@@ -527,6 +537,11 @@
|
|
|
527
537
|
"sha256": "3cd317e94402fd6d8693f91e1d3d70c6928ef0dce18f355829de6c21847305c8",
|
|
528
538
|
"executable": false
|
|
529
539
|
},
|
|
540
|
+
{
|
|
541
|
+
"path": "native/codex/plugins/devlyn/skills/_shared/unattended-tools.py",
|
|
542
|
+
"sha256": "aac9d8cc8216ad1ece32e06df926c2d7b3099f59f43ff23bb98516efa16e8c58",
|
|
543
|
+
"executable": false
|
|
544
|
+
},
|
|
530
545
|
{
|
|
531
546
|
"path": "native/codex/plugins/devlyn/skills/_shared/warm-build-dirs.py",
|
|
532
547
|
"sha256": "ee9ccbc94b614ff5ae04142bca415a4d7d6a4f569066b494ffcf3d9c612051d7",
|
|
@@ -539,7 +554,7 @@
|
|
|
539
554
|
},
|
|
540
555
|
{
|
|
541
556
|
"path": "native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md",
|
|
542
|
-
"sha256": "
|
|
557
|
+
"sha256": "d91c9e08c32d8b3c8bfa6a6e50fdb351bd2e53aace579362f1f325ff163d4937",
|
|
543
558
|
"executable": false
|
|
544
559
|
},
|
|
545
560
|
{
|
|
@@ -564,12 +579,12 @@
|
|
|
564
579
|
},
|
|
565
580
|
{
|
|
566
581
|
"path": "native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py",
|
|
567
|
-
"sha256": "
|
|
582
|
+
"sha256": "f25f30356e1fee4aa3a437a5724b298eb08f0079ac1fb7f797ff6b1e87427189",
|
|
568
583
|
"executable": false
|
|
569
584
|
},
|
|
570
585
|
{
|
|
571
586
|
"path": "native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py",
|
|
572
|
-
"sha256": "
|
|
587
|
+
"sha256": "07fa3c9222a980e17bc8793d67b702e25ae72d2485a3d17e855ed423ac29c95a",
|
|
573
588
|
"executable": false
|
|
574
589
|
},
|
|
575
590
|
{
|
|
@@ -1714,12 +1729,12 @@
|
|
|
1714
1729
|
},
|
|
1715
1730
|
{
|
|
1716
1731
|
"path": "package.json",
|
|
1717
|
-
"sha256": "
|
|
1732
|
+
"sha256": "905220c6971b71629c932a0c8ed9e98e891c04e85661661878511278a0afa9b0",
|
|
1718
1733
|
"executable": false
|
|
1719
1734
|
}
|
|
1720
1735
|
],
|
|
1721
1736
|
"digest": {
|
|
1722
1737
|
"algorithm": "sha256",
|
|
1723
|
-
"value": "
|
|
1738
|
+
"value": "1b0c7766afd6cf3778367bd19903dcd867c8e278047b24d608b39dc5c2b3fabe"
|
|
1724
1739
|
}
|
|
1725
1740
|
}
|