@sagentlab/navarch-runtime 0.1.59 → 0.1.61

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.
@@ -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([
@@ -387,6 +425,14 @@ class GitWorktree {
387
425
  .split("\n")
388
426
  .some((line) => line.trim() === this.taskOwnershipTrailer);
389
427
  }
428
+ /**
429
+ * The ownership line a push to the delivery branch must keep, for the
430
+ * pre-push guard (`NAVARCH_DELIVERY_TASK_MARKER`). Null for a resumed legacy
431
+ * full-UUID branch, whose name alone proves ownership.
432
+ */
433
+ get deliveryTaskMarker() {
434
+ return this.branch.endsWith(this.legacyTaskBranchSuffix) ? null : this.taskOwnershipTrailer;
435
+ }
390
436
  /** Records full task ownership without lengthening the human-facing branch name. */
391
437
  async createTaskOwnershipCommit() {
392
438
  await this.runGit([
@@ -657,6 +703,11 @@ async function pathExists(value) {
657
703
  * the allocated `navarch/...` branch (for example onto Codex's default
658
704
  * `codex/...` prefix). `--no-verify` bypasses any hook, so
659
705
  * `resolveDeliveryHead` also repairs the worktree before completion.
706
+ *
707
+ * With `NAVARCH_DELIVERY_TASK_MARKER` it also refuses a delivery-branch push
708
+ * whose history lost the task's ownership commit (an amend, reset, or rebase
709
+ * drops the empty claim commit). A retry only resumes a short-suffix branch
710
+ * that carries the marker, so an unmarked push strands the task.
660
711
  */
661
712
  const DELIVERY_BRANCH_GUARD_HOOK = `#!/bin/sh
662
713
  # Managed by @sagentlab/navarch-runtime (git-worktree.cts).
@@ -672,6 +723,18 @@ while read -r local_ref local_sha remote_ref remote_sha; do
672
723
  if [ "$branch" != "$allowed" ]; then
673
724
  printf '%s\\n' "navarch: refusing to push '$branch'; this task's delivery branch is '$allowed'." >&2
674
725
  status=1
726
+ continue
727
+ fi
728
+ marker="\${NAVARCH_DELIVERY_TASK_MARKER:-}"
729
+ [ -n "$marker" ] || continue
730
+ case "$local_sha" in
731
+ *[!0]*) ;;
732
+ *) continue ;;
733
+ esac
734
+ if [ -z "$(git log -1 --format=%H --fixed-strings --grep="$marker" "$local_sha" 2>/dev/null)" ]; then
735
+ printf '%s\\n' "navarch: refusing to push '$branch'; its history no longer contains the task ownership line '$marker' (the empty 'chore(navarch): claim task branch' commit was amended, reset, or rebased away)." >&2
736
+ printf '%s\\n' "navarch: restore it with: git commit --allow-empty -m 'chore(navarch): claim task branch' -m '$marker' && git push" >&2
737
+ status=1
675
738
  fi
676
739
  done
677
740
  exit "$status"
package/dist/session.cjs CHANGED
@@ -420,8 +420,12 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
420
420
  await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), promptText, "utf8");
421
421
  }
422
422
  // Preparation may resume an existing task branch with an older slug or ID format.
423
- if (!allowBranchRename)
423
+ if (!allowBranchRename) {
424
424
  sessionEnv.NAVARCH_DELIVERY_BRANCH = gitWorktree.branch;
425
+ const marker = gitWorktree.deliveryTaskMarker;
426
+ if (marker)
427
+ sessionEnv.NAVARCH_DELIVERY_TASK_MARKER = marker;
428
+ }
425
429
  if (sandbox) {
426
430
  await sandbox.create();
427
431
  // Security-policy refusals (a mount outside the allowlist, an egress
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
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",