devlyn-cli 4.2.26 → 4.3.3
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/devlyn.js +510 -0
- package/bin/engines.js +20 -15
- package/bin/harness-activation.js +1 -2
- package/bin/release.js +16 -3
- package/bin/skill-history.json +5 -0
- package/config/skills/_shared/harness-store.py +202 -0
- package/config/skills/_shared/task-completion.md +6 -3
- package/config/skills/_shared/warm-build-dirs.py +97 -2
- 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 +165 -15
- package/harness/AGENTS.md +37 -0
- package/harness/CLAUDE.md +37 -0
- package/native/claude/.claude-plugin/plugin.json +6 -0
- package/native/claude/skills/_shared/.ruff.toml +2 -0
- package/native/claude/skills/_shared/adapters/claude.md +15 -0
- package/native/claude/skills/_shared/adapters/codex.md +14 -0
- package/native/claude/skills/_shared/adapters/omp.md +6 -0
- package/native/claude/skills/_shared/codex-monitored.sh +263 -0
- package/native/claude/skills/_shared/engine-doctor.sh +70 -0
- package/native/claude/skills/_shared/engine-preflight.md +29 -0
- package/native/claude/skills/_shared/expected-contract.py +242 -0
- package/native/claude/skills/_shared/expected.schema.json +120 -0
- package/native/claude/skills/_shared/harness-store.py +202 -0
- package/native/claude/skills/_shared/invocation-receipt.py +499 -0
- package/native/claude/skills/_shared/platform-support.py +395 -0
- package/native/claude/skills/_shared/role-config.py +247 -0
- package/native/claude/skills/_shared/runtime-principles.md +35 -0
- package/native/claude/skills/_shared/task-complete.py +2865 -0
- package/native/claude/skills/_shared/task-completion.md +249 -0
- package/native/claude/skills/_shared/warm-build-dirs.py +1065 -0
- package/native/claude/skills/devlyn-engines/SKILL.md +63 -0
- package/native/claude/skills/devlyn-ideate/SKILL.md +132 -0
- package/native/claude/skills/devlyn-ideate/references/elicitation.md +29 -0
- package/native/claude/skills/devlyn-ideate/references/loop.md +123 -0
- package/native/claude/skills/devlyn-ideate/references/package-format.md +128 -0
- package/native/claude/skills/devlyn-ideate/references/plan-add.md +12 -0
- package/native/claude/skills/devlyn-ideate/scripts/acceptance.py +882 -0
- package/native/claude/skills/devlyn-ideate/scripts/queue.py +2191 -0
- package/native/codex/.agents/plugins/marketplace.json +12 -0
- package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +6 -0
- package/native/codex/plugins/devlyn/skills/_shared/.ruff.toml +2 -0
- package/native/codex/plugins/devlyn/skills/_shared/adapters/claude.md +15 -0
- package/native/codex/plugins/devlyn/skills/_shared/adapters/codex.md +14 -0
- package/native/codex/plugins/devlyn/skills/_shared/adapters/omp.md +6 -0
- package/native/codex/plugins/devlyn/skills/_shared/codex-monitored.sh +263 -0
- package/native/codex/plugins/devlyn/skills/_shared/engine-doctor.sh +70 -0
- package/native/codex/plugins/devlyn/skills/_shared/engine-preflight.md +29 -0
- package/native/codex/plugins/devlyn/skills/_shared/expected-contract.py +242 -0
- package/native/codex/plugins/devlyn/skills/_shared/expected.schema.json +120 -0
- package/native/codex/plugins/devlyn/skills/_shared/harness-store.py +202 -0
- package/native/codex/plugins/devlyn/skills/_shared/invocation-receipt.py +499 -0
- package/native/codex/plugins/devlyn/skills/_shared/platform-support.py +395 -0
- package/native/codex/plugins/devlyn/skills/_shared/role-config.py +247 -0
- package/native/codex/plugins/devlyn/skills/_shared/runtime-principles.md +35 -0
- package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +2865 -0
- package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +249 -0
- package/native/codex/plugins/devlyn/skills/_shared/warm-build-dirs.py +1065 -0
- package/native/codex/plugins/devlyn/skills/devlyn-engines/SKILL.md +63 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +132 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/elicitation.md +29 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +123 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/package-format.md +128 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/plan-add.md +12 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py +882 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +2191 -0
- package/package.json +3 -2
- package/release.json +1630 -0
|
@@ -170,6 +170,67 @@ 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 bindable(anchor, common):
|
|
212
|
+
"""Whether a first add may bind its release: the queue reads and holds no unfinished work. A queue that cannot be read
|
|
213
|
+
is not proven settled, so the add captures unbound, as its skill source adds, and status and drain report why."""
|
|
214
|
+
try:
|
|
215
|
+
return not unfinished(view(anchor, common, pure=True))
|
|
216
|
+
except (LoopError, OSError):
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def release_settled_binding(anchor, common, *, interrupted_add=False):
|
|
221
|
+
"""Remove the binding once nothing needs it. The bound release keeps one from an interrupted first add (no record
|
|
222
|
+
yet) so its retry rebinds the same identity; another release (`interrupted_add`) drops that too, since a binding
|
|
223
|
+
published before its capture captured nothing. Returns whether it was removed."""
|
|
224
|
+
with lock(common, "queue.lock", blocking=True):
|
|
225
|
+
if not harness_call("read_binding", common):
|
|
226
|
+
return False
|
|
227
|
+
v = view(anchor, common, pure=True)
|
|
228
|
+
if unfinished(v) or not (v["records"] or interrupted_add):
|
|
229
|
+
return False
|
|
230
|
+
(common / "devlyn-loops/harness.json").unlink()
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
|
|
173
234
|
def sections(text, path):
|
|
174
235
|
found, current, fence = {}, None, False
|
|
175
236
|
for line in text.split("\n"):
|
|
@@ -686,6 +747,7 @@ def derive_state(anchor, common, row, claims, rev):
|
|
|
686
747
|
if packet.exists():
|
|
687
748
|
bound = read_json(packet)
|
|
688
749
|
require(isinstance(bound, dict) and not acceptance()["PACKET_KEYS"] - bound.keys(), f"{packet}: malformed task packet")
|
|
750
|
+
harness_call("packet_schema", bound)
|
|
689
751
|
require(bound["worktree"] == receipt["worktree"] and bound["task"] == identity and bound["branch"] == receipt["branch"]
|
|
690
752
|
and bound["allocation_base"] == receipt["baseline"],
|
|
691
753
|
f"{packet}: conflicting packet ownership")
|
|
@@ -713,11 +775,13 @@ def derive_state(anchor, common, row, claims, rev):
|
|
|
713
775
|
return state
|
|
714
776
|
|
|
715
777
|
|
|
716
|
-
def view(anchor, common, local_only=False):
|
|
778
|
+
def view(anchor, common, local_only=False, *, pure=False):
|
|
717
779
|
"""Reconcile the queue with every receipt and recovery ref before anything is selected."""
|
|
718
|
-
with lock(common, "queue.lock", blocking=True):
|
|
780
|
+
with contextlib.nullcontext() if pure else lock(common, "queue.lock", blocking=True):
|
|
781
|
+
if not pure:
|
|
782
|
+
enforce_binding(common)
|
|
719
783
|
recorded = records(common)
|
|
720
|
-
cleanup = clean(common, recorded)
|
|
784
|
+
cleanup = [] if pure else clean(common, recorded)
|
|
721
785
|
rows = queue_view(anchor, recorded)
|
|
722
786
|
claims, unreadable, damaged = {}, [], {}
|
|
723
787
|
canonical = {receipt_path(common, row["identity"]): row["identity"] for row in rows if row["identity"]}
|
|
@@ -737,7 +801,7 @@ def view(anchor, common, local_only=False):
|
|
|
737
801
|
if identity := canonical.get(path):
|
|
738
802
|
damaged[identity] = str(exc)
|
|
739
803
|
v = {"anchor": anchor, "common": common, "rows": rows, "records": recorded, "packages": {}, "manifests": {}, "states": {},
|
|
740
|
-
"unreadable": unreadable, "cleanup": cleanup, "local_only": local_only}
|
|
804
|
+
"unreadable": unreadable, "cleanup": cleanup, "local_only": local_only, "pure": pure}
|
|
741
805
|
errors = {}
|
|
742
806
|
for row in v["rows"]:
|
|
743
807
|
if row["identity"]:
|
|
@@ -902,7 +966,7 @@ def local_start(v, loop):
|
|
|
902
966
|
return None, f"{branch}, the branch {loop} was added on, {where}; bring {base} into {branch}, or plan the work as a new loop"
|
|
903
967
|
if drift := package_drift(v, loop, start, branch):
|
|
904
968
|
return start, drift
|
|
905
|
-
if drift := instruction_drift(v["anchor"], start):
|
|
969
|
+
if drift := instruction_drift(v["anchor"], start, pure=v.get("pure", False)):
|
|
906
970
|
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
971
|
return start, None
|
|
908
972
|
|
|
@@ -992,12 +1056,14 @@ def package_drift(v, loop, start, where):
|
|
|
992
1056
|
"bytes") if files else None
|
|
993
1057
|
|
|
994
1058
|
|
|
995
|
-
def instruction_drift(anchor, rev):
|
|
1059
|
+
def instruction_drift(anchor, rev, *, pure=False):
|
|
996
1060
|
"""The installed instruction files whose content at `rev` differs from the anchor checkout's (both absent is equal): a
|
|
997
1061
|
task starting at `rev` would run without them. Git compares them as `git status` does: a link by its target, line endings
|
|
998
1062
|
under the checkout's conversion rules, and an untracked or ignored file as one still to commit."""
|
|
999
1063
|
names = ("CLAUDE.md", "AGENTS.md")
|
|
1000
|
-
|
|
1064
|
+
# Git's porcelain diff refreshes the index independently of GIT_OPTIONAL_LOCKS.
|
|
1065
|
+
options = ("-c", "diff.autoRefreshIndex=false") if pure else ()
|
|
1066
|
+
differ = git(anchor, *options, "diff", "--name-only", "--no-renames", rev, "--", *names).split("\n")
|
|
1001
1067
|
differ += git(anchor, "ls-files", "--others", "--", *names).split("\n")
|
|
1002
1068
|
return ", ".join(name for name in names if name in differ)
|
|
1003
1069
|
|
|
@@ -1088,7 +1154,7 @@ def ensure_packet(v, row, path, receipt):
|
|
|
1088
1154
|
return {"path": str(worktree / rel), "sha256": hashlib.sha256(show(worktree, inputs, rel)).hexdigest()}
|
|
1089
1155
|
|
|
1090
1156
|
packet = {
|
|
1091
|
-
"schema_version":
|
|
1157
|
+
"schema_version": 2, "task": identity, "title": spec["title"], "kind": spec["kind"],
|
|
1092
1158
|
"worktree": str(worktree), "branch": branch, "receipt": str(path), "scratch": str(path.parent / "scratch"),
|
|
1093
1159
|
"allocation_base": baseline, "inputs_sha": inputs,
|
|
1094
1160
|
"contract": identity_of(f"docs/specs/{loop}/{task}/spec.md"),
|
|
@@ -1105,6 +1171,31 @@ def ensure_packet(v, row, path, receipt):
|
|
|
1105
1171
|
"runner": [sys.executable, str(Path(__file__).with_name("acceptance.py")), "run", "--packet", str(packet_path)],
|
|
1106
1172
|
"obligations": OBLIGATIONS,
|
|
1107
1173
|
}
|
|
1174
|
+
instructions = packet["methodology"]
|
|
1175
|
+
binding = harness_call("read_binding", v["common"])
|
|
1176
|
+
if binding:
|
|
1177
|
+
defaults = {"source": "release", **binding["defaults"]}
|
|
1178
|
+
else:
|
|
1179
|
+
# An unbound queue's executor still gets the principles: the invoking skill's own copy.
|
|
1180
|
+
own = SKILLS / "_shared/runtime-principles.md"
|
|
1181
|
+
defaults = {"source": "skill", **harness_call("file_identity", own)} if own.is_file() else None
|
|
1182
|
+
if defaults is None and not instructions:
|
|
1183
|
+
raise Waiting(f"{identity}: no principles reach its executor — neither CLAUDE.md/AGENTS.md at the inputs commit nor "
|
|
1184
|
+
f"{SKILLS / '_shared/runtime-principles.md'}; reinstall devlyn-cli to restore that file, then drain again")
|
|
1185
|
+
packet.update(harness=binding["release"] if binding else None,
|
|
1186
|
+
methodology={"defaults": defaults, "projectInstructions": instructions}, workerPrompt=None)
|
|
1187
|
+
if binding:
|
|
1188
|
+
harness_call("check_binding", binding, harness_call("resolve", SKILLS))
|
|
1189
|
+
bound_skills = Path(binding["release"]["root"]) / "config/skills"
|
|
1190
|
+
packet["runner"][1] = str(bound_skills / "devlyn-ideate/scripts/acceptance.py")
|
|
1191
|
+
prompt = path.parent / "worker-prompt.md"
|
|
1192
|
+
body = (f"Execute task packet {packet_path}. Work only in {worktree} on its owned branch.\n"
|
|
1193
|
+
f"Read {binding['defaults']['path']} and apply it as defaults; explicit user and native owner/project instructions win.\n"
|
|
1194
|
+
f"Set DEVLYN_SKILL_DIR={bound_skills / 'devlyn-ideate'} and DEVLYN_SHARED_DIR={bound_skills / '_shared'}.\n"
|
|
1195
|
+
f"Follow Executor exchange and Review records in {bound_skills / 'devlyn-ideate/references/loop.md'}.\n"
|
|
1196
|
+
f"Meet the packet obligations, run its runner and write {packet['submission']}. Never push, open a pull request or merge.\n")
|
|
1197
|
+
acceptance()["atomic_write"](prompt, body.encode("utf-8"))
|
|
1198
|
+
packet["workerPrompt"] = harness_call("file_identity", prompt)
|
|
1108
1199
|
write_json(packet_path, packet)
|
|
1109
1200
|
return packet_path, packet
|
|
1110
1201
|
|
|
@@ -1125,6 +1216,7 @@ def ensure_submission(identity, packet_path, packet, executor):
|
|
|
1125
1216
|
An attempt's start is recorded before the spawn and its exit after the wait, so a start without an exit may have
|
|
1126
1217
|
left a live executor: drain observes once and waits if it is active. Only established cessation permits adoption
|
|
1127
1218
|
or another execution. Where writers cannot be observed the task fails as interrupted-unobservable."""
|
|
1219
|
+
harness_call("verify_packet", packet, packet_path, SKILLS)
|
|
1128
1220
|
submission = Path(packet["submission"])
|
|
1129
1221
|
worktree = Path(packet["worktree"])
|
|
1130
1222
|
log = submission.with_name("executions.log")
|
|
@@ -1144,6 +1236,10 @@ def ensure_submission(identity, packet_path, packet, executor):
|
|
|
1144
1236
|
raise LoopError(f"cannot establish that an interrupted execution stopped: {exc}; repair writer observation, then drain again") from exc
|
|
1145
1237
|
if submission.exists():
|
|
1146
1238
|
return None
|
|
1239
|
+
defaults = packet["methodology"].get("defaults") if packet.get("schema_version") == 2 else None
|
|
1240
|
+
if packet.get("harness") is None and defaults and not Path(defaults["path"]).is_file():
|
|
1241
|
+
# An installation's own copy moves with its updates; only its absence matters, and only before a spawn.
|
|
1242
|
+
raise Waiting(f"{identity}: its principles file {defaults['path']} is gone; reinstall devlyn-cli to restore it, then drain again")
|
|
1147
1243
|
argv = [part.replace("{packet}", str(packet_path)) for part in executor]
|
|
1148
1244
|
if any("{worktree_git_dir}" in part for part in argv): # Optional: a Codex executor lists it as a writable root.
|
|
1149
1245
|
git_dir = git(worktree, "rev-parse", "--path-format=absolute", "--git-dir")
|
|
@@ -1217,6 +1313,8 @@ def advance(v, row, opts):
|
|
|
1217
1313
|
if not path.exists() and (refused := allocate(v, row, opts)):
|
|
1218
1314
|
return refused
|
|
1219
1315
|
receipt = read_json(path)
|
|
1316
|
+
if receipt.get("acceptance") and (path.parent / "packet.json").exists():
|
|
1317
|
+
harness_call("verify_packet", read_json(path.parent / "packet.json"), path.parent / "packet.json", SKILLS)
|
|
1220
1318
|
if not receipt.get("acceptance"):
|
|
1221
1319
|
state["phase"] = "inputs"
|
|
1222
1320
|
failure = v["states"][identity].get("inputs_changed")
|
|
@@ -1269,7 +1367,9 @@ def summary(v, row):
|
|
|
1269
1367
|
branch = receipt["branch"]
|
|
1270
1368
|
if receipt.get("acceptance") and receipt.get("delivery") not in SETTLED:
|
|
1271
1369
|
if receipt.get("queue"):
|
|
1272
|
-
|
|
1370
|
+
binding = harness_call("read_binding", v["common"])
|
|
1371
|
+
skills = Path(binding["release"]["root"]) / "config/skills" if binding else SKILLS
|
|
1372
|
+
argv = [sys.executable, str(skills / "_shared/task-complete.py"), "complete", "--receipt", str(state["path"])]
|
|
1273
1373
|
if receipt.get("writers_released"):
|
|
1274
1374
|
argv.append("--writers-stopped")
|
|
1275
1375
|
if not receipt.get("local_only") and not receipt.get("mode_override"):
|
|
@@ -1420,6 +1520,7 @@ def prune_parents(v, root):
|
|
|
1420
1520
|
def drain(args):
|
|
1421
1521
|
anchor, common = repository(args.repo)
|
|
1422
1522
|
args.repo = anchor # Preserve the repository root for failures reported by main.
|
|
1523
|
+
enforce_binding(common, anchor)
|
|
1423
1524
|
diagnostics = skill_diagnostics(anchor)
|
|
1424
1525
|
require(any("{packet}" in part for part in args.executor), "executor argv must contain {packet}, replaced by the task packet path")
|
|
1425
1526
|
args.default_worktree_root = args.worktree_root is None
|
|
@@ -1456,15 +1557,17 @@ def drain(args):
|
|
|
1456
1557
|
last["cleanup"].extend(args.root_diagnostics)
|
|
1457
1558
|
annotate(last, refused)
|
|
1458
1559
|
prune_parents(last, args.worktree_root)
|
|
1560
|
+
try:
|
|
1561
|
+
release_settled_binding(anchor, common)
|
|
1562
|
+
except (LoopError, OSError) as exc:
|
|
1563
|
+
last["cleanup"].append(f"queue binding not released: {exc}; the next status or drain retries")
|
|
1459
1564
|
return {"status": status, "reason": reason, **diagnostics,
|
|
1460
1565
|
"reports": write_reports(last, status, reason, diagnostics["warnings"]), "counts": counts(last),
|
|
1461
1566
|
"tasks": [summary(last, row) for row in last["rows"] if row["identity"]], "cleanup": last["cleanup"]}
|
|
1462
1567
|
|
|
1463
1568
|
|
|
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)
|
|
1569
|
+
def status_summary(v):
|
|
1570
|
+
anchor = v["anchor"]
|
|
1468
1571
|
row = next_task(v, set())
|
|
1469
1572
|
annotate(v, {})
|
|
1470
1573
|
tasks = [summary(v, r) for r in v["rows"] if r["identity"]]
|
|
@@ -1475,6 +1578,41 @@ def status(args):
|
|
|
1475
1578
|
"delivery": [t for t in tasks if t.get("resume")], "cleanup": v["cleanup"]}
|
|
1476
1579
|
|
|
1477
1580
|
|
|
1581
|
+
def status(args):
|
|
1582
|
+
anchor, common = repository(args.repo)
|
|
1583
|
+
args.repo = anchor
|
|
1584
|
+
enforce_binding(common, anchor)
|
|
1585
|
+
result = status_summary(view(anchor, common, args.local_only))
|
|
1586
|
+
release_settled_binding(anchor, common)
|
|
1587
|
+
return result
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
def inspect(args):
|
|
1591
|
+
anchor, common = repository(args.repo)
|
|
1592
|
+
try:
|
|
1593
|
+
result = status_summary(view(anchor, common, pure=True))
|
|
1594
|
+
except (LoopError, OSError) as exc:
|
|
1595
|
+
result = {"status": "BLOCKED", "reason": str(exc), **skill_diagnostics(anchor)}
|
|
1596
|
+
binding_path = common / "devlyn-loops/harness.json"
|
|
1597
|
+
binding = None
|
|
1598
|
+
compatibility = {"compatible": True, "legacy": not (binding_path.exists() or binding_path.is_symlink()), "reasons": [], "recoveryArgv": None,
|
|
1599
|
+
"taskPacket": {"inspect": [1, 2], "execute": [1, 2], "write": 2}, "queueBinding": {"read": [1, 1], "write": 1}}
|
|
1600
|
+
try:
|
|
1601
|
+
binding = read_json(binding_path) if not compatibility["legacy"] else None
|
|
1602
|
+
enforce_binding(common)
|
|
1603
|
+
for packet in (common / "devlyn-completion").glob("*/packet.json"):
|
|
1604
|
+
harness_call("packet_schema", read_json(packet))
|
|
1605
|
+
except (LoopError, OSError) as exc:
|
|
1606
|
+
compatibility.update(compatible=False, reasons=[str(exc)])
|
|
1607
|
+
try:
|
|
1608
|
+
valid = harness_call("read_binding", common)
|
|
1609
|
+
if valid:
|
|
1610
|
+
compatibility["recoveryArgv"] = harness_call("recovery_argv", valid, ["inspect", "--repo", str(anchor), "--json"])
|
|
1611
|
+
except (LoopError, OSError):
|
|
1612
|
+
pass # The diagnostic above describes the malformed binding; no safe argv can be inferred.
|
|
1613
|
+
return {**result, "binding": binding, "compatibility": compatibility}
|
|
1614
|
+
|
|
1615
|
+
|
|
1478
1616
|
def legacy_target(anchor, number):
|
|
1479
1617
|
"""The pending legacy row on line `number` of the legacy queue, as add records it: its exact text and its occurrence
|
|
1480
1618
|
among equal rows, line endings aside; None for any other line."""
|
|
@@ -1501,7 +1639,9 @@ def add(args):
|
|
|
1501
1639
|
ref, package_dir = CAPTURES + loop, f"docs/specs/{loop}"
|
|
1502
1640
|
require(meta.name == "meta.md" and meta.parent.parent == anchor / "docs/specs" and re.fullmatch(ID, loop),
|
|
1503
1641
|
f"package must be <repo>/docs/specs/<loop-id>/meta.md: {meta}")
|
|
1642
|
+
enforce_binding(common, anchor)
|
|
1504
1643
|
with lock(common, "queue.lock", blocking=True):
|
|
1644
|
+
enforce_binding(common)
|
|
1505
1645
|
recorded = records(common)
|
|
1506
1646
|
notes = clean(common, recorded)
|
|
1507
1647
|
target = legacy_target(anchor, args.materialize) if args.materialize else None
|
|
@@ -1540,6 +1680,11 @@ def add(args):
|
|
|
1540
1680
|
require(" ".join(target["line"][6:].split()) in intent, "meta.md '## Intent' must reproduce the legacy row's intent verbatim")
|
|
1541
1681
|
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
1682
|
files = {rel: package_bytes(anchor, None, rel) for rel in paths} | {loop_queue(loop): loop_rows(loop, package, target)}
|
|
1683
|
+
# The lock covers both first binding and first capture; an interrupted binding is authoritative on retry.
|
|
1684
|
+
if harness_call("read_binding", common) is None:
|
|
1685
|
+
invoking = harness_call("resolve", SKILLS)
|
|
1686
|
+
if invoking is not None and bindable(anchor, common):
|
|
1687
|
+
write_json(common / "devlyn-loops/harness.json", invoking)
|
|
1543
1688
|
tree = build_tree(anchor, None, files)
|
|
1544
1689
|
if captured:
|
|
1545
1690
|
require(tree == git(anchor, "rev-parse", captured + "^{tree}"), interrupted)
|
|
@@ -2016,6 +2161,9 @@ def main():
|
|
|
2016
2161
|
action = actions.add_parser(name)
|
|
2017
2162
|
action.add_argument("--repo", default=".")
|
|
2018
2163
|
action.add_argument("--local-only", "--no-push", action="store_true")
|
|
2164
|
+
inspector = actions.add_parser("inspect")
|
|
2165
|
+
inspector.add_argument("--repo", default=".")
|
|
2166
|
+
inspector.add_argument("--json", action="store_true")
|
|
2019
2167
|
actions.choices["drain"].add_argument("--worktree-root")
|
|
2020
2168
|
actions.choices["drain"].add_argument("executor", nargs="+", help="host executor argv after --; {packet} becomes the packet path")
|
|
2021
2169
|
args = parser.parse_args()
|
|
@@ -2025,9 +2173,11 @@ def main():
|
|
|
2025
2173
|
print(f"queue self-test: PASS ({result.testsRun} tests)")
|
|
2026
2174
|
return 0 if result.wasSuccessful() else 1
|
|
2027
2175
|
if args.action is None:
|
|
2028
|
-
parser.error("check, add, status or drain is required")
|
|
2176
|
+
parser.error("check, add, status, inspect or drain is required")
|
|
2177
|
+
if args.action == "inspect":
|
|
2178
|
+
os.environ["GIT_OPTIONAL_LOCKS"] = "0" # git diff may otherwise refresh the index even with unchanged bytes.
|
|
2029
2179
|
try:
|
|
2030
|
-
result = {"check": check, "add": add, "status": status, "drain": drain}[args.action](args)
|
|
2180
|
+
result = {"check": check, "add": add, "status": status, "drain": drain, "inspect": inspect}[args.action](args)
|
|
2031
2181
|
except (LoopError, OSError) as exc:
|
|
2032
2182
|
result = {"status": "BLOCKED", "reason": str(exc)}
|
|
2033
2183
|
if args.action in {"status", "drain"}:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!-- devlyn:instructions:begin sha256=019ab670237e90f2e40ea0dbff216bbf2b523a04419ca9c5f5f403482cf55874 -->
|
|
2
|
+
Project-specific instructions outside this managed block take precedence over these defaults.
|
|
3
|
+
|
|
4
|
+
# Project Instructions
|
|
5
|
+
|
|
6
|
+
devlyn-cli installs the `devlyn-ideate` and `devlyn-engines` skills alongside the principles below, which are non-negotiable on every change — yours and any sub-agent's.
|
|
7
|
+
|
|
8
|
+
## North Star
|
|
9
|
+
|
|
10
|
+
This contract serves one goal: any capable engine — Claude, GPT/Codex, or a future adapter-equipped model — takes a user's intent (prompt, spec, or queue entry) end-to-end to shipped, engineer-quality software, hands-free, with consistent quality across engines. When rules below conflict or feel ambiguous in context, resolve toward this goal.
|
|
11
|
+
|
|
12
|
+
## Core principles
|
|
13
|
+
|
|
14
|
+
Seven rules govern every change. Cite them by name when a decision touches one.
|
|
15
|
+
|
|
16
|
+
1. **No workaround** — fix the root cause, never the symptom. No `any`, no `@ts-ignore`, no silent `catch`, no hardcoded fallback that hides a broken contract. No config bypasses.
|
|
17
|
+
2. **No overengineering** — smallest change that closes the goal. New abstractions require an observed failure mode they prevent. Subtractive-first: ask "what can I delete instead?" before writing anything new. Remove code your change makes unused; only report unrelated pre-existing dead code.
|
|
18
|
+
3. **No guesswork** — verify with the actual files, logs, diffs, and run output before forming conclusions. State the falsifiable prediction BEFORE the experiment; record raw results AFTER. Retroactive prediction edits are dishonest.
|
|
19
|
+
4. **Worldclass** — code that survives review at a non-trivial codebase. Zero CRITICAL, zero HIGH security/design findings on the shippable path.
|
|
20
|
+
5. **Best practice** — idiomatic for the language and framework. Use standard primitives; do not hand-roll what the library already provides.
|
|
21
|
+
6. **Optimized** — use efficient code and avoid unnecessary work without sacrificing correctness or the user's requirements.
|
|
22
|
+
7. **Production ready** — error states are explicit and visible; behavior under failure is what the user expects, not silent corruption.
|
|
23
|
+
|
|
24
|
+
Three discipline rules govern HOW the principles are applied:
|
|
25
|
+
|
|
26
|
+
- **Root cause via flexible why-chain.** Keep asking "why?" until you find the violated invariant. **If the answer surfaces in 2 questions, stop.** If it takes 5 or 7, keep going. Strict counts are wrong; until-found is right.
|
|
27
|
+
- **First-principles thinking.** Challenge the requirement before optimizing the answer. Surface unstated assumptions, ambiguities, tradeoffs, and simpler alternatives BEFORE implementing — do not silently pick one interpretation when multiple exist, do not hide confusion, push back when a simpler path is genuinely better. Most "we have to do X" assumptions are habit, not necessity. Reduce the problem to its irreducible truths and rebuild from there.
|
|
28
|
+
- **Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.** — Saint-Exupéry. This is the operating definition of "done." A change is finished when no further line, branch, flag, or doc paragraph can be removed without breaking a learned failure mode. Not before.
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
- `devlyn-ideate` — loop designer and intent queue: `plan <intent or document>` writes a validated loop package, `add` queues its tasks, `status` (or no arguments) reports the queue, `drain` executes it serially.
|
|
33
|
+
- Delivery — direct work edits the current checkout; deliver completed, verified requests with changes by project policy (default PR and merge), following `_shared/task-completion.md` in `.claude/skills/` or `.agents/skills/` (project, else `~`). With known concurrent writers, isolate before editing; on unexplained changes, pause writes until safe or isolated. An ideate drain delivers every task. Explicit limits win; commit alone stays local.
|
|
34
|
+
- Executor — `devlyn-engines` shows it and pins it in `.devlyn/engines.json`. A pinned executor does the implementation work, direct or drained: when it is not you, delegate to it, and when it is unavailable stop with `BLOCKED:<engine>-unavailable`. Without a pin, you are the executor.
|
|
35
|
+
|
|
36
|
+
Each skill's `SKILL.md` is the source of truth for its flags and workflow.
|
|
37
|
+
<!-- devlyn:instructions:end -->
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!-- devlyn:instructions:begin sha256=019ab670237e90f2e40ea0dbff216bbf2b523a04419ca9c5f5f403482cf55874 -->
|
|
2
|
+
Project-specific instructions outside this managed block take precedence over these defaults.
|
|
3
|
+
|
|
4
|
+
# Project Instructions
|
|
5
|
+
|
|
6
|
+
devlyn-cli installs the `devlyn-ideate` and `devlyn-engines` skills alongside the principles below, which are non-negotiable on every change — yours and any sub-agent's.
|
|
7
|
+
|
|
8
|
+
## North Star
|
|
9
|
+
|
|
10
|
+
This contract serves one goal: any capable engine — Claude, GPT/Codex, or a future adapter-equipped model — takes a user's intent (prompt, spec, or queue entry) end-to-end to shipped, engineer-quality software, hands-free, with consistent quality across engines. When rules below conflict or feel ambiguous in context, resolve toward this goal.
|
|
11
|
+
|
|
12
|
+
## Core principles
|
|
13
|
+
|
|
14
|
+
Seven rules govern every change. Cite them by name when a decision touches one.
|
|
15
|
+
|
|
16
|
+
1. **No workaround** — fix the root cause, never the symptom. No `any`, no `@ts-ignore`, no silent `catch`, no hardcoded fallback that hides a broken contract. No config bypasses.
|
|
17
|
+
2. **No overengineering** — smallest change that closes the goal. New abstractions require an observed failure mode they prevent. Subtractive-first: ask "what can I delete instead?" before writing anything new. Remove code your change makes unused; only report unrelated pre-existing dead code.
|
|
18
|
+
3. **No guesswork** — verify with the actual files, logs, diffs, and run output before forming conclusions. State the falsifiable prediction BEFORE the experiment; record raw results AFTER. Retroactive prediction edits are dishonest.
|
|
19
|
+
4. **Worldclass** — code that survives review at a non-trivial codebase. Zero CRITICAL, zero HIGH security/design findings on the shippable path.
|
|
20
|
+
5. **Best practice** — idiomatic for the language and framework. Use standard primitives; do not hand-roll what the library already provides.
|
|
21
|
+
6. **Optimized** — use efficient code and avoid unnecessary work without sacrificing correctness or the user's requirements.
|
|
22
|
+
7. **Production ready** — error states are explicit and visible; behavior under failure is what the user expects, not silent corruption.
|
|
23
|
+
|
|
24
|
+
Three discipline rules govern HOW the principles are applied:
|
|
25
|
+
|
|
26
|
+
- **Root cause via flexible why-chain.** Keep asking "why?" until you find the violated invariant. **If the answer surfaces in 2 questions, stop.** If it takes 5 or 7, keep going. Strict counts are wrong; until-found is right.
|
|
27
|
+
- **First-principles thinking.** Challenge the requirement before optimizing the answer. Surface unstated assumptions, ambiguities, tradeoffs, and simpler alternatives BEFORE implementing — do not silently pick one interpretation when multiple exist, do not hide confusion, push back when a simpler path is genuinely better. Most "we have to do X" assumptions are habit, not necessity. Reduce the problem to its irreducible truths and rebuild from there.
|
|
28
|
+
- **Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.** — Saint-Exupéry. This is the operating definition of "done." A change is finished when no further line, branch, flag, or doc paragraph can be removed without breaking a learned failure mode. Not before.
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
- `devlyn-ideate` — loop designer and intent queue: `plan <intent or document>` writes a validated loop package, `add` queues its tasks, `status` (or no arguments) reports the queue, `drain` executes it serially.
|
|
33
|
+
- Delivery — direct work edits the current checkout; deliver completed, verified requests with changes by project policy (default PR and merge), following `_shared/task-completion.md` in `.claude/skills/` or `.agents/skills/` (project, else `~`). With known concurrent writers, isolate before editing; on unexplained changes, pause writes until safe or isolated. An ideate drain delivers every task. Explicit limits win; commit alone stays local.
|
|
34
|
+
- Executor — `devlyn-engines` shows it and pins it in `.devlyn/engines.json`. A pinned executor does the implementation work, direct or drained: when it is not you, delegate to it, and when it is unavailable stop with `BLOCKED:<engine>-unavailable`. Without a pin, you are the executor.
|
|
35
|
+
|
|
36
|
+
Each skill's `SKILL.md` is the source of truth for its flags and workflow.
|
|
37
|
+
<!-- devlyn:instructions:end -->
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Claude adapter
|
|
2
|
+
|
|
3
|
+
## Invocation
|
|
4
|
+
|
|
5
|
+
A Claude executor starts as
|
|
6
|
+
`claude -p "<prompt>" --dangerously-skip-permissions --add-dir "<common git dir>"`,
|
|
7
|
+
where `<common git dir>` is `git rev-parse --path-format=absolute --git-common-dir`:
|
|
8
|
+
a linked task worktree commits there.
|
|
9
|
+
|
|
10
|
+
**Availability probe**: `command -v claude >/dev/null 2>&1`; record
|
|
11
|
+
`claude --version` as evidence. The probe is necessary, not sufficient:
|
|
12
|
+
`claude -p` needs network access to the Anthropic API, so in a network-denying
|
|
13
|
+
sandbox (e.g. Codex CLI's default `workspace-write`) it fails even though the
|
|
14
|
+
binary resolves. A drain blocks when the executor cannot start, and a task
|
|
15
|
+
whose executor exits without a submission waits for the next drain.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Codex adapter
|
|
2
|
+
|
|
3
|
+
## Invocation
|
|
4
|
+
|
|
5
|
+
A Codex executor runs through `_shared/codex-monitored.sh`, never a raw or piped
|
|
6
|
+
`codex exec`: `bash "$DEVLYN_SHARED_DIR/codex-monitored.sh" <arguments for codex exec> "<prompt>"`.
|
|
7
|
+
The `workspace-write` sandbox keeps `.git` read-only, so an executor that commits adds as
|
|
8
|
+
writable roots the common Git directory (`git rev-parse --path-format=absolute --git-common-dir`)
|
|
9
|
+
plus, in a linked worktree, its own Git directory (`git rev-parse --path-format=absolute --git-dir`),
|
|
10
|
+
listed exactly because the sandbox protects it even under a listed parent. It also
|
|
11
|
+
denies network, which installing a new dependency needs:
|
|
12
|
+
`-s workspace-write -c 'sandbox_workspace_write.writable_roots=["<git dir>","<worktree git dir>"]' -c 'sandbox_workspace_write.network_access=true'`.
|
|
13
|
+
Deliver a multiline prompt as exact file bytes: set `DEVLYN_CODEX_PROMPT_FILE=<file>`
|
|
14
|
+
and pass the sole prompt argument `-`.
|