@adhdev/daemon-standalone 1.0.57 → 1.0.58-rc.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-standalone",
3
- "version": "1.0.57",
3
+ "version": "1.0.58-rc.2",
4
4
  "description": "ADHDev standalone daemon — embedded HTTP/WS server for local dashboard",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -37,7 +37,7 @@
37
37
  "license": "AGPL-3.0-or-later",
38
38
  "dependencies": {
39
39
  "@adhdev/ghostty-vt-node": "*",
40
- "@adhdev/session-host-core": "*",
40
+ "@adhdev/session-host-core": "1.0.58-rc.2",
41
41
  "@xterm/addon-serialize": "^0.14.0",
42
42
  "@xterm/xterm": "^6.0.0",
43
43
  "chalk": "^5.3.0",
@@ -1413,7 +1413,23 @@ var MESH_SEND_TASK_TOOL = {
1413
1413
  enum: ["when_idle", "interrupt"],
1414
1414
  description: "How to deliver when the target session is BUSY. Default 'when_idle': never disturbs the running turn \u2014 the task is queued and auto-delivered the moment the session goes idle. \u2605'interrupt' ABORTS the turn currently in flight by pressing the provider's own stop control (Ctrl-C, or ESC on antigravity-cli), then delivers this task once the session settles. THE WORK IN PROGRESS IS DISCARDED \u2014 whatever the agent had not yet finished is lost, and any partial edits it was mid-way through are left as they are. Use it only when the running turn is genuinely going the wrong way and finishing it is worse than losing it. If the target provider cannot interrupt (no stop control declared, or an empty stop key), the dispatch is REJECTED rather than quietly falling back to when_idle \u2014 so a steering attempt never reports success while the session actually runs on to completion under the old instructions. Re-send with 'when_idle' if delivery-after-completion is acceptable. Has no effect on an idle session (delivered immediately either way)."
1415
1415
  },
1416
- deliveryMode: { type: "string", enum: ["when_idle", "interrupt"], description: "CamelCase alias for delivery_mode." }
1416
+ deliveryMode: { type: "string", enum: ["when_idle", "interrupt"], description: "CamelCase alias for delivery_mode." },
1417
+ // GRAPH-MEASUREMENT-DIRECT — the decision record for the DIRECT surface.
1418
+ //
1419
+ // ★ WHY IT IS HERE AT ALL. This tool is the MAJORITY dispatch surface (~67%
1420
+ // of dispatches in the graph-adoption investigation) and it carried no
1421
+ // decision field, so two thirds of all routing judgements were structurally
1422
+ // unmeasurable. `decision_missing` on mesh_enqueue_task described only the
1423
+ // enqueue minority and was silent — not negative — about the rest.
1424
+ //
1425
+ // ★ OPTIONAL, exactly like mesh_enqueue_task's (see the note there): phase E
1426
+ // measures and does not enforce, and a required field would reject every
1427
+ // existing caller. Omission degrades to decision_missing, never an error.
1428
+ orchestration_decision: {
1429
+ type: "object",
1430
+ description: "Record of your dispatch decision, for adoption measurement: {decision, direct_reason, ready_worker_tasks, known_graph_steps, capability_blockers}. On this DIRECT surface, direct_reason says why dispatching into an existing session beat queueing a task \u2014 one of same_subject_continuation, investigation_handoff, idle_session_reuse, queue_bypass_urgent, new_subject, legacy_client, operator_override. These are the cases the operating rules name: same-subject continuation, the investigate\u2192fix handoff, reusing an idle session for a follow-up/retry/cleanup delta, or a deliberate queue bypass. new_subject is the one the rules do NOT endorse \u2014 a genuinely new topic should get its own task even when a session sits idle \u2014 and reporting it returns an unsanctioned_direct_dispatch advisory. Report it honestly anyway: it is recorded, never refused. Optional and never rejected: omitting it is recorded as decision_missing. Provenance only \u2014 it never changes execution."
1431
+ },
1432
+ orchestrationDecision: { type: "object", description: "CamelCase alias for orchestration_decision." }
1417
1433
  },
1418
1434
  required: ["node_id", "session_id", "message", "difficulty"]
1419
1435
  }
@@ -1535,7 +1551,7 @@ var MESH_FAST_FORWARD_NODE_TOOL = {
1535
1551
  mode: { type: "string", enum: ["merge", "push"], description: "merge (default): git merge --ff-only to absorb upstream. push: strict ff-only push of local commits to origin/<branch>; refuses any non-fast-forward." },
1536
1552
  branch: { type: "string", description: "Optional guard: require the node's current branch to match this branch before planning/executing." },
1537
1553
  execute: { type: "boolean", description: "When true, apply the fast-forward/push if all safety gates pass. Defaults false/dry-run." },
1538
- dry_run: { type: "boolean", description: "Preview only. Defaults true unless execute=true; dry_run=true overrides execute." },
1554
+ dry_run: { type: "boolean", description: "Preview only. Defaults true unless execute=true; dry_run=true overrides execute. dry_run=false is NOT an execute trigger \u2014 it only declines to veto, so passing it alone is rejected with dry_run_false_requires_execute rather than silently previewing. Use execute=true to apply." },
1539
1555
  update_submodules: { type: "boolean", description: 'mode="merge" only: when true, if the root fast-forward changes gitlinks, run only git submodule update --init --recursive and verify submodules clean.' },
1540
1556
  push_submodules: { type: "boolean", description: 'mode="push" only: also ff-only push submodule HEADs to their origin main. Gated by mesh policy allowAutoPublishSubmoduleMainCommits \u2014 skipped unless that policy is enabled. Defaults false (root push only).' }
1541
1557
  },
@@ -1898,7 +1914,7 @@ var MESH_PRUNE_STALE_DIRECT_TOOL = {
1898
1914
  type: "object",
1899
1915
  properties: {
1900
1916
  execute: { type: "boolean", description: "When true, actually delete the orphaned records. Defaults false (dry run). Ignored when dry_run=true." },
1901
- dry_run: { type: "boolean", description: "Force a preview without mutation even if execute=true. Defaults to dry-run behavior when execute is not set." },
1917
+ dry_run: { type: "boolean", description: "Force a preview without mutation even if execute=true. Defaults to dry-run behavior when execute is not set. dry_run=false is NOT an execute trigger \u2014 passing it alone is rejected with dry_run_false_requires_execute rather than silently previewing. Use execute=true to prune." },
1902
1918
  include_terminal: { type: "boolean", description: "Also prune terminal (completed/failed) direct dispatch store rows in addition to orphans. Defaults false." }
1903
1919
  }
1904
1920
  }
@@ -2028,7 +2044,7 @@ var MESH_MAGI_REVIEW_TOOL = {
2028
2044
  require_independent_evidence: { type: "boolean", description: "Default true \u2014 high-impact claims with no file:line/source evidence are routed to needs_verification." },
2029
2045
  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." },
2030
2046
  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." },
2031
- wait_timeout_ms: { type: "number", description: 'Max time to wait for replica completion before returning a partial "missing K of N" synthesis. Default ~4 min.' },
2047
+ wait_timeout_ms: { type: "number", description: 'Max time to wait for replica completion before returning a partial "missing K of N" synthesis. Default 8 min, max 20 min.' },
2032
2048
  auto_cleanup: { type: "boolean", description: "Default = mesh policy magiSessionCleanup (ON / stop_and_delete unless overridden). Once all replicas are terminal, stop+delete ONLY the worker sessions THIS fan-out auto-launched (marker-verified) so repeated reviews don't accumulate idle worker sessions. Reused/coordinator/other sessions are never touched. Set false to preserve auto-launched worker sessions for inspection. No effect on a partial (non-terminal) collection." }
2033
2049
  },
2034
2050
  required: ["question", "task_kind"]
@@ -2044,7 +2060,7 @@ var MESH_MAGI_COLLECT_TOOL = {
2044
2060
  task_kind: { type: "string", enum: ["claim_audit", "rca", "design", "freeform"], description: "Optional override of the task_kind used to parse replica answers. Normally recovered automatically from the original dispatch \u2014 only set this if the dispatched ledger entry was pruned and auto-recovery falls back to claim_audit incorrectly." },
2045
2061
  require_independent_evidence: { type: "boolean", description: "Default true \u2014 high-impact claims with no file:line/source evidence are routed to needs_verification." },
2046
2062
  wait: { type: "boolean", description: "Default false (snapshot). Set true to block for outstanding replicas up to wait_timeout_ms before synthesizing." },
2047
- wait_timeout_ms: { type: "number", description: "When wait=true, max time to wait for remaining replica completion. Default ~4 min." },
2063
+ wait_timeout_ms: { type: "number", description: "When wait=true, max time to wait for remaining replica completion. Default 8 min, max 20 min." },
2048
2064
  auto_cleanup: { type: "boolean", description: "Default = mesh policy magiSessionCleanup (ON / stop_and_delete). When the collection is terminal, stop+delete ONLY the worker sessions THIS fan-out auto-launched (marker-verified). Reused/coordinator/other sessions are never touched. Set false to preserve them. No effect on a partial (non-terminal) snapshot." },
2049
2065
  verbose: { type: "boolean", description: "Default false. When true, each synthesis.replicas[] entry also carries rawAnswer \u2014 the replica's raw end-user answer text (capped). Omitted by default to keep the payload small; the structured clusters already carry the parsed claims." }
2050
2066
  },
@@ -6515,9 +6531,12 @@ var import_daemon_core7 = require("@adhdev/daemon-core");
6515
6531
  var MAGI_MAX_REPLICAS = 12;
6516
6532
  var MAGI_MIN_TARGETS = 2;
6517
6533
  var MAGI_CLUSTER_JACCARD = 0.4;
6518
- var MAGI_DEFAULT_WAIT_MS = 18e4;
6519
- var MAGI_MAX_WAIT_MS = 6e5;
6534
+ var MAGI_DEFAULT_WAIT_MS = 48e4;
6535
+ var MAGI_MAX_WAIT_MS = 12e5;
6520
6536
  var MAGI_POLL_INTERVAL_MS = 5e3;
6537
+ function resolveMagiWaitTimeoutMs(raw) {
6538
+ return Math.min(MAGI_MAX_WAIT_MS, Math.max(MAGI_POLL_INTERVAL_MS, Number(raw) || MAGI_DEFAULT_WAIT_MS));
6539
+ }
6521
6540
  var VALID_TASK_KINDS = ["claim_audit", "rca", "design", "freeform"];
6522
6541
  var DEFAULT_TASK_KIND = "claim_audit";
6523
6542
  function normalizeMagiTaskKind(raw) {
@@ -6898,7 +6917,11 @@ function synthesizeMagiResponses(responses, opts = {}) {
6898
6917
  const replicasMissing = Math.max(0, replicasExpected - replicasAnswered);
6899
6918
  const lossDominated = replicasMissing > 0 && replicasMissing >= replicasAnswered;
6900
6919
  if (lossDominated) {
6901
- independenceBanner = `independence not achieved \u2014 only ${replicasAnswered} of ${replicasExpected} replica(s) answered (${replicasMissing} missing/dropped), collapsing the answering set to ${distinctProviders} provider(s) and ${distinctNodes} machine(s). This is a replica-loss/collection failure, not a low-diversity panel \u2014 inspect the dropped replicas (stale/unparseable/no-session) before re-reading the diversity spans. Agreements are routed to needs_verification.`;
6920
+ const notAnswered = responses.filter((r) => !(r.source.ok && r.response));
6921
+ const pendingCount = notAnswered.filter((r) => r.source.error === "replica_deadline_exceeded").length;
6922
+ const failedCount = notAnswered.length - pendingCount;
6923
+ const dropBreakdown = pendingCount > 0 && failedCount > 0 ? ` (${pendingCount} still pending past the deadline \u2014 re-collect may recover them; ${failedCount} genuinely failed/unparseable/stale \u2014 those need a panel swap)` : pendingCount > 0 ? ` (all ${pendingCount} still pending past the deadline \u2014 re-collect with mesh_magi_collect may recover them, this is NOT a failed panel)` : ` (all ${failedCount} genuinely failed/unparseable/stale \u2014 re-collecting will not recover them, consider a panel swap)`;
6924
+ independenceBanner = `independence not achieved \u2014 only ${replicasAnswered} of ${replicasExpected} replica(s) answered (${replicasMissing} missing/dropped), collapsing the answering set to ${distinctProviders} provider(s) and ${distinctNodes} machine(s). This is a replica-loss/collection failure, not a low-diversity panel${dropBreakdown}. Agreements are routed to needs_verification.`;
6902
6925
  } else {
6903
6926
  independenceBanner = `independence not achieved \u2014 the answering replicas span ${distinctProviders} provider(s) and ${distinctNodes} machine(s); their agreements are source-coupled and routed to needs_verification.`;
6904
6927
  }
@@ -7295,6 +7318,8 @@ async function meshMagiKindPanelSet(ctx, args) {
7295
7318
  const scope = magiPanelScope(meshId, ctx.mesh.name);
7296
7319
  try {
7297
7320
  const current = (0, import_daemon_core6.getMagiKindPanel)(kind, meshId) ?? [];
7321
+ const ignoredFields = (0, import_daemon_core6.collectIgnoredMagiSlotFields)(args.slots);
7322
+ const ignoredNote = ignoredFields.length ? { ignoredFields, ignoredFieldsNote: "These keys are not part of the MAGI slot schema and were DROPPED (the panel was still saved without them). A MAGI panel decides WHO answers independently; per-slot routing axes like thinkingLevel/difficulty/maxParallel belong on the node capability slots (mesh_node_slots_set)." } : {};
7298
7323
  if (!write) {
7299
7324
  const preview = (0, import_daemon_core6.normalizeMagiSlots)(args.slots, ctx.mesh.nodes.map((n) => n.id));
7300
7325
  return JSON.stringify({
@@ -7305,6 +7330,7 @@ async function meshMagiKindPanelSet(ctx, args) {
7305
7330
  replacement: true,
7306
7331
  currentSlots: current,
7307
7332
  slots: preview,
7333
+ ...ignoredNote,
7308
7334
  note: `Dry-run only \u2014 no file written. This is a WHOLESALE replacement of the kind's slot list for mesh '${meshId}' (machine-local ~/.adhdev/meshes.json); the currentSlots would be fully replaced. Other meshes on this machine are unaffected. Re-run with write=true after explicit user approval.`
7309
7335
  }, null, 2);
7310
7336
  }
@@ -7317,6 +7343,7 @@ async function meshMagiKindPanelSet(ctx, args) {
7317
7343
  replacement: true,
7318
7344
  previousSlots: current,
7319
7345
  slots,
7346
+ ...ignoredNote,
7320
7347
  nextAction: "Verify with mesh_magi_kind_panel_list, then mesh_magi_review({ task_kind }) resolves this binding."
7321
7348
  }, null, 2);
7322
7349
  } catch (e) {
@@ -7431,7 +7458,7 @@ async function meshMagiReview(ctx, args) {
7431
7458
  const mode = readString(args.mode);
7432
7459
  const requireIndependentEvidence = (args.require_independent_evidence ?? args.requireIndependentEvidence) !== false;
7433
7460
  const wait = args.wait !== false;
7434
- const waitTimeoutMs = Math.min(MAGI_MAX_WAIT_MS, Math.max(MAGI_POLL_INTERVAL_MS, Number(args.wait_timeout_ms ?? args.waitTimeoutMs) || MAGI_DEFAULT_WAIT_MS));
7461
+ const waitTimeoutMs = resolveMagiWaitTimeoutMs(args.wait_timeout_ms ?? args.waitTimeoutMs);
7435
7462
  const consensusGroupId = `magi_${(0, import_node_crypto.randomUUID)().replace(/-/g, "")}`;
7436
7463
  const titleQ = question.length > 80 ? `${question.slice(0, 77)}...` : question;
7437
7464
  const mission = (0, import_daemon_core6.upsertMeshMission)(ctx.mesh.id, {
@@ -7606,7 +7633,7 @@ async function meshMagiCollect(ctx, args) {
7606
7633
  }
7607
7634
  const requireIndependentEvidence = (args.require_independent_evidence ?? args.requireIndependentEvidence) !== false;
7608
7635
  const wait = args.wait === true;
7609
- const timeoutMs = wait ? Math.min(MAGI_MAX_WAIT_MS, Math.max(MAGI_POLL_INTERVAL_MS, Number(args.wait_timeout_ms ?? args.waitTimeoutMs) || MAGI_DEFAULT_WAIT_MS)) : 0;
7636
+ const timeoutMs = wait ? resolveMagiWaitTimeoutMs(args.wait_timeout_ms ?? args.waitTimeoutMs) : 0;
7610
7637
  const replicaTaskIds = replicaTasks.map((t) => readString(t.id)).filter(Boolean);
7611
7638
  const collected = await collectMagiResponses(ctx, { replicaTaskIds, timeoutMs, taskKind });
7612
7639
  const synthesis = synthesizeMagiResponses(collected.responses, {
@@ -8050,7 +8077,7 @@ ${magiOutputContractFor(kind)}`;
8050
8077
  finalized.set(taskId, prov);
8051
8078
  return true;
8052
8079
  }
8053
- source.error = isSchemaFailure ? `schema_invalid: ${kindResult.failReason}` : "unparseable_output";
8080
+ source.error = isSchemaFailure ? `schema_invalid: ${kindResult.failReason}` : "replica_deadline_exceeded";
8054
8081
  finalized.set(taskId, { source, response: emptyResponse() });
8055
8082
  return true;
8056
8083
  }
@@ -8277,6 +8304,15 @@ function computeIdleDispatchAckRisk(sessionWasIdle, dispatchPreRecorded, session
8277
8304
  }
8278
8305
  async function meshPruneStaleDirect(ctx, args = {}) {
8279
8306
  await refreshMeshFromDaemon(ctx);
8307
+ if (args.dry_run === false && args.execute !== true) {
8308
+ return JSON.stringify({
8309
+ success: false,
8310
+ code: "dry_run_false_requires_execute",
8311
+ executed: false,
8312
+ error: "dry_run:false alone does not execute \u2014 it only declines to veto. Pass execute:true to actually prune.",
8313
+ nextAction: "Re-run mesh_prune_stale_direct(execute: true) to prune, or omit dry_run to preview."
8314
+ }, null, 2);
8315
+ }
8280
8316
  const execute = args.execute === true && args.dry_run !== true;
8281
8317
  const includeTerminal = args.include_terminal === true;
8282
8318
  const liveNodes = await collectMeshViewQueueNodesWithLiveSessions(ctx);
@@ -8363,6 +8399,16 @@ async function meshSendTask(ctx, args) {
8363
8399
  });
8364
8400
  }
8365
8401
  const difficulty = difficultyRaw;
8402
+ const rawDecision = args.orchestration_decision ?? args.orchestrationDecision;
8403
+ const decisionMissing = rawDecision === void 0 || rawDecision === null;
8404
+ const orchestration = (0, import_daemon_core6.normalizeOrchestrationDecision)(rawDecision, "direct");
8405
+ const orchestrationWarning = {
8406
+ ...orchestration.unsanctionedDirect ? {
8407
+ unsanctionedDirect: orchestration.unsanctionedDirect,
8408
+ unsanctionedDirectHint: import_daemon_core6.MESH_UNSANCTIONED_DIRECT_HINT
8409
+ } : {},
8410
+ ...decisionMissing ? { orchestrationDecisionMissing: true } : {}
8411
+ };
8366
8412
  const modeValidation = (0, import_daemon_core6.validateMeshTaskModeRequest)(requestedTaskMode, message, readonly);
8367
8413
  if (!modeValidation.valid) {
8368
8414
  return JSON.stringify({
@@ -8519,6 +8565,17 @@ async function meshSendTask(ctx, args) {
8519
8565
  ...readonly ? { readonly: true } : {},
8520
8566
  dispatchedAt
8521
8567
  });
8568
+ (0, import_daemon_core6.recordDirectDispatchDecision)(ctx.mesh.id, {
8569
+ taskId,
8570
+ via: "p2p_direct",
8571
+ nodeId: args.node_id,
8572
+ ...dispatchedSessionId ? { sessionId: dispatchedSessionId } : {},
8573
+ ...missionId ? { missionId } : {},
8574
+ ...ctx.coordinatorSessionId ? { coordinatorSessionId: ctx.coordinatorSessionId } : {},
8575
+ decision: orchestration.decision,
8576
+ ...decisionMissing ? { decisionMissing: true } : {},
8577
+ ...orchestration.unsanctionedDirect ? { unsanctionedDirect: orchestration.unsanctionedDirect.reportedReason } : {}
8578
+ });
8522
8579
  } catch {
8523
8580
  }
8524
8581
  }
@@ -8531,7 +8588,10 @@ async function meshSendTask(ctx, args) {
8531
8588
  taskMode,
8532
8589
  ...result2.success && result2.providerType ? { providerType: result2.providerType } : {},
8533
8590
  dispatched: result2.success === true,
8534
- ...result2.success ? buildMissionInactiveWarning(ctx, missionId) ?? {} : {}
8591
+ ...result2.success ? buildMissionInactiveWarning(ctx, missionId) ?? {} : {},
8592
+ // GRAPH-MEASUREMENT-DIRECT: advisory only, and only on a dispatch that
8593
+ // actually happened — a failed dispatch made no routing decision to report on.
8594
+ ...result2.success ? orchestrationWarning : {}
8535
8595
  });
8536
8596
  }
8537
8597
  if (args.session_id) {
@@ -8836,6 +8896,17 @@ async function meshSendTask(ctx, args) {
8836
8896
  ...readonly ? { readonly: true } : {},
8837
8897
  dispatchedAt
8838
8898
  });
8899
+ (0, import_daemon_core6.recordDirectDispatchDecision)(ctx.mesh.id, {
8900
+ taskId,
8901
+ via: "local_direct",
8902
+ nodeId: args.node_id,
8903
+ sessionId: args.session_id,
8904
+ ...missionId ? { missionId } : {},
8905
+ ...ctx.coordinatorSessionId ? { coordinatorSessionId: ctx.coordinatorSessionId } : {},
8906
+ decision: orchestration.decision,
8907
+ ...decisionMissing ? { decisionMissing: true } : {},
8908
+ ...orchestration.unsanctionedDirect ? { unsanctionedDirect: orchestration.unsanctionedDirect.reportedReason } : {}
8909
+ });
8839
8910
  } catch {
8840
8911
  }
8841
8912
  let deliveryId;
@@ -8869,7 +8940,9 @@ async function meshSendTask(ctx, args) {
8869
8940
  // session whose dispatch row did NOT survive pre-record. A successfully
8870
8941
  // pre-recorded idle dispatch (the NOTIF-DROP / CANON-A path) is not at risk.
8871
8942
  ...computeIdleDispatchAckRisk(sessionWasIdle, dispatchPreRecorded, args.session_id),
8872
- ...buildMissionInactiveWarning(ctx, missionId) ?? {}
8943
+ ...buildMissionInactiveWarning(ctx, missionId) ?? {},
8944
+ // GRAPH-MEASUREMENT-DIRECT: advisory only — never blocks, never re-routes.
8945
+ ...orchestrationWarning
8873
8946
  });
8874
8947
  }
8875
8948
  const task = (0, import_daemon_core6.enqueueTask)(ctx.mesh.id, message, {
@@ -9423,6 +9496,20 @@ async function meshFastForwardNode(ctx, args) {
9423
9496
  blockingReasons: ["node_read_only"]
9424
9497
  }, null, 2);
9425
9498
  }
9499
+ if (args.dry_run === false && args.execute !== true) {
9500
+ return JSON.stringify({
9501
+ success: false,
9502
+ code: "dry_run_false_requires_execute",
9503
+ nodeId: args.node_id,
9504
+ workspace: node.workspace,
9505
+ allowed: false,
9506
+ willRun: false,
9507
+ executed: false,
9508
+ blockingReasons: ["dry_run_false_requires_execute"],
9509
+ error: "dry_run:false alone does not execute \u2014 it only declines to veto. Pass execute:true to actually apply the fast-forward.",
9510
+ nextAction: `Re-run mesh_fast_forward_node(node_id: "${args.node_id}", execute: true) to apply, or omit dry_run to preview.`
9511
+ }, null, 2);
9512
+ }
9426
9513
  try {
9427
9514
  const dryRun = args.dry_run === true || args.execute !== true;
9428
9515
  const result = await commandForNode(ctx, node, "fast_forward_mesh_node", {