@rallycry/conveyor-mcp 4.3.27 → 4.3.29

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.
@@ -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", {
@@ -781,6 +816,50 @@ var ConveyorConnection = class {
781
816
  socket.off("pty:data", handler);
782
817
  };
783
818
  }
819
+ // ── Board card collection (conveyor-wait) ───────────────────────────
820
+ /**
821
+ * Fetch one page of a project's live card collection.
822
+ *
823
+ * A cursor-less call also joins the scope room, which is what starts (or
824
+ * restarts, after a reconnect) delta delivery. Cursor-bearing calls are pure
825
+ * paging and join nothing, so always lead with the cursor-less call.
826
+ */
827
+ subscribeToCardCollection(projectId, opts = {}) {
828
+ return this.callService("taskService", "collection:subscribe", {
829
+ collection: "cardsByProject",
830
+ scopeId: projectId,
831
+ cursor: opts.cursor ?? null,
832
+ limit: opts.limit
833
+ });
834
+ }
835
+ /**
836
+ * Listen for card collection deltas on a project scope. The event name is
837
+ * the room name by design — no id parsing client-side. Returns an
838
+ * unsubscribe function.
839
+ */
840
+ onCardDelta(projectId, handler) {
841
+ const socket = this.socket;
842
+ if (!socket) throw new Error("Not connected");
843
+ const event = `taskService:collection:cardsByProject:${projectId}`;
844
+ socket.on(event, handler);
845
+ return () => {
846
+ socket.off(event, handler);
847
+ };
848
+ }
849
+ /**
850
+ * Run `handler` on every reconnect of the underlying socket. A reconnect
851
+ * drops every room this socket had joined, so long-lived subscribers must
852
+ * re-subscribe and reconcile whatever they missed. Returns an unsubscribe
853
+ * function.
854
+ */
855
+ onReconnect(handler) {
856
+ const socket = this.socket;
857
+ if (!socket) throw new Error("Not connected");
858
+ socket.on("connect", handler);
859
+ return () => {
860
+ socket.off("connect", handler);
861
+ };
862
+ }
784
863
  // ── Connection lifecycle ────────────────────────────────────────────
785
864
  disconnect() {
786
865
  this.socket?.disconnect();
@@ -791,4 +870,3 @@ var ConveyorConnection = class {
791
870
  export {
792
871
  ConveyorConnection
793
872
  };
794
- //# sourceMappingURL=chunk-Y6ZJUNDX.js.map
@@ -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
+ };
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-BKGMG2T3.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
  }
@@ -393,13 +396,13 @@ var tagRef = f.string({
393
396
  var getTagContract = defineToolContract({
394
397
  name: "get_tag",
395
398
  agent: {
396
- description: "Read one tag's full glossary entry: description, the full markdown overview (the term's spec \u2014 philosophy, mechanics, invariants), linked files/rules (each with its verified-link status \u2014 ok/stale/unchecked \u2014 from the periodic repo check), parent/child tags, active-card count, attachment count (files labelled as examples of the term), and recent revisions with their reasons. Call this whenever a chat message, plan, or tag list points at a term you need the full context for. A response with `overviewPath` set means the overview is sourced from that repo file \u2014 prefer Reading the path from your checkout (branch-correct); the served overview is the base-branch materialization (`overviewSource.state`: ok/pending/stale).",
399
+ description: "Read one tag's full glossary entry: description, the full markdown overview (the term's spec \u2014 philosophy, mechanics, invariants), linked files/rules (each with its verified-link status \u2014 ok/stale/unchecked \u2014 from the periodic repo check), parent/child tags, active-card count, attachment count (files labelled as examples of the term), and recent revisions with their reasons. Call this whenever a chat message, plan, or tag list points at a term you need the full context for. A response with `overviewPath` set means the overview is sourced from that repo file \u2014 prefer Reading the path from your checkout (branch-correct); the served overview is materialized from the project's dev branch (the PR base; default branch when the repo has no dev branch) (`overviewSource.state`: ok/pending/stale).",
397
400
  fields: {
398
401
  tag: tagRef
399
402
  }
400
403
  },
401
404
  mcp: {
402
- description: "Read one tag's full glossary entry \u2014 description, markdown overview, context links (each with its verified-link status: ok/stale/unchecked plus last-checked provenance), parent/child tags, active-card count, attachment count (files labelled as examples of the term \u2014 read the tiles with list_tag_attachments), and recent revisions with provenance. A response with `overviewPath` set means the overview is sourced from that repo file at the base branch (`overviewSource.state`: ok/pending/stale) \u2014 clients with a checkout can read the path directly for the branch-correct copy. Pass projectId to target a specific project; otherwise the configured default project is used.",
405
+ description: "Read one tag's full glossary entry \u2014 description, markdown overview, context links (each with its verified-link status: ok/stale/unchecked plus last-checked provenance), parent/child tags, active-card count, attachment count (files labelled as examples of the term \u2014 read the tiles with list_tag_attachments), and recent revisions with provenance. A response with `overviewPath` set means the overview is sourced from that repo file at the project's dev branch (the PR base; default branch when the repo has no dev branch) (`overviewSource.state`: ok/pending/stale) \u2014 clients with a checkout can read the path directly for the branch-correct copy. Pass projectId to target a specific project; otherwise the configured default project is used.",
403
406
  fields: {
404
407
  projectId: mcpProjectId,
405
408
  tag: tagRef
@@ -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: {
@@ -1335,12 +1340,26 @@ function registerListTasks(server2, conn2) {
1335
1340
  }
1336
1341
  );
1337
1342
  }
1343
+ function splitCardRef(ref) {
1344
+ const slash = ref.indexOf("/");
1345
+ if (slash <= 0) return { cardSlug: ref };
1346
+ const projectSlug = ref.slice(0, slash);
1347
+ const cardSlug = ref.slice(slash + 1);
1348
+ if (!cardSlug) return { cardSlug: ref };
1349
+ return { projectSlug, cardSlug };
1350
+ }
1351
+ async function resolveCardRef(conn2, ref, projectId2) {
1352
+ const { projectSlug, cardSlug } = splitCardRef(ref);
1353
+ if (!projectSlug) return { cardSlug, projectId: projectId2 };
1354
+ return { cardSlug, projectId: await conn2.resolveProjectIdBySlug(projectSlug) };
1355
+ }
1338
1356
  function registerGetTask(server2, conn2) {
1339
1357
  registerContractTool(
1340
1358
  server2,
1341
1359
  getTaskContract,
1342
1360
  async (params) => {
1343
- const task = await conn2.getTask(params.taskId, params.projectId);
1361
+ const ref = await resolveCardRef(conn2, params.taskId, params.projectId);
1362
+ const task = await conn2.getTask(ref.cardSlug, ref.projectId);
1344
1363
  return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1345
1364
  },
1346
1365
  { alwaysLoad: true }
@@ -1349,13 +1368,16 @@ function registerGetTask(server2, conn2) {
1349
1368
  function registerGetCardBySlug(server2, conn2) {
1350
1369
  server2.tool(
1351
1370
  "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.",
1371
+ '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
1372
  {
1354
1373
  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'")
1374
+ slug: z5.string().describe(
1375
+ "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)"
1376
+ )
1356
1377
  },
1357
1378
  async (params) => {
1358
- const task = await conn2.getCardBySlug(params.slug, params.projectId);
1379
+ const ref = await resolveCardRef(conn2, params.slug, params.projectId);
1380
+ const task = await conn2.getCardBySlug(ref.cardSlug, ref.projectId);
1359
1381
  return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1360
1382
  }
1361
1383
  );
@@ -1391,7 +1413,7 @@ function registerCreateTask(server2, conn2) {
1391
1413
  function registerUpdateTask(server2, conn2) {
1392
1414
  server2.tool(
1393
1415
  "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.",
1416
+ "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
1417
  {
1396
1418
  projectId: z5.string().optional().describe("Target Conveyor project ID"),
1397
1419
  taskId: z5.string().describe("The task ID"),
@@ -1409,6 +1431,9 @@ function registerUpdateTask(server2, conn2) {
1409
1431
  subProjectId: z5.string().nullable().optional().describe(
1410
1432
  "Assign the task to a sub-project board; null moves it back to the parent board. Omit to leave unchanged."
1411
1433
  ),
1434
+ githubBranch: z5.string().nullable().optional().describe(
1435
+ "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."
1436
+ ),
1412
1437
  addTags: z5.array(z5.string()).optional().describe(
1413
1438
  '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
1439
  ),
@@ -1757,6 +1782,8 @@ var MIME_BY_EXT = {
1757
1782
  ".txt": "text/plain",
1758
1783
  ".log": "text/plain",
1759
1784
  ".md": "text/markdown",
1785
+ ".mmd": "text/vnd.mermaid",
1786
+ ".mermaid": "text/vnd.mermaid",
1760
1787
  ".csv": "text/csv",
1761
1788
  ".html": "text/html",
1762
1789
  ".css": "text/css",
@@ -2589,4 +2616,3 @@ process.on("SIGTERM", () => {
2589
2616
  conn.disconnect();
2590
2617
  process.exit(0);
2591
2618
  });
2592
- //# sourceMappingURL=cli.js.map