@rallycry/conveyor-mcp 4.3.10 → 4.3.12

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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ConveyorConnection
4
- } from "./chunk-KOYKRWVY.js";
4
+ } from "./chunk-64PP4HVL.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { createRequire } from "module";
@@ -170,20 +170,28 @@ function registerUpdateProjectSettings(server2, conn2) {
170
170
  }
171
171
  );
172
172
  }
173
+ var contextPathSchema = z3.object({
174
+ type: z3.enum(["rule", "doc", "file", "folder"]).describe("Kind of context link \u2014 a rule/doc file, a source file, or a folder"),
175
+ path: z3.string().describe("Repo-relative path, e.g. '.claude/rules/refactor-verification.md'"),
176
+ label: z3.string().optional().describe("Optional human-readable label for the link")
177
+ });
173
178
  function registerManageTags(server2, conn2) {
174
179
  server2.tool(
175
180
  "manage_tags",
176
- "List, create, update, or delete project tags. create requires name (optional color/description); update/delete require the tag id. Mutations require a Moderate project role.",
181
+ "List, create, update, or delete project tags. create requires name (optional color/description/contextPaths); update/delete require the tag id. contextPaths wire files/rules/docs into the agent context of any card carrying the tag (replace-set \u2014 pass the full list, [] clears). Mutations require a Moderate project role.",
177
182
  {
178
183
  action: z3.enum(["list", "create", "update", "delete"]).describe("Operation to perform"),
179
184
  projectId: z3.string().optional().describe("Target Conveyor project ID (list/create)"),
180
185
  id: z3.string().optional().describe("Tag ID (update/delete)"),
181
186
  name: z3.string().optional().describe("Tag name"),
182
187
  color: z3.string().optional().describe("Hex color, e.g. #ff0000"),
183
- description: z3.string().optional().describe("Tag description")
188
+ description: z3.string().optional().describe("Tag description"),
189
+ contextPaths: z3.array(contextPathSchema).max(20).optional().describe(
190
+ "Context links (rules/docs/files/folders) auto-loaded into the agent context of cards with this tag. Replace-set: the full list replaces the tag's existing links; pass [] to clear. Only used on create/update."
191
+ )
184
192
  },
185
193
  async (params) => {
186
- const { action, projectId: projectId2, id, name, color, description } = params;
194
+ const { action, projectId: projectId2, id, name, color, description, contextPaths } = params;
187
195
  if (action === "list") return jsonResult(await conn2.listTagsDetailed(projectId2));
188
196
  if (action === "create") {
189
197
  if (!name) return textResult("name is required for create.");
@@ -192,7 +200,8 @@ function registerManageTags(server2, conn2) {
192
200
  projectId: projectId2,
193
201
  name,
194
202
  ...color !== void 0 && { color },
195
- ...description !== void 0 && { description }
203
+ ...description !== void 0 && { description },
204
+ ...contextPaths !== void 0 && { contextPaths }
196
205
  })
197
206
  );
198
207
  }
@@ -203,7 +212,8 @@ function registerManageTags(server2, conn2) {
203
212
  id,
204
213
  ...name !== void 0 && { name },
205
214
  ...color !== void 0 && { color },
206
- ...description !== void 0 && { description }
215
+ ...description !== void 0 && { description },
216
+ ...contextPaths !== void 0 && { contextPaths }
207
217
  })
208
218
  );
209
219
  }
@@ -261,7 +271,733 @@ function registerProjectConfigTools(server2, conn2) {
261
271
  }
262
272
 
263
273
  // src/tools/tasks.ts
274
+ import { z as z5 } from "zod";
275
+
276
+ // ../shared/dist/tool-contracts/index.js
277
+ var f = {
278
+ string(opts) {
279
+ return { kind: "string", ...opts };
280
+ },
281
+ number(opts) {
282
+ return { kind: "number", ...opts };
283
+ },
284
+ boolean(opts) {
285
+ return { kind: "boolean", ...opts };
286
+ },
287
+ enum(values, opts) {
288
+ return { kind: "enum", values, ...opts };
289
+ },
290
+ array(item, opts) {
291
+ return { kind: "array", item, ...opts };
292
+ },
293
+ object(fields, opts) {
294
+ return { kind: "object", fields, ...opts };
295
+ },
296
+ optional(inner) {
297
+ return { kind: "optional", inner };
298
+ }
299
+ };
300
+ function compileString(z9, spec) {
301
+ let schema = z9.string();
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 compileNumber(z9, spec) {
307
+ let schema = z9.number();
308
+ if (spec.int) schema = schema.int();
309
+ if (spec.positive) schema = schema.positive();
310
+ if (spec.nonnegative) schema = schema.nonnegative();
311
+ if (spec.min !== void 0) schema = schema.min(spec.min);
312
+ if (spec.max !== void 0) schema = schema.max(spec.max);
313
+ return schema;
314
+ }
315
+ function compileArray(z9, spec) {
316
+ let schema = z9.array(compileField(z9, spec.item));
317
+ if (spec.min !== void 0) schema = schema.min(spec.min);
318
+ return schema;
319
+ }
320
+ function compileBase(z9, spec) {
321
+ switch (spec.kind) {
322
+ case "string":
323
+ return compileString(z9, spec);
324
+ case "number":
325
+ return compileNumber(z9, spec);
326
+ case "boolean":
327
+ return z9.boolean();
328
+ case "enum":
329
+ return z9.enum([...spec.values]);
330
+ case "array":
331
+ return compileArray(z9, spec);
332
+ case "object":
333
+ return z9.object(compileShape(z9, spec.fields));
334
+ }
335
+ }
336
+ function compileField(z9, spec) {
337
+ if (spec.kind === "optional") {
338
+ return compileField(z9, spec.inner).optional();
339
+ }
340
+ const schema = compileBase(z9, spec);
341
+ return spec.desc === void 0 ? schema : schema.describe(spec.desc);
342
+ }
343
+ function compileShape(z9, fields) {
344
+ const shape = {};
345
+ for (const [key, spec] of Object.entries(fields)) {
346
+ shape[key] = compileField(z9, spec);
347
+ }
348
+ return shape;
349
+ }
350
+ function defineToolContract(contract) {
351
+ return contract;
352
+ }
353
+ var mcpProjectId = f.optional(f.string({ desc: "Target Conveyor project ID" }));
354
+ var getTaskContract = defineToolContract({
355
+ name: "get_task",
356
+ agent: {
357
+ 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.",
358
+ fields: {
359
+ slug_or_id: f.string({ desc: "The task slug (e.g. 'my-task') or CUID" })
360
+ }
361
+ },
362
+ mcp: {
363
+ 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.",
364
+ fields: {
365
+ projectId: mcpProjectId,
366
+ taskId: f.string({
367
+ desc: "The task ID or slug (the value in a card URL, /cards/<slug>)"
368
+ })
369
+ }
370
+ }
371
+ });
372
+ var postToChatContract = defineToolContract({
373
+ name: "post_to_chat",
374
+ agent: {
375
+ 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.",
376
+ fields: {
377
+ message: f.optional(f.string({ desc: "The message to post to the team" })),
378
+ content: f.optional(
379
+ f.string({
380
+ desc: "Alias of `message` (the external conveyor-mcp surface names this field `content`). Provide exactly one of the two."
381
+ })
382
+ ),
383
+ task_id: f.optional(
384
+ f.string({
385
+ desc: "Child task ID to post to. Omit to post to the current task's chat."
386
+ })
387
+ ),
388
+ milestone: f.optional(
389
+ f.enum(["plan_ready", "implementation_complete", "blocked"], {
390
+ desc: "Declare a narrative milestone instead of a routine update. Use SPARINGLY \u2014 only when the plan is ready, implementation is complete, or you are blocked. Milestones appear on the card's activity timeline and in Slack."
391
+ })
392
+ )
393
+ }
394
+ },
395
+ mcp: {
396
+ description: "Post a message to a task's chat. Pass projectId to target a specific project; otherwise the configured default project is used.",
397
+ fields: {
398
+ projectId: mcpProjectId,
399
+ taskId: f.string({ desc: "The task ID" }),
400
+ content: f.optional(f.string({ desc: "Message content" })),
401
+ message: f.optional(
402
+ f.string({
403
+ desc: "Alias of `content` (the in-pod agent surface names this field `message`). Provide exactly one of the two."
404
+ })
405
+ )
406
+ }
407
+ }
408
+ });
409
+ var readTaskChatContract = defineToolContract({
410
+ name: "read_task_chat",
411
+ agent: {
412
+ 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.",
413
+ fields: {
414
+ limit: f.optional(f.number({ desc: "Number of recent messages to fetch (default 20)" })),
415
+ task_id: f.optional(
416
+ f.string({
417
+ desc: "Child task ID to read chat from. Omit to read the current task's chat."
418
+ })
419
+ )
420
+ }
421
+ },
422
+ mcp: {
423
+ 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.",
424
+ fields: {
425
+ projectId: mcpProjectId,
426
+ taskId: f.string({ desc: "The task ID" }),
427
+ limit: f.optional(f.number({ desc: "Max messages to return (default 50)" }))
428
+ }
429
+ }
430
+ });
431
+ var listTagsContract = defineToolContract({
432
+ name: "list_tags",
433
+ agent: {
434
+ description: "List this project's tags (id, name, color). Use the ids with update_tag.",
435
+ fields: {}
436
+ },
437
+ mcp: {
438
+ description: "List all project tags with their names, IDs, and colors. Pass projectId to target a specific project; otherwise the configured default project is used.",
439
+ fields: {
440
+ projectId: mcpProjectId
441
+ }
442
+ }
443
+ });
444
+ var childTaskIdForMerge = f.string({
445
+ desc: "The child task ID whose PR should be approved and merged"
446
+ });
447
+ var approveAndMergePrContract = defineToolContract({
448
+ name: "approve_and_merge_pr",
449
+ agent: {
450
+ description: "Approve and merge a child task's PR. Preconditions: child in ReviewPR. Returns { merged }: true = merged (status\u2192ReviewDev); false = automerge queued, wait for ReviewDev.",
451
+ fields: {
452
+ childTaskId: childTaskIdForMerge
453
+ }
454
+ },
455
+ mcp: {
456
+ description: "Approve and merge a child task's pull request. Pass projectId to target a specific project; otherwise the configured default project is used. Only succeeds if all CI/CD checks are passing. The child task must be in ReviewPR status with a PR.",
457
+ fields: {
458
+ projectId: mcpProjectId,
459
+ childTaskId: childTaskIdForMerge
460
+ }
461
+ }
462
+ });
463
+ var tasksContracts = [
464
+ getTaskContract,
465
+ postToChatContract,
466
+ readTaskChatContract,
467
+ listTagsContract,
468
+ approveAndMergePrContract
469
+ ];
470
+ var mcpChecklistTaskId = f.string({ desc: "The task ID or slug" });
471
+ var testStatuses = f.optional(
472
+ f.array(f.enum(["open", "approved", "rejected"]), {
473
+ desc: "Filter tests by status: open | approved | rejected"
474
+ })
475
+ );
476
+ var setManualTestItems = f.array(
477
+ f.object({ title: f.string({ min: 1, desc: "A concise, actionable test step" }) }),
478
+ { min: 1, desc: "List of manual test steps to add" }
479
+ );
480
+ var titleToEdit = f.string({ min: 1, desc: "The current title of the manual test to edit" });
481
+ var newTitle = f.string({ min: 1, desc: "The new title for the manual test" });
482
+ var titleToRemove = f.string({ min: 1, desc: "The title of the manual test to remove" });
483
+ var titleToApprove = f.string({ min: 1, desc: "The title of the manual test to approve" });
484
+ var titleToReject = f.string({ min: 1, desc: "The title of the manual test to reject" });
485
+ var rejectReason = f.string({
486
+ min: 1,
487
+ max: 2e3,
488
+ desc: "Why the test failed \u2014 what went wrong, shown to the team"
489
+ });
490
+ var listManualTestsContract = defineToolContract({
491
+ name: "list_manual_tests",
492
+ agent: {
493
+ description: "List the manual test checklist items for the current task. Use to see what manual verification steps have already been recorded.",
494
+ fields: {}
495
+ },
496
+ mcp: {
497
+ 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.",
498
+ fields: {
499
+ projectId: mcpProjectId,
500
+ taskId: f.string({ desc: "The task ID or slug (the value in a card URL, /cards/<slug>)" })
501
+ }
502
+ }
503
+ });
504
+ var queryManualTestsContract = defineToolContract({
505
+ name: "query_manual_tests",
506
+ agent: {
507
+ 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.",
508
+ fields: {
509
+ cardStatuses: f.optional(
510
+ f.array(f.string(), {
511
+ desc: 'Filter tasks by card status, e.g. ["ReviewDev", "ReviewLive"]'
512
+ })
513
+ ),
514
+ testStatuses
515
+ }
516
+ },
517
+ mcp: {
518
+ 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.",
519
+ fields: {
520
+ projectId: mcpProjectId,
521
+ cardStatuses: f.optional(
522
+ f.array(f.string(), {
523
+ desc: 'Filter tasks by card/column status, e.g. ["ReviewDev", "ReviewLive"]'
524
+ })
525
+ ),
526
+ testStatuses
527
+ }
528
+ }
529
+ });
530
+ var setManualTestsContract = defineToolContract({
531
+ name: "set_manual_tests",
532
+ agent: {
533
+ description: "Add manual test steps to the task checklist. Existing items with the same title are automatically skipped (deduplication). Use to record specific manual verification steps that reviewers should follow when testing this PR.",
534
+ fields: {
535
+ items: setManualTestItems
536
+ }
537
+ },
538
+ mcp: {
539
+ description: "Add manual test steps to a task's checklist. Pass projectId to target a specific project; otherwise the configured default project is used. Existing items with the same title are automatically skipped (deduplication). Use to record specific manual verification steps that reviewers should follow when testing the task's PR.",
540
+ fields: {
541
+ projectId: mcpProjectId,
542
+ taskId: mcpChecklistTaskId,
543
+ items: setManualTestItems
544
+ }
545
+ }
546
+ });
547
+ var editManualTestContract = defineToolContract({
548
+ name: "edit_manual_test",
549
+ agent: {
550
+ 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.",
551
+ fields: {
552
+ title: titleToEdit,
553
+ newTitle
554
+ }
555
+ },
556
+ mcp: {
557
+ 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.",
558
+ fields: {
559
+ projectId: mcpProjectId,
560
+ taskId: mcpChecklistTaskId,
561
+ title: titleToEdit,
562
+ newTitle
563
+ }
564
+ }
565
+ });
566
+ var removeManualTestContract = defineToolContract({
567
+ name: "remove_manual_test",
568
+ agent: {
569
+ 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.",
570
+ fields: {
571
+ title: titleToRemove
572
+ }
573
+ },
574
+ mcp: {
575
+ 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).",
576
+ fields: {
577
+ projectId: mcpProjectId,
578
+ taskId: mcpChecklistTaskId,
579
+ title: titleToRemove
580
+ }
581
+ }
582
+ });
583
+ var approveManualTestContract = defineToolContract({
584
+ name: "approve_manual_test",
585
+ agent: {
586
+ 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.",
587
+ fields: {
588
+ title: titleToApprove
589
+ }
590
+ },
591
+ mcp: {
592
+ 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.",
593
+ fields: {
594
+ projectId: mcpProjectId,
595
+ taskId: mcpChecklistTaskId,
596
+ title: titleToApprove
597
+ }
598
+ }
599
+ });
600
+ var rejectManualTestContract = defineToolContract({
601
+ name: "reject_manual_test",
602
+ agent: {
603
+ 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.",
604
+ fields: {
605
+ title: titleToReject,
606
+ reason: rejectReason
607
+ }
608
+ },
609
+ mcp: {
610
+ 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.",
611
+ fields: {
612
+ projectId: mcpProjectId,
613
+ taskId: mcpChecklistTaskId,
614
+ title: titleToReject,
615
+ reason: rejectReason
616
+ }
617
+ }
618
+ });
619
+ var checklistContracts = [
620
+ listManualTestsContract,
621
+ queryManualTestsContract,
622
+ setManualTestsContract,
623
+ editManualTestContract,
624
+ removeManualTestContract,
625
+ approveManualTestContract,
626
+ rejectManualTestContract
627
+ ];
628
+ var getDependenciesContract = defineToolContract({
629
+ name: "get_dependencies",
630
+ agent: {
631
+ 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.",
632
+ fields: {}
633
+ },
634
+ mcp: {
635
+ 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.",
636
+ fields: {
637
+ projectId: mcpProjectId,
638
+ taskId: f.string({ desc: "The task ID" })
639
+ }
640
+ }
641
+ });
642
+ var addDependencyContract = defineToolContract({
643
+ name: "add_dependency",
644
+ agent: {
645
+ 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.",
646
+ fields: {
647
+ depends_on_slug_or_id: f.string({ desc: "Slug or ID of the task this task depends on" })
648
+ }
649
+ },
650
+ mcp: {
651
+ 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.",
652
+ fields: {
653
+ projectId: mcpProjectId,
654
+ taskId: f.string({ desc: "The task ID that will be blocked" }),
655
+ dependsOnSlugOrId: f.string({ desc: "Slug or ID of the task this one depends on" })
656
+ }
657
+ }
658
+ });
659
+ var removeDependencyContract = defineToolContract({
660
+ name: "remove_dependency",
661
+ agent: {
662
+ 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.",
663
+ fields: {
664
+ depends_on_slug_or_id: f.string({ desc: "Slug or ID of the task to remove as dependency" })
665
+ }
666
+ },
667
+ mcp: {
668
+ 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.",
669
+ fields: {
670
+ projectId: mcpProjectId,
671
+ taskId: f.string({ desc: "The task ID to unblock" }),
672
+ dependsOnSlugOrId: f.string({ desc: "Slug or ID of the dependency to remove" })
673
+ }
674
+ }
675
+ });
676
+ var dependenciesContracts = [
677
+ getDependenciesContract,
678
+ addDependencyContract,
679
+ removeDependencyContract
680
+ ];
681
+ var SP_DESCRIPTION = "Story point value (1=Common, 2=Magic, 3=Rare, 5=Unique)";
682
+ 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.";
683
+ 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.";
684
+ 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.";
685
+ var MCP_STATUS_ENUM = [
686
+ "Planning",
687
+ "Open",
688
+ "InProgress",
689
+ "ReviewPR",
690
+ "ReviewDev",
691
+ "ReviewLive",
692
+ "Complete",
693
+ "Cancelled"
694
+ ];
695
+ var createSubtaskContract = defineToolContract({
696
+ name: "create_subtask",
697
+ agent: {
698
+ description: "Create a subtask under the current parent task. Use when breaking a complex parent into smaller pieces during planning. For post-task follow-ups use create_follow_up_task.",
699
+ fields: {
700
+ title: f.string({ desc: "Subtask title" }),
701
+ description: f.optional(f.string({ desc: "Brief description" })),
702
+ plan: f.optional(f.string({ desc: "Implementation plan in markdown" })),
703
+ ordinal: f.optional(f.number({ desc: "Step/order number (0-based)" })),
704
+ storyPointValue: f.optional(f.number({ desc: SP_DESCRIPTION })),
705
+ followParentStatus: f.optional(f.boolean({ desc: AGENT_FOLLOW_PARENT_STATUS })),
706
+ dependsOn: f.optional(f.array(f.string(), { desc: AGENT_DEPENDS_ON }))
707
+ }
708
+ },
709
+ mcp: {
710
+ 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.",
711
+ fields: {
712
+ projectId: mcpProjectId,
713
+ parentTaskId: f.string({ desc: "The parent task ID" }),
714
+ title: f.string({ desc: "Subtask title" }),
715
+ description: f.optional(f.string({ desc: "Subtask description" })),
716
+ plan: f.optional(f.string({ desc: "Subtask implementation plan (markdown)" })),
717
+ ordinal: f.optional(f.number({ desc: "Ordering position among siblings" })),
718
+ storyPointValue: f.optional(f.number({ desc: SP_DESCRIPTION })),
719
+ followParentStatus: f.optional(f.boolean({ desc: MCP_FOLLOW_PARENT_STATUS })),
720
+ dependsOn: f.optional(
721
+ f.array(f.string(), {
722
+ 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."
723
+ })
724
+ ),
725
+ tags: f.optional(
726
+ f.array(f.string(), {
727
+ desc: 'Tag names to assign to the subtask (e.g. ["refactor"]). Unknown names are rejected \u2014 create the tag first with manage_tags. Use list_tags to see available tags.'
728
+ })
729
+ )
730
+ }
731
+ }
732
+ });
733
+ var updateSubtaskContract = defineToolContract({
734
+ name: "update_subtask",
735
+ agent: {
736
+ 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_plan.",
737
+ fields: {
738
+ subtaskId: f.string({ desc: "The subtask ID to update" }),
739
+ title: f.optional(f.string()),
740
+ description: f.optional(f.string()),
741
+ plan: f.optional(f.string()),
742
+ status: f.optional(
743
+ f.enum(["Planning", "Open"], {
744
+ desc: 'Move the child between "Planning" and "Open". "Open" marks it ready to execute \u2014 required before start_child_cloud_build. Execution statuses transition automatically.'
745
+ })
746
+ ),
747
+ agentIdOrName: f.optional(
748
+ f.string({
749
+ desc: "Assign a project agent to the child (agent id or exact name from the Project Agents list). Required before start_child_cloud_build."
750
+ })
751
+ ),
752
+ ordinal: f.optional(f.number()),
753
+ storyPointValue: f.optional(f.number({ desc: SP_DESCRIPTION })),
754
+ followParentStatus: f.optional(f.boolean({ desc: AGENT_FOLLOW_PARENT_STATUS })),
755
+ dependsOn: f.optional(
756
+ f.array(f.string(), {
757
+ desc: `${AGENT_DEPENDS_ON} Replaces the full dependency set \u2014 pass [] to clear all, omit to leave unchanged.`
758
+ })
759
+ )
760
+ }
761
+ },
762
+ mcp: {
763
+ 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.",
764
+ fields: {
765
+ projectId: mcpProjectId,
766
+ subtaskId: f.string({ desc: "The subtask ID" }),
767
+ title: f.optional(f.string({ desc: "New title" })),
768
+ description: f.optional(f.string({ desc: "New description" })),
769
+ plan: f.optional(f.string({ desc: "New plan (markdown)" })),
770
+ status: f.optional(f.enum(MCP_STATUS_ENUM, { desc: "New status" })),
771
+ ordinal: f.optional(f.number({ desc: "New ordering position among siblings" })),
772
+ storyPointValue: f.optional(f.number({ desc: SP_DESCRIPTION })),
773
+ followParentStatus: f.optional(f.boolean({ desc: MCP_FOLLOW_PARENT_STATUS })),
774
+ dependsOn: f.optional(
775
+ f.array(f.string(), {
776
+ desc: "Replace the sibling subtask ids/slugs this subtask blocks on (pass [] to clear). Omit to leave dependencies unchanged."
777
+ })
778
+ )
779
+ }
780
+ }
781
+ });
782
+ var deleteSubtaskContract = defineToolContract({
783
+ name: "delete_subtask",
784
+ agent: {
785
+ description: "Delete a subtask by id. When to use: a subtask was created in error or is no longer needed. Returns: confirmation string.",
786
+ fields: {
787
+ subtaskId: f.string({ desc: "The subtask ID to delete" })
788
+ }
789
+ },
790
+ mcp: {
791
+ 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.",
792
+ fields: {
793
+ projectId: mcpProjectId,
794
+ subtaskId: f.string({ desc: "The subtask ID to delete" })
795
+ }
796
+ }
797
+ });
798
+ var listSubtasksContract = defineToolContract({
799
+ name: "list_subtasks",
800
+ agent: {
801
+ description: "List all subtasks under the current parent task. Default compact view returns per child: status, agent, story points, PR number/state, dependencies, and holdsBuildSlot \u2014 plus packSlots (in-flight environments vs the PACK_CHILD_LIMIT cap, and which children hold the slots). Use to coordinate child work; pass verbose:true only when you need full description/plan text (large). For non-child tasks use get_task.",
802
+ fields: {
803
+ verbose: f.optional(
804
+ f.boolean({
805
+ desc: "Return full task rows including description and plan text (large \u2014 can exceed tool result limits on big packs). Default: compact orchestration view."
806
+ })
807
+ )
808
+ }
809
+ },
810
+ mcp: {
811
+ 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.",
812
+ fields: {
813
+ projectId: mcpProjectId,
814
+ taskId: f.string({ desc: "The parent task ID" })
815
+ }
816
+ }
817
+ });
818
+ var subtasksContracts = [
819
+ createSubtaskContract,
820
+ updateSubtaskContract,
821
+ deleteSubtaskContract,
822
+ listSubtasksContract
823
+ ];
824
+ var mcpTaskIdOrSlug = f.string({ desc: "The task ID or slug" });
825
+ var listTaskFilesContract = defineToolContract({
826
+ name: "list_task_files",
827
+ agent: {
828
+ description: "List all files attached to this task with metadata. Use before fetching a specific file to see what is available and how large each is. For file contents use get_attachment.",
829
+ fields: {}
830
+ },
831
+ mcp: {
832
+ description: "List all files attached to a task with metadata (no contents \u2014 fast and small). 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.",
833
+ fields: {
834
+ projectId: mcpProjectId,
835
+ taskId: mcpTaskIdOrSlug
836
+ }
837
+ }
838
+ });
839
+ var getAttachmentContract = defineToolContract({
840
+ name: "get_attachment",
841
+ agent: {
842
+ description: "Fetch one task file's content plus metadata by file ID. Call list_task_files first to discover IDs and check sizes \u2014 large binaries may be truncated by the service's size limit.",
843
+ fields: {
844
+ fileId: f.string({ desc: "The file ID to retrieve" })
845
+ }
846
+ },
847
+ mcp: {
848
+ 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, or fetch `downloadUrl` for the whole file. Call list_task_files first to discover IDs and sizes.",
849
+ fields: {
850
+ projectId: mcpProjectId,
851
+ taskId: mcpTaskIdOrSlug,
852
+ fileId: f.string({ desc: "The file ID to fetch" }),
853
+ offset: f.optional(
854
+ f.number({
855
+ int: true,
856
+ nonnegative: true,
857
+ desc: "Byte offset into text content (paging). Default 0."
858
+ })
859
+ ),
860
+ maxBytes: f.optional(
861
+ f.number({
862
+ int: true,
863
+ positive: true,
864
+ desc: "Max bytes of text content to return from offset."
865
+ })
866
+ )
867
+ }
868
+ }
869
+ });
870
+ var uploadAttachmentContract = defineToolContract({
871
+ name: "upload_attachment",
872
+ agent: {
873
+ description: "Upload an image file (e.g. a Playwright screenshot) as a task attachment AND post it to the task chat in one step \u2014 no follow-up post_to_chat call needed. Supports png/jpg/gif/webp.",
874
+ fields: {
875
+ path: f.string({
876
+ desc: "Path to the image file \u2014 absolute, or relative to the workspace root"
877
+ }),
878
+ title: f.optional(
879
+ f.string({ desc: "Short caption posted with the image (defaults to the file name)" })
880
+ )
881
+ }
882
+ },
883
+ mcp: {
884
+ 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.",
885
+ fields: {
886
+ projectId: mcpProjectId,
887
+ taskId: mcpTaskIdOrSlug,
888
+ path: f.string({ desc: "Absolute path to the local file to upload" }),
889
+ comment: f.optional(
890
+ f.string({ desc: "When set, also posts the attachment to the task chat with this text" })
891
+ ),
892
+ mimeType: f.optional(
893
+ f.string({ desc: "Override the mime type inferred from the file extension" })
894
+ )
895
+ }
896
+ }
897
+ });
898
+ var attachmentsContracts = [
899
+ listTaskFilesContract,
900
+ getAttachmentContract,
901
+ uploadAttachmentContract
902
+ ];
903
+ var createSuggestionContract = defineToolContract({
904
+ name: "create_suggestion",
905
+ agent: {
906
+ 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.",
907
+ fields: {
908
+ title: f.string({ min: 1, desc: "Short title" }),
909
+ description: f.optional(f.string({ desc: "1-3 sentences: what should change and why" })),
910
+ tag_names: f.optional(f.array(f.string(), { desc: "Tag names to categorize" }))
911
+ }
912
+ },
913
+ mcp: {
914
+ 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.",
915
+ fields: {
916
+ projectId: mcpProjectId,
917
+ title: f.string({ desc: "Suggestion title" }),
918
+ description: f.optional(f.string({ desc: "Suggestion details (markdown)" })),
919
+ tagNames: f.optional(
920
+ f.array(f.string(), {
921
+ desc: 'Tag names to categorize the suggestion (e.g., ["agent-runner"])'
922
+ })
923
+ )
924
+ }
925
+ }
926
+ });
927
+ var suggestionsContracts = [createSuggestionContract];
928
+ var createPullRequestContract = defineToolContract({
929
+ name: "create_pull_request",
930
+ agent: {
931
+ 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.",
932
+ fields: {
933
+ title: f.string({ desc: "The PR title" }),
934
+ body: f.string({ desc: "The PR description/body in markdown" }),
935
+ branch: f.optional(
936
+ f.string({
937
+ desc: "The head branch name for the PR. If the task doesn't have a branch set, this will be used. Defaults to the task's existing branch."
938
+ })
939
+ ),
940
+ baseBranch: f.optional(
941
+ f.string({
942
+ desc: "The base branch to target for the PR (e.g. 'main', 'develop'). Defaults to the project's configured dev branch."
943
+ })
944
+ ),
945
+ commitMessage: f.optional(
946
+ f.string({
947
+ desc: "Commit message for staging uncommitted changes. If not provided, a default message based on the PR title will be used."
948
+ })
949
+ ),
950
+ skipVerify: f.optional(
951
+ f.boolean({
952
+ 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."
953
+ })
954
+ )
955
+ }
956
+ },
957
+ mcp: {
958
+ 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.",
959
+ fields: {
960
+ projectId: mcpProjectId,
961
+ taskId: f.string({ desc: "The task ID whose branch should be opened as a PR" }),
962
+ title: f.string({ desc: "Pull request title" }),
963
+ body: f.string({ desc: "Pull request body (markdown)" }),
964
+ head: f.optional(
965
+ f.string({ desc: "Source branch for the PR (defaults to the task's branch)" })
966
+ ),
967
+ base: f.optional(
968
+ f.string({ desc: "Target branch for the PR (defaults to the repo default)" })
969
+ )
970
+ }
971
+ }
972
+ });
973
+ var pullRequestContracts = [createPullRequestContract];
974
+ var TOOL_CONTRACTS = Object.fromEntries(
975
+ [
976
+ ...tasksContracts,
977
+ ...checklistContracts,
978
+ ...dependenciesContracts,
979
+ ...subtasksContracts,
980
+ ...attachmentsContracts,
981
+ ...suggestionsContracts,
982
+ ...pullRequestContracts
983
+ ].map((contract) => [contract.name, contract])
984
+ );
985
+
986
+ // src/tools/contract-tool.ts
264
987
  import { z as z4 } from "zod";
988
+ function mcpShape(surface) {
989
+ return compileShape(z4, surface.fields);
990
+ }
991
+ function registerContractTool(server2, contract, handler) {
992
+ server2.tool(
993
+ contract.name,
994
+ contract.mcp.description,
995
+ mcpShape(contract.mcp),
996
+ handler
997
+ );
998
+ }
999
+
1000
+ // src/tools/tasks-format.ts
265
1001
  var CLI_EVENT_FORMATTERS = {
266
1002
  thinking: (data) => String(data.message ?? ""),
267
1003
  tool_use: (data) => `${data.tool}: ${String(data.input ?? "").slice(0, 1e3)}`,
@@ -319,6 +1055,8 @@ function summarizeTask(task) {
319
1055
  summary.hasPlan = typeof plan === "string" && plan.length > 0;
320
1056
  return summary;
321
1057
  }
1058
+
1059
+ // src/tools/tasks.ts
322
1060
  var STATUS_ENUM = [
323
1061
  "Planning",
324
1062
  "Open",
@@ -331,10 +1069,10 @@ var STATUS_ENUM = [
331
1069
  ];
332
1070
  var CARD_TYPE_ENUM = ["task", "incident", "suggestion"];
333
1071
  var RISK_ENUM = ["critical", "high", "medium", "low"];
334
- var BOARD_FILTER = z4.string().nullable().optional().describe(
1072
+ var BOARD_FILTER = z5.string().nullable().optional().describe(
335
1073
  "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."
336
1074
  );
337
- var BOARD_ASSIGN = z4.string().nullable().optional().describe(
1075
+ var BOARD_ASSIGN = z5.string().nullable().optional().describe(
338
1076
  "Assign the card to a sub-project board. Omit to use the connection's default board (CONVEYOR_SUBPROJECT_ID) when set, else the parent project; pass null to force the parent project. Use list_accessible_subprojects to find board IDs."
339
1077
  );
340
1078
  function registerListTasks(server2, conn2) {
@@ -342,15 +1080,15 @@ function registerListTasks(server2, conn2) {
342
1080
  "list_tasks",
343
1081
  "List project cards, optionally filtered by card type, status, or assignment (a specific assignee, or unassigned tasks). Defaults to type=task \u2014 pass typeFilters to list incidents/suggestions. Results are relevance-ordered: highest priority first then newest; suggestions-only queries rank by upvote score. Pass projectId to target a specific project; otherwise the configured default project is used. Returns summaries \u2014 plan omitted, description truncated; use get_task for full details.",
344
1082
  {
345
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
346
- status: z4.enum(STATUS_ENUM).optional().describe("Filter by task status"),
347
- typeFilters: z4.array(z4.enum(CARD_TYPE_ENUM)).optional().describe(
1083
+ projectId: z5.string().optional().describe("Target Conveyor project ID"),
1084
+ status: z5.enum(STATUS_ENUM).optional().describe("Filter by task status"),
1085
+ typeFilters: z5.array(z5.enum(CARD_TYPE_ENUM)).optional().describe(
348
1086
  'Card types to include, e.g. ["incident"] or ["task", "incident"]. Omit for tasks only.'
349
1087
  ),
350
- assigneeId: z4.string().optional().describe("Filter by assigned user ID"),
351
- unassigned: z4.boolean().optional().describe("Only return tasks with no assignee (mutually exclusive with assigneeId)"),
1088
+ assigneeId: z5.string().optional().describe("Filter by assigned user ID"),
1089
+ unassigned: z5.boolean().optional().describe("Only return tasks with no assignee (mutually exclusive with assigneeId)"),
352
1090
  subProjectId: BOARD_FILTER,
353
- limit: z4.number().optional().describe("Max tasks to return (default 50)")
1091
+ limit: z5.number().optional().describe("Max tasks to return (default 50)")
354
1092
  },
355
1093
  async (params) => {
356
1094
  const tasks = await conn2.listTasks(params);
@@ -361,26 +1099,18 @@ function registerListTasks(server2, conn2) {
361
1099
  );
362
1100
  }
363
1101
  function registerGetTask(server2, conn2) {
364
- server2.tool(
365
- "get_task",
366
- "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.",
367
- {
368
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
369
- taskId: z4.string().describe("The task ID or slug (the value in a card URL, /cards/<slug>)")
370
- },
371
- async (params) => {
372
- const task = await conn2.getTask(params.taskId, params.projectId);
373
- return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
374
- }
375
- );
1102
+ registerContractTool(server2, getTaskContract, async (params) => {
1103
+ const task = await conn2.getTask(params.taskId, params.projectId);
1104
+ return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1105
+ });
376
1106
  }
377
1107
  function registerGetCardBySlug(server2, conn2) {
378
1108
  server2.tool(
379
1109
  "get_card_by_slug",
380
1110
  "Get full card details by the slug from a card URL (/cards/<slug>) instead of a task ID. Pass projectId to target a specific project; otherwise the configured default project is used.",
381
1111
  {
382
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
383
- slug: z4.string().describe("The card slug from a Conveyor card URL, e.g. 'ship-it'")
1112
+ projectId: z5.string().optional().describe("Target Conveyor project ID"),
1113
+ slug: z5.string().describe("The card slug from a Conveyor card URL, e.g. 'ship-it'")
384
1114
  },
385
1115
  async (params) => {
386
1116
  const task = await conn2.getCardBySlug(params.slug, params.projectId);
@@ -393,12 +1123,15 @@ function registerCreateTask(server2, conn2) {
393
1123
  "create_task",
394
1124
  "Create a new task with title, description, and optional plan. Pass projectId to target a specific project; otherwise the configured default project is used. Icon, story points, and agent assignment are auto-filled when a task is created in (or later moved to) a status beyond Planning \u2014 don't spend turns on them.",
395
1125
  {
396
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
397
- title: z4.string().describe("Task title"),
398
- description: z4.string().optional().describe("Task description"),
399
- plan: z4.string().optional().describe("Task implementation plan (markdown)"),
400
- status: z4.enum(["Planning", "Open"]).optional().describe("Initial status (default: Planning)"),
401
- subProjectId: BOARD_ASSIGN
1126
+ projectId: z5.string().optional().describe("Target Conveyor project ID"),
1127
+ title: z5.string().describe("Task title"),
1128
+ description: z5.string().optional().describe("Task description"),
1129
+ plan: z5.string().optional().describe("Task implementation plan (markdown)"),
1130
+ status: z5.enum(["Planning", "Open"]).optional().describe("Initial status (default: Planning)"),
1131
+ subProjectId: BOARD_ASSIGN,
1132
+ tags: z5.array(z5.string()).optional().describe(
1133
+ 'Tag names to assign to the new card (e.g. ["refactor"]). Unknown names are rejected \u2014 create the tag first with manage_tags. Use list_tags to see available tags.'
1134
+ )
402
1135
  },
403
1136
  async (params) => {
404
1137
  const task = await conn2.createTask(params);
@@ -416,26 +1149,37 @@ function registerCreateTask(server2, conn2) {
416
1149
  function registerUpdateTask(server2, conn2) {
417
1150
  server2.tool(
418
1151
  "update_task",
419
- "Update task fields: title, description, plan, status, risk, or assignment. 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. 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.",
1152
+ "Update task fields: title, description, plan, status, risk, assignment, 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. 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.",
420
1153
  {
421
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
422
- taskId: z4.string().describe("The task ID"),
423
- title: z4.string().optional().describe("New title"),
424
- description: z4.string().optional().describe("New description"),
425
- plan: z4.string().optional().describe("New plan (markdown)"),
426
- status: z4.enum(STATUS_ENUM).optional().describe("New status"),
427
- risk: z4.enum(RISK_ENUM).nullable().optional().describe(
1154
+ projectId: z5.string().optional().describe("Target Conveyor project ID"),
1155
+ taskId: z5.string().describe("The task ID"),
1156
+ title: z5.string().optional().describe("New title"),
1157
+ description: z5.string().optional().describe("New description"),
1158
+ plan: z5.string().optional().describe("New plan (markdown)"),
1159
+ status: z5.enum(STATUS_ENUM).optional().describe("New status"),
1160
+ risk: z5.enum(RISK_ENUM).nullable().optional().describe(
428
1161
  "Risk level \u2014 how much important surface the task touches (critical/high/medium/low). Pass null to clear."
429
1162
  ),
430
- assignedUserId: z4.string().nullable().optional().describe("User ID to assign, or null"),
431
- subProjectId: z4.string().nullable().optional().describe(
1163
+ assignedUserId: z5.string().nullable().optional().describe("User ID to assign, or null"),
1164
+ subProjectId: z5.string().nullable().optional().describe(
432
1165
  "Assign the task to a sub-project board; null moves it back to the parent board. Omit to leave unchanged."
1166
+ ),
1167
+ addTags: z5.array(z5.string()).optional().describe(
1168
+ '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.'
1169
+ ),
1170
+ removeTags: z5.array(z5.string()).optional().describe(
1171
+ "Tag names to remove from the card. Removing a tag the card doesn't have is a no-op."
433
1172
  )
434
1173
  },
435
1174
  async (params) => {
436
1175
  const result = await conn2.updateTask(params);
1176
+ const parts = [`Task ${result.id} updated`];
1177
+ if (result.status) parts.push(`status: ${result.status}`);
1178
+ if ((result.addedTags ?? []).length > 0) parts.push(`+tags: ${result.addedTags.join(", ")}`);
1179
+ if ((result.removedTags ?? []).length > 0)
1180
+ parts.push(`-tags: ${result.removedTags.join(", ")}`);
437
1181
  return {
438
- content: [{ type: "text", text: `Task ${result.id} updated (status: ${result.status})` }]
1182
+ content: [{ type: "text", text: parts.join(" \xB7 ") }]
439
1183
  };
440
1184
  }
441
1185
  );
@@ -445,9 +1189,9 @@ function registerMoveCard(server2, conn2) {
445
1189
  "move_card",
446
1190
  "Move an eligible Planning or Open task, incident, or suggestion card to another project. Cards with identification or automation in progress, active compute, pull requests, deployments, releases, reviews, or delivered reporter email cannot move. Project-specific metadata is cleared instead of mapped by name. Pass projectId for the source project; otherwise the configured default project is used.",
447
1191
  {
448
- projectId: z4.string().optional().describe("Source Conveyor project ID"),
449
- taskId: z4.string().describe("Card ID or slug"),
450
- destinationProjectId: z4.string().describe("Destination Conveyor project ID")
1192
+ projectId: z5.string().optional().describe("Source Conveyor project ID"),
1193
+ taskId: z5.string().describe("Card ID or slug"),
1194
+ destinationProjectId: z5.string().describe("Destination Conveyor project ID")
451
1195
  },
452
1196
  async (params) => {
453
1197
  const result = await conn2.moveCard(params);
@@ -464,44 +1208,37 @@ function registerMoveCard(server2, conn2) {
464
1208
  );
465
1209
  }
466
1210
  function registerChatTools(server2, conn2) {
467
- server2.tool(
468
- "read_task_chat",
469
- "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.",
470
- {
471
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
472
- taskId: z4.string().describe("The task ID"),
473
- limit: z4.number().optional().describe("Max messages to return (default 50)")
474
- },
475
- async (params) => {
476
- const messages = await conn2.getTaskChat(params.taskId, params.limit, params.projectId);
477
- return { content: [{ type: "text", text: JSON.stringify(messages, null, 2) }] };
478
- }
479
- );
480
- server2.tool(
481
- "post_to_chat",
482
- "Post a message to a task's chat. Pass projectId to target a specific project; otherwise the configured default project is used.",
483
- {
484
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
485
- taskId: z4.string().describe("The task ID"),
486
- content: z4.string().describe("Message content")
487
- },
488
- async (params) => {
489
- await conn2.postToTaskChat(params.taskId, params.content, params.projectId);
490
- return { content: [{ type: "text", text: "Message posted" }] };
1211
+ registerContractTool(server2, readTaskChatContract, async (params) => {
1212
+ const messages = await conn2.getTaskChat(params.taskId, params.limit, params.projectId);
1213
+ return { content: [{ type: "text", text: JSON.stringify(messages, null, 2) }] };
1214
+ });
1215
+ registerContractTool(server2, postToChatContract, async (params) => {
1216
+ const text = params.content ?? params.message;
1217
+ if (text === void 0) {
1218
+ return {
1219
+ content: [
1220
+ {
1221
+ type: "text",
1222
+ text: "Nothing to post \u2014 provide `content` (or its alias `message`)."
1223
+ }
1224
+ ]
1225
+ };
491
1226
  }
492
- );
1227
+ await conn2.postToTaskChat(params.taskId, text, params.projectId);
1228
+ return { content: [{ type: "text", text: "Message posted" }] };
1229
+ });
493
1230
  }
494
1231
  function registerGetTaskCli(server2, conn2) {
495
1232
  server2.tool(
496
1233
  "get_task_logs",
497
1234
  "Read CLI execution logs from a task. Pass projectId to target a specific project; otherwise the configured default project is used. Returns agent reasoning, tool calls, setup output, and other execution events. For human chat use read_task_chat.",
498
1235
  {
499
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
500
- taskId: z4.string().describe("The task ID or slug"),
501
- source: z4.enum(["agent", "application"]).optional().describe(
1236
+ projectId: z5.string().optional().describe("Target Conveyor project ID"),
1237
+ taskId: z5.string().describe("The task ID or slug"),
1238
+ source: z5.enum(["agent", "application"]).optional().describe(
502
1239
  "Filter by log source: 'agent' for reasoning/tool calls, 'application' for setup/dev-server output"
503
1240
  ),
504
- limit: z4.number().optional().describe("Max entries to return (default 50, max 500)")
1241
+ limit: z5.number().optional().describe("Max entries to return (default 50, max 500)")
505
1242
  },
506
1243
  async ({ taskId, source, limit, projectId: projectId2 }) => {
507
1244
  const effectiveLimit = Math.min(limit ?? 50, 500);
@@ -520,8 +1257,8 @@ function registerGetTaskSessions(server2, conn2) {
520
1257
  "get_task_sessions",
521
1258
  "Read compute-session state for a task: legacy CodespaceSession rows plus v3 workspaces, including purpose=review review workspaces. Shows pod identity, liveness, lifecycle, and code-review claim state. Use to diagnose stalled/dead agents or code-review runs. Pass projectId to target a specific project; otherwise the configured default project is used.",
522
1259
  {
523
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
524
- taskId: z4.string().describe("The task ID or slug")
1260
+ projectId: z5.string().optional().describe("Target Conveyor project ID"),
1261
+ taskId: z5.string().describe("The task ID or slug")
525
1262
  },
526
1263
  async ({ taskId, projectId: projectId2 }) => {
527
1264
  const tasks = await conn2.getTaskSessions(taskId, projectId2);
@@ -539,17 +1276,17 @@ function registerSearchTasks(server2, conn2) {
539
1276
  "search_tasks",
540
1277
  "Search cards by tag name, text query, status, type, and/or assignment. Defaults to type=task \u2014 pass typeFilters to include incidents/suggestions. Results are relevance-ordered: highest priority first then newest; suggestions-only queries rank by upvote score. Pass projectId to target a specific project; otherwise the configured default project is used. Use tag names like 'agent-runner', not IDs. Returns summaries \u2014 plan omitted, description truncated; use get_task for full details.",
541
1278
  {
542
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
543
- tagNames: z4.array(z4.string()).optional().describe('Tag names to filter by (e.g., ["agent-runner", "chat"])'),
544
- searchQuery: z4.string().optional().describe("Text search on title and description"),
545
- statusFilters: z4.array(z4.enum(STATUS_ENUM)).optional().describe("Filter by one or more statuses"),
546
- typeFilters: z4.array(z4.enum(CARD_TYPE_ENUM)).optional().describe(
1279
+ projectId: z5.string().optional().describe("Target Conveyor project ID"),
1280
+ tagNames: z5.array(z5.string()).optional().describe('Tag names to filter by (e.g., ["agent-runner", "chat"])'),
1281
+ searchQuery: z5.string().optional().describe("Text search on title and description"),
1282
+ statusFilters: z5.array(z5.enum(STATUS_ENUM)).optional().describe("Filter by one or more statuses"),
1283
+ typeFilters: z5.array(z5.enum(CARD_TYPE_ENUM)).optional().describe(
547
1284
  'Card types to include (default ["task"]). Pass e.g. ["incident"] or list several to search across types.'
548
1285
  ),
549
- assigneeId: z4.string().optional().describe("Filter by assigned user ID"),
550
- unassigned: z4.boolean().optional().describe("Only return tasks with no assignee (mutually exclusive with assigneeId)"),
1286
+ assigneeId: z5.string().optional().describe("Filter by assigned user ID"),
1287
+ unassigned: z5.boolean().optional().describe("Only return tasks with no assignee (mutually exclusive with assigneeId)"),
551
1288
  subProjectId: BOARD_FILTER,
552
- limit: z4.number().optional().describe("Max results to return (default 20)")
1289
+ limit: z5.number().optional().describe("Max results to return (default 20)")
553
1290
  },
554
1291
  async (params) => {
555
1292
  const tasks = await conn2.searchTasks(params);
@@ -560,26 +1297,19 @@ function registerSearchTasks(server2, conn2) {
560
1297
  );
561
1298
  }
562
1299
  function registerListTags(server2, conn2) {
563
- server2.tool(
564
- "list_tags",
565
- "List all project tags with their names, IDs, and colors. Pass projectId to target a specific project; otherwise the configured default project is used.",
566
- {
567
- projectId: z4.string().optional().describe("Target Conveyor project ID")
568
- },
569
- async (params) => {
570
- const tags = await conn2.listTags(params.projectId);
571
- return { content: [{ type: "text", text: JSON.stringify(tags, null, 2) }] };
572
- }
573
- );
1300
+ registerContractTool(server2, listTagsContract, async (params) => {
1301
+ const tags = await conn2.listTags(params.projectId);
1302
+ return { content: [{ type: "text", text: JSON.stringify(tags, null, 2) }] };
1303
+ });
574
1304
  }
575
1305
  function registerReviewTools(server2, conn2) {
576
1306
  server2.tool(
577
1307
  "approve_task",
578
1308
  "Move a task forward in the review flow (ReviewPR -> ReviewDev, or -> Complete). Pass projectId to target a specific project; otherwise the configured default project is used.",
579
1309
  {
580
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
581
- taskId: z4.string().describe("The task ID"),
582
- risk: z4.enum(RISK_ENUM).optional().describe("Optional risk level to record; defaults to a raise-only 'low' on approval.")
1310
+ projectId: z5.string().optional().describe("Target Conveyor project ID"),
1311
+ taskId: z5.string().describe("The task ID"),
1312
+ risk: z5.enum(RISK_ENUM).optional().describe("Optional risk level to record; defaults to a raise-only 'low' on approval.")
583
1313
  },
584
1314
  async (params) => {
585
1315
  const result = await conn2.approveTask(params.taskId, params.projectId, params.risk);
@@ -588,33 +1318,25 @@ function registerReviewTools(server2, conn2) {
588
1318
  };
589
1319
  }
590
1320
  );
591
- server2.tool(
592
- "approve_and_merge_pr",
593
- "Approve and merge a child task's pull request. Pass projectId to target a specific project; otherwise the configured default project is used. Only succeeds if all CI/CD checks are passing. The child task must be in ReviewPR status with a PR.",
594
- {
595
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
596
- childTaskId: z4.string().describe("The child task ID whose PR should be approved and merged")
597
- },
598
- async (params) => {
599
- const result = await conn2.approveAndMergePR(params.childTaskId, params.projectId);
600
- return {
601
- content: [
602
- {
603
- type: "text",
604
- text: `PR #${result.prNumber} approved and merged for task ${result.childTaskId}`
605
- }
606
- ]
607
- };
608
- }
609
- );
1321
+ registerContractTool(server2, approveAndMergePrContract, async (params) => {
1322
+ const result = await conn2.approveAndMergePR(params.childTaskId, params.projectId);
1323
+ return {
1324
+ content: [
1325
+ {
1326
+ type: "text",
1327
+ text: `PR #${result.prNumber} approved and merged for task ${result.childTaskId}`
1328
+ }
1329
+ ]
1330
+ };
1331
+ });
610
1332
  server2.tool(
611
1333
  "request_changes",
612
1334
  "Post feedback and send task back to InProgress for more work. Pass projectId to target a specific project; otherwise the configured default project is used.",
613
1335
  {
614
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
615
- taskId: z4.string().describe("The task ID"),
616
- feedback: z4.string().describe("Feedback message describing requested changes"),
617
- risk: z4.enum(RISK_ENUM).optional().describe("Optional risk level to record; defaults to a raise-only 'medium' on changes.")
1336
+ projectId: z5.string().optional().describe("Target Conveyor project ID"),
1337
+ taskId: z5.string().describe("The task ID"),
1338
+ feedback: z5.string().describe("Feedback message describing requested changes"),
1339
+ risk: z5.enum(RISK_ENUM).optional().describe("Optional risk level to record; defaults to a raise-only 'medium' on changes.")
618
1340
  },
619
1341
  async (params) => {
620
1342
  await conn2.requestChanges(params.taskId, params.feedback, params.projectId, params.risk);
@@ -631,9 +1353,9 @@ function registerReviewerTools(server2, conn2) {
631
1353
  "add_reviewer",
632
1354
  "Add a project member as a reviewer on a task. Pass projectId to target a specific project; otherwise the configured default project is used. Idempotent \u2014 adding an existing reviewer is a no-op.",
633
1355
  {
634
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
635
- taskId: z4.string().describe("The task ID or slug"),
636
- userId: z4.string().describe("User ID of the reviewer (use list_project_members to resolve a name or email)")
1356
+ projectId: z5.string().optional().describe("Target Conveyor project ID"),
1357
+ taskId: z5.string().describe("The task ID or slug"),
1358
+ userId: z5.string().describe("User ID of the reviewer (use list_project_members to resolve a name or email)")
637
1359
  },
638
1360
  async (params) => {
639
1361
  const result = await conn2.addReviewer(params);
@@ -651,9 +1373,9 @@ function registerReviewerTools(server2, conn2) {
651
1373
  "remove_reviewer",
652
1374
  "Remove a reviewer from a task. Pass projectId to target a specific project; otherwise the configured default project is used. Idempotent \u2014 removing a non-reviewer is a no-op.",
653
1375
  {
654
- projectId: z4.string().optional().describe("Target Conveyor project ID"),
655
- taskId: z4.string().describe("The task ID or slug"),
656
- userId: z4.string().describe("User ID of the reviewer to remove")
1376
+ projectId: z5.string().optional().describe("Target Conveyor project ID"),
1377
+ taskId: z5.string().describe("The task ID or slug"),
1378
+ userId: z5.string().describe("User ID of the reviewer to remove")
657
1379
  },
658
1380
  async (params) => {
659
1381
  const result = await conn2.removeReviewer(params);
@@ -685,7 +1407,7 @@ function registerTaskTools(server2, conn2) {
685
1407
  }
686
1408
 
687
1409
  // src/tools/builds.ts
688
- import { z as z5 } from "zod";
1410
+ import { z as z6 } from "zod";
689
1411
  function textResult2(result) {
690
1412
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
691
1413
  }
@@ -694,8 +1416,8 @@ function registerTaskLifecycleTools(server2, conn2) {
694
1416
  "stop_task",
695
1417
  "Compatibility alias for the legacy stop path. stop_task now performs the same durable sleep behavior as sleep_task, preserving task Claudespace state while stopping compute. Pass projectId to target a specific project; otherwise the configured default project is used.",
696
1418
  {
697
- projectId: z5.string().optional().describe("Target Conveyor project ID"),
698
- taskId: z5.string().describe("The task ID")
1419
+ projectId: z6.string().optional().describe("Target Conveyor project ID"),
1420
+ taskId: z6.string().describe("The task ID")
699
1421
  },
700
1422
  async (params) => textResult2(await conn2.sleepTask(params.taskId, params.projectId))
701
1423
  );
@@ -703,8 +1425,8 @@ function registerTaskLifecycleTools(server2, conn2) {
703
1425
  "sleep_task",
704
1426
  "Sleep a task Claudespace, stopping compute while preserving durable state. Pass projectId to target a specific project; otherwise the configured default project is used.",
705
1427
  {
706
- projectId: z5.string().optional().describe("Target Conveyor project ID"),
707
- taskId: z5.string().describe("The task ID")
1428
+ projectId: z6.string().optional().describe("Target Conveyor project ID"),
1429
+ taskId: z6.string().describe("The task ID")
708
1430
  },
709
1431
  async (params) => textResult2(await conn2.sleepTask(params.taskId, params.projectId))
710
1432
  );
@@ -712,8 +1434,8 @@ function registerTaskLifecycleTools(server2, conn2) {
712
1434
  "resume_task",
713
1435
  "Resume a sleeping task Claudespace. Pass projectId to target a specific project; otherwise the configured default project is used.",
714
1436
  {
715
- projectId: z5.string().optional().describe("Target Conveyor project ID"),
716
- taskId: z5.string().describe("The task ID")
1437
+ projectId: z6.string().optional().describe("Target Conveyor project ID"),
1438
+ taskId: z6.string().describe("The task ID")
717
1439
  },
718
1440
  async (params) => textResult2(await conn2.resumeTask(params.taskId, params.projectId))
719
1441
  );
@@ -721,8 +1443,8 @@ function registerTaskLifecycleTools(server2, conn2) {
721
1443
  "delete_task_environment",
722
1444
  "Delete a task environment, including durable Claudespace state. Pass projectId to target a specific project; otherwise the configured default project is used.",
723
1445
  {
724
- projectId: z5.string().optional().describe("Target Conveyor project ID"),
725
- taskId: z5.string().describe("The task ID")
1446
+ projectId: z6.string().optional().describe("Target Conveyor project ID"),
1447
+ taskId: z6.string().describe("The task ID")
726
1448
  },
727
1449
  async (params) => textResult2(await conn2.deleteTaskEnvironment(params.taskId, params.projectId))
728
1450
  );
@@ -732,8 +1454,8 @@ function registerBuildTools(server2, conn2) {
732
1454
  "start_task",
733
1455
  "Start a cloud build (codespace) for a task. Pass projectId to target a specific project; otherwise the configured default project is used.",
734
1456
  {
735
- projectId: z5.string().optional().describe("Target Conveyor project ID"),
736
- taskId: z5.string().describe("The task ID")
1457
+ projectId: z6.string().optional().describe("Target Conveyor project ID"),
1458
+ taskId: z6.string().describe("The task ID")
737
1459
  },
738
1460
  async (params) => {
739
1461
  const result = await conn2.startBuild(params.taskId, params.projectId);
@@ -745,8 +1467,8 @@ function registerBuildTools(server2, conn2) {
745
1467
  "create_release",
746
1468
  "Create a release for the project \u2014 the same flow as the Release button in the web UI. Pass projectId to target a specific project; otherwise the configured default project is used. Creates a release task with a release/YYYY.MM.N branch and a PR from the dev branch to the default branch. Omit taskIds to release ALL cards currently in Review (Dev); pass a subset to cherry-pick \u2014 a cloud build agent then cherry-picks those changes and resolves conflicts. Fails if a release is already in progress or no cards are in Review (Dev).",
747
1469
  {
748
- projectId: z5.string().optional().describe("Target Conveyor project ID"),
749
- taskIds: z5.array(z5.string()).optional().describe(
1470
+ projectId: z6.string().optional().describe("Target Conveyor project ID"),
1471
+ taskIds: z6.array(z6.string()).optional().describe(
750
1472
  "Task IDs in Review (Dev) to cherry-pick into the release. Omit to release all of them."
751
1473
  )
752
1474
  },
@@ -759,8 +1481,8 @@ function registerBuildTools(server2, conn2) {
759
1481
  "get_build_status",
760
1482
  "Check codespace and agent status for a task. Pass projectId to target a specific project; otherwise the configured default project is used.",
761
1483
  {
762
- projectId: z5.string().optional().describe("Target Conveyor project ID"),
763
- taskId: z5.string().describe("The task ID")
1484
+ projectId: z6.string().optional().describe("Target Conveyor project ID"),
1485
+ taskId: z6.string().describe("The task ID")
764
1486
  },
765
1487
  async (params) => {
766
1488
  const status = await conn2.getBuildStatus(params.taskId, params.projectId);
@@ -772,7 +1494,6 @@ function registerBuildTools(server2, conn2) {
772
1494
  // src/tools/attachments.ts
773
1495
  import { readFile, stat } from "fs/promises";
774
1496
  import { basename, extname } from "path";
775
- import { z as z6 } from "zod";
776
1497
  var MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024;
777
1498
  var MIME_BY_EXT = {
778
1499
  ".png": "image/png",
@@ -805,18 +1526,10 @@ function inferMimeType(filePath, override) {
805
1526
  return MIME_BY_EXT[extname(filePath).toLowerCase()] ?? "application/octet-stream";
806
1527
  }
807
1528
  function registerListTaskFiles(server2, conn2) {
808
- server2.tool(
809
- "list_task_files",
810
- "List all files attached to a task with metadata (no contents \u2014 fast and small). 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.",
811
- {
812
- projectId: z6.string().optional().describe("Target Conveyor project ID"),
813
- taskId: z6.string().describe("The task ID or slug")
814
- },
815
- async (params) => {
816
- const files = await conn2.listTaskFiles(params.taskId, params.projectId);
817
- return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
818
- }
819
- );
1529
+ registerContractTool(server2, listTaskFilesContract, async (params) => {
1530
+ const files = await conn2.listTaskFiles(params.taskId, params.projectId);
1531
+ return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
1532
+ });
820
1533
  }
821
1534
  function buildTextContent(file, content) {
822
1535
  const start = file.contentByteOffset ?? 0;
@@ -848,90 +1561,68 @@ function buildAttachmentContent(file) {
848
1561
  return [{ type: "text", text: JSON.stringify(metadata, null, 2) }];
849
1562
  }
850
1563
  function registerGetAttachment(server2, conn2) {
851
- server2.tool(
852
- "get_attachment",
853
- "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, or fetch `downloadUrl` for the whole file. Call list_task_files first to discover IDs and sizes.",
854
- {
855
- projectId: z6.string().optional().describe("Target Conveyor project ID"),
856
- taskId: z6.string().describe("The task ID or slug"),
857
- fileId: z6.string().describe("The file ID to fetch"),
858
- offset: z6.number().int().nonnegative().optional().describe("Byte offset into text content (paging). Default 0."),
859
- maxBytes: z6.number().int().positive().optional().describe("Max bytes of text content to return from offset.")
860
- },
861
- async (params) => {
862
- const file = await conn2.getAttachment(params.taskId, params.fileId, {
863
- offset: params.offset,
864
- maxBytes: params.maxBytes,
865
- projectId: params.projectId
866
- });
867
- return { content: buildAttachmentContent(file) };
868
- }
869
- );
1564
+ registerContractTool(server2, getAttachmentContract, async (params) => {
1565
+ const file = await conn2.getAttachment(params.taskId, params.fileId, {
1566
+ offset: params.offset,
1567
+ maxBytes: params.maxBytes,
1568
+ projectId: params.projectId
1569
+ });
1570
+ return { content: buildAttachmentContent(file) };
1571
+ });
870
1572
  }
871
1573
  function registerUploadAttachment(server2, conn2) {
872
- server2.tool(
873
- "upload_attachment",
874
- "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.",
875
- {
876
- projectId: z6.string().optional().describe("Target Conveyor project ID"),
877
- taskId: z6.string().describe("The task ID or slug"),
878
- path: z6.string().describe("Absolute path to the local file to upload"),
879
- comment: z6.string().optional().describe("When set, also posts the attachment to the task chat with this text"),
880
- mimeType: z6.string().optional().describe("Override the mime type inferred from the file extension")
881
- },
882
- async (params) => {
883
- const info = await stat(params.path).catch(() => null);
884
- if (!info?.isFile()) {
885
- return { content: [{ type: "text", text: `File not found: ${params.path}` }] };
886
- }
887
- if (info.size > MAX_FILE_SIZE_BYTES) {
888
- return {
889
- content: [
890
- {
891
- type: "text",
892
- text: `File is ${info.size} bytes \u2014 exceeds the ${MAX_FILE_SIZE_BYTES} byte (25MB) upload limit.`
893
- }
894
- ]
895
- };
896
- }
897
- const fileName = basename(params.path);
898
- const mimeType = inferMimeType(params.path, params.mimeType);
899
- const { fileId, uploadUrl } = await conn2.requestFileUpload(params.taskId, {
900
- fileName,
901
- mimeType,
902
- fileSize: info.size,
903
- projectId: params.projectId
904
- });
905
- const body = await readFile(params.path);
906
- const res = await fetch(uploadUrl, {
907
- method: "PUT",
908
- headers: { "Content-Type": mimeType },
909
- body
910
- });
911
- if (!res.ok) {
912
- return {
913
- content: [
914
- {
915
- type: "text",
916
- text: `Upload to storage failed: HTTP ${res.status} ${await res.text().catch(() => "")}`
917
- }
918
- ]
919
- };
920
- }
921
- const result = await conn2.confirmFileUpload(
922
- params.taskId,
923
- fileId,
924
- params.comment,
925
- params.projectId
926
- );
927
- const lines = [
928
- `Uploaded ${result.fileName} (${info.size} bytes, ${mimeType}). File ID: ${result.fileId}`
929
- ];
930
- if (result.downloadUrl) lines.push(`downloadUrl: ${result.downloadUrl}`);
931
- if (result.messageId) lines.push("Posted to the task chat.");
932
- return { content: [{ type: "text", text: lines.join("\n") }] };
1574
+ registerContractTool(server2, uploadAttachmentContract, async (params) => {
1575
+ const info = await stat(params.path).catch(() => null);
1576
+ if (!info?.isFile()) {
1577
+ return { content: [{ type: "text", text: `File not found: ${params.path}` }] };
933
1578
  }
934
- );
1579
+ if (info.size > MAX_FILE_SIZE_BYTES) {
1580
+ return {
1581
+ content: [
1582
+ {
1583
+ type: "text",
1584
+ text: `File is ${info.size} bytes \u2014 exceeds the ${MAX_FILE_SIZE_BYTES} byte (25MB) upload limit.`
1585
+ }
1586
+ ]
1587
+ };
1588
+ }
1589
+ const fileName = basename(params.path);
1590
+ const mimeType = inferMimeType(params.path, params.mimeType);
1591
+ const { fileId, uploadUrl } = await conn2.requestFileUpload(params.taskId, {
1592
+ fileName,
1593
+ mimeType,
1594
+ fileSize: info.size,
1595
+ projectId: params.projectId
1596
+ });
1597
+ const body = await readFile(params.path);
1598
+ const res = await fetch(uploadUrl, {
1599
+ method: "PUT",
1600
+ headers: { "Content-Type": mimeType },
1601
+ body
1602
+ });
1603
+ if (!res.ok) {
1604
+ return {
1605
+ content: [
1606
+ {
1607
+ type: "text",
1608
+ text: `Upload to storage failed: HTTP ${res.status} ${await res.text().catch(() => "")}`
1609
+ }
1610
+ ]
1611
+ };
1612
+ }
1613
+ const result = await conn2.confirmFileUpload(
1614
+ params.taskId,
1615
+ fileId,
1616
+ params.comment,
1617
+ params.projectId
1618
+ );
1619
+ const lines = [
1620
+ `Uploaded ${result.fileName} (${info.size} bytes, ${mimeType}). File ID: ${result.fileId}`
1621
+ ];
1622
+ if (result.downloadUrl) lines.push(`downloadUrl: ${result.downloadUrl}`);
1623
+ if (result.messageId) lines.push("Posted to the task chat.");
1624
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1625
+ });
935
1626
  }
936
1627
  function registerAttachmentTools(server2, conn2) {
937
1628
  registerListTaskFiles(server2, conn2);
@@ -940,126 +1631,45 @@ function registerAttachmentTools(server2, conn2) {
940
1631
  }
941
1632
 
942
1633
  // src/tools/pull-request.ts
943
- import { z as z7 } from "zod";
944
1634
  function registerPullRequestTools(server2, conn2) {
945
- server2.tool(
946
- "create_pull_request",
947
- "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.",
948
- {
949
- projectId: z7.string().optional().describe("Target Conveyor project ID"),
950
- taskId: z7.string().describe("The task ID whose branch should be opened as a PR"),
951
- title: z7.string().describe("Pull request title"),
952
- body: z7.string().describe("Pull request body (markdown)"),
953
- head: z7.string().optional().describe("Source branch for the PR (defaults to the task's branch)"),
954
- base: z7.string().optional().describe("Target branch for the PR (defaults to the repo default)")
955
- },
956
- async (params) => {
957
- const result = await conn2.createPullRequest(params);
958
- return {
959
- content: [{ type: "text", text: `PR #${result.prNumber} opened: ${result.prUrl}` }]
960
- };
961
- }
962
- );
1635
+ registerContractTool(server2, createPullRequestContract, async (params) => {
1636
+ const result = await conn2.createPullRequest(params);
1637
+ return {
1638
+ content: [{ type: "text", text: `PR #${result.prNumber} opened: ${result.prUrl}` }]
1639
+ };
1640
+ });
963
1641
  }
964
1642
 
965
1643
  // src/tools/subtasks.ts
966
- import { z as z8 } from "zod";
967
- var STATUS_ENUM2 = [
968
- "Planning",
969
- "Open",
970
- "InProgress",
971
- "ReviewPR",
972
- "ReviewDev",
973
- "ReviewLive",
974
- "Complete",
975
- "Cancelled"
976
- ];
977
- var SP_DESCRIPTION = "Story point value (1=Common, 2=Magic, 3=Rare, 5=Unique)";
978
- var FOLLOW_PARENT_STATUS_DESCRIPTION = "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.";
979
1644
  function registerCreateSubtask(server2, conn2) {
980
- server2.tool(
981
- "create_subtask",
982
- "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.",
983
- {
984
- projectId: z8.string().optional().describe("Target Conveyor project ID"),
985
- parentTaskId: z8.string().describe("The parent task ID"),
986
- title: z8.string().describe("Subtask title"),
987
- description: z8.string().optional().describe("Subtask description"),
988
- plan: z8.string().optional().describe("Subtask implementation plan (markdown)"),
989
- ordinal: z8.number().optional().describe("Ordering position among siblings"),
990
- storyPointValue: z8.number().optional().describe(SP_DESCRIPTION),
991
- followParentStatus: z8.boolean().optional().describe(FOLLOW_PARENT_STATUS_DESCRIPTION),
992
- dependsOn: z8.array(z8.string()).optional().describe(
993
- "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."
994
- )
995
- },
996
- async (params) => {
997
- const subtask = await conn2.createSubtask(params);
998
- return {
999
- content: [{ type: "text", text: `Subtask created: ${subtask.id} (slug: ${subtask.slug})` }]
1000
- };
1001
- }
1002
- );
1645
+ registerContractTool(server2, createSubtaskContract, async (params) => {
1646
+ const subtask = await conn2.createSubtask(params);
1647
+ return {
1648
+ content: [{ type: "text", text: `Subtask created: ${subtask.id} (slug: ${subtask.slug})` }]
1649
+ };
1650
+ });
1003
1651
  }
1004
1652
  function registerUpdateSubtask(server2, conn2) {
1005
- server2.tool(
1006
- "update_subtask",
1007
- "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.",
1008
- {
1009
- projectId: z8.string().optional().describe("Target Conveyor project ID"),
1010
- subtaskId: z8.string().describe("The subtask ID"),
1011
- title: z8.string().optional().describe("New title"),
1012
- description: z8.string().optional().describe("New description"),
1013
- plan: z8.string().optional().describe("New plan (markdown)"),
1014
- status: z8.enum(STATUS_ENUM2).optional().describe("New status"),
1015
- ordinal: z8.number().optional().describe("New ordering position among siblings"),
1016
- storyPointValue: z8.number().optional().describe(SP_DESCRIPTION),
1017
- followParentStatus: z8.boolean().optional().describe(FOLLOW_PARENT_STATUS_DESCRIPTION),
1018
- dependsOn: z8.array(z8.string()).optional().describe(
1019
- "Replace the sibling subtask ids/slugs this subtask blocks on (pass [] to clear). Omit to leave dependencies unchanged."
1020
- )
1021
- },
1022
- async (params) => {
1023
- const result = await conn2.updateSubtask(params);
1024
- return {
1025
- content: [
1026
- { type: "text", text: `Subtask ${result.id} updated (status: ${result.status})` }
1027
- ]
1028
- };
1029
- }
1030
- );
1653
+ registerContractTool(server2, updateSubtaskContract, async (params) => {
1654
+ const result = await conn2.updateSubtask(params);
1655
+ return {
1656
+ content: [{ type: "text", text: `Subtask ${result.id} updated (status: ${result.status})` }]
1657
+ };
1658
+ });
1031
1659
  }
1032
1660
  function registerListSubtasks(server2, conn2) {
1033
- server2.tool(
1034
- "list_subtasks",
1035
- "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.",
1036
- {
1037
- projectId: z8.string().optional().describe("Target Conveyor project ID"),
1038
- taskId: z8.string().describe("The parent task ID")
1039
- },
1040
- async (params) => {
1041
- const subtasks = await conn2.listSubtasks(params.taskId, params.projectId);
1042
- return { content: [{ type: "text", text: JSON.stringify(subtasks, null, 2) }] };
1043
- }
1044
- );
1661
+ registerContractTool(server2, listSubtasksContract, async (params) => {
1662
+ const subtasks = await conn2.listSubtasks(params.taskId, params.projectId);
1663
+ return { content: [{ type: "text", text: JSON.stringify(subtasks, null, 2) }] };
1664
+ });
1045
1665
  }
1046
1666
  function registerDeleteSubtask(server2, conn2) {
1047
- server2.tool(
1048
- "delete_subtask",
1049
- "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.",
1050
- {
1051
- projectId: z8.string().optional().describe("Target Conveyor project ID"),
1052
- subtaskId: z8.string().describe("The subtask ID to delete")
1053
- },
1054
- async (params) => {
1055
- const result = await conn2.deleteSubtask(params.subtaskId, params.projectId);
1056
- return {
1057
- content: [
1058
- { type: "text", text: result.deleted ? "Subtask deleted" : "Subtask not deleted" }
1059
- ]
1060
- };
1061
- }
1062
- );
1667
+ registerContractTool(server2, deleteSubtaskContract, async (params) => {
1668
+ const result = await conn2.deleteSubtask(params.subtaskId, params.projectId);
1669
+ return {
1670
+ content: [{ type: "text", text: result.deleted ? "Subtask deleted" : "Subtask not deleted" }]
1671
+ };
1672
+ });
1063
1673
  }
1064
1674
  function registerSubtaskTools(server2, conn2) {
1065
1675
  registerCreateSubtask(server2, conn2);
@@ -1069,50 +1679,23 @@ function registerSubtaskTools(server2, conn2) {
1069
1679
  }
1070
1680
 
1071
1681
  // src/tools/dependencies.ts
1072
- import { z as z9 } from "zod";
1073
1682
  function registerGetDependencies(server2, conn2) {
1074
- server2.tool(
1075
- "get_dependencies",
1076
- "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.",
1077
- {
1078
- projectId: z9.string().optional().describe("Target Conveyor project ID"),
1079
- taskId: z9.string().describe("The task ID")
1080
- },
1081
- async (params) => {
1082
- const deps = await conn2.getDependencies(params.taskId, params.projectId);
1083
- return { content: [{ type: "text", text: JSON.stringify(deps, null, 2) }] };
1084
- }
1085
- );
1683
+ registerContractTool(server2, getDependenciesContract, async (params) => {
1684
+ const deps = await conn2.getDependencies(params.taskId, params.projectId);
1685
+ return { content: [{ type: "text", text: JSON.stringify(deps, null, 2) }] };
1686
+ });
1086
1687
  }
1087
1688
  function registerAddDependency(server2, conn2) {
1088
- server2.tool(
1089
- "add_dependency",
1090
- "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.",
1091
- {
1092
- projectId: z9.string().optional().describe("Target Conveyor project ID"),
1093
- taskId: z9.string().describe("The task ID that will be blocked"),
1094
- dependsOnSlugOrId: z9.string().describe("Slug or ID of the task this one depends on")
1095
- },
1096
- async (params) => {
1097
- await conn2.addDependency(params);
1098
- return { content: [{ type: "text", text: "Dependency added" }] };
1099
- }
1100
- );
1689
+ registerContractTool(server2, addDependencyContract, async (params) => {
1690
+ await conn2.addDependency(params);
1691
+ return { content: [{ type: "text", text: "Dependency added" }] };
1692
+ });
1101
1693
  }
1102
1694
  function registerRemoveDependency(server2, conn2) {
1103
- server2.tool(
1104
- "remove_dependency",
1105
- "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.",
1106
- {
1107
- projectId: z9.string().optional().describe("Target Conveyor project ID"),
1108
- taskId: z9.string().describe("The task ID to unblock"),
1109
- dependsOnSlugOrId: z9.string().describe("Slug or ID of the dependency to remove")
1110
- },
1111
- async (params) => {
1112
- await conn2.removeDependency(params);
1113
- return { content: [{ type: "text", text: "Dependency removed" }] };
1114
- }
1115
- );
1695
+ registerContractTool(server2, removeDependencyContract, async (params) => {
1696
+ await conn2.removeDependency(params);
1697
+ return { content: [{ type: "text", text: "Dependency removed" }] };
1698
+ });
1116
1699
  }
1117
1700
  function registerDependencyTools(server2, conn2) {
1118
1701
  registerGetDependencies(server2, conn2);
@@ -1121,27 +1704,15 @@ function registerDependencyTools(server2, conn2) {
1121
1704
  }
1122
1705
 
1123
1706
  // src/tools/suggestions.ts
1124
- import { z as z10 } from "zod";
1125
1707
  function registerSuggestionTools(server2, conn2) {
1126
- server2.tool(
1127
- "create_suggestion",
1128
- "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.",
1129
- {
1130
- projectId: z10.string().optional().describe("Target Conveyor project ID"),
1131
- title: z10.string().describe("Suggestion title"),
1132
- description: z10.string().optional().describe("Suggestion details (markdown)"),
1133
- tagNames: z10.array(z10.string()).optional().describe('Tag names to categorize the suggestion (e.g., ["agent-runner"])')
1134
- },
1135
- async (params) => {
1136
- const result = await conn2.createSuggestion(params);
1137
- const text = result.merged ? `Suggestion merged into existing suggestion ${result.mergedIntoId} (upvote recorded)` : `Suggestion created: ${result.id}`;
1138
- return { content: [{ type: "text", text }] };
1139
- }
1140
- );
1708
+ registerContractTool(server2, createSuggestionContract, async (params) => {
1709
+ const result = await conn2.createSuggestion(params);
1710
+ const text = result.merged ? `Suggestion merged into existing suggestion ${result.mergedIntoId} (upvote recorded)` : `Suggestion created: ${result.id}`;
1711
+ return { content: [{ type: "text", text }] };
1712
+ });
1141
1713
  }
1142
1714
 
1143
1715
  // src/tools/checklists.ts
1144
- import { z as z11 } from "zod";
1145
1716
  function renderManualTestGroups(groups) {
1146
1717
  const lines = [];
1147
1718
  for (const g of groups) {
@@ -1150,8 +1721,8 @@ function renderManualTestGroups(groups) {
1150
1721
  const mark = t.status === "approved" ? "\u2713" : t.status === "rejected" ? "\u2717" : "\u25CB";
1151
1722
  lines.push(` ${mark} ${t.title} \u2014 ${t.status}`);
1152
1723
  if (t.status === "rejected") {
1153
- for (const f of t.failures ?? []) {
1154
- lines.push(` \u26A0 ${f.userName ?? "Someone"}: ${f.reason ?? "(no message)"}`);
1724
+ for (const f2 of t.failures ?? []) {
1725
+ lines.push(` \u26A0 ${f2.userName ?? "Someone"}: ${f2.reason ?? "(no message)"}`);
1155
1726
  }
1156
1727
  }
1157
1728
  }
@@ -1160,139 +1731,75 @@ function renderManualTestGroups(groups) {
1160
1731
  return lines.join("\n").trimEnd();
1161
1732
  }
1162
1733
  function registerListManualTests(server2, conn2) {
1163
- server2.tool(
1164
- "list_manual_tests",
1165
- "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.",
1166
- {
1167
- projectId: z11.string().optional().describe("Target Conveyor project ID"),
1168
- taskId: z11.string().describe("The task ID or slug (the value in a card URL, /cards/<slug>)")
1169
- },
1170
- async (params) => {
1171
- const items = await conn2.listManualTests(params.taskId, params.projectId);
1172
- if (items.length === 0) {
1173
- return { content: [{ type: "text", text: "No manual tests recorded for this task." }] };
1174
- }
1175
- const lines = items.flatMap((item, i) => {
1176
- const checked = item.checked ? "[x]" : "[ ]";
1177
- const row = [`${i + 1}. ${checked} ${item.title}`];
1178
- for (const f of item.failures ?? []) {
1179
- const who = f.userName ?? "Someone";
1180
- const reason = f.reason ?? "(no message)";
1181
- row.push(` \u26A0 Failed (${who}): ${reason}`);
1182
- }
1183
- return row;
1184
- });
1185
- return { content: [{ type: "text", text: lines.join("\n") }] };
1734
+ registerContractTool(server2, listManualTestsContract, async (params) => {
1735
+ const items = await conn2.listManualTests(params.taskId, params.projectId);
1736
+ if (items.length === 0) {
1737
+ return { content: [{ type: "text", text: "No manual tests recorded for this task." }] };
1186
1738
  }
1187
- );
1739
+ const lines = items.flatMap((item, i) => {
1740
+ const checked = item.checked ? "[x]" : "[ ]";
1741
+ const row = [`${i + 1}. ${checked} ${item.title}`];
1742
+ for (const f2 of item.failures ?? []) {
1743
+ const who = f2.userName ?? "Someone";
1744
+ const reason = f2.reason ?? "(no message)";
1745
+ row.push(` \u26A0 Failed (${who}): ${reason}`);
1746
+ }
1747
+ return row;
1748
+ });
1749
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1750
+ });
1188
1751
  }
1189
1752
  function registerSetManualTests(server2, conn2) {
1190
- server2.tool(
1191
- "set_manual_tests",
1192
- "Add manual test steps to a task's checklist. Pass projectId to target a specific project; otherwise the configured default project is used. Existing items with the same title are automatically skipped (deduplication). Use to record specific manual verification steps that reviewers should follow when testing the task's PR.",
1193
- {
1194
- projectId: z11.string().optional().describe("Target Conveyor project ID"),
1195
- taskId: z11.string().describe("The task ID or slug"),
1196
- items: z11.array(z11.object({ title: z11.string().min(1).describe("A concise, actionable test step") })).min(1).describe("List of manual test steps to add")
1197
- },
1198
- async (params) => {
1199
- const result = await conn2.setManualTests(params.taskId, params.items, params.projectId);
1200
- const parts = [`Created ${result.created} manual test item(s).`];
1201
- if (result.skipped > 0) parts.push(`Skipped ${result.skipped} duplicate(s).`);
1202
- return { content: [{ type: "text", text: parts.join(" ") }] };
1203
- }
1204
- );
1753
+ registerContractTool(server2, setManualTestsContract, async (params) => {
1754
+ const result = await conn2.setManualTests(params.taskId, params.items, params.projectId);
1755
+ const parts = [`Created ${result.created} manual test item(s).`];
1756
+ if (result.skipped > 0) parts.push(`Skipped ${result.skipped} duplicate(s).`);
1757
+ return { content: [{ type: "text", text: parts.join(" ") }] };
1758
+ });
1205
1759
  }
1206
1760
  function registerEditManualTest(server2, conn2) {
1207
- server2.tool(
1208
- "edit_manual_test",
1209
- "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.",
1210
- {
1211
- projectId: z11.string().optional().describe("Target Conveyor project ID"),
1212
- taskId: z11.string().describe("The task ID or slug"),
1213
- title: z11.string().min(1).describe("The current title of the manual test to edit"),
1214
- newTitle: z11.string().min(1).describe("The new title for the manual test")
1215
- },
1216
- async (params) => {
1217
- await conn2.editManualTest(params.taskId, params.title, params.newTitle, params.projectId);
1218
- return { content: [{ type: "text", text: `Updated manual test to "${params.newTitle}".` }] };
1219
- }
1220
- );
1761
+ registerContractTool(server2, editManualTestContract, async (params) => {
1762
+ await conn2.editManualTest(params.taskId, params.title, params.newTitle, params.projectId);
1763
+ return { content: [{ type: "text", text: `Updated manual test to "${params.newTitle}".` }] };
1764
+ });
1221
1765
  }
1222
1766
  function registerRemoveManualTest(server2, conn2) {
1223
- server2.tool(
1224
- "remove_manual_test",
1225
- "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).",
1226
- {
1227
- projectId: z11.string().optional().describe("Target Conveyor project ID"),
1228
- taskId: z11.string().describe("The task ID or slug"),
1229
- title: z11.string().min(1).describe("The title of the manual test to remove")
1230
- },
1231
- async (params) => {
1232
- await conn2.removeManualTest(params.taskId, params.title, params.projectId);
1233
- return { content: [{ type: "text", text: `Removed manual test "${params.title}".` }] };
1234
- }
1235
- );
1767
+ registerContractTool(server2, removeManualTestContract, async (params) => {
1768
+ await conn2.removeManualTest(params.taskId, params.title, params.projectId);
1769
+ return { content: [{ type: "text", text: `Removed manual test "${params.title}".` }] };
1770
+ });
1236
1771
  }
1237
1772
  function registerApproveManualTest(server2, conn2) {
1238
- server2.tool(
1239
- "approve_manual_test",
1240
- "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.",
1241
- {
1242
- projectId: z11.string().optional().describe("Target Conveyor project ID"),
1243
- taskId: z11.string().describe("The task ID or slug"),
1244
- title: z11.string().min(1).describe("The title of the manual test to approve")
1245
- },
1246
- async (params) => {
1247
- await conn2.approveManualTest(params.taskId, params.title, params.projectId);
1248
- return { content: [{ type: "text", text: `Approved manual test "${params.title}".` }] };
1249
- }
1250
- );
1773
+ registerContractTool(server2, approveManualTestContract, async (params) => {
1774
+ await conn2.approveManualTest(params.taskId, params.title, params.projectId);
1775
+ return { content: [{ type: "text", text: `Approved manual test "${params.title}".` }] };
1776
+ });
1251
1777
  }
1252
1778
  function registerRejectManualTest(server2, conn2) {
1253
- server2.tool(
1254
- "reject_manual_test",
1255
- "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.",
1256
- {
1257
- projectId: z11.string().optional().describe("Target Conveyor project ID"),
1258
- taskId: z11.string().describe("The task ID or slug"),
1259
- title: z11.string().min(1).describe("The title of the manual test to reject"),
1260
- reason: z11.string().min(1).max(2e3).describe("Why the test failed \u2014 what went wrong, shown to the team")
1261
- },
1262
- async (params) => {
1263
- await conn2.rejectManualTest(params.taskId, params.title, params.reason, params.projectId);
1264
- return {
1265
- content: [
1266
- {
1267
- type: "text",
1268
- text: `Flagged an issue with manual test "${params.title}": ${params.reason}`
1269
- }
1270
- ]
1271
- };
1272
- }
1273
- );
1779
+ registerContractTool(server2, rejectManualTestContract, async (params) => {
1780
+ await conn2.rejectManualTest(params.taskId, params.title, params.reason, params.projectId);
1781
+ return {
1782
+ content: [
1783
+ {
1784
+ type: "text",
1785
+ text: `Flagged an issue with manual test "${params.title}": ${params.reason}`
1786
+ }
1787
+ ]
1788
+ };
1789
+ });
1274
1790
  }
1275
1791
  function registerQueryManualTests(server2, conn2) {
1276
- server2.tool(
1277
- "query_manual_tests",
1278
- "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.",
1279
- {
1280
- projectId: z11.string().optional().describe("Target Conveyor project ID"),
1281
- cardStatuses: z11.array(z11.string()).optional().describe('Filter tasks by card/column status, e.g. ["ReviewDev", "ReviewLive"]'),
1282
- testStatuses: z11.array(z11.enum(["open", "approved", "rejected"])).optional().describe("Filter tests by status: open | approved | rejected")
1283
- },
1284
- async (params) => {
1285
- const groups = await conn2.queryManualTests({
1286
- projectId: params.projectId,
1287
- cardStatuses: params.cardStatuses,
1288
- testStatuses: params.testStatuses
1289
- });
1290
- if (groups.length === 0) {
1291
- return { content: [{ type: "text", text: "No manual tests match those filters." }] };
1292
- }
1293
- return { content: [{ type: "text", text: renderManualTestGroups(groups) }] };
1792
+ registerContractTool(server2, queryManualTestsContract, async (params) => {
1793
+ const groups = await conn2.queryManualTests({
1794
+ projectId: params.projectId,
1795
+ cardStatuses: params.cardStatuses,
1796
+ testStatuses: params.testStatuses
1797
+ });
1798
+ if (groups.length === 0) {
1799
+ return { content: [{ type: "text", text: "No manual tests match those filters." }] };
1294
1800
  }
1295
- );
1801
+ return { content: [{ type: "text", text: renderManualTestGroups(groups) }] };
1802
+ });
1296
1803
  }
1297
1804
  function registerChecklistTools(server2, conn2) {
1298
1805
  registerListManualTests(server2, conn2);
@@ -1305,7 +1812,7 @@ function registerChecklistTools(server2, conn2) {
1305
1812
  }
1306
1813
 
1307
1814
  // src/tools/workspace.ts
1308
- import { z as z12 } from "zod";
1815
+ import { z as z7 } from "zod";
1309
1816
 
1310
1817
  // src/workspace-ssh-tunnel.ts
1311
1818
  import net from "net";
@@ -1425,8 +1932,8 @@ function registerAttachInfoTool(server2, conn2) {
1425
1932
  "workspace_attach_info",
1426
1933
  "Return SSH/SFTP attach metadata for a running task Claudespace, plus hosted preview URLs and configured preview ports. Optionally installs an OpenSSH public key for this attach session.",
1427
1934
  {
1428
- taskId: z12.string().describe("The task ID"),
1429
- sshPublicKey: z12.string().optional().describe("Optional OpenSSH public key to install into the workspace")
1935
+ taskId: z7.string().describe("The task ID"),
1936
+ sshPublicKey: z7.string().optional().describe("Optional OpenSSH public key to install into the workspace")
1430
1937
  },
1431
1938
  async ({ taskId, sshPublicKey }) => {
1432
1939
  const info = await conn2.getWorkspaceAttachInfo(taskId, sshPublicKey);
@@ -1439,7 +1946,7 @@ function registerPreviewUrlsTool(server2, conn2) {
1439
1946
  "workspace_preview_urls",
1440
1947
  "Return the hosted preview URLs and preview ports for a running task Claudespace. This mirrors the web UI preview link metadata.",
1441
1948
  {
1442
- taskId: z12.string().describe("The task ID")
1949
+ taskId: z7.string().describe("The task ID")
1443
1950
  },
1444
1951
  async ({ taskId }) => {
1445
1952
  const info = await conn2.getWorkspaceAttachInfo(taskId);
@@ -1468,10 +1975,10 @@ function registerStartTunnelTool(server2, conn2, startTunnel) {
1468
1975
  "workspace_start_tunnel",
1469
1976
  "Start a local loopback tunnel through the MCP server to a running task Claudespace port. Use port 2222 for SSH/SFTP, or one of previewPorts for app access.",
1470
1977
  {
1471
- taskId: z12.string().describe("The task ID"),
1472
- port: z12.number().optional().describe("Remote Claudespace port. Defaults to SSH port 2222."),
1473
- preferredLocalPort: z12.number().optional().describe("Preferred local loopback port. If omitted, the OS chooses one."),
1474
- sshPublicKey: z12.string().optional().describe("Optional OpenSSH public key to install before opening the tunnel")
1978
+ taskId: z7.string().describe("The task ID"),
1979
+ port: z7.number().optional().describe("Remote Claudespace port. Defaults to SSH port 2222."),
1980
+ preferredLocalPort: z7.number().optional().describe("Preferred local loopback port. If omitted, the OS chooses one."),
1981
+ sshPublicKey: z7.string().optional().describe("Optional OpenSSH public key to install before opening the tunnel")
1475
1982
  },
1476
1983
  async ({ taskId, port, preferredLocalPort, sshPublicKey }) => {
1477
1984
  const info = await conn2.getWorkspaceAttachInfo(taskId, sshPublicKey);
@@ -1524,7 +2031,7 @@ function registerStopTunnelTool(server2) {
1524
2031
  "workspace_stop_tunnel",
1525
2032
  "Stop a local workspace tunnel previously opened by workspace_start_tunnel.",
1526
2033
  {
1527
- tunnelId: z12.string().describe("Tunnel id returned by workspace_start_tunnel")
2034
+ tunnelId: z7.string().describe("Tunnel id returned by workspace_start_tunnel")
1528
2035
  },
1529
2036
  async ({ tunnelId }) => {
1530
2037
  const tunnel = activeTunnels.get(tunnelId);
@@ -1543,7 +2050,7 @@ function registerWorkspaceTools(server2, conn2, deps = {}) {
1543
2050
  }
1544
2051
 
1545
2052
  // src/tools/logs.ts
1546
- import { z as z13 } from "zod";
2053
+ import { z as z8 } from "zod";
1547
2054
  var SEVERITY_ENUM = [
1548
2055
  "DEBUG",
1549
2056
  "INFO",
@@ -1678,18 +2185,18 @@ function registerGrafanaLogTool(server2, conn2) {
1678
2185
  "query_grafana_logs",
1679
2186
  "Query the project's connected Grafana (Loki) logs \u2014 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 can't express the query (it REPLACES them). The response header echoes the composed LogQL \u2014 iterate on it. Returns compact lines: '<time> <SEVERITY> [<service>] <message>'.",
1680
2187
  {
1681
- projectId: z13.string().optional().describe("Target Conveyor project ID"),
1682
- env: z13.enum(["prod", "dev"]).optional().describe("Configured Grafana env mapping to scope by (default prod)"),
1683
- sinceMinutes: z13.number().int().min(1).max(10080).optional().describe(
2188
+ projectId: z8.string().optional().describe("Target Conveyor project ID"),
2189
+ env: z8.enum(["prod", "dev"]).optional().describe("Configured Grafana env mapping to scope by (default prod)"),
2190
+ sinceMinutes: z8.number().int().min(1).max(10080).optional().describe(
1684
2191
  "Relative time window ending now, in minutes (default 60). Ignored if startTime is set."
1685
2192
  ),
1686
- startTime: z13.string().optional().describe("ISO 8601 lower bound (overrides sinceMinutes)"),
1687
- endTime: z13.string().optional().describe("ISO 8601 upper bound (default now)"),
1688
- level: z13.enum(["debug", "info", "warn", "error", "fatal"]).optional().describe("Minimum severity, inclusive \u2014 error returns error and above"),
1689
- services: z13.array(z13.string()).optional().describe("Restrict to these service_name label values"),
1690
- search: z13.string().max(256).optional().describe("Substring line filter (exact substring, not regex)"),
1691
- logql: z13.string().max(2e3).optional().describe("Advanced: raw LogQL query \u2014 REPLACES env/services/level/search composition"),
1692
- limit: z13.number().int().min(1).max(200).optional().describe("Max entries (default 50)")
2193
+ startTime: z8.string().optional().describe("ISO 8601 lower bound (overrides sinceMinutes)"),
2194
+ endTime: z8.string().optional().describe("ISO 8601 upper bound (default now)"),
2195
+ level: z8.enum(["debug", "info", "warn", "error", "fatal"]).optional().describe("Minimum severity, inclusive \u2014 error returns error and above"),
2196
+ services: z8.array(z8.string()).optional().describe("Restrict to these service_name label values"),
2197
+ search: z8.string().max(256).optional().describe("Substring line filter (exact substring, not regex)"),
2198
+ logql: z8.string().max(2e3).optional().describe("Advanced: raw LogQL query \u2014 REPLACES env/services/level/search composition"),
2199
+ limit: z8.number().int().min(1).max(200).optional().describe("Max entries (default 50)")
1693
2200
  },
1694
2201
  async (params) => {
1695
2202
  const text = await runQueryGrafanaLogs(conn2, params);
@@ -1702,25 +2209,25 @@ function registerLogTools(server2, conn2) {
1702
2209
  "query_gcp_logs",
1703
2210
  "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'). 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. Pass projectId to target a specific project; otherwise the configured default project is used.",
1704
2211
  {
1705
- projectId: z13.string().optional().describe("Target Conveyor project ID"),
1706
- env: z13.enum(["prod", "dev", "claudespace"]).optional().describe("GCP environment slot to query (default prod)"),
1707
- sinceMinutes: z13.number().int().min(1).max(10080).optional().describe(
2212
+ projectId: z8.string().optional().describe("Target Conveyor project ID"),
2213
+ env: z8.enum(["prod", "dev", "claudespace"]).optional().describe("GCP environment slot to query (default prod)"),
2214
+ sinceMinutes: z8.number().int().min(1).max(10080).optional().describe(
1708
2215
  "Relative time window ending now, in minutes (default 60). Ignored if startTime is set."
1709
2216
  ),
1710
- startTime: z13.string().optional().describe("ISO 8601 lower bound (overrides sinceMinutes)"),
1711
- endTime: z13.string().optional().describe("ISO 8601 upper bound (default now)"),
1712
- severity: z13.enum(SEVERITY_ENUM).optional().describe("Minimum severity, inclusive \u2014 ERROR returns ERROR and above"),
1713
- services: z13.array(z13.string()).optional().describe(
2217
+ startTime: z8.string().optional().describe("ISO 8601 lower bound (overrides sinceMinutes)"),
2218
+ endTime: z8.string().optional().describe("ISO 8601 upper bound (default now)"),
2219
+ severity: z8.enum(SEVERITY_ENUM).optional().describe("Minimum severity, inclusive \u2014 ERROR returns ERROR and above"),
2220
+ services: z8.array(z8.string()).optional().describe(
1714
2221
  "Restrict to these Cloud Run service names (prod/dev only). Defaults to all services linked in project settings."
1715
2222
  ),
1716
- sqlInstances: z13.array(z13.string()).optional().describe("Restrict to these Cloud SQL instance names (prod/dev only)"),
1717
- allServices: z13.boolean().optional().describe(
2223
+ sqlInstances: z8.array(z8.string()).optional().describe("Restrict to these Cloud SQL instance names (prod/dev only)"),
2224
+ allServices: z8.boolean().optional().describe(
1718
2225
  "Set true to search ALL logs in the GCP project, ignoring the linked-resource scope"
1719
2226
  ),
1720
- search: z13.string().max(256).optional().describe("Free-text search across all log fields (exact substring, not regex)"),
1721
- filter: z13.string().max(1e3).optional().describe("Advanced: raw Cloud Logging filter expression, ANDed with the scope"),
1722
- limit: z13.number().int().min(1).max(200).optional().describe("Max entries per page (default 50)"),
1723
- pageToken: z13.string().optional().describe("Opaque token from a previous response to fetch the next page")
2227
+ search: z8.string().max(256).optional().describe("Free-text search across all log fields (exact substring, not regex)"),
2228
+ filter: z8.string().max(1e3).optional().describe("Advanced: raw Cloud Logging filter expression, ANDed with the scope"),
2229
+ limit: z8.number().int().min(1).max(200).optional().describe("Max entries per page (default 50)"),
2230
+ pageToken: z8.string().optional().describe("Opaque token from a previous response to fetch the next page")
1724
2231
  },
1725
2232
  async (params) => {
1726
2233
  const text = await runQueryGcpLogs(conn2, params);