@codazen/harmonica-mcp 5.1.0 → 5.3.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 +363 -71
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -21287,6 +21287,23 @@ watchPullRequest:
21287
21287
 
21288
21288
  Re-read the policy before each step rather than caching it, so a human revoking
21289
21289
  a toggle mid-flight takes effect on your next action.`;
21290
+ var WORK_ITEM_LIFECYCLE_INSTRUCTIONS = `WORK ITEM LIFECYCLE (CHANNEL-INDEPENDENT)
21291
+
21292
+ When you perform substantive work for a known Work Item, keep that Work Item's
21293
+ status accurate even when the work happens outside a Harmonica Session.
21294
+
21295
+ - Before starting, get the Work Item and transition it from \`not_started\` to
21296
+ \`in_progress\` with \`transition_work_item_status\`. Resume a \`blocked\` item
21297
+ only when its blocker has cleared. Do not reopen a \`done\` item unless new
21298
+ work genuinely belongs to that same item.
21299
+ - When its requested outcome is actually complete, transition it to \`done\`.
21300
+ If work cannot proceed, transition it to \`blocked\`. A conversation, task,
21301
+ or Session ending is not evidence that every associated Work Item is done.
21302
+ - Manage multiple Work Items independently. Leave untouched items unchanged;
21303
+ completing or blocking one must not change the others. A Session association
21304
+ is useful lineage, but it is never required in order to update status.
21305
+ - If the status tool is unavailable or fails, say plainly that the Work Item
21306
+ status was not updated rather than implying that it was.`;
21290
21307
  async function createMcpServer(ctx, client, registerTools, registerResources) {
21291
21308
  let guidelines;
21292
21309
  try {
@@ -21294,7 +21311,11 @@ async function createMcpServer(ctx, client, registerTools, registerResources) {
21294
21311
  } catch (err) {
21295
21312
  console.warn("[MCP] Failed to load beat guidelines for server instructions:", err);
21296
21313
  }
21297
- const instructions = [guidelines, DELIVERY_LATITUDE_INSTRUCTIONS].filter(Boolean).join("\n\n");
21314
+ const instructions = [
21315
+ guidelines,
21316
+ DELIVERY_LATITUDE_INSTRUCTIONS,
21317
+ WORK_ITEM_LIFECYCLE_INSTRUCTIONS
21318
+ ].filter(Boolean).join("\n\n");
21298
21319
  const server = new McpServer({
21299
21320
  name: "harmonica",
21300
21321
  version: "0.1.0"
@@ -22429,9 +22450,9 @@ var CHECK_MAX_SCORES = {
22429
22450
  var BeatQualityGateError = class extends Error {
22430
22451
  code = "beat_quality_gate_failed";
22431
22452
  failure;
22432
- constructor(failure) {
22433
- super(failure.message);
22434
- this.failure = failure;
22453
+ constructor(failure2) {
22454
+ super(failure2.message);
22455
+ this.failure = failure2;
22435
22456
  }
22436
22457
  };
22437
22458
  var PlanVersionsIdempotencyError = class extends Error {
@@ -22480,20 +22501,30 @@ var POLL_MAX_CONSECUTIVE_ERRORS = 3;
22480
22501
  async function pollForCheck(client, taskId) {
22481
22502
  const deadline = Date.now() + POLL_TIMEOUT_MS;
22482
22503
  let consecutiveErrors = 0;
22504
+ let consecutiveMissing = 0;
22483
22505
  while (Date.now() < deadline) {
22484
22506
  const [task, err] = await client.getTask(taskId).then(
22485
22507
  (t) => [t, null],
22486
22508
  (e) => [null, e]
22487
22509
  );
22488
22510
  if (err) {
22511
+ consecutiveMissing = 0;
22489
22512
  consecutiveErrors++;
22490
22513
  if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS)
22491
22514
  throw new Error(`getTask failed ${consecutiveErrors} consecutive times: ${err instanceof Error ? err.message : String(err)}`);
22492
22515
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
22493
22516
  continue;
22494
22517
  }
22518
+ if (!task) {
22519
+ consecutiveErrors = 0;
22520
+ consecutiveMissing++;
22521
+ if (consecutiveMissing >= POLL_MAX_CONSECUTIVE_ERRORS)
22522
+ throw new Error(`Task ${taskId} not found after ${consecutiveMissing} consecutive attempts`);
22523
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
22524
+ continue;
22525
+ }
22495
22526
  consecutiveErrors = 0;
22496
- if (!task) throw new Error(`Task ${taskId} not found`);
22527
+ consecutiveMissing = 0;
22497
22528
  if (task.status === "completed") {
22498
22529
  if (!task.result) throw new CheckResultPendingError(taskId);
22499
22530
  return task.result;
@@ -22801,6 +22832,18 @@ ${revision.description}`);
22801
22832
  }
22802
22833
  return sections.join("\n");
22803
22834
  }
22835
+ function formatRevisionPreview(revisionId, preview) {
22836
+ if (!preview) return `No preview recorded for revision: ${revisionId}`;
22837
+ 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.`;
22838
+ return [
22839
+ `# Preview: ${revisionId}`,
22840
+ `**URL:** ${preview.url}`,
22841
+ `**Built from:** ${preview.commitSha}`,
22842
+ `**Recorded:** ${preview.recordedAt}`,
22843
+ `
22844
+ ${freshness}`
22845
+ ].join("\n");
22846
+ }
22804
22847
 
22805
22848
  // ../../libs/harmonica-services/src/mcp/formatters/note-formatter.ts
22806
22849
  var TYPE_LABELS = {
@@ -23309,6 +23352,26 @@ ${formatDeliveryPolicy(
23309
23352
  }
23310
23353
  }
23311
23354
  );
23355
+ server.tool(
23356
+ "get_revision_preview",
23357
+ "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.",
23358
+ {
23359
+ revisionId: external_exports.string().describe("The revision ID (e.g., rev-abc123)")
23360
+ },
23361
+ async ({ revisionId }) => {
23362
+ try {
23363
+ const revision = await client.getRevision(revisionId);
23364
+ if (!revision) {
23365
+ return { content: [{ type: "text", text: `Revision not found: ${revisionId}` }], isError: true };
23366
+ }
23367
+ const preview = await client.resolveRevisionPreview(revisionId);
23368
+ return { content: [{ type: "text", text: formatRevisionPreview(revisionId, preview) }] };
23369
+ } catch (err) {
23370
+ const message = err instanceof Error ? err.message : String(err);
23371
+ return { content: [{ type: "text", text: `Failed to get revision preview: ${message}` }], isError: true };
23372
+ }
23373
+ }
23374
+ );
23312
23375
  }
23313
23376
  async function logBeatActivity(client, params) {
23314
23377
  try {
@@ -24227,12 +24290,14 @@ var POLL_MAX_CONSECUTIVE_ERRORS2 = 3;
24227
24290
  async function pollForCheck2(client, taskId) {
24228
24291
  const deadline = Date.now() + POLL_TIMEOUT_MS2;
24229
24292
  let consecutiveErrors = 0;
24293
+ let consecutiveMissing = 0;
24230
24294
  while (Date.now() < deadline) {
24231
24295
  const [task, err] = await client.getTask(taskId).then(
24232
24296
  (t) => [t, null],
24233
24297
  (e) => [null, e]
24234
24298
  );
24235
24299
  if (err) {
24300
+ consecutiveMissing = 0;
24236
24301
  consecutiveErrors++;
24237
24302
  if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS2) {
24238
24303
  throw new Error(`getTask failed ${consecutiveErrors} times: ${err instanceof Error ? err.message : String(err)}`);
@@ -24240,8 +24305,16 @@ async function pollForCheck2(client, taskId) {
24240
24305
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
24241
24306
  continue;
24242
24307
  }
24308
+ if (!task) {
24309
+ consecutiveErrors = 0;
24310
+ consecutiveMissing++;
24311
+ if (consecutiveMissing >= POLL_MAX_CONSECUTIVE_ERRORS2)
24312
+ throw new Error(`Task ${taskId} not found after ${consecutiveMissing} consecutive attempts`);
24313
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
24314
+ continue;
24315
+ }
24243
24316
  consecutiveErrors = 0;
24244
- if (!task) throw new Error(`Task ${taskId} not found`);
24317
+ consecutiveMissing = 0;
24245
24318
  if (task.status === "completed") {
24246
24319
  if (!task.result) throw new CheckResultPendingError(taskId);
24247
24320
  return task.result;
@@ -25344,6 +25417,14 @@ var RevisionScmSchema = external_exports.object({
25344
25417
  state: external_exports.enum(["draft", "open", "merged", "closed"]),
25345
25418
  mergeSha: external_exports.string().optional()
25346
25419
  });
25420
+ var RevisionPreviewSchema = external_exports.object({
25421
+ url: external_exports.string(),
25422
+ commitSha: external_exports.string(),
25423
+ recordedAt: external_exports.string()
25424
+ });
25425
+ var RevisionPreviewApiSchema = RevisionPreviewSchema.extend({
25426
+ isCurrent: external_exports.boolean()
25427
+ });
25347
25428
  var RevisionApiSchema = external_exports.object({
25348
25429
  revisionId: external_exports.string(),
25349
25430
  beatId: external_exports.string(),
@@ -25686,6 +25767,7 @@ var NotificationSchema2 = external_exports.object({
25686
25767
  systemId: external_exports.string(),
25687
25768
  beatId: external_exports.string().optional(),
25688
25769
  revisionId: external_exports.string().optional(),
25770
+ workItemIds: external_exports.array(external_exports.string()).optional(),
25689
25771
  noteId: external_exports.string().optional(),
25690
25772
  checkId: external_exports.string().optional(),
25691
25773
  actorType: external_exports.enum(["user", "system"]),
@@ -25881,6 +25963,7 @@ var StartSessionRequestBodySchema = external_exports.object({
25881
25963
  systemId: external_exports.string().min(1),
25882
25964
  message: external_exports.string().min(1),
25883
25965
  revisionId: external_exports.string().min(1).optional(),
25966
+ workItemIds: external_exports.array(external_exports.string().min(1)).max(50).optional(),
25884
25967
  /** Caller's own Session id — nests the new Session under it (B-231). */
25885
25968
  parentSessionId: external_exports.string().min(1).optional(),
25886
25969
  enableWebSearch: external_exports.boolean().optional()
@@ -25950,7 +26033,8 @@ var WORK_ITEM_EFFORT_SIZES = [
25950
26033
  "S",
25951
26034
  "M",
25952
26035
  "L",
25953
- "XL"
26036
+ "XL",
26037
+ "XXL"
25954
26038
  ];
25955
26039
  var WorkItemEffortSizeSchema = external_exports.enum(WORK_ITEM_EFFORT_SIZES);
25956
26040
  var HumanAssigneeApiSchema = external_exports.object({
@@ -26090,9 +26174,24 @@ var WORK_ORDER_SIZES = [
26090
26174
  "S",
26091
26175
  "M",
26092
26176
  "L",
26093
- "XL"
26177
+ "XL",
26178
+ "XXL"
26094
26179
  ];
26095
26180
  var WorkOrderSizeApiSchema = external_exports.enum(WORK_ORDER_SIZES);
26181
+ var WORK_ORDER_SIZE_VALUE_UNITS = {
26182
+ XS: 4,
26183
+ // half a person-day
26184
+ S: 8,
26185
+ // one person-day
26186
+ M: 24,
26187
+ // three person-days
26188
+ L: 40,
26189
+ // one person-week
26190
+ XL: 80,
26191
+ // two person-weeks
26192
+ XXL: 160
26193
+ // four person-weeks
26194
+ };
26096
26195
  var WORK_REQUEST_ORIGINS = [
26097
26196
  "client",
26098
26197
  "internal"
@@ -26135,6 +26234,9 @@ var WorkOrderApiSchema = external_exports.object({
26135
26234
  needsDecisionReason: NeedsDecisionReasonApiSchema.optional(),
26136
26235
  /** Absent when the Order arrived before it was sized, which the composition counts. */
26137
26236
  size: WorkOrderSizeApiSchema.optional(),
26237
+ archived: external_exports.boolean().optional(),
26238
+ archivedAt: external_exports.string().optional(),
26239
+ archivedReason: external_exports.string().optional(),
26138
26240
  createdBy: external_exports.string().optional(),
26139
26241
  version: external_exports.number().optional(),
26140
26242
  createdAt: external_exports.string(),
@@ -26185,6 +26287,30 @@ var AccountApiSchema = external_exports.object({
26185
26287
  updatedAt: external_exports.string()
26186
26288
  }).passthrough();
26187
26289
  var AccountsResponseSchema = external_exports.object({ accounts: external_exports.array(AccountApiSchema) }).passthrough();
26290
+ var ManagedRelationshipStatusApiSchema = external_exports.enum(["active", "suspended", "revoked"]);
26291
+ var ManagedRelationshipApiSchema = external_exports.object({
26292
+ relationshipId: external_exports.string(),
26293
+ agencyOrgId: external_exports.string(),
26294
+ customerOrgId: external_exports.string(),
26295
+ agencyAccountId: external_exports.string(),
26296
+ accountIds: external_exports.array(external_exports.string()),
26297
+ status: ManagedRelationshipStatusApiSchema,
26298
+ createdBy: external_exports.string(),
26299
+ statusChangedBy: external_exports.string().nullish(),
26300
+ statusChangedAt: external_exports.string().nullish(),
26301
+ suspendedBy: external_exports.string().nullish(),
26302
+ suspendedAt: external_exports.string().nullish(),
26303
+ reactivatedBy: external_exports.string().nullish(),
26304
+ reactivatedAt: external_exports.string().nullish(),
26305
+ revokedBy: external_exports.string().nullish(),
26306
+ revokedAt: external_exports.string().nullish(),
26307
+ version: external_exports.number(),
26308
+ createdAt: external_exports.string(),
26309
+ updatedAt: external_exports.string()
26310
+ }).passthrough();
26311
+ var ManagedRelationshipsResponseSchema = external_exports.object({
26312
+ relationships: external_exports.array(ManagedRelationshipApiSchema)
26313
+ }).passthrough();
26188
26314
  var AccountSystemApiSchema = external_exports.object({
26189
26315
  systemId: external_exports.string(),
26190
26316
  accountId: external_exports.string(),
@@ -26922,6 +27048,100 @@ ${formatLayerDetail(layer)}` }] };
26922
27048
  );
26923
27049
  }
26924
27050
 
27051
+ // ../../libs/harmonica-services/src/mcp/tools/managed-relationship-tools.ts
27052
+ function formatRelationship(relationship) {
27053
+ if (!relationship) return "Managed relationship not found.";
27054
+ return [
27055
+ `ID: ${relationship.relationshipId}`,
27056
+ `Agency Organization: ${relationship.agencyOrgId}`,
27057
+ `Customer Organization: ${relationship.customerOrgId}`,
27058
+ `Agency Account: ${relationship.agencyAccountId}`,
27059
+ `Customer Accounts: ${relationship.accountIds.join(", ")}`,
27060
+ `Status: ${relationship.status}`
27061
+ ].join("\n");
27062
+ }
27063
+ function failure(action, error2) {
27064
+ const message = error2 instanceof Error ? error2.message : String(error2);
27065
+ return {
27066
+ content: [{ type: "text", text: `Failed to ${action}: ${message}` }],
27067
+ isError: true
27068
+ };
27069
+ }
27070
+ function registerManagedRelationshipTools(server, _ctx, client) {
27071
+ server.tool(
27072
+ "create_managed_relationship",
27073
+ "Connect an agency Organization to a separately owned customer Organization and grant the agency access only to the named customer Accounts. Both Organizations, the agency commercial Account, and every customer Account must already exist. Phase 1 is administrator-provisioned; no approval request is sent.",
27074
+ {
27075
+ agencyOrgId: external_exports.string().describe("The managing agency Organization ID"),
27076
+ customerOrgId: external_exports.string().describe("The customer-owned Organization ID"),
27077
+ agencyAccountId: external_exports.string().describe("The agency-owned commercial Account for this customer"),
27078
+ accountIds: external_exports.array(external_exports.string()).min(1).max(10).describe("Customer-owned Account IDs delegated to the agency")
27079
+ },
27080
+ async (input) => {
27081
+ try {
27082
+ const relationship = await client.createManagedRelationship(input);
27083
+ return { content: [{ type: "text", text: `Created managed relationship.
27084
+ ${formatRelationship(relationship)}` }] };
27085
+ } catch (error2) {
27086
+ return failure("create managed relationship", error2);
27087
+ }
27088
+ }
27089
+ );
27090
+ server.tool(
27091
+ "list_managed_relationships",
27092
+ "List customer Organizations and Account scopes managed by an agency Organization.",
27093
+ { agencyOrgId: external_exports.string().describe("The managing agency Organization ID") },
27094
+ async ({ agencyOrgId }) => {
27095
+ try {
27096
+ const relationships = await client.listManagedRelationships(agencyOrgId);
27097
+ const text = relationships.length === 0 ? "No managed customer organizations found." : relationships.map(formatRelationship).join("\n\n");
27098
+ return { content: [{ type: "text", text }] };
27099
+ } catch (error2) {
27100
+ return failure("list managed relationships", error2);
27101
+ }
27102
+ }
27103
+ );
27104
+ server.tool(
27105
+ "get_managed_relationship",
27106
+ "Get one managed relationship and its explicitly delegated customer Account IDs.",
27107
+ { relationshipId: external_exports.string().describe("The managed relationship ID") },
27108
+ async ({ relationshipId }) => {
27109
+ try {
27110
+ return {
27111
+ content: [{
27112
+ type: "text",
27113
+ text: formatRelationship(await client.getManagedRelationship(relationshipId))
27114
+ }]
27115
+ };
27116
+ } catch (error2) {
27117
+ return failure("get managed relationship", error2);
27118
+ }
27119
+ }
27120
+ );
27121
+ server.tool(
27122
+ "set_managed_relationship_status",
27123
+ "Suspend, reactivate, or permanently revoke a managed relationship. Revoked relationships cannot be reactivated.",
27124
+ {
27125
+ relationshipId: external_exports.string().describe("The managed relationship ID"),
27126
+ status: external_exports.enum(["active", "suspended", "revoked"]).describe("The target status")
27127
+ },
27128
+ async ({ relationshipId, status }) => {
27129
+ try {
27130
+ const relationship = await client.setManagedRelationshipStatus(relationshipId, status);
27131
+ if (!relationship) {
27132
+ return {
27133
+ content: [{ type: "text", text: "Managed relationship not found." }],
27134
+ isError: true
27135
+ };
27136
+ }
27137
+ return { content: [{ type: "text", text: formatRelationship(relationship) }] };
27138
+ } catch (error2) {
27139
+ return failure("update managed relationship", error2);
27140
+ }
27141
+ }
27142
+ );
27143
+ }
27144
+
26925
27145
  // ../../libs/harmonica-services/src/mcp/tools/membership-tools.ts
26926
27146
  var orgIdArg = external_exports.string().describe("The organization ID \u2014 see list_organizations");
26927
27147
  function registerMembershipTools(server, ctx, client) {
@@ -28292,22 +28512,16 @@ ${formatTree(tree.root)}` }] };
28292
28512
  }
28293
28513
  let scopeSystemId = systemId;
28294
28514
  if (!scopeSystemId) {
28295
- let bindings;
28296
28515
  let bindingsFetchFailed = false;
28297
28516
  try {
28298
- bindings = await client.getNotebookScopeBindings(notebookId);
28517
+ const bindings = await client.getNotebookScopeBindings(notebookId);
28299
28518
  scopeSystemId = bindings?.find((b) => b.scopeType === "system")?.scopeId;
28300
28519
  } catch {
28301
28520
  bindingsFetchFailed = true;
28302
28521
  }
28303
28522
  if (!scopeSystemId) {
28304
- if (!bindingsFetchFailed) {
28305
- const hasLegacyProjectBinding = bindings?.some((b) => b.scopeType === "project");
28306
- if (hasLegacyProjectBinding) {
28307
- return { content: [{ type: "text", text: `Notebook "${notebookId}" has only a legacy 'project' scope binding \u2014 record may need B-247 migration re-check.` }], isError: true };
28308
- }
28309
- }
28310
- return { content: [{ type: "text", text: "No system scope: pass systemId or bind the notebook to a system first." }], isError: true };
28523
+ const msg = bindingsFetchFailed ? "Failed to look up notebook scope binding \u2014 this may be transient. Pass an explicit systemId to proceed." : "No system scope: pass systemId or bind the notebook to a system first.";
28524
+ return { content: [{ type: "text", text: msg }], isError: true };
28311
28525
  }
28312
28526
  }
28313
28527
  let tree;
@@ -28362,7 +28576,7 @@ ${formatTree(tree.root)}` }] };
28362
28576
  );
28363
28577
  server.tool(
28364
28578
  "update_notebook_collection",
28365
- "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.",
28579
+ "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.",
28366
28580
  {
28367
28581
  notebookId: external_exports.string().describe("The Notebook ID"),
28368
28582
  elementId: external_exports.string().describe("The collection section's elementId (from get_notebook_tree)"),
@@ -28411,10 +28625,9 @@ ${formatTree(tree.root)}` }] };
28411
28625
  return { content: [{ type: "text", text: `Element "${elementId}" is not a live collection \u2014 its configuration cannot be edited here.` }], isError: true };
28412
28626
  }
28413
28627
  const base = existing.query;
28414
- const normalizedScope = systemId ? `system:${systemId}` : base.scope.replace(/^project:/, "system:");
28415
- const colonIdx = normalizedScope.indexOf(":");
28416
- if (colonIdx <= 0 || colonIdx === normalizedScope.length - 1) {
28417
- return { content: [{ type: "text", text: `Stored scope '${base.scope}' has no system id \u2014 pass an explicit systemId to re-bind this collection.` }], isError: true };
28628
+ const normalizedScope = systemId ? `system:${systemId}` : base.scope;
28629
+ if (!normalizedScope.startsWith("system:") || normalizedScope.length <= "system:".length) {
28630
+ return { content: [{ type: "text", text: `Stored scope '${base.scope}' is not a valid system scope \u2014 pass an explicit systemId to re-bind this collection.` }], isError: true };
28418
28631
  }
28419
28632
  const mergedQuery = {
28420
28633
  source: "notes",
@@ -28503,9 +28716,9 @@ ${scopeText}` }] };
28503
28716
  );
28504
28717
  server.tool(
28505
28718
  "list_notebooks",
28506
- "List Notebooks reachable from a scope (org, teamspace, or system). Returns all notebooks bound to that scope.",
28719
+ "List Notebooks reachable from a scope (org, teamspace, system, or account). Returns all notebooks bound to that scope.",
28507
28720
  {
28508
- scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, or project"),
28721
+ scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, system, or account"),
28509
28722
  scopeId: external_exports.string().min(1).describe("ID of the scope entity"),
28510
28723
  includeArchived: external_exports.boolean().optional().describe("Include archived Notebooks in the results. Default: false (archived excluded).")
28511
28724
  },
@@ -28566,10 +28779,10 @@ ${formatNotebook(updated)}` }] };
28566
28779
  );
28567
28780
  server.tool(
28568
28781
  "add_notebook_shortcut",
28569
- "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.",
28782
+ "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.",
28570
28783
  {
28571
28784
  notebookId: external_exports.string().describe("The Notebook ID to shortcut"),
28572
- scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Consuming scope type \u2014 org, teamspace, project, or account"),
28785
+ scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Consuming scope type \u2014 org, teamspace, system, or account"),
28573
28786
  scopeId: external_exports.string().min(1).describe("ID of the consuming scope entity"),
28574
28787
  createdBy: external_exports.string().optional().describe("Actor ID \u2014 defaults to the session user")
28575
28788
  },
@@ -28598,7 +28811,7 @@ ${formatNotebook(updated)}` }] };
28598
28811
  "Remove a Notebook shortcut from a scope. Only removes the shortcut reference \u2014 never affects the source Notebook or its real scope bindings.",
28599
28812
  {
28600
28813
  notebookId: external_exports.string().describe("The Notebook ID"),
28601
- scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Consuming scope type \u2014 org, teamspace, project, or account"),
28814
+ scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Consuming scope type \u2014 org, teamspace, system, or account"),
28602
28815
  scopeId: external_exports.string().min(1).describe("ID of the consuming scope entity")
28603
28816
  },
28604
28817
  async ({ notebookId, scopeType, scopeId }) => {
@@ -28621,7 +28834,7 @@ ${formatNotebook(updated)}` }] };
28621
28834
  "list_notebook_shortcuts",
28622
28835
  "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.",
28623
28836
  {
28624
- scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, project, or account"),
28837
+ scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, system, or account"),
28625
28838
  scopeId: external_exports.string().min(1).describe("ID of the scope entity")
28626
28839
  },
28627
28840
  async ({ scopeType, scopeId }) => {
@@ -28976,12 +29189,14 @@ var ROOT_CODA_POLL_MAX_CONSECUTIVE_ERRORS = 3;
28976
29189
  async function pollForRootCodaCheck(client, taskId) {
28977
29190
  const deadline = Date.now() + ROOT_CODA_POLL_TIMEOUT_MS;
28978
29191
  let consecutiveErrors = 0;
29192
+ let consecutiveMissing = 0;
28979
29193
  while (Date.now() < deadline) {
28980
29194
  const [task, err] = await client.getTask(taskId).then(
28981
29195
  (t) => [t, null],
28982
29196
  (e) => [null, e]
28983
29197
  );
28984
29198
  if (err) {
29199
+ consecutiveMissing = 0;
28985
29200
  consecutiveErrors++;
28986
29201
  if (consecutiveErrors >= ROOT_CODA_POLL_MAX_CONSECUTIVE_ERRORS) {
28987
29202
  throw new Error(`getTask failed ${consecutiveErrors} times: ${err instanceof Error ? err.message : String(err)}`);
@@ -28989,8 +29204,16 @@ async function pollForRootCodaCheck(client, taskId) {
28989
29204
  await new Promise((r) => setTimeout(r, ROOT_CODA_POLL_INTERVAL_MS));
28990
29205
  continue;
28991
29206
  }
29207
+ if (!task) {
29208
+ consecutiveErrors = 0;
29209
+ consecutiveMissing++;
29210
+ if (consecutiveMissing >= ROOT_CODA_POLL_MAX_CONSECUTIVE_ERRORS)
29211
+ throw new Error(`Task ${taskId} not found after ${consecutiveMissing} consecutive attempts`);
29212
+ await new Promise((r) => setTimeout(r, ROOT_CODA_POLL_INTERVAL_MS));
29213
+ continue;
29214
+ }
28992
29215
  consecutiveErrors = 0;
28993
- if (!task) throw new Error(`Task ${taskId} not found`);
29216
+ consecutiveMissing = 0;
28994
29217
  if (task.status === "completed") {
28995
29218
  if (!task.result) throw new CheckResultPendingError(taskId);
28996
29219
  const result = task.result;
@@ -29333,12 +29556,14 @@ var POLL_MAX_CONSECUTIVE_ERRORS4 = 3;
29333
29556
  async function pollForCheck4(client, taskId) {
29334
29557
  const deadline = Date.now() + POLL_TIMEOUT_MS4;
29335
29558
  let consecutiveErrors = 0;
29559
+ let consecutiveMissing = 0;
29336
29560
  while (Date.now() < deadline) {
29337
29561
  const [task, err] = await client.getTask(taskId).then(
29338
29562
  (t) => [t, null],
29339
29563
  (e) => [null, e]
29340
29564
  );
29341
29565
  if (err) {
29566
+ consecutiveMissing = 0;
29342
29567
  consecutiveErrors++;
29343
29568
  if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS4) {
29344
29569
  throw new Error(`getTask failed ${consecutiveErrors} times: ${err instanceof Error ? err.message : String(err)}`);
@@ -29346,8 +29571,16 @@ async function pollForCheck4(client, taskId) {
29346
29571
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS4));
29347
29572
  continue;
29348
29573
  }
29574
+ if (!task) {
29575
+ consecutiveErrors = 0;
29576
+ consecutiveMissing++;
29577
+ if (consecutiveMissing >= POLL_MAX_CONSECUTIVE_ERRORS4)
29578
+ throw new Error(`Task ${taskId} not found after ${consecutiveMissing} consecutive attempts`);
29579
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS4));
29580
+ continue;
29581
+ }
29349
29582
  consecutiveErrors = 0;
29350
- if (!task) throw new Error(`Task ${taskId} not found`);
29583
+ consecutiveMissing = 0;
29351
29584
  if (task.status === "completed") {
29352
29585
  if (!task.result) throw new CheckResultPendingError(taskId);
29353
29586
  return task.result;
@@ -30684,19 +30917,16 @@ function registerSnapshotTools(server, ctx, client) {
30684
30917
  );
30685
30918
  }
30686
30919
  function normalizeSnapshot(raw) {
30687
- const anyRaw = raw;
30688
30920
  return {
30689
30921
  ...raw,
30690
- // Compat: v3 and earlier snapshots have 'project' instead of 'system'
30691
- system: raw.system ?? anyRaw["project"],
30922
+ system: raw.system,
30692
30923
  beats: raw.beats ?? [],
30693
30924
  beatIndex: raw.beatIndex ?? [],
30694
30925
  proposals: raw.proposals ?? [],
30695
30926
  proposalIndex: raw.proposalIndex ?? [],
30696
30927
  revisions: raw.revisions ?? [],
30697
30928
  revisionIndex: raw.revisionIndex ?? [],
30698
- // Compat: v3 and earlier snapshots have 'projectRevisionIndex'
30699
- systemRevisionIndex: raw.systemRevisionIndex ?? anyRaw["projectRevisionIndex"] ?? [],
30929
+ systemRevisionIndex: raw.systemRevisionIndex ?? [],
30700
30930
  activities: raw.activities ?? [],
30701
30931
  activityIndex: raw.activityIndex ?? [],
30702
30932
  notes: raw.notes ?? [],
@@ -30709,8 +30939,7 @@ function normalizeSnapshot(raw) {
30709
30939
  promptLogs: raw.promptLogs ?? [],
30710
30940
  beatVersions: raw.beatVersions ?? [],
30711
30941
  beatBeatVersionIndex: raw.beatBeatVersionIndex ?? [],
30712
- // Compat: v3 and earlier snapshots have 'projectBeatVersionIndex'
30713
- systemBeatVersionIndex: raw.systemBeatVersionIndex ?? anyRaw["projectBeatVersionIndex"] ?? [],
30942
+ systemBeatVersionIndex: raw.systemBeatVersionIndex ?? [],
30714
30943
  beatVersionDropAssignments: raw.beatVersionDropAssignments ?? [],
30715
30944
  drops: raw.drops ?? [],
30716
30945
  accountDropIndex: raw.accountDropIndex ?? [],
@@ -30806,8 +31035,11 @@ function assertSnapshotShape(val, sourceHint) {
30806
31035
  if (typeof obj["version"] !== "number" || obj["version"] <= 0) {
30807
31036
  throw new Error(`Snapshot${sourceHint} has an invalid or missing 'version' field \u2014 it may not be a valid project snapshot.`);
30808
31037
  }
30809
- const systemOrProject = obj["system"] ?? obj["project"];
30810
- if (typeof systemOrProject !== "object" || systemOrProject === null || typeof systemOrProject["systemId"] !== "string") {
31038
+ const system = obj["system"];
31039
+ if (typeof system !== "object" || system === null || typeof system["systemId"] !== "string") {
31040
+ if (typeof obj["project"] === "object" && obj["project"] !== null) {
31041
+ throw new Error(`Snapshot${sourceHint} uses the v1-v3 'project' field format, which is no longer supported. Export a fresh snapshot from your environment and re-import.`);
31042
+ }
30811
31043
  throw new Error(`Snapshot${sourceHint} is missing 'system.systemId' \u2014 it may not be a valid project snapshot.`);
30812
31044
  }
30813
31045
  }
@@ -31417,7 +31649,7 @@ ${t.acceptanceCriteria}` : null,
31417
31649
  };
31418
31650
  server.tool(
31419
31651
  "create_track",
31420
- 'Create a Track under a Movement. Use for scoped initiatives \u2014 client SOWs, internal phases, engagement containers \u2014 that collect Deliverables and Drops. Do NOT use for software capabilities the product provides to end users (those are Beats). Recognition cues: "this engagement", "this SOW", "this phase of work", "the client project", "this track" \u2192 Track.',
31652
+ 'Create a Track under a Movement. Use for scoped initiatives \u2014 client SOWs, internal phases, engagement containers \u2014 that collect work items and Drops. Do NOT use for software capabilities the product provides to end users (those are Beats). Recognition cues: "this engagement", "this SOW", "this phase of work", "the client project", "this track" \u2192 Track.',
31421
31653
  createTrackSchema,
31422
31654
  createTrackHandler
31423
31655
  );
@@ -31857,24 +32089,47 @@ function registerValueVelocityTools(server, ctx, client) {
31857
32089
  );
31858
32090
  }
31859
32091
 
31860
- // ../../libs/harmonica-services/src/mcp/tools/work-item-tools.ts
31861
- var WORK_ITEM_STATUS_ENUM = ["not_started", "in_progress", "blocked", "done"];
31862
- function formatOwner(owner) {
31863
- return owner ? `${owner.name} <${owner.email}>` : "unassigned";
31864
- }
32092
+ // ../../libs/harmonica-services/src/tool-schemas/sizing.ts
32093
+ var WORK_ORDER_SIZE_ENUM = [
32094
+ "XS",
32095
+ "S",
32096
+ "M",
32097
+ "L",
32098
+ "XL",
32099
+ "XXL"
32100
+ ];
31865
32101
  var WORK_ITEM_EFFORT_SIZE_ENUM = [
31866
32102
  "XS",
31867
32103
  "S",
31868
32104
  "M",
31869
32105
  "L",
31870
- "XL"
32106
+ "XL",
32107
+ "XXL"
31871
32108
  ];
32109
+ var SIZE_LABELS = {
32110
+ XS: "half a person-day",
32111
+ S: "one person-day",
32112
+ M: "three person-days",
32113
+ L: "one person-week",
32114
+ XL: "two person-weeks",
32115
+ XXL: "four person-weeks"
32116
+ };
32117
+ var SIZE_TABLE = WORK_ORDER_SIZE_ENUM.map(
32118
+ (size) => `${size}=${SIZE_LABELS[size]} (${WORK_ORDER_SIZE_VALUE_UNITS[size]} VU)`
32119
+ ).join(", ");
32120
+ var SIZE_BASIS = `Estimate the effort with NO AI assistance \u2014 the fully-human person-hours. Do NOT size what it will actually take with AI. Person-hours, not calendar time: two people for a week is XL, not L. ${SIZE_TABLE}.`;
32121
+
32122
+ // ../../libs/harmonica-services/src/mcp/tools/work-item-tools.ts
32123
+ var WORK_ITEM_STATUS_ENUM = ["not_started", "in_progress", "blocked", "done"];
32124
+ function formatOwner(owner) {
32125
+ return owner ? `${owner.name} <${owner.email}>` : "unassigned";
32126
+ }
31872
32127
  function registerWorkItemTools(server, _ctx, client) {
31873
32128
  server.tool(
31874
32129
  "list_track_work_items",
31875
- 'List all work items under a Track (DeliverableGroup). A work item is an owned, statused unit of operational work. Recognition cues: "what work is on this track", "list the work items" \u2192 list_track_work_items.',
32130
+ 'List all work items under a Track. A work item is an owned, statused unit of operational work. Recognition cues: "what work is on this track", "list the work items" \u2192 list_track_work_items.',
31876
32131
  {
31877
- trackId: external_exports.string().describe("The Track (DeliverableGroup) ID")
32132
+ trackId: external_exports.string().describe("The Track ID")
31878
32133
  },
31879
32134
  async ({ trackId }) => {
31880
32135
  const workItems = await client.listTrackWorkItems(trackId);
@@ -31920,15 +32175,15 @@ function registerWorkItemTools(server, _ctx, client) {
31920
32175
  );
31921
32176
  server.tool(
31922
32177
  "create_work_item",
31923
- 'Create a work item under a Track (DeliverableGroup). A work item is an owned, statused unit of operational work \u2014 it is NOT a Beat. Starts in "not_started" status. Optionally link it to a Beat Version for capability lineage (v2, N-4E09-6574) \u2014 the link is one-directional and never required. Optionally link it to a Work Order it delivers part of; leaving it unattached is a supported state, not a gap to fill (N-4E09-7160). Optionally link it to a Bar it is planned into; leaving it unplanned is likewise a real state, not a gap (B-298).',
32178
+ 'Create a work item under a Track. A work item is an owned, statused unit of operational work \u2014 it is NOT a Beat. Starts in "not_started" status. Optionally link it to a Beat Version for capability lineage (v2, N-4E09-6574) \u2014 the link is one-directional and never required. Optionally link it to a Work Order it delivers part of; leaving it unattached is a supported state, not a gap to fill (N-4E09-7160). Optionally link it to a Bar it is planned into; leaving it unplanned is likewise a real state, not a gap (B-298).',
31924
32179
  {
31925
- trackId: external_exports.string().describe("The Track (DeliverableGroup) ID this work item belongs to"),
32180
+ trackId: external_exports.string().describe("The Track ID this work item belongs to"),
31926
32181
  title: external_exports.string().min(1).max(300).describe("Work item title"),
31927
32182
  ownerName: external_exports.string().min(1).optional().describe("Optional owner display name (requires ownerEmail too)"),
31928
32183
  ownerEmail: external_exports.string().email().optional().describe("Optional owner email (requires ownerName too). A work item may be left unowned."),
31929
32184
  committedEstimateHours: external_exports.number().nonnegative().optional().describe("Optional committed estimate in hours"),
31930
- effortSize: external_exports.enum(WORK_ITEM_EFFORT_SIZE_ENUM).optional().describe("Optional rough t-shirt effort size (delivery lens) \u2014 independent of valueUnits"),
31931
- valueUnits: external_exports.number().nonnegative().optional().describe("Optional Value Units (commercial lens) \u2014 independent of effortSize"),
32185
+ effortSize: external_exports.enum(WORK_ITEM_EFFORT_SIZE_ENUM).optional().describe(`Optional rough t-shirt effort size \u2014 seeds valueUnits. ${SIZE_BASIS}`),
32186
+ valueUnits: external_exports.number().nonnegative().optional().describe(`Optional Value Units \u2014 fully-human person-hours. Defaults from effortSize; pass only to override. ${SIZE_BASIS}`),
31932
32187
  costPerValueUnit: external_exports.number().nonnegative().optional().describe("Optional price per Value Unit"),
31933
32188
  createdBy: external_exports.string().optional().describe("Optional creator identity for audit"),
31934
32189
  beatVersionId: external_exports.string().min(1).optional().describe("Optional Beat Version to link for capability lineage. Must exist."),
@@ -31976,7 +32231,9 @@ function registerWorkItemTools(server, _ctx, client) {
31976
32231
  unassignOwner: external_exports.boolean().optional().describe("Pass true to clear the owner, leaving the work item unowned. Cannot be combined with ownerName/ownerEmail."),
31977
32232
  committedEstimateHours: external_exports.number().nonnegative().optional().describe("New committed estimate in hours"),
31978
32233
  actualHours: external_exports.number().nonnegative().optional().describe("New actual hours logged"),
31979
- effortSize: external_exports.enum(WORK_ITEM_EFFORT_SIZE_ENUM).nullable().optional().describe("Rough t-shirt effort size (delivery lens). Pass null to clear; omit to leave untouched."),
32234
+ effortSize: external_exports.enum(WORK_ITEM_EFFORT_SIZE_ENUM).nullable().optional().describe(
32235
+ `Rough t-shirt effort size. Pass null to clear; omit to leave untouched. ${SIZE_BASIS}`
32236
+ ),
31980
32237
  valueUnits: external_exports.number().nonnegative().nullable().optional().describe("Value Units (commercial lens). Pass null to clear; omit to leave untouched."),
31981
32238
  costPerValueUnit: external_exports.number().nonnegative().nullable().optional().describe("Price per Value Unit. Pass null to clear; omit to leave untouched."),
31982
32239
  beatVersionId: external_exports.string().min(1).nullable().optional().describe("Link to a Beat Version for capability lineage. Pass null to unlink; omit to leave the current link untouched."),
@@ -32037,10 +32294,10 @@ function registerWorkItemTools(server, _ctx, client) {
32037
32294
  );
32038
32295
  server.tool(
32039
32296
  "move_work_item",
32040
- "Move a work item to a different Track (DeliverableGroup). Re-derives its teamspace/org scope from the new Track.",
32297
+ "Move a work item to a different Track. Re-derives its teamspace/org scope from the new Track.",
32041
32298
  {
32042
32299
  workItemId: external_exports.string().describe("The work item ID"),
32043
- newTrackId: external_exports.string().describe("The destination Track (DeliverableGroup) ID. Must exist.")
32300
+ newTrackId: external_exports.string().describe("The destination Track ID. Must exist.")
32044
32301
  },
32045
32302
  async ({ workItemId, newTrackId }) => {
32046
32303
  const wi = await client.moveWorkItem(workItemId, newTrackId);
@@ -32083,13 +32340,12 @@ function registerWorkItemTools(server, _ctx, client) {
32083
32340
  }
32084
32341
 
32085
32342
  // ../../libs/harmonica-services/src/tool-schemas/work-order.ts
32086
- var WORK_ORDER_SIZE_ENUM = ["XS", "S", "M", "L", "XL"];
32087
32343
  var create_work_order = {
32088
32344
  workRequestId: external_exports.string().min(1).describe("The Work Request this order was split from \u2014 immutable lineage"),
32089
32345
  title: external_exports.string().min(1).max(300).describe("Work order title"),
32090
32346
  description: external_exports.string().max(1e4).optional().describe("Optional detail beyond the title"),
32091
32347
  size: external_exports.enum(WORK_ORDER_SIZE_ENUM).optional().describe(
32092
- "Optional t-shirt size. An order can arrive before it is sized; a request's composition counts unsized orders rather than hiding them."
32348
+ "Optional t-shirt size. An order can arrive before it is sized; a request's composition counts unsized orders rather than hiding them. " + SIZE_BASIS
32093
32349
  ),
32094
32350
  createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
32095
32351
  };
@@ -32112,13 +32368,6 @@ var NEEDS_DECISION_REASON_ENUM = [
32112
32368
  "exceeds_capacity",
32113
32369
  "incoherent_with_decision"
32114
32370
  ];
32115
- var WORK_ORDER_SIZE_ENUM2 = [
32116
- "XS",
32117
- "S",
32118
- "M",
32119
- "L",
32120
- "XL"
32121
- ];
32122
32371
  function oneLine(value) {
32123
32372
  return value.replace(/[\r\n]+/g, " ");
32124
32373
  }
@@ -32134,14 +32383,14 @@ function registerWorkOrderTools(server, _ctx, client) {
32134
32383
  // exactly the caller this alias exists to greet. Requiring it would answer
32135
32384
  // the migrated caller with a schema error instead of the migration message.
32136
32385
  {
32137
- trackId: external_exports.string().optional().describe("Ignored \u2014 the Track (DeliverableGroup) ID is no longer meaningful for Order scoping")
32386
+ trackId: external_exports.string().optional().describe("Ignored \u2014 the Track ID is no longer meaningful for Order scoping")
32138
32387
  },
32139
32388
  async () => {
32140
32389
  return {
32141
32390
  content: [
32142
32391
  {
32143
32392
  type: "text",
32144
- text: "list_track_work_orders is deprecated. Use list_movement_work_orders(movementId) to fetch all Orders for an engagement \u2014 Orders are now scoped to their Movement, not their Track. If you have only a Track id, its Movement is on the Track itself: get_deliverable_group(trackId) returns movementId."
32393
+ text: "list_track_work_orders is deprecated. Use list_movement_work_orders(movementId) to fetch all Orders for an engagement \u2014 Orders are now scoped to their Movement, not their Track. If you have only a Track id, its Movement is on the Track itself: get_track(trackId) returns movementId."
32145
32394
  }
32146
32395
  ],
32147
32396
  // The caller asked for a list and is getting none. A result without
@@ -32226,7 +32475,9 @@ function registerWorkOrderTools(server, _ctx, client) {
32226
32475
  workOrderId: external_exports.string().describe("The Work Order ID"),
32227
32476
  title: external_exports.string().min(1).max(300).optional().describe("New title"),
32228
32477
  description: external_exports.string().max(1e4).nullable().optional().describe("New description. Pass null to clear; omit to leave untouched."),
32229
- 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.")
32478
+ size: external_exports.enum(WORK_ORDER_SIZE_ENUM).nullable().optional().describe(
32479
+ "New t-shirt size. Pass null to return it to unsized; omit to leave untouched. " + SIZE_BASIS
32480
+ )
32230
32481
  },
32231
32482
  async ({ workOrderId, title, description, size }) => {
32232
32483
  if (title === void 0 && description === void 0 && size === void 0) {
@@ -32256,7 +32507,7 @@ function registerWorkOrderTools(server, _ctx, client) {
32256
32507
  // the callers who have already stopped sending these.
32257
32508
  {
32258
32509
  workOrderId: external_exports.string().optional().describe("Ignored \u2014 a Work Order is no longer moveable between Tracks"),
32259
- newTrackId: external_exports.string().optional().describe("Ignored \u2014 a Track (DeliverableGroup) is no longer a valid parent for Orders")
32510
+ newTrackId: external_exports.string().optional().describe("Ignored \u2014 a Track is no longer a valid parent for Orders")
32260
32511
  },
32261
32512
  async () => {
32262
32513
  return {
@@ -32458,6 +32709,14 @@ function registerWorkRequestTools(server, _ctx, client) {
32458
32709
  );
32459
32710
  }
32460
32711
 
32712
+ // ../../libs/harmonica-services/src/work-item-ids.ts
32713
+ function normalizeWorkItemIds(value) {
32714
+ if (!Array.isArray(value)) return [];
32715
+ return [...new Set(
32716
+ value.filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean)
32717
+ )];
32718
+ }
32719
+
32461
32720
  // ../../libs/harmonica-services/src/mcp/tools/workflow-tools.ts
32462
32721
  var TASK_STATUSES = ["pending", "running", "completed", "failed"];
32463
32722
  function registerWorkflowTools(server, ctx, client) {
@@ -32648,17 +32907,19 @@ ${JSON.stringify(task.result, null, 2)}
32648
32907
  systemId: external_exports.string().min(1).describe("The system ID \u2014 the trusted scope the session is rooted at"),
32649
32908
  message: external_exports.string().min(1).describe("Free-form first message describing what the session should do"),
32650
32909
  revisionId: external_exports.string().min(1).optional().describe("Optional Revision ID (rev-<uuid>). When provided, the Worker routes to the typed implementation session path (prepareRevisionImplementationSession), enabling file hint generation and richer session context. Omit for investigation, planning, or freeform sessions."),
32910
+ workItemIds: external_exports.array(external_exports.string().min(1)).max(50).optional().describe("Optional Work Items associated with this session at kickoff. Each moves to in_progress when execution starts; the agent manages each item outcome independently."),
32651
32911
  parentSessionId: external_exports.string().min(1).optional().describe("Your current Session ID (sess-<uuid>). Pass it when you are kicking off this session as part of your own work \u2014 the new session then nests under yours in the session tree and can report back to you. Omit for a standalone session.")
32652
32912
  },
32653
- async ({ systemId, message, revisionId, parentSessionId }) => {
32913
+ async ({ systemId, message, revisionId, workItemIds, parentSessionId }) => {
32654
32914
  try {
32655
32915
  await assertSystemExists(client, systemId);
32656
32916
  const task = await client.startSession(
32657
32917
  systemId,
32658
32918
  message,
32659
32919
  { name: ctx.user.name, email: ctx.user.email },
32660
- revisionId?.trim() || parentSessionId?.trim() ? {
32920
+ revisionId?.trim() || workItemIds?.length || parentSessionId?.trim() ? {
32661
32921
  ...revisionId?.trim() && { revisionId: revisionId.trim() },
32922
+ ...workItemIds?.length && { workItemIds: normalizeWorkItemIds(workItemIds) },
32662
32923
  ...parentSessionId?.trim() && { parentSessionId: parentSessionId.trim() }
32663
32924
  } : void 0
32664
32925
  );
@@ -32886,6 +33147,7 @@ var TOOL_PROFILES = Object.freeze({
32886
33147
  registerOrganizationTools,
32887
33148
  registerTeamspaceTools,
32888
33149
  registerMembershipTools,
33150
+ registerManagedRelationshipTools,
32889
33151
  registerSystemTools,
32890
33152
  registerSystemLifecycleTools,
32891
33153
  allow(registerBeatVersionTools, ["relocate_beat_version"])
@@ -32965,6 +33227,7 @@ function registerAllTools(server, ctx, client, profile) {
32965
33227
  registerValueVelocityTools(server, ctx, client);
32966
33228
  registerNotebookTools(server, ctx, client);
32967
33229
  registerAccountTools(server, ctx, client);
33230
+ registerManagedRelationshipTools(server, ctx, client);
32968
33231
  registerMovementTools(server, ctx, client);
32969
33232
  registerExternalMappingTools(server, ctx, client);
32970
33233
  registerWorkItemTools(server, ctx, client);
@@ -33664,6 +33927,8 @@ function createHttpClient(config2) {
33664
33927
  return { revision: result.revision, warnings: result.warnings ?? [] };
33665
33928
  },
33666
33929
  getRevision: (revisionId) => request("GET", `/api/revisions/${encodeURIComponent(revisionId)}`),
33930
+ // `request` maps 404 to undefined, which is what absence must be here.
33931
+ resolveRevisionPreview: (revisionId) => request("GET", `/api/revisions/${encodeURIComponent(revisionId)}/preview`),
33667
33932
  listConceptRevisions: async (systemId, options) => {
33668
33933
  const qs = options?.includeArchived ? "?includeArchived=true" : "";
33669
33934
  const result = await request("GET", `/api/systems/${encodeURIComponent(systemId)}/revisions/pending${qs}`);
@@ -34305,6 +34570,7 @@ function createHttpClient(config2) {
34305
34570
  // Agent sessions
34306
34571
  startSession: async (systemId, message, triggeredBy, options) => {
34307
34572
  const trimmedRevisionId = options?.revisionId?.trim();
34573
+ const workItemIds = normalizeWorkItemIds(options?.workItemIds);
34308
34574
  const trimmedParentSessionId = options?.parentSessionId?.trim();
34309
34575
  const result = await request(
34310
34576
  "POST",
@@ -34314,6 +34580,7 @@ function createHttpClient(config2) {
34314
34580
  message,
34315
34581
  ...triggeredBy && { triggeredBy },
34316
34582
  ...trimmedRevisionId && { revisionId: trimmedRevisionId },
34583
+ ...workItemIds?.length && { workItemIds },
34317
34584
  ...trimmedParentSessionId && { parentSessionId: trimmedParentSessionId }
34318
34585
  }
34319
34586
  );
@@ -34952,6 +35219,31 @@ function createHttpClient(config2) {
34952
35219
  const res = await request("GET", `/api/accounts/${encodeURIComponent(accountId)}/movements`);
34953
35220
  return res?.movements ?? [];
34954
35221
  },
35222
+ createManagedRelationship: async (input) => {
35223
+ const { agencyOrgId, ...body } = input;
35224
+ const result = await request(
35225
+ "POST",
35226
+ `/api/organizations/${encodeURIComponent(agencyOrgId)}/managed-relationships`,
35227
+ body
35228
+ );
35229
+ if (result === void 0) throw new Error("Resource not found when creating managed relationship");
35230
+ return result;
35231
+ },
35232
+ getManagedRelationship: async (relationshipId) => request("GET", `/api/managed-relationships/${encodeURIComponent(relationshipId)}`),
35233
+ listManagedRelationships: async (agencyOrgId) => {
35234
+ const result = await request(
35235
+ "GET",
35236
+ `/api/organizations/${encodeURIComponent(agencyOrgId)}/managed-relationships`
35237
+ );
35238
+ return result?.relationships ?? [];
35239
+ },
35240
+ setManagedRelationshipStatus: async (relationshipId, status) => {
35241
+ return request(
35242
+ "PATCH",
35243
+ `/api/managed-relationships/${encodeURIComponent(relationshipId)}/status`,
35244
+ { status }
35245
+ );
35246
+ },
34955
35247
  listChildAccounts: async (accountId) => {
34956
35248
  const res = await request("GET", `/api/accounts/${encodeURIComponent(accountId)}/children`);
34957
35249
  return res?.accounts ?? [];
@@ -35471,7 +35763,7 @@ function loadConfig() {
35471
35763
  };
35472
35764
  }
35473
35765
  async function main() {
35474
- console.error(`[harmonica-mcp] v${"5.1.0"} starting\u2026`);
35766
+ console.error(`[harmonica-mcp] v${"5.3.0"} starting\u2026`);
35475
35767
  const config2 = loadConfig();
35476
35768
  const client = createHttpClient({
35477
35769
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "5.1.0",
3
+ "version": "5.3.0",
4
4
  "description": "MCP server for Harmonica — connect any MCP-compatible AI assistant to Harmonica",
5
5
  "license": "MIT",
6
6
  "bin": {