devlyn-cli 3.3.0 → 3.3.1

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 CHANGED
@@ -38,7 +38,7 @@ Each skill's `SKILL.md` is the source of truth for flags and workflow. Do not du
38
38
 
39
39
  Engine roles: orchestrator = whichever CLI the user opened (this contract is symmetric with CLAUDE.md. If you are Codex orchestrating `/devlyn:resolve`, the phase machinery is mandatory regardless of task size; if you will not run it, say so explicitly and stop — do not silently degrade to ad-hoc execution); legacy executor (with optional separate worker/primary/pair profiles) uses the canonical skill's orchestrator-supported default, overridable per run with `--engine <name>` or durably via machine-local `.devlyn/engines.json` `{"executor": "<name>", "pair_judge_priority": ["<name>", ...]}`; PLAN is orchestrator-fixed and never inherits `--engine` or the executor pin; pair judge = first available OTHER engine, default for VERIFY whenever available; risk probes remain conditional and --no-pair opts out. Pins fail closed (`BLOCKED:<engine>-unavailable` / `BLOCKED:invalid-engine-config`); new engines plug in by shipping `_shared/adapters/<name>.md`. `/devlyn:engines` (no args) shows the role table + detected engines; `executor <name>` / `pair <name>,...` / `clear` manage the pins. The executor pin binds the orchestrator in plain conversation too, not only inside a skill run: when you would implement directly and executor is pinned to a non-default engine, route that work through the pin — via `/devlyn:resolve` (reads it at PHASE 0) or by delegating to that engine; no pin / no file → use the canonical skill's orchestrator-supported default. The pair-judge pin stays pipeline-scoped. `/devlyn:engines role` configures explicit engine/model/effort for supported worker and judge routes; `/devlyn:resolve --role-config <path>` overrides roles for one run. Canonical skills define precedence, evidence and unsupported-route errors; absent profiles preserve existing defaults.
40
40
 
41
- Conversational entry: Before loading a workflow or writing files, inspect the requested change and relevant callers/tests. Quoted phase names, past logs and skill-file paths are context, not an instruction to invoke that workflow; follow the current user request. Default to direct execution when inspection makes the requested behavior, affected boundaries and verification tractable in the current context, including bounded multi-file work. Preserve requested behavior, scope, explicit constraints and the executor pin; make scoped edits, run required checks, review the final diff, and report changes with evidence. One concise route/check explanation suffices; ordinary direct work needs no spec, pipeline state, phase workers or extra approval. For direct/full/queue work, the outer owner reads `references/task-completion.md` relative to the installed `devlyn:resolve` skill's directory: prospective task-branch ownership (own linked worktree), scoped commit acceptance, then push + PR after required archive/queue commits. Local-only/no-push wins; delivery pending/failure stays separate from product verification.
41
+ Conversational entry: Before loading a workflow or writing files, inspect the requested change and relevant callers/tests. Quoted phase names, past logs and skill-file paths are context, not an instruction to invoke that workflow; follow the current user request. Default to direct execution when inspection makes the requested behavior, affected boundaries and verification tractable in the current context, including bounded multi-file work. Preserve requested behavior, scope, explicit constraints and the executor pin; make scoped edits, run required checks, review the final diff, and report changes with evidence. One concise route/check explanation suffices; ordinary direct work needs no spec, pipeline state, phase workers or extra approval. For direct/full/queue work, the outer owner reads `references/task-completion.md` relative to the installed `devlyn:resolve` skill's directory: prospective task-branch ownership (own linked worktree), scoped commit acceptance, then PR/merge and recoverable owned-resource cleanup after required archive/queue commits. Local-only/no-push wins; delivery pending/failure stays separate from product verification.
42
42
 
43
43
  Use full `/devlyn:resolve` automatically only when inspection identifies interacting requirements or verification work too complex to manage reliably in the current context, such as coupled durable state, concurrent ownership and failure recovery across boundaries. Name the concrete interaction or verification gap. Domain labels (security/auth/payment/persistence/concurrency/API), file count and a spec document alone do not trigger it. Investigate or clarify missing intent/access first; a pipeline cannot supply them. Direct work still requires risk-proportionate checks and independent review for consequential changes. Explicit resolve (even small), explicit spec-mode workflows and queue drains retain all canonical phases, independent verification, pins and failure handling. If the full-route condition emerges mid-edit, carry the existing delta into its scope/evidence; do not reset the baseline or reconfirm existing authorization.
44
44
 
package/CLAUDE.md CHANGED
@@ -51,7 +51,7 @@ Each skill's `SKILL.md` is the source of truth for its flags and workflow — do
51
51
 
52
52
  ### Conversational entry + full-route handoff
53
53
 
54
- Before loading a workflow or writing files, inspect the requested change and relevant callers/tests. Quoted phase names, past logs and skill-file paths are context, not an instruction to invoke that workflow; follow the current user request. Default to direct execution when inspection makes the requested behavior, affected boundaries and verification tractable in the current context, including bounded multi-file work. Preserve requested behavior, scope, explicit constraints and the executor pin; make scoped edits, run required checks, review the final diff, and report changes with evidence. One concise route/check explanation suffices; ordinary direct work needs no spec, pipeline state, phase workers or extra approval. For direct/full/queue work, the outer owner reads `references/task-completion.md` relative to the installed `devlyn:resolve` skill's directory: prospective task-branch ownership (own linked worktree), scoped commit acceptance, then push + PR after required archive/queue commits. Local-only/no-push wins; delivery pending/failure stays separate from product verification.
54
+ Before loading a workflow or writing files, inspect the requested change and relevant callers/tests. Quoted phase names, past logs and skill-file paths are context, not an instruction to invoke that workflow; follow the current user request. Default to direct execution when inspection makes the requested behavior, affected boundaries and verification tractable in the current context, including bounded multi-file work. Preserve requested behavior, scope, explicit constraints and the executor pin; make scoped edits, run required checks, review the final diff, and report changes with evidence. One concise route/check explanation suffices; ordinary direct work needs no spec, pipeline state, phase workers or extra approval. For direct/full/queue work, the outer owner reads `references/task-completion.md` relative to the installed `devlyn:resolve` skill's directory: prospective task-branch ownership (own linked worktree), scoped commit acceptance, then PR/merge and recoverable owned-resource cleanup after required archive/queue commits. Local-only/no-push wins; delivery pending/failure stays separate from product verification.
55
55
 
56
56
  Use full `/devlyn:resolve` automatically only when inspection identifies interacting requirements or verification work too complex to manage reliably in the current context, such as coupled durable state, concurrent ownership and failure recovery across boundaries. Name the concrete interaction or verification gap. Domain labels (security/auth/payment/persistence/concurrency/API), file count and a spec document alone do not trigger it. Investigate or clarify missing intent/access first; a pipeline cannot supply them. Direct work still requires risk-proportionate checks and independent review for consequential changes. Explicit resolve (even small), explicit spec-mode workflows and queue drains retain all canonical phases, independent verification, pins and failure handling. If the full-route condition emerges mid-edit, carry the existing delta into its scope/evidence; do not reset the baseline or reconfirm existing authorization.
57
57
 
package/README.md CHANGED
@@ -84,10 +84,10 @@ PLAN → IMPLEMENT → BUILD_GATE → CLEANUP → VERIFY (fresh subagent
84
84
  Common flags: `--engine claude|codex|omp` (default: the orchestrator-supported default), `--role-config <path>` (one-run worker/judge profiles, see below), `--bypass build-gate,cleanup`, `--pair-verify` (force pair-mode JUDGE in VERIFY), `--no-pair` (intentional solo VERIFY), `--risk-probes` / `--no-risk-probes`, `--perf` (per-phase timing).
85
85
  `--pair-verify` and `--no-pair` are mutually exclusive; using both stops with `BLOCKED:invalid-flags`.
86
86
 
87
- Each task gets its own linked worktree; accepted tasks default to scoped commit → push → PR.
88
- After the PR merges, either mode cleans the worktree and branch its session
89
- released; anything in use is kept. Set `git config --local devlyn.completionMode auto` to
90
- also request a normal protected merge; `task-complete.py complete --mode auto|pr`
87
+ Each task gets its own linked worktree; accepted tasks default to scoped commit → push → PR
88
+ → merge when the repository allows it (otherwise the PR waits for a person). After the
89
+ PR merges, the worktree and branch its session released are cleaned; anything in use is
90
+ kept. Set `git config --local devlyn.completionMode pr` to stop at the PR; `task-complete.py complete --mode auto|pr`
91
91
  overrides one task. Local-only/no-push instructions take precedence; existing
92
92
  branches cannot be adopted. Full runs require successful archive before delivery;
93
93
  direct tasks use their actual checks and root acceptance. Verify-only never
@@ -222,6 +222,7 @@
222
222
  "9f62c5b2a94e8b5e81528ccbdb6f78d147d14882108568b2cb3b7f85212694c0",
223
223
  "a02eb2d8d77553327fb77377dbfedeab059b2e68e0a33c469b9be54a5d55182b",
224
224
  "a3a15249968f64085b0cec8c0826688a4c3a1e4cf09b4879054baa086d5cbca1",
225
+ "a4da26a46ab83815d8ca5d747c68a907ad67dad485adbc6c863b982ee23b307e",
225
226
  "a5d6ff585d696254d6727cd1e6d7f8825881f90210eff744265e211ced715ea3",
226
227
  "a7ff913df40945a8a16b6632214020a368119ff665953d4a07ec58330b39f101",
227
228
  "aafc680a7ac73e100f4a17d2c4070190e68563f78614c39f4c2f58f1e291d9a9",
@@ -775,6 +776,7 @@
775
776
  "e29fec7d54a144ea4867a3764fccdee6849d58d0f32eacdd56b2c317863fa1c6",
776
777
  "e2e0fc585321ee2013cccaed95d47c49fbca71b54c67abad9080d19d70e3c1a5",
777
778
  "e3b2f7be15a793f07cbe8e51aa2feeac51d4c67077b94e5e20bad2e14be07fc5",
779
+ "e468c242cc38c390f9679fca61f355848f58d51570249dfbb65b4f2e26a9dfb1",
778
780
  "e4bafa0f553e899b65f05feabf4cbe86d8246c57cbba745eb79e6aa858f40a6b",
779
781
  "e5160f4da789914e7f93694a55b9f80b8d06c3b0a126ff60f7627b0b418ae292",
780
782
  "e55a62fa1a993f5df1b44d1c55ef27915938d58a1c357604f3dc150f795672df",
@@ -172,7 +172,7 @@ def policy(receipt, override):
172
172
  require(config.returncode == 1 or config.stdout in {"auto\n", "pr\n"}, "invalid local devlyn.completionMode; use auto|pr")
173
173
  value = config.stdout.strip() if config.returncode == 0 else None
174
174
  require(override is None or override in {"auto", "pr"}, "invalid task mode; use auto|pr")
175
- return override or value or "pr"
175
+ return override or value or "auto"
176
176
 
177
177
 
178
178
  def allocate(args):
@@ -685,8 +685,6 @@ def complete(args):
685
685
  inspect_workspace(current)
686
686
  bind_acceptance(receipt, path, args.acceptance)
687
687
  info = repo_policy(receipt)
688
- if mode == "auto":
689
- require(info["mergeCommitAllowed"], "repository disallows merge commits; use pr mode or resolve policy with repository owner")
690
688
  if receipt.get("pr_number"):
691
689
  pr = json.loads(gh(receipt, "pr", "view", str(receipt["pr_number"]), "--json", PR_FIELDS))
692
690
  else:
@@ -725,12 +723,18 @@ def complete(args):
725
723
  inspect_workspace(receipt)
726
724
  verify_files(Path(receipt["worktree"]), receipt["files"])
727
725
  require(remote_head(receipt, receipt["branch"]) == receipt["publish_sha"], "remote head race before merge")
726
+ refused = None
728
727
  if not pr.get("autoMergeRequest"):
729
- gh(receipt, "pr", "merge", str(pr["number"]), "--auto", "--merge", "--match-head-commit", receipt["publish_sha"])
728
+ try:
729
+ require(info["mergeCommitAllowed"], "repository disallows merge commits")
730
+ gh(receipt, "pr", "merge", str(pr["number"]), "--auto", "--merge", "--match-head-commit", receipt["publish_sha"])
731
+ except CompletionError as error:
732
+ refused = str(error)
730
733
  pr = json.loads(gh(receipt, "pr", "view", str(pr["number"]), "--json", PR_FIELDS))
731
734
  validate_pr(receipt, pr)
732
735
  if pr["state"] != "MERGED":
733
- return result("PENDING")
736
+ # Merge only when the repository allows it; a refused request leaves the PR for a person.
737
+ return dict(result("PR"), merge_refused=refused) if refused and not pr.get("autoMergeRequest") else result("PENDING")
734
738
  receipt["merge"] = pr
735
739
  atomic_json(path, receipt)
736
740
  cleanup(receipt, path, pr)
@@ -1242,7 +1246,7 @@ class CompletionTests(unittest.TestCase):
1242
1246
  self.g("config", "url." + alias + ".insteadOf", intended)
1243
1247
  self.allocate(); self.accept()
1244
1248
  self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["remote_url"], {"literal": intended, "fetch": alias, "push": alias})
1245
- result, _ = self.complete()
1249
+ result, _ = self.complete("--mode", "pr")
1246
1250
  self.assertEqual(result["status"], "PR")
1247
1251
  self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], self.sha)
1248
1252
  d = json.loads(self.data.read_text(encoding="utf-8"))
@@ -1374,6 +1378,15 @@ class CompletionTests(unittest.TestCase):
1374
1378
  self.assertEqual(result["status"], "COMPLETE")
1375
1379
  self.assertFalse(self.task.exists())
1376
1380
 
1381
+ @unittest.skipUnless(sys.platform == "darwin" or sys.platform.startswith("linux"), "writer observation requires POSIX")
1382
+ def test_default_mode_merges_when_possible_and_cleans(self):
1383
+ self.allocate(); self.accept()
1384
+ result, _ = self.complete("--writers-stopped")
1385
+ self.assertEqual(result["status"], "COMPLETE")
1386
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("merges"), 1)
1387
+ self.assertFalse(self.task.exists())
1388
+ self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
1389
+
1377
1390
  def test_release_is_refused_from_inside_the_task_tree(self):
1378
1391
  self.allocate(); self.accept()
1379
1392
  _, r = self.complete("--mode", "pr", "--writers-stopped", cwd=self.task, success=False)
@@ -1710,10 +1723,12 @@ class CompletionTests(unittest.TestCase):
1710
1723
 
1711
1724
  def test_interrupted_external_effects(self):
1712
1725
  self.allocate(); self.accept()
1713
- for effect in ("push", "create", "merge"):
1726
+ for effect in ("push", "create"):
1714
1727
  self.configure(**{"interrupt_"+effect: True})
1715
1728
  _, r = self.complete("--writers-stopped", "--mode", "auto", success=False)
1716
1729
  self.assertNotEqual(r.returncode, 0)
1730
+ # A merge command that fails after merging is judged by the PR state, not its exit code.
1731
+ self.configure(interrupt_merge=True)
1717
1732
  result, _ = self.complete("--writers-stopped")
1718
1733
  self.assertEqual(result["status"], "COMPLETE")
1719
1734
  d = json.loads(self.data.read_text(encoding="utf-8"))
@@ -1801,16 +1816,19 @@ class CompletionTests(unittest.TestCase):
1801
1816
  def test_policy_and_local_only_persist(self):
1802
1817
  self.allocate(); self.accept()
1803
1818
  self.configure(merge_allowed=False)
1804
- _, r = self.complete("--mode", "auto", success=False)
1805
- self.assertNotEqual(r.returncode, 0)
1806
- self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("pushs",0), 0)
1819
+ result, _ = self.complete()
1820
+ self.assertEqual((result["status"], result["merge_refused"]), ("PR", "repository disallows merge commits"))
1821
+ d = json.loads(self.data.read_text(encoding="utf-8"))
1822
+ self.assertEqual((d.get("pushs", 0), d.get("merges", 0)), (1, 0))
1807
1823
  self.complete("--mode", "pr")
1808
1824
  result, _ = self.complete()
1809
1825
  self.assertEqual(result["status"], "PR")
1826
+ self.assertNotIn("merge_refused", result)
1810
1827
  self.configure(merge_allowed=True, auto_allowed=False, pending=True)
1811
- _, r = self.complete("--mode", "auto", success=False)
1812
- self.assertNotEqual(r.returncode, 0)
1813
- self.assertIn("disallows auto merge", json.loads(r.stdout)["reason"])
1828
+ result, _ = self.complete("--mode", "auto")
1829
+ self.assertEqual(result["status"], "PR")
1830
+ self.assertIn("disallows auto merge", result["merge_refused"])
1831
+ self.assertTrue(self.task.exists())
1814
1832
  result, _ = self.complete("--no-push")
1815
1833
  self.assertEqual(result["status"], "LOCAL_ONLY")
1816
1834
  result, _ = self.complete(acceptance=False)
@@ -101,19 +101,19 @@ python3 "$DEVLYN_SHARED_DIR/task-complete.py" complete \
101
101
  ```
102
102
 
103
103
  Project policy is `git config --local devlyn.completionMode auto|pr`; absent means
104
- `pr`. Invalid local values fail before external effects, even with an override.
104
+ `auto`. Invalid local values fail before external effects, even with an override.
105
105
  Use `complete --mode auto|pr` for one task, or `--local-only`/`--no-push` to honor
106
106
  the user's delivery restriction. These per-task choices persist in the receipt.
107
107
 
108
108
  `pr` pushes the exact accepted task ref and creates/reuses its exact repository,
109
109
  head and base PR. While OPEN, it cancels owned auto-merge, reports `PR` with its URL
110
110
  and retains the checkout. Once MERGED by any method, either mode cleans released
111
- resources in that call, on resume or at the next allocation. Opt-in `auto` also requests
111
+ resources in that call, on resume or at the next allocation. Default `auto` also requests
112
112
  `gh pr merge --auto --merge --match-head-commit <accepted SHA>`. Repository
113
113
  checks/reviews and merge policy remain authoritative: no admin bypass, strategy
114
- fallback or repository setting changes. A repository that disallows
115
- merge commits fails visibly; disabled auto merge may also reject a pending
116
- request. Command success alone never proves merge.
114
+ fallback or repository setting changes. When the repository disallows merge
115
+ commits or refuses the request, delivery reports `PR` with `merge_refused` and
116
+ the PR waits for a person. Command success alone never proves merge.
117
117
 
118
118
  `PENDING` retains the workspace and reports a receipt-based resume command.
119
119
  `BLOCKED` includes the cause and the same retry path; repair the reported condition
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn-cli",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
4
4
  "description": "Loop & harness engineering for AI coding agents (Claude Code, Codex, and any AGENTS.md-driven agent) — take intent to shipped, engineer-quality software hands-free with phase-gated pipelines and agent orchestration",
5
5
  "homepage": "https://github.com/fysoul17/devlyn-cli#readme",
6
6
  "bin": {