@gethmy/mcp 2.8.1 → 2.8.2

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
@@ -1121,6 +1121,87 @@ function getDisplayLinkType(linkType, direction) {
1121
1121
  return linkType;
1122
1122
  return LINK_TYPE_INVERSES[linkType];
1123
1123
  }
1124
+ // ../harmony-shared/dist/commentSerializer.js
1125
+ var CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
1126
+ function authorLabel(c) {
1127
+ if (c.author_type === "agent")
1128
+ return "AI agent";
1129
+ return c.author?.full_name || c.author?.email || "teammate";
1130
+ }
1131
+ function criticalIds(comments) {
1132
+ const keep = new Set;
1133
+ for (const c of comments) {
1134
+ if (c.comment_type === "decision")
1135
+ keep.add(c.id);
1136
+ if (c.supersedes_id) {
1137
+ keep.add(c.id);
1138
+ keep.add(c.supersedes_id);
1139
+ }
1140
+ if (c.confirms_id) {
1141
+ keep.add(c.id);
1142
+ keep.add(c.confirms_id);
1143
+ }
1144
+ }
1145
+ return keep;
1146
+ }
1147
+ function serializeCommentThread(comments, options = {}) {
1148
+ const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
1149
+ const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
1150
+ if (visible.length === 0)
1151
+ return "";
1152
+ const indexById = new Map;
1153
+ visible.forEach((c, i) => {
1154
+ indexById.set(c.id, i + 1);
1155
+ });
1156
+ let rendered = visible;
1157
+ let elidedCount = 0;
1158
+ if (maxComments && visible.length > maxComments) {
1159
+ const keep = criticalIds(visible);
1160
+ const recentThreshold = visible.length - maxComments;
1161
+ rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
1162
+ elidedCount = visible.length - rendered.length;
1163
+ }
1164
+ const ref = (id) => {
1165
+ const n = indexById.get(id);
1166
+ return n ? `#${n}` : `#${id.slice(0, 8)}`;
1167
+ };
1168
+ const lines = [];
1169
+ if (elidedCount > 0) {
1170
+ lines.push({
1171
+ at: visible[0]?.created_at ?? "",
1172
+ text: `(${elidedCount} earlier comment(s) omitted for brevity)`
1173
+ });
1174
+ }
1175
+ for (const c of rendered) {
1176
+ const tags = [];
1177
+ if (c.edited_at)
1178
+ tags.push("edited");
1179
+ if (c.supersedes_id)
1180
+ tags.push(`supersedes ${ref(c.supersedes_id)}`);
1181
+ if (c.confirms_id)
1182
+ tags.push(`confirms ${ref(c.confirms_id)}`);
1183
+ if (c.resolved_at)
1184
+ tags.push("resolved");
1185
+ const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
1186
+ const header = `[${ref(c.id)} | ${c.author_type} | ${authorLabel(c)} | ${c.comment_type} | ${c.created_at}${tagStr}]`;
1187
+ lines.push({ at: c.created_at, text: `${header}
1188
+ ${c.body.trim()}` });
1189
+ }
1190
+ for (const a of activity) {
1191
+ const actor = a.actor ? `${a.actor} ` : "";
1192
+ lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
1193
+ }
1194
+ lines.sort((a, b) => a.at.localeCompare(b.at));
1195
+ const body = lines.map((l) => l.text).join(`
1196
+
1197
+ `);
1198
+ const instruction = includeInstructions ? `
1199
+
1200
+ ${CONFLICT_INSTRUCTION}` : "";
1201
+ return `## ${heading} (oldest → newest)
1202
+
1203
+ ${body}${instruction}`;
1204
+ }
1124
1205
  // ../harmony-shared/dist/constants.js
1125
1206
  var TIMINGS = {
1126
1207
  SEARCH_DEBOUNCE: 300,
@@ -1473,6 +1554,28 @@ class HarmonyApiClient {
1473
1554
  async deleteSubtask(subtaskId) {
1474
1555
  return this.request("DELETE", `/subtasks/${subtaskId}`);
1475
1556
  }
1557
+ async addComment(cardId, body, opts) {
1558
+ return this.request("POST", `/cards/${cardId}/comments`, {
1559
+ body,
1560
+ authorType: "agent",
1561
+ commentType: opts?.commentType,
1562
+ supersedesId: opts?.supersedesId,
1563
+ confirmsId: opts?.confirmsId,
1564
+ agentSessionId: opts?.agentSessionId
1565
+ });
1566
+ }
1567
+ async getComments(cardId, opts) {
1568
+ const qs = new URLSearchParams;
1569
+ if (opts?.limit != null)
1570
+ qs.set("limit", String(opts.limit));
1571
+ if (opts?.offset != null)
1572
+ qs.set("offset", String(opts.offset));
1573
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
1574
+ return this.request("GET", `/cards/${cardId}/comments${suffix}`);
1575
+ }
1576
+ async updateComment(commentId, updates) {
1577
+ return this.request("PATCH", `/comments/${commentId}`, updates);
1578
+ }
1476
1579
  async startAgentSession(cardId, data) {
1477
1580
  return this.request("POST", `/cards/${cardId}/agent-context`, data);
1478
1581
  }
@@ -1830,6 +1933,24 @@ class HarmonyApiClient {
1830
1933
  assembledContext: assembledContextStr,
1831
1934
  assemblyId
1832
1935
  });
1936
+ try {
1937
+ const { comments } = await this.getComments(options.cardId, {
1938
+ limit: 200
1939
+ });
1940
+ if (Array.isArray(comments) && comments.length > 0) {
1941
+ const section = serializeCommentThread(comments, {
1942
+ heading: "Comments",
1943
+ maxComments: 40
1944
+ });
1945
+ if (section)
1946
+ result.prompt = `${result.prompt}
1947
+
1948
+ ${section}`;
1949
+ }
1950
+ } catch (err) {
1951
+ const msg = err instanceof Error ? err.message : String(err);
1952
+ console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
1953
+ }
1833
1954
  try {
1834
1955
  await this.recordPromptHistory({
1835
1956
  cardId: cardData.id,
@@ -3132,6 +3253,66 @@ var TOOLS = {
3132
3253
  required: ["subtaskId"]
3133
3254
  }
3134
3255
  },
3256
+ harmony_add_comment: {
3257
+ description: "Post a comment on a card as the agent. Use this to converse with the human in the open: report progress, ask a question, record a decision, or note a finding — instead of editing the card description. Set supersedesId to correct an earlier comment, confirmsId to reaffirm one. When the thread conflicts, prefer the latest comment unless a later one confirms an earlier finding; cite the comment id(s) you relied on.",
3258
+ inputSchema: {
3259
+ type: "object",
3260
+ properties: {
3261
+ cardId: { type: "string", description: "Card UUID to comment on" },
3262
+ body: { type: "string", description: "Comment body (Markdown)" },
3263
+ commentType: {
3264
+ type: "string",
3265
+ enum: [
3266
+ "message",
3267
+ "progress",
3268
+ "question",
3269
+ "blocker",
3270
+ "decision",
3271
+ "summary",
3272
+ "finding"
3273
+ ],
3274
+ description: "Type of comment. 'question'/'blocker' signal you need a human; default 'message'."
3275
+ },
3276
+ supersedesId: {
3277
+ type: "string",
3278
+ description: "Comment id this comment corrects/updates"
3279
+ },
3280
+ confirmsId: {
3281
+ type: "string",
3282
+ description: "Comment id this comment reaffirms"
3283
+ }
3284
+ },
3285
+ required: ["cardId", "body"]
3286
+ }
3287
+ },
3288
+ harmony_get_comments: {
3289
+ description: "Get the comment thread on a card (oldest → newest). Returns human + agent comments with author, type, edited/resolved state, and supersede/confirm links. Read this before acting so you weigh later comments over earlier ones they contradict.",
3290
+ inputSchema: {
3291
+ type: "object",
3292
+ properties: {
3293
+ cardId: { type: "string" },
3294
+ limit: { type: "number" },
3295
+ offset: { type: "number" }
3296
+ },
3297
+ required: ["cardId"]
3298
+ }
3299
+ },
3300
+ harmony_update_comment: {
3301
+ description: "Update one of your own comments: edit the body, pin it, or resolve a question/blocker once it has been answered.",
3302
+ inputSchema: {
3303
+ type: "object",
3304
+ properties: {
3305
+ commentId: { type: "string" },
3306
+ body: { type: "string" },
3307
+ pinned: { type: "boolean" },
3308
+ resolve: {
3309
+ type: "boolean",
3310
+ description: "Mark (true) or clear (false) the resolved state"
3311
+ }
3312
+ },
3313
+ required: ["commentId"]
3314
+ }
3315
+ },
3135
3316
  harmony_list_workspaces: {
3136
3317
  description: "List all workspaces the user has access to",
3137
3318
  inputSchema: {
@@ -4393,6 +4574,49 @@ async function handleToolCall(name, args, deps) {
4393
4574
  await client3.deleteSubtask(subtaskId);
4394
4575
  return { success: true };
4395
4576
  }
4577
+ case "harmony_add_comment": {
4578
+ const cardId = z.string().uuid().parse(args.cardId);
4579
+ const body = z.string().min(1).max(1e4).parse(args.body);
4580
+ const commentType = args.commentType !== undefined ? z.enum([
4581
+ "message",
4582
+ "progress",
4583
+ "question",
4584
+ "blocker",
4585
+ "decision",
4586
+ "summary",
4587
+ "finding"
4588
+ ]).parse(args.commentType) : undefined;
4589
+ const supersedesId = args.supersedesId !== undefined ? z.string().uuid().parse(args.supersedesId) : undefined;
4590
+ const confirmsId = args.confirmsId !== undefined ? z.string().uuid().parse(args.confirmsId) : undefined;
4591
+ const result = await client3.addComment(cardId, body, {
4592
+ commentType,
4593
+ supersedesId,
4594
+ confirmsId
4595
+ });
4596
+ return { success: true, ...result };
4597
+ }
4598
+ case "harmony_get_comments": {
4599
+ const cardId = z.string().uuid().parse(args.cardId);
4600
+ const limit = args.limit !== undefined ? z.number().int().min(1).max(500).parse(args.limit) : undefined;
4601
+ const offset = args.offset !== undefined ? z.number().int().min(0).parse(args.offset) : undefined;
4602
+ const result = await client3.getComments(cardId, { limit, offset });
4603
+ return { success: true, ...result };
4604
+ }
4605
+ case "harmony_update_comment": {
4606
+ const commentId = z.string().uuid().parse(args.commentId);
4607
+ const updates = {};
4608
+ if (args.body !== undefined) {
4609
+ updates.body = z.string().min(1).max(1e4).parse(args.body);
4610
+ }
4611
+ if (args.pinned !== undefined) {
4612
+ updates.pinned = z.boolean().parse(args.pinned);
4613
+ }
4614
+ if (args.resolve !== undefined) {
4615
+ updates.resolve = z.boolean().parse(args.resolve);
4616
+ }
4617
+ const result = await client3.updateComment(commentId, updates);
4618
+ return { success: true, ...result };
4619
+ }
4396
4620
  case "harmony_list_workspaces": {
4397
4621
  const result = await client3.listWorkspaces();
4398
4622
  return { success: true, ...result };
package/dist/index.js CHANGED
@@ -932,6 +932,87 @@ function getDisplayLinkType(linkType, direction) {
932
932
  return linkType;
933
933
  return LINK_TYPE_INVERSES[linkType];
934
934
  }
935
+ // ../harmony-shared/dist/commentSerializer.js
936
+ var CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
937
+ function authorLabel(c) {
938
+ if (c.author_type === "agent")
939
+ return "AI agent";
940
+ return c.author?.full_name || c.author?.email || "teammate";
941
+ }
942
+ function criticalIds(comments) {
943
+ const keep = new Set;
944
+ for (const c of comments) {
945
+ if (c.comment_type === "decision")
946
+ keep.add(c.id);
947
+ if (c.supersedes_id) {
948
+ keep.add(c.id);
949
+ keep.add(c.supersedes_id);
950
+ }
951
+ if (c.confirms_id) {
952
+ keep.add(c.id);
953
+ keep.add(c.confirms_id);
954
+ }
955
+ }
956
+ return keep;
957
+ }
958
+ function serializeCommentThread(comments, options = {}) {
959
+ const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
960
+ const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
961
+ if (visible.length === 0)
962
+ return "";
963
+ const indexById = new Map;
964
+ visible.forEach((c, i) => {
965
+ indexById.set(c.id, i + 1);
966
+ });
967
+ let rendered = visible;
968
+ let elidedCount = 0;
969
+ if (maxComments && visible.length > maxComments) {
970
+ const keep = criticalIds(visible);
971
+ const recentThreshold = visible.length - maxComments;
972
+ rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
973
+ elidedCount = visible.length - rendered.length;
974
+ }
975
+ const ref = (id) => {
976
+ const n = indexById.get(id);
977
+ return n ? `#${n}` : `#${id.slice(0, 8)}`;
978
+ };
979
+ const lines = [];
980
+ if (elidedCount > 0) {
981
+ lines.push({
982
+ at: visible[0]?.created_at ?? "",
983
+ text: `(${elidedCount} earlier comment(s) omitted for brevity)`
984
+ });
985
+ }
986
+ for (const c of rendered) {
987
+ const tags = [];
988
+ if (c.edited_at)
989
+ tags.push("edited");
990
+ if (c.supersedes_id)
991
+ tags.push(`supersedes ${ref(c.supersedes_id)}`);
992
+ if (c.confirms_id)
993
+ tags.push(`confirms ${ref(c.confirms_id)}`);
994
+ if (c.resolved_at)
995
+ tags.push("resolved");
996
+ const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
997
+ const header = `[${ref(c.id)} | ${c.author_type} | ${authorLabel(c)} | ${c.comment_type} | ${c.created_at}${tagStr}]`;
998
+ lines.push({ at: c.created_at, text: `${header}
999
+ ${c.body.trim()}` });
1000
+ }
1001
+ for (const a of activity) {
1002
+ const actor = a.actor ? `${a.actor} ` : "";
1003
+ lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
1004
+ }
1005
+ lines.sort((a, b) => a.at.localeCompare(b.at));
1006
+ const body = lines.map((l) => l.text).join(`
1007
+
1008
+ `);
1009
+ const instruction = includeInstructions ? `
1010
+
1011
+ ${CONFLICT_INSTRUCTION}` : "";
1012
+ return `## ${heading} (oldest → newest)
1013
+
1014
+ ${body}${instruction}`;
1015
+ }
935
1016
  // ../harmony-shared/dist/constants.js
936
1017
  var TIMINGS = {
937
1018
  SEARCH_DEBOUNCE: 300,
@@ -1469,6 +1550,28 @@ class HarmonyApiClient {
1469
1550
  async deleteSubtask(subtaskId) {
1470
1551
  return this.request("DELETE", `/subtasks/${subtaskId}`);
1471
1552
  }
1553
+ async addComment(cardId, body, opts) {
1554
+ return this.request("POST", `/cards/${cardId}/comments`, {
1555
+ body,
1556
+ authorType: "agent",
1557
+ commentType: opts?.commentType,
1558
+ supersedesId: opts?.supersedesId,
1559
+ confirmsId: opts?.confirmsId,
1560
+ agentSessionId: opts?.agentSessionId
1561
+ });
1562
+ }
1563
+ async getComments(cardId, opts) {
1564
+ const qs = new URLSearchParams;
1565
+ if (opts?.limit != null)
1566
+ qs.set("limit", String(opts.limit));
1567
+ if (opts?.offset != null)
1568
+ qs.set("offset", String(opts.offset));
1569
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
1570
+ return this.request("GET", `/cards/${cardId}/comments${suffix}`);
1571
+ }
1572
+ async updateComment(commentId, updates) {
1573
+ return this.request("PATCH", `/comments/${commentId}`, updates);
1574
+ }
1472
1575
  async startAgentSession(cardId, data) {
1473
1576
  return this.request("POST", `/cards/${cardId}/agent-context`, data);
1474
1577
  }
@@ -1826,6 +1929,24 @@ class HarmonyApiClient {
1826
1929
  assembledContext: assembledContextStr,
1827
1930
  assemblyId
1828
1931
  });
1932
+ try {
1933
+ const { comments } = await this.getComments(options.cardId, {
1934
+ limit: 200
1935
+ });
1936
+ if (Array.isArray(comments) && comments.length > 0) {
1937
+ const section = serializeCommentThread(comments, {
1938
+ heading: "Comments",
1939
+ maxComments: 40
1940
+ });
1941
+ if (section)
1942
+ result.prompt = `${result.prompt}
1943
+
1944
+ ${section}`;
1945
+ }
1946
+ } catch (err) {
1947
+ const msg = err instanceof Error ? err.message : String(err);
1948
+ console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
1949
+ }
1829
1950
  try {
1830
1951
  await this.recordPromptHistory({
1831
1952
  cardId: cardData.id,
@@ -3128,6 +3249,66 @@ var TOOLS = {
3128
3249
  required: ["subtaskId"]
3129
3250
  }
3130
3251
  },
3252
+ harmony_add_comment: {
3253
+ description: "Post a comment on a card as the agent. Use this to converse with the human in the open: report progress, ask a question, record a decision, or note a finding — instead of editing the card description. Set supersedesId to correct an earlier comment, confirmsId to reaffirm one. When the thread conflicts, prefer the latest comment unless a later one confirms an earlier finding; cite the comment id(s) you relied on.",
3254
+ inputSchema: {
3255
+ type: "object",
3256
+ properties: {
3257
+ cardId: { type: "string", description: "Card UUID to comment on" },
3258
+ body: { type: "string", description: "Comment body (Markdown)" },
3259
+ commentType: {
3260
+ type: "string",
3261
+ enum: [
3262
+ "message",
3263
+ "progress",
3264
+ "question",
3265
+ "blocker",
3266
+ "decision",
3267
+ "summary",
3268
+ "finding"
3269
+ ],
3270
+ description: "Type of comment. 'question'/'blocker' signal you need a human; default 'message'."
3271
+ },
3272
+ supersedesId: {
3273
+ type: "string",
3274
+ description: "Comment id this comment corrects/updates"
3275
+ },
3276
+ confirmsId: {
3277
+ type: "string",
3278
+ description: "Comment id this comment reaffirms"
3279
+ }
3280
+ },
3281
+ required: ["cardId", "body"]
3282
+ }
3283
+ },
3284
+ harmony_get_comments: {
3285
+ description: "Get the comment thread on a card (oldest → newest). Returns human + agent comments with author, type, edited/resolved state, and supersede/confirm links. Read this before acting so you weigh later comments over earlier ones they contradict.",
3286
+ inputSchema: {
3287
+ type: "object",
3288
+ properties: {
3289
+ cardId: { type: "string" },
3290
+ limit: { type: "number" },
3291
+ offset: { type: "number" }
3292
+ },
3293
+ required: ["cardId"]
3294
+ }
3295
+ },
3296
+ harmony_update_comment: {
3297
+ description: "Update one of your own comments: edit the body, pin it, or resolve a question/blocker once it has been answered.",
3298
+ inputSchema: {
3299
+ type: "object",
3300
+ properties: {
3301
+ commentId: { type: "string" },
3302
+ body: { type: "string" },
3303
+ pinned: { type: "boolean" },
3304
+ resolve: {
3305
+ type: "boolean",
3306
+ description: "Mark (true) or clear (false) the resolved state"
3307
+ }
3308
+ },
3309
+ required: ["commentId"]
3310
+ }
3311
+ },
3131
3312
  harmony_list_workspaces: {
3132
3313
  description: "List all workspaces the user has access to",
3133
3314
  inputSchema: {
@@ -4389,6 +4570,49 @@ async function handleToolCall(name, args, deps) {
4389
4570
  await client3.deleteSubtask(subtaskId);
4390
4571
  return { success: true };
4391
4572
  }
4573
+ case "harmony_add_comment": {
4574
+ const cardId = z.string().uuid().parse(args.cardId);
4575
+ const body = z.string().min(1).max(1e4).parse(args.body);
4576
+ const commentType = args.commentType !== undefined ? z.enum([
4577
+ "message",
4578
+ "progress",
4579
+ "question",
4580
+ "blocker",
4581
+ "decision",
4582
+ "summary",
4583
+ "finding"
4584
+ ]).parse(args.commentType) : undefined;
4585
+ const supersedesId = args.supersedesId !== undefined ? z.string().uuid().parse(args.supersedesId) : undefined;
4586
+ const confirmsId = args.confirmsId !== undefined ? z.string().uuid().parse(args.confirmsId) : undefined;
4587
+ const result = await client3.addComment(cardId, body, {
4588
+ commentType,
4589
+ supersedesId,
4590
+ confirmsId
4591
+ });
4592
+ return { success: true, ...result };
4593
+ }
4594
+ case "harmony_get_comments": {
4595
+ const cardId = z.string().uuid().parse(args.cardId);
4596
+ const limit = args.limit !== undefined ? z.number().int().min(1).max(500).parse(args.limit) : undefined;
4597
+ const offset = args.offset !== undefined ? z.number().int().min(0).parse(args.offset) : undefined;
4598
+ const result = await client3.getComments(cardId, { limit, offset });
4599
+ return { success: true, ...result };
4600
+ }
4601
+ case "harmony_update_comment": {
4602
+ const commentId = z.string().uuid().parse(args.commentId);
4603
+ const updates = {};
4604
+ if (args.body !== undefined) {
4605
+ updates.body = z.string().min(1).max(1e4).parse(args.body);
4606
+ }
4607
+ if (args.pinned !== undefined) {
4608
+ updates.pinned = z.boolean().parse(args.pinned);
4609
+ }
4610
+ if (args.resolve !== undefined) {
4611
+ updates.resolve = z.boolean().parse(args.resolve);
4612
+ }
4613
+ const result = await client3.updateComment(commentId, updates);
4614
+ return { success: true, ...result };
4615
+ }
4392
4616
  case "harmony_list_workspaces": {
4393
4617
  const result = await client3.listWorkspaces();
4394
4618
  return { success: true, ...result };
@@ -724,6 +724,87 @@ function getDisplayLinkType(linkType, direction) {
724
724
  return linkType;
725
725
  return LINK_TYPE_INVERSES[linkType];
726
726
  }
727
+ // ../harmony-shared/dist/commentSerializer.js
728
+ var CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
729
+ function authorLabel(c) {
730
+ if (c.author_type === "agent")
731
+ return "AI agent";
732
+ return c.author?.full_name || c.author?.email || "teammate";
733
+ }
734
+ function criticalIds(comments) {
735
+ const keep = new Set;
736
+ for (const c of comments) {
737
+ if (c.comment_type === "decision")
738
+ keep.add(c.id);
739
+ if (c.supersedes_id) {
740
+ keep.add(c.id);
741
+ keep.add(c.supersedes_id);
742
+ }
743
+ if (c.confirms_id) {
744
+ keep.add(c.id);
745
+ keep.add(c.confirms_id);
746
+ }
747
+ }
748
+ return keep;
749
+ }
750
+ function serializeCommentThread(comments, options = {}) {
751
+ const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
752
+ const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
753
+ if (visible.length === 0)
754
+ return "";
755
+ const indexById = new Map;
756
+ visible.forEach((c, i) => {
757
+ indexById.set(c.id, i + 1);
758
+ });
759
+ let rendered = visible;
760
+ let elidedCount = 0;
761
+ if (maxComments && visible.length > maxComments) {
762
+ const keep = criticalIds(visible);
763
+ const recentThreshold = visible.length - maxComments;
764
+ rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
765
+ elidedCount = visible.length - rendered.length;
766
+ }
767
+ const ref = (id) => {
768
+ const n = indexById.get(id);
769
+ return n ? `#${n}` : `#${id.slice(0, 8)}`;
770
+ };
771
+ const lines = [];
772
+ if (elidedCount > 0) {
773
+ lines.push({
774
+ at: visible[0]?.created_at ?? "",
775
+ text: `(${elidedCount} earlier comment(s) omitted for brevity)`
776
+ });
777
+ }
778
+ for (const c of rendered) {
779
+ const tags = [];
780
+ if (c.edited_at)
781
+ tags.push("edited");
782
+ if (c.supersedes_id)
783
+ tags.push(`supersedes ${ref(c.supersedes_id)}`);
784
+ if (c.confirms_id)
785
+ tags.push(`confirms ${ref(c.confirms_id)}`);
786
+ if (c.resolved_at)
787
+ tags.push("resolved");
788
+ const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
789
+ const header = `[${ref(c.id)} | ${c.author_type} | ${authorLabel(c)} | ${c.comment_type} | ${c.created_at}${tagStr}]`;
790
+ lines.push({ at: c.created_at, text: `${header}
791
+ ${c.body.trim()}` });
792
+ }
793
+ for (const a of activity) {
794
+ const actor = a.actor ? `${a.actor} ` : "";
795
+ lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
796
+ }
797
+ lines.sort((a, b) => a.at.localeCompare(b.at));
798
+ const body = lines.map((l) => l.text).join(`
799
+
800
+ `);
801
+ const instruction = includeInstructions ? `
802
+
803
+ ${CONFLICT_INSTRUCTION}` : "";
804
+ return `## ${heading} (oldest → newest)
805
+
806
+ ${body}${instruction}`;
807
+ }
727
808
  // ../harmony-shared/dist/constants.js
728
809
  var TIMINGS = {
729
810
  SEARCH_DEBOUNCE: 300,
@@ -1076,6 +1157,28 @@ class HarmonyApiClient {
1076
1157
  async deleteSubtask(subtaskId) {
1077
1158
  return this.request("DELETE", `/subtasks/${subtaskId}`);
1078
1159
  }
1160
+ async addComment(cardId, body, opts) {
1161
+ return this.request("POST", `/cards/${cardId}/comments`, {
1162
+ body,
1163
+ authorType: "agent",
1164
+ commentType: opts?.commentType,
1165
+ supersedesId: opts?.supersedesId,
1166
+ confirmsId: opts?.confirmsId,
1167
+ agentSessionId: opts?.agentSessionId
1168
+ });
1169
+ }
1170
+ async getComments(cardId, opts) {
1171
+ const qs = new URLSearchParams;
1172
+ if (opts?.limit != null)
1173
+ qs.set("limit", String(opts.limit));
1174
+ if (opts?.offset != null)
1175
+ qs.set("offset", String(opts.offset));
1176
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
1177
+ return this.request("GET", `/cards/${cardId}/comments${suffix}`);
1178
+ }
1179
+ async updateComment(commentId, updates) {
1180
+ return this.request("PATCH", `/comments/${commentId}`, updates);
1181
+ }
1079
1182
  async startAgentSession(cardId, data) {
1080
1183
  return this.request("POST", `/cards/${cardId}/agent-context`, data);
1081
1184
  }
@@ -1433,6 +1536,24 @@ class HarmonyApiClient {
1433
1536
  assembledContext: assembledContextStr,
1434
1537
  assemblyId
1435
1538
  });
1539
+ try {
1540
+ const { comments } = await this.getComments(options.cardId, {
1541
+ limit: 200
1542
+ });
1543
+ if (Array.isArray(comments) && comments.length > 0) {
1544
+ const section = serializeCommentThread(comments, {
1545
+ heading: "Comments",
1546
+ maxComments: 40
1547
+ });
1548
+ if (section)
1549
+ result.prompt = `${result.prompt}
1550
+
1551
+ ${section}`;
1552
+ }
1553
+ } catch (err) {
1554
+ const msg = err instanceof Error ? err.message : String(err);
1555
+ console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
1556
+ }
1436
1557
  try {
1437
1558
  await this.recordPromptHistory({
1438
1559
  cardId: cardData.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.8.1",
3
+ "version": "2.8.2",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/api-client.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { getDisplayLinkType } from "@harmony/shared";
1
+ import {
2
+ type Comment,
3
+ getDisplayLinkType,
4
+ serializeCommentThread,
5
+ } from "@harmony/shared";
2
6
  import { getApiKey, getApiUrl } from "./config.js";
3
7
 
4
8
  export interface ApiResponse<T = unknown> {
@@ -617,6 +621,46 @@ export class HarmonyApiClient {
617
621
  return this.request("DELETE", `/subtasks/${subtaskId}`);
618
622
  }
619
623
 
624
+ // ============ COMMENT OPERATIONS ============
625
+
626
+ async addComment(
627
+ cardId: string,
628
+ body: string,
629
+ opts?: {
630
+ commentType?: string;
631
+ supersedesId?: string;
632
+ confirmsId?: string;
633
+ agentSessionId?: string;
634
+ },
635
+ ): Promise<{ comment: unknown }> {
636
+ return this.request("POST", `/cards/${cardId}/comments`, {
637
+ body,
638
+ authorType: "agent",
639
+ commentType: opts?.commentType,
640
+ supersedesId: opts?.supersedesId,
641
+ confirmsId: opts?.confirmsId,
642
+ agentSessionId: opts?.agentSessionId,
643
+ });
644
+ }
645
+
646
+ async getComments(
647
+ cardId: string,
648
+ opts?: { limit?: number; offset?: number },
649
+ ): Promise<{ comments: unknown[] }> {
650
+ const qs = new URLSearchParams();
651
+ if (opts?.limit != null) qs.set("limit", String(opts.limit));
652
+ if (opts?.offset != null) qs.set("offset", String(opts.offset));
653
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
654
+ return this.request("GET", `/cards/${cardId}/comments${suffix}`);
655
+ }
656
+
657
+ async updateComment(
658
+ commentId: string,
659
+ updates: { body?: string; pinned?: boolean; resolve?: boolean },
660
+ ): Promise<{ comment: unknown }> {
661
+ return this.request("PATCH", `/comments/${commentId}`, updates);
662
+ }
663
+
620
664
  // ============ AGENT CONTEXT OPERATIONS ============
621
665
 
622
666
  async startAgentSession(
@@ -1448,6 +1492,26 @@ export class HarmonyApiClient {
1448
1492
  assemblyId,
1449
1493
  });
1450
1494
 
1495
+ // Append the card's comment thread (people + agents). This is the central
1496
+ // injection point: every caller of harmony_generate_prompt (daemon, Claude
1497
+ // Code, in-app builder) gets the recency-ordered thread + conflict rule.
1498
+ // Best-effort — never fail prompt generation on a comments fetch error.
1499
+ try {
1500
+ const { comments } = await this.getComments(options.cardId, {
1501
+ limit: 200,
1502
+ });
1503
+ if (Array.isArray(comments) && comments.length > 0) {
1504
+ const section = serializeCommentThread(comments as Comment[], {
1505
+ heading: "Comments",
1506
+ maxComments: 40,
1507
+ });
1508
+ if (section) result.prompt = `${result.prompt}\n\n${section}`;
1509
+ }
1510
+ } catch (err) {
1511
+ const msg = err instanceof Error ? err.message : String(err);
1512
+ console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
1513
+ }
1514
+
1451
1515
  // AGP P2: persist a session-linked snapshot. Best-effort — never fail
1452
1516
  // prompt generation just because logging didn't land.
1453
1517
  try {
package/src/server.ts CHANGED
@@ -636,6 +636,72 @@ export const TOOLS = {
636
636
  },
637
637
  },
638
638
 
639
+ // Comment operations
640
+ harmony_add_comment: {
641
+ description:
642
+ "Post a comment on a card as the agent. Use this to converse with the human in the open: report progress, ask a question, record a decision, or note a finding — instead of editing the card description. Set supersedesId to correct an earlier comment, confirmsId to reaffirm one. When the thread conflicts, prefer the latest comment unless a later one confirms an earlier finding; cite the comment id(s) you relied on.",
643
+ inputSchema: {
644
+ type: "object",
645
+ properties: {
646
+ cardId: { type: "string", description: "Card UUID to comment on" },
647
+ body: { type: "string", description: "Comment body (Markdown)" },
648
+ commentType: {
649
+ type: "string",
650
+ enum: [
651
+ "message",
652
+ "progress",
653
+ "question",
654
+ "blocker",
655
+ "decision",
656
+ "summary",
657
+ "finding",
658
+ ],
659
+ description:
660
+ "Type of comment. 'question'/'blocker' signal you need a human; default 'message'.",
661
+ },
662
+ supersedesId: {
663
+ type: "string",
664
+ description: "Comment id this comment corrects/updates",
665
+ },
666
+ confirmsId: {
667
+ type: "string",
668
+ description: "Comment id this comment reaffirms",
669
+ },
670
+ },
671
+ required: ["cardId", "body"],
672
+ },
673
+ },
674
+ harmony_get_comments: {
675
+ description:
676
+ "Get the comment thread on a card (oldest → newest). Returns human + agent comments with author, type, edited/resolved state, and supersede/confirm links. Read this before acting so you weigh later comments over earlier ones they contradict.",
677
+ inputSchema: {
678
+ type: "object",
679
+ properties: {
680
+ cardId: { type: "string" },
681
+ limit: { type: "number" },
682
+ offset: { type: "number" },
683
+ },
684
+ required: ["cardId"],
685
+ },
686
+ },
687
+ harmony_update_comment: {
688
+ description:
689
+ "Update one of your own comments: edit the body, pin it, or resolve a question/blocker once it has been answered.",
690
+ inputSchema: {
691
+ type: "object",
692
+ properties: {
693
+ commentId: { type: "string" },
694
+ body: { type: "string" },
695
+ pinned: { type: "boolean" },
696
+ resolve: {
697
+ type: "boolean",
698
+ description: "Mark (true) or clear (false) the resolved state",
699
+ },
700
+ },
701
+ required: ["commentId"],
702
+ },
703
+ },
704
+
639
705
  // Context operations
640
706
  harmony_list_workspaces: {
641
707
  description: "List all workspaces the user has access to",
@@ -2177,6 +2243,71 @@ async function handleToolCall(
2177
2243
  return { success: true };
2178
2244
  }
2179
2245
 
2246
+ // Comment operations
2247
+ case "harmony_add_comment": {
2248
+ const cardId = z.string().uuid().parse(args.cardId);
2249
+ const body = z.string().min(1).max(10_000).parse(args.body);
2250
+ const commentType =
2251
+ args.commentType !== undefined
2252
+ ? z
2253
+ .enum([
2254
+ "message",
2255
+ "progress",
2256
+ "question",
2257
+ "blocker",
2258
+ "decision",
2259
+ "summary",
2260
+ "finding",
2261
+ ])
2262
+ .parse(args.commentType)
2263
+ : undefined;
2264
+ const supersedesId =
2265
+ args.supersedesId !== undefined
2266
+ ? z.string().uuid().parse(args.supersedesId)
2267
+ : undefined;
2268
+ const confirmsId =
2269
+ args.confirmsId !== undefined
2270
+ ? z.string().uuid().parse(args.confirmsId)
2271
+ : undefined;
2272
+ const result = await client.addComment(cardId, body, {
2273
+ commentType,
2274
+ supersedesId,
2275
+ confirmsId,
2276
+ });
2277
+ return { success: true, ...result };
2278
+ }
2279
+
2280
+ case "harmony_get_comments": {
2281
+ const cardId = z.string().uuid().parse(args.cardId);
2282
+ const limit =
2283
+ args.limit !== undefined
2284
+ ? z.number().int().min(1).max(500).parse(args.limit)
2285
+ : undefined;
2286
+ const offset =
2287
+ args.offset !== undefined
2288
+ ? z.number().int().min(0).parse(args.offset)
2289
+ : undefined;
2290
+ const result = await client.getComments(cardId, { limit, offset });
2291
+ return { success: true, ...result };
2292
+ }
2293
+
2294
+ case "harmony_update_comment": {
2295
+ const commentId = z.string().uuid().parse(args.commentId);
2296
+ const updates: { body?: string; pinned?: boolean; resolve?: boolean } =
2297
+ {};
2298
+ if (args.body !== undefined) {
2299
+ updates.body = z.string().min(1).max(10_000).parse(args.body);
2300
+ }
2301
+ if (args.pinned !== undefined) {
2302
+ updates.pinned = z.boolean().parse(args.pinned);
2303
+ }
2304
+ if (args.resolve !== undefined) {
2305
+ updates.resolve = z.boolean().parse(args.resolve);
2306
+ }
2307
+ const result = await client.updateComment(commentId, updates);
2308
+ return { success: true, ...result };
2309
+ }
2310
+
2180
2311
  // Context operations
2181
2312
  case "harmony_list_workspaces": {
2182
2313
  const result = await client.listWorkspaces();