@love-moon/conductor-cli 0.7.7 → 0.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @love-moon/conductor-cli
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 959dd1d: Add project-configured worker and reviewer task groups, task-group discovery in
8
+ the SDK and `conductor task group`, and a lightweight daemon protocol for
9
+ refreshing the project agent registry.
10
+ - fe76139: Add `conductor task create` for creating app tasks with title, prompt, backend,
11
+ project resolution, and optional parent task-card grouping. App tasks now
12
+ require an online compatible daemon, and grouping results are exposed to
13
+ callers so partial success is visible without retrying task creation.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [959dd1d]
18
+ - Updated dependencies [fe76139]
19
+ - @love-moon/conductor-sdk@0.8.0
20
+ - @love-moon/ai-sdk@0.8.0
21
+
3
22
  ## 0.7.7
4
23
 
5
24
  ### Patch Changes
@@ -5,7 +5,10 @@
5
5
  *
6
6
  * Subcommands:
7
7
  * list [--project ...] [--issue <id>] [--status ...]
8
+ * create --title <title> [--prompt <prompt>] [--backend <backend>]
9
+ * [--parent-task-id <id>] [--project ...]
8
10
  * show <id>
11
+ * group [<id>]
9
12
  * send <id> [<message>] [--stdin] [--from-file FILE] [--metadata-json '{...}']
10
13
  * insert <id> [<message>] [--stdin] [--from-file FILE] [--target-reply-to <msg-id>]
11
14
  * messages <id> [--limit N] [--before <msg-id>]
@@ -63,6 +66,7 @@ function taskAsObject(task) {
63
66
  sessionId: task.sessionId,
64
67
  createdAt: task.createdAt,
65
68
  updatedAt: task.updatedAt,
69
+ grouping: task.grouping ?? undefined,
66
70
  };
67
71
  }
68
72
 
@@ -243,6 +247,54 @@ async function handleList(argv, deps) {
243
247
  return EXIT.OK;
244
248
  }
245
249
 
250
+ async function handleCreate(argv, deps) {
251
+ const apis = await buildApis(deps);
252
+ const project = await resolveProject(apis, {
253
+ env: deps.env,
254
+ cwd: deps.cwd,
255
+ project: argv.project,
256
+ });
257
+ const title = String(argv.title ?? "").trim();
258
+ if (!title) {
259
+ const err = new Error("--title must not be empty");
260
+ err.code = "ARGS";
261
+ throw err;
262
+ }
263
+
264
+ const body = {
265
+ projectId: project.id,
266
+ title,
267
+ taskType: "ai_task",
268
+ ...(argv.prompt !== undefined ? { initialContent: String(argv.prompt) } : {}),
269
+ ...(argv.backend ? { backendType: String(argv.backend) } : {}),
270
+ ...(argv.parentTaskId ? { parentTaskId: String(argv.parentTaskId) } : {}),
271
+ metadata: buildAuditMetadata(deps.env),
272
+ };
273
+ if (argv.dryRun) {
274
+ emitDryRun(
275
+ deps.stdout,
276
+ argv.json,
277
+ makeDryRunPayload("POST", `${buildBaseUrl(apis.config)}/api/tasks`, body),
278
+ );
279
+ return EXIT.OK;
280
+ }
281
+
282
+ const created = await apis.tasks.createTask(body);
283
+ const obj = taskAsObject(created);
284
+ if (argv.json) {
285
+ printJson(deps.stdout, obj);
286
+ return EXIT.OK;
287
+ }
288
+ printPretty(deps.stdout, `Created app task ${obj.id}: ${obj.title}`);
289
+ if (obj.grouping?.grouped === false) {
290
+ printPretty(
291
+ deps.stderr,
292
+ `Warning: ${obj.grouping.warning || `task was not grouped with ${obj.grouping.parentTaskId}`}. The task itself was created successfully.`,
293
+ );
294
+ }
295
+ return EXIT.OK;
296
+ }
297
+
246
298
  async function handleShow(argv, deps) {
247
299
  const apis = await buildApis(deps);
248
300
  const task = await apis.tasks.getTask(argv.id);
@@ -267,6 +319,45 @@ async function handleShow(argv, deps) {
267
319
  return EXIT.OK;
268
320
  }
269
321
 
322
+ async function handleGroup(argv, deps) {
323
+ const apis = await buildApis(deps);
324
+ const taskId =
325
+ (argv.id && String(argv.id).trim()) ||
326
+ (deps.env.CONDUCTOR_TASK_ID && String(deps.env.CONDUCTOR_TASK_ID).trim());
327
+ if (!taskId) {
328
+ const err = new Error(
329
+ "No task id: pass <id> or run inside a task (CONDUCTOR_TASK_ID)",
330
+ );
331
+ err.statusCode = 400;
332
+ throw err;
333
+ }
334
+ const group = await apis.tasks.getTaskGroup(taskId);
335
+ if (argv.json) {
336
+ printJson(deps.stdout, group);
337
+ return EXIT.OK;
338
+ }
339
+ if (!group.groupId) {
340
+ printPretty(deps.stdout, "(task is not in a group)");
341
+ return EXIT.OK;
342
+ }
343
+ printPretty(deps.stdout, `group ${group.groupId}`);
344
+ printPretty(
345
+ deps.stdout,
346
+ `${pad("ROLE", 10)} ${pad("TASK ID", 24)} ${pad("AGENT", 20)} STATUS`,
347
+ );
348
+ for (const member of group.members) {
349
+ const selfMark = member.isSelf ? " (you)" : "";
350
+ printPretty(
351
+ deps.stdout,
352
+ `${pad(member.role || "?", 10)} ${pad(member.taskId, 24)} ${pad(
353
+ member.agent || "-",
354
+ 20,
355
+ )} ${member.status || ""}${selfMark}`,
356
+ );
357
+ }
358
+ return EXIT.OK;
359
+ }
360
+
270
361
  async function handleSend(argv, deps) {
271
362
  const apis = await buildApis(deps);
272
363
  const content = readMessageInput({
@@ -485,6 +576,31 @@ export async function main(argvInput = hideBin(process.argv), deps = {}) {
485
576
  exitCode = await handleList(argv, { ...handlerDeps, configFile: argv.configFile });
486
577
  },
487
578
  )
579
+ .command(
580
+ "create",
581
+ "Create a new app task through the frontend task pipeline",
582
+ (cmd) => cmd
583
+ .option("title", {
584
+ type: "string",
585
+ demandOption: true,
586
+ describe: "Task title",
587
+ })
588
+ .option("prompt", {
589
+ type: "string",
590
+ describe: "Initial user prompt",
591
+ })
592
+ .option("backend", {
593
+ type: "string",
594
+ describe: "AI backend type, for example codex or claude",
595
+ })
596
+ .option("parent-task-id", {
597
+ type: "string",
598
+ describe: "Display the new task in the same task-card group as this task",
599
+ }),
600
+ async (argv) => {
601
+ exitCode = await handleCreate(argv, { ...handlerDeps, configFile: argv.configFile });
602
+ },
603
+ )
488
604
  .command(
489
605
  "show <id>",
490
606
  "Show one task's detail",
@@ -493,6 +609,14 @@ export async function main(argvInput = hideBin(process.argv), deps = {}) {
493
609
  exitCode = await handleShow(argv, { ...handlerDeps, configFile: argv.configFile });
494
610
  },
495
611
  )
612
+ .command(
613
+ "group [id]",
614
+ "Show the multi-agent group a task belongs to (defaults to $CONDUCTOR_TASK_ID)",
615
+ (cmd) => cmd.positional("id", { type: "string" }),
616
+ async (argv) => {
617
+ exitCode = await handleGroup(argv, { ...handlerDeps, configFile: argv.configFile });
618
+ },
619
+ )
496
620
  .command(
497
621
  "send <id> [message]",
498
622
  "Send a user message into a running task",
package/bin/conductor.js CHANGED
@@ -14,7 +14,7 @@
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
18
  */
19
19
 
20
20
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -134,7 +134,7 @@ Subcommands:
134
134
  serve-ai Start an OpenAI-compatible local AI server
135
135
  project Manage Conductor projects (list/show/create/...)
136
136
  issue Manage issues (list/show/create/update/start/done)
137
- task Manage tasks (list/show/send/messages/schedule)
137
+ task Manage tasks (create/list/show/send/messages/schedule)
138
138
 
139
139
  Options:
140
140
  -h, --help Show this help message
@@ -156,6 +156,7 @@ Examples:
156
156
  conductor update
157
157
  conductor project list
158
158
  conductor issue create --title "Refactor module" --priority P2
159
+ conductor task create --title "Refactor module" --prompt "Extract the parser" --backend codex
159
160
  conductor task send <task-id> "please add a unit test"
160
161
  conductor task schedule create <task-id> "follow up" --delay 10m
161
162
 
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.8.0",
4
+ "gitCommitId": "04d4f62",
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.8.0",
28
+ "@love-moon/conductor-sdk": "0.8.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.8.0"
42
42
  },
43
43
  "pnpm": {
44
44
  "onlyBuiltDependencies": [
package/src/daemon.js CHANGED
@@ -1843,6 +1843,14 @@ export function startDaemon(config = {}, deps = {}) {
1843
1843
  "# (svg/png/jpg/gif/webp/ico/avif) relative to this .conductor/ directory.",
1844
1844
  "# Remove this line to use the default folder icon.",
1845
1845
  "",
1846
+ "# Optional agent registry used by the task-composer worker/reviewer picker.",
1847
+ "# Agent docs are workspace-relative paths; backend is optional.",
1848
+ "# agents:",
1849
+ "# feature-dev:",
1850
+ "# doc: claw/agents/feature-dev.md",
1851
+ "# description: Implements features end to end.",
1852
+ "# backend: codex",
1853
+ "",
1846
1854
  "worktree:",
1847
1855
  " sync_branch: false",
1848
1856
  " sync_submodules: true",
@@ -1856,6 +1864,13 @@ export function startDaemon(config = {}, deps = {}) {
1856
1864
  ].join("\n");
1857
1865
 
1858
1866
  const MAX_PROJECT_ICON_IMAGE_BYTES = 128 * 1024;
1867
+ const MAX_PROJECT_AGENT_ENTRIES = 64;
1868
+ const MAX_PROJECT_AGENT_NAME_LENGTH = 64;
1869
+ const MAX_PROJECT_AGENT_DOC_LENGTH = 512;
1870
+ const MAX_PROJECT_AGENT_DESCRIPTION_LENGTH = 512;
1871
+ const MAX_PROJECT_AGENT_BACKEND_LENGTH = 64;
1872
+ const PROJECT_AGENT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1873
+ const PROJECT_AGENT_BACKEND_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1859
1874
  const PROJECT_ICON_MIME_BY_EXTENSION = {
1860
1875
  ".svg": "image/svg+xml",
1861
1876
  ".png": "image/png",
@@ -1954,6 +1969,118 @@ export function startDaemon(config = {}, deps = {}) {
1954
1969
  return null;
1955
1970
  }
1956
1971
 
1972
+ function normalizeProjectAgentSetting(name, value) {
1973
+ const normalizedName = normalizeOptionalString(name);
1974
+ if (
1975
+ !normalizedName ||
1976
+ normalizedName.length > MAX_PROJECT_AGENT_NAME_LENGTH ||
1977
+ !PROJECT_AGENT_NAME_RE.test(normalizedName)
1978
+ ) {
1979
+ return null;
1980
+ }
1981
+
1982
+ let docValue = null;
1983
+ let descriptionValue = null;
1984
+ let backendValue = null;
1985
+ if (typeof value === "string") {
1986
+ docValue = value;
1987
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
1988
+ docValue = value.doc ?? value.path;
1989
+ descriptionValue = value.description;
1990
+ backendValue = value.backend;
1991
+ } else {
1992
+ return null;
1993
+ }
1994
+
1995
+ const doc = normalizeOptionalString(docValue);
1996
+ if (
1997
+ !doc ||
1998
+ doc.length > MAX_PROJECT_AGENT_DOC_LENGTH ||
1999
+ path.isAbsolute(doc) ||
2000
+ /^[A-Za-z]:[\\/]/.test(doc) ||
2001
+ /^[/\\]{2}/.test(doc)
2002
+ ) {
2003
+ return null;
2004
+ }
2005
+ const normalizedDoc = path.posix.normalize(doc.split("\\").join("/"));
2006
+ if (normalizedDoc === ".." || normalizedDoc.startsWith("../")) {
2007
+ return null;
2008
+ }
2009
+
2010
+ const description = normalizeOptionalString(descriptionValue);
2011
+ const backend = normalizeOptionalString(backendValue)?.toLowerCase() || null;
2012
+ return {
2013
+ name: normalizedName,
2014
+ doc,
2015
+ description:
2016
+ description && description.length <= MAX_PROJECT_AGENT_DESCRIPTION_LENGTH
2017
+ ? description
2018
+ : null,
2019
+ backend:
2020
+ backend &&
2021
+ backend.length <= MAX_PROJECT_AGENT_BACKEND_LENGTH &&
2022
+ PROJECT_AGENT_BACKEND_RE.test(backend)
2023
+ ? backend
2024
+ : null,
2025
+ };
2026
+ }
2027
+
2028
+ function normalizeProjectAgentsSetting(agentsNode) {
2029
+ if (!agentsNode || typeof agentsNode !== "object") {
2030
+ return [];
2031
+ }
2032
+ const result = [];
2033
+ const seen = new Set();
2034
+ const push = (entry) => {
2035
+ if (
2036
+ entry &&
2037
+ result.length < MAX_PROJECT_AGENT_ENTRIES &&
2038
+ !seen.has(entry.name)
2039
+ ) {
2040
+ seen.add(entry.name);
2041
+ result.push(entry);
2042
+ }
2043
+ };
2044
+
2045
+ if (Array.isArray(agentsNode)) {
2046
+ for (const item of agentsNode) {
2047
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
2048
+ continue;
2049
+ }
2050
+ const keys = Object.keys(item);
2051
+ if (keys.length === 1) {
2052
+ push(normalizeProjectAgentSetting(keys[0], item[keys[0]]));
2053
+ } else if (Object.prototype.hasOwnProperty.call(item, "name")) {
2054
+ push(normalizeProjectAgentSetting(item.name, item));
2055
+ }
2056
+ }
2057
+ return result;
2058
+ }
2059
+
2060
+ for (const [name, value] of Object.entries(agentsNode)) {
2061
+ push(normalizeProjectAgentSetting(name, value));
2062
+ }
2063
+ return result;
2064
+ }
2065
+
2066
+ function readProjectAgentsSetting(projectWorkspacePath) {
2067
+ for (const settingsPath of getProjectSettingsCandidates(projectWorkspacePath)) {
2068
+ if (!existsSyncFn(settingsPath)) {
2069
+ continue;
2070
+ }
2071
+ try {
2072
+ const parsed = yaml.load(readFileSyncFn(settingsPath, "utf8"));
2073
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2074
+ return [];
2075
+ }
2076
+ return normalizeProjectAgentsSetting(parsed.agents);
2077
+ } catch {
2078
+ return [];
2079
+ }
2080
+ }
2081
+ return [];
2082
+ }
2083
+
1957
2084
  function readProjectWorktreeSettings(projectWorkspacePath) {
1958
2085
  for (const settingsPath of getProjectSettingsCandidates(projectWorkspacePath)) {
1959
2086
  if (!existsSyncFn(settingsPath)) {
@@ -2804,6 +2931,7 @@ export function startDaemon(config = {}, deps = {}) {
2804
2931
  };
2805
2932
  const advertisedCapabilities = [
2806
2933
  "project_path_validation",
2934
+ "project_agents_registry",
2807
2935
  "restart_daemon",
2808
2936
  "refresh_session_inplace",
2809
2937
  CUSTOM_COMMANDS_CAPABILITY,
@@ -5027,6 +5155,11 @@ export function startDaemon(config = {}, deps = {}) {
5027
5155
  }
5028
5156
  if (event.type === "validate_project_path") {
5029
5157
  void handleValidateProjectPath(event.payload);
5158
+ return;
5159
+ }
5160
+ if (event.type === "get_project_agents") {
5161
+ void handleGetProjectAgents(event.payload);
5162
+ return;
5030
5163
  }
5031
5164
  if (event.type === "ai_manager_request") {
5032
5165
  handleAiManagerRequest(client, aiManagerHandlers, event.payload).catch((error) => {
@@ -5132,6 +5265,7 @@ export function startDaemon(config = {}, deps = {}) {
5132
5265
  lastCommitAt: null,
5133
5266
  fileCount: null,
5134
5267
  icon: null,
5268
+ agents: [],
5135
5269
  error: null,
5136
5270
  errorCode: null,
5137
5271
  validatedAt,
@@ -5186,6 +5320,7 @@ export function startDaemon(config = {}, deps = {}) {
5186
5320
  ? snapshot.fileCount
5187
5321
  : null,
5188
5322
  icon: readProjectIconSetting(effectiveWorkspace),
5323
+ agents: readProjectAgentsSetting(effectiveWorkspace),
5189
5324
  };
5190
5325
  }
5191
5326
  } catch (error) {
@@ -5210,6 +5345,7 @@ export function startDaemon(config = {}, deps = {}) {
5210
5345
  git_remote_url: result.gitRemoteUrl,
5211
5346
  file_count: result.fileCount,
5212
5347
  icon: result.icon,
5348
+ agents: result.agents,
5213
5349
  error: result.error,
5214
5350
  error_code: result.errorCode,
5215
5351
  validated_at: result.validatedAt,
@@ -5220,6 +5356,69 @@ export function startDaemon(config = {}, deps = {}) {
5220
5356
  }
5221
5357
  }
5222
5358
 
5359
+ async function handleGetProjectAgents(payload) {
5360
+ const requestId = payload?.request_id ? String(payload.request_id).trim() : "";
5361
+ const rawWorkspacePath = payload?.workspace_path ? String(payload.workspace_path).trim() : "";
5362
+ const resolvedAt = new Date().toISOString();
5363
+
5364
+ if (!requestId || !rawWorkspacePath) {
5365
+ logError(`Invalid get_project_agents payload: ${JSON.stringify(payload)}`);
5366
+ return;
5367
+ }
5368
+
5369
+ let result = {
5370
+ workspacePath: null,
5371
+ agents: [],
5372
+ error: null,
5373
+ errorCode: null,
5374
+ };
5375
+ try {
5376
+ const resolvedPath = path.resolve(rawWorkspacePath);
5377
+ if (!existsSyncFn(resolvedPath)) {
5378
+ result = {
5379
+ ...result,
5380
+ error: `Workspace path does not exist on daemon ${AGENT_NAME}: ${rawWorkspacePath}`,
5381
+ errorCode: "workspace_not_found",
5382
+ };
5383
+ } else if (!statSyncFn(resolvedPath).isDirectory()) {
5384
+ result = {
5385
+ ...result,
5386
+ error: `Workspace path is not a directory on daemon ${AGENT_NAME}: ${rawWorkspacePath}`,
5387
+ errorCode: "workspace_not_directory",
5388
+ };
5389
+ } else {
5390
+ result = {
5391
+ ...result,
5392
+ workspacePath: resolvedPath,
5393
+ agents: readProjectAgentsSetting(resolvedPath),
5394
+ };
5395
+ }
5396
+ } catch (error) {
5397
+ result = {
5398
+ ...result,
5399
+ error: `Failed to read project agents on daemon ${AGENT_NAME}: ${error?.message || error}`,
5400
+ errorCode: "project_agents_read_failed",
5401
+ };
5402
+ }
5403
+
5404
+ try {
5405
+ await client.sendJson({
5406
+ type: "project_agents_resolved",
5407
+ payload: {
5408
+ request_id: requestId,
5409
+ daemon_host: AGENT_NAME,
5410
+ workspace_path: result.workspacePath,
5411
+ agents: result.agents,
5412
+ error: result.error,
5413
+ error_code: result.errorCode,
5414
+ resolved_at: resolvedAt,
5415
+ },
5416
+ });
5417
+ } catch (error) {
5418
+ logError(`Failed to report project_agents_resolved for ${rawWorkspacePath}: ${error?.message || error}`);
5419
+ }
5420
+ }
5421
+
5223
5422
  function stopActiveTaskProcess(
5224
5423
  taskId,
5225
5424
  {
@@ -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
  }