devlyn-cli 4.2.16 → 4.2.17
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
|
@@ -338,6 +338,7 @@
|
|
|
338
338
|
"aba66b674683935e4c28c93333896dc116ffe67e7a35fe30b3599a8be507e94e",
|
|
339
339
|
"b2e4437fa981317362d03ee032e1cfd8b686316e966c6ba12ecd619a009c80a6",
|
|
340
340
|
"c1b8e3ebf38e2be8972aa003e9614a782cb4f195c69d20b95a475ebf9aaaab9e",
|
|
341
|
+
"c8e375aab574148542f392b093cbbf3f2e8dc623826b45e35df0a1a2c3862226",
|
|
341
342
|
"ce282a55328c2d071cacc377670fbebea4da4b9a7fee81b25293fb26763f17c9",
|
|
342
343
|
"cf61a917a804503ef1e26fbb1cf498181e843a0758ec001f84edb7bcac837448",
|
|
343
344
|
"d6433bf0f96dd9dd485c3623885255738654674b3e23cfcc1a2cfd24f3cda96e",
|
|
@@ -96,7 +96,7 @@ An absolute path to a package's `meta.md` is added as it is; anything else is pl
|
|
|
96
96
|
|
|
97
97
|
## status
|
|
98
98
|
|
|
99
|
-
Run `python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" status --repo .` and report its counts, `next`, `blockers`, pending deliveries with their resume commands and any `cleanup` entry.
|
|
99
|
+
Run `python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" status --repo .` and report its counts, `next`, `blockers`, `warnings`, pending deliveries with their resume commands and any `cleanup` entry.
|
|
100
100
|
|
|
101
101
|
## drain
|
|
102
102
|
|
|
@@ -125,4 +125,4 @@ A drain can run for hours. Do not end the turn until it exits: run it in the for
|
|
|
125
125
|
- Other `WAITING`: report each task’s reason and recovery command. Task-local failures leave dependents and barrier-held siblings waiting while independent work continues; the next drain retries eligible work once. An interrupted executor is observed once per drain and cannot be adopted or respawned until it stops. `BLOCKED` means authoritative queue reconstruction or preflight failed, including a held drain lock. Never edit receipts, refs or queue rows to get past either result.
|
|
126
126
|
- An interrupted drain is resumed by running `drain` again; accepted work is never replayed, and a failed task is never rerun: plan its work again as a new loop.
|
|
127
127
|
|
|
128
|
-
Report each task's product result, delivery status, PR URL, resume command, assumptions and unresolved questions, plus whole-loop acceptance, the package's `## Decisions and assumptions`, the drain report paths and its exact `Bring into` command; leave running it to the user. Report every `cleanup` diagnostic, including unavailable reports and retained parents
|
|
128
|
+
Report each task's product result, delivery status, PR URL, resume command, assumptions and unresolved questions, plus whole-loop acceptance, the package's `## Decisions and assumptions`, the drain report paths and its exact `Bring into` command; leave running it to the user. Report every `cleanup` diagnostic, including unavailable reports and retained parents, and all `warnings`; completed products keep their verdict. A package copy left in the checkout can prevent the `Bring into` command until it is removed.
|
|
@@ -11,7 +11,7 @@ python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" drain --repo . [--local-only] [--wo
|
|
|
11
11
|
|
|
12
12
|
Both print one JSON object; exit 1 is `BLOCKED` with a `reason`.
|
|
13
13
|
|
|
14
|
-
- `status` takes `queue.lock` in the repository's Git directory and finishes `add`'s cleanup (step 1); it reports reconciled counts (`pending`, `active`, `accepted`, `failed`, `blocked`, `legacy_pending`), the `next` task, `blockers`, deliveries still pending with their resume (drain again until the terminal commit is attached, then the printed completion command; add `--writers-stopped` only after stopping the task's writers), and under `cleanup` the package copies `add` kept or could not remove.
|
|
14
|
+
- `status` takes `queue.lock` in the repository's Git directory and finishes `add`'s cleanup (step 1); it reports the running `skill` root and version, any `warnings`, reconciled counts (`pending`, `active`, `accepted`, `failed`, `blocked`, `legacy_pending`), the `next` task, `blockers`, deliveries still pending with their resume (drain again until the terminal commit is attached, then the printed completion command; add `--writers-stopped` only after stopping the task's writers), and under `cleanup` the package copies `add` kept or could not remove.
|
|
15
15
|
- `drain` ends `DRAINED` (no runnable task remains; open deliveries show per task), `WAITING` (a task waits with its reason, or legacy rows need planning) or `BLOCKED` (unreadable or ambiguous authoritative queue, invalid invocation, preflight refusal or held drain lock). Progress lines `devlyn-loop: <task>: <event>` go to stderr.
|
|
16
16
|
- `--local-only` (alias `--no-push`) keeps the loop local. A manifest `delivery: local-only` and any earlier local receipt of the loop keep that restriction for later drains. A local loop needs no remote; nothing is fetched or pushed. An `auto`/`pr` loop none of whose tasks has a pushed PR runs as a local loop from its capture and the branch it was added on. Once one has, that PR is never rewritten to local and the loop is never run locally: its tasks wait, naming the PR, while other work continues.
|
|
17
17
|
- Worktrees default to `<repo parent>/<repo name>.devlyn/<loop-id>/<task-id>`.
|
|
@@ -112,6 +112,34 @@ def read_json(path):
|
|
|
112
112
|
raise LoopError(f"{path}: {exc}") from exc
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
def skill_diagnostics(anchor):
|
|
116
|
+
running = SKILLS / ".devlyn-install.json"
|
|
117
|
+
markers = dict.fromkeys([running, *(anchor / host / "skills/.devlyn-install.json" for host in (".claude", ".agents"))])
|
|
118
|
+
versions, errors = {}, {}
|
|
119
|
+
for marker in markers:
|
|
120
|
+
try:
|
|
121
|
+
data = json.loads(marker.read_text(encoding="utf-8"))
|
|
122
|
+
if not isinstance(data, dict) or not isinstance(data.get("version"), (str, type(None))):
|
|
123
|
+
raise ValueError("install marker must be an object with a string or null version")
|
|
124
|
+
versions[marker] = data.get("version")
|
|
125
|
+
except FileNotFoundError:
|
|
126
|
+
continue # Global-only and source checkouts need no repository install marker.
|
|
127
|
+
except (OSError, ValueError, RecursionError) as exc:
|
|
128
|
+
errors[marker] = str(exc)
|
|
129
|
+
skill = {"root": str(SKILLS), "version": versions.get(running)}
|
|
130
|
+
warnings = []
|
|
131
|
+
for marker in markers:
|
|
132
|
+
if marker in errors:
|
|
133
|
+
detail = f"unreadable: {errors[marker]}"
|
|
134
|
+
elif running not in errors and marker in versions and versions[marker] != skill["version"]:
|
|
135
|
+
detail = f"version {json.dumps(versions[marker])}"
|
|
136
|
+
else:
|
|
137
|
+
continue
|
|
138
|
+
warnings.append(f"Running skill {skill['root']} (version {json.dumps(skill['version'])}); "
|
|
139
|
+
f"install marker {marker} ({detail}); run npx devlyn-cli@latest update.")
|
|
140
|
+
return {"skill": skill, "warnings": warnings}
|
|
141
|
+
|
|
142
|
+
|
|
115
143
|
def write_json(path, value):
|
|
116
144
|
acceptance()["write_json"](path, value)
|
|
117
145
|
|
|
@@ -1314,7 +1342,7 @@ def bring_in(v, loop):
|
|
|
1314
1342
|
return f"{receipt['base']}: git merge --ff {receipt['remote']}/{receipt['base']}"
|
|
1315
1343
|
|
|
1316
1344
|
|
|
1317
|
-
def write_reports(v, status, reason):
|
|
1345
|
+
def write_reports(v, status, reason, warnings):
|
|
1318
1346
|
paths = []
|
|
1319
1347
|
for loop in dict.fromkeys(row["loop"] for row in v["rows"] if row["identity"]):
|
|
1320
1348
|
path = v["common"] / "devlyn-loops" / loop / "drain-report.md"
|
|
@@ -1324,6 +1352,8 @@ def write_reports(v, status, reason):
|
|
|
1324
1352
|
f"- Queue: {f'{CAPTURES}{loop}:{loop_queue(loop)}' if loop in v['records'] else v['anchor'] / loop_queue(loop)}",
|
|
1325
1353
|
f"- Drain: {status}" + (f" — {reason}" if reason else ""), f"- Whole-loop acceptance: {loop_acceptance(v, loop, items)}",
|
|
1326
1354
|
*([f"- Bring into {line}"] if (line := bring_in(v, loop)) else []), ""]
|
|
1355
|
+
if warnings:
|
|
1356
|
+
lines += ["## Warnings", "", *(f"- {warning}" for warning in warnings), ""]
|
|
1327
1357
|
try:
|
|
1328
1358
|
meta, _ = load_manifest(v["anchor"], loop, capture(v, loop))
|
|
1329
1359
|
decisions = sections(
|
|
@@ -1393,6 +1423,8 @@ def prune_parents(v, root):
|
|
|
1393
1423
|
|
|
1394
1424
|
def drain(args):
|
|
1395
1425
|
anchor, common = repository(args.repo)
|
|
1426
|
+
args.repo = anchor # Preserve the repository root for failures reported by main.
|
|
1427
|
+
diagnostics = skill_diagnostics(anchor)
|
|
1396
1428
|
require(any("{packet}" in part for part in args.executor), "executor argv must contain {packet}, replaced by the task packet path")
|
|
1397
1429
|
args.worktree_root = Path(args.worktree_root).resolve() if args.worktree_root else anchor.parent / f"{anchor.name}.devlyn"
|
|
1398
1430
|
with lock(common, "drain.lock", blocking=False):
|
|
@@ -1422,20 +1454,22 @@ def drain(args):
|
|
|
1422
1454
|
with contextlib.suppress(LoopError, OSError):
|
|
1423
1455
|
last = view(anchor, common, args.local_only)
|
|
1424
1456
|
if last is None:
|
|
1425
|
-
return {"status": status, "reason": reason}
|
|
1457
|
+
return {"status": status, "reason": reason, **diagnostics}
|
|
1426
1458
|
annotate(last, refused)
|
|
1427
1459
|
prune_parents(last, args.worktree_root)
|
|
1428
|
-
return {"status": status, "reason": reason,
|
|
1460
|
+
return {"status": status, "reason": reason, **diagnostics,
|
|
1461
|
+
"reports": write_reports(last, status, reason, diagnostics["warnings"]), "counts": counts(last),
|
|
1429
1462
|
"tasks": [summary(last, row) for row in last["rows"] if row["identity"]], "cleanup": last["cleanup"]}
|
|
1430
1463
|
|
|
1431
1464
|
|
|
1432
1465
|
def status(args):
|
|
1433
1466
|
anchor, common = repository(args.repo)
|
|
1467
|
+
args.repo = anchor # Preserve the repository root for failures reported by main.
|
|
1434
1468
|
v = view(anchor, common, args.local_only)
|
|
1435
1469
|
row = next_task(v, set())
|
|
1436
1470
|
annotate(v, {})
|
|
1437
1471
|
tasks = [summary(v, r) for r in v["rows"] if r["identity"]]
|
|
1438
|
-
return {"status": "OK", "counts": counts(v), "next": row["identity"] if row else None,
|
|
1472
|
+
return {"status": "OK", **skill_diagnostics(anchor), "counts": counts(v), "next": row["identity"] if row else None,
|
|
1439
1473
|
"blockers": [f"{t['identity']}: {t['reason']}" for t in tasks if t.get("reason") and t["result"] in {"pending", "active", "blocked"}]
|
|
1440
1474
|
+ [f"legacy row {r['index'] + 1} needs planning: {r['text']}" for r in v["rows"] if not r["identity"] and r["mark"] == " "]
|
|
1441
1475
|
+ [f"unreadable receipt {item}" for item in v["unreadable"]],
|
|
@@ -1997,6 +2031,8 @@ def main():
|
|
|
1997
2031
|
result = {"check": check, "add": add, "status": status, "drain": drain}[args.action](args)
|
|
1998
2032
|
except (LoopError, OSError) as exc:
|
|
1999
2033
|
result = {"status": "BLOCKED", "reason": str(exc)}
|
|
2034
|
+
if args.action in {"status", "drain"}:
|
|
2035
|
+
result.update(skill_diagnostics(Path(args.repo)))
|
|
2000
2036
|
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
|
|
2001
2037
|
return 1 if result["status"] == "BLOCKED" else 0
|
|
2002
2038
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.17",
|
|
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": {
|