devlyn-cli 4.2.21 → 4.2.23

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.
@@ -238,6 +238,7 @@
238
238
  "6d55968e33c5b5d6e3a7efd67319584a16cd2ed3121bca9b5337f2abfd583566",
239
239
  "6dcd1ec9b76ba8cdb6af964b9eb6bb9e7a1e57b9a050501638d7f99b83ee878a",
240
240
  "6e32040ced878f6cd2a2fb3848d1b7ff43020244442b3f84349ccc063c96d191",
241
+ "6ea40bc01fa865ef84f4d6aa55178a40f815dd8c58c38b62d06b7dccc3200198",
241
242
  "6eda1f1ba7cfa0773c7248f51b449014ecc2d954b0dbb8e06d2e0476f41548fc",
242
243
  "6ee5b6030d798abf42b9cfc72a181c94215a1f5c219538a502d926996b1f8fef",
243
244
  "6f04a962b5ea29922a81fd4ac5402474a6c1320c2f660ce6c80837f0367575e7",
@@ -381,6 +382,7 @@
381
382
  "e3ee2da50e961046da3f5b68b0c328570307fb21a93424ae22536b8f216b55a3",
382
383
  "e43e4b5b86044fbf7be8d7fd1e3e27cd2089bfc8243e07dc2c3fc034c19794ea",
383
384
  "e44015948c7d04e60f3fed7e348c2052538e7784d8fe7ea3edf9f52de07448dd",
385
+ "e4ffeb63061fdd0c2332d1d793349d6ddeedee73791b46cb3c57b16ed0fe11ae",
384
386
  "e54b2aa51360267b8f397fb0d902e13af7ae1e3a9194dc7b83f8011fc1174f9d",
385
387
  "e562f976c71f5e66f5a55c3eb0fc18a0aa5e6b443d41478117e6b58d58b7bcd1",
386
388
  "e60304667adad561168f4d49a6ace28e6dee00458824a18934a7fbadcc9ffe87",
@@ -681,6 +683,7 @@
681
683
  "6d56b772caa15cfa6346d4f22753dbc908fdf15f18a9a5adbdba689330ba3862",
682
684
  "6da7471bca36b3cd82bac8d573b0f366c0f79b13aeb567f7b21035cdea665c08",
683
685
  "6e65a2349808a10c71f91167d867e1a1bf08637bd9e2be5daadb75342283ce8d",
686
+ "6ea40bc01fa865ef84f4d6aa55178a40f815dd8c58c38b62d06b7dccc3200198",
684
687
  "7076c6738e6d3e2739680f57c9ab3c7522ec0b9ace00b663619bcd7370ce570a",
685
688
  "7077c653a094715491e5677609a83b55ead0ad0ad6e123e80fe1ecb85bb41b6b",
686
689
  "70d3765d35b64e632cb8eeff7865f840a1a94f973f88f1133fc5fad3b36437f0",
@@ -927,6 +930,7 @@
927
930
  "e3b2f7be15a793f07cbe8e51aa2feeac51d4c67077b94e5e20bad2e14be07fc5",
928
931
  "e468c242cc38c390f9679fca61f355848f58d51570249dfbb65b4f2e26a9dfb1",
929
932
  "e4bafa0f553e899b65f05feabf4cbe86d8246c57cbba745eb79e6aa858f40a6b",
933
+ "e4ffeb63061fdd0c2332d1d793349d6ddeedee73791b46cb3c57b16ed0fe11ae",
930
934
  "e5160f4da789914e7f93694a55b9f80b8d06c3b0a126ff60f7627b0b418ae292",
931
935
  "e55a62fa1a993f5df1b44d1c55ef27915938d58a1c357604f3dc150f795672df",
932
936
  "e5aac9117f5af3491245b785bbeeae02ff2068727d73fd11fda3db9e4f99c86f",
@@ -8,6 +8,7 @@
8
8
  "06ab3a4b9e1c97d9b3dcd238148d775f1165f3fc08898af2f3b76149162b78d3",
9
9
  "082124278a538e4ea8b6622d767f996e3d818ec85d6e0d8ddddafe9bb03118b2",
10
10
  "08b96c407cb56b0a0242b3918e1618b21eca06caf15b8a1eec00aee6b023e08e",
11
+ "08c91060cfec04518de582599d1892ad50f6e6cec69aecd8678760c3e0039fa0",
11
12
  "0a60dd9b96442be24a571dd5fe6cac426bba7dd7a06ad17f3211f4f9fd9b1741",
12
13
  "0b5c3f72d18f8eea538bfef7192b207ff5cbac25c7e0d2ce080cc725bfdaa2e2",
13
14
  "0f397a4edd6b1b3063d27f6205dc6b3d069fa616624bccf661539b174535fc7e",
@@ -100,6 +101,7 @@
100
101
  "978e4210e49b42827c98cf6e40c868c87dc6e3533a2b779ca2fa05abfc16ad0f",
101
102
  "9815b6e6963e068872a6dbeae24068143bf92042bb035f67bafd1a224d65bcb0",
102
103
  "998343c148618b937556b58b83749c05d5aa3283280d751c118da6a542fb87f2",
104
+ "9dd3ec225818efe0ad3726192db94ff129154e995617a9d3c150fc58ad0f9f2c",
103
105
  "9f14ca629e41730b1a9fb5d0ad1a39cff558fe34758d6ceccbb9de498f9f384e",
104
106
  "a11950a5880346f03257c688fd8bc6f8c29fc0b7c05c90e8f5211d5fb7665774",
105
107
  "a405a99b988f6edd2eabb0504b36309bb4cf0c3b97eab26bb7ef257e4515dbf3",
@@ -21,6 +21,7 @@ import stat
21
21
  import subprocess
22
22
  import sys
23
23
  import tempfile
24
+ import time
24
25
  import unittest
25
26
  import uuid
26
27
 
@@ -789,11 +790,35 @@ def complete(args):
789
790
  require(remote_head(receipt, receipt["branch"]) == receipt["publish_sha"], "remote head race before merge")
790
791
  refused = None
791
792
  if not pr.get("autoMergeRequest"):
792
- try:
793
- require(info["mergeCommitAllowed"], "repository disallows merge commits")
794
- gh(receipt, "pr", "merge", str(pr["number"]), "--auto", "--merge", "--match-head-commit", receipt["publish_sha"])
795
- except CompletionError as error:
796
- refused = str(error)
793
+ if not info["mergeCommitAllowed"]:
794
+ refused = "repository disallows merge commits"
795
+ else:
796
+ owner, name = receipt["repository"].split("/")
797
+ auto_allowed = json.loads(command([
798
+ "gh", "api", "graphql", "--hostname", "github.com", "-f",
799
+ "query=query($owner:String!,$name:String!){repository(owner:$owner,name:$name){autoMergeAllowed}}",
800
+ "-f", "owner="+owner, "-f", "name="+name,
801
+ "--jq", ".data.repository.autoMergeAllowed | tojson",
802
+ ]))
803
+ require(isinstance(auto_allowed, bool), "repository autoMergeAllowed must be a JSON boolean")
804
+ auto = ["--auto"] if auto_allowed else []
805
+ if not auto:
806
+ # UNKNOWN can reach gh's direct merge mutation; wait for a
807
+ # resolved state and permit only gh's immediate-merge set.
808
+ for attempt in range(10):
809
+ state = json.loads(gh(receipt, "pr", "view", str(pr["number"]), "--json", "mergeStateStatus"))["mergeStateStatus"]
810
+ require(isinstance(state, str) and state, "invalid mergeStateStatus")
811
+ if state != "UNKNOWN":
812
+ break
813
+ if attempt < 9:
814
+ time.sleep(2)
815
+ if state not in {"CLEAN", "HAS_HOOKS", "UNSTABLE"}:
816
+ refused = "pull request is not mergeable: " + state
817
+ if not refused:
818
+ try:
819
+ gh(receipt, "pr", "merge", str(pr["number"]), *auto, "--merge", "--match-head-commit", receipt["publish_sha"])
820
+ except CompletionError as error:
821
+ refused = str(error)
797
822
  pr = json.loads(gh(receipt, "pr", "view", str(pr["number"]), "--json", PR_FIELDS))
798
823
  validate_pr(receipt, pr)
799
824
  if pr["state"] != "MERGED":
@@ -885,6 +910,16 @@ def remote(ref):
885
910
  if a[:2] == ['repo', 'view']:
886
911
  assert a[2:] == ['github.com/test/project', '--json', 'nameWithOwner,url,defaultBranchRef,mergeCommitAllowed'], 'unsupported gh repo view arguments: '+str(a)
887
912
  print(json.dumps({'nameWithOwner':'test/project', 'url':'https://github.com/test/project', 'defaultBranchRef':{'name':d.get('default_branch','main')}, 'mergeCommitAllowed':d.get('merge_allowed',True)}))
913
+ elif a[:1] == ['api']:
914
+ assert a == ['api', 'graphql', '--hostname', 'github.com', '-f',
915
+ 'query=query($owner:String!,$name:String!){repository(owner:$owner,name:$name){autoMergeAllowed}}',
916
+ '-f', 'owner=test', '-f', 'name=project',
917
+ '--jq', '.data.repository.autoMergeAllowed | tojson'], 'unsupported gh api arguments: '+str(a)
918
+ d['auto_setting_reads'] = d.get('auto_setting_reads',0)+1
919
+ save()
920
+ if d.get('auto_setting_error'):
921
+ print('auto-merge setting read failed',file=sys.stderr); sys.exit(1)
922
+ print(d.get('auto_setting_output', json.dumps(d.get('auto_allowed',True))))
888
923
  elif a[:2] == ['pr', 'list']:
889
924
  assert '--repo' in a and a[a.index('--repo')+1] == 'github.com/test/project'
890
925
  print(json.dumps([pr for pr in prs if pr['headRefName'] == a[a.index('--head')+1]]))
@@ -898,21 +933,42 @@ elif a[:2] == ['pr', 'create']:
898
933
  print(url)
899
934
  if d.pop('interrupt_create',False): save(); sys.exit(1)
900
935
  elif a[:2] == ['pr', 'view']:
901
- print(json.dumps(prs[int(a[2])-1]))
936
+ pr = prs[int(a[2])-1]
937
+ fields = a[4]
938
+ assert a[3:] == ['--json', fields, '--repo', 'github.com/test/project']
939
+ assert fields in ('mergeStateStatus', 'number,url,headRefName,baseRefName,headRefOid,headRepository,headRepositoryOwner,isCrossRepository,state,mergedAt,mergeCommit,autoMergeRequest')
940
+ if fields == 'mergeStateStatus':
941
+ if d.get('merge_state_error'):
942
+ print('merge state read failed',file=sys.stderr); sys.exit(1)
943
+ if d.get('merge_states'):
944
+ d['merge_state'] = d['merge_states'].pop(0)
945
+ state = d.get('merge_state', 'BLOCKED' if d.get('pending') else 'CLEAN')
946
+ d.setdefault('merge_state_views', []).append(state)
947
+ save()
948
+ print(json.dumps({'mergeStateStatus': state}))
949
+ else:
950
+ print(json.dumps(pr))
902
951
  elif a[:2] == ['pr', 'merge'] and '--disable-auto' in a:
903
952
  d['disables'] = d.get('disables',0)+1
904
953
  prs[int(a[2])-1]['autoMergeRequest'] = None
905
954
  save()
906
955
  elif a[:2] == ['pr', 'merge']:
907
- assert '--admin' not in a and '--delete-branch' not in a
908
- assert '--auto' in a and '--merge' in a and '--match-head-commit' in a
909
956
  pr = prs[int(a[2])-1]
910
957
  sha = a[a.index('--match-head-commit')+1]
958
+ auto = ['--auto'] if '--auto' in a else []
959
+ assert a[3:] == auto + ['--merge', '--match-head-commit', sha, '--repo', 'github.com/test/project']
911
960
  assert sha == remote('refs/heads/'+pr['headRefName'])
912
961
  d['merges'] = d.get('merges',0)+1
913
- if not d.get('auto_allowed',True) and d.get('pending'):
962
+ d.setdefault('merge_calls', []).append(a)
963
+ state = d.get('merge_state', 'BLOCKED' if d.get('pending') else 'CLEAN')
964
+ immediately_mergeable = state in {'CLEAN', 'HAS_HOOKS', 'UNSTABLE'}
965
+ if auto and not immediately_mergeable and not d.get('auto_allowed',True):
914
966
  save(); print('repository disallows auto merge',file=sys.stderr); sys.exit(1)
915
- if d.get('pending'):
967
+ if d.get('merge_error'):
968
+ save(); print('merge attempt refused',file=sys.stderr); sys.exit(1)
969
+ if not auto and not immediately_mergeable and state != 'UNKNOWN':
970
+ save(); print('pull request is not mergeable: '+state,file=sys.stderr); sys.exit(1)
971
+ if auto and not immediately_mergeable:
916
972
  pr['autoMergeRequest'] = {'enabledAt':'now'}
917
973
  else:
918
974
  g = [os.environ['REAL_GIT'],'--git-dir',d['bare']]
@@ -1042,7 +1098,7 @@ class CompletionTests(unittest.TestCase):
1042
1098
 
1043
1099
  def merge_pr(self):
1044
1100
  self.configure(pending=False)
1045
- self.run_cmd(["gh", "pr", "merge", "1", "--auto", "--merge", "--match-head-commit", self.sha])
1101
+ self.run_cmd(["gh", "pr", "merge", "1", "--auto", "--merge", "--match-head-commit", self.sha, "--repo", "github.com/test/project"])
1046
1102
 
1047
1103
  def anchor_state(self, work=None):
1048
1104
  work = work or self.work
@@ -2119,6 +2175,127 @@ ctypes.CDLL(None).pthread_exit(None)
2119
2175
  self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture").split()[0], race)
2120
2176
  self.assertNotEqual(self.g("branch", "--list", "task/fixture"), "")
2121
2177
 
2178
+ def test_auto_disallowed_unknown_then_clean_merges_directly(self):
2179
+ self.allocate(); self.accept()
2180
+ self.configure(auto_allowed=False, merge_states=["UNKNOWN", "UNKNOWN", "CLEAN"])
2181
+ result, _ = self.complete("--mode", "auto", "--writers-stopped")
2182
+ self.assertEqual(result["status"], "COMPLETE")
2183
+ d = json.loads(self.data.read_text(encoding="utf-8"))
2184
+ self.assertEqual(d["merge_state_views"], ["UNKNOWN", "UNKNOWN", "CLEAN"])
2185
+ self.assertEqual(d["merge_calls"], [["pr", "merge", "1", "--merge", "--match-head-commit", self.sha,
2186
+ "--repo", "github.com/test/project"]])
2187
+ self.assertEqual(d["prs"][0]["state"], "MERGED")
2188
+ self.assertIsNone(d["prs"][0]["autoMergeRequest"])
2189
+
2190
+ def test_auto_disallowed_blocked_refuses_without_merge(self):
2191
+ self.allocate(); self.accept()
2192
+ for state in ("BLOCKED", "BEHIND", "DIRTY", "DRAFT", "NEW_STATE"):
2193
+ with self.subTest(state=state):
2194
+ self.configure(auto_allowed=False, merge_state=state)
2195
+ result, _ = self.complete("--mode", "auto")
2196
+ self.assertEqual(result["status"], "PR")
2197
+ self.assertIn("not mergeable: " + state, result["merge_refused"])
2198
+ d = json.loads(self.data.read_text(encoding="utf-8"))
2199
+ self.assertEqual(d["merge_state_views"], ["BLOCKED", "BEHIND", "DIRTY", "DRAFT", "NEW_STATE"])
2200
+ self.assertEqual(d.get("merge_calls", []), [])
2201
+ self.assertEqual(d["prs"][0]["state"], "OPEN")
2202
+ self.assertIsNone(d["prs"][0]["autoMergeRequest"])
2203
+ self.assertTrue(self.task.exists())
2204
+
2205
+ def test_auto_allowed_unknown_keeps_auto_pending(self):
2206
+ self.allocate(); self.accept()
2207
+ self.configure(auto_allowed=True, merge_state="UNKNOWN")
2208
+ result, _ = self.complete("--mode", "auto")
2209
+ self.assertEqual(result["status"], "PENDING")
2210
+ self.assertNotIn("merge_refused", result)
2211
+ d = json.loads(self.data.read_text(encoding="utf-8"))
2212
+ self.assertEqual(d.get("merge_state_views", []), [])
2213
+ self.assertEqual(d["merge_calls"], [["pr", "merge", "1", "--auto", "--merge", "--match-head-commit", self.sha,
2214
+ "--repo", "github.com/test/project"]])
2215
+ self.assertEqual(d["prs"][0]["state"], "OPEN")
2216
+ self.assertTrue(d["prs"][0]["autoMergeRequest"])
2217
+ self.assertTrue(self.task.exists())
2218
+ self.configure(auto_setting_error=True)
2219
+ result, _ = self.complete()
2220
+ self.assertEqual(result["status"], "PENDING")
2221
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8"))["auto_setting_reads"], 1)
2222
+
2223
+ def test_auto_disallowed_unknown_bound_refuses_without_merge(self):
2224
+ self.allocate(); self.accept()
2225
+ self.configure(auto_allowed=False, merge_state="UNKNOWN")
2226
+ result, _ = self.complete("--mode", "auto")
2227
+ self.assertEqual(result["status"], "PR")
2228
+ self.assertIn("not mergeable: UNKNOWN", result["merge_refused"])
2229
+ d = json.loads(self.data.read_text(encoding="utf-8"))
2230
+ self.assertEqual(d["merge_state_views"], ["UNKNOWN"] * 10)
2231
+ self.assertEqual(d.get("merge_calls", []), [])
2232
+ self.assertEqual(d["prs"][0]["state"], "OPEN")
2233
+ self.assertIsNone(d["prs"][0]["autoMergeRequest"])
2234
+ self.assertTrue(self.task.exists())
2235
+
2236
+ def test_auto_disallowed_other_immediate_states_attempt_direct_merge(self):
2237
+ self.allocate(); self.accept()
2238
+ for state in ("HAS_HOOKS", "UNSTABLE"):
2239
+ with self.subTest(state=state):
2240
+ self.configure(auto_allowed=False, merge_state=state, merge_error=True, merge_calls=[])
2241
+ result, _ = self.complete("--mode", "auto")
2242
+ self.assertEqual(result["status"], "PR")
2243
+ self.assertIn("merge attempt refused", result["merge_refused"])
2244
+ d = json.loads(self.data.read_text(encoding="utf-8"))
2245
+ self.assertEqual(d["merge_calls"], [["pr", "merge", "1", "--merge",
2246
+ "--match-head-commit", self.sha, "--repo", "github.com/test/project"]])
2247
+
2248
+ def test_auto_setting_read_failure_or_invalid_value_blocks_with_resume(self):
2249
+ self.allocate(); self.accept()
2250
+ for values in ({"auto_setting_error": True}, *({"auto_setting_error": False, "auto_setting_output": output}
2251
+ for output in ('null', '"true"', '"false"', '0', '1', '{}', '[]', '', 'invalid'))):
2252
+ with self.subTest(values=values):
2253
+ self.configure(**values)
2254
+ result, process = self.complete("--mode", "auto", "--writers-stopped", success=False)
2255
+ self.assertNotEqual(process.returncode, 0)
2256
+ self.assertEqual(result["status"], "BLOCKED")
2257
+ if values.get("auto_setting_output") == 'null':
2258
+ self.assertEqual(result["reason"], "repository autoMergeAllowed must be a JSON boolean")
2259
+ self.assertIn(str(self.receipt), result["resume"])
2260
+ self.assertNotIn("merge_refused", result)
2261
+ d = json.loads(self.data.read_text(encoding="utf-8"))
2262
+ self.assertEqual(d.get("merge_calls", []), [])
2263
+ self.assertEqual(d["prs"][0]["state"], "OPEN")
2264
+ self.configure(auto_setting_output='false')
2265
+ result, _ = self.cli(*shlex.split(result["resume"])[2:])
2266
+ self.assertEqual(result["status"], "COMPLETE")
2267
+
2268
+ def test_auto_disallowed_status_poll_failure_blocks_with_resume(self):
2269
+ self.allocate(); self.accept()
2270
+ self.configure(auto_allowed=False, merge_state_error=True)
2271
+ result, process = self.complete("--mode", "auto", "--writers-stopped", success=False)
2272
+ self.assertNotEqual(process.returncode, 0)
2273
+ self.assertEqual(result["status"], "BLOCKED")
2274
+ self.assertIn("merge state read failed", result["reason"])
2275
+ self.assertIn(str(self.receipt), result["resume"])
2276
+ self.assertNotIn("merge_refused", result)
2277
+ d = json.loads(self.data.read_text(encoding="utf-8"))
2278
+ self.assertEqual(d.get("merge_calls", []), [])
2279
+ self.assertEqual(d["prs"][0]["state"], "OPEN")
2280
+ self.configure(merge_state_error=False)
2281
+ result, _ = self.cli(*shlex.split(result["resume"])[2:])
2282
+ self.assertEqual(result["status"], "COMPLETE")
2283
+
2284
+ def test_merge_attempt_refusal_keeps_pr(self):
2285
+ self.allocate(); self.accept()
2286
+ self.configure(auto_allowed=False, merge_error=True)
2287
+ result, _ = self.complete("--mode", "auto")
2288
+ self.assertEqual(result["status"], "PR")
2289
+ self.assertIn("merge attempt refused", result["merge_refused"])
2290
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8"))["merges"], 1)
2291
+
2292
+ def test_pr_mode_does_not_read_auto_setting(self):
2293
+ self.allocate(); self.accept()
2294
+ self.configure(auto_setting_error=True)
2295
+ result, _ = self.complete("--mode", "pr")
2296
+ self.assertEqual(result["status"], "PR")
2297
+ self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("auto_setting_reads", 0), 0)
2298
+
2122
2299
  def test_policy_and_local_only_persist(self):
2123
2300
  self.allocate(); self.accept()
2124
2301
  self.configure(merge_allowed=False)
@@ -2133,7 +2310,7 @@ ctypes.CDLL(None).pthread_exit(None)
2133
2310
  self.configure(merge_allowed=True, auto_allowed=False, pending=True)
2134
2311
  result, _ = self.complete("--mode", "auto")
2135
2312
  self.assertEqual(result["status"], "PR")
2136
- self.assertIn("disallows auto merge", result["merge_refused"])
2313
+ self.assertIn("not mergeable: BLOCKED", result["merge_refused"])
2137
2314
  self.assertTrue(self.task.exists())
2138
2315
  # Local-only cannot rewrite the delivery of a pushed PR; a local allocation keeps it (test_local_chain_*).
2139
2316
  result, _ = self.complete("--no-push", success=False)
@@ -13,7 +13,10 @@ commit request uses `--local-only`, a PR request `--mode pr`, and a merge reques
13
13
  `--mode auto`. Local-only/no-push restrictions prevent publication; no-commit or
14
14
  just-edit instructions skip delivery. Existing session-wide restrictions remain
15
15
  in force until changed. Read-only work, unchanged work and incidental generated
16
- files do not trigger delivery. An ideate drain delivers every task.
16
+ files do not trigger delivery.
17
+
18
+ Product file limits cover task edits, not delivery bookkeeping (worktree,
19
+ branch, receipt, uncommitted `.devlyn/` files); delivery restrictions still apply.
17
20
 
18
21
  When delivery is implicit and no GitHub origin is configured, use the existing
19
22
  local-only route and report `LOCAL_ONLY`, the commit and why no PR/merge occurred.
@@ -32,8 +35,7 @@ checkout and ask whether to continue there or isolate. With no one to ask, move
32
35
  only your own edits, and only when the task's context can be rebuilt in the
33
36
  worktree; otherwise leave the checkout as it is and report the task blocked.
34
37
  Isolation changes the work location, not delivery scope. Allocate for the
35
- resolved delivery policy and reuse that worktree when suitable. Isolation
36
- without delivery uses `--local-base`. A local receipt remains local; later
38
+ resolved delivery policy and reuse that worktree when suitable. A local receipt remains local; later
37
39
  publication requires a fresh publication allocation, candidate and acceptance.
38
40
 
39
41
  ## Allocate for delivery or isolation
@@ -47,7 +49,7 @@ BLOCKED:shared-dir-unresolved. Never select another installation.
47
49
  For direct delivery, run the bound task-complete.py after completing the request's
48
50
  edits and checks, before committing the delivery candidate. Allocate before
49
51
  editing when concurrency requires isolation; if isolation is unavailable, do
50
- not write to the shared checkout. Queue drains retain their allocation order.
52
+ not write to the shared checkout.
51
53
 
52
54
  ```sh
53
55
  python3 "$DEVLYN_SHARED_DIR/task-complete.py" allocate --repo . \
@@ -122,7 +124,7 @@ python3 "$DEVLYN_SHARED_DIR/task-complete.py" clean-scratch \
122
124
 
123
125
  This empties only the prospectively owned scratch, preserves its directory
124
126
  identity for interrupted cleanup and later builds, checks live users and reports
125
- the logical byte count separately from actual free disk space. It does not depend on a product PASS or rewrite that verdict.
127
+ the logical byte count separately from actual free disk space.
126
128
  Unknown historical roots, source/recovery data, shared caches and terminal
127
129
  history are never adopted or swept. A refused cleanup does not block source
128
130
  delivery; it stays visible in the handoff with its receipt and retry command
@@ -182,7 +184,7 @@ the user's delivery restriction. These per-task choices persist in the receipt;
182
184
  head and base PR. While OPEN, it cancels owned auto-merge, reports `PR` with its URL
183
185
  and retains the checkout. Once MERGED by any method, either mode cleans released
184
186
  resources in that call, on resume or at the next allocation. Default `auto` also requests
185
- `gh pr merge --auto --merge --match-head-commit <accepted SHA>`. Repository
187
+ a `--merge --match-head-commit <accepted SHA>` merge. Repository
186
188
  checks/reviews and merge policy remain authoritative: no admin bypass, strategy
187
189
  fallback or repository setting changes. When the repository disallows merge
188
190
  commits or refuses the request, delivery reports `PR` with `merge_refused` and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn-cli",
3
- "version": "4.2.21",
3
+ "version": "4.2.23",
4
4
  "description": "Loop & harness engineering for AI coding agents (Claude Code, Codex, and any AGENTS.md-driven agent) — core engineering principles for CLAUDE.md/AGENTS.md and a loop designer that splits one intent into tasks agents drain hands-free",
5
5
  "homepage": "https://github.com/fysoul17/devlyn-cli#readme",
6
6
  "bin": {