@gethmy/mcp 2.13.4 → 2.15.0

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/src/server.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  destroyAutoSession,
23
23
  initAutoSession,
24
24
  markExplicit,
25
+ noteSessionStatus,
25
26
  shutdownAllSessions,
26
27
  trackActivity,
27
28
  untrack,
@@ -121,6 +122,65 @@ function requireExactlyOneScope(scope: {
121
122
  }
122
123
  }
123
124
 
125
+ /**
126
+ * Backward-compat dispatch shim (#605). Consolidated tools dropped the old
127
+ * names from the advertised TOOLS list (the token win), but `handleToolCall`
128
+ * still dispatches them so external integrations calling the old names keep
129
+ * working through a deprecation window. Logged once per name to stderr (never
130
+ * stdout — that carries the MCP protocol on stdio).
131
+ */
132
+ const DEPRECATED_TOOL_ALIASES: Record<string, string> = {
133
+ harmony_get_card_by_short_id: "harmony_get_card",
134
+ harmony_bulk_get_cards: "harmony_get_card",
135
+ harmony_upload_artifact: "harmony_upload",
136
+ harmony_upload_card_attachment: "harmony_upload",
137
+ harmony_request_artifact_upload_url: "harmony_request_upload_url",
138
+ harmony_request_card_attachment_upload_url: "harmony_request_upload_url",
139
+ harmony_finalize_artifact: "harmony_finalize_upload",
140
+ harmony_finalize_card_attachment: "harmony_finalize_upload",
141
+ };
142
+ const _warnedDeprecatedTools = new Set<string>();
143
+ function warnDeprecatedTool(oldName: string): void {
144
+ if (_warnedDeprecatedTools.has(oldName)) return;
145
+ _warnedDeprecatedTools.add(oldName);
146
+ const replacement = DEPRECATED_TOOL_ALIASES[oldName];
147
+ console.error(
148
+ `[harmony-mcp] Tool "${oldName}" is deprecated (still accepted) — ` +
149
+ `use "${replacement}" instead. The old name is no longer advertised and ` +
150
+ `will be removed in a future major version.`,
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Removed-capability shim (#612). The legacy steps_version=1 "Automation" macro
156
+ * was retired when Harmony committed to the single stage-Playbook model. These
157
+ * tools have no v2 replacement, so — following the #605 hidden-dispatch pattern —
158
+ * they are dropped from the advertised TOOLS list but their switch cases remain,
159
+ * returning a one-time deprecation notice (warned once to stderr) instead of an
160
+ * unknown-tool error, so an integration calling the old name fails legibly.
161
+ */
162
+ const DEPRECATED_REMOVED_TOOLS: Record<string, string> = {
163
+ harmony_run_playbook:
164
+ "Playbooks are stage models run by the board and agent daemon as a card moves through their stages — there is no server-side macro to trigger. This tool no longer runs anything.",
165
+ harmony_save_card_as_playbook:
166
+ "The steps_version=1 automation macro was removed. Build a stage playbook with harmony_create_playbook instead.",
167
+ };
168
+ const _warnedRemovedTools = new Set<string>();
169
+ function deprecatedRemovedToolResult(name: string): {
170
+ success: false;
171
+ deprecated: true;
172
+ message: string;
173
+ } {
174
+ const message = DEPRECATED_REMOVED_TOOLS[name];
175
+ if (!_warnedRemovedTools.has(name)) {
176
+ _warnedRemovedTools.add(name);
177
+ console.error(
178
+ `[harmony-mcp] Tool "${name}" is deprecated and no longer functional — ${message}`,
179
+ );
180
+ }
181
+ return { success: false, deprecated: true, message };
182
+ }
183
+
124
184
  /** PUT raw bytes straight to a Supabase signed upload URL (the token rides in
125
185
  * the URL query). Used by the stdio internal handshake; the hosted server can't
126
186
  * read local disk, so there the agent drives this PUT itself via the two-step
@@ -625,49 +685,37 @@ export const TOOLS = {
625
685
  },
626
686
  },
627
687
  harmony_get_card: {
628
- description: "Get detailed information about a specific card by UUID",
629
- inputSchema: {
630
- type: "object",
631
- properties: {
632
- cardId: { type: "string", description: "Card UUID" },
633
- },
634
- required: ["cardId"],
635
- },
636
- },
637
- harmony_get_card_by_short_id: {
638
- description: "Get a card by its short ID (e.g., #42) within a project",
688
+ description:
689
+ "Fetch one or many cards. Provide exactly one of: `cardId` (UUID, full detail), " +
690
+ "`shortId` (e.g. 42, full detail), or `shortIds` (array, max 100 — compact " +
691
+ "summaries: id, shortId, title, column, priority, assignee, labels, archived, " +
692
+ "plus any short ids not found). `shortId`/`shortIds` need project context. " +
693
+ "Prefer one `shortIds` call over repeated single fetches for multiple cards.",
639
694
  inputSchema: {
640
695
  type: "object",
641
696
  properties: {
642
- projectId: {
697
+ cardId: {
643
698
  type: "string",
644
- description: "Project ID (optional if context set)",
699
+ description: "Card UUID full detail for one card.",
645
700
  },
646
701
  shortId: {
647
702
  type: "number",
648
- description: "Short ID number (e.g., 42 for card #42)",
703
+ description:
704
+ "Short ID (e.g. 42 for card #42) — full detail, project-scoped.",
649
705
  },
650
- },
651
- required: ["shortId"],
652
- },
653
- },
654
- harmony_bulk_get_cards: {
655
- description:
656
- "Fetch multiple cards by short id in one call. Returns compact summaries " +
657
- "(id, shortId, title, column, priority, assignee, labels, archived) plus " +
658
- "any short ids not found. Requires project context. Prefer this over " +
659
- "repeated harmony_get_card_by_short_id when referencing multiple cards.",
660
- inputSchema: {
661
- type: "object",
662
- properties: {
663
706
  shortIds: {
664
707
  type: "array",
665
708
  items: { type: "number" },
666
709
  description:
667
- "Card short ids, e.g. [400, 401, 402]. Max 100 per call.",
710
+ "Short IDs, e.g. [400, 401, 402] compact summaries in one call. Max 100.",
711
+ },
712
+ projectId: {
713
+ type: "string",
714
+ description:
715
+ "Project ID for shortId/shortIds (optional if context set).",
668
716
  },
669
717
  },
670
- required: ["shortIds"],
718
+ required: [],
671
719
  },
672
720
  },
673
721
  harmony_bulk_archive_cards: {
@@ -837,186 +885,162 @@ export const TOOLS = {
837
885
  required: ["cardId"],
838
886
  },
839
887
  },
840
- harmony_upload_card_attachment: {
888
+ harmony_upload: {
841
889
  description:
842
- "Upload a file attachment to a card. Provide `filePath` (local path the server reads, direct-to-storage — local/stdio mode) or `base64Data` (works everywhere, small-file fallback). Max 5MB; allowed: PNG, JPEG, GIF, WebP, HEIC/HEIF, PDF, DOC/DOCX, XLS/XLSX, TXT, CSV. Returns the attachment + a signed URL. Large files on the hosted MCP server: use harmony_request_card_attachment_upload_url + harmony_finalize_card_attachment instead.",
890
+ 'Upload a file in one call. `target: "card_attachment"` attaches a file to a card ' +
891
+ "(max 5MB; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV) — requires cardId. " +
892
+ '`target: "artifact"` hosts a self-contained HTML doc (text/html, max 2MB) linked to ' +
893
+ "exactly one of cardId/planId/workspaceId, rendered in-app in a sandboxed iframe. Provide " +
894
+ "the bytes as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). " +
895
+ "Returns the attachment/artifact + a signed URL (artifact: use harmony_share_artifact for a " +
896
+ "public link). Large files on the hosted MCP server: use harmony_request_upload_url + " +
897
+ "harmony_finalize_upload instead.",
843
898
  inputSchema: {
844
899
  type: "object",
845
900
  properties: {
846
- cardId: { type: "string", description: "Card UUID" },
901
+ target: {
902
+ type: "string",
903
+ enum: ["card_attachment", "artifact"],
904
+ description:
905
+ "What to upload: a card file attachment or a hosted HTML artifact.",
906
+ },
907
+ cardId: {
908
+ type: "string",
909
+ description:
910
+ "Card UUID. Required for card_attachment; one of cardId/planId/workspaceId for artifact.",
911
+ },
912
+ planId: {
913
+ type: "string",
914
+ description:
915
+ "Plan UUID (artifact only — link the artifact to a plan).",
916
+ },
917
+ workspaceId: {
918
+ type: "string",
919
+ description: "Workspace UUID (artifact only — standalone artifact).",
920
+ },
847
921
  filePath: {
848
922
  type: "string",
849
923
  description:
850
- "Absolute path to a local file the MCP server process can read. Mutually exclusive with base64Data.",
924
+ "Absolute path to a local file the server can read. Mutually exclusive with base64Data.",
851
925
  },
852
926
  base64Data: {
853
927
  type: "string",
854
928
  description:
855
- "Base64-encoded file bytes (a `data:` URL prefix is accepted and stripped). Requires fileName. Mutually exclusive with filePath.",
929
+ "Base64-encoded bytes (a `data:` URL prefix is accepted and stripped). Mutually exclusive with filePath.",
856
930
  },
857
931
  fileName: {
858
932
  type: "string",
859
933
  description:
860
- "File name including extension (e.g. 'screenshot.png'). Required with base64Data; defaults to the basename of filePath otherwise.",
934
+ "File name including extension (card_attachment: required with base64Data, else defaults to the filePath basename).",
935
+ },
936
+ title: {
937
+ type: "string",
938
+ description:
939
+ "Artifact display title (defaults to the file basename).",
861
940
  },
862
941
  contentType: {
863
942
  type: "string",
864
943
  description:
865
- "Optional MIME type (e.g. 'image/png'). Inferred from the file extension when omitted.",
944
+ "Optional MIME type (card_attachment; inferred from the extension when omitted).",
866
945
  },
867
946
  },
868
- required: ["cardId"],
947
+ required: ["target"],
869
948
  },
870
949
  },
871
- harmony_request_card_attachment_upload_url: {
950
+ harmony_request_upload_url: {
872
951
  description:
873
- "Step 1 of the large-file / hosted-MCP upload handshake (use when the server can't read local disk). Mints a one-shot signed Storage upload URL under the card. Returns { uploadUrl, token, storagePath, fileType }. Then PUT the raw bytes to uploadUrl (no bytes through the model context) and call harmony_finalize_card_attachment with storagePath. Max 5MB; same allowed types as harmony_upload_card_attachment.",
952
+ "Step 1 of the large-file / hosted-MCP upload handshake (use when the server can't read " +
953
+ 'local disk). Mints a one-shot signed Storage upload URL. `target: "card_attachment"` ' +
954
+ '(requires cardId, fileName, size; max 5MB) or `target: "artifact"` (one of ' +
955
+ "cardId/planId/workspaceId; text/html, max 2MB). Returns { uploadUrl, token, storagePath, " +
956
+ "[fileType] }. Then PUT the raw bytes to uploadUrl (no bytes through the model context) and " +
957
+ "call harmony_finalize_upload with the storagePath.",
874
958
  inputSchema: {
875
959
  type: "object",
876
960
  properties: {
877
- cardId: { type: "string", description: "Card UUID" },
878
- fileName: {
961
+ target: {
879
962
  type: "string",
880
- description: "File name including extension (e.g. 'screenshot.png').",
963
+ enum: ["card_attachment", "artifact"],
964
+ description:
965
+ "What to upload: a card file attachment or a hosted HTML artifact.",
881
966
  },
882
- fileType: {
967
+ cardId: {
883
968
  type: "string",
884
969
  description:
885
- "Optional MIME type (e.g. 'image/png'). Inferred from the file extension when omitted.",
886
- },
887
- size: {
888
- type: "number",
889
- description: "File size in bytes (rejected early if over 5MB).",
970
+ "Card UUID. Required for card_attachment; one of cardId/planId/workspaceId for artifact.",
890
971
  },
891
- },
892
- required: ["cardId", "fileName", "size"],
893
- },
894
- },
895
- harmony_finalize_card_attachment: {
896
- description:
897
- "Step 2 of the upload handshake. After PUTting the bytes to the signed uploadUrl, call this with storagePath to validate and register the attachment. The server re-downloads the object and enforces size + an allowlisted content-type (magic-byte sniff, never the declared type) + an optional sha256 integrity check, deleting and failing on any mismatch. Returns the attachment + a signed URL.",
898
- inputSchema: {
899
- type: "object",
900
- properties: {
901
- cardId: { type: "string", description: "Card UUID" },
902
- storagePath: {
972
+ planId: { type: "string", description: "Plan UUID (artifact only)." },
973
+ workspaceId: {
903
974
  type: "string",
904
- description:
905
- "The storagePath returned by harmony_request_card_attachment_upload_url.",
975
+ description: "Workspace UUID (artifact only).",
906
976
  },
907
977
  fileName: {
908
978
  type: "string",
909
- description: "File name including extension (e.g. 'screenshot.png').",
979
+ description:
980
+ "File name including extension (required for card_attachment).",
910
981
  },
982
+ title: { type: "string", description: "Artifact display title." },
911
983
  fileType: {
912
984
  type: "string",
913
985
  description:
914
- "Optional MIME type; inferred from the extension when omitted.",
986
+ "card_attachment MIME type (inferred from the extension when omitted).",
915
987
  },
916
- sha256: {
988
+ contentType: {
917
989
  type: "string",
918
990
  description:
919
- "Optional hex SHA-256 of the uploaded bytes; verified against the stored object.",
991
+ "artifact MIME type; only 'text/html' is accepted (the default).",
920
992
  },
921
993
  size: {
922
994
  type: "number",
923
995
  description:
924
- "Optional byte size (advisory; re-validated server-side).",
996
+ "File size in bytes (rejected early if over the target's limit).",
925
997
  },
926
998
  },
927
- required: ["cardId", "storagePath", "fileName"],
928
- },
929
- },
930
- harmony_classify_card: {
931
- description:
932
- "Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the type label (feature/bug/idea). Call right after creating a card to classify it in-flow. Idempotent; never touches the user-owned `model_override`.",
933
- inputSchema: {
934
- type: "object",
935
- properties: {
936
- cardId: { type: "string", description: "Card UUID" },
937
- },
938
- required: ["cardId"],
999
+ required: ["target"],
939
1000
  },
940
1001
  },
941
- harmony_upload_artifact: {
1002
+ harmony_finalize_upload: {
942
1003
  description:
943
- "Host a self-contained HTML document (design draft, diagram) and link it to a card, plan, or workspace; rendered in-app in a sandboxed iframe. Provide exactly one of cardId/planId/workspaceId, and the HTML as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). text/html only, max 2MB. Returns the artifact + a signed URL; use harmony_share_artifact for a public link. Large files on the hosted MCP server: use harmony_request_artifact_upload_url + harmony_finalize_artifact instead.",
1004
+ "Step 2 of the upload handshake. After PUTting the bytes to the signed uploadUrl, call this " +
1005
+ "with the storagePath to validate and register. The server re-downloads the object and " +
1006
+ "enforces size + content-type (magic-byte sniff, never the declared type) + an optional " +
1007
+ 'sha256 integrity check, deleting and failing on any mismatch. `target: "card_attachment"` ' +
1008
+ '(cardId, storagePath, fileName) or `target: "artifact"` (storagePath + the same one of ' +
1009
+ "cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed URL.",
944
1010
  inputSchema: {
945
1011
  type: "object",
946
1012
  properties: {
947
- title: {
948
- type: "string",
949
- description: "Display title (defaults to the file basename).",
950
- },
951
- cardId: { type: "string", description: "Link to this card (UUID)." },
952
- planId: { type: "string", description: "Link to this plan (UUID)." },
953
- workspaceId: {
1013
+ target: {
954
1014
  type: "string",
1015
+ enum: ["card_attachment", "artifact"],
955
1016
  description:
956
- "Attach to this workspace as a standalone artifact (UUID).",
1017
+ "What to finalize: a card file attachment or a hosted HTML artifact.",
957
1018
  },
958
- filePath: {
959
- type: "string",
960
- description:
961
- "Absolute path to a local .html file the MCP server process can read. Mutually exclusive with base64Data.",
962
- },
963
- base64Data: {
1019
+ storagePath: {
964
1020
  type: "string",
965
1021
  description:
966
- "Base64-encoded HTML bytes (a `data:` URL prefix is accepted and stripped). Mutually exclusive with filePath.",
1022
+ "The storagePath returned by harmony_request_upload_url.",
967
1023
  },
968
- },
969
- required: [],
970
- },
971
- },
972
- harmony_request_artifact_upload_url: {
973
- description:
974
- "Step 1 of the large-file / hosted-MCP artifact upload handshake (use when the server can't read local disk). Mints a one-shot signed Storage upload URL. Provide exactly one of cardId/planId/workspaceId. Returns { uploadUrl, token, storagePath }. Then PUT the HTML bytes to uploadUrl and call harmony_finalize_artifact with storagePath. text/html only, max 2MB.",
975
- inputSchema: {
976
- type: "object",
977
- properties: {
978
- title: {
1024
+ cardId: {
979
1025
  type: "string",
980
1026
  description:
981
- "Display title (defaults to the file basename at finalize).",
1027
+ "Card UUID. Required for card_attachment; scope for artifact.",
982
1028
  },
983
- cardId: { type: "string", description: "Link to this card (UUID)." },
984
- planId: { type: "string", description: "Link to this plan (UUID)." },
1029
+ planId: { type: "string", description: "Plan UUID (artifact only)." },
985
1030
  workspaceId: {
986
1031
  type: "string",
987
- description:
988
- "Attach to this workspace as a standalone artifact (UUID).",
989
- },
990
- contentType: {
991
- type: "string",
992
- description: "MIME type; only 'text/html' is accepted (the default).",
993
- },
994
- size: {
995
- type: "number",
996
- description: "File size in bytes (rejected early if over 2MB).",
1032
+ description: "Workspace UUID (artifact only).",
997
1033
  },
998
- },
999
- required: [],
1000
- },
1001
- },
1002
- harmony_finalize_artifact: {
1003
- description:
1004
- "Step 2 of the artifact upload handshake. After PUTting the HTML bytes to the signed uploadUrl, call this with storagePath to validate and register the artifact. The server re-downloads the object and enforces size + text/html (magic-byte sniff) + an optional sha256 integrity check, deleting and failing on any mismatch. Pass the same one of cardId/planId/workspaceId used for the upload URL. Returns the artifact + a signed URL; use harmony_share_artifact for a public link.",
1005
- inputSchema: {
1006
- type: "object",
1007
- properties: {
1008
- storagePath: {
1034
+ fileName: {
1009
1035
  type: "string",
1010
1036
  description:
1011
- "The storagePath returned by harmony_request_artifact_upload_url.",
1037
+ "File name including extension (required for card_attachment).",
1012
1038
  },
1013
- title: { type: "string", description: "Display title." },
1014
- cardId: { type: "string", description: "Link to this card (UUID)." },
1015
- planId: { type: "string", description: "Link to this plan (UUID)." },
1016
- workspaceId: {
1039
+ title: { type: "string", description: "Artifact display title." },
1040
+ fileType: {
1017
1041
  type: "string",
1018
1042
  description:
1019
- "Attach to this workspace as a standalone artifact (UUID).",
1043
+ "card_attachment MIME type; inferred from the extension when omitted.",
1020
1044
  },
1021
1045
  sha256: {
1022
1046
  type: "string",
@@ -1029,7 +1053,18 @@ export const TOOLS = {
1029
1053
  "Optional byte size (advisory; re-validated server-side).",
1030
1054
  },
1031
1055
  },
1032
- required: ["storagePath"],
1056
+ required: ["target", "storagePath"],
1057
+ },
1058
+ },
1059
+ harmony_classify_card: {
1060
+ description:
1061
+ "Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the type label (feature/bug/idea). Call right after creating a card to classify it in-flow. Idempotent; never touches the user-owned `model_override`.",
1062
+ inputSchema: {
1063
+ type: "object",
1064
+ properties: {
1065
+ cardId: { type: "string", description: "Card UUID" },
1066
+ },
1067
+ required: ["cardId"],
1033
1068
  },
1034
1069
  },
1035
1070
  harmony_share_artifact: {
@@ -1062,6 +1097,28 @@ export const TOOLS = {
1062
1097
  required: ["cardId"],
1063
1098
  },
1064
1099
  },
1100
+ harmony_add_external_link: {
1101
+ description:
1102
+ "Attach an external reference URL (e.g. a PR/MR link, doc, or dashboard) to a card. Durable — survives description edits. Use this in addition to writing the PR link into the description.",
1103
+ inputSchema: {
1104
+ type: "object",
1105
+ properties: {
1106
+ cardId: {
1107
+ type: "string",
1108
+ description: "Card UUID",
1109
+ },
1110
+ url: {
1111
+ type: "string",
1112
+ description: "The URL to attach",
1113
+ },
1114
+ title: {
1115
+ type: "string",
1116
+ description: "Optional human-readable title for the link",
1117
+ },
1118
+ },
1119
+ required: ["cardId", "url"],
1120
+ },
1121
+ },
1065
1122
 
1066
1123
  // Subtask operations
1067
1124
  harmony_create_subtask: {
@@ -1119,7 +1176,7 @@ export const TOOLS = {
1119
1176
  // Comment operations
1120
1177
  harmony_add_comment: {
1121
1178
  description:
1122
- "Post a comment on a card as the agent — 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.",
1179
+ "Post a comment on a card as the agent — 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. To answer an open question, reply to it with replyToId.",
1123
1180
  inputSchema: {
1124
1181
  type: "object",
1125
1182
  properties: {
@@ -1147,6 +1204,11 @@ export const TOOLS = {
1147
1204
  type: "string",
1148
1205
  description: "Comment id this comment reaffirms",
1149
1206
  },
1207
+ replyToId: {
1208
+ type: "string",
1209
+ description:
1210
+ "Comment id this is a one-level reply to. To answer an open question, reply to it. If you also set supersedesId/confirmsId, it must equal replyToId.",
1211
+ },
1150
1212
  },
1151
1213
  required: ["cardId", "body"],
1152
1214
  },
@@ -2101,7 +2163,7 @@ export const TOOLS = {
2101
2163
 
2102
2164
  harmony_list_playbook: {
2103
2165
  description:
2104
- "List a workspace's playbooks (reusable process definitions). Returns each playbook's name, version, steps_version, and state. Read-only.",
2166
+ "List a workspace's playbooks (reusable process definitions). Returns each playbook's name, version, and state. Read-only.",
2105
2167
  inputSchema: {
2106
2168
  type: "object",
2107
2169
  properties: {
@@ -2126,24 +2188,9 @@ export const TOOLS = {
2126
2188
  },
2127
2189
  },
2128
2190
 
2129
- harmony_run_playbook: {
2130
- description:
2131
- "Run a playbook server-side and return the finalized run. Only legacy automation playbooks (steps_version 1) are runnable; stage playbooks (steps_version 2) are rejected. The server drives every step to completion.",
2132
- inputSchema: {
2133
- type: "object",
2134
- properties: {
2135
- playbookId: {
2136
- type: "string",
2137
- description: "Playbook ID to run (UUID)",
2138
- },
2139
- },
2140
- required: ["playbookId"],
2141
- },
2142
- },
2143
-
2144
2191
  harmony_create_playbook: {
2145
2192
  description:
2146
- "Create a new playbook in a workspace. Default steps_version 1 is a legacy automation macro (an array of tool steps); steps_version 2 is the Method stage model (an array of stage objects).",
2193
+ "Create a new playbook (a reusable process definition) in a workspace. A playbook is an ordered set of stages bound to board columns, run by people and the agent daemon as a card moves through them.",
2147
2194
  inputSchema: {
2148
2195
  type: "object",
2149
2196
  properties: {
@@ -2153,16 +2200,9 @@ export const TOOLS = {
2153
2200
  },
2154
2201
  name: { type: "string", description: "Playbook name" },
2155
2202
  description: { type: "string", description: "Playbook description" },
2156
- stepsVersion: {
2157
- type: "number",
2158
- enum: [1, 2],
2159
- description:
2160
- "1 = automation macro (tool steps), 2 = Method stage model (stage objects). Default 1.",
2161
- },
2162
2203
  steps: {
2163
2204
  type: "array",
2164
- description:
2165
- "Steps (steps_version 1: tool-step objects) or stages (steps_version 2: stage objects).",
2205
+ description: "The playbook's ordered stage objects.",
2166
2206
  items: { type: "object" },
2167
2207
  },
2168
2208
  },
@@ -2184,7 +2224,7 @@ export const TOOLS = {
2184
2224
  description: { type: "string", description: "New description" },
2185
2225
  steps: {
2186
2226
  type: "array",
2187
- description: "New steps (v1) or stages (v2) array.",
2227
+ description: "New ordered stage objects.",
2188
2228
  items: { type: "object" },
2189
2229
  },
2190
2230
  enabled: {
@@ -2201,22 +2241,6 @@ export const TOOLS = {
2201
2241
  },
2202
2242
  },
2203
2243
 
2204
- harmony_save_card_as_playbook: {
2205
- description:
2206
- "Save an existing card as a new steps_version 1 (automation) playbook, seeding one create-card step from the card. Returns the created playbook.",
2207
- inputSchema: {
2208
- type: "object",
2209
- properties: {
2210
- cardId: { type: "string", description: "Card ID to template (UUID)" },
2211
- name: {
2212
- type: "string",
2213
- description: "Name for the new playbook (defaults to the card title)",
2214
- },
2215
- },
2216
- required: ["cardId"],
2217
- },
2218
- },
2219
-
2220
2244
  // ============ ONBOARDING TOOLS ============
2221
2245
  harmony_signup: {
2222
2246
  description:
@@ -2807,30 +2831,41 @@ async function handleToolCall(
2807
2831
  return { success: true, ...result, count: result.cards.length };
2808
2832
  }
2809
2833
 
2834
+ // Unified card fetch (#605): cardId (full) | shortId (full) | shortIds (compact).
2835
+ // Old names dispatch here too, behind the deprecation shim.
2836
+ case "harmony_get_card_by_short_id":
2837
+ case "harmony_bulk_get_cards":
2810
2838
  case "harmony_get_card": {
2839
+ if (name !== "harmony_get_card") warnDeprecatedTool(name);
2840
+ const hasShortIds = args.shortIds != null;
2841
+ const hasShortId = args.shortId != null;
2842
+ const hasCardId = args.cardId != null;
2843
+ if ([hasShortIds, hasShortId, hasCardId].filter(Boolean).length !== 1) {
2844
+ throw new Error(
2845
+ "Provide exactly one of: cardId (UUID), shortId (number), or shortIds (number[]).",
2846
+ );
2847
+ }
2848
+ if (hasShortIds) {
2849
+ const shortIds = z
2850
+ .array(z.number().int().positive())
2851
+ .min(1)
2852
+ .max(100)
2853
+ .parse(args.shortIds);
2854
+ const projectId = getProjectId();
2855
+ const result = await client.bulkGetCards(projectId, shortIds);
2856
+ return { success: true, ...result };
2857
+ }
2858
+ if (hasShortId) {
2859
+ const shortId = z.number().int().positive().parse(args.shortId);
2860
+ const projectId = (args.projectId as string) || getProjectId();
2861
+ const result = await client.getCardByShortId(projectId, shortId);
2862
+ return { success: true, ...result };
2863
+ }
2811
2864
  const cardId = z.string().uuid().parse(args.cardId);
2812
2865
  const result = await client.getCard(cardId);
2813
2866
  return { success: true, ...result };
2814
2867
  }
2815
2868
 
2816
- case "harmony_get_card_by_short_id": {
2817
- const shortId = z.number().int().positive().parse(args.shortId);
2818
- const projectId = (args.projectId as string) || getProjectId();
2819
- const result = await client.getCardByShortId(projectId, shortId);
2820
- return { success: true, ...result };
2821
- }
2822
-
2823
- case "harmony_bulk_get_cards": {
2824
- const shortIds = z
2825
- .array(z.number().int().positive())
2826
- .min(1)
2827
- .max(100)
2828
- .parse(args.shortIds);
2829
- const projectId = getProjectId();
2830
- const result = await client.bulkGetCards(projectId, shortIds);
2831
- return { success: true, ...result };
2832
- }
2833
-
2834
2869
  case "harmony_bulk_archive_cards": {
2835
2870
  const shortIds = z
2836
2871
  .array(z.number().int().positive())
@@ -2978,71 +3013,85 @@ async function handleToolCall(
2978
3013
  return result;
2979
3014
  }
2980
3015
 
2981
- case "harmony_upload_card_attachment": {
2982
- const cardId = z.string().uuid().parse(args.cardId);
3016
+ // Unified upload (#605): target = card_attachment | artifact. Old names
3017
+ // (harmony_upload_artifact / harmony_upload_card_attachment) dispatch here
3018
+ // behind the deprecation shim; target is inferred from the old name.
3019
+ case "harmony_upload_card_attachment":
3020
+ case "harmony_upload_artifact":
3021
+ case "harmony_upload": {
3022
+ if (name !== "harmony_upload") warnDeprecatedTool(name);
3023
+ const target =
3024
+ name === "harmony_upload_card_attachment"
3025
+ ? "card_attachment"
3026
+ : name === "harmony_upload_artifact"
3027
+ ? "artifact"
3028
+ : z.enum(["card_attachment", "artifact"]).parse(args.target);
3029
+
2983
3030
  const filePath =
2984
3031
  args.filePath != null ? z.string().parse(args.filePath) : undefined;
2985
3032
  const base64Data =
2986
3033
  args.base64Data != null ? z.string().parse(args.base64Data) : undefined;
2987
- const fileName =
2988
- args.fileName != null ? z.string().parse(args.fileName) : undefined;
2989
- const contentType =
2990
- args.contentType != null
2991
- ? z.string().parse(args.contentType)
2992
- : undefined;
2993
-
2994
3034
  if (filePath && base64Data) {
2995
3035
  throw new Error("Provide either filePath or base64Data, not both.");
2996
3036
  }
2997
3037
 
2998
- if (filePath) {
2999
- // Server can read the file → upload direct-to-storage via the handshake
3000
- // (no base64 through the model context or the edge-fn JSON body).
3001
- const bytes = await readFileForUpload(
3002
- filePath,
3003
- MAX_ATTACHMENT_SIZE,
3004
- "attachment",
3005
- );
3006
- const resolvedName = fileName || basename(filePath);
3007
- const signed = await client.requestCardAttachmentUploadUrl(cardId, {
3008
- fileName: resolvedName,
3009
- fileType: contentType,
3010
- size: bytes.byteLength,
3011
- });
3012
- await putToSignedUrl(
3013
- signed.uploadUrl,
3014
- bytes,
3015
- contentType || signed.fileType || "application/octet-stream",
3016
- );
3017
- return await client.finalizeCardAttachment(cardId, {
3018
- storagePath: signed.storagePath,
3019
- fileName: resolvedName,
3020
- fileType: contentType || signed.fileType,
3021
- sha256: sha256Hex(bytes),
3022
- size: bytes.byteLength,
3023
- });
3024
- }
3025
-
3026
- if (base64Data) {
3027
- if (!fileName) {
3028
- throw new Error("fileName is required when using base64Data.");
3029
- }
3030
- if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
3031
- throw new Error(
3032
- `File is over the 5MB attachment limit. Use the harmony_request_card_attachment_upload_url + harmony_finalize_card_attachment handshake for large files.`,
3038
+ if (target === "card_attachment") {
3039
+ const cardId = z.string().uuid().parse(args.cardId);
3040
+ const fileName =
3041
+ args.fileName != null ? z.string().parse(args.fileName) : undefined;
3042
+ const contentType =
3043
+ args.contentType != null
3044
+ ? z.string().parse(args.contentType)
3045
+ : undefined;
3046
+
3047
+ if (filePath) {
3048
+ // Server can read the file → upload direct-to-storage via the
3049
+ // handshake (no base64 through the model context or edge-fn body).
3050
+ const bytes = await readFileForUpload(
3051
+ filePath,
3052
+ MAX_ATTACHMENT_SIZE,
3053
+ "attachment",
3054
+ );
3055
+ const resolvedName = fileName || basename(filePath);
3056
+ const signed = await client.requestCardAttachmentUploadUrl(cardId, {
3057
+ fileName: resolvedName,
3058
+ fileType: contentType,
3059
+ size: bytes.byteLength,
3060
+ });
3061
+ await putToSignedUrl(
3062
+ signed.uploadUrl,
3063
+ bytes,
3064
+ contentType || signed.fileType || "application/octet-stream",
3033
3065
  );
3066
+ return await client.finalizeCardAttachment(cardId, {
3067
+ storagePath: signed.storagePath,
3068
+ fileName: resolvedName,
3069
+ fileType: contentType || signed.fileType,
3070
+ sha256: sha256Hex(bytes),
3071
+ size: bytes.byteLength,
3072
+ });
3034
3073
  }
3035
- return await client.uploadCardAttachment(cardId, {
3036
- fileName,
3037
- data: base64Data,
3038
- fileType: contentType,
3039
- });
3040
- }
3041
3074
 
3042
- throw new Error("Provide either filePath or base64Data.");
3043
- }
3075
+ if (base64Data) {
3076
+ if (!fileName) {
3077
+ throw new Error("fileName is required when using base64Data.");
3078
+ }
3079
+ if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
3080
+ throw new Error(
3081
+ `File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`,
3082
+ );
3083
+ }
3084
+ return await client.uploadCardAttachment(cardId, {
3085
+ fileName,
3086
+ data: base64Data,
3087
+ fileType: contentType,
3088
+ });
3089
+ }
3090
+
3091
+ throw new Error("Provide either filePath or base64Data.");
3092
+ }
3044
3093
 
3045
- case "harmony_upload_artifact": {
3094
+ // target === "artifact"
3046
3095
  const title =
3047
3096
  args.title != null ? z.string().parse(args.title) : undefined;
3048
3097
  const cardId =
@@ -3055,17 +3104,7 @@ async function handleToolCall(
3055
3104
  : undefined;
3056
3105
  requireExactlyOneScope({ cardId, planId, workspaceId });
3057
3106
 
3058
- const filePath =
3059
- args.filePath != null ? z.string().parse(args.filePath) : undefined;
3060
- const base64Data =
3061
- args.base64Data != null ? z.string().parse(args.base64Data) : undefined;
3062
- if (filePath && base64Data) {
3063
- throw new Error("Provide either filePath or base64Data, not both.");
3064
- }
3065
-
3066
3107
  if (filePath) {
3067
- // Server can read the file → upload direct-to-storage via the handshake
3068
- // (no base64 through the model context or the edge-fn JSON body).
3069
3108
  const bytes = await readFileForUpload(
3070
3109
  filePath,
3071
3110
  MAX_ARTIFACT_SIZE,
@@ -3095,7 +3134,7 @@ async function handleToolCall(
3095
3134
  if (base64Data) {
3096
3135
  if (base64ByteLength(base64Data) > MAX_ARTIFACT_SIZE) {
3097
3136
  throw new Error(
3098
- `Artifact is over the 2MB limit. Use the harmony_request_artifact_upload_url + harmony_finalize_artifact handshake for large files.`,
3137
+ `Artifact is over the 2MB limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`,
3099
3138
  );
3100
3139
  }
3101
3140
  return await client.uploadArtifact({
@@ -3110,7 +3149,37 @@ async function handleToolCall(
3110
3149
  throw new Error("Provide either filePath or base64Data.");
3111
3150
  }
3112
3151
 
3113
- case "harmony_request_artifact_upload_url": {
3152
+ // Unified upload handshake step 1 (#605). Old names shimmed; target inferred.
3153
+ case "harmony_request_artifact_upload_url":
3154
+ case "harmony_request_card_attachment_upload_url":
3155
+ case "harmony_request_upload_url": {
3156
+ if (name !== "harmony_request_upload_url") warnDeprecatedTool(name);
3157
+ const target =
3158
+ name === "harmony_request_card_attachment_upload_url"
3159
+ ? "card_attachment"
3160
+ : name === "harmony_request_artifact_upload_url"
3161
+ ? "artifact"
3162
+ : z.enum(["card_attachment", "artifact"]).parse(args.target);
3163
+
3164
+ if (target === "card_attachment") {
3165
+ const cardId = z.string().uuid().parse(args.cardId);
3166
+ const fileName = z.string().parse(args.fileName);
3167
+ const fileType =
3168
+ args.fileType != null ? z.string().parse(args.fileType) : undefined;
3169
+ const size = z.number().positive().parse(args.size);
3170
+ if (size > MAX_ATTACHMENT_SIZE) {
3171
+ throw new Error(
3172
+ `Declared size ${size} bytes is over the ${MAX_ATTACHMENT_SIZE}-byte (5MB) attachment limit.`,
3173
+ );
3174
+ }
3175
+ return await client.requestCardAttachmentUploadUrl(cardId, {
3176
+ fileName,
3177
+ fileType,
3178
+ size,
3179
+ });
3180
+ }
3181
+
3182
+ // target === "artifact"
3114
3183
  const title =
3115
3184
  args.title != null ? z.string().parse(args.title) : undefined;
3116
3185
  const cardId =
@@ -3143,8 +3212,40 @@ async function handleToolCall(
3143
3212
  });
3144
3213
  }
3145
3214
 
3146
- case "harmony_finalize_artifact": {
3215
+ // Unified upload handshake step 2 (#605). Old names shimmed; target inferred.
3216
+ case "harmony_finalize_artifact":
3217
+ case "harmony_finalize_card_attachment":
3218
+ case "harmony_finalize_upload": {
3219
+ if (name !== "harmony_finalize_upload") warnDeprecatedTool(name);
3220
+ const target =
3221
+ name === "harmony_finalize_card_attachment"
3222
+ ? "card_attachment"
3223
+ : name === "harmony_finalize_artifact"
3224
+ ? "artifact"
3225
+ : z.enum(["card_attachment", "artifact"]).parse(args.target);
3147
3226
  const storagePath = z.string().parse(args.storagePath);
3227
+
3228
+ if (target === "card_attachment") {
3229
+ const cardId = z.string().uuid().parse(args.cardId);
3230
+ const fileName = z.string().parse(args.fileName);
3231
+ const fileType =
3232
+ args.fileType != null ? z.string().parse(args.fileType) : undefined;
3233
+ const sha256 =
3234
+ args.sha256 != null ? z.string().parse(args.sha256) : undefined;
3235
+ const size =
3236
+ args.size != null
3237
+ ? z.number().positive().parse(args.size)
3238
+ : undefined;
3239
+ return await client.finalizeCardAttachment(cardId, {
3240
+ storagePath,
3241
+ fileName,
3242
+ fileType,
3243
+ sha256,
3244
+ size,
3245
+ });
3246
+ }
3247
+
3248
+ // target === "artifact"
3148
3249
  const title =
3149
3250
  args.title != null ? z.string().parse(args.title) : undefined;
3150
3251
  const cardId =
@@ -3171,43 +3272,6 @@ async function handleToolCall(
3171
3272
  });
3172
3273
  }
3173
3274
 
3174
- case "harmony_request_card_attachment_upload_url": {
3175
- const cardId = z.string().uuid().parse(args.cardId);
3176
- const fileName = z.string().parse(args.fileName);
3177
- const fileType =
3178
- args.fileType != null ? z.string().parse(args.fileType) : undefined;
3179
- const size = z.number().positive().parse(args.size);
3180
- if (size > MAX_ATTACHMENT_SIZE) {
3181
- throw new Error(
3182
- `Declared size ${size} bytes is over the ${MAX_ATTACHMENT_SIZE}-byte (5MB) attachment limit.`,
3183
- );
3184
- }
3185
- return await client.requestCardAttachmentUploadUrl(cardId, {
3186
- fileName,
3187
- fileType,
3188
- size,
3189
- });
3190
- }
3191
-
3192
- case "harmony_finalize_card_attachment": {
3193
- const cardId = z.string().uuid().parse(args.cardId);
3194
- const storagePath = z.string().parse(args.storagePath);
3195
- const fileName = z.string().parse(args.fileName);
3196
- const fileType =
3197
- args.fileType != null ? z.string().parse(args.fileType) : undefined;
3198
- const sha256 =
3199
- args.sha256 != null ? z.string().parse(args.sha256) : undefined;
3200
- const size =
3201
- args.size != null ? z.number().positive().parse(args.size) : undefined;
3202
- return await client.finalizeCardAttachment(cardId, {
3203
- storagePath,
3204
- fileName,
3205
- fileType,
3206
- sha256,
3207
- size,
3208
- });
3209
- }
3210
-
3211
3275
  case "harmony_share_artifact": {
3212
3276
  const artifactId = z.string().uuid().parse(args.artifactId);
3213
3277
  const expiresInDays =
@@ -3230,6 +3294,16 @@ async function handleToolCall(
3230
3294
  return result;
3231
3295
  }
3232
3296
 
3297
+ case "harmony_add_external_link": {
3298
+ const cardId = z.string().uuid().parse(args.cardId);
3299
+ const url = z.string().min(1).max(2048).parse(args.url);
3300
+ const title = args.title
3301
+ ? z.string().max(200).parse(args.title)
3302
+ : undefined;
3303
+ const result = await client.addExternalLink(cardId, url, title);
3304
+ return { success: true, ...result };
3305
+ }
3306
+
3233
3307
  case "harmony_classify_card": {
3234
3308
  const cardId = z.string().uuid().parse(args.cardId);
3235
3309
  const result = await client.classifyCard(cardId);
@@ -3307,10 +3381,15 @@ async function handleToolCall(
3307
3381
  args.confirmsId !== undefined
3308
3382
  ? z.string().uuid().parse(args.confirmsId)
3309
3383
  : undefined;
3384
+ const replyToId =
3385
+ args.replyToId !== undefined
3386
+ ? z.string().uuid().parse(args.replyToId)
3387
+ : undefined;
3310
3388
  const result = await client.addComment(cardId, body, {
3311
3389
  commentType,
3312
3390
  supersedesId,
3313
3391
  confirmsId,
3392
+ replyToId,
3314
3393
  });
3315
3394
  return { success: true, ...result };
3316
3395
  }
@@ -3606,15 +3685,24 @@ async function handleToolCall(
3606
3685
  .map((a) => a.description)
3607
3686
  .filter((d): d is string => typeof d === "string" && d.length > 0);
3608
3687
 
3688
+ const reportedStatus = args.status as
3689
+ | "working"
3690
+ | "blocked"
3691
+ | "waiting"
3692
+ | "paused"
3693
+ | undefined;
3694
+
3695
+ // Keep the auto-session heartbeat in step with the agent's own status
3696
+ // reports: `paused`/`blocked`/`waiting` stop the 60s heartbeat (intentional
3697
+ // non-heartbeat states), a later `working` resumes it (card #608).
3698
+ if (reportedStatus) {
3699
+ noteSessionStatus(cardId, reportedStatus, deps.getScopeId?.());
3700
+ }
3701
+
3609
3702
  const result = await client.updateAgentProgress(cardId, {
3610
3703
  agentIdentifier,
3611
3704
  agentName,
3612
- status: args.status as
3613
- | "working"
3614
- | "blocked"
3615
- | "waiting"
3616
- | "paused"
3617
- | undefined,
3705
+ status: reportedStatus,
3618
3706
  progressPercent,
3619
3707
  currentTask: args.currentTask as string | undefined,
3620
3708
  blockers: args.blockers as string[] | undefined,
@@ -4717,25 +4805,19 @@ async function handleToolCall(
4717
4805
  return { success: true, playbook: result.playbook, runs: result.runs };
4718
4806
  }
4719
4807
 
4720
- case "harmony_run_playbook": {
4721
- const playbookId = z.string().uuid().parse(args.playbookId);
4722
- const result = await client.runPlaybook(playbookId);
4723
- return { success: true, run: result.run };
4724
- }
4808
+ // Deprecated (#612) — dropped from the advertised TOOLS list; the case
4809
+ // remains so an old caller gets a legible notice, not an unknown-tool error.
4810
+ case "harmony_run_playbook":
4811
+ return deprecatedRemovedToolResult("harmony_run_playbook");
4725
4812
 
4726
4813
  case "harmony_create_playbook": {
4727
4814
  const workspaceId = (args.workspaceId as string) || getWorkspaceId();
4728
4815
  const name = z.string().min(1).max(200).parse(args.name);
4729
- const stepsVersion =
4730
- args.stepsVersion !== undefined
4731
- ? z.union([z.literal(1), z.literal(2)]).parse(args.stepsVersion)
4732
- : undefined;
4733
4816
  const result = await client.createPlaybook({
4734
4817
  workspaceId,
4735
4818
  name,
4736
4819
  description: args.description as string | undefined,
4737
4820
  steps: args.steps,
4738
- stepsVersion,
4739
4821
  });
4740
4822
  return { success: true, playbook: result.playbook };
4741
4823
  }
@@ -4752,14 +4834,9 @@ async function handleToolCall(
4752
4834
  return { success: true, playbook: result.playbook };
4753
4835
  }
4754
4836
 
4755
- case "harmony_save_card_as_playbook": {
4756
- const cardId = z.string().uuid().parse(args.cardId);
4757
- const result = await client.savePlaybookFromCard({
4758
- cardId,
4759
- name: args.name as string | undefined,
4760
- });
4761
- return { success: true, playbook: result.playbook };
4762
- }
4837
+ // Deprecated (#612) — see harmony_run_playbook above.
4838
+ case "harmony_save_card_as_playbook":
4839
+ return deprecatedRemovedToolResult("harmony_save_card_as_playbook");
4763
4840
 
4764
4841
  // ============ ONBOARDING TOOLS ============
4765
4842
  case "harmony_signup": {