@sagentlab/navarch-runtime 0.1.111 → 0.1.112

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.
@@ -209,9 +209,9 @@ class ConversationLoop {
209
209
  ...(shutdownSignal ? { signal: shutdownSignal } : {}),
210
210
  ...this.options.turnOverrides,
211
211
  log: this.log,
212
- prepareWorkspace: async ({ conversationId, repositoryUrl, mode, signal }) => {
212
+ prepareWorkspace: async ({ conversationId, repositoryUrl, workingBranch, mode, signal }) => {
213
213
  const workspace = await (0, workspace_cjs_1.prepareConversationWorkspace)({
214
- workspaceRoot: config.workspaceRoot, conversationId, repositoryUrl, env, signal, mode,
214
+ workspaceRoot: config.workspaceRoot, conversationId, repositoryUrl, workingBranch, env, signal, mode,
215
215
  ...(this.options.gitRunner ? { runner: this.options.gitRunner } : {}),
216
216
  });
217
217
  const guard = await (0, worktree_guard_cjs_1.prepareWorktreeGuard)({
@@ -298,7 +298,9 @@ async function runConversationTurn(deps, conversationId, turnId) {
298
298
  if (snapshot.mode === "workspace" && !deps.workspaceEnabled)
299
299
  throw new Error("Workspace chat is disabled on this machine");
300
300
  prompt = (0, prompt_cjs_1.buildConversationPrompt)(snapshot, journal.turnId);
301
- prepared = await deps.prepareWorkspace({ conversationId, repositoryUrl, mode: snapshot.mode ?? "read_only", signal: controller.signal });
301
+ // Older servers omit the working branch: the checkout stays on the default branch.
302
+ const workingBranch = (0, workspace_cjs_1.validWorkingBranch)(claim.workingBranch);
303
+ prepared = await deps.prepareWorkspace({ conversationId, repositoryUrl, workingBranch, mode: snapshot.mode ?? "read_only", signal: controller.signal });
302
304
  }
303
305
  catch (error) {
304
306
  if (!run.lost)
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.conversationEnv = conversationEnv;
7
7
  exports.validRepositoryUrl = validRepositoryUrl;
8
+ exports.validWorkingBranch = validWorkingBranch;
8
9
  exports.conversationRoot = conversationRoot;
9
10
  exports.prepareConversationWorkspace = prepareConversationWorkspace;
10
11
  exports.removeConversationWorkspace = removeConversationWorkspace;
@@ -50,6 +51,22 @@ function validRepositoryUrl(value) {
50
51
  return null;
51
52
  return `https://github.com/${fullName}.git`;
52
53
  }
54
+ /**
55
+ * The project's working branch from the claim, checked as strictly as the
56
+ * hosted path's `buildWorkingBranchCheckoutCommand`
57
+ * (worker/hosted-agent/delivery.ts): a plain ref name that cannot be read as a
58
+ * git option. Absent (an older server, or no working branch) is null; anything
59
+ * else that is not a plain name throws, so the turn never runs on a branch it
60
+ * was not given.
61
+ */
62
+ function validWorkingBranch(value) {
63
+ if (value === undefined || value === null)
64
+ return null;
65
+ if (typeof value !== "string" || value.length > 255 || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(value) || value.includes("..")) {
66
+ throw new Error("working branch is not a valid branch name");
67
+ }
68
+ return value;
69
+ }
53
70
  function conversationRoot(workspaceRoot, conversationId) {
54
71
  return node_path_1.default.join(workspaceRoot, "conversations", (0, journal_cjs_1.assertUuid)(conversationId, "conversation id"));
55
72
  }
@@ -67,6 +84,11 @@ async function prepareConversationWorkspace(options) {
67
84
  const url = validRepositoryUrl(options.repositoryUrl);
68
85
  if (!url)
69
86
  throw new Error("repository URL is not a plain https URL");
87
+ const branch = validWorkingBranch(options.workingBranch);
88
+ // The marker records what the checkout was cloned from, so a changed
89
+ // repository or working branch is noticed. Default-branch checkouts keep the
90
+ // bare URL that earlier runtimes wrote.
91
+ const identity = branch ? `${url}\nbranch=${branch}` : url;
70
92
  const runner = options.runner ?? sandbox_cjs_1.nodeCommandRunner;
71
93
  const root = options.mode === "workspace"
72
94
  ? node_path_1.default.join(options.workspaceRoot, "chat-workspaces", (0, journal_cjs_1.assertUuid)(options.conversationId, "conversation id"))
@@ -83,7 +105,7 @@ async function prepareConversationWorkspace(options) {
83
105
  if (options.mode === "workspace") {
84
106
  await node_fs_1.promises.writeFile(node_path_1.default.join(workspace.metaDir, "preserve-workspace"), "Workspace chat: retain until the operator removes it.\n", { mode: 0o600 });
85
107
  }
86
- if (hasCheckout && recorded === url)
108
+ if (hasCheckout && recorded === identity)
87
109
  return workspace;
88
110
  if (options.mode === "workspace") {
89
111
  const exists = await node_fs_1.promises.lstat(workspace.checkout).then(() => true, (error) => {
@@ -94,7 +116,7 @@ async function prepareConversationWorkspace(options) {
94
116
  if (exists)
95
117
  throw new Error("Checkout identity changed; preserving the existing workspace. Start a new conversation.");
96
118
  }
97
- // New conversation, a changed repository URL, or an interrupted clone.
119
+ // New conversation, a changed repository URL or working branch, or an interrupted clone.
98
120
  await node_fs_1.promises.rm(workspace.checkout, { recursive: true, force: true });
99
121
  const env = {
100
122
  ...options.env,
@@ -104,12 +126,12 @@ async function prepareConversationWorkspace(options) {
104
126
  GIT_CONFIG_KEY_0: "core.hooksPath",
105
127
  GIT_CONFIG_VALUE_0: process.platform === "win32" ? "NUL" : "/dev/null",
106
128
  };
107
- const result = await runner.run("git", ["clone", "--quiet", "--filter=blob:none", "--no-tags", "--", url, workspace.checkout], { cwd: root, env, timeoutMs: CLONE_TIMEOUT_MS, signal: options.signal });
129
+ const result = await runner.run("git", ["clone", "--quiet", "--filter=blob:none", "--no-tags", ...(branch ? ["--branch", branch] : []), "--", url, workspace.checkout], { cwd: root, env, timeoutMs: CLONE_TIMEOUT_MS, signal: options.signal });
108
130
  if (result.code !== 0) {
109
131
  await node_fs_1.promises.rm(workspace.checkout, { recursive: true, force: true }).catch(() => undefined);
110
132
  throw new Error(`git clone failed with exit ${result.code}: ${result.stderr.slice(-500)}`);
111
133
  }
112
- await node_fs_1.promises.writeFile(marker, url, { encoding: "utf8", mode: 0o600 });
134
+ await node_fs_1.promises.writeFile(marker, identity, { encoding: "utf8", mode: 0o600 });
113
135
  return workspace;
114
136
  }
115
137
  async function removeConversationWorkspace(workspaceRoot, conversationId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.111",
3
+ "version": "0.1.112",
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",