@rallycry/conveyor-mcp 5.0.3 → 5.0.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.
package/README.md CHANGED
@@ -76,6 +76,12 @@ CONVEYOR_PROJECT_ID = "<project-id>"
76
76
  | `CONVEYOR_PROJECT_ID` | no | Default project for unqualified tools. Omit it for multi-project mode. |
77
77
  | `CONVEYOR_SUBPROJECT_ID` | no | Default board. Unqualified `create_task`/`list_tasks`/`search_tasks` target it. |
78
78
 
79
+ ## How the server exits
80
+
81
+ The server's lifetime is its stdin pipe. It exits when stdin reaches EOF, on
82
+ `SIGINT` or `SIGTERM`, and when its parent process changes — so a client that
83
+ force-quits, crashes, or is torn down never leaves a server behind.
84
+
79
85
  ## Why `npx -y …@latest`
80
86
 
81
87
  Every form above launches the server through
@@ -0,0 +1,45 @@
1
+ // src/lifecycle.ts
2
+ function watchForClientExit(onExit, deps = {}) {
3
+ const stdin = deps.stdin ?? process.stdin;
4
+ const signals = deps.signals ?? process;
5
+ const getPpid = deps.getPpid ?? (() => process.ppid);
6
+ const intervalMs = deps.intervalMs ?? 3e4;
7
+ const watchStdin = deps.watchStdin ?? true;
8
+ let fired = false;
9
+ const fire = () => {
10
+ if (fired) return;
11
+ fired = true;
12
+ onExit();
13
+ };
14
+ signals.on("SIGINT", fire);
15
+ signals.on("SIGTERM", fire);
16
+ signals.on("SIGHUP", fire);
17
+ if (watchStdin && stdin.isTTY !== true) {
18
+ if (deps.resumeStdin) stdin.resume?.();
19
+ stdin.on("end", fire);
20
+ stdin.on("close", fire);
21
+ }
22
+ const initialPpid = getPpid();
23
+ const orphanCheck = setInterval(() => {
24
+ if (getPpid() !== initialPpid) fire();
25
+ }, intervalMs);
26
+ orphanCheck.unref?.();
27
+ return fire;
28
+ }
29
+ function installLifecycle(conn, deps = {}) {
30
+ const exit = deps.exit ?? ((code) => process.exit(code));
31
+ let stopping = false;
32
+ function stop(code = 0) {
33
+ if (stopping) return;
34
+ stopping = true;
35
+ conn.disconnect();
36
+ exit(code);
37
+ }
38
+ watchForClientExit(() => stop(), deps);
39
+ return stop;
40
+ }
41
+
42
+ export {
43
+ watchForClientExit,
44
+ installLifecycle
45
+ };
@@ -49,10 +49,14 @@ function enrichToolError(error, fallback) {
49
49
  var ConveyorConnection = class {
50
50
  socket = null;
51
51
  config;
52
+ socketFactory;
52
53
  /** project slug → id, for resolving `<project>/<card>` card paths. */
53
54
  projectSlugIds = /* @__PURE__ */ new Map();
54
- constructor(config) {
55
+ /** Session rooms this connection has joined, replayed after every reconnect. */
56
+ subscribedSessionIds = /* @__PURE__ */ new Set();
57
+ constructor(config, socketFactory = io) {
55
58
  this.config = config;
59
+ this.socketFactory = socketFactory;
56
60
  }
57
61
  get projectId() {
58
62
  return this.resolveProjectId();
@@ -86,12 +90,32 @@ var ConveyorConnection = class {
86
90
  if (Array.isArray(record.data)) return record.data;
87
91
  return [];
88
92
  }
93
+ /**
94
+ * Re-join every room this connection needs.
95
+ *
96
+ * Runs on the FIRST connect and on every reconnect. A socket.io reconnect
97
+ * starts a fresh server-side socket that belongs to no rooms, and Cloud Run
98
+ * cuts each websocket at its 60-minute request limit, so a subscribe sent
99
+ * only once left long-lived MCP connections silently outside the project room
100
+ * for the rest of the day.
101
+ */
102
+ rejoinRooms() {
103
+ const socket = this.socket;
104
+ if (!socket) return;
105
+ socket.emit("projectService:subscribe", { id: this.config.projectId });
106
+ for (const sessionId of this.subscribedSessionIds) {
107
+ socket.emit("agentSessionService:subscribe", {
108
+ entryId: sessionId,
109
+ requiredLevel: "Read"
110
+ });
111
+ }
112
+ }
89
113
  connect() {
90
114
  return new Promise((resolve, reject) => {
91
115
  let settled = false;
92
116
  let attempts = 0;
93
117
  const maxAttempts = 15;
94
- this.socket = io(
118
+ this.socket = this.socketFactory(
95
119
  this.config.apiUrl,
96
120
  buildConveyorSocketOptions({
97
121
  projectToken: this.config.projectToken,
@@ -99,12 +123,15 @@ var ConveyorConnection = class {
99
123
  })
100
124
  );
101
125
  this.socket.on("connect", () => {
126
+ this.rejoinRooms();
102
127
  if (!settled) {
103
128
  settled = true;
104
- this.socket?.emit("projectService:subscribe", { id: this.config.projectId });
105
129
  resolve();
106
130
  }
107
131
  });
132
+ this.socket.on("disconnect", (reason) => {
133
+ if (reason === "io server disconnect") this.socket?.connect();
134
+ });
108
135
  this.socket.on("connect_error", (err) => {
109
136
  const message = err?.message ?? "unknown error";
110
137
  if (!settled && AUTH_ERROR_RE.test(message)) {
@@ -547,6 +574,52 @@ var ConveyorConnection = class {
547
574
  ...rest
548
575
  });
549
576
  }
577
+ // ── Decisions ───────────────────────────────────────────────────────
578
+ //
579
+ // Every method resolves the project the same way the rest of the surface
580
+ // does, so a caller with a configured default never has to pass one.
581
+ createDecision(params) {
582
+ const { projectId, ...rest } = params;
583
+ return this.call("createProjectDecision", {
584
+ projectId: this.resolveProjectId(projectId),
585
+ ...rest
586
+ });
587
+ }
588
+ listDecisions(params) {
589
+ const { projectId, ...rest } = params;
590
+ return this.call("listProjectDecisions", {
591
+ projectId: this.resolveProjectId(projectId),
592
+ ...rest
593
+ });
594
+ }
595
+ getDecision(params) {
596
+ const { projectId, ...rest } = params;
597
+ return this.call("getProjectDecision", {
598
+ projectId: this.resolveProjectId(projectId),
599
+ ...rest
600
+ });
601
+ }
602
+ voteDecision(params) {
603
+ const { projectId, ...rest } = params;
604
+ return this.call("voteProjectDecision", {
605
+ projectId: this.resolveProjectId(projectId),
606
+ ...rest
607
+ });
608
+ }
609
+ resolveDecision(params) {
610
+ const { projectId, ...rest } = params;
611
+ return this.call("resolveProjectDecision", {
612
+ projectId: this.resolveProjectId(projectId),
613
+ ...rest
614
+ });
615
+ }
616
+ updateDecision(params) {
617
+ const { projectId, ...rest } = params;
618
+ return this.call("updateProjectDecision", {
619
+ projectId: this.resolveProjectId(projectId),
620
+ ...rest
621
+ });
622
+ }
550
623
  // ── Meetings ────────────────────────────────────────────────────────
551
624
  listMeetings(params) {
552
625
  const { projectId, ...rest } = params;
@@ -943,6 +1016,7 @@ var ConveyorConnection = class {
943
1016
  subscribeToSession(sessionId) {
944
1017
  const socket = this.socket;
945
1018
  if (!socket) throw new Error("Not connected");
1019
+ this.subscribedSessionIds.add(sessionId);
946
1020
  socket.emit("agentSessionService:subscribe", {
947
1021
  entryId: sessionId,
948
1022
  requiredLevel: "Read"
@@ -1013,6 +1087,7 @@ var ConveyorConnection = class {
1013
1087
  disconnect() {
1014
1088
  this.socket?.disconnect();
1015
1089
  this.socket = null;
1090
+ this.subscribedSessionIds.clear();
1016
1091
  }
1017
1092
  };
1018
1093
 
package/dist/cli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ installLifecycle
4
+ } from "./chunk-5KU3QS6A.js";
2
5
  import {
3
6
  ConveyorConnection
4
- } from "./chunk-XLDG5QEX.js";
7
+ } from "./chunk-77XLXIFP.js";
5
8
 
6
9
  // src/cli.ts
7
10
  import { createRequire } from "module";
@@ -69,7 +72,7 @@ function registerProjectTools(server2, conn2) {
69
72
  // src/tools/connection.ts
70
73
  import { z as z4 } from "zod";
71
74
 
72
- // ../shared/dist/chunk-42BS7Y35.js
75
+ // ../shared/dist/chunk-6NPOGQPP.js
73
76
  import { z as z2 } from "zod";
74
77
  var DEFAULT_SONNET_MODEL = "claude-sonnet-5";
75
78
  var DEFAULT_OPUS_MODEL = "claude-opus-5";
@@ -303,14 +306,14 @@ var f = {
303
306
  return { kind: "nullable", inner };
304
307
  }
305
308
  };
306
- function compileString(z11, spec) {
307
- let schema = z11.string();
309
+ function compileString(z13, spec) {
310
+ let schema = z13.string();
308
311
  if (spec.min !== void 0) schema = schema.min(spec.min);
309
312
  if (spec.max !== void 0) schema = schema.max(spec.max);
310
313
  return schema;
311
314
  }
312
- function compileNumber(z11, spec) {
313
- let schema = z11.number();
315
+ function compileNumber(z13, spec) {
316
+ let schema = z13.number();
314
317
  if (spec.int) schema = schema.int();
315
318
  if (spec.positive) schema = schema.positive();
316
319
  if (spec.nonnegative) schema = schema.nonnegative();
@@ -318,49 +321,49 @@ function compileNumber(z11, spec) {
318
321
  if (spec.max !== void 0) schema = schema.max(spec.max);
319
322
  return schema;
320
323
  }
321
- function compileArray(z11, spec) {
322
- let schema = z11.array(compileField(z11, spec.item));
324
+ function compileArray(z13, spec) {
325
+ let schema = z13.array(compileField(z13, spec.item));
323
326
  if (spec.min !== void 0) schema = schema.min(spec.min);
324
327
  return schema;
325
328
  }
326
- function compileBase(z11, spec) {
329
+ function compileBase(z13, spec) {
327
330
  switch (spec.kind) {
328
331
  case "string":
329
- return compileString(z11, spec);
332
+ return compileString(z13, spec);
330
333
  case "number":
331
- return compileNumber(z11, spec);
334
+ return compileNumber(z13, spec);
332
335
  case "boolean":
333
- return z11.boolean();
336
+ return z13.boolean();
334
337
  case "enum":
335
- return z11.enum([...spec.values]);
338
+ return z13.enum([...spec.values]);
336
339
  case "array":
337
- return compileArray(z11, spec);
340
+ return compileArray(z13, spec);
338
341
  case "object":
339
- return z11.object(compileShape(z11, spec.fields));
342
+ return z13.object(compileShape(z13, spec.fields));
340
343
  }
341
344
  }
342
345
  function descriptionOf(spec) {
343
346
  if (spec.kind === "optional" || spec.kind === "nullable") return descriptionOf(spec.inner);
344
347
  return spec.desc;
345
348
  }
346
- function compileUndescribed(z11, spec) {
349
+ function compileUndescribed(z13, spec) {
347
350
  if (spec.kind === "optional") {
348
- return compileUndescribed(z11, spec.inner).optional();
351
+ return compileUndescribed(z13, spec.inner).optional();
349
352
  }
350
353
  if (spec.kind === "nullable") {
351
- return compileUndescribed(z11, spec.inner).nullable();
354
+ return compileUndescribed(z13, spec.inner).nullable();
352
355
  }
353
- return compileBase(z11, spec);
356
+ return compileBase(z13, spec);
354
357
  }
355
- function compileField(z11, spec) {
356
- const schema = compileUndescribed(z11, spec);
358
+ function compileField(z13, spec) {
359
+ const schema = compileUndescribed(z13, spec);
357
360
  const desc = descriptionOf(spec);
358
361
  return desc === void 0 ? schema : schema.describe(desc);
359
362
  }
360
- function compileShape(z11, fields) {
363
+ function compileShape(z13, fields) {
361
364
  const shape = {};
362
365
  for (const [key, spec] of Object.entries(fields)) {
363
- shape[key] = compileField(z11, spec);
366
+ shape[key] = compileField(z13, spec);
364
367
  }
365
368
  return shape;
366
369
  }
@@ -473,7 +476,7 @@ var SEARCH_STATUSES = [
473
476
  "Complete",
474
477
  "Cancelled"
475
478
  ];
476
- var SEARCH_CARD_TYPES = ["task", "incident", "suggestion"];
479
+ var SEARCH_CARD_TYPES = ["task", "incident", "suggestion", "decision"];
477
480
  var searchFilterFields = {
478
481
  tagNames: f.optional(
479
482
  f.array(f.string(), {
@@ -1166,6 +1169,235 @@ var createSuggestionContract = defineToolContract({
1166
1169
  }
1167
1170
  });
1168
1171
  var suggestionsContracts = [createSuggestionContract];
1172
+ var OPTION_KINDS = ["current", "planned", "proposed"];
1173
+ var STATUS_LABELS = ["Open", "Decided", "Cancelled"];
1174
+ var optionDescription = "What this option means in the product, written as GIVEN / WHEN / THEN. Concrete enough that a reader six months from now can tell which behaviour was chosen.";
1175
+ var optionsDesc = "Two or three options. More than three means the question is not framed yet \u2014 narrow it or split it. Mark the option describing what the code does today as kind: current.";
1176
+ var defaultOptionDesc = "1-based position of the option that wins if nobody decides by the deadline. Required: silence has to become a decision, never a stall.";
1177
+ var agentOption = f.object(
1178
+ {
1179
+ label: f.string({ min: 1, desc: "Short name for the option, under 80 characters" }),
1180
+ description: f.string({ min: 1, desc: optionDescription }),
1181
+ kind: f.optional(
1182
+ f.enum(OPTION_KINDS, {
1183
+ desc: 'current = what the code does today, planned = already agreed but unbuilt, proposed = new. Defaults to "proposed".'
1184
+ })
1185
+ )
1186
+ },
1187
+ { desc: "One option people can choose" }
1188
+ );
1189
+ var createDecisionContract = defineToolContract({
1190
+ name: "create_decision",
1191
+ agent: {
1192
+ description: `Raise a product decision: a fork the plan cannot be right without. Two or three options, one default, a deadline. Run list_decisions FIRST \u2014 an answered decision is the answer, and re-asking it wastes everybody's time. Do NOT file a decision for an assumption you can simply state: an assumed behaviour belongs in the plan as a GIVEN / WHEN / THEN line under "Assumed behaviours". Returns the decision's id, slug, and url.`,
1193
+ fields: {
1194
+ question: f.string({
1195
+ min: 1,
1196
+ desc: "The question, as one plain sentence ending in a question mark"
1197
+ }),
1198
+ summary: f.optional(
1199
+ f.string({
1200
+ desc: cardDescriptionDesc("One or two sentences of context for the card face")
1201
+ })
1202
+ ),
1203
+ context: f.string({
1204
+ min: 1,
1205
+ desc: "Markdown background: why this is being asked now, and what breaks if it is answered wrongly"
1206
+ }),
1207
+ options: f.array(agentOption, { min: 2, desc: optionsDesc }),
1208
+ default_option: f.number({ int: true, min: 1, max: 3, desc: defaultOptionDesc }),
1209
+ deadline_hours: f.optional(
1210
+ f.number({ min: 1, max: 720, desc: "Hours until the default applies. Default 72." })
1211
+ ),
1212
+ impacted_cards: f.optional(
1213
+ f.array(f.string(), {
1214
+ desc: "Slugs or ids of cards this decision blocks. They cannot start until it is settled, and they are un-blocked the moment it is. Blocking a card needs Moderate access: without it the decision is still raised and each ref comes back under refused."
1215
+ })
1216
+ ),
1217
+ include_current_card: f.optional(
1218
+ f.boolean({
1219
+ desc: "Also block the card this session is working on. Defaults to true \u2014 you are raising this because your own plan is stuck on it. Pass false to raise the question without stopping your own work."
1220
+ })
1221
+ ),
1222
+ tag_names: f.optional(
1223
+ f.array(f.string(), {
1224
+ desc: "Tag names. The settled decision is written into each tag's overview, which is how the next agent finds it."
1225
+ })
1226
+ ),
1227
+ raised_by_pr: f.optional(
1228
+ f.number({ int: true, min: 1, desc: "PR number that surfaced the question" })
1229
+ )
1230
+ }
1231
+ },
1232
+ mcp: {
1233
+ description: "Raise a product decision on a Conveyor project: a fork that has to be settled before the work can be planned correctly. Two or three options, one default that applies if nobody decides, and a deadline. Search list_decisions first \u2014 a Decided decision is the answer. Pass projectId to target a specific project; otherwise the configured default project is used.",
1234
+ fields: {
1235
+ projectId: mcpProjectId,
1236
+ question: f.string({ desc: "The question, as one plain sentence" }),
1237
+ summary: f.optional(f.string({ desc: cardDescriptionDesc("Context for the card face") })),
1238
+ context: f.string({ desc: "Markdown background: why this is being asked now" }),
1239
+ options: f.array(agentOption, { min: 2, desc: optionsDesc }),
1240
+ defaultOption: f.number({ int: true, min: 1, max: 3, desc: defaultOptionDesc }),
1241
+ deadlineHours: f.optional(
1242
+ f.number({ min: 1, max: 720, desc: "Hours until the default applies. Default 72." })
1243
+ ),
1244
+ impactedCards: f.optional(
1245
+ f.array(f.string(), {
1246
+ desc: "Slugs or ids of cards this decision blocks. Needs Moderate access; without it each ref comes back under refused and the decision is still raised."
1247
+ })
1248
+ ),
1249
+ tagNames: f.optional(f.array(f.string(), { desc: "Tag names to categorize the decision" })),
1250
+ raisedByPr: f.optional(
1251
+ f.number({ int: true, min: 1, desc: "PR number that surfaced the question" })
1252
+ )
1253
+ }
1254
+ }
1255
+ });
1256
+ var listDecisionsContract = defineToolContract({
1257
+ name: "list_decisions",
1258
+ agent: {
1259
+ description: `Search this project's decisions. Run this BEFORE planning anything in an area: "have we already decided how X behaves?" Decided rows carry their resolution, so the list doubles as the decision log. Filter by card to see what is blocking a specific card.`,
1260
+ fields: {
1261
+ status: f.optional(
1262
+ f.enum(STATUS_LABELS, {
1263
+ desc: "Open = still being voted on, Decided = settled, Cancelled = dropped"
1264
+ })
1265
+ ),
1266
+ tag: f.optional(f.string({ desc: 'Tag name, e.g. "slack"' })),
1267
+ card: f.optional(
1268
+ f.string({ desc: "Slug or id of a card \u2014 returns the decisions it is blocked by" })
1269
+ ),
1270
+ query: f.optional(f.string({ desc: "Text search on the question" })),
1271
+ limit: f.optional(f.number({ desc: "Max rows to return (default 50, max 200)" }))
1272
+ }
1273
+ },
1274
+ mcp: {
1275
+ description: `List and search a Conveyor project's decisions. Decided rows include their resolution, so this is the fastest way to answer "did we already settle this?". Pass projectId to target a specific project; otherwise the configured default project is used.`,
1276
+ fields: {
1277
+ projectId: mcpProjectId,
1278
+ status: f.optional(f.enum(STATUS_LABELS, { desc: "Filter by state" })),
1279
+ tag: f.optional(f.string({ desc: "Tag name to filter by" })),
1280
+ card: f.optional(f.string({ desc: "Card slug or id \u2014 decisions blocking that card" })),
1281
+ query: f.optional(f.string({ desc: "Text search on the question" })),
1282
+ limit: f.optional(
1283
+ f.number({ int: true, min: 1, max: 200, desc: "Max rows to return (default 50)" })
1284
+ )
1285
+ }
1286
+ }
1287
+ });
1288
+ var getDecisionContract = defineToolContract({
1289
+ name: "get_decision",
1290
+ agent: {
1291
+ description: "Read one decision in full: its context, every option with its GIVEN / WHEN / THEN, every vote with who cast it and why, the running tally, the cards it blocks, and the resolution once there is one. Use read_task_chat with the same id for the discussion thread.",
1292
+ fields: {
1293
+ decision: f.string({ min: 1, desc: "Decision slug or id" })
1294
+ }
1295
+ },
1296
+ mcp: {
1297
+ description: "Read one Conveyor decision in full \u2014 context, options, votes with reasons, tally, impacted cards, and the resolution. Pass projectId to disambiguate a slug; otherwise the configured default project is used.",
1298
+ fields: {
1299
+ projectId: mcpProjectId,
1300
+ decision: f.string({ desc: "Decision slug or id" })
1301
+ }
1302
+ }
1303
+ });
1304
+ var voteDecisionContract = defineToolContract({
1305
+ name: "vote_decision",
1306
+ agent: {
1307
+ description: "Vote on an open decision. One vote per voter: voting again replaces your previous one. Always give a reason \u2014 the resolution weighs reasons over counts, and a vote with no reason carries almost nothing.",
1308
+ fields: {
1309
+ decision: f.string({ min: 1, desc: "Decision slug or id" }),
1310
+ option: f.string({ min: 1, desc: "Option id, or its 1-based position" }),
1311
+ reason: f.optional(
1312
+ f.string({ desc: "Why this option, in one or two sentences (max 255 characters)" })
1313
+ )
1314
+ }
1315
+ },
1316
+ mcp: {
1317
+ description: "Vote on an open Conveyor decision, with a reason. One vote per voter \u2014 voting again replaces the previous one.",
1318
+ fields: {
1319
+ projectId: mcpProjectId,
1320
+ decision: f.string({ desc: "Decision slug or id" }),
1321
+ option: f.string({ desc: "Option id, or its 1-based position" }),
1322
+ reason: f.optional(f.string({ desc: "Why this option (max 255 characters)" }))
1323
+ }
1324
+ }
1325
+ });
1326
+ var resolveDecisionContract = defineToolContract({
1327
+ name: "resolve_decision",
1328
+ agent: {
1329
+ description: "Settle a decision. This is not a formality: the resolution is written into the plan of every card the decision blocked, into each of their chats, and into the overview of every tag it carries, and those cards become startable. Say what was decided in one or two sentences \u2014 a reader months later should not need to open the thread.",
1330
+ fields: {
1331
+ decision: f.string({ min: 1, desc: "Decision slug or id" }),
1332
+ option: f.string({ min: 1, desc: "Winning option id, or its 1-based position" }),
1333
+ resolution: f.string({
1334
+ min: 1,
1335
+ desc: "What was decided, in one or two plain sentences (max 255 characters)"
1336
+ }),
1337
+ notes: f.optional(
1338
+ f.string({ desc: "Markdown reasoning. Link any verdict attachment here." })
1339
+ )
1340
+ }
1341
+ },
1342
+ mcp: {
1343
+ description: "Settle a Conveyor decision. The resolution is written into every impacted card's plan and chat and into its tags' overviews, and the impacted cards become startable.",
1344
+ fields: {
1345
+ projectId: mcpProjectId,
1346
+ decision: f.string({ desc: "Decision slug or id" }),
1347
+ option: f.string({ desc: "Winning option id, or its 1-based position" }),
1348
+ resolution: f.string({ desc: "What was decided (max 255 characters)" }),
1349
+ notes: f.optional(f.string({ desc: "Markdown reasoning" }))
1350
+ }
1351
+ }
1352
+ });
1353
+ var updateDecisionContract = defineToolContract({
1354
+ name: "update_decision",
1355
+ agent: {
1356
+ description: "Change an open decision: extend its deadline, link or unlink the cards it blocks, or cancel it. Cancelling un-blocks the impacted cards without answering the question, so only do it when the question stopped mattering.",
1357
+ fields: {
1358
+ decision: f.string({ min: 1, desc: "Decision slug or id" }),
1359
+ deadline_hours: f.optional(f.number({ desc: "New deadline, in hours from now. 1 to 720." })),
1360
+ add_impacted_cards: f.optional(
1361
+ f.array(f.string(), { desc: "Slugs or ids of cards to block on this decision" })
1362
+ ),
1363
+ remove_impacted_cards: f.optional(
1364
+ f.array(f.string(), { desc: "Ids of cards to stop blocking" })
1365
+ ),
1366
+ status: f.optional(
1367
+ f.enum(["Cancelled", "Open"], {
1368
+ desc: "Cancelled drops the question; Open reopens a settled one and clears its answer"
1369
+ })
1370
+ )
1371
+ }
1372
+ },
1373
+ mcp: {
1374
+ description: "Change an open Conveyor decision: extend the deadline, link or unlink impacted cards, cancel it, or reopen a settled one.",
1375
+ fields: {
1376
+ projectId: mcpProjectId,
1377
+ decision: f.string({ desc: "Decision slug or id" }),
1378
+ deadlineHours: f.optional(
1379
+ f.number({ min: 1, max: 720, desc: "New deadline, in hours from now" })
1380
+ ),
1381
+ addImpactedCards: f.optional(
1382
+ f.array(f.string(), { desc: "Slugs or ids of cards to block on this decision" })
1383
+ ),
1384
+ removeImpactedCards: f.optional(
1385
+ f.array(f.string(), { desc: "Ids of cards to stop blocking" })
1386
+ ),
1387
+ status: f.optional(
1388
+ f.enum(["Cancelled", "Open"], { desc: "Cancel it, or reopen a settled one" })
1389
+ )
1390
+ }
1391
+ }
1392
+ });
1393
+ var decisionsContracts = [
1394
+ createDecisionContract,
1395
+ listDecisionsContract,
1396
+ getDecisionContract,
1397
+ voteDecisionContract,
1398
+ resolveDecisionContract,
1399
+ updateDecisionContract
1400
+ ];
1169
1401
  var createPullRequestContract = defineToolContract({
1170
1402
  name: "create_pull_request",
1171
1403
  agent: {
@@ -1826,6 +2058,7 @@ var TOOL_CONTRACTS = Object.fromEntries(
1826
2058
  ...subtasksContracts,
1827
2059
  ...attachmentsContracts,
1828
2060
  ...suggestionsContracts,
2061
+ ...decisionsContracts,
1829
2062
  ...pullRequestContracts,
1830
2063
  ...integrationsContracts,
1831
2064
  ...driveContracts,
@@ -3309,6 +3542,8 @@ import { z as z72 } from "zod";
3309
3542
  import { z as z82 } from "zod";
3310
3543
  import { z as z92 } from "zod";
3311
3544
  import { z as z102 } from "zod";
3545
+ import { z as z11 } from "zod";
3546
+ import { z as z12 } from "zod";
3312
3547
  var ACHIEVEMENT_RARITIES = [
3313
3548
  {
3314
3549
  key: "common",
@@ -4797,135 +5032,291 @@ var DeleteTaskAuditRequestSchema = z92.object({
4797
5032
  var MarkInitialPromptSubmittedRequestSchema = z92.object({
4798
5033
  sessionId: z92.string()
4799
5034
  });
5035
+ var DECISION_STATUS_LABELS = ["Open", "Decided", "Cancelled"];
5036
+ var DECISION_OPTION_KINDS = ["current", "planned", "proposed"];
5037
+ var DECISION_VOTE_SOURCES = ["web", "slack", "discord", "agent"];
5038
+ var DECISION_OPTIONS_MIN = 2;
5039
+ var DECISION_OPTIONS_MAX = 3;
5040
+ var DECISION_OPTION_LABEL_MAX = 80;
5041
+ var DECISION_QUESTION_MAX = 200;
5042
+ var DECISION_RESOLUTION_MAX = CARD_DESCRIPTION_MAX;
5043
+ var DECISION_VOTE_REASON_MAX = CARD_DESCRIPTION_MAX;
5044
+ var DECISION_DEADLINE_MIN_HOURS = 1;
5045
+ var DECISION_DEADLINE_MAX_DAYS = 30;
5046
+ var DECISION_DEADLINE_MAX_HOURS = DECISION_DEADLINE_MAX_DAYS * 24;
5047
+ var DECISION_LIST_MAX_LIMIT = 200;
5048
+ var decisionIdSchema = z102.string().cuid("Invalid decision ID");
5049
+ var optionInputSchema = z102.object({
5050
+ label: z102.string().min(1, "Option label is required").max(DECISION_OPTION_LABEL_MAX),
5051
+ description: z102.string().min(1, "Describe the option as GIVEN / WHEN / THEN"),
5052
+ kind: z102.enum(DECISION_OPTION_KINDS).optional()
5053
+ });
5054
+ var optionsArraySchema = z102.array(optionInputSchema).min(DECISION_OPTIONS_MIN, `A decision needs at least ${DECISION_OPTIONS_MIN} options`).max(
5055
+ DECISION_OPTIONS_MAX,
5056
+ `A decision takes at most ${DECISION_OPTIONS_MAX} options \u2014 more than that means the question is not framed yet`
5057
+ );
5058
+ var CreateDecisionRequestSchema = z102.object({
5059
+ projectId: z102.string().cuid("Invalid project ID"),
5060
+ /** The question, as one plain sentence. */
5061
+ title: z102.string().min(1, "A decision needs a question").max(DECISION_QUESTION_MAX),
5062
+ description: z102.string().max(CARD_DESCRIPTION_MAX).optional(),
5063
+ context: z102.string().min(1, "Context is required \u2014 say why this is being asked now"),
5064
+ options: optionsArraySchema,
5065
+ /** 1-based position of the option that wins on silence. */
5066
+ defaultOptionOrdinal: z102.number().int().min(1).max(DECISION_OPTIONS_MAX),
5067
+ deadlineHours: z102.number().min(DECISION_DEADLINE_MIN_HOURS).max(DECISION_DEADLINE_MAX_HOURS).optional(),
5068
+ /** Cards this decision blocks, by cuid or slug. */
5069
+ impactedCardRefs: z102.array(z102.string().min(1)).max(50).optional(),
5070
+ tagNames: z102.array(z102.string().min(1)).max(10).optional(),
5071
+ raisedByTaskId: z102.string().min(1).optional(),
5072
+ raisedByPrNumber: z102.number().int().positive().optional()
5073
+ });
5074
+ var VoteDecisionRequestSchema = z102.object({
5075
+ decisionId: decisionIdSchema,
5076
+ optionId: z102.string().cuid("Invalid option ID"),
5077
+ reason: z102.string().max(DECISION_VOTE_REASON_MAX).optional(),
5078
+ source: z102.enum(DECISION_VOTE_SOURCES).optional()
5079
+ });
5080
+ var RemoveDecisionVoteRequestSchema = z102.object({
5081
+ decisionId: decisionIdSchema
5082
+ });
5083
+ var ResolveDecisionRequestSchema = z102.object({
5084
+ decisionId: decisionIdSchema,
5085
+ chosenOptionId: z102.string().cuid("Invalid option ID"),
5086
+ resolution: z102.string().min(1, "A resolution is required \u2014 say what was decided").max(DECISION_RESOLUTION_MAX),
5087
+ resolutionNotes: z102.string().optional()
5088
+ });
5089
+ var CancelDecisionRequestSchema = z102.object({
5090
+ decisionId: decisionIdSchema,
5091
+ /** Why it is being dropped. Posted to the decision thread. */
5092
+ reason: z102.string().max(DECISION_RESOLUTION_MAX).optional()
5093
+ });
5094
+ var ReopenDecisionRequestSchema = z102.object({
5095
+ decisionId: decisionIdSchema,
5096
+ deadlineHours: z102.number().min(DECISION_DEADLINE_MIN_HOURS).max(DECISION_DEADLINE_MAX_HOURS).optional()
5097
+ });
5098
+ var UpdateDecisionRequestSchema = z102.object({
5099
+ decisionId: decisionIdSchema,
5100
+ title: z102.string().min(1).max(200).optional(),
5101
+ description: z102.string().max(CARD_DESCRIPTION_MAX).optional(),
5102
+ context: z102.string().min(1).optional(),
5103
+ deadlineAt: z102.string().datetime().optional(),
5104
+ /** Only accepted while the decision has zero votes. */
5105
+ options: optionsArraySchema.optional(),
5106
+ defaultOptionOrdinal: z102.number().int().min(1).max(DECISION_OPTIONS_MAX).optional(),
5107
+ addImpactedCardRefs: z102.array(z102.string().min(1)).max(50).optional(),
5108
+ removeImpactedCardIds: z102.array(z102.string().min(1)).max(50).optional()
5109
+ });
5110
+ var GetDecisionRequestSchema = z102.object({
5111
+ /** cuid or slug. A slug needs `projectId` to resolve unambiguously. */
5112
+ decisionId: z102.string().min(1),
5113
+ projectId: z102.string().cuid().optional()
5114
+ });
5115
+ var ListDecisionsRequestSchema = z102.object({
5116
+ projectId: z102.string().cuid("Invalid project ID"),
5117
+ status: z102.enum(DECISION_STATUS_LABELS).optional(),
5118
+ /** Tag name, not id — an agent knows the tag it is working under. */
5119
+ tag: z102.string().min(1).optional(),
5120
+ /** Decisions that this card is blocked by (cuid or slug). */
5121
+ cardId: z102.string().min(1).optional(),
5122
+ query: z102.string().min(1).optional(),
5123
+ limit: z102.number().int().min(1).max(DECISION_LIST_MAX_LIMIT).optional()
5124
+ });
5125
+ var optionInput = z11.object({
5126
+ label: z11.string().min(1).max(DECISION_OPTION_LABEL_MAX),
5127
+ description: z11.string().min(1),
5128
+ kind: z11.enum(DECISION_OPTION_KINDS).optional()
5129
+ });
5130
+ var optionsInput = z11.array(optionInput).min(DECISION_OPTIONS_MIN).max(DECISION_OPTIONS_MAX);
5131
+ var deadlineHours = z11.number().min(DECISION_DEADLINE_MIN_HOURS).max(DECISION_DEADLINE_MAX_HOURS).optional();
5132
+ var CreateProjectDecisionRequestSchema = z11.object({
5133
+ projectId: z11.string(),
5134
+ question: z11.string().min(1).max(200),
5135
+ summary: z11.string().max(CARD_DESCRIPTION_MAX).optional(),
5136
+ context: z11.string().min(1),
5137
+ options: optionsInput,
5138
+ defaultOption: z11.number().int().min(1).max(DECISION_OPTIONS_MAX),
5139
+ deadlineHours,
5140
+ impactedCards: z11.array(z11.string().min(1)).max(50).optional(),
5141
+ /**
5142
+ * Pod surface only: also block the card this session is working on. The
5143
+ * session's task id is resolved SERVER-side from `sessionId` — a
5144
+ * client-supplied card id would let a pod block any card in the project.
5145
+ */
5146
+ includeCurrentCard: z11.boolean().optional(),
5147
+ sessionId: z11.string().optional(),
5148
+ tagNames: z11.array(z11.string().min(1)).max(10).optional(),
5149
+ raisedByPr: z11.number().int().positive().optional(),
5150
+ requestingUserId: z11.string().optional()
5151
+ });
5152
+ var ListProjectDecisionsRequestSchema = z11.object({
5153
+ projectId: z11.string(),
5154
+ status: z11.enum(DECISION_STATUS_LABELS).optional(),
5155
+ tag: z11.string().min(1).optional(),
5156
+ card: z11.string().min(1).optional(),
5157
+ query: z11.string().min(1).optional(),
5158
+ limit: z11.number().int().min(1).max(DECISION_LIST_MAX_LIMIT).optional(),
5159
+ requestingUserId: z11.string().optional()
5160
+ });
5161
+ var GetProjectDecisionRequestSchema = z11.object({
5162
+ projectId: z11.string(),
5163
+ decision: z11.string().min(1),
5164
+ requestingUserId: z11.string().optional()
5165
+ });
5166
+ var VoteProjectDecisionRequestSchema = z11.object({
5167
+ projectId: z11.string(),
5168
+ decision: z11.string().min(1),
5169
+ /** An option id, or its 1-based position. */
5170
+ option: z11.string().min(1),
5171
+ reason: z11.string().max(CARD_DESCRIPTION_MAX).optional(),
5172
+ requestingUserId: z11.string().optional()
5173
+ });
5174
+ var ResolveProjectDecisionRequestSchema = z11.object({
5175
+ projectId: z11.string(),
5176
+ decision: z11.string().min(1),
5177
+ option: z11.string().min(1),
5178
+ resolution: z11.string().min(1).max(DECISION_RESOLUTION_MAX),
5179
+ notes: z11.string().optional(),
5180
+ requestingUserId: z11.string().optional()
5181
+ });
5182
+ var UpdateProjectDecisionRequestSchema = z11.object({
5183
+ projectId: z11.string(),
5184
+ decision: z11.string().min(1),
5185
+ deadlineHours,
5186
+ addImpactedCards: z11.array(z11.string().min(1)).max(50).optional(),
5187
+ removeImpactedCards: z11.array(z11.string().min(1)).max(50).optional(),
5188
+ status: z11.enum(["Cancelled", "Open"]).optional(),
5189
+ requestingUserId: z11.string().optional()
5190
+ });
4800
5191
  var MEETING_CHECKLIST_TITLE_MAX = 300;
4801
5192
  var MEETING_TRANSCRIPT_MAX_CHARS = 2e6;
4802
5193
  var MEETING_TITLE_MAX = 200;
4803
5194
  var MEETING_OCCURRED_AT_MIN_YEAR = 2e3;
4804
5195
  var MEETING_OCCURRED_AT_MAX_FUTURE_MS = 48 * 60 * 60 * 1e3;
4805
5196
  var OCCURRED_AT_RANGE_MESSAGE = `occurredAt must be a real date: no earlier than ${MEETING_OCCURRED_AT_MIN_YEAR}, and no more than 48 hours in the future.`;
4806
- var MeetingOccurredAtSchema = z102.string().datetime().refine((value) => {
5197
+ var MeetingOccurredAtSchema = z12.string().datetime().refine((value) => {
4807
5198
  const ms = Date.parse(value);
4808
5199
  if (Number.isNaN(ms)) return false;
4809
5200
  if (ms > Date.now() + MEETING_OCCURRED_AT_MAX_FUTURE_MS) return false;
4810
5201
  return new Date(ms).getUTCFullYear() >= MEETING_OCCURRED_AT_MIN_YEAR;
4811
5202
  }, OCCURRED_AT_RANGE_MESSAGE);
4812
- var CreateMeetingFromTranscriptRequestSchema = z102.object({
4813
- projectId: z102.string().cuid(),
4814
- rawText: z102.string().min(1).max(MEETING_TRANSCRIPT_MAX_CHARS),
4815
- title: z102.string().min(1).max(MEETING_TITLE_MAX).optional(),
5203
+ var CreateMeetingFromTranscriptRequestSchema = z12.object({
5204
+ projectId: z12.string().cuid(),
5205
+ rawText: z12.string().min(1).max(MEETING_TRANSCRIPT_MAX_CHARS),
5206
+ title: z12.string().min(1).max(MEETING_TITLE_MAX).optional(),
4816
5207
  /** ISO 8601. Defaults to now when the source carries no date. */
4817
5208
  occurredAt: MeetingOccurredAtSchema.optional(),
4818
5209
  /** Override auto-detection. Rarely needed; detection handles the three formats. */
4819
- format: z102.enum(["text", "vtt", "srt"]).optional(),
4820
- source: z102.enum(["manual", "slack"]).optional()
5210
+ format: z12.enum(["text", "vtt", "srt"]).optional(),
5211
+ source: z12.enum(["manual", "slack"]).optional()
4821
5212
  });
4822
- var GetMeetingRequestSchema = z102.object({
4823
- projectId: z102.string().cuid(),
4824
- meetingId: z102.string().cuid()
5213
+ var GetMeetingRequestSchema = z12.object({
5214
+ projectId: z12.string().cuid(),
5215
+ meetingId: z12.string().cuid()
4825
5216
  });
4826
- var UpdateMeetingRequestSchema = z102.object({
4827
- projectId: z102.string().cuid(),
4828
- meetingId: z102.string().cuid(),
4829
- title: z102.string().min(1).max(MEETING_TITLE_MAX).optional(),
5217
+ var UpdateMeetingRequestSchema = z12.object({
5218
+ projectId: z12.string().cuid(),
5219
+ meetingId: z12.string().cuid(),
5220
+ title: z12.string().min(1).max(MEETING_TITLE_MAX).optional(),
4830
5221
  occurredAt: MeetingOccurredAtSchema.optional()
4831
5222
  });
4832
- var RegenerateMeetingSummaryRequestSchema = z102.object({
4833
- projectId: z102.string().cuid(),
4834
- meetingId: z102.string().cuid()
5223
+ var RegenerateMeetingSummaryRequestSchema = z12.object({
5224
+ projectId: z12.string().cuid(),
5225
+ meetingId: z12.string().cuid()
4835
5226
  });
4836
- var DeleteMeetingRequestSchema = z102.object({
4837
- projectId: z102.string().cuid(),
4838
- meetingId: z102.string().cuid()
5227
+ var DeleteMeetingRequestSchema = z12.object({
5228
+ projectId: z12.string().cuid(),
5229
+ meetingId: z12.string().cuid()
4839
5230
  });
4840
- var checklistTitle = z102.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX);
4841
- var ListMeetingChecklistRequestSchema = z102.object({
4842
- projectId: z102.string().cuid(),
4843
- meetingId: z102.string().cuid()
5231
+ var checklistTitle = z12.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX);
5232
+ var ListMeetingChecklistRequestSchema = z12.object({
5233
+ projectId: z12.string().cuid(),
5234
+ meetingId: z12.string().cuid()
4844
5235
  });
4845
- var AddMeetingChecklistItemsRequestSchema = z102.object({
4846
- projectId: z102.string().cuid(),
4847
- meetingId: z102.string().cuid(),
4848
- items: z102.array(z102.object({ title: checklistTitle })).min(1).max(50)
5236
+ var AddMeetingChecklistItemsRequestSchema = z12.object({
5237
+ projectId: z12.string().cuid(),
5238
+ meetingId: z12.string().cuid(),
5239
+ items: z12.array(z12.object({ title: checklistTitle })).min(1).max(50)
4849
5240
  });
4850
- var UpdateMeetingChecklistItemRequestSchema = z102.object({
4851
- projectId: z102.string().cuid(),
4852
- meetingId: z102.string().cuid(),
4853
- itemId: z102.string().cuid(),
5241
+ var UpdateMeetingChecklistItemRequestSchema = z12.object({
5242
+ projectId: z12.string().cuid(),
5243
+ meetingId: z12.string().cuid(),
5244
+ itemId: z12.string().cuid(),
4854
5245
  title: checklistTitle.optional(),
4855
- ordinal: z102.number().int().min(0).optional(),
5246
+ ordinal: z12.number().int().min(0).optional(),
4856
5247
  /** Explicit null clears the link; undefined leaves it alone. */
4857
- linkedTaskId: z102.string().cuid().nullable().optional()
5248
+ linkedTaskId: z12.string().cuid().nullable().optional()
4858
5249
  }).refine(
4859
5250
  (v) => v.title !== void 0 || v.ordinal !== void 0 || v.linkedTaskId !== void 0,
4860
5251
  "Pass at least one of title, ordinal, or linkedTaskId."
4861
5252
  );
4862
- var DeleteMeetingChecklistItemRequestSchema = z102.object({
4863
- projectId: z102.string().cuid(),
4864
- meetingId: z102.string().cuid(),
4865
- itemId: z102.string().cuid()
4866
- });
4867
- var SetMeetingChecklistItemCheckedRequestSchema = z102.object({
4868
- projectId: z102.string().cuid(),
4869
- meetingId: z102.string().cuid(),
4870
- itemId: z102.string().cuid(),
4871
- checked: z102.boolean(),
5253
+ var DeleteMeetingChecklistItemRequestSchema = z12.object({
5254
+ projectId: z12.string().cuid(),
5255
+ meetingId: z12.string().cuid(),
5256
+ itemId: z12.string().cuid()
5257
+ });
5258
+ var SetMeetingChecklistItemCheckedRequestSchema = z12.object({
5259
+ projectId: z12.string().cuid(),
5260
+ meetingId: z12.string().cuid(),
5261
+ itemId: z12.string().cuid(),
5262
+ checked: z12.boolean(),
4872
5263
  /** Attach the card in the same call that ticks the item. */
4873
- linkedTaskId: z102.string().cuid().nullable().optional()
5264
+ linkedTaskId: z12.string().cuid().nullable().optional()
4874
5265
  });
4875
- var ListMeetingsRequestSchema = z102.object({
4876
- projectId: z102.string().cuid(),
4877
- limit: z102.number().int().min(1).max(50).optional(),
4878
- search: z102.string().max(200).optional()
5266
+ var ListMeetingsRequestSchema = z12.object({
5267
+ projectId: z12.string().cuid(),
5268
+ limit: z12.number().int().min(1).max(50).optional(),
5269
+ search: z12.string().max(200).optional()
4879
5270
  });
4880
- var ReadMeetingTranscriptRequestSchema = z102.object({
4881
- projectId: z102.string().cuid(),
4882
- meetingId: z102.string().cuid(),
4883
- offset: z102.number().int().min(0).optional(),
4884
- limit: z102.number().int().min(1).max(500).optional()
5271
+ var ReadMeetingTranscriptRequestSchema = z12.object({
5272
+ projectId: z12.string().cuid(),
5273
+ meetingId: z12.string().cuid(),
5274
+ offset: z12.number().int().min(0).optional(),
5275
+ limit: z12.number().int().min(1).max(500).optional()
4885
5276
  });
4886
5277
  var MEETING_SUMMARY_MAX_CHARS = 5e4;
4887
- var AddProjectMeetingChecklistItemsRequestSchema = z102.object({
4888
- projectId: z102.string().cuid(),
4889
- meetingId: z102.string().cuid(),
4890
- items: z102.array(z102.object({ title: z102.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX) })).min(1).max(50),
4891
- requestingUserId: z102.string().optional()
4892
- });
4893
- var CheckProjectMeetingChecklistItemRequestSchema = z102.object({
4894
- projectId: z102.string().cuid(),
4895
- meetingId: z102.string().cuid(),
4896
- title: z102.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
4897
- checked: z102.boolean(),
5278
+ var AddProjectMeetingChecklistItemsRequestSchema = z12.object({
5279
+ projectId: z12.string().cuid(),
5280
+ meetingId: z12.string().cuid(),
5281
+ items: z12.array(z12.object({ title: z12.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX) })).min(1).max(50),
5282
+ requestingUserId: z12.string().optional()
5283
+ });
5284
+ var CheckProjectMeetingChecklistItemRequestSchema = z12.object({
5285
+ projectId: z12.string().cuid(),
5286
+ meetingId: z12.string().cuid(),
5287
+ title: z12.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
5288
+ checked: z12.boolean(),
4898
5289
  /** Card id or slug. Resolved server-side and required to be in the project. */
4899
- linkedTask: z102.string().min(1).optional(),
4900
- requestingUserId: z102.string().optional()
4901
- });
4902
- var EditProjectMeetingChecklistItemRequestSchema = z102.object({
4903
- projectId: z102.string().cuid(),
4904
- meetingId: z102.string().cuid(),
4905
- title: z102.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
4906
- newTitle: z102.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
4907
- requestingUserId: z102.string().optional()
4908
- });
4909
- var RemoveProjectMeetingChecklistItemRequestSchema = z102.object({
4910
- projectId: z102.string().cuid(),
4911
- meetingId: z102.string().cuid(),
4912
- title: z102.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
4913
- requestingUserId: z102.string().optional()
4914
- });
4915
- var CreateProjectMeetingRequestSchema = z102.object({
4916
- projectId: z102.string().cuid(),
4917
- rawText: z102.string().min(1).max(MEETING_TRANSCRIPT_MAX_CHARS),
4918
- title: z102.string().min(1).max(MEETING_TITLE_MAX).optional(),
5290
+ linkedTask: z12.string().min(1).optional(),
5291
+ requestingUserId: z12.string().optional()
5292
+ });
5293
+ var EditProjectMeetingChecklistItemRequestSchema = z12.object({
5294
+ projectId: z12.string().cuid(),
5295
+ meetingId: z12.string().cuid(),
5296
+ title: z12.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
5297
+ newTitle: z12.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
5298
+ requestingUserId: z12.string().optional()
5299
+ });
5300
+ var RemoveProjectMeetingChecklistItemRequestSchema = z12.object({
5301
+ projectId: z12.string().cuid(),
5302
+ meetingId: z12.string().cuid(),
5303
+ title: z12.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
5304
+ requestingUserId: z12.string().optional()
5305
+ });
5306
+ var CreateProjectMeetingRequestSchema = z12.object({
5307
+ projectId: z12.string().cuid(),
5308
+ rawText: z12.string().min(1).max(MEETING_TRANSCRIPT_MAX_CHARS),
5309
+ title: z12.string().min(1).max(MEETING_TITLE_MAX).optional(),
4919
5310
  occurredAt: MeetingOccurredAtSchema.optional(),
4920
- requestingUserId: z102.string().optional()
5311
+ requestingUserId: z12.string().optional()
4921
5312
  });
4922
- var UpdateProjectMeetingRequestSchema = z102.object({
4923
- projectId: z102.string().cuid(),
4924
- meetingId: z102.string().cuid(),
4925
- title: z102.string().min(1).max(MEETING_TITLE_MAX).optional(),
5313
+ var UpdateProjectMeetingRequestSchema = z12.object({
5314
+ projectId: z12.string().cuid(),
5315
+ meetingId: z12.string().cuid(),
5316
+ title: z12.string().min(1).max(MEETING_TITLE_MAX).optional(),
4926
5317
  occurredAt: MeetingOccurredAtSchema.optional(),
4927
- summary: z102.string().min(1).max(MEETING_SUMMARY_MAX_CHARS).optional(),
4928
- requestingUserId: z102.string().optional()
5318
+ summary: z12.string().min(1).max(MEETING_SUMMARY_MAX_CHARS).optional(),
5319
+ requestingUserId: z12.string().optional()
4929
5320
  }).refine(
4930
5321
  (v) => v.title !== void 0 || v.occurredAt !== void 0 || v.summary !== void 0,
4931
5322
  "Pass at least one of title, occurredAt, or summary."
@@ -5262,13 +5653,17 @@ var CARD_TYPE_SURFACE = {
5262
5653
  task: "board",
5263
5654
  chat: "board",
5264
5655
  incident: "report",
5265
- suggestion: "report"
5656
+ suggestion: "report",
5657
+ // Decisions get their own surface: they are neither built on the board nor
5658
+ // triaged in the report feeds. Cards → Decisions is their only feed.
5659
+ decision: "decision"
5266
5660
  };
5267
5661
  var surfaceTypes = (surface) => Object.keys(CARD_TYPE_SURFACE).filter(
5268
5662
  (type) => CARD_TYPE_SURFACE[type] === surface
5269
5663
  );
5270
5664
  var BOARD_CARD_TYPES = surfaceTypes("board");
5271
5665
  var REPORT_CARD_TYPES = surfaceTypes("report");
5666
+ var DECISION_CARD_TYPES = surfaceTypes("decision");
5272
5667
 
5273
5668
  // src/tools/logs.ts
5274
5669
  function registerLogTools(server2, conn2) {
@@ -5449,6 +5844,83 @@ ${res.lines.join("\n")}${footer}` }] };
5449
5844
  });
5450
5845
  }
5451
5846
 
5847
+ // src/tools/decisions.ts
5848
+ function registerDecisionReads(server2, conn2) {
5849
+ registerContractTool(server2, listDecisionsContract, async (params) => {
5850
+ const res = await conn2.listDecisions(params);
5851
+ if (res.decisions.length === 0) {
5852
+ return {
5853
+ content: [
5854
+ {
5855
+ type: "text",
5856
+ text: "No decisions match. Nothing has been settled here, so the choice is open \u2014 state it as an assumption in the plan, or raise a decision with create_decision if the plan cannot be right without an answer."
5857
+ }
5858
+ ]
5859
+ };
5860
+ }
5861
+ return { content: [{ type: "text", text: JSON.stringify(res.decisions, null, 2) }] };
5862
+ });
5863
+ registerContractTool(server2, getDecisionContract, async (params) => {
5864
+ const res = await conn2.getDecision(params);
5865
+ return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }] };
5866
+ });
5867
+ }
5868
+ function registerDecisionWrites(server2, conn2) {
5869
+ registerContractTool(server2, createDecisionContract, async (params) => {
5870
+ const { defaultOption, deadlineHours: deadlineHours2, impactedCards, tagNames, raisedByPr, ...rest } = params;
5871
+ const res = await conn2.createDecision({
5872
+ ...rest,
5873
+ defaultOption,
5874
+ deadlineHours: deadlineHours2,
5875
+ impactedCards,
5876
+ tagNames,
5877
+ raisedByPr
5878
+ });
5879
+ const refused = res.refused.length > 0 ? `
5880
+ Not linked: ${res.refused.map((r) => `${r.ref} (${r.reason})`).join("; ")}` : "";
5881
+ return {
5882
+ content: [
5883
+ {
5884
+ type: "text",
5885
+ text: `Raised decision ${res.slug} \u2014 ${res.url}
5886
+ Blocking ${res.impactedLinked.length} card(s).${refused}`
5887
+ }
5888
+ ]
5889
+ };
5890
+ });
5891
+ registerContractTool(server2, voteDecisionContract, async (params) => {
5892
+ const res = await conn2.voteDecision(params);
5893
+ const tally = res.options.map((o) => `${o.label}: ${o.votes}`).join(", ");
5894
+ return { content: [{ type: "text", text: `Voted on ${res.slug}. Tally \u2014 ${tally}.` }] };
5895
+ });
5896
+ registerContractTool(server2, resolveDecisionContract, async (params) => {
5897
+ const res = await conn2.resolveDecision(params);
5898
+ return {
5899
+ content: [
5900
+ {
5901
+ type: "text",
5902
+ text: `Settled ${res.slug} as "${res.chosenOption}". ${res.impactedCount} card(s) were written back to and are no longer blocked.`
5903
+ }
5904
+ ]
5905
+ };
5906
+ });
5907
+ registerContractTool(server2, updateDecisionContract, async (params) => {
5908
+ const res = await conn2.updateDecision(params);
5909
+ return {
5910
+ content: [
5911
+ {
5912
+ type: "text",
5913
+ text: `Updated ${res.slug} \u2014 ${res.status}, ${res.impactedCount} impacted card(s), deadline ${res.deadlineAt}.`
5914
+ }
5915
+ ]
5916
+ };
5917
+ });
5918
+ }
5919
+ function registerDecisionTools(server2, conn2) {
5920
+ registerDecisionReads(server2, conn2);
5921
+ registerDecisionWrites(server2, conn2);
5922
+ }
5923
+
5452
5924
  // src/tools/index.ts
5453
5925
  function registerAllTools(server2, conn2) {
5454
5926
  registerProjectTools(server2, conn2);
@@ -5467,6 +5939,7 @@ function registerAllTools(server2, conn2) {
5467
5939
  registerIntegrationTools(server2, conn2);
5468
5940
  registerDriveTools(server2, conn2);
5469
5941
  registerMeetingTools(server2, conn2);
5942
+ registerDecisionTools(server2, conn2);
5470
5943
  }
5471
5944
 
5472
5945
  // src/cli.ts
@@ -5497,11 +5970,5 @@ var server = new McpServer({
5497
5970
  registerAllTools(server, conn);
5498
5971
  var transport = new StdioServerTransport();
5499
5972
  await server.connect(transport);
5500
- process.on("SIGINT", () => {
5501
- conn.disconnect();
5502
- process.exit(0);
5503
- });
5504
- process.on("SIGTERM", () => {
5505
- conn.disconnect();
5506
- process.exit(0);
5507
- });
5973
+ var stop = installLifecycle(conn);
5974
+ server.server.onclose = () => stop();
@@ -1,4 +1,5 @@
1
- import { ListDriveFilesResponse, ReadDriveFileResponse, DriveFileDTO, DeleteDriveFileResponse, ProjectIntegrationsSummary, GaAnalyticsSummaryDTO, ProjectChannelDTO, ReadChannelMessagesResponse, PostChannelMessageResponse, ListMeetingsResponse, MeetingForAgent, ReadMeetingTranscriptResponse, MeetingChecklistItemForAgent } from '@project/shared';
1
+ import { io, Socket } from 'socket.io-client';
2
+ import { ListDriveFilesResponse, ReadDriveFileResponse, DriveFileDTO, DeleteDriveFileResponse, ProjectIntegrationsSummary, GaAnalyticsSummaryDTO, ProjectChannelDTO, ReadChannelMessagesResponse, PostChannelMessageResponse, CreateProjectDecisionRequest, CreateProjectDecisionResponse, ListProjectDecisionsRequest, ProjectDecisionRow, ProjectDecisionDetail, VoteProjectDecisionRequest, ResolveProjectDecisionRequest, UpdateProjectDecisionRequest, ListMeetingsResponse, MeetingForAgent, ReadMeetingTranscriptResponse, MeetingChecklistItemForAgent } from '@project/shared';
2
3
 
3
4
  interface ConveyorMcpConfig {
4
5
  apiUrl: string;
@@ -310,12 +311,20 @@ interface PrioritySummary {
310
311
  color: string;
311
312
  description?: string | null;
312
313
  }
314
+ /**
315
+ * Creates the underlying socket. Defaults to socket.io's `io`; tests inject a
316
+ * fake so the reconnect wiring in `connect()` can be driven without a server.
317
+ */
318
+ type ConveyorSocketFactory = (url: string, options: Parameters<typeof io>[1]) => Pick<Socket, "on" | "emit" | "close" | "connect" | "disconnect">;
313
319
  declare class ConveyorConnection {
314
320
  private socket;
315
321
  private config;
322
+ private socketFactory;
316
323
  /** project slug → id, for resolving `<project>/<card>` card paths. */
317
324
  private projectSlugIds;
318
- constructor(config: ConveyorMcpConfig);
325
+ /** Session rooms this connection has joined, replayed after every reconnect. */
326
+ private subscribedSessionIds;
327
+ constructor(config: ConveyorMcpConfig, socketFactory?: ConveyorSocketFactory);
319
328
  get projectId(): string;
320
329
  private resolveProjectId;
321
330
  /** The configured default board (CONVEYOR_SUBPROJECT_ID), if any. */
@@ -329,6 +338,16 @@ declare class ConveyorConnection {
329
338
  */
330
339
  private resolveSubProjectId;
331
340
  private normalizeProjectList;
341
+ /**
342
+ * Re-join every room this connection needs.
343
+ *
344
+ * Runs on the FIRST connect and on every reconnect. A socket.io reconnect
345
+ * starts a fresh server-side socket that belongs to no rooms, and Cloud Run
346
+ * cuts each websocket at its 60-minute request limit, so a subscribe sent
347
+ * only once left long-lived MCP connections silently outside the project room
348
+ * for the rest of the day.
349
+ */
350
+ private rejoinRooms;
332
351
  connect(): Promise<void>;
333
352
  private call;
334
353
  /**
@@ -670,6 +689,27 @@ declare class ConveyorConnection {
670
689
  text: string;
671
690
  threadTs?: string;
672
691
  }): Promise<PostChannelMessageResponse>;
692
+ createDecision(params: Omit<CreateProjectDecisionRequest, "projectId"> & {
693
+ projectId?: string;
694
+ }): Promise<CreateProjectDecisionResponse>;
695
+ listDecisions(params: Omit<ListProjectDecisionsRequest, "projectId"> & {
696
+ projectId?: string;
697
+ }): Promise<{
698
+ decisions: ProjectDecisionRow[];
699
+ }>;
700
+ getDecision(params: {
701
+ projectId?: string;
702
+ decision: string;
703
+ }): Promise<ProjectDecisionDetail>;
704
+ voteDecision(params: Omit<VoteProjectDecisionRequest, "projectId"> & {
705
+ projectId?: string;
706
+ }): Promise<ProjectDecisionDetail>;
707
+ resolveDecision(params: Omit<ResolveProjectDecisionRequest, "projectId"> & {
708
+ projectId?: string;
709
+ }): Promise<ProjectDecisionDetail>;
710
+ updateDecision(params: Omit<UpdateProjectDecisionRequest, "projectId"> & {
711
+ projectId?: string;
712
+ }): Promise<ProjectDecisionDetail>;
673
713
  listMeetings(params: {
674
714
  projectId?: string;
675
715
  limit?: number;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { A as ActivePtySession, d as ConveyorConnection, e as ConveyorMcpConfig, P as PtyAttachSnapshot, c as PtyDataChunk } from './connection-B7CwszOV.js';
1
+ export { A as ActivePtySession, d as ConveyorConnection, e as ConveyorMcpConfig, P as PtyAttachSnapshot, c as PtyDataChunk } from './connection-DclP31lm.js';
2
2
  export { AttachTunnelOptions, ResolvedPtySession, RunTunnelOptions, TunnelConnection, TunnelHandle, TunnelSession, TunnelTty, WaitForPtySessionOptions, attachTunnel, runTunnel, waitForPtySession } from './tunnel.js';
3
+ import 'socket.io-client';
3
4
  import '@project/shared';
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-HIVOGGE3.js";
6
6
  import {
7
7
  ConveyorConnection
8
- } from "./chunk-XLDG5QEX.js";
8
+ } from "./chunk-77XLXIFP.js";
9
9
  export {
10
10
  ConveyorConnection,
11
11
  attachTunnel,
@@ -2,9 +2,12 @@
2
2
  import {
3
3
  runTunnel
4
4
  } from "./chunk-HIVOGGE3.js";
5
+ import {
6
+ watchForClientExit
7
+ } from "./chunk-5KU3QS6A.js";
5
8
  import {
6
9
  ConveyorConnection
7
- } from "./chunk-XLDG5QEX.js";
10
+ } from "./chunk-77XLXIFP.js";
8
11
 
9
12
  // src/tunnel-cli.ts
10
13
  var HELP = `conveyor-tunnel \u2014 attach your local terminal to a cloud Claude Code session.
@@ -125,8 +128,7 @@ function cleanupAndExit(code) {
125
128
  conn.disconnect();
126
129
  process.exit(code);
127
130
  }
128
- process.on("SIGINT", () => cleanupAndExit(0));
129
- process.on("SIGTERM", () => cleanupAndExit(0));
131
+ watchForClientExit(() => cleanupAndExit(0), { watchStdin: false });
130
132
  try {
131
133
  await conn.connect();
132
134
  log("Connected to Conveyor API.");
package/dist/tunnel.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { A as ActivePtySession, P as PtyAttachSnapshot, c as PtyDataChunk } from './connection-B7CwszOV.js';
1
+ import { A as ActivePtySession, P as PtyAttachSnapshot, c as PtyDataChunk } from './connection-DclP31lm.js';
2
+ import 'socket.io-client';
2
3
  import '@project/shared';
3
4
 
4
5
  /**
package/dist/wait-cli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ watchForClientExit
4
+ } from "./chunk-5KU3QS6A.js";
2
5
  import {
3
6
  ConveyorConnection
4
- } from "./chunk-XLDG5QEX.js";
7
+ } from "./chunk-77XLXIFP.js";
5
8
  import {
6
9
  runWait
7
10
  } from "./chunk-OPZL4NDT.js";
@@ -257,8 +260,7 @@ function interrupt() {
257
260
  exiting = true;
258
261
  emitResult({ reason: "interrupted" }, () => shutdown(0));
259
262
  }
260
- process.on("SIGINT", interrupt);
261
- process.on("SIGTERM", interrupt);
263
+ watchForClientExit(interrupt, { resumeStdin: true });
262
264
  try {
263
265
  await conn.connect();
264
266
  } catch (err) {
@@ -1,5 +1,6 @@
1
- import { C as CardCollectionPage, a as CardCollectionDelta } from './connection-B7CwszOV.js';
1
+ import { C as CardCollectionPage, a as CardCollectionDelta } from './connection-DclP31lm.js';
2
2
  import { WaitFilter, WaitResult } from './wait.js';
3
+ import 'socket.io-client';
3
4
  import '@project/shared';
4
5
 
5
6
  /**
package/dist/wait.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { b as CardCollectionItem } from './connection-B7CwszOV.js';
1
+ import { b as CardCollectionItem } from './connection-DclP31lm.js';
2
+ import 'socket.io-client';
2
3
  import '@project/shared';
3
4
 
4
5
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-mcp",
3
- "version": "5.0.3",
3
+ "version": "5.0.5",
4
4
  "description": "Conveyor MCP server for Claude Code PM integration",
5
5
  "keywords": [
6
6
  "claude",