@codazen/harmonica-mcp 5.2.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 +270 -60
  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 {
@@ -25746,6 +25767,7 @@ var NotificationSchema2 = external_exports.object({
25746
25767
  systemId: external_exports.string(),
25747
25768
  beatId: external_exports.string().optional(),
25748
25769
  revisionId: external_exports.string().optional(),
25770
+ workItemIds: external_exports.array(external_exports.string()).optional(),
25749
25771
  noteId: external_exports.string().optional(),
25750
25772
  checkId: external_exports.string().optional(),
25751
25773
  actorType: external_exports.enum(["user", "system"]),
@@ -25941,6 +25963,7 @@ var StartSessionRequestBodySchema = external_exports.object({
25941
25963
  systemId: external_exports.string().min(1),
25942
25964
  message: external_exports.string().min(1),
25943
25965
  revisionId: external_exports.string().min(1).optional(),
25966
+ workItemIds: external_exports.array(external_exports.string().min(1)).max(50).optional(),
25944
25967
  /** Caller's own Session id — nests the new Session under it (B-231). */
25945
25968
  parentSessionId: external_exports.string().min(1).optional(),
25946
25969
  enableWebSearch: external_exports.boolean().optional()
@@ -26010,7 +26033,8 @@ var WORK_ITEM_EFFORT_SIZES = [
26010
26033
  "S",
26011
26034
  "M",
26012
26035
  "L",
26013
- "XL"
26036
+ "XL",
26037
+ "XXL"
26014
26038
  ];
26015
26039
  var WorkItemEffortSizeSchema = external_exports.enum(WORK_ITEM_EFFORT_SIZES);
26016
26040
  var HumanAssigneeApiSchema = external_exports.object({
@@ -26150,9 +26174,24 @@ var WORK_ORDER_SIZES = [
26150
26174
  "S",
26151
26175
  "M",
26152
26176
  "L",
26153
- "XL"
26177
+ "XL",
26178
+ "XXL"
26154
26179
  ];
26155
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
+ };
26156
26195
  var WORK_REQUEST_ORIGINS = [
26157
26196
  "client",
26158
26197
  "internal"
@@ -26195,6 +26234,9 @@ var WorkOrderApiSchema = external_exports.object({
26195
26234
  needsDecisionReason: NeedsDecisionReasonApiSchema.optional(),
26196
26235
  /** Absent when the Order arrived before it was sized, which the composition counts. */
26197
26236
  size: WorkOrderSizeApiSchema.optional(),
26237
+ archived: external_exports.boolean().optional(),
26238
+ archivedAt: external_exports.string().optional(),
26239
+ archivedReason: external_exports.string().optional(),
26198
26240
  createdBy: external_exports.string().optional(),
26199
26241
  version: external_exports.number().optional(),
26200
26242
  createdAt: external_exports.string(),
@@ -26245,6 +26287,30 @@ var AccountApiSchema = external_exports.object({
26245
26287
  updatedAt: external_exports.string()
26246
26288
  }).passthrough();
26247
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();
26248
26314
  var AccountSystemApiSchema = external_exports.object({
26249
26315
  systemId: external_exports.string(),
26250
26316
  accountId: external_exports.string(),
@@ -26982,6 +27048,100 @@ ${formatLayerDetail(layer)}` }] };
26982
27048
  );
26983
27049
  }
26984
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
+
26985
27145
  // ../../libs/harmonica-services/src/mcp/tools/membership-tools.ts
26986
27146
  var orgIdArg = external_exports.string().describe("The organization ID \u2014 see list_organizations");
26987
27147
  function registerMembershipTools(server, ctx, client) {
@@ -28352,22 +28512,16 @@ ${formatTree(tree.root)}` }] };
28352
28512
  }
28353
28513
  let scopeSystemId = systemId;
28354
28514
  if (!scopeSystemId) {
28355
- let bindings;
28356
28515
  let bindingsFetchFailed = false;
28357
28516
  try {
28358
- bindings = await client.getNotebookScopeBindings(notebookId);
28517
+ const bindings = await client.getNotebookScopeBindings(notebookId);
28359
28518
  scopeSystemId = bindings?.find((b) => b.scopeType === "system")?.scopeId;
28360
28519
  } catch {
28361
28520
  bindingsFetchFailed = true;
28362
28521
  }
28363
28522
  if (!scopeSystemId) {
28364
- if (!bindingsFetchFailed) {
28365
- const hasLegacyProjectBinding = bindings?.some((b) => b.scopeType === "project");
28366
- if (hasLegacyProjectBinding) {
28367
- 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 };
28368
- }
28369
- }
28370
- 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 };
28371
28525
  }
28372
28526
  }
28373
28527
  let tree;
@@ -28471,10 +28625,9 @@ ${formatTree(tree.root)}` }] };
28471
28625
  return { content: [{ type: "text", text: `Element "${elementId}" is not a live collection \u2014 its configuration cannot be edited here.` }], isError: true };
28472
28626
  }
28473
28627
  const base = existing.query;
28474
- const normalizedScope = systemId ? `system:${systemId}` : base.scope.replace(/^project:/, "system:");
28475
- const colonIdx = normalizedScope.indexOf(":");
28476
- if (colonIdx <= 0 || colonIdx === normalizedScope.length - 1) {
28477
- 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 };
28478
28631
  }
28479
28632
  const mergedQuery = {
28480
28633
  source: "notes",
@@ -30764,19 +30917,16 @@ function registerSnapshotTools(server, ctx, client) {
30764
30917
  );
30765
30918
  }
30766
30919
  function normalizeSnapshot(raw) {
30767
- const anyRaw = raw;
30768
30920
  return {
30769
30921
  ...raw,
30770
- // Compat: v3 and earlier snapshots have 'project' instead of 'system'
30771
- system: raw.system ?? anyRaw["project"],
30922
+ system: raw.system,
30772
30923
  beats: raw.beats ?? [],
30773
30924
  beatIndex: raw.beatIndex ?? [],
30774
30925
  proposals: raw.proposals ?? [],
30775
30926
  proposalIndex: raw.proposalIndex ?? [],
30776
30927
  revisions: raw.revisions ?? [],
30777
30928
  revisionIndex: raw.revisionIndex ?? [],
30778
- // Compat: v3 and earlier snapshots have 'projectRevisionIndex'
30779
- systemRevisionIndex: raw.systemRevisionIndex ?? anyRaw["projectRevisionIndex"] ?? [],
30929
+ systemRevisionIndex: raw.systemRevisionIndex ?? [],
30780
30930
  activities: raw.activities ?? [],
30781
30931
  activityIndex: raw.activityIndex ?? [],
30782
30932
  notes: raw.notes ?? [],
@@ -30789,8 +30939,7 @@ function normalizeSnapshot(raw) {
30789
30939
  promptLogs: raw.promptLogs ?? [],
30790
30940
  beatVersions: raw.beatVersions ?? [],
30791
30941
  beatBeatVersionIndex: raw.beatBeatVersionIndex ?? [],
30792
- // Compat: v3 and earlier snapshots have 'projectBeatVersionIndex'
30793
- systemBeatVersionIndex: raw.systemBeatVersionIndex ?? anyRaw["projectBeatVersionIndex"] ?? [],
30942
+ systemBeatVersionIndex: raw.systemBeatVersionIndex ?? [],
30794
30943
  beatVersionDropAssignments: raw.beatVersionDropAssignments ?? [],
30795
30944
  drops: raw.drops ?? [],
30796
30945
  accountDropIndex: raw.accountDropIndex ?? [],
@@ -30886,8 +31035,11 @@ function assertSnapshotShape(val, sourceHint) {
30886
31035
  if (typeof obj["version"] !== "number" || obj["version"] <= 0) {
30887
31036
  throw new Error(`Snapshot${sourceHint} has an invalid or missing 'version' field \u2014 it may not be a valid project snapshot.`);
30888
31037
  }
30889
- const systemOrProject = obj["system"] ?? obj["project"];
30890
- 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
+ }
30891
31043
  throw new Error(`Snapshot${sourceHint} is missing 'system.systemId' \u2014 it may not be a valid project snapshot.`);
30892
31044
  }
30893
31045
  }
@@ -31497,7 +31649,7 @@ ${t.acceptanceCriteria}` : null,
31497
31649
  };
31498
31650
  server.tool(
31499
31651
  "create_track",
31500
- '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.',
31501
31653
  createTrackSchema,
31502
31654
  createTrackHandler
31503
31655
  );
@@ -31937,24 +32089,47 @@ function registerValueVelocityTools(server, ctx, client) {
31937
32089
  );
31938
32090
  }
31939
32091
 
31940
- // ../../libs/harmonica-services/src/mcp/tools/work-item-tools.ts
31941
- var WORK_ITEM_STATUS_ENUM = ["not_started", "in_progress", "blocked", "done"];
31942
- function formatOwner(owner) {
31943
- return owner ? `${owner.name} <${owner.email}>` : "unassigned";
31944
- }
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
+ ];
31945
32101
  var WORK_ITEM_EFFORT_SIZE_ENUM = [
31946
32102
  "XS",
31947
32103
  "S",
31948
32104
  "M",
31949
32105
  "L",
31950
- "XL"
32106
+ "XL",
32107
+ "XXL"
31951
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
+ }
31952
32127
  function registerWorkItemTools(server, _ctx, client) {
31953
32128
  server.tool(
31954
32129
  "list_track_work_items",
31955
- '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.',
31956
32131
  {
31957
- trackId: external_exports.string().describe("The Track (DeliverableGroup) ID")
32132
+ trackId: external_exports.string().describe("The Track ID")
31958
32133
  },
31959
32134
  async ({ trackId }) => {
31960
32135
  const workItems = await client.listTrackWorkItems(trackId);
@@ -32000,15 +32175,15 @@ function registerWorkItemTools(server, _ctx, client) {
32000
32175
  );
32001
32176
  server.tool(
32002
32177
  "create_work_item",
32003
- '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).',
32004
32179
  {
32005
- 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"),
32006
32181
  title: external_exports.string().min(1).max(300).describe("Work item title"),
32007
32182
  ownerName: external_exports.string().min(1).optional().describe("Optional owner display name (requires ownerEmail too)"),
32008
32183
  ownerEmail: external_exports.string().email().optional().describe("Optional owner email (requires ownerName too). A work item may be left unowned."),
32009
32184
  committedEstimateHours: external_exports.number().nonnegative().optional().describe("Optional committed estimate in hours"),
32010
- effortSize: external_exports.enum(WORK_ITEM_EFFORT_SIZE_ENUM).optional().describe("Optional rough t-shirt effort size (delivery lens) \u2014 independent of valueUnits"),
32011
- 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}`),
32012
32187
  costPerValueUnit: external_exports.number().nonnegative().optional().describe("Optional price per Value Unit"),
32013
32188
  createdBy: external_exports.string().optional().describe("Optional creator identity for audit"),
32014
32189
  beatVersionId: external_exports.string().min(1).optional().describe("Optional Beat Version to link for capability lineage. Must exist."),
@@ -32056,7 +32231,9 @@ function registerWorkItemTools(server, _ctx, client) {
32056
32231
  unassignOwner: external_exports.boolean().optional().describe("Pass true to clear the owner, leaving the work item unowned. Cannot be combined with ownerName/ownerEmail."),
32057
32232
  committedEstimateHours: external_exports.number().nonnegative().optional().describe("New committed estimate in hours"),
32058
32233
  actualHours: external_exports.number().nonnegative().optional().describe("New actual hours logged"),
32059
- 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
+ ),
32060
32237
  valueUnits: external_exports.number().nonnegative().nullable().optional().describe("Value Units (commercial lens). Pass null to clear; omit to leave untouched."),
32061
32238
  costPerValueUnit: external_exports.number().nonnegative().nullable().optional().describe("Price per Value Unit. Pass null to clear; omit to leave untouched."),
32062
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."),
@@ -32117,10 +32294,10 @@ function registerWorkItemTools(server, _ctx, client) {
32117
32294
  );
32118
32295
  server.tool(
32119
32296
  "move_work_item",
32120
- "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.",
32121
32298
  {
32122
32299
  workItemId: external_exports.string().describe("The work item ID"),
32123
- newTrackId: external_exports.string().describe("The destination Track (DeliverableGroup) ID. Must exist.")
32300
+ newTrackId: external_exports.string().describe("The destination Track ID. Must exist.")
32124
32301
  },
32125
32302
  async ({ workItemId, newTrackId }) => {
32126
32303
  const wi = await client.moveWorkItem(workItemId, newTrackId);
@@ -32163,13 +32340,12 @@ function registerWorkItemTools(server, _ctx, client) {
32163
32340
  }
32164
32341
 
32165
32342
  // ../../libs/harmonica-services/src/tool-schemas/work-order.ts
32166
- var WORK_ORDER_SIZE_ENUM = ["XS", "S", "M", "L", "XL"];
32167
32343
  var create_work_order = {
32168
32344
  workRequestId: external_exports.string().min(1).describe("The Work Request this order was split from \u2014 immutable lineage"),
32169
32345
  title: external_exports.string().min(1).max(300).describe("Work order title"),
32170
32346
  description: external_exports.string().max(1e4).optional().describe("Optional detail beyond the title"),
32171
32347
  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."
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
32173
32349
  ),
32174
32350
  createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
32175
32351
  };
@@ -32192,13 +32368,6 @@ var NEEDS_DECISION_REASON_ENUM = [
32192
32368
  "exceeds_capacity",
32193
32369
  "incoherent_with_decision"
32194
32370
  ];
32195
- var WORK_ORDER_SIZE_ENUM2 = [
32196
- "XS",
32197
- "S",
32198
- "M",
32199
- "L",
32200
- "XL"
32201
- ];
32202
32371
  function oneLine(value) {
32203
32372
  return value.replace(/[\r\n]+/g, " ");
32204
32373
  }
@@ -32214,14 +32383,14 @@ function registerWorkOrderTools(server, _ctx, client) {
32214
32383
  // exactly the caller this alias exists to greet. Requiring it would answer
32215
32384
  // the migrated caller with a schema error instead of the migration message.
32216
32385
  {
32217
- 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")
32218
32387
  },
32219
32388
  async () => {
32220
32389
  return {
32221
32390
  content: [
32222
32391
  {
32223
32392
  type: "text",
32224
- 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."
32225
32394
  }
32226
32395
  ],
32227
32396
  // The caller asked for a list and is getting none. A result without
@@ -32306,7 +32475,9 @@ function registerWorkOrderTools(server, _ctx, client) {
32306
32475
  workOrderId: external_exports.string().describe("The Work Order ID"),
32307
32476
  title: external_exports.string().min(1).max(300).optional().describe("New title"),
32308
32477
  description: external_exports.string().max(1e4).nullable().optional().describe("New description. Pass null to clear; 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.")
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
+ )
32310
32481
  },
32311
32482
  async ({ workOrderId, title, description, size }) => {
32312
32483
  if (title === void 0 && description === void 0 && size === void 0) {
@@ -32336,7 +32507,7 @@ function registerWorkOrderTools(server, _ctx, client) {
32336
32507
  // the callers who have already stopped sending these.
32337
32508
  {
32338
32509
  workOrderId: external_exports.string().optional().describe("Ignored \u2014 a Work Order is no longer moveable between Tracks"),
32339
- 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")
32340
32511
  },
32341
32512
  async () => {
32342
32513
  return {
@@ -32538,6 +32709,14 @@ function registerWorkRequestTools(server, _ctx, client) {
32538
32709
  );
32539
32710
  }
32540
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
+
32541
32720
  // ../../libs/harmonica-services/src/mcp/tools/workflow-tools.ts
32542
32721
  var TASK_STATUSES = ["pending", "running", "completed", "failed"];
32543
32722
  function registerWorkflowTools(server, ctx, client) {
@@ -32728,17 +32907,19 @@ ${JSON.stringify(task.result, null, 2)}
32728
32907
  systemId: external_exports.string().min(1).describe("The system ID \u2014 the trusted scope the session is rooted at"),
32729
32908
  message: external_exports.string().min(1).describe("Free-form first message describing what the session should do"),
32730
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."),
32731
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.")
32732
32912
  },
32733
- async ({ systemId, message, revisionId, parentSessionId }) => {
32913
+ async ({ systemId, message, revisionId, workItemIds, parentSessionId }) => {
32734
32914
  try {
32735
32915
  await assertSystemExists(client, systemId);
32736
32916
  const task = await client.startSession(
32737
32917
  systemId,
32738
32918
  message,
32739
32919
  { name: ctx.user.name, email: ctx.user.email },
32740
- revisionId?.trim() || parentSessionId?.trim() ? {
32920
+ revisionId?.trim() || workItemIds?.length || parentSessionId?.trim() ? {
32741
32921
  ...revisionId?.trim() && { revisionId: revisionId.trim() },
32922
+ ...workItemIds?.length && { workItemIds: normalizeWorkItemIds(workItemIds) },
32742
32923
  ...parentSessionId?.trim() && { parentSessionId: parentSessionId.trim() }
32743
32924
  } : void 0
32744
32925
  );
@@ -32966,6 +33147,7 @@ var TOOL_PROFILES = Object.freeze({
32966
33147
  registerOrganizationTools,
32967
33148
  registerTeamspaceTools,
32968
33149
  registerMembershipTools,
33150
+ registerManagedRelationshipTools,
32969
33151
  registerSystemTools,
32970
33152
  registerSystemLifecycleTools,
32971
33153
  allow(registerBeatVersionTools, ["relocate_beat_version"])
@@ -33045,6 +33227,7 @@ function registerAllTools(server, ctx, client, profile) {
33045
33227
  registerValueVelocityTools(server, ctx, client);
33046
33228
  registerNotebookTools(server, ctx, client);
33047
33229
  registerAccountTools(server, ctx, client);
33230
+ registerManagedRelationshipTools(server, ctx, client);
33048
33231
  registerMovementTools(server, ctx, client);
33049
33232
  registerExternalMappingTools(server, ctx, client);
33050
33233
  registerWorkItemTools(server, ctx, client);
@@ -34387,6 +34570,7 @@ function createHttpClient(config2) {
34387
34570
  // Agent sessions
34388
34571
  startSession: async (systemId, message, triggeredBy, options) => {
34389
34572
  const trimmedRevisionId = options?.revisionId?.trim();
34573
+ const workItemIds = normalizeWorkItemIds(options?.workItemIds);
34390
34574
  const trimmedParentSessionId = options?.parentSessionId?.trim();
34391
34575
  const result = await request(
34392
34576
  "POST",
@@ -34396,6 +34580,7 @@ function createHttpClient(config2) {
34396
34580
  message,
34397
34581
  ...triggeredBy && { triggeredBy },
34398
34582
  ...trimmedRevisionId && { revisionId: trimmedRevisionId },
34583
+ ...workItemIds?.length && { workItemIds },
34399
34584
  ...trimmedParentSessionId && { parentSessionId: trimmedParentSessionId }
34400
34585
  }
34401
34586
  );
@@ -35034,6 +35219,31 @@ function createHttpClient(config2) {
35034
35219
  const res = await request("GET", `/api/accounts/${encodeURIComponent(accountId)}/movements`);
35035
35220
  return res?.movements ?? [];
35036
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
+ },
35037
35247
  listChildAccounts: async (accountId) => {
35038
35248
  const res = await request("GET", `/api/accounts/${encodeURIComponent(accountId)}/children`);
35039
35249
  return res?.accounts ?? [];
@@ -35553,7 +35763,7 @@ function loadConfig() {
35553
35763
  };
35554
35764
  }
35555
35765
  async function main() {
35556
- console.error(`[harmonica-mcp] v${"5.2.0"} starting\u2026`);
35766
+ console.error(`[harmonica-mcp] v${"5.3.0"} starting\u2026`);
35557
35767
  const config2 = loadConfig();
35558
35768
  const client = createHttpClient({
35559
35769
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "5.2.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": {