@codazen/harmonica-mcp 5.0.0 → 5.2.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.
Files changed (2) hide show
  1. package/dist/index.js +321 -137
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22032,22 +22032,33 @@ function registerAuditIntegrityTools(server, ctx, client) {
22032
22032
  );
22033
22033
  }
22034
22034
 
22035
- // ../../libs/harmonica-services/src/mcp/tools/bar-tools.ts
22035
+ // ../../libs/harmonica-services/src/tool-schemas/bar.ts
22036
22036
  var BAR_STATUSES = ["open", "closed"];
22037
22037
  var idSegmentSchema = external_exports.string().min(1).max(128).refine((id) => !id.includes("#"), {
22038
22038
  message: "must not contain '#' \u2014 it is a DynamoDB key delimiter"
22039
22039
  });
22040
+ var create_bar = {
22041
+ accountId: idSegmentSchema.describe("The Account ID this Bar belongs to"),
22042
+ barId: idSegmentSchema.describe(
22043
+ 'Caller-supplied Bar ID (must not contain "#")'
22044
+ ),
22045
+ startDate: external_exports.string().datetime().describe("ISO 8601 start of the planning period"),
22046
+ endDate: external_exports.string().datetime().describe("ISO 8601 end of the planning period; must be after startDate"),
22047
+ status: external_exports.enum(BAR_STATUSES).optional().describe('Defaults to "open"')
22048
+ };
22049
+ var get_bar = {
22050
+ barId: idSegmentSchema.describe("The Bar ID")
22051
+ };
22052
+ var list_account_bars = {
22053
+ accountId: idSegmentSchema.describe("The Account ID")
22054
+ };
22055
+
22056
+ // ../../libs/harmonica-services/src/mcp/tools/bar-tools.ts
22040
22057
  function registerBarTools(server, _ctx, client) {
22041
22058
  server.tool(
22042
22059
  "create_bar",
22043
22060
  "Create a Bar \u2014 the recurring planning-period time-box a client Account plans and delivers within (e.g. one or two weeks). Bars are Account-scoped and independent of any CadenceConfig already set on the Account; the caller supplies explicit start/end dates.",
22044
- {
22045
- accountId: idSegmentSchema.describe("The Account ID this Bar belongs to"),
22046
- barId: idSegmentSchema.describe('Caller-supplied Bar ID (must not contain "#")'),
22047
- startDate: external_exports.string().datetime().describe("ISO 8601 start of the planning period"),
22048
- endDate: external_exports.string().datetime().describe("ISO 8601 end of the planning period; must be after startDate"),
22049
- status: external_exports.enum(BAR_STATUSES).optional().describe('Defaults to "open"')
22050
- },
22061
+ create_bar,
22051
22062
  async ({ accountId, barId, startDate, endDate, status }) => {
22052
22063
  const startMs = new Date(startDate).getTime();
22053
22064
  const endMs = new Date(endDate).getTime();
@@ -22077,9 +22088,7 @@ function registerBarTools(server, _ctx, client) {
22077
22088
  server.tool(
22078
22089
  "get_bar",
22079
22090
  "Get a Bar by ID.",
22080
- {
22081
- barId: idSegmentSchema.describe("The Bar ID")
22082
- },
22091
+ get_bar,
22083
22092
  async ({ barId }) => {
22084
22093
  try {
22085
22094
  const bar = await client.getBar(barId);
@@ -22098,28 +22107,28 @@ function registerBarTools(server, _ctx, client) {
22098
22107
  }
22099
22108
  }
22100
22109
  );
22101
- server.tool(
22102
- "list_account_bars",
22103
- "List an Account's Bars, chronological by start date.",
22104
- {
22105
- accountId: idSegmentSchema.describe("The Account ID")
22106
- },
22107
- async ({ accountId }) => {
22108
- try {
22109
- const bars = await client.listAccountBars(accountId);
22110
- if (bars === void 0) {
22111
- return { content: [{ type: "text", text: `Account not found: ${accountId}` }], isError: true };
22112
- }
22113
- if (bars.length === 0) {
22114
- return { content: [{ type: "text", text: "No bars found." }] };
22110
+ for (const name of ["list_account_bars", "list_bars"]) {
22111
+ server.tool(
22112
+ name,
22113
+ "List an Account's Bars, chronological by start date.",
22114
+ list_account_bars,
22115
+ async ({ accountId }) => {
22116
+ try {
22117
+ const bars = await client.listAccountBars(accountId);
22118
+ if (bars === void 0) {
22119
+ return { content: [{ type: "text", text: `Account not found: ${accountId}` }], isError: true };
22120
+ }
22121
+ if (bars.length === 0) {
22122
+ return { content: [{ type: "text", text: "No bars found." }] };
22123
+ }
22124
+ const text = bars.map((b) => `${b.barId} ${b.startDate} \u2192 ${b.endDate} \u2014 ${b.status}`).join("\n");
22125
+ return { content: [{ type: "text", text }] };
22126
+ } catch (err) {
22127
+ return { content: [{ type: "text", text: `Failed to list bars: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
22115
22128
  }
22116
- const text = bars.map((b) => `${b.barId} ${b.startDate} \u2192 ${b.endDate} \u2014 ${b.status}`).join("\n");
22117
- return { content: [{ type: "text", text }] };
22118
- } catch (err) {
22119
- return { content: [{ type: "text", text: `Failed to list bars: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
22120
22129
  }
22121
- }
22122
- );
22130
+ );
22131
+ }
22123
22132
  }
22124
22133
 
22125
22134
  // ../../libs/harmonica-services/src/mcp/tools/baseline-tools.ts
@@ -22471,20 +22480,30 @@ var POLL_MAX_CONSECUTIVE_ERRORS = 3;
22471
22480
  async function pollForCheck(client, taskId) {
22472
22481
  const deadline = Date.now() + POLL_TIMEOUT_MS;
22473
22482
  let consecutiveErrors = 0;
22483
+ let consecutiveMissing = 0;
22474
22484
  while (Date.now() < deadline) {
22475
22485
  const [task, err] = await client.getTask(taskId).then(
22476
22486
  (t) => [t, null],
22477
22487
  (e) => [null, e]
22478
22488
  );
22479
22489
  if (err) {
22490
+ consecutiveMissing = 0;
22480
22491
  consecutiveErrors++;
22481
22492
  if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS)
22482
22493
  throw new Error(`getTask failed ${consecutiveErrors} consecutive times: ${err instanceof Error ? err.message : String(err)}`);
22483
22494
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
22484
22495
  continue;
22485
22496
  }
22497
+ if (!task) {
22498
+ consecutiveErrors = 0;
22499
+ consecutiveMissing++;
22500
+ if (consecutiveMissing >= POLL_MAX_CONSECUTIVE_ERRORS)
22501
+ throw new Error(`Task ${taskId} not found after ${consecutiveMissing} consecutive attempts`);
22502
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
22503
+ continue;
22504
+ }
22486
22505
  consecutiveErrors = 0;
22487
- if (!task) throw new Error(`Task ${taskId} not found`);
22506
+ consecutiveMissing = 0;
22488
22507
  if (task.status === "completed") {
22489
22508
  if (!task.result) throw new CheckResultPendingError(taskId);
22490
22509
  return task.result;
@@ -22792,6 +22811,18 @@ ${revision.description}`);
22792
22811
  }
22793
22812
  return sections.join("\n");
22794
22813
  }
22814
+ function formatRevisionPreview(revisionId, preview) {
22815
+ if (!preview) return `No preview recorded for revision: ${revisionId}`;
22816
+ const freshness = preview.isCurrent ? "Current \u2014 built from the head commit." : `Stale \u2014 built from ${preview.commitSha?.slice(0, 7) ?? "an unknown commit"}, which is no longer the head. Still opens.`;
22817
+ return [
22818
+ `# Preview: ${revisionId}`,
22819
+ `**URL:** ${preview.url}`,
22820
+ `**Built from:** ${preview.commitSha}`,
22821
+ `**Recorded:** ${preview.recordedAt}`,
22822
+ `
22823
+ ${freshness}`
22824
+ ].join("\n");
22825
+ }
22795
22826
 
22796
22827
  // ../../libs/harmonica-services/src/mcp/formatters/note-formatter.ts
22797
22828
  var TYPE_LABELS = {
@@ -23300,6 +23331,26 @@ ${formatDeliveryPolicy(
23300
23331
  }
23301
23332
  }
23302
23333
  );
23334
+ server.tool(
23335
+ "get_revision_preview",
23336
+ "Get the preview link recorded for a Revision \u2014 the running environment its own pipeline published, and whether that build is still the pull request's head. A Revision with no recorded preview is reported plainly, not as an error: one whose change artifact is a document never gets one. A stale link still opens; it was built before the latest commit.",
23337
+ {
23338
+ revisionId: external_exports.string().describe("The revision ID (e.g., rev-abc123)")
23339
+ },
23340
+ async ({ revisionId }) => {
23341
+ try {
23342
+ const revision = await client.getRevision(revisionId);
23343
+ if (!revision) {
23344
+ return { content: [{ type: "text", text: `Revision not found: ${revisionId}` }], isError: true };
23345
+ }
23346
+ const preview = await client.resolveRevisionPreview(revisionId);
23347
+ return { content: [{ type: "text", text: formatRevisionPreview(revisionId, preview) }] };
23348
+ } catch (err) {
23349
+ const message = err instanceof Error ? err.message : String(err);
23350
+ return { content: [{ type: "text", text: `Failed to get revision preview: ${message}` }], isError: true };
23351
+ }
23352
+ }
23353
+ );
23303
23354
  }
23304
23355
  async function logBeatActivity(client, params) {
23305
23356
  try {
@@ -24218,12 +24269,14 @@ var POLL_MAX_CONSECUTIVE_ERRORS2 = 3;
24218
24269
  async function pollForCheck2(client, taskId) {
24219
24270
  const deadline = Date.now() + POLL_TIMEOUT_MS2;
24220
24271
  let consecutiveErrors = 0;
24272
+ let consecutiveMissing = 0;
24221
24273
  while (Date.now() < deadline) {
24222
24274
  const [task, err] = await client.getTask(taskId).then(
24223
24275
  (t) => [t, null],
24224
24276
  (e) => [null, e]
24225
24277
  );
24226
24278
  if (err) {
24279
+ consecutiveMissing = 0;
24227
24280
  consecutiveErrors++;
24228
24281
  if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS2) {
24229
24282
  throw new Error(`getTask failed ${consecutiveErrors} times: ${err instanceof Error ? err.message : String(err)}`);
@@ -24231,8 +24284,16 @@ async function pollForCheck2(client, taskId) {
24231
24284
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
24232
24285
  continue;
24233
24286
  }
24287
+ if (!task) {
24288
+ consecutiveErrors = 0;
24289
+ consecutiveMissing++;
24290
+ if (consecutiveMissing >= POLL_MAX_CONSECUTIVE_ERRORS2)
24291
+ throw new Error(`Task ${taskId} not found after ${consecutiveMissing} consecutive attempts`);
24292
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
24293
+ continue;
24294
+ }
24234
24295
  consecutiveErrors = 0;
24235
- if (!task) throw new Error(`Task ${taskId} not found`);
24296
+ consecutiveMissing = 0;
24236
24297
  if (task.status === "completed") {
24237
24298
  if (!task.result) throw new CheckResultPendingError(taskId);
24238
24299
  return task.result;
@@ -25212,6 +25273,41 @@ var BeatDraftSuggestionSchema = external_exports.object({
25212
25273
  });
25213
25274
  var BeatDraftSuggestionsSchema = external_exports.array(BeatDraftSuggestionSchema);
25214
25275
 
25276
+ // ../../libs/harmonica-schemas/src/progress-evidence.ts
25277
+ var ProgressEvidenceQuerySchema = external_exports.object({
25278
+ start: external_exports.string().min(1).max(40),
25279
+ end: external_exports.string().min(1).max(40),
25280
+ timezone: external_exports.string().min(1).max(100),
25281
+ contributor: external_exports.string().trim().min(1).max(256).optional(),
25282
+ limit: external_exports.coerce.number().int().min(1).max(100).optional(),
25283
+ cursor: external_exports.string().min(1).max(4096).optional()
25284
+ }).strict();
25285
+ var ProgressCitationSchema = external_exports.object({ kind: external_exports.string(), id: external_exports.string(), url: external_exports.string() });
25286
+ var ProgressEvidenceItemSchema = external_exports.object({
25287
+ id: external_exports.string(),
25288
+ source: external_exports.enum(["activity", "session"]),
25289
+ occurredAt: external_exports.string(),
25290
+ recordedAt: external_exports.string(),
25291
+ summary: external_exports.string(),
25292
+ classification: external_exports.enum(["planned", "progressed", "delivered"]),
25293
+ attribution: external_exports.object({ basis: external_exports.enum(["recorded_actor", "session_initiator", "unknown"]), actorId: external_exports.string().nullable(), agentId: external_exports.string().optional() }),
25294
+ citations: external_exports.array(ProgressCitationSchema),
25295
+ context: external_exports.array(external_exports.object({ kind: external_exports.string(), id: external_exports.string(), title: external_exports.string().optional(), asOf: external_exports.literal("current") })),
25296
+ deliveryProof: external_exports.object({ kind: external_exports.literal("recorded_release"), occurredAt: external_exports.string(), citation: ProgressCitationSchema }).nullable()
25297
+ });
25298
+ var ProgressCoverageGapSchema = external_exports.object({ source: external_exports.string(), code: external_exports.string(), message: external_exports.string(), entityId: external_exports.string().optional() });
25299
+ var ProgressEvidenceResponseSchema = external_exports.object({
25300
+ systemId: external_exports.string(),
25301
+ accountId: external_exports.string().nullable(),
25302
+ window: external_exports.object({ start: external_exports.string(), end: external_exports.string(), timezone: external_exports.string() }),
25303
+ contributor: external_exports.string().nullable(),
25304
+ watermark: external_exports.string(),
25305
+ items: external_exports.array(ProgressEvidenceItemSchema),
25306
+ nextCursor: external_exports.string().optional(),
25307
+ coverage: external_exports.object({ status: external_exports.literal("partial"), gaps: external_exports.array(ProgressCoverageGapSchema) }),
25308
+ metrics: external_exports.object({ queryCalls: external_exports.number(), batchCalls: external_exports.number(), scannedCount: external_exports.number(), referenceCount: external_exports.number(), elapsedMs: external_exports.number() })
25309
+ });
25310
+
25215
25311
  // ../../libs/harmonica-schemas/src/api-responses.ts
25216
25312
  var HumanAssigneeSchema = external_exports.object({ email: external_exports.string(), name: external_exports.string() });
25217
25313
  var AgentAssigneeSchema = external_exports.object({ agentId: external_exports.string(), name: external_exports.string() });
@@ -25300,6 +25396,14 @@ var RevisionScmSchema = external_exports.object({
25300
25396
  state: external_exports.enum(["draft", "open", "merged", "closed"]),
25301
25397
  mergeSha: external_exports.string().optional()
25302
25398
  });
25399
+ var RevisionPreviewSchema = external_exports.object({
25400
+ url: external_exports.string(),
25401
+ commitSha: external_exports.string(),
25402
+ recordedAt: external_exports.string()
25403
+ });
25404
+ var RevisionPreviewApiSchema = RevisionPreviewSchema.extend({
25405
+ isCurrent: external_exports.boolean()
25406
+ });
25303
25407
  var RevisionApiSchema = external_exports.object({
25304
25408
  revisionId: external_exports.string(),
25305
25409
  beatId: external_exports.string(),
@@ -25913,7 +26017,11 @@ var HumanAssigneeApiSchema = external_exports.object({
25913
26017
  name: external_exports.string(),
25914
26018
  email: external_exports.string()
25915
26019
  });
26020
+ var ArchiveWorkItemRequestSchema = external_exports.object({ reason: external_exports.string().trim().min(1).max(1e3) });
25916
26021
  var WorkItemApiSchema = external_exports.object({
26022
+ archived: external_exports.boolean().optional(),
26023
+ archivedAt: external_exports.string().optional(),
26024
+ archivedReason: external_exports.string().optional(),
25917
26025
  workItemId: external_exports.string(),
25918
26026
  trackId: external_exports.string(),
25919
26027
  teamspaceId: external_exports.string(),
@@ -25947,6 +26055,12 @@ var WorkItemApiSchema = external_exports.object({
25947
26055
  * not, so a move on either side can separate the two.
25948
26056
  */
25949
26057
  barId: external_exports.string().min(1).optional(),
26058
+ /**
26059
+ * Optional per-Track fractional ordering rank (N-4E09-7665). Absent when
26060
+ * the item is unranked — an unranked item is a first-class state. The
26061
+ * Work Queue uses this to order items within their displayed group.
26062
+ */
26063
+ rank: external_exports.number().optional(),
25950
26064
  createdBy: external_exports.string().optional(),
25951
26065
  version: external_exports.number().optional(),
25952
26066
  createdAt: external_exports.string(),
@@ -27017,19 +27131,19 @@ function registerMovementTools(server, _ctx, client) {
27017
27131
  );
27018
27132
  server.tool(
27019
27133
  "create_movement",
27020
- 'Create a Movement in a Teamspace. A Movement is a Teamspace-scoped client-SOW container for an engagement \u2014 it is NOT a Beat. Recognition cues: "new SOW", "create a contract scope", "start an engagement container" \u2192 create_movement.',
27134
+ `Create a Movement for an Account, in one of that Account's Teamspaces. A Movement is a client-SOW container for an engagement \u2014 it is NOT a Beat. Recognition cues: "new SOW", "create a contract scope", "start an engagement container" \u2192 create_movement.`,
27021
27135
  {
27022
27136
  teamspaceId: external_exports.string().describe("The teamspace ID"),
27023
27137
  title: external_exports.string().min(1).max(300).describe('Movement title (e.g. "Q3 Platform Modernisation SOW")'),
27024
27138
  description: external_exports.string().max(2e3).optional().describe("Optional description"),
27025
- accountId: external_exports.string().min(1).max(128).optional().describe("Optional Account this Movement rolls up to. Must belong to the same org as the Teamspace.")
27139
+ accountId: external_exports.string().min(1).max(128).describe("The Account this Movement belongs to. Required (rev-950e828e). Must be in the same org as the Teamspace and linked to it.")
27026
27140
  },
27027
27141
  async ({ teamspaceId, title, description, accountId }) => {
27028
27142
  const m = await client.createMovement({
27029
27143
  teamspaceId,
27030
27144
  title,
27031
27145
  ...description !== void 0 && { description },
27032
- ...accountId !== void 0 && { accountId }
27146
+ accountId
27033
27147
  });
27034
27148
  return {
27035
27149
  content: [
@@ -27048,7 +27162,7 @@ function registerMovementTools(server, _ctx, client) {
27048
27162
  movementId: external_exports.string().describe("The movement ID"),
27049
27163
  title: external_exports.string().min(1).max(300).optional().describe("New title"),
27050
27164
  description: external_exports.string().max(2e3).optional().describe("New description"),
27051
- accountId: external_exports.string().max(128).nullable().optional().describe("Account this Movement rolls up to. Must belong to the same org. Pass null (or an empty string) to unlink.")
27165
+ accountId: external_exports.string().min(1).max(128).optional().describe("Reassign this Movement to a different Account. Must be in the same org and linked to the teamspace. Omit to leave unchanged \u2014 a Movement cannot be left without an Account (rev-950e828e).")
27052
27166
  },
27053
27167
  async ({ movementId, title, description, accountId }) => {
27054
27168
  const hasUpdates = title !== void 0 || description !== void 0 || accountId !== void 0;
@@ -27058,12 +27172,11 @@ function registerMovementTools(server, _ctx, client) {
27058
27172
  isError: true
27059
27173
  };
27060
27174
  }
27061
- const resolvedAccountId = accountId === "" ? null : accountId;
27062
27175
  try {
27063
27176
  const m = await client.updateMovement(movementId, {
27064
27177
  ...title !== void 0 && { title },
27065
27178
  ...description !== void 0 && { description },
27066
- ...accountId !== void 0 && { accountId: resolvedAccountId }
27179
+ ...accountId !== void 0 && { accountId }
27067
27180
  });
27068
27181
  if (!m) {
27069
27182
  return {
@@ -27071,7 +27184,7 @@ function registerMovementTools(server, _ctx, client) {
27071
27184
  isError: true
27072
27185
  };
27073
27186
  }
27074
- const account = accountId !== void 0 ? resolvedAccountId === null ? " \u2014 Account: (unlinked)" : ` \u2014 Account: ${resolvedAccountId}` : m.accountId ? ` \u2014 Account: ${m.accountId}` : "";
27187
+ const account = ` \u2014 Account: ${accountId ?? m.accountId}`;
27075
27188
  return {
27076
27189
  content: [
27077
27190
  {
@@ -28309,7 +28422,7 @@ ${formatTree(tree.root)}` }] };
28309
28422
  );
28310
28423
  server.tool(
28311
28424
  "update_notebook_collection",
28312
- "Reconfigure a placed live-collection section in place \u2014 change what it collects (note type, project scope, limit, order, significance tier) or its heading, without removing and re-adding it. Only the fields you pass change; the rest keep their current values. Identify the section by its elementId (from get_notebook_tree) and the page it sits on.",
28425
+ "Reconfigure a placed live-collection section in place \u2014 change what it collects (note type, system scope, limit, order, significance tier) or its heading, without removing and re-adding it. Only the fields you pass change; the rest keep their current values. Identify the section by its elementId (from get_notebook_tree) and the page it sits on.",
28313
28426
  {
28314
28427
  notebookId: external_exports.string().describe("The Notebook ID"),
28315
28428
  elementId: external_exports.string().describe("The collection section's elementId (from get_notebook_tree)"),
@@ -28450,9 +28563,9 @@ ${scopeText}` }] };
28450
28563
  );
28451
28564
  server.tool(
28452
28565
  "list_notebooks",
28453
- "List Notebooks reachable from a scope (org, teamspace, or system). Returns all notebooks bound to that scope.",
28566
+ "List Notebooks reachable from a scope (org, teamspace, system, or account). Returns all notebooks bound to that scope.",
28454
28567
  {
28455
- scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, or project"),
28568
+ scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, system, or account"),
28456
28569
  scopeId: external_exports.string().min(1).describe("ID of the scope entity"),
28457
28570
  includeArchived: external_exports.boolean().optional().describe("Include archived Notebooks in the results. Default: false (archived excluded).")
28458
28571
  },
@@ -28513,10 +28626,10 @@ ${formatNotebook(updated)}` }] };
28513
28626
  );
28514
28627
  server.tool(
28515
28628
  "add_notebook_shortcut",
28516
- "Add a curated, read-only shortcut from a scope (org, teamspace, project, or account) to a Notebook owned elsewhere in the SAME organization. Unlike set_notebook_scopes, a shortcut grants no rename/archive/delete rights over the Notebook in the consuming scope \u2014 it is a link, not ownership.",
28629
+ "Add a curated, read-only shortcut from a scope (org, teamspace, system, or account) to a Notebook owned elsewhere in the SAME organization. Unlike set_notebook_scopes, a shortcut grants no rename/archive/delete rights over the Notebook in the consuming scope \u2014 it is a link, not ownership.",
28517
28630
  {
28518
28631
  notebookId: external_exports.string().describe("The Notebook ID to shortcut"),
28519
- scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Consuming scope type \u2014 org, teamspace, project, or account"),
28632
+ scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Consuming scope type \u2014 org, teamspace, system, or account"),
28520
28633
  scopeId: external_exports.string().min(1).describe("ID of the consuming scope entity"),
28521
28634
  createdBy: external_exports.string().optional().describe("Actor ID \u2014 defaults to the session user")
28522
28635
  },
@@ -28545,7 +28658,7 @@ ${formatNotebook(updated)}` }] };
28545
28658
  "Remove a Notebook shortcut from a scope. Only removes the shortcut reference \u2014 never affects the source Notebook or its real scope bindings.",
28546
28659
  {
28547
28660
  notebookId: external_exports.string().describe("The Notebook ID"),
28548
- scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Consuming scope type \u2014 org, teamspace, project, or account"),
28661
+ scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Consuming scope type \u2014 org, teamspace, system, or account"),
28549
28662
  scopeId: external_exports.string().min(1).describe("ID of the consuming scope entity")
28550
28663
  },
28551
28664
  async ({ notebookId, scopeType, scopeId }) => {
@@ -28568,7 +28681,7 @@ ${formatNotebook(updated)}` }] };
28568
28681
  "list_notebook_shortcuts",
28569
28682
  "List a scope\u2019s curated Notebook shortcuts \u2014 Notebooks owned elsewhere in the org that this scope has chosen to surface, hydrated with each source Notebook\u2019s live title/status.",
28570
28683
  {
28571
- scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, project, or account"),
28684
+ scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, system, or account"),
28572
28685
  scopeId: external_exports.string().min(1).describe("ID of the scope entity")
28573
28686
  },
28574
28687
  async ({ scopeType, scopeId }) => {
@@ -28923,12 +29036,14 @@ var ROOT_CODA_POLL_MAX_CONSECUTIVE_ERRORS = 3;
28923
29036
  async function pollForRootCodaCheck(client, taskId) {
28924
29037
  const deadline = Date.now() + ROOT_CODA_POLL_TIMEOUT_MS;
28925
29038
  let consecutiveErrors = 0;
29039
+ let consecutiveMissing = 0;
28926
29040
  while (Date.now() < deadline) {
28927
29041
  const [task, err] = await client.getTask(taskId).then(
28928
29042
  (t) => [t, null],
28929
29043
  (e) => [null, e]
28930
29044
  );
28931
29045
  if (err) {
29046
+ consecutiveMissing = 0;
28932
29047
  consecutiveErrors++;
28933
29048
  if (consecutiveErrors >= ROOT_CODA_POLL_MAX_CONSECUTIVE_ERRORS) {
28934
29049
  throw new Error(`getTask failed ${consecutiveErrors} times: ${err instanceof Error ? err.message : String(err)}`);
@@ -28936,8 +29051,16 @@ async function pollForRootCodaCheck(client, taskId) {
28936
29051
  await new Promise((r) => setTimeout(r, ROOT_CODA_POLL_INTERVAL_MS));
28937
29052
  continue;
28938
29053
  }
29054
+ if (!task) {
29055
+ consecutiveErrors = 0;
29056
+ consecutiveMissing++;
29057
+ if (consecutiveMissing >= ROOT_CODA_POLL_MAX_CONSECUTIVE_ERRORS)
29058
+ throw new Error(`Task ${taskId} not found after ${consecutiveMissing} consecutive attempts`);
29059
+ await new Promise((r) => setTimeout(r, ROOT_CODA_POLL_INTERVAL_MS));
29060
+ continue;
29061
+ }
28939
29062
  consecutiveErrors = 0;
28940
- if (!task) throw new Error(`Task ${taskId} not found`);
29063
+ consecutiveMissing = 0;
28941
29064
  if (task.status === "completed") {
28942
29065
  if (!task.result) throw new CheckResultPendingError(taskId);
28943
29066
  const result = task.result;
@@ -29280,12 +29403,14 @@ var POLL_MAX_CONSECUTIVE_ERRORS4 = 3;
29280
29403
  async function pollForCheck4(client, taskId) {
29281
29404
  const deadline = Date.now() + POLL_TIMEOUT_MS4;
29282
29405
  let consecutiveErrors = 0;
29406
+ let consecutiveMissing = 0;
29283
29407
  while (Date.now() < deadline) {
29284
29408
  const [task, err] = await client.getTask(taskId).then(
29285
29409
  (t) => [t, null],
29286
29410
  (e) => [null, e]
29287
29411
  );
29288
29412
  if (err) {
29413
+ consecutiveMissing = 0;
29289
29414
  consecutiveErrors++;
29290
29415
  if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS4) {
29291
29416
  throw new Error(`getTask failed ${consecutiveErrors} times: ${err instanceof Error ? err.message : String(err)}`);
@@ -29293,8 +29418,16 @@ async function pollForCheck4(client, taskId) {
29293
29418
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS4));
29294
29419
  continue;
29295
29420
  }
29421
+ if (!task) {
29422
+ consecutiveErrors = 0;
29423
+ consecutiveMissing++;
29424
+ if (consecutiveMissing >= POLL_MAX_CONSECUTIVE_ERRORS4)
29425
+ throw new Error(`Task ${taskId} not found after ${consecutiveMissing} consecutive attempts`);
29426
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS4));
29427
+ continue;
29428
+ }
29296
29429
  consecutiveErrors = 0;
29297
- if (!task) throw new Error(`Task ${taskId} not found`);
29430
+ consecutiveMissing = 0;
29298
29431
  if (task.status === "completed") {
29299
29432
  if (!task.result) throw new CheckResultPendingError(taskId);
29300
29433
  return task.result;
@@ -29521,6 +29654,24 @@ function registerPortfolioCoherenceTools(server, ctx, client) {
29521
29654
  );
29522
29655
  }
29523
29656
 
29657
+ // ../../libs/harmonica-services/src/mcp/tools/progress-evidence-tools.ts
29658
+ function registerProgressEvidenceTools(server, _ctx, client) {
29659
+ server.tool(
29660
+ "get_progress_evidence",
29661
+ "Read cited progress evidence for a system and [start,end) reporting window. Calendar dates use the required IANA timezone; timestamps need an offset. Optional contributor matches recorded actors or explicitly labeled session initiators, never assignees. Returns planned/progressed/delivered evidence, source gaps, a watermark and continuation. Follow nextCursor even on empty pages. No writes.",
29662
+ { systemId: external_exports.string().min(1).max(128).regex(/^[^#]+$/), ...ProgressEvidenceQuerySchema.shape },
29663
+ async ({ systemId, ...query }) => {
29664
+ try {
29665
+ const report = await client.getProgressEvidence(systemId, query);
29666
+ if (!report) return { isError: true, content: [{ type: "text", text: "System not found." }] };
29667
+ return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
29668
+ } catch (error2) {
29669
+ return { isError: true, content: [{ type: "text", text: `Progress evidence query failed: ${error2 instanceof Error ? error2.message : String(error2)}` }] };
29670
+ }
29671
+ }
29672
+ );
29673
+ }
29674
+
29524
29675
  // ../../libs/harmonica-services/src/mcp/tools/pulse-report-tools.ts
29525
29676
  function formatBeatRow(r) {
29526
29677
  const downbeatState = r.hasDownbeat ? "" : " \u2014 no Downbeat ever set";
@@ -31235,6 +31386,30 @@ function registerTeamspaceTools(server, ctx, client) {
31235
31386
  );
31236
31387
  }
31237
31388
 
31389
+ // ../../libs/harmonica-services/src/tool-schemas/track.ts
31390
+ var get_track = {
31391
+ trackId: external_exports.string().describe("The Track ID")
31392
+ };
31393
+ var update_track = {
31394
+ trackId: external_exports.string().describe("The Track ID"),
31395
+ title: external_exports.string().min(1).max(300).optional().describe("New title"),
31396
+ description: external_exports.string().max(2e3).optional().describe("New description"),
31397
+ deliverables: external_exports.string().max(2e4).optional().describe(`Replace the Track's deliverables Markdown ("" clears)`),
31398
+ acceptanceCriteria: external_exports.string().max(2e4).optional().describe(`Replace the Track's acceptance-criteria Markdown ("" clears)`),
31399
+ vuRate: external_exports.number().nonnegative().optional().describe("New VU rate")
31400
+ };
31401
+ var transition_track = {
31402
+ trackId: external_exports.string().describe("The Track ID"),
31403
+ targetState: external_exports.enum(["proposed", "approved", "active", "fulfilled", "cancelled"]).describe("Target lifecycle state"),
31404
+ approvalNoteId: external_exports.string().optional().describe(
31405
+ 'Required when transitioning to "approved" \u2014 the Note ID capturing the approval decision'
31406
+ ),
31407
+ reason: external_exports.string().optional().describe("Reason for the transition (especially useful for cancellation)"),
31408
+ confirm: external_exports.boolean().optional().default(false).describe(
31409
+ "false (default) = dry-run preview; true = execute the transition"
31410
+ )
31411
+ };
31412
+
31238
31413
  // ../../libs/harmonica-services/src/mcp/tools/track-tools.ts
31239
31414
  function registerTrackTools(server, ctx, client) {
31240
31415
  server.tool(
@@ -31255,9 +31430,7 @@ function registerTrackTools(server, ctx, client) {
31255
31430
  server.tool(
31256
31431
  "get_track",
31257
31432
  "Get a Track by ID. Returns full details including commercial lifecycle status, VU rate, and audit stamps.",
31258
- {
31259
- trackId: external_exports.string().describe("The Track ID")
31260
- },
31433
+ get_track,
31261
31434
  async ({ trackId }) => {
31262
31435
  const t = await client.getTrack(trackId);
31263
31436
  if (!t) {
@@ -31331,14 +31504,7 @@ ${t.acceptanceCriteria}` : null,
31331
31504
  server.tool(
31332
31505
  "update_track",
31333
31506
  "Update mutable content fields on a Track (title, description, deliverables, acceptanceCriteria, vuRate). To change commercial status use transition_track instead.",
31334
- {
31335
- trackId: external_exports.string().describe("The Track ID"),
31336
- title: external_exports.string().min(1).max(300).optional().describe("New title"),
31337
- description: external_exports.string().max(2e3).optional().describe("New description"),
31338
- deliverables: external_exports.string().max(2e4).optional().describe(`Replace the Track's deliverables Markdown ("" clears)`),
31339
- acceptanceCriteria: external_exports.string().max(2e4).optional().describe(`Replace the Track's acceptance-criteria Markdown ("" clears)`),
31340
- vuRate: external_exports.number().nonnegative().optional().describe("New VU rate")
31341
- },
31507
+ update_track,
31342
31508
  async ({ trackId, title, description, deliverables, acceptanceCriteria, vuRate }) => {
31343
31509
  const t = await client.updateTrack(trackId, {
31344
31510
  ...title !== void 0 && { title },
@@ -31365,13 +31531,7 @@ ${t.acceptanceCriteria}` : null,
31365
31531
  server.tool(
31366
31532
  "transition_track",
31367
31533
  'Transition a Track through its commercial lifecycle: draft \u2192 proposed \u2192 approved \u2192 active \u2192 fulfilled. Cancelled is reachable from any non-terminal state. Advancing to "approved" requires an approvalNoteId. Use confirm=false (default) to preview the transition without writing; confirm=true to execute.',
31368
- {
31369
- trackId: external_exports.string().describe("The Track ID"),
31370
- targetState: external_exports.enum(["proposed", "approved", "active", "fulfilled", "cancelled"]).describe("Target lifecycle state"),
31371
- approvalNoteId: external_exports.string().optional().describe('Required when transitioning to "approved" \u2014 the Note ID capturing the approval decision'),
31372
- reason: external_exports.string().optional().describe("Reason for the transition (especially useful for cancellation)"),
31373
- confirm: external_exports.boolean().optional().default(false).describe("false (default) = dry-run preview; true = execute the transition")
31374
- },
31534
+ transition_track,
31375
31535
  async ({ trackId, targetState, approvalNoteId, reason, confirm }) => {
31376
31536
  if (!confirm) {
31377
31537
  return {
@@ -32002,6 +32162,24 @@ function registerWorkItemTools(server, _ctx, client) {
32002
32162
  );
32003
32163
  }
32004
32164
 
32165
+ // ../../libs/harmonica-services/src/tool-schemas/work-order.ts
32166
+ var WORK_ORDER_SIZE_ENUM = ["XS", "S", "M", "L", "XL"];
32167
+ var create_work_order = {
32168
+ workRequestId: external_exports.string().min(1).describe("The Work Request this order was split from \u2014 immutable lineage"),
32169
+ title: external_exports.string().min(1).max(300).describe("Work order title"),
32170
+ description: external_exports.string().max(1e4).optional().describe("Optional detail beyond the title"),
32171
+ size: external_exports.enum(WORK_ORDER_SIZE_ENUM).optional().describe(
32172
+ "Optional t-shirt size. An order can arrive before it is sized; a request's composition counts unsized orders rather than hiding them."
32173
+ ),
32174
+ createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
32175
+ };
32176
+ var get_work_order = {
32177
+ workOrderId: external_exports.string().describe("The Work Order ID (e.g., wo-abc123)")
32178
+ };
32179
+ var list_movement_work_orders = {
32180
+ movementId: external_exports.string().describe("The Movement (engagement) ID")
32181
+ };
32182
+
32005
32183
  // ../../libs/harmonica-services/src/mcp/tools/work-order-tools.ts
32006
32184
  var WORK_INTAKE_STATUS_ENUM = [
32007
32185
  "requested",
@@ -32014,7 +32192,7 @@ var NEEDS_DECISION_REASON_ENUM = [
32014
32192
  "exceeds_capacity",
32015
32193
  "incoherent_with_decision"
32016
32194
  ];
32017
- var WORK_ORDER_SIZE_ENUM = [
32195
+ var WORK_ORDER_SIZE_ENUM2 = [
32018
32196
  "XS",
32019
32197
  "S",
32020
32198
  "M",
@@ -32026,8 +32204,7 @@ function oneLine(value) {
32026
32204
  }
32027
32205
  function formatOrderLine(o) {
32028
32206
  const reason = o.needsDecisionReason ? ` [${o.needsDecisionReason}]` : "";
32029
- const track = o.trackId ? `on ${o.trackId}` : "unplaced";
32030
- return `[${o.workOrderId}] ${oneLine(o.title)} \u2014 ${o.status}${reason}, ${o.size ?? "unsized"}, ${track}`;
32207
+ return `[${o.workOrderId}] ${oneLine(o.title)} \u2014 ${o.status}${reason}, ${o.size ?? "unsized"}`;
32031
32208
  }
32032
32209
  function registerWorkOrderTools(server, _ctx, client) {
32033
32210
  server.tool(
@@ -32057,7 +32234,7 @@ function registerWorkOrderTools(server, _ctx, client) {
32057
32234
  );
32058
32235
  server.tool(
32059
32236
  "list_work_request_work_orders",
32060
- "List the Work Orders one Work Request split into \u2014 the lineage view. A four-way split shows four orders here, each possibly on a different Track and in a different state.",
32237
+ "List the Work Orders one Work Request split into \u2014 the lineage view. A four-way split shows four orders here, each with its own Items and independent state.",
32061
32238
  {
32062
32239
  workRequestId: external_exports.string().describe("The Work Request ID")
32063
32240
  },
@@ -32069,10 +32246,8 @@ function registerWorkOrderTools(server, _ctx, client) {
32069
32246
  );
32070
32247
  server.tool(
32071
32248
  "get_work_order",
32072
- "Get a Work Order by ID. Returns its title, status (with the routing reason when it is awaiting a decision), t-shirt size, the Track it sits on, and the Work Request it was split from.",
32073
- {
32074
- workOrderId: external_exports.string().describe("The Work Order ID (e.g., wo-abc123)")
32075
- },
32249
+ "Get a Work Order by ID. Returns its title, status (with the routing reason when it is awaiting a decision), t-shirt size, and the Work Request it was split from.",
32250
+ get_work_order,
32076
32251
  async ({ workOrderId }) => {
32077
32252
  const wo = await client.getWorkOrder(workOrderId);
32078
32253
  if (!wo) {
@@ -32082,7 +32257,6 @@ function registerWorkOrderTools(server, _ctx, client) {
32082
32257
  `ID: ${wo.workOrderId}`,
32083
32258
  `Title: ${oneLine(wo.title)}`,
32084
32259
  `From Work Request: ${wo.workRequestId}`,
32085
- `Track: ${wo.trackId}`,
32086
32260
  `Status: ${wo.status}`,
32087
32261
  wo.needsDecisionReason ? `Routing Reason: ${wo.needsDecisionReason}` : null,
32088
32262
  wo.size !== void 0 ? `Size: ${wo.size}` : "Size: unsized",
@@ -32094,41 +32268,37 @@ function registerWorkOrderTools(server, _ctx, client) {
32094
32268
  return { content: [{ type: "text", text: lines }] };
32095
32269
  }
32096
32270
  );
32097
- server.tool(
32098
- "create_work_order",
32099
- `Split a Work Request into an agreed Work Order. Called once per piece: a request covering two separate lines of work becomes two orders. Starts in "requested" status \u2014 moving it past the coherence gate is transition_work_order_status. The Order's scope is derived from the Work Request's Movement; no Track is required.`,
32100
- {
32101
- workRequestId: external_exports.string().min(1).describe("The Work Request this order was split from \u2014 immutable lineage"),
32102
- title: external_exports.string().min(1).max(300).describe("Work order title"),
32103
- description: external_exports.string().max(1e4).optional().describe("Optional detail beyond the title"),
32104
- size: external_exports.enum(WORK_ORDER_SIZE_ENUM).optional().describe("Optional t-shirt size. An order can arrive before it is sized; a request's composition counts unsized orders rather than hiding them."),
32105
- createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
32106
- },
32107
- async ({ workRequestId, title, description, size, createdBy }) => {
32108
- const wo = await client.createWorkOrder({
32109
- workRequestId,
32110
- title,
32111
- ...description !== void 0 && { description },
32112
- ...size !== void 0 && { size },
32113
- ...createdBy !== void 0 && { createdBy }
32114
- });
32115
- return {
32116
- content: [{ type: "text", text: `Created work order: ${formatOrderLine(wo)}` }]
32117
- };
32118
- }
32119
- );
32120
- server.tool(
32121
- "list_movement_work_orders",
32122
- 'List the Work Orders belonging to a Movement (engagement). A Work Order is the internal unit of agreed work \u2014 it is split from a Work Request and scoped to the Movement that owns the Request. Recognition cues: "what is committed on this engagement", "list the orders" \u2192 list_movement_work_orders.',
32123
- {
32124
- movementId: external_exports.string().describe("The Movement (engagement) ID")
32125
- },
32126
- async ({ movementId }) => {
32127
- const orders = await client.listMovementWorkOrders(movementId);
32128
- const text = orders.length === 0 ? "No work orders found." : orders.map(formatOrderLine).join("\n");
32129
- return { content: [{ type: "text", text }] };
32130
- }
32131
- );
32271
+ for (const name of ["create_work_order", "promote_work_request"]) {
32272
+ server.tool(
32273
+ name,
32274
+ `Create (promote) a Work Order from a Work Request. Each call creates another Order; it does not accept the Order or change Request status. Called once per piece: a request covering two separate lines of work becomes two orders. Starts in "requested" status \u2014 moving it past the coherence gate is transition_work_order_status. The Order's scope is derived from the Work Request's Movement; Tracks are assigned only to Work Items.`,
32275
+ create_work_order,
32276
+ async ({ workRequestId, title, description, size, createdBy }) => {
32277
+ const wo = await client.createWorkOrder({
32278
+ workRequestId,
32279
+ title,
32280
+ ...description !== void 0 && { description },
32281
+ ...size !== void 0 && { size },
32282
+ ...createdBy !== void 0 && { createdBy }
32283
+ });
32284
+ return {
32285
+ content: [{ type: "text", text: `Created work order: ${formatOrderLine(wo)}` }]
32286
+ };
32287
+ }
32288
+ );
32289
+ }
32290
+ for (const name of ["list_movement_work_orders", "list_work_orders"]) {
32291
+ server.tool(
32292
+ name,
32293
+ 'List the Work Orders belonging to a Movement (engagement). A Work Order is the internal unit of agreed work \u2014 it is split from a Work Request and scoped to the Movement that owns the Request. Recognition cues: "what is committed on this engagement", "list the orders" \u2192 list_movement_work_orders.',
32294
+ list_movement_work_orders,
32295
+ async ({ movementId }) => {
32296
+ const orders = await client.listMovementWorkOrders(movementId);
32297
+ const text = orders.length === 0 ? "No work orders found." : orders.map(formatOrderLine).join("\n");
32298
+ return { content: [{ type: "text", text }] };
32299
+ }
32300
+ );
32301
+ }
32132
32302
  server.tool(
32133
32303
  "update_work_order",
32134
32304
  "Update content fields on a Work Order (title, description, size). To change its status use transition_work_order_status. There is no moving an Order between Tracks \u2014 an Order belongs to its Work Request, and a Track is attribution carried by the Work Items (set workOrderId on them via update_work_item). The Work Request it came from cannot be changed \u2014 that lineage is immutable.",
@@ -32136,7 +32306,7 @@ function registerWorkOrderTools(server, _ctx, client) {
32136
32306
  workOrderId: external_exports.string().describe("The Work Order ID"),
32137
32307
  title: external_exports.string().min(1).max(300).optional().describe("New title"),
32138
32308
  description: external_exports.string().max(1e4).nullable().optional().describe("New description. Pass null to clear; omit to leave untouched."),
32139
- size: external_exports.enum(WORK_ORDER_SIZE_ENUM).nullable().optional().describe("New t-shirt size. Pass null to return it to unsized; omit to leave untouched.")
32309
+ size: external_exports.enum(WORK_ORDER_SIZE_ENUM2).nullable().optional().describe("New t-shirt size. Pass null to return it to unsized; omit to leave untouched.")
32140
32310
  },
32141
32311
  async ({ workOrderId, title, description, size }) => {
32142
32312
  if (title === void 0 && description === void 0 && size === void 0) {
@@ -32225,11 +32395,27 @@ function registerWorkOrderTools(server, _ctx, client) {
32225
32395
  );
32226
32396
  }
32227
32397
 
32398
+ // ../../libs/harmonica-services/src/tool-schemas/work-request.ts
32399
+ var WORK_REQUEST_ORIGIN_ENUM = ["client", "internal"];
32400
+ var create_work_request = {
32401
+ movementId: external_exports.string().describe("The Movement (engagement) this request is filed against"),
32402
+ title: external_exports.string().min(1).max(300).describe("Short title for the ask"),
32403
+ body: external_exports.string().max(1e4).optional().describe("The ask as written, in the requester's own words"),
32404
+ requesterName: external_exports.string().min(1).describe("Who is asking \u2014 display name"),
32405
+ requesterEmail: external_exports.string().email().describe("Who is asking \u2014 email"),
32406
+ origin: external_exports.enum(WORK_REQUEST_ORIGIN_ENUM).describe(
32407
+ 'Where the ask came from: "client" or "internal". One entity covers both \u2014 only the entry point differs.'
32408
+ ),
32409
+ createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
32410
+ };
32411
+ var get_work_request = {
32412
+ workRequestId: external_exports.string().describe("The Work Request ID (e.g., wr-abc123)")
32413
+ };
32414
+ var list_movement_work_requests = {
32415
+ movementId: external_exports.string().describe("The Movement (engagement) ID")
32416
+ };
32417
+
32228
32418
  // ../../libs/harmonica-services/src/mcp/tools/work-request-tools.ts
32229
- var WORK_REQUEST_ORIGIN_ENUM = [
32230
- "client",
32231
- "internal"
32232
- ];
32233
32419
  function oneLine2(value) {
32234
32420
  return value.replace(/[\r\n]+/g, " ");
32235
32421
  }
@@ -32243,9 +32429,7 @@ function registerWorkRequestTools(server, _ctx, client) {
32243
32429
  server.tool(
32244
32430
  "list_movement_work_requests",
32245
32431
  `List the Work Requests filed against a Movement. A Work Request is the customer's ask in their own language, before it is sized or agreed. Recognition cues: "what has the client asked for", "list the requests on this engagement" \u2192 list_movement_work_requests.`,
32246
- {
32247
- movementId: external_exports.string().describe("The Movement (engagement) ID")
32248
- },
32432
+ list_movement_work_requests,
32249
32433
  async ({ movementId }) => {
32250
32434
  const requests = await client.listMovementWorkRequests(movementId);
32251
32435
  const text = requests.length === 0 ? "No work requests found." : requests.map((wr) => `[${wr.workRequestId}] ${oneLine2(wr.title)} \u2014 from ${oneLine2(wr.requester.name)} (${wr.origin})`).join("\n");
@@ -32255,9 +32439,7 @@ function registerWorkRequestTools(server, _ctx, client) {
32255
32439
  server.tool(
32256
32440
  "get_work_request",
32257
32441
  "Get a Work Request with the Work Orders it split into and their combined size composition. Note there is no request-level status: each Order carries its own, because a split whose halves sit in different states cannot be reduced to one word.",
32258
- {
32259
- workRequestId: external_exports.string().describe("The Work Request ID (e.g., wr-abc123)")
32260
- },
32442
+ get_work_request,
32261
32443
  async ({ workRequestId }) => {
32262
32444
  const detail = await client.getWorkRequestDetail(workRequestId);
32263
32445
  if (!detail) {
@@ -32267,10 +32449,9 @@ function registerWorkRequestTools(server, _ctx, client) {
32267
32449
  const orderLines = orders.length === 0 ? ["Orders: none yet \u2014 this request has not been split"] : [
32268
32450
  "Orders:",
32269
32451
  ...orders.map((o) => {
32270
- const track = o.trackTitle ? `${oneLine2(o.trackTitle)} (${o.trackId})` : o.trackId;
32271
32452
  const reason = o.needsDecisionReason ? ` [${o.needsDecisionReason}]` : "";
32272
32453
  const sized = o.size ?? "unsized";
32273
- return ` [${o.workOrderId}] ${oneLine2(o.title)} \u2014 ${o.status}${reason}, ${sized}, on ${track}`;
32454
+ return ` [${o.workOrderId}] ${oneLine2(o.title)} \u2014 ${o.status}${reason}, ${sized}`;
32274
32455
  })
32275
32456
  ];
32276
32457
  const lines = [
@@ -32292,15 +32473,7 @@ function registerWorkRequestTools(server, _ctx, client) {
32292
32473
  server.tool(
32293
32474
  "create_work_request",
32294
32475
  "File a Work Request against a Movement \u2014 how work arrives, in the requester's own language, before it is sized or agreed. Splitting it into Work Orders is a separate step (create_work_order).",
32295
- {
32296
- movementId: external_exports.string().describe("The Movement (engagement) this request is filed against"),
32297
- title: external_exports.string().min(1).max(300).describe("Short title for the ask"),
32298
- body: external_exports.string().max(1e4).optional().describe("The ask as written, in the requester's own words"),
32299
- requesterName: external_exports.string().min(1).describe("Who is asking \u2014 display name"),
32300
- requesterEmail: external_exports.string().email().describe("Who is asking \u2014 email"),
32301
- origin: external_exports.enum(WORK_REQUEST_ORIGIN_ENUM).describe('Where the ask came from: "client" or "internal". One entity covers both \u2014 only the entry point differs.'),
32302
- createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
32303
- },
32476
+ create_work_request,
32304
32477
  async ({ movementId, title, body, requesterName, requesterEmail, origin, createdBy }) => {
32305
32478
  const wr = await client.createWorkRequest({
32306
32479
  movementId,
@@ -32858,6 +33031,7 @@ function registerAllTools(server, ctx, client, profile) {
32858
33031
  registerNextActionsTools(server, ctx, client);
32859
33032
  registerCheckTools(server, ctx, client);
32860
33033
  registerActivityTools(server, ctx, client);
33034
+ registerProgressEvidenceTools(server, ctx, client);
32861
33035
  registerAuditIntegrityTools(server, ctx, client);
32862
33036
  registerAnalysisTools(server, ctx, client);
32863
33037
  registerOrganizationTools(server, ctx, client);
@@ -33570,6 +33744,8 @@ function createHttpClient(config2) {
33570
33744
  return { revision: result.revision, warnings: result.warnings ?? [] };
33571
33745
  },
33572
33746
  getRevision: (revisionId) => request("GET", `/api/revisions/${encodeURIComponent(revisionId)}`),
33747
+ // `request` maps 404 to undefined, which is what absence must be here.
33748
+ resolveRevisionPreview: (revisionId) => request("GET", `/api/revisions/${encodeURIComponent(revisionId)}/preview`),
33573
33749
  listConceptRevisions: async (systemId, options) => {
33574
33750
  const qs = options?.includeArchived ? "?includeArchived=true" : "";
33575
33751
  const result = await request("GET", `/api/systems/${encodeURIComponent(systemId)}/revisions/pending${qs}`);
@@ -34523,10 +34699,13 @@ function createHttpClient(config2) {
34523
34699
  const { teamspaceId, title, description, accountId } = input;
34524
34700
  const body = {
34525
34701
  title,
34526
- ...description !== void 0 && { description },
34527
- ...accountId !== void 0 && { accountId }
34702
+ ...description !== void 0 && { description }
34528
34703
  };
34529
- const result = await request("POST", `/api/teamspaces/${encodeURIComponent(teamspaceId)}/movements`, body);
34704
+ const result = await request(
34705
+ "POST",
34706
+ `/api/teamspaces/${encodeURIComponent(teamspaceId)}/accounts/${encodeURIComponent(accountId)}/movements`,
34707
+ body
34708
+ );
34530
34709
  if (result === void 0) throw new Error("Resource not found when creating Movement");
34531
34710
  const parsed = MovementApiSchema.safeParse(result);
34532
34711
  if (!parsed.success) throw new Error(`Unexpected Movement shape from API: ${parsed.error.message}`);
@@ -34936,6 +35115,11 @@ function createHttpClient(config2) {
34936
35115
  const qs = asOf ? `?asOf=${encodeURIComponent(asOf)}` : "";
34937
35116
  return request("GET", `/api/accounts/${encodeURIComponent(accountId)}/cadence-schedule${qs}`);
34938
35117
  },
35118
+ getProgressEvidence: async (systemId, query) => {
35119
+ const qs = new URLSearchParams();
35120
+ for (const [key, value] of Object.entries(query)) if (value !== void 0) qs.set(key, String(value));
35121
+ return request("GET", `/api/systems/${encodeURIComponent(systemId)}/progress-evidence?${qs}`);
35122
+ },
34939
35123
  getPulseReport: async (accountId, asOf) => {
34940
35124
  const qs = asOf ? `?asOf=${encodeURIComponent(asOf)}` : "";
34941
35125
  return request("GET", `/api/accounts/${encodeURIComponent(accountId)}/pulse-report${qs}`);
@@ -35369,7 +35553,7 @@ function loadConfig() {
35369
35553
  };
35370
35554
  }
35371
35555
  async function main() {
35372
- console.error(`[harmonica-mcp] v${"5.0.0"} starting\u2026`);
35556
+ console.error(`[harmonica-mcp] v${"5.2.0"} starting\u2026`);
35373
35557
  const config2 = loadConfig();
35374
35558
  const client = createHttpClient({
35375
35559
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "description": "MCP server for Harmonica — connect any MCP-compatible AI assistant to Harmonica",
5
5
  "license": "MIT",
6
6
  "bin": {