@adhdev/daemon-standalone 1.0.28-rc.1 → 1.0.28-rc.11

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/public/index.html CHANGED
@@ -7,9 +7,9 @@
7
7
  <meta name="description" content="ADHDev self-hosted dashboard for controlling AI agents" />
8
8
  <link rel="icon" href="/otter-logo.png" />
9
9
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
10
- <script type="module" crossorigin src="/assets/index-CX3wQvni.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-DJ5eteX0.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="/assets/vendor-DyCWA2YZ.js">
12
- <link rel="stylesheet" crossorigin href="/assets/index-B51nIfEq.css">
12
+ <link rel="stylesheet" crossorigin href="/assets/index-8TNNoifN.css">
13
13
  </head>
14
14
  <body>
15
15
  <!-- Apply theme immediately to prevent FOIT (Flash of Incorrect Theme) -->
@@ -393,6 +393,8 @@ var CANONICAL_MESH_TOOL_NAMES = [
393
393
  "mesh_approve",
394
394
  "mesh_answer_question",
395
395
  "mesh_list_pending_approvals",
396
+ "mesh_create",
397
+ "mesh_add_node",
396
398
  "mesh_clone_node",
397
399
  "mesh_remove_node",
398
400
  "mesh_refine_node",
@@ -998,12 +1000,19 @@ var MESH_FAST_FORWARD_NODE_TOOL = {
998
1000
  };
999
1001
  var MESH_RESTART_DAEMON_TOOL = {
1000
1002
  name: "mesh_restart_daemon",
1001
- description: `Update a mesh node's daemon to the latest published version on its release channel and restart it \u2014 the same path as the dashboard "preview update" button, exposed as a mesh command so a coordinator can roll a worker daemon onto a freshly deployed version without a manual restart round-trip. No agent session is launched. Idle-gated: a node whose daemon has an active session (generating / waiting_approval / starting) is refused with code "blocking_sessions" so an in-flight turn is never interrupted. If the node is already on the latest version it is a no-op (no restart), matching the dashboard button (returns alreadyLatest:true). Targets a single node \u2014 call other (idle) nodes first; restarting the coordinator's OWN daemon is naturally refused while its calling turn is active. Passing channel switches the daemon's release channel (and server URL) before restarting; omit it to keep the daemon on its configured channel.`,
1003
+ description: `Restart a mesh node's daemon, optionally updating it first \u2014 the same path as the dashboard "preview update" button, exposed as a mesh command so a coordinator can roll a worker daemon without a manual restart round-trip. No agent session is launched. mode="upgrade" (default): update to the latest published version on the release channel, then restart; already-latest is a no-op (no restart, returns alreadyLatest:true). mode="restart": pure re-spawn with no reinstall \u2014 restarts even when already latest, with much shorter downtime; use it to reset wedged daemon state (memory leaks, zombie sessions). Idle-gated: a node whose daemon has an active session (generating / waiting_approval / starting) is refused with code "blocking_sessions" so an in-flight turn is never interrupted. self_only=true waives ONLY this mesh's own coordinator session (the structural self-deadlock case \u2014 the coordinator is always generating while it calls). Other sessions still refuse. force=true bypasses the gate entirely: in-flight turns die and the unpersisted pendingOutboundQueue is lost. when_idle=true schedules the restart to run automatically once the daemon goes idle (the safest path \u2014 no queue loss); cancel_when_idle=true cancels it and every response reports the schedule under deferredRestart. kill_session_host=true additionally stops the session-host process, destroying ALL hosted CLI sessions (hard refresh; this is what Windows already does on every upgrade). Default off. Note: on Windows any daemon restart/upgrade terminates all hosted sessions regardless of options; on POSIX hosted sessions survive a plain restart and rebind on next boot. Passing channel switches the daemon's release channel (and server URL) before restarting; omit it to keep the daemon on its configured channel.`,
1002
1004
  inputSchema: {
1003
1005
  type: "object",
1004
1006
  properties: {
1005
- node_id: { type: "string", description: "Target node ID \u2014 the daemon that owns this node is updated and restarted." },
1006
- channel: { type: "string", enum: ["stable", "preview"], description: "Optional release channel to update from. Defaults to the daemon's configured updateChannel. Setting it also repoints the daemon's server URL to that channel." }
1007
+ node_id: { type: "string", description: "Target node ID \u2014 the daemon that owns this node is restarted (and updated, in upgrade mode)." },
1008
+ channel: { type: "string", enum: ["stable", "preview"], description: "Optional release channel to update from (upgrade mode only). Defaults to the daemon's configured updateChannel. Setting it also repoints the daemon's server URL to that channel." },
1009
+ mode: { type: "string", enum: ["upgrade", "restart"], description: "upgrade (default): update to latest on channel, then restart (already-latest is a no-op). restart: pure re-spawn, no reinstall \u2014 restarts even when already latest." },
1010
+ force: { type: "boolean", description: "Bypass the idle-gate entirely. Destructive: in-flight turns are killed and the in-memory pendingOutboundQueue is permanently lost. Default false." },
1011
+ self_only: { type: "boolean", description: "Waive only this mesh's own coordinator session when it blocks the restart (the coordinator self-deadlock). Other nodes' active sessions still refuse. Default false." },
1012
+ when_idle: { type: "boolean", description: "If blocked, schedule the restart to execute automatically once the daemon goes idle (safest \u2014 no pendingOutboundQueue loss). The schedule expires after timeout_ms (default 30 min). Default false." },
1013
+ cancel_when_idle: { type: "boolean", description: "Cancel a previously scheduled when_idle restart on the owning daemon." },
1014
+ timeout_ms: { type: "number", description: "Expiry for a when_idle schedule in milliseconds (default 1800000 = 30 min, max 6 h)." },
1015
+ kill_session_host: { type: "boolean", description: "Hard refresh: also stop the session-host process, destroying ALL hosted CLI sessions on the machine. Default false." }
1007
1016
  },
1008
1017
  required: ["node_id"]
1009
1018
  }
@@ -1116,6 +1125,41 @@ var MESH_LIST_PENDING_APPROVALS_TOOL = {
1116
1125
  properties: {}
1117
1126
  }
1118
1127
  };
1128
+ var MESH_CREATE_TOOL = {
1129
+ name: "mesh_create",
1130
+ description: "Bootstrap a brand-new mesh for a Git repository \u2014 the first step for an MCP-only agent that has no mesh yet. Mirrors `adhdev mesh create <name>`. A mesh groups one repo's workspaces/nodes so the coordinator can delegate work across them. You MUST supply the repo identity: pass repo_remote_url (the git remote URL, e.g. git@github.com:user/repo.git or https://github.com/user/repo.git \u2014 the identity is derived from it) OR repo_identity (an explicit normalized identity like github.com/user/repo). At least one is required; there is no auto-detection here (the CLI auto-detects from the current dir, but the MCP server may run elsewhere). Set add_current:true to also register a node in the same call (uses workspace if given, else the daemon's current working directory) \u2014 handy when the daemon runs in the repo you want as the base node. BOOT-GATE / WHEN CALLABLE: this tool is reachable in STANDARD mode (adhdev mcp, no --repo-mesh) \u2014 the bootstrap context where no mesh exists \u2014 and also in mesh mode (where it creates a SEPARATE additional mesh). It is NOT reachable before any mesh exists via mesh mode, because `adhdev mcp --repo-mesh <id>` refuses to start without an existing meshId. So the intended flow is: run standard-mode MCP \u2192 mesh_create \u2192 mesh_add_node \u2192 then relaunch as `adhdev mcp --repo-mesh <returned mesh_id>`. Returns mesh_id (and node_id when add_current is used); use mesh_id for the follow-up mesh_add_node call and to launch mesh mode.",
1131
+ inputSchema: {
1132
+ type: "object",
1133
+ properties: {
1134
+ name: { type: "string", description: 'Human-readable mesh name (e.g. "adhdev-main"). Trimmed, max 100 chars.' },
1135
+ repo_remote_url: { type: "string", description: "Git remote URL of the repo (e.g. git@github.com:user/repo.git). The repo identity is normalized from this. Provide this OR repo_identity." },
1136
+ repo_identity: { type: "string", description: "Explicit normalized repo identity (e.g. github.com/user/repo). Provide this OR repo_remote_url. Wins over repo_remote_url when both are given." },
1137
+ default_branch: { type: "string", description: 'Default branch for the repo (e.g. "main"). Optional; used as the merge/convergence target.' },
1138
+ add_current: { type: "boolean", description: "Also register a node in this same call (parity with CLI --add-current). Uses `workspace` if provided, otherwise the daemon's current working directory." },
1139
+ workspace: { type: "string", description: "Absolute workspace path to register when add_current:true. Ignored unless add_current is true. Defaults to the daemon's cwd." }
1140
+ },
1141
+ required: ["name"]
1142
+ }
1143
+ };
1144
+ var MESH_ADD_NODE_TOOL = {
1145
+ name: "mesh_add_node",
1146
+ description: "Register a workspace as a node in an EXISTING mesh \u2014 the second bootstrap step after mesh_create (or to add more nodes later). Mirrors `adhdev mesh add-node <mesh_id>` with --workspace / --read-only / --provider-priority. A node is a repo checkout on a daemon that the coordinator can launch agents on and delegate tasks to. mesh_id is REQUIRED in standard mode (pass the id returned by mesh_create); in mesh mode it defaults to the active mesh. workspace is the absolute path to the repo checkout ON THE DAEMON that owns it \u2014 the local base node is added by the daemon that created the mesh. NOTE: this registers an EXISTING directory as a node (including marking one as a worktree via is_worktree). To CREATE a fresh git worktree + branch for isolated parallel work, use mesh_clone_node instead \u2014 that runs the actual `git worktree add`. Returns node_id + workspace so you can immediately target the node with mesh_launch_session / mesh_send_task / mesh_enqueue_task.",
1147
+ inputSchema: {
1148
+ type: "object",
1149
+ properties: {
1150
+ mesh_id: { type: "string", description: "Target mesh id (from mesh_create / mesh_status). Required in standard mode; defaults to the active mesh in mesh mode." },
1151
+ workspace: { type: "string", description: "Absolute path to the repo checkout on the owning daemon (e.g. /Users/me/work/repo). Must be unique within the mesh." },
1152
+ read_only: { type: "boolean", description: "Mark the node read-only (no launches/mutations targeted here). Parity with CLI --read-only." },
1153
+ provider_priority: {
1154
+ type: "array",
1155
+ items: { type: "string" },
1156
+ description: 'Ordered provider types this node prefers when mesh_launch_session omits an explicit type (e.g. ["claude-cli","codex"]). Parity with CLI --provider-priority. A comma-separated string is also accepted.'
1157
+ },
1158
+ is_worktree: { type: "boolean", description: "Mark this workspace as an existing local git worktree (parity with CLI --worktree). This only tags an already-present worktree dir; it does NOT create one \u2014 use mesh_clone_node to create a worktree+branch." }
1159
+ },
1160
+ required: ["workspace"]
1161
+ }
1162
+ };
1119
1163
  var MESH_CLONE_NODE_TOOL = {
1120
1164
  name: "mesh_clone_node",
1121
1165
  description: "Create a new worktree-based node from an existing node for isolated parallel work. Creates a git worktree on a new branch so multiple tasks can run on separate branches simultaneously.",
@@ -1406,7 +1450,6 @@ var MESH_MAGI_REVIEW_TOOL = {
1406
1450
  n: { type: "number", description: "Global replica override per slot (clamped by the total-replica guard cap, default 12)." },
1407
1451
  task_kind: { type: "string", enum: ["claim_audit", "rca", "design", "freeform"], description: "REQUIRED. Selects (1) the SINGLE output schema injected into each replica prompt and the strict parser used at collection (no schema-on-schema conflict), AND (2) the user-configured kind-panel binding that supplies the fan-out slots (mesh settings \u2192 magiKindPanels; errors magi_kind_not_configured if that kind has no configured slots \u2014 no named-panel/inline/preset fallback). claim_audit: {claims[],top_findings[],open_questions[]}. rca: {rootCause,failsAt,mechanism,evidence[],fixDirection,confidence}. design: {recommendation,rationale,alternatives[],tradeoffs[],risks[],evidence[],confidence}. freeform: no schema \u2014 natural-language answer, parsing/evidence checks waived, cross-verification is weak. Every kind except freeform requires non-empty evidence[]; an empty-evidence or schema-invalid answer triggers ONE delta re-request before being dropped as unparseable. Do NOT also embed an output-format schema in the question \u2014 it collides with this contract (a warning is surfaced if detected)." },
1408
1452
  mode: { type: "string", enum: ["rca", "investigation", "claim_audit", "design_review", "code_audit"], description: "Synthesis emphasis hint \u2014 affects labels only, never the agent count or schema. Distinct from task_kind (which selects the output schema)." },
1409
- use_judge: { type: "boolean", description: "Default false (clustering synthesis). STUB: judge synthesis is not yet implemented \u2014 passing true currently falls back to clustering with a warning. Reserved interface only." },
1410
1453
  require_independent_evidence: { type: "boolean", description: "Default true \u2014 high-impact claims with no file:line/source evidence are routed to needs_verification." },
1411
1454
  include_stale: { type: "boolean", description: "Default false. By default, panel slots whose node HEAD commit differs from the coordinator reference commit are EXCLUDED (they would investigate different code). Set true to fan out to them anyway \u2014 results will be git-skewed and a warning is surfaced. If exclusion drops the panel below 2 independent targets the call errors rather than degrading to N=1; include_stale=true is one way to recover." },
1412
1455
  wait: { type: "boolean", description: "Default true \u2014 collect replica outputs and return the synthesis. Set false to dispatch async and return a consensusGroupId handle; collect later with mesh_magi_collect." },
@@ -1543,6 +1586,8 @@ var ALL_MESH_TOOLS = [
1543
1586
  MESH_APPROVE_TOOL,
1544
1587
  MESH_ANSWER_QUESTION_TOOL,
1545
1588
  MESH_LIST_PENDING_APPROVALS_TOOL,
1589
+ MESH_CREATE_TOOL,
1590
+ MESH_ADD_NODE_TOOL,
1546
1591
  MESH_CLONE_NODE_TOOL,
1547
1592
  MESH_REMOVE_NODE_TOOL,
1548
1593
  MESH_REFINE_NODE_TOOL,
@@ -2044,7 +2089,19 @@ function buildDirectTaskPayload(message, via, opts) {
2044
2089
  ...opts.targetSessionId ? { targetSessionId: opts.targetSessionId } : {},
2045
2090
  ...opts.dispatchedToIdleSession !== void 0 ? { dispatchedToIdleSession: opts.dispatchedToIdleSession } : {},
2046
2091
  ...opts.coordinatorSessionId ? { coordinatorSessionId: opts.coordinatorSessionId } : {},
2047
- ...opts.coordinatorDaemonId ? { coordinatorDaemonId: opts.coordinatorDaemonId } : {}
2092
+ ...opts.coordinatorDaemonId ? { coordinatorDaemonId: opts.coordinatorDaemonId } : {},
2093
+ // Uniform routing rationale (mirrors the queue-claim task_dispatched shape) so both
2094
+ // paths render identically in mesh_task_history / the dashboard. The legacy top-level
2095
+ // `source`/`via`/`providerType` fields above are preserved verbatim for existing
2096
+ // consumers (mesh-active-work / mesh-events-stale key on payload.source === 'direct').
2097
+ routingDecision: {
2098
+ source: "direct",
2099
+ via,
2100
+ ...opts.selectedNodeId ? { selectedNodeId: opts.selectedNodeId } : {},
2101
+ ...opts.providerType ? { resolvedProviderType: opts.providerType } : {},
2102
+ ...opts.resolvedModel ? { resolvedModel: opts.resolvedModel } : {},
2103
+ ...opts.resolvedThinkingLevel ? { resolvedThinkingLevel: opts.resolvedThinkingLevel } : {}
2104
+ }
2048
2105
  };
2049
2106
  }
2050
2107
  function findNode(mesh, nodeId) {
@@ -2809,11 +2866,27 @@ function isGitStatusDirty(status) {
2809
2866
  if (Array.isArray(status?.submodules) && status.submodules.some((submodule) => submodule?.dirty || submodule?.outOfSync || submodule?.error)) return true;
2810
2867
  return countUncommittedChanges(status) > 0;
2811
2868
  }
2869
+ var ROUTING_SKIPPED_COMPACT_MAX = 5;
2870
+ function compactRoutingDecision(routing) {
2871
+ const out = {};
2872
+ for (const [k, v] of Object.entries(routing)) {
2873
+ if (k === "skippedCandidates" && Array.isArray(v)) {
2874
+ const kept = v.slice(0, ROUTING_SKIPPED_COMPACT_MAX);
2875
+ out[k] = kept;
2876
+ if (v.length > kept.length) out.skippedCandidatesDropped = v.length - kept.length;
2877
+ } else {
2878
+ out[k] = v;
2879
+ }
2880
+ }
2881
+ return out;
2882
+ }
2812
2883
  function slimLedgerPayload(payload) {
2813
2884
  const slim = {};
2814
2885
  for (const [k, v] of Object.entries(payload)) {
2815
2886
  if (k === "message" || k === "taskSummary") {
2816
2887
  slim[k] = typeof v === "string" && v.length > 200 ? v.slice(0, 200) + "\u2026" : v;
2888
+ } else if (k === "routingDecision" && v && typeof v === "object" && !Array.isArray(v)) {
2889
+ slim[k] = compactRoutingDecision(v);
2817
2890
  } else if (k === "evidence" || k === "workerResult" || k === "gitStatus" || k === "validationResults") {
2818
2891
  } else if (k === "finalSummary") {
2819
2892
  slim[k] = typeof v === "string" && v.length > 300 ? v.slice(0, 300) + "\u2026" : v;
@@ -5532,8 +5605,6 @@ async function meshMagiReview(ctx, args) {
5532
5605
  }, null, 2);
5533
5606
  }
5534
5607
  const questionSchemaWarning = detectQuestionOutputSchemaConflict(question);
5535
- const useJudge = (args.use_judge ?? args.useJudge) === true;
5536
- const judgeWarning = useJudge ? "use_judge=true requested, but judge synthesis is not yet implemented \u2014 falling back to clustering synthesis." : null;
5537
5608
  await refreshMeshFromDaemon(ctx);
5538
5609
  const referenceCommit = resolveMagiReferenceCommit(ctx);
5539
5610
  const referenceSubmoduleKey = resolveMagiReferenceSubmoduleKey(ctx);
@@ -5639,7 +5710,6 @@ Target: ${args.target}` : ""}`,
5639
5710
  panel: panelName,
5640
5711
  taskKind,
5641
5712
  ...questionSchemaWarning ? { questionSchemaWarning } : {},
5642
- ...judgeWarning ? { judgeWarning } : {},
5643
5713
  question,
5644
5714
  replicaCount: replicaRecords.length,
5645
5715
  replicas: replicaRecords.map((r) => ({ taskId: r.taskId, provider: r.provider, targetNodeId: r.targetNodeId })),
@@ -6488,6 +6558,7 @@ async function meshSendTask(ctx, args) {
6488
6558
  taskMode,
6489
6559
  providerType,
6490
6560
  targetSessionId: dispatchedSessionId,
6561
+ ...args.node_id ? { selectedNodeId: args.node_id } : {},
6491
6562
  ...ctx.coordinatorSessionId ? { coordinatorSessionId: ctx.coordinatorSessionId } : {},
6492
6563
  // COORD-EVENT-MISROUTE: persist the dispatching coordinator daemon anchor
6493
6564
  // (same value stamped into meshContext above) so a transcript-reconcile
@@ -6650,6 +6721,7 @@ async function meshSendTask(ctx, args) {
6650
6721
  providerType: resolvedProviderType,
6651
6722
  targetSessionId: args.session_id,
6652
6723
  dispatchedToIdleSession: sessionWasIdle,
6724
+ ...args.node_id ? { selectedNodeId: args.node_id } : {},
6653
6725
  ...ctx.coordinatorSessionId ? { coordinatorSessionId: ctx.coordinatorSessionId } : {},
6654
6726
  // COORD-EVENT-MISROUTE: persist the dispatching coordinator daemon anchor so a
6655
6727
  // transcript-reconcile synth recovers it from the ledger rather than stamping
@@ -7300,7 +7372,14 @@ async function meshRestartDaemon(ctx, args) {
7300
7372
  meshId: ctx.mesh.id,
7301
7373
  nodeId: node.id,
7302
7374
  inlineMesh: ctx.mesh,
7303
- ...args.channel ? { channel: args.channel } : {}
7375
+ ...args.channel ? { channel: args.channel } : {},
7376
+ ...args.mode ? { mode: args.mode } : {},
7377
+ ...args.force === true ? { force: true } : {},
7378
+ ...args.self_only === true ? { selfOnly: true } : {},
7379
+ ...args.when_idle === true ? { whenIdle: true } : {},
7380
+ ...args.cancel_when_idle === true ? { cancelWhenIdle: true } : {},
7381
+ ...typeof args.timeout_ms === "number" ? { timeoutMs: args.timeout_ms } : {},
7382
+ ...args.kill_session_host === true ? { killSessionHost: true } : {}
7304
7383
  });
7305
7384
  return JSON.stringify(unwrapCommandPayload(result), null, 2);
7306
7385
  } catch (e) {
@@ -7391,6 +7470,100 @@ async function meshRemoveNode(ctx, args) {
7391
7470
  return JSON.stringify({ ...result || {}, ...transportFallback ? { transportFallback } : {} }, null, 2);
7392
7471
  }
7393
7472
 
7473
+ // src/tools/mesh-tools-crud.ts
7474
+ async function meshCreate(transport, args) {
7475
+ const name = typeof args?.name === "string" ? args.name.trim() : "";
7476
+ if (!name) {
7477
+ return JSON.stringify({ success: false, error: "name required" }, null, 2);
7478
+ }
7479
+ const repoRemoteUrl = typeof args?.repo_remote_url === "string" ? args.repo_remote_url.trim() : "";
7480
+ const repoIdentity = typeof args?.repo_identity === "string" ? args.repo_identity.trim() : "";
7481
+ const defaultBranch = typeof args?.default_branch === "string" ? args.default_branch.trim() : "";
7482
+ if (!repoRemoteUrl && !repoIdentity) {
7483
+ return JSON.stringify({
7484
+ success: false,
7485
+ error: "Either repo_remote_url or repo_identity is required. Pass the repo's git remote URL (e.g. git@github.com:user/repo.git) or an explicit identity (e.g. github.com/user/repo)."
7486
+ }, null, 2);
7487
+ }
7488
+ const createResult = await transport.command("create_mesh", {
7489
+ name,
7490
+ ...repoRemoteUrl ? { repoRemoteUrl } : {},
7491
+ ...repoIdentity ? { repoIdentity } : {},
7492
+ ...defaultBranch ? { defaultBranch } : {}
7493
+ });
7494
+ const createPayload = unwrapCommandPayload(createResult);
7495
+ const mesh = createPayload?.mesh;
7496
+ if (!createPayload?.success || !mesh?.id) {
7497
+ return JSON.stringify({
7498
+ success: false,
7499
+ error: createPayload?.error || "create_mesh failed",
7500
+ raw: createPayload ?? createResult
7501
+ }, null, 2);
7502
+ }
7503
+ const out = {
7504
+ success: true,
7505
+ mesh_id: mesh.id,
7506
+ name: mesh.name,
7507
+ repo_identity: mesh.repoIdentity,
7508
+ default_branch: mesh.defaultBranch,
7509
+ node_count: Array.isArray(mesh.nodes) ? mesh.nodes.length : 0,
7510
+ next_step: `Add the base node with mesh_add_node (mesh_id: "${mesh.id}", workspace: <repo path>), then start mesh mode with: adhdev mcp --repo-mesh ${mesh.id}`
7511
+ };
7512
+ if (args?.add_current === true) {
7513
+ const workspace = typeof args?.workspace === "string" && args.workspace.trim() ? args.workspace.trim() : "";
7514
+ const addResult = await transport.command("add_mesh_node", {
7515
+ meshId: mesh.id,
7516
+ ...workspace ? { workspace } : {},
7517
+ inlineMesh: mesh
7518
+ });
7519
+ const addPayload = unwrapCommandPayload(addResult);
7520
+ if (addPayload?.success && addPayload?.node?.id) {
7521
+ out.node_id = addPayload.node.id;
7522
+ out.node_workspace = addPayload.node.workspace;
7523
+ } else {
7524
+ out.add_current_error = addPayload?.error || "add_mesh_node failed (mesh was still created)";
7525
+ }
7526
+ }
7527
+ return JSON.stringify(out, null, 2);
7528
+ }
7529
+ async function meshAddNode(transport, args, defaultMeshId) {
7530
+ const meshId = typeof args?.mesh_id === "string" && args.mesh_id.trim() ? args.mesh_id.trim() : typeof defaultMeshId === "string" ? defaultMeshId.trim() : "";
7531
+ const workspace = typeof args?.workspace === "string" ? args.workspace.trim() : "";
7532
+ if (!meshId) {
7533
+ return JSON.stringify({ success: false, error: "mesh_id required (create one first with mesh_create)." }, null, 2);
7534
+ }
7535
+ if (!workspace) {
7536
+ return JSON.stringify({ success: false, error: "workspace required (absolute path to the repo checkout on the target daemon)." }, null, 2);
7537
+ }
7538
+ const providerPriority = Array.isArray(args?.provider_priority) ? args.provider_priority.map((v) => typeof v === "string" ? v.trim() : "").filter(Boolean) : typeof args?.provider_priority === "string" ? args.provider_priority.split(",").map((v) => v.trim()).filter(Boolean) : [];
7539
+ const addResult = await transport.command("add_mesh_node", {
7540
+ meshId,
7541
+ workspace,
7542
+ ...args?.read_only === true ? { readOnly: true } : {},
7543
+ ...providerPriority.length ? { providerPriority } : {},
7544
+ ...args?.is_worktree === true ? { isLocalWorktree: true } : {},
7545
+ ...args?.inline_mesh ? { inlineMesh: args.inline_mesh } : {}
7546
+ });
7547
+ const addPayload = unwrapCommandPayload(addResult);
7548
+ if (!addPayload?.success || !addPayload?.node?.id) {
7549
+ return JSON.stringify({
7550
+ success: false,
7551
+ error: addPayload?.error || "add_mesh_node failed",
7552
+ code: addPayload?.code,
7553
+ raw: addPayload ?? addResult
7554
+ }, null, 2);
7555
+ }
7556
+ return JSON.stringify({
7557
+ success: true,
7558
+ mesh_id: meshId,
7559
+ node_id: addPayload.node.id,
7560
+ workspace: addPayload.node.workspace,
7561
+ read_only: addPayload.node.policy?.readOnly === true,
7562
+ provider_priority: addPayload.node.policy?.providerPriority,
7563
+ next_step: `Node registered. Launch an agent on it with mesh_launch_session (node_id: "${addPayload.node.id}") or delegate work with mesh_send_task / mesh_enqueue_task.`
7564
+ }, null, 2);
7565
+ }
7566
+
7394
7567
  // src/tools/mesh-tools-refine.ts
7395
7568
  async function meshRefineConfigSchema(ctx) {
7396
7569
  const node = resolveRefineConfigNode(ctx);
@@ -8770,6 +8943,12 @@ async function startMcpServer(opts) {
8770
8943
  case "mesh_list_pending_approvals":
8771
8944
  text = await meshListPendingApprovals(meshCtx, a);
8772
8945
  break;
8946
+ case "mesh_create":
8947
+ text = await meshCreate(meshCtx.transport, a);
8948
+ break;
8949
+ case "mesh_add_node":
8950
+ text = await meshAddNode(meshCtx.transport, a, meshCtx.mesh.id);
8951
+ break;
8773
8952
  case "mesh_clone_node":
8774
8953
  text = await meshCloneNode(meshCtx, a);
8775
8954
  break;
@@ -8907,6 +9086,11 @@ async function startMcpServer(opts) {
8907
9086
  GIT_DIFF_TOOL,
8908
9087
  GIT_CHECKPOINT_TOOL,
8909
9088
  GIT_PUSH_TOOL,
9089
+ // Mesh bootstrap: create a mesh + register its first node from an MCP-only agent.
9090
+ // Exposed in standard mode precisely because this is the no-mesh-yet context —
9091
+ // mesh mode refuses to boot without an existing meshId (see the mesh-mode block above).
9092
+ MESH_CREATE_TOOL,
9093
+ MESH_ADD_NODE_TOOL,
8910
9094
  ...isLocal ? [SCREENSHOT_TOOL] : []
8911
9095
  ];
8912
9096
  const server = new import_server.Server(
@@ -8996,6 +9180,14 @@ async function startMcpServer(opts) {
8996
9180
  const text = await checkPending(transport, { format: a.format });
8997
9181
  return { content: [{ type: "text", text }] };
8998
9182
  }
9183
+ case "mesh_create": {
9184
+ const text = await meshCreate(transport, a);
9185
+ return { content: [{ type: "text", text }] };
9186
+ }
9187
+ case "mesh_add_node": {
9188
+ const text = await meshAddNode(transport, a);
9189
+ return { content: [{ type: "text", text }] };
9190
+ }
8999
9191
  default:
9000
9192
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
9001
9193
  }