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",
|
package/bin/skill-history.json
CHANGED
|
@@ -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
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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("
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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": {
|