@love-moon/conductor-cli 0.7.7 → 0.9.0

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/conductor.js CHANGED
@@ -14,7 +14,8 @@
14
14
  * serve-ai - Start an OpenAI-compatible local AI server
15
15
  * project - Manage Conductor projects (list/show/create/...)
16
16
  * issue - Manage issues (list/show/create/update/start/done)
17
- * task - Manage tasks (list/show/send/messages/schedule)
17
+ * task - Manage tasks (create/list/show/send/messages/schedule)
18
+ * remote-exec - Run a command on another daemon's host
18
19
  */
19
20
 
20
21
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -50,6 +51,7 @@ export function runConductorCli(args = argv, deps = {}) {
50
51
  "project",
51
52
  "issue",
52
53
  "task",
54
+ "remote-exec",
53
55
  ];
54
56
 
55
57
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
@@ -134,7 +136,8 @@ Subcommands:
134
136
  serve-ai Start an OpenAI-compatible local AI server
135
137
  project Manage Conductor projects (list/show/create/...)
136
138
  issue Manage issues (list/show/create/update/start/done)
137
- task Manage tasks (list/show/send/messages/schedule)
139
+ task Manage tasks (create/list/show/send/messages/schedule)
140
+ remote-exec Run a command on another daemon's host
138
141
 
139
142
  Options:
140
143
  -h, --help Show this help message
@@ -156,8 +159,10 @@ Examples:
156
159
  conductor update
157
160
  conductor project list
158
161
  conductor issue create --title "Refactor module" --priority P2
162
+ conductor task create --title "Refactor module" --prompt "Extract the parser" --backend codex
159
163
  conductor task send <task-id> "please add a unit test"
160
164
  conductor task schedule create <task-id> "follow up" --delay 10m
165
+ conductor remote-exec --target ubuntu --workspace /home/duino/ws/holomotion ls .
161
166
 
162
167
  For subcommand-specific help:
163
168
  conductor fire --help
@@ -171,6 +176,7 @@ For subcommand-specific help:
171
176
  conductor project --help
172
177
  conductor issue --help
173
178
  conductor task --help
179
+ conductor remote-exec --help
174
180
 
175
181
  Version: ${pkgJson.version}
176
182
  `);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.7.7",
4
- "gitCommitId": "26b2dbb",
3
+ "version": "0.9.0",
4
+ "gitCommitId": "606e5a6",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -24,8 +24,8 @@
24
24
  "test": "node --test test/*.test.js"
25
25
  },
26
26
  "dependencies": {
27
- "@love-moon/ai-sdk": "0.7.7",
28
- "@love-moon/conductor-sdk": "0.7.7",
27
+ "@love-moon/ai-sdk": "0.9.0",
28
+ "@love-moon/conductor-sdk": "0.9.0",
29
29
  "@github/copilot-sdk": "^0.3.0",
30
30
  "chrome-launcher": "^1.2.1",
31
31
  "chrome-remote-interface": "^0.33.0",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "optionalDependencies": {
40
40
  "@roamhq/wrtc": "^0.10.0",
41
- "@love-moon/chat-web": "0.7.7"
41
+ "@love-moon/chat-web": "0.9.0"
42
42
  },
43
43
  "pnpm": {
44
44
  "onlyBuiltDependencies": [
@@ -5,7 +5,7 @@
5
5
  import { AiManager } from "@love-moon/ai-sdk";
6
6
 
7
7
  const VALID_ACTIONS = new Set(["status", "quota", "list_accounts", "switch_account"]);
8
- const BASE_QUOTA_TOOLS = ["codex", "claude", "kimi", "copilot"];
8
+ const BASE_QUOTA_TOOLS = ["codex", "claude", "kimi", "copilot", "dsh"];
9
9
 
10
10
  /**
11
11
  * @param {object} opts
@@ -23,7 +23,7 @@ export function createAiManagerHandlers(opts = {}) {
23
23
  manager.getCurrentCodexAccount().catch(() => null),
24
24
  ]);
25
25
  const network = {};
26
- const tools = ["codex", "claude", "kimi", "copilot"];
26
+ const tools = [...BASE_QUOTA_TOOLS];
27
27
  await Promise.all(
28
28
  tools.map(async (tool) => {
29
29
  if (install[tool]?.installed) {
@@ -70,6 +70,9 @@ export function createAiManagerHandlers(opts = {}) {
70
70
  addJob("copilot", () => manager.getCopilotQuota({
71
71
  forceRefresh,
72
72
  }));
73
+ addJob("dsh", () => manager.getDshQuota({
74
+ forceRefresh,
75
+ }));
73
76
  const externalBackends = pickExternalQuotaBackends(args);
74
77
  for (const backend of externalBackends) {
75
78
  if (!tools.has(backend)) {
package/src/daemon.js CHANGED
@@ -30,6 +30,11 @@ import {
30
30
  createCustomCommandHandlers,
31
31
  handleCustomCommandsRequest,
32
32
  } from "./custom-command-handlers.js";
33
+ import {
34
+ REMOTE_EXEC_CAPABILITY,
35
+ createRemoteExecHandlers,
36
+ handleRemoteExecRequest,
37
+ } from "./remote-exec-handlers.js";
33
38
  import { resolveResumeContext } from "./fire/resume.js";
34
39
  import {
35
40
  filterRuntimeSupportedAllowCliList,
@@ -213,6 +218,32 @@ function getFireTmuxModeEnabled(userConfig) {
213
218
  return false;
214
219
  }
215
220
 
221
+ // Whether this host will accept `remote-exec`. Unlike `pty_task` (gated on a
222
+ // node-pty probe) and `custom_commands` (opt-in per script), remote exec would
223
+ // otherwise be unconditional, leaving a shared CI box or a root-owned daemon no
224
+ // way to decline. Defaults to enabled to match the other daemon capabilities.
225
+ //
226
+ // Resolution order:
227
+ // 1. CONDUCTOR_REMOTE_EXEC env var ("1"/"true"/"on" enable, "0"/"false"/"off" disable)
228
+ // 2. remote_exec boolean in the resolved Conductor config.yaml
229
+ // 3. Default: true
230
+ function getRemoteExecEnabled(userConfig) {
231
+ const rawEnv = process.env.CONDUCTOR_REMOTE_EXEC;
232
+ if (typeof rawEnv === "string" && rawEnv.trim()) {
233
+ const normalized = rawEnv.trim().toLowerCase();
234
+ if (normalized === "1" || normalized === "true" || normalized === "on" || normalized === "yes") {
235
+ return true;
236
+ }
237
+ if (normalized === "0" || normalized === "false" || normalized === "off" || normalized === "no") {
238
+ return false;
239
+ }
240
+ }
241
+ if (userConfig && typeof userConfig === "object" && userConfig.remote_exec === false) {
242
+ return false;
243
+ }
244
+ return true;
245
+ }
246
+
216
247
  function normalizePlanLimitType(limitType) {
217
248
  if (typeof limitType !== "string") {
218
249
  return null;
@@ -799,6 +830,7 @@ export function startDaemon(config = {}, deps = {}) {
799
830
  // warning and silently fall back to direct spawn rather than failing every
800
831
  // create_task with ENOENT.
801
832
  const FIRE_TMUX_MODE_ENABLED = getFireTmuxModeEnabled(userConfig);
833
+ const remoteExecEnabled = getRemoteExecEnabled(userConfig);
802
834
 
803
835
  // Get allow_cli_list from config
804
836
  const RAW_ALLOW_CLI_LIST = getRawAllowCliList(userConfig);
@@ -1843,6 +1875,14 @@ export function startDaemon(config = {}, deps = {}) {
1843
1875
  "# (svg/png/jpg/gif/webp/ico/avif) relative to this .conductor/ directory.",
1844
1876
  "# Remove this line to use the default folder icon.",
1845
1877
  "",
1878
+ "# Optional agent registry used by the task-composer worker/reviewer picker.",
1879
+ "# Agent docs are workspace-relative paths; backend is optional.",
1880
+ "# agents:",
1881
+ "# feature-dev:",
1882
+ "# doc: claw/agents/feature-dev.md",
1883
+ "# description: Implements features end to end.",
1884
+ "# backend: codex",
1885
+ "",
1846
1886
  "worktree:",
1847
1887
  " sync_branch: false",
1848
1888
  " sync_submodules: true",
@@ -1856,6 +1896,13 @@ export function startDaemon(config = {}, deps = {}) {
1856
1896
  ].join("\n");
1857
1897
 
1858
1898
  const MAX_PROJECT_ICON_IMAGE_BYTES = 128 * 1024;
1899
+ const MAX_PROJECT_AGENT_ENTRIES = 64;
1900
+ const MAX_PROJECT_AGENT_NAME_LENGTH = 64;
1901
+ const MAX_PROJECT_AGENT_DOC_LENGTH = 512;
1902
+ const MAX_PROJECT_AGENT_DESCRIPTION_LENGTH = 512;
1903
+ const MAX_PROJECT_AGENT_BACKEND_LENGTH = 64;
1904
+ const PROJECT_AGENT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1905
+ const PROJECT_AGENT_BACKEND_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1859
1906
  const PROJECT_ICON_MIME_BY_EXTENSION = {
1860
1907
  ".svg": "image/svg+xml",
1861
1908
  ".png": "image/png",
@@ -1954,6 +2001,118 @@ export function startDaemon(config = {}, deps = {}) {
1954
2001
  return null;
1955
2002
  }
1956
2003
 
2004
+ function normalizeProjectAgentSetting(name, value) {
2005
+ const normalizedName = normalizeOptionalString(name);
2006
+ if (
2007
+ !normalizedName ||
2008
+ normalizedName.length > MAX_PROJECT_AGENT_NAME_LENGTH ||
2009
+ !PROJECT_AGENT_NAME_RE.test(normalizedName)
2010
+ ) {
2011
+ return null;
2012
+ }
2013
+
2014
+ let docValue = null;
2015
+ let descriptionValue = null;
2016
+ let backendValue = null;
2017
+ if (typeof value === "string") {
2018
+ docValue = value;
2019
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
2020
+ docValue = value.doc ?? value.path;
2021
+ descriptionValue = value.description;
2022
+ backendValue = value.backend;
2023
+ } else {
2024
+ return null;
2025
+ }
2026
+
2027
+ const doc = normalizeOptionalString(docValue);
2028
+ if (
2029
+ !doc ||
2030
+ doc.length > MAX_PROJECT_AGENT_DOC_LENGTH ||
2031
+ path.isAbsolute(doc) ||
2032
+ /^[A-Za-z]:[\\/]/.test(doc) ||
2033
+ /^[/\\]{2}/.test(doc)
2034
+ ) {
2035
+ return null;
2036
+ }
2037
+ const normalizedDoc = path.posix.normalize(doc.split("\\").join("/"));
2038
+ if (normalizedDoc === ".." || normalizedDoc.startsWith("../")) {
2039
+ return null;
2040
+ }
2041
+
2042
+ const description = normalizeOptionalString(descriptionValue);
2043
+ const backend = normalizeOptionalString(backendValue)?.toLowerCase() || null;
2044
+ return {
2045
+ name: normalizedName,
2046
+ doc,
2047
+ description:
2048
+ description && description.length <= MAX_PROJECT_AGENT_DESCRIPTION_LENGTH
2049
+ ? description
2050
+ : null,
2051
+ backend:
2052
+ backend &&
2053
+ backend.length <= MAX_PROJECT_AGENT_BACKEND_LENGTH &&
2054
+ PROJECT_AGENT_BACKEND_RE.test(backend)
2055
+ ? backend
2056
+ : null,
2057
+ };
2058
+ }
2059
+
2060
+ function normalizeProjectAgentsSetting(agentsNode) {
2061
+ if (!agentsNode || typeof agentsNode !== "object") {
2062
+ return [];
2063
+ }
2064
+ const result = [];
2065
+ const seen = new Set();
2066
+ const push = (entry) => {
2067
+ if (
2068
+ entry &&
2069
+ result.length < MAX_PROJECT_AGENT_ENTRIES &&
2070
+ !seen.has(entry.name)
2071
+ ) {
2072
+ seen.add(entry.name);
2073
+ result.push(entry);
2074
+ }
2075
+ };
2076
+
2077
+ if (Array.isArray(agentsNode)) {
2078
+ for (const item of agentsNode) {
2079
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
2080
+ continue;
2081
+ }
2082
+ const keys = Object.keys(item);
2083
+ if (keys.length === 1) {
2084
+ push(normalizeProjectAgentSetting(keys[0], item[keys[0]]));
2085
+ } else if (Object.prototype.hasOwnProperty.call(item, "name")) {
2086
+ push(normalizeProjectAgentSetting(item.name, item));
2087
+ }
2088
+ }
2089
+ return result;
2090
+ }
2091
+
2092
+ for (const [name, value] of Object.entries(agentsNode)) {
2093
+ push(normalizeProjectAgentSetting(name, value));
2094
+ }
2095
+ return result;
2096
+ }
2097
+
2098
+ function readProjectAgentsSetting(projectWorkspacePath) {
2099
+ for (const settingsPath of getProjectSettingsCandidates(projectWorkspacePath)) {
2100
+ if (!existsSyncFn(settingsPath)) {
2101
+ continue;
2102
+ }
2103
+ try {
2104
+ const parsed = yaml.load(readFileSyncFn(settingsPath, "utf8"));
2105
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2106
+ return [];
2107
+ }
2108
+ return normalizeProjectAgentsSetting(parsed.agents);
2109
+ } catch {
2110
+ return [];
2111
+ }
2112
+ }
2113
+ return [];
2114
+ }
2115
+
1957
2116
  function readProjectWorktreeSettings(projectWorkspacePath) {
1958
2117
  for (const settingsPath of getProjectSettingsCandidates(projectWorkspacePath)) {
1959
2118
  if (!existsSyncFn(settingsPath)) {
@@ -2069,11 +2228,19 @@ export function startDaemon(config = {}, deps = {}) {
2069
2228
 
2070
2229
  if (linkStat) {
2071
2230
  if (!linkStat.isSymbolicLink()) {
2072
- throw new Error(
2073
- `worktree symlink destination already exists and is not a symlink: ${linkPath}. ` +
2074
- `Refusing to replace it because it may hold real data — remove it manually, ` +
2075
- `or drop "${configuredPath}" from worktree.symlink in .conductor/settings.yaml.`,
2231
+ // A real file/dir at the destination means something materialised
2232
+ // data inside the worktree (e.g. `pnpm install` replaced the
2233
+ // node_modules link with a real directory). It may hold real data,
2234
+ // so never clobber it — but throwing here made every task in this
2235
+ // worktree permanently un-restartable. Keep the local copy and skip
2236
+ // the link; remove the path manually (or drop the entry from
2237
+ // worktree.symlink) to restore sharing with the project workspace.
2238
+ logError(
2239
+ `[worktree] skipping symlink for ${configuredPath}: destination already exists and ` +
2240
+ `is not a symlink: ${linkPath}. Keeping the local copy — remove it manually to ` +
2241
+ `restore sharing via worktree.symlink in .conductor/settings.yaml.`,
2076
2242
  );
2243
+ continue;
2077
2244
  }
2078
2245
  // Compare the link's TARGET, not whether that target resolves. A link
2079
2246
  // that already points at the right place is correct even when the
@@ -2804,10 +2971,17 @@ export function startDaemon(config = {}, deps = {}) {
2804
2971
  };
2805
2972
  const advertisedCapabilities = [
2806
2973
  "project_path_validation",
2974
+ "project_agents_registry",
2807
2975
  "restart_daemon",
2808
2976
  "refresh_session_inplace",
2977
+ "task_attachments_v1",
2809
2978
  CUSTOM_COMMANDS_CAPABILITY,
2810
2979
  ];
2980
+ if (remoteExecEnabled) {
2981
+ advertisedCapabilities.push(REMOTE_EXEC_CAPABILITY);
2982
+ } else {
2983
+ log("[remote-exec] Disabled by config (remote_exec: false); capability not advertised");
2984
+ }
2811
2985
  if (ptyTaskCapabilityEnabled) {
2812
2986
  advertisedCapabilities.push("pty_task", "terminal_snapshot");
2813
2987
  }
@@ -2816,6 +2990,9 @@ export function startDaemon(config = {}, deps = {}) {
2816
2990
  }
2817
2991
  const aiManagerHandlers = createAiManagerHandlers({ configPath: effectiveConfigPath });
2818
2992
  const customCommandHandlers = createCustomCommandHandlers({ configPath: effectiveConfigPath });
2993
+ const remoteExecHandlers = remoteExecEnabled
2994
+ ? createRemoteExecHandlers({ defaultWorkspace: homeDir })
2995
+ : null;
2819
2996
 
2820
2997
  const client = createWebSocketClient(sdkConfig, {
2821
2998
  extraHeaders,
@@ -5027,6 +5204,11 @@ export function startDaemon(config = {}, deps = {}) {
5027
5204
  }
5028
5205
  if (event.type === "validate_project_path") {
5029
5206
  void handleValidateProjectPath(event.payload);
5207
+ return;
5208
+ }
5209
+ if (event.type === "get_project_agents") {
5210
+ void handleGetProjectAgents(event.payload);
5211
+ return;
5030
5212
  }
5031
5213
  if (event.type === "ai_manager_request") {
5032
5214
  handleAiManagerRequest(client, aiManagerHandlers, event.payload).catch((error) => {
@@ -5038,6 +5220,40 @@ export function startDaemon(config = {}, deps = {}) {
5038
5220
  logError(`Unhandled custom_commands_request failure: ${error?.message || error}`);
5039
5221
  });
5040
5222
  }
5223
+ if (event.type === "remote_exec_request") {
5224
+ // Remote exec is the only execution path that leaves no Task row and no
5225
+ // per-run log file, so record it here — otherwise a run is invisible
5226
+ // once the daemon restarts. argv is deliberately omitted: it routinely
5227
+ // carries secrets and this log is collected by `collect_logs`.
5228
+ const execArgs = event?.payload?.args && typeof event.payload.args === "object" ? event.payload.args : {};
5229
+ log(
5230
+ `[remote-exec] ${event?.payload?.action || "?"} command=${execArgs.command || ""} ` +
5231
+ `argc=${Array.isArray(execArgs.args) ? execArgs.args.length : 0} cwd=${execArgs.workspace || "<default>"}`,
5232
+ );
5233
+ // Fail fast rather than letting the caller wait out its full timeout —
5234
+ // it has no other way to learn that this daemon will never answer.
5235
+ const rejectReason = !remoteExecHandlers
5236
+ ? "remote exec is disabled on this daemon (remote_exec: false)"
5237
+ : daemonShuttingDown
5238
+ ? "daemon is shutting down"
5239
+ : "";
5240
+ if (rejectReason) {
5241
+ void client
5242
+ .sendJson({
5243
+ type: "remote_exec_response",
5244
+ payload: {
5245
+ request_id: event?.payload?.request_id ? String(event.payload.request_id) : "",
5246
+ action: event?.payload?.action ? String(event.payload.action) : "",
5247
+ error: rejectReason,
5248
+ },
5249
+ })
5250
+ .catch(() => {});
5251
+ return;
5252
+ }
5253
+ handleRemoteExecRequest(client, remoteExecHandlers, event.payload).catch((error) => {
5254
+ logError(`Unhandled remote_exec_request failure: ${error?.message || error}`);
5255
+ });
5256
+ }
5041
5257
  if (event.type === "restart_daemon") {
5042
5258
  void handleRestartDaemon(event.payload).catch((error) => {
5043
5259
  logError(`Unhandled restart_daemon failure: ${error?.message || error}`);
@@ -5132,6 +5348,7 @@ export function startDaemon(config = {}, deps = {}) {
5132
5348
  lastCommitAt: null,
5133
5349
  fileCount: null,
5134
5350
  icon: null,
5351
+ agents: [],
5135
5352
  error: null,
5136
5353
  errorCode: null,
5137
5354
  validatedAt,
@@ -5186,6 +5403,7 @@ export function startDaemon(config = {}, deps = {}) {
5186
5403
  ? snapshot.fileCount
5187
5404
  : null,
5188
5405
  icon: readProjectIconSetting(effectiveWorkspace),
5406
+ agents: readProjectAgentsSetting(effectiveWorkspace),
5189
5407
  };
5190
5408
  }
5191
5409
  } catch (error) {
@@ -5210,6 +5428,7 @@ export function startDaemon(config = {}, deps = {}) {
5210
5428
  git_remote_url: result.gitRemoteUrl,
5211
5429
  file_count: result.fileCount,
5212
5430
  icon: result.icon,
5431
+ agents: result.agents,
5213
5432
  error: result.error,
5214
5433
  error_code: result.errorCode,
5215
5434
  validated_at: result.validatedAt,
@@ -5220,6 +5439,69 @@ export function startDaemon(config = {}, deps = {}) {
5220
5439
  }
5221
5440
  }
5222
5441
 
5442
+ async function handleGetProjectAgents(payload) {
5443
+ const requestId = payload?.request_id ? String(payload.request_id).trim() : "";
5444
+ const rawWorkspacePath = payload?.workspace_path ? String(payload.workspace_path).trim() : "";
5445
+ const resolvedAt = new Date().toISOString();
5446
+
5447
+ if (!requestId || !rawWorkspacePath) {
5448
+ logError(`Invalid get_project_agents payload: ${JSON.stringify(payload)}`);
5449
+ return;
5450
+ }
5451
+
5452
+ let result = {
5453
+ workspacePath: null,
5454
+ agents: [],
5455
+ error: null,
5456
+ errorCode: null,
5457
+ };
5458
+ try {
5459
+ const resolvedPath = path.resolve(rawWorkspacePath);
5460
+ if (!existsSyncFn(resolvedPath)) {
5461
+ result = {
5462
+ ...result,
5463
+ error: `Workspace path does not exist on daemon ${AGENT_NAME}: ${rawWorkspacePath}`,
5464
+ errorCode: "workspace_not_found",
5465
+ };
5466
+ } else if (!statSyncFn(resolvedPath).isDirectory()) {
5467
+ result = {
5468
+ ...result,
5469
+ error: `Workspace path is not a directory on daemon ${AGENT_NAME}: ${rawWorkspacePath}`,
5470
+ errorCode: "workspace_not_directory",
5471
+ };
5472
+ } else {
5473
+ result = {
5474
+ ...result,
5475
+ workspacePath: resolvedPath,
5476
+ agents: readProjectAgentsSetting(resolvedPath),
5477
+ };
5478
+ }
5479
+ } catch (error) {
5480
+ result = {
5481
+ ...result,
5482
+ error: `Failed to read project agents on daemon ${AGENT_NAME}: ${error?.message || error}`,
5483
+ errorCode: "project_agents_read_failed",
5484
+ };
5485
+ }
5486
+
5487
+ try {
5488
+ await client.sendJson({
5489
+ type: "project_agents_resolved",
5490
+ payload: {
5491
+ request_id: requestId,
5492
+ daemon_host: AGENT_NAME,
5493
+ workspace_path: result.workspacePath,
5494
+ agents: result.agents,
5495
+ error: result.error,
5496
+ error_code: result.errorCode,
5497
+ resolved_at: resolvedAt,
5498
+ },
5499
+ });
5500
+ } catch (error) {
5501
+ logError(`Failed to report project_agents_resolved for ${rawWorkspacePath}: ${error?.message || error}`);
5502
+ }
5503
+ }
5504
+
5223
5505
  function stopActiveTaskProcess(
5224
5506
  taskId,
5225
5507
  {
@@ -6019,6 +6301,10 @@ export function startDaemon(config = {}, deps = {}) {
6019
6301
  PWD: taskDir,
6020
6302
  CONDUCTOR_PROJECT_ID: projectId,
6021
6303
  CONDUCTOR_TASK_ID: taskId,
6304
+ // Fire derives its own stable host identity from the owning daemon plus
6305
+ // the task. Passing the resolved name keeps that identity meaningful
6306
+ // even when the daemon was named through the config file.
6307
+ CONDUCTOR_DAEMON_NAME: AGENT_NAME,
6022
6308
  CONDUCTOR_LAUNCHED_BY_DAEMON: "1",
6023
6309
  ...(cliCommand ? { CONDUCTOR_CLI_COMMAND: cliCommand } : {}),
6024
6310
  };
@@ -6533,7 +6819,11 @@ export function startDaemon(config = {}, deps = {}) {
6533
6819
  resolvedResumeCwd = await resolveRestartCwd({
6534
6820
  taskId: normalizedTargetTaskId,
6535
6821
  projectId: normalizedProjectId,
6536
- backendType: effectiveBackend,
6822
+ // A fork starts a fresh target-backend session, but its workspace
6823
+ // still belongs to the source task. Resolve the source session in
6824
+ // its own provider namespace; target + source session id is not a
6825
+ // meaningful pair for cross-backend handoff.
6826
+ backendType: sourceBackendType,
6537
6827
  launchConfig: targetLaunchConfig,
6538
6828
  sessionId: normalizedSourceSessionId,
6539
6829
  sourceSessionFilePath: sourceSessionFilePath ? String(sourceSessionFilePath) : "",
@@ -6721,6 +7011,7 @@ export function startDaemon(config = {}, deps = {}) {
6721
7011
  PWD: taskDir,
6722
7012
  CONDUCTOR_PROJECT_ID: normalizedProjectId,
6723
7013
  CONDUCTOR_TASK_ID: normalizedTargetTaskId,
7014
+ CONDUCTOR_DAEMON_NAME: AGENT_NAME,
6724
7015
  CONDUCTOR_LAUNCHED_BY_DAEMON: "1",
6725
7016
  ...(cliCommand ? { CONDUCTOR_CLI_COMMAND: cliCommand } : {}),
6726
7017
  };
@@ -192,8 +192,14 @@ export async function resolveProject(apis, options = {}) {
192
192
  if (!apis.projects || typeof apis.projects.resolveProject !== "function") {
193
193
  throw new Error("ProjectsApi.resolveProject is not available in conductor-sdk");
194
194
  }
195
+ const explicitProject = String(options.project ?? "").trim();
196
+ if (explicitProject) {
197
+ if (typeof apis.projects.getProject !== "function") {
198
+ throw new Error("ProjectsApi.getProject is not available in conductor-sdk");
199
+ }
200
+ return apis.projects.getProject(explicitProject);
201
+ }
195
202
  return apis.projects.resolveProject({
196
- project: options.project,
197
203
  env: options.env || process.env,
198
204
  cwd: options.cwd || process.cwd(),
199
205
  });
@@ -332,11 +338,30 @@ export function emitDryRun(stream, json, payload) {
332
338
  }
333
339
  }
334
340
 
341
+ /**
342
+ * Pull a human-readable reason out of a backend error payload. Backend routes
343
+ * respond with `{ error }` (occasionally `{ message }`), which the SDK attaches
344
+ * to BackendApiError.details. Surfacing it turns an opaque "Backend responded
345
+ * with 409" into the actual cause (e.g. "Project daemon X is offline").
346
+ */
347
+ function backendErrorDetail(error) {
348
+ const details = error && typeof error === "object" ? error.details : null;
349
+ if (!details) return null;
350
+ if (typeof details === "string") return details.trim() || null;
351
+ if (typeof details === "object") {
352
+ const reason = details.error ?? details.message;
353
+ if (typeof reason === "string" && reason.trim()) return reason.trim();
354
+ }
355
+ return null;
356
+ }
357
+
335
358
  /**
336
359
  * Translate an unknown error into a printable + exit-coded form.
337
360
  */
338
361
  export function reportError(consoleErr, error) {
339
362
  const message = error instanceof Error ? error.message : String(error);
340
- consoleErr.error(`Error: ${message}`);
363
+ const detail = backendErrorDetail(error);
364
+ const line = detail && detail !== message ? `${message}: ${detail}` : message;
365
+ consoleErr.error(`Error: ${line}`);
341
366
  return exitCodeForError(error);
342
367
  }