devlyn-cli 4.3.28 → 4.3.30
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/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/README.md +10 -10
- package/bin/devlyn.js +92 -58
- package/bin/engines.js +10 -10
- package/bin/harness-activation.js +369 -23
- package/bin/harness-context.js +7 -7
- package/bin/instruction-templates.json +2 -0
- package/bin/skill-history.json +12 -0
- package/config/skills/_shared/engine-doctor.sh +2 -2
- package/config/skills/_shared/engine-preflight.md +2 -4
- package/config/skills/_shared/managed-storage.md +80 -0
- package/config/skills/_shared/platform-support.py +67 -28
- package/config/skills/_shared/role-config.py +16 -7
- package/config/skills/_shared/runtime-principles.md +1 -1
- package/config/skills/_shared/storage-client.py +31 -7
- package/config/skills/_shared/task-complete.py +1 -1
- package/config/skills/_shared/task-completion.md +5 -91
- package/config/skills/devlyn-engines/SKILL.md +1 -1
- package/config/skills/devlyn-ideate/SKILL.md +2 -2
- package/config/skills/devlyn-ideate/references/executor.md +62 -0
- package/config/skills/devlyn-ideate/references/loop.md +4 -63
- package/config/skills/devlyn-ideate/references/package-format.md +1 -2
- package/config/skills/devlyn-ideate/scripts/acceptance.py +442 -21
- package/config/skills/devlyn-ideate/scripts/queue.py +58 -10
- package/harness/AGENTS.md +2 -2
- package/harness/CLAUDE.md +2 -2
- package/native/claude/.claude-plugin/plugin.json +1 -1
- package/native/claude/skills/_shared/engine-doctor.sh +2 -2
- package/native/claude/skills/_shared/engine-preflight.md +2 -4
- package/native/claude/skills/_shared/managed-storage.md +80 -0
- package/native/claude/skills/_shared/platform-support.py +67 -28
- package/native/claude/skills/_shared/role-config.py +16 -7
- package/native/claude/skills/_shared/runtime-principles.md +1 -1
- package/native/claude/skills/_shared/storage-client.py +31 -7
- package/native/claude/skills/_shared/task-complete.py +1 -1
- package/native/claude/skills/_shared/task-completion.md +5 -91
- package/native/claude/skills/devlyn-engines/SKILL.md +1 -1
- package/native/claude/skills/devlyn-ideate/SKILL.md +2 -2
- package/native/claude/skills/devlyn-ideate/references/executor.md +62 -0
- package/native/claude/skills/devlyn-ideate/references/loop.md +4 -63
- package/native/claude/skills/devlyn-ideate/references/package-format.md +1 -2
- package/native/claude/skills/devlyn-ideate/scripts/acceptance.py +442 -21
- package/native/claude/skills/devlyn-ideate/scripts/queue.py +58 -10
- package/native/codex/.agents/plugins/marketplace.json +1 -1
- package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +1 -1
- package/native/codex/plugins/devlyn/skills/_shared/engine-doctor.sh +2 -2
- package/native/codex/plugins/devlyn/skills/_shared/engine-preflight.md +2 -4
- package/native/codex/plugins/devlyn/skills/_shared/managed-storage.md +80 -0
- package/native/codex/plugins/devlyn/skills/_shared/platform-support.py +67 -28
- package/native/codex/plugins/devlyn/skills/_shared/role-config.py +16 -7
- package/native/codex/plugins/devlyn/skills/_shared/runtime-principles.md +1 -1
- package/native/codex/plugins/devlyn/skills/_shared/storage-client.py +31 -7
- package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +1 -1
- package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +5 -91
- package/native/codex/plugins/devlyn/skills/devlyn-engines/SKILL.md +1 -1
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +2 -2
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/executor.md +62 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +4 -63
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/package-format.md +1 -2
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py +442 -21
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +58 -10
- package/package.json +1 -11
- package/release.json +99 -69
- /package/config/skills/_shared/adapters/{claude.md → claude.adapter.md} +0 -0
- /package/config/skills/_shared/adapters/{codex.md → codex.adapter.md} +0 -0
- /package/config/skills/_shared/adapters/{omp.md → omp.adapter.md} +0 -0
- /package/native/claude/skills/_shared/adapters/{claude.md → claude.adapter.md} +0 -0
- /package/native/claude/skills/_shared/adapters/{codex.md → codex.adapter.md} +0 -0
- /package/native/claude/skills/_shared/adapters/{omp.md → omp.adapter.md} +0 -0
- /package/native/codex/plugins/devlyn/skills/_shared/adapters/{claude.md → claude.adapter.md} +0 -0
- /package/native/codex/plugins/devlyn/skills/_shared/adapters/{codex.md → codex.adapter.md} +0 -0
- /package/native/codex/plugins/devlyn/skills/_shared/adapters/{omp.md → omp.adapter.md} +0 -0
|
@@ -29,7 +29,7 @@ check_binary() {
|
|
|
29
29
|
|
|
30
30
|
check_adapter() {
|
|
31
31
|
# $1 = target name
|
|
32
|
-
if [ -f "$ADAPTERS_DIR/$1.md" ]; then printf 'yes'; else printf 'no'; fi
|
|
32
|
+
if [ -f "$ADAPTERS_DIR/$1.adapter.md" ]; then printf 'yes'; else printf 'no'; fi
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
check_executor() {
|
|
@@ -47,7 +47,7 @@ for i in "${!TARGETS[@]}"; do
|
|
|
47
47
|
kind="${KINDS[$i]}"
|
|
48
48
|
binary="$(check_binary "${BINARIES[$i]}")"
|
|
49
49
|
adapter="$(check_adapter "$target")"
|
|
50
|
-
executor="$(check_executor "$ADAPTERS_DIR/$target.md")"
|
|
50
|
+
executor="$(check_executor "$ADAPTERS_DIR/$target.adapter.md")"
|
|
51
51
|
|
|
52
52
|
note='-'
|
|
53
53
|
case "$kind" in
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# Shared — Executor Pre-flight
|
|
2
2
|
|
|
3
|
-
Used by `/devlyn-ideate` and `/devlyn-engines`: one rule for which engine executes implementation work and what happens when it is missing.
|
|
4
|
-
|
|
5
3
|
## Selection
|
|
6
4
|
|
|
7
|
-
The executor is the `executor` pin in `.devlyn/engines.json` at the checkout root (set with `/devlyn-engines executor <name>`), else the CLI the user opened. A linked worktree or subdirectory has no pin unless one is set there.
|
|
5
|
+
The executor is the `executor` pin in `.devlyn/engines.json` at the checkout root (set with `/devlyn-engines executor <name>`), else the CLI the user opened. A linked worktree or subdirectory has no pin unless one is set there. Only Claude and Codex can execute, so a drain from any other CLI needs a claude or codex pin. Keys earlier releases wrote for retired pipeline roles (`pair_judge_priority`, `roles`) stay as written and select nothing: a worker profile never becomes the executor.
|
|
8
6
|
|
|
9
7
|
Before dispatching to the executor, run:
|
|
10
8
|
|
|
@@ -16,7 +14,7 @@ A pin is a promise. When the pinned engine's CLI is unavailable, the command exi
|
|
|
16
14
|
|
|
17
15
|
## Engines
|
|
18
16
|
|
|
19
|
-
An engine is pinnable when `_shared/adapters/<name>.md` exists and does not declare `executor: no
|
|
17
|
+
An engine is pinnable when `_shared/adapters/<name>.adapter.md` exists and does not declare `executor: no`. Adapters ship with devlyn-cli releases, and a reinstall replaces `_shared`. An adapter's `## Invocation` says how to start it. On native Windows, use native Node/npm and Python (`python3` on PATH), with Git for Windows Bash for the shipped shell wrapper.
|
|
20
18
|
|
|
21
19
|
## Reporting a blocked engine
|
|
22
20
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Managed storage
|
|
2
|
+
|
|
3
|
+
New allocation and warming modes come from enrollment state alone:
|
|
4
|
+
|
|
5
|
+
| `~/.devlyn/storage-client.json` | recovery marker | unenroll journal | Mode |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| absent | absent | not read | standalone (never enrolled) |
|
|
8
|
+
| valid | any | not read | managed; a fenced authority refuses |
|
|
9
|
+
| invalid or unreadable | any | not read | refuse; repair registration |
|
|
10
|
+
| absent | present | verified `retired: true` | standalone (unenrollment complete) |
|
|
11
|
+
| absent | present | absent, unreadable or incomplete | refuse; Open Devlyn to repair its storage registration |
|
|
12
|
+
|
|
13
|
+
An unfinished retirement takes precedence over enrollment; follow its
|
|
14
|
+
operation-specific retained-helper resume to reconcile or cancel as named.
|
|
15
|
+
The journal `storage-client-unenroll.json` is read no-follow for retirement
|
|
16
|
+
recovery and the last two rows. Schema 1, canonical UUIDv7 operation, permanent `storage-client-fence.json`,
|
|
17
|
+
matching operation/root/lock identities and Rust's archive ordering/path
|
|
18
|
+
validation are required. If native root/lock identities or the home path changed,
|
|
19
|
+
completed retirement permits standalone only when `storage`, `control`,
|
|
20
|
+
`harness/state.json` and `storage-client.json` are all absent under the current
|
|
21
|
+
lifecycle root (checked without following links). Live inventory requires repair.
|
|
22
|
+
No repository-wide receipt scan selects mode. Other keys can allocate after
|
|
23
|
+
completed unenrollment; a leftover managed key retains its own recovery command.
|
|
24
|
+
|
|
25
|
+
Existing operations keep their managed identity. Receipts pin the control
|
|
26
|
+
identity; current enrollment verifies the helper before allocation. Recovery uses
|
|
27
|
+
the current verified `storage-client-recovery/pins.json`, including after uninstall
|
|
28
|
+
or unenrollment, and requires its authority binding to match the receipt, except
|
|
29
|
+
for a proven moved task: use its unflagged, operation-specific `recover-storage`
|
|
30
|
+
resume to release it, retain its files, and archive its receipt under `moved/`. Missing
|
|
31
|
+
or invalid pins retain resources and require repair; saved artifacts are never a
|
|
32
|
+
fallback. Enrolled checkout preserves
|
|
33
|
+
`managed_pending`/`managed_owned` and Rust's `storage-allocation.json` subreceipt.
|
|
34
|
+
After an interrupted reply, use the returned `recover-storage` resume. Never
|
|
35
|
+
delete managed receipts or replace an unfinished operation ID.
|
|
36
|
+
|
|
37
|
+
Owner `recover-storage --receipt <receipt.json> --operation-id <saved ID>
|
|
38
|
+
--for-removal` fences allocation and reconciles closure and the exact cache set.
|
|
39
|
+
Never-published allocations are abandoned. Accepted LOCAL_ONLY, FAILED, and PRs
|
|
40
|
+
observed CLOSED without merge share custody/recovery-ref verification and fenced
|
|
41
|
+
removal, retaining task branches and a durable terminal delivery. Release writers
|
|
42
|
+
with `complete --writers-stopped` first. A queue-produced FAILED checkout is
|
|
43
|
+
advanced by `read-tree -m -u <source_sha> <publish_sha>` only when HEAD is at the
|
|
44
|
+
attached publish commit and the index and worktree are exactly at source; other
|
|
45
|
+
differences refuse as dirty.
|
|
46
|
+
|
|
47
|
+
Removal-finished receipts with final COMPLETE, LOCAL_ONLY, FAILED or CLOSED
|
|
48
|
+
delivery move under `devlyn-completion/retired/`, regardless of scratch status.
|
|
49
|
+
Retirement moves scratch and its saved cleanup record with the audit and deletes
|
|
50
|
+
nothing. `complete --writers-stopped` saves the writer release there and retries
|
|
51
|
+
pending scratch, including an interrupted cleanup with no saved result; the next
|
|
52
|
+
allocation retries completed deliveries with released writers. `clean-scratch`
|
|
53
|
+
remains available there and never downgrades CLEAN.
|
|
54
|
+
Saved managed `complete`, `recover-storage` and `clean-scratch` resumes carry an
|
|
55
|
+
operation ID validated under the receipt lock. Without an ID, a locator selects
|
|
56
|
+
its active receipt. An ID selects only that operation, including its verified
|
|
57
|
+
retired receipt; a mismatched ID refuses. Retired paths also remain usable.
|
|
58
|
+
A new allocation may reuse a retired key without disturbing its audit.
|
|
59
|
+
|
|
60
|
+
Retained submodule repositories in the admin directory block removal even when
|
|
61
|
+
the checkout is absent; preserve/publish their source and explicitly resolve
|
|
62
|
+
them. Manual checkout removal is advised only after portable source checks and
|
|
63
|
+
record preservation succeed; changed or new source requires retention. If native
|
|
64
|
+
Windows or unobservable writers then require retention, stop all
|
|
65
|
+
writers, remove the task checkout by hand, keep its branch and receipt, then run
|
|
66
|
+
the returned owner-removal resume. The absent-tree path completes the fenced
|
|
67
|
+
registration removal. Scratch cleanup requires positive allocation closure,
|
|
68
|
+
without a cache-removal fence; actual checkout removal owns that fence, including
|
|
69
|
+
empty cache sets. Missing helpers or uncertain closure retain resources with a
|
|
70
|
+
repair/resume action. Compatible recovery artifacts and Rust's LaunchGate remain
|
|
71
|
+
required while managed obligations exist.
|
|
72
|
+
|
|
73
|
+
Managed allocations currently start cold; `build_dirs` retains each returned cache
|
|
74
|
+
and publication lease ID. Managed warming is deferred. Standalone allocations keep
|
|
75
|
+
the warming behavior in [task-completion.md](task-completion.md).
|
|
76
|
+
|
|
77
|
+
For managed receipts, use the saved `clean-scratch` resume with `--operation-id <saved ID>`.
|
|
78
|
+
|
|
79
|
+
New managed allocations use the recovery ref `refs/devlyn/completed/<id>.<operation>` so branch reuse
|
|
80
|
+
preserves earlier recovery refs.
|
|
@@ -327,7 +327,9 @@ if os.name == "nt":
|
|
|
327
327
|
self.creating = False
|
|
328
328
|
self.interrupted_signal = None
|
|
329
329
|
|
|
330
|
-
def start(self, argv, stdin):
|
|
330
|
+
def start(self, argv, stdin, **popen):
|
|
331
|
+
"""Launch argv, a list or a command line passed verbatim, inside this job; `popen` passes cwd, env and
|
|
332
|
+
streams to the enclosed bootstrap, whose target inherits them."""
|
|
331
333
|
self.handle = _kernel.CreateJobObjectW(None, None)
|
|
332
334
|
limits = _ExtendedLimits()
|
|
333
335
|
limits.BasicLimitInformation.LimitFlags = 0x2000 # KILL_ON_JOB_CLOSE; no breakaway.
|
|
@@ -346,13 +348,15 @@ if os.name == "nt":
|
|
|
346
348
|
os.set_handle_inheritable(handle, True)
|
|
347
349
|
startup = subprocess.STARTUPINFO()
|
|
348
350
|
startup.lpAttributeList = {"handle_list": inherited}
|
|
351
|
+
# A list travels element by element, a command line verbatim as one element; a mode word tells which.
|
|
349
352
|
command = [sys.executable, "-I", "-S", str(Path(__file__).resolve()), "--job-bootstrap",
|
|
350
|
-
str(inherited[0]), str(inherited[2]), str(inherited[1]),
|
|
353
|
+
str(inherited[0]), str(inherited[2]), str(inherited[1]),
|
|
354
|
+
*(("line", argv) if isinstance(argv, str) else ("argv", *argv))]
|
|
351
355
|
# Retain the Popen object even if __init__ raises after native creation.
|
|
352
356
|
self.child = subprocess.Popen.__new__(subprocess.Popen)
|
|
353
357
|
self.creating = True
|
|
354
358
|
try:
|
|
355
|
-
self.child.__init__(command, stdin=stdin, startupinfo=startup, close_fds=True)
|
|
359
|
+
self.child.__init__(command, stdin=stdin, startupinfo=startup, close_fds=True, **popen)
|
|
356
360
|
finally:
|
|
357
361
|
self.creating = False
|
|
358
362
|
if self.interrupted_signal is not None:
|
|
@@ -424,6 +428,11 @@ if os.name == "nt":
|
|
|
424
428
|
|
|
425
429
|
def _job_bootstrap(argv):
|
|
426
430
|
job, allow, ready = map(int, argv[:3])
|
|
431
|
+
mode, command = argv[3], argv[4:]
|
|
432
|
+
if mode == "line":
|
|
433
|
+
command, = command
|
|
434
|
+
elif mode != "argv":
|
|
435
|
+
raise ValueError(f"unknown job target mode {mode!r}")
|
|
427
436
|
with contextlib.ExitStack() as handles:
|
|
428
437
|
allow_fd = msvcrt.open_osfhandle(allow, os.O_RDONLY | os.O_BINARY)
|
|
429
438
|
handles.callback(os.close, allow_fd)
|
|
@@ -436,50 +445,77 @@ if os.name == "nt":
|
|
|
436
445
|
os.write(ready_fd, b"R")
|
|
437
446
|
if _read_control(allow_fd, 1) != b"G":
|
|
438
447
|
raise OSError("Windows job launch was not authorized")
|
|
439
|
-
with subprocess.Popen(
|
|
448
|
+
with subprocess.Popen(command, stdin=sys.stdin.buffer, close_fds=True) as target:
|
|
440
449
|
os.write(ready_fd, target.pid.to_bytes(4, "little"))
|
|
441
450
|
return target.wait()
|
|
442
451
|
|
|
443
452
|
|
|
453
|
+
def _signal_group(child, sig):
|
|
454
|
+
"""Send `sig` to the child's process group; False once it has no member this process may signal. An exited leader
|
|
455
|
+
is reaped first: until then it still counts as a member."""
|
|
456
|
+
child.poll()
|
|
457
|
+
try:
|
|
458
|
+
os.killpg(child.pid, sig)
|
|
459
|
+
except ProcessLookupError:
|
|
460
|
+
return False
|
|
461
|
+
except PermissionError: # EPERM: no member this process may signal; with the leader alive the run cannot be ended.
|
|
462
|
+
if child.poll() is None:
|
|
463
|
+
raise
|
|
464
|
+
return False
|
|
465
|
+
return True
|
|
466
|
+
|
|
467
|
+
|
|
444
468
|
def terminate_tree(child, job=None):
|
|
445
469
|
if job is not None:
|
|
446
470
|
job.terminate()
|
|
447
471
|
return
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
time.sleep(max(0, deadline - time.monotonic()))
|
|
460
|
-
try:
|
|
461
|
-
os.killpg(child.pid, signal.SIGKILL)
|
|
462
|
-
except ProcessLookupError:
|
|
463
|
-
pass # Every member already exited during the grace period.
|
|
464
|
-
child.wait()
|
|
472
|
+
# SIGTERM and up to 5 s for the group to leave, then SIGKILL and about 1 s more; what still answers then is dead but
|
|
473
|
+
# unreaped or may not be signalled, which cannot be told apart. A member this process cannot signal, or one that left
|
|
474
|
+
# the session, is not owned; acceptance.py's evidence is safe from it because no check process holds an evidence file.
|
|
475
|
+
for sig, seconds in ((signal.SIGTERM, 5), (signal.SIGKILL, 1)):
|
|
476
|
+
deadline = time.monotonic() + seconds
|
|
477
|
+
alive = _signal_group(child, sig)
|
|
478
|
+
while alive and time.monotonic() < deadline:
|
|
479
|
+
time.sleep(0.01)
|
|
480
|
+
alive = _signal_group(child, 0)
|
|
481
|
+
if not alive:
|
|
482
|
+
return
|
|
465
483
|
|
|
466
484
|
|
|
467
|
-
def run_process(argv, stdin, seconds, *, heartbeat=0):
|
|
485
|
+
def run_process(argv, stdin, seconds, *, heartbeat=0, on_timeout=124, **popen):
|
|
486
|
+
"""Run argv as an owned tree (its own POSIX session or Windows job), ended whole however the run ends; `popen`
|
|
487
|
+
passes cwd, env and streams. Returns the exit status, or `on_timeout` once `seconds` pass. On Windows argv may be a
|
|
488
|
+
command line passed verbatim."""
|
|
468
489
|
child = None
|
|
469
490
|
job = _WindowsJob() if os.name == "nt" else None
|
|
470
491
|
previous = {}
|
|
492
|
+
received = [] # Every handled signal; the first ends the run with 128 + it once the tree has ended.
|
|
493
|
+
deferring = False # While set, a signal only waits: a POSIX child's creation and the teardown are not interrupted.
|
|
471
494
|
|
|
472
495
|
def interrupted(signum, _frame):
|
|
496
|
+
received.append(signum)
|
|
473
497
|
# CPython stores the native handle only after CreateProcess's finally block.
|
|
474
498
|
if job is not None and job.creating:
|
|
475
499
|
job.interrupted_signal = signum
|
|
476
|
-
|
|
477
|
-
|
|
500
|
+
elif not deferring:
|
|
501
|
+
raise SystemExit(128 + signum)
|
|
478
502
|
|
|
479
503
|
try:
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
504
|
+
handled = [signal.SIGTERM, signal.SIGINT]
|
|
505
|
+
if os.name != "nt" and signal.getsignal(signal.SIGHUP) is not signal.SIG_IGN:
|
|
506
|
+
# A closed terminal hangs up the runner, not the child's own session; an inherited ignore (nohup) stays.
|
|
507
|
+
handled.append(signal.SIGHUP)
|
|
508
|
+
for sig in handled:
|
|
509
|
+
previous[sig] = signal.getsignal(sig) # Saved first: a signal right after installation finds it to restore.
|
|
510
|
+
signal.signal(sig, interrupted)
|
|
511
|
+
if job is not None:
|
|
512
|
+
child = job.start(argv, stdin, **popen)
|
|
513
|
+
else:
|
|
514
|
+
deferring = True # Raised inside Popen, a signal would leave the new session unowned.
|
|
515
|
+
child = subprocess.Popen(argv, stdin=stdin, start_new_session=True, **popen)
|
|
516
|
+
deferring = False
|
|
517
|
+
if received:
|
|
518
|
+
raise SystemExit(128 + received[0])
|
|
483
519
|
started = time.monotonic()
|
|
484
520
|
next_heartbeat = started + heartbeat
|
|
485
521
|
if heartbeat:
|
|
@@ -492,17 +528,20 @@ def run_process(argv, stdin, seconds, *, heartbeat=0):
|
|
|
492
528
|
if seconds and now - started >= seconds:
|
|
493
529
|
if heartbeat:
|
|
494
530
|
print(f"[codex-monitored] timeout: elapsed={int(now-started)}s limit={seconds}s", file=sys.stderr, flush=True)
|
|
495
|
-
return
|
|
531
|
+
return on_timeout
|
|
496
532
|
if heartbeat and now >= next_heartbeat:
|
|
497
533
|
print(f"[codex-monitored] heartbeat: elapsed={int(now-started)}s", file=sys.stderr, flush=True)
|
|
498
534
|
next_heartbeat = now + heartbeat
|
|
499
535
|
finally:
|
|
536
|
+
deferring = True # Teardown runs to its end.
|
|
500
537
|
try:
|
|
501
538
|
if job is not None or child is not None:
|
|
502
539
|
terminate_tree(child, job)
|
|
503
540
|
finally:
|
|
504
541
|
for sig, handler in previous.items():
|
|
505
542
|
signal.signal(sig, handler)
|
|
543
|
+
if received: # Whenever it came and whatever teardown raised, a signal ends the run.
|
|
544
|
+
raise SystemExit(128 + received[0])
|
|
506
545
|
|
|
507
546
|
|
|
508
547
|
if __name__ == "__main__" and os.name == "nt":
|
|
@@ -56,13 +56,19 @@ def name(value, field):
|
|
|
56
56
|
return value
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
INELIGIBLE = re.compile(r"^executor: no\s*$", re.M)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Named `<engine>.adapter.md`: on a case-insensitive filesystem Claude Code loads `claude.md` as nested instructions.
|
|
59
63
|
def adapter(engine, shared=SHARED):
|
|
60
64
|
name(engine, "engine")
|
|
61
|
-
path = shared / "adapters" / f"{engine}.md"
|
|
65
|
+
path = shared / "adapters" / f"{engine}.adapter.md"
|
|
62
66
|
if not path.is_file():
|
|
63
|
-
|
|
67
|
+
valid = sorted(p.name.removesuffix(".adapter.md") for p in path.parent.glob("*.adapter.md")
|
|
68
|
+
if not INELIGIBLE.search(p.read_text(encoding="utf-8")))
|
|
69
|
+
fail(f"no adapter for {engine}; valid: {', '.join(valid)}")
|
|
64
70
|
text = path.read_text(encoding="utf-8")
|
|
65
|
-
if
|
|
71
|
+
if INELIGIBLE.search(text):
|
|
66
72
|
fail(f"{engine} is ineligible as executor")
|
|
67
73
|
return text
|
|
68
74
|
|
|
@@ -178,15 +184,18 @@ def self_test():
|
|
|
178
184
|
# Pins are validated before any byte changes.
|
|
179
185
|
ineligible = work / "shared"
|
|
180
186
|
(ineligible / "adapters").mkdir(parents=True)
|
|
181
|
-
(ineligible / "adapters/judge.md").write_text("## Role eligibility\n\nexecutor: no\n", encoding="utf-8")
|
|
182
|
-
(ineligible / "adapters/codex.md").write_text("# Codex adapter\n", encoding="utf-8")
|
|
187
|
+
(ineligible / "adapters/judge.adapter.md").write_text("## Role eligibility\n\nexecutor: no\n", encoding="utf-8")
|
|
188
|
+
(ineligible / "adapters/codex.adapter.md").write_text("# Codex adapter\n", encoding="utf-8")
|
|
183
189
|
path.write_bytes(legacy)
|
|
184
|
-
|
|
190
|
+
# A refused name lists only the engines that can execute.
|
|
191
|
+
for engine, shared, message in (("grok", SHARED, "no adapter for grok; valid: claude, codex"),
|
|
192
|
+
("grok", ineligible, "no adapter for grok; valid: codex"),
|
|
193
|
+
("judge", ineligible, "ineligible as executor"),
|
|
185
194
|
("../codex", SHARED, "nonempty identifier")):
|
|
186
195
|
try:
|
|
187
196
|
edit(work, engine, shared=shared)
|
|
188
197
|
except ValueError as exc:
|
|
189
|
-
assert "BLOCKED:invalid-engine-config"
|
|
198
|
+
assert str(exc).startswith("BLOCKED:invalid-engine-config:") and str(exc).endswith(message), exc
|
|
190
199
|
else:
|
|
191
200
|
raise AssertionError(f"{engine} was pinned")
|
|
192
201
|
assert path.read_bytes() == legacy
|
|
@@ -29,7 +29,7 @@ Three discipline rules govern HOW the principles are applied:
|
|
|
29
29
|
## Quick Start
|
|
30
30
|
|
|
31
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
|
|
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`. 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
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
34
|
|
|
35
35
|
Each skill's `SKILL.md` is the source of truth for its flags and workflow.
|
|
@@ -42,7 +42,6 @@ import uuid
|
|
|
42
42
|
MAX_REQUEST = 128 * 1024
|
|
43
43
|
MAX_REPLY = 4 * 1024 * 1024
|
|
44
44
|
EXCHANGE_SECONDS = 15.0
|
|
45
|
-
BUSY_SECONDS = 2.0
|
|
46
45
|
MANAGED_PENDING = "managed_pending"
|
|
47
46
|
MANAGED_OWNED = "managed_owned"
|
|
48
47
|
MANAGED_STATES = frozenset({MANAGED_PENDING, MANAGED_OWNED})
|
|
@@ -726,7 +725,9 @@ class Client:
|
|
|
726
725
|
def call(self, request):
|
|
727
726
|
data = encode_request(request) # Freeze validated bytes before retry.
|
|
728
727
|
expected = REQUESTS[request["op"]][1]
|
|
729
|
-
|
|
728
|
+
# Behind a busy worker, wait as long as one exchange may take, as devlynd's own clients
|
|
729
|
+
# do (EXCHANGE_BUDGET): one slow request must not fail the call.
|
|
730
|
+
busy_deadline, backoff = time.monotonic()+EXCHANGE_SECONDS, 0.01
|
|
730
731
|
try:
|
|
731
732
|
while True:
|
|
732
733
|
try:
|
|
@@ -1113,6 +1114,7 @@ def moved_operation(saved):
|
|
|
1113
1114
|
pins = lifecycle_evidence(root, root / 'storage-client-recovery/pins.json', observed)
|
|
1114
1115
|
if pins is None:
|
|
1115
1116
|
return None # No retained binding can establish a moved receipt.
|
|
1117
|
+
require_known_pins(pins)
|
|
1116
1118
|
validate(pins, {'schema': 'u32', 'enrollment': ENROLLMENT_SHAPE, 'runtime': ARTIFACT_SHAPE, 'node': 'str'}, exact=True)
|
|
1117
1119
|
require(pins['schema'] == pins['enrollment']['schema'] == 1, 'unsupported recovery pins')
|
|
1118
1120
|
if (all(saved.get(key) == pins['enrollment'][key] for key in ('control_directory', 'control_identity'))
|
|
@@ -1174,6 +1176,8 @@ def retirement_refusal(root=None):
|
|
|
1174
1176
|
require(fence is not None, 'retirement fence missing')
|
|
1175
1177
|
record = recovery_registration()
|
|
1176
1178
|
except (StorageRefusal, OSError, ValueError, RuntimeError) as exc:
|
|
1179
|
+
if isinstance(exc, StorageRefusal) and exc.code == 'storage_unsupported':
|
|
1180
|
+
raise # An update answers it; Devlyn can repair nothing a newer Devlyn wrote.
|
|
1177
1181
|
detail = exc.diagnostic if isinstance(exc, StorageRefusal) else str(exc)
|
|
1178
1182
|
repair = 'Open Devlyn to repair its storage registration'
|
|
1179
1183
|
raise StorageRefusal('storage_unavailable', detail, phase='enrollment', message=repair, resume=repair) from exc
|
|
@@ -1188,6 +1192,22 @@ def retirement_refusal(root=None):
|
|
|
1188
1192
|
message='Finish this storage retirement; resume ' + resume + alternative, resume=resume)
|
|
1189
1193
|
|
|
1190
1194
|
|
|
1195
|
+
def require_known_pins(pins):
|
|
1196
|
+
"""Pins a newer Devlyn wrote refuse as unsupported, as the managed read's newer record does, never as pins to
|
|
1197
|
+
repair: only that Devlyn may change them."""
|
|
1198
|
+
enrollment = pins.get('enrollment') if type(pins) is dict else None
|
|
1199
|
+
schemas = (pins.get('schema') if type(pins) is dict else None,
|
|
1200
|
+
enrollment.get('schema') if type(enrollment) is dict else None)
|
|
1201
|
+
require(not any(type(schema) is int and schema > 1 for schema in schemas),
|
|
1202
|
+
'recovery pins from a newer Devlyn', 'storage_unsupported')
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def invalid_pins(exc):
|
|
1206
|
+
detail = exc.diagnostic if isinstance(exc, StorageRefusal) else str(exc)
|
|
1207
|
+
return StorageRefusal('storage_unavailable', f'recovery pins invalid: {detail}', phase='enrollment',
|
|
1208
|
+
message='Retain the workspace and receipt; repair Devlyn recovery pins and retry')
|
|
1209
|
+
|
|
1210
|
+
|
|
1191
1211
|
def recovery_registration():
|
|
1192
1212
|
"""Current retained pins survive replacement of the app and unenrollment."""
|
|
1193
1213
|
try:
|
|
@@ -1199,6 +1219,10 @@ def recovery_registration():
|
|
|
1199
1219
|
require(info is not None and stat.S_ISDIR(info.st_mode), 'recovery pins directory missing/invalid')
|
|
1200
1220
|
observed = [(marker, info)]
|
|
1201
1221
|
pins = read_evidence_json(marker / 'pins.json', observed)
|
|
1222
|
+
except (StorageRefusal, OSError, ValueError, RuntimeError) as exc:
|
|
1223
|
+
raise invalid_pins(exc) from exc
|
|
1224
|
+
require_known_pins(pins)
|
|
1225
|
+
try:
|
|
1202
1226
|
validate(pins, {'schema': 'u32', 'enrollment': ENROLLMENT_SHAPE,
|
|
1203
1227
|
'runtime': ARTIFACT_SHAPE, 'node': 'str'}, exact=True)
|
|
1204
1228
|
record = pins['enrollment']
|
|
@@ -1216,9 +1240,7 @@ def recovery_registration():
|
|
|
1216
1240
|
require(after is None if before is None else _same_file(before, after), f'recovery pins changed: {path}')
|
|
1217
1241
|
return record
|
|
1218
1242
|
except (StorageRefusal, OSError, ValueError, RuntimeError) as exc:
|
|
1219
|
-
|
|
1220
|
-
raise StorageRefusal('storage_unavailable', f'recovery pins invalid: {detail}', phase='enrollment',
|
|
1221
|
-
message='Retain the workspace and receipt; repair Devlyn recovery pins and retry') from exc
|
|
1243
|
+
raise invalid_pins(exc) from exc
|
|
1222
1244
|
|
|
1223
1245
|
|
|
1224
1246
|
def windows_profile():
|
|
@@ -1480,7 +1502,9 @@ def queue_handoff(common, binding):
|
|
|
1480
1502
|
helper = runpy.run_path(str(Path(__file__).with_name('task-complete.py')))
|
|
1481
1503
|
text += ' resume ' + helper['receipt_resume']('recover-storage', receipt, saved)
|
|
1482
1504
|
holders.append(text)
|
|
1483
|
-
except (StorageRefusal, OSError, ValueError, TypeError, AttributeError, KeyError):
|
|
1505
|
+
except (StorageRefusal, OSError, ValueError, TypeError, AttributeError, KeyError) as exc:
|
|
1506
|
+
if isinstance(exc, StorageRefusal) and exc.code == 'storage_unsupported':
|
|
1507
|
+
return f'{receipt}: {exc}' # Evidence a newer Devlyn wrote: update, never repair.
|
|
1484
1508
|
if adapter_marker:
|
|
1485
1509
|
holders.append(str(receipt))
|
|
1486
1510
|
continue
|
|
@@ -1491,7 +1515,7 @@ def queue_handoff(common, binding):
|
|
|
1491
1515
|
return drain if managed else None
|
|
1492
1516
|
return ('this queue waits for the tasks in progress here: ' + '; '.join(holders)
|
|
1493
1517
|
+ ". Finish each with this release, and follow any resume a refusal names; a moved task's resume releases it and keeps its files; "
|
|
1494
|
-
+ '`recover-storage --for-removal` removes one only when the owner says so (
|
|
1518
|
+
+ '`recover-storage --for-removal` removes one only when the owner says so (managed-storage.md); '
|
|
1495
1519
|
+ ('then start it with Drain in Devlyn' if managed else 'then run this again'))
|
|
1496
1520
|
|
|
1497
1521
|
|
|
@@ -328,7 +328,7 @@ def storage_call(name, *args, adapter=False, receipt_path=None, resume=None, **k
|
|
|
328
328
|
if resume:
|
|
329
329
|
if resume not in message:
|
|
330
330
|
message += '; resume ' + resume
|
|
331
|
-
|
|
331
|
+
elif exc.code != 'storage_unsupported': # An update is that refusal's remedy, never a repair.
|
|
332
332
|
message += '; repair Devlyn enrollment and retry'
|
|
333
333
|
error = CompletionError(message)
|
|
334
334
|
error.storage_error = exc.result(receipt=path, resume=resume)
|
|
@@ -62,9 +62,6 @@ The repository comes from the remote's GitHub URL and the base from the reposito
|
|
|
62
62
|
A non-default base is refused at allocation.
|
|
63
63
|
Every delivery allocation owns a linked worktree; `--worktree` is required and
|
|
64
64
|
must name an absent path.
|
|
65
|
-
Cleanup removes empty parents through the exact per-repo root shown above or its legacy `.devlyn.noindex`/`.devlyn` root, keeping the shared `.devlyn-worktrees` container. Allocation also sweeps empty legacy roots for this repo.
|
|
66
|
-
On Windows, allocation marks only a newly created shared container hidden and creates the worktree not content-indexed (content only; names stay indexed); files copied in keep their own attributes.
|
|
67
|
-
A marking failure continues allocation and is reported in `warnings`.
|
|
68
65
|
Remote allocation first cleans earlier merged deliveries, then allocation refuses with `BLOCKED:disk`
|
|
69
66
|
without creating a receipt, branch or worktree when that filesystem has less than max(10 GiB, 2%) free; free space and allocate again.
|
|
70
67
|
A publication allocation normally starts at the exact fetched remote base,
|
|
@@ -96,75 +93,7 @@ can an interrupted standalone receipt that never reached `allocation: owned`:
|
|
|
96
93
|
remove its worktree and branch if present, delete its receipt directory, then allocate again.
|
|
97
94
|
Unreceipted tasks remain owner-managed.
|
|
98
95
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
| `~/.devlyn/storage-client.json` | recovery marker | unenroll journal | Mode |
|
|
102
|
-
|---|---|---|---|
|
|
103
|
-
| absent | absent | not read | standalone (never enrolled) |
|
|
104
|
-
| valid | any | not read | managed; a fenced authority refuses |
|
|
105
|
-
| invalid or unreadable | any | not read | refuse; repair registration |
|
|
106
|
-
| absent | present | verified `retired: true` | standalone (unenrollment complete) |
|
|
107
|
-
| absent | present | absent, unreadable or incomplete | refuse; Open Devlyn to repair its storage registration |
|
|
108
|
-
|
|
109
|
-
An unfinished retirement takes precedence over enrollment; follow its
|
|
110
|
-
operation-specific retained-helper resume to reconcile or cancel as named.
|
|
111
|
-
The journal `storage-client-unenroll.json` is read no-follow for retirement
|
|
112
|
-
recovery and the last two rows. Schema 1, canonical UUIDv7 operation, permanent `storage-client-fence.json`,
|
|
113
|
-
matching operation/root/lock identities and Rust's archive ordering/path
|
|
114
|
-
validation are required. If native root/lock identities or the home path changed,
|
|
115
|
-
completed retirement permits standalone only when `storage`, `control`,
|
|
116
|
-
`harness/state.json` and `storage-client.json` are all absent under the current
|
|
117
|
-
lifecycle root (checked without following links). Live inventory requires repair.
|
|
118
|
-
No repository-wide receipt scan selects mode. Other keys can allocate after
|
|
119
|
-
completed unenrollment; a leftover managed key retains its own recovery command.
|
|
120
|
-
|
|
121
|
-
Existing operations keep their managed identity. Receipts pin the control
|
|
122
|
-
identity; current enrollment verifies the helper before allocation. Recovery uses
|
|
123
|
-
the current verified `storage-client-recovery/pins.json`, including after uninstall
|
|
124
|
-
or unenrollment, and requires its authority binding to match the receipt, except
|
|
125
|
-
for a proven moved task: use its unflagged, operation-specific `recover-storage`
|
|
126
|
-
resume to release it, retain its files, and archive its receipt under `moved/`. Missing
|
|
127
|
-
or invalid pins retain resources and require repair; saved artifacts are never a
|
|
128
|
-
fallback. Enrolled checkout preserves
|
|
129
|
-
`managed_pending`/`managed_owned` and Rust's `storage-allocation.json` subreceipt.
|
|
130
|
-
After an interrupted reply, use the returned `recover-storage` resume. Never
|
|
131
|
-
delete managed receipts or replace an unfinished operation ID.
|
|
132
|
-
|
|
133
|
-
Owner `recover-storage --receipt <receipt.json> --operation-id <saved ID>
|
|
134
|
-
--for-removal` fences allocation and reconciles closure and the exact cache set.
|
|
135
|
-
Never-published allocations are abandoned. Accepted LOCAL_ONLY, FAILED, and PRs
|
|
136
|
-
observed CLOSED without merge share custody/recovery-ref verification and fenced
|
|
137
|
-
removal, retaining task branches and a durable terminal delivery. Release writers
|
|
138
|
-
with `complete --writers-stopped` first. A queue-produced FAILED checkout is
|
|
139
|
-
advanced by `read-tree -m -u <source_sha> <publish_sha>` only when HEAD is at the
|
|
140
|
-
attached publish commit and the index and worktree are exactly at source; other
|
|
141
|
-
differences refuse as dirty.
|
|
142
|
-
|
|
143
|
-
Removal-finished receipts with final COMPLETE, LOCAL_ONLY, FAILED or CLOSED
|
|
144
|
-
delivery move under `devlyn-completion/retired/`, regardless of scratch status.
|
|
145
|
-
Retirement moves scratch and its saved cleanup record with the audit and deletes
|
|
146
|
-
nothing. `complete --writers-stopped` saves the writer release there and retries
|
|
147
|
-
pending scratch, including an interrupted cleanup with no saved result; the next
|
|
148
|
-
allocation retries completed deliveries with released writers. `clean-scratch`
|
|
149
|
-
remains available there and never downgrades CLEAN.
|
|
150
|
-
Saved managed `complete`, `recover-storage` and `clean-scratch` resumes carry an
|
|
151
|
-
operation ID validated under the receipt lock. Without an ID, a locator selects
|
|
152
|
-
its active receipt. An ID selects only that operation, including its verified
|
|
153
|
-
retired receipt; a mismatched ID refuses. Retired paths also remain usable.
|
|
154
|
-
A new allocation may reuse a retired key without disturbing its audit.
|
|
155
|
-
|
|
156
|
-
Retained submodule repositories in the admin directory block removal even when
|
|
157
|
-
the checkout is absent; preserve/publish their source and explicitly resolve
|
|
158
|
-
them. Manual checkout removal is advised only after portable source checks and
|
|
159
|
-
record preservation succeed; changed or new source requires retention. If native
|
|
160
|
-
Windows or unobservable writers then require retention, stop all
|
|
161
|
-
writers, remove the task checkout by hand, keep its branch and receipt, then run
|
|
162
|
-
the returned owner-removal resume. The absent-tree path completes the fenced
|
|
163
|
-
registration removal. Scratch cleanup requires positive allocation closure,
|
|
164
|
-
without a cache-removal fence; actual checkout removal owns that fence, including
|
|
165
|
-
empty cache sets. Missing helpers or uncertain closure retain resources with a
|
|
166
|
-
repair/resume action. Compatible recovery artifacts and Rust's LaunchGate remain
|
|
167
|
-
required while managed obligations exist.
|
|
96
|
+
Work is standalone when `~/.devlyn/storage-client.json` and its recovery marker `~/.devlyn/storage-client-recovery` are both absent and the receipt in hand is not a managed one; otherwise read [managed-storage.md](managed-storage.md) first.
|
|
168
97
|
|
|
169
98
|
Copy attributable task edits into the allocated worktree as a patch, never a
|
|
170
99
|
stash. Establish attribution from the pre-edit contents and starting Git state,
|
|
@@ -183,10 +112,6 @@ require the affected checks again.
|
|
|
183
112
|
Keep the original edits through pending or failed delivery; only reconciliation
|
|
184
113
|
removes them. Preserve all other work and report anything left behind.
|
|
185
114
|
|
|
186
|
-
Managed allocations currently start cold; `build_dirs` retains each returned cache
|
|
187
|
-
and publication lease ID. Managed warming is deferred. Standalone allocations keep
|
|
188
|
-
the warming behavior below.
|
|
189
|
-
|
|
190
115
|
Build in the task worktree's own default build directory, never in one
|
|
191
116
|
another checkout builds in, even one build at a time: tools such as Cargo take
|
|
192
117
|
one checkout's outputs as fresh for the other. When the shared repository-local
|
|
@@ -194,9 +119,7 @@ Git config `devlyn.warmBuildDirs` is true, or it is unset and the Cargo workspac
|
|
|
194
119
|
Cargo.toml sets `[workspace.metadata.devlyn] warm-build-dirs = true`,
|
|
195
120
|
allocation seeds that directory from a built checkout of the repository, so
|
|
196
121
|
the first build does not rebuild every dependency; `build_dirs` in its result
|
|
197
|
-
and receipt says what it did, and a skip means an ordinary cold build.
|
|
198
|
-
Git setting takes precedence on this machine: false disables and invalid values
|
|
199
|
-
skip; global, system, include and per-worktree values are not consulted. Seeding
|
|
122
|
+
and receipt says what it did, and a skip means an ordinary cold build. Seeding
|
|
200
123
|
keeps dependency outputs that may hold the other checkout's paths, and those
|
|
201
124
|
follow that checkout's later rebuilds. `warm-build-dirs.py <worktree>` does the same for a
|
|
202
125
|
worktree made without allocation. A warmed directory leaves with the worktree
|
|
@@ -216,14 +139,9 @@ release build caches when parking or after a failed run, use:
|
|
|
216
139
|
```sh
|
|
217
140
|
python3 "$DEVLYN_SHARED_DIR/task-complete.py" clean-scratch \
|
|
218
141
|
--receipt '<returned receipt.json>' --writers-stopped
|
|
219
|
-
# For managed receipts, use the saved resume with --operation-id <saved ID>.
|
|
220
142
|
```
|
|
221
143
|
|
|
222
|
-
|
|
223
|
-
identity for interrupted cleanup and later builds, checks live users and reports
|
|
224
|
-
the logical byte count separately from actual free disk space.
|
|
225
|
-
Unknown historical roots, source/recovery data, shared caches and terminal
|
|
226
|
-
history are never adopted or swept. A refused cleanup does not block source
|
|
144
|
+
A refused cleanup does not block source
|
|
227
145
|
delivery; it stays visible in the handoff with its receipt and retry command
|
|
228
146
|
(`CLEANUP_PENDING` after delivered source). No task is described as cleaned
|
|
229
147
|
while owned disposable files remain. On resumption, finish any pending scratch
|
|
@@ -321,8 +239,7 @@ the next allocation or resume after the restart.
|
|
|
321
239
|
On native Windows, locking and file durability are supported, but the helper cannot prove writer cessation and reports `writer observation unsupported on this platform; retain workspace`. Even `--writers-stopped` cannot authorize deletion without that observation: retain the workspace, its task refs and external receipt/custody, report delivery separately, and preserve the receipt-based resume command. A merged or on-base delivery therefore reports `CLEANUP_PENDING` (delivery `COMPLETE`) with `workspace_cleanup` naming the reason and resume. Do not kill unknown processes or force worktree removal to make completion pass.
|
|
322
240
|
|
|
323
241
|
The receipt directory holds byte-verified `custody/`, `manifest.json` and a guarded
|
|
324
|
-
`refs/devlyn/completed/<id>` recovery ref
|
|
325
|
-
`<id>.<operation>` so branch reuse preserves earlier recovery refs). Check evidence is copied there
|
|
242
|
+
`refs/devlyn/completed/<id>` recovery ref. Check evidence is copied there
|
|
326
243
|
before publication; removal rechecks it, the recovery ref and the merge or accepted commit on base.
|
|
327
244
|
Ignored build output and other ignored files are disposable under native
|
|
328
245
|
non-force worktree removal. The tree's `.devlyn/` files are byte/mode-verified
|
|
@@ -331,9 +248,6 @@ are preserved beside it as `records.<digest>/.devlyn`, never rewriting earlier c
|
|
|
331
248
|
Non-regular files or symlinks there, partial/corrupt custody, an unreadable
|
|
332
249
|
earlier copy or a mismatched `records.<digest>` copy retain the tree for repair.
|
|
333
250
|
|
|
334
|
-
|
|
335
|
-
checked out; their checkout is never modified. Local ref deletion is guarded by
|
|
336
|
-
the expected SHA; remote deletion uses an exact force-with-lease solely as
|
|
337
|
-
compare-and-delete, never for product publication. A changed remote branch is
|
|
251
|
+
A changed remote branch is
|
|
338
252
|
retained. Retry uses the external receipt/evidence after removal. Recovery
|
|
339
253
|
refs/evidence have no automatic expiry.
|
|
@@ -56,7 +56,7 @@ Verify DEVLYN_SHARED_DIR/role-config.py and DEVLYN_SHARED_DIR/engine-doctor.sh b
|
|
|
56
56
|
|
|
57
57
|
## Subcommands
|
|
58
58
|
|
|
59
|
-
- `executor <name>` — run the status command with `--set-executor <name>`. It refuses a name with no `_shared/adapters/<name>.md`, or whose adapter declares `executor: no`, and lists the valid names. An engine whose CLI is
|
|
59
|
+
- `executor <name>` — run the status command with `--set-executor <name>`. It refuses a name with no `_shared/adapters/<name>.adapter.md`, or whose adapter declares `executor: no`, and lists the valid names. An engine whose CLI is unavailable is still pinned, with a warning: pins are promises, and dispatch stops with `BLOCKED:<name>-unavailable` until installed.
|
|
60
60
|
- `clear` — run the status command with `--clear`: it removes the executor pin and the inactive keys, preserves unrelated keys, and deletes an empty file.
|
|
61
61
|
- `pair` and `role` were removed with the retired pipeline roles: say so and change nothing.
|
|
62
62
|
|