@rallycry/conveyor-mcp 4.3.28 → 4.3.30

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.
@@ -0,0 +1,162 @@
1
+ // src/wait.ts
2
+ var WAIT_CARD_TYPES = ["task", "incident", "suggestion", "chat"];
3
+ var WAIT_SCOPES = ["mine", "unclaimed", "all"];
4
+ var WAIT_STATUSES = [
5
+ "Planning",
6
+ "Open",
7
+ "InProgress",
8
+ "ReviewPR",
9
+ "ReviewDev",
10
+ "ReviewLive",
11
+ "Complete",
12
+ "Cancelled"
13
+ ];
14
+ var DEFAULT_WAIT_TYPES = ["task", "incident", "suggestion"];
15
+ var DEFAULT_WAIT_SCOPES = ["mine", "unclaimed"];
16
+ var DEFAULT_WAIT_STATUSES = ["Open"];
17
+ function splitList(value) {
18
+ return value.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
19
+ }
20
+ function normalizeToken(value) {
21
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
22
+ }
23
+ function parseWaitTypes(value) {
24
+ if (value === void 0) return [...DEFAULT_WAIT_TYPES];
25
+ const out = [];
26
+ for (const raw of splitList(value)) {
27
+ const token = normalizeToken(raw);
28
+ if (token === "all") return [...WAIT_CARD_TYPES];
29
+ const match = WAIT_CARD_TYPES.find((type) => type === token || `${type}s` === token);
30
+ if (!match) {
31
+ throw new Error(`Unknown card type "${raw}". Valid: ${WAIT_CARD_TYPES.join(", ")}, all.`);
32
+ }
33
+ if (!out.includes(match)) out.push(match);
34
+ }
35
+ if (out.length === 0) return [...DEFAULT_WAIT_TYPES];
36
+ return out;
37
+ }
38
+ function parseWaitScopes(value) {
39
+ if (value === void 0) return [...DEFAULT_WAIT_SCOPES];
40
+ const out = [];
41
+ for (const raw of splitList(value)) {
42
+ const token = normalizeToken(raw);
43
+ if (token === "all" || token === "any") return ["all"];
44
+ const match = WAIT_SCOPES.find((scope) => scope === token);
45
+ if (!match) {
46
+ throw new Error(`Unknown scope "${raw}". Valid: ${WAIT_SCOPES.join(", ")}.`);
47
+ }
48
+ if (!out.includes(match)) out.push(match);
49
+ }
50
+ if (out.length === 0) return [...DEFAULT_WAIT_SCOPES];
51
+ return out;
52
+ }
53
+ function parseWaitStatuses(value) {
54
+ if (value === void 0) return [...DEFAULT_WAIT_STATUSES];
55
+ const out = [];
56
+ for (const raw of splitList(value)) {
57
+ const token = normalizeToken(raw);
58
+ if (token === "all" || token === "any") return [...WAIT_STATUSES];
59
+ const match = WAIT_STATUSES.find((status) => normalizeToken(status) === token);
60
+ if (!match) {
61
+ throw new Error(`Unknown status "${raw}". Valid: ${WAIT_STATUSES.join(", ")}, all.`);
62
+ }
63
+ if (!out.includes(match)) out.push(match);
64
+ }
65
+ if (out.length === 0) return [...DEFAULT_WAIT_STATUSES];
66
+ return out;
67
+ }
68
+ function toWaitCard(item) {
69
+ return {
70
+ id: item.id,
71
+ slug: item.slug ?? null,
72
+ title: item.title ?? null,
73
+ type: item.type ?? null,
74
+ status: item.status ?? null,
75
+ assignedUserId: item.assignedUser?.id ?? null
76
+ };
77
+ }
78
+ function matchesFilter(card, filter) {
79
+ const type = card.type ?? "task";
80
+ if (!filter.types.includes(type)) return false;
81
+ if (card.status === null || !filter.statuses.includes(card.status)) return false;
82
+ return matchesScope(card, filter);
83
+ }
84
+ function matchesScope(card, filter) {
85
+ if (filter.scopes.includes("all")) return true;
86
+ if (filter.scopes.includes("unclaimed") && card.assignedUserId === null) return true;
87
+ if (filter.scopes.includes("mine") && filter.userId !== null && card.assignedUserId === filter.userId) {
88
+ return true;
89
+ }
90
+ return false;
91
+ }
92
+ var MatchSet = class {
93
+ constructor(filter) {
94
+ this.filter = filter;
95
+ }
96
+ filter;
97
+ matched = /* @__PURE__ */ new Set();
98
+ get size() {
99
+ return this.matched.size;
100
+ }
101
+ has(id) {
102
+ return this.matched.has(id);
103
+ }
104
+ /** Seed from the first snapshot. Pre-existing matches never trigger. */
105
+ seed(cards) {
106
+ this.matched.clear();
107
+ for (const card of cards) {
108
+ if (matchesFilter(card, this.filter)) this.matched.add(card.id);
109
+ }
110
+ }
111
+ /**
112
+ * Re-seed after a `reset` delta or a reconnect gap, returning every card
113
+ * that entered matching state while we were not listening.
114
+ *
115
+ * `complete` says whether the snapshot walk reached the end of the scope.
116
+ * When it did not, ids we no longer see are merged forward rather than
117
+ * dropped — forgetting a still-matching card would re-report it as new on
118
+ * its next delta.
119
+ */
120
+ reseed(cards, complete = true) {
121
+ const triggers = [];
122
+ const next = new Set(complete ? [] : this.matched);
123
+ for (const card of cards) {
124
+ if (!matchesFilter(card, this.filter)) continue;
125
+ next.add(card.id);
126
+ if (!this.matched.has(card.id)) triggers.push(card);
127
+ }
128
+ this.matched.clear();
129
+ for (const id of next) this.matched.add(id);
130
+ return triggers;
131
+ }
132
+ /** Apply an `added`/`updated` delta. Returns the card only on entry. */
133
+ upsert(card) {
134
+ const matches = matchesFilter(card, this.filter);
135
+ const held = this.matched.has(card.id);
136
+ if (matches && !held) {
137
+ this.matched.add(card.id);
138
+ return card;
139
+ }
140
+ if (!matches && held) this.matched.delete(card.id);
141
+ return null;
142
+ }
143
+ /** Apply a `removed` delta. A deleted card is never new work. */
144
+ remove(id) {
145
+ this.matched.delete(id);
146
+ }
147
+ };
148
+
149
+ export {
150
+ WAIT_CARD_TYPES,
151
+ WAIT_SCOPES,
152
+ WAIT_STATUSES,
153
+ DEFAULT_WAIT_TYPES,
154
+ DEFAULT_WAIT_SCOPES,
155
+ DEFAULT_WAIT_STATUSES,
156
+ parseWaitTypes,
157
+ parseWaitScopes,
158
+ parseWaitStatuses,
159
+ toWaitCard,
160
+ matchesFilter,
161
+ MatchSet
162
+ };
@@ -116,4 +116,3 @@ export {
116
116
  attachTunnel,
117
117
  runTunnel
118
118
  };
119
- //# sourceMappingURL=chunk-N2XC2PGJ.js.map
@@ -0,0 +1,175 @@
1
+ import {
2
+ MatchSet,
3
+ toWaitCard
4
+ } from "./chunk-D3Q2QZJA.js";
5
+
6
+ // src/wait-runner.ts
7
+ var SNAPSHOT_PAGE_LIMIT = 500;
8
+ var SNAPSHOT_MAX_PAGES = 20;
9
+ var RESYNC_ATTEMPTS = 4;
10
+ var RESYNC_RETRY_DELAY_MS = 3e3;
11
+ var WaitSession = class {
12
+ constructor(options) {
13
+ this.options = options;
14
+ this.tracker = new MatchSet(options.filter);
15
+ }
16
+ options;
17
+ tracker;
18
+ /**
19
+ * Deltas that land before (or during) a snapshot are held, then replayed
20
+ * against the freshly seeded set. Replaying an already-matching card is a
21
+ * no-op, so the overlap is safe.
22
+ */
23
+ buffered = [];
24
+ seeded = false;
25
+ done = false;
26
+ offDelta;
27
+ offReconnect;
28
+ timer;
29
+ /** Serializes snapshots: a reset arriving mid-resync queues, it does not race. */
30
+ chain = Promise.resolve();
31
+ settle;
32
+ abort;
33
+ run() {
34
+ return new Promise((resolve, reject) => {
35
+ const { source, projectId, timeoutMs, log = () => {
36
+ } } = this.options;
37
+ this.settle = resolve;
38
+ this.abort = reject;
39
+ this.offDelta = source.onCardDelta(projectId, (delta) => this.applyDelta(delta));
40
+ this.offReconnect = source.onReconnect(() => {
41
+ log("Reconnected \u2014 re-snapshotting the board scope.");
42
+ this.schedule(false);
43
+ });
44
+ this.timer = setTimeout(() => this.finish({ reason: "timeout" }), timeoutMs);
45
+ this.schedule(true);
46
+ });
47
+ }
48
+ teardown() {
49
+ this.done = true;
50
+ if (this.timer !== void 0) clearTimeout(this.timer);
51
+ this.offDelta?.();
52
+ this.offReconnect?.();
53
+ }
54
+ finish(result) {
55
+ if (this.done) return;
56
+ this.teardown();
57
+ this.settle?.(result);
58
+ }
59
+ fail(error) {
60
+ if (this.done) return;
61
+ this.teardown();
62
+ this.abort?.(error instanceof Error ? error : new Error(String(error)));
63
+ }
64
+ schedule(initial) {
65
+ this.chain = this.chain.then(() => initial ? this.seedOrResync(true) : this.resyncWithRetry()).catch((err) => this.fail(err));
66
+ }
67
+ /**
68
+ * Re-snapshot after a reset or a reconnect, retrying a transient failure.
69
+ *
70
+ * Giving up here is not a soft failure: deltas buffer while the session is
71
+ * unseeded, so an abandoned resync would leave the wait deaf until its
72
+ * deadline. Rethrow only once every attempt is spent. The initial seed stays
73
+ * fatal by contrast — that failure means a bad project id or revoked access.
74
+ */
75
+ async resyncWithRetry() {
76
+ const { resyncRetryDelayMs = RESYNC_RETRY_DELAY_MS, log } = this.options;
77
+ for (let attempt = 1; attempt <= RESYNC_ATTEMPTS; attempt++) {
78
+ if (this.done) return;
79
+ try {
80
+ await this.seedOrResync(false);
81
+ return;
82
+ } catch (err) {
83
+ if (attempt === RESYNC_ATTEMPTS) throw err;
84
+ const message = err instanceof Error ? err.message : String(err);
85
+ log?.(
86
+ `Re-snapshot attempt ${attempt} failed (${message}) \u2014 retrying in ${Math.round(resyncRetryDelayMs / 1e3)}s.`
87
+ );
88
+ await this.delay(resyncRetryDelayMs);
89
+ }
90
+ }
91
+ }
92
+ /** A sleep that never holds the process open past the wait itself. */
93
+ delay(ms) {
94
+ return new Promise((resolve) => {
95
+ const timer = setTimeout(resolve, ms);
96
+ timer.unref?.();
97
+ });
98
+ }
99
+ applyDelta(delta) {
100
+ if (this.done) return;
101
+ if (!this.seeded) {
102
+ this.buffered.push(delta);
103
+ return;
104
+ }
105
+ switch (delta.type) {
106
+ case "added":
107
+ case "updated": {
108
+ const entered = this.tracker.upsert(toWaitCard(delta.item));
109
+ if (entered) this.finish({ reason: "event", card: entered });
110
+ break;
111
+ }
112
+ case "removed":
113
+ this.tracker.remove(delta.id);
114
+ break;
115
+ case "reset":
116
+ this.options.log?.("Board scope reset \u2014 re-snapshotting.");
117
+ this.schedule(false);
118
+ break;
119
+ }
120
+ }
121
+ /** Walk the scope to the end, or until the page cap stops us. */
122
+ async snapshotAll() {
123
+ const {
124
+ source,
125
+ projectId,
126
+ pageLimit = SNAPSHOT_PAGE_LIMIT,
127
+ maxPages = SNAPSHOT_MAX_PAGES
128
+ } = this.options;
129
+ const cards = [];
130
+ let cursor = null;
131
+ let complete = false;
132
+ for (let page = 0; page < maxPages; page++) {
133
+ const result = await source.subscribeToCardCollection(projectId, {
134
+ cursor,
135
+ limit: pageLimit
136
+ });
137
+ for (const item of result.items ?? []) cards.push(toWaitCard(item));
138
+ cursor = result.nextCursor ?? null;
139
+ if (cursor === null) {
140
+ complete = true;
141
+ break;
142
+ }
143
+ }
144
+ return { cards, complete };
145
+ }
146
+ async seedOrResync(initial) {
147
+ this.seeded = false;
148
+ const { cards, complete } = await this.snapshotAll();
149
+ if (this.done) return;
150
+ if (initial) {
151
+ this.tracker.seed(cards);
152
+ this.options.log?.(
153
+ `Watching ${cards.length} card(s) \u2014 ${this.tracker.size} already match; waiting for new work.`
154
+ );
155
+ } else {
156
+ const triggers = this.tracker.reseed(cards, complete);
157
+ if (triggers.length > 0) {
158
+ this.finish({ reason: "event", card: triggers[0] });
159
+ return;
160
+ }
161
+ }
162
+ this.seeded = true;
163
+ for (const delta of this.buffered.splice(0)) {
164
+ if (this.done) return;
165
+ this.applyDelta(delta);
166
+ }
167
+ }
168
+ };
169
+ function runWait(options) {
170
+ return new WaitSession(options).run();
171
+ }
172
+
173
+ export {
174
+ runWait
175
+ };
@@ -49,6 +49,8 @@ function enrichToolError(error, fallback) {
49
49
  var ConveyorConnection = class {
50
50
  socket = null;
51
51
  config;
52
+ /** project slug → id, for resolving `<project>/<card>` card paths. */
53
+ projectSlugIds = /* @__PURE__ */ new Map();
52
54
  constructor(config) {
53
55
  this.config = config;
54
56
  }
@@ -225,7 +227,7 @@ var ConveyorConnection = class {
225
227
  async updateTask(params) {
226
228
  const { projectId, addTags, removeTags, ...rest } = params;
227
229
  const resolvedProjectId = this.resolveProjectId(projectId);
228
- const hasCoreUpdate = rest.title !== void 0 || rest.description !== void 0 || rest.plan !== void 0 || rest.status !== void 0 || rest.risk !== void 0 || rest.storyPointValue !== void 0 || rest.assignedUserId !== void 0 || rest.subProjectId !== void 0;
230
+ const hasCoreUpdate = rest.title !== void 0 || rest.description !== void 0 || rest.plan !== void 0 || rest.status !== void 0 || rest.risk !== void 0 || rest.storyPointValue !== void 0 || rest.assignedUserId !== void 0 || rest.subProjectId !== void 0 || rest.githubBranch !== void 0;
229
231
  const removedTags = removeTags ?? [];
230
232
  const addedTags = addTags ?? [];
231
233
  if (removedTags.length > 0) {
@@ -317,6 +319,39 @@ var ConveyorConnection = class {
317
319
  const response = await this.call("listAccessibleProjects", { pageSize: 100 });
318
320
  return this.normalizeProjectList(response);
319
321
  }
322
+ /**
323
+ * Resolve a project SLUG (as pasted in a `<project>/<card>` card path) to its
324
+ * id.
325
+ *
326
+ * Cached, because a slug→id mapping effectively never changes and this sits
327
+ * in front of an ordinary card lookup. A miss refetches once before failing,
328
+ * so a project created after the cache warmed still resolves.
329
+ *
330
+ * Throws naming the slug rather than falling back to the configured default
331
+ * project: the caller pasted a specific project, and silently answering about
332
+ * a different one is the worst possible outcome here.
333
+ */
334
+ async resolveProjectIdBySlug(slug) {
335
+ const cached = this.projectSlugIds.get(slug);
336
+ if (cached) return cached;
337
+ await this.refreshProjectSlugCache();
338
+ const resolved = this.projectSlugIds.get(slug);
339
+ if (!resolved) {
340
+ throw new Error(
341
+ `No accessible Conveyor project has the slug "${slug}". Use list_projects to see the projects this connection can reach.`
342
+ );
343
+ }
344
+ return resolved;
345
+ }
346
+ async refreshProjectSlugCache() {
347
+ for (const project of await this.listProjects()) {
348
+ if (typeof project !== "object" || project === null) continue;
349
+ const { slug, id } = project;
350
+ if (typeof slug === "string" && typeof id === "string") {
351
+ this.projectSlugIds.set(slug, id);
352
+ }
353
+ }
354
+ }
320
355
  // ── Build Management ────────────────────────────────────────────────
321
356
  startBuild(taskId, projectId) {
322
357
  return this.call("startProjectBuild", {
@@ -580,6 +615,18 @@ var ConveyorConnection = class {
580
615
  }
581
616
  return result;
582
617
  }
618
+ /**
619
+ * Move a card under a new parent, or detach it with an explicit null.
620
+ * `parentTaskId` rides the rest-spread rather than a conditional one so a
621
+ * null survives the call — stripping it would silently no-op a detach.
622
+ */
623
+ setTaskParent(params) {
624
+ const { projectId, ...rest } = params;
625
+ return this.call("setProjectTaskParent", {
626
+ projectId: this.resolveProjectId(projectId),
627
+ ...rest
628
+ });
629
+ }
583
630
  updateSubtask(params) {
584
631
  const { projectId, ...rest } = params;
585
632
  return this.call("updateProjectSubtask", {
@@ -781,6 +828,50 @@ var ConveyorConnection = class {
781
828
  socket.off("pty:data", handler);
782
829
  };
783
830
  }
831
+ // ── Board card collection (conveyor-wait) ───────────────────────────
832
+ /**
833
+ * Fetch one page of a project's live card collection.
834
+ *
835
+ * A cursor-less call also joins the scope room, which is what starts (or
836
+ * restarts, after a reconnect) delta delivery. Cursor-bearing calls are pure
837
+ * paging and join nothing, so always lead with the cursor-less call.
838
+ */
839
+ subscribeToCardCollection(projectId, opts = {}) {
840
+ return this.callService("taskService", "collection:subscribe", {
841
+ collection: "cardsByProject",
842
+ scopeId: projectId,
843
+ cursor: opts.cursor ?? null,
844
+ limit: opts.limit
845
+ });
846
+ }
847
+ /**
848
+ * Listen for card collection deltas on a project scope. The event name is
849
+ * the room name by design — no id parsing client-side. Returns an
850
+ * unsubscribe function.
851
+ */
852
+ onCardDelta(projectId, handler) {
853
+ const socket = this.socket;
854
+ if (!socket) throw new Error("Not connected");
855
+ const event = `taskService:collection:cardsByProject:${projectId}`;
856
+ socket.on(event, handler);
857
+ return () => {
858
+ socket.off(event, handler);
859
+ };
860
+ }
861
+ /**
862
+ * Run `handler` on every reconnect of the underlying socket. A reconnect
863
+ * drops every room this socket had joined, so long-lived subscribers must
864
+ * re-subscribe and reconcile whatever they missed. Returns an unsubscribe
865
+ * function.
866
+ */
867
+ onReconnect(handler) {
868
+ const socket = this.socket;
869
+ if (!socket) throw new Error("Not connected");
870
+ socket.on("connect", handler);
871
+ return () => {
872
+ socket.off("connect", handler);
873
+ };
874
+ }
784
875
  // ── Connection lifecycle ────────────────────────────────────────────
785
876
  disconnect() {
786
877
  this.socket?.disconnect();
@@ -791,4 +882,3 @@ var ConveyorConnection = class {
791
882
  export {
792
883
  ConveyorConnection
793
884
  };
794
- //# sourceMappingURL=chunk-Y6ZJUNDX.js.map
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ConveyorConnection
4
- } from "./chunk-Y6ZJUNDX.js";
4
+ } from "./chunk-X3EHPZNH.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { createRequire } from "module";
@@ -215,7 +215,10 @@ var getTaskContract = defineToolContract({
215
215
  fields: {
216
216
  projectId: mcpProjectId,
217
217
  taskId: f.string({
218
- desc: "The task ID or slug (the value in a card URL, /cards/<slug>)"
218
+ // The path form is MCP-only on purpose: an in-pod agent is bound to one
219
+ // project, where a bare slug already resolves, so the `agent` half of
220
+ // this contract deliberately says nothing about it.
221
+ desc: "The task ID or slug (the value in a card URL, /cards/<slug>), or a path-form reference '<project-slug>/<card-slug>' from the board's Copy path action"
219
222
  })
220
223
  }
221
224
  }
@@ -632,10 +635,11 @@ var MCP_STATUS_ENUM = [
632
635
  "Complete",
633
636
  "Cancelled"
634
637
  ];
638
+ var AGENT_TAGS = `Glossary tag names to label the child with, e.g. ["agent", "pack"]. Names, not ids, matched case-insensitively against this project's tags (use list_tags to see them). A name that matches nothing comes back in the result and never fails the create.`;
635
639
  var createSubtaskContract = defineToolContract({
636
640
  name: "create_subtask",
637
641
  agent: {
638
- 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.",
642
+ description: "Create a subtask (a child card) under the CURRENT card. This is how a card becomes a pack: the first child turns this card into the pack parent, and the children build as one unit. Use when breaking the current card into smaller pieces during planning. For a sibling card that lands after this one merges, use create_follow_up_task.",
639
643
  fields: {
640
644
  title: f.string({ desc: "Subtask title" }),
641
645
  description: f.optional(f.string({ desc: cardDescriptionDesc("Brief description") })),
@@ -643,7 +647,8 @@ var createSubtaskContract = defineToolContract({
643
647
  ordinal: f.optional(f.number({ desc: "Step/order number (0-based)" })),
644
648
  storyPointValue: f.optional(f.number({ desc: SP_DESCRIPTION })),
645
649
  followParentStatus: f.optional(f.boolean({ desc: AGENT_FOLLOW_PARENT_STATUS })),
646
- dependsOn: f.optional(f.array(f.string(), { desc: AGENT_DEPENDS_ON }))
650
+ dependsOn: f.optional(f.array(f.string(), { desc: AGENT_DEPENDS_ON })),
651
+ tags: f.optional(f.array(f.string(), { desc: AGENT_TAGS }))
647
652
  }
648
653
  },
649
654
  mcp: {
@@ -755,11 +760,46 @@ var listSubtasksContract = defineToolContract({
755
760
  }
756
761
  }
757
762
  });
763
+ var SET_PARENT_ORDINAL = "Position among the new parent's children. Defaults to after every existing sibling. A detached card has no siblings to order against, so it only matters when you adopt.";
764
+ var SET_PARENT_WARNINGS = "The move is reported, never refused: a card that already has a pull request, live compute, a release, children of its own, or dependency edges that now cross packs comes back with a warning in the result. Its branch is NOT re-cut against the new parent's feature branch.";
765
+ var setTaskParentContract = defineToolContract({
766
+ name: "set_task_parent",
767
+ agent: {
768
+ description: `Adopt an EXISTING card into this card's pack, or eject one of this card's children. The card keeps its own chat, plan, and history \u2014 it moves under (or out from) the current card, inherits the board, and lands after the current children. To make a NEW child use create_subtask instead. ${SET_PARENT_WARNINGS}`,
769
+ fields: {
770
+ taskId: f.string({
771
+ desc: "Id, slug, or branch name of the card to move. Must live in this project."
772
+ }),
773
+ detach: f.optional(
774
+ f.boolean({
775
+ desc: "Eject the card from this pack instead of adopting it \u2014 it becomes a standalone card and stops following the parent's status. The card must already be a child of the current card."
776
+ })
777
+ ),
778
+ ordinal: f.optional(f.number({ desc: SET_PARENT_ORDINAL })),
779
+ followParentStatus: f.optional(f.boolean({ desc: AGENT_FOLLOW_PARENT_STATUS }))
780
+ }
781
+ },
782
+ mcp: {
783
+ description: `Re-parent an existing card: pass a parentTaskId to adopt it into that card's pack, or null to detach it into a standalone card. The card keeps its chat, plan, and history; adopting also moves it to the parent's board and orders it after the parent's existing children. Use create_subtask for a NEW child, and move_card to move a card to another PROJECT. Pass projectId to target a specific project; otherwise the configured default project is used. ${SET_PARENT_WARNINGS}`,
784
+ fields: {
785
+ projectId: mcpProjectId,
786
+ taskId: f.string({ desc: "Id or slug of the card to move" }),
787
+ parentTaskId: f.nullable(
788
+ f.string({
789
+ desc: "Id or slug of the new parent, or null to detach the card from its current parent. Must be in the same project, and can be neither the card itself nor one of its own descendants."
790
+ })
791
+ ),
792
+ ordinal: f.optional(f.number({ desc: SET_PARENT_ORDINAL })),
793
+ followParentStatus: f.optional(f.boolean({ desc: MCP_FOLLOW_PARENT_STATUS }))
794
+ }
795
+ }
796
+ });
758
797
  var subtasksContracts = [
759
798
  createSubtaskContract,
760
799
  updateSubtaskContract,
761
800
  deleteSubtaskContract,
762
- listSubtasksContract
801
+ listSubtasksContract,
802
+ setTaskParentContract
763
803
  ];
764
804
  var mcpTaskIdOrSlug = f.string({ desc: "The task ID or slug" });
765
805
  var listTaskFilesContract = defineToolContract({
@@ -1335,12 +1375,26 @@ function registerListTasks(server2, conn2) {
1335
1375
  }
1336
1376
  );
1337
1377
  }
1378
+ function splitCardRef(ref) {
1379
+ const slash = ref.indexOf("/");
1380
+ if (slash <= 0) return { cardSlug: ref };
1381
+ const projectSlug = ref.slice(0, slash);
1382
+ const cardSlug = ref.slice(slash + 1);
1383
+ if (!cardSlug) return { cardSlug: ref };
1384
+ return { projectSlug, cardSlug };
1385
+ }
1386
+ async function resolveCardRef(conn2, ref, projectId2) {
1387
+ const { projectSlug, cardSlug } = splitCardRef(ref);
1388
+ if (!projectSlug) return { cardSlug, projectId: projectId2 };
1389
+ return { cardSlug, projectId: await conn2.resolveProjectIdBySlug(projectSlug) };
1390
+ }
1338
1391
  function registerGetTask(server2, conn2) {
1339
1392
  registerContractTool(
1340
1393
  server2,
1341
1394
  getTaskContract,
1342
1395
  async (params) => {
1343
- const task = await conn2.getTask(params.taskId, params.projectId);
1396
+ const ref = await resolveCardRef(conn2, params.taskId, params.projectId);
1397
+ const task = await conn2.getTask(ref.cardSlug, ref.projectId);
1344
1398
  return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1345
1399
  },
1346
1400
  { alwaysLoad: true }
@@ -1349,13 +1403,16 @@ function registerGetTask(server2, conn2) {
1349
1403
  function registerGetCardBySlug(server2, conn2) {
1350
1404
  server2.tool(
1351
1405
  "get_card_by_slug",
1352
- "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.",
1406
+ 'Get full card details by the slug from a card URL (/cards/<slug>) instead of a task ID. Also accepts the board\'s "Copy path" form, `<project-slug>/<card-slug>` \u2014 an embedded project slug wins over projectId and over the configured default. Pass projectId to target a specific project; otherwise the configured default project is used.',
1353
1407
  {
1354
1408
  projectId: z5.string().optional().describe("Target Conveyor project ID"),
1355
- slug: z5.string().describe("The card slug from a Conveyor card URL, e.g. 'ship-it'")
1409
+ slug: z5.string().describe(
1410
+ "The card slug from a Conveyor card URL, e.g. 'ship-it', or a path-form reference 'my-project/ship-it' (the board's Copy path action)"
1411
+ )
1356
1412
  },
1357
1413
  async (params) => {
1358
- const task = await conn2.getCardBySlug(params.slug, params.projectId);
1414
+ const ref = await resolveCardRef(conn2, params.slug, params.projectId);
1415
+ const task = await conn2.getCardBySlug(ref.cardSlug, ref.projectId);
1359
1416
  return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1360
1417
  }
1361
1418
  );
@@ -1391,7 +1448,7 @@ function registerCreateTask(server2, conn2) {
1391
1448
  function registerUpdateTask(server2, conn2) {
1392
1449
  server2.tool(
1393
1450
  "update_task",
1394
- "Update task fields: title, description, plan, status, risk, story points, 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; pass storyPointValue only to correct the sizing yourself. For subtasks use update_subtask.",
1451
+ "Update task fields: title, description, plan, status, risk, story points, assignment, githubBranch, 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; pass storyPointValue only to correct the sizing yourself. For subtasks use update_subtask.",
1395
1452
  {
1396
1453
  projectId: z5.string().optional().describe("Target Conveyor project ID"),
1397
1454
  taskId: z5.string().describe("The task ID"),
@@ -1409,6 +1466,9 @@ function registerUpdateTask(server2, conn2) {
1409
1466
  subProjectId: z5.string().nullable().optional().describe(
1410
1467
  "Assign the task to a sub-project board; null moves it back to the parent board. Omit to leave unchanged."
1411
1468
  ),
1469
+ githubBranch: z5.string().nullable().optional().describe(
1470
+ "Record the task's ACTUAL working branch (e.g. a locally-driven pack's branch cut at claim time, so identification never mints a competing name and pack-child merges settle against reality), or null to detach. The branch must already exist on origin, and a live workspace bound to a different branch rejects the write."
1471
+ ),
1412
1472
  addTags: z5.array(z5.string()).optional().describe(
1413
1473
  '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.'
1414
1474
  ),
@@ -1964,11 +2024,28 @@ function registerDeleteSubtask(server2, conn2) {
1964
2024
  };
1965
2025
  });
1966
2026
  }
2027
+ function registerSetTaskParent(server2, conn2) {
2028
+ registerContractTool(server2, setTaskParentContract, async (params) => {
2029
+ const result = await conn2.setTaskParent(params);
2030
+ const from = result.previousParentTaskId ?? "none";
2031
+ const to = result.parentSlug ?? result.parentTaskId ?? "none";
2032
+ const warnings = result.warnings.length > 0 ? ` Warnings: ${result.warnings.join(", ")}.` : "";
2033
+ return {
2034
+ content: [
2035
+ {
2036
+ type: "text",
2037
+ text: `Card ${result.slug} re-parented: ${from} -> ${to} (status: ${result.status}).${warnings}`
2038
+ }
2039
+ ]
2040
+ };
2041
+ });
2042
+ }
1967
2043
  function registerSubtaskTools(server2, conn2) {
1968
2044
  registerCreateSubtask(server2, conn2);
1969
2045
  registerUpdateSubtask(server2, conn2);
1970
2046
  registerListSubtasks(server2, conn2);
1971
2047
  registerDeleteSubtask(server2, conn2);
2048
+ registerSetTaskParent(server2, conn2);
1972
2049
  }
1973
2050
 
1974
2051
  // src/tools/dependencies.ts
@@ -2591,4 +2668,3 @@ process.on("SIGTERM", () => {
2591
2668
  conn.disconnect();
2592
2669
  process.exit(0);
2593
2670
  });
2594
- //# sourceMappingURL=cli.js.map