devlyn-cli 4.3.12 → 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.
Files changed (33) hide show
  1. package/bin/cleanup-history.json +499 -1
  2. package/bin/devlyn.js +240 -36
  3. package/bin/skill-history.json +3 -0
  4. package/config/skills/_shared/task-complete.py +81 -19
  5. package/config/skills/_shared/task-completion.md +3 -1
  6. package/native/claude/.claude-plugin/plugin.json +1 -1
  7. package/native/claude/skills/_shared/task-complete.py +81 -19
  8. package/native/claude/skills/_shared/task-completion.md +3 -1
  9. package/native/codex/.agents/plugins/marketplace.json +1 -1
  10. package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +1 -1
  11. package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +81 -19
  12. package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +3 -1
  13. package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
  14. package/node_modules/jsonc-parser/LICENSE.md +21 -0
  15. package/node_modules/jsonc-parser/README.md +364 -0
  16. package/node_modules/jsonc-parser/SECURITY.md +41 -0
  17. package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
  18. package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
  19. package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
  20. package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
  21. package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
  22. package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
  23. package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
  24. package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
  25. package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
  26. package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
  27. package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
  28. package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
  29. package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
  30. package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
  31. package/node_modules/jsonc-parser/package.json +37 -0
  32. package/package.json +3 -1
  33. package/release.json +110 -15
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn",
3
- "version": "4.3.12",
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-12",
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.12",
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.
@@ -0,0 +1,76 @@
1
+ 3.3.0 2022-06-24
2
+ =================
3
+ - `JSONVisitor.onObjectBegin` and `JSONVisitor.onArrayBegin` can now return `false` to instruct the visitor that no children should be visited.
4
+
5
+
6
+ 3.2.0 2022-08-30
7
+ =================
8
+ - update the version of the bundled Javascript files to `es2020`.
9
+ - include all `const enum` values in the bundled JavaScript files (`ScanError`, `SyntaxKind`, `ParseErrorCode`).
10
+
11
+ 3.1.0 2022-07-07
12
+ ==================
13
+ * added new API `FormattingOptions.keepLines` : It leaves the initial line positions in the formatting.
14
+
15
+ 3.0.0 2020-11-13
16
+ ==================
17
+ * fixed API spec for `parseTree`. Can return `undefine` for empty input.
18
+ * added new API `FormattingOptions.insertFinalNewline`.
19
+
20
+
21
+ 2.3.0 2020-07-03
22
+ ==================
23
+ * new API `ModificationOptions.isArrayInsertion`: If `JSONPath` refers to an index of an array and `isArrayInsertion` is `true`, then `modify` will insert a new item at that location instead of overwriting its contents.
24
+ * `ModificationOptions.formattingOptions` is now optional. If not set, newly inserted content will not be formatted.
25
+
26
+
27
+ 2.2.0 2019-10-25
28
+ ==================
29
+ * added `ParseOptions.allowEmptyContent`. Default is `false`.
30
+ * new API `getNodeType`: Returns the type of a value returned by parse.
31
+ * `parse`: Fix issue with empty property name
32
+
33
+ 2.1.0 2019-03-29
34
+ ==================
35
+ * `JSONScanner` and `JSONVisitor` return lineNumber / character.
36
+
37
+ 2.0.0 2018-04-12
38
+ ==================
39
+ * renamed `Node.columnOffset` to `Node.colonOffset`
40
+ * new API `getNodePath`: Gets the JSON path of the given JSON DOM node
41
+ * new API `findNodeAtOffset`: Finds the most inner node at the given offset. If `includeRightBound` is set, also finds nodes that end at the given offset.
42
+
43
+ 1.0.3 2018-03-07
44
+ ==================
45
+ * provide ems modules
46
+
47
+ 1.0.2 2018-03-05
48
+ ==================
49
+ * added the `visit.onComment` API, reported when comments are allowed.
50
+ * added the `ParseErrorCode.InvalidCommentToken` enum value, reported when comments are disallowed.
51
+
52
+ 1.0.1
53
+ ==================
54
+ * added the `format` API: computes edits to format a JSON document.
55
+ * added the `modify` API: computes edits to insert, remove or replace a property or value in a JSON document.
56
+ * added the `allyEdits` API: applies edits to a document
57
+
58
+ 1.0.0
59
+ ==================
60
+ * remove nls dependency (remove `getParseErrorMessage`)
61
+
62
+ 0.4.2 / 2017-05-05
63
+ ==================
64
+ * added `ParseError.offset` & `ParseError.length`
65
+
66
+ 0.4.1 / 2017-04-02
67
+ ==================
68
+ * added `ParseOptions.allowTrailingComma`
69
+
70
+ 0.4.0 / 2017-02-23
71
+ ==================
72
+ * fix for `getLocation`. Now `getLocation` inside an object will always return a property from inside that property. Can be empty string if the object has no properties or if the offset is before a actual property `{ "a": { | }} will return location ['a', ' ']`
73
+
74
+ 0.3.0 / 2017-01-17
75
+ ==================
76
+ * Updating to typescript 2.0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Microsoft
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.