devlyn-cli 4.3.0 → 4.3.4
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/cleanup-history.json +76 -1
- package/bin/devlyn.js +172 -48
- package/bin/skill-history.json +133 -2
- package/config/skills/_shared/harness-store.py +8 -2
- package/config/skills/_shared/platform-support.py +12 -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/scripts/queue.py +10 -1
- 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 +207 -0
- package/native/claude/skills/_shared/invocation-receipt.py +499 -0
- package/native/claude/skills/_shared/platform-support.py +407 -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 +207 -0
- package/native/codex/plugins/devlyn/skills/_shared/invocation-receipt.py +499 -0
- package/native/codex/plugins/devlyn/skills/_shared/platform-support.py +407 -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 +1 -1
- package/release.json +1630 -0
|
@@ -15,6 +15,18 @@ import sys
|
|
|
15
15
|
import time
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def managed_node():
|
|
19
|
+
"""The Node that runs devlyn-cli's own scripts: the app's override is authoritative, and an invalid one never
|
|
20
|
+
falls back to PATH. Project and engine commands keep the session's own Node."""
|
|
21
|
+
value = os.environ.get("DEVLYN_NODE")
|
|
22
|
+
if value is None:
|
|
23
|
+
return None
|
|
24
|
+
node = Path(value)
|
|
25
|
+
if not node.is_absolute() or not node.is_file() or not os.access(node, os.X_OK):
|
|
26
|
+
raise OSError("DEVLYN_NODE must be an absolute path to an executable file")
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
|
|
18
30
|
def configure_utf8():
|
|
19
31
|
for stream in (sys.stdin, sys.stdout, sys.stderr):
|
|
20
32
|
if hasattr(stream, "reconfigure"):
|
|
@@ -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
|
|
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
|
-
|
|
117
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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,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,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"
|