@vornrun/mcp 0.7.1-beta.4 → 0.7.1-beta.6

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 +213 -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,13 @@ 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,
538
+ editable_text TEXT,
539
+ edited_text TEXT,
526
540
  FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
527
541
  );
528
542
 
@@ -1081,7 +1095,58 @@ function migrateSchema(d) {
1081
1095
  })();
1082
1096
  logger_default.info("[database] migrated schema to version 21 (package connections belong to sdk)");
1083
1097
  }
1098
+ if (version < 22) {
1099
+ d.transaction(() => {
1100
+ const runCols = d.prepare("PRAGMA table_info(workflow_runs)").all();
1101
+ if (!runCols.some((c) => c.name === "definition")) {
1102
+ d.exec("ALTER TABLE workflow_runs ADD COLUMN definition TEXT");
1103
+ }
1104
+ d.prepare(
1105
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '22')"
1106
+ ).run();
1107
+ })();
1108
+ logger_default.info("[database] migrated schema to version 22 (the definition a run started with)");
1109
+ }
1110
+ if (version < 23) {
1111
+ d.transaction(() => {
1112
+ const nodeCols = d.prepare("PRAGMA table_info(workflow_run_nodes)").all();
1113
+ for (const [column, type] of GATE_COLUMNS) {
1114
+ if (!nodeCols.some((c) => c.name === column)) {
1115
+ d.exec(`ALTER TABLE workflow_run_nodes ADD COLUMN ${column} ${type}`);
1116
+ }
1117
+ }
1118
+ d.prepare(
1119
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '23')"
1120
+ ).run();
1121
+ })();
1122
+ logger_default.info("[database] migrated schema to version 23 (what an approval gate asked and heard)");
1123
+ }
1124
+ if (version < 24) {
1125
+ d.transaction(() => {
1126
+ const nodeCols = d.prepare("PRAGMA table_info(workflow_run_nodes)").all();
1127
+ for (const [column, type] of GATE_EDIT_COLUMNS) {
1128
+ if (!nodeCols.some((c) => c.name === column)) {
1129
+ d.exec(`ALTER TABLE workflow_run_nodes ADD COLUMN ${column} ${type}`);
1130
+ }
1131
+ }
1132
+ d.prepare(
1133
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '24')"
1134
+ ).run();
1135
+ })();
1136
+ logger_default.info("[database] migrated schema to version 24 (the text a reviewer edited at a gate)");
1137
+ }
1084
1138
  }
1139
+ var GATE_COLUMNS = [
1140
+ ["message", "TEXT"],
1141
+ ["view_token", "TEXT"],
1142
+ ["round", "INTEGER"],
1143
+ ["feedback", "TEXT"],
1144
+ ["rejected_at", "TEXT"]
1145
+ ];
1146
+ var GATE_EDIT_COLUMNS = [
1147
+ ["editable_text", "TEXT"],
1148
+ ["edited_text", "TEXT"]
1149
+ ];
1085
1150
  var REVISIONED_TABLES = [
1086
1151
  "projects",
1087
1152
  "tasks",
@@ -1156,7 +1221,8 @@ function verifySchema(d) {
1156
1221
  {
1157
1222
  column: "connector_inbox_disposition",
1158
1223
  ddl: "ALTER TABLE workflow_runs ADD COLUMN connector_inbox_disposition TEXT"
1159
- }
1224
+ },
1225
+ { column: "definition", ddl: "ALTER TABLE workflow_runs ADD COLUMN definition TEXT" }
1160
1226
  ],
1161
1227
  connector_inbox: [
1162
1228
  {
@@ -1191,7 +1257,11 @@ function verifySchema(d) {
1191
1257
  ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN structured_output TEXT"
1192
1258
  },
1193
1259
  // Which pass of a loop produced this row.
1194
- { column: "iteration", ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN iteration INTEGER" }
1260
+ { column: "iteration", ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN iteration INTEGER" },
1261
+ ...GATE_COLUMNS.map(([column, type]) => ({
1262
+ column,
1263
+ ddl: `ALTER TABLE workflow_run_nodes ADD COLUMN ${column} ${type}`
1264
+ }))
1195
1265
  ],
1196
1266
  tasks: [
1197
1267
  {
@@ -1815,16 +1885,12 @@ var ConfigManager = class {
1815
1885
  cb(config);
1816
1886
  }
1817
1887
  }
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
- */
1888
+ /** Reload when a writer touches `.db-signal` (`dbSignalChange`) after changing the configuration. */
1822
1889
  watchDb() {
1823
1890
  if (this.dbWatcher) return;
1824
- const WATCH_SUFFIXES = [".db-signal", ".db-wal", ".db"];
1825
1891
  try {
1826
- this.dbWatcher = fs3.watch(getDataDir(), (eventType, filename) => {
1827
- if (!filename || !WATCH_SUFFIXES.some((s) => filename.endsWith(s))) return;
1892
+ this.dbWatcher = fs4.watch(getDataDir(), (_eventType, filename) => {
1893
+ if (filename !== ".db-signal") return;
1828
1894
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
1829
1895
  this.debounceTimer = setTimeout(() => {
1830
1896
  this.notifyChanged();
@@ -1855,8 +1921,8 @@ var configManager = new ConfigManager();
1855
1921
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1856
1922
 
1857
1923
  // src/tools/tasks.ts
1858
- import crypto2 from "crypto";
1859
- import path4 from "path";
1924
+ import crypto3 from "crypto";
1925
+ import path5 from "path";
1860
1926
  import { z as z2 } from "zod";
1861
1927
 
1862
1928
  // src/validation.ts
@@ -1885,8 +1951,8 @@ var V = {
1885
1951
  };
1886
1952
 
1887
1953
  // ../server/src/rpc-client.ts
1888
- import fs4 from "fs";
1889
- import path3 from "path";
1954
+ import fs5 from "fs";
1955
+ import path4 from "path";
1890
1956
  import os2 from "os";
1891
1957
  import { execFileSync as execFileSync2 } from "child_process";
1892
1958
  import { WebSocket } from "ws";
@@ -1895,13 +1961,13 @@ function named(value) {
1895
1961
  return value?.trim() ? value : void 0;
1896
1962
  }
1897
1963
  function dataDir() {
1898
- return dataDirOverride ?? named(process.env.VORN_DATA_DIR) ?? path3.join(os2.homedir(), ".vorn");
1964
+ return dataDirOverride ?? named(process.env.VORN_DATA_DIR) ?? path4.join(os2.homedir(), ".vorn");
1899
1965
  }
1900
1966
  function portFile() {
1901
- return path3.join(dataDir(), WS_PORT_FILENAME);
1967
+ return path4.join(dataDir(), WS_PORT_FILENAME);
1902
1968
  }
1903
1969
  function localTokenFile() {
1904
- return path3.join(dataDir(), LOCAL_TOKEN_FILENAME);
1970
+ return path4.join(dataDir(), LOCAL_TOKEN_FILENAME);
1905
1971
  }
1906
1972
  function tokenFileMissingMessage() {
1907
1973
  return `Vorn local credential not found (${localTokenFile()}).
@@ -1913,7 +1979,7 @@ MCP included, reaches a server that moved.`;
1913
1979
  }
1914
1980
  function readLocalToken() {
1915
1981
  try {
1916
- const token = fs4.readFileSync(localTokenFile(), "utf-8").trim();
1982
+ const token = fs5.readFileSync(localTokenFile(), "utf-8").trim();
1917
1983
  if (!token) throw new Error("empty");
1918
1984
  return token;
1919
1985
  } catch {
@@ -2033,8 +2099,8 @@ function discoverAndHeal() {
2033
2099
  cacheTimestamp = now;
2034
2100
  if (discovered) {
2035
2101
  try {
2036
- fs4.mkdirSync(dataDir(), { recursive: true });
2037
- fs4.writeFileSync(portFile(), JSON.stringify({ port: discovered }), "utf-8");
2102
+ fs5.mkdirSync(dataDir(), { recursive: true });
2103
+ fs5.writeFileSync(portFile(), JSON.stringify({ port: discovered }), "utf-8");
2038
2104
  } catch {
2039
2105
  }
2040
2106
  return { port: discovered };
@@ -2043,7 +2109,7 @@ function discoverAndHeal() {
2043
2109
  }
2044
2110
  function readPort() {
2045
2111
  try {
2046
- const raw = fs4.readFileSync(portFile(), "utf-8").trim();
2112
+ const raw = fs5.readFileSync(portFile(), "utf-8").trim();
2047
2113
  if (!raw) return { port: null, reason: "invalid" };
2048
2114
  if (raw.startsWith("{")) {
2049
2115
  const parsed = JSON.parse(raw);
@@ -2298,7 +2364,7 @@ function registerTaskTools(server) {
2298
2364
  const now = (/* @__PURE__ */ new Date()).toISOString();
2299
2365
  const status = args.status ?? "todo";
2300
2366
  const task = {
2301
- id: crypto2.randomUUID(),
2367
+ id: crypto3.randomUUID(),
2302
2368
  projectName: args.project_name,
2303
2369
  title: args.title,
2304
2370
  description: args.description ?? "",
@@ -2481,12 +2547,12 @@ function registerTaskTools(server) {
2481
2547
  };
2482
2548
  }
2483
2549
  const cwd = args.cwd || process.cwd();
2484
- const normalizedCwd = path4.resolve(cwd);
2550
+ const normalizedCwd = path5.resolve(cwd);
2485
2551
  const projects = await dbListProjects();
2486
2552
  let matchedProject = null;
2487
2553
  let matchLen = 0;
2488
2554
  for (const p of projects) {
2489
- const normalizedPath = path4.resolve(p.path);
2555
+ const normalizedPath = path5.resolve(p.path);
2490
2556
  if (normalizedCwd.startsWith(normalizedPath) && normalizedPath.length > matchLen) {
2491
2557
  matchedProject = p;
2492
2558
  matchLen = normalizedPath.length;
@@ -2514,7 +2580,7 @@ function registerTaskTools(server) {
2514
2580
  let matchedTask = null;
2515
2581
  for (const t of projectTasks) {
2516
2582
  if (t.worktreePath) {
2517
- const normalizedWorktree = path4.resolve(t.worktreePath);
2583
+ const normalizedWorktree = path5.resolve(t.worktreePath);
2518
2584
  if (normalizedCwd.startsWith(normalizedWorktree)) {
2519
2585
  matchedTask = t;
2520
2586
  break;
@@ -2998,12 +3064,43 @@ function registerSessionTools(server) {
2998
3064
  }
2999
3065
 
3000
3066
  // src/tools/workflows.ts
3001
- import crypto3 from "crypto";
3067
+ import crypto4 from "crypto";
3002
3068
 
3003
3069
  // ../shared/src/workflow-graph.ts
3070
+ var MAX_GATE_ROUNDS = 10;
3071
+ function nodesBetween(from, to, edges) {
3072
+ if (from === to) return /* @__PURE__ */ new Set();
3073
+ const { successors, predecessors } = buildGraph(edges);
3074
+ const reach = (start, next) => {
3075
+ const seen = /* @__PURE__ */ new Set([start]);
3076
+ const queue = [start];
3077
+ while (queue.length > 0) {
3078
+ for (const id of next.get(queue.shift()) ?? []) {
3079
+ if (!seen.has(id)) {
3080
+ seen.add(id);
3081
+ queue.push(id);
3082
+ }
3083
+ }
3084
+ }
3085
+ return seen;
3086
+ };
3087
+ const below = reach(from, successors);
3088
+ if (!below.has(to)) return /* @__PURE__ */ new Set();
3089
+ const above = reach(to, predecessors);
3090
+ return new Set([...below].filter((id) => above.has(id)));
3091
+ }
3004
3092
  function isSignInWait(state) {
3005
3093
  return state.status === "waiting" && state.waitingFor === "signIn";
3006
3094
  }
3095
+ function buildGraph(edges) {
3096
+ const successors = /* @__PURE__ */ new Map();
3097
+ const predecessors = /* @__PURE__ */ new Map();
3098
+ for (const e of edges) {
3099
+ successors.set(e.source, [...successors.get(e.source) || [], e.target]);
3100
+ predecessors.set(e.target, [...predecessors.get(e.target) || [], e.source]);
3101
+ }
3102
+ return { successors, predecessors };
3103
+ }
3007
3104
 
3008
3105
  // src/tools/workflows.ts
3009
3106
  import { z as z5 } from "zod";
@@ -3364,7 +3461,7 @@ function buildGraphFromFlat(trigger, actions) {
3364
3461
  const nodes = [];
3365
3462
  const edges = [];
3366
3463
  const triggerNode = {
3367
- id: crypto3.randomUUID(),
3464
+ id: crypto4.randomUUID(),
3368
3465
  type: "trigger",
3369
3466
  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
3467
  config: trigger,
@@ -3375,7 +3472,7 @@ function buildGraphFromFlat(trigger, actions) {
3375
3472
  const NODE_GAP = 140;
3376
3473
  for (let i = 0; i < actions.length; i++) {
3377
3474
  const action = actions[i];
3378
- const nodeId = crypto3.randomUUID();
3475
+ const nodeId = crypto4.randomUUID();
3379
3476
  nodes.push({
3380
3477
  id: nodeId,
3381
3478
  type: "launchAgent",
@@ -3384,7 +3481,7 @@ function buildGraphFromFlat(trigger, actions) {
3384
3481
  position: { x: 0, y: (i + 1) * NODE_GAP }
3385
3482
  });
3386
3483
  edges.push({
3387
- id: crypto3.randomUUID(),
3484
+ id: crypto4.randomUUID(),
3388
3485
  source: prevId,
3389
3486
  target: nodeId
3390
3487
  });
@@ -3484,12 +3581,37 @@ function annotateWaitingGates(runs, workflows) {
3484
3581
  asks: "Sign in to its connection in the Vorn app, and this step runs again"
3485
3582
  };
3486
3583
  }
3487
- const asks = gateMessage(workflow, state.nodeId);
3584
+ const asks = state.message ?? gateMessage(workflow, state.nodeId);
3488
3585
  return asks ? { ...state, asks } : state;
3489
3586
  })
3490
3587
  };
3491
3588
  });
3492
3589
  }
3590
+ var ANSWERED = {
3591
+ approve: "Approved",
3592
+ reject: "Rejected",
3593
+ changes: "Sent back"
3594
+ };
3595
+ function validateGateFeedback(nodes, edges) {
3596
+ const errors = [];
3597
+ for (const node of nodes) {
3598
+ if (node.type !== "approval" || node.config?.feedback === void 0) continue;
3599
+ const feedback = node.config.feedback;
3600
+ const name = node.label || node.id;
3601
+ const from = typeof feedback?.from === "string" ? feedback.from : "";
3602
+ const source2 = nodes.find((n) => n.id === from);
3603
+ if (!source2) errors.push(`gate "${name}" redoes from unknown step "${from}"`);
3604
+ else if (source2.type === "trigger") errors.push(`gate "${name}" cannot redo from its trigger`);
3605
+ else if (nodesBetween(from, node.id, edges).size === 0) {
3606
+ errors.push(`gate "${name}" redoes from "${source2.label || from}", which does not lead to it`);
3607
+ }
3608
+ const rounds = Number(feedback?.maxRounds);
3609
+ if (!Number.isInteger(rounds) || rounds < 2 || rounds > MAX_GATE_ROUNDS) {
3610
+ errors.push(`gate "${name}" needs maxRounds between 2 and ${MAX_GATE_ROUNDS}`);
3611
+ }
3612
+ }
3613
+ return errors;
3614
+ }
3493
3615
  async function runById(runId) {
3494
3616
  const recent = (await listAllWorkflowRuns(void 0, 500)).find((r) => r.runId === runId);
3495
3617
  if (recent) return recent;
@@ -3561,9 +3683,8 @@ function registerWorkflowTools(server) {
3561
3683
  if (args.nodes && args.edges) {
3562
3684
  nodes = args.nodes;
3563
3685
  edges = args.edges;
3564
- const loopErrors = validateLoopBodies(
3565
- nodes
3566
- );
3686
+ const loose = nodes;
3687
+ const loopErrors = [...validateLoopBodies(loose), ...validateGateFeedback(loose, edges)];
3567
3688
  if (loopErrors.length > 0) {
3568
3689
  return {
3569
3690
  content: [{ type: "text", text: `Error: ${loopErrors.join("; ")}` }],
@@ -3578,7 +3699,7 @@ function registerWorkflowTools(server) {
3578
3699
  edges = graph.edges;
3579
3700
  }
3580
3701
  const workflow = {
3581
- id: crypto3.randomUUID(),
3702
+ id: crypto4.randomUUID(),
3582
3703
  name: args.name,
3583
3704
  icon: args.icon ?? "Zap",
3584
3705
  iconColor: args.icon_color ?? "#6366f1",
@@ -3622,9 +3743,9 @@ function registerWorkflowTools(server) {
3622
3743
  const updates = {};
3623
3744
  if (args.name !== void 0) updates.name = args.name;
3624
3745
  if (args.nodes !== void 0) {
3625
- const loopErrors = validateLoopBodies(
3626
- args.nodes
3627
- );
3746
+ const loose = args.nodes;
3747
+ const edges = args.edges ?? workflow.edges;
3748
+ const loopErrors = [...validateLoopBodies(loose), ...validateGateFeedback(loose, edges)];
3628
3749
  if (loopErrors.length > 0) {
3629
3750
  return {
3630
3751
  content: [{ type: "text", text: `Error: ${loopErrors.join("; ")}` }],
@@ -3753,10 +3874,18 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3753
3874
  );
3754
3875
  server.tool(
3755
3876
  "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.",
3877
+ "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
3878
  {
3758
3879
  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"),
3880
+ decision: z5.enum(["approve", "reject", "changes"]).describe(
3881
+ "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"
3882
+ ),
3883
+ comment: z5.string().max(2e4).optional().describe(
3884
+ "Required for changes: what to change. Optional on approve and reject; kept with the run."
3885
+ ),
3886
+ edited: z5.string().max(2e5).optional().describe(
3887
+ "Your rewrite of the gate's editable text, when it has one. Kept beside the original, and read by later steps as {{steps.<gate>.text}}. Ignored on reject."
3888
+ ),
3760
3889
  node_id: V.id.optional().describe("The waiting node, when a run has more than one gate open")
3761
3890
  },
3762
3891
  async (args) => {
@@ -3789,15 +3918,36 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3789
3918
  isError: true
3790
3919
  };
3791
3920
  }
3921
+ if (args.decision === "changes" && !args.comment?.trim()) {
3922
+ return {
3923
+ content: [
3924
+ { type: "text", text: "Error: changes needs a comment saying what to change." }
3925
+ ],
3926
+ isError: true
3927
+ };
3928
+ }
3792
3929
  const workflow = (await dbListWorkflows()).find((w) => w.id === run.workflowId);
3793
3930
  const gateNode = approvalNode(workflow, target.nodeId);
3794
- const asked = askedBy(gateNode);
3931
+ const asked = run.nodeStates.find((n) => n.nodeId === target.nodeId)?.message ?? askedBy(gateNode);
3795
3932
  try {
3796
- await rpcCall("workflow:resolveGate", {
3933
+ const answer = await rpcCall("workflow:resolveGate", {
3797
3934
  runId: args.run_id,
3798
3935
  nodeId: target.nodeId,
3799
- decision: args.decision
3936
+ decision: args.decision,
3937
+ ...args.comment?.trim() && { comment: args.comment.trim() },
3938
+ ...args.edited?.trim() && { edited: args.edited.trim() }
3800
3939
  });
3940
+ if (answer?.accepted === false) {
3941
+ return {
3942
+ content: [
3943
+ {
3944
+ type: "text",
3945
+ 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."
3946
+ }
3947
+ ],
3948
+ isError: true
3949
+ };
3950
+ }
3801
3951
  } catch (err) {
3802
3952
  return {
3803
3953
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
@@ -3809,7 +3959,7 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
3809
3959
  content: [
3810
3960
  {
3811
3961
  type: "text",
3812
- text: `${args.decision === "approve" ? "Approved" : "Rejected"} "${gate}" on run ${args.run_id}${run.workflowName ? ` of "${run.workflowName}"` : ""}.${asked ? `
3962
+ text: `${ANSWERED[args.decision]} "${gate}" on run ${args.run_id}${run.workflowName ? ` of "${run.workflowName}"` : ""}.${asked ? `
3813
3963
 
3814
3964
  What it asked: ${asked}` : ""}
3815
3965
 
@@ -4018,9 +4168,11 @@ Warning: ${unnamed.length} step(s) point at a connection this install could not
4018
4168
  isError: true
4019
4169
  };
4020
4170
  }
4021
- const loopErrors = validateLoopBodies(
4022
- parsed.nodes
4023
- );
4171
+ const loose = parsed.nodes;
4172
+ const loopErrors = [
4173
+ ...validateLoopBodies(loose),
4174
+ ...validateGateFeedback(loose, parsed.edges)
4175
+ ];
4024
4176
  if (loopErrors.length > 0) {
4025
4177
  return {
4026
4178
  content: [{ type: "text", text: `Error: ${loopErrors.join("; ")}` }],
@@ -4080,7 +4232,7 @@ function registerConfigTools(server) {
4080
4232
  }
4081
4233
 
4082
4234
  // src/tools/workspaces.ts
4083
- import crypto4 from "crypto";
4235
+ import crypto5 from "crypto";
4084
4236
  import { z as z6 } from "zod";
4085
4237
  function registerWorkspaceTools(server) {
4086
4238
  server.tool("list_workspaces", "List all workspaces", async () => {
@@ -4099,7 +4251,7 @@ function registerWorkspaceTools(server) {
4099
4251
  const existing = await dbListWorkspaces();
4100
4252
  const maxOrder = existing.reduce((max, w) => Math.max(max, w.order), 0);
4101
4253
  const workspace = {
4102
- id: crypto4.randomUUID(),
4254
+ id: crypto5.randomUUID(),
4103
4255
  name: args.name,
4104
4256
  order: maxOrder + 1,
4105
4257
  ...args.icon && { icon: args.icon },
@@ -4507,7 +4659,7 @@ function plural(count, word) {
4507
4659
  }
4508
4660
 
4509
4661
  // src/tools/browser.ts
4510
- import crypto5 from "crypto";
4662
+ import crypto6 from "crypto";
4511
4663
  import { z as z8 } from "zod";
4512
4664
  function sessionId(env = process.env) {
4513
4665
  const id = env.VORN_SESSION_ID;
@@ -4534,7 +4686,7 @@ function source(label) {
4534
4686
  return label.includes("WEB PAGE") || label.includes("BROWSER") ? "page" : "device";
4535
4687
  }
4536
4688
  function pageResult(data, label = "WEB PAGE CONTENT") {
4537
- const nonce = crypto5.randomUUID();
4689
+ const nonce = crypto6.randomUUID();
4538
4690
  return {
4539
4691
  content: [
4540
4692
  {
@@ -4983,7 +5135,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
4983
5135
  console.error = (...args) => _origError("[mcp:error]", ...args);
4984
5136
  async function main() {
4985
5137
  configManager.init();
4986
- const version = true ? "0.7.1-beta.4" : createRequire(import.meta.url)("../package.json").version;
5138
+ const version = true ? "0.7.1-beta.6" : createRequire(import.meta.url)("../package.json").version;
4987
5139
  const server = createMcpServer(version);
4988
5140
  const transport = new StdioServerTransport();
4989
5141
  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.4",
3
+ "version": "0.7.1-beta.6",
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.4",
42
- "@vornrun/shared": "0.7.1-beta.4",
41
+ "@vornrun/server": "0.7.1-beta.6",
42
+ "@vornrun/shared": "0.7.1-beta.6",
43
43
  "tsup": "^8.5.1",
44
44
  "tsx": "^4.23.1",
45
45
  "typescript": "^6.0.3"