devlyn-cli 4.3.31 → 4.3.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +4 -2
  2. package/bin/devlyn.js +109 -22
  3. package/bin/harness-activation.js +4 -1
  4. package/bin/harness-status.js +15 -8
  5. package/bin/skill-history.json +10 -0
  6. package/config/skills/_shared/bring-in.py +10 -0
  7. package/config/skills/_shared/task-complete.py +787 -87
  8. package/config/skills/_shared/task-completion.md +6 -6
  9. package/config/skills/_shared/warm-build-dirs.py +13 -0
  10. package/config/skills/devlyn-ideate/SKILL.md +3 -2
  11. package/config/skills/devlyn-ideate/references/loop.md +2 -2
  12. package/config/skills/devlyn-ideate/scripts/acceptance.py +11 -0
  13. package/config/skills/devlyn-ideate/scripts/queue.py +685 -10
  14. package/native/claude/.claude-plugin/plugin.json +1 -1
  15. package/native/claude/skills/_shared/bring-in.py +10 -0
  16. package/native/claude/skills/_shared/task-complete.py +787 -87
  17. package/native/claude/skills/_shared/task-completion.md +6 -6
  18. package/native/claude/skills/_shared/warm-build-dirs.py +13 -0
  19. package/native/claude/skills/devlyn-ideate/SKILL.md +3 -2
  20. package/native/claude/skills/devlyn-ideate/references/loop.md +2 -2
  21. package/native/claude/skills/devlyn-ideate/scripts/acceptance.py +11 -0
  22. package/native/claude/skills/devlyn-ideate/scripts/queue.py +685 -10
  23. package/native/codex/.agents/plugins/marketplace.json +1 -1
  24. package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +1 -1
  25. package/native/codex/plugins/devlyn/skills/_shared/bring-in.py +10 -0
  26. package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +787 -87
  27. package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +6 -6
  28. package/native/codex/plugins/devlyn/skills/_shared/warm-build-dirs.py +13 -0
  29. package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +3 -2
  30. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +2 -2
  31. package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py +11 -0
  32. package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +685 -10
  33. package/package.json +1 -1
  34. package/release.json +35 -35
@@ -201,14 +201,14 @@ publication mode pushes no task commit and opens no PR. Delivery is `COMPLETE` w
201
201
  `pr` pushes the exact accepted task ref and creates/reuses its exact repository,
202
202
  head and base PR. While OPEN, it cancels owned auto-merge, reports `PR` with its URL
203
203
  and retains the checkout. Once MERGED by any method, either mode cleans released
204
- resources in that call, on resume or at the next allocation. Default `auto` also requests
205
- a `--merge --match-head-commit <accepted SHA>` merge. Repository
206
- checks/reviews and merge policy remain authoritative: no admin bypass, strategy
204
+ resources in that call, on resume or at the next allocation. Default `auto` also
205
+ merges the accepted commit. Merge policy remains authoritative: no admin bypass, strategy
207
206
  fallback or repository setting changes. When the repository disallows merge
208
- commits or refuses the request, delivery reports `PR` with `merge_refused` and
207
+ commits or refuses the merge, delivery reports `PR` with `merge_refused` and
209
208
  the PR waits for a person. Command success alone never proves merge.
210
209
 
211
- `PENDING` retains the workspace and reports a receipt-based resume command.
210
+ `PENDING` retains the workspace; its receipt-based resume command merges once
211
+ required checks or reviews pass, or a person merges it.
212
212
  `BLOCKED` names the cause and retry action; repair that condition before retrying.
213
213
  Resume reobserves Git/PR state under a per-receipt lock, reuses the original
214
214
  acceptance and performs only missing eligible effects.
@@ -236,7 +236,7 @@ affected resources. Use `git worktree lock` to keep a tree.
236
236
  A VM that shared the tree (Docker Desktop after a bind mount) can keep files open
237
237
  until it restarts; the helper names it when identifiable and retries cleanup on
238
238
  the next allocation or resume after the restart.
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.
239
+ On native Windows, a tree a program has a file open in, or runs in, is kept until that program lets go; one a program only watches may be removed. A managed tree instead waits for removal by hand, as its reason says; scratch that holds files is retained. Do not kill unknown processes or force worktree removal to make completion pass.
240
240
 
241
241
  The receipt directory holds byte-verified `custody/`, `manifest.json` and a guarded
242
242
  `refs/devlyn/completed/<id>` recovery ref. Check evidence is copied there
@@ -706,9 +706,14 @@ class WarmTests(unittest.TestCase):
706
706
  (bin_dir / "cargo").write_text(FAKE_CARGO)
707
707
  (bin_dir / "cargo").chmod(0o755)
708
708
  self.fixture, self.log = self.root / "fixture.json", self.root / "cargo.log"
709
+ # A commit's detached automatic maintenance would still write into .git while a test reads the tree. Appended to
710
+ # the inherited GIT_CONFIG_COUNT entries, which this environment keeps.
711
+ count = int(os.environ.get("GIT_CONFIG_COUNT") or 0)
709
712
  self.environment = mock.patch.dict(os.environ, {
710
713
  "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}", "FAKE_CARGO": str(self.fixture),
711
714
  "FAKE_CARGO_LOG": str(self.log), "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_NOSYSTEM": "1",
715
+ "GIT_CONFIG_COUNT": str(count + 1), f"GIT_CONFIG_KEY_{count}": "maintenance.auto",
716
+ f"GIT_CONFIG_VALUE_{count}": "false",
712
717
  "PYTHONPATH": str(self.root) + os.pathsep + os.environ.get("PYTHONPATH", ""),
713
718
  "PYTHONDONTWRITEBYTECODE": "1",
714
719
  "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t", "GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t"})
@@ -749,6 +754,14 @@ class WarmTests(unittest.TestCase):
749
754
  def git(self, work, *args):
750
755
  return subprocess.run(["git", "-C", str(work), *args], check=True, capture_output=True, text=True).stdout
751
756
 
757
+ def test_fixture_commit_starts_no_automatic_maintenance(self):
758
+ # Its detached child would still write into .git while a test reads the tree or cleanup removes it.
759
+ trace = self.root / "trace.json"
760
+ os.environ["GIT_TRACE2_EVENT"] = str(trace) # setUp's patch restores the environment
761
+ self.git(self.anchor, "commit", "-q", "--allow-empty", "-m", "traced")
762
+ children = [line for line in trace.read_text(encoding="utf-8").splitlines() if '"event":"child_start"' in line]
763
+ self.assertEqual(children, [])
764
+
752
765
  def write_fixture(self, **extra):
753
766
  packages = [
754
767
  {"name": "app", "source": None, "manifest_path": str(self.work / "Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}]},
@@ -59,6 +59,7 @@ Verify DEVLYN_SKILL_DIR/scripts/queue.py before running it. In omp, use `printf
59
59
  | `plan <intent or absolute document path>` | Inspect, elicit what is necessary and write a validated loop package. Does not enqueue or execute. |
60
60
  | `add <intent or absolute package path>` | Plan when necessary, then capture the package and queue its tasks in dependency order. Does not execute. |
61
61
  | `status` | Reconciled pending, active, accepted and failed counts; the next runnable task; delivery and recovery blockers. |
62
+ | `withdraw <loop-id>` | `queue.py withdraw --repo . <loop-id>` takes back a queued loop none of whose tasks has started and that replaced no legacy row. |
62
63
  | `drain` | Resume or drain the queue serially under existing execution authorization. |
63
64
  | Bare intent | Plan; when the request authorizes implementation, continue through add and drain without reconfirmation. |
64
65
  | No arguments | Status. |
@@ -104,7 +105,7 @@ For read-only consumers, `python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" inspect -
104
105
 
105
106
  ## drain
106
107
 
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.
108
+ `status`, `add`, `withdraw` 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.
108
109
 
109
110
  See [loop.md](references/loop.md) for an unclear result or `--worktree-root`. The executor is the configured route, the `.devlyn/engines.json` `executor` pin or else this CLI, checked per `_shared/engine-preflight.md`. Pass its argv after `--`. `<git dir>` is the output of `git rev-parse --path-format=absolute --git-common-dir`, where the executor commits and writes its submission; the drain fills `{worktree_git_dir}` with the task worktree's own Git directory, which Codex's sandbox keeps read-only unless that exact directory is listed:
110
111
 
@@ -113,7 +114,7 @@ See [loop.md](references/loop.md) for an unclear result or `--worktree-root`. Th
113
114
  | Claude | `claude -p "<prompt>" --dangerously-skip-permissions --add-dir "<git dir>"` (drain prepends `--settings <temporary-policy.json> --no-chrome` and any `--disallowedTools=<list>` denies after `claude`) |
114
115
  | Codex | `bash "<DEVLYN_SHARED_DIR>/codex-monitored.sh" --skip-git-repo-check -s workspace-write -c 'sandbox_workspace_write.writable_roots=["<git dir>","{worktree_git_dir}"]' -c 'sandbox_workspace_write.network_access=true' "<prompt>"` |
115
116
 
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:
117
+ `<prompt>` carries the absolute binding values, while `{packet}` stays literal for the drain to fill:
117
118
 
118
119
  ```text
119
120
  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, except a Devlyn managed block (devlyn:instructions markers), which those defaults replace; work only in its owned worktree under the installed instructions; meet its obligations, then write its submission and any review records as <DEVLYN_SKILL_DIR>/references/executor.md specifies. 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.
@@ -18,7 +18,7 @@ Both print one JSON object; exit 1 is `BLOCKED` with a `reason`.
18
18
 
19
19
  The host supplies the executor and maps its configured engine to the argv; the loop has no model preference. Each `{packet}` in the argv is replaced by the task packet path, and each `{worktree_git_dir}` by the task worktree's absolute Git directory. The executor runs with the task worktree as its working directory and the null device as stdin; its stdout and stderr are appended to `executor.stdout` and `executor.stderr` in the packet's `evidence_dir`, never a pipe.
20
20
 
21
- `<common Gitdir>/devlyn-loops/drain.lock` is held for the controller's lifetime, so a second drain from any worktree of the repository is refused. `queue.lock` is held only to read the add records and queue files, to add a loop and to make terminal transitions, so `add` works during a drain.
21
+ `<common Gitdir>/devlyn-loops/drain.lock` is held for the controller's lifetime, so a second drain from any worktree of the repository is refused. `queue.lock` is held only to read the add records and queue files, to add or withdraw a loop and to make terminal transitions, so `add` works during a drain.
22
22
 
23
23
  ## Steps
24
24
 
@@ -32,7 +32,7 @@ The host supplies the executor and maps its configured engine to the argv; the l
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.
35
- 9. **Deliver** after waiting for the executor, with `task-complete.py complete --receipt <receipt> --writers-stopped`, plus `--local-only` or, until the receipt records a mode, the packet's `--mode auto|pr`, so a per-task `--mode` persists. Local-only keeps source, custody and the recovery ref with no remote effect. `auto`/`pr` publish the terminal commit under the completion contract; a pending or refused delivery keeps product acceptance, resources and a resume command. Where writer cessation cannot be observed, or a process still uses the tree, a merged delivery settles (`COMPLETE`) with its workspace, task ref and custody retained and reported under workspace cleanup; retained cleanup never blocks the queue. Failed products are never published. The receipt's `delivery` field checkpoints the latest outcome. A local chain is never turned into stacked PRs; publishing it requires integrated acceptance of the actual publication candidate.
35
+ 9. **Deliver** after waiting for the executor, with `task-complete.py complete --receipt <receipt> --writers-stopped`, plus `--local-only` or, until the receipt records a mode, the packet's `--mode auto|pr`, so a per-task `--mode` persists. Local-only keeps source, custody and the recovery ref with no remote effect. `auto`/`pr` publish the terminal commit under the completion contract; a pending or refused delivery keeps product acceptance, resources and a resume command. A merged delivery whose cleanup is refused settles (`COMPLETE`) with what the refusal guards retained and reported under workspace cleanup; retained cleanup never blocks the queue. Failed products are never published. The receipt's `delivery` field checkpoints the latest outcome. A local chain is never turned into stacked PRs; publishing it requires integrated acceptance of the actual publication candidate.
36
36
  10. **Recover** from durable state only:
37
37
 
38
38
  | Interrupted | Resume |
@@ -515,6 +515,8 @@ class AcceptanceTests(unittest.TestCase):
515
515
  self.addCleanup(self.temp.cleanup)
516
516
  self.root = Path(self.temp.name).resolve()
517
517
  self.work = self.root / "work tree"
518
+ # A commit's detached automatic maintenance would still write into .git while a test reads the tree.
519
+ (self.root / "gitconfig").write_text("[maintenance]\n\tauto = false\n", encoding="utf-8")
518
520
  env = {"GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": str(self.root / "gitconfig"), "GIT_AUTHOR_NAME": "Fixture",
519
521
  "GIT_AUTHOR_EMAIL": "fixture@example.invalid", "GIT_COMMITTER_NAME": "Fixture", "GIT_COMMITTER_EMAIL": "fixture@example.invalid"}
520
522
  self.saved = {key: os.environ.get(key) for key in env}
@@ -543,6 +545,15 @@ class AcceptanceTests(unittest.TestCase):
543
545
  self.g("commit", "-q", "-m", message)
544
546
  return self.g("rev-parse", "HEAD")
545
547
 
548
+ def test_fixture_commit_starts_no_automatic_maintenance(self):
549
+ # Its detached child would still write into .git while a test reads the tree or cleanup removes it.
550
+ from unittest import mock
551
+ trace = self.root / "trace.json"
552
+ with mock.patch.dict(os.environ, GIT_TRACE2_EVENT=str(trace)):
553
+ self.g("commit", "-q", "--allow-empty", "-m", "traced")
554
+ children = [line for line in trace.read_text(encoding="utf-8").splitlines() if '"event":"child_start"' in line]
555
+ self.assertEqual(children, [])
556
+
546
557
  def inputs(self, expected=None, review=()):
547
558
  base = self.g("rev-parse", "HEAD")
548
559
  files = {"docs/specs/l/meta.md": "# meta\n", "docs/specs/l/t/spec.md": "# task R1 R2\n",