@sagentlab/navarch-runtime 0.1.60 → 0.1.62

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/README.md CHANGED
@@ -8,8 +8,9 @@ machine.
8
8
 
9
9
  The machine operator selects **Claude Code**, **OpenAI Codex**, **Google
10
10
  Gemini CLI**, **OpenCode**, or an **Agent Client Protocol (ACP)** agent when connecting a worker. Any selected agent can
11
- run any project task; task eligibility depends on capabilities and project
12
- gates, not agent type — see "Choosing an agent" below.
11
+ run any unpinned project task; task eligibility depends on capabilities and
12
+ project gates. A task pinned to a runtime waits for a worker that advertises
13
+ that adapter and runs on it — see "Choosing an agent" below.
13
14
 
14
15
  See [`docs/agent-platform-project-plan.md`](../docs/agent-platform-project-plan.md)
15
16
  §3.8/§3.9/§3.11 and [`docs/navarch/implementation-plan.md`](../docs/navarch/implementation-plan.md)
@@ -369,7 +370,7 @@ unchanged across the deployment.
369
370
  | `NAVARCH_SANDBOX_EGRESS_NETWORK` | _(unset)_ | Docker network that enforces a profile's egress allowlist. Docker cannot filter by domain itself, so an allowlist profile without this fails closed to `--network=none` and records the denial. |
370
371
  | `NAVARCH_DOCKER_IMAGE` | `ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0` | Version-pinned per-session image with Node 20, git, GitHub CLI, ripgrep, jq, SSH, and Claude Code 2.1.218. Override with an image tag or digest you control. |
371
372
  | `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent adapter: `claude-code`, `codex`, `gemini`, `opencode`, `acp`, or `qoder`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
372
- | `NAVARCH_RUNTIMES` | resolved local agent choice | Comma list of installed/authenticated adapters advertised to dispatch. The control plane chooses among these per project/task. |
373
+ | `NAVARCH_RUNTIMES` | resolved local agent choice | Comma list of installed/authenticated adapters advertised to dispatch. Unpinned tasks run on the preferred agent; a task runtime pin selects the matching adapter. |
373
374
  | `NAVARCH_UPDATE_CHANNEL` | `stable` | Release channel advertised by the worker (`stable` or `canary`); the server-managed machine channel remains authoritative. |
374
375
  | `NAVARCH_AUTO_UPDATE` | on under `supervise` | Set `off`, `false`, or `0` to report releases without staging or activating them. Automatic activation is always off under plain `start`. |
375
376
  | `NAVARCH_CLAUDE_BIN` | `claude` | Path/name of the Claude Code CLI binary. |
@@ -530,8 +531,13 @@ export NAVARCH_RUNTIMES=claude-code,codex,gemini,opencode,acp,qoder
530
531
  For the legacy single-runtime setting, priority is `start --agent` →
531
532
  `NAVARCH_AGENT` → the locally saved choice → `claude-code`.
532
533
  `NAVARCH_RUNTIMES` expands what the worker advertises. For normal projects,
533
- the locally selected `NAVARCH_AGENT` handles every claimed task. Sandbox
534
- projects remain constrained to Claude Code.
534
+ the locally selected `NAVARCH_AGENT` handles every unpinned task. A task with a
535
+ runtime override (for example `gemini`) is claimed only by a worker that
536
+ advertises that adapter, and the claim returns it as the session runtime.
537
+ Sandbox projects remain constrained to Claude Code, so they claim only unpinned
538
+ or `claude-code`-pinned tasks. List only adapters this worker can really run:
539
+ in Docker mode that means CLIs installed in `NAVARCH_DOCKER_IMAGE`, otherwise
540
+ pinned tasks for a missing adapter are claimed and then fail.
535
541
 
536
542
  All five BYO adapters implement the same `AgentAdapter` interface
537
543
  (`src/adapters/types.cts`) and run either directly on the host or via
@@ -37,6 +37,8 @@ class GitWorktree {
37
37
  sessionTokenMarker;
38
38
  hostedCheckpointRef;
39
39
  hostedCheckpointLocalRef;
40
+ /** Set when the resumed remote branch had lost its ownership commit and must be re-marked. */
41
+ adoptedUnmarkedBranch = false;
40
42
  constructor(options) {
41
43
  const projectKey = safePathSegment(options.projectId);
42
44
  const sessionKey = safePathSegment(options.sessionId);
@@ -185,7 +187,9 @@ class GitWorktree {
185
187
  this.worktreePath,
186
188
  startRef,
187
189
  ], false);
188
- if (!taskRef && !staleTip)
190
+ // An adopted branch gets its marker back, or the pre-push guard refuses
191
+ // every delivery push and the next retry is stuck again.
192
+ if ((!taskRef && !staleTip) || this.adoptedUnmarkedBranch)
189
193
  await this.createTaskOwnershipCommit();
190
194
  if (checkpoint)
191
195
  await this.applyHostedCheckpoint(checkpoint);
@@ -293,6 +297,7 @@ class GitWorktree {
293
297
  if (tip.code !== 0 || !/^[0-9a-f]{40,64}$/i.test(staleSha))
294
298
  return null;
295
299
  const owned = this.branch.endsWith(this.legacyTaskBranchSuffix) ||
300
+ this.adoptedUnmarkedBranch ||
296
301
  (await this.hasTaskOwnershipMarker(localRef));
297
302
  if (!owned) {
298
303
  throw new Error(`Local branch ${this.branch} already exists in the cached repository but does not ` +
@@ -360,6 +365,9 @@ class GitWorktree {
360
365
  const matches = [...ownedShortMatches, ...legacyMatches];
361
366
  if (matches.length === 0) {
362
367
  if (shortMatches.length > 0) {
368
+ const adopted = await this.adoptUnmarkedOwnBranch(shortMatches);
369
+ if (adopted)
370
+ return adopted;
363
371
  throw new Error(`Remote branch ${shortMatches[0]} shares task suffix ${this.taskBranchSuffix} but does not ` +
364
372
  `contain ownership marker ${this.taskOwnershipTrailer}; refusing to resume or fork.`);
365
373
  }
@@ -372,6 +380,36 @@ class GitWorktree {
372
380
  this.branch = remoteRef.replace(/^refs\/remotes\/origin\//, "");
373
381
  return remoteRef;
374
382
  }
383
+ /**
384
+ * An agent that amends, resets, or rebases away the empty claim commit and
385
+ * pushes leaves this task's branch without its marker, and every retry then
386
+ * refused it (carova #10). The branch is still ours when it carries exactly
387
+ * the name this task allocates (slug and suffix) and none of its own commits
388
+ * claim another task; anything else stays refused.
389
+ */
390
+ async adoptUnmarkedOwnBranch(shortMatches) {
391
+ const ownRef = `refs/remotes/origin/${this.branch}`;
392
+ if (shortMatches.length !== 1 || shortMatches[0] !== ownRef)
393
+ return null;
394
+ const base = await this.resolveStartRef().catch(() => null);
395
+ if (!base)
396
+ return null;
397
+ const log = await this.runner.run("git", [
398
+ "--git-dir",
399
+ this.repositoryPath,
400
+ "log",
401
+ "--format=%B",
402
+ ownRef,
403
+ "--not",
404
+ base,
405
+ ]);
406
+ if (log.code !== 0)
407
+ return null;
408
+ if (log.stdout.split("\n").some((line) => /^Navarch-Task-ID:/i.test(line.trim())))
409
+ return null;
410
+ this.adoptedUnmarkedBranch = true;
411
+ return ownRef;
412
+ }
375
413
  /** Short branch names are ambiguous without a full task-ID marker in their history. */
376
414
  async hasTaskOwnershipMarker(ref) {
377
415
  const log = await this.runGit([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",