devlyn-cli 4.2.25 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/codex-projection.js +129 -0
- package/bin/devlyn.js +522 -0
- package/bin/engines.js +40 -0
- package/bin/harness-activation.js +426 -0
- package/bin/harness-status.js +171 -0
- package/bin/instructions.js +13 -3
- package/bin/release.js +381 -0
- package/bin/skill-history.json +5 -0
- package/config/skills/_shared/adapters/omp.md +3 -0
- package/config/skills/_shared/harness-store.py +201 -0
- package/config/skills/_shared/runtime-principles.md +12 -2
- package/config/skills/_shared/warm-build-dirs.py +154 -83
- package/config/skills/devlyn-ideate/SKILL.md +5 -1
- package/config/skills/devlyn-ideate/references/loop.md +12 -2
- package/config/skills/devlyn-ideate/references/plan-add.md +12 -0
- package/config/skills/devlyn-ideate/scripts/acceptance.py +5 -0
- package/config/skills/devlyn-ideate/scripts/queue.py +156 -15
- package/package.json +8 -1
|
@@ -170,6 +170,58 @@ def lock(common, name, *, blocking):
|
|
|
170
170
|
yield
|
|
171
171
|
|
|
172
172
|
|
|
173
|
+
def harness_call(name, *args):
|
|
174
|
+
helper = shared("harness-store")
|
|
175
|
+
try:
|
|
176
|
+
return helper[name](*args)
|
|
177
|
+
except helper["HarnessError"] as exc:
|
|
178
|
+
raise LoopError(str(exc)) from exc
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def enforce_binding(common, anchor=None):
|
|
182
|
+
binding = harness_call("read_binding", common)
|
|
183
|
+
try:
|
|
184
|
+
harness_call("check_binding", binding, harness_call("resolve", SKILLS))
|
|
185
|
+
except (LoopError, OSError) as exc:
|
|
186
|
+
# A queue that settled outside queue.py (its last delivery finished through task-complete.py) keeps a binding no
|
|
187
|
+
# work needs: another release drops it instead of refusing. Callers outside the queue lock pass `anchor`.
|
|
188
|
+
if binding and anchor is not None and release_settled_binding(anchor, common, interrupted_add=True):
|
|
189
|
+
return enforce_binding(common)
|
|
190
|
+
if binding:
|
|
191
|
+
argv = harness_call("recovery_argv", binding, sys.argv[1:])
|
|
192
|
+
raise LoopError(f"{exc}; recovery argv: {json.dumps(argv, ensure_ascii=False)}") from exc
|
|
193
|
+
raise
|
|
194
|
+
return binding
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def unfinished(v):
|
|
198
|
+
if v["unreadable"] or counts(v)["legacy_pending"]:
|
|
199
|
+
return True
|
|
200
|
+
for state in v["states"].values():
|
|
201
|
+
receipt = state["receipt"]
|
|
202
|
+
# A blocked task waits on a failed prerequisite for good, as drain counts it: it holds no binding.
|
|
203
|
+
if state.get("invalid") or state["kind"] in {"pending", "active"} or receipt and (
|
|
204
|
+
not receipt.get("queue") or receipt.get("delivery") not in SETTLED):
|
|
205
|
+
return True
|
|
206
|
+
# An interrupted capture with no add record is still resumable work.
|
|
207
|
+
refs = git(v["anchor"], "for-each-ref", "--format=%(refname)", CAPTURES).splitlines()
|
|
208
|
+
return any(ref.removeprefix(CAPTURES) not in v["records"] for ref in refs)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def release_settled_binding(anchor, common, *, interrupted_add=False):
|
|
212
|
+
"""Remove the binding once nothing needs it. The bound release keeps one from an interrupted first add (no record
|
|
213
|
+
yet) so its retry rebinds the same identity; another release (`interrupted_add`) drops that too, since a binding
|
|
214
|
+
published before its capture captured nothing. Returns whether it was removed."""
|
|
215
|
+
with lock(common, "queue.lock", blocking=True):
|
|
216
|
+
if not harness_call("read_binding", common):
|
|
217
|
+
return False
|
|
218
|
+
v = view(anchor, common, pure=True)
|
|
219
|
+
if unfinished(v) or not (v["records"] or interrupted_add):
|
|
220
|
+
return False
|
|
221
|
+
(common / "devlyn-loops/harness.json").unlink()
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
|
|
173
225
|
def sections(text, path):
|
|
174
226
|
found, current, fence = {}, None, False
|
|
175
227
|
for line in text.split("\n"):
|
|
@@ -686,6 +738,7 @@ def derive_state(anchor, common, row, claims, rev):
|
|
|
686
738
|
if packet.exists():
|
|
687
739
|
bound = read_json(packet)
|
|
688
740
|
require(isinstance(bound, dict) and not acceptance()["PACKET_KEYS"] - bound.keys(), f"{packet}: malformed task packet")
|
|
741
|
+
harness_call("packet_schema", bound)
|
|
689
742
|
require(bound["worktree"] == receipt["worktree"] and bound["task"] == identity and bound["branch"] == receipt["branch"]
|
|
690
743
|
and bound["allocation_base"] == receipt["baseline"],
|
|
691
744
|
f"{packet}: conflicting packet ownership")
|
|
@@ -713,11 +766,13 @@ def derive_state(anchor, common, row, claims, rev):
|
|
|
713
766
|
return state
|
|
714
767
|
|
|
715
768
|
|
|
716
|
-
def view(anchor, common, local_only=False):
|
|
769
|
+
def view(anchor, common, local_only=False, *, pure=False):
|
|
717
770
|
"""Reconcile the queue with every receipt and recovery ref before anything is selected."""
|
|
718
|
-
with lock(common, "queue.lock", blocking=True):
|
|
771
|
+
with contextlib.nullcontext() if pure else lock(common, "queue.lock", blocking=True):
|
|
772
|
+
if not pure:
|
|
773
|
+
enforce_binding(common)
|
|
719
774
|
recorded = records(common)
|
|
720
|
-
cleanup = clean(common, recorded)
|
|
775
|
+
cleanup = [] if pure else clean(common, recorded)
|
|
721
776
|
rows = queue_view(anchor, recorded)
|
|
722
777
|
claims, unreadable, damaged = {}, [], {}
|
|
723
778
|
canonical = {receipt_path(common, row["identity"]): row["identity"] for row in rows if row["identity"]}
|
|
@@ -737,7 +792,7 @@ def view(anchor, common, local_only=False):
|
|
|
737
792
|
if identity := canonical.get(path):
|
|
738
793
|
damaged[identity] = str(exc)
|
|
739
794
|
v = {"anchor": anchor, "common": common, "rows": rows, "records": recorded, "packages": {}, "manifests": {}, "states": {},
|
|
740
|
-
"unreadable": unreadable, "cleanup": cleanup, "local_only": local_only}
|
|
795
|
+
"unreadable": unreadable, "cleanup": cleanup, "local_only": local_only, "pure": pure}
|
|
741
796
|
errors = {}
|
|
742
797
|
for row in v["rows"]:
|
|
743
798
|
if row["identity"]:
|
|
@@ -902,7 +957,7 @@ def local_start(v, loop):
|
|
|
902
957
|
return None, f"{branch}, the branch {loop} was added on, {where}; bring {base} into {branch}, or plan the work as a new loop"
|
|
903
958
|
if drift := package_drift(v, loop, start, branch):
|
|
904
959
|
return start, drift
|
|
905
|
-
if drift := instruction_drift(v["anchor"], start):
|
|
960
|
+
if drift := instruction_drift(v["anchor"], start, pure=v.get("pure", False)):
|
|
906
961
|
return start, f"the installed instructions ({drift}) in this checkout differ from those in its start commit {start}, so it would run without them; {remedy}"
|
|
907
962
|
return start, None
|
|
908
963
|
|
|
@@ -992,12 +1047,14 @@ def package_drift(v, loop, start, where):
|
|
|
992
1047
|
"bytes") if files else None
|
|
993
1048
|
|
|
994
1049
|
|
|
995
|
-
def instruction_drift(anchor, rev):
|
|
1050
|
+
def instruction_drift(anchor, rev, *, pure=False):
|
|
996
1051
|
"""The installed instruction files whose content at `rev` differs from the anchor checkout's (both absent is equal): a
|
|
997
1052
|
task starting at `rev` would run without them. Git compares them as `git status` does: a link by its target, line endings
|
|
998
1053
|
under the checkout's conversion rules, and an untracked or ignored file as one still to commit."""
|
|
999
1054
|
names = ("CLAUDE.md", "AGENTS.md")
|
|
1000
|
-
|
|
1055
|
+
# Git's porcelain diff refreshes the index independently of GIT_OPTIONAL_LOCKS.
|
|
1056
|
+
options = ("-c", "diff.autoRefreshIndex=false") if pure else ()
|
|
1057
|
+
differ = git(anchor, *options, "diff", "--name-only", "--no-renames", rev, "--", *names).split("\n")
|
|
1001
1058
|
differ += git(anchor, "ls-files", "--others", "--", *names).split("\n")
|
|
1002
1059
|
return ", ".join(name for name in names if name in differ)
|
|
1003
1060
|
|
|
@@ -1088,7 +1145,7 @@ def ensure_packet(v, row, path, receipt):
|
|
|
1088
1145
|
return {"path": str(worktree / rel), "sha256": hashlib.sha256(show(worktree, inputs, rel)).hexdigest()}
|
|
1089
1146
|
|
|
1090
1147
|
packet = {
|
|
1091
|
-
"schema_version":
|
|
1148
|
+
"schema_version": 2, "task": identity, "title": spec["title"], "kind": spec["kind"],
|
|
1092
1149
|
"worktree": str(worktree), "branch": branch, "receipt": str(path), "scratch": str(path.parent / "scratch"),
|
|
1093
1150
|
"allocation_base": baseline, "inputs_sha": inputs,
|
|
1094
1151
|
"contract": identity_of(f"docs/specs/{loop}/{task}/spec.md"),
|
|
@@ -1105,6 +1162,31 @@ def ensure_packet(v, row, path, receipt):
|
|
|
1105
1162
|
"runner": [sys.executable, str(Path(__file__).with_name("acceptance.py")), "run", "--packet", str(packet_path)],
|
|
1106
1163
|
"obligations": OBLIGATIONS,
|
|
1107
1164
|
}
|
|
1165
|
+
instructions = packet["methodology"]
|
|
1166
|
+
binding = harness_call("read_binding", v["common"])
|
|
1167
|
+
if binding:
|
|
1168
|
+
defaults = {"source": "release", **binding["defaults"]}
|
|
1169
|
+
else:
|
|
1170
|
+
# An unbound queue's executor still gets the principles: the invoking skill's own copy.
|
|
1171
|
+
own = SKILLS / "_shared/runtime-principles.md"
|
|
1172
|
+
defaults = {"source": "skill", **harness_call("file_identity", own)} if own.is_file() else None
|
|
1173
|
+
if defaults is None and not instructions:
|
|
1174
|
+
raise Waiting(f"{identity}: no principles reach its executor — neither CLAUDE.md/AGENTS.md at the inputs commit nor "
|
|
1175
|
+
f"{SKILLS / '_shared/runtime-principles.md'}; reinstall devlyn-cli to restore that file, then drain again")
|
|
1176
|
+
packet.update(harness=binding["release"] if binding else None,
|
|
1177
|
+
methodology={"defaults": defaults, "projectInstructions": instructions}, workerPrompt=None)
|
|
1178
|
+
if binding:
|
|
1179
|
+
harness_call("check_binding", binding, harness_call("resolve", SKILLS))
|
|
1180
|
+
bound_skills = Path(binding["release"]["root"]) / "config/skills"
|
|
1181
|
+
packet["runner"][1] = str(bound_skills / "devlyn-ideate/scripts/acceptance.py")
|
|
1182
|
+
prompt = path.parent / "worker-prompt.md"
|
|
1183
|
+
body = (f"Execute task packet {packet_path}. Work only in {worktree} on its owned branch.\n"
|
|
1184
|
+
f"Read {binding['defaults']['path']} and apply it as defaults; explicit user and native owner/project instructions win.\n"
|
|
1185
|
+
f"Set DEVLYN_SKILL_DIR={bound_skills / 'devlyn-ideate'} and DEVLYN_SHARED_DIR={bound_skills / '_shared'}.\n"
|
|
1186
|
+
f"Follow Executor exchange and Review records in {bound_skills / 'devlyn-ideate/references/loop.md'}.\n"
|
|
1187
|
+
f"Meet the packet obligations, run its runner and write {packet['submission']}. Never push, open a pull request or merge.\n")
|
|
1188
|
+
acceptance()["atomic_write"](prompt, body.encode("utf-8"))
|
|
1189
|
+
packet["workerPrompt"] = harness_call("file_identity", prompt)
|
|
1108
1190
|
write_json(packet_path, packet)
|
|
1109
1191
|
return packet_path, packet
|
|
1110
1192
|
|
|
@@ -1125,6 +1207,7 @@ def ensure_submission(identity, packet_path, packet, executor):
|
|
|
1125
1207
|
An attempt's start is recorded before the spawn and its exit after the wait, so a start without an exit may have
|
|
1126
1208
|
left a live executor: drain observes once and waits if it is active. Only established cessation permits adoption
|
|
1127
1209
|
or another execution. Where writers cannot be observed the task fails as interrupted-unobservable."""
|
|
1210
|
+
harness_call("verify_packet", packet, packet_path, SKILLS)
|
|
1128
1211
|
submission = Path(packet["submission"])
|
|
1129
1212
|
worktree = Path(packet["worktree"])
|
|
1130
1213
|
log = submission.with_name("executions.log")
|
|
@@ -1144,6 +1227,10 @@ def ensure_submission(identity, packet_path, packet, executor):
|
|
|
1144
1227
|
raise LoopError(f"cannot establish that an interrupted execution stopped: {exc}; repair writer observation, then drain again") from exc
|
|
1145
1228
|
if submission.exists():
|
|
1146
1229
|
return None
|
|
1230
|
+
defaults = packet["methodology"].get("defaults") if packet.get("schema_version") == 2 else None
|
|
1231
|
+
if packet.get("harness") is None and defaults and not Path(defaults["path"]).is_file():
|
|
1232
|
+
# An installation's own copy moves with its updates; only its absence matters, and only before a spawn.
|
|
1233
|
+
raise Waiting(f"{identity}: its principles file {defaults['path']} is gone; reinstall devlyn-cli to restore it, then drain again")
|
|
1147
1234
|
argv = [part.replace("{packet}", str(packet_path)) for part in executor]
|
|
1148
1235
|
if any("{worktree_git_dir}" in part for part in argv): # Optional: a Codex executor lists it as a writable root.
|
|
1149
1236
|
git_dir = git(worktree, "rev-parse", "--path-format=absolute", "--git-dir")
|
|
@@ -1217,6 +1304,8 @@ def advance(v, row, opts):
|
|
|
1217
1304
|
if not path.exists() and (refused := allocate(v, row, opts)):
|
|
1218
1305
|
return refused
|
|
1219
1306
|
receipt = read_json(path)
|
|
1307
|
+
if receipt.get("acceptance") and (path.parent / "packet.json").exists():
|
|
1308
|
+
harness_call("verify_packet", read_json(path.parent / "packet.json"), path.parent / "packet.json", SKILLS)
|
|
1220
1309
|
if not receipt.get("acceptance"):
|
|
1221
1310
|
state["phase"] = "inputs"
|
|
1222
1311
|
failure = v["states"][identity].get("inputs_changed")
|
|
@@ -1269,7 +1358,9 @@ def summary(v, row):
|
|
|
1269
1358
|
branch = receipt["branch"]
|
|
1270
1359
|
if receipt.get("acceptance") and receipt.get("delivery") not in SETTLED:
|
|
1271
1360
|
if receipt.get("queue"):
|
|
1272
|
-
|
|
1361
|
+
binding = harness_call("read_binding", v["common"])
|
|
1362
|
+
skills = Path(binding["release"]["root"]) / "config/skills" if binding else SKILLS
|
|
1363
|
+
argv = [sys.executable, str(skills / "_shared/task-complete.py"), "complete", "--receipt", str(state["path"])]
|
|
1273
1364
|
if receipt.get("writers_released"):
|
|
1274
1365
|
argv.append("--writers-stopped")
|
|
1275
1366
|
if not receipt.get("local_only") and not receipt.get("mode_override"):
|
|
@@ -1420,6 +1511,7 @@ def prune_parents(v, root):
|
|
|
1420
1511
|
def drain(args):
|
|
1421
1512
|
anchor, common = repository(args.repo)
|
|
1422
1513
|
args.repo = anchor # Preserve the repository root for failures reported by main.
|
|
1514
|
+
enforce_binding(common, anchor)
|
|
1423
1515
|
diagnostics = skill_diagnostics(anchor)
|
|
1424
1516
|
require(any("{packet}" in part for part in args.executor), "executor argv must contain {packet}, replaced by the task packet path")
|
|
1425
1517
|
args.default_worktree_root = args.worktree_root is None
|
|
@@ -1456,15 +1548,17 @@ def drain(args):
|
|
|
1456
1548
|
last["cleanup"].extend(args.root_diagnostics)
|
|
1457
1549
|
annotate(last, refused)
|
|
1458
1550
|
prune_parents(last, args.worktree_root)
|
|
1551
|
+
try:
|
|
1552
|
+
release_settled_binding(anchor, common)
|
|
1553
|
+
except (LoopError, OSError) as exc:
|
|
1554
|
+
last["cleanup"].append(f"queue binding not released: {exc}; the next status or drain retries")
|
|
1459
1555
|
return {"status": status, "reason": reason, **diagnostics,
|
|
1460
1556
|
"reports": write_reports(last, status, reason, diagnostics["warnings"]), "counts": counts(last),
|
|
1461
1557
|
"tasks": [summary(last, row) for row in last["rows"] if row["identity"]], "cleanup": last["cleanup"]}
|
|
1462
1558
|
|
|
1463
1559
|
|
|
1464
|
-
def
|
|
1465
|
-
anchor
|
|
1466
|
-
args.repo = anchor # Preserve the repository root for failures reported by main.
|
|
1467
|
-
v = view(anchor, common, args.local_only)
|
|
1560
|
+
def status_summary(v):
|
|
1561
|
+
anchor = v["anchor"]
|
|
1468
1562
|
row = next_task(v, set())
|
|
1469
1563
|
annotate(v, {})
|
|
1470
1564
|
tasks = [summary(v, r) for r in v["rows"] if r["identity"]]
|
|
@@ -1475,6 +1569,41 @@ def status(args):
|
|
|
1475
1569
|
"delivery": [t for t in tasks if t.get("resume")], "cleanup": v["cleanup"]}
|
|
1476
1570
|
|
|
1477
1571
|
|
|
1572
|
+
def status(args):
|
|
1573
|
+
anchor, common = repository(args.repo)
|
|
1574
|
+
args.repo = anchor
|
|
1575
|
+
enforce_binding(common, anchor)
|
|
1576
|
+
result = status_summary(view(anchor, common, args.local_only))
|
|
1577
|
+
release_settled_binding(anchor, common)
|
|
1578
|
+
return result
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
def inspect(args):
|
|
1582
|
+
anchor, common = repository(args.repo)
|
|
1583
|
+
try:
|
|
1584
|
+
result = status_summary(view(anchor, common, pure=True))
|
|
1585
|
+
except (LoopError, OSError) as exc:
|
|
1586
|
+
result = {"status": "BLOCKED", "reason": str(exc), **skill_diagnostics(anchor)}
|
|
1587
|
+
binding_path = common / "devlyn-loops/harness.json"
|
|
1588
|
+
binding = None
|
|
1589
|
+
compatibility = {"compatible": True, "legacy": not (binding_path.exists() or binding_path.is_symlink()), "reasons": [], "recoveryArgv": None,
|
|
1590
|
+
"taskPacket": {"inspect": [1, 2], "execute": [1, 2], "write": 2}, "queueBinding": {"read": [1, 1], "write": 1}}
|
|
1591
|
+
try:
|
|
1592
|
+
binding = read_json(binding_path) if not compatibility["legacy"] else None
|
|
1593
|
+
enforce_binding(common)
|
|
1594
|
+
for packet in (common / "devlyn-completion").glob("*/packet.json"):
|
|
1595
|
+
harness_call("packet_schema", read_json(packet))
|
|
1596
|
+
except (LoopError, OSError) as exc:
|
|
1597
|
+
compatibility.update(compatible=False, reasons=[str(exc)])
|
|
1598
|
+
try:
|
|
1599
|
+
valid = harness_call("read_binding", common)
|
|
1600
|
+
if valid:
|
|
1601
|
+
compatibility["recoveryArgv"] = harness_call("recovery_argv", valid, ["inspect", "--repo", str(anchor), "--json"])
|
|
1602
|
+
except (LoopError, OSError):
|
|
1603
|
+
pass # The diagnostic above describes the malformed binding; no safe argv can be inferred.
|
|
1604
|
+
return {**result, "binding": binding, "compatibility": compatibility}
|
|
1605
|
+
|
|
1606
|
+
|
|
1478
1607
|
def legacy_target(anchor, number):
|
|
1479
1608
|
"""The pending legacy row on line `number` of the legacy queue, as add records it: its exact text and its occurrence
|
|
1480
1609
|
among equal rows, line endings aside; None for any other line."""
|
|
@@ -1501,7 +1630,9 @@ def add(args):
|
|
|
1501
1630
|
ref, package_dir = CAPTURES + loop, f"docs/specs/{loop}"
|
|
1502
1631
|
require(meta.name == "meta.md" and meta.parent.parent == anchor / "docs/specs" and re.fullmatch(ID, loop),
|
|
1503
1632
|
f"package must be <repo>/docs/specs/<loop-id>/meta.md: {meta}")
|
|
1633
|
+
enforce_binding(common, anchor)
|
|
1504
1634
|
with lock(common, "queue.lock", blocking=True):
|
|
1635
|
+
enforce_binding(common)
|
|
1505
1636
|
recorded = records(common)
|
|
1506
1637
|
notes = clean(common, recorded)
|
|
1507
1638
|
target = legacy_target(anchor, args.materialize) if args.materialize else None
|
|
@@ -1540,6 +1671,11 @@ def add(args):
|
|
|
1540
1671
|
require(" ".join(target["line"][6:].split()) in intent, "meta.md '## Intent' must reproduce the legacy row's intent verbatim")
|
|
1541
1672
|
paths = [f"{package_dir}/meta.md", *(f"{package_dir}/{task}/{name}" for task in package["tasks"] for name in ("spec.md", "spec.expected.json"))]
|
|
1542
1673
|
files = {rel: package_bytes(anchor, None, rel) for rel in paths} | {loop_queue(loop): loop_rows(loop, package, target)}
|
|
1674
|
+
# The lock covers both first binding and first capture; an interrupted binding is authoritative on retry.
|
|
1675
|
+
if harness_call("read_binding", common) is None:
|
|
1676
|
+
invoking = harness_call("resolve", SKILLS)
|
|
1677
|
+
if invoking is not None and not unfinished(view(anchor, common, pure=True)):
|
|
1678
|
+
write_json(common / "devlyn-loops/harness.json", invoking)
|
|
1543
1679
|
tree = build_tree(anchor, None, files)
|
|
1544
1680
|
if captured:
|
|
1545
1681
|
require(tree == git(anchor, "rev-parse", captured + "^{tree}"), interrupted)
|
|
@@ -2016,6 +2152,9 @@ def main():
|
|
|
2016
2152
|
action = actions.add_parser(name)
|
|
2017
2153
|
action.add_argument("--repo", default=".")
|
|
2018
2154
|
action.add_argument("--local-only", "--no-push", action="store_true")
|
|
2155
|
+
inspector = actions.add_parser("inspect")
|
|
2156
|
+
inspector.add_argument("--repo", default=".")
|
|
2157
|
+
inspector.add_argument("--json", action="store_true")
|
|
2019
2158
|
actions.choices["drain"].add_argument("--worktree-root")
|
|
2020
2159
|
actions.choices["drain"].add_argument("executor", nargs="+", help="host executor argv after --; {packet} becomes the packet path")
|
|
2021
2160
|
args = parser.parse_args()
|
|
@@ -2025,9 +2164,11 @@ def main():
|
|
|
2025
2164
|
print(f"queue self-test: PASS ({result.testsRun} tests)")
|
|
2026
2165
|
return 0 if result.wasSuccessful() else 1
|
|
2027
2166
|
if args.action is None:
|
|
2028
|
-
parser.error("check, add, status or drain is required")
|
|
2167
|
+
parser.error("check, add, status, inspect or drain is required")
|
|
2168
|
+
if args.action == "inspect":
|
|
2169
|
+
os.environ["GIT_OPTIONAL_LOCKS"] = "0" # git diff may otherwise refresh the index even with unchanged bytes.
|
|
2029
2170
|
try:
|
|
2030
|
-
result = {"check": check, "add": add, "status": status, "drain": drain}[args.action](args)
|
|
2171
|
+
result = {"check": check, "add": add, "status": status, "drain": drain, "inspect": inspect}[args.action](args)
|
|
2031
2172
|
except (LoopError, OSError) as exc:
|
|
2032
2173
|
result = {"status": "BLOCKED", "reason": str(exc)}
|
|
2033
2174
|
if args.action in {"status", "drain"}:
|
package/package.json
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
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": {
|
|
7
7
|
"devlyn": "bin/devlyn.js"
|
|
8
8
|
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"prepack": "node -e \"require('./bin/release').buildRelease()\"",
|
|
11
|
+
"postpack": "node -e \"require('./bin/release').cleanGenerated()\""
|
|
12
|
+
},
|
|
9
13
|
"files": [
|
|
10
14
|
"bin",
|
|
11
15
|
"config",
|
|
16
|
+
"harness",
|
|
17
|
+
"native",
|
|
18
|
+
"release.json",
|
|
12
19
|
"!config/skills/preflight-workspace",
|
|
13
20
|
"!config/skills/preflight-workspace/**",
|
|
14
21
|
"!config/skills/devlyn:ideate-workspace",
|