@rallycry/conveyor-mcp 5.0.15 → 5.0.17
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/dist/{chunk-CRHA7PSA.js → chunk-6JLKJUYV.js} +15 -6
- package/dist/chunk-IRVUONC3.js +2559 -0
- package/dist/cli.js +1605 -3831
- package/dist/{connection-eGuRasFp.d.ts → connection-Ctg-QM1s.d.ts} +13 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/site-assets-cli.d.ts +1 -0
- package/dist/site-assets-cli.js +100 -0
- package/dist/tunnel-cli.js +1 -1
- package/dist/tunnel.d.ts +1 -1
- package/dist/wait-cli.js +1 -1
- package/dist/wait-runner.d.ts +1 -1
- package/dist/wait.d.ts +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,2559 @@
|
|
|
1
|
+
// ../shared/dist/chunk-SFSQ7NKP.js
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { z as z2 } from "zod";
|
|
4
|
+
var DEFAULT_SONNET_MODEL = "claude-sonnet-5-5";
|
|
5
|
+
var DEFAULT_OPUS_MODEL = "claude-opus-5-5";
|
|
6
|
+
var DEFAULT_HAIKU_MODEL = "claude-haiku-4-5-20251001";
|
|
7
|
+
var FABLE_MODEL = "claude-fable-5-1";
|
|
8
|
+
var PREVIOUS_SONNET_MODEL = "claude-sonnet-5";
|
|
9
|
+
var PREVIOUS_OPUS_MODEL = "claude-opus-5";
|
|
10
|
+
function formatModelId(provider, model) {
|
|
11
|
+
return `${provider}/${model}`;
|
|
12
|
+
}
|
|
13
|
+
var JEV_MODEL = "jev-1.13.0";
|
|
14
|
+
var JEV_MODEL_ID = formatModelId("typesafe", JEV_MODEL);
|
|
15
|
+
var JEV_ZEN_MODEL = "jev-1.13";
|
|
16
|
+
var JEV_ZEN_MODEL_ID = formatModelId("zen", JEV_ZEN_MODEL);
|
|
17
|
+
var JEV_INPUT_PRICE_PER_TOKEN = 0.042 / 1e6;
|
|
18
|
+
var JEV_STATE_MAX_CHARS = 1e5;
|
|
19
|
+
var PTY_STREAM_PORT_BASE = 7420;
|
|
20
|
+
var PTY_STREAM_PORT_ATTEMPTS = 8;
|
|
21
|
+
var PREVIEW_PORT_DENY_LIST = [
|
|
22
|
+
5432,
|
|
23
|
+
6379,
|
|
24
|
+
9200,
|
|
25
|
+
...Array.from({ length: PTY_STREAM_PORT_ATTEMPTS }, (_, i) => PTY_STREAM_PORT_BASE + i)
|
|
26
|
+
];
|
|
27
|
+
function normalizeCheckpointPath(value) {
|
|
28
|
+
let normalized = value.trim().replace(/\/{2,}/g, "/");
|
|
29
|
+
normalized = normalized.split("/").filter((segment) => segment !== ".").join("/");
|
|
30
|
+
while (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
var checkpointPathSchema = z.string().transform(normalizeCheckpointPath).pipe(
|
|
34
|
+
z.string().min(1).refine((value) => value !== ".", "Checkpoint paths must name a repository entry").refine((value) => !value.startsWith("/"), "Checkpoint paths must be repository-relative").refine(
|
|
35
|
+
(value) => !/^[A-Za-z]:[\\/]/.test(value) && !value.includes("\\"),
|
|
36
|
+
"Checkpoint paths must use repository-relative POSIX syntax"
|
|
37
|
+
).refine(
|
|
38
|
+
(value) => !value.split("/").includes(".."),
|
|
39
|
+
"Checkpoint paths must not traverse a parent directory"
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
var secretNameSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
|
|
43
|
+
var checkpointKeySchema = z.string().regex(/^[0-9a-f]{64}$/);
|
|
44
|
+
var checkpointDigestRefSchema = z.string().regex(/^[^\s@]+@sha256:[0-9a-f]{64}$/);
|
|
45
|
+
var ACTIONS_PREBAKE_REGISTRY_PATTERN = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::(?:[1-9][0-9]{0,4}))?(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/;
|
|
46
|
+
var actionsPrebakeRegistrySchema = z.string().trim().min(1).regex(
|
|
47
|
+
ACTIONS_PREBAKE_REGISTRY_PATTERN,
|
|
48
|
+
"Actions prebake registry must be a lowercase host[:port] with an optional path prefix"
|
|
49
|
+
).refine((value) => {
|
|
50
|
+
const port = /:([0-9]+)(?:\/|$)/.exec(value)?.[1];
|
|
51
|
+
return !port || Number(port) <= 65535;
|
|
52
|
+
}, "Actions prebake registry port must be between 1 and 65535");
|
|
53
|
+
function uniqueSortedArray(item, minimum = 0) {
|
|
54
|
+
return z.array(item).min(minimum).superRefine((values, ctx) => {
|
|
55
|
+
if (new Set(values).size !== values.length) {
|
|
56
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Duplicate values are not allowed" });
|
|
57
|
+
}
|
|
58
|
+
}).transform((values) => [...values].sort());
|
|
59
|
+
}
|
|
60
|
+
var projectCheckpointSettingsSchema = z.object({
|
|
61
|
+
enabled: z.literal(true),
|
|
62
|
+
cacheCommand: z.string().trim().min(1),
|
|
63
|
+
cacheInputPaths: uniqueSortedArray(checkpointPathSchema, 1),
|
|
64
|
+
reusableArtifactPaths: uniqueSortedArray(checkpointPathSchema, 1),
|
|
65
|
+
finalizeCommand: z.string().trim().min(1),
|
|
66
|
+
credentialEpoch: z.string().trim().min(1),
|
|
67
|
+
requiredSecretNames: uniqueSortedArray(secretNameSchema).optional(),
|
|
68
|
+
optionalSecretNames: uniqueSortedArray(secretNameSchema).optional(),
|
|
69
|
+
bakeWebAppBuild: z.boolean().optional()
|
|
70
|
+
}).superRefine((checkpoint, ctx) => {
|
|
71
|
+
const required = new Set(checkpoint.requiredSecretNames ?? []);
|
|
72
|
+
for (const name of checkpoint.optionalSecretNames ?? []) {
|
|
73
|
+
if (required.has(name)) {
|
|
74
|
+
ctx.addIssue({
|
|
75
|
+
code: z.ZodIssueCode.custom,
|
|
76
|
+
path: ["optionalSecretNames"],
|
|
77
|
+
message: "A secret cannot be both required and optional"
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
var CARD_DESCRIPTION_MAX = 255;
|
|
83
|
+
var CARD_DESCRIPTION_LIMIT_MESSAGE = `Card descriptions are capped at ${CARD_DESCRIPTION_MAX} characters \u2014 write 1-2 plain sentences a non-engineer can read; put technical detail in the plan or card chat.`;
|
|
84
|
+
var CARD_DESCRIPTION_FIELD_HINT = `max ${CARD_DESCRIPTION_MAX} chars, 1-2 plain sentences a non-engineer can read \u2014 put technical detail in the plan`;
|
|
85
|
+
var MAX_MANUAL_TESTS_PER_TASK = 5;
|
|
86
|
+
var MAX_MANUAL_TEST_TITLE_LENGTH = 200;
|
|
87
|
+
var ManualTestItemsSchema = z2.array(z2.object({ title: z2.string().min(1).max(MAX_MANUAL_TEST_TITLE_LENGTH) })).min(1).max(MAX_MANUAL_TESTS_PER_TASK);
|
|
88
|
+
function describeManualTestRejection(item) {
|
|
89
|
+
return item.reason === "cap" ? `Rejected "${item.title}": the card already holds ${MAX_MANUAL_TESTS_PER_TASK} manual tests. Use remove_manual_test or edit_manual_test to consolidate.` : `Rejected "${item.title}": names an automated step (lint, tests, CI, shell); manual tests describe what a person does in the app.`;
|
|
90
|
+
}
|
|
91
|
+
var DEFAULT_CI_WAIT_TIMEOUT_MINUTES = 45;
|
|
92
|
+
var MAX_CI_WAIT_TIMEOUT_MINUTES = 180;
|
|
93
|
+
var SEVERITY_ENUM = [
|
|
94
|
+
"DEBUG",
|
|
95
|
+
"INFO",
|
|
96
|
+
"NOTICE",
|
|
97
|
+
"WARNING",
|
|
98
|
+
"ERROR",
|
|
99
|
+
"CRITICAL",
|
|
100
|
+
"ALERT",
|
|
101
|
+
"EMERGENCY"
|
|
102
|
+
];
|
|
103
|
+
var MAX_LINE_CHARS = 400;
|
|
104
|
+
var DEFAULT_SINCE_MINUTES = 60;
|
|
105
|
+
var FULL_MODE_MAX_ENTRIES = 10;
|
|
106
|
+
function truncateLine(text) {
|
|
107
|
+
const oneLine = text.replace(/\s*\n\s*/g, " \u23CE ");
|
|
108
|
+
if (oneLine.length <= MAX_LINE_CHARS) return oneLine;
|
|
109
|
+
const overflow = oneLine.length - MAX_LINE_CHARS;
|
|
110
|
+
return `${oneLine.slice(0, MAX_LINE_CHARS)}\u2026[+${overflow}c]`;
|
|
111
|
+
}
|
|
112
|
+
function entrySource(entry) {
|
|
113
|
+
return entry.resource.service_name ?? entry.resource.pod_name ?? entry.resource.database_id ?? entry.resourceType ?? "-";
|
|
114
|
+
}
|
|
115
|
+
var PAYLOAD_SKIP_KEYS = /* @__PURE__ */ new Set(["message", "severity", "timestamp", "level", "stack"]);
|
|
116
|
+
var PAYLOAD_PRIORITY = [
|
|
117
|
+
"error",
|
|
118
|
+
"outcome",
|
|
119
|
+
"serviceName",
|
|
120
|
+
"methodName",
|
|
121
|
+
"userId",
|
|
122
|
+
"taskId",
|
|
123
|
+
"sessionId",
|
|
124
|
+
"workspaceId",
|
|
125
|
+
"projectId",
|
|
126
|
+
"durationMs"
|
|
127
|
+
];
|
|
128
|
+
var PAYLOAD_VALUE_MAX_CHARS = 160;
|
|
129
|
+
function compactPayloadValue(value) {
|
|
130
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
131
|
+
const flat = (raw ?? "undefined").replace(/\s+/g, " ");
|
|
132
|
+
const quoted = typeof value === "string" && /[\s"]/.test(flat) ? JSON.stringify(flat) : flat;
|
|
133
|
+
return quoted.length > PAYLOAD_VALUE_MAX_CHARS ? `${quoted.slice(0, PAYLOAD_VALUE_MAX_CHARS)}\u2026` : quoted;
|
|
134
|
+
}
|
|
135
|
+
function formatPayloadSuffix(payloadJson) {
|
|
136
|
+
if (!payloadJson) return "";
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(payloadJson);
|
|
140
|
+
} catch {
|
|
141
|
+
const flat = payloadJson.replace(/\s+/g, " ");
|
|
142
|
+
return ` | payload=${flat.slice(0, PAYLOAD_VALUE_MAX_CHARS)}\u2026[truncated, use full]`;
|
|
143
|
+
}
|
|
144
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "";
|
|
145
|
+
const obj = parsed;
|
|
146
|
+
const rank = (key) => {
|
|
147
|
+
const i = PAYLOAD_PRIORITY.indexOf(key);
|
|
148
|
+
return i === -1 ? PAYLOAD_PRIORITY.length : i;
|
|
149
|
+
};
|
|
150
|
+
const parts = Object.keys(obj).filter((k) => !PAYLOAD_SKIP_KEYS.has(k) && obj[k] !== void 0 && obj[k] !== null).sort((a, b) => rank(a) - rank(b) || a.localeCompare(b)).map((k) => `${k}=${compactPayloadValue(obj[k])}`);
|
|
151
|
+
return parts.length > 0 ? ` | ${parts.join(" ")}` : "";
|
|
152
|
+
}
|
|
153
|
+
function httpPrefixOf(entry) {
|
|
154
|
+
return entry.httpRequest?.status ? `http ${entry.httpRequest.status} ${entry.httpRequest.method ?? ""} ${entry.httpRequest.url ?? ""}`.trim() + " \u2014 " : "";
|
|
155
|
+
}
|
|
156
|
+
function formatLogEntryLine(entry) {
|
|
157
|
+
const httpPrefix = httpPrefixOf(entry);
|
|
158
|
+
return `${entry.timestamp} ${entry.severity.padEnd(7)} [${entrySource(entry)}] ${truncateLine(
|
|
159
|
+
`${httpPrefix}${entry.message}${formatPayloadSuffix(entry.payload)}`
|
|
160
|
+
)}`;
|
|
161
|
+
}
|
|
162
|
+
function formatPayloadFull(payloadJson) {
|
|
163
|
+
let parsed;
|
|
164
|
+
try {
|
|
165
|
+
parsed = JSON.parse(payloadJson);
|
|
166
|
+
} catch {
|
|
167
|
+
return payloadJson;
|
|
168
|
+
}
|
|
169
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
170
|
+
return JSON.stringify(parsed, null, 2);
|
|
171
|
+
}
|
|
172
|
+
const obj = Object.fromEntries(
|
|
173
|
+
Object.entries(parsed).filter(([k]) => !PAYLOAD_SKIP_KEYS.has(k))
|
|
174
|
+
);
|
|
175
|
+
return Object.keys(obj).length > 0 ? JSON.stringify(obj, null, 2) : "";
|
|
176
|
+
}
|
|
177
|
+
function formatLogEntryFull(entry) {
|
|
178
|
+
const head = `${entry.timestamp} ${entry.severity} [${entrySource(entry)}]${entry.insertId ? ` insertId=${entry.insertId}` : ""}`;
|
|
179
|
+
const payload = entry.payload ? formatPayloadFull(entry.payload) : "";
|
|
180
|
+
return [
|
|
181
|
+
head,
|
|
182
|
+
`${httpPrefixOf(entry)}${entry.message}`,
|
|
183
|
+
...payload ? [payload] : [],
|
|
184
|
+
...entry.payloadTruncated ? ["\u2026[payload truncated server-side]"] : []
|
|
185
|
+
].join("\n");
|
|
186
|
+
}
|
|
187
|
+
function fullModeLimit(limit) {
|
|
188
|
+
return Math.min(limit ?? FULL_MODE_MAX_ENTRIES, FULL_MODE_MAX_ENTRIES);
|
|
189
|
+
}
|
|
190
|
+
function formatFullEntries(entries) {
|
|
191
|
+
return entries.map((entry, i) => (i === 0 ? "" : "\n") + formatLogEntryFull(entry));
|
|
192
|
+
}
|
|
193
|
+
async function runQueryGcpLogs(port, params, now = Date.now) {
|
|
194
|
+
const startTime = params.startTime ?? new Date(now() - (params.sinceMinutes ?? DEFAULT_SINCE_MINUTES) * 6e4).toISOString();
|
|
195
|
+
const full = params.full === true;
|
|
196
|
+
const limit = full ? fullModeLimit(params.limit) : params.limit;
|
|
197
|
+
const result = await port.queryGcpLogs({
|
|
198
|
+
projectId: params.projectId,
|
|
199
|
+
env: params.env,
|
|
200
|
+
severity: params.severity,
|
|
201
|
+
services: params.services,
|
|
202
|
+
sqlInstances: params.sqlInstances,
|
|
203
|
+
allServices: params.allServices,
|
|
204
|
+
search: params.search,
|
|
205
|
+
filter: params.filter,
|
|
206
|
+
startTime,
|
|
207
|
+
endTime: params.endTime,
|
|
208
|
+
limit,
|
|
209
|
+
pageToken: params.pageToken,
|
|
210
|
+
...full ? { full: true } : {}
|
|
211
|
+
});
|
|
212
|
+
if (result.error) return result.error;
|
|
213
|
+
const header = [
|
|
214
|
+
`env=${params.env ?? "prod"}`,
|
|
215
|
+
`window=${startTime}\u2192${params.endTime ?? "now"}`,
|
|
216
|
+
...params.severity ? [`minSeverity=${params.severity}`] : [],
|
|
217
|
+
`scope=${result.scopedServices ? `[${result.scopedServices.join(", ")}]` : "all"}`,
|
|
218
|
+
`entries=${result.entries.length}`,
|
|
219
|
+
...full ? ["full=true"] : []
|
|
220
|
+
].join(" ");
|
|
221
|
+
const lines = full ? formatFullEntries(result.entries) : result.entries.map(formatLogEntryLine);
|
|
222
|
+
const footer = result.nextPageToken ? [`-- more available: pass pageToken="${result.nextPageToken}" to continue`] : [];
|
|
223
|
+
if (lines.length === 0) {
|
|
224
|
+
return [
|
|
225
|
+
header,
|
|
226
|
+
"(no matching log entries \u2014 widen the window, lower minSeverity, or drop filters)"
|
|
227
|
+
].join("\n");
|
|
228
|
+
}
|
|
229
|
+
return [header, ...lines, ...footer].join("\n");
|
|
230
|
+
}
|
|
231
|
+
async function runQueryGrafanaLogs(port, params, now = Date.now) {
|
|
232
|
+
const startTime = params.startTime ?? new Date(now() - (params.sinceMinutes ?? DEFAULT_SINCE_MINUTES) * 6e4).toISOString();
|
|
233
|
+
const result = await port.queryGrafanaLogs({
|
|
234
|
+
projectId: params.projectId,
|
|
235
|
+
env: params.env,
|
|
236
|
+
level: params.level,
|
|
237
|
+
services: params.services,
|
|
238
|
+
search: params.search,
|
|
239
|
+
logql: params.logql,
|
|
240
|
+
startTime,
|
|
241
|
+
endTime: params.endTime,
|
|
242
|
+
limit: params.limit
|
|
243
|
+
});
|
|
244
|
+
if (result.error) return result.error;
|
|
245
|
+
const header = [
|
|
246
|
+
`env=${params.env ?? "prod"}`,
|
|
247
|
+
`window=${startTime}\u2192${params.endTime ?? "now"}`,
|
|
248
|
+
...params.level ? [`minLevel=${params.level}`] : [],
|
|
249
|
+
...result.logql ? [`logql=${truncateLine(result.logql)}`] : [],
|
|
250
|
+
`entries=${result.entries.length}`
|
|
251
|
+
].join(" ");
|
|
252
|
+
const lines = result.entries.map(formatLogEntryLine);
|
|
253
|
+
const oldest = result.entries.map((e) => e.timestamp).sort()[0];
|
|
254
|
+
const footer = result.hasMore ? [`-- hit the limit: older lines exist \u2014 pass endTime="${oldest}" to page further back`] : [];
|
|
255
|
+
if (lines.length === 0) {
|
|
256
|
+
return [
|
|
257
|
+
header,
|
|
258
|
+
"(no matching log entries \u2014 widen the window, lower minLevel, or drop filters)"
|
|
259
|
+
].join("\n");
|
|
260
|
+
}
|
|
261
|
+
return [header, ...lines, ...footer].join("\n");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ../shared/dist/tool-contracts/index.js
|
|
265
|
+
var f = {
|
|
266
|
+
string(opts) {
|
|
267
|
+
return { kind: "string", ...opts };
|
|
268
|
+
},
|
|
269
|
+
number(opts) {
|
|
270
|
+
return { kind: "number", ...opts };
|
|
271
|
+
},
|
|
272
|
+
boolean(opts) {
|
|
273
|
+
return { kind: "boolean", ...opts };
|
|
274
|
+
},
|
|
275
|
+
enum(values, opts) {
|
|
276
|
+
return { kind: "enum", values, ...opts };
|
|
277
|
+
},
|
|
278
|
+
array(item, opts) {
|
|
279
|
+
return { kind: "array", item, ...opts };
|
|
280
|
+
},
|
|
281
|
+
object(fields, opts) {
|
|
282
|
+
return { kind: "object", fields, ...opts };
|
|
283
|
+
},
|
|
284
|
+
optional(inner) {
|
|
285
|
+
return { kind: "optional", inner };
|
|
286
|
+
},
|
|
287
|
+
nullable(inner) {
|
|
288
|
+
return { kind: "nullable", inner };
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
function compileString(z3, spec) {
|
|
292
|
+
let schema = z3.string();
|
|
293
|
+
if (spec.min !== void 0) schema = schema.min(spec.min);
|
|
294
|
+
if (spec.max !== void 0) schema = schema.max(spec.max);
|
|
295
|
+
return schema;
|
|
296
|
+
}
|
|
297
|
+
function compileNumber(z3, spec) {
|
|
298
|
+
let schema = z3.number();
|
|
299
|
+
if (spec.int) schema = schema.int();
|
|
300
|
+
if (spec.positive) schema = schema.positive();
|
|
301
|
+
if (spec.nonnegative) schema = schema.nonnegative();
|
|
302
|
+
if (spec.min !== void 0) schema = schema.min(spec.min);
|
|
303
|
+
if (spec.max !== void 0) schema = schema.max(spec.max);
|
|
304
|
+
return schema;
|
|
305
|
+
}
|
|
306
|
+
function compileArray(z3, spec) {
|
|
307
|
+
let schema = z3.array(compileField(z3, spec.item));
|
|
308
|
+
if (spec.min !== void 0) schema = schema.min(spec.min);
|
|
309
|
+
if (spec.max !== void 0) schema = schema.max(spec.max);
|
|
310
|
+
return schema;
|
|
311
|
+
}
|
|
312
|
+
function compileBase(z3, spec) {
|
|
313
|
+
switch (spec.kind) {
|
|
314
|
+
case "string":
|
|
315
|
+
return compileString(z3, spec);
|
|
316
|
+
case "number":
|
|
317
|
+
return compileNumber(z3, spec);
|
|
318
|
+
case "boolean":
|
|
319
|
+
return z3.boolean();
|
|
320
|
+
case "enum":
|
|
321
|
+
return z3.enum([...spec.values]);
|
|
322
|
+
case "array":
|
|
323
|
+
return compileArray(z3, spec);
|
|
324
|
+
case "object":
|
|
325
|
+
return z3.object(compileShape(z3, spec.fields));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function descriptionOf(spec) {
|
|
329
|
+
if (spec.kind === "optional" || spec.kind === "nullable") return descriptionOf(spec.inner);
|
|
330
|
+
return spec.desc;
|
|
331
|
+
}
|
|
332
|
+
function compileUndescribed(z3, spec) {
|
|
333
|
+
if (spec.kind === "optional") {
|
|
334
|
+
return compileUndescribed(z3, spec.inner).optional();
|
|
335
|
+
}
|
|
336
|
+
if (spec.kind === "nullable") {
|
|
337
|
+
return compileUndescribed(z3, spec.inner).nullable();
|
|
338
|
+
}
|
|
339
|
+
return compileBase(z3, spec);
|
|
340
|
+
}
|
|
341
|
+
function compileField(z3, spec) {
|
|
342
|
+
const schema = compileUndescribed(z3, spec);
|
|
343
|
+
const desc = descriptionOf(spec);
|
|
344
|
+
return desc === void 0 ? schema : schema.describe(desc);
|
|
345
|
+
}
|
|
346
|
+
function compileShape(z3, fields) {
|
|
347
|
+
const shape = {};
|
|
348
|
+
for (const [key, spec] of Object.entries(fields)) {
|
|
349
|
+
shape[key] = compileField(z3, spec);
|
|
350
|
+
}
|
|
351
|
+
return shape;
|
|
352
|
+
}
|
|
353
|
+
function defineToolContract(contract) {
|
|
354
|
+
return contract;
|
|
355
|
+
}
|
|
356
|
+
var mcpProjectId = f.optional(f.string({ desc: "Target Conveyor project ID" }));
|
|
357
|
+
var cardDescriptionDesc = (lead) => `${lead} \u2014 ${CARD_DESCRIPTION_FIELD_HINT}`;
|
|
358
|
+
var storyPointValueDesc = "Story point value (1=Common, 2=Magic, 3=Rare, 5=Unique)";
|
|
359
|
+
var priorityValueDesc = "Priority value from manage_priorities list (1 = the project's highest level; seeded defaults are 1=High, 2=Medium, 3=Low)";
|
|
360
|
+
var getTaskContract = defineToolContract({
|
|
361
|
+
name: "get_task",
|
|
362
|
+
agent: {
|
|
363
|
+
description: "Look up any task by slug or ID. Returns JSON with id, slug, title, description, plan, status, branch, githubPRNumber, githubPRUrl, storyPoints. For children use list_subtasks.",
|
|
364
|
+
fields: {
|
|
365
|
+
slug_or_id: f.string({ desc: "The task slug (e.g. 'my-task') or CUID" })
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
mcp: {
|
|
369
|
+
description: "Get full task details including plan, chat history, PR info, subtasks, and build status. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
370
|
+
fields: {
|
|
371
|
+
projectId: mcpProjectId,
|
|
372
|
+
taskId: f.string({
|
|
373
|
+
// The path form is MCP-only on purpose: an in-pod agent is bound to one
|
|
374
|
+
// project, where a bare slug already resolves, so the `agent` half of
|
|
375
|
+
// this contract deliberately says nothing about it.
|
|
376
|
+
desc: "The task ID or slug (the value in a card URL, /cards/<slug>), or a path-form reference '<project-slug>/<card-slug>' from the board's Copy path action"
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
var postToChatContract = defineToolContract({
|
|
382
|
+
name: "post_to_chat",
|
|
383
|
+
agent: {
|
|
384
|
+
description: "Post a message to the task chat for the team to see. Your turn output is NOT shown in chat, so this is the only way the team sees your status, summaries, and questions. Omit task_id to post to the current task's chat; pass a child's ID to message its chat.",
|
|
385
|
+
fields: {
|
|
386
|
+
message: f.optional(f.string({ desc: "The message to post to the team" })),
|
|
387
|
+
content: f.optional(
|
|
388
|
+
f.string({
|
|
389
|
+
desc: "Alias of `message` (the external conveyor-mcp surface names this field `content`). Provide exactly one of the two."
|
|
390
|
+
})
|
|
391
|
+
),
|
|
392
|
+
task_id: f.optional(
|
|
393
|
+
f.string({
|
|
394
|
+
desc: "Child task ID to post to. Omit to post to the current task's chat."
|
|
395
|
+
})
|
|
396
|
+
),
|
|
397
|
+
milestone: f.optional(
|
|
398
|
+
f.enum(["plan_ready", "implementation_complete", "blocked"], {
|
|
399
|
+
desc: "Declare a narrative milestone instead of a routine update. Use SPARINGLY \u2014 only when the plan is ready (plan_ready), implementation is complete (implementation_complete), or you are parked and cannot continue until a human acts (blocked). `blocked` posts a note and enables reply delivery but pages nobody; to page a person, ask them with AskUserQuestion. Never use `blocked` for a finished plan, finished work, or a tool error you can retry. Milestones appear on the card's activity timeline and in Slack."
|
|
400
|
+
})
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
mcp: {
|
|
405
|
+
description: "Post a message to a task's chat. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
406
|
+
fields: {
|
|
407
|
+
projectId: mcpProjectId,
|
|
408
|
+
taskId: f.string({ desc: "The task ID" }),
|
|
409
|
+
content: f.optional(f.string({ desc: "Message content" })),
|
|
410
|
+
message: f.optional(
|
|
411
|
+
f.string({
|
|
412
|
+
desc: "Alias of `content` (the in-pod agent surface names this field `message`). Provide exactly one of the two."
|
|
413
|
+
})
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
var readTaskChatContract = defineToolContract({
|
|
419
|
+
name: "read_task_chat",
|
|
420
|
+
agent: {
|
|
421
|
+
description: "Read recent human/user chat messages for a task. Omit task_id for the current task; pass a child ID for a child's chat. For agent logs use get_execution_logs.",
|
|
422
|
+
fields: {
|
|
423
|
+
limit: f.optional(f.number({ desc: "Number of recent messages to fetch (default 20)" })),
|
|
424
|
+
task_id: f.optional(
|
|
425
|
+
f.string({
|
|
426
|
+
desc: "Child task ID to read chat from. Omit to read the current task's chat."
|
|
427
|
+
})
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
mcp: {
|
|
432
|
+
description: "Read messages from a task's chat. Pass projectId to target a specific project; otherwise the configured default project is used. For agent execution logs use get_task_logs.",
|
|
433
|
+
fields: {
|
|
434
|
+
projectId: mcpProjectId,
|
|
435
|
+
taskId: f.string({ desc: "The task ID" }),
|
|
436
|
+
limit: f.optional(f.number({ desc: "Max messages to return (default 50)" }))
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
var listTagsDetail = f.optional(
|
|
441
|
+
f.enum(["summary", "full"], {
|
|
442
|
+
desc: `"summary" (default) returns one compact row per tag with a contextPathCount. "full" also ships each tag's contextPaths inline (each link with its verification status). Full mode can exceed a client's tool-output budget on a large glossary; use get_tag for one term instead.`
|
|
443
|
+
})
|
|
444
|
+
);
|
|
445
|
+
var listTagsContract = defineToolContract({
|
|
446
|
+
name: "list_tags",
|
|
447
|
+
agent: {
|
|
448
|
+
description: "List the whole project glossary in one call. The default summary gives every tag's id, name, color, description, parent/child tag ids, whether it carries a full overview (hasOverview), its attachmentCount (files labelled as examples of the term), and its contextPathCount (the rule/doc/file/folder links it wires into agent context \u2014 `0` means none). Pass detail: \"full\" to ship the contextPaths themselves inline. Use get_tag for one term's full overview and full link verification. Use the ids with get_tag / update_tag.",
|
|
449
|
+
fields: {
|
|
450
|
+
detail: listTagsDetail
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
mcp: {
|
|
454
|
+
description: 'List all project tags in one call. The default summary gives names, IDs, colors, descriptions, hierarchy (parent/child ids), a hasOverview flag, an attachmentCount (files labelled as examples of the term \u2014 read the tiles with list_tag_attachments), and a contextPathCount (the rule/doc/file/folder links each tag wires into agent context \u2014 `0` means none). Pass detail: "full" to ship the contextPaths themselves inline. Call get_tag for one term\'s full overview and full link verification. Pass projectId to target a specific project; otherwise the configured default project is used.',
|
|
455
|
+
fields: {
|
|
456
|
+
projectId: mcpProjectId,
|
|
457
|
+
detail: listTagsDetail
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
var SEARCH_STATUSES = [
|
|
462
|
+
"Planning",
|
|
463
|
+
"Open",
|
|
464
|
+
"InProgress",
|
|
465
|
+
"ReviewPR",
|
|
466
|
+
"ReviewDev",
|
|
467
|
+
"ReviewLive",
|
|
468
|
+
"Complete",
|
|
469
|
+
"Cancelled"
|
|
470
|
+
];
|
|
471
|
+
var SEARCH_CARD_TYPES = ["task", "incident", "suggestion", "decision"];
|
|
472
|
+
var searchFilterFields = {
|
|
473
|
+
tagNames: f.optional(
|
|
474
|
+
f.array(f.string(), {
|
|
475
|
+
desc: 'Tag names to filter by, e.g. ["agent", "mcp"]. Names, not ids, and matched case-insensitively. Use list_tags to see the glossary.'
|
|
476
|
+
})
|
|
477
|
+
),
|
|
478
|
+
tagMatch: f.optional(
|
|
479
|
+
f.enum(["any", "all"], {
|
|
480
|
+
desc: 'How to combine tagNames: "any" (default) returns cards carrying at least one, "all" returns only cards carrying every one.'
|
|
481
|
+
})
|
|
482
|
+
),
|
|
483
|
+
includeChildTags: f.optional(
|
|
484
|
+
f.boolean({
|
|
485
|
+
desc: "Also match cards carrying any descendant of the named tags in the tag hierarchy. Use this to sweep a whole area from its parent tag, e.g. cloud-build also matching release and deployment. Default false."
|
|
486
|
+
})
|
|
487
|
+
),
|
|
488
|
+
searchQuery: f.optional(f.string({ desc: "Text search on title and description" })),
|
|
489
|
+
statusFilters: f.optional(
|
|
490
|
+
f.array(f.enum(SEARCH_STATUSES), { desc: "Filter by one or more statuses" })
|
|
491
|
+
),
|
|
492
|
+
typeFilters: f.optional(
|
|
493
|
+
f.array(f.enum(SEARCH_CARD_TYPES), {
|
|
494
|
+
desc: 'Card types to include (default ["task"]). Pass e.g. ["incident"] or list several to search across types.'
|
|
495
|
+
})
|
|
496
|
+
),
|
|
497
|
+
assigneeId: f.optional(f.string({ desc: "Filter by assigned user ID" })),
|
|
498
|
+
unassigned: f.optional(
|
|
499
|
+
f.boolean({
|
|
500
|
+
desc: "Only return cards with no assignee (mutually exclusive with assigneeId)"
|
|
501
|
+
})
|
|
502
|
+
),
|
|
503
|
+
limit: f.optional(f.number({ desc: "Max results to return (default 20)" }))
|
|
504
|
+
};
|
|
505
|
+
var searchResultNote = "Every result lists the tags it carries, so you can see why it matched. Results are relevance-ordered: highest priority first then newest; suggestions-only queries rank by upvote score. Returns summaries \u2014 plan omitted, description truncated; use get_task for full details.";
|
|
506
|
+
var searchTasksContract = defineToolContract({
|
|
507
|
+
name: "search_tasks",
|
|
508
|
+
agent: {
|
|
509
|
+
description: `Search this project's cards by tag, text, status, type, and/or assignment. Tags are the project's glossary, so tagNames is the fastest way to find prior work in the area you are touching \u2014 e.g. every ReviewPR card tagged "mcp". Searches the whole project, not just the current card. Defaults to type=task \u2014 pass typeFilters for incidents/suggestions. ${searchResultNote}`,
|
|
510
|
+
fields: searchFilterFields
|
|
511
|
+
},
|
|
512
|
+
mcp: {
|
|
513
|
+
description: `Search cards by tag name, text query, status, type, and/or assignment. Defaults to type=task \u2014 pass typeFilters to include incidents/suggestions. Pass projectId to target a specific project; otherwise the configured default project is used. Use tag names like 'agent-runner', not IDs. ${searchResultNote}`,
|
|
514
|
+
fields: {
|
|
515
|
+
projectId: mcpProjectId,
|
|
516
|
+
...searchFilterFields,
|
|
517
|
+
subProjectId: f.optional(
|
|
518
|
+
f.nullable(
|
|
519
|
+
f.string({
|
|
520
|
+
desc: "Filter to a sub-project board. Omit to use the connection's default board (CONVEYOR_SUBPROJECT_ID) when set, else the whole project; pass null to force the whole project. Use list_accessible_subprojects to find board IDs."
|
|
521
|
+
})
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
var getConnectionContextContract = defineToolContract({
|
|
528
|
+
name: "get_connection_context",
|
|
529
|
+
agent: {
|
|
530
|
+
description: "Resolve WHAT this session is bound to \u2014 call this FIRST, before inferring anything from names or branch strings. Returns the current task (id, title, status, branch, baseBranch, whether it is a pack parent, and its parentTaskId when it is itself a child), its project (id, name), and the acting agent (id, model, mode, isAuto). This is the pod-side half of the same tool the external MCP surface exposes, so a shared skill can open with one call in both places; it reports no user account, because a pod is bound to a card rather than to a person's session.",
|
|
531
|
+
fields: {}
|
|
532
|
+
},
|
|
533
|
+
mcp: {
|
|
534
|
+
description: "Resolve WHO this connection is and WHAT project/board it points at \u2014 call this FIRST, before inferring anything from names. Returns the effective account, project, and board (sub-project) each with BOTH its immutable ID and its human-readable name/slug, the granted capabilities (read/create/update/chat/files/build), management URLs, and a one-line summary. Removes the ambiguity between a project's canonical name and a board label. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
535
|
+
fields: {
|
|
536
|
+
projectId: mcpProjectId
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
var tasksContracts = [
|
|
541
|
+
getTaskContract,
|
|
542
|
+
postToChatContract,
|
|
543
|
+
readTaskChatContract,
|
|
544
|
+
listTagsContract,
|
|
545
|
+
searchTasksContract,
|
|
546
|
+
getConnectionContextContract
|
|
547
|
+
];
|
|
548
|
+
var STATUS_ENUM = [
|
|
549
|
+
"Planning",
|
|
550
|
+
"Open",
|
|
551
|
+
"InProgress",
|
|
552
|
+
"ReviewPR",
|
|
553
|
+
"ReviewDev",
|
|
554
|
+
"ReviewLive",
|
|
555
|
+
"Complete",
|
|
556
|
+
"Cancelled"
|
|
557
|
+
];
|
|
558
|
+
var RISK_ENUM = ["critical", "high", "medium", "low"];
|
|
559
|
+
var GITHUB_BRANCH_MEANING = "Record the task's ACTUAL working branch (e.g. a locally-driven pack's branch cut at claim time, so identification never mints a competing name and pack-child merges settle against reality).";
|
|
560
|
+
var GITHUB_BRANCH_DESC = "Record the task's ACTUAL working branch (e.g. a locally-driven pack's branch cut at claim time, so identification never mints a competing name and pack-child merges settle against reality), or null to detach. The branch must already exist on origin, and a live workspace bound to a different branch rejects the write.";
|
|
561
|
+
var updateTaskContract = defineToolContract({
|
|
562
|
+
name: "update_task",
|
|
563
|
+
agent: {
|
|
564
|
+
description: "Update the CURRENT task's title, description, plan, githubBranch, or status \u2014 the one tool the shared Conveyor skills use for card writes, so the same skill text works here and in a local MCP session. Pass task_id ONLY with status, to move one of this card's children (claim it InProgress, promote it to Open); every other field always applies to the current task, because the server scopes plan/title writes to this session. Status rides the same path a human or a build uses, so watchdog and board semantics hold \u2014 for an emergency override that skips those, use force_update_task_status.",
|
|
565
|
+
fields: {
|
|
566
|
+
title: f.optional(f.string({ desc: "New title" })),
|
|
567
|
+
description: f.optional(f.string({ desc: cardDescriptionDesc("New description") })),
|
|
568
|
+
plan: f.optional(f.string({ desc: "New plan (markdown)" })),
|
|
569
|
+
status: f.optional(f.enum(STATUS_ENUM, { desc: "New status" })),
|
|
570
|
+
// Not nullable here, unlike the mcp surface: the pod's underlying
|
|
571
|
+
// `updateTaskProperties` takes `githubBranch?: string` with no null
|
|
572
|
+
// branch, so a pod can RECORD a branch but cannot detach one. Offering
|
|
573
|
+
// null would advertise a write the server would silently drop.
|
|
574
|
+
githubBranch: f.optional(
|
|
575
|
+
f.string({
|
|
576
|
+
min: 1,
|
|
577
|
+
desc: `${GITHUB_BRANCH_MEANING} The pod does NOT validate this: the value is written through with no check that the ref exists on origin, and no guard against repointing the record away from the branch this pod is actually running on (which strands the compute). Push the branch first, and only name a branch this session genuinely owns. Detaching (clearing) a branch is not available from a pod \u2014 use the external MCP surface for that.`
|
|
578
|
+
})
|
|
579
|
+
),
|
|
580
|
+
task_id: f.optional(
|
|
581
|
+
f.string({
|
|
582
|
+
// min:1 is load-bearing: "" is falsy, so an empty task_id would slip
|
|
583
|
+
// past both the refusal below and the child-routing branch, and
|
|
584
|
+
// silently update the CURRENT card — the exact mis-targeting this
|
|
585
|
+
// field's guard exists to prevent.
|
|
586
|
+
min: 1,
|
|
587
|
+
desc: "Child task ID to move. Valid ONLY alongside status \u2014 pairing it with title/description/plan/githubBranch is rejected rather than silently applied to the current card. Omit to update the current task."
|
|
588
|
+
})
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
mcp: {
|
|
593
|
+
description: "Update task fields: title, description, plan, status, risk, story points, priority, assignment, githubBranch, onHold, or tags. Set status to claim a card (InProgress), triage it (Open), or cancel it (Cancelled); for review approvals prefer approve_task / request_changes, which guard against stale-state races. Set onHold to park a card (true) or bring it back to the active board (false) without cancelling it. Tags are additive/subtractive \u2014 pass addTags/removeTags with tag names (not a replace-set). Pass projectId to target a specific project; otherwise the configured default project is used. Moving a task beyond Planning auto-fills any missing icon, story points, and agent assignment \u2014 don't spend turns on them; pass storyPointValue only to correct the sizing yourself. For subtasks use update_subtask.",
|
|
594
|
+
fields: {
|
|
595
|
+
projectId: mcpProjectId,
|
|
596
|
+
taskId: f.string({ desc: "The task ID" }),
|
|
597
|
+
title: f.optional(f.string({ desc: "New title" })),
|
|
598
|
+
description: f.optional(f.string({ desc: cardDescriptionDesc("New description") })),
|
|
599
|
+
plan: f.optional(f.string({ desc: "New plan (markdown)" })),
|
|
600
|
+
status: f.optional(f.enum(STATUS_ENUM, { desc: "New status" })),
|
|
601
|
+
risk: f.optional(
|
|
602
|
+
f.nullable(
|
|
603
|
+
f.enum(RISK_ENUM, {
|
|
604
|
+
desc: "Risk level \u2014 how much important surface the task touches (critical/high/medium/low). Pass null to clear."
|
|
605
|
+
})
|
|
606
|
+
)
|
|
607
|
+
),
|
|
608
|
+
storyPointValue: f.optional(
|
|
609
|
+
f.nullable(
|
|
610
|
+
f.number({
|
|
611
|
+
desc: `${storyPointValueDesc}. The tiers are per-project \u2014 a value the project has not configured is rejected. Pass null to clear it; a card beyond Planning with no story point is re-filled by identification.`
|
|
612
|
+
})
|
|
613
|
+
)
|
|
614
|
+
),
|
|
615
|
+
priorityValue: f.optional(
|
|
616
|
+
f.nullable(
|
|
617
|
+
f.number({
|
|
618
|
+
desc: `${priorityValueDesc}. The levels are per-project \u2014 a value the project has not configured is rejected. Pass null to clear it.`
|
|
619
|
+
})
|
|
620
|
+
)
|
|
621
|
+
),
|
|
622
|
+
assignedUserId: f.optional(f.nullable(f.string({ desc: "User ID to assign, or null" }))),
|
|
623
|
+
subProjectId: f.optional(
|
|
624
|
+
f.nullable(
|
|
625
|
+
f.string({
|
|
626
|
+
desc: "Assign the task to a sub-project board; null moves it back to the parent board. Omit to leave unchanged."
|
|
627
|
+
})
|
|
628
|
+
)
|
|
629
|
+
),
|
|
630
|
+
githubBranch: f.optional(f.nullable(f.string({ desc: GITHUB_BRANCH_DESC }))),
|
|
631
|
+
onHold: f.optional(
|
|
632
|
+
f.boolean({
|
|
633
|
+
desc: "Park the card on hold (true) or return it to the active board (false). On hold hides the card from the active columns without cancelling it; a card already at an active work status is demoted to Open when parked."
|
|
634
|
+
})
|
|
635
|
+
),
|
|
636
|
+
addTags: f.optional(
|
|
637
|
+
f.array(f.string(), {
|
|
638
|
+
desc: 'Tag names to add to the card (e.g. ["refactor"]). Additive \u2014 existing tags are kept. Unknown names are rejected; use list_tags to see available tags or manage_tags to create one.'
|
|
639
|
+
})
|
|
640
|
+
),
|
|
641
|
+
removeTags: f.optional(
|
|
642
|
+
f.array(f.string(), {
|
|
643
|
+
desc: "Tag names to remove from the card. Removing a tag the card doesn't have is a no-op."
|
|
644
|
+
})
|
|
645
|
+
)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
var taskUpdateContracts = [updateTaskContract];
|
|
650
|
+
var tagRef = f.string({
|
|
651
|
+
desc: "Tag id, or the exact tag name (case-insensitive)",
|
|
652
|
+
min: 1,
|
|
653
|
+
max: 100
|
|
654
|
+
});
|
|
655
|
+
var getTagContract = defineToolContract({
|
|
656
|
+
name: "get_tag",
|
|
657
|
+
agent: {
|
|
658
|
+
description: "Read one tag's full glossary entry: description, the full markdown overview (the term's spec \u2014 philosophy, mechanics, invariants), linked files/rules (each with its verified-link status \u2014 ok/stale/unchecked \u2014 from the periodic repo check), parent/child tags, active-card count, attachment count (files labelled as examples of the term), and recent revisions with their reasons. Call this whenever a chat message, plan, or tag list points at a term you need the full context for. A response with `overviewPath` set means the overview is sourced from that repo file \u2014 prefer Reading the path from your checkout (branch-correct); the served overview is materialized from the project's dev branch (the PR base; default branch when the repo has no dev branch) (`overviewSource.state`: ok/pending/stale).",
|
|
659
|
+
fields: {
|
|
660
|
+
tag: tagRef
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
mcp: {
|
|
664
|
+
description: "Read one tag's full glossary entry \u2014 description, markdown overview, context links (each with its verified-link status: ok/stale/unchecked plus last-checked provenance), parent/child tags, active-card count, attachment count (files labelled as examples of the term \u2014 read the tiles with list_tag_attachments), and recent revisions with provenance. A response with `overviewPath` set means the overview is sourced from that repo file at the project's dev branch (the PR base; default branch when the repo has no dev branch) (`overviewSource.state`: ok/pending/stale) \u2014 clients with a checkout can read the path directly for the branch-correct copy. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
665
|
+
fields: {
|
|
666
|
+
projectId: mcpProjectId,
|
|
667
|
+
tag: tagRef
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
var tagsContracts = [getTagContract];
|
|
672
|
+
var mcpChecklistTaskId = f.string({ desc: "The task ID or slug" });
|
|
673
|
+
var testStatuses = f.optional(
|
|
674
|
+
f.array(f.enum(["open", "approved", "rejected"]), {
|
|
675
|
+
desc: "Filter tests by status: open | approved | rejected"
|
|
676
|
+
})
|
|
677
|
+
);
|
|
678
|
+
var setManualTestItems = f.array(
|
|
679
|
+
f.object({
|
|
680
|
+
title: f.string({
|
|
681
|
+
min: 1,
|
|
682
|
+
max: MAX_MANUAL_TEST_TITLE_LENGTH,
|
|
683
|
+
desc: "One sentence a non-engineer can follow: where to go, what to do, what they should see. No commands, no code."
|
|
684
|
+
})
|
|
685
|
+
}),
|
|
686
|
+
{
|
|
687
|
+
min: 1,
|
|
688
|
+
max: MAX_MANUAL_TESTS_PER_TASK,
|
|
689
|
+
desc: `Manual tests to add. A card holds at most ${MAX_MANUAL_TESTS_PER_TASK}; most changes need 1, and a change with no user-visible behavior needs none.`
|
|
690
|
+
}
|
|
691
|
+
);
|
|
692
|
+
var setManualTestsBar = 'A manual test is a real user path in plain language ("Open a card with a PR: the Pull Request slat sits directly under Description"), not an engineering check. Never record lint, typecheck, unit tests, CI, curl, database, log, or deploy steps; those run automatically. Record at most 5 per card, usually 1, and none when the change has no user-visible behavior. Existing items with the same title are skipped; items that name an automated step or exceed the cap are rejected and reported back.';
|
|
693
|
+
var titleToEdit = f.string({ min: 1, desc: "The current title of the manual test to edit" });
|
|
694
|
+
var newTitle = f.string({ min: 1, desc: "The new title for the manual test" });
|
|
695
|
+
var titleToRemove = f.string({ min: 1, desc: "The title of the manual test to remove" });
|
|
696
|
+
var titleToApprove = f.string({ min: 1, desc: "The title of the manual test to approve" });
|
|
697
|
+
var titleToReject = f.string({ min: 1, desc: "The title of the manual test to reject" });
|
|
698
|
+
var rejectReason = f.string({
|
|
699
|
+
min: 1,
|
|
700
|
+
max: 2e3,
|
|
701
|
+
desc: "Why the test failed \u2014 what went wrong, shown to the team"
|
|
702
|
+
});
|
|
703
|
+
var listManualTestsContract = defineToolContract({
|
|
704
|
+
name: "list_manual_tests",
|
|
705
|
+
agent: {
|
|
706
|
+
description: "List the manual test checklist items for the current task. Use to see what manual verification steps have already been recorded.",
|
|
707
|
+
fields: {}
|
|
708
|
+
},
|
|
709
|
+
mcp: {
|
|
710
|
+
description: "List the manual test checklist items for a task. Pass projectId to target a specific project; otherwise the configured default project is used. Use to see what manual verification steps have already been recorded.",
|
|
711
|
+
fields: {
|
|
712
|
+
projectId: mcpProjectId,
|
|
713
|
+
taskId: f.string({ desc: "The task ID or slug (the value in a card URL, /cards/<slug>)" })
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
var queryManualTestsContract = defineToolContract({
|
|
718
|
+
name: "query_manual_tests",
|
|
719
|
+
agent: {
|
|
720
|
+
description: "Query manual tests across many tasks in this project, grouped by task. Filter by card status (ReviewDev, ReviewLive, Complete, ...) and/or test status (open | approved | rejected). Use to answer 'show all OPEN manual tests in ReviewDev' or 'show all REJECTED manual tests with the failing reason'. With no filters it defaults to the needs-attention view: open+rejected tests on ReviewDev/ReviewLive cards.",
|
|
721
|
+
fields: {
|
|
722
|
+
cardStatuses: f.optional(
|
|
723
|
+
f.array(f.string(), {
|
|
724
|
+
desc: 'Filter tasks by card status, e.g. ["ReviewDev", "ReviewLive"]'
|
|
725
|
+
})
|
|
726
|
+
),
|
|
727
|
+
testStatuses
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
mcp: {
|
|
731
|
+
description: "Query manual tests across many tasks in a project, grouped by task. Filter by card status (e.g. ReviewDev, ReviewLive, Complete) and/or test status (open | approved | rejected). Use to answer questions like 'show all OPEN manual tests in ReviewDev' or 'show all REJECTED manual tests in ReviewDev/ReviewLive with the failing reason'. With no filters it defaults to the needs-attention view: open+rejected tests on ReviewDev/ReviewLive cards. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
732
|
+
fields: {
|
|
733
|
+
projectId: mcpProjectId,
|
|
734
|
+
cardStatuses: f.optional(
|
|
735
|
+
f.array(f.string(), {
|
|
736
|
+
desc: 'Filter tasks by card/column status, e.g. ["ReviewDev", "ReviewLive"]'
|
|
737
|
+
})
|
|
738
|
+
),
|
|
739
|
+
testStatuses
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
var setManualTestsContract = defineToolContract({
|
|
744
|
+
name: "set_manual_tests",
|
|
745
|
+
agent: {
|
|
746
|
+
description: `Record the manual tests a person should run in the app to sign off on this card. ${setManualTestsBar}`,
|
|
747
|
+
fields: {
|
|
748
|
+
items: setManualTestItems
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
mcp: {
|
|
752
|
+
description: `Record the manual tests a person should run in the app to sign off on a card. Pass projectId to target a specific project; otherwise the configured default project is used. ${setManualTestsBar}`,
|
|
753
|
+
fields: {
|
|
754
|
+
projectId: mcpProjectId,
|
|
755
|
+
taskId: mcpChecklistTaskId,
|
|
756
|
+
items: setManualTestItems
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
var editManualTestContract = defineToolContract({
|
|
761
|
+
name: "edit_manual_test",
|
|
762
|
+
agent: {
|
|
763
|
+
description: "Rename an existing manual test step. Identify the test by its current title (case-insensitive); pass the new title to replace it. Use to correct or refine a recorded manual verification step.",
|
|
764
|
+
fields: {
|
|
765
|
+
title: titleToEdit,
|
|
766
|
+
newTitle
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
mcp: {
|
|
770
|
+
description: "Rename an existing manual test step on a task. Pass projectId to target a specific project; otherwise the configured default project is used. Identify the test by its current title (case-insensitive) and pass the new title to replace it.",
|
|
771
|
+
fields: {
|
|
772
|
+
projectId: mcpProjectId,
|
|
773
|
+
taskId: mcpChecklistTaskId,
|
|
774
|
+
title: titleToEdit,
|
|
775
|
+
newTitle
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
var removeManualTestContract = defineToolContract({
|
|
780
|
+
name: "remove_manual_test",
|
|
781
|
+
agent: {
|
|
782
|
+
description: "Remove an existing manual test step from the task checklist. Identify the test by its title (case-insensitive). Use to delete a stale or incorrect manual verification step.",
|
|
783
|
+
fields: {
|
|
784
|
+
title: titleToRemove
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
mcp: {
|
|
788
|
+
description: "Remove an existing manual test step from a task's checklist. Pass projectId to target a specific project; otherwise the configured default project is used. Identify the test by its title (case-insensitive).",
|
|
789
|
+
fields: {
|
|
790
|
+
projectId: mcpProjectId,
|
|
791
|
+
taskId: mcpChecklistTaskId,
|
|
792
|
+
title: titleToRemove
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
var approveManualTestContract = defineToolContract({
|
|
797
|
+
name: "approve_manual_test",
|
|
798
|
+
agent: {
|
|
799
|
+
description: "Sign off on (approve) a manual test step on behalf of your authenticated user. Identify the test by its title (case-insensitive). Use after you have verified the step passes.",
|
|
800
|
+
fields: {
|
|
801
|
+
title: titleToApprove
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
mcp: {
|
|
805
|
+
description: "Sign off on (approve) a manual test step on a task on behalf of your authenticated user. Pass projectId to target a specific project; otherwise the configured default project is used. Identify the test by its title (case-insensitive). Use after you have verified the step passes.",
|
|
806
|
+
fields: {
|
|
807
|
+
projectId: mcpProjectId,
|
|
808
|
+
taskId: mcpChecklistTaskId,
|
|
809
|
+
title: titleToApprove
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
var rejectManualTestContract = defineToolContract({
|
|
814
|
+
name: "reject_manual_test",
|
|
815
|
+
agent: {
|
|
816
|
+
description: "Flag an issue with (reject) a manual test step on behalf of your authenticated user, recording the reason. Identify the test by its title (case-insensitive). Use when the step fails verification.",
|
|
817
|
+
fields: {
|
|
818
|
+
title: titleToReject,
|
|
819
|
+
reason: rejectReason
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
mcp: {
|
|
823
|
+
description: "Flag an issue with (reject) a manual test step on a task on behalf of your authenticated user, recording the reason. Pass projectId to target a specific project; otherwise the configured default project is used. Identify the test by its title (case-insensitive). Use when the step fails verification.",
|
|
824
|
+
fields: {
|
|
825
|
+
projectId: mcpProjectId,
|
|
826
|
+
taskId: mcpChecklistTaskId,
|
|
827
|
+
title: titleToReject,
|
|
828
|
+
reason: rejectReason
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
var checklistContracts = [
|
|
833
|
+
listManualTestsContract,
|
|
834
|
+
queryManualTestsContract,
|
|
835
|
+
setManualTestsContract,
|
|
836
|
+
editManualTestContract,
|
|
837
|
+
removeManualTestContract,
|
|
838
|
+
approveManualTestContract,
|
|
839
|
+
rejectManualTestContract
|
|
840
|
+
];
|
|
841
|
+
var getDependenciesContract = defineToolContract({
|
|
842
|
+
name: "get_dependencies",
|
|
843
|
+
agent: {
|
|
844
|
+
description: "Get this task's dependencies and their met/unmet status (met = merged to dev). Use to confirm blockers merged, or see why a task cannot start. For task state use get_task.",
|
|
845
|
+
fields: {}
|
|
846
|
+
},
|
|
847
|
+
mcp: {
|
|
848
|
+
description: "Get a task's dependencies and their met/unmet status (met = merged to dev). Pass projectId to target a specific project; otherwise the configured default project is used. Use to confirm blockers merged, or see why a task cannot start. For task state use get_task.",
|
|
849
|
+
fields: {
|
|
850
|
+
projectId: mcpProjectId,
|
|
851
|
+
taskId: f.string({ desc: "The task ID" })
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
var addDependencyContract = defineToolContract({
|
|
856
|
+
name: "add_dependency",
|
|
857
|
+
agent: {
|
|
858
|
+
description: "Add a blocking dependency \u2014 this task cannot start until the named task is merged to dev. For post-task follow-ups use create_follow_up_task instead.",
|
|
859
|
+
fields: {
|
|
860
|
+
depends_on_slug_or_id: f.string({ desc: "Slug or ID of the task this task depends on" })
|
|
861
|
+
}
|
|
862
|
+
},
|
|
863
|
+
mcp: {
|
|
864
|
+
description: "Add a blocking dependency \u2014 this task cannot start until the named task is merged to dev. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
865
|
+
fields: {
|
|
866
|
+
projectId: mcpProjectId,
|
|
867
|
+
taskId: f.string({ desc: "The task ID that will be blocked" }),
|
|
868
|
+
dependsOnSlugOrId: f.string({ desc: "Slug or ID of the task this one depends on" })
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
var removeDependencyContract = defineToolContract({
|
|
873
|
+
name: "remove_dependency",
|
|
874
|
+
agent: {
|
|
875
|
+
description: "Remove a previously added dependency from this task. When to use: the dependency was added in error or is no longer relevant. Returns: confirmation string.",
|
|
876
|
+
fields: {
|
|
877
|
+
depends_on_slug_or_id: f.string({ desc: "Slug or ID of the task to remove as dependency" })
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
mcp: {
|
|
881
|
+
description: "Remove a previously added dependency from a task. Pass projectId to target a specific project; otherwise the configured default project is used. The task is no longer blocked by the named task. Returns: confirmation string.",
|
|
882
|
+
fields: {
|
|
883
|
+
projectId: mcpProjectId,
|
|
884
|
+
taskId: f.string({ desc: "The task ID to unblock" }),
|
|
885
|
+
dependsOnSlugOrId: f.string({ desc: "Slug or ID of the dependency to remove" })
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
var dependenciesContracts = [
|
|
890
|
+
getDependenciesContract,
|
|
891
|
+
addDependencyContract,
|
|
892
|
+
removeDependencyContract
|
|
893
|
+
];
|
|
894
|
+
var SP_DESCRIPTION = storyPointValueDesc;
|
|
895
|
+
var AGENT_FOLLOW_PARENT_STATUS = "Child mirrors the parent task's status automatically \u2014 for subtasks that ship on the parent's branch/PR with no build or PR of their own. Manual status writes on a follower stick only until the parent's next transition.";
|
|
896
|
+
var MCP_FOLLOW_PARENT_STATUS = "When true, this subtask mirrors the parent task's status automatically \u2014 for children that ship on the parent's branch/PR and have no build or PR of their own. Manual status writes on a follower stick only until the parent's next transition.";
|
|
897
|
+
var AGENT_DEPENDS_ON = "Sibling subtask ids or slugs this subtask blocks on (it won't start until they merge to dev). Set explicit dependency metadata here instead of describing order in the plan text \u2014 the pack runner schedules children off these edges. Omit / leave empty for independent children so they run in parallel.";
|
|
898
|
+
var MCP_STATUS_ENUM = [
|
|
899
|
+
"Planning",
|
|
900
|
+
"Open",
|
|
901
|
+
"InProgress",
|
|
902
|
+
"ReviewPR",
|
|
903
|
+
"ReviewDev",
|
|
904
|
+
"ReviewLive",
|
|
905
|
+
"Complete",
|
|
906
|
+
"Cancelled"
|
|
907
|
+
];
|
|
908
|
+
var AGENT_TAGS = `Glossary tag names to label the child with, e.g. ["agent", "pack"]. Names, not ids, matched case-insensitively against this project's tags (use list_tags to see them). A name that matches nothing comes back in the result and never fails the create.`;
|
|
909
|
+
var createSubtaskContract = defineToolContract({
|
|
910
|
+
name: "create_subtask",
|
|
911
|
+
agent: {
|
|
912
|
+
description: "Create a subtask (a child card) under the CURRENT card. This is how a card becomes a pack: the first child turns this card into the pack parent, and the children build as one unit. Use when breaking the current card into smaller pieces during planning. For a sibling card that lands after this one merges, use create_follow_up_task.",
|
|
913
|
+
fields: {
|
|
914
|
+
title: f.string({ desc: "Subtask title" }),
|
|
915
|
+
description: f.optional(f.string({ desc: cardDescriptionDesc("Brief description") })),
|
|
916
|
+
plan: f.optional(f.string({ desc: "Implementation plan in markdown" })),
|
|
917
|
+
ordinal: f.optional(f.number({ desc: "Step/order number (0-based)" })),
|
|
918
|
+
storyPointValue: f.optional(f.number({ desc: SP_DESCRIPTION })),
|
|
919
|
+
followParentStatus: f.optional(f.boolean({ desc: AGENT_FOLLOW_PARENT_STATUS })),
|
|
920
|
+
dependsOn: f.optional(f.array(f.string(), { desc: AGENT_DEPENDS_ON })),
|
|
921
|
+
tags: f.optional(f.array(f.string(), { desc: AGENT_TAGS }))
|
|
922
|
+
}
|
|
923
|
+
},
|
|
924
|
+
mcp: {
|
|
925
|
+
description: "Create a subtask under a parent task. Pass projectId to target a specific project; otherwise the configured default project is used. Subtasks break a larger task into independently buildable pieces. For children that instead ship on the parent's own branch/PR (e.g. per-theme tracking cards for one bundled PR), set followParentStatus so their status rides the parent's automatically.",
|
|
926
|
+
fields: {
|
|
927
|
+
projectId: mcpProjectId,
|
|
928
|
+
parentTaskId: f.string({ desc: "The parent task ID" }),
|
|
929
|
+
title: f.string({ desc: "Subtask title" }),
|
|
930
|
+
description: f.optional(f.string({ desc: cardDescriptionDesc("Subtask description") })),
|
|
931
|
+
plan: f.optional(f.string({ desc: "Subtask implementation plan (markdown)" })),
|
|
932
|
+
ordinal: f.optional(f.number({ desc: "Ordering position among siblings" })),
|
|
933
|
+
storyPointValue: f.optional(f.number({ desc: SP_DESCRIPTION })),
|
|
934
|
+
followParentStatus: f.optional(f.boolean({ desc: MCP_FOLLOW_PARENT_STATUS })),
|
|
935
|
+
dependsOn: f.optional(
|
|
936
|
+
f.array(f.string(), {
|
|
937
|
+
desc: "Sibling subtask ids or slugs this subtask blocks on (it won't start until they merge to dev). Set explicit dependency metadata here instead of describing order in the plan text. Omit / leave empty for independent children so they run in parallel."
|
|
938
|
+
})
|
|
939
|
+
),
|
|
940
|
+
tags: f.optional(
|
|
941
|
+
f.array(f.string(), {
|
|
942
|
+
desc: 'Tag names to assign to the subtask (e.g. ["refactor"]). Unknown names are rejected before the subtask is created, so nothing is written and a corrected retry creates exactly one subtask. Create the tag first with manage_tags. Use list_tags to see available tags.'
|
|
943
|
+
})
|
|
944
|
+
)
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
var updateSubtaskContract = defineToolContract({
|
|
949
|
+
name: "update_subtask",
|
|
950
|
+
agent: {
|
|
951
|
+
description: "Update an existing subtask's fields (title, description, plan, ordinal, storyPointValue, dependsOn) \u2014 and the sanctioned path to make a child buildable: promote it to status Open, assign its agent (agentIdOrName), and set story points. Setting story points does NOT auto-promote; set status explicitly. For the current task use update_task.",
|
|
952
|
+
fields: {
|
|
953
|
+
subtaskId: f.string({ desc: "The subtask ID to update" }),
|
|
954
|
+
title: f.optional(f.string()),
|
|
955
|
+
description: f.optional(f.string({ desc: cardDescriptionDesc("New description") })),
|
|
956
|
+
plan: f.optional(f.string()),
|
|
957
|
+
status: f.optional(
|
|
958
|
+
f.enum(["Planning", "Open"], {
|
|
959
|
+
desc: 'Move the child between "Planning" and "Open". "Open" marks it ready to execute \u2014 the pack runner takes Open children in dependency order. Execution statuses transition automatically.'
|
|
960
|
+
})
|
|
961
|
+
),
|
|
962
|
+
agentIdOrName: f.optional(
|
|
963
|
+
f.string({
|
|
964
|
+
desc: "Assign a project agent to the child (agent id or exact name from the Project Agents list)."
|
|
965
|
+
})
|
|
966
|
+
),
|
|
967
|
+
ordinal: f.optional(f.number()),
|
|
968
|
+
storyPointValue: f.optional(f.number({ desc: SP_DESCRIPTION })),
|
|
969
|
+
followParentStatus: f.optional(f.boolean({ desc: AGENT_FOLLOW_PARENT_STATUS })),
|
|
970
|
+
dependsOn: f.optional(
|
|
971
|
+
f.array(f.string(), {
|
|
972
|
+
desc: `${AGENT_DEPENDS_ON} Replaces the full dependency set \u2014 pass [] to clear all, omit to leave unchanged.`
|
|
973
|
+
})
|
|
974
|
+
)
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
mcp: {
|
|
978
|
+
description: "Update a subtask's fields: title, description, plan, status, ordering, story points, or dependencies. Pass projectId to target a specific project; otherwise the configured default project is used. Moving a subtask beyond Planning auto-fills missing story points and agent assignment \u2014 don't spend turns on them.",
|
|
979
|
+
fields: {
|
|
980
|
+
projectId: mcpProjectId,
|
|
981
|
+
subtaskId: f.string({ desc: "The subtask ID" }),
|
|
982
|
+
title: f.optional(f.string({ desc: "New title" })),
|
|
983
|
+
description: f.optional(f.string({ desc: cardDescriptionDesc("New description") })),
|
|
984
|
+
plan: f.optional(f.string({ desc: "New plan (markdown)" })),
|
|
985
|
+
status: f.optional(f.enum(MCP_STATUS_ENUM, { desc: "New status" })),
|
|
986
|
+
ordinal: f.optional(f.number({ desc: "New ordering position among siblings" })),
|
|
987
|
+
storyPointValue: f.optional(f.number({ desc: SP_DESCRIPTION })),
|
|
988
|
+
followParentStatus: f.optional(f.boolean({ desc: MCP_FOLLOW_PARENT_STATUS })),
|
|
989
|
+
dependsOn: f.optional(
|
|
990
|
+
f.array(f.string(), {
|
|
991
|
+
desc: "Replace the sibling subtask ids/slugs this subtask blocks on (pass [] to clear). Omit to leave dependencies unchanged."
|
|
992
|
+
})
|
|
993
|
+
)
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
var deleteSubtaskContract = defineToolContract({
|
|
998
|
+
name: "delete_subtask",
|
|
999
|
+
agent: {
|
|
1000
|
+
description: "Delete a subtask by id. When to use: a subtask was created in error or is no longer needed. Returns: confirmation string.",
|
|
1001
|
+
fields: {
|
|
1002
|
+
subtaskId: f.string({ desc: "The subtask ID to delete" })
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
mcp: {
|
|
1006
|
+
description: "Delete a subtask by ID. Pass projectId to target a specific project; otherwise the configured default project is used. This is permanent \u2014 use update_subtask to set status to Cancelled if you only want to close it.",
|
|
1007
|
+
fields: {
|
|
1008
|
+
projectId: mcpProjectId,
|
|
1009
|
+
subtaskId: f.string({ desc: "The subtask ID to delete" })
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
var listSubtasksContract = defineToolContract({
|
|
1014
|
+
name: "list_subtasks",
|
|
1015
|
+
agent: {
|
|
1016
|
+
description: "List all subtasks under the current parent task. Default compact view returns per child: status, agent, story points, PR number/state, and dependencies (dependsOn + allDependenciesMet). Use to pick the next ready child and record progress; pass verbose:true only when you need full description/plan text (large). For non-child tasks use get_task.",
|
|
1017
|
+
fields: {
|
|
1018
|
+
verbose: f.optional(
|
|
1019
|
+
f.boolean({
|
|
1020
|
+
desc: "Return full task rows including description and plan text (large \u2014 can exceed tool result limits on big packs). Default: compact orchestration view."
|
|
1021
|
+
})
|
|
1022
|
+
)
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
mcp: {
|
|
1026
|
+
description: "List all subtasks of a parent task with their status and ordering. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
1027
|
+
fields: {
|
|
1028
|
+
projectId: mcpProjectId,
|
|
1029
|
+
taskId: f.string({ desc: "The parent task ID" })
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
var SET_PARENT_ORDINAL = "Position among the new parent's children. Defaults to after every existing sibling. A detached card has no siblings to order against, so it only matters when you adopt.";
|
|
1034
|
+
var SET_PARENT_WARNINGS = "The move is reported, never refused: a card that already has a pull request, live compute, a release, children of its own, or dependency edges that now cross packs comes back with a warning in the result. Its branch is NOT re-cut against the new parent's feature branch.";
|
|
1035
|
+
var setTaskParentContract = defineToolContract({
|
|
1036
|
+
name: "set_task_parent",
|
|
1037
|
+
agent: {
|
|
1038
|
+
description: `Adopt an EXISTING card into this card's pack, or eject one of this card's children. The card keeps its own chat, plan, and history \u2014 it moves under (or out from) the current card, inherits the board, and lands after the current children. To make a NEW child use create_subtask instead. ${SET_PARENT_WARNINGS}`,
|
|
1039
|
+
fields: {
|
|
1040
|
+
taskId: f.string({
|
|
1041
|
+
desc: "Id, slug, or branch name of the card to move. Must live in this project."
|
|
1042
|
+
}),
|
|
1043
|
+
detach: f.optional(
|
|
1044
|
+
f.boolean({
|
|
1045
|
+
desc: "Eject the card from this pack instead of adopting it \u2014 it becomes a standalone card and stops following the parent's status. The card must already be a child of the current card."
|
|
1046
|
+
})
|
|
1047
|
+
),
|
|
1048
|
+
ordinal: f.optional(f.number({ desc: SET_PARENT_ORDINAL })),
|
|
1049
|
+
followParentStatus: f.optional(f.boolean({ desc: AGENT_FOLLOW_PARENT_STATUS }))
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1052
|
+
mcp: {
|
|
1053
|
+
description: `Re-parent an existing card: pass a parentTaskId to adopt it into that card's pack, or null to detach it into a standalone card. The card keeps its chat, plan, and history; adopting also moves it to the parent's board and orders it after the parent's existing children. Use create_subtask for a NEW child, and move_card to move a card to another PROJECT. Pass projectId to target a specific project; otherwise the configured default project is used. ${SET_PARENT_WARNINGS}`,
|
|
1054
|
+
fields: {
|
|
1055
|
+
projectId: mcpProjectId,
|
|
1056
|
+
taskId: f.string({ desc: "Id or slug of the card to move" }),
|
|
1057
|
+
parentTaskId: f.nullable(
|
|
1058
|
+
f.string({
|
|
1059
|
+
desc: "Id or slug of the new parent, or null to detach the card from its current parent. Must be in the same project, and can be neither the card itself nor one of its own descendants."
|
|
1060
|
+
})
|
|
1061
|
+
),
|
|
1062
|
+
ordinal: f.optional(f.number({ desc: SET_PARENT_ORDINAL })),
|
|
1063
|
+
followParentStatus: f.optional(f.boolean({ desc: MCP_FOLLOW_PARENT_STATUS }))
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
var subtasksContracts = [
|
|
1068
|
+
createSubtaskContract,
|
|
1069
|
+
updateSubtaskContract,
|
|
1070
|
+
deleteSubtaskContract,
|
|
1071
|
+
listSubtasksContract,
|
|
1072
|
+
setTaskParentContract
|
|
1073
|
+
];
|
|
1074
|
+
var mcpTaskIdOrSlug = f.string({ desc: "The task ID or slug" });
|
|
1075
|
+
var agentTaskIdOrSlug = f.optional(
|
|
1076
|
+
f.string({
|
|
1077
|
+
desc: "Omit for the current card; pass a parent, sibling, or any other card in this project (id or slug) to read its files instead."
|
|
1078
|
+
})
|
|
1079
|
+
);
|
|
1080
|
+
var listTaskFilesContract = defineToolContract({
|
|
1081
|
+
name: "list_task_files",
|
|
1082
|
+
agent: {
|
|
1083
|
+
description: "List all files attached to this task with metadata. Showcase files (the card's deliverables, `showcase: true` on each row) list first; a row with `supersedesFileId` is a newer version of that file. Use before fetching a specific file to see what is available and how large each is. For file contents use get_attachment. Pass task_id to list another card's files in this project, for example the pack parent's source documents.",
|
|
1084
|
+
fields: {
|
|
1085
|
+
task_id: agentTaskIdOrSlug
|
|
1086
|
+
}
|
|
1087
|
+
},
|
|
1088
|
+
mcp: {
|
|
1089
|
+
description: "List all files attached to a task with metadata (no contents \u2014 fast and small). Showcase files (the card's deliverables, `showcase: true` on each row) list first; a row with `supersedesFileId` is a newer version of that file. Pass projectId to target a specific project; otherwise the configured default project is used. Use before fetching a specific file to see what is available and how large each is. For file contents use get_attachment.",
|
|
1090
|
+
fields: {
|
|
1091
|
+
projectId: mcpProjectId,
|
|
1092
|
+
taskId: mcpTaskIdOrSlug
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
var getAttachmentContract = defineToolContract({
|
|
1097
|
+
name: "get_attachment",
|
|
1098
|
+
agent: {
|
|
1099
|
+
description: "Fetch one task file by ID. Text and images under the inline size caps come back inline. Any other file (PDF, Word, Excel, zip, large logs) is saved into this pod, and the result names the local path: open it with the Read tool or Bash. A document downloadUrl answers 404 without a signed-in project member's session, so do not curl or WebFetch it. Call list_task_files first to discover IDs and sizes. Pass task_id when the file is attached to another card in this project, such as the pack parent.",
|
|
1100
|
+
fields: {
|
|
1101
|
+
fileId: f.string({ desc: "The file ID to retrieve" }),
|
|
1102
|
+
task_id: agentTaskIdOrSlug
|
|
1103
|
+
}
|
|
1104
|
+
},
|
|
1105
|
+
mcp: {
|
|
1106
|
+
description: "Fetch one task file's content plus metadata by file ID (accepts task id or slug). Pass projectId to target a specific project; otherwise the configured default project is used. Images are returned as viewable image blocks. Large text files (logs, JSON) are returned in pages \u2014 use `offset`/`maxBytes` to read more. This tool is the way to read bytes: a document `downloadUrl` (anything that is not an image or video) answers 404 unless the request carries a signed-in project member's session, so do not curl or fetch it. Call list_task_files first to discover IDs and sizes.",
|
|
1107
|
+
fields: {
|
|
1108
|
+
projectId: mcpProjectId,
|
|
1109
|
+
taskId: mcpTaskIdOrSlug,
|
|
1110
|
+
fileId: f.string({ desc: "The file ID to fetch" }),
|
|
1111
|
+
offset: f.optional(
|
|
1112
|
+
f.number({
|
|
1113
|
+
int: true,
|
|
1114
|
+
nonnegative: true,
|
|
1115
|
+
desc: "Byte offset into text content (paging). Default 0."
|
|
1116
|
+
})
|
|
1117
|
+
),
|
|
1118
|
+
maxBytes: f.optional(
|
|
1119
|
+
f.number({
|
|
1120
|
+
int: true,
|
|
1121
|
+
positive: true,
|
|
1122
|
+
desc: "Max bytes of text content to return from offset."
|
|
1123
|
+
})
|
|
1124
|
+
)
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
var attachmentTags = f.optional(
|
|
1129
|
+
f.array(f.string(), {
|
|
1130
|
+
desc: `Glossary tag names this file is a relevant example of. MUST be a JSON array of quoted strings, e.g. ["ops-hub", "platform-support"] \u2014 bare unquoted words are invalid JSON and fail the whole call before it is parsed (21 fleet calls in one week died this way). Use it when the file shows a tagged entity in a particular state: the tag's page lists its recent tagged attachments, so a reader can see what the entity looks like across the app and spot visual changes over time. Names are matched case-insensitively within the project; a name that matches no tag is reported back and never fails the upload. Max 5.`
|
|
1131
|
+
})
|
|
1132
|
+
);
|
|
1133
|
+
var attachmentShowcase = f.optional(
|
|
1134
|
+
f.boolean({
|
|
1135
|
+
desc: "Mark this file as a showcase: the finished result of the card. Showcase files render first with a thumbnail on the card, are what a tag gallery shows, always post to chat, get a long-lived link, are kept past the 90-day purge, and are appended to the PR body under '## Showcase'. Use it for the final capture, not for scoping or before shots."
|
|
1136
|
+
})
|
|
1137
|
+
);
|
|
1138
|
+
var attachmentSupersedes = f.optional(
|
|
1139
|
+
f.string({
|
|
1140
|
+
desc: "File id (from list_task_files) of an older file on this card that this upload replaces, e.g. the previous cut of a video. The old file loses showcase and collapses under the new one as history; the new one inherits its showcase flag and tag labels. Naming a file that already has a newer version replaces the newest one. A bad id never fails the upload \u2014 the reason comes back instead."
|
|
1141
|
+
})
|
|
1142
|
+
);
|
|
1143
|
+
var uploadAttachmentContract = defineToolContract({
|
|
1144
|
+
name: "upload_attachment",
|
|
1145
|
+
agent: {
|
|
1146
|
+
description: "Upload a file (doc, notes, data, diagram, screenshot \u2014 any file type, up to 25MB) as a task attachment AND post it to the task chat in one step \u2014 no follow-up post_to_chat call needed. This is how you deliver a file the user should keep: it attaches to the card. Never publish deliverables as an external Claude artifact. Pass `showcase: true` for the capture that shows the finished result, `supersedes` with the old file's id when this is a newer version of a file already on the card, and `tags` when the file is a good example of a glossary tag in some state.",
|
|
1147
|
+
fields: {
|
|
1148
|
+
path: f.string({
|
|
1149
|
+
desc: "Path to the file \u2014 absolute, or relative to the workspace root"
|
|
1150
|
+
}),
|
|
1151
|
+
title: f.optional(
|
|
1152
|
+
f.string({ desc: "Short caption posted with the file (defaults to the file name)" })
|
|
1153
|
+
),
|
|
1154
|
+
comment: f.optional(
|
|
1155
|
+
f.string({
|
|
1156
|
+
desc: "Alias of `title` (the external conveyor-mcp surface names this field `comment`). Provide at most one of the two."
|
|
1157
|
+
})
|
|
1158
|
+
),
|
|
1159
|
+
tags: attachmentTags,
|
|
1160
|
+
showcase: attachmentShowcase,
|
|
1161
|
+
supersedes: attachmentSupersedes
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1164
|
+
mcp: {
|
|
1165
|
+
description: "Upload a local file as a task attachment (any file type, up to 25MB). Pass projectId to target a specific project; otherwise the configured default project is used. The file appears under the task's Files. Pass `comment` to also post it to the task chat in the same step, `showcase: true` for the capture that shows the finished result (a showcase file always posts to chat), `supersedes` with the old file's id when this is a newer version of a file already on the card, and `tags` when the file is a good example of a glossary tag in some state.",
|
|
1166
|
+
fields: {
|
|
1167
|
+
projectId: mcpProjectId,
|
|
1168
|
+
taskId: mcpTaskIdOrSlug,
|
|
1169
|
+
path: f.string({ desc: "Absolute path to the local file to upload" }),
|
|
1170
|
+
comment: f.optional(
|
|
1171
|
+
f.string({ desc: "When set, also posts the attachment to the task chat with this text" })
|
|
1172
|
+
),
|
|
1173
|
+
title: f.optional(
|
|
1174
|
+
f.string({
|
|
1175
|
+
desc: "Alias of `comment` (the in-pod agent surface names this field `title`). Provide at most one of the two."
|
|
1176
|
+
})
|
|
1177
|
+
),
|
|
1178
|
+
tags: attachmentTags,
|
|
1179
|
+
showcase: attachmentShowcase,
|
|
1180
|
+
supersedes: attachmentSupersedes,
|
|
1181
|
+
mimeType: f.optional(
|
|
1182
|
+
f.string({ desc: "Override the mime type inferred from the file extension" })
|
|
1183
|
+
)
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
var DELETE_ATTACHMENT_WARNING = "Permanent, with no trash and no undo (unlike drive_delete_file): the file and its bytes are gone, and the chat message that announced it keeps its text but loses the attachment. Use it to remove a superseded upload once its replacement is attached. `fileId` comes from list_task_files.";
|
|
1188
|
+
var deleteAttachmentContract = defineToolContract({
|
|
1189
|
+
name: "delete_attachment",
|
|
1190
|
+
agent: {
|
|
1191
|
+
description: `Delete one attachment from this task. ${DELETE_ATTACHMENT_WARNING} A pod can only delete files on its own card.`,
|
|
1192
|
+
fields: {
|
|
1193
|
+
fileId: f.string({ desc: "The file ID to delete" })
|
|
1194
|
+
}
|
|
1195
|
+
},
|
|
1196
|
+
mcp: {
|
|
1197
|
+
description: `Delete one attachment from a task (accepts task id or slug). ${DELETE_ATTACHMENT_WARNING} Requires Moderate on the project, the same bar as the web Files delete button. Pass projectId to target a specific project; otherwise the configured default project is used.`,
|
|
1198
|
+
fields: {
|
|
1199
|
+
projectId: mcpProjectId,
|
|
1200
|
+
taskId: mcpTaskIdOrSlug,
|
|
1201
|
+
fileId: f.string({ desc: "The file ID to delete" })
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
var SET_SHOWCASE_BEHAVIOR = "Mark an already-uploaded attachment as the card's showcase (`showcase: true`) or clear the flag (`showcase: false`) \u2014 the same flag upload_attachment sets at upload time, so promote an earlier capture here instead of uploading it again. A flip to showcase posts the file to the task chat once; a file that a chat message already carries is not posted again. `fileId` comes from list_task_files.";
|
|
1206
|
+
var showcaseFlag = f.boolean({
|
|
1207
|
+
desc: "true marks the file as the card's showcase; false clears the flag."
|
|
1208
|
+
});
|
|
1209
|
+
var setShowcaseContract = defineToolContract({
|
|
1210
|
+
name: "set_showcase",
|
|
1211
|
+
agent: {
|
|
1212
|
+
description: `${SET_SHOWCASE_BEHAVIOR} A pod can only change files on its own card.`,
|
|
1213
|
+
fields: {
|
|
1214
|
+
fileId: f.string({ desc: "The file ID to change" }),
|
|
1215
|
+
showcase: showcaseFlag
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
mcp: {
|
|
1219
|
+
description: `${SET_SHOWCASE_BEHAVIOR} Requires Moderate on the project, the same bar as the star on the card's Files list. Pass projectId to target a specific project; otherwise the configured default project is used.`,
|
|
1220
|
+
fields: {
|
|
1221
|
+
projectId: mcpProjectId,
|
|
1222
|
+
taskId: f.string({ desc: "The task ID or slug the file is attached to" }),
|
|
1223
|
+
fileId: f.string({ desc: "The file ID to change" }),
|
|
1224
|
+
showcase: showcaseFlag
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
var attachmentsContracts = [
|
|
1229
|
+
listTaskFilesContract,
|
|
1230
|
+
getAttachmentContract,
|
|
1231
|
+
uploadAttachmentContract,
|
|
1232
|
+
deleteAttachmentContract,
|
|
1233
|
+
setShowcaseContract
|
|
1234
|
+
];
|
|
1235
|
+
var createSuggestionContract = defineToolContract({
|
|
1236
|
+
name: "create_suggestion",
|
|
1237
|
+
agent: {
|
|
1238
|
+
description: "File a project suggestion (idea/improvement for maintainers to review). Duplicates are AI-deduped into an existing suggestion with an upvote. Returns the suggestion id.",
|
|
1239
|
+
fields: {
|
|
1240
|
+
title: f.string({ min: 1, desc: "Short title" }),
|
|
1241
|
+
description: f.optional(
|
|
1242
|
+
f.string({ desc: cardDescriptionDesc("What should change and why") })
|
|
1243
|
+
),
|
|
1244
|
+
tag_names: f.optional(f.array(f.string(), { desc: "Tag names to categorize" }))
|
|
1245
|
+
}
|
|
1246
|
+
},
|
|
1247
|
+
mcp: {
|
|
1248
|
+
description: "Suggest a feature, improvement, rule, or idea for the project. Pass projectId to target a specific project; otherwise the configured default project is used. Duplicates are deduped and your upvote is recorded.",
|
|
1249
|
+
fields: {
|
|
1250
|
+
projectId: mcpProjectId,
|
|
1251
|
+
title: f.string({ desc: "Suggestion title" }),
|
|
1252
|
+
description: f.optional(f.string({ desc: cardDescriptionDesc("Suggestion details") })),
|
|
1253
|
+
tagNames: f.optional(
|
|
1254
|
+
f.array(f.string(), {
|
|
1255
|
+
desc: 'Tag names to categorize the suggestion (e.g., ["agent-runner"])'
|
|
1256
|
+
})
|
|
1257
|
+
)
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
var suggestionsContracts = [createSuggestionContract];
|
|
1262
|
+
var OPTION_KINDS = ["current", "planned", "proposed"];
|
|
1263
|
+
var STATUS_LABELS = ["Open", "Decided", "Cancelled"];
|
|
1264
|
+
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.";
|
|
1265
|
+
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.";
|
|
1266
|
+
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.";
|
|
1267
|
+
var agentOption = f.object(
|
|
1268
|
+
{
|
|
1269
|
+
label: f.string({ min: 1, desc: "Short name for the option, under 80 characters" }),
|
|
1270
|
+
description: f.string({ min: 1, desc: optionDescription }),
|
|
1271
|
+
kind: f.optional(
|
|
1272
|
+
f.enum(OPTION_KINDS, {
|
|
1273
|
+
desc: 'current = what the code does today, planned = already agreed but unbuilt, proposed = new. Defaults to "proposed".'
|
|
1274
|
+
})
|
|
1275
|
+
)
|
|
1276
|
+
},
|
|
1277
|
+
{ desc: "One option people can choose" }
|
|
1278
|
+
);
|
|
1279
|
+
var createDecisionContract = defineToolContract({
|
|
1280
|
+
name: "create_decision",
|
|
1281
|
+
agent: {
|
|
1282
|
+
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 in full (the same JSON as get_decision, with option ids and the deadline) plus which cards were linked.`,
|
|
1283
|
+
fields: {
|
|
1284
|
+
question: f.string({
|
|
1285
|
+
min: 1,
|
|
1286
|
+
desc: "The question, as one plain sentence ending in a question mark"
|
|
1287
|
+
}),
|
|
1288
|
+
summary: f.optional(
|
|
1289
|
+
f.string({
|
|
1290
|
+
desc: cardDescriptionDesc("One or two sentences of context for the card face")
|
|
1291
|
+
})
|
|
1292
|
+
),
|
|
1293
|
+
context: f.string({
|
|
1294
|
+
min: 1,
|
|
1295
|
+
desc: "Markdown background: why this is being asked now, and what breaks if it is answered wrongly"
|
|
1296
|
+
}),
|
|
1297
|
+
options: f.array(agentOption, { min: 2, desc: optionsDesc }),
|
|
1298
|
+
default_option: f.number({ int: true, min: 1, max: 3, desc: defaultOptionDesc }),
|
|
1299
|
+
deadline_hours: f.optional(
|
|
1300
|
+
f.number({ min: 1, max: 720, desc: "Hours until the default applies. Default 72." })
|
|
1301
|
+
),
|
|
1302
|
+
impacted_cards: f.optional(
|
|
1303
|
+
f.array(f.string(), {
|
|
1304
|
+
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."
|
|
1305
|
+
})
|
|
1306
|
+
),
|
|
1307
|
+
include_current_card: f.optional(
|
|
1308
|
+
f.boolean({
|
|
1309
|
+
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."
|
|
1310
|
+
})
|
|
1311
|
+
),
|
|
1312
|
+
tag_names: f.optional(
|
|
1313
|
+
f.array(f.string(), {
|
|
1314
|
+
desc: "Tag names. The settled decision is written into each tag's overview, which is how the next agent finds it."
|
|
1315
|
+
})
|
|
1316
|
+
),
|
|
1317
|
+
raised_by_pr: f.optional(
|
|
1318
|
+
f.number({ int: true, min: 1, desc: "PR number that surfaced the question" })
|
|
1319
|
+
),
|
|
1320
|
+
raised_by_card: f.optional(
|
|
1321
|
+
f.string({
|
|
1322
|
+
desc: "Slug or id of the card that raised the question. A link back, not a block \u2014 use impacted_cards to block a card."
|
|
1323
|
+
})
|
|
1324
|
+
),
|
|
1325
|
+
deadline_at: f.optional(
|
|
1326
|
+
f.string({
|
|
1327
|
+
desc: "Absolute deadline as an ISO 8601 timestamp, e.g. 2026-09-25T17:00:00Z. Wins over deadline_hours when both are given; must be in the future and within 720 hours."
|
|
1328
|
+
})
|
|
1329
|
+
)
|
|
1330
|
+
}
|
|
1331
|
+
},
|
|
1332
|
+
mcp: {
|
|
1333
|
+
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.",
|
|
1334
|
+
fields: {
|
|
1335
|
+
projectId: mcpProjectId,
|
|
1336
|
+
question: f.string({ desc: "The question, as one plain sentence" }),
|
|
1337
|
+
summary: f.optional(f.string({ desc: cardDescriptionDesc("Context for the card face") })),
|
|
1338
|
+
context: f.string({ desc: "Markdown background: why this is being asked now" }),
|
|
1339
|
+
options: f.array(agentOption, { min: 2, desc: optionsDesc }),
|
|
1340
|
+
defaultOption: f.number({ int: true, min: 1, max: 3, desc: defaultOptionDesc }),
|
|
1341
|
+
deadlineHours: f.optional(
|
|
1342
|
+
f.number({ min: 1, max: 720, desc: "Hours until the default applies. Default 72." })
|
|
1343
|
+
),
|
|
1344
|
+
impactedCards: f.optional(
|
|
1345
|
+
f.array(f.string(), {
|
|
1346
|
+
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."
|
|
1347
|
+
})
|
|
1348
|
+
),
|
|
1349
|
+
tagNames: f.optional(f.array(f.string(), { desc: "Tag names to categorize the decision" })),
|
|
1350
|
+
raisedByPr: f.optional(
|
|
1351
|
+
f.number({ int: true, min: 1, desc: "PR number that surfaced the question" })
|
|
1352
|
+
),
|
|
1353
|
+
raisedByCard: f.optional(
|
|
1354
|
+
f.string({
|
|
1355
|
+
desc: "Slug or id of the card that raised the question. A link back, not a block \u2014 use impactedCards to block a card."
|
|
1356
|
+
})
|
|
1357
|
+
),
|
|
1358
|
+
deadlineAt: f.optional(
|
|
1359
|
+
f.string({
|
|
1360
|
+
desc: "Absolute deadline (ISO 8601). Wins over deadlineHours when both are given; must be in the future and within 720 hours."
|
|
1361
|
+
})
|
|
1362
|
+
)
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
var listDecisionsContract = defineToolContract({
|
|
1367
|
+
name: "list_decisions",
|
|
1368
|
+
agent: {
|
|
1369
|
+
description: `Search this project's decisions. Run this BEFORE planning anything in an area: "have we already decided how X behaves?" Each row carries the summary, who raised it (and from which card or PR), when, and \u2014 once settled \u2014 the resolution, so the list doubles as the decision log. Filter by card to see what is blocking a specific card.`,
|
|
1370
|
+
fields: {
|
|
1371
|
+
status: f.optional(
|
|
1372
|
+
f.enum(STATUS_LABELS, {
|
|
1373
|
+
desc: "Open = still being voted on, Decided = settled, Cancelled = dropped"
|
|
1374
|
+
})
|
|
1375
|
+
),
|
|
1376
|
+
tag: f.optional(f.string({ desc: 'Tag name, e.g. "slack"' })),
|
|
1377
|
+
card: f.optional(
|
|
1378
|
+
f.string({ desc: "Slug or id of a card \u2014 returns the decisions it is blocked by" })
|
|
1379
|
+
),
|
|
1380
|
+
query: f.optional(f.string({ desc: "Text search on the question" })),
|
|
1381
|
+
limit: f.optional(f.number({ desc: "Max rows to return (default 50, max 200)" }))
|
|
1382
|
+
}
|
|
1383
|
+
},
|
|
1384
|
+
mcp: {
|
|
1385
|
+
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.`,
|
|
1386
|
+
fields: {
|
|
1387
|
+
projectId: mcpProjectId,
|
|
1388
|
+
status: f.optional(f.enum(STATUS_LABELS, { desc: "Filter by state" })),
|
|
1389
|
+
tag: f.optional(f.string({ desc: "Tag name to filter by" })),
|
|
1390
|
+
card: f.optional(f.string({ desc: "Card slug or id \u2014 decisions blocking that card" })),
|
|
1391
|
+
query: f.optional(f.string({ desc: "Text search on the question" })),
|
|
1392
|
+
limit: f.optional(
|
|
1393
|
+
f.number({ int: true, min: 1, max: 200, desc: "Max rows to return (default 50)" })
|
|
1394
|
+
)
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
var getDecisionContract = defineToolContract({
|
|
1399
|
+
name: "get_decision",
|
|
1400
|
+
agent: {
|
|
1401
|
+
description: "Read one decision in full: its summary and context, who raised it and from where, 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.",
|
|
1402
|
+
fields: {
|
|
1403
|
+
decision: f.string({ min: 1, desc: "Decision slug or id" })
|
|
1404
|
+
}
|
|
1405
|
+
},
|
|
1406
|
+
mcp: {
|
|
1407
|
+
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.",
|
|
1408
|
+
fields: {
|
|
1409
|
+
projectId: mcpProjectId,
|
|
1410
|
+
decision: f.string({ desc: "Decision slug or id" })
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
var voteDecisionContract = defineToolContract({
|
|
1415
|
+
name: "vote_decision",
|
|
1416
|
+
agent: {
|
|
1417
|
+
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.",
|
|
1418
|
+
fields: {
|
|
1419
|
+
decision: f.string({ min: 1, desc: "Decision slug or id" }),
|
|
1420
|
+
option: f.string({ min: 1, desc: "Option id, or its 1-based position" }),
|
|
1421
|
+
reason: f.optional(
|
|
1422
|
+
f.string({ desc: "Why this option, in one or two sentences (max 255 characters)" })
|
|
1423
|
+
)
|
|
1424
|
+
}
|
|
1425
|
+
},
|
|
1426
|
+
mcp: {
|
|
1427
|
+
description: "Vote on an open Conveyor decision, with a reason. One vote per voter \u2014 voting again replaces the previous one.",
|
|
1428
|
+
fields: {
|
|
1429
|
+
projectId: mcpProjectId,
|
|
1430
|
+
decision: f.string({ desc: "Decision slug or id" }),
|
|
1431
|
+
option: f.string({ desc: "Option id, or its 1-based position" }),
|
|
1432
|
+
reason: f.optional(f.string({ desc: "Why this option (max 255 characters)" }))
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
var resolveDecisionContract = defineToolContract({
|
|
1437
|
+
name: "resolve_decision",
|
|
1438
|
+
agent: {
|
|
1439
|
+
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.",
|
|
1440
|
+
fields: {
|
|
1441
|
+
decision: f.string({ min: 1, desc: "Decision slug or id" }),
|
|
1442
|
+
option: f.string({ min: 1, desc: "Winning option id, or its 1-based position" }),
|
|
1443
|
+
resolution: f.string({
|
|
1444
|
+
min: 1,
|
|
1445
|
+
desc: "What was decided, in one or two plain sentences (max 255 characters)"
|
|
1446
|
+
}),
|
|
1447
|
+
notes: f.optional(
|
|
1448
|
+
f.string({ desc: "Markdown reasoning. Link any verdict attachment here." })
|
|
1449
|
+
)
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
mcp: {
|
|
1453
|
+
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.",
|
|
1454
|
+
fields: {
|
|
1455
|
+
projectId: mcpProjectId,
|
|
1456
|
+
decision: f.string({ desc: "Decision slug or id" }),
|
|
1457
|
+
option: f.string({ desc: "Winning option id, or its 1-based position" }),
|
|
1458
|
+
resolution: f.string({ desc: "What was decided (max 255 characters)" }),
|
|
1459
|
+
notes: f.optional(f.string({ desc: "Markdown reasoning" }))
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
var updateDecisionContract = defineToolContract({
|
|
1464
|
+
name: "update_decision",
|
|
1465
|
+
agent: {
|
|
1466
|
+
description: "Change an open decision: fix the question, summary, context or tags, rewrite the options while nobody has voted, move the 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.",
|
|
1467
|
+
fields: {
|
|
1468
|
+
decision: f.string({ min: 1, desc: "Decision slug or id" }),
|
|
1469
|
+
question: f.optional(f.string({ min: 1, desc: "New question, one plain sentence" })),
|
|
1470
|
+
summary: f.optional(
|
|
1471
|
+
f.string({ desc: cardDescriptionDesc("New one- or two-sentence summary") })
|
|
1472
|
+
),
|
|
1473
|
+
context: f.optional(f.string({ min: 1, desc: "New markdown background" })),
|
|
1474
|
+
options: f.optional(
|
|
1475
|
+
f.array(agentOption, {
|
|
1476
|
+
min: 2,
|
|
1477
|
+
desc: "Replacement options. Accepted only while the decision has no votes \u2014 once somebody has voted, cancel it and raise a new one instead. Send default_option with it: the old default no longer exists."
|
|
1478
|
+
})
|
|
1479
|
+
),
|
|
1480
|
+
default_option: f.optional(
|
|
1481
|
+
f.number({ int: true, min: 1, max: 3, desc: "1-based position of the new default" })
|
|
1482
|
+
),
|
|
1483
|
+
tag_names: f.optional(
|
|
1484
|
+
f.array(f.string(), { desc: "Replacement tag names (replaces the current set)" })
|
|
1485
|
+
),
|
|
1486
|
+
deadline_hours: f.optional(f.number({ desc: "New deadline, in hours from now. 1 to 720." })),
|
|
1487
|
+
deadline_at: f.optional(
|
|
1488
|
+
f.string({
|
|
1489
|
+
desc: "New absolute deadline (ISO 8601). Wins over deadline_hours; must be in the future and within 720 hours."
|
|
1490
|
+
})
|
|
1491
|
+
),
|
|
1492
|
+
add_impacted_cards: f.optional(
|
|
1493
|
+
f.array(f.string(), { desc: "Slugs or ids of cards to block on this decision" })
|
|
1494
|
+
),
|
|
1495
|
+
remove_impacted_cards: f.optional(
|
|
1496
|
+
f.array(f.string(), { desc: "Ids of cards to stop blocking" })
|
|
1497
|
+
),
|
|
1498
|
+
status: f.optional(
|
|
1499
|
+
f.enum(["Cancelled", "Open"], {
|
|
1500
|
+
desc: "Cancelled drops the question; Open reopens a settled one and clears its answer"
|
|
1501
|
+
})
|
|
1502
|
+
)
|
|
1503
|
+
}
|
|
1504
|
+
},
|
|
1505
|
+
mcp: {
|
|
1506
|
+
description: "Change an open Conveyor decision: fix the question, summary, context or tags, rewrite the options while nobody has voted, move the deadline, link or unlink impacted cards, cancel it, or reopen a settled one.",
|
|
1507
|
+
fields: {
|
|
1508
|
+
projectId: mcpProjectId,
|
|
1509
|
+
decision: f.string({ desc: "Decision slug or id" }),
|
|
1510
|
+
question: f.optional(f.string({ desc: "New question" })),
|
|
1511
|
+
summary: f.optional(f.string({ desc: cardDescriptionDesc("New summary") })),
|
|
1512
|
+
context: f.optional(f.string({ desc: "New markdown background" })),
|
|
1513
|
+
options: f.optional(
|
|
1514
|
+
f.array(agentOption, {
|
|
1515
|
+
min: 2,
|
|
1516
|
+
desc: "Replacement options. Accepted only while the decision has no votes. Send defaultOption with it: the old default no longer exists."
|
|
1517
|
+
})
|
|
1518
|
+
),
|
|
1519
|
+
defaultOption: f.optional(
|
|
1520
|
+
f.number({ int: true, min: 1, max: 3, desc: "1-based position of the new default" })
|
|
1521
|
+
),
|
|
1522
|
+
tagNames: f.optional(f.array(f.string(), { desc: "Replacement tag names" })),
|
|
1523
|
+
deadlineHours: f.optional(
|
|
1524
|
+
f.number({ min: 1, max: 720, desc: "New deadline, in hours from now" })
|
|
1525
|
+
),
|
|
1526
|
+
deadlineAt: f.optional(
|
|
1527
|
+
f.string({ desc: "New absolute deadline (ISO 8601). Wins over deadlineHours." })
|
|
1528
|
+
),
|
|
1529
|
+
addImpactedCards: f.optional(
|
|
1530
|
+
f.array(f.string(), { desc: "Slugs or ids of cards to block on this decision" })
|
|
1531
|
+
),
|
|
1532
|
+
removeImpactedCards: f.optional(
|
|
1533
|
+
f.array(f.string(), { desc: "Ids of cards to stop blocking" })
|
|
1534
|
+
),
|
|
1535
|
+
status: f.optional(
|
|
1536
|
+
f.enum(["Cancelled", "Open"], { desc: "Cancel it, or reopen a settled one" })
|
|
1537
|
+
)
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
var decisionsContracts = [
|
|
1542
|
+
createDecisionContract,
|
|
1543
|
+
listDecisionsContract,
|
|
1544
|
+
getDecisionContract,
|
|
1545
|
+
voteDecisionContract,
|
|
1546
|
+
resolveDecisionContract,
|
|
1547
|
+
updateDecisionContract
|
|
1548
|
+
];
|
|
1549
|
+
var createPullRequestContract = defineToolContract({
|
|
1550
|
+
name: "create_pull_request",
|
|
1551
|
+
agent: {
|
|
1552
|
+
description: "Create a GitHub PR for this task. Auto-stages, commits (commitMessage or title default), pushes to origin, then opens the PR. Always use this instead of gh CLI or raw git.",
|
|
1553
|
+
fields: {
|
|
1554
|
+
title: f.string({ desc: "The PR title" }),
|
|
1555
|
+
body: f.string({
|
|
1556
|
+
desc: "The PR description/body in markdown. If the diff changes anything a user sees rendered, the body MUST show the visual proof inline, not just point at the card: upload the capture with upload_attachment, then embed the downloadUrl it returns under a '## Screenshots' heading ( for images, [caption](url) for video). Only image and video URLs belong in a PR body: a document downloadUrl (csv, pdf, md, \u2026) needs a signed-in project member and shows 404 to a reviewer on GitHub, so point at the card instead. Attach to the card as well \u2014 the card is the durable home, the PR body is what the reviewer reads. Showcase attachments (uploaded with `showcase: true`) are appended under '## Showcase' automatically when Conveyor creates the PR; mention them inline yourself only if you want them next to specific text."
|
|
1557
|
+
}),
|
|
1558
|
+
branch: f.optional(
|
|
1559
|
+
f.string({
|
|
1560
|
+
desc: "The head branch name for the PR. Defaults to the workspace's current checkout, which is also the branch the commits are pushed to. Pass it explicitly if you pushed to a different branch, or if the card may have been renamed (a rename can re-slug the task's stored branch away from the one you are working on)."
|
|
1561
|
+
})
|
|
1562
|
+
),
|
|
1563
|
+
baseBranch: f.optional(
|
|
1564
|
+
f.string({
|
|
1565
|
+
desc: "The base branch to target for the PR (e.g. 'main', 'develop'). Defaults to the project's configured dev branch."
|
|
1566
|
+
})
|
|
1567
|
+
),
|
|
1568
|
+
commitMessage: f.optional(
|
|
1569
|
+
f.string({
|
|
1570
|
+
desc: "Commit message for staging uncommitted changes. If not provided, a default message based on the PR title will be used."
|
|
1571
|
+
})
|
|
1572
|
+
),
|
|
1573
|
+
skipVerify: f.optional(
|
|
1574
|
+
f.boolean({
|
|
1575
|
+
desc: "Controls the local pre-push quality gate (lint/typecheck/test). Defaults to true (--no-verify): the push skips the local gate because you should run gates yourself before opening the PR and CI re-runs them on the resulting PR. Running the full gate synchronously during the push would block the agent's event loop long enough to drop the Conveyor socket connection. Pass false to force the local pre-push hook to run."
|
|
1576
|
+
})
|
|
1577
|
+
)
|
|
1578
|
+
}
|
|
1579
|
+
},
|
|
1580
|
+
mcp: {
|
|
1581
|
+
description: "Open a GitHub pull request for a task's existing branch (the branch must already be pushed to origin). Pass projectId to target a specific project; otherwise the configured default project is used. Moves the task to ReviewPR. Returns the PR number and URL.",
|
|
1582
|
+
fields: {
|
|
1583
|
+
projectId: mcpProjectId,
|
|
1584
|
+
taskId: f.string({ desc: "The task ID whose branch should be opened as a PR" }),
|
|
1585
|
+
title: f.string({ desc: "Pull request title" }),
|
|
1586
|
+
body: f.string({
|
|
1587
|
+
desc: "Pull request body (markdown). For a diff that changes rendered UI, embed the visual proof inline: upload the capture with upload_attachment and paste the downloadUrl it returns as , in addition to leaving it on the card. Showcase attachments (uploaded with `showcase: true`) are appended under '## Showcase' automatically when Conveyor creates the PR; mention them inline yourself only if you want them next to specific text."
|
|
1588
|
+
}),
|
|
1589
|
+
head: f.optional(
|
|
1590
|
+
f.string({ desc: "Source branch for the PR (defaults to the task's branch)" })
|
|
1591
|
+
),
|
|
1592
|
+
base: f.optional(
|
|
1593
|
+
f.string({ desc: "Target branch for the PR (defaults to the repo default)" })
|
|
1594
|
+
)
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
var pullRequestContracts = [createPullRequestContract];
|
|
1599
|
+
var REGISTRY_RULE = "Only channels an admin registered as work channels in project settings are reachable \u2014 an unregistered channel is invisible here no matter what the bot can see in the workspace, and the card-sync channels are NOT automatically included.";
|
|
1600
|
+
var NO_STORAGE = "Messages are fetched live from Slack/Discord on every call and are never stored by Conveyor.";
|
|
1601
|
+
var listProjectIntegrationsContract = defineToolContract({
|
|
1602
|
+
name: "list_project_integrations",
|
|
1603
|
+
agent: {
|
|
1604
|
+
description: `Find out what this project is connected to before assuming a capability exists \u2014 repository forge, Slack or Discord, email, GCP, Grafana, Google Analytics, Google Drive, Cloudflare, and whether incident reporting is set up. Also lists the registered work channels you may read or post in, and how many meetings have been ingested \u2014 so you can tell whether reading meetings is worth doing before calling a meeting tool. Credential-free: every value says whether something is configured, never what the secret is. Call this when you are about to reach for an integration and want to know whether it is there, rather than trying and handling a failure.`,
|
|
1605
|
+
fields: {}
|
|
1606
|
+
},
|
|
1607
|
+
mcp: {
|
|
1608
|
+
description: `Report which integrations a project has configured \u2014 repository forge, Slack/Discord, email, GCP, Grafana, Google Analytics, Google Drive, Cloudflare, incident reporting \u2014 plus the work channels registered for agent access and the project's ingested-meeting count. Returns configuration booleans only, never credential material. Pass projectId to target a specific project; otherwise the configured default project is used.`,
|
|
1609
|
+
fields: {
|
|
1610
|
+
projectId: mcpProjectId
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
var listProjectChannelsContract = defineToolContract({
|
|
1615
|
+
name: "list_project_channels",
|
|
1616
|
+
agent: {
|
|
1617
|
+
description: `List the Slack or Discord channels this project has registered as work channels, each with the admin's description of what happens there and whether you may read it and post into it. Start here before read_channel_messages \u2014 the description is what tells you which channel is worth consulting. ${REGISTRY_RULE}`,
|
|
1618
|
+
fields: {}
|
|
1619
|
+
},
|
|
1620
|
+
mcp: {
|
|
1621
|
+
description: `List a project's registered work channels: provider, channel id and name, the admin's description of what the channel is for, and the allowRead/allowPost grants. ${REGISTRY_RULE} Pass projectId to target a specific project; otherwise the configured default project is used.`,
|
|
1622
|
+
fields: {
|
|
1623
|
+
projectId: mcpProjectId
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
var CHANNEL_ID_DESC = "Provider channel id, exactly as list_project_channels reports it (Slack `C\u2026`, Discord a numeric id). Not the channel name.";
|
|
1628
|
+
var LIMIT_DESC = "How many messages to return, newest first. Defaults to 25, maximum 50. Prefer a small read plus a follow-up over one large one \u2014 channel history spends context fast.";
|
|
1629
|
+
var BEFORE_DESC = "Page further back: return messages older than this cursor. Pass the `olderCursor` from a previous call; a null cursor there means you have reached the start of the channel.";
|
|
1630
|
+
var AFTER_DESC = "Return only messages newer than this cursor. Use to catch up on a channel.";
|
|
1631
|
+
var THREAD_DESC = "Read one thread instead of the channel surface. Pass a `threadTs` seen on a message from a previous read.";
|
|
1632
|
+
var readChannelMessagesContract = defineToolContract({
|
|
1633
|
+
name: "read_channel_messages",
|
|
1634
|
+
agent: {
|
|
1635
|
+
description: `Read recent messages from a registered work channel, newest first \u2014 to catch up on a discussion, find the context behind a decision, or check whether something was already raised. Each message reports its author, whether that author is a bot (Conveyor's own card feed posts show up here too, so check this before treating a message as a teammate's), its text, and a cursor for paging further back. ${REGISTRY_RULE} ${NO_STORAGE}`,
|
|
1636
|
+
fields: {
|
|
1637
|
+
channelId: f.string({ desc: CHANNEL_ID_DESC }),
|
|
1638
|
+
limit: f.optional(f.number({ desc: LIMIT_DESC, int: true, min: 1, max: 50 })),
|
|
1639
|
+
before: f.optional(f.string({ desc: BEFORE_DESC })),
|
|
1640
|
+
after: f.optional(f.string({ desc: AFTER_DESC })),
|
|
1641
|
+
threadTs: f.optional(f.string({ desc: THREAD_DESC }))
|
|
1642
|
+
}
|
|
1643
|
+
},
|
|
1644
|
+
mcp: {
|
|
1645
|
+
description: `Read recent messages from a project's registered work channel, newest first. Each message carries author name, a bot flag, text, a provider-native cursor, and a timestamp; the response carries an olderCursor for paging back. ${REGISTRY_RULE} ${NO_STORAGE} Pass projectId to target a specific project; otherwise the configured default project is used.`,
|
|
1646
|
+
fields: {
|
|
1647
|
+
projectId: mcpProjectId,
|
|
1648
|
+
channelId: f.string({ desc: CHANNEL_ID_DESC }),
|
|
1649
|
+
limit: f.optional(f.number({ desc: LIMIT_DESC, int: true, min: 1, max: 50 })),
|
|
1650
|
+
before: f.optional(f.string({ desc: BEFORE_DESC })),
|
|
1651
|
+
after: f.optional(f.string({ desc: AFTER_DESC })),
|
|
1652
|
+
threadTs: f.optional(f.string({ desc: THREAD_DESC }))
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1656
|
+
var POST_TEXT_DESC = "What to say, in plain text or light markdown. An attribution footer naming you is appended automatically, so do not sign the message yourself. Channel-wide pings (@everyone, @here, <!channel>) are rendered as ordinary text and will not notify anyone.";
|
|
1657
|
+
var POST_THREAD_DESC = "Reply inside a thread rather than to the channel. Prefer this when responding to a specific message \u2014 pass the `threadTs` from the message you are answering.";
|
|
1658
|
+
var POST_WARNING = "This is visible to everyone in the channel and cannot be edited or deleted afterwards. Posting requires a per-channel grant that is OFF by default, separate from read access, so most registered channels will refuse it.";
|
|
1659
|
+
var postChannelMessageContract = defineToolContract({
|
|
1660
|
+
name: "post_channel_message",
|
|
1661
|
+
agent: {
|
|
1662
|
+
description: `Post a message into a registered work channel \u2014 to answer a question aimed at you, report something the team is waiting on, or ask for input you genuinely need. ${POST_WARNING} ${REGISTRY_RULE}`,
|
|
1663
|
+
fields: {
|
|
1664
|
+
channelId: f.string({ desc: CHANNEL_ID_DESC }),
|
|
1665
|
+
text: f.string({ desc: POST_TEXT_DESC, min: 1, max: 1800 }),
|
|
1666
|
+
threadTs: f.optional(f.string({ desc: POST_THREAD_DESC }))
|
|
1667
|
+
}
|
|
1668
|
+
},
|
|
1669
|
+
mcp: {
|
|
1670
|
+
description: `Post a message into a project's registered work channel, attributed to the acting agent or user. ${POST_WARNING} ${REGISTRY_RULE} Pass projectId to target a specific project; otherwise the configured default project is used.`,
|
|
1671
|
+
fields: {
|
|
1672
|
+
projectId: mcpProjectId,
|
|
1673
|
+
channelId: f.string({ desc: CHANNEL_ID_DESC }),
|
|
1674
|
+
text: f.string({ desc: POST_TEXT_DESC, min: 1, max: 1800 }),
|
|
1675
|
+
threadTs: f.optional(f.string({ desc: POST_THREAD_DESC }))
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
var ANALYTICS_RANGE = "How many days back to summarize, ending today. Defaults to 28; 90 is the maximum, because GA4 retention makes longer windows unreliable.";
|
|
1680
|
+
var ANALYTICS_CAMPAIGN = "Restrict to one campaign name. The campaigns breakdown is always unfiltered, so read it first to see what exists.";
|
|
1681
|
+
var ANALYTICS_SHAPE = "Returns headline totals (sessions, active users, new users, page views, average session duration, bounce rate) plus the top five pages, traffic sources, landing pages and campaigns. A project that has not set up GA4 answers configured:false with a reason rather than failing \u2014 that is a normal state, not an error.";
|
|
1682
|
+
var getAnalyticsSummaryContract = defineToolContract({
|
|
1683
|
+
name: "get_analytics_summary",
|
|
1684
|
+
agent: {
|
|
1685
|
+
description: `Read this project's Google Analytics 4 traffic \u2014 real numbers for questions like "did that launch land" or "which pages actually get read", instead of guessing. ${ANALYTICS_SHAPE}`,
|
|
1686
|
+
fields: {
|
|
1687
|
+
rangeDays: f.optional(f.number({ desc: ANALYTICS_RANGE, int: true, min: 1, max: 90 })),
|
|
1688
|
+
campaign: f.optional(f.string({ desc: ANALYTICS_CAMPAIGN, max: 200 }))
|
|
1689
|
+
}
|
|
1690
|
+
},
|
|
1691
|
+
mcp: {
|
|
1692
|
+
description: `Read a project's Google Analytics 4 traffic summary. ${ANALYTICS_SHAPE} Pass projectId to target a specific project; otherwise the configured default project is used.`,
|
|
1693
|
+
fields: {
|
|
1694
|
+
projectId: mcpProjectId,
|
|
1695
|
+
rangeDays: f.optional(f.number({ desc: ANALYTICS_RANGE, int: true, min: 1, max: 90 })),
|
|
1696
|
+
campaign: f.optional(f.string({ desc: ANALYTICS_CAMPAIGN, max: 200 }))
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
var integrationsContracts = [
|
|
1701
|
+
listProjectIntegrationsContract,
|
|
1702
|
+
listProjectChannelsContract,
|
|
1703
|
+
readChannelMessagesContract,
|
|
1704
|
+
postChannelMessageContract,
|
|
1705
|
+
getAnalyticsSummaryContract
|
|
1706
|
+
];
|
|
1707
|
+
var MAX_CONTENT_CHARS = 1e6;
|
|
1708
|
+
var FILE_ID = "Drive file id, as returned by drive_list_files";
|
|
1709
|
+
var ROOT_DEFAULT = "Defaults to the project's connected root folder.";
|
|
1710
|
+
var MCP_PROJECT_TAIL = "Pass projectId to target a specific project; otherwise the configured default project is used.";
|
|
1711
|
+
var driveListFilesContract = defineToolContract({
|
|
1712
|
+
name: "drive_list_files",
|
|
1713
|
+
agent: {
|
|
1714
|
+
description: "List files and folders in the project's connected Google Drive folder. Omit folderId to list the project's root folder. Returns id, name, mimeType, size, and modified time for each entry.",
|
|
1715
|
+
fields: {
|
|
1716
|
+
folderId: f.optional(f.string({ desc: `Folder to list. ${ROOT_DEFAULT}` })),
|
|
1717
|
+
search: f.optional(f.string({ desc: "Only return names containing this text", max: 200 })),
|
|
1718
|
+
limit: f.optional(
|
|
1719
|
+
f.number({ desc: "Max entries (default 100)", int: true, min: 1, max: 200 })
|
|
1720
|
+
)
|
|
1721
|
+
}
|
|
1722
|
+
},
|
|
1723
|
+
mcp: {
|
|
1724
|
+
description: `List files and folders in a project's connected Google Drive folder. Omit folderId to list the project's root folder. Returns id, name, mimeType, size, and modified time for each entry. ${MCP_PROJECT_TAIL}`,
|
|
1725
|
+
fields: {
|
|
1726
|
+
projectId: mcpProjectId,
|
|
1727
|
+
folderId: f.optional(f.string({ desc: `Folder to list. ${ROOT_DEFAULT}` })),
|
|
1728
|
+
search: f.optional(f.string({ desc: "Only return names containing this text", max: 200 })),
|
|
1729
|
+
limit: f.optional(
|
|
1730
|
+
f.number({ desc: "Max entries (default 100)", int: true, min: 1, max: 200 })
|
|
1731
|
+
)
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
var driveReadFileContract = defineToolContract({
|
|
1736
|
+
name: "drive_read_file",
|
|
1737
|
+
agent: {
|
|
1738
|
+
description: "Read a file's text content from the project's connected Google Drive folder. Google Docs, Sheets, and Slides are exported to text automatically. Content over 100 KB is truncated.",
|
|
1739
|
+
fields: { fileId: f.string({ desc: FILE_ID }) }
|
|
1740
|
+
},
|
|
1741
|
+
mcp: {
|
|
1742
|
+
description: `Read a file's text content from a project's connected Google Drive folder. Google Docs, Sheets, and Slides are exported to text automatically. Content over 100 KB is truncated. ${MCP_PROJECT_TAIL}`,
|
|
1743
|
+
fields: { projectId: mcpProjectId, fileId: f.string({ desc: FILE_ID }) }
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
var driveCreateFileContract = defineToolContract({
|
|
1747
|
+
name: "drive_create_file",
|
|
1748
|
+
agent: {
|
|
1749
|
+
description: "Create a new file in the project's connected Google Drive folder. Use drive_update_file to change an existing file instead.",
|
|
1750
|
+
fields: {
|
|
1751
|
+
name: f.string({ desc: "File name, without any path separators", min: 1, max: 255 }),
|
|
1752
|
+
content: f.string({ desc: "File content, UTF-8 text", max: MAX_CONTENT_CHARS }),
|
|
1753
|
+
mimeType: f.optional(f.string({ desc: "MIME type (default text/plain)" })),
|
|
1754
|
+
folderId: f.optional(f.string({ desc: `Destination folder. ${ROOT_DEFAULT}` }))
|
|
1755
|
+
}
|
|
1756
|
+
},
|
|
1757
|
+
mcp: {
|
|
1758
|
+
description: `Create a new file in a project's connected Google Drive folder. Use drive_update_file to change an existing file instead. ${MCP_PROJECT_TAIL}`,
|
|
1759
|
+
fields: {
|
|
1760
|
+
projectId: mcpProjectId,
|
|
1761
|
+
name: f.string({ desc: "File name, without any path separators", min: 1, max: 255 }),
|
|
1762
|
+
content: f.string({ desc: "File content, UTF-8 text", max: MAX_CONTENT_CHARS }),
|
|
1763
|
+
mimeType: f.optional(f.string({ desc: "MIME type (default text/plain)" })),
|
|
1764
|
+
folderId: f.optional(f.string({ desc: `Destination folder. ${ROOT_DEFAULT}` }))
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
});
|
|
1768
|
+
var driveUpdateFileContract = defineToolContract({
|
|
1769
|
+
name: "drive_update_file",
|
|
1770
|
+
agent: {
|
|
1771
|
+
description: "Replace the content of an existing file in the project's connected Google Drive folder. This overwrites the whole file. Google-native documents cannot be overwritten.",
|
|
1772
|
+
fields: {
|
|
1773
|
+
fileId: f.string({ desc: FILE_ID }),
|
|
1774
|
+
content: f.string({ desc: "Replacement content, UTF-8 text", max: MAX_CONTENT_CHARS }),
|
|
1775
|
+
mimeType: f.optional(f.string({ desc: "MIME type (defaults to the file's current type)" }))
|
|
1776
|
+
}
|
|
1777
|
+
},
|
|
1778
|
+
mcp: {
|
|
1779
|
+
description: `Replace the content of an existing file in a project's connected Google Drive folder. This overwrites the whole file \u2014 read it first if you are editing rather than replacing. Google-native documents cannot be overwritten. ${MCP_PROJECT_TAIL}`,
|
|
1780
|
+
fields: {
|
|
1781
|
+
projectId: mcpProjectId,
|
|
1782
|
+
fileId: f.string({ desc: FILE_ID }),
|
|
1783
|
+
content: f.string({ desc: "Replacement content, UTF-8 text", max: MAX_CONTENT_CHARS }),
|
|
1784
|
+
mimeType: f.optional(f.string({ desc: "MIME type (defaults to the file's current type)" }))
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
var driveDeleteFileContract = defineToolContract({
|
|
1789
|
+
name: "drive_delete_file",
|
|
1790
|
+
agent: {
|
|
1791
|
+
description: "Move a file in the project's connected Google Drive folder to the Drive trash. The file is recoverable from the trash; it is never permanently deleted.",
|
|
1792
|
+
fields: { fileId: f.string({ desc: FILE_ID }) }
|
|
1793
|
+
},
|
|
1794
|
+
mcp: {
|
|
1795
|
+
description: `Move a file in a project's connected Google Drive folder to the Drive trash. The file is recoverable from the trash; it is never permanently deleted. ${MCP_PROJECT_TAIL}`,
|
|
1796
|
+
fields: { projectId: mcpProjectId, fileId: f.string({ desc: FILE_ID }) }
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
var driveCreateFolderContract = defineToolContract({
|
|
1800
|
+
name: "drive_create_folder",
|
|
1801
|
+
agent: {
|
|
1802
|
+
description: "Create a folder inside the project's connected Google Drive folder.",
|
|
1803
|
+
fields: {
|
|
1804
|
+
name: f.string({ desc: "Folder name, without any path separators", min: 1, max: 255 }),
|
|
1805
|
+
folderId: f.optional(f.string({ desc: `Parent folder. ${ROOT_DEFAULT}` }))
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
mcp: {
|
|
1809
|
+
description: `Create a folder inside a project's connected Google Drive folder. ${MCP_PROJECT_TAIL}`,
|
|
1810
|
+
fields: {
|
|
1811
|
+
projectId: mcpProjectId,
|
|
1812
|
+
name: f.string({ desc: "Folder name, without any path separators", min: 1, max: 255 }),
|
|
1813
|
+
folderId: f.optional(f.string({ desc: `Parent folder. ${ROOT_DEFAULT}` }))
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
var driveContracts = [
|
|
1818
|
+
driveListFilesContract,
|
|
1819
|
+
driveReadFileContract,
|
|
1820
|
+
driveCreateFileContract,
|
|
1821
|
+
driveUpdateFileContract,
|
|
1822
|
+
driveDeleteFileContract,
|
|
1823
|
+
driveCreateFolderContract
|
|
1824
|
+
];
|
|
1825
|
+
var MEETING_ID = "Meeting id, as returned by list_meetings.";
|
|
1826
|
+
var MCP_TAIL = "Pass projectId to target a specific project; otherwise the configured default project is used.";
|
|
1827
|
+
var SUMMARY_NOTE = 'A meeting whose summary is still being written reports status "processing" and a null summary \u2014 that is normal shortly after ingestion, not an error.';
|
|
1828
|
+
var listMeetingsContract = defineToolContract({
|
|
1829
|
+
name: "list_meetings",
|
|
1830
|
+
agent: {
|
|
1831
|
+
description: `List this project's meetings, newest first \u2014 each with its title, date, source, status, participants, and a short summary preview. Start here when you are asked about "the meeting", "what did we decide", or "what came out of that call". ${SUMMARY_NOTE}`,
|
|
1832
|
+
fields: {
|
|
1833
|
+
limit: f.optional(
|
|
1834
|
+
f.number({
|
|
1835
|
+
desc: "How many to return (default 20, maximum 50).",
|
|
1836
|
+
int: true,
|
|
1837
|
+
min: 1,
|
|
1838
|
+
max: 50
|
|
1839
|
+
})
|
|
1840
|
+
),
|
|
1841
|
+
search: f.optional(
|
|
1842
|
+
f.string({ desc: "Case-insensitive match on the meeting title.", max: 200 })
|
|
1843
|
+
)
|
|
1844
|
+
}
|
|
1845
|
+
},
|
|
1846
|
+
mcp: {
|
|
1847
|
+
description: `List a project's meetings, newest first, with title, date, source, status, participants, and a summary preview. ${SUMMARY_NOTE} ${MCP_TAIL}`,
|
|
1848
|
+
fields: {
|
|
1849
|
+
projectId: mcpProjectId,
|
|
1850
|
+
limit: f.optional(
|
|
1851
|
+
f.number({
|
|
1852
|
+
desc: "How many to return (default 20, maximum 50).",
|
|
1853
|
+
int: true,
|
|
1854
|
+
min: 1,
|
|
1855
|
+
max: 50
|
|
1856
|
+
})
|
|
1857
|
+
),
|
|
1858
|
+
search: f.optional(
|
|
1859
|
+
f.string({ desc: "Case-insensitive match on the meeting title.", max: 200 })
|
|
1860
|
+
)
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
});
|
|
1864
|
+
var getMeetingContract = defineToolContract({
|
|
1865
|
+
name: "get_meeting",
|
|
1866
|
+
agent: {
|
|
1867
|
+
description: `Read one meeting's details and its FULL AI summary \u2014 the overview, the decisions, and the proposed next steps. **Read this before reaching for the transcript**: the summary usually answers the question, and a transcript costs far more context. Also returns the participant list, the segment count, and the meeting's web link. ${SUMMARY_NOTE}`,
|
|
1868
|
+
fields: { meetingId: f.string({ desc: MEETING_ID }) }
|
|
1869
|
+
},
|
|
1870
|
+
mcp: {
|
|
1871
|
+
description: `Read one meeting's details and full AI summary (overview, decisions, proposed next steps), plus participants, segment count, and its web link. Prefer this over the transcript \u2014 the summary usually answers the question at a fraction of the context. ${SUMMARY_NOTE} ${MCP_TAIL}`,
|
|
1872
|
+
fields: { projectId: mcpProjectId, meetingId: f.string({ desc: MEETING_ID }) }
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
var OFFSET_DESC = "Segment index to start from (default 0). Pass the nextOffset from the previous page to continue.";
|
|
1876
|
+
var LIMIT_DESC2 = "Segments per page (default 200, maximum 500). A long meeting runs to thousands, so page it rather than asking for everything.";
|
|
1877
|
+
var readMeetingTranscriptContract = defineToolContract({
|
|
1878
|
+
name: "read_meeting_transcript",
|
|
1879
|
+
agent: {
|
|
1880
|
+
description: `Read a page of a meeting's raw transcript as \`[hh:mm:ss] Speaker: text\` lines. Use this when you need someone's exact words \u2014 a quote, a caveat, or the reasoning behind a decision the summary only states. For "what happened in the meeting", read get_meeting instead. The response carries nextOffset when more segments remain.`,
|
|
1881
|
+
fields: {
|
|
1882
|
+
meetingId: f.string({ desc: MEETING_ID }),
|
|
1883
|
+
offset: f.optional(f.number({ desc: OFFSET_DESC, int: true, min: 0 })),
|
|
1884
|
+
limit: f.optional(f.number({ desc: LIMIT_DESC2, int: true, min: 1, max: 500 }))
|
|
1885
|
+
}
|
|
1886
|
+
},
|
|
1887
|
+
mcp: {
|
|
1888
|
+
description: `Read a page of a meeting's raw transcript as \`[hh:mm:ss] Speaker: text\` lines. Use it for exact wording; prefer get_meeting's summary for what happened. The response carries nextOffset when more segments remain. ${MCP_TAIL}`,
|
|
1889
|
+
fields: {
|
|
1890
|
+
projectId: mcpProjectId,
|
|
1891
|
+
meetingId: f.string({ desc: MEETING_ID }),
|
|
1892
|
+
offset: f.optional(f.number({ desc: OFFSET_DESC, int: true, min: 0 })),
|
|
1893
|
+
limit: f.optional(f.number({ desc: LIMIT_DESC2, int: true, min: 1, max: 500 }))
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
var RAW_TEXT_DESC = "The transcript itself. Plain text with `Speaker: line` labels, or a WebVTT/SRT file's contents \u2014 the format is detected, and the speaker labels become the participant list. Paste the whole thing; it is stored as the meeting's record.";
|
|
1898
|
+
var OCCURRED_AT_DESC = "When the meeting happened, ISO 8601 (e.g. 2026-09-02T15:00:00Z). Defaults to now. Must be a real date \u2014 no earlier than 2000, and no more than 48 hours ahead.";
|
|
1899
|
+
var TITLE_DESC = "Meeting title. Defaults to `Meeting <date>` when you do not name one.";
|
|
1900
|
+
var createMeetingContract = defineToolContract({
|
|
1901
|
+
name: "create_meeting",
|
|
1902
|
+
agent: {
|
|
1903
|
+
description: `File a transcript as a project meeting. Use this when you have the text of a call \u2014 a paste, an export, notes captured elsewhere \u2014 and it belongs in the project's record. The transcript is parsed into speaker segments and an AI summary is written straight after, so the meeting reads "processing" for a few seconds before its overview appears. This creates a MEETING, not cards: turning what was decided into work is still create_task's job.`,
|
|
1904
|
+
fields: {
|
|
1905
|
+
rawText: f.string({ desc: RAW_TEXT_DESC, min: 1 }),
|
|
1906
|
+
title: f.optional(f.string({ desc: TITLE_DESC, max: 200 })),
|
|
1907
|
+
occurredAt: f.optional(f.string({ desc: OCCURRED_AT_DESC }))
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
mcp: {
|
|
1911
|
+
description: `File a transcript as a project meeting. The text is parsed into speaker segments and summarized automatically, so the meeting reports status "processing" briefly before its overview lands. Creates a meeting only \u2014 use the card tools to turn decisions into work. ${MCP_TAIL}`,
|
|
1912
|
+
fields: {
|
|
1913
|
+
projectId: mcpProjectId,
|
|
1914
|
+
rawText: f.string({ desc: RAW_TEXT_DESC, min: 1 }),
|
|
1915
|
+
title: f.optional(f.string({ desc: TITLE_DESC, max: 200 })),
|
|
1916
|
+
occurredAt: f.optional(f.string({ desc: OCCURRED_AT_DESC }))
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
var SUMMARY_DESC = "Replace the meeting's overview with this text. Markdown, and it is what every reader sees from then on \u2014 write the whole overview, not a note about it. Writing a summary marks the meeting ready, so a later regenerate is the only thing that overwrites it.";
|
|
1921
|
+
var updateMeetingContract = defineToolContract({
|
|
1922
|
+
name: "update_meeting",
|
|
1923
|
+
agent: {
|
|
1924
|
+
description: `Correct a meeting's title or date, or replace its AI summary with a better one. This is how a rewritten overview actually persists \u2014 read the transcript, write the summary you want, and pass it here. Pass at least one field; the ones you leave out are untouched.`,
|
|
1925
|
+
fields: {
|
|
1926
|
+
meetingId: f.string({ desc: MEETING_ID }),
|
|
1927
|
+
title: f.optional(f.string({ desc: TITLE_DESC, max: 200 })),
|
|
1928
|
+
occurredAt: f.optional(f.string({ desc: OCCURRED_AT_DESC })),
|
|
1929
|
+
summary: f.optional(f.string({ desc: SUMMARY_DESC, min: 1 }))
|
|
1930
|
+
}
|
|
1931
|
+
},
|
|
1932
|
+
mcp: {
|
|
1933
|
+
description: `Correct a meeting's title or date, or replace its AI summary with a rewritten one. Pass at least one field; omitted fields are left alone. ${MCP_TAIL}`,
|
|
1934
|
+
fields: {
|
|
1935
|
+
projectId: mcpProjectId,
|
|
1936
|
+
meetingId: f.string({ desc: MEETING_ID }),
|
|
1937
|
+
title: f.optional(f.string({ desc: TITLE_DESC, max: 200 })),
|
|
1938
|
+
occurredAt: f.optional(f.string({ desc: OCCURRED_AT_DESC })),
|
|
1939
|
+
summary: f.optional(f.string({ desc: SUMMARY_DESC, min: 1 }))
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
var CHECKLIST_ITEM_TITLE = "The item's exact title, as get_meeting lists it. Matched case-insensitively.";
|
|
1944
|
+
var CHECKLIST_INTRO = "A meeting's checklist is its follow-ups: one line each, ticked when done, optionally pointing at the Conveyor card that carries the work. The AI summary's proposed next steps are seeded here automatically.";
|
|
1945
|
+
var addMeetingChecklistItemsContract = defineToolContract({
|
|
1946
|
+
name: "add_meeting_checklist_items",
|
|
1947
|
+
agent: {
|
|
1948
|
+
description: `Add follow-up items to a meeting's checklist. ${CHECKLIST_INTRO} Use this for a step the summary missed, or one that came out of a later conversation. Items whose title already exists are skipped, so re-running this is safe.`,
|
|
1949
|
+
fields: {
|
|
1950
|
+
meetingId: f.string({ desc: MEETING_ID }),
|
|
1951
|
+
items: f.array(
|
|
1952
|
+
f.object({ title: f.string({ desc: "One follow-up, as a single line.", min: 1 }) }),
|
|
1953
|
+
{ desc: "The items to add, in the order they should appear." }
|
|
1954
|
+
)
|
|
1955
|
+
}
|
|
1956
|
+
},
|
|
1957
|
+
mcp: {
|
|
1958
|
+
description: `Add follow-up items to a meeting's checklist. ${CHECKLIST_INTRO} Existing titles are skipped, so re-running is safe. ${MCP_TAIL}`,
|
|
1959
|
+
fields: {
|
|
1960
|
+
projectId: mcpProjectId,
|
|
1961
|
+
meetingId: f.string({ desc: MEETING_ID }),
|
|
1962
|
+
items: f.array(
|
|
1963
|
+
f.object({ title: f.string({ desc: "One follow-up, as a single line.", min: 1 }) }),
|
|
1964
|
+
{ desc: "The items to add, in the order they should appear." }
|
|
1965
|
+
)
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
var LINKED_TASK_DESC = "Card id or slug to attach to this item \u2014 the card that carries the work. Must be in the same project. Pass it in the same call that ticks the item when you just filed the card.";
|
|
1970
|
+
var checkMeetingChecklistItemContract = defineToolContract({
|
|
1971
|
+
name: "check_meeting_checklist_item",
|
|
1972
|
+
agent: {
|
|
1973
|
+
description: `Tick or untick one of a meeting's checklist items, and optionally attach the card that carries it. This is how a next step becomes tracked work: file the card with create_task, then call this with the item's title and the new card's slug. Ticking records YOU as the person who did it. Unticking clears that, but keeps the linked card.`,
|
|
1974
|
+
fields: {
|
|
1975
|
+
meetingId: f.string({ desc: MEETING_ID }),
|
|
1976
|
+
title: f.string({ desc: CHECKLIST_ITEM_TITLE }),
|
|
1977
|
+
checked: f.boolean({ desc: "true to tick the item, false to untick it." }),
|
|
1978
|
+
linkedTask: f.optional(f.string({ desc: LINKED_TASK_DESC }))
|
|
1979
|
+
}
|
|
1980
|
+
},
|
|
1981
|
+
mcp: {
|
|
1982
|
+
description: `Tick or untick a meeting checklist item, optionally attaching the card that carries it. Ticking records the acting user. Unticking clears that but keeps the link. ${MCP_TAIL}`,
|
|
1983
|
+
fields: {
|
|
1984
|
+
projectId: mcpProjectId,
|
|
1985
|
+
meetingId: f.string({ desc: MEETING_ID }),
|
|
1986
|
+
title: f.string({ desc: CHECKLIST_ITEM_TITLE }),
|
|
1987
|
+
checked: f.boolean({ desc: "true to tick the item, false to untick it." }),
|
|
1988
|
+
linkedTask: f.optional(f.string({ desc: LINKED_TASK_DESC }))
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
var editMeetingChecklistItemContract = defineToolContract({
|
|
1993
|
+
name: "edit_meeting_checklist_item",
|
|
1994
|
+
agent: {
|
|
1995
|
+
description: `Reword one of a meeting's checklist items. Use it to sharpen a vague next step into something someone can act on; to mark it done use check_meeting_checklist_item instead.`,
|
|
1996
|
+
fields: {
|
|
1997
|
+
meetingId: f.string({ desc: MEETING_ID }),
|
|
1998
|
+
title: f.string({ desc: CHECKLIST_ITEM_TITLE }),
|
|
1999
|
+
newTitle: f.string({ desc: "The replacement text, as a single line.", min: 1 })
|
|
2000
|
+
}
|
|
2001
|
+
},
|
|
2002
|
+
mcp: {
|
|
2003
|
+
description: `Reword a meeting checklist item. To mark it done use check_meeting_checklist_item. ${MCP_TAIL}`,
|
|
2004
|
+
fields: {
|
|
2005
|
+
projectId: mcpProjectId,
|
|
2006
|
+
meetingId: f.string({ desc: MEETING_ID }),
|
|
2007
|
+
title: f.string({ desc: CHECKLIST_ITEM_TITLE }),
|
|
2008
|
+
newTitle: f.string({ desc: "The replacement text, as a single line.", min: 1 })
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
2012
|
+
var removeMeetingChecklistItemContract = defineToolContract({
|
|
2013
|
+
name: "remove_meeting_checklist_item",
|
|
2014
|
+
agent: {
|
|
2015
|
+
description: `Delete one of a meeting's checklist items. For a step that turned out not to be needed. An item someone already ticked is a record that work happened \u2014 untick it or leave it rather than deleting it.`,
|
|
2016
|
+
fields: {
|
|
2017
|
+
meetingId: f.string({ desc: MEETING_ID }),
|
|
2018
|
+
title: f.string({ desc: CHECKLIST_ITEM_TITLE })
|
|
2019
|
+
}
|
|
2020
|
+
},
|
|
2021
|
+
mcp: {
|
|
2022
|
+
description: `Delete a meeting checklist item. Prefer unticking over deleting an item someone already completed. ${MCP_TAIL}`,
|
|
2023
|
+
fields: {
|
|
2024
|
+
projectId: mcpProjectId,
|
|
2025
|
+
meetingId: f.string({ desc: MEETING_ID }),
|
|
2026
|
+
title: f.string({ desc: CHECKLIST_ITEM_TITLE })
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
var meetingsContracts = [
|
|
2031
|
+
listMeetingsContract,
|
|
2032
|
+
getMeetingContract,
|
|
2033
|
+
readMeetingTranscriptContract,
|
|
2034
|
+
createMeetingContract,
|
|
2035
|
+
updateMeetingContract,
|
|
2036
|
+
addMeetingChecklistItemsContract,
|
|
2037
|
+
checkMeetingChecklistItemContract,
|
|
2038
|
+
editMeetingChecklistItemContract,
|
|
2039
|
+
removeMeetingChecklistItemContract
|
|
2040
|
+
];
|
|
2041
|
+
var SINCE_MINUTES = f.optional(
|
|
2042
|
+
f.number({
|
|
2043
|
+
desc: "Relative time window ending now, in minutes (default 60). Ignored if startTime is set.",
|
|
2044
|
+
int: true,
|
|
2045
|
+
min: 1,
|
|
2046
|
+
max: 10080
|
|
2047
|
+
})
|
|
2048
|
+
);
|
|
2049
|
+
var START_TIME = f.optional(f.string({ desc: "ISO 8601 lower bound (overrides sinceMinutes)" }));
|
|
2050
|
+
var END_TIME = f.optional(f.string({ desc: "ISO 8601 upper bound (default now)" }));
|
|
2051
|
+
var LIMIT = f.optional(
|
|
2052
|
+
f.number({ desc: "Max entries per page (default 50)", int: true, min: 1, max: 200 })
|
|
2053
|
+
);
|
|
2054
|
+
var RAW_QUERY_MAX = 2e3;
|
|
2055
|
+
var gcpFields = {
|
|
2056
|
+
env: f.optional(
|
|
2057
|
+
f.enum(["prod", "dev", "claudespace"], {
|
|
2058
|
+
desc: "GCP environment slot to query (default prod)"
|
|
2059
|
+
})
|
|
2060
|
+
),
|
|
2061
|
+
sinceMinutes: SINCE_MINUTES,
|
|
2062
|
+
startTime: START_TIME,
|
|
2063
|
+
endTime: END_TIME,
|
|
2064
|
+
severity: f.optional(
|
|
2065
|
+
f.enum(SEVERITY_ENUM, {
|
|
2066
|
+
desc: "Minimum severity, inclusive \u2014 ERROR returns ERROR and above"
|
|
2067
|
+
})
|
|
2068
|
+
),
|
|
2069
|
+
services: f.optional(
|
|
2070
|
+
f.array(f.string(), {
|
|
2071
|
+
desc: "Restrict to these Cloud Run service names (prod/dev only). Defaults to all services linked in project settings."
|
|
2072
|
+
})
|
|
2073
|
+
),
|
|
2074
|
+
sqlInstances: f.optional(
|
|
2075
|
+
f.array(f.string(), { desc: "Restrict to these Cloud SQL instance names (prod/dev only)" })
|
|
2076
|
+
),
|
|
2077
|
+
allServices: f.optional(
|
|
2078
|
+
f.boolean({
|
|
2079
|
+
desc: "Set true to search ALL logs in the GCP project, ignoring the linked-resource scope"
|
|
2080
|
+
})
|
|
2081
|
+
),
|
|
2082
|
+
search: f.optional(
|
|
2083
|
+
f.string({
|
|
2084
|
+
desc: "Free-text search across all log fields (exact substring, not regex)",
|
|
2085
|
+
max: 256
|
|
2086
|
+
})
|
|
2087
|
+
),
|
|
2088
|
+
filter: f.optional(
|
|
2089
|
+
f.string({
|
|
2090
|
+
desc: "Advanced: raw Cloud Logging filter expression, ANDed with the scope (it cannot widen it)",
|
|
2091
|
+
max: RAW_QUERY_MAX
|
|
2092
|
+
})
|
|
2093
|
+
),
|
|
2094
|
+
limit: LIMIT,
|
|
2095
|
+
pageToken: f.optional(
|
|
2096
|
+
f.string({ desc: "Opaque token from a previous response to fetch the next page" })
|
|
2097
|
+
),
|
|
2098
|
+
full: f.optional(
|
|
2099
|
+
f.boolean({
|
|
2100
|
+
desc: 'Render each entry in full: message and structured payload untruncated, multi-line. Pair with a narrow search or filter (e.g. insertId="\u2026"); full mode returns at most 10 entries.'
|
|
2101
|
+
})
|
|
2102
|
+
)
|
|
2103
|
+
};
|
|
2104
|
+
var GCP_SHARED_DESC = "Envs: 'prod' and 'dev' are the project's Cloud Run apps + Cloud SQL databases (scoped by default to the resources linked in project settings); 'claudespace' is the project's GKE agent-pod namespace. Start broad (severity=ERROR, sinceMinutes=60), then narrow with services/search. Returns compact lines: '<time> <SEVERITY> [<source>] <message> | key=value \u2026' (the key=value tail is the entry's structured payload \u2014 error details, service/method, actor and entity ids). When the response ends with a pageToken line, pass that token back as pageToken for the next page. To read a whole entry (long message, full payload), re-query narrowly with full=true.";
|
|
2105
|
+
var queryGcpLogsContract = defineToolContract({
|
|
2106
|
+
name: "query_gcp_logs",
|
|
2107
|
+
agent: {
|
|
2108
|
+
description: `Query Google Cloud Logging for this project's linked GCP environments \u2014 use this to investigate a production or dev failure directly rather than guessing from the code. ${GCP_SHARED_DESC}`,
|
|
2109
|
+
fields: gcpFields
|
|
2110
|
+
},
|
|
2111
|
+
mcp: {
|
|
2112
|
+
description: `Query Google Cloud Logging for a project's linked GCP environments \u2014 use this to investigate production or dev issues directly ('something broke on prod'). ${GCP_SHARED_DESC} Pass projectId to target a specific project; otherwise the configured default project is used.`,
|
|
2113
|
+
fields: { projectId: mcpProjectId, ...gcpFields }
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
var grafanaFields = {
|
|
2117
|
+
env: f.optional(
|
|
2118
|
+
f.enum(["prod", "dev"], {
|
|
2119
|
+
desc: "Configured Grafana env mapping to scope by (default prod)"
|
|
2120
|
+
})
|
|
2121
|
+
),
|
|
2122
|
+
sinceMinutes: SINCE_MINUTES,
|
|
2123
|
+
startTime: START_TIME,
|
|
2124
|
+
endTime: END_TIME,
|
|
2125
|
+
level: f.optional(
|
|
2126
|
+
f.enum(["debug", "info", "warn", "error", "fatal"], {
|
|
2127
|
+
desc: "Minimum severity, inclusive \u2014 error returns error and above"
|
|
2128
|
+
})
|
|
2129
|
+
),
|
|
2130
|
+
services: f.optional(
|
|
2131
|
+
f.array(f.string(), { desc: "Restrict to these service_name label values" })
|
|
2132
|
+
),
|
|
2133
|
+
search: f.optional(
|
|
2134
|
+
f.string({ desc: "Substring line filter (exact substring, not regex)", max: 256 })
|
|
2135
|
+
),
|
|
2136
|
+
logql: f.optional(
|
|
2137
|
+
f.string({
|
|
2138
|
+
desc: "Advanced: raw LogQL query \u2014 REPLACES env/services/level/search composition",
|
|
2139
|
+
max: RAW_QUERY_MAX
|
|
2140
|
+
})
|
|
2141
|
+
),
|
|
2142
|
+
limit: LIMIT
|
|
2143
|
+
};
|
|
2144
|
+
var GRAFANA_SHARED_DESC = "the APPLICATION logs shipped to Grafana Cloud/Loki, complementing query_gcp_logs (GCP infrastructure logs). Start with structured filters (env, level=error, sinceMinutes=60, services), then narrow with search; pass raw LogQL via logql only when structured filters cannot express the query (it REPLACES them). The response header echoes the composed LogQL \u2014 iterate on it. Returns compact lines: '<time> <SEVERITY> [<service>] <message>'.";
|
|
2145
|
+
var queryGrafanaLogsContract = defineToolContract({
|
|
2146
|
+
name: "query_grafana_logs",
|
|
2147
|
+
agent: {
|
|
2148
|
+
description: `Query this project's connected Grafana (Loki) logs \u2014 ${GRAFANA_SHARED_DESC}`,
|
|
2149
|
+
fields: grafanaFields
|
|
2150
|
+
},
|
|
2151
|
+
mcp: {
|
|
2152
|
+
description: `Query the project's connected Grafana (Loki) logs \u2014 ${GRAFANA_SHARED_DESC} Pass projectId to target a specific project; otherwise the configured default project is used.`,
|
|
2153
|
+
fields: { projectId: mcpProjectId, ...grafanaFields }
|
|
2154
|
+
}
|
|
2155
|
+
});
|
|
2156
|
+
var logsContracts = [
|
|
2157
|
+
queryGcpLogsContract,
|
|
2158
|
+
queryGrafanaLogsContract
|
|
2159
|
+
];
|
|
2160
|
+
var DETECTORS = ["partner", "player", "internal", "monitor", "agent", "unknown"];
|
|
2161
|
+
var LINK_KINDS = ["deployment", "origin", "fix"];
|
|
2162
|
+
var ATTRIBUTIONS = ["suspected", "confirmed"];
|
|
2163
|
+
var TIMESTAMP_HINT = "ISO 8601 timestamp; pass an empty string to clear it back to unknown. Never substitute card creation or closure time.";
|
|
2164
|
+
var DESCRIPTION = "Record an incident's outcome and attribution: impact, environment, who detected it, when the failure started, when it was detected, and when service was actually restored (alert clearance alone is not recovery). Optionally link a deployment, the origin task whose change caused it, or a fix task; attribution is 'suspected' until confirmed. remove_link_id soft-removes a wrong link (reason required). Every change is audited. Read the current record with get_task (field `incident`).";
|
|
2165
|
+
var updateIncidentContract = defineToolContract({
|
|
2166
|
+
name: "update_incident",
|
|
2167
|
+
agent: {
|
|
2168
|
+
description: DESCRIPTION,
|
|
2169
|
+
fields: {
|
|
2170
|
+
incident_slug_or_id: f.optional(
|
|
2171
|
+
f.string({
|
|
2172
|
+
desc: "Incident card slug or ID. Omit when this session's card is the incident."
|
|
2173
|
+
})
|
|
2174
|
+
),
|
|
2175
|
+
impact: f.optional(f.string({ desc: "Who/what was affected and how badly" })),
|
|
2176
|
+
environment: f.optional(f.string({ desc: "Affected environment, e.g. prod, staging, test" })),
|
|
2177
|
+
detected_by: f.optional(f.enum(DETECTORS, { desc: "Who first noticed the failure" })),
|
|
2178
|
+
failure_started_at: f.optional(
|
|
2179
|
+
f.string({ desc: `When the failure began. ${TIMESTAMP_HINT}` })
|
|
2180
|
+
),
|
|
2181
|
+
detected_at: f.optional(f.string({ desc: `When it was first detected. ${TIMESTAMP_HINT}` })),
|
|
2182
|
+
service_restored_at: f.optional(
|
|
2183
|
+
f.string({ desc: `When service was observed restored. ${TIMESTAMP_HINT}` })
|
|
2184
|
+
),
|
|
2185
|
+
synthetic: f.optional(
|
|
2186
|
+
f.boolean({ desc: "True for probe/acceptance incidents (excluded from delivery metrics)" })
|
|
2187
|
+
),
|
|
2188
|
+
link_kind: f.optional(f.enum(LINK_KINDS, { desc: "Add or update a link of this kind" })),
|
|
2189
|
+
link_task_slug_or_id: f.optional(
|
|
2190
|
+
f.string({ desc: "Origin or fix task slug/ID (link_kind origin|fix)" })
|
|
2191
|
+
),
|
|
2192
|
+
link_deployment_id: f.optional(f.string({ desc: "Deployment ID (link_kind deployment)" })),
|
|
2193
|
+
attribution: f.optional(
|
|
2194
|
+
f.enum(ATTRIBUTIONS, { desc: "Deployment/origin attribution; defaults to suspected" })
|
|
2195
|
+
),
|
|
2196
|
+
required_immediate_intervention: f.optional(
|
|
2197
|
+
f.boolean({
|
|
2198
|
+
desc: "Deployment links: did the failure need a rollback, hotfix, or fix-forward?"
|
|
2199
|
+
})
|
|
2200
|
+
),
|
|
2201
|
+
link_note: f.optional(f.string({ desc: "Short evidence note for the link" })),
|
|
2202
|
+
remove_link_id: f.optional(f.string({ desc: "ID of an existing link to remove" })),
|
|
2203
|
+
reason: f.optional(f.string({ desc: "Why this changed (kept in the audit history)" }))
|
|
2204
|
+
}
|
|
2205
|
+
},
|
|
2206
|
+
mcp: {
|
|
2207
|
+
description: `${DESCRIPTION} Pass projectId to target a specific project; otherwise the configured default project is used.`,
|
|
2208
|
+
fields: {
|
|
2209
|
+
projectId: mcpProjectId,
|
|
2210
|
+
taskId: f.string({ desc: "The incident card ID or slug" }),
|
|
2211
|
+
impact: f.optional(f.string({ desc: "Who/what was affected and how badly" })),
|
|
2212
|
+
environment: f.optional(f.string({ desc: "Affected environment, e.g. prod, staging, test" })),
|
|
2213
|
+
detectedBy: f.optional(f.enum(DETECTORS, { desc: "Who first noticed the failure" })),
|
|
2214
|
+
failureStartedAt: f.optional(f.string({ desc: `When the failure began. ${TIMESTAMP_HINT}` })),
|
|
2215
|
+
detectedAt: f.optional(f.string({ desc: `When it was first detected. ${TIMESTAMP_HINT}` })),
|
|
2216
|
+
serviceRestoredAt: f.optional(
|
|
2217
|
+
f.string({ desc: `When service was observed restored. ${TIMESTAMP_HINT}` })
|
|
2218
|
+
),
|
|
2219
|
+
synthetic: f.optional(
|
|
2220
|
+
f.boolean({ desc: "True for probe/acceptance incidents (excluded from delivery metrics)" })
|
|
2221
|
+
),
|
|
2222
|
+
linkKind: f.optional(f.enum(LINK_KINDS, { desc: "Add or update a link of this kind" })),
|
|
2223
|
+
linkTaskSlugOrId: f.optional(
|
|
2224
|
+
f.string({ desc: "Origin or fix task slug/ID (linkKind origin|fix)" })
|
|
2225
|
+
),
|
|
2226
|
+
linkDeploymentId: f.optional(f.string({ desc: "Deployment ID (linkKind deployment)" })),
|
|
2227
|
+
attribution: f.optional(
|
|
2228
|
+
f.enum(ATTRIBUTIONS, { desc: "Deployment/origin attribution; defaults to suspected" })
|
|
2229
|
+
),
|
|
2230
|
+
requiredImmediateIntervention: f.optional(
|
|
2231
|
+
f.boolean({
|
|
2232
|
+
desc: "Deployment links: did the failure need a rollback, hotfix, or fix-forward?"
|
|
2233
|
+
})
|
|
2234
|
+
),
|
|
2235
|
+
linkNote: f.optional(f.string({ desc: "Short evidence note for the link" })),
|
|
2236
|
+
removeLinkId: f.optional(f.string({ desc: "ID of an existing link to remove" })),
|
|
2237
|
+
reason: f.optional(f.string({ desc: "Why this changed (kept in the audit history)" }))
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
});
|
|
2241
|
+
var incidentsContracts = [updateIncidentContract];
|
|
2242
|
+
var SHA = f.optional(
|
|
2243
|
+
f.string({
|
|
2244
|
+
desc: "Commit sha to watch, full or abbreviated. Defaults to the head of prNumber, else this card's PR, else the tip of this card's branch.",
|
|
2245
|
+
min: 7,
|
|
2246
|
+
max: 40
|
|
2247
|
+
})
|
|
2248
|
+
);
|
|
2249
|
+
var PR_NUMBER = f.optional(
|
|
2250
|
+
f.number({
|
|
2251
|
+
desc: "PR number whose current head to watch. Use this for a child's PR when orchestrating a pack.",
|
|
2252
|
+
int: true,
|
|
2253
|
+
positive: true
|
|
2254
|
+
})
|
|
2255
|
+
);
|
|
2256
|
+
var TIMEOUT_MINUTES = f.optional(
|
|
2257
|
+
f.number({
|
|
2258
|
+
desc: `Give up after this many minutes (default ${DEFAULT_CI_WAIT_TIMEOUT_MINUTES}, max ${MAX_CI_WAIT_TIMEOUT_MINUTES}); you are woken with timed_out.`,
|
|
2259
|
+
int: true,
|
|
2260
|
+
min: 1,
|
|
2261
|
+
max: MAX_CI_WAIT_TIMEOUT_MINUTES
|
|
2262
|
+
})
|
|
2263
|
+
);
|
|
2264
|
+
var waitForChecksContract = defineToolContract({
|
|
2265
|
+
name: "wait_for_checks",
|
|
2266
|
+
agent: {
|
|
2267
|
+
description: "Wait for CI without holding the pod. Records the commit to watch on this session and returns immediately: if CI already finished you get the result now; otherwise the reply says `parked` and you must END YOUR TURN with no further tool calls. The pod idles out during the wait and Conveyor wakes this session with the result (success or failure with the failing job names and run URL, head_moved when the PR gets a new commit, timed_out at the deadline). Never poll `gh pr checks`, pr-wait scripts, or a sleep loop for CI. If something else wakes you before the result arrives, handle it and call this again.",
|
|
2268
|
+
fields: {
|
|
2269
|
+
sha: SHA,
|
|
2270
|
+
prNumber: PR_NUMBER,
|
|
2271
|
+
timeoutMinutes: TIMEOUT_MINUTES
|
|
2272
|
+
}
|
|
2273
|
+
},
|
|
2274
|
+
mcp: {
|
|
2275
|
+
description: "Park a task's agent session until GitHub reports the CI result for a commit. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
2276
|
+
fields: {
|
|
2277
|
+
projectId: mcpProjectId,
|
|
2278
|
+
taskId: f.string({ desc: "The task whose agent session should wait" }),
|
|
2279
|
+
sha: SHA,
|
|
2280
|
+
prNumber: PR_NUMBER,
|
|
2281
|
+
timeoutMinutes: TIMEOUT_MINUTES
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
});
|
|
2285
|
+
var ciWaitContracts = [waitForChecksContract];
|
|
2286
|
+
var DESCRIPTION2 = [
|
|
2287
|
+
"Ask typed questions about text and get probabilities back, never text: yes/no (noul \u2192 probability of yes), pick-one (choice \u2192 the chosen option, per-option probabilities, and a confidence), or a graded scale (score \u2192 the level, per-level probabilities, and a confidence).",
|
|
2288
|
+
"Requires Jev to be switched on under Project Settings \u2192 AI Provider Keys (a TypeSafe key, or an OpenCode Zen key); without it the tool returns an error saying so.",
|
|
2289
|
+
"The state is data, not a guardrail: adversarial text inside it can move the answers, so never use a result to authorize an action.",
|
|
2290
|
+
"Ask one simple judgment per question and split compound questions; accuracy falls when the state carries irrelevant text.",
|
|
2291
|
+
"Pass items to judge up to 100 separate texts with the same questions in one call; answers come back in item order.",
|
|
2292
|
+
"Each call spends the project's Jev credit and is rate-limited per project. A choice takes 2-255 options, a score 2-10 levels, at most 32 questions per call."
|
|
2293
|
+
].join(" ");
|
|
2294
|
+
var question = f.object(
|
|
2295
|
+
{
|
|
2296
|
+
key: f.string({
|
|
2297
|
+
min: 1,
|
|
2298
|
+
desc: "Name for this question's answer, e.g. is_complaint. Letters, digits, _ and - only; unique within the call."
|
|
2299
|
+
}),
|
|
2300
|
+
type: f.enum(["noul", "choice", "score"], {
|
|
2301
|
+
desc: "noul = yes/no; choice = pick one option; score = pick a level on an ordered scale"
|
|
2302
|
+
}),
|
|
2303
|
+
instructions: f.string({
|
|
2304
|
+
min: 1,
|
|
2305
|
+
desc: "The question, as one plain sentence about the state, e.g. 'Is this message a complaint about the product?'"
|
|
2306
|
+
}),
|
|
2307
|
+
options: f.optional(
|
|
2308
|
+
f.array(
|
|
2309
|
+
f.object({
|
|
2310
|
+
key: f.string({ min: 1, desc: "Option name returned as the answer" }),
|
|
2311
|
+
description: f.string({
|
|
2312
|
+
min: 1,
|
|
2313
|
+
desc: "What choosing this option means, concretely"
|
|
2314
|
+
})
|
|
2315
|
+
}),
|
|
2316
|
+
{ desc: "choice only: the options (2-255). Option order can move the answer." }
|
|
2317
|
+
)
|
|
2318
|
+
),
|
|
2319
|
+
levels: f.optional(
|
|
2320
|
+
f.array(f.string({ min: 1 }), {
|
|
2321
|
+
desc: "score only: 2-10 level descriptions, lowest first"
|
|
2322
|
+
})
|
|
2323
|
+
)
|
|
2324
|
+
},
|
|
2325
|
+
{ desc: "One typed question" }
|
|
2326
|
+
);
|
|
2327
|
+
var stateDesc = "The thing being judged, as plain text or JSON. Required unless items is given; with items it is ignored.";
|
|
2328
|
+
var itemsDesc = "Up to 100 separate texts (each up to 8,000 characters), each judged on its own with the same questions. Use this for bulk classification instead of one call per item.";
|
|
2329
|
+
var questionsDesc = "1-32 questions, all answered independently against the same state.";
|
|
2330
|
+
var evaluateQuestionsContract = defineToolContract({
|
|
2331
|
+
name: "evaluate_questions",
|
|
2332
|
+
agent: {
|
|
2333
|
+
description: DESCRIPTION2,
|
|
2334
|
+
fields: {
|
|
2335
|
+
state: f.optional(f.string({ desc: stateDesc })),
|
|
2336
|
+
questions: f.array(question, { min: 1, desc: questionsDesc }),
|
|
2337
|
+
items: f.optional(f.array(f.string(), { desc: itemsDesc }))
|
|
2338
|
+
}
|
|
2339
|
+
},
|
|
2340
|
+
mcp: {
|
|
2341
|
+
description: `${DESCRIPTION2} Pass projectId to target a specific project; otherwise the configured default project is used.`,
|
|
2342
|
+
fields: {
|
|
2343
|
+
projectId: mcpProjectId,
|
|
2344
|
+
state: f.optional(f.string({ desc: stateDesc })),
|
|
2345
|
+
questions: f.array(question, { min: 1, desc: questionsDesc }),
|
|
2346
|
+
items: f.optional(f.array(f.string(), { desc: itemsDesc }))
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
});
|
|
2350
|
+
var evaluateContracts = [evaluateQuestionsContract];
|
|
2351
|
+
var listSiteAssetTargetsContract = defineToolContract({
|
|
2352
|
+
name: "list_site_asset_targets",
|
|
2353
|
+
agent: {
|
|
2354
|
+
description: "List the registered static-site media targets this card's project may publish to.",
|
|
2355
|
+
fields: {}
|
|
2356
|
+
},
|
|
2357
|
+
mcp: {
|
|
2358
|
+
description: "List registered static-site media targets for a project.",
|
|
2359
|
+
fields: { projectId: mcpProjectId }
|
|
2360
|
+
}
|
|
2361
|
+
});
|
|
2362
|
+
var uploadSiteAssetsContract = defineToolContract({
|
|
2363
|
+
name: "upload_site_assets",
|
|
2364
|
+
agent: {
|
|
2365
|
+
description: "Upload up to 50 workspace image or video files (25 MiB each) to a registered site using Conveyor identity. Call list_site_asset_targets first for the registered siteId. Returns verified immutable keys and public URLs per file.",
|
|
2366
|
+
fields: {
|
|
2367
|
+
siteId: f.string({ min: 1, max: 100 }),
|
|
2368
|
+
paths: f.array(f.string({ min: 1 }), { min: 1, max: 50 })
|
|
2369
|
+
}
|
|
2370
|
+
},
|
|
2371
|
+
mcp: {
|
|
2372
|
+
description: "Upload up to 50 workspace image or video files (25 MiB each) to a registered site using Conveyor identity. Call list_site_asset_targets first for the registered siteId. Returns verified immutable keys and public URLs per file.",
|
|
2373
|
+
fields: {
|
|
2374
|
+
projectId: mcpProjectId,
|
|
2375
|
+
siteId: f.string({ min: 1, max: 100 }),
|
|
2376
|
+
paths: f.array(f.string({ min: 1 }), { min: 1, max: 50 })
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
var siteAssetsContracts = [
|
|
2381
|
+
listSiteAssetTargetsContract,
|
|
2382
|
+
uploadSiteAssetsContract
|
|
2383
|
+
];
|
|
2384
|
+
function validateSiteAssetResult(value, expected) {
|
|
2385
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2386
|
+
throw new Error("verify returned invalid JSON");
|
|
2387
|
+
const result = value;
|
|
2388
|
+
for (const key of ["key", "publicUrl", "sha256", "sizeBytes", "mimeType"]) {
|
|
2389
|
+
if (result[key] !== expected[key])
|
|
2390
|
+
throw new Error(`verify response ${key} does not match upload capability`);
|
|
2391
|
+
}
|
|
2392
|
+
if (result.status !== "uploaded" && result.status !== "existing")
|
|
2393
|
+
throw new Error("verify response has invalid status");
|
|
2394
|
+
return { ...expected, status: result.status };
|
|
2395
|
+
}
|
|
2396
|
+
async function readReply(response) {
|
|
2397
|
+
if (!response.body) throw new Error("verify returned an empty response");
|
|
2398
|
+
const reader = response.body.getReader();
|
|
2399
|
+
const decoder = new TextDecoder();
|
|
2400
|
+
let text = "";
|
|
2401
|
+
let size = 0;
|
|
2402
|
+
try {
|
|
2403
|
+
while (true) {
|
|
2404
|
+
const { done, value } = await reader.read();
|
|
2405
|
+
if (done) break;
|
|
2406
|
+
size += value.byteLength;
|
|
2407
|
+
if (size > 16384) throw new Error("verify response exceeds 16KiB");
|
|
2408
|
+
text += decoder.decode(value, { stream: true });
|
|
2409
|
+
}
|
|
2410
|
+
text += decoder.decode();
|
|
2411
|
+
} finally {
|
|
2412
|
+
await reader.cancel();
|
|
2413
|
+
reader.releaseLock();
|
|
2414
|
+
}
|
|
2415
|
+
try {
|
|
2416
|
+
return JSON.parse(text);
|
|
2417
|
+
} catch {
|
|
2418
|
+
throw new Error("verify returned invalid JSON");
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
async function transferSiteAsset(capability, bytes, expected) {
|
|
2422
|
+
const options = {
|
|
2423
|
+
headers: capability.headers,
|
|
2424
|
+
redirect: "error",
|
|
2425
|
+
signal: AbortSignal.timeout(6e4)
|
|
2426
|
+
};
|
|
2427
|
+
let put;
|
|
2428
|
+
try {
|
|
2429
|
+
put = await fetch(capability.uploadUrl, { ...options, method: "PUT", body: bytes });
|
|
2430
|
+
} catch {
|
|
2431
|
+
throw new Error("Upload transfer failed or timed out; retry this file");
|
|
2432
|
+
}
|
|
2433
|
+
await put.body?.cancel();
|
|
2434
|
+
if (!put.ok) throw new Error(`upload HTTP ${put.status}; retry or check site access`);
|
|
2435
|
+
let verify;
|
|
2436
|
+
try {
|
|
2437
|
+
verify = await fetch(`${new URL(capability.uploadUrl).origin}/_site-assets/verify`, {
|
|
2438
|
+
...options,
|
|
2439
|
+
method: "POST",
|
|
2440
|
+
signal: AbortSignal.timeout(6e4)
|
|
2441
|
+
});
|
|
2442
|
+
} catch {
|
|
2443
|
+
throw new Error("Upload verification failed or timed out; retry this file");
|
|
2444
|
+
}
|
|
2445
|
+
if (!verify.ok) {
|
|
2446
|
+
await verify.body?.cancel();
|
|
2447
|
+
throw new Error(`verify HTTP ${verify.status}; retry or check site access`);
|
|
2448
|
+
}
|
|
2449
|
+
return {
|
|
2450
|
+
...validateSiteAssetResult(await readReply(verify), expected),
|
|
2451
|
+
status: put.status === 201 ? "uploaded" : "existing"
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
var TOOL_CONTRACTS = Object.fromEntries(
|
|
2455
|
+
[
|
|
2456
|
+
...tasksContracts,
|
|
2457
|
+
...taskUpdateContracts,
|
|
2458
|
+
...tagsContracts,
|
|
2459
|
+
...checklistContracts,
|
|
2460
|
+
...dependenciesContracts,
|
|
2461
|
+
...subtasksContracts,
|
|
2462
|
+
...attachmentsContracts,
|
|
2463
|
+
...suggestionsContracts,
|
|
2464
|
+
...decisionsContracts,
|
|
2465
|
+
...pullRequestContracts,
|
|
2466
|
+
...integrationsContracts,
|
|
2467
|
+
...driveContracts,
|
|
2468
|
+
...meetingsContracts,
|
|
2469
|
+
...logsContracts,
|
|
2470
|
+
...ciWaitContracts,
|
|
2471
|
+
...evaluateContracts,
|
|
2472
|
+
...incidentsContracts,
|
|
2473
|
+
...siteAssetsContracts
|
|
2474
|
+
].map((contract) => [contract.name, contract])
|
|
2475
|
+
);
|
|
2476
|
+
|
|
2477
|
+
export {
|
|
2478
|
+
DEFAULT_SONNET_MODEL,
|
|
2479
|
+
DEFAULT_OPUS_MODEL,
|
|
2480
|
+
DEFAULT_HAIKU_MODEL,
|
|
2481
|
+
FABLE_MODEL,
|
|
2482
|
+
PREVIOUS_SONNET_MODEL,
|
|
2483
|
+
PREVIOUS_OPUS_MODEL,
|
|
2484
|
+
formatModelId,
|
|
2485
|
+
JEV_STATE_MAX_CHARS,
|
|
2486
|
+
CARD_DESCRIPTION_MAX,
|
|
2487
|
+
CARD_DESCRIPTION_LIMIT_MESSAGE,
|
|
2488
|
+
ManualTestItemsSchema,
|
|
2489
|
+
describeManualTestRejection,
|
|
2490
|
+
MAX_CI_WAIT_TIMEOUT_MINUTES,
|
|
2491
|
+
runQueryGcpLogs,
|
|
2492
|
+
runQueryGrafanaLogs,
|
|
2493
|
+
compileShape,
|
|
2494
|
+
cardDescriptionDesc,
|
|
2495
|
+
priorityValueDesc,
|
|
2496
|
+
getTaskContract,
|
|
2497
|
+
postToChatContract,
|
|
2498
|
+
readTaskChatContract,
|
|
2499
|
+
listTagsContract,
|
|
2500
|
+
searchTasksContract,
|
|
2501
|
+
getConnectionContextContract,
|
|
2502
|
+
updateTaskContract,
|
|
2503
|
+
getTagContract,
|
|
2504
|
+
listManualTestsContract,
|
|
2505
|
+
queryManualTestsContract,
|
|
2506
|
+
setManualTestsContract,
|
|
2507
|
+
editManualTestContract,
|
|
2508
|
+
removeManualTestContract,
|
|
2509
|
+
approveManualTestContract,
|
|
2510
|
+
rejectManualTestContract,
|
|
2511
|
+
getDependenciesContract,
|
|
2512
|
+
addDependencyContract,
|
|
2513
|
+
removeDependencyContract,
|
|
2514
|
+
createSubtaskContract,
|
|
2515
|
+
updateSubtaskContract,
|
|
2516
|
+
deleteSubtaskContract,
|
|
2517
|
+
listSubtasksContract,
|
|
2518
|
+
setTaskParentContract,
|
|
2519
|
+
listTaskFilesContract,
|
|
2520
|
+
getAttachmentContract,
|
|
2521
|
+
uploadAttachmentContract,
|
|
2522
|
+
deleteAttachmentContract,
|
|
2523
|
+
setShowcaseContract,
|
|
2524
|
+
createSuggestionContract,
|
|
2525
|
+
createDecisionContract,
|
|
2526
|
+
listDecisionsContract,
|
|
2527
|
+
getDecisionContract,
|
|
2528
|
+
voteDecisionContract,
|
|
2529
|
+
resolveDecisionContract,
|
|
2530
|
+
updateDecisionContract,
|
|
2531
|
+
createPullRequestContract,
|
|
2532
|
+
listProjectIntegrationsContract,
|
|
2533
|
+
listProjectChannelsContract,
|
|
2534
|
+
readChannelMessagesContract,
|
|
2535
|
+
postChannelMessageContract,
|
|
2536
|
+
getAnalyticsSummaryContract,
|
|
2537
|
+
driveListFilesContract,
|
|
2538
|
+
driveReadFileContract,
|
|
2539
|
+
driveCreateFileContract,
|
|
2540
|
+
driveUpdateFileContract,
|
|
2541
|
+
driveDeleteFileContract,
|
|
2542
|
+
driveCreateFolderContract,
|
|
2543
|
+
listMeetingsContract,
|
|
2544
|
+
getMeetingContract,
|
|
2545
|
+
readMeetingTranscriptContract,
|
|
2546
|
+
createMeetingContract,
|
|
2547
|
+
updateMeetingContract,
|
|
2548
|
+
addMeetingChecklistItemsContract,
|
|
2549
|
+
checkMeetingChecklistItemContract,
|
|
2550
|
+
editMeetingChecklistItemContract,
|
|
2551
|
+
removeMeetingChecklistItemContract,
|
|
2552
|
+
queryGcpLogsContract,
|
|
2553
|
+
queryGrafanaLogsContract,
|
|
2554
|
+
updateIncidentContract,
|
|
2555
|
+
evaluateQuestionsContract,
|
|
2556
|
+
listSiteAssetTargetsContract,
|
|
2557
|
+
uploadSiteAssetsContract,
|
|
2558
|
+
transferSiteAsset
|
|
2559
|
+
};
|