@vornrun/mcp 0.7.1-beta.3 → 0.7.1-beta.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.
Files changed (2) hide show
  1. package/dist/index.js +189 -61
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import "module";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
 
7
7
  // ../server/src/config-manager.ts
8
- import fs3 from "fs";
8
+ import fs4 from "fs";
9
9
 
10
10
  // ../shared/src/agent-defaults.ts
11
11
  var DEFAULT_AGENT_COMMANDS = {
@@ -34,9 +34,9 @@ var DEFAULT_AGENT_COMMANDS = {
34
34
 
35
35
  // ../server/src/database.ts
36
36
  import Database from "libsql";
37
- import path2 from "path";
37
+ import path3 from "path";
38
38
  import os from "os";
39
- import fs2 from "fs";
39
+ import fs3 from "fs";
40
40
  import { randomUUID } from "crypto";
41
41
 
42
42
  // ../server/src/logger.ts
@@ -118,6 +118,12 @@ var STRIP_ENV_KEYS = NEVER_BORROWED_ENV.keys;
118
118
  var STRIP_ENV_PREFIXES = [...NEVER_BORROWED_ENV.prefixes, BOOTSTRAP_ENV_VAR];
119
119
  var STRIP_ENV_KEYS_UPPER = STRIP_ENV_KEYS.map((k) => k.toUpperCase());
120
120
 
121
+ // ../server/src/workflows/gate-views.ts
122
+ import fs2 from "fs";
123
+ import path2 from "path";
124
+ import crypto2 from "crypto";
125
+ var GATE_VIEW_MAX_BYTES = 5 * 1024 * 1024;
126
+
121
127
  // ../server/src/default-workflows.ts
122
128
  var DEFAULT_TASK_WORKFLOW_ID = "system:default-task-workflow";
123
129
  var DEV_SERVER_WORKFLOW_ID = "system:dev-server-on-restore";
@@ -195,10 +201,10 @@ function buildDevServerWorkflow() {
195
201
  }
196
202
 
197
203
  // ../server/src/database.ts
198
- var DEFAULT_DATA_DIR = path2.join(os.homedir(), ".vorn");
204
+ var DEFAULT_DATA_DIR = path3.join(os.homedir(), ".vorn");
199
205
  var resolvedDataDir = null;
200
206
  function dbPath() {
201
- return path2.join(getDataDir(), "vorn.db");
207
+ return path3.join(getDataDir(), "vorn.db");
202
208
  }
203
209
  var db = null;
204
210
  function getDb() {
@@ -213,8 +219,8 @@ function getDataDir() {
213
219
  }
214
220
  function initDatabase(dataDir2) {
215
221
  resolvedDataDir = dataDir2 ?? DEFAULT_DATA_DIR;
216
- if (!fs2.existsSync(getDataDir())) {
217
- fs2.mkdirSync(getDataDir(), { recursive: true, mode: 448 });
222
+ if (!fs3.existsSync(getDataDir())) {
223
+ fs3.mkdirSync(getDataDir(), { recursive: true, mode: 448 });
218
224
  }
219
225
  try {
220
226
  db = new Database(dbPath());
@@ -288,13 +294,13 @@ function recoverCorruptDatabase() {
288
294
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
289
295
  const backupPath = `${dbPath()}.corrupt-${timestamp}`;
290
296
  try {
291
- if (fs2.existsSync(dbPath())) {
292
- fs2.copyFileSync(dbPath(), backupPath);
297
+ if (fs3.existsSync(dbPath())) {
298
+ fs3.copyFileSync(dbPath(), backupPath);
293
299
  logger_default.info(`[database] Backed up corrupt database to ${backupPath}`);
294
300
  }
295
301
  for (const suffix of ["", "-wal", "-shm"]) {
296
302
  const file = dbPath() + suffix;
297
- if (fs2.existsSync(file)) fs2.unlinkSync(file);
303
+ if (fs3.existsSync(file)) fs3.unlinkSync(file);
298
304
  }
299
305
  } catch (backupErr) {
300
306
  logger_default.error({ backupErr }, "[database] Failed to back up corrupt database:");
@@ -496,7 +502,8 @@ function createSchema() {
496
502
  connector_item TEXT,
497
503
  connector_inbox_id INTEGER,
498
504
  connector_inbox_lease_token TEXT,
499
- connector_inbox_disposition TEXT
505
+ connector_inbox_disposition TEXT,
506
+ definition TEXT
500
507
  );
501
508
 
502
509
  CREATE TABLE IF NOT EXISTS workflow_run_nodes (
@@ -523,6 +530,11 @@ function createSchema() {
523
530
  worktree_name TEXT,
524
531
  worktree_origin TEXT,
525
532
  waiting_for TEXT,
533
+ message TEXT,
534
+ view_token TEXT,
535
+ round INTEGER,
536
+ feedback TEXT,
537
+ rejected_at TEXT,
526
538
  FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
527
539
  );
528
540
 
@@ -1081,7 +1093,40 @@ function migrateSchema(d) {
1081
1093
  })();
1082
1094
  logger_default.info("[database] migrated schema to version 21 (package connections belong to sdk)");
1083
1095
  }
1096
+ if (version < 22) {
1097
+ d.transaction(() => {
1098
+ const runCols = d.prepare("PRAGMA table_info(workflow_runs)").all();
1099
+ if (!runCols.some((c) => c.name === "definition")) {
1100
+ d.exec("ALTER TABLE workflow_runs ADD COLUMN definition TEXT");
1101
+ }
1102
+ d.prepare(
1103
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '22')"
1104
+ ).run();
1105
+ })();
1106
+ logger_default.info("[database] migrated schema to version 22 (the definition a run started with)");
1107
+ }
1108
+ if (version < 23) {
1109
+ d.transaction(() => {
1110
+ const nodeCols = d.prepare("PRAGMA table_info(workflow_run_nodes)").all();
1111
+ for (const [column, type] of GATE_COLUMNS) {
1112
+ if (!nodeCols.some((c) => c.name === column)) {
1113
+ d.exec(`ALTER TABLE workflow_run_nodes ADD COLUMN ${column} ${type}`);
1114
+ }
1115
+ }
1116
+ d.prepare(
1117
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '23')"
1118
+ ).run();
1119
+ })();
1120
+ logger_default.info("[database] migrated schema to version 23 (what an approval gate asked and heard)");
1121
+ }
1084
1122
  }
1123
+ var GATE_COLUMNS = [
1124
+ ["message", "TEXT"],
1125
+ ["view_token", "TEXT"],
1126
+ ["round", "INTEGER"],
1127
+ ["feedback", "TEXT"],
1128
+ ["rejected_at", "TEXT"]
1129
+ ];
1085
1130
  var REVISIONED_TABLES = [
1086
1131
  "projects",
1087
1132
  "tasks",
@@ -1156,7 +1201,8 @@ function verifySchema(d) {
1156
1201
  {
1157
1202
  column: "connector_inbox_disposition",
1158
1203
  ddl: "ALTER TABLE workflow_runs ADD COLUMN connector_inbox_disposition TEXT"
1159
- }
1204
+ },
1205
+ { column: "definition", ddl: "ALTER TABLE workflow_runs ADD COLUMN definition TEXT" }
1160
1206
  ],
1161
1207
  connector_inbox: [
1162
1208
  {
@@ -1191,7 +1237,11 @@ function verifySchema(d) {
1191
1237
  ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN structured_output TEXT"
1192
1238
  },
1193
1239
  // Which pass of a loop produced this row.
1194
- { column: "iteration", ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN iteration INTEGER" }
1240
+ { column: "iteration", ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN iteration INTEGER" },
1241
+ ...GATE_COLUMNS.map(([column, type]) => ({
1242
+ column,
1243
+ ddl: `ALTER TABLE workflow_run_nodes ADD COLUMN ${column} ${type}`
1244
+ }))
1195
1245
  ],
1196
1246
  tasks: [
1197
1247
  {
@@ -1815,16 +1865,12 @@ var ConfigManager = class {
1815
1865
  cb(config);
1816
1866
  }
1817
1867
  }
1818
- /**
1819
- * Watch for external DB writes (e.g. MCP stdio process).
1820
- * Detects: .db-signal (explicit), .db-wal changes, and .db changes (post-checkpoint).
1821
- */
1868
+ /** Reload when a writer touches `.db-signal` (`dbSignalChange`) after changing the configuration. */
1822
1869
  watchDb() {
1823
1870
  if (this.dbWatcher) return;
1824
- const WATCH_SUFFIXES = [".db-signal", ".db-wal", ".db"];
1825
1871
  try {
1826
- this.dbWatcher = fs3.watch(getDataDir(), (eventType, filename) => {
1827
- if (!filename || !WATCH_SUFFIXES.some((s) => filename.endsWith(s))) return;
1872
+ this.dbWatcher = fs4.watch(getDataDir(), (_eventType, filename) => {
1873
+ if (filename !== ".db-signal") return;
1828
1874
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
1829
1875
  this.debounceTimer = setTimeout(() => {
1830
1876
  this.notifyChanged();
@@ -1855,8 +1901,8 @@ var configManager = new ConfigManager();
1855
1901
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1856
1902
 
1857
1903
  // src/tools/tasks.ts
1858
- import crypto2 from "crypto";
1859
- import path4 from "path";
1904
+ import crypto3 from "crypto";
1905
+ import path5 from "path";
1860
1906
  import { z as z2 } from "zod";
1861
1907
 
1862
1908
  // src/validation.ts
@@ -1885,8 +1931,8 @@ var V = {
1885
1931
  };
1886
1932
 
1887
1933
  // ../server/src/rpc-client.ts
1888
- import fs4 from "fs";
1889
- import path3 from "path";
1934
+ import fs5 from "fs";
1935
+ import path4 from "path";
1890
1936
  import os2 from "os";
1891
1937
  import { execFileSync as execFileSync2 } from "child_process";
1892
1938
  import { WebSocket } from "ws";
@@ -1895,13 +1941,13 @@ function named(value) {
1895
1941
  return value?.trim() ? value : void 0;
1896
1942
  }
1897
1943
  function dataDir() {
1898
- return dataDirOverride ?? named(process.env.VORN_DATA_DIR) ?? path3.join(os2.homedir(), ".vorn");
1944
+ return dataDirOverride ?? named(process.env.VORN_DATA_DIR) ?? path4.join(os2.homedir(), ".vorn");
1899
1945
  }
1900
1946
  function portFile() {
1901
- return path3.join(dataDir(), WS_PORT_FILENAME);
1947
+ return path4.join(dataDir(), WS_PORT_FILENAME);
1902
1948
  }
1903
1949
  function localTokenFile() {
1904
- return path3.join(dataDir(), LOCAL_TOKEN_FILENAME);
1950
+ return path4.join(dataDir(), LOCAL_TOKEN_FILENAME);
1905
1951
  }
1906
1952
  function tokenFileMissingMessage() {
1907
1953
  return `Vorn local credential not found (${localTokenFile()}).
@@ -1913,7 +1959,7 @@ MCP included, reaches a server that moved.`;
1913
1959
  }
1914
1960
  function readLocalToken() {
1915
1961
  try {
1916
- const token = fs4.readFileSync(localTokenFile(), "utf-8").trim();
1962
+ const token = fs5.readFileSync(localTokenFile(), "utf-8").trim();
1917
1963
  if (!token) throw new Error("empty");
1918
1964
  return token;
1919
1965
  } catch {
@@ -2033,8 +2079,8 @@ function discoverAndHeal() {
2033
2079
  cacheTimestamp = now;
2034
2080
  if (discovered) {
2035
2081
  try {
2036
- fs4.mkdirSync(dataDir(), { recursive: true });
2037
- fs4.writeFileSync(portFile(), JSON.stringify({ port: discovered }), "utf-8");
2082
+ fs5.mkdirSync(dataDir(), { recursive: true });
2083
+ fs5.writeFileSync(portFile(), JSON.stringify({ port: discovered }), "utf-8");
2038
2084
  } catch {
2039
2085
  }
2040
2086
  return { port: discovered };
@@ -2043,7 +2089,7 @@ function discoverAndHeal() {
2043
2089
  }
2044
2090
  function readPort() {
2045
2091
  try {
2046
- const raw = fs4.readFileSync(portFile(), "utf-8").trim();
2092
+ const raw = fs5.readFileSync(portFile(), "utf-8").trim();
2047
2093
  if (!raw) return { port: null, reason: "invalid" };
2048
2094
  if (raw.startsWith("{")) {
2049
2095
  const parsed = JSON.parse(raw);
@@ -2298,7 +2344,7 @@ function registerTaskTools(server) {
2298
2344
  const now = (/* @__PURE__ */ new Date()).toISOString();
2299
2345
  const status = args.status ?? "todo";
2300
2346
  const task = {
2301
- id: crypto2.randomUUID(),
2347
+ id: crypto3.randomUUID(),
2302
2348
  projectName: args.project_name,
2303
2349
  title: args.title,
2304
2350
  description: args.description ?? "",
@@ -2481,12 +2527,12 @@ function registerTaskTools(server) {
2481
2527
  };
2482
2528
  }
2483
2529
  const cwd = args.cwd || process.cwd();
2484
- const normalizedCwd = path4.resolve(cwd);
2530
+ const normalizedCwd = path5.resolve(cwd);
2485
2531
  const projects = await dbListProjects();
2486
2532
  let matchedProject = null;
2487
2533
  let matchLen = 0;
2488
2534
  for (const p of projects) {
2489
- const normalizedPath = path4.resolve(p.path);
2535
+ const normalizedPath = path5.resolve(p.path);
2490
2536
  if (normalizedCwd.startsWith(normalizedPath) && normalizedPath.length > matchLen) {
2491
2537
  matchedProject = p;
2492
2538
  matchLen = normalizedPath.length;
@@ -2514,7 +2560,7 @@ function registerTaskTools(server) {
2514
2560
  let matchedTask = null;
2515
2561
  for (const t of projectTasks) {
2516
2562
  if (t.worktreePath) {
2517
- const normalizedWorktree = path4.resolve(t.worktreePath);
2563
+ const normalizedWorktree = path5.resolve(t.worktreePath);
2518
2564
  if (normalizedCwd.startsWith(normalizedWorktree)) {
2519
2565
  matchedTask = t;
2520
2566
  break;
@@ -2998,12 +3044,43 @@ function registerSessionTools(server) {
2998
3044
  }
2999
3045
 
3000
3046
  // src/tools/workflows.ts
3001
- import crypto3 from "crypto";
3047
+ import crypto4 from "crypto";
3002
3048
 
3003
3049
  // ../shared/src/workflow-graph.ts
3050
+ var MAX_GATE_ROUNDS = 10;
3051
+ function nodesBetween(from, to, edges) {
3052
+ if (from === to) return /* @__PURE__ */ new Set();
3053
+ const { successors, predecessors } = buildGraph(edges);
3054
+ const reach = (start, next) => {
3055
+ const seen = /* @__PURE__ */ new Set([start]);
3056
+ const queue = [start];
3057
+ while (queue.length > 0) {
3058
+ for (const id of next.get(queue.shift()) ?? []) {
3059
+ if (!seen.has(id)) {
3060
+ seen.add(id);
3061
+ queue.push(id);
3062
+ }
3063
+ }
3064
+ }
3065
+ return seen;
3066
+ };
3067
+ const below = reach(from, successors);
3068
+ if (!below.has(to)) return /* @__PURE__ */ new Set();
3069
+ const above = reach(to, predecessors);
3070
+ return new Set([...below].filter((id) => above.has(id)));
3071
+ }
3004
3072
  function isSignInWait(state) {
3005
3073
  return state.status === "waiting" && state.waitingFor === "signIn";
3006
3074
  }
3075
+ function buildGraph(edges) {
3076
+ const successors = /* @__PURE__ */ new Map();
3077
+ const predecessors = /* @__PURE__ */ new Map();
3078
+ for (const e of edges) {
3079
+ successors.set(e.source, [...successors.get(e.source) || [], e.target]);
3080
+ predecessors.set(e.target, [...predecessors.get(e.target) || [], e.source]);
3081
+ }
3082
+ return { successors, predecessors };
3083
+ }
3007
3084
 
3008
3085
  // src/tools/workflows.ts
3009
3086
  import { z as z5 } from "zod";
@@ -3364,7 +3441,7 @@ function buildGraphFromFlat(trigger, actions) {
3364
3441
  const nodes = [];
3365
3442
  const edges = [];
3366
3443
  const triggerNode = {
3367
- id: crypto3.randomUUID(),
3444
+ id: crypto4.randomUUID(),
3368
3445
  type: "trigger",
3369
3446
  label: trigger.triggerType === "manual" ? "Manual Trigger" : trigger.triggerType === "once" ? "Schedule (Once)" : trigger.triggerType === "recurring" ? "Schedule (Recurring)" : trigger.triggerType === "taskCreated" ? "When Task Created" : trigger.triggerType === "taskStatusChanged" ? "When Task Status Changes" : "Trigger",
3370
3447
  config: trigger,
@@ -3375,7 +3452,7 @@ function buildGraphFromFlat(trigger, actions) {
3375
3452
  const NODE_GAP = 140;
3376
3453
  for (let i = 0; i < actions.length; i++) {
3377
3454
  const action = actions[i];
3378
- const nodeId = crypto3.randomUUID();
3455
+ const nodeId = crypto4.randomUUID();
3379
3456
  nodes.push({
3380
3457
  id: nodeId,
3381
3458
  type: "launchAgent",
@@ -3384,7 +3461,7 @@ function buildGraphFromFlat(trigger, actions) {
3384
3461
  position: { x: 0, y: (i + 1) * NODE_GAP }
3385
3462
  });
3386
3463
  edges.push({
3387
- id: crypto3.randomUUID(),
3464
+ id: crypto4.randomUUID(),
3388
3465
  source: prevId,
3389
3466
  target: nodeId
3390
3467
  });
@@ -3484,12 +3561,37 @@ function annotateWaitingGates(runs, workflows) {
3484
3561
  asks: "Sign in to its connection in the Vorn app, and this step runs again"
3485
3562
  };
3486
3563
  }
3487
- const asks = gateMessage(workflow, state.nodeId);
3564
+ const asks = state.message ?? gateMessage(workflow, state.nodeId);
3488
3565
  return asks ? { ...state, asks } : state;
3489
3566
  })
3490
3567
  };
3491
3568
  });
3492
3569
  }
3570
+ var ANSWERED = {
3571
+ approve: "Approved",
3572
+ reject: "Rejected",
3573
+ changes: "Sent back"
3574
+ };
3575
+ function validateGateFeedback(nodes, edges) {
3576
+ const errors = [];
3577
+ for (const node of nodes) {
3578
+ if (node.type !== "approval" || node.config?.feedback === void 0) continue;
3579
+ const feedback = node.config.feedback;
3580
+ const name = node.label || node.id;
3581
+ const from = typeof feedback?.from === "string" ? feedback.from : "";
3582
+ const source2 = nodes.find((n) => n.id === from);
3583
+ if (!source2) errors.push(`gate "${name}" redoes from unknown step "${from}"`);
3584
+ else if (source2.type === "trigger") errors.push(`gate "${name}" cannot redo from its trigger`);
3585
+ else if (nodesBetween(from, node.id, edges).size === 0) {
3586
+ errors.push(`gate "${name}" redoes from "${source2.label || from}", which does not lead to it`);
3587
+ }
3588
+ const rounds = Number(feedback?.maxRounds);
3589
+ if (!Number.isInteger(rounds) || rounds < 2 || rounds > MAX_GATE_ROUNDS) {
3590
+ errors.push(`gate "${name}" needs maxRounds between 2 and ${MAX_GATE_ROUNDS}`);
3591
+ }
3592
+ }
3593
+ return errors;
3594
+ }
3493
3595
  async function runById(runId) {
3494
3596
  const recent = (await listAllWorkflowRuns(void 0, 500)).find((r) => r.runId === runId);
3495
3597
  if (recent) return recent;
@@ -3561,9 +3663,8 @@ function registerWorkflowTools(server) {
3561
3663
  if (args.nodes && args.edges) {
3562
3664
  nodes = args.nodes;
3563
3665
  edges = args.edges;
3564
- const loopErrors = validateLoopBodies(
3565
- nodes
3566
- );
3666
+ const loose = nodes;
3667
+ const loopErrors = [...validateLoopBodies(loose), ...validateGateFeedback(loose, edges)];
3567
3668
  if (loopErrors.length > 0) {
3568
3669
  return {
3569
3670
  content: [{ type: "text", text: `Error: ${loopErrors.join("; ")}` }],
@@ -3578,7 +3679,7 @@ function registerWorkflowTools(server) {
3578
3679
  edges = graph.edges;
3579
3680
  }
3580
3681
  const workflow = {
3581
- id: crypto3.randomUUID(),
3682
+ id: crypto4.randomUUID(),
3582
3683
  name: args.name,
3583
3684
  icon: args.icon ?? "Zap",
3584
3685
  iconColor: args.icon_color ?? "#6366f1",
@@ -3622,9 +3723,9 @@ function registerWorkflowTools(server) {
3622
3723
  const updates = {};
3623
3724
  if (args.name !== void 0) updates.name = args.name;
3624
3725
  if (args.nodes !== void 0) {
3625
- const loopErrors = validateLoopBodies(
3626
- args.nodes
3627
- );
3726
+ const loose = args.nodes;
3727
+ const edges = args.edges ?? workflow.edges;
3728
+ const loopErrors = [...validateLoopBodies(loose), ...validateGateFeedback(loose, edges)];
3628
3729
  if (loopErrors.length > 0) {
3629
3730
  return {
3630
3731
  content: [{ type: "text", text: `Error: ${loopErrors.join("; ")}` }],
@@ -3753,10 +3854,15 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3753
3854
  );
3754
3855
  server.tool(
3755
3856
  "resolve_gate",
3756
- "Approve or reject the approval gate a workflow run is parked on, the way the Vorn app does. Requires the Vorn app to be running: the decision is broadcast, and the instance holding the run is what resumes it. Read what is being approved first \u2014 list_workflow_runs names the waiting node and what it asks.",
3857
+ "Approve, reject, or send back the approval gate a workflow run is parked on, the way the Vorn app does. Requires the Vorn app to be running: the decision is broadcast, and the instance holding the run is what resumes it. Read what is being approved first \u2014 list_workflow_runs names the waiting node and what it asks.",
3757
3858
  {
3758
3859
  run_id: V.id.describe("Run ID (from list_workflow_runs)"),
3759
- decision: z5.enum(["approve", "reject"]).describe("approve lets the run go on; reject ends it"),
3860
+ decision: z5.enum(["approve", "reject", "changes"]).describe(
3861
+ "approve lets the run go on; reject ends it; changes sends the work back to the step the gate redoes from, with your comment, and the gate asks again"
3862
+ ),
3863
+ comment: z5.string().max(2e4).optional().describe(
3864
+ "Required for changes: what to change. Optional on approve and reject; kept with the run."
3865
+ ),
3760
3866
  node_id: V.id.optional().describe("The waiting node, when a run has more than one gate open")
3761
3867
  },
3762
3868
  async (args) => {
@@ -3789,15 +3895,35 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3789
3895
  isError: true
3790
3896
  };
3791
3897
  }
3898
+ if (args.decision === "changes" && !args.comment?.trim()) {
3899
+ return {
3900
+ content: [
3901
+ { type: "text", text: "Error: changes needs a comment saying what to change." }
3902
+ ],
3903
+ isError: true
3904
+ };
3905
+ }
3792
3906
  const workflow = (await dbListWorkflows()).find((w) => w.id === run.workflowId);
3793
3907
  const gateNode = approvalNode(workflow, target.nodeId);
3794
- const asked = askedBy(gateNode);
3908
+ const asked = run.nodeStates.find((n) => n.nodeId === target.nodeId)?.message ?? askedBy(gateNode);
3795
3909
  try {
3796
- await rpcCall("workflow:resolveGate", {
3910
+ const answer = await rpcCall("workflow:resolveGate", {
3797
3911
  runId: args.run_id,
3798
3912
  nodeId: target.nodeId,
3799
- decision: args.decision
3913
+ decision: args.decision,
3914
+ ...args.comment?.trim() && { comment: args.comment.trim() }
3800
3915
  });
3916
+ if (answer?.accepted === false) {
3917
+ return {
3918
+ content: [
3919
+ {
3920
+ type: "text",
3921
+ text: args.decision === "changes" ? "Error: this gate takes no changes now: it has no step to redo from, or its rounds are used up. Approve or reject it instead." : "Error: the gate did not take that answer. Check list_workflow_runs."
3922
+ }
3923
+ ],
3924
+ isError: true
3925
+ };
3926
+ }
3801
3927
  } catch (err) {
3802
3928
  return {
3803
3929
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
@@ -3809,7 +3935,7 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3809
3935
  content: [
3810
3936
  {
3811
3937
  type: "text",
3812
- text: `${args.decision === "approve" ? "Approved" : "Rejected"} "${gate}" on run ${args.run_id}${run.workflowName ? ` of "${run.workflowName}"` : ""}.${asked ? `
3938
+ text: `${ANSWERED[args.decision]} "${gate}" on run ${args.run_id}${run.workflowName ? ` of "${run.workflowName}"` : ""}.${asked ? `
3813
3939
 
3814
3940
  What it asked: ${asked}` : ""}
3815
3941
 
@@ -4018,9 +4144,11 @@ Warning: ${unnamed.length} step(s) point at a connection this install could not
4018
4144
  isError: true
4019
4145
  };
4020
4146
  }
4021
- const loopErrors = validateLoopBodies(
4022
- parsed.nodes
4023
- );
4147
+ const loose = parsed.nodes;
4148
+ const loopErrors = [
4149
+ ...validateLoopBodies(loose),
4150
+ ...validateGateFeedback(loose, parsed.edges)
4151
+ ];
4024
4152
  if (loopErrors.length > 0) {
4025
4153
  return {
4026
4154
  content: [{ type: "text", text: `Error: ${loopErrors.join("; ")}` }],
@@ -4080,7 +4208,7 @@ function registerConfigTools(server) {
4080
4208
  }
4081
4209
 
4082
4210
  // src/tools/workspaces.ts
4083
- import crypto4 from "crypto";
4211
+ import crypto5 from "crypto";
4084
4212
  import { z as z6 } from "zod";
4085
4213
  function registerWorkspaceTools(server) {
4086
4214
  server.tool("list_workspaces", "List all workspaces", async () => {
@@ -4099,7 +4227,7 @@ function registerWorkspaceTools(server) {
4099
4227
  const existing = await dbListWorkspaces();
4100
4228
  const maxOrder = existing.reduce((max, w) => Math.max(max, w.order), 0);
4101
4229
  const workspace = {
4102
- id: crypto4.randomUUID(),
4230
+ id: crypto5.randomUUID(),
4103
4231
  name: args.name,
4104
4232
  order: maxOrder + 1,
4105
4233
  ...args.icon && { icon: args.icon },
@@ -4507,7 +4635,7 @@ function plural(count, word) {
4507
4635
  }
4508
4636
 
4509
4637
  // src/tools/browser.ts
4510
- import crypto5 from "crypto";
4638
+ import crypto6 from "crypto";
4511
4639
  import { z as z8 } from "zod";
4512
4640
  function sessionId(env = process.env) {
4513
4641
  const id = env.VORN_SESSION_ID;
@@ -4534,7 +4662,7 @@ function source(label) {
4534
4662
  return label.includes("WEB PAGE") || label.includes("BROWSER") ? "page" : "device";
4535
4663
  }
4536
4664
  function pageResult(data, label = "WEB PAGE CONTENT") {
4537
- const nonce = crypto5.randomUUID();
4665
+ const nonce = crypto6.randomUUID();
4538
4666
  return {
4539
4667
  content: [
4540
4668
  {
@@ -4983,7 +5111,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
4983
5111
  console.error = (...args) => _origError("[mcp:error]", ...args);
4984
5112
  async function main() {
4985
5113
  configManager.init();
4986
- const version = true ? "0.7.1-beta.3" : createRequire(import.meta.url)("../package.json").version;
5114
+ const version = true ? "0.7.1-beta.5" : createRequire(import.meta.url)("../package.json").version;
4987
5115
  const server = createMcpServer(version);
4988
5116
  const transport = new StdioServerTransport();
4989
5117
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vornrun/mcp",
3
- "version": "0.7.1-beta.3",
3
+ "version": "0.7.1-beta.5",
4
4
  "description": "Vorn MCP server — task management, git, and workflow tools for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,8 +38,8 @@
38
38
  "zod": "^4.5.4"
39
39
  },
40
40
  "devDependencies": {
41
- "@vornrun/server": "0.7.1-beta.3",
42
- "@vornrun/shared": "0.7.1-beta.3",
41
+ "@vornrun/server": "0.7.1-beta.5",
42
+ "@vornrun/shared": "0.7.1-beta.5",
43
43
  "tsup": "^8.5.1",
44
44
  "tsx": "^4.23.1",
45
45
  "typescript": "^6.0.3"