conductor-oss 0.2.18 → 0.2.20

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.
Files changed (187) hide show
  1. package/dist/commands/dashboard.js +2 -2
  2. package/dist/commands/start.d.ts +3 -6
  3. package/dist/commands/start.d.ts.map +1 -1
  4. package/dist/commands/start.js +450 -220
  5. package/dist/commands/start.js.map +1 -1
  6. package/dist/index.js +0 -4
  7. package/dist/index.js.map +1 -1
  8. package/native/conductor +0 -0
  9. package/node_modules/@conductor-oss/core/dist/session-manager.js +1 -1
  10. package/node_modules/@conductor-oss/core/dist/session-manager.js.map +1 -1
  11. package/node_modules/@conductor-oss/plugin-agent-amp/package.json +1 -1
  12. package/node_modules/@conductor-oss/plugin-agent-ccr/package.json +1 -1
  13. package/node_modules/@conductor-oss/plugin-agent-claude-code/package.json +1 -1
  14. package/node_modules/@conductor-oss/plugin-agent-codex/package.json +1 -1
  15. package/node_modules/@conductor-oss/plugin-agent-cursor-cli/package.json +1 -1
  16. package/node_modules/@conductor-oss/plugin-agent-droid/package.json +1 -1
  17. package/node_modules/@conductor-oss/plugin-agent-gemini/package.json +1 -1
  18. package/node_modules/@conductor-oss/plugin-agent-github-copilot/package.json +1 -1
  19. package/node_modules/@conductor-oss/plugin-agent-opencode/package.json +1 -1
  20. package/node_modules/@conductor-oss/plugin-agent-qwen-code/package.json +1 -1
  21. package/node_modules/@conductor-oss/plugin-mcp-server/package.json +1 -1
  22. package/node_modules/@conductor-oss/plugin-notifier-desktop/package.json +1 -1
  23. package/node_modules/@conductor-oss/plugin-notifier-discord/package.json +1 -1
  24. package/node_modules/@conductor-oss/plugin-runtime-tmux/package.json +1 -1
  25. package/node_modules/@conductor-oss/plugin-scm-github/package.json +1 -1
  26. package/node_modules/@conductor-oss/plugin-terminal-web/package.json +1 -1
  27. package/node_modules/@conductor-oss/plugin-tracker-github/package.json +1 -1
  28. package/node_modules/@conductor-oss/plugin-workspace-worktree/package.json +1 -1
  29. package/package.json +21 -22
  30. package/web/.next/standalone/packages/web/.next/BUILD_ID +1 -1
  31. package/web/.next/standalone/packages/web/.next/app-path-routes-manifest.json +0 -1
  32. package/web/.next/standalone/packages/web/.next/build-manifest.json +2 -2
  33. package/web/.next/standalone/packages/web/.next/prerender-manifest.json +3 -3
  34. package/web/.next/standalone/packages/web/.next/routes-manifest.json +0 -6
  35. package/web/.next/standalone/packages/web/.next/server/app/_global-error.html +2 -2
  36. package/web/.next/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
  37. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  38. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  39. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  40. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  41. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  42. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page/server-reference-manifest.json +7 -7
  43. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  44. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  45. package/web/.next/standalone/packages/web/.next/server/app/_not-found.html +1 -1
  46. package/web/.next/standalone/packages/web/.next/server/app/_not-found.rsc +3 -3
  47. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  48. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  49. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  50. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  51. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  52. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  53. package/web/.next/standalone/packages/web/.next/server/app/api/access/route.js.nft.json +1 -1
  54. package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route.js.nft.json +1 -1
  55. package/web/.next/standalone/packages/web/.next/server/app/api/boards/route.js.nft.json +1 -1
  56. package/web/.next/standalone/packages/web/.next/server/app/api/config/route.js.nft.json +1 -1
  57. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route.js.nft.json +1 -1
  58. package/web/.next/standalone/packages/web/.next/server/app/api/events/route.js.nft.json +1 -1
  59. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route.js.nft.json +1 -1
  60. package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route.js.nft.json +1 -1
  61. package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route.js.nft.json +1 -1
  62. package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route.js.nft.json +1 -1
  63. package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route.js.nft.json +1 -1
  64. package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route.js.nft.json +1 -1
  65. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route.js.nft.json +1 -1
  66. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route.js.nft.json +1 -1
  67. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route.js.nft.json +1 -1
  68. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route.js.nft.json +1 -1
  69. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feed/route.js.nft.json +1 -1
  70. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route.js.nft.json +1 -1
  71. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route.js.nft.json +1 -1
  72. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route.js.nft.json +1 -1
  73. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route.js.nft.json +1 -1
  74. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route.js.nft.json +1 -1
  75. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route.js.nft.json +1 -1
  76. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -1
  77. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  78. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route.js.nft.json +1 -1
  79. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route.js.nft.json +1 -1
  80. package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route.js.nft.json +1 -1
  81. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route.js.nft.json +1 -1
  82. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js +4 -4
  83. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  84. package/web/.next/standalone/packages/web/.next/server/app/index.html +1 -1
  85. package/web/.next/standalone/packages/web/.next/server/app/index.rsc +4 -4
  86. package/web/.next/standalone/packages/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  87. package/web/.next/standalone/packages/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
  88. package/web/.next/standalone/packages/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  89. package/web/.next/standalone/packages/web/.next/server/app/index.segments/_index.segment.rsc +3 -3
  90. package/web/.next/standalone/packages/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  91. package/web/.next/standalone/packages/web/.next/server/app/page/server-reference-manifest.json +7 -7
  92. package/web/.next/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
  93. package/web/.next/standalone/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
  94. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page/server-reference-manifest.json +7 -7
  95. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  96. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  97. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page/server-reference-manifest.json +7 -7
  98. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page.js.nft.json +1 -1
  99. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page_client-reference-manifest.js +1 -1
  100. package/web/.next/standalone/packages/web/.next/server/app/unlock/page/server-reference-manifest.json +7 -7
  101. package/web/.next/standalone/packages/web/.next/server/app/unlock/page.js.nft.json +1 -1
  102. package/web/.next/standalone/packages/web/.next/server/app/unlock/page_client-reference-manifest.js +1 -1
  103. package/web/.next/standalone/packages/web/.next/server/app-paths-manifest.json +0 -1
  104. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1ed2e6c1._.js +1 -1
  105. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__3d6b30a3._.js +1 -1
  106. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__a0b6570d._.js +3 -0
  107. package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0e4dc4f7.js +2 -1
  108. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__12eb9005._.js → [root-of-the-server]__48817f02._.js} +2 -2
  109. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__6622b514._.js +1 -1
  110. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__869d9ac0._.js +1 -1
  111. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__9dc23e5a._.js +1 -1
  112. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_0e1412de._.js +1 -1
  113. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_69e05fca._.js +1 -1
  114. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_80efe193._.js +1 -1
  115. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_b6d31783._.js +1 -1
  116. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_c0f0e227._.js +1 -1
  117. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_f36ddaa9._.js +1 -1
  118. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_38e7b35d._.js +3 -0
  119. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_f665760b._.js +3 -0
  120. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_f2ebd7a9._.js +1 -1
  121. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_79316445._.js +1 -1
  122. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_a078c137._.js +1 -1
  123. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_app_page_tsx_cd282e82._.js +1 -1
  124. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{packages_web_src_components_3809c507._.js → packages_web_src_components_6cab1c8c._.js} +1 -1
  125. package/web/.next/standalone/packages/web/.next/server/functions-config-manifest.json +1 -2
  126. package/web/.next/standalone/packages/web/.next/server/pages/404.html +1 -1
  127. package/web/.next/standalone/packages/web/.next/server/pages/500.html +2 -2
  128. package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
  129. package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.json +8 -8
  130. package/web/.next/standalone/packages/web/.next/static/chunks/062888342200567f.js +1 -0
  131. package/web/.next/{static/chunks/28dd6ef2af62b509.js → standalone/packages/web/.next/static/chunks/3b757dce050133c8.js} +2 -2
  132. package/web/.next/standalone/packages/web/.next/static/chunks/{c959976264f14eba.js → 5c7e8425945ad682.js} +1 -1
  133. package/web/.next/standalone/packages/web/.next/static/chunks/6c9a11faed9daf4d.js +1 -0
  134. package/web/.next/standalone/packages/web/.next/static/chunks/8221b78965a50858.js +1 -0
  135. package/web/.next/standalone/packages/web/.next/static/chunks/8de84e208e201d72.css +3 -0
  136. package/web/.next/standalone/packages/web/.next/static/chunks/{e862e73b22fe29c2.js → c4ea57fb949fb623.js} +1 -1
  137. package/web/.next/standalone/packages/web/src/components/layout/TopBar.tsx +8 -10
  138. package/web/.next/standalone/packages/web/src/features/dashboard/DashboardClient.tsx +32 -16
  139. package/web/.next/standalone/packages/web/src/features/sessions/SessionPageClient.tsx +12 -2
  140. package/web/.next/standalone/packages/web/src/proxy.ts +1 -0
  141. package/web/.next/static/chunks/062888342200567f.js +1 -0
  142. package/web/.next/{standalone/packages/web/.next/static/chunks/28dd6ef2af62b509.js → static/chunks/3b757dce050133c8.js} +2 -2
  143. package/web/.next/static/chunks/{c959976264f14eba.js → 5c7e8425945ad682.js} +1 -1
  144. package/web/.next/static/chunks/6c9a11faed9daf4d.js +1 -0
  145. package/web/.next/static/chunks/8221b78965a50858.js +1 -0
  146. package/web/.next/static/chunks/8de84e208e201d72.css +3 -0
  147. package/web/.next/static/chunks/{e862e73b22fe29c2.js → c4ea57fb949fb623.js} +1 -1
  148. package/dist/commands/watch.d.ts +0 -12
  149. package/dist/commands/watch.d.ts.map +0 -1
  150. package/dist/commands/watch.js +0 -84
  151. package/dist/commands/watch.js.map +0 -1
  152. package/dist/commands/webhook.d.ts +0 -12
  153. package/dist/commands/webhook.d.ts.map +0 -1
  154. package/dist/commands/webhook.js +0 -59
  155. package/dist/commands/webhook.js.map +0 -1
  156. package/node_modules/@conductor-oss/plugin-webhook/dist/index.d.ts +0 -28
  157. package/node_modules/@conductor-oss/plugin-webhook/dist/index.d.ts.map +0 -1
  158. package/node_modules/@conductor-oss/plugin-webhook/dist/index.js +0 -295
  159. package/node_modules/@conductor-oss/plugin-webhook/dist/index.js.map +0 -1
  160. package/node_modules/@conductor-oss/plugin-webhook/package.json +0 -11
  161. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route/app-paths-manifest.json +0 -3
  162. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route/build-manifest.json +0 -11
  163. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route/server-reference-manifest.json +0 -4
  164. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js +0 -8
  165. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js.map +0 -5
  166. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js.nft.json +0 -1
  167. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route_client-reference-manifest.js +0 -2
  168. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__7633d324._.js +0 -4
  169. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__bf8faac8._.js +0 -4
  170. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_agents_route_actions_29063d1a.js +0 -3
  171. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_78af9fdf._.js +0 -3
  172. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_c4bad84a._.js +0 -3
  173. package/web/.next/standalone/packages/web/.next/static/chunks/1e67fbc3874d3f51.js +0 -1
  174. package/web/.next/standalone/packages/web/.next/static/chunks/2b2a24dff50e7dc9.js +0 -1
  175. package/web/.next/standalone/packages/web/.next/static/chunks/483eb2824f5282c7.js +0 -1
  176. package/web/.next/standalone/packages/web/.next/static/chunks/860d84e1f09476a4.css +0 -3
  177. package/web/.next/standalone/packages/web/src/app/api/agents/route.ts +0 -223
  178. package/web/.next/static/chunks/1e67fbc3874d3f51.js +0 -1
  179. package/web/.next/static/chunks/2b2a24dff50e7dc9.js +0 -1
  180. package/web/.next/static/chunks/483eb2824f5282c7.js +0 -1
  181. package/web/.next/static/chunks/860d84e1f09476a4.css +0 -3
  182. /package/web/.next/standalone/packages/web/.next/static/{eSF3qxz6RT8UXiwr-uibJ → P8IBAo_woCzrGXJiYCZto}/_buildManifest.js +0 -0
  183. /package/web/.next/standalone/packages/web/.next/static/{eSF3qxz6RT8UXiwr-uibJ → P8IBAo_woCzrGXJiYCZto}/_clientMiddlewareManifest.json +0 -0
  184. /package/web/.next/standalone/packages/web/.next/static/{eSF3qxz6RT8UXiwr-uibJ → P8IBAo_woCzrGXJiYCZto}/_ssgManifest.js +0 -0
  185. /package/web/.next/static/{eSF3qxz6RT8UXiwr-uibJ → P8IBAo_woCzrGXJiYCZto}/_buildManifest.js +0 -0
  186. /package/web/.next/static/{eSF3qxz6RT8UXiwr-uibJ → P8IBAo_woCzrGXJiYCZto}/_clientMiddlewareManifest.json +0 -0
  187. /package/web/.next/static/{eSF3qxz6RT8UXiwr-uibJ → P8IBAo_woCzrGXJiYCZto}/_ssgManifest.js +0 -0
@@ -1,22 +1,19 @@
1
1
  /**
2
2
  * `co start`
3
3
  *
4
- * Starts the lifecycle manager and web dashboard.
5
- * Runs in the foreground -- designed for LaunchAgent / systemd usage.
6
- *
7
- * The lifecycle manager polls sessions periodically, advancing their
8
- * state machine (checking CI, reviews, merging, sending reactions, etc.).
9
- * The web dashboard provides a browser UI for monitoring and interaction.
4
+ * Starts the Rust backend and web dashboard in the foreground.
5
+ * The JS launcher is intentionally thin: it resolves paths, launches
6
+ * the Rust backend, and wires the frontend to it.
10
7
  */
11
8
  import { spawn, spawnSync } from "node:child_process";
12
9
  import { randomBytes } from "node:crypto";
13
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
11
  import { homedir } from "node:os";
15
- import { dirname, join, resolve } from "node:path";
12
+ import { basename, dirname, join, resolve } from "node:path";
16
13
  import chalk from "chalk";
17
14
  import ora from "ora";
15
+ import { parse as parseYaml } from "yaml";
18
16
  import { buildConductorBoard, buildConductorYaml } from "@conductor-oss/core";
19
- import { createServices, loadConfig } from "../services.js";
20
17
  function commandExists(command) {
21
18
  const checker = process.platform === "win32" ? "where" : "which";
22
19
  const result = spawnSync(checker, [command], { stdio: "ignore" });
@@ -46,14 +43,43 @@ function resolveBuiltinRemoteAuth(enabled) {
46
43
  sessionSecret,
47
44
  };
48
45
  }
46
+ function asObject(value) {
47
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
48
+ return {};
49
+ }
50
+ return value;
51
+ }
52
+ function asTrimmedString(value) {
53
+ if (typeof value !== "string")
54
+ return null;
55
+ const trimmed = value.trim();
56
+ return trimmed.length > 0 ? trimmed : null;
57
+ }
58
+ function asBoolean(value) {
59
+ return value === true;
60
+ }
61
+ function coercePort(value, fallback) {
62
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535) {
63
+ return value;
64
+ }
65
+ if (typeof value === "string") {
66
+ const parsed = parseInt(value.trim(), 10);
67
+ if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535) {
68
+ return parsed;
69
+ }
70
+ }
71
+ return fallback;
72
+ }
49
73
  function resolveTrustedHeaderAuth(config) {
74
+ const access = asObject(config["access"]);
75
+ const trustedHeaders = asObject(access["trustedHeaders"]);
50
76
  return {
51
- enabled: config.access?.trustedHeaders?.enabled === true,
52
- provider: config.access?.trustedHeaders?.provider === "generic" ? "generic" : "cloudflare-access",
53
- emailHeader: config.access?.trustedHeaders?.emailHeader?.trim() || "Cf-Access-Authenticated-User-Email",
54
- jwtHeader: config.access?.trustedHeaders?.jwtHeader?.trim() || "Cf-Access-Jwt-Assertion",
55
- teamDomain: config.access?.trustedHeaders?.teamDomain?.trim() || null,
56
- audience: config.access?.trustedHeaders?.audience?.trim() || null,
77
+ enabled: asBoolean(trustedHeaders["enabled"]),
78
+ provider: trustedHeaders["provider"] === "generic" ? "generic" : "cloudflare-access",
79
+ emailHeader: asTrimmedString(trustedHeaders["emailHeader"]) || "Cf-Access-Authenticated-User-Email",
80
+ jwtHeader: asTrimmedString(trustedHeaders["jwtHeader"]) || "Cf-Access-Jwt-Assertion",
81
+ teamDomain: asTrimmedString(trustedHeaders["teamDomain"]),
82
+ audience: asTrimmedString(trustedHeaders["audience"]),
57
83
  };
58
84
  }
59
85
  export function extractCloudflareTunnelUrl(output) {
@@ -162,13 +188,40 @@ async function waitForDashboard(url, timeoutMs = 15_000) {
162
188
  catch {
163
189
  // Dashboard is still starting.
164
190
  }
165
- await new Promise((resolve) => setTimeout(resolve, 500));
191
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 500));
166
192
  }
167
193
  return false;
168
194
  }
169
195
  function getDefaultLauncherWorkspace() {
170
196
  return resolve(homedir(), ".openclaw", "workspace");
171
197
  }
198
+ function resolveWorkspacePathHint(configHint) {
199
+ if (!configHint) {
200
+ return getDefaultLauncherWorkspace();
201
+ }
202
+ const resolved = resolve(configHint);
203
+ if (existsSync(resolved) && basename(resolved).match(/^conductor\.ya?ml$/i)) {
204
+ return dirname(resolved);
205
+ }
206
+ return resolved;
207
+ }
208
+ function findConfigFile(startDir) {
209
+ const baseDir = resolveWorkspacePathHint(startDir);
210
+ let currentDir = baseDir;
211
+ for (;;) {
212
+ for (const filename of ["conductor.yaml", "conductor.yml"]) {
213
+ const candidate = join(currentDir, filename);
214
+ if (existsSync(candidate)) {
215
+ return candidate;
216
+ }
217
+ }
218
+ const parentDir = dirname(currentDir);
219
+ if (parentDir === currentDir) {
220
+ return null;
221
+ }
222
+ currentDir = parentDir;
223
+ }
224
+ }
172
225
  function ensureDashboardBootstrapWorkspace() {
173
226
  const workspacePath = getDefaultLauncherWorkspace();
174
227
  const configPath = join(workspacePath, "conductor.yaml");
@@ -190,55 +243,279 @@ function ensureDashboardBootstrapWorkspace() {
190
243
  }
191
244
  return { workspacePath, configPath };
192
245
  }
246
+ function loadLauncherSettings(configHint) {
247
+ const explicitConfigPath = configHint && basename(configHint).match(/^conductor\.ya?ml$/i)
248
+ ? resolve(configHint)
249
+ : null;
250
+ const configPath = explicitConfigPath ?? findConfigFile(configHint ?? undefined);
251
+ if (!configPath) {
252
+ const bootstrap = ensureDashboardBootstrapWorkspace();
253
+ return {
254
+ workspacePath: bootstrap.workspacePath,
255
+ configPath: bootstrap.configPath,
256
+ dashboardPort: 4747,
257
+ backendPort: 4748,
258
+ access: {
259
+ requireAuth: false,
260
+ defaultRole: "operator",
261
+ trustedHeaders: resolveTrustedHeaderAuth({}),
262
+ },
263
+ };
264
+ }
265
+ const parsed = parseYaml(readFileSync(configPath, "utf8"));
266
+ const config = asObject(parsed);
267
+ const access = asObject(config["access"]);
268
+ const server = asObject(config["server"]);
269
+ return {
270
+ workspacePath: dirname(configPath),
271
+ configPath,
272
+ dashboardPort: 4747,
273
+ backendPort: coercePort(server["port"], 4748),
274
+ access: {
275
+ requireAuth: asBoolean(access["requireAuth"]),
276
+ defaultRole: asTrimmedString(access["defaultRole"]),
277
+ trustedHeaders: resolveTrustedHeaderAuth(config),
278
+ },
279
+ };
280
+ }
281
+ function resolveBackendPort(cliValue, configuredPort) {
282
+ const raw = cliValue?.trim() || process.env["CONDUCTOR_BACKEND_PORT"]?.trim();
283
+ if (!raw)
284
+ return configuredPort;
285
+ return coercePort(raw, configuredPort);
286
+ }
287
+ function resolveFrontendPort(cliValue, configuredPort) {
288
+ const raw = cliValue?.trim() || process.env["PORT"]?.trim();
289
+ if (!raw)
290
+ return configuredPort;
291
+ return coercePort(raw, configuredPort);
292
+ }
293
+ function resolveRepoCargoRoot(workspacePath) {
294
+ const candidate = resolve(workspacePath);
295
+ if (existsSync(join(candidate, "Cargo.toml"))
296
+ && existsSync(join(candidate, "crates", "conductor-cli", "Cargo.toml"))) {
297
+ return candidate;
298
+ }
299
+ return null;
300
+ }
301
+ function resolveBundledRustBinary() {
302
+ const binaryName = process.platform === "win32" ? "conductor.exe" : "conductor";
303
+ const cliDir = new URL(".", import.meta.url).pathname;
304
+ const candidates = [
305
+ resolve(cliDir, "..", "..", "native", binaryName),
306
+ resolve(cliDir, "..", "..", "..", "native", binaryName),
307
+ ];
308
+ for (const candidate of candidates) {
309
+ if (existsSync(candidate)) {
310
+ return candidate;
311
+ }
312
+ }
313
+ return null;
314
+ }
315
+ function detectNativeBinaryFormat(binaryPath) {
316
+ try {
317
+ const header = readFileSync(binaryPath).subarray(0, 4);
318
+ if (header.length >= 2 && header[0] === 0x4d && header[1] === 0x5a) {
319
+ return "pe";
320
+ }
321
+ if (header.length >= 4 && header[0] === 0x7f && header[1] === 0x45 && header[2] === 0x4c && header[3] === 0x46) {
322
+ return "elf";
323
+ }
324
+ if (header.length >= 4) {
325
+ const magic = header.readUInt32BE(0);
326
+ if (magic === 0xfeedface
327
+ || magic === 0xcefaedfe
328
+ || magic === 0xfeedfacf
329
+ || magic === 0xcffaedfe
330
+ || magic === 0xcafebabe
331
+ || magic === 0xbebafeca
332
+ || magic === 0xcafebabf) {
333
+ return "macho";
334
+ }
335
+ }
336
+ }
337
+ catch {
338
+ // ignore and treat as unknown
339
+ }
340
+ return "unknown";
341
+ }
342
+ function isCompatibleNativeBinary(binaryPath) {
343
+ const format = detectNativeBinaryFormat(binaryPath);
344
+ if (process.platform === "darwin")
345
+ return format === "macho";
346
+ if (process.platform === "linux")
347
+ return format === "elf";
348
+ if (process.platform === "win32")
349
+ return format === "pe";
350
+ return true;
351
+ }
352
+ function describeNativeBinaryHostMismatch(binaryPath) {
353
+ const format = detectNativeBinaryFormat(binaryPath);
354
+ return `Bundled Rust backend is incompatible with ${process.platform}-${process.arch} (binary format: ${format}).`;
355
+ }
356
+ function resolveRustBackendLaunch(workspacePath, configPath, backendPort) {
357
+ const bundledBinary = resolveBundledRustBinary();
358
+ if (bundledBinary) {
359
+ if (!isCompatibleNativeBinary(bundledBinary)) {
360
+ return {
361
+ launch: null,
362
+ reason: describeNativeBinaryHostMismatch(bundledBinary),
363
+ };
364
+ }
365
+ return {
366
+ launch: {
367
+ cmd: bundledBinary,
368
+ args: [
369
+ "--workspace",
370
+ workspacePath,
371
+ "--config",
372
+ configPath,
373
+ "start",
374
+ "--host",
375
+ "127.0.0.1",
376
+ "--port",
377
+ String(backendPort),
378
+ ],
379
+ cwd: workspacePath,
380
+ label: "bundled Rust backend",
381
+ },
382
+ };
383
+ }
384
+ const repoCargoRoot = resolveRepoCargoRoot(workspacePath);
385
+ if (!repoCargoRoot) {
386
+ return {
387
+ launch: null,
388
+ reason: "No compatible bundled Rust backend was found, and this install does not have a repo-local Cargo fallback.",
389
+ };
390
+ }
391
+ const binaryName = process.platform === "win32" ? "conductor.exe" : "conductor";
392
+ const prebuiltCandidates = [
393
+ join(repoCargoRoot, "target", "release", binaryName),
394
+ join(repoCargoRoot, "target", "debug", binaryName),
395
+ ];
396
+ for (const candidate of prebuiltCandidates) {
397
+ if (existsSync(candidate)) {
398
+ return {
399
+ launch: {
400
+ cmd: candidate,
401
+ args: [
402
+ "--workspace",
403
+ workspacePath,
404
+ "--config",
405
+ configPath,
406
+ "start",
407
+ "--host",
408
+ "127.0.0.1",
409
+ "--port",
410
+ String(backendPort),
411
+ ],
412
+ cwd: repoCargoRoot,
413
+ label: "prebuilt Rust backend",
414
+ },
415
+ };
416
+ }
417
+ }
418
+ return {
419
+ launch: {
420
+ cmd: "cargo",
421
+ args: [
422
+ "run",
423
+ "-p",
424
+ "conductor-cli",
425
+ "--",
426
+ "--workspace",
427
+ workspacePath,
428
+ "--config",
429
+ configPath,
430
+ "start",
431
+ "--host",
432
+ "127.0.0.1",
433
+ "--port",
434
+ String(backendPort),
435
+ ],
436
+ cwd: repoCargoRoot,
437
+ label: "cargo-run Rust backend",
438
+ },
439
+ };
440
+ }
441
+ async function waitForHttpService(url, timeoutMs = 15_000) {
442
+ const startedAt = Date.now();
443
+ while (Date.now() - startedAt < timeoutMs) {
444
+ try {
445
+ const response = await fetch(url, { redirect: "manual" });
446
+ if (response.ok || response.status < 500) {
447
+ return true;
448
+ }
449
+ }
450
+ catch {
451
+ // Service is still starting.
452
+ }
453
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 500));
454
+ }
455
+ return false;
456
+ }
457
+ async function killStalePortListener(port) {
458
+ try {
459
+ const { execSync } = await import("node:child_process");
460
+ const pids = execSync(`lsof -ti :${port} -sTCP:LISTEN 2>/dev/null`, { encoding: "utf8" }).trim();
461
+ if (!pids) {
462
+ return;
463
+ }
464
+ for (const pid of pids.split("\n").filter(Boolean)) {
465
+ if (pid !== String(process.pid)) {
466
+ process.kill(Number(pid), "SIGTERM");
467
+ console.log(`[port] Killed stale process ${pid} on port ${port}`);
468
+ }
469
+ }
470
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 1000));
471
+ }
472
+ catch {
473
+ // no stale process — expected
474
+ }
475
+ }
476
+ function resolveDashboardWebMode(mode) {
477
+ switch (mode?.trim().toLowerCase()) {
478
+ case "dev":
479
+ return "dev";
480
+ case "production":
481
+ case "prod":
482
+ return "production";
483
+ case "standalone":
484
+ return "standalone";
485
+ default:
486
+ return "auto";
487
+ }
488
+ }
193
489
  export function registerStart(program) {
194
490
  program
195
491
  .command("start")
196
- .description("Start lifecycle manager + board watcher + web dashboard (foreground)")
492
+ .description("Start the Rust backend and web dashboard (foreground)")
197
493
  .option("--no-dashboard", "Skip starting the web dashboard")
198
- .option("--no-watcher", "Skip starting the board watcher")
494
+ .option("--no-watcher", "Deprecated. Rust backend startup no longer uses the JS watcher")
199
495
  .option("--open", "Open the dashboard in your default browser")
200
496
  .option("--tunnel", "Expose the dashboard on a free public Cloudflare Quick Tunnel")
201
497
  .option("--host <host>", "Dashboard bind host. Defaults to 127.0.0.1 for local-only access")
202
498
  .option("-p, --port <port>", "Dashboard port override")
203
- .option("-w, --workspace <path>", "Obsidian workspace path")
499
+ .option("--no-backend", "Do not launch a separate local Rust backend")
500
+ .option("--backend-port <port>", "Rust backend port override (default: from config or 4748)")
501
+ .option("-w, --workspace <path>", "Workspace path or conductor.yaml path")
204
502
  .action(async (opts) => {
205
503
  try {
206
- const explicitWorkspaceHint = opts.workspace ?? process.env["CONDUCTOR_WORKSPACE"];
207
- let config;
208
- if (explicitWorkspaceHint) {
209
- config = await loadConfig(explicitWorkspaceHint);
210
- }
211
- else {
212
- try {
213
- config = await loadConfig();
214
- }
215
- catch (err) {
216
- const message = err instanceof Error ? err.message : String(err);
217
- if (!/No conductor\.ya?ml found/i.test(message)) {
218
- throw err;
219
- }
220
- const bootstrap = ensureDashboardBootstrapWorkspace();
221
- process.env["CONDUCTOR_WORKSPACE"] = bootstrap.workspacePath;
222
- process.env["CO_CONFIG_PATH"] = bootstrap.configPath;
223
- config = await loadConfig(bootstrap.workspacePath);
224
- }
225
- }
226
- const workspacePath = opts.workspace
227
- ?? process.env["CONDUCTOR_WORKSPACE"]
228
- ?? (config.configPath ? dirname(config.configPath) : `${process.env["HOME"]}/.conductor/workspace`);
229
- if (!process.env["CONDUCTOR_WORKSPACE"]) {
230
- process.env["CONDUCTOR_WORKSPACE"] = workspacePath;
231
- }
232
- if (!process.env["CO_CONFIG_PATH"] && config.configPath) {
233
- process.env["CO_CONFIG_PATH"] = config.configPath;
234
- }
235
- const { sessionManager, registry } = await createServices(config);
236
- const supportedAgents = registry.list("agent").map((agent) => agent.name);
237
- // Mutable ref — set after boardWatcher is created, used by lifecycle callback
238
- let boardWatcherRef = null;
239
- const port = opts.port ? parseInt(opts.port, 10) : (config.port ?? 3000);
504
+ const configHint = opts.workspace
505
+ || process.env["CO_CONFIG_PATH"]?.trim()
506
+ || process.env["CONDUCTOR_WORKSPACE"]
507
+ || null;
508
+ const settings = loadLauncherSettings(configHint);
509
+ const workspacePath = settings.workspacePath;
510
+ const configPath = settings.configPath;
511
+ const dashboardPort = resolveFrontendPort(opts.port, settings.dashboardPort);
512
+ const backendPort = resolveBackendPort(opts.backendPort, settings.backendPort);
513
+ const explicitBackendUrl = process.env["CONDUCTOR_BACKEND_URL"]?.trim() || null;
514
+ const bindHost = opts.host?.trim() || "127.0.0.1";
240
515
  const shutdownTasks = [];
241
516
  let isShuttingDown = false;
517
+ process.env["CONDUCTOR_WORKSPACE"] = workspacePath;
518
+ process.env["CO_CONFIG_PATH"] = configPath;
242
519
  const requestShutdown = () => {
243
520
  void (async () => {
244
521
  if (isShuttingDown)
@@ -248,8 +525,8 @@ export function registerStart(program) {
248
525
  try {
249
526
  await task();
250
527
  }
251
- catch (err) {
252
- console.error(err);
528
+ catch (error) {
529
+ console.error(error);
253
530
  }
254
531
  }
255
532
  process.exit(0);
@@ -262,100 +539,82 @@ export function registerStart(program) {
262
539
  console.log(chalk.bold.cyan(" Conductor -- Starting"));
263
540
  console.log(chalk.dim(line));
264
541
  console.log();
265
- // ---- Start lifecycle manager ----
266
- const spinner = ora("Starting lifecycle manager").start();
267
- const core = await import("@conductor-oss/core");
268
- core.syncWorkspaceSupportFiles(config, {
269
- workspacePath,
270
- agentNames: supportedAgents,
271
- });
272
- // Startup config sync: regenerate drifted project-local conductor.yaml mirrors
273
- if (typeof core.startupConfigSync === "function") {
274
- const syncResult = core.startupConfigSync(config);
275
- if (syncResult.fixed > 0) {
276
- console.log(chalk.dim(` Synced ${syncResult.fixed} project-local config(s) from canonical`));
277
- }
278
- }
279
- if (typeof core.createLifecycleManager !== "function") {
280
- spinner.warn("Lifecycle manager not yet implemented in @conductor-oss/core");
281
- }
282
- else {
283
- const lifecycle = core.createLifecycleManager({
284
- config,
285
- sessionManager,
286
- onStatusChange: (sessionId, newStatus, projectId) => {
287
- console.log(`[lifecycle] Status change: ${sessionId} → ${newStatus} (${projectId})`);
288
- // Trigger immediate board sync
289
- boardWatcherRef?.updateNow();
290
- },
291
- });
292
- lifecycle.start(10_000); // Poll every 10s (was 30s)
293
- spinner.succeed("Lifecycle manager running");
294
- // Graceful shutdown
295
- shutdownTasks.push(() => {
296
- console.log(chalk.dim("\nShutting down lifecycle manager..."));
297
- lifecycle.stop();
298
- });
542
+ if (opts.watcher === false) {
543
+ console.log(chalk.dim(" JS watcher flag ignored: runtime ownership has moved to the Rust backend path."));
299
544
  }
300
- // ---- Start board watcher ----
301
- if (opts.watcher !== false) {
302
- const watchSpinner = ora("Starting board watcher").start();
303
- try {
304
- const boardPatternsOrConfig = config.boards?.length ? config.boards : config;
305
- const boards = core.discoverBoards(workspacePath, boardPatternsOrConfig);
306
- if (boards.length === 0) {
307
- watchSpinner.warn("No CONDUCTOR.md boards found");
308
- }
309
- else {
310
- const boardProjectMap = core.buildBoardProjectMap(boards, config);
311
- const boardWatcher = core.createBoardWatcher({
312
- config,
313
- sessionManager,
314
- agentNames: supportedAgents,
315
- boardPaths: boards,
316
- boardProjectMap,
317
- pollIntervalMs: 5000,
318
- workspacePath,
319
- onDispatch: (projectId, sessionId, task) => {
320
- console.log(`[board-watcher] Dispatched ${sessionId} -> ${projectId}: "${task}"`);
545
+ // ---- Start Rust backend ----
546
+ let backendProcess = null;
547
+ const shouldLaunchBackend = opts.backend !== false && !explicitBackendUrl;
548
+ const backendUrl = explicitBackendUrl ?? (shouldLaunchBackend ? `http://127.0.0.1:${backendPort}` : null);
549
+ if (shouldLaunchBackend) {
550
+ const backendSpinner = ora(`Starting Rust backend on http://127.0.0.1:${backendPort}`).start();
551
+ const resolution = resolveRustBackendLaunch(workspacePath, configPath, backendPort);
552
+ const launch = resolution.launch;
553
+ if (!launch) {
554
+ throw new Error(resolution.reason ?? "Rust backend binary was not found. Build or package the Rust backend first.");
555
+ }
556
+ else {
557
+ try {
558
+ await killStalePortListener(backendPort);
559
+ let backendStartError = null;
560
+ backendProcess = spawn(launch.cmd, launch.args, {
561
+ cwd: launch.cwd,
562
+ stdio: "inherit",
563
+ detached: false,
564
+ env: {
565
+ ...process.env,
321
566
  },
322
567
  });
323
- boardWatcher.start();
324
- boardWatcherRef = boardWatcher;
325
- shutdownTasks.push(() => boardWatcher.stop());
326
- watchSpinner.succeed(`Board watcher running (${boards.length} boards)`);
568
+ backendProcess.once("error", (error) => {
569
+ backendStartError = error;
570
+ });
571
+ const backendReady = await waitForHttpService(`${backendUrl}/api/health`);
572
+ if (!backendReady) {
573
+ const reason = (backendStartError ? String(backendStartError) : null)
574
+ || (backendProcess.exitCode !== null
575
+ ? `Rust backend exited with code ${backendProcess.exitCode}`
576
+ : `Rust backend did not become ready at ${backendUrl} in time.`);
577
+ throw new Error(reason);
578
+ }
579
+ backendSpinner.succeed(`Rust backend running on ${backendUrl} (${launch.label})`);
580
+ shutdownTasks.push(() => {
581
+ if (backendProcess && backendProcess.exitCode === null) {
582
+ backendProcess.kill("SIGTERM");
583
+ }
584
+ });
585
+ }
586
+ catch (error) {
587
+ backendSpinner.fail(`Rust backend failed: ${error}`);
588
+ throw error;
327
589
  }
328
590
  }
329
- catch (err) {
330
- watchSpinner.warn(`Board watcher failed: ${err}`);
331
- }
591
+ }
592
+ else if (explicitBackendUrl) {
593
+ console.log(chalk.dim(` Backend: using existing Rust backend at ${explicitBackendUrl}`));
594
+ }
595
+ else {
596
+ console.log(chalk.yellow(" Backend: not launched; frontend API requests will fail without CONDUCTOR_BACKEND_URL."));
332
597
  }
333
598
  // ---- Start web dashboard ----
334
599
  let dashboardProcess = null;
335
600
  let publicDashboardUrl = null;
336
601
  let unlockDashboardUrl = null;
337
- const bindHost = opts.host?.trim() || "127.0.0.1";
338
602
  const externalAccessRequested = opts.tunnel === true || !isLoopbackHost(bindHost);
339
603
  const clerkConfigured = Boolean(process.env["NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"] && process.env["CLERK_SECRET_KEY"]);
340
- const trustedHeaderAuth = resolveTrustedHeaderAuth(config);
604
+ const trustedHeaderAuth = settings.access.trustedHeaders;
341
605
  const builtinRemoteAuth = resolveBuiltinRemoteAuth(externalAccessRequested && !clerkConfigured);
342
606
  if (opts.dashboard !== false) {
343
607
  const dashSpinner = ora("Starting web dashboard").start();
344
608
  try {
345
609
  const cliDir = new URL(".", import.meta.url).pathname;
346
- const { dirname, join, resolve } = await import("node:path");
347
- const { cpSync, existsSync, mkdirSync } = await import("node:fs");
348
- // Search order:
349
- // 1. Standalone build inside CLI package (npm install -g)
350
- // 2. Sibling packages/web in monorepo dev setup
351
- // 3. config.configPath-relative monorepo root
610
+ const { cpSync, readdirSync, statSync } = await import("node:fs");
352
611
  let webDir = null;
353
612
  const candidates = [
354
- resolve(cliDir, "..", "web"), // npm: cli/dist/../web
355
- resolve(cliDir, "..", "..", "..", "web"), // npm: cli/dist/../../web
356
- resolve(cliDir, "..", "..", "web"), // monorepo: packages/cli/dist/../../web = packages/web
357
- config.configPath ? resolve(config.configPath, "..", "packages", "web") : null,
358
- ].filter(Boolean);
613
+ resolve(cliDir, "..", "web"),
614
+ resolve(cliDir, "..", "..", "..", "web"),
615
+ resolve(cliDir, "..", "..", "web"),
616
+ resolve(dirname(configPath), "packages", "web"),
617
+ ];
359
618
  for (const candidate of candidates) {
360
619
  if (existsSync(join(candidate, "package.json"))) {
361
620
  webDir = candidate;
@@ -366,52 +625,49 @@ export function registerStart(program) {
366
625
  dashSpinner.warn("Dashboard not found. Run: pnpm --filter @conductor-oss/web build");
367
626
  return;
368
627
  }
369
- // Kill any stale process holding the dashboard port (prevents EADDRINUSE on restart)
370
- try {
371
- const { execSync } = await import("node:child_process");
372
- const pids = execSync(`lsof -ti :${port} -sTCP:LISTEN 2>/dev/null`, { encoding: "utf8" }).trim();
373
- if (pids) {
374
- for (const pid of pids.split("\n").filter(Boolean)) {
375
- if (pid !== String(process.pid)) {
376
- process.kill(Number(pid), "SIGTERM");
377
- console.log(`[dashboard] Killed stale process ${pid} on port ${port}`);
378
- }
379
- }
380
- await new Promise(r => setTimeout(r, 1000));
381
- }
382
- }
383
- catch { /* no stale process — expected */ }
384
- // Prefer standalone build (output: standalone in next.config), then production, then dev
385
- // Find standalone server.js (location varies by monorepo nesting)
386
- const { readdirSync, statSync: fsStat } = await import("node:fs");
628
+ await killStalePortListener(dashboardPort);
629
+ const webMode = resolveDashboardWebMode(process.env["CONDUCTOR_WEB_MODE"]);
630
+ const isSourceCheckout = existsSync(join(webDir, "src", "app", "page.tsx"))
631
+ && existsSync(join(webDir, "next.config.ts"));
632
+ const preferDevServer = webMode === "dev" || (webMode === "auto" && isSourceCheckout);
387
633
  const standaloneDir = join(webDir, ".next", "standalone");
634
+ const hasNextBuild = existsSync(join(webDir, ".next"));
388
635
  let standaloneServer = null;
389
636
  const searchQueue = [standaloneDir];
390
- for (let depth = 0; depth < 6 && searchQueue.length > 0 && !standaloneServer; depth++) {
637
+ for (let depth = 0; depth < 6 && searchQueue.length > 0 && !standaloneServer; depth += 1) {
391
638
  const nextQueue = [];
392
- for (const d of searchQueue) {
393
- const candidate = join(d, "server.js");
639
+ for (const currentDir of searchQueue) {
640
+ const candidate = join(currentDir, "server.js");
394
641
  if (existsSync(candidate)) {
395
642
  standaloneServer = candidate;
396
643
  break;
397
644
  }
398
645
  try {
399
- for (const entry of readdirSync(d)) {
400
- const full = join(d, String(entry));
401
- if (fsStat(full).isDirectory() && entry !== "node_modules") {
646
+ for (const entry of readdirSync(currentDir)) {
647
+ const full = join(currentDir, String(entry));
648
+ if (statSync(full).isDirectory() && entry !== "node_modules") {
402
649
  nextQueue.push(full);
403
650
  }
404
651
  }
405
652
  }
406
- catch { /* ignore */ }
653
+ catch {
654
+ // ignore
655
+ }
407
656
  }
408
657
  searchQueue.splice(0, searchQueue.length, ...nextQueue);
409
658
  }
410
- const hasNextBuild = existsSync(join(webDir, ".next"));
411
659
  let cmd;
412
660
  let args;
413
661
  let dashboardCwd = webDir;
414
- if (standaloneServer) {
662
+ if (preferDevServer) {
663
+ cmd = "pnpm";
664
+ args = ["run", "dev", "--hostname", bindHost, "--port", String(dashboardPort)];
665
+ }
666
+ else if (webMode === "production" && hasNextBuild) {
667
+ cmd = "pnpm";
668
+ args = ["run", "start", "--hostname", bindHost, "--port", String(dashboardPort)];
669
+ }
670
+ else if (standaloneServer) {
415
671
  const standaloneAppDir = dirname(standaloneServer);
416
672
  const standaloneStaticDir = join(standaloneAppDir, ".next", "static");
417
673
  const sourceStaticDir = join(webDir, ".next", "static");
@@ -429,27 +685,12 @@ export function registerStart(program) {
429
685
  dashboardCwd = standaloneDir;
430
686
  }
431
687
  else if (hasNextBuild) {
432
- // Use pnpm run start (next start) — reliable, serves static assets correctly
433
688
  cmd = "pnpm";
434
- args = [
435
- "run",
436
- "start",
437
- "--hostname",
438
- bindHost,
439
- "--port",
440
- String(port),
441
- ];
689
+ args = ["run", "start", "--hostname", bindHost, "--port", String(dashboardPort)];
442
690
  }
443
691
  else {
444
692
  cmd = "pnpm";
445
- args = [
446
- "run",
447
- "dev",
448
- "--hostname",
449
- bindHost,
450
- "--port",
451
- String(port),
452
- ];
693
+ args = ["run", "dev", "--hostname", bindHost, "--port", String(dashboardPort)];
453
694
  }
454
695
  dashboardProcess = spawn(cmd, args, {
455
696
  cwd: dashboardCwd,
@@ -457,10 +698,16 @@ export function registerStart(program) {
457
698
  detached: false,
458
699
  env: {
459
700
  ...process.env,
460
- PORT: String(port),
701
+ PORT: String(dashboardPort),
461
702
  HOSTNAME: bindHost,
462
703
  CONDUCTOR_WORKSPACE: workspacePath,
463
- CO_CONFIG_PATH: config.configPath,
704
+ CO_CONFIG_PATH: configPath,
705
+ ...(backendUrl
706
+ ? {
707
+ CONDUCTOR_BACKEND_URL: backendUrl,
708
+ CONDUCTOR_BACKEND_PORT: String(backendPort),
709
+ }
710
+ : {}),
464
711
  ...(builtinRemoteAuth
465
712
  ? {
466
713
  CONDUCTOR_REMOTE_ACCESS_TOKEN: builtinRemoteAuth.accessToken,
@@ -481,25 +728,21 @@ export function registerStart(program) {
481
728
  : {}),
482
729
  }
483
730
  : {}),
484
- ...(config.access?.requireAuth
485
- ? {
486
- CONDUCTOR_REQUIRE_AUTH: "true",
487
- }
731
+ ...(settings.access.requireAuth
732
+ ? { CONDUCTOR_REQUIRE_AUTH: "true" }
488
733
  : {}),
489
- ...(config.access?.defaultRole
490
- ? {
491
- CONDUCTOR_ACCESS_DEFAULT_ROLE: config.access.defaultRole,
492
- }
734
+ ...(settings.access.defaultRole
735
+ ? { CONDUCTOR_ACCESS_DEFAULT_ROLE: settings.access.defaultRole }
493
736
  : {}),
494
737
  },
495
738
  });
496
739
  dashboardProcess.on("error", () => {
497
740
  dashSpinner.warn("Dashboard failed to start. Try: cd packages/web && pnpm build");
498
741
  });
499
- const dashboardInternalUrl = `http://127.0.0.1:${port}`;
742
+ const dashboardInternalUrl = `http://127.0.0.1:${dashboardPort}`;
500
743
  const dashboardUrl = isLoopbackHost(bindHost)
501
- ? `http://localhost:${port}`
502
- : `http://${bindHost}:${port}`;
744
+ ? `http://localhost:${dashboardPort}`
745
+ : `http://${bindHost}:${dashboardPort}`;
503
746
  if (builtinRemoteAuth) {
504
747
  unlockDashboardUrl = buildRemoteUnlockUrl(dashboardUrl, builtinRemoteAuth.accessToken);
505
748
  }
@@ -518,7 +761,6 @@ export function registerStart(program) {
518
761
  }
519
762
  });
520
763
  publicDashboardUrl = await tunnel.url;
521
- config.dashboardUrl = publicDashboardUrl;
522
764
  if (builtinRemoteAuth) {
523
765
  unlockDashboardUrl = buildRemoteUnlockUrl(publicDashboardUrl, builtinRemoteAuth.accessToken);
524
766
  }
@@ -546,33 +788,22 @@ export function registerStart(program) {
546
788
  });
547
789
  }
548
790
  }
549
- catch {
550
- dashSpinner.warn("Could not start dashboard.");
551
- }
552
- }
553
- // ---- Start webhook server (if enabled in config) ----
554
- if (config.webhook?.enabled) {
555
- const webhookSpinner = ora("Starting webhook server").start();
556
- try {
557
- const { createWebhookServer } = await import("@conductor-oss/plugin-webhook");
558
- const webhookServer = createWebhookServer(config, config.webhook);
559
- await webhookServer.start();
560
- shutdownTasks.push(() => webhookServer.stop());
561
- webhookSpinner.succeed(`Webhook server running on port ${config.webhook.port}`);
562
- }
563
- catch (err) {
564
- webhookSpinner.warn(`Webhook server failed to start: ${err}`);
791
+ catch (error) {
792
+ dashSpinner.warn(`Could not start dashboard: ${error}`);
565
793
  }
566
794
  }
567
795
  // ---- Summary ----
568
796
  console.log();
569
797
  console.log(chalk.bold.green("Conductor is running."));
570
- console.log(chalk.dim(` Config: ${config.configPath}`));
798
+ console.log(chalk.dim(` Config: ${configPath}`));
571
799
  if (opts.dashboard !== false) {
572
800
  const dashboardSummaryUrl = isLoopbackHost(bindHost)
573
- ? `http://localhost:${port}`
574
- : `http://${bindHost}:${port}`;
801
+ ? `http://localhost:${dashboardPort}`
802
+ : `http://${bindHost}:${dashboardPort}`;
575
803
  console.log(chalk.dim(` Dashboard: ${dashboardSummaryUrl}`));
804
+ if (backendUrl) {
805
+ console.log(chalk.dim(` Backend: ${backendUrl}`));
806
+ }
576
807
  if (publicDashboardUrl) {
577
808
  console.log(chalk.dim(` Public: ${publicDashboardUrl}`));
578
809
  }
@@ -594,30 +825,29 @@ export function registerStart(program) {
594
825
  }
595
826
  }
596
827
  }
597
- if (opts.watcher !== false) {
598
- console.log(chalk.dim(" Watcher: Obsidian CONDUCTOR.md boards"));
599
- }
600
- if (config.webhook?.enabled) {
601
- console.log(chalk.dim(` Webhook: http://localhost:${config.webhook.port}/api/webhook`));
602
- }
828
+ console.log(chalk.dim(" Runtime: Rust backend + Next frontend"));
603
829
  console.log(chalk.dim(" Press Ctrl-C to stop.\n"));
604
- // Keep process alive. Dashboard is optional for orchestrator health.
605
- // If it crashes (e.g. EADDRINUSE), keep lifecycle + board watcher running.
606
830
  if (dashboardProcess) {
607
831
  dashboardProcess.on("exit", (code, signal) => {
608
832
  if (code !== 0 && code !== null) {
609
833
  console.error(chalk.yellow(`Dashboard exited with code ${code}${signal ? ` (signal ${signal})` : ""}. ` +
610
- "Keeping orchestrator core services running."));
834
+ "Keeping the Rust backend running."));
835
+ }
836
+ });
837
+ }
838
+ if (backendProcess) {
839
+ backendProcess.on("exit", (code, signal) => {
840
+ if (code !== 0 && code !== null) {
841
+ console.error(chalk.red(`Rust backend exited with code ${code}${signal ? ` (signal ${signal})` : ""}.`));
611
842
  }
612
843
  });
613
844
  }
614
- // Always keep process alive via interval heartbeat.
615
845
  setInterval(() => {
616
- // heartbeat -- lifecycle manager / watcher run on their own intervals
846
+ // Keep the launcher attached while child processes run.
617
847
  }, 60_000);
618
848
  }
619
- catch (err) {
620
- console.error(chalk.red(`Failed to start: ${err}`));
849
+ catch (error) {
850
+ console.error(chalk.red(`Failed to start: ${error}`));
621
851
  process.exit(1);
622
852
  }
623
853
  });