devlyn-cli 4.2.4 → 4.2.5

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
@@ -880,6 +880,7 @@ function install(targets, global, optionalNames = []) {
880
880
  else installClaudeCore();
881
881
  }
882
882
  log(`\n✅ devlyn ${PKG.version} installed`, 'green');
883
+ if (global) log('The principles block installs per project: run npx devlyn-cli in the project without --global.');
883
884
  noticeGlobalDrift(roots);
884
885
  return roots;
885
886
  }
@@ -1,10 +1,11 @@
1
1
  {
2
- "source": "devlyn-cli@4.1.0 (npm tarball sha1 398ff07d3d689b56cc22e18ac50260bc6e4e9e85); devlyn-cli@4.2.0 (npm tarball sha1 c93c2fb1cf5630b18d2eba0085182c14fddbe2d5); devlyn-cli@4.2.4 (_shared and devlyn-ideate, fingerprinted from an install of its packed tarball); whole-tree fingerprints from bin/skill-ownership.js; deprecated commands: commands/ and optional-commands/*/ files of devlyn-cli 0.0.1-0.5.8 (every published version that shipped them), LF-normalized SHA-256",
2
+ "source": "devlyn-cli@4.1.0 (npm tarball sha1 398ff07d3d689b56cc22e18ac50260bc6e4e9e85); devlyn-cli@4.2.0 (npm tarball sha1 c93c2fb1cf5630b18d2eba0085182c14fddbe2d5); devlyn-cli@4.2.4 and 4.2.5 (_shared and devlyn-ideate, fingerprinted from an install of each packed tarball); whole-tree fingerprints from bin/skill-ownership.js; deprecated commands: commands/ and optional-commands/*/ files of devlyn-cli 0.0.1-0.5.8 (every published version that shipped them), LF-normalized SHA-256",
3
3
  "skills": {
4
4
  "_shared": [
5
5
  "eb3327d5e6940bd5e0459fa28c9ab323d9c58ccfdeb9426c19cd027cc4e5c4ed",
6
6
  "8c80926ab03b2477e52264495b35c2116f65fc51b02e8c56e441b5705872392a",
7
- "3c81c575cc43d70488865b440204cb399091405d9b7f81dbf047a32d3d5ac045"
7
+ "3c81c575cc43d70488865b440204cb399091405d9b7f81dbf047a32d3d5ac045",
8
+ "6546d6b833701f2641a004802cfc17ec62442e5d0003f396cb0231b676ba9a4a"
8
9
  ],
9
10
  "devlyn-design-ui": [
10
11
  "91080266232fb62b6f1d45932184f065589e12eb66f02fb6442527c6dfcd81b6"
@@ -16,7 +17,8 @@
16
17
  "devlyn-ideate": [
17
18
  "8ff5148cc65b4608c35b29afd6fef20d42d8da93a094144d98cfc63e74089b6e",
18
19
  "5824f07e455fe1c3942bf595a12e935258d4c0a851ca1bf741db915603d1c4e0",
19
- "7f451e48e391643b34f6249244bc47d93d6303875e7b092771e3af0718343bc1"
20
+ "7f451e48e391643b34f6249244bc47d93d6303875e7b092771e3af0718343bc1",
21
+ "d6433bf0f96dd9dd485c3623885255738654674b3e23cfcc1a2cfd24f3cda96e"
20
22
  ],
21
23
  "devlyn-queue": [
22
24
  "4c7d988dcd154c7bf658d2d24cd7868955df9d854ed87eb784742d6d4bb229e0"
@@ -102,7 +102,8 @@ def resolve(work, default_engine, *, available=None, shared=SHARED):
102
102
  """The project's executor pin, else the invoking CLI; status only, never a dispatch decision."""
103
103
  config, source = read_config(Path(work) / ".devlyn/engines.json", optional=True, shared=shared)
104
104
  engine = config.get("executor", default_engine)
105
- adapter(engine, shared=shared)
105
+ # read_config validates pins; the invoking host needs no adapter to report its own default.
106
+ name(engine, "engine")
106
107
  available = available or (lambda candidate: shutil.which(candidate) is not None)
107
108
  return {"executor": {"engine": engine, "source": "engines.json" if "executor" in config else "default",
108
109
  "availability": "CLI-present/auth-unchecked" if available(engine) else "CLI-unavailable"},
@@ -113,6 +114,7 @@ def select(work, default_engine, *, available=None, shared=SHARED):
113
114
  """The executor for dispatch. A pin is a promise: an unavailable pinned engine is never substituted."""
114
115
  result = resolve(work, default_engine, available=available, shared=shared)
115
116
  executor = result["executor"]
117
+ adapter(executor["engine"], shared=shared)
116
118
  if executor["source"] == "engines.json" and executor["availability"] == "CLI-unavailable":
117
119
  fail(f"pinned executor {executor['engine']} is unavailable; install and authenticate its CLI, "
118
120
  f"verify `{executor['engine']} --version`, and retry, or pin an available engine with `devlyn-engines executor <name>`",
@@ -375,9 +375,14 @@ def gh(receipt, *args):
375
375
  return command(["gh", *args, "--repo", "github.com/"+receipt["repository"]])
376
376
 
377
377
 
378
+ def repository_info(repository):
379
+ info = json.loads(command(["gh", "repo", "view", "github.com/"+repository, "--json", "nameWithOwner,url,defaultBranchRef,mergeCommitAllowed"]))
380
+ require(info["nameWithOwner"].lower() == repository.lower() and info["url"].lower() == "https://github.com/"+repository.lower(), "GitHub repository identity changed")
381
+ return info
382
+
383
+
378
384
  def repo_policy(receipt):
379
- info = json.loads(command(["gh", "repo", "view", "github.com/"+receipt["repository"], "--json", "nameWithOwner,url,defaultBranchRef,mergeCommitAllowed"]))
380
- require(info["nameWithOwner"].lower() == receipt["repository"].lower() and info["url"].lower() == "https://github.com/"+receipt["repository"].lower(), "GitHub repository identity changed")
385
+ info = repository_info(receipt["repository"])
381
386
  require(info["defaultBranchRef"]["name"] == receipt["base"] and receipt["branch"] != info["defaultBranchRef"]["name"], "base/default branch changed; retain resources")
382
387
  return info
383
388
 
@@ -799,7 +804,7 @@ def main():
799
804
  attachment = actions.add_parser("attach")
800
805
  attachment.add_argument("--receipt", required=True)
801
806
  attachment.add_argument("--commit", required=True)
802
- attachment.add_argument("--file", default="docs/specs/queue.md")
807
+ attachment.add_argument("--file", required=True)
803
808
  completion = actions.add_parser("complete")
804
809
  completion.add_argument("--receipt", required=True)
805
810
  completion.add_argument("--acceptance")
@@ -2141,6 +2146,12 @@ class CompletionTests(unittest.TestCase):
2141
2146
  result, _ = self.cli("accept", "--receipt", self.receipt, "--acceptance", acceptance, success=False)
2142
2147
  self.assertEqual(result["status"], "FAILED" if verdict == "FAILED" else "BLOCKED", result)
2143
2148
 
2149
+ def test_attach_requires_an_explicit_queue_file(self):
2150
+ result = self.run_cmd([sys.executable, str(Path(__file__).resolve()), 'attach',
2151
+ '--receipt', str(self.root / 'absent.json'), '--commit', 'HEAD'], success=False)
2152
+ self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
2153
+ self.assertIn('the following arguments are required: --file', result.stderr)
2154
+
2144
2155
  def test_attach_requires_the_terminal_checkout_only_for_a_publishable_result(self):
2145
2156
  for verdict in ("ACCEPTED", "FAILED"):
2146
2157
  with self.subTest(verdict=verdict):
@@ -100,6 +100,8 @@ Run `python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" status --repo .` and report it
100
100
 
101
101
  ## drain
102
102
 
103
+ `status`, `add` and `drain` write the repository's Git directory, and auto/pr delivery needs network access, so a sandboxed host runs them with escalation or full access.
104
+
103
105
  Follow [loop.md](references/loop.md). The executor is the configured route, the `.devlyn/engines.json` `executor` pin or else this CLI, checked per `_shared/engine-preflight.md`. Pass its argv after `--`. `<git dir>` is the output of `git rev-parse --path-format=absolute --git-common-dir`, where the executor commits and writes its submission; the drain fills `{worktree_git_dir}` with the task worktree's own Git directory, which Codex's sandbox keeps read-only unless that exact directory is listed:
104
106
 
105
107
  | Executor | argv |
@@ -11,7 +11,7 @@ python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" drain --repo . [--local-only] [--wo
11
11
 
12
12
  Both print one JSON object; exit 1 is `BLOCKED` with a `reason`.
13
13
 
14
- - `status` writes nothing beyond finishing `add`'s cleanup (step 1): reconciled counts (`pending`, `active`, `accepted`, `failed`, `blocked`, `legacy_pending`), the `next` task, `blockers`, deliveries still pending with their resume (drain again until the terminal commit is attached, then `task-complete.py complete --receipt <receipt>`), and under `cleanup` the package copies `add` kept or could not remove.
14
+ - `status` takes `queue.lock` in the repository's Git directory and finishes `add`'s cleanup (step 1); it reports reconciled counts (`pending`, `active`, `accepted`, `failed`, `blocked`, `legacy_pending`), the `next` task, `blockers`, deliveries still pending with their resume (drain again until the terminal commit is attached, then `task-complete.py complete --receipt <receipt>`), and under `cleanup` the package copies `add` kept or could not remove.
15
15
  - `drain` ends `DRAINED` (no runnable task remains; open deliveries show per task), `WAITING` (a task waits with its reason, or legacy rows need planning) or `BLOCKED` (conflict, invalid input or a recovery blocker). Progress lines `devlyn-loop: <task>: <event>` go to stderr.
16
16
  - `--local-only` (alias `--no-push`) keeps the loop local. A manifest `delivery: local-only` and any earlier local receipt of the loop keep that restriction for later drains. A local loop needs no remote; nothing is fetched or pushed. An `auto`/`pr` loop none of whose tasks has a pushed PR runs as a local loop from its capture and the branch it was added on. Once one has, that PR is never rewritten to local and the loop is never run locally: its tasks wait, naming the PR, while other work continues.
17
17
  - Worktrees default to `<repo parent>/<repo name>.devlyn/<loop-id>/<task-id>`.
@@ -843,9 +843,9 @@ def evidence_ignored(anchor, common, start):
843
843
 
844
844
 
845
845
  def allocate(v, row, opts):
846
- """Allocate the task's owned worktree, or return why it waits: origin names no GitHub repository, or its refreshed
847
- remote base holds another version of the loop's package or lacks the checkout's instructions (waiting() checks a local
848
- start before selection)."""
846
+ """Allocate the task's owned worktree, or return why it waits: origin names no GitHub repository, its base is not the
847
+ default, or its refreshed remote base holds another version of the loop's package or lacks the checkout's instructions
848
+ (waiting() checks a local start before selection)."""
849
849
  identity, loop, task = row["identity"], row["loop"], row["task"]
850
850
  package = v["packages"][loop]
851
851
  manifest = package["manifest"]
@@ -868,6 +868,13 @@ def allocate(v, row, opts):
868
868
  except helper["CompletionError"]:
869
869
  return (f"{manifest['delivery']} delivery needs an origin remote of the form https://github.com/<owner>/<repo>, "
870
870
  "git@github.com:<owner>/<repo> or ssh://git@github.com/<owner>/<repo>; set one, or drain with --local-only")
871
+ try:
872
+ default = helper["repository_info"](values["repository"])["defaultBranchRef"]["name"]
873
+ except (helper["CompletionError"], OSError, KeyError, TypeError, ValueError) as exc:
874
+ return f"cannot check delivery repository: {exc}"
875
+ if manifest["base_ref"] != default:
876
+ return (f"{manifest['delivery']} delivery base {manifest['base_ref']} is not the repository's default branch {default}; "
877
+ f"plan the loop on {default}, or drain with --local-only")
871
878
  try:
872
879
  start = helper["remote_base"]({"common_gitdir": str(common), "remote": "origin", "base": manifest["base_ref"]})
873
880
  except helper["CompletionError"] as exc:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn-cli",
3
- "version": "4.2.4",
3
+ "version": "4.2.5",
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": {