devlyn-cli 4.3.13 → 4.3.14

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/bin/devlyn.js CHANGED
@@ -2067,13 +2067,17 @@ function cleanupDeliver(scope, items, current) {
2067
2067
  if (!python) return { error: cleanupReason('requirement-missing', { name: 'Python 3.11' }) };
2068
2068
  const run = (args, cwd) => spawnSync(python, ['-B', helper, ...args], { cwd, encoding: 'utf8', env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' } });
2069
2069
  const lastJson = (text) => { try { return JSON.parse(text.trim().split('\n').at(-1)); } catch { return null; } };
2070
+ const failureCode = (reason) => /\b(authentication|unauthorized|gh auth login|could not read Username|HTTP 40[13])\b/i.test(reason) ? 'auth-failed' : 'delivery-failed';
2070
2071
  const short = crypto.randomBytes(4).toString('hex');
2071
2072
  const branch = `devlyn/cleanup-${short}`;
2072
2073
  const worktree = path.join(path.dirname(scope.root), `${path.basename(scope.root)}.devlyn.noindex`, `devlyn-cleanup-${short}`);
2073
2074
  const allocated = lastJson(run(['allocate', '--repo', scope.root, '--task', `devlyn-cli ${PKG.version} legacy cleanup`,
2074
2075
  '--branch', branch, '--worktree', worktree, '--repository', scope.remote.repository,
2075
2076
  '--remote', 'origin', '--base', scope.remote.base], scope.root).stdout);
2076
- if (allocated?.status !== 'ALLOCATED') return { error: cleanupReason('delivery-failed', { detail: allocated?.reason || 'allocation failed' }) };
2077
+ if (allocated?.status !== 'ALLOCATED') {
2078
+ const reason = allocated?.reason || 'allocation failed';
2079
+ return { error: cleanupReason(failureCode(reason), { detail: reason }) };
2080
+ }
2077
2081
  const release = (error = null) => {
2078
2082
  for (const args of [['worktree', 'remove', '--force', worktree], ['branch', '-D', branch]]) {
2079
2083
  const result = cleanupGit(scope.root, args);
@@ -2108,8 +2112,7 @@ function cleanupDeliver(scope, items, current) {
2108
2112
  if (status === 'COMPLETE' || status === 'MERGED') return { ...cleanupUpdateCheckout(scope), delivery: { state: 'merged', url: completed.pr || null } };
2109
2113
  if (completed?.pr) return { delivery: { state: 'pr-open', url: completed.pr } };
2110
2114
  const reason = completed?.reason || 'delivery did not complete';
2111
- const auth = /\b(authentication|unauthorized|gh auth login|could not read Username|HTTP 40[13])\b/i.test(reason);
2112
- return { error: cleanupReason(auth ? 'auth-failed' : 'delivery-failed', { detail: reason, ...(completed?.resume ? { resume: completed.resume } : {}) }) };
2115
+ return { error: cleanupReason(failureCode(reason), { detail: reason, ...(completed?.resume ? { resume: completed.resume } : {}) }) };
2113
2116
  }
2114
2117
 
2115
2118
  // With no fresh counterpart, the base settled an item only if it changed the path's content or
@@ -52,6 +52,7 @@
52
52
  "40f50f3c3c0d030d1efeb6955c0c6f32f518e737d8dd3bec6610ff02badebbdb",
53
53
  "430d156eb8c62c7edba3d660050f90b267f928976c3a3688e95d54f00dc34747",
54
54
  "450d578ef177a9e8f0b693940ad64b41c1d9719975ba6305f62a7c82ac3a892f",
55
+ "45ef27af9ec27089e2861c13b882521fc6bf073dc6b7a356b326b4a27c5e6e24",
55
56
  "4685be6a94ad5587ce53785c14a60cccf9a852d9d23391dba0a5bcd0c64931cf",
56
57
  "484db9dd2635de5f5acbcf692e2f329f33c0a483951e27bcd77bcbafbc052e19",
57
58
  "4ecb004b24a8adfe41c7416c3fa24e62cf76baa3b276ae1ce5091caef77e57b8",
@@ -769,6 +770,7 @@
769
770
  "4.3.11",
770
771
  "4.3.12",
771
772
  "4.3.13",
773
+ "4.3.14",
772
774
  "4.3.2",
773
775
  "4.3.3",
774
776
  "4.3.4",
@@ -324,10 +324,12 @@ def allocate(args, *, exclude_receipts=(), warnings=None):
324
324
  if local:
325
325
  receipt["local_only"] = True # Local work needs no remote: nothing is fetched or pushed.
326
326
  else:
327
- require(args.repository and args.base, "a remote allocation needs --repository <owner/repo> and --base <branch>; allocate local work with --local-base or --from-receipt")
327
+ if args.repository is None:
328
+ receipt["repository"] = repository_from_url(gref(receipt, "remote", "get-url", "--all", args.remote))
328
329
  policy(receipt, None)
329
330
  receipt["remote_url"] = remote_url(receipt)
330
331
  require("\n" not in receipt["remote_url"]["push"] and receipt["remote_url"]["push"] == receipt["remote_url"]["fetch"], "split/multiple remote URLs are unsupported")
332
+ repo_policy(receipt, allocating=True)
331
333
  target = Path(args.worktree).resolve() # A relative, `..` or symlinked (macOS /tmp) path names its real location.
332
334
  require(not target.exists() and all(not target.is_relative_to(p) and not p.is_relative_to(target) for p in map(Path, registrations(receipt))), "linked worktree must be an absent path disjoint from every registered worktree")
333
335
  receipt["worktree"] = str(target)
@@ -541,9 +543,17 @@ def repository_info(repository):
541
543
  return info
542
544
 
543
545
 
544
- def repo_policy(receipt):
546
+ def repo_policy(receipt, *, allocating=False):
545
547
  info = repository_info(receipt["repository"])
546
- require(info["defaultBranchRef"]["name"] == receipt["base"] and receipt["branch"] != info["defaultBranchRef"]["name"], "base/default branch changed; retain resources")
548
+ default = info["defaultBranchRef"]["name"]
549
+ if allocating:
550
+ if receipt["base"] is None:
551
+ gref(receipt, "check-ref-format", "refs/heads/" + default)
552
+ receipt["base"] = default
553
+ require(receipt["branch"] != receipt["base"], "cannot own a base/default branch")
554
+ require(default == receipt["base"] and receipt["branch"] != default,
555
+ f"base {receipt['base']!r} differs from default branch {default!r}; the helper delivers only to the default branch; use a local allocation with --local-base for other bases"
556
+ if allocating else "base/default branch changed; retain resources")
547
557
  return info
548
558
 
549
559
 
@@ -1409,7 +1419,8 @@ class CompletionTests(unittest.TestCase):
1409
1419
  from unittest.mock import patch
1410
1420
  target = self.root / "absent" / "new"
1411
1421
  exists = Path.exists
1412
- with patch.object(Path, "exists", lambda path: False if path == target or path in target.parents else exists(path)), \
1422
+ with patch.dict(os.environ, self.env), \
1423
+ patch.object(Path, "exists", lambda path: False if path == target or path in target.parents else exists(path)), \
1413
1424
  patch.object(shutil, "disk_usage") as usage:
1414
1425
  with self.assertRaisesRegex(CompletionError, f"no existing ancestor of {re.escape(str(target))} to measure free space"):
1415
1426
  allocate(self.allocation_args("new", target))
@@ -1677,6 +1688,62 @@ class CompletionTests(unittest.TestCase):
1677
1688
  self.g("--no-literal-pathspecs", "check-ignore", "-q", "--", record, work=self.task)
1678
1689
  self.assertEqual(self.g("status", "--porcelain", "--untracked-files=all", work=self.task), "")
1679
1690
 
1691
+ def test_allocate_derives_repository(self):
1692
+ result, _ = self.cli("allocate", "--repo", self.work, "--task", "fixture",
1693
+ "--branch", "task/fixture", "--base", "main", "--worktree", self.root / "linked")
1694
+ self.assertEqual(result["status"], "ALLOCATED")
1695
+ self.assertEqual(read_json(Path(result["receipt"]))["repository"], "test/project")
1696
+
1697
+ def test_allocate_derives_default_base(self):
1698
+ self.g("push", "origin", "main:refs/heads/release")
1699
+ self.configure(default_branch="release")
1700
+ for name, flags in (("explicit", ["--repository", "test/project"]), ("derived", [])):
1701
+ with self.subTest(name=name):
1702
+ result, _ = self.cli("allocate", "--repo", self.work, "--task", name,
1703
+ "--branch", "task/" + name, "--worktree", self.root / name, *flags)
1704
+ self.assertEqual(result["status"], "ALLOCATED")
1705
+ receipt = read_json(Path(result["receipt"]))
1706
+ self.assertEqual((receipt["repository"], receipt["base"]), ("test/project", "release"))
1707
+ self.assertEqual(self.g("rev-parse", "HEAD", work=Path(result["worktree"])), receipt["baseline"])
1708
+
1709
+ def test_allocate_rejects_nondefault_base_before_resources(self):
1710
+ self.configure(default_branch="release")
1711
+ self.g("update-ref", "-d", "refs/remotes/origin/main")
1712
+ refs, worktrees = self.g("show-ref"), self.g("worktree", "list", "--porcelain")
1713
+ result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
1714
+ "--branch", "task/fixture", "--repository", "test/project", "--base", "main",
1715
+ "--worktree", self.root / "linked", success=False)
1716
+ self.assertNotEqual(process.returncode, 0)
1717
+ self.assertEqual(result["status"], "BLOCKED")
1718
+ for text in ("main", "release", "only to the default branch", "--local-base"):
1719
+ self.assertIn(text, result["reason"])
1720
+ self.assertEqual(self.g("show-ref"), refs)
1721
+ self.assertEqual(self.g("worktree", "list", "--porcelain"), worktrees)
1722
+ self.assertFalse((self.work / ".git/devlyn-completion").exists())
1723
+ self.assertFalse((self.root / "linked").exists())
1724
+
1725
+ def test_allocate_rejects_derived_base_as_task_branch(self):
1726
+ self.configure(default_branch="release")
1727
+ refs = self.g("show-ref")
1728
+ result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
1729
+ "--branch", "release", "--worktree", self.root / "linked", success=False)
1730
+ self.assertNotEqual(process.returncode, 0)
1731
+ self.assertEqual(result["status"], "BLOCKED")
1732
+ self.assertIn("cannot own a base/default branch", result["reason"])
1733
+ self.assertEqual(self.g("show-ref"), refs)
1734
+ self.assertEqual(self.g("branch", "--list", "release"), "")
1735
+ self.assertFalse((self.work / ".git/devlyn-completion").exists())
1736
+ self.assertFalse((self.root / "linked").exists())
1737
+
1738
+ def test_allocate_rejects_explicit_repository_mismatch(self):
1739
+ result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
1740
+ "--branch", "task/fixture", "--repository", "test/other", "--base", "main",
1741
+ "--worktree", self.root / "linked", success=False)
1742
+ self.assertNotEqual(process.returncode, 0)
1743
+ self.assertEqual(result, {"status": "BLOCKED", "reason": "repository differs from remote"})
1744
+ self.assertFalse((self.work / ".git/devlyn-completion").exists())
1745
+ self.assertFalse((self.root / "linked").exists())
1746
+
1680
1747
  def test_allocate_base_with_same_named_tag(self):
1681
1748
  self.g("tag", "main")
1682
1749
  result = self.allocate()
@@ -1687,6 +1754,7 @@ class CompletionTests(unittest.TestCase):
1687
1754
  base = "main\u00a0"
1688
1755
  self.g("switch", "-c", base)
1689
1756
  self.g("push", "origin", "HEAD:refs/heads/"+base)
1757
+ self.configure(default_branch=base)
1690
1758
  result = self.allocate(base=base)
1691
1759
  self.assertEqual(result["status"], "ALLOCATED")
1692
1760
  self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["base"], base)
@@ -1711,6 +1779,7 @@ class CompletionTests(unittest.TestCase):
1711
1779
  def test_allocate_rejects_short_base_alias(self):
1712
1780
  self.g("branch", "heads/main")
1713
1781
  self.g("tag", "main")
1782
+ self.configure(default_branch="heads/main")
1714
1783
  self.assert_base_identity_rejected("main", "heads/main")
1715
1784
 
1716
1785
  def test_allocate_uses_remote_base_with_dirty_foreign_anchor(self):
@@ -2295,7 +2364,8 @@ ctypes.CDLL(None).pthread_exit(None)
2295
2364
  self.merge_pr()
2296
2365
  receipt, task = self.receipt, self.task
2297
2366
  self.configure(default_branch="changed")
2298
- result = self.allocate("next")
2367
+ self.g("push", "origin", "main:refs/heads/changed")
2368
+ result = self.allocate("next", base="changed")
2299
2369
  self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "RETAINED")])
2300
2370
  self.assertIn("base/default branch changed", result["reconciled"][0]["reason"])
2301
2371
  self.assertTrue(task.exists())
@@ -2398,12 +2468,13 @@ ctypes.CDLL(None).pthread_exit(None)
2398
2468
  self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
2399
2469
  self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
2400
2470
  self.configure(default_branch="changed")
2471
+ self.g("push", "origin", "main:refs/heads/changed")
2401
2472
  server = json.loads(self.data.read_text())
2402
- result = self.allocate("retry")
2473
+ result = self.allocate("retry", base="changed")
2403
2474
  self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
2404
2475
  self.assertFalse(any(scratch.iterdir()))
2405
2476
  self.assertEqual(json.loads(self.data.read_text()), server)
2406
- self.assertEqual(self.allocate("later")["reconciled"], [])
2477
+ self.assertEqual(self.allocate("later", base="changed")["reconciled"], [])
2407
2478
 
2408
2479
  def test_reconcile_retries_unattempted_scratch_cleanup(self):
2409
2480
  scratch = Path(self.allocate()["scratch"])
@@ -3158,14 +3229,10 @@ ctypes.CDLL(None).pthread_exit(None)
3158
3229
  self.assertFalse((self.root / "next").exists())
3159
3230
 
3160
3231
  def test_local_allocation_needs_no_repository(self):
3161
- """E4. Prediction: without --repository, a remote allocation is refused naming it, with nothing created, while a
3162
- local allocation, here beside an origin that is not GitHub, is ALLOCATED and its local-only completion returns
3163
- LOCAL_ONLY with nothing pushed. Before: argparse required --repository, so both failed as an argparse error."""
3232
+ """A local allocation beside a non-GitHub origin needs no repository and completes without publication."""
3164
3233
  args = ["allocate", "--repo", self.work, "--base", "main"]
3165
- refused, _ = self.cli(*args, "--task", "next", "--branch", "task/next", "--worktree", self.root / "next", success=False)
3166
- self.assertIn("--repository", refused["reason"])
3167
- self.assertEqual((self.g("branch", "--list", "task/next"), (self.root / "next").exists()), ("", False))
3168
3234
  self.g("remote", "set-url", "origin", "https://gitlab.com/team/project.git")
3235
+ (self.bin / "gh").write_text("#!/bin/sh\nexit 1\n", encoding="utf-8")
3169
3236
  result, _ = self.cli(*args, "--task", "fixture", "--branch", "task/fixture", "--worktree", self.root / "linked",
3170
3237
  "--local-base", self.g("rev-parse", "HEAD"))
3171
3238
  self.assertEqual(result["status"], "ALLOCATED")
@@ -3175,13 +3242,8 @@ ctypes.CDLL(None).pthread_exit(None)
3175
3242
  self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("pushs", 0), 0)
3176
3243
 
3177
3244
  def test_local_allocation_needs_no_base(self):
3178
- """P4-2. Prediction: a local allocation with --local-base and no --base, the form agents wrote in 3 of 3 timed
3179
- runs, is ALLOCATED and completes LOCAL_ONLY, while a remote allocation without --base is refused naming it, with
3180
- nothing created."""
3245
+ """A local allocation with --local-base and no --base is ALLOCATED and completes LOCAL_ONLY."""
3181
3246
  args = ["allocate", "--repo", self.work, "--task", "fixture", "--branch", "task/fixture", "--worktree", self.root / "linked"]
3182
- refused, _ = self.cli(*args, "--repository", "test/project", success=False)
3183
- self.assertIn("--base", refused["reason"])
3184
- self.assertEqual((self.g("branch", "--list", "task/fixture"), (self.root / "linked").exists()), ("", False))
3185
3247
  result, _ = self.cli(*args, "--local-base", self.g("rev-parse", "HEAD"))
3186
3248
  self.assertEqual(result["status"], "ALLOCATED")
3187
3249
  self.receipt, self.task = Path(result["receipt"]), Path(result["worktree"])
@@ -55,9 +55,11 @@ not write to the shared checkout.
55
55
  python3 "$DEVLYN_SHARED_DIR/task-complete.py" allocate --repo . \
56
56
  --task '<task identity>' --branch '<absent task branch>' \
57
57
  --worktree '<repo parent>/.devlyn-worktrees/<repo name>/<task directory>' \
58
- --repository '<owner/repo>' --remote origin --base main
58
+ --remote origin
59
59
  ```
60
60
 
61
+ The repository comes from the remote's GitHub URL and the base from the repository's default branch; passing either flag checks it instead.
62
+ A non-default base is refused at allocation.
61
63
  Every delivery allocation owns a linked worktree; `--worktree` is required and
62
64
  must name an absent path.
63
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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn",
3
- "version": "4.3.13",
3
+ "version": "4.3.14",
4
4
  "description": "Devlyn harness skills",
5
5
  "skills": "./skills/"
6
6
  }
@@ -324,10 +324,12 @@ def allocate(args, *, exclude_receipts=(), warnings=None):
324
324
  if local:
325
325
  receipt["local_only"] = True # Local work needs no remote: nothing is fetched or pushed.
326
326
  else:
327
- require(args.repository and args.base, "a remote allocation needs --repository <owner/repo> and --base <branch>; allocate local work with --local-base or --from-receipt")
327
+ if args.repository is None:
328
+ receipt["repository"] = repository_from_url(gref(receipt, "remote", "get-url", "--all", args.remote))
328
329
  policy(receipt, None)
329
330
  receipt["remote_url"] = remote_url(receipt)
330
331
  require("\n" not in receipt["remote_url"]["push"] and receipt["remote_url"]["push"] == receipt["remote_url"]["fetch"], "split/multiple remote URLs are unsupported")
332
+ repo_policy(receipt, allocating=True)
331
333
  target = Path(args.worktree).resolve() # A relative, `..` or symlinked (macOS /tmp) path names its real location.
332
334
  require(not target.exists() and all(not target.is_relative_to(p) and not p.is_relative_to(target) for p in map(Path, registrations(receipt))), "linked worktree must be an absent path disjoint from every registered worktree")
333
335
  receipt["worktree"] = str(target)
@@ -541,9 +543,17 @@ def repository_info(repository):
541
543
  return info
542
544
 
543
545
 
544
- def repo_policy(receipt):
546
+ def repo_policy(receipt, *, allocating=False):
545
547
  info = repository_info(receipt["repository"])
546
- require(info["defaultBranchRef"]["name"] == receipt["base"] and receipt["branch"] != info["defaultBranchRef"]["name"], "base/default branch changed; retain resources")
548
+ default = info["defaultBranchRef"]["name"]
549
+ if allocating:
550
+ if receipt["base"] is None:
551
+ gref(receipt, "check-ref-format", "refs/heads/" + default)
552
+ receipt["base"] = default
553
+ require(receipt["branch"] != receipt["base"], "cannot own a base/default branch")
554
+ require(default == receipt["base"] and receipt["branch"] != default,
555
+ f"base {receipt['base']!r} differs from default branch {default!r}; the helper delivers only to the default branch; use a local allocation with --local-base for other bases"
556
+ if allocating else "base/default branch changed; retain resources")
547
557
  return info
548
558
 
549
559
 
@@ -1409,7 +1419,8 @@ class CompletionTests(unittest.TestCase):
1409
1419
  from unittest.mock import patch
1410
1420
  target = self.root / "absent" / "new"
1411
1421
  exists = Path.exists
1412
- with patch.object(Path, "exists", lambda path: False if path == target or path in target.parents else exists(path)), \
1422
+ with patch.dict(os.environ, self.env), \
1423
+ patch.object(Path, "exists", lambda path: False if path == target or path in target.parents else exists(path)), \
1413
1424
  patch.object(shutil, "disk_usage") as usage:
1414
1425
  with self.assertRaisesRegex(CompletionError, f"no existing ancestor of {re.escape(str(target))} to measure free space"):
1415
1426
  allocate(self.allocation_args("new", target))
@@ -1677,6 +1688,62 @@ class CompletionTests(unittest.TestCase):
1677
1688
  self.g("--no-literal-pathspecs", "check-ignore", "-q", "--", record, work=self.task)
1678
1689
  self.assertEqual(self.g("status", "--porcelain", "--untracked-files=all", work=self.task), "")
1679
1690
 
1691
+ def test_allocate_derives_repository(self):
1692
+ result, _ = self.cli("allocate", "--repo", self.work, "--task", "fixture",
1693
+ "--branch", "task/fixture", "--base", "main", "--worktree", self.root / "linked")
1694
+ self.assertEqual(result["status"], "ALLOCATED")
1695
+ self.assertEqual(read_json(Path(result["receipt"]))["repository"], "test/project")
1696
+
1697
+ def test_allocate_derives_default_base(self):
1698
+ self.g("push", "origin", "main:refs/heads/release")
1699
+ self.configure(default_branch="release")
1700
+ for name, flags in (("explicit", ["--repository", "test/project"]), ("derived", [])):
1701
+ with self.subTest(name=name):
1702
+ result, _ = self.cli("allocate", "--repo", self.work, "--task", name,
1703
+ "--branch", "task/" + name, "--worktree", self.root / name, *flags)
1704
+ self.assertEqual(result["status"], "ALLOCATED")
1705
+ receipt = read_json(Path(result["receipt"]))
1706
+ self.assertEqual((receipt["repository"], receipt["base"]), ("test/project", "release"))
1707
+ self.assertEqual(self.g("rev-parse", "HEAD", work=Path(result["worktree"])), receipt["baseline"])
1708
+
1709
+ def test_allocate_rejects_nondefault_base_before_resources(self):
1710
+ self.configure(default_branch="release")
1711
+ self.g("update-ref", "-d", "refs/remotes/origin/main")
1712
+ refs, worktrees = self.g("show-ref"), self.g("worktree", "list", "--porcelain")
1713
+ result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
1714
+ "--branch", "task/fixture", "--repository", "test/project", "--base", "main",
1715
+ "--worktree", self.root / "linked", success=False)
1716
+ self.assertNotEqual(process.returncode, 0)
1717
+ self.assertEqual(result["status"], "BLOCKED")
1718
+ for text in ("main", "release", "only to the default branch", "--local-base"):
1719
+ self.assertIn(text, result["reason"])
1720
+ self.assertEqual(self.g("show-ref"), refs)
1721
+ self.assertEqual(self.g("worktree", "list", "--porcelain"), worktrees)
1722
+ self.assertFalse((self.work / ".git/devlyn-completion").exists())
1723
+ self.assertFalse((self.root / "linked").exists())
1724
+
1725
+ def test_allocate_rejects_derived_base_as_task_branch(self):
1726
+ self.configure(default_branch="release")
1727
+ refs = self.g("show-ref")
1728
+ result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
1729
+ "--branch", "release", "--worktree", self.root / "linked", success=False)
1730
+ self.assertNotEqual(process.returncode, 0)
1731
+ self.assertEqual(result["status"], "BLOCKED")
1732
+ self.assertIn("cannot own a base/default branch", result["reason"])
1733
+ self.assertEqual(self.g("show-ref"), refs)
1734
+ self.assertEqual(self.g("branch", "--list", "release"), "")
1735
+ self.assertFalse((self.work / ".git/devlyn-completion").exists())
1736
+ self.assertFalse((self.root / "linked").exists())
1737
+
1738
+ def test_allocate_rejects_explicit_repository_mismatch(self):
1739
+ result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
1740
+ "--branch", "task/fixture", "--repository", "test/other", "--base", "main",
1741
+ "--worktree", self.root / "linked", success=False)
1742
+ self.assertNotEqual(process.returncode, 0)
1743
+ self.assertEqual(result, {"status": "BLOCKED", "reason": "repository differs from remote"})
1744
+ self.assertFalse((self.work / ".git/devlyn-completion").exists())
1745
+ self.assertFalse((self.root / "linked").exists())
1746
+
1680
1747
  def test_allocate_base_with_same_named_tag(self):
1681
1748
  self.g("tag", "main")
1682
1749
  result = self.allocate()
@@ -1687,6 +1754,7 @@ class CompletionTests(unittest.TestCase):
1687
1754
  base = "main\u00a0"
1688
1755
  self.g("switch", "-c", base)
1689
1756
  self.g("push", "origin", "HEAD:refs/heads/"+base)
1757
+ self.configure(default_branch=base)
1690
1758
  result = self.allocate(base=base)
1691
1759
  self.assertEqual(result["status"], "ALLOCATED")
1692
1760
  self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["base"], base)
@@ -1711,6 +1779,7 @@ class CompletionTests(unittest.TestCase):
1711
1779
  def test_allocate_rejects_short_base_alias(self):
1712
1780
  self.g("branch", "heads/main")
1713
1781
  self.g("tag", "main")
1782
+ self.configure(default_branch="heads/main")
1714
1783
  self.assert_base_identity_rejected("main", "heads/main")
1715
1784
 
1716
1785
  def test_allocate_uses_remote_base_with_dirty_foreign_anchor(self):
@@ -2295,7 +2364,8 @@ ctypes.CDLL(None).pthread_exit(None)
2295
2364
  self.merge_pr()
2296
2365
  receipt, task = self.receipt, self.task
2297
2366
  self.configure(default_branch="changed")
2298
- result = self.allocate("next")
2367
+ self.g("push", "origin", "main:refs/heads/changed")
2368
+ result = self.allocate("next", base="changed")
2299
2369
  self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "RETAINED")])
2300
2370
  self.assertIn("base/default branch changed", result["reconciled"][0]["reason"])
2301
2371
  self.assertTrue(task.exists())
@@ -2398,12 +2468,13 @@ ctypes.CDLL(None).pthread_exit(None)
2398
2468
  self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
2399
2469
  self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
2400
2470
  self.configure(default_branch="changed")
2471
+ self.g("push", "origin", "main:refs/heads/changed")
2401
2472
  server = json.loads(self.data.read_text())
2402
- result = self.allocate("retry")
2473
+ result = self.allocate("retry", base="changed")
2403
2474
  self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
2404
2475
  self.assertFalse(any(scratch.iterdir()))
2405
2476
  self.assertEqual(json.loads(self.data.read_text()), server)
2406
- self.assertEqual(self.allocate("later")["reconciled"], [])
2477
+ self.assertEqual(self.allocate("later", base="changed")["reconciled"], [])
2407
2478
 
2408
2479
  def test_reconcile_retries_unattempted_scratch_cleanup(self):
2409
2480
  scratch = Path(self.allocate()["scratch"])
@@ -3158,14 +3229,10 @@ ctypes.CDLL(None).pthread_exit(None)
3158
3229
  self.assertFalse((self.root / "next").exists())
3159
3230
 
3160
3231
  def test_local_allocation_needs_no_repository(self):
3161
- """E4. Prediction: without --repository, a remote allocation is refused naming it, with nothing created, while a
3162
- local allocation, here beside an origin that is not GitHub, is ALLOCATED and its local-only completion returns
3163
- LOCAL_ONLY with nothing pushed. Before: argparse required --repository, so both failed as an argparse error."""
3232
+ """A local allocation beside a non-GitHub origin needs no repository and completes without publication."""
3164
3233
  args = ["allocate", "--repo", self.work, "--base", "main"]
3165
- refused, _ = self.cli(*args, "--task", "next", "--branch", "task/next", "--worktree", self.root / "next", success=False)
3166
- self.assertIn("--repository", refused["reason"])
3167
- self.assertEqual((self.g("branch", "--list", "task/next"), (self.root / "next").exists()), ("", False))
3168
3234
  self.g("remote", "set-url", "origin", "https://gitlab.com/team/project.git")
3235
+ (self.bin / "gh").write_text("#!/bin/sh\nexit 1\n", encoding="utf-8")
3169
3236
  result, _ = self.cli(*args, "--task", "fixture", "--branch", "task/fixture", "--worktree", self.root / "linked",
3170
3237
  "--local-base", self.g("rev-parse", "HEAD"))
3171
3238
  self.assertEqual(result["status"], "ALLOCATED")
@@ -3175,13 +3242,8 @@ ctypes.CDLL(None).pthread_exit(None)
3175
3242
  self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("pushs", 0), 0)
3176
3243
 
3177
3244
  def test_local_allocation_needs_no_base(self):
3178
- """P4-2. Prediction: a local allocation with --local-base and no --base, the form agents wrote in 3 of 3 timed
3179
- runs, is ALLOCATED and completes LOCAL_ONLY, while a remote allocation without --base is refused naming it, with
3180
- nothing created."""
3245
+ """A local allocation with --local-base and no --base is ALLOCATED and completes LOCAL_ONLY."""
3181
3246
  args = ["allocate", "--repo", self.work, "--task", "fixture", "--branch", "task/fixture", "--worktree", self.root / "linked"]
3182
- refused, _ = self.cli(*args, "--repository", "test/project", success=False)
3183
- self.assertIn("--base", refused["reason"])
3184
- self.assertEqual((self.g("branch", "--list", "task/fixture"), (self.root / "linked").exists()), ("", False))
3185
3247
  result, _ = self.cli(*args, "--local-base", self.g("rev-parse", "HEAD"))
3186
3248
  self.assertEqual(result["status"], "ALLOCATED")
3187
3249
  self.receipt, self.task = Path(result["receipt"]), Path(result["worktree"])
@@ -55,9 +55,11 @@ not write to the shared checkout.
55
55
  python3 "$DEVLYN_SHARED_DIR/task-complete.py" allocate --repo . \
56
56
  --task '<task identity>' --branch '<absent task branch>' \
57
57
  --worktree '<repo parent>/.devlyn-worktrees/<repo name>/<task directory>' \
58
- --repository '<owner/repo>' --remote origin --base main
58
+ --remote origin
59
59
  ```
60
60
 
61
+ The repository comes from the remote's GitHub URL and the base from the repository's default branch; passing either flag checks it instead.
62
+ A non-default base is refused at allocation.
61
63
  Every delivery allocation owns a linked worktree; `--worktree` is required and
62
64
  must name an absent path.
63
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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "devlyn-harness-v4-3-13",
2
+ "name": "devlyn-harness-v4-3-14",
3
3
  "plugins": [
4
4
  {
5
5
  "name": "devlyn",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn",
3
- "version": "4.3.13",
3
+ "version": "4.3.14",
4
4
  "description": "Devlyn harness skills",
5
5
  "skills": "./skills/"
6
6
  }
@@ -324,10 +324,12 @@ def allocate(args, *, exclude_receipts=(), warnings=None):
324
324
  if local:
325
325
  receipt["local_only"] = True # Local work needs no remote: nothing is fetched or pushed.
326
326
  else:
327
- require(args.repository and args.base, "a remote allocation needs --repository <owner/repo> and --base <branch>; allocate local work with --local-base or --from-receipt")
327
+ if args.repository is None:
328
+ receipt["repository"] = repository_from_url(gref(receipt, "remote", "get-url", "--all", args.remote))
328
329
  policy(receipt, None)
329
330
  receipt["remote_url"] = remote_url(receipt)
330
331
  require("\n" not in receipt["remote_url"]["push"] and receipt["remote_url"]["push"] == receipt["remote_url"]["fetch"], "split/multiple remote URLs are unsupported")
332
+ repo_policy(receipt, allocating=True)
331
333
  target = Path(args.worktree).resolve() # A relative, `..` or symlinked (macOS /tmp) path names its real location.
332
334
  require(not target.exists() and all(not target.is_relative_to(p) and not p.is_relative_to(target) for p in map(Path, registrations(receipt))), "linked worktree must be an absent path disjoint from every registered worktree")
333
335
  receipt["worktree"] = str(target)
@@ -541,9 +543,17 @@ def repository_info(repository):
541
543
  return info
542
544
 
543
545
 
544
- def repo_policy(receipt):
546
+ def repo_policy(receipt, *, allocating=False):
545
547
  info = repository_info(receipt["repository"])
546
- require(info["defaultBranchRef"]["name"] == receipt["base"] and receipt["branch"] != info["defaultBranchRef"]["name"], "base/default branch changed; retain resources")
548
+ default = info["defaultBranchRef"]["name"]
549
+ if allocating:
550
+ if receipt["base"] is None:
551
+ gref(receipt, "check-ref-format", "refs/heads/" + default)
552
+ receipt["base"] = default
553
+ require(receipt["branch"] != receipt["base"], "cannot own a base/default branch")
554
+ require(default == receipt["base"] and receipt["branch"] != default,
555
+ f"base {receipt['base']!r} differs from default branch {default!r}; the helper delivers only to the default branch; use a local allocation with --local-base for other bases"
556
+ if allocating else "base/default branch changed; retain resources")
547
557
  return info
548
558
 
549
559
 
@@ -1409,7 +1419,8 @@ class CompletionTests(unittest.TestCase):
1409
1419
  from unittest.mock import patch
1410
1420
  target = self.root / "absent" / "new"
1411
1421
  exists = Path.exists
1412
- with patch.object(Path, "exists", lambda path: False if path == target or path in target.parents else exists(path)), \
1422
+ with patch.dict(os.environ, self.env), \
1423
+ patch.object(Path, "exists", lambda path: False if path == target or path in target.parents else exists(path)), \
1413
1424
  patch.object(shutil, "disk_usage") as usage:
1414
1425
  with self.assertRaisesRegex(CompletionError, f"no existing ancestor of {re.escape(str(target))} to measure free space"):
1415
1426
  allocate(self.allocation_args("new", target))
@@ -1677,6 +1688,62 @@ class CompletionTests(unittest.TestCase):
1677
1688
  self.g("--no-literal-pathspecs", "check-ignore", "-q", "--", record, work=self.task)
1678
1689
  self.assertEqual(self.g("status", "--porcelain", "--untracked-files=all", work=self.task), "")
1679
1690
 
1691
+ def test_allocate_derives_repository(self):
1692
+ result, _ = self.cli("allocate", "--repo", self.work, "--task", "fixture",
1693
+ "--branch", "task/fixture", "--base", "main", "--worktree", self.root / "linked")
1694
+ self.assertEqual(result["status"], "ALLOCATED")
1695
+ self.assertEqual(read_json(Path(result["receipt"]))["repository"], "test/project")
1696
+
1697
+ def test_allocate_derives_default_base(self):
1698
+ self.g("push", "origin", "main:refs/heads/release")
1699
+ self.configure(default_branch="release")
1700
+ for name, flags in (("explicit", ["--repository", "test/project"]), ("derived", [])):
1701
+ with self.subTest(name=name):
1702
+ result, _ = self.cli("allocate", "--repo", self.work, "--task", name,
1703
+ "--branch", "task/" + name, "--worktree", self.root / name, *flags)
1704
+ self.assertEqual(result["status"], "ALLOCATED")
1705
+ receipt = read_json(Path(result["receipt"]))
1706
+ self.assertEqual((receipt["repository"], receipt["base"]), ("test/project", "release"))
1707
+ self.assertEqual(self.g("rev-parse", "HEAD", work=Path(result["worktree"])), receipt["baseline"])
1708
+
1709
+ def test_allocate_rejects_nondefault_base_before_resources(self):
1710
+ self.configure(default_branch="release")
1711
+ self.g("update-ref", "-d", "refs/remotes/origin/main")
1712
+ refs, worktrees = self.g("show-ref"), self.g("worktree", "list", "--porcelain")
1713
+ result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
1714
+ "--branch", "task/fixture", "--repository", "test/project", "--base", "main",
1715
+ "--worktree", self.root / "linked", success=False)
1716
+ self.assertNotEqual(process.returncode, 0)
1717
+ self.assertEqual(result["status"], "BLOCKED")
1718
+ for text in ("main", "release", "only to the default branch", "--local-base"):
1719
+ self.assertIn(text, result["reason"])
1720
+ self.assertEqual(self.g("show-ref"), refs)
1721
+ self.assertEqual(self.g("worktree", "list", "--porcelain"), worktrees)
1722
+ self.assertFalse((self.work / ".git/devlyn-completion").exists())
1723
+ self.assertFalse((self.root / "linked").exists())
1724
+
1725
+ def test_allocate_rejects_derived_base_as_task_branch(self):
1726
+ self.configure(default_branch="release")
1727
+ refs = self.g("show-ref")
1728
+ result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
1729
+ "--branch", "release", "--worktree", self.root / "linked", success=False)
1730
+ self.assertNotEqual(process.returncode, 0)
1731
+ self.assertEqual(result["status"], "BLOCKED")
1732
+ self.assertIn("cannot own a base/default branch", result["reason"])
1733
+ self.assertEqual(self.g("show-ref"), refs)
1734
+ self.assertEqual(self.g("branch", "--list", "release"), "")
1735
+ self.assertFalse((self.work / ".git/devlyn-completion").exists())
1736
+ self.assertFalse((self.root / "linked").exists())
1737
+
1738
+ def test_allocate_rejects_explicit_repository_mismatch(self):
1739
+ result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
1740
+ "--branch", "task/fixture", "--repository", "test/other", "--base", "main",
1741
+ "--worktree", self.root / "linked", success=False)
1742
+ self.assertNotEqual(process.returncode, 0)
1743
+ self.assertEqual(result, {"status": "BLOCKED", "reason": "repository differs from remote"})
1744
+ self.assertFalse((self.work / ".git/devlyn-completion").exists())
1745
+ self.assertFalse((self.root / "linked").exists())
1746
+
1680
1747
  def test_allocate_base_with_same_named_tag(self):
1681
1748
  self.g("tag", "main")
1682
1749
  result = self.allocate()
@@ -1687,6 +1754,7 @@ class CompletionTests(unittest.TestCase):
1687
1754
  base = "main\u00a0"
1688
1755
  self.g("switch", "-c", base)
1689
1756
  self.g("push", "origin", "HEAD:refs/heads/"+base)
1757
+ self.configure(default_branch=base)
1690
1758
  result = self.allocate(base=base)
1691
1759
  self.assertEqual(result["status"], "ALLOCATED")
1692
1760
  self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["base"], base)
@@ -1711,6 +1779,7 @@ class CompletionTests(unittest.TestCase):
1711
1779
  def test_allocate_rejects_short_base_alias(self):
1712
1780
  self.g("branch", "heads/main")
1713
1781
  self.g("tag", "main")
1782
+ self.configure(default_branch="heads/main")
1714
1783
  self.assert_base_identity_rejected("main", "heads/main")
1715
1784
 
1716
1785
  def test_allocate_uses_remote_base_with_dirty_foreign_anchor(self):
@@ -2295,7 +2364,8 @@ ctypes.CDLL(None).pthread_exit(None)
2295
2364
  self.merge_pr()
2296
2365
  receipt, task = self.receipt, self.task
2297
2366
  self.configure(default_branch="changed")
2298
- result = self.allocate("next")
2367
+ self.g("push", "origin", "main:refs/heads/changed")
2368
+ result = self.allocate("next", base="changed")
2299
2369
  self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "RETAINED")])
2300
2370
  self.assertIn("base/default branch changed", result["reconciled"][0]["reason"])
2301
2371
  self.assertTrue(task.exists())
@@ -2398,12 +2468,13 @@ ctypes.CDLL(None).pthread_exit(None)
2398
2468
  self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
2399
2469
  self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
2400
2470
  self.configure(default_branch="changed")
2471
+ self.g("push", "origin", "main:refs/heads/changed")
2401
2472
  server = json.loads(self.data.read_text())
2402
- result = self.allocate("retry")
2473
+ result = self.allocate("retry", base="changed")
2403
2474
  self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
2404
2475
  self.assertFalse(any(scratch.iterdir()))
2405
2476
  self.assertEqual(json.loads(self.data.read_text()), server)
2406
- self.assertEqual(self.allocate("later")["reconciled"], [])
2477
+ self.assertEqual(self.allocate("later", base="changed")["reconciled"], [])
2407
2478
 
2408
2479
  def test_reconcile_retries_unattempted_scratch_cleanup(self):
2409
2480
  scratch = Path(self.allocate()["scratch"])
@@ -3158,14 +3229,10 @@ ctypes.CDLL(None).pthread_exit(None)
3158
3229
  self.assertFalse((self.root / "next").exists())
3159
3230
 
3160
3231
  def test_local_allocation_needs_no_repository(self):
3161
- """E4. Prediction: without --repository, a remote allocation is refused naming it, with nothing created, while a
3162
- local allocation, here beside an origin that is not GitHub, is ALLOCATED and its local-only completion returns
3163
- LOCAL_ONLY with nothing pushed. Before: argparse required --repository, so both failed as an argparse error."""
3232
+ """A local allocation beside a non-GitHub origin needs no repository and completes without publication."""
3164
3233
  args = ["allocate", "--repo", self.work, "--base", "main"]
3165
- refused, _ = self.cli(*args, "--task", "next", "--branch", "task/next", "--worktree", self.root / "next", success=False)
3166
- self.assertIn("--repository", refused["reason"])
3167
- self.assertEqual((self.g("branch", "--list", "task/next"), (self.root / "next").exists()), ("", False))
3168
3234
  self.g("remote", "set-url", "origin", "https://gitlab.com/team/project.git")
3235
+ (self.bin / "gh").write_text("#!/bin/sh\nexit 1\n", encoding="utf-8")
3169
3236
  result, _ = self.cli(*args, "--task", "fixture", "--branch", "task/fixture", "--worktree", self.root / "linked",
3170
3237
  "--local-base", self.g("rev-parse", "HEAD"))
3171
3238
  self.assertEqual(result["status"], "ALLOCATED")
@@ -3175,13 +3242,8 @@ ctypes.CDLL(None).pthread_exit(None)
3175
3242
  self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("pushs", 0), 0)
3176
3243
 
3177
3244
  def test_local_allocation_needs_no_base(self):
3178
- """P4-2. Prediction: a local allocation with --local-base and no --base, the form agents wrote in 3 of 3 timed
3179
- runs, is ALLOCATED and completes LOCAL_ONLY, while a remote allocation without --base is refused naming it, with
3180
- nothing created."""
3245
+ """A local allocation with --local-base and no --base is ALLOCATED and completes LOCAL_ONLY."""
3181
3246
  args = ["allocate", "--repo", self.work, "--task", "fixture", "--branch", "task/fixture", "--worktree", self.root / "linked"]
3182
- refused, _ = self.cli(*args, "--repository", "test/project", success=False)
3183
- self.assertIn("--base", refused["reason"])
3184
- self.assertEqual((self.g("branch", "--list", "task/fixture"), (self.root / "linked").exists()), ("", False))
3185
3247
  result, _ = self.cli(*args, "--local-base", self.g("rev-parse", "HEAD"))
3186
3248
  self.assertEqual(result["status"], "ALLOCATED")
3187
3249
  self.receipt, self.task = Path(result["receipt"]), Path(result["worktree"])
@@ -55,9 +55,11 @@ not write to the shared checkout.
55
55
  python3 "$DEVLYN_SHARED_DIR/task-complete.py" allocate --repo . \
56
56
  --task '<task identity>' --branch '<absent task branch>' \
57
57
  --worktree '<repo parent>/.devlyn-worktrees/<repo name>/<task directory>' \
58
- --repository '<owner/repo>' --remote origin --base main
58
+ --remote origin
59
59
  ```
60
60
 
61
+ The repository comes from the remote's GitHub URL and the base from the repository's default branch; passing either flag checks it instead.
62
+ A non-default base is refused at allocation.
61
63
  Every delivery allocation owns a linked worktree; `--worktree` is required and
62
64
  must name an absent path.
63
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn-cli",
3
- "version": "4.3.13",
3
+ "version": "4.3.14",
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": {
package/release.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "devlyn-cli",
4
- "version": "4.3.13",
4
+ "version": "4.3.14",
5
5
  "principles": {
6
6
  "defaults": "config/skills/_shared/runtime-principles.md",
7
7
  "memory": {
@@ -129,7 +129,7 @@
129
129
  },
130
130
  {
131
131
  "path": "bin/devlyn.js",
132
- "sha256": "b702d9ef857644536a9ad39cec1079ee0dff916f4de821652fbb20fdcb290e16",
132
+ "sha256": "bbf2a2ee5309eab804cc3e03fd4c9bd0e0c71db67cfacc764c2f5e611968b4bf",
133
133
  "executable": true
134
134
  },
135
135
  {
@@ -164,7 +164,7 @@
164
164
  },
165
165
  {
166
166
  "path": "bin/skill-history.json",
167
- "sha256": "b02b70036ae0c32abe63659af2bc35ee97da907c7abf9961a3d6fe77bfc13d08",
167
+ "sha256": "eb54a16283da49f9817a7b306f3be3c26bb0a01b8372a8377ee68461789ae43a",
168
168
  "executable": false
169
169
  },
170
170
  {
@@ -244,12 +244,12 @@
244
244
  },
245
245
  {
246
246
  "path": "config/skills/_shared/task-complete.py",
247
- "sha256": "4938b56189b20b320ed50e275b7d7febab6858ec74ba39f3069ea3ef13309a73",
247
+ "sha256": "f9709f6855f511401205788aecf8aa59e268855b7673b84ed617adee56a245dc",
248
248
  "executable": false
249
249
  },
250
250
  {
251
251
  "path": "config/skills/_shared/task-completion.md",
252
- "sha256": "d4e0b9803334812a3c541e9e92393bf5e712a481c09284f767a72f7209445ba9",
252
+ "sha256": "8b9a8706ab912165f2eef66bc39f2261f88df2b1b682d8d90d7f05f6014e965e",
253
253
  "executable": false
254
254
  },
255
255
  {
@@ -309,7 +309,7 @@
309
309
  },
310
310
  {
311
311
  "path": "native/claude/.claude-plugin/plugin.json",
312
- "sha256": "1b295e8072b6b433a4881b51de2f4c357954e554a521a569476271da60278ad9",
312
+ "sha256": "69ae38cdcbcd7e7c695bf44241103657690be50e971baba15ec592b3e29c71df",
313
313
  "executable": false
314
314
  },
315
315
  {
@@ -384,12 +384,12 @@
384
384
  },
385
385
  {
386
386
  "path": "native/claude/skills/_shared/task-complete.py",
387
- "sha256": "4938b56189b20b320ed50e275b7d7febab6858ec74ba39f3069ea3ef13309a73",
387
+ "sha256": "f9709f6855f511401205788aecf8aa59e268855b7673b84ed617adee56a245dc",
388
388
  "executable": false
389
389
  },
390
390
  {
391
391
  "path": "native/claude/skills/_shared/task-completion.md",
392
- "sha256": "d4e0b9803334812a3c541e9e92393bf5e712a481c09284f767a72f7209445ba9",
392
+ "sha256": "8b9a8706ab912165f2eef66bc39f2261f88df2b1b682d8d90d7f05f6014e965e",
393
393
  "executable": false
394
394
  },
395
395
  {
@@ -439,12 +439,12 @@
439
439
  },
440
440
  {
441
441
  "path": "native/codex/.agents/plugins/marketplace.json",
442
- "sha256": "6309fa2c98e6634eb270bc627fddc96bcec3d684773758106466dc516dde7617",
442
+ "sha256": "9cd5c06de1b20c1fec885ad85ee1409505c4d03e74e60ca6774247aa93269b8d",
443
443
  "executable": false
444
444
  },
445
445
  {
446
446
  "path": "native/codex/plugins/devlyn/.codex-plugin/plugin.json",
447
- "sha256": "1b295e8072b6b433a4881b51de2f4c357954e554a521a569476271da60278ad9",
447
+ "sha256": "69ae38cdcbcd7e7c695bf44241103657690be50e971baba15ec592b3e29c71df",
448
448
  "executable": false
449
449
  },
450
450
  {
@@ -519,12 +519,12 @@
519
519
  },
520
520
  {
521
521
  "path": "native/codex/plugins/devlyn/skills/_shared/task-complete.py",
522
- "sha256": "4938b56189b20b320ed50e275b7d7febab6858ec74ba39f3069ea3ef13309a73",
522
+ "sha256": "f9709f6855f511401205788aecf8aa59e268855b7673b84ed617adee56a245dc",
523
523
  "executable": false
524
524
  },
525
525
  {
526
526
  "path": "native/codex/plugins/devlyn/skills/_shared/task-completion.md",
527
- "sha256": "d4e0b9803334812a3c541e9e92393bf5e712a481c09284f767a72f7209445ba9",
527
+ "sha256": "8b9a8706ab912165f2eef66bc39f2261f88df2b1b682d8d90d7f05f6014e965e",
528
528
  "executable": false
529
529
  },
530
530
  {
@@ -1714,12 +1714,12 @@
1714
1714
  },
1715
1715
  {
1716
1716
  "path": "package.json",
1717
- "sha256": "ef560ee79cc756a40b8ce1b590a3f3b1944027f435c5d9821c0aef91f064b966",
1717
+ "sha256": "bac25159f616f9a0583452dbf8eb902d2d746d095f2f70526792efa6e34fac0f",
1718
1718
  "executable": false
1719
1719
  }
1720
1720
  ],
1721
1721
  "digest": {
1722
1722
  "algorithm": "sha256",
1723
- "value": "5fddf59395abd5dd015567d1184ef98ad970f987ee0573b6e1f5732171115573"
1723
+ "value": "4f420dc3d285818f7e5ed5bc007c4a718a2d2ed690c862321cc84fc14b907b2f"
1724
1724
  }
1725
1725
  }