devlyn-cli 4.2.3 → 4.2.4
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/bin/skill-history.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"source": "devlyn-cli@4.1.0 (npm tarball sha1 398ff07d3d689b56cc22e18ac50260bc6e4e9e85); devlyn-cli@4.2.0 (npm tarball sha1 c93c2fb1cf5630b18d2eba0085182c14fddbe2d5); whole-tree fingerprints from bin/skill-ownership.js; deprecated commands: commands/ and optional-commands/*/ files of devlyn-cli 0.0.1-0.5.8 (every published version that shipped them), LF-normalized SHA-256",
|
|
2
|
+
"source": "devlyn-cli@4.1.0 (npm tarball sha1 398ff07d3d689b56cc22e18ac50260bc6e4e9e85); devlyn-cli@4.2.0 (npm tarball sha1 c93c2fb1cf5630b18d2eba0085182c14fddbe2d5); devlyn-cli@4.2.4 (_shared and devlyn-ideate, fingerprinted from an install of its packed tarball); whole-tree fingerprints from bin/skill-ownership.js; deprecated commands: commands/ and optional-commands/*/ files of devlyn-cli 0.0.1-0.5.8 (every published version that shipped them), LF-normalized SHA-256",
|
|
3
3
|
"skills": {
|
|
4
4
|
"_shared": [
|
|
5
5
|
"eb3327d5e6940bd5e0459fa28c9ab323d9c58ccfdeb9426c19cd027cc4e5c4ed",
|
|
6
|
-
"8c80926ab03b2477e52264495b35c2116f65fc51b02e8c56e441b5705872392a"
|
|
6
|
+
"8c80926ab03b2477e52264495b35c2116f65fc51b02e8c56e441b5705872392a",
|
|
7
|
+
"3c81c575cc43d70488865b440204cb399091405d9b7f81dbf047a32d3d5ac045"
|
|
7
8
|
],
|
|
8
9
|
"devlyn-design-ui": [
|
|
9
10
|
"91080266232fb62b6f1d45932184f065589e12eb66f02fb6442527c6dfcd81b6"
|
|
@@ -14,7 +15,8 @@
|
|
|
14
15
|
],
|
|
15
16
|
"devlyn-ideate": [
|
|
16
17
|
"8ff5148cc65b4608c35b29afd6fef20d42d8da93a094144d98cfc63e74089b6e",
|
|
17
|
-
"5824f07e455fe1c3942bf595a12e935258d4c0a851ca1bf741db915603d1c4e0"
|
|
18
|
+
"5824f07e455fe1c3942bf595a12e935258d4c0a851ca1bf741db915603d1c4e0",
|
|
19
|
+
"7f451e48e391643b34f6249244bc47d93d6303875e7b092771e3af0718343bc1"
|
|
18
20
|
],
|
|
19
21
|
"devlyn-queue": [
|
|
20
22
|
"4c7d988dcd154c7bf658d2d24cd7868955df9d854ed87eb784742d6d4bb229e0"
|
|
@@ -22,6 +22,7 @@ import subprocess
|
|
|
22
22
|
import sys
|
|
23
23
|
import tempfile
|
|
24
24
|
import unittest
|
|
25
|
+
import uuid
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class CompletionError(Exception):
|
|
@@ -122,21 +123,22 @@ def verify_files(root, files):
|
|
|
122
123
|
|
|
123
124
|
|
|
124
125
|
def custody(work, destination, files):
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
# Publish only a complete copy. A hard crash's private staging directory is never reused.
|
|
127
|
+
with tempfile.TemporaryDirectory(prefix=".custody-", dir=destination.parent) as temporary:
|
|
128
|
+
root = Path(temporary)
|
|
129
|
+
for relative, expected in files.items():
|
|
130
|
+
source = safe_path(work, relative)
|
|
131
|
+
target = safe_path(root, relative)
|
|
132
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
132
133
|
with source.open("rb") as incoming, target.open("xb") as outgoing:
|
|
133
134
|
shutil.copyfileobj(incoming, outgoing)
|
|
134
135
|
outgoing.flush()
|
|
135
136
|
os.fsync(outgoing.fileno())
|
|
136
137
|
target.chmod(expected["mode"])
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
require(file_record(target) == expected, f"custody copy mismatch; retain original: {relative}")
|
|
139
|
+
verify_files(work, files)
|
|
140
|
+
verify_files(root, files)
|
|
141
|
+
root.rename(destination)
|
|
140
142
|
atomic_json(destination.parent / "manifest.json", files)
|
|
141
143
|
|
|
142
144
|
|
|
@@ -309,7 +311,12 @@ def bind_acceptance(receipt, path, supplied):
|
|
|
309
311
|
else:
|
|
310
312
|
raise CompletionError("acceptance kind must be direct|loop")
|
|
311
313
|
files = snapshot_files(work, paths)
|
|
312
|
-
|
|
314
|
+
destination = safe_path(path.parent, "custody")
|
|
315
|
+
if destination.exists():
|
|
316
|
+
# An unbound copy is never authoritative; retain it like an orphan staging directory.
|
|
317
|
+
require(destination.is_dir(), "custody is not a directory; retain and inspect")
|
|
318
|
+
destination.rename(path.parent / (".custody-" + uuid.uuid4().hex))
|
|
319
|
+
custody(work, destination, files)
|
|
313
320
|
recovery = ref_sha(receipt, receipt["recovery_ref"])
|
|
314
321
|
require(recovery in {None, sha}, "recovery ref changed")
|
|
315
322
|
if recovery is None:
|
|
@@ -1793,21 +1800,37 @@ class CompletionTests(unittest.TestCase):
|
|
|
1793
1800
|
|
|
1794
1801
|
def test_custody_failure_and_evidence_tamper(self):
|
|
1795
1802
|
self.allocate(); self.accept()
|
|
1803
|
+
self.cli("accept", "--receipt", self.receipt, "--acceptance", self.acceptance)
|
|
1796
1804
|
bad = self.receipt.parent / "custody/.devlyn/checks.txt"
|
|
1797
|
-
bad.
|
|
1798
|
-
bad.write_text("partial interrupted copy", encoding="utf-8")
|
|
1805
|
+
bad.write_text("corrupted bound copy", encoding="utf-8")
|
|
1799
1806
|
_, r = self.complete("--writers-stopped", success=False)
|
|
1800
1807
|
self.assertNotEqual(r.returncode, 0)
|
|
1801
1808
|
self.assertTrue(self.task.exists())
|
|
1802
1809
|
self.assertEqual((self.task / ".devlyn/checks.txt").read_text(encoding="utf-8"), "actual fixture check: accepted bytes\n")
|
|
1803
1810
|
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
1804
|
-
|
|
1811
|
+
shutil.copy2(self.task / ".devlyn/checks.txt", bad) # Repair only the identified corrupt fixture copy.
|
|
1805
1812
|
self.complete("--mode", "pr")
|
|
1806
1813
|
(self.task / ".devlyn/checks.txt").write_text("altered after acceptance", encoding="utf-8")
|
|
1807
1814
|
_, r = self.complete("--mode", "auto", success=False)
|
|
1808
1815
|
self.assertNotEqual(r.returncode, 0)
|
|
1809
1816
|
self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("merges",0), 0)
|
|
1810
1817
|
|
|
1818
|
+
def test_unbound_partial_custody_is_retained_through_cleanup(self):
|
|
1819
|
+
self.allocate(); self.accept()
|
|
1820
|
+
partial = self.receipt.parent / "custody/.devlyn/checks.txt"
|
|
1821
|
+
partial.parent.mkdir(parents=True)
|
|
1822
|
+
partial.write_text("partial interrupted copy", encoding="utf-8")
|
|
1823
|
+
original = file_record(partial)
|
|
1824
|
+
result, _ = self.complete("--writers-stopped", "--mode", "auto")
|
|
1825
|
+
self.assertEqual(result["status"], "COMPLETE")
|
|
1826
|
+
self.assertEqual(result["scratch_cleanup"]["status"], "CLEAN")
|
|
1827
|
+
self.assertFalse(self.task.exists())
|
|
1828
|
+
retained = list(self.receipt.parent.glob(".custody-*"))
|
|
1829
|
+
self.assertEqual(len(retained), 1)
|
|
1830
|
+
self.assertEqual(file_record(retained[0] / ".devlyn/checks.txt"), original)
|
|
1831
|
+
self.assertEqual(self.complete(acceptance=False)[0]["status"], "COMPLETE")
|
|
1832
|
+
self.assertEqual(file_record(retained[0] / ".devlyn/checks.txt"), original)
|
|
1833
|
+
|
|
1811
1834
|
def test_actual_foreign_writer_and_registration(self):
|
|
1812
1835
|
# H4. Prediction: a process still using the merged task's tree settles the delivery COMPLETE with the workspace
|
|
1813
1836
|
# retained (CLEANUP_PENDING) and the process named, as unobservable writers do. Before: complete ended BLOCKED
|
|
@@ -41,6 +41,7 @@ The host supplies the executor and maps its configured engine to the argv; the l
|
|
|
41
41
|
| Inputs commit | A branch head equal to the expected inputs tree is adopted; anything else blocks. |
|
|
42
42
|
| During execution | Each attempt's start is recorded before its spawn and its exit after it. A start without an exit waits until task-complete observes no process using the worktree, then adopts the submission that executor wrote or runs the executor again; where writers cannot be observed (native Windows) the task becomes `[F] interrupted-unobservable` with its workspace retained, its dependents become prerequisite-blocked and independent work continues. Exactly-once execution is not promised. |
|
|
43
43
|
| During checks | Acceptance reruns; incomplete evidence stays unreferenced. |
|
|
44
|
+
| During custody binding | Acceptance reruns while the receipt is unbound. Any unbound custody copy, complete or partial, is set aside in a unique hidden sibling before fresh custody is published; it and orphan staging copies stay retained and unreferenced. A bound receipt resumes its original result with strict evidence validation. |
|
|
44
45
|
| Accepted, before terminal commit | Only the missing transition is created. |
|
|
45
46
|
| Terminal committed, before attachment | The commit is validated and attached, also when attachment already moved the recovery ref; nothing reruns. |
|
|
46
47
|
| Terminal committed, before delivery | Only the missing delivery effects run. |
|
|
@@ -1069,6 +1069,8 @@ def advance(v, row, opts):
|
|
|
1069
1069
|
except LoopError as exc:
|
|
1070
1070
|
return f"delivery blocked: {exc}"
|
|
1071
1071
|
progress(identity, f"delivery {result['status']}")
|
|
1072
|
+
if result.get("merge_refused"):
|
|
1073
|
+
return f"merge refused: {result['merge_refused']}; a person merges the PR"
|
|
1072
1074
|
return None
|
|
1073
1075
|
|
|
1074
1076
|
|
|
@@ -1093,9 +1095,11 @@ def summary(v, row):
|
|
|
1093
1095
|
questions=[r.removeprefix("needs-review: ") for r in acceptance_record.get("reasons", []) if r.startswith("needs-review: ")],
|
|
1094
1096
|
worktree=receipt["worktree"] if receipt.get("worktree") and Path(receipt["worktree"]).exists() else None,
|
|
1095
1097
|
cleanup=f"{c['status']} — {c['reason']}; resume: {c['resume']}" if (c := receipt.get("workspace_cleanup")) else None,
|
|
1096
|
-
branch=receipt.get("branch")
|
|
1098
|
+
branch=receipt.get("branch") if receipt.get("branch") and ref_value(v["anchor"], "refs/heads/" + receipt["branch"]) else None,
|
|
1099
|
+
recovery_ref=receipt.get("recovery_ref"),
|
|
1097
1100
|
custody=str(Path(state["path"]).parent / "custody") if receipt.get("files") else None,
|
|
1098
|
-
scratch=(receipt.get("scratch_cleanup") or {}).get("
|
|
1101
|
+
scratch=(f"{c['status']} — {c['reason']}; resume: {c['resume']}" if (c := receipt.get("scratch_cleanup") or {}).get("reason")
|
|
1102
|
+
else c.get("status", "NOT_CLEANED")))
|
|
1099
1103
|
return item
|
|
1100
1104
|
|
|
1101
1105
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.4",
|
|
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": {
|