@adhdev/daemon-standalone 1.0.56-rc.3 → 1.0.56-rc.5

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.
@@ -40,6 +40,7 @@ var import_daemon_core = require("@adhdev/daemon-core");
40
40
  var DEFAULT_IPC_PORT = import_daemon_core.IDENTITY.defaultPort;
41
41
  var DEFAULT_IPC_PATH = "/ipc";
42
42
  var DEFAULT_IPC_COMMAND_TIMEOUT_MS = 15e3;
43
+ var IPC_COMMAND_TIMEOUT_ENV = "ADHDEV_IPC_COMMAND_TIMEOUT_MS";
43
44
  var IPC_COMMAND_TIMEOUTS_MS = {
44
45
  mesh_relay_command: 12e4,
45
46
  agent_command: 3e4,
@@ -67,6 +68,42 @@ var IPC_COMMAND_TIMEOUTS_MS = {
67
68
  refine_mesh_node: 3e4,
68
69
  batch_refine_mesh_nodes: 3e4
69
70
  };
71
+ var IPC_PROBE_RETRYABLE_COMMANDS = /* @__PURE__ */ new Set([
72
+ "get_status_metadata",
73
+ "get_mesh",
74
+ "mesh_status",
75
+ "git_status",
76
+ "git_diff_summary",
77
+ "git_log",
78
+ "git_diff_file",
79
+ "read_chat",
80
+ "get_spec_debug",
81
+ "get_chat_debug_bundle",
82
+ "list_coordinator_prompts",
83
+ "list_provider_availability"
84
+ ]);
85
+ var IPC_PROBE_RETRY_MAX_ENV = "ADHDEV_IPC_PROBE_RETRY_MAX";
86
+ var IPC_PROBE_RETRY_BACKOFF_MS_ENV = "ADHDEV_IPC_PROBE_RETRY_BACKOFF_MS";
87
+ var DEFAULT_IPC_PROBE_RETRY_MAX = 1;
88
+ var DEFAULT_IPC_PROBE_RETRY_BACKOFF_MS = 2e3;
89
+ function parsePositiveInt(raw, fallback) {
90
+ if (raw === void 0 || raw.trim() === "") return fallback;
91
+ const parsed = Number.parseInt(raw, 10);
92
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
93
+ }
94
+ function resolveDefaultCommandTimeoutMs() {
95
+ const parsed = parsePositiveInt(process.env[IPC_COMMAND_TIMEOUT_ENV], DEFAULT_IPC_COMMAND_TIMEOUT_MS);
96
+ return parsed > 0 ? parsed : DEFAULT_IPC_COMMAND_TIMEOUT_MS;
97
+ }
98
+ function resolveProbeRetryMax() {
99
+ return parsePositiveInt(process.env[IPC_PROBE_RETRY_MAX_ENV], DEFAULT_IPC_PROBE_RETRY_MAX);
100
+ }
101
+ function resolveProbeRetryBackoffMs() {
102
+ return parsePositiveInt(process.env[IPC_PROBE_RETRY_BACKOFF_MS_ENV], DEFAULT_IPC_PROBE_RETRY_BACKOFF_MS);
103
+ }
104
+ function isRetryableProbeCommand(type) {
105
+ return IPC_PROBE_RETRYABLE_COMMANDS.has(type);
106
+ }
70
107
  var WS_CONNECTING = 0;
71
108
  var WS_OPEN = 1;
72
109
  var POOL_IDLE_EVICT_MS = 5 * 6e4;
@@ -76,9 +113,10 @@ function buildRequestId() {
76
113
  return `mcp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
77
114
  }
78
115
  function getTimeoutMs(type, nestedCommand) {
116
+ const fallback = resolveDefaultCommandTimeoutMs();
79
117
  return Math.max(
80
- IPC_COMMAND_TIMEOUTS_MS[type] ?? DEFAULT_IPC_COMMAND_TIMEOUT_MS,
81
- IPC_COMMAND_TIMEOUTS_MS[nestedCommand] ?? DEFAULT_IPC_COMMAND_TIMEOUT_MS
118
+ IPC_COMMAND_TIMEOUTS_MS[type] ?? fallback,
119
+ IPC_COMMAND_TIMEOUTS_MS[nestedCommand] ?? fallback
82
120
  );
83
121
  }
84
122
  function getOrCreateConnection(WebSocketCtor, url) {
@@ -203,7 +241,20 @@ var IpcTransport = class {
203
241
  args
204
242
  });
205
243
  }
206
- sendIpcCommand(type, args) {
244
+ async sendIpcCommand(type, args) {
245
+ const maxRetries = isRetryableProbeCommand(type) ? resolveProbeRetryMax() : 0;
246
+ for (let attempt = 0; ; attempt += 1) {
247
+ try {
248
+ return await this.sendIpcCommandOnce(type, args);
249
+ } catch (e) {
250
+ if (!e?.ipcTimeout || attempt >= maxRetries) throw e;
251
+ const backoffMs = resolveProbeRetryBackoffMs();
252
+ console.error(`[ipc] probe command '${type}' timed out; retrying (${attempt + 1}/${maxRetries}) in ${backoffMs}ms \u2014 read-only command, safe to re-send`);
253
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
254
+ }
255
+ }
256
+ }
257
+ sendIpcCommandOnce(type, args) {
207
258
  const WebSocketCtor = globalThis.WebSocket;
208
259
  if (!WebSocketCtor) {
209
260
  return Promise.reject(new Error("WebSocket is not available in this Node runtime; Node 20+ is required for daemon IPC mode"));
@@ -229,7 +280,9 @@ var IpcTransport = class {
229
280
  }
230
281
  const timer = setTimeout(() => {
231
282
  conn.pending.delete(requestId);
232
- reject(new Error(`Daemon IPC ${diagnosticParts.join(" ")} timed out after ${Math.round(timeoutMs / 1e3)}s (requestId=${requestId})`));
283
+ const error = new Error(`Daemon IPC ${diagnosticParts.join(" ")} timed out after ${Math.round(timeoutMs / 1e3)}s (requestId=${requestId})`);
284
+ error.ipcTimeout = true;
285
+ reject(error);
233
286
  }, timeoutMs);
234
287
  conn.pending.set(requestId, { resolve, reject, timer });
235
288
  conn.lastUsedAt = Date.now();
@@ -574,9 +627,14 @@ function buildMagiPanelProposal(detected, opts = {}) {
574
627
  var CANONICAL_MESH_TOOL_NAMES = [
575
628
  "mesh_status",
576
629
  "mesh_list_nodes",
577
- "mesh_enqueue_task",
630
+ // GRAPH-ORCHESTRATION Phase F — batch before task, mirroring ALL_MESH_TOOLS.
578
631
  "mesh_enqueue_batch",
632
+ "mesh_enqueue_task",
579
633
  "mesh_view_queue",
634
+ // GRAPH-ORCHESTRATION Phase E — the coordinator gate + graph view surface.
635
+ "mesh_graph_view",
636
+ "mesh_graph_gate_claim",
637
+ "mesh_graph_gate_release",
580
638
  "mesh_queue_cancel",
581
639
  "mesh_queue_requeue",
582
640
  "mesh_send_task",
@@ -973,9 +1031,30 @@ var MESH_LIST_NODES_TOOL = {
973
1031
  }
974
1032
  }
975
1033
  };
1034
+ var ENQUEUE_TOOL_GROUP = "mesh.enqueue";
1035
+ var ENQUEUE_TOOL_GROUP_MEMBERS = ["mesh_enqueue_batch", "mesh_enqueue_task"];
1036
+ var ENQUEUE_DISCOVERY_KEYWORDS = ["enqueue", "delegate", "task", "graph", "dependency"];
1037
+ var ENQUEUE_RANK_QUERIES = ["enqueue", "delegate"];
1038
+ var ENQUEUE_BATCH_DISCOVERY_META = {
1039
+ toolGroup: ENQUEUE_TOOL_GROUP,
1040
+ toolGroupMembers: ENQUEUE_TOOL_GROUP_MEMBERS,
1041
+ discoveryKeywords: ENQUEUE_DISCOVERY_KEYWORDS,
1042
+ discoveryRankQueries: ENQUEUE_RANK_QUERIES,
1043
+ discoveryRank: 0,
1044
+ enqueueRole: "default"
1045
+ };
1046
+ var ENQUEUE_TASK_DISCOVERY_META = {
1047
+ toolGroup: ENQUEUE_TOOL_GROUP,
1048
+ toolGroupMembers: ENQUEUE_TOOL_GROUP_MEMBERS,
1049
+ discoveryKeywords: ENQUEUE_DISCOVERY_KEYWORDS,
1050
+ discoveryRankQueries: ENQUEUE_RANK_QUERIES,
1051
+ discoveryRank: 10,
1052
+ enqueueRole: "single_task_fallback"
1053
+ };
976
1054
  var MESH_ENQUEUE_TASK_TOOL = {
977
1055
  name: "mesh_enqueue_task",
978
- description: "Add a new task to the mesh work queue. Idle nodes will automatically pull and execute tasks from this queue. Use this instead of mesh_send_task when you do not need to target a specific node. For a dependency-wired MULTI-task graph prefer mesh_enqueue_batch: it inserts the whole set atomically and lets depends_on reference batch-local refs, so no half-registered chain can be left behind. Supports task-level priority (high tasks are pulled ahead of older normal/low tasks), not_before delayed execution (hold a task pending until a time), maxRetries (auto-fail after N requeues), and duplicate detection (by default warns in the response when an in-flight task with the same message+target already exists; pass block_duplicate=true to refuse instead, or allow_duplicate=true to silence the warning).",
1056
+ description: "SINGLE-TASK FALLBACK. Use only when exactly one new worker task is currently known and no downstream graph step can yet be declared. If two or more steps are known\u2014including steps that need outputs, worktree preparation, a condition, or a coordinator action\u2014load and use mesh_enqueue_batch instead. Same-session continuation belongs in mesh_send_task. Adds the task to the mesh work queue; idle nodes automatically pull and execute from it. Use this instead of mesh_send_task when you do not need to target a specific node. Supports task-level priority (high tasks are pulled ahead of older normal/low tasks), not_before delayed execution (hold a task pending until a time), maxRetries (auto-fail after N requeues), and duplicate detection (by default warns in the response when an in-flight task with the same message+target already exists; pass block_duplicate=true to refuse instead, or allow_duplicate=true to silence the warning).",
1057
+ _meta: ENQUEUE_TASK_DISCOVERY_META,
979
1058
  inputSchema: {
980
1059
  type: "object",
981
1060
  properties: {
@@ -1013,7 +1092,8 @@ var MESH_ENQUEUE_TASK_TOOL = {
1013
1092
  };
1014
1093
  var MESH_ENQUEUE_BATCH_TOOL = {
1015
1094
  name: "mesh_enqueue_batch",
1016
- description: "G5: atomically enqueue a dependency-wired set of tasks \u2014 either EVERY task in the batch is inserted or NONE is (a mid-batch error such as a dependency cycle, invalid difficulty, or guardrail violation rolls the whole batch back). Use this instead of N sequential mesh_enqueue_task calls whenever you are wiring a multi-task graph: give each task a batch-local `ref` label and name sibling refs in `depends_on` (forward references allowed \u2014 array order does not matter). A depends_on value that is not a ref in this batch must be an EXISTING queue task id; anything else is rejected. Per-task fields are the same as mesh_enqueue_task. Top-level mission_id applies to every task that lacks its own.",
1095
+ description: "DEFAULT enqueue surface for a plan with two or more known graph steps. Atomically persists the graph plan and worker queue entries. Supports batch-local refs, completion dependencies, selected predecessor outputs through `inputs_from`, declarative `run_if`, delayed `workspace_ref` preparation, and coordinator gates. Worker dispatch still requires the shared dependency predicate: all active worker dependencies completed and no system block. Git workspace preparation is a compensated saga and is reported separately from DB atomicity. Atomicity in detail: either EVERY task in the batch is inserted or NONE is (a mid-batch error such as a dependency cycle, invalid difficulty, or guardrail violation rolls the whole batch back). Give each task a batch-local `ref` label and name sibling refs in `depends_on` (forward references allowed \u2014 array order does not matter). A depends_on value that is not a ref in this batch must be an EXISTING queue task id; anything else is rejected. Per-task fields are the same as mesh_enqueue_task. Top-level mission_id applies to every task that lacks its own. `inputs_from` binds selected predecessor outputs into a later task (no hand-copying worker text), `run_if` branches on those outputs, `gates` declare steps that stop for a coordinator action (Refinery landing, approval, CI wait, publish, deploy) with downstream tasks pointing at them via `gated_by`, and `workspaces` + `workspace_ref` prepare a worktree later for a task that needs one. These are strictly additive: a batch using only message/depends_on behaves exactly as before. Only graph-using batches create a graph, inspectable with mesh_graph_view.",
1096
+ _meta: ENQUEUE_BATCH_DISCOVERY_META,
1017
1097
  inputSchema: {
1018
1098
  type: "object",
1019
1099
  properties: {
@@ -1046,21 +1126,158 @@ var MESH_ENQUEUE_BATCH_TOOL = {
1046
1126
  not_before: { type: "number", description: "G7 delayed execution: hold the task pending until this time (epoch-ms, relative-ms, or ISO string)." },
1047
1127
  notBefore: { type: "number", description: "CamelCase alias for not_before. Also accepts an ISO-8601 timestamp string." },
1048
1128
  max_retries: { type: "number", description: "P3 retry cap (same semantics as mesh_enqueue_task)." },
1049
- maxRetries: { type: "number", description: "CamelCase alias for max_retries." }
1129
+ maxRetries: { type: "number", description: "CamelCase alias for max_retries." },
1130
+ // ── batch v2 graph fields (design :568-570). All optional; a batch
1131
+ // using none of them takes the unchanged compatibility path. ──
1132
+ inputs_from: {
1133
+ type: "array",
1134
+ description: "Bind SELECTED outputs of predecessor steps into this task, instead of hand-copying a worker's text into the instruction. Each entry is {from: <ref of a predecessor task or gate>, select: <RFC-6901 JSON Pointer into that step's completion envelope>, as: <binding name>, required?: bool}. Bound values are appended to your immutable instruction inside a clearly-marked untrusted-evidence envelope with provenance and a digest \u2014 they can never alter routing, permissions, task mode or model. Using this makes the task wait for the graph to bind it before it becomes claimable.",
1135
+ items: { type: "object" }
1136
+ },
1137
+ inputsFrom: { type: "array", description: "CamelCase alias for inputs_from.", items: { type: "object" } },
1138
+ run_if: { type: "object", description: "Declarative condition deciding whether this task runs at all, evaluated against predecessor outputs and released gate outcomes (e.g. only run the deploy when the gate outcome was `passed`). A condition that is false SKIPS the task \u2014 skipped is terminal and, deliberately, is NOT treated as completed, so it never satisfies a downstream dependency. A malformed condition fails closed rather than defaulting to true." },
1139
+ runIf: { type: "object", description: "CamelCase alias for run_if." },
1140
+ on_false: { type: "string", enum: ["skip"], description: "What to do when run_if is false. Only `skip` is defined (and is the default)." },
1141
+ onFalse: { type: "string", enum: ["skip"], description: "CamelCase alias for on_false." },
1142
+ on_upstream_skip: { type: "string", enum: ["skip", "omit_dependency"], description: 'What happens to this task when an upstream step is SKIPPED. `skip` (default) propagates the skip. `omit_dependency` drops that edge from this task\'s dependency projection so it can still run \u2014 the explicit way to say "run anyway if that branch was not taken".' },
1143
+ onUpstreamSkip: { type: "string", enum: ["skip", "omit_dependency"], description: "CamelCase alias for on_upstream_skip." },
1144
+ workspace_ref: { type: "string", description: "Run this task in a worktree declared in the top-level `workspaces` array, prepared LATER rather than before the batch is accepted. The task stays held until that worktree is ready, then it is pinned to it automatically. Worktree preparation is a compensated saga: it is reported separately as workspacePreparation and is never part of the batch's DB atomicity." },
1145
+ workspaceRef: { type: "string", description: "CamelCase alias for workspace_ref." },
1146
+ gated_by: { type: "array", items: { type: "string" }, description: 'Refs of gates in this batch\'s `gates` array that must be RELEASED before this task may run. This is how you say "do not start until I have landed/approved/deployed". Do NOT put a gate ref in depends_on \u2014 a gate is an intentional stop, not an ordinary queue dependency, and listing one there is rejected.' },
1147
+ gatedBy: { type: "array", items: { type: "string" }, description: "CamelCase alias for gated_by." }
1050
1148
  },
1051
1149
  required: ["message", "difficulty"]
1052
1150
  }
1053
1151
  },
1152
+ gates: {
1153
+ type: "array",
1154
+ description: "Coordinator GATES: graph steps that intentionally stop progress until YOU act (Refinery landing, approval, CI wait, publish, deploy). The daemon never performs the action and never auto-passes a gate \u2014 you claim it (mesh_graph_gate_claim), do the thing, then release it (mesh_graph_gate_release). Declare the gate here and point downstream tasks at it with gated_by, so the whole plan can be submitted at once instead of waiting to enqueue the later steps by hand. Gate refs share ONE namespace with task and workspace refs.",
1155
+ items: {
1156
+ type: "object",
1157
+ properties: {
1158
+ ref: { type: "string", description: "Label for this gate; downstream tasks name it in gated_by." },
1159
+ action: { type: "string", enum: ["refinery", "approval", "ci_wait", "publish", "deploy", "custom"], description: "What kind of action this gate is waiting for. A metadata label only \u2014 it tells you (and the view) what the gate means; the daemon never executes it." },
1160
+ instructions: { type: "string", description: "What the coordinator must do at this gate, shown when the gate opens." },
1161
+ depends_on: { type: "array", items: { type: "string" }, description: "Refs of tasks/gates in this batch that must complete before this gate OPENS for a coordinator." },
1162
+ on_timeout: { type: "string", enum: ["hold", "cancel_downstream", "fail_graph"], description: "What happens when the gate passes its deadline. `hold` (default) keeps downstream blocked for an explicit reclaim; `cancel_downstream` cancels the pending downstream branch; `fail_graph` fails the graph. There is deliberately NO auto-release option: a timeout is never treated as the action having succeeded." },
1163
+ deadline_seconds: { type: "number", description: "Seconds from when the gate OPENS until its on_timeout policy fires." },
1164
+ lease_seconds: { type: "number", description: "Default claim lease length for this gate (a claim may override it)." },
1165
+ eligible_coordinator_session_id: { type: "string", description: "Restrict claiming to one coordinator session." }
1166
+ },
1167
+ required: ["ref"]
1168
+ }
1169
+ },
1170
+ workspaces: {
1171
+ type: "array",
1172
+ description: 'Worktrees to prepare LATER for tasks that name them via workspace_ref \u2014 the way to plan "clone a worktree, then work in it" as one batch instead of enqueueing, waiting, and enqueueing again. Preparation is a compensated saga with owned-resource cleanup: it happens outside the batch transaction and is reported as workspacePreparation, so `atomic: true` never claims the git side effects happened.',
1173
+ items: {
1174
+ type: "object",
1175
+ properties: {
1176
+ ref: { type: "string", description: "Label tasks use in workspace_ref. Shares one namespace with task and gate refs." },
1177
+ source_node_id: { type: "string", description: "Node whose workspace the worktree is cloned from." },
1178
+ purpose: { type: "string", description: "Short label folded into the derived branch name." },
1179
+ base_revision: { type: "string", description: "Base revision to prepare from." },
1180
+ desired_path: { type: "string", description: "Optional explicit worktree path." },
1181
+ cleanup_on_graph_failure: { type: "boolean", description: "Remove the worktree this graph created if the graph fails. Only ever removes a worktree the saga itself owns." }
1182
+ },
1183
+ required: ["ref"]
1184
+ }
1185
+ },
1186
+ batch_id: { type: "string", description: "Your own idempotency key for this batch. Re-sending the SAME batch_id with an identical plan replays and inserts nothing new; the same batch_id with a different plan is rejected (batch_id_conflict). Use it whenever a retry might duplicate work. Supplying it records the batch as a graph (so the key can be enforced), which is why the response then carries graphId \u2014 task behavior is unchanged." },
1187
+ batchId: { type: "string", description: "CamelCase alias for batch_id." },
1188
+ orchestration_decision: {
1189
+ type: "object",
1190
+ description: "Optional record of your planning decision, for adoption measurement: {decision, ready_worker_tasks, known_graph_steps, single_reason, capability_blockers}. It is stored as provenance with the graph and never changes execution."
1191
+ },
1192
+ orchestrationDecision: { type: "object", description: "CamelCase alias for orchestration_decision." },
1054
1193
  mission_id: { type: "string", description: "Mission every task in this batch belongs to unless an entry overrides it (full/exact id). For multi-task work, create the mission first (mesh_mission_upsert) and pass it here. An unresolvable id rejects the WHOLE batch (atomic) before anything is inserted." },
1055
1194
  missionId: { type: "string", description: "CamelCase alias for mission_id." },
1056
1195
  block_duplicate: { type: "boolean", description: "G4: when any entry matches an in-flight task with the same message+target, refuse the WHOLE batch (it is atomic) with code duplicate_suspect. Default false = warn-only via duplicateSuspects in the response." },
1057
1196
  blockDuplicate: { type: "boolean", description: "CamelCase alias for block_duplicate." },
1058
1197
  allow_duplicate: { type: "boolean", description: "G4: skip duplicate detection entirely for every entry (intentional re-enqueue)." },
1059
- allowDuplicate: { type: "boolean", description: "CamelCase alias for allow_duplicate." }
1198
+ allowDuplicate: { type: "boolean", description: "CamelCase alias for allow_duplicate." },
1199
+ on_dependency_failure: {
1200
+ type: "string",
1201
+ enum: ["block", "cancel"],
1202
+ description: "on_dependency_failure controls downstream tasks when a required worker task fails or is cancelled. `block` (default) keeps downstream pending and automatically recovers if the predecessor is retried and later completes. `cancel` terminally cancels the dependent branch; it is not revived by predecessor retry."
1203
+ },
1204
+ onDependencyFailure: {
1205
+ type: "string",
1206
+ enum: ["block", "cancel"],
1207
+ description: "CamelCase alias for on_dependency_failure."
1208
+ }
1060
1209
  },
1061
1210
  required: ["tasks"]
1062
1211
  }
1063
1212
  };
1213
+ var MESH_GRAPH_GATE_CLAIM_TOOL = {
1214
+ name: "mesh_graph_gate_claim",
1215
+ description: "Take the lease on a coordinator GATE that is awaiting a coordinator. A gate is a graph step that intentionally STOPS progress until a human/coordinator does something the daemon must not do itself \u2014 a Refinery landing, an approval, waiting on CI, a publish, a deploy. The daemon NEVER performs a gate action and NEVER auto-passes a gate: the only way through is your own mesh_graph_gate_release. Claim returns a monotonically increasing leaseGeneration and an opaque fencingToken \u2014 you MUST present both at release, so keep them. A gate whose lease has lapsed can be taken over by a new claim at a HIGHER generation; when that happens the response sets ambiguousExternalOutcome, meaning the previous owner may already have performed the external side effect \u2014 reconcile external evidence (did the merge/publish already land?) before doing it again. Use mesh_graph_view to find gates awaiting a coordinator.",
1216
+ inputSchema: {
1217
+ type: "object",
1218
+ properties: {
1219
+ gate_id: { type: "string", description: "The gate to claim (from mesh_graph_view or the mesh_enqueue_batch response)." },
1220
+ gateId: { type: "string", description: "CamelCase alias for gate_id." },
1221
+ lease_seconds: { type: "number", description: "How long to hold the lease before it lapses. Defaults to the gate spec's lease_seconds, then 900s. Pick a duration that covers the real action \u2014 a lapsed lease cannot release (elapsed time is never completion evidence)." },
1222
+ leaseSeconds: { type: "number", description: "CamelCase alias for lease_seconds." },
1223
+ extend_deadline_seconds: { type: "number", description: "Push the gate DEADLINE out by this many seconds from now. Distinct from the lease: the deadline is when the on_timeout policy (hold / cancel_downstream / fail_graph) fires. Reclaiming a gate that expired under on_timeout=hold does NOT refresh its deadline unless you pass this, so the next sweep would expire it again." },
1224
+ extendDeadlineSeconds: { type: "number", description: "CamelCase alias for extend_deadline_seconds." },
1225
+ coordinator_session_id: { type: "string", description: "Owner session for the lease. Defaults to this coordinator session; pass explicitly only when driving a gate on behalf of another session." },
1226
+ coordinatorSessionId: { type: "string", description: "CamelCase alias for coordinator_session_id." }
1227
+ },
1228
+ required: ["gate_id"]
1229
+ }
1230
+ };
1231
+ var MESH_GRAPH_GATE_RELEASE_TOOL = {
1232
+ name: "mesh_graph_gate_release",
1233
+ description: "Release a coordinator gate you hold the lease on \u2014 the ONLY way a gate lets its downstream work run. Requires the leaseGeneration + fencingToken from your mesh_graph_gate_claim: a stale generation or wrong token is refused (stale_fence), and an EXPIRED lease can never release (re-claim first, and reconcile whether the external action already happened). Pass an idempotency_key of your choosing: re-sending the identical release with the same key is a safe no-op success, while the same key with a DIFFERENT payload is rejected as a conflict. `outcome` (passed / failed / rejected, or an action-specific label) and any structured `result` / `evidence` become readable by downstream tasks through run_if and inputs_from, so a gate decision can steer the rest of the graph. Validation failures roll the WHOLE release back: the gate stays claimed and downstream stays blocked.",
1234
+ inputSchema: {
1235
+ type: "object",
1236
+ properties: {
1237
+ gate_id: { type: "string", description: "The gate being released." },
1238
+ gateId: { type: "string", description: "CamelCase alias for gate_id." },
1239
+ fencing_token: { type: "string", description: "The opaque token returned by mesh_graph_gate_claim. Required." },
1240
+ fencingToken: { type: "string", description: "CamelCase alias for fencing_token." },
1241
+ lease_generation: { type: "number", description: "The leaseGeneration returned by mesh_graph_gate_claim. Required \u2014 a stale generation is refused." },
1242
+ leaseGeneration: { type: "number", description: "CamelCase alias for lease_generation." },
1243
+ idempotency_key: { type: "string", description: "Your own key for this release. Re-sending the identical release with the same key is a no-op success; the same key with a different payload is a conflict. Required." },
1244
+ idempotencyKey: { type: "string", description: "CamelCase alias for idempotency_key." },
1245
+ outcome: { type: "string", description: "The gate outcome: passed | failed | rejected, or an action-specific structured label. Downstream run_if conditions read it as /gate_outcome." },
1246
+ result: { type: "object", description: "Optional action-specific structured result, exposed to downstream bindings as /result/... (e.g. the merged commit sha)." },
1247
+ evidence: { type: "object", description: "Optional evidence references/digests, exposed to downstream bindings as /evidence/... ." },
1248
+ patches: {
1249
+ type: "array",
1250
+ description: "Optional pre-assignment patches to DIRECT downstream nodes. Only run_if, on_false, inputs_from and workspace_ref may be patched \u2014 message, routing, permissions, task mode and model are immutable by policy, and a task that is already claimed cannot be patched at all.",
1251
+ items: {
1252
+ type: "object",
1253
+ properties: {
1254
+ node: { type: "string", description: "Ref or node id of a DIRECT downstream node of this gate." },
1255
+ base_spec_patch: { type: "object", description: "Keys to merge into that node's spec. Allowed keys: run_if, on_false, inputs_from, workspace_ref." },
1256
+ baseSpecPatch: { type: "object", description: "CamelCase alias for base_spec_patch." }
1257
+ },
1258
+ required: ["node"]
1259
+ }
1260
+ }
1261
+ },
1262
+ required: ["gate_id", "fencing_token", "lease_generation", "idempotency_key", "outcome"]
1263
+ }
1264
+ };
1265
+ var MESH_GRAPH_VIEW_TOOL = {
1266
+ name: "mesh_graph_view",
1267
+ description: "Inspect orchestration GRAPHS on this mesh: node states and refs, active edges, materialization receipts, coordinator gates (with who holds the lease and what is blocked), delayed workspace sagas, derived dependency failures, and the next action a coordinator actually has to take. Read-only. Defaults to in-flight graphs; pass include_terminal=true for completed ones. Only mesh_enqueue_batch requests that use graph features (gates, inputs_from, run_if, workspace_ref) create a graph \u2014 a plain depends_on batch runs on the ordinary queue and appears in mesh_view_queue instead. Do not poll this waiting for generating work; use it when you need to know why something is blocked or which gate is waiting on you.",
1268
+ inputSchema: {
1269
+ type: "object",
1270
+ properties: {
1271
+ graph_id: { type: "string", description: "Show exactly this graph (including terminal ones)." },
1272
+ graphId: { type: "string", description: "CamelCase alias for graph_id." },
1273
+ batch_id: { type: "string", description: "Show the graph committed under this batch_id." },
1274
+ batchId: { type: "string", description: "CamelCase alias for batch_id." },
1275
+ include_terminal: { type: "boolean", description: "Include completed/failed/cancelled graphs. Default false (in-flight only)." },
1276
+ includeTerminal: { type: "boolean", description: "CamelCase alias for include_terminal." },
1277
+ limit: { type: "number", description: "Max graphs to return (default 20)." }
1278
+ }
1279
+ }
1280
+ };
1064
1281
  var MESH_VIEW_QUEUE_TOOL = {
1065
1282
  name: "mesh_view_queue",
1066
1283
  description: "View the mesh work queue with source-of-truth active counts separated from historical completed/failed/cancelled records. Do not repeatedly call this to wait for generating assigned work; wait for pendingCoordinatorEvents/completion events or an explicit user status request.",
@@ -1889,9 +2106,17 @@ var MESH_COORDINATOR_PROMPT_APPEND_SET_TOOL = {
1889
2106
  var ALL_MESH_TOOLS = [
1890
2107
  MESH_STATUS_TOOL,
1891
2108
  MESH_LIST_NODES_TOOL,
1892
- MESH_ENQUEUE_TASK_TOOL,
2109
+ // GRAPH-ORCHESTRATION Phase F — batch BEFORE task. Registry order is what a
2110
+ // client that lists tools without ranking sees first, so the default enqueue
2111
+ // surface leads and the single-task fallback follows it.
1893
2112
  MESH_ENQUEUE_BATCH_TOOL,
2113
+ MESH_ENQUEUE_TASK_TOOL,
1894
2114
  MESH_VIEW_QUEUE_TOOL,
2115
+ // GRAPH-ORCHESTRATION Phase E — placed next to the queue/enqueue tools so a
2116
+ // coordinator that loaded the batch schema also discovers how to pass a gate.
2117
+ MESH_GRAPH_VIEW_TOOL,
2118
+ MESH_GRAPH_GATE_CLAIM_TOOL,
2119
+ MESH_GRAPH_GATE_RELEASE_TOOL,
1895
2120
  MESH_QUEUE_CANCEL_TOOL,
1896
2121
  MESH_QUEUE_REQUEUE_TOOL,
1897
2122
  MESH_SEND_TASK_TOOL,
@@ -2470,6 +2695,15 @@ function annotateRapidReadChatAdvisory(payload, options) {
2470
2695
  // src/tools/mesh-tools-internal.ts
2471
2696
  var import_daemon_core6 = require("@adhdev/daemon-core");
2472
2697
  var import_node_crypto = require("crypto");
2698
+ function recordMeshCoordinatorToolCall(ctx, tool) {
2699
+ const sessionId = ctx.coordinatorSessionId ?? null;
2700
+ return (0, import_daemon_core5.recordMeshToolCall)({
2701
+ meshId: ctx.mesh.id,
2702
+ tool,
2703
+ sessionId,
2704
+ callerRole: sessionId ? "coordinator" : "unknown"
2705
+ });
2706
+ }
2473
2707
  var SESSION_PROVIDER_METADATA_TTL_MS = 30 * 6e4;
2474
2708
  var meshSessionProviderMetadata = /* @__PURE__ */ new Map();
2475
2709
  function getSessionMetadata(key) {
@@ -4030,7 +4264,7 @@ function summarizePendingEventProtocolMetrics(pendingEvents) {
4030
4264
  };
4031
4265
  }
4032
4266
  async function meshStatus(ctx, args = {}) {
4033
- const rateResult = (0, import_daemon_core6.recordMeshToolCall)({ meshId: ctx.mesh.id, tool: "mesh_status" });
4267
+ const rateResult = recordMeshCoordinatorToolCall(ctx, "mesh_status");
4034
4268
  const compact = args.verbose === true ? false : args.compact ?? true;
4035
4269
  await refreshMeshFromDaemon(ctx);
4036
4270
  const { mesh, transport } = ctx;
@@ -4604,6 +4838,277 @@ async function meshListNodes(ctx) {
4604
4838
  }, null, 2);
4605
4839
  }
4606
4840
 
4841
+ // src/tools/mesh-tools-graph.ts
4842
+ function readGraphTaskFields(entry) {
4843
+ const inputsFrom = entry.inputs_from ?? entry.inputsFrom;
4844
+ const runIf = entry.run_if ?? entry.runIf;
4845
+ const onFalse = entry.on_false ?? entry.onFalse;
4846
+ const onUpstreamSkip = readString(entry.on_upstream_skip) || readString(entry.onUpstreamSkip) || void 0;
4847
+ const workspaceRef = readString(entry.workspace_ref) || readString(entry.workspaceRef) || void 0;
4848
+ const rawGatedBy = entry.gated_by ?? entry.gatedBy;
4849
+ const gatedBy = Array.isArray(rawGatedBy) ? rawGatedBy.map((g) => readString(g)).filter((g) => !!g) : void 0;
4850
+ return {
4851
+ ...inputsFrom !== void 0 ? { inputs_from: inputsFrom } : {},
4852
+ ...runIf !== void 0 ? { run_if: runIf } : {},
4853
+ ...onFalse !== void 0 ? { on_false: onFalse } : {},
4854
+ ...onUpstreamSkip ? { on_upstream_skip: onUpstreamSkip } : {},
4855
+ ...workspaceRef ? { workspace_ref: workspaceRef } : {},
4856
+ ...gatedBy && gatedBy.length > 0 ? { gated_by: gatedBy } : {}
4857
+ };
4858
+ }
4859
+ function normalizeWorkspaceDeclarations(raw) {
4860
+ if (!Array.isArray(raw)) return [];
4861
+ return raw.map((w) => ({
4862
+ ref: readString(w?.ref) || "",
4863
+ ...readString(w?.source_node_id ?? w?.sourceNodeId) ? { source_node_id: readString(w.source_node_id ?? w.sourceNodeId) } : {},
4864
+ ...readString(w?.purpose) ? { purpose: readString(w.purpose) } : {},
4865
+ ...readString(w?.base_revision ?? w?.baseRevision) ? { base_revision: readString(w.base_revision ?? w.baseRevision) } : {},
4866
+ ...readString(w?.desired_path ?? w?.desiredPath) ? { desired_path: readString(w.desired_path ?? w.desiredPath) } : {},
4867
+ ...(w?.cleanup_on_graph_failure ?? w?.cleanupOnGraphFailure) === true ? { cleanup_on_graph_failure: true } : {}
4868
+ }));
4869
+ }
4870
+ function buildGraphPlanShape(specs, rawEntries, gates, workspaces, hasExplicitBatchId) {
4871
+ const gateSpecs = Array.isArray(gates) ? gates : [];
4872
+ const workspaceSpecs = normalizeWorkspaceDeclarations(workspaces);
4873
+ const tasks = specs.map((spec, i) => ({ ...spec, ...readGraphTaskFields(rawEntries[i] ?? {}) }));
4874
+ return {
4875
+ tasks,
4876
+ gates: gateSpecs,
4877
+ workspaces: workspaceSpecs,
4878
+ useGraphPath: hasExplicitBatchId || (0, import_daemon_core6.requestUsesGraphV2)({ tasks, gates: gateSpecs, workspaces: workspaceSpecs })
4879
+ };
4880
+ }
4881
+ var GATE_RELEASE_ERROR_CODES = [
4882
+ "gate_not_found",
4883
+ "gate_release_conflict",
4884
+ "gate_already_released",
4885
+ "gate_not_claimed",
4886
+ "stale_fence",
4887
+ "gate_lease_expired",
4888
+ "gate_upstream_unsettled",
4889
+ "gate_patch_not_downstream",
4890
+ "gate_patch_forbidden",
4891
+ "task_already_claimed",
4892
+ "graph_node_not_found"
4893
+ ];
4894
+ function classifyGateError(message) {
4895
+ return GATE_RELEASE_ERROR_CODES.find((code) => message.startsWith(`${code}:`) || message.includes(`${code}:`));
4896
+ }
4897
+ function resolveGateSession(ctx, explicit) {
4898
+ return readString(explicit) || ctx.coordinatorSessionId || void 0;
4899
+ }
4900
+ async function meshGraphGateClaim(ctx, args) {
4901
+ recordMeshCoordinatorToolCall(ctx, "mesh_graph_gate_claim");
4902
+ const gateId = readString(args.gate_id) || readString(args.gateId);
4903
+ if (!gateId) {
4904
+ return JSON.stringify({
4905
+ success: false,
4906
+ code: "missing_gate_id",
4907
+ error: "mesh_graph_gate_claim requires gate_id. Use mesh_graph_view to list gates awaiting a coordinator."
4908
+ });
4909
+ }
4910
+ const coordinatorSessionId = resolveGateSession(ctx, args.coordinator_session_id ?? args.coordinatorSessionId);
4911
+ if (!coordinatorSessionId) {
4912
+ return JSON.stringify({
4913
+ success: false,
4914
+ code: "missing_coordinator_session",
4915
+ error: "No coordinator session id is available for this call, so a gate lease cannot be attributed to an owner. Pass coordinator_session_id explicitly."
4916
+ });
4917
+ }
4918
+ const leaseSeconds = readNumber(args.lease_seconds ?? args.leaseSeconds);
4919
+ const extendDeadlineSeconds = readNumber(args.extend_deadline_seconds ?? args.extendDeadlineSeconds);
4920
+ try {
4921
+ const result = (0, import_daemon_core6.claimMeshGraphGate)({
4922
+ meshId: ctx.mesh.id,
4923
+ gateId,
4924
+ coordinatorSessionId,
4925
+ ...leaseSeconds !== void 0 ? { leaseSeconds } : {},
4926
+ ...extendDeadlineSeconds !== void 0 ? { extendDeadlineSeconds } : {}
4927
+ });
4928
+ if (!result.claimed) {
4929
+ return JSON.stringify({
4930
+ success: false,
4931
+ claimed: false,
4932
+ code: result.reason ?? "gate_not_claimable",
4933
+ gateId,
4934
+ ...result.gate ? { gateState: result.gate.state, action: result.gate.action } : {},
4935
+ error: describeClaimRefusal(result.reason, result.gate?.state)
4936
+ });
4937
+ }
4938
+ (0, import_daemon_core6.recordGraphGateClaimed)(ctx.mesh.id, {
4939
+ graphId: result.gate.graphId,
4940
+ gateId,
4941
+ ref: result.gate.ref,
4942
+ action: result.gate.action,
4943
+ generation: result.leaseGeneration,
4944
+ ownerSessionId: coordinatorSessionId,
4945
+ leaseExpiresAt: result.leaseExpiresAt,
4946
+ ambiguousExternalOutcome: result.ambiguousExternalOutcome,
4947
+ previousLeaseOwnerSessionId: result.previousLeaseOwnerSessionId
4948
+ });
4949
+ return JSON.stringify({
4950
+ success: true,
4951
+ claimed: true,
4952
+ gateId,
4953
+ graphId: result.gate.graphId,
4954
+ ...result.gate.ref ? { ref: result.gate.ref } : {},
4955
+ action: result.gate.action,
4956
+ ...result.gate.instructions ? { instructions: result.gate.instructions } : {},
4957
+ // ★ Both values are REQUIRED by mesh_graph_gate_release. Losing them
4958
+ // means the lease must lapse before anyone can act on the gate again.
4959
+ leaseGeneration: result.leaseGeneration,
4960
+ fencingToken: result.fencingToken,
4961
+ leaseExpiresAt: result.leaseExpiresAt,
4962
+ ...result.deadlineAt ? { deadlineAt: result.deadlineAt } : {},
4963
+ onTimeout: result.gate.onTimeout,
4964
+ ...result.ambiguousExternalOutcome ? {
4965
+ ambiguousExternalOutcome: true,
4966
+ previousLeaseOwnerSessionId: result.previousLeaseOwnerSessionId,
4967
+ ambiguousExternalOutcomeHint: "This claim TOOK OVER from a previous owner whose lease lapsed. That owner may already have performed the external side effect (merge, publish, deploy). Reconcile external evidence \u2014 check whether the action already landed \u2014 BEFORE performing it again, then release with the real outcome."
4968
+ } : {},
4969
+ nextStep: "Perform the gate action yourself, then call mesh_graph_gate_release with this leaseGeneration + fencingToken and an idempotency_key. The daemon never performs a gate action and never auto-releases: the gate stays shut until you release it (a deadline can only EXPIRE it, never pass it)."
4970
+ });
4971
+ } catch (e) {
4972
+ const message = e?.message || String(e);
4973
+ return JSON.stringify({ success: false, claimed: false, gateId, code: classifyGateError(message), error: message });
4974
+ }
4975
+ }
4976
+ function describeClaimRefusal(reason, state) {
4977
+ switch (reason) {
4978
+ case "gate_not_found":
4979
+ return "No such gate on this mesh. Use mesh_graph_view to list gates.";
4980
+ case "gate_not_eligible":
4981
+ return "This gate is restricted to a different coordinator session (eligible_coordinator_session_id).";
4982
+ case "gate_not_awaiting":
4983
+ return "The gate is still `declared` \u2014 its predecessors have not completed yet, so it is not open for a coordinator. Wait for the upstream work; the gate opens itself when its predecessors settle.";
4984
+ case "gate_lease_held":
4985
+ return "Another coordinator holds a LIVE lease on this gate. Wait for the lease to lapse \u2014 a live lease is never taken over, because the holder may be mid-action.";
4986
+ case "gate_claim_race":
4987
+ return "Another claim won the race for this gate. Re-read the gate state and retry if it is still awaiting.";
4988
+ default:
4989
+ if (reason?.startsWith("gate_terminal:")) {
4990
+ return `The gate is terminal (${state ?? reason.slice("gate_terminal:".length)}) and cannot be claimed. A released gate stays released; an expired gate under cancel_downstream/fail_graph has already applied its policy.`;
4991
+ }
4992
+ return `The gate could not be claimed (${reason ?? "unknown reason"}).`;
4993
+ }
4994
+ }
4995
+ async function meshGraphGateRelease(ctx, args) {
4996
+ recordMeshCoordinatorToolCall(ctx, "mesh_graph_gate_release");
4997
+ const gateId = readString(args.gate_id) || readString(args.gateId);
4998
+ const fencingToken = readString(args.fencing_token) || readString(args.fencingToken);
4999
+ const leaseGeneration = readNumber(args.lease_generation ?? args.leaseGeneration);
5000
+ const idempotencyKey = readString(args.idempotency_key) || readString(args.idempotencyKey);
5001
+ const outcome = readString(args.outcome);
5002
+ const missing = [
5003
+ !gateId ? "gate_id" : null,
5004
+ !fencingToken ? "fencing_token" : null,
5005
+ leaseGeneration === void 0 ? "lease_generation" : null,
5006
+ !idempotencyKey ? "idempotency_key" : null,
5007
+ !outcome ? "outcome" : null
5008
+ ].filter((f) => f !== null);
5009
+ if (missing.length > 0) {
5010
+ return JSON.stringify({
5011
+ success: false,
5012
+ code: "missing_release_fields",
5013
+ missing,
5014
+ error: `mesh_graph_gate_release requires ${missing.join(", ")}. fencing_token and lease_generation come from the mesh_graph_gate_claim response; idempotency_key is yours to choose and makes a retried release a no-op instead of a double release.`
5015
+ });
5016
+ }
5017
+ const patches = (args.patches ?? []).map((p) => ({
5018
+ node: readString(p?.node) || "",
5019
+ baseSpecPatch: p?.base_spec_patch ?? p?.baseSpecPatch ?? {}
5020
+ })).filter((p) => p.node.length > 0);
5021
+ try {
5022
+ const result = (0, import_daemon_core6.releaseMeshGraphGate)({
5023
+ meshId: ctx.mesh.id,
5024
+ gateId,
5025
+ fencingToken,
5026
+ leaseGeneration,
5027
+ idempotencyKey,
5028
+ outcome,
5029
+ ...args.result !== void 0 ? { result: args.result } : {},
5030
+ ...args.evidence !== void 0 ? { evidence: args.evidence } : {},
5031
+ ...patches.length > 0 ? { patches } : {}
5032
+ });
5033
+ (0, import_daemon_core6.recordGraphGateReleased)(ctx.mesh.id, {
5034
+ graphId: result.gate.graphId,
5035
+ gateId,
5036
+ ref: result.gate.ref,
5037
+ action: result.gate.action,
5038
+ outcome,
5039
+ generation: leaseGeneration,
5040
+ releaseDigest: result.gate.releaseEvidenceDigest,
5041
+ materializedNodeIds: result.materializedNodeIds,
5042
+ duplicate: result.duplicate
5043
+ });
5044
+ const queueTrigger = result.materializedNodeIds.length > 0 ? await triggerMeshQueueAndReport(ctx) : void 0;
5045
+ return JSON.stringify({
5046
+ success: true,
5047
+ released: true,
5048
+ // A replayed release (same key + same digest) is a NO-OP SUCCESS, not an
5049
+ // error — that is what makes a retried release safe (design :420-421).
5050
+ duplicate: result.duplicate,
5051
+ gateId,
5052
+ graphId: result.gate.graphId,
5053
+ ...result.gate.ref ? { ref: result.gate.ref } : {},
5054
+ outcome,
5055
+ materializedNodeIds: result.materializedNodeIds,
5056
+ materializedCount: result.materializedNodeIds.length,
5057
+ ...queueTrigger ? { queueTrigger } : {},
5058
+ ...result.duplicate ? { duplicateHint: "This idempotency_key + payload was already committed; nothing changed. Re-sending an identical release is safe." } : {}
5059
+ });
5060
+ } catch (e) {
5061
+ const message = e?.message || String(e);
5062
+ const code = classifyGateError(message);
5063
+ return JSON.stringify({
5064
+ success: false,
5065
+ released: false,
5066
+ gateId,
5067
+ ...code ? { code } : {},
5068
+ error: message,
5069
+ ...code === "gate_lease_expired" ? {
5070
+ hint: "The lease expired before the release. Elapsed time is never completion evidence, so the release is refused. Re-claim the gate (you get a HIGHER generation), reconcile whether the external action already landed, then release."
5071
+ } : {},
5072
+ ...code === "stale_fence" ? { hint: "Another coordinator has claimed this gate since your claim. Your token is stale \u2014 re-claim before releasing." } : {},
5073
+ ...code === "gate_release_conflict" ? { hint: "This idempotency_key was already used with a DIFFERENT payload. Use a new key, or re-send the identical payload." } : {}
5074
+ });
5075
+ }
5076
+ }
5077
+ async function meshGraphView(ctx, args) {
5078
+ recordMeshCoordinatorToolCall(ctx, "mesh_graph_view");
5079
+ try {
5080
+ await refreshMeshFromDaemon(ctx);
5081
+ const graphId = readString(args.graph_id) || readString(args.graphId);
5082
+ const batchId = readString(args.batch_id) || readString(args.batchId);
5083
+ const includeTerminal = args.include_terminal === true || args.includeTerminal === true;
5084
+ const graphs = (0, import_daemon_core6.buildMeshGraphViews)(ctx.mesh.id, {
5085
+ ...graphId ? { graphId } : {},
5086
+ ...batchId ? { batchId } : {},
5087
+ activeOnly: !includeTerminal,
5088
+ ...readNumber(args.limit) !== void 0 ? { limit: readNumber(args.limit) } : {}
5089
+ });
5090
+ const pendingActions = graphs.flatMap((g) => (g.nextCoordinatorAction ?? []).map((a) => ({ graphId: g.graphId, ...a })));
5091
+ return JSON.stringify({
5092
+ success: true,
5093
+ meshId: ctx.mesh.id,
5094
+ graphCount: graphs.length,
5095
+ ...graphId || batchId ? {} : { scope: includeTerminal ? "all" : "in_flight" },
5096
+ graphs,
5097
+ ...pendingActions.length > 0 ? { pendingCoordinatorActions: pendingActions } : {},
5098
+ ...graphs.length === 0 ? {
5099
+ hint: graphId || batchId ? "No graph with that id on this mesh." : "No in-flight orchestration graphs. Only mesh_enqueue_batch requests that use graph features (gates, inputs_from, run_if, workspace_ref) create a graph; a plain depends_on batch runs on the unchanged queue path. Pass include_terminal=true to see completed graphs."
5100
+ } : {}
5101
+ });
5102
+ } catch (e) {
5103
+ return JSON.stringify({ success: false, error: e?.message || String(e) });
5104
+ }
5105
+ }
5106
+ function readNumber(value) {
5107
+ if (typeof value === "number" && Number.isFinite(value)) return value;
5108
+ if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
5109
+ return void 0;
5110
+ }
5111
+
4607
5112
  // src/tools/mesh-tools-queue.ts
4608
5113
  function normalizeDedupMessage(message) {
4609
5114
  return (message || "").trim().replace(/\s+/g, " ").toLowerCase();
@@ -4894,7 +5399,8 @@ var BATCH_ENQUEUE_ERROR_CODES = [
4894
5399
  "task_graph_too_large",
4895
5400
  "empty_task_graph",
4896
5401
  "missing_task_difficulty",
4897
- "invalid_task_difficulty"
5402
+ "invalid_task_difficulty",
5403
+ "invalid_on_dependency_failure"
4898
5404
  ];
4899
5405
  async function meshEnqueueBatch(ctx, args) {
4900
5406
  const rawTasks = Array.isArray(args.tasks) ? args.tasks : void 0;
@@ -4913,6 +5419,20 @@ async function meshEnqueueBatch(ctx, args) {
4913
5419
  });
4914
5420
  }
4915
5421
  const batchMissionId = readString(args.missionId) || readString(args.mission_id) || void 0;
5422
+ const rawFailurePolicy = args.on_dependency_failure ?? args.onDependencyFailure;
5423
+ let onDependencyFailure;
5424
+ if (rawFailurePolicy !== void 0) {
5425
+ try {
5426
+ onDependencyFailure = (0, import_daemon_core6.parseOnDependencyFailurePolicy)(rawFailurePolicy);
5427
+ } catch (e) {
5428
+ const message = e instanceof import_daemon_core6.MeshGraphPolicyError || e instanceof Error ? e.message : "invalid_on_dependency_failure";
5429
+ return JSON.stringify({
5430
+ success: false,
5431
+ code: "invalid_on_dependency_failure",
5432
+ error: message
5433
+ });
5434
+ }
5435
+ }
4916
5436
  if (batchMissionId && !(0, import_daemon_core6.getMeshMission)(ctx.mesh.id, batchMissionId)) {
4917
5437
  return JSON.stringify({
4918
5438
  success: false,
@@ -4924,6 +5444,7 @@ async function meshEnqueueBatch(ctx, args) {
4924
5444
  const allowDuplicate = args.allowDuplicate === true || args.allow_duplicate === true;
4925
5445
  const blockDuplicate = args.blockDuplicate === true || args.block_duplicate === true;
4926
5446
  const specs = [];
5447
+ const rawGraphEntries = [];
4927
5448
  const normalizedEntries = [];
4928
5449
  const duplicateSuspects = [];
4929
5450
  for (let i = 0; i < rawTasks.length; i++) {
@@ -4955,6 +5476,7 @@ async function meshEnqueueBatch(ctx, args) {
4955
5476
  }
4956
5477
  }
4957
5478
  normalizedEntries.push({ ...v, ...ref ? { ref } : {} });
5479
+ rawGraphEntries.push(entry);
4958
5480
  specs.push({
4959
5481
  ...ref ? { ref } : {},
4960
5482
  message: v.message,
@@ -4983,18 +5505,75 @@ async function meshEnqueueBatch(ctx, args) {
4983
5505
  atomic: true
4984
5506
  });
4985
5507
  }
5508
+ const batchIdArg = readString(args.batch_id) || readString(args.batchId) || void 0;
5509
+ const plan = buildGraphPlanShape(specs, rawGraphEntries, args.gates, args.workspaces, !!batchIdArg);
5510
+ const useGraphPath = plan.useGraphPath;
5511
+ const decision = (0, import_daemon_core6.normalizeOrchestrationDecision)(
5512
+ args.orchestration_decision ?? args.orchestrationDecision,
5513
+ "batch"
5514
+ );
4986
5515
  let tasks;
5516
+ let graphPlan;
4987
5517
  try {
4988
- tasks = (0, import_daemon_core6.enqueueTaskGraph)(ctx.mesh.id, specs);
5518
+ if (useGraphPath) {
5519
+ graphPlan = (0, import_daemon_core6.commitMeshGraphPlan)({
5520
+ meshId: ctx.mesh.id,
5521
+ tasks: plan.tasks,
5522
+ gates: plan.gates,
5523
+ workspaces: plan.workspaces,
5524
+ ...batchIdArg ? { batchId: batchIdArg } : {},
5525
+ ...batchMissionId ? { missionId: batchMissionId } : {},
5526
+ ...onDependencyFailure ? { onDependencyFailure } : {},
5527
+ ...ctx.coordinatorSessionId ? { sourceCoordinatorSessionId: ctx.coordinatorSessionId } : {},
5528
+ enqueueSurface: "batch",
5529
+ orchestrationDecision: decision.decision
5530
+ });
5531
+ tasks = graphPlan.tasks;
5532
+ } else {
5533
+ tasks = (0, import_daemon_core6.enqueueTaskGraph)(ctx.mesh.id, specs);
5534
+ }
4989
5535
  } catch (e) {
4990
5536
  const message = e?.message || String(e);
4991
- const code = BATCH_ENQUEUE_ERROR_CODES.find((c) => message.includes(c));
5537
+ const code = e instanceof import_daemon_core6.MeshGraphPlanError ? e.code : BATCH_ENQUEUE_ERROR_CODES.find((c) => message.includes(c));
5538
+ if (useGraphPath) {
5539
+ (0, import_daemon_core6.recordGraphEnqueueRolledBack)(ctx.mesh.id, {
5540
+ code: code ?? "graph_plan_failed",
5541
+ ...batchIdArg ? { batchId: batchIdArg } : {},
5542
+ taskCount: specs.length,
5543
+ error: message
5544
+ });
5545
+ } else {
5546
+ (0, import_daemon_core6.recordGraphEnqueueValidationFailed)(ctx.mesh.id, {
5547
+ code: code ?? "batch_enqueue_failed",
5548
+ ...batchIdArg ? { batchId: batchIdArg } : {},
5549
+ taskCount: specs.length
5550
+ });
5551
+ }
4992
5552
  return JSON.stringify({
4993
5553
  success: false,
4994
5554
  ...code ? { code } : {},
4995
5555
  error: message,
4996
5556
  enqueued: 0,
4997
- atomic: true
5557
+ atomic: true,
5558
+ ...e instanceof import_daemon_core6.MeshGraphPlanError && e.extra ? e.extra : {}
5559
+ });
5560
+ }
5561
+ if (graphPlan) {
5562
+ (0, import_daemon_core6.recordGraphEnqueueCommitted)(ctx.mesh.id, {
5563
+ graphId: graphPlan.graphId,
5564
+ batchId: graphPlan.batchId,
5565
+ enqueueSurface: "batch",
5566
+ schemaVersion: 2,
5567
+ planDigest: graphPlan.planDigest,
5568
+ ...batchMissionId ? { missionId: batchMissionId } : {},
5569
+ ...ctx.coordinatorSessionId ? { coordinatorSessionId: ctx.coordinatorSessionId } : {},
5570
+ taskCount: graphPlan.tasks.length,
5571
+ gateCount: graphPlan.gates.length,
5572
+ workspaceCount: graphPlan.workspaces.length,
5573
+ dependencyEdgeCount: graphPlan.dependencyEdgeCount,
5574
+ onDependencyFailure: onDependencyFailure ?? "block",
5575
+ orchestrationDecision: decision.decision,
5576
+ ...graphPlan.replayed ? { replayed: true } : {}
4998
5577
  });
4999
5578
  }
5000
5579
  const distinctMissionIds = [...new Set(specs.map((s) => s.missionId).filter((m) => !!m))];
@@ -5017,12 +5596,13 @@ async function meshEnqueueBatch(ctx, args) {
5017
5596
  const queueTrigger = await triggerMeshQueueAndReport(ctx);
5018
5597
  let eagerPushDeferredCount = 0;
5019
5598
  if (ctx.transport instanceof IpcTransport) {
5020
- const dependencyStatusById = new Map(
5021
- (0, import_daemon_core6.getQueue)(ctx.mesh.id).map((t) => [t.id, t.status])
5022
- );
5599
+ const liveQueue = (0, import_daemon_core6.getQueue)(ctx.mesh.id);
5600
+ const dependencyStatusById = new Map(liveQueue.map((t) => [t.id, t.status]));
5601
+ const liveById = new Map(liveQueue.map((t) => [t.id, t]));
5023
5602
  const coordinatorDaemonId = resolveCoordinatorDaemonId(ctx);
5024
5603
  const dispatchPromises = [];
5025
- tasks.forEach((task, i) => {
5604
+ tasks.forEach((snapshot, i) => {
5605
+ const task = liveById.get(snapshot.id) ?? snapshot;
5026
5606
  if (!(0, import_daemon_core6.taskDependenciesSatisfied)(task, dependencyStatusById)) {
5027
5607
  eagerPushDeferredCount++;
5028
5608
  return;
@@ -5044,11 +5624,37 @@ async function meshEnqueueBatch(ctx, args) {
5044
5624
  source: "queue",
5045
5625
  atomic: true,
5046
5626
  enqueued: tasks.length,
5627
+ ...onDependencyFailure ? { on_dependency_failure: onDependencyFailure } : {},
5628
+ // ── batch v2 additive response fields (design :582) ──
5629
+ // Present only on the graph path: an old-path batch creates no graph, so
5630
+ // reporting a graphId for it would be a lie.
5631
+ ...graphPlan ? {
5632
+ graphId: graphPlan.graphId,
5633
+ batchId: graphPlan.batchId,
5634
+ planDigest: graphPlan.planDigest,
5635
+ // design :585-588 — DB atomicity NEVER implies the git worktree exists.
5636
+ workspacePreparation: graphPlan.workspacePreparation,
5637
+ ...graphPlan.replayed ? {
5638
+ replayed: true,
5639
+ replayedHint: "A graph with this batch_id and an identical plan digest already existed; nothing was re-inserted. Re-sending the same batch_id with a DIFFERENT plan is rejected as batch_id_conflict."
5640
+ } : {},
5641
+ ...graphPlan.gates.length > 0 ? {
5642
+ gates: graphPlan.gates,
5643
+ gateHint: "Gates are declared shut. When their predecessors complete they open (awaiting_coordinator) and BLOCK their downstream tasks. Pass one with mesh_graph_gate_claim \u2192 do the action yourself \u2192 mesh_graph_gate_release. The daemon never performs a gate action and a deadline can only expire a gate, never pass it."
5644
+ } : {},
5645
+ ...graphPlan.workspaces.length > 0 ? { workspaces: graphPlan.workspaces } : {},
5646
+ ...graphPlan.heldNodeIds.length > 0 ? {
5647
+ graphHeldTasks: graphPlan.heldNodeIds.length,
5648
+ graphHeldHint: "Tasks declaring graph features (bound upstream outputs, a condition, a delayed worktree, or a gate) are held until the graph settles them \u2014 that is what makes their final instruction knowable. Tasks with only plain dependencies are NOT held; they use the unchanged queue dependency predicate."
5649
+ } : {}
5650
+ } : {},
5651
+ ...decision.batchCapabilityAvailable ? { batchCapabilityAvailable: decision.batchCapabilityAvailable } : {},
5047
5652
  tasks: tasks.map((task, i) => ({
5048
5653
  ...normalizedEntries[i].ref ? { ref: normalizedEntries[i].ref } : {},
5049
5654
  taskId: task.id,
5050
5655
  status: task.status,
5051
5656
  taskMode: task.taskMode,
5657
+ ...graphPlan?.nodeIdByIndex[i] ? { nodeId: graphPlan.nodeIdByIndex[i] } : {},
5052
5658
  ...Array.isArray(task.dependsOn) && task.dependsOn.length > 0 ? { dependsOn: task.dependsOn } : {},
5053
5659
  ...task.targetNodeId ? { targetNodeId: task.targetNodeId } : {},
5054
5660
  ...task.priority ? { priority: task.priority } : {},
@@ -5066,7 +5672,7 @@ async function meshEnqueueBatch(ctx, args) {
5066
5672
  });
5067
5673
  }
5068
5674
  async function meshViewQueue(ctx, args) {
5069
- const rateResult = (0, import_daemon_core6.recordMeshToolCall)({ meshId: ctx.mesh.id, tool: "mesh_view_queue" });
5675
+ const rateResult = recordMeshCoordinatorToolCall(ctx, "mesh_view_queue");
5070
5676
  const compact = args.verbose === true ? false : args.compact ?? true;
5071
5677
  try {
5072
5678
  await refreshMeshFromDaemon(ctx);
@@ -5074,9 +5680,10 @@ async function meshViewQueue(ctx, args) {
5074
5680
  const view = normalizeQueueViewMode(args.view);
5075
5681
  const rawQueue = (0, import_daemon_core6.getQueue)(ctx.mesh.id);
5076
5682
  const statusById = new Map(rawQueue.map((task) => [task.id, task.status]));
5683
+ const depMetaById = new Map(rawQueue.map((task) => [task.id, task]));
5077
5684
  const withDependencies = rawQueue.map((task) => {
5078
5685
  if (!Array.isArray(task.dependsOn) || task.dependsOn.length === 0) return task;
5079
- const depState = (0, import_daemon_core6.describeTaskDependencyState)(task, statusById);
5686
+ const depState = (0, import_daemon_core6.describeTaskDependencyState)(task, statusById, depMetaById);
5080
5687
  return { ...task, ...depState };
5081
5688
  });
5082
5689
  const liveNodes = await collectMeshViewQueueNodesWithLiveSessionsVerified(ctx);
@@ -8372,7 +8979,7 @@ async function meshAnswerQuestion(ctx, args) {
8372
8979
  return JSON.stringify(result, null, 2);
8373
8980
  }
8374
8981
  async function meshListPendingApprovals(ctx, _args = {}) {
8375
- (0, import_daemon_core6.recordMeshToolCall)({ meshId: ctx.mesh.id, tool: "mesh_list_pending_approvals" });
8982
+ recordMeshCoordinatorToolCall(ctx, "mesh_list_pending_approvals");
8376
8983
  await refreshMeshFromDaemon(ctx);
8377
8984
  const liveNodes = await collectMeshViewQueueNodesWithLiveSessions(ctx);
8378
8985
  let ledgerEntries = (0, import_daemon_core6.readLedgerEntries)(ctx.mesh.id, { tail: 200 });
@@ -10297,6 +10904,15 @@ async function startMcpServer(opts) {
10297
10904
  case "mesh_view_queue":
10298
10905
  text = await meshViewQueue(meshCtx, a);
10299
10906
  break;
10907
+ case "mesh_graph_view":
10908
+ text = await meshGraphView(meshCtx, a);
10909
+ break;
10910
+ case "mesh_graph_gate_claim":
10911
+ text = await meshGraphGateClaim(meshCtx, a);
10912
+ break;
10913
+ case "mesh_graph_gate_release":
10914
+ text = await meshGraphGateRelease(meshCtx, a);
10915
+ break;
10300
10916
  case "mesh_queue_cancel":
10301
10917
  text = await meshQueueCancel(meshCtx, a);
10302
10918
  break;