devlyn-cli 4.3.0 → 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 (62) hide show
  1. package/bin/skill-history.json +3 -0
  2. package/config/skills/_shared/harness-store.py +2 -1
  3. package/config/skills/_shared/task-completion.md +6 -3
  4. package/config/skills/_shared/warm-build-dirs.py +97 -2
  5. package/config/skills/devlyn-ideate/scripts/queue.py +10 -1
  6. package/harness/AGENTS.md +37 -0
  7. package/harness/CLAUDE.md +37 -0
  8. package/native/claude/.claude-plugin/plugin.json +6 -0
  9. package/native/claude/skills/_shared/.ruff.toml +2 -0
  10. package/native/claude/skills/_shared/adapters/claude.md +15 -0
  11. package/native/claude/skills/_shared/adapters/codex.md +14 -0
  12. package/native/claude/skills/_shared/adapters/omp.md +6 -0
  13. package/native/claude/skills/_shared/codex-monitored.sh +263 -0
  14. package/native/claude/skills/_shared/engine-doctor.sh +70 -0
  15. package/native/claude/skills/_shared/engine-preflight.md +29 -0
  16. package/native/claude/skills/_shared/expected-contract.py +242 -0
  17. package/native/claude/skills/_shared/expected.schema.json +120 -0
  18. package/native/claude/skills/_shared/harness-store.py +202 -0
  19. package/native/claude/skills/_shared/invocation-receipt.py +499 -0
  20. package/native/claude/skills/_shared/platform-support.py +395 -0
  21. package/native/claude/skills/_shared/role-config.py +247 -0
  22. package/native/claude/skills/_shared/runtime-principles.md +35 -0
  23. package/native/claude/skills/_shared/task-complete.py +2865 -0
  24. package/native/claude/skills/_shared/task-completion.md +249 -0
  25. package/native/claude/skills/_shared/warm-build-dirs.py +1065 -0
  26. package/native/claude/skills/devlyn-engines/SKILL.md +63 -0
  27. package/native/claude/skills/devlyn-ideate/SKILL.md +132 -0
  28. package/native/claude/skills/devlyn-ideate/references/elicitation.md +29 -0
  29. package/native/claude/skills/devlyn-ideate/references/loop.md +123 -0
  30. package/native/claude/skills/devlyn-ideate/references/package-format.md +128 -0
  31. package/native/claude/skills/devlyn-ideate/references/plan-add.md +12 -0
  32. package/native/claude/skills/devlyn-ideate/scripts/acceptance.py +882 -0
  33. package/native/claude/skills/devlyn-ideate/scripts/queue.py +2191 -0
  34. package/native/codex/.agents/plugins/marketplace.json +12 -0
  35. package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +6 -0
  36. package/native/codex/plugins/devlyn/skills/_shared/.ruff.toml +2 -0
  37. package/native/codex/plugins/devlyn/skills/_shared/adapters/claude.md +15 -0
  38. package/native/codex/plugins/devlyn/skills/_shared/adapters/codex.md +14 -0
  39. package/native/codex/plugins/devlyn/skills/_shared/adapters/omp.md +6 -0
  40. package/native/codex/plugins/devlyn/skills/_shared/codex-monitored.sh +263 -0
  41. package/native/codex/plugins/devlyn/skills/_shared/engine-doctor.sh +70 -0
  42. package/native/codex/plugins/devlyn/skills/_shared/engine-preflight.md +29 -0
  43. package/native/codex/plugins/devlyn/skills/_shared/expected-contract.py +242 -0
  44. package/native/codex/plugins/devlyn/skills/_shared/expected.schema.json +120 -0
  45. package/native/codex/plugins/devlyn/skills/_shared/harness-store.py +202 -0
  46. package/native/codex/plugins/devlyn/skills/_shared/invocation-receipt.py +499 -0
  47. package/native/codex/plugins/devlyn/skills/_shared/platform-support.py +395 -0
  48. package/native/codex/plugins/devlyn/skills/_shared/role-config.py +247 -0
  49. package/native/codex/plugins/devlyn/skills/_shared/runtime-principles.md +35 -0
  50. package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +2865 -0
  51. package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +249 -0
  52. package/native/codex/plugins/devlyn/skills/_shared/warm-build-dirs.py +1065 -0
  53. package/native/codex/plugins/devlyn/skills/devlyn-engines/SKILL.md +63 -0
  54. package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +132 -0
  55. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/elicitation.md +29 -0
  56. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +123 -0
  57. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/package-format.md +128 -0
  58. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/plan-add.md +12 -0
  59. package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py +882 -0
  60. package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +2191 -0
  61. package/package.json +1 -1
  62. package/release.json +1630 -0
@@ -80,6 +80,7 @@
80
80
  "69a3fa5c1e7c5358cf7571936c8c09f5cc1d13999c8490ac10533af2091c4226",
81
81
  "6a523474b70ba99aff5472020cdde9ca1e7c91f8ee65db03c9e712adfac6d31d",
82
82
  "6d1a5458b23196b6f6fde6b7e9dc097ed173cb5914de439e512a032ca1892812",
83
+ "6e70044cab5a3fb1f78b7148c2111e4298ce8f5f8e902220430ca316bb61255d",
83
84
  "6fa2b5e089dcd00613c6e9800d2e763da9e5b2c80e6a7ef5d54d5ad7113e8399",
84
85
  "71fb125e20420d6f3fcd3396093fcdc8d552bab9626c3cbd962351e0d19adac8",
85
86
  "725395f70eec7e2ceae1f3aa122893433923b098a1e0d6202dd410979ef4eea0",
@@ -146,6 +147,7 @@
146
147
  "d3b40b9f0e15bf90ca557c0198a8fe5ff1d046b9aaa54b181c743d8e006f9c34",
147
148
  "d424feceffc005f220e33a19cac73f33bd40d120cc14da33725b8a990704bcf6",
148
149
  "d7388e36d76647a89ce52de615271928cf9fc78f980df399d2bc2e6d6675ec82",
150
+ "d85fa24b9b2c439742c9a62d5d4e07a031a1cfe6024bbc06f98eff7063424179",
149
151
  "dac3da11ea5686382f5c9892a0b8e4ec25e625e5946283d8e2ad944aa84e1df2",
150
152
  "db867d1fd98058bb20e41e942abd95f08fbaa9465928025f69f0a0308fa71514",
151
153
  "dd0bfdfd2db0b5f9ddef75d1b9ce3247698739fc551d292366dccd506fef6a36",
@@ -320,6 +322,7 @@
320
322
  "0dc69c20434966015176d83baa6f95b3421f9e13d0b1bd608a22f0560eae1049",
321
323
  "0fe9b270d670f70da9d2535969b6117684b7c1517806b3029ebaf9624da8006c",
322
324
  "1582c202d361af927c2d0843d4bcb72ccc98a0a2a9d8344d118d680f70f7e1e4",
325
+ "19207315f23c9a12bf2ced0e4e79bb7dfdc7cc5d0c9dd94095d9c6dfe1e8fea8",
323
326
  "1d8c3bd31c8b755acd0c3c9f416acb5024dfa99fbf5e2c14d8d6df00a24fe9b4",
324
327
  "300d1af60cc53285935dc4b5b370e157707218d844a4303b96d7826eef36ed43",
325
328
  "302b1a6c109087dce02ed6af184d6aeeca6cecfd40ebe638beecf8bdf21cf59c",
@@ -52,7 +52,8 @@ def release_ref(value):
52
52
 
53
53
  def verify_release(root):
54
54
  root = Path(root)
55
- script = "const r=require(process.argv[1]);r.runJson(()=>r.verifyRelease(process.argv[2]));"
55
+ # As harness-status verifies it: an npm-installed package keeps npm's own .bin shims, which are checked.
56
+ script = "const r=require(process.argv[1]);r.runJson(()=>r.verifyRelease(process.argv[2],{npmInstall:true}));"
56
57
  result = subprocess.run(["node", "-e", script, str(root / "bin/release.js"), str(root)], capture_output=True, text=True, encoding="utf-8")
57
58
  require(result.returncode == 0, f"release verification failed: {root}: {result.stderr.strip()}")
58
59
  try:
@@ -108,13 +108,16 @@ removes them. Preserve all other work and report anything left behind.
108
108
 
109
109
  Build in the task worktree's own default build directory, never in one
110
110
  another checkout builds in, even one build at a time: tools such as Cargo take
111
- one checkout's outputs as fresh for the other. When a Cargo workspace's
111
+ one checkout's outputs as fresh for the other. When the shared repository-local
112
+ Git config `devlyn.warmBuildDirs` is true, or it is unset and the Cargo workspace's
112
113
  Cargo.toml sets `[workspace.metadata.devlyn] warm-build-dirs = true`,
113
114
  allocation seeds that directory from a built checkout of the repository, so
114
115
  the first build does not rebuild every dependency; `build_dirs` in its result
115
116
  and receipt says what it did, and a skip means an ordinary cold build. The
116
- opt-in is the workspace's: seeding keeps dependency outputs that may hold the
117
- other checkout's paths, and those follow that checkout's later rebuilds. `warm-build-dirs.py <worktree>` does the same for a
117
+ Git setting takes precedence on this machine: false disables and invalid values
118
+ skip; global, system, include and per-worktree values are not consulted. Seeding
119
+ keeps dependency outputs that may hold the other checkout's paths, and those
120
+ follow that checkout's later rebuilds. `warm-build-dirs.py <worktree>` does the same for a
118
121
  worktree made without allocation. A warmed directory leaves with the worktree
119
122
  after merge, so a pending, failed or local-only task keeps it.
120
123
 
@@ -31,6 +31,9 @@ a crate that compiled a path in where the reading above does not see it) still n
31
31
  donor's files: when the donor rebuilds or removes them, the recipient can change behavior
32
32
  without a warning. That is why a workspace opts in, with `[workspace.metadata.devlyn]
33
33
  warm-build-dirs = true` in its Cargo.toml, once its dependencies do not use such paths.
34
+ The repository's shared local Git config `devlyn.warmBuildDirs` overrides that opt-in on
35
+ this machine: true enables, false disables, unset uses Cargo.toml, and invalid values skip.
36
+ Global, system, include and per-worktree config values are not consulted.
34
37
 
35
38
  The donor is only read: its dev build lock is taken from an existing lock file opened read-only,
36
39
  without waiting, while the layouts are cloned. The whole operation has a time limit. Every skip
@@ -89,6 +92,14 @@ class Skip(Exception):
89
92
  """An expected reason not to warm; the build starts cold."""
90
93
 
91
94
 
95
+ class CommandFailed(Skip):
96
+ """A command failed; retain its exit code and diagnostics for callers."""
97
+
98
+ def __init__(self, argv, result):
99
+ self.result = result
100
+ super().__init__(f"{' '.join(map(str, argv[:3]))} failed: {(result.stderr or result.stdout).strip()[-400:]}")
101
+
102
+
92
103
  class Unsafe(Exception):
93
104
  """A safety check failed; nothing was published and the donor was not written."""
94
105
 
@@ -120,7 +131,7 @@ def run(argv, *, cwd, deadline, env=None):
120
131
  except subprocess.TimeoutExpired as exc:
121
132
  raise TimeUp(f"{' '.join(map(str, argv[:3]))} timed out") from exc
122
133
  if result.returncode != 0:
123
- raise Skip(f"{' '.join(map(str, argv[:3]))} failed: {(result.stderr or result.stdout).strip()[-400:]}")
134
+ raise CommandFailed(argv, result)
124
135
  return result.stdout
125
136
 
126
137
 
@@ -166,7 +177,19 @@ def no_symlink_between(root, path):
166
177
  def destination(work, deadline, stack):
167
178
  """The worktree's own Cargo target directory and a descriptor for its parent, when it can be warmed."""
168
179
  found = metadata(work, deadline)
169
- if ((found.get("metadata") or {}).get("devlyn") or {}).get("warm-build-dirs") is not True:
180
+ setting = "git config --local devlyn.warmBuildDirs"
181
+ try:
182
+ enabled = git(work, "config", "--local", "--type=bool", "--get", "devlyn.warmBuildDirs", deadline=deadline)
183
+ except CommandFailed as exc:
184
+ if exc.result.returncode != 1:
185
+ error = (exc.result.stderr or exc.result.stdout or str(exc)).splitlines()[0]
186
+ raise Skip(f"cannot read {setting}: {error}") from exc
187
+ enabled = None
188
+ except OSError as exc:
189
+ raise Skip(f"cannot read {setting}: {exc}") from exc
190
+ if enabled == "false":
191
+ raise Skip(f"turned off: {setting} is false")
192
+ if enabled != "true" and ((found.get("metadata") or {}).get("devlyn") or {}).get("warm-build-dirs") is not True:
170
193
  raise Skip("not enabled: the workspace's Cargo.toml has no [workspace.metadata.devlyn] warm-build-dirs = true")
171
194
  if any(key in os.environ for key in REDIRECTS) and metadata(work, deadline, env=without_redirects()) != found:
172
195
  raise Skip("the target directory comes from this environment, which later commands may not share")
@@ -799,6 +822,78 @@ class WarmTests(unittest.TestCase):
799
822
  self.assertEqual([], list(self.staging.iterdir()))
800
823
  self.assertEqual("", self.git(self.work, "status", "--porcelain", "--untracked-files=all"))
801
824
 
825
+ @unittest.skipIf(os.name == "nt", "warming is POSIX-only")
826
+ def test_local_git_true_seeds_without_cargo_opt_in(self):
827
+ self.write_fixture(workspace_metadata=None)
828
+ self.git(self.anchor, "config", "--local", "devlyn.warmBuildDirs", "true")
829
+ [result] = self.warm()
830
+ self.assertEqual("seeded", result["status"], result)
831
+ self.assertTrue((self.work / "target" / "debug" / "deps" / f"libserde-{'1' * 16}.rlib").is_file())
832
+ self.assert_nothing_left()
833
+
834
+ @unittest.skipIf(os.name == "nt", "warming is POSIX-only")
835
+ def test_local_git_false_overrides_cargo_opt_in(self):
836
+ self.git(self.anchor, "config", "--local", "devlyn.warmBuildDirs", "false")
837
+ [result] = self.warm()
838
+ self.assertEqual("skipped", result["status"], result)
839
+ self.assertEqual("turned off: git config --local devlyn.warmBuildDirs is false", result["reason"])
840
+ self.assertFalse((self.work / "target").exists())
841
+ self.assert_nothing_left()
842
+
843
+ @unittest.skipIf(os.name == "nt", "warming is POSIX-only")
844
+ def test_invalid_local_git_value_skips_with_git_error(self):
845
+ self.git(self.anchor, "config", "--local", "devlyn.warmBuildDirs", "invalid")
846
+ [result] = self.warm()
847
+ self.assertEqual("skipped", result["status"], result)
848
+ self.assertIn("git config --local devlyn.warmBuildDirs", result["reason"])
849
+ self.assertIn("fatal: bad boolean config value 'invalid' for 'devlyn.warmbuilddirs'", result["reason"])
850
+ self.assertFalse((self.work / "target").exists())
851
+ self.assert_nothing_left()
852
+
853
+ @unittest.skipIf(os.name == "nt", "warming is POSIX-only")
854
+ def test_unset_local_git_value_uses_cargo_opt_in(self):
855
+ for opted_in in (False, True):
856
+ with self.subTest(opted_in=opted_in):
857
+ self.write_fixture(workspace_metadata={"devlyn": {"warm-build-dirs": opted_in}})
858
+ [result] = self.warm()
859
+ self.assertEqual("seeded" if opted_in else "skipped", result["status"], result)
860
+ if not opted_in:
861
+ self.assertIn("not enabled", result["reason"])
862
+ self.assert_nothing_left()
863
+
864
+ @unittest.skipIf(os.name == "nt", "warming is POSIX-only")
865
+ def test_other_git_config_failures_skip_with_first_error_line(self):
866
+ config = self.anchor / ".git" / "config"
867
+ config.write_text(config.read_text() + "\n[broken\nsecond bad line\n")
868
+ error = subprocess.run(["git", "-C", str(self.work), "config", "--local", "--type=bool",
869
+ "--get", "devlyn.warmBuildDirs"], capture_output=True, text=True)
870
+ self.assertNotIn(error.returncode, (0, 1))
871
+ [result] = self.warm()
872
+ self.assertEqual("skipped", result["status"], result)
873
+ self.assertEqual(f"cannot read git config --local devlyn.warmBuildDirs: {error.stderr.splitlines()[0]}",
874
+ result["reason"])
875
+ self.assertFalse((self.work / "target").exists())
876
+ self.assertEqual([], list(self.staging.iterdir()))
877
+
878
+ @unittest.skipIf(os.name == "nt", "warming is POSIX-only")
879
+ def test_global_include_and_per_worktree_config_do_not_override_cargo(self):
880
+ global_config, included = self.root / "global-config", self.root / "included-config"
881
+ os.environ["GIT_CONFIG_GLOBAL"] = str(global_config)
882
+ self.git(self.anchor, "config", "--local", "include.path", str(included))
883
+ self.git(self.anchor, "config", "--local", "extensions.worktreeConfig", "true")
884
+ for opted_in in (False, True):
885
+ with self.subTest(opted_in=opted_in):
886
+ value = "false" if opted_in else "true"
887
+ for config in (global_config, included):
888
+ config.write_text(f"[devlyn]\n\twarmBuildDirs = {value}\n")
889
+ self.git(self.work, "config", "--worktree", "devlyn.warmBuildDirs", value)
890
+ self.write_fixture(workspace_metadata={"devlyn": {"warm-build-dirs": opted_in}})
891
+ [result] = self.warm()
892
+ self.assertEqual("seeded" if opted_in else "skipped", result["status"], result)
893
+ if not opted_in:
894
+ self.assertIn("not enabled", result["reason"])
895
+ self.assert_nothing_left()
896
+
802
897
  @unittest.skipIf(os.name == "nt", "warming is POSIX-only")
803
898
  def test_seeds_only_what_carries_over_and_never_writes_the_donor(self):
804
899
  before = self.snapshot(self.donor)
@@ -208,6 +208,15 @@ def unfinished(v):
208
208
  return any(ref.removeprefix(CAPTURES) not in v["records"] for ref in refs)
209
209
 
210
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
+
211
220
  def release_settled_binding(anchor, common, *, interrupted_add=False):
212
221
  """Remove the binding once nothing needs it. The bound release keeps one from an interrupted first add (no record
213
222
  yet) so its retry rebinds the same identity; another release (`interrupted_add`) drops that too, since a binding
@@ -1674,7 +1683,7 @@ def add(args):
1674
1683
  # The lock covers both first binding and first capture; an interrupted binding is authoritative on retry.
1675
1684
  if harness_call("read_binding", common) is None:
1676
1685
  invoking = harness_call("resolve", SKILLS)
1677
- if invoking is not None and not unfinished(view(anchor, common, pure=True)):
1686
+ if invoking is not None and bindable(anchor, common):
1678
1687
  write_json(common / "devlyn-loops/harness.json", invoking)
1679
1688
  tree = build_tree(anchor, None, files)
1680
1689
  if captured:
@@ -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.
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env bash
2
+ # codex-monitored.sh — run `codex exec` in a monitored shape that keeps the
3
+ # outer claude -p API stream from going silent during long Codex calls.
4
+ #
5
+ # WHY (iter-0009, post iter-0006/0007/0008):
6
+ # • iter-0007 isolation proved a single foreground `codex exec` Bash dispatch
7
+ # can starve the outer API stream of bytes during a 10+ min run; Anthropic's
8
+ # byte-level idle watchdog fires (~300s) and kills the orchestrator.
9
+ # • iter-0008 saw the orchestrator pick `codex exec ... 2>&1 | tail -200` from
10
+ # its own pattern prior — `tail` on a pipe buffers until EOF, suppressing
11
+ # ALL bytes. Same starvation, amplified.
12
+ # • iter-0008 also documented codex 0.124.0 reads stdin as a `<stdin>` block
13
+ # when the prompt is passed as an arg AND stdin is open; without
14
+ # `< /dev/null` the call hangs indefinitely.
15
+ #
16
+ # WHAT THIS WRAPPER DOES:
17
+ # 1. Refuses to run if stdout is a pipe. Piping wrapper output to text tools
18
+ # (tail/head/awk/sed/grep without --line-buffered) re-introduces the
19
+ # iter-0008 starvation mechanism — the downstream tool buffers until EOF
20
+ # and the outer claude -p byte-watchdog never sees bytes. Exits 64 with a
21
+ # clear message so the orchestrator can self-correct on retry.
22
+ # (Round 2 finding #1 fix: shim alone does not defeat `| tail`; the
23
+ # wrapper must reject the pipe shape directly.)
24
+ # 2. Closes stdin unless DEVLYN_CODEX_PROMPT_FILE supplies the sole prompt.
25
+ # 3. Streams codex stdout to OUR stdout line-by-line — the caller reads
26
+ # stdout as the Codex reply, so we MUST NOT swallow it (e.g.
27
+ # `tail -n 200`). codex stderr forwards to OUR stderr.
28
+ # 4. Emits a `[codex-monitored] heartbeat` line every CODEX_MONITORED_HEARTBEAT
29
+ # seconds (default 30s) on STDERR while codex is alive. Heartbeat-on-stderr
30
+ # keeps the orchestrator's combined-output stream non-silent without
31
+ # polluting the codex-reply view of stdout.
32
+ # 5. Forwards SIGTERM/SIGINT from the outer watchdog to the codex child so a
33
+ # timeout actually reaps codex (otherwise process group kill races with
34
+ # backgrounded codex).
35
+ # 6. Preserves codex's exact exit code.
36
+ #
37
+ # USAGE:
38
+ # bash codex-monitored.sh -C <repo> -s read-only "<prompt>"
39
+ # bash codex-monitored.sh resume --last
40
+ # (Args after the script name are passed verbatim to `codex exec`.)
41
+ #
42
+ # ENV OVERRIDES:
43
+ # CODEX_MONITORED_HEARTBEAT — heartbeat interval seconds (default 30).
44
+ # CODEX_MONITORED_TIMEOUT_SEC — optional hard timeout. When >0, kill the
45
+ # codex process group and exit 124.
46
+ # CODEX_BIN — real codex binary path. Default:
47
+ # CODEX_REAL_BIN when set, else `codex`.
48
+ # Set this when the shim has put us first
49
+ # on PATH.
50
+ # CODEX_MONITORED_ALLOW_PIPED — set non-empty to skip the pipe-stdout
51
+ # refusal. Reserved for tests; don't use
52
+ # in skill prompts.
53
+ # DEVLYN_CODEX_PROMPT_FILE — exact binary stdin prompt; requires sole `-`.
54
+
55
+ set -uo pipefail
56
+
57
+ # iter-0019 — solo_claude (L1) arm enforcement (defense in depth alongside
58
+ # scripts/codex-shim/codex). If this env is set, the wrapper refuses to invoke
59
+ # codex at all, regardless of how it was reached. Two enforcement points
60
+ # protect against the case where one is bypassed: the shim catches PATH-based
61
+ # resolution, and this wrapper catches direct-path invocations of
62
+ # codex-monitored.sh that don't go through the shim.
63
+ if [ -n "${CODEX_BLOCKED:-}" ]; then
64
+ printf '[codex-monitored] CODEX_BLOCKED=%s — refusing codex invocation (solo_claude / L1 arm enforcement). args: %s\n' \
65
+ "${CODEX_BLOCKED}" "$*" >&2
66
+ exit 126
67
+ fi
68
+
69
+ HEARTBEAT_SEC="${CODEX_MONITORED_HEARTBEAT:-30}"
70
+ TIMEOUT_SEC="${CODEX_MONITORED_TIMEOUT_SEC:-0}"
71
+ CODEX_BIN="${CODEX_BIN:-${CODEX_REAL_BIN:-codex}}"
72
+ START=$(date +%s)
73
+ TIMEOUT_FLAG=""
74
+ CODEX_ARGS=("$@")
75
+
76
+ for codex_arg in "${CODEX_ARGS[@]}"; do
77
+ case "$codex_arg" in
78
+ --dangerously-bypass-approvals-and-sandbox|--yolo)
79
+ printf '[codex-monitored] error: forbidden Codex bypass flag: %s\n' "$codex_arg" >&2
80
+ exit 64
81
+ ;;
82
+ danger-full-access|-s=danger-full-access|--sandbox=danger-full-access)
83
+ printf '[codex-monitored] error: forbidden Codex sandbox: danger-full-access\n' >&2
84
+ exit 64
85
+ ;;
86
+ esac
87
+ done
88
+
89
+ require_nonnegative_int() {
90
+ local name="$1"
91
+ local value="$2"
92
+ case "$value" in
93
+ ''|*[!0-9]*)
94
+ printf '[codex-monitored] error: %s must be a non-negative integer (got %s)\n' \
95
+ "$name" "$value" >&2
96
+ exit 64
97
+ ;;
98
+ esac
99
+ }
100
+
101
+ require_positive_int() {
102
+ local name="$1"
103
+ local value="$2"
104
+ require_nonnegative_int "$name" "$value"
105
+ if [ "$value" -le 0 ]; then
106
+ printf '[codex-monitored] error: %s must be > 0 (got %s)\n' \
107
+ "$name" "$value" >&2
108
+ exit 64
109
+ fi
110
+ }
111
+
112
+ require_positive_int CODEX_MONITORED_HEARTBEAT "$HEARTBEAT_SEC"
113
+ require_nonnegative_int CODEX_MONITORED_TIMEOUT_SEC "$TIMEOUT_SEC"
114
+
115
+ # --- Pipe-stdout refusal (iter-0009 R2 finding #1) -------------------------
116
+ # `[ -p /dev/stdout ]` is the POSIX test for "is fd 1 a FIFO/pipe". Verified
117
+ # correct on macOS via lsof: distinguishes piped (`| cat`) from redirected
118
+ # (`> file`) and from claude-bash-tool capture (regular file). Without this
119
+ # refusal, `bash WRAPPER ... 2>&1 | tail -200` would buffer wrapper output —
120
+ # including the heartbeat on stderr after `2>&1` — until EOF, reproducing
121
+ # the iter-0008 byte-watchdog kill.
122
+ if [ -z "${CODEX_MONITORED_ALLOW_PIPED:-}" ] && [ -p /dev/stdout ]; then
123
+ cat >&2 <<'EOF'
124
+ [codex-monitored] error: stdout is a pipe.
125
+
126
+ Piping the wrapper to tail/head/awk/sed/grep buffers wrapper output until EOF,
127
+ which starves the outer claude -p byte-watchdog (iter-0008 starvation mechanism)
128
+ and kills the run after ~300s with empty transcript.
129
+
130
+ Fix: invoke the wrapper directly so the bash tool captures its stdout. The
131
+ wrapper streams full Codex output and emits a heartbeat on stderr; you do NOT
132
+ need to truncate.
133
+
134
+ WRONG: bash codex-monitored.sh ... 2>&1 | tail -200
135
+ RIGHT: bash codex-monitored.sh ...
136
+
137
+ If you absolutely must filter, use a line-buffered tool (e.g. `grep --line-buffered`)
138
+ and set CODEX_MONITORED_ALLOW_PIPED=1 in the wrapper's environment.
139
+ EOF
140
+ exit 64
141
+ fi
142
+
143
+ # --- Heartbeat + signal forwarding ----------------------------------------
144
+ heartbeat_loop() {
145
+ local pid="$1"
146
+ while kill -0 "$pid" 2>/dev/null; do
147
+ sleep "$HEARTBEAT_SEC"
148
+ if kill -0 "$pid" 2>/dev/null; then
149
+ local elapsed=$(( $(date +%s) - START ))
150
+ printf '[codex-monitored] heartbeat: elapsed=%ds\n' "$elapsed" >&2
151
+ fi
152
+ done
153
+ }
154
+
155
+ timeout_loop() {
156
+ local pid="$1"
157
+ local seconds="$2"
158
+ local flag="$3"
159
+ [ "$seconds" -gt 0 ] || return 0
160
+ sleep "$seconds"
161
+ if kill -0 "$pid" 2>/dev/null; then
162
+ : > "$flag"
163
+ printf '[codex-monitored] timeout: elapsed=%ds limit=%ds\n' \
164
+ "$(( $(date +%s) - START ))" "$seconds" >&2
165
+ kill -TERM -- "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
166
+ sleep 5
167
+ kill -KILL -- "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
168
+ fi
169
+ }
170
+
171
+ terminate_process_group() {
172
+ local pgid="$1"
173
+ local reason="$2"
174
+ if ! kill -0 -- "-$pgid" 2>/dev/null; then
175
+ return 0
176
+ fi
177
+ printf '[codex-monitored] reap: reason=%s pgid=%s\n' "$reason" "$pgid" >&2
178
+ kill -TERM -- "-$pgid" 2>/dev/null || true
179
+ local i
180
+ for i in 1 2 3 4 5; do
181
+ sleep 1
182
+ if ! kill -0 -- "-$pgid" 2>/dev/null; then
183
+ return 0
184
+ fi
185
+ done
186
+ kill -KILL -- "-$pgid" 2>/dev/null || true
187
+ }
188
+
189
+ terminate_monitor_group() {
190
+ local pid="$1"
191
+ kill -TERM -- "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
192
+ kill -KILL -- "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
193
+ wait "$pid" 2>/dev/null || true
194
+ }
195
+
196
+ forward_signal() {
197
+ local sig="$1"
198
+ if [ -n "${CODEX_PID:-}" ] && kill -0 "$CODEX_PID" 2>/dev/null; then
199
+ kill -"$sig" -- "-$CODEX_PID" 2>/dev/null || kill -"$sig" "$CODEX_PID" 2>/dev/null || true
200
+ fi
201
+ if [ -n "${HB_PID:-}" ]; then
202
+ terminate_monitor_group "$HB_PID"
203
+ HB_PID=""
204
+ fi
205
+ if [ -n "${WATCHDOG_PID:-}" ]; then
206
+ terminate_monitor_group "$WATCHDOG_PID"
207
+ WATCHDOG_PID=""
208
+ fi
209
+ }
210
+
211
+ cleanup() {
212
+ forward_signal TERM
213
+ [ -z "$TIMEOUT_FLAG" ] || rm -f "$TIMEOUT_FLAG"
214
+ }
215
+
216
+ trap 'forward_signal TERM; exit 143' TERM
217
+ trap 'forward_signal INT; exit 130' INT
218
+ trap cleanup EXIT
219
+
220
+ printf '[codex-monitored] start: ts=%s heartbeat=%ds timeout=%ss bin=%s\n' \
221
+ "$(date -u +%FT%TZ)" "$HEARTBEAT_SEC" "$TIMEOUT_SEC" "$CODEX_BIN" >&2
222
+
223
+ RECEIPT_HELPER="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/invocation-receipt.py"
224
+ # Native Python owns Windows PIDs and the timeout tree; MSYS PIDs are different.
225
+ if [ "${OS:-}" = Windows_NT ]; then
226
+ exec python3 "$RECEIPT_HELPER" dispatch --binary "$CODEX_BIN" \
227
+ --timeout "$TIMEOUT_SEC" --heartbeat "$HEARTBEAT_SEC" -- "${CODEX_ARGS[@]}" < /dev/null
228
+ fi
229
+
230
+ # The dispatcher snapshots file input, seals actual argv, then execs in this group.
231
+ set -m
232
+ python3 "$RECEIPT_HELPER" dispatch --binary "$CODEX_BIN" \
233
+ --timeout "$TIMEOUT_SEC" --heartbeat "$HEARTBEAT_SEC" -- "${CODEX_ARGS[@]}" < /dev/null &
234
+ CODEX_PID=$!
235
+ printf '[codex-monitored] codex pid=%d\n' "$CODEX_PID" >&2
236
+
237
+ heartbeat_loop "$CODEX_PID" &
238
+ HB_PID=$!
239
+
240
+ if [ "$TIMEOUT_SEC" -gt 0 ]; then
241
+ TIMEOUT_FLAG=$(mktemp "${TMPDIR:-/tmp}/codex-monitored-timeout.XXXXXX")
242
+ rm -f "$TIMEOUT_FLAG"
243
+ timeout_loop "$CODEX_PID" "$TIMEOUT_SEC" "$TIMEOUT_FLAG" &
244
+ WATCHDOG_PID=$!
245
+ fi
246
+ set +m
247
+
248
+ wait "$CODEX_PID"
249
+ EXIT=$?
250
+ terminate_process_group "$CODEX_PID" "post-exit-descendants"
251
+ CODEX_PID=""
252
+ forward_signal TERM
253
+ if [ -n "$TIMEOUT_FLAG" ] && [ -f "$TIMEOUT_FLAG" ]; then
254
+ EXIT=124
255
+ fi
256
+
257
+ if [ -n "${DEVLYN_CODEX_PROMPT_FILE:-}" ]; then
258
+ python3 "$RECEIPT_HELPER" complete-dispatch --exit-code "$EXIT" || exit 64
259
+ fi
260
+
261
+ printf '[codex-monitored] codex exited: code=%d elapsed=%ds\n' \
262
+ "$EXIT" $(( $(date +%s) - START )) >&2
263
+ exit "$EXIT"