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.
Files changed (70) hide show
  1. package/bin/devlyn.js +510 -0
  2. package/bin/engines.js +20 -15
  3. package/bin/harness-activation.js +1 -2
  4. package/bin/release.js +16 -3
  5. package/bin/skill-history.json +5 -0
  6. package/config/skills/_shared/harness-store.py +202 -0
  7. package/config/skills/_shared/task-completion.md +6 -3
  8. package/config/skills/_shared/warm-build-dirs.py +97 -2
  9. package/config/skills/devlyn-ideate/SKILL.md +5 -1
  10. package/config/skills/devlyn-ideate/references/loop.md +12 -2
  11. package/config/skills/devlyn-ideate/references/plan-add.md +12 -0
  12. package/config/skills/devlyn-ideate/scripts/acceptance.py +5 -0
  13. package/config/skills/devlyn-ideate/scripts/queue.py +165 -15
  14. package/harness/AGENTS.md +37 -0
  15. package/harness/CLAUDE.md +37 -0
  16. package/native/claude/.claude-plugin/plugin.json +6 -0
  17. package/native/claude/skills/_shared/.ruff.toml +2 -0
  18. package/native/claude/skills/_shared/adapters/claude.md +15 -0
  19. package/native/claude/skills/_shared/adapters/codex.md +14 -0
  20. package/native/claude/skills/_shared/adapters/omp.md +6 -0
  21. package/native/claude/skills/_shared/codex-monitored.sh +263 -0
  22. package/native/claude/skills/_shared/engine-doctor.sh +70 -0
  23. package/native/claude/skills/_shared/engine-preflight.md +29 -0
  24. package/native/claude/skills/_shared/expected-contract.py +242 -0
  25. package/native/claude/skills/_shared/expected.schema.json +120 -0
  26. package/native/claude/skills/_shared/harness-store.py +202 -0
  27. package/native/claude/skills/_shared/invocation-receipt.py +499 -0
  28. package/native/claude/skills/_shared/platform-support.py +395 -0
  29. package/native/claude/skills/_shared/role-config.py +247 -0
  30. package/native/claude/skills/_shared/runtime-principles.md +35 -0
  31. package/native/claude/skills/_shared/task-complete.py +2865 -0
  32. package/native/claude/skills/_shared/task-completion.md +249 -0
  33. package/native/claude/skills/_shared/warm-build-dirs.py +1065 -0
  34. package/native/claude/skills/devlyn-engines/SKILL.md +63 -0
  35. package/native/claude/skills/devlyn-ideate/SKILL.md +132 -0
  36. package/native/claude/skills/devlyn-ideate/references/elicitation.md +29 -0
  37. package/native/claude/skills/devlyn-ideate/references/loop.md +123 -0
  38. package/native/claude/skills/devlyn-ideate/references/package-format.md +128 -0
  39. package/native/claude/skills/devlyn-ideate/references/plan-add.md +12 -0
  40. package/native/claude/skills/devlyn-ideate/scripts/acceptance.py +882 -0
  41. package/native/claude/skills/devlyn-ideate/scripts/queue.py +2191 -0
  42. package/native/codex/.agents/plugins/marketplace.json +12 -0
  43. package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +6 -0
  44. package/native/codex/plugins/devlyn/skills/_shared/.ruff.toml +2 -0
  45. package/native/codex/plugins/devlyn/skills/_shared/adapters/claude.md +15 -0
  46. package/native/codex/plugins/devlyn/skills/_shared/adapters/codex.md +14 -0
  47. package/native/codex/plugins/devlyn/skills/_shared/adapters/omp.md +6 -0
  48. package/native/codex/plugins/devlyn/skills/_shared/codex-monitored.sh +263 -0
  49. package/native/codex/plugins/devlyn/skills/_shared/engine-doctor.sh +70 -0
  50. package/native/codex/plugins/devlyn/skills/_shared/engine-preflight.md +29 -0
  51. package/native/codex/plugins/devlyn/skills/_shared/expected-contract.py +242 -0
  52. package/native/codex/plugins/devlyn/skills/_shared/expected.schema.json +120 -0
  53. package/native/codex/plugins/devlyn/skills/_shared/harness-store.py +202 -0
  54. package/native/codex/plugins/devlyn/skills/_shared/invocation-receipt.py +499 -0
  55. package/native/codex/plugins/devlyn/skills/_shared/platform-support.py +395 -0
  56. package/native/codex/plugins/devlyn/skills/_shared/role-config.py +247 -0
  57. package/native/codex/plugins/devlyn/skills/_shared/runtime-principles.md +35 -0
  58. package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +2865 -0
  59. package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +249 -0
  60. package/native/codex/plugins/devlyn/skills/_shared/warm-build-dirs.py +1065 -0
  61. package/native/codex/plugins/devlyn/skills/devlyn-engines/SKILL.md +63 -0
  62. package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +132 -0
  63. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/elicitation.md +29 -0
  64. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +123 -0
  65. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/package-format.md +128 -0
  66. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/plan-add.md +12 -0
  67. package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py +882 -0
  68. package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +2191 -0
  69. package/package.json +3 -2
  70. 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
- differ = git(anchor, "diff", "--name-only", "--no-renames", rev, "--", *names).split("\n")
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": 1, "task": identity, "title": spec["title"], "kind": spec["kind"],
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
- argv = [sys.executable, str(SKILLS / "_shared/task-complete.py"), "complete", "--receipt", str(state["path"])]
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 status(args):
1465
- anchor, common = repository(args.repo)
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,6 @@
1
+ {
2
+ "name": "devlyn",
3
+ "version": "4.3.3",
4
+ "description": "Devlyn harness skills",
5
+ "skills": "./skills/"
6
+ }
@@ -0,0 +1,2 @@
1
+ # Vendored devlyn helpers are checked by devlyn's own validation.
2
+ exclude = ["*"]
@@ -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 `-`.
@@ -0,0 +1,6 @@
1
+ # oh-my-pi adapter
2
+
3
+ executor: no
4
+
5
+ Engine `omp`, the oh-my-pi CLI.
6
+ Stub: model-call execution and failure handling have not been qualified.