devlyn-cli 4.2.25 → 4.3.0
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/codex-projection.js +129 -0
- package/bin/devlyn.js +522 -0
- package/bin/engines.js +40 -0
- package/bin/harness-activation.js +426 -0
- package/bin/harness-status.js +171 -0
- package/bin/instructions.js +13 -3
- package/bin/release.js +381 -0
- package/bin/skill-history.json +5 -0
- package/config/skills/_shared/adapters/omp.md +3 -0
- package/config/skills/_shared/harness-store.py +201 -0
- package/config/skills/_shared/runtime-principles.md +12 -2
- package/config/skills/_shared/warm-build-dirs.py +154 -83
- package/config/skills/devlyn-ideate/SKILL.md +5 -1
- package/config/skills/devlyn-ideate/references/loop.md +12 -2
- package/config/skills/devlyn-ideate/references/plan-add.md +12 -0
- package/config/skills/devlyn-ideate/scripts/acceptance.py +5 -0
- package/config/skills/devlyn-ideate/scripts/queue.py +156 -15
- package/package.json +8 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
The following Devlyn instructions are defaults for this session. Applicable explicit user instructions and native owner/project instructions take precedence over these defaults. This delegation applies only to the Devlyn defaults below; it does not change other engine instructions or permissions.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Project Instructions
|
|
4
|
+
|
|
5
|
+
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.
|
|
4
6
|
|
|
5
7
|
## North Star
|
|
6
8
|
|
|
@@ -23,3 +25,11 @@ Three discipline rules govern HOW the principles are applied:
|
|
|
23
25
|
- **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.
|
|
24
26
|
- **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.
|
|
25
27
|
- **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.
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
- `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.
|
|
32
|
+
- 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.
|
|
33
|
+
- 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.
|
|
34
|
+
|
|
35
|
+
Each skill's `SKILL.md` is the source of truth for its flags and workflow.
|
|
@@ -16,12 +16,16 @@ Linux reflink; never a copy), removes what must not carry over, and moves the re
|
|
|
16
16
|
them: on reuse Cargo rewrites a script's own OUT_DIR in its directives, not paths elsewhere
|
|
17
17
|
(aws-lc-rs's `cargo:include` into aws-lc-sys) nor inside the text it wrote (tauri's permission
|
|
18
18
|
lists);
|
|
19
|
-
- every unit of a package whose
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
- every unit of a package whose crates read OUT_DIR while compiling, by rustc's dep-info record
|
|
20
|
+
of each env!/option_env! read in any spelling, and whose sources or generated Rust compile it
|
|
21
|
+
in other than through include!/include_str!/include_bytes!: Cargo never recompiles that path
|
|
22
|
+
(fingerprint/dep_info.rs). Telling an include from a compiled-in path is a best-effort
|
|
23
|
+
reading of the source, not a proof;
|
|
24
|
+
- incremental caches, final artifacts beside the unit directories, and outputs no fingerprint
|
|
25
|
+
claims.
|
|
26
|
+
|
|
27
|
+
Removal goes by unit hash across the layouts, so no Cargo command touches the clone. Cargo
|
|
28
|
+
rebuilds what it then finds missing, and everything that depends on it. A path that
|
|
25
29
|
survives inside compiled output (a proc-macro's install name, debug maps, a native library or
|
|
26
30
|
a crate that compiled a path in where the reading above does not see it) still names the
|
|
27
31
|
donor's files: when the donor rebuilds or removes them, the recipient can change behavior
|
|
@@ -63,12 +67,15 @@ SNIFF_BYTES = 8192 # a NUL among the first bytes marks a binary file
|
|
|
63
67
|
# a compiled-in path, and only the plain include!(concat!(env!("OUT_DIR"), ...)) forms, which
|
|
64
68
|
# embed contents, offset one each; an env!/option_env! whose argument is not a literal counts too.
|
|
65
69
|
_SPACE = r"(?:\s|//[^\n]*|/\*[\s\S]*?\*/)*"
|
|
66
|
-
#
|
|
67
|
-
|
|
70
|
+
# A literal that can decode to OUT_DIR holds only its letters, \x and \u{} escapes and string
|
|
71
|
+
# continuations; every quote may start one (a lookahead, so candidates overlap).
|
|
72
|
+
STRING_CANDIDATE = re.compile(r'"(?=((?:[OUTDIR_]|\\x[0-9a-fA-F]{2}|\\u\{[0-9a-fA-F_]+\}|\\\r?\n\s*)+)")')
|
|
68
73
|
ESCAPE = re.compile(r'\\(?:x([0-9a-fA-F]{2})|u\{([0-9a-fA-F_]+)\}|\n\s*|(.))')
|
|
69
|
-
NONLITERAL_ENV = re.compile(rf'
|
|
74
|
+
NONLITERAL_ENV = re.compile(rf'env{_SPACE}!{_SPACE}[(\[{{]{_SPACE}(?!r?#*")')
|
|
75
|
+
# Both start with a literal word, which re scans for quickly; the word's left boundary is checked
|
|
76
|
+
# on each match (a lookbehind first would make re try every position).
|
|
70
77
|
INCLUDED_OUT_DIR = re.compile(
|
|
71
|
-
r'
|
|
78
|
+
r'include(?:_str|_bytes)?\s*!\s*\(\s*concat\s*!\s*\(\s*env\s*!\s*\(\s*"OUT_DIR"\s*\)')
|
|
72
79
|
REDIRECTS = ("CARGO_TARGET_DIR", "CARGO_BUILD_TARGET_DIR", "CARGO_BUILD_BUILD_DIR")
|
|
73
80
|
RUSTC_ENV = re.compile(r"^cargo::?rustc-env=[^=]+=")
|
|
74
81
|
# Runs in a child so the time limit can stop a long clone; argv: source, target.
|
|
@@ -284,18 +291,20 @@ def publish(staging_parent, staging_name, target_parent, target_name):
|
|
|
284
291
|
|
|
285
292
|
def check_clone(staging, deadline):
|
|
286
293
|
"""No Git data, no special file, and no link that leads out of the clone."""
|
|
287
|
-
|
|
294
|
+
pending = [staging]
|
|
295
|
+
while pending:
|
|
288
296
|
deadline.left()
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
with os.scandir(pending.pop()) as entries:
|
|
298
|
+
for item in entries:
|
|
299
|
+
if item.name.casefold() == ".git":
|
|
300
|
+
raise Unsafe(f"{item.path} is Git data")
|
|
301
|
+
if item.is_symlink():
|
|
302
|
+
if not Path(os.path.realpath(item.path)).is_relative_to(staging):
|
|
303
|
+
raise Unsafe(f"{item.path} links out of the clone")
|
|
304
|
+
elif item.is_dir(follow_symlinks=False):
|
|
305
|
+
pending.append(item.path)
|
|
306
|
+
elif not item.is_file(follow_symlinks=False):
|
|
307
|
+
raise Unsafe(f"{item.path} is not a regular file or directory")
|
|
299
308
|
|
|
300
309
|
|
|
301
310
|
def mentions(data, roots):
|
|
@@ -311,7 +320,7 @@ def is_text(path):
|
|
|
311
320
|
return b"\0" not in stream.read(SNIFF_BYTES)
|
|
312
321
|
|
|
313
322
|
|
|
314
|
-
def foreign_runs(staging, layout, roots, deadline):
|
|
323
|
+
def foreign_runs(staging, layout, roots, readers, deadline):
|
|
315
324
|
"""Hashes of build-script runs whose kept results name a donor root Cargo does not rewrite."""
|
|
316
325
|
found = set()
|
|
317
326
|
for run_dir in (staging / layout / "build").iterdir():
|
|
@@ -338,7 +347,10 @@ def foreign_runs(staging, layout, roots, deadline):
|
|
|
338
347
|
deadline.left()
|
|
339
348
|
if is_text(path):
|
|
340
349
|
data = path.read_bytes()
|
|
341
|
-
|
|
350
|
+
# Generated Rust is compiled only through an include, which reads OUT_DIR.
|
|
351
|
+
foreign = mentions(data, needles) or (
|
|
352
|
+
name.endswith(".rs") and UNIT.match(run_dir.name)["name"] in readers
|
|
353
|
+
and compiles_path(data.decode(errors="replace")))
|
|
342
354
|
if foreign:
|
|
343
355
|
break
|
|
344
356
|
if foreign:
|
|
@@ -357,10 +369,20 @@ def decoded(literal):
|
|
|
357
369
|
return ESCAPE.sub(one, literal)
|
|
358
370
|
|
|
359
371
|
|
|
372
|
+
def at_word(text, start, prefix=""):
|
|
373
|
+
"""Whether text[start:] begins a word (or one led by `prefix`, as option_ leads env)."""
|
|
374
|
+
if prefix and text.endswith(prefix, 0, start):
|
|
375
|
+
start -= len(prefix)
|
|
376
|
+
return start == 0 or not (text[start - 1].isalnum() or text[start - 1] == "_")
|
|
377
|
+
|
|
378
|
+
|
|
360
379
|
def compiles_path(text):
|
|
361
380
|
"""Whether Rust source may compile its OUT_DIR in other than through an include macro."""
|
|
362
|
-
|
|
363
|
-
|
|
381
|
+
# Without OUT_DIR spelled out or a backslash, no literal can decode to it.
|
|
382
|
+
literals = (sum(decoded(match) == "OUT_DIR" for match in STRING_CANDIDATE.findall(text))
|
|
383
|
+
if "OUT_DIR" in text or "\\" in text else 0)
|
|
384
|
+
included = sum(at_word(text, m.start()) for m in INCLUDED_OUT_DIR.finditer(text)) if "OUT_DIR" in text else 0
|
|
385
|
+
return literals > included or any(at_word(text, m.start(), "option_") for m in NONLITERAL_ENV.finditer(text))
|
|
364
386
|
|
|
365
387
|
|
|
366
388
|
def compiles_out_dir(package, deadline):
|
|
@@ -415,36 +437,61 @@ def remove_unclassified(staging, layout, deadline):
|
|
|
415
437
|
remove(item, deadline)
|
|
416
438
|
|
|
417
439
|
|
|
440
|
+
def crates_reading_out_dir(staging, found, crates, deadline):
|
|
441
|
+
"""The crates among these that may have read OUT_DIR while compiling, by rustc's dep-info.
|
|
442
|
+
|
|
443
|
+
rustc writes `# env-dep:OUT_DIR=...` into a unit's deps/<crate>-<hash>.d for every
|
|
444
|
+
env!/option_env! read of it, whatever the spelling or macro that made it. A crate counts as
|
|
445
|
+
not reading it only when every kept output of it has that file and none says so. Any
|
|
446
|
+
hashed output counts, with or without the `lib` prefix: rlibs, rmeta, dylibs, Windows DLLs
|
|
447
|
+
and static libraries, test executables.
|
|
448
|
+
"""
|
|
449
|
+
reading, recorded, outputs = set(), {}, {}
|
|
450
|
+
for layout in found:
|
|
451
|
+
for item in (staging / layout / "deps").iterdir():
|
|
452
|
+
deadline.left()
|
|
453
|
+
if not (match := UNIT.match(item.name)):
|
|
454
|
+
continue
|
|
455
|
+
if item.name.endswith(".d"):
|
|
456
|
+
if match["name"] in crates:
|
|
457
|
+
recorded.setdefault(match["name"], set()).add(match["hash"])
|
|
458
|
+
if b"\n# env-dep:OUT_DIR=" in b"\n" + item.read_bytes():
|
|
459
|
+
reading.add(match["name"])
|
|
460
|
+
continue
|
|
461
|
+
stem = match["name"]
|
|
462
|
+
for crate in {stem, stem.removeprefix("lib")} & crates:
|
|
463
|
+
outputs.setdefault(crate, set()).add(match["hash"])
|
|
464
|
+
return reading | {crate for crate, hashes in outputs.items() if hashes - recorded.get(crate, set())}
|
|
465
|
+
|
|
466
|
+
|
|
418
467
|
def sanitize(work, staging, found, roots, deadline):
|
|
419
468
|
for layout in found:
|
|
420
469
|
remove_unclassified(staging, layout, deadline)
|
|
421
|
-
|
|
422
|
-
resolved = metadata(work, deadline, env=env, deps=True)
|
|
423
|
-
if not (Path(resolved["target_directory"]).resolve() == Path(resolved["build_directory"]).resolve() == staging.resolve()):
|
|
424
|
-
raise Unsafe("Cargo does not resolve the staging directory as both target and build directory")
|
|
470
|
+
resolved = metadata(work, deadline, env=without_redirects(), deps=True)
|
|
425
471
|
names = {package["name"] for package in resolved["packages"]}
|
|
426
|
-
path_packages =
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
472
|
+
path_packages = {package["name"] for package in resolved["packages"] if package["source"] is None}
|
|
473
|
+
# Every version of a package counts on its own; a name is removed if any version must be.
|
|
474
|
+
scripted = [package for package in resolved["packages"]
|
|
475
|
+
if package["source"] is not None and any("custom-build" in t["kind"] for t in package["targets"])]
|
|
476
|
+
crates = [{t["name"].replace("-", "_") for t in package["targets"] if "custom-build" not in t["kind"]}
|
|
477
|
+
for package in scripted]
|
|
478
|
+
reading = crates_reading_out_dir(staging, found, set().union(*crates), deadline)
|
|
479
|
+
reader_packages = [package for package, own in zip(scripted, crates) if own & reading]
|
|
480
|
+
readers = {package["name"] for package in reader_packages}
|
|
481
|
+
out_dir_packages = {package["name"] for package in reader_packages if compiles_out_dir(package, deadline)}
|
|
430
482
|
removed = set()
|
|
431
483
|
for layout in found:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
run(base, cwd=work, deadline=deadline, env=env) # Without --target only the host layout's outputs go.
|
|
444
|
-
triples = [arg for layout in found[1:] for arg in ("--target", layout.parts[0])]
|
|
445
|
-
if triples:
|
|
446
|
-
run([*base, *triples], cwd=work, deadline=deadline, env=env)
|
|
447
|
-
return sorted(removed | set(clean))
|
|
484
|
+
units = {m["hash"]: m["name"] for item in (staging / layout / ".fingerprint").iterdir()
|
|
485
|
+
if (m := UNIT.match(item.name))}
|
|
486
|
+
drop = {digest for digest, name in units.items()
|
|
487
|
+
if name not in names or name in path_packages or name in out_dir_packages}
|
|
488
|
+
drop |= foreign_runs(staging, layout, roots, readers, deadline)
|
|
489
|
+
removed |= {units[digest] for digest in drop if digest in units}
|
|
490
|
+
# Outputs without a fingerprint belong to no unit Cargo would reuse.
|
|
491
|
+
drop |= {m["hash"] for sub in ("build", "deps", "examples") if (staging / layout / sub).is_dir()
|
|
492
|
+
for item in (staging / layout / sub).iterdir() if (m := UNIT.match(item.name)) and m["hash"] not in units}
|
|
493
|
+
remove_units(staging, layout, drop, deadline)
|
|
494
|
+
return sorted(removed)
|
|
448
495
|
|
|
449
496
|
|
|
450
497
|
def warm_cargo(work, anchor, staging_root, deadline):
|
|
@@ -575,34 +622,24 @@ def self_test():
|
|
|
575
622
|
return 0 if result.wasSuccessful() else 1
|
|
576
623
|
|
|
577
624
|
|
|
578
|
-
# Fixtures use real Git and a fake `cargo` on PATH that answers `metadata` from a JSON file
|
|
579
|
-
#
|
|
580
|
-
# copies instead, in tests only, so every rule still runs there.
|
|
625
|
+
# Fixtures use real Git and a fake `cargo` on PATH that answers `metadata` from a JSON file;
|
|
626
|
+
# a filesystem without cloning copies instead, in tests only, so every rule still runs there.
|
|
581
627
|
FAKE_CARGO = r"""#!/usr/bin/env python3
|
|
582
|
-
import json, os, pathlib,
|
|
628
|
+
import json, os, pathlib, sys, time
|
|
583
629
|
argv = sys.argv[1:]
|
|
584
630
|
fixture = json.loads(pathlib.Path(os.environ["FAKE_CARGO"]).read_text())
|
|
585
631
|
with open(os.environ["FAKE_CARGO_LOG"], "a") as log:
|
|
586
632
|
log.write(json.dumps(argv) + "\n")
|
|
587
633
|
time.sleep(fixture.get("sleep", 0))
|
|
588
634
|
target = pathlib.Path(os.environ.get("CARGO_TARGET_DIR") or os.getcwd() + "/" + fixture.get("target", "target"))
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
names = [argv[i + 1] for i, a in enumerate(argv) if a == "-p"]
|
|
598
|
-
triples = [argv[i + 1] for i, a in enumerate(argv) if a == "--target"]
|
|
599
|
-
for layout in [directory / t / "debug" for t in triples] or [directory / "debug"]:
|
|
600
|
-
for name in names:
|
|
601
|
-
for sub in (".fingerprint", "build", "deps"):
|
|
602
|
-
for item in (layout / sub).glob("*"):
|
|
603
|
-
stem = item.name.split(".")[0].removeprefix("lib")
|
|
604
|
-
if stem.rsplit("-", 1)[0].replace("_", "-") == name:
|
|
605
|
-
shutil.rmtree(item) if item.is_dir() else item.unlink()
|
|
635
|
+
assert argv[0] == "metadata", argv # warming never runs another cargo command
|
|
636
|
+
found = {"target_directory": str(target), "packages": fixture["packages"] if "--no-deps" not in argv else [],
|
|
637
|
+
"metadata": fixture.get("workspace_metadata", {"devlyn": {"warm-build-dirs": True}})}
|
|
638
|
+
if not fixture.get("old_cargo"):
|
|
639
|
+
found["build_directory"] = fixture.get("build_directory", str(target))
|
|
640
|
+
print(json.dumps(found))
|
|
641
|
+
# The full resolve runs while warming, after the clone: the moment to race the destination.
|
|
642
|
+
if "--no-deps" not in argv:
|
|
606
643
|
if fixture.get("swap") and not os.path.islink("build"):
|
|
607
644
|
os.rename("build", "build-moved")
|
|
608
645
|
os.symlink(fixture["swap"], "build")
|
|
@@ -669,20 +706,29 @@ class WarmTests(unittest.TestCase):
|
|
|
669
706
|
|
|
670
707
|
def write_fixture(self, **extra):
|
|
671
708
|
packages = [
|
|
672
|
-
{"name": "app", "source": None, "manifest_path": str(self.work / "Cargo.toml"), "targets": [{"kind": ["lib"]}]},
|
|
673
|
-
{"name": "serde", "source": "registry", "manifest_path": str(self.root / "registry/serde/Cargo.toml"), "targets": [{"kind": ["lib"]}]},
|
|
674
|
-
{"name": "ring", "source": "registry", "manifest_path": str(self.root / "registry/ring/Cargo.toml"), "targets": [{"kind": ["lib"]}, {"kind": ["custom-build"]}]},
|
|
675
|
-
{"name": "tauri", "source": "registry", "manifest_path": str(self.root / "registry/tauri/Cargo.toml"), "targets": [{"kind": ["lib"]}, {"kind": ["custom-build"]}]},
|
|
676
|
-
{"name": "aws-lc-rs", "source": "registry", "manifest_path": str(self.root / "registry/aws-lc-rs/Cargo.toml"), "targets": [{"kind": ["lib"]}, {"kind": ["custom-build"]}]},
|
|
677
|
-
{"name": "baked", "source": "registry", "manifest_path": str(self.root / "registry/baked/Cargo.toml"), "targets": [{"kind": ["lib"]}, {"kind": ["custom-build"]}]},
|
|
678
|
-
{"name": "exporter", "source": "registry", "manifest_path": str(self.root / "registry/exporter/Cargo.toml"), "targets": [{"kind": ["lib"]}, {"kind": ["custom-build"]}]},
|
|
679
|
-
{"name": "generator", "source": "registry", "manifest_path": str(self.root / "registry/generator/Cargo.toml"), "targets": [{"kind": ["lib"]}, {"kind": ["custom-build"]}]},
|
|
709
|
+
{"name": "app", "source": None, "manifest_path": str(self.work / "Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}]},
|
|
710
|
+
{"name": "serde", "source": "registry", "manifest_path": str(self.root / "registry/serde/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}]},
|
|
711
|
+
{"name": "ring", "source": "registry", "manifest_path": str(self.root / "registry/ring/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
|
|
712
|
+
{"name": "tauri", "source": "registry", "manifest_path": str(self.root / "registry/tauri/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
|
|
713
|
+
{"name": "aws-lc-rs", "source": "registry", "manifest_path": str(self.root / "registry/aws-lc-rs/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
|
|
714
|
+
{"name": "baked", "source": "registry", "manifest_path": str(self.root / "registry/baked/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
|
|
715
|
+
{"name": "exporter", "source": "registry", "manifest_path": str(self.root / "registry/exporter/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
|
|
716
|
+
{"name": "generator", "source": "registry", "manifest_path": str(self.root / "registry/generator/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
|
|
717
|
+
{"name": "windowsdll", "source": "registry", "manifest_path": str(self.root / "registry/windowsdll/Cargo.toml"), "targets": [{"kind": ["dylib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
|
|
718
|
+
# rustc left no dep-info for its library, so nothing proves it never read OUT_DIR.
|
|
719
|
+
{"name": "undocumented", "source": "registry", "manifest_path": str(self.root / "registry/undocumented/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
|
|
720
|
+
# Two versions of one package: only 1.0 compiles OUT_DIR in.
|
|
721
|
+
{"name": "dual", "source": "registry", "manifest_path": str(self.root / "registry/dual-1/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
|
|
722
|
+
{"name": "dual", "source": "registry", "manifest_path": str(self.root / "registry/dual-2/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
|
|
680
723
|
]
|
|
724
|
+
for package in packages:
|
|
725
|
+
package["targets"] = [{**t, "name": package["name"] if t["name"] == "LIB" else t["name"]} for t in package["targets"]]
|
|
681
726
|
for package in packages[1:]:
|
|
682
727
|
source = Path(package["manifest_path"]).parent / "src"
|
|
683
728
|
source.mkdir(parents=True, exist_ok=True)
|
|
684
|
-
|
|
685
|
-
|
|
729
|
+
compiled = Path(package["manifest_path"]).parent.name in {"baked", "undocumented", "dual-1", "windowsdll"}
|
|
730
|
+
(source / "lib.rs").write_text('pub const DIR: &str = env!(/* why */ "OUT_DIR");\n' if compiled
|
|
731
|
+
else 'include!(concat!(\n env!("OUT_DIR"), "/gen.rs"));\n')
|
|
686
732
|
self.fixture.write_text(json.dumps({"packages": packages, **extra}))
|
|
687
733
|
|
|
688
734
|
def unit(self, layout, sub, name, digest, files=None):
|
|
@@ -706,6 +752,24 @@ class WarmTests(unittest.TestCase):
|
|
|
706
752
|
self.unit(layout, ".fingerprint", name, digest)
|
|
707
753
|
(deps / f"lib{name}-{digest}.rlib").write_bytes(b"rlib")
|
|
708
754
|
(self.donor / layout / ".cargo-lock").write_bytes(b"")
|
|
755
|
+
# rustc's dep-info: which crates read OUT_DIR while compiling.
|
|
756
|
+
for crate, digest, reads in (("serde", "1" * 16, False), ("baked", "8" * 16, True), ("generator", "b" * 16, True),
|
|
757
|
+
("ring", "c" * 16, False), ("dual", "1a" * 8, True), ("dual", "2b" * 8, True)):
|
|
758
|
+
record = f"# env-dep:OUT_DIR={self.donor}/{layout}/build/{crate}-x/out\n" if reads else ""
|
|
759
|
+
(deps / f"{crate}-{digest}.d").write_text(f"/src/{crate}/lib.rs:\n\n# env-dep:CARGO_PKG_NAME={crate}\n{record}")
|
|
760
|
+
self.unit(layout, ".fingerprint", crate, digest)
|
|
761
|
+
(deps / "libstray-dddddddddddddddd.rlib").write_bytes(b"rlib")
|
|
762
|
+
self.unit(layout, ".fingerprint", "undocumented", "e" * 16)
|
|
763
|
+
(deps / f"libundocumented-{'e' * 16}.rlib").write_bytes(b"rlib")
|
|
764
|
+
# A Windows-style DLL (no `lib` prefix) without dep-info, of a crate that compiles OUT_DIR in.
|
|
765
|
+
self.unit(layout, ".fingerprint", "windowsdll", "9a" * 8)
|
|
766
|
+
(deps / f"windowsdll-{'9a' * 8}.dll").write_bytes(b"dll")
|
|
767
|
+
for digest in ("1a" * 8, "2b" * 8):
|
|
768
|
+
(deps / f"libdual-{digest}.rlib").write_bytes(b"rlib")
|
|
769
|
+
# A run that points into the donor and has no fingerprint left.
|
|
770
|
+
self.unit(layout, "build", "orphanrun", "f" * 16, {
|
|
771
|
+
"root-output": f"{self.donor}/{layout}/build/orphanrun-{'f' * 16}/out".encode(),
|
|
772
|
+
"output": f"cargo:include={self.donor}/{layout}/build/aws-lc-sys-{'6' * 16}/out\n".encode()})
|
|
709
773
|
(self.donor / layout / "incremental" / "app-x").mkdir(parents=True)
|
|
710
774
|
# Final artifacts Cargo puts beside the unit directories, here of the unused `gone`.
|
|
711
775
|
(self.donor / layout / "old-tool").write_bytes(b"bin")
|
|
@@ -743,11 +807,18 @@ class WarmTests(unittest.TestCase):
|
|
|
743
807
|
target = self.work / "target"
|
|
744
808
|
for layout in ("debug", "x86_64-pc-windows-msvc/debug"):
|
|
745
809
|
kept = {p.name for p in (target / layout / ".fingerprint").iterdir()}
|
|
746
|
-
|
|
810
|
+
# generator keeps its library unit: Cargo rebuilds it after rerunning the removed run.
|
|
811
|
+
self.assertEqual({f"serde-{'1' * 16}", f"ring-{'4' * 16}", f"ring-{'c' * 16}", f"generator-{'b' * 16}"}, kept, layout)
|
|
812
|
+
self.assertFalse((target / layout / "deps" / "libstray-dddddddddddddddd.rlib").exists())
|
|
747
813
|
self.assertEqual({".fingerprint", "build", "deps", "examples", ".cargo-lock"}, {p.name for p in (target / layout).iterdir()})
|
|
748
814
|
self.assertEqual([], list((target / layout / "examples").iterdir()))
|
|
749
815
|
self.assertTrue((target / layout / "build" / f"ring-{'4' * 16}" / "out" / "libring.a").exists())
|
|
750
|
-
self.assertEqual(["app", "aws-lc-rs", "baked", "exporter", "generator", "gone", "tauri"
|
|
816
|
+
self.assertEqual(["app", "aws-lc-rs", "baked", "dual", "exporter", "generator", "gone", "tauri", "undocumented",
|
|
817
|
+
"windowsdll"],
|
|
818
|
+
result["removed"])
|
|
819
|
+
for layout in ("debug", "x86_64-pc-windows-msvc/debug"):
|
|
820
|
+
self.assertFalse((target / layout / "build" / f"orphanrun-{'f' * 16}").exists())
|
|
821
|
+
self.assertFalse(any((target / layout / "deps").glob("libdual-*")))
|
|
751
822
|
self.assertEqual(str(self.donor), result["donor"])
|
|
752
823
|
self.assertEqual(before, self.snapshot(self.donor))
|
|
753
824
|
self.assert_nothing_left()
|
|
@@ -876,7 +947,7 @@ class WarmTests(unittest.TestCase):
|
|
|
876
947
|
'option_env!("OUT_DIR")', 'myinclude!(concat!(env!("OUT_DIR"), "/a.rs"))', 'env![ "OUT_DIR" ]',
|
|
877
948
|
'env/**/!("OUT_DIR")', 'env!(/* why */ "OUT_DIR")', 'env!("OUT\\x5fDIR")', 'env!("\\u{4f}UT_DIR")',
|
|
878
949
|
'use std::env as e; e!("OUT_DIR")', 'env!(concat!("OUT", "_DIR"))', 'env!(// note\n"OUT_DIR")',
|
|
879
|
-
'env!("\\u{4_f}UT_DIR")', 'env!("OUT_\\\n DIR")', 'let a = \'"\'; env!("OUT_DIR")',
|
|
950
|
+
'env!("\\u{4_f}UT_DIR")', 'env!("\\u{0_0_0_0_4_f}UT_DIR")', 'env!("OUT_\\\n DIR")', 'let a = \'"\'; env!("OUT_DIR")',
|
|
880
951
|
'include!(concat!(env!(/* c */ "OUT_DIR"), "/a.rs"))']
|
|
881
952
|
for text in compiled:
|
|
882
953
|
self.assertTrue(compiles_path(text), text)
|
|
@@ -94,10 +94,14 @@ A removed flag stops with its instruction and selects no other behavior.
|
|
|
94
94
|
|
|
95
95
|
An absolute path to a package's `meta.md` is added as it is; anything else is planned first. Add with `python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" add '<absolute meta.md>'`, the only writer of a loop's queue file `docs/specs/<loop-id>/queue.md`; never edit one by hand. In every delivery mode add makes no commit on the checked-out branch and leaves the branch, the index and tracked files as they are: it holds the package and its rows at `refs/devlyn/captures/<loop-id>`, records the add and removes the package copies equal to that capture. Report the capture ref and its `git show` command, any `cleanup` entry, and that a revision needs a new loop id. A retry of the same add reports it as added, also once the copies are gone. Tasks start from committed state, a local loop from the branch it was added on and `auto`/`pr` from the remote base, so commit, and for `auto`/`pr` push, whatever the plan depends on. A pending legacy raw-intent row of `docs/specs/queue.md` is replaced in place by a package whose `## Intent` reproduces the row verbatim, added with `--materialize <line>`.
|
|
96
96
|
|
|
97
|
+
Release-backed queues retain their verified release in `<git-common-dir>/devlyn-loops/harness.json` until all tasks, recovery and delivery settle. A different release refuses with the exact recovery argv; use it. Existing unfinished unbound queues and standalone installations remain legacy.
|
|
98
|
+
|
|
97
99
|
## status
|
|
98
100
|
|
|
99
101
|
Run `python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" status --repo .` and report its counts, `next`, `blockers`, `warnings`, pending deliveries with their resume commands and any `cleanup` entry.
|
|
100
102
|
|
|
103
|
+
For read-only consumers, `python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" inspect --repo . --json` returns the status summary plus `binding` and `compatibility`, without cleanup, locks or writes.
|
|
104
|
+
|
|
101
105
|
## drain
|
|
102
106
|
|
|
103
107
|
`status`, `add` and `drain` write the repository's Git directory, and auto/pr delivery needs network access, so a sandboxed host runs them with escalation or full access.
|
|
@@ -112,7 +116,7 @@ Follow [loop.md](references/loop.md). The executor is the configured route, the
|
|
|
112
116
|
Another engine needs an argv that starts one fresh non-interactive session able, without prompts, to edit, run commands and commit in its task worktree and to write the packet's submission file. `<prompt>` carries the absolute binding values, while `{packet}` stays literal for the drain to fill:
|
|
113
117
|
|
|
114
118
|
```text
|
|
115
|
-
Execute the devlyn loop task packet {packet}: work only in its owned worktree under the installed instructions; meet its obligations, then write its submission and any review records as "Executor exchange" and "Review records" in <DEVLYN_SKILL_DIR>/references/loop.md specify.
|
|
119
|
+
Execute the devlyn loop task packet {packet}: read its workerPrompt when present; read methodology.defaults.path when named and apply those verified defaults, with explicit user and native owner/project instructions taking precedence; work only in its owned worktree under the installed instructions; meet its obligations, then write its submission and any review records as "Executor exchange" and "Review records" in <DEVLYN_SKILL_DIR>/references/loop.md specify. Use the literal DEVLYN_SKILL_DIR and DEVLYN_SHARED_DIR bindings in workerPrompt when present; otherwise set DEVLYN_SKILL_DIR=<DEVLYN_SKILL_DIR> and DEVLYN_SHARED_DIR=<DEVLYN_SHARED_DIR> from these literal values. Never use inherited environment values. Never push, open a pull request or merge: the drain delivers.
|
|
116
120
|
```
|
|
117
121
|
|
|
118
122
|
```sh
|
|
@@ -28,7 +28,7 @@ The host supplies the executor and maps its configured engine to the argv; the l
|
|
|
28
28
|
- Local: the first task uses `--local-base <HEAD of the branch the loop was added on>`, the add record's branch, never whichever branch is checked out; that HEAD must descend from the manifest `base_sha`, or the task waits naming it. Instructions committed there before this first allocation are therefore used. Its receipt keeps that start, and a later task with no accepted predecessor reuses it, also after the first task failed; a fixed start is never recalculated. Later tasks use `--from-receipt <latest accepted receipt of the loop>`, which starts from that receipt's exact `source_sha`, never its terminal commit, current HEAD or workspace. Every prerequisite's accepted source must be an ancestor of that frontier; divergent dependencies need a planned integration task, because the driver never merges. A failure leaves the frontier unchanged.
|
|
29
29
|
- `auto`/`pr`: `--start` the refreshed remote base, never an anchor-local commit, so no unpushed anchor commit reaches a task PR; push whatever the plan depends on. A task allocated while that base lacks `docs/specs/<loop-id>/` is the loop's carrier; a carrier that fails is never published, and the next task allocated becomes the carrier. The start must contain each prerequisite's merge commit, checked before allocation and again before every execution or resume. A squash merge needs no original-source ancestry. A refresh failure waits for restored remote access or the base ref. A missing merge before allocation waits for that merge to be restored on the remote base; an allocated baseline is immutable and needs a new loop or verified recovery of a never-executed allocation.
|
|
30
30
|
4. **Commit scoped inputs** on the owned branch before any executor write, only what the allocation base lacks. A base without `docs/specs/<loop-id>/` gets the package directory from the capture with the loop's queue file, its settled rows keeping their marks (receipt-proven `[x]` and `[F]`, prerequisite-blocked `[F]`): a local loop's first task, or an `auto`/`pr` carrier, whose PR lands the package before tasks diverge. Otherwise a local task commits the base's queue file with the loop's receipt-proven terminal rows (typically a predecessor's `[x]`), every other byte unchanged, and an `auto`/`pr` task commits nothing, so its PR changes only its own row and its product. A base that already carries them gets no inputs commit; unrelated product commits stay out.
|
|
31
|
-
5. **Exchange one packet.** Write `<receipt dir>/packet.json`, then run the executor. An executor that exits without writing its submission leaves its task active, waiting with the exit code and `executor.stderr`, and the next drain runs it again; one that cannot start keeps its allocation, packet and durable start/not-started log, waits for executable, permissions or workspace repair, and retries on the next drain.
|
|
31
|
+
5. **Exchange one packet.** Verify the bound release, defaults, packet and worker prompt before execution or recovery; changed or missing resources refuse dispatch. Write `<receipt dir>/packet.json`, then run the executor. An executor that exits without writing its submission leaves its task active, waiting with the exit code and `executor.stderr`, and the next drain runs it again; one that cannot start keeps its allocation, packet and durable start/not-started log, waits for executable, permissions or workspace repair, and retries on the next drain.
|
|
32
32
|
6. **Derive acceptance** with `acceptance.py accept`. It binds the committed contract to the packet digests; requires the candidate to be the owned branch head with HEAD checked out on that branch, to descend from the inputs, to leave the package (its queue file included) and `docs/specs/queue.md` untouched and to have a clean worktree; executes each declared command on that source, recording raw stdout/stderr and exit, timeout or spawn outcomes (a runner result is reused only when it is wholly clean — no reasons, so no failed check and no source change during its checks — for the same source and contract digests, with unchanged streams); evaluates the file and diff guards against the inputs; confirms the source is unchanged; checks review records; and writes `.devlyn/loop/acceptance.json`. Missing checks, missing review coverage and open binding findings cannot produce `[x]`, and acceptance weakening is not repair. This establishes command outcomes and evidence completeness, not a reviewer's semantic judgment; deliberate evidence forgery is outside the trust model.
|
|
33
33
|
7. **Bind before terminal metadata.** `task-complete.py accept --receipt <receipt> --acceptance <worktree>/.devlyn/loop/acceptance.json` takes evidence custody and points the recovery ref at the source. Failed results are bound the same way; they never become a frontier or a publishable product.
|
|
34
34
|
8. **Commit the terminal transition** under the queue lock: one commit whose sole parent is the bound source and whose only change is this task's row in `docs/specs/<loop-id>/queue.md` (`[x]`, or `[F] — <first reason> (receipt <id>)`), validated as the only legal transition. Then `task-complete.py attach --receipt <receipt> --commit <terminal> --file docs/specs/<loop-id>/queue.md` records it separately from the source and moves the recovery ref to it.
|
|
@@ -53,6 +53,14 @@ Expected task-local operational failures leave the selected task waiting with `<
|
|
|
53
53
|
|
|
54
54
|
Package cleanup, parent pruning and report failures appear as diagnostics under `cleanup`, with retained paths and repair guidance; they do not change product acceptance or completed delivery. The next drain retries them.
|
|
55
55
|
|
|
56
|
+
## Release binding and read-only consumers
|
|
57
|
+
|
|
58
|
+
`<git-common-dir>/devlyn-loops/harness.json` is authoritative across linked worktrees: `{schemaVersion: 1, release: {package, version, digest, root}, defaults: {path, sha256}}`. Paths are absolute and hashes cover exact bytes. The queue lock covers binding before the first capture. An interrupted first add, unfinished tasks, recovery conflicts and pending delivery retain it; settlement removes it. A different release refuses with `recovery argv` as a JSON argv array, preserving the caller’s arguments. No caller substitutes another implementation.
|
|
59
|
+
|
|
60
|
+
`queue.py inspect --repo <dir> --json` reads without cleanup, locks or writes. It returns the status summary plus `binding` (null for legacy) and `compatibility`: `compatible`, `legacy`, `reasons`, `recoveryArgv`, `taskPacket` ranges and `queueBinding` ranges. Incompatible or unreadable state is reported, never migrated. Apps should use this reader instead of `status`, which still reconciles cleanup. For a mismatch, `recoveryArgv` names the bound queue’s inspect invocation; use the same bound queue entry point for authorized add/drain operations.
|
|
61
|
+
|
|
62
|
+
Codex projection writers place `.devlyn-harness-projection.json` at the copied plugin root, beside `skills` and `.codex-plugin`: `{schemaVersion: 1, release: <ReleaseRef>, source: "native/codex/plugins/devlyn"}`. The reader verifies the named release and every projected file’s bytes/mode against that subtree (excluding only the marker). Symlinked skill invocations resolve through their real path. Standalone installs and existing unfinished unbound queues retain legacy behavior; new legacy packets use v2 with a null release, and their `defaults` name the invoking installation's own `_shared/runtime-principles.md` (`source: "skill"`). No engine applicability is inferred from instruction filenames: bound queues use verified release defaults with native instructions taking precedence. Worker and runner paths point to the bound release, independent of a projection cache’s lifetime.
|
|
63
|
+
|
|
56
64
|
## Executor exchange
|
|
57
65
|
|
|
58
66
|
The packet (`<receipt dir>/packet.json`) carries everything a fresh executor needs:
|
|
@@ -66,7 +74,9 @@ The packet (`<receipt dir>/packet.json`) carries everything a fresh executor nee
|
|
|
66
74
|
| `requirements`, `review_requirements` | Requirement IDs and those needing review evidence. |
|
|
67
75
|
| `shared_constraints` | The meta-prompt's `Constraints and exclusions`. |
|
|
68
76
|
| `dependencies` | `{task, receipt, source_sha}` of each accepted prerequisite. |
|
|
69
|
-
| `
|
|
77
|
+
| `schema_version`, `harness` | New packets use version 2 and the bound release reference, or null for legacy unbound queues. Existing v1 packets execute unchanged. |
|
|
78
|
+
| `methodology` | `projectInstructions`: `{path, sha256}` of instruction bytes at the inputs commit. `defaults`: `{source: "release", path, sha256}` from the verified binding; for an unbound queue `{source: "skill", path, sha256}` of the installation's own `_shared/runtime-principles.md`. A task whose executor would get no principles at all (no defaults and no committed CLAUDE.md/AGENTS.md) waits with that reason instead of running. Read the named defaults; explicit user and native owner/project instructions take precedence. No native engine applicability is inferred from filenames. |
|
|
79
|
+
| `workerPrompt` | `{path, sha256}` of the task’s worker instructions, or null for legacy unbound queues. Read it before working. |
|
|
70
80
|
| `delivery` | `local-only`, `auto` or `pr`. |
|
|
71
81
|
| `evidence_dir`, `runner` | Where loop evidence lives (`<worktree>/.devlyn/loop`, ignored by Git) and the runner argv. |
|
|
72
82
|
| `submission`, `obligations` | Where to write the submission, and the completion obligations. |
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Planning and capture only
|
|
2
|
+
|
|
3
|
+
This reference grants plan and add operations only. Never drain, implement, run an executor or deliver code from this planning session.
|
|
4
|
+
|
|
5
|
+
Resolve this reference’s actual containing skill directory (the parent of `references`), resolving symlinks; bind it as `DEVLYN_SKILL_DIR`. Resolve its sibling `_shared` as `DEVLYN_SHARED_DIR`. Verify the loaded files; missing or conflicting source identity is `BLOCKED:skill-source-unresolved`. Never substitute another installation or inherited environment values.
|
|
6
|
+
|
|
7
|
+
1. Inspect project facts and follow [elicitation.md](elicitation.md). Ask only when the unresolved answer changes authorized behavior, scope, data semantics, acceptance or delivery. Existing authorization remains effective. Under autonomous planning, use conventional, reversible defaults for ordinary details and record each assumption; material ambiguity stops affected work as needs-review with a concrete question. Independent authorized planning may continue.
|
|
8
|
+
2. Write `docs/specs/<loop-id>/meta.md` and self-contained task contracts per [package-format.md](package-format.md). Preserve an input document unchanged and carry every substantive requirement into the package. Use this execution policy in the package: for ordinary details within authorized scope, follow established project conventions, otherwise the narrowest literal or conventional reading consistent with the request; choices must be low-consequence and reversible, recorded with their reasons. Material ambiguity means competing intended outcomes, unresolved persistent data/state semantics or public surface beyond the request; stop affected work as needs-review with a concrete question. Independent authorized work may continue. Never omit requested work or weaken acceptance. Set delivery from explicit restrictions or the project completion policy; without one GitHub origin, record local-only as an assumption, or ask when the user explicitly requested a PR/merge.
|
|
9
|
+
3. Run `python3 "<DEVLYN_SKILL_DIR>/scripts/queue.py" check '<absolute meta.md>'` and repair until `VALID`. Plan stops here: report the path, tasks, dependencies and assumptions.
|
|
10
|
+
4. When add is authorized, run `python3 "<DEVLYN_SKILL_DIR>/scripts/queue.py" add '<absolute meta.md>'`. It alone writes queue state, captures without committing on the checked-out branch and removes matching untracked package copies. Report the capture ref, its `git show` command and cleanup diagnostics. Revisions need a new loop id. A legacy pending row is materialized with `--materialize <line>` and its verbatim intent in the package.
|
|
11
|
+
|
|
12
|
+
Report capture and stop. A separate authorized execution session owns drain.
|
|
@@ -89,6 +89,11 @@ def load_packet(path):
|
|
|
89
89
|
packet = read_json(path)
|
|
90
90
|
if not isinstance(packet, dict) or PACKET_KEYS - set(packet):
|
|
91
91
|
raise AcceptanceError(f"{path}: task packet lacks {sorted(PACKET_KEYS - set(packet if isinstance(packet, dict) else {}))}")
|
|
92
|
+
helper = shared("harness-store")
|
|
93
|
+
try:
|
|
94
|
+
helper["verify_packet"](packet, path, SKILLS)
|
|
95
|
+
except (helper["HarnessError"], OSError) as exc:
|
|
96
|
+
raise AcceptanceError(str(exc)) from exc
|
|
92
97
|
return packet
|
|
93
98
|
|
|
94
99
|
|