@codazen/harmonica-mcp 4.4.2 → 5.0.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 +1768 -1497
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -21584,12 +21584,12 @@ async function assertMovementExists(client, movementId) {
21584
21584
  throw new Error(`Movement not found: "${movementId}"`);
21585
21585
  }
21586
21586
  }
21587
- async function fetchDeliverableGroup(client, deliverableGroupId) {
21588
- const group = await client.getDeliverableGroup(deliverableGroupId);
21589
- if (!group) {
21590
- throw new Error(`DeliverableGroup not found: "${deliverableGroupId}"`);
21587
+ async function fetchTrack(client, trackId) {
21588
+ const track = await client.getTrack(trackId);
21589
+ if (!track) {
21590
+ throw new Error(`Track not found: "${trackId}"`);
21591
21591
  }
21592
- return group;
21592
+ return track;
21593
21593
  }
21594
21594
  async function fetchNotebook(client, notebookId) {
21595
21595
  const notebook = await client.getNotebook(notebookId);
@@ -24101,260 +24101,6 @@ function registerClaimTools(server, ctx, client) {
24101
24101
  );
24102
24102
  }
24103
24103
 
24104
- // ../../libs/harmonica-services/src/mcp/tools/deliverable-group-tools.ts
24105
- function registerDeliverableGroupTools(server, ctx, client) {
24106
- server.tool(
24107
- "list_deliverable_groups",
24108
- "List all DeliverableGroups for a Teamspace. A DeliverableGroup is a scoped initiative \u2014 client SOW, internal phase, or engagement container \u2014 that organises Deliverables and Drops.",
24109
- {
24110
- teamspaceId: external_exports.string().describe("The teamspace ID")
24111
- },
24112
- async ({ teamspaceId }) => {
24113
- const groups = await client.listTeamspaceDeliverableGroups(teamspaceId);
24114
- const text = groups.length === 0 ? "No deliverable groups found." : groups.map(
24115
- (g) => `[${g.deliverableGroupId}] ${g.groupCode} \u2014 ${g.title} (${g.status})`
24116
- ).join("\n");
24117
- return { content: [{ type: "text", text }] };
24118
- }
24119
- );
24120
- server.tool(
24121
- "list_movement_deliverable_groups",
24122
- `List the DeliverableGroups scoped to a Movement (client SOW). Returns the commitments grouped under that engagement. Recognition cues: "what's in this SOW", "this engagement's deliverable groups" \u2192 list_movement_deliverable_groups.`,
24123
- {
24124
- movementId: external_exports.string().describe("The Movement ID (e.g., mv-abc123)")
24125
- },
24126
- async ({ movementId }) => {
24127
- await assertMovementExists(client, movementId);
24128
- const groups = await client.listMovementDeliverableGroups(movementId);
24129
- const text = groups.length === 0 ? "No deliverable groups found for this Movement." : groups.map(
24130
- (g) => `[${g.deliverableGroupId}] ${g.groupCode} \u2014 ${g.title} (${g.status})`
24131
- ).join("\n");
24132
- return { content: [{ type: "text", text }] };
24133
- }
24134
- );
24135
- server.tool(
24136
- "get_deliverable_group",
24137
- "Get a DeliverableGroup by ID. Returns full details including commercial lifecycle status, VU rate, and audit stamps.",
24138
- {
24139
- deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID")
24140
- },
24141
- async ({ deliverableGroupId }) => {
24142
- const g = await client.getDeliverableGroup(deliverableGroupId);
24143
- if (!g) {
24144
- return {
24145
- content: [{ type: "text", text: `DeliverableGroup not found: ${deliverableGroupId}` }]
24146
- };
24147
- }
24148
- const lines = [
24149
- `ID: ${g.deliverableGroupId}`,
24150
- `Code: ${g.groupCode}`,
24151
- `Title: ${g.title}`,
24152
- `Teamspace: ${g.teamspaceId}`,
24153
- `Status: ${g.status}`,
24154
- `VU Rate: ${g.vuRate}`,
24155
- g.description ? `Description: ${g.description}` : null,
24156
- g.deliverables ? `Deliverables:
24157
- ${g.deliverables}` : null,
24158
- g.acceptanceCriteria ? `Acceptance Criteria:
24159
- ${g.acceptanceCriteria}` : null,
24160
- g.parentGroupId ? `Parent Group: ${g.parentGroupId}` : null,
24161
- g.proposedAt ? `Proposed: ${g.proposedAt}` : null,
24162
- g.approvedAt ? `Approved: ${g.approvedAt}` : null,
24163
- `Version: ${g.version}`,
24164
- `Created: ${g.createdAt}`,
24165
- `Updated: ${g.updatedAt}`
24166
- ].filter(Boolean).join("\n");
24167
- return { content: [{ type: "text", text: lines }] };
24168
- }
24169
- );
24170
- const createTrackSchema = {
24171
- teamspaceId: external_exports.string().describe("The teamspace ID"),
24172
- title: external_exports.string().min(1).max(300).describe('Track title (e.g. "SOW-2026-Q2 / Platform Modernisation")'),
24173
- description: external_exports.string().max(2e3).optional().describe("Optional description"),
24174
- deliverables: external_exports.string().max(2e4).optional().describe("The Track's deliverables as Markdown bullet points (parallel to acceptanceCriteria)"),
24175
- acceptanceCriteria: external_exports.string().max(2e4).optional().describe("The Track's acceptance criteria (Markdown), parallel to its deliverables"),
24176
- vuRate: external_exports.number().nonnegative().describe("Value-Unit rate (billing rate for this engagement)"),
24177
- systemId: external_exports.string().optional().describe("Optional System to scope this Track to"),
24178
- movementId: external_exports.string().optional().describe("Optional Movement (client SOW) to scope this Track to"),
24179
- parentGroupId: external_exports.string().optional().describe("Optional parent Track ID for nested grouping (Phase 0 scaffolding)")
24180
- };
24181
- const createTrackHandler = async ({ teamspaceId, title, description, deliverables, acceptanceCriteria, vuRate, systemId, movementId, parentGroupId }) => {
24182
- const teamspace = await fetchTeamspace(client, teamspaceId);
24183
- if (movementId !== void 0) {
24184
- await assertMovementExists(client, movementId);
24185
- }
24186
- if (parentGroupId !== void 0) {
24187
- await fetchDeliverableGroup(client, parentGroupId);
24188
- }
24189
- const g = await client.createDeliverableGroup({
24190
- teamspaceId,
24191
- orgId: teamspace.orgId,
24192
- title,
24193
- ...description !== void 0 && { description },
24194
- ...deliverables !== void 0 && { deliverables },
24195
- ...acceptanceCriteria !== void 0 && { acceptanceCriteria },
24196
- vuRate,
24197
- ...systemId !== void 0 && { systemId },
24198
- ...movementId !== void 0 && { movementId },
24199
- ...parentGroupId !== void 0 && { parentGroupId },
24200
- createdBy: ctx.user.userId
24201
- });
24202
- return {
24203
- content: [
24204
- {
24205
- type: "text",
24206
- text: `Created Track: [${g.deliverableGroupId}] ${g.groupCode} \u2014 ${g.title} (${g.status})`
24207
- }
24208
- ]
24209
- };
24210
- };
24211
- server.tool(
24212
- "create_track",
24213
- 'Create a Track (a DeliverableGroup) in a Teamspace. 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.',
24214
- createTrackSchema,
24215
- createTrackHandler
24216
- );
24217
- server.tool(
24218
- "create_deliverable_group",
24219
- "DEPRECATED alias for create_track \u2014 use create_track instead. Creates a Track (DeliverableGroup) in a Teamspace.",
24220
- createTrackSchema,
24221
- createTrackHandler
24222
- );
24223
- server.tool(
24224
- "update_deliverable_group",
24225
- "Update mutable content fields on a DeliverableGroup / Track (title, description, deliverables, acceptanceCriteria, vuRate). To change commercial status use transition_deliverable_group instead.",
24226
- {
24227
- deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID"),
24228
- title: external_exports.string().min(1).max(300).optional().describe("New title"),
24229
- description: external_exports.string().max(2e3).optional().describe("New description"),
24230
- deliverables: external_exports.string().max(2e4).optional().describe(`Replace the Track's deliverables Markdown ("" clears)`),
24231
- acceptanceCriteria: external_exports.string().max(2e4).optional().describe(`Replace the Track's acceptance-criteria Markdown ("" clears)`),
24232
- vuRate: external_exports.number().nonnegative().optional().describe("New VU rate")
24233
- },
24234
- async ({ deliverableGroupId, title, description, deliverables, acceptanceCriteria, vuRate }) => {
24235
- const g = await client.updateDeliverableGroup(deliverableGroupId, {
24236
- ...title !== void 0 && { title },
24237
- ...description !== void 0 && { description },
24238
- ...deliverables !== void 0 && { deliverables },
24239
- ...acceptanceCriteria !== void 0 && { acceptanceCriteria },
24240
- ...vuRate !== void 0 && { vuRate }
24241
- });
24242
- if (!g) {
24243
- return {
24244
- content: [{ type: "text", text: `DeliverableGroup not found: ${deliverableGroupId}` }]
24245
- };
24246
- }
24247
- return {
24248
- content: [
24249
- {
24250
- type: "text",
24251
- text: `Updated DeliverableGroup: [${g.deliverableGroupId}] ${g.groupCode} \u2014 ${g.title} (${g.status})`
24252
- }
24253
- ]
24254
- };
24255
- }
24256
- );
24257
- server.tool(
24258
- "transition_deliverable_group",
24259
- 'Transition a DeliverableGroup 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.',
24260
- {
24261
- deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID"),
24262
- targetState: external_exports.enum(["proposed", "approved", "active", "fulfilled", "cancelled"]).describe("Target lifecycle state"),
24263
- approvalNoteId: external_exports.string().optional().describe('Required when transitioning to "approved" \u2014 the Note ID capturing the approval decision'),
24264
- reason: external_exports.string().optional().describe("Reason for the transition (especially useful for cancellation)"),
24265
- confirm: external_exports.boolean().optional().default(false).describe("false (default) = dry-run preview; true = execute the transition")
24266
- },
24267
- async ({ deliverableGroupId, targetState, approvalNoteId, reason, confirm }) => {
24268
- if (!confirm) {
24269
- return {
24270
- content: [
24271
- {
24272
- type: "text",
24273
- text: `Dry-run: would transition DeliverableGroup ${deliverableGroupId} \u2192 ${targetState}${approvalNoteId ? ` (approvalNoteId: ${approvalNoteId})` : ""}${reason ? ` \u2014 ${reason}` : ""}. Call again with confirm=true to execute.`
24274
- }
24275
- ]
24276
- };
24277
- }
24278
- const g = await client.transitionDeliverableGroupStatus(
24279
- deliverableGroupId,
24280
- targetState,
24281
- {
24282
- ...approvalNoteId !== void 0 && { approvalNoteId },
24283
- ...reason !== void 0 && { reason }
24284
- }
24285
- );
24286
- if (!g) {
24287
- return {
24288
- content: [{ type: "text", text: `DeliverableGroup not found: ${deliverableGroupId}` }]
24289
- };
24290
- }
24291
- return {
24292
- content: [
24293
- {
24294
- type: "text",
24295
- text: `Transitioned DeliverableGroup [${g.deliverableGroupId}] ${g.groupCode} \u2192 ${g.status}`
24296
- }
24297
- ]
24298
- };
24299
- }
24300
- );
24301
- server.tool(
24302
- "list_deliverable_group_drops",
24303
- 'List all Drops linked to a DeliverableGroup. Use to answer "what releases are part of this phase?" or "which software drops are tied to this engagement milestone?"',
24304
- {
24305
- deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID")
24306
- },
24307
- async ({ deliverableGroupId }) => {
24308
- const items = await client.listDeliverableGroupDrops(deliverableGroupId);
24309
- const text = items.length === 0 ? "No drops linked to this group." : items.map((d) => `[${d.dropId}] account: ${d.accountId}`).join("\n");
24310
- return { content: [{ type: "text", text }] };
24311
- }
24312
- );
24313
- server.tool(
24314
- "assign_drop_to_deliverable_group",
24315
- `Link a Drop to a DeliverableGroup so it appears on the parent Movement's timed-release axis (the Drops shown on the Movement overview). A Drop belongs to at most one group; linking moves it if it was in another group. Use to answer "put this release under this SOW/phase" or "add a Drop to this Movement".`,
24316
- {
24317
- deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID"),
24318
- dropId: external_exports.string().describe("The Drop ID to link to the group")
24319
- },
24320
- async ({ deliverableGroupId, dropId }) => {
24321
- await fetchDeliverableGroup(client, deliverableGroupId);
24322
- const drop = await client.getDrop(dropId);
24323
- if (!drop) {
24324
- return { content: [{ type: "text", text: `Drop not found: ${dropId}` }] };
24325
- }
24326
- await assertAccountExists(client, drop.accountId);
24327
- const ok = await client.setDropDeliverableGroup(deliverableGroupId, dropId);
24328
- if (!ok) {
24329
- return { content: [{ type: "text", text: `Drop not found: ${dropId}` }] };
24330
- }
24331
- return {
24332
- content: [{ type: "text", text: `Linked Drop ${dropId} to DeliverableGroup ${deliverableGroupId}.` }]
24333
- };
24334
- }
24335
- );
24336
- server.tool(
24337
- "list_deliverable_group_beat_versions",
24338
- 'List the Beat Versions linked to a DeliverableGroup via its work items (Work Plan). Use to answer "what feature increments are scoped under this SOW/phase?"',
24339
- {
24340
- deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID")
24341
- },
24342
- async ({ deliverableGroupId }) => {
24343
- await fetchDeliverableGroup(client, deliverableGroupId);
24344
- const workItems = await client.listTrackWorkItems(deliverableGroupId);
24345
- const bvIds = [...new Set(
24346
- workItems.map((wi) => wi.beatVersionId).filter((id) => id !== void 0)
24347
- )];
24348
- if (bvIds.length === 0) {
24349
- return { content: [{ type: "text", text: "No beat versions linked via work items in this group." }] };
24350
- }
24351
- const hydrated = await Promise.all(bvIds.map((id) => client.getBeatVersion(id)));
24352
- const text = hydrated.filter((bv) => bv !== void 0).map((bv) => `[${bv.beatVersionId}] v${bv.versionNumber} \u2014 ${bv.title} (${bv.status})`).join("\n");
24353
- return { content: [{ type: "text", text }] };
24354
- }
24355
- );
24356
- }
24357
-
24358
24104
  // ../../libs/harmonica-services/src/mcp/tools/downbeat-harmony-report-tools.ts
24359
24105
  function formatQualifyingBeat(b) {
24360
24106
  return `- \`${b.beatId}\` ${b.title}: ${b.signals.join(", ")}`;
@@ -25100,1147 +24846,371 @@ ${members}${parent}`;
25100
24846
  );
25101
24847
  }
25102
24848
 
25103
- // ../../libs/harmonica-services/src/mcp/tools/feature-flag-tools.ts
25104
- var STAGE_ENUM = external_exports.enum([
25105
- "local",
25106
- "staging",
25107
- "staging-alpha",
25108
- "staging-beta",
25109
- "alpha",
25110
- "beta",
25111
- "prod"
25112
- ]);
25113
- function registerFeatureFlagTools(server, ctx, client) {
25114
- server.tool(
25115
- "list_flags",
25116
- "List feature flags and their enabled stages. Use check_flag to test a single flag against a specific stage, or get_flag for full detail on one flag.",
25117
- {
25118
- includeArchived: external_exports.boolean().optional().describe("Include archived flags. Default false"),
25119
- search: external_exports.string().optional().describe("Case-insensitive search against flag name and description"),
25120
- cursor: external_exports.string().optional().describe("Pagination cursor from a prior list_flags result"),
25121
- limit: external_exports.number().int().min(1).max(100).optional().describe("Flags per page. Default 20")
25122
- },
25123
- async ({ includeArchived, search, cursor, limit }) => {
25124
- try {
25125
- const page = await client.listFlags({ includeArchived, search, cursor, limit });
25126
- if (page.flags.length === 0) {
25127
- return { content: [{ type: "text", text: "No flags found." }] };
25128
- }
25129
- return { content: [{ type: "text", text: formatFlagList(page.flags, page.total, page.nextCursor) }] };
25130
- } catch (err) {
25131
- const message = err instanceof Error ? err.message : String(err);
25132
- return { content: [{ type: "text", text: `Failed to list flags: ${message}` }], isError: true };
25133
- }
25134
- }
25135
- );
25136
- server.tool(
25137
- "get_flag",
25138
- "Get full detail for a single feature flag by name, including its enabled stages and optional change history.",
25139
- {
25140
- name: external_exports.string().describe("Flag name"),
25141
- includeHistory: external_exports.boolean().optional().describe("Include change history entries. Default false")
25142
- },
25143
- async ({ name, includeHistory }) => {
25144
- try {
25145
- const flag = await client.getFlag(name, { includeHistory });
25146
- if (!flag) {
25147
- return { content: [{ type: "text", text: `Flag "${name}" not found.` }] };
25148
- }
25149
- return { content: [{ type: "text", text: formatFlag(flag, flag.history) }] };
25150
- } catch (err) {
25151
- const message = err instanceof Error ? err.message : String(err);
25152
- return { content: [{ type: "text", text: `Failed to get flag: ${message}` }], isError: true };
25153
- }
25154
- }
25155
- );
25156
- server.tool(
25157
- "check_flag",
25158
- "Check whether a feature flag is enabled in a specific stage. Returns a simple enabled/disabled answer.",
25159
- {
25160
- name: external_exports.string().describe("Flag name"),
25161
- stage: STAGE_ENUM.describe("Deployment stage to check against")
25162
- },
25163
- async ({ name, stage }) => {
25164
- try {
25165
- const enabled = await client.checkFlag(name, stage);
25166
- const status = enabled ? "enabled" : "disabled";
25167
- return {
25168
- content: [{
25169
- type: "text",
25170
- text: `Flag "${name}" is **${status}** in stage "${stage}".`
25171
- }]
25172
- };
25173
- } catch (err) {
25174
- const message = err instanceof Error ? err.message : String(err);
25175
- return { content: [{ type: "text", text: `Failed to check flag: ${message}` }], isError: true };
25176
- }
25177
- }
25178
- );
25179
- server.tool(
25180
- "create_flag",
25181
- "Create or update a feature flag with the specified enabled stages. Idempotent \u2014 calling with an existing flag name replaces its enabledIn list.",
25182
- {
25183
- name: external_exports.string().min(1).describe('Flag name (e.g. "rbac:enforcement")'),
25184
- enabledIn: external_exports.array(STAGE_ENUM).describe("Stages to enable the flag in"),
25185
- description: external_exports.string().optional().describe("Human-readable description of what this flag gates"),
25186
- note: external_exports.string().optional().describe("Reason for this change (recorded in change history)")
25187
- },
25188
- async ({ name, enabledIn, description, note }) => {
25189
- if (!ctx.user.email) {
25190
- return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
25191
- }
25192
- try {
25193
- const flag = await client.createFlag(name, enabledIn, { description, note, updatedBy: ctx.user.email });
25194
- return { content: [{ type: "text", text: formatFlag(flag) }] };
25195
- } catch (err) {
25196
- const message = err instanceof Error ? err.message : String(err);
25197
- return { content: [{ type: "text", text: `Failed to create flag: ${message}` }], isError: true };
25198
- }
25199
- }
25200
- );
25201
- server.tool(
25202
- "set_flag",
25203
- "Update which stages a feature flag is enabled in. Replaces the current enabledIn list entirely.",
25204
- {
25205
- name: external_exports.string().min(1).describe("Flag name"),
25206
- enabledIn: external_exports.array(STAGE_ENUM).describe("New set of stages to enable the flag in. Pass [] to disable everywhere without archiving."),
25207
- note: external_exports.string().optional().describe("Reason for this change (recorded in change history)")
25208
- },
25209
- async ({ name, enabledIn, note }) => {
25210
- if (!ctx.user.email) {
25211
- return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
25212
- }
25213
- try {
25214
- const flag = await client.setFlag(name, enabledIn, { note, updatedBy: ctx.user.email });
25215
- return { content: [{ type: "text", text: formatFlag(flag) }] };
25216
- } catch (err) {
25217
- const message = err instanceof Error ? err.message : String(err);
25218
- return { content: [{ type: "text", text: `Failed to set flag: ${message}` }], isError: true };
25219
- }
25220
- }
25221
- );
25222
- server.tool(
25223
- "archive_flag",
25224
- "Archive a feature flag. Clears its enabledIn list and marks it archived so it cannot be re-enabled via set_flag. Use unarchive_flag to restore it.",
25225
- {
25226
- name: external_exports.string().min(1).describe("Flag name"),
25227
- note: external_exports.string().optional().describe("Reason for archiving (recorded in change history)")
25228
- },
25229
- async ({ name, note }) => {
25230
- if (!ctx.user.email) {
25231
- return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
25232
- }
25233
- try {
25234
- const flag = await client.archiveFlag(name, { note, updatedBy: ctx.user.email });
25235
- return { content: [{ type: "text", text: formatFlag(flag) }] };
25236
- } catch (err) {
25237
- const message = err instanceof Error ? err.message : String(err);
25238
- return { content: [{ type: "text", text: `Failed to archive flag: ${message}` }], isError: true };
25239
- }
25240
- }
25241
- );
25242
- server.tool(
25243
- "unarchive_flag",
25244
- "Restore an archived feature flag to operable state. The flag is re-enabled with an empty enabledIn list \u2014 use set_flag afterward to enable it in specific stages.",
25245
- {
25246
- name: external_exports.string().min(1).describe("Flag name"),
25247
- note: external_exports.string().optional().describe("Reason for restoring (recorded in change history)")
25248
- },
25249
- async ({ name, note }) => {
25250
- if (!ctx.user.email) {
25251
- return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
25252
- }
25253
- try {
25254
- const flag = await client.unarchiveFlag(name, { note, updatedBy: ctx.user.email });
25255
- return { content: [{ type: "text", text: formatFlag(flag) }] };
25256
- } catch (err) {
25257
- const message = err instanceof Error ? err.message : String(err);
25258
- return { content: [{ type: "text", text: `Failed to unarchive flag: ${message}` }], isError: true };
25259
- }
25260
- }
25261
- );
25262
- }
25263
- function formatFlag(flag, history) {
25264
- const archived = flag.archivedAt ? " **(archived)**" : "";
25265
- const enabled = flag.enabledIn.length > 0 ? flag.enabledIn.join(", ") : "(none)";
25266
- const parts = [
25267
- `# Flag: ${flag.name}${archived}`,
25268
- `**Scope:** ${flag.scope}`,
25269
- `**Enabled in:** ${enabled}`,
25270
- flag.description ? `**Description:** ${flag.description}` : null,
25271
- `**Last updated:** ${flag.updatedAt.slice(0, 10)} by ${flag.updatedBy}`,
25272
- flag.archivedAt ? `**Archived:** ${flag.archivedAt.slice(0, 10)} by ${flag.archivedBy}` : null
25273
- ].filter(Boolean).join("\n");
25274
- if (!history?.length) return parts;
25275
- const historyLines = history.map((h) => {
25276
- const prev = h.previousEnabledIn.length > 0 ? h.previousEnabledIn.join(", ") : "(none)";
25277
- const next = h.newEnabledIn.length > 0 ? h.newEnabledIn.join(", ") : "(none)";
25278
- const note = h.note ? ` \u2014 ${h.note}` : "";
25279
- return `- ${h.updatedAt.slice(0, 10)} ${h.updatedBy}: ${prev} \u2192 ${next}${note}`;
25280
- }).join("\n");
25281
- return `${parts}
24849
+ // ../../libs/harmonica-schemas/src/azure-marketplace.ts
24850
+ var AzureMarketplaceSubscriptionSchema = external_exports.object({
24851
+ id: external_exports.string(),
24852
+ publisherId: external_exports.string().optional(),
24853
+ offerId: external_exports.string().optional(),
24854
+ name: external_exports.string().optional(),
24855
+ saasSubscriptionStatus: external_exports.string().optional(),
24856
+ planId: external_exports.string().optional(),
24857
+ quantity: external_exports.number().optional(),
24858
+ beneficiary: external_exports.object({
24859
+ emailId: external_exports.string().optional(),
24860
+ objectId: external_exports.string().optional(),
24861
+ tenantId: external_exports.string().optional()
24862
+ }).optional(),
24863
+ purchaser: external_exports.object({
24864
+ emailId: external_exports.string().optional(),
24865
+ objectId: external_exports.string().optional(),
24866
+ tenantId: external_exports.string().optional()
24867
+ }).optional()
24868
+ });
24869
+ var AzureSubscribedSchema = external_exports.object({
24870
+ action: external_exports.literal("Subscribe"),
24871
+ // Non-blank, not merely non-empty. A bare z.string() accepts "", which would
24872
+ // derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
24873
+ // slug invariant. And .min(1) alone still admits " ", which normalizes to
24874
+ // nothing and falls through to the hash fallback, quietly provisioning an org
24875
+ // under a hashed slug for input that is plainly garbage. Reject it at the
24876
+ // boundary instead.
24877
+ subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
24878
+ message: "subscriptionId must not be blank"
24879
+ }),
24880
+ offerId: external_exports.string(),
24881
+ planId: external_exports.string(),
24882
+ quantity: external_exports.number().optional(),
24883
+ timeStamp: external_exports.string(),
24884
+ operationId: external_exports.string(),
24885
+ subscription: AzureMarketplaceSubscriptionSchema
24886
+ });
24887
+ var AzureUnsubscribedSchema = external_exports.object({
24888
+ action: external_exports.literal("Unsubscribe"),
24889
+ // Non-blank, not merely non-empty. A bare z.string() accepts "", which would
24890
+ // derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
24891
+ // slug invariant. And .min(1) alone still admits " ", which normalizes to
24892
+ // nothing and falls through to the hash fallback, quietly provisioning an org
24893
+ // under a hashed slug for input that is plainly garbage. Reject it at the
24894
+ // boundary instead.
24895
+ subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
24896
+ message: "subscriptionId must not be blank"
24897
+ }),
24898
+ offerId: external_exports.string(),
24899
+ planId: external_exports.string(),
24900
+ timeStamp: external_exports.string(),
24901
+ operationId: external_exports.string(),
24902
+ subscription: AzureMarketplaceSubscriptionSchema
24903
+ });
24904
+ var AzureChangePlanSchema = external_exports.object({
24905
+ action: external_exports.literal("ChangePlan"),
24906
+ // Non-blank, not merely non-empty. A bare z.string() accepts "", which would
24907
+ // derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
24908
+ // slug invariant. And .min(1) alone still admits " ", which normalizes to
24909
+ // nothing and falls through to the hash fallback, quietly provisioning an org
24910
+ // under a hashed slug for input that is plainly garbage. Reject it at the
24911
+ // boundary instead.
24912
+ subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
24913
+ message: "subscriptionId must not be blank"
24914
+ }),
24915
+ offerId: external_exports.string(),
24916
+ planId: external_exports.string(),
24917
+ timeStamp: external_exports.string(),
24918
+ operationId: external_exports.string(),
24919
+ subscription: AzureMarketplaceSubscriptionSchema
24920
+ });
24921
+ var AzureSuspendSchema = external_exports.object({
24922
+ action: external_exports.literal("Suspend"),
24923
+ // Non-blank, not merely non-empty. A bare z.string() accepts "", which would
24924
+ // derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
24925
+ // slug invariant. And .min(1) alone still admits " ", which normalizes to
24926
+ // nothing and falls through to the hash fallback, quietly provisioning an org
24927
+ // under a hashed slug for input that is plainly garbage. Reject it at the
24928
+ // boundary instead.
24929
+ subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
24930
+ message: "subscriptionId must not be blank"
24931
+ }),
24932
+ offerId: external_exports.string(),
24933
+ planId: external_exports.string(),
24934
+ timeStamp: external_exports.string(),
24935
+ operationId: external_exports.string(),
24936
+ subscription: AzureMarketplaceSubscriptionSchema
24937
+ });
24938
+ var AzureReinstateSchema = external_exports.object({
24939
+ action: external_exports.literal("Reinstate"),
24940
+ // Non-blank, not merely non-empty. A bare z.string() accepts "", which would
24941
+ // derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
24942
+ // slug invariant. And .min(1) alone still admits " ", which normalizes to
24943
+ // nothing and falls through to the hash fallback, quietly provisioning an org
24944
+ // under a hashed slug for input that is plainly garbage. Reject it at the
24945
+ // boundary instead.
24946
+ subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
24947
+ message: "subscriptionId must not be blank"
24948
+ }),
24949
+ offerId: external_exports.string(),
24950
+ planId: external_exports.string(),
24951
+ timeStamp: external_exports.string(),
24952
+ operationId: external_exports.string(),
24953
+ subscription: AzureMarketplaceSubscriptionSchema
24954
+ });
24955
+ var AzureMarketplaceWebhookEventSchema = external_exports.discriminatedUnion("action", [
24956
+ AzureSubscribedSchema,
24957
+ AzureUnsubscribedSchema,
24958
+ AzureChangePlanSchema,
24959
+ AzureSuspendSchema,
24960
+ AzureReinstateSchema
24961
+ ]);
24962
+ var AZURE_MARKETPLACE_WEBHOOK_ACTIONS = AzureMarketplaceWebhookEventSchema.options.map(
24963
+ (o) => o.shape.action.value
24964
+ );
24965
+ var AzureMarketplaceActivateBodySchema = external_exports.object({
24966
+ token: external_exports.string().min(1)
24967
+ });
25282
24968
 
25283
- ## Change History
25284
- ${historyLines}`;
25285
- }
25286
- function formatFlagList(flags, total, nextCursor) {
25287
- const rows = flags.map((f) => {
25288
- const enabled = f.enabledIn.length > 0 ? f.enabledIn.join(", ") : "(none)";
25289
- const archived = f.archivedAt ? " *(archived)*" : "";
25290
- return `| ${f.name}${archived} | ${enabled} | ${f.updatedAt.slice(0, 10)} |`;
25291
- });
25292
- const table = [
25293
- `# Feature Flags (${total} total, showing ${flags.length})`,
25294
- "",
25295
- "| Name | Enabled In | Updated |",
25296
- "|---|---|---|",
25297
- ...rows
25298
- ].join("\n");
25299
- if (!nextCursor) return table;
25300
- return `${table}
24969
+ // ../../libs/harmonica-schemas/src/compose-beat-versions.ts
24970
+ var BeatVersionDraftSchema = external_exports.object({
24971
+ title: external_exports.string().min(1).max(300),
24972
+ description: external_exports.string().min(1).max(2e3),
24973
+ scope: external_exports.string().max(1e3).optional(),
24974
+ changeSummary: external_exports.string().min(1).max(500),
24975
+ changeImportance: external_exports.enum(["minor", "moderate", "major"]),
24976
+ priority: external_exports.number().int().min(1).optional(),
24977
+ tags: external_exports.array(external_exports.string()).optional(),
24978
+ estimatedEffort: external_exports.enum(["XS", "S", "M", "L", "XL"]).optional()
24979
+ });
24980
+ var LLMBeatVersionsCompositionOutputSchema = external_exports.object({
24981
+ drafts: external_exports.array(BeatVersionDraftSchema).min(2).max(5)
24982
+ });
25301
24983
 
25302
- More flags available. Pass \`cursor: "${nextCursor}"\` to list_flags to continue.`;
25303
- }
24984
+ // ../../libs/harmonica-schemas/src/note-extraction.ts
24985
+ var EXTRACTABLE_NOTE_TYPES = ["context", "constraint", "guidance", "decision"];
24986
+ var DECISION_SIGNIFICANCE_VALUES = ["strategic", "structural", "implementation"];
24987
+ var ExtractedNoteRawSchema = external_exports.object({
24988
+ noteType: external_exports.enum(EXTRACTABLE_NOTE_TYPES),
24989
+ content: external_exports.string().min(1),
24990
+ rationale: external_exports.string().optional(),
24991
+ confidence: external_exports.number().min(0).max(1),
24992
+ significance: external_exports.enum(DECISION_SIGNIFICANCE_VALUES).optional()
24993
+ });
24994
+ var NoteExtractionResponseSchema = external_exports.array(ExtractedNoteRawSchema);
25304
24995
 
25305
- // ../../libs/harmonica-services/src/mcp/tools/layer-tools.ts
25306
- function registerLayerTools(server, ctx, client) {
25307
- server.tool(
25308
- "list_system_layers",
25309
- "List the architectural Layers declared on a System (e.g. frontend, backend, data). Layers are an evaluation lens for plan/build quality checks, not a work container.",
25310
- {
25311
- systemId: external_exports.string().describe("The System ID")
25312
- },
25313
- async ({ systemId }) => {
25314
- try {
25315
- await assertSystemExists(client, systemId);
25316
- } catch (err) {
25317
- const message = err instanceof Error ? err.message : String(err);
25318
- return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
25319
- }
25320
- let layers;
25321
- try {
25322
- layers = await client.listSystemLayers(systemId);
25323
- } catch (err) {
25324
- const message = err instanceof Error ? err.message : String(err);
25325
- return { content: [{ type: "text", text: `Failed to list Layers: ${message}` }], isError: true };
25326
- }
25327
- const text = formatLayerList(layers);
25328
- return { content: [{ type: "text", text }] };
25329
- }
25330
- );
25331
- server.tool(
25332
- "get_layer",
25333
- "Get full details of a single Layer by ID, including its repo mapping and parent Layer (if it was split from one).",
25334
- {
25335
- systemId: external_exports.string().describe("The System ID (for access control)"),
25336
- layerId: external_exports.string().describe("The Layer ID")
25337
- },
25338
- async ({ systemId, layerId }) => {
25339
- try {
25340
- await assertSystemExists(client, systemId);
25341
- } catch (err) {
25342
- const message = err instanceof Error ? err.message : String(err);
25343
- return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
25344
- }
25345
- let layer;
25346
- try {
25347
- layer = await client.getLayer(layerId);
25348
- } catch (err) {
25349
- const message = err instanceof Error ? err.message : String(err);
25350
- return { content: [{ type: "text", text: `Failed to get Layer: ${message}` }], isError: true };
25351
- }
25352
- if (!layer) {
25353
- return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
25354
- }
25355
- if (layer.systemId !== systemId) {
25356
- return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
25357
- }
25358
- const text = formatLayerDetail(layer);
25359
- return { content: [{ type: "text", text }] };
25360
- }
25361
- );
25362
- server.tool(
25363
- "create_layer",
25364
- "Declare a new architectural Layer on a System (e.g. frontend, backend, data). Layers are an evaluation lens for plan/build quality checks. layerKey must be a lowercase slug and unique per System.",
25365
- {
25366
- systemId: external_exports.string().describe("The System ID"),
25367
- layerKey: external_exports.string().regex(/^[a-z]([a-z0-9-]*[a-z0-9])?$/, "layerKey must be a lowercase slug (letters, digits, hyphens; start with a letter, no trailing hyphen)").describe('Stable lowercase slug for the Layer role, e.g. "frontend" or "agent-workers"'),
25368
- name: external_exports.string().describe('Display name, e.g. "Frontend"'),
25369
- description: external_exports.string().optional().describe("Optional description of the Layer"),
25370
- parentLayerId: external_exports.string().optional().describe("Optional parent Layer to nest under (must be a Layer on the same System)"),
25371
- repoPatterns: external_exports.array(external_exports.string()).optional().describe("Optional repo path patterns owned by this Layer")
25372
- },
25373
- async ({ systemId, layerKey, name, description, parentLayerId, repoPatterns }) => {
25374
- try {
25375
- await assertSystemExists(client, systemId);
25376
- } catch (err) {
25377
- const message = err instanceof Error ? err.message : String(err);
25378
- return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
25379
- }
25380
- let layer;
25381
- try {
25382
- layer = await client.createSystemLayer({
25383
- systemId,
25384
- layerKey,
25385
- name,
25386
- ...description !== void 0 && { description },
25387
- ...parentLayerId !== void 0 && { parentLayerId },
25388
- ...repoPatterns !== void 0 && { repoPatterns }
25389
- });
25390
- } catch (err) {
25391
- const message = err instanceof Error ? err.message : String(err);
25392
- return { content: [{ type: "text", text: `Failed to create Layer: ${message}` }], isError: true };
25393
- }
25394
- if (!layer) {
25395
- return { content: [{ type: "text", text: `System "${systemId}" not found or not writable.` }], isError: true };
25396
- }
25397
- return { content: [{ type: "text", text: `Created Layer.
25398
-
25399
- ${formatLayerDetail(layer)}` }] };
25400
- }
25401
- );
25402
- server.tool(
25403
- "update_layer",
25404
- "Update a Layer's editable fields (name, description, repo mapping). Omitted fields are left unchanged. Archiving/reviving a Layer goes through transition_layer, not this tool. Requires write access to the System.",
25405
- {
25406
- systemId: external_exports.string().describe("The System ID the Layer belongs to (for access control)"),
25407
- layerId: external_exports.string().describe("The Layer ID to update"),
25408
- name: external_exports.string().optional().describe("New display name"),
25409
- description: external_exports.string().optional().describe("New description"),
25410
- repoPatterns: external_exports.array(external_exports.string()).optional().describe("Replacement repo mapping (replaces the existing list)")
25411
- },
25412
- async ({ systemId, layerId, name, description, repoPatterns }) => {
25413
- try {
25414
- await assertSystemExists(client, systemId);
25415
- } catch (err) {
25416
- const message = err instanceof Error ? err.message : String(err);
25417
- return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
25418
- }
25419
- let existing;
25420
- try {
25421
- existing = await client.getLayer(layerId);
25422
- } catch (err) {
25423
- const message = err instanceof Error ? err.message : String(err);
25424
- return { content: [{ type: "text", text: `Failed to load Layer: ${message}` }], isError: true };
25425
- }
25426
- if (!existing) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
25427
- if (existing.systemId !== systemId) {
25428
- return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
25429
- }
25430
- let layer;
25431
- try {
25432
- layer = await client.updateSystemLayer(layerId, {
25433
- ...name !== void 0 && { name },
25434
- ...description !== void 0 && { description },
25435
- ...repoPatterns !== void 0 && { repoPatterns }
25436
- });
25437
- } catch (err) {
25438
- const message = err instanceof Error ? err.message : String(err);
25439
- return { content: [{ type: "text", text: `Failed to update Layer: ${message}` }], isError: true };
25440
- }
25441
- if (!layer) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
25442
- return { content: [{ type: "text", text: `Updated Layer.
24996
+ // ../../libs/harmonica-schemas/src/plan-quality.ts
24997
+ var PLAN_QUALITY_DIMENSIONS = [
24998
+ "scoped",
24999
+ "directional",
25000
+ "testable",
25001
+ "traceable",
25002
+ "assignable",
25003
+ "riskAware",
25004
+ "completeness"
25005
+ ];
25006
+ var PlanDimensionScoreSchema = external_exports.object({
25007
+ dimension: external_exports.enum(PLAN_QUALITY_DIMENSIONS),
25008
+ label: external_exports.string(),
25009
+ score: external_exports.number().min(1).max(5),
25010
+ rationale: external_exports.string(),
25011
+ suggestions: external_exports.array(external_exports.string())
25012
+ });
25013
+ var PlanQualityLLMResponseSchema = external_exports.object({
25014
+ dimensions: external_exports.array(PlanDimensionScoreSchema).min(1),
25015
+ summary: external_exports.string(),
25016
+ topSuggestion: external_exports.string()
25017
+ });
25443
25018
 
25444
- ${formatLayerDetail(layer)}` }] };
25445
- }
25446
- );
25447
- server.tool(
25448
- "transition_layer",
25449
- "Archive or revive a Layer. Archiving requires a reason (audit trail); reviving sets it back to active. Requires write access to the System.",
25450
- {
25451
- systemId: external_exports.string().describe("The System ID the Layer belongs to (for access control)"),
25452
- layerId: external_exports.string().describe("The Layer ID to transition"),
25453
- status: external_exports.enum(["active", "archived"]).describe('Target status: "archived" to retire, "active" to revive'),
25454
- reason: external_exports.string().optional().describe('Why the Layer is being archived (required when status is "archived")')
25455
- },
25456
- async ({ systemId, layerId, status, reason }) => {
25457
- try {
25458
- await assertSystemExists(client, systemId);
25459
- } catch (err) {
25460
- const message = err instanceof Error ? err.message : String(err);
25461
- return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
25462
- }
25463
- let existing;
25464
- try {
25465
- existing = await client.getLayer(layerId);
25466
- } catch (err) {
25467
- const message = err instanceof Error ? err.message : String(err);
25468
- return { content: [{ type: "text", text: `Failed to load Layer: ${message}` }], isError: true };
25469
- }
25470
- if (!existing) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
25471
- if (existing.systemId !== systemId) {
25472
- return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
25473
- }
25474
- let layer;
25475
- try {
25476
- layer = await client.setSystemLayerStatus(layerId, status, reason);
25477
- } catch (err) {
25478
- const message = err instanceof Error ? err.message : String(err);
25479
- return { content: [{ type: "text", text: `Failed to transition Layer: ${message}` }], isError: true };
25480
- }
25481
- if (!layer) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
25482
- return { content: [{ type: "text", text: `Layer ${status === "archived" ? "archived" : "revived"}.
25019
+ // ../../libs/harmonica-schemas/src/revision-quality.ts
25020
+ var REVISION_QUALITY_DIMENSIONS = [
25021
+ "scoped",
25022
+ "traceable",
25023
+ "testable",
25024
+ "reviewReady",
25025
+ "nonDuplicating"
25026
+ ];
25027
+ var RevisionDimensionScoreSchema = external_exports.object({
25028
+ dimension: external_exports.enum(REVISION_QUALITY_DIMENSIONS),
25029
+ label: external_exports.string(),
25030
+ score: external_exports.number().min(1).max(5),
25031
+ rationale: external_exports.string(),
25032
+ suggestions: external_exports.array(external_exports.string())
25033
+ });
25034
+ var RevisionQualityLLMResponseSchema = external_exports.object({
25035
+ dimensions: external_exports.array(RevisionDimensionScoreSchema).min(1),
25036
+ summary: external_exports.string(),
25037
+ topSuggestion: external_exports.string()
25038
+ });
25483
25039
 
25484
- ${formatLayerDetail(layer)}` }] };
25485
- }
25486
- );
25487
- }
25040
+ // ../../libs/harmonica-schemas/src/beat-quality.ts
25041
+ var BEAT_QUALITY_DIMENSIONS = [
25042
+ "distinctive",
25043
+ "harmonious",
25044
+ "substantial",
25045
+ "durable",
25046
+ "clear",
25047
+ "strategicallyAligned",
25048
+ "outcomeOriented"
25049
+ ];
25050
+ var BeatDimensionScoreSchema = external_exports.object({
25051
+ dimension: external_exports.enum(BEAT_QUALITY_DIMENSIONS),
25052
+ label: external_exports.string(),
25053
+ score: external_exports.number().min(1).max(5),
25054
+ rationale: external_exports.string(),
25055
+ suggestions: external_exports.array(external_exports.string())
25056
+ });
25057
+ var BeatQualityLLMResponseSchema = external_exports.object({
25058
+ dimensions: external_exports.array(BeatDimensionScoreSchema).length(BEAT_QUALITY_DIMENSIONS.length).refine(
25059
+ (arr) => new Set(arr.map((d) => d.dimension)).size === arr.length,
25060
+ { message: "Each quality dimension must appear exactly once" }
25061
+ ),
25062
+ summary: external_exports.string(),
25063
+ topSuggestion: external_exports.string()
25064
+ });
25488
25065
 
25489
- // ../../libs/harmonica-services/src/mcp/tools/membership-tools.ts
25490
- var orgIdArg = external_exports.string().describe("The organization ID \u2014 see list_organizations");
25491
- function registerMembershipTools(server, ctx, client) {
25492
- server.tool(
25493
- "list_org_members",
25494
- "List all members of an organization",
25495
- {
25496
- orgId: orgIdArg
25497
- },
25498
- async ({ orgId }) => {
25499
- try {
25500
- const members = await client.listOrgMembers(orgId);
25501
- if (members.length === 0) {
25502
- return { content: [{ type: "text", text: "No members found." }] };
25503
- }
25504
- const header = "| User ID | Name | Role | Title |";
25505
- const divider = "|---------|------|------|-------|";
25506
- const rows = members.map(
25507
- (m) => `| ${m.userId} | ${m.name} | ${m.role} | ${m.title ?? "\u2014"} |`
25508
- );
25509
- return { content: [{ type: "text", text: [header, divider, ...rows].join("\n") }] };
25510
- } catch (err) {
25511
- const message = err instanceof Error ? err.message : String(err);
25512
- return { content: [{ type: "text", text: `Failed to list members: ${message}` }], isError: true };
25513
- }
25514
- }
25515
- );
25516
- server.tool(
25517
- "add_member",
25518
- "Grant a user access to an organization. Their name and avatar are filled in when they first sign in.",
25519
- {
25520
- orgId: orgIdArg,
25521
- email: external_exports.string().email().describe("Email address (used as the user ID)"),
25522
- role: external_exports.enum(["member", "admin", "client"]).optional().describe('Organization role (defaults to "member")')
25523
- },
25524
- async ({ orgId, email: email2, role }) => {
25525
- try {
25526
- const member = await client.addOrgMember(orgId, { email: email2, role });
25527
- const lines = [
25528
- "Member added successfully.",
25529
- "",
25530
- `**User ID:** ${member.userId}`,
25531
- `**Email:** ${member.email}`,
25532
- `**Name:** ${member.name}`,
25533
- `**Role:** ${member.role}`,
25534
- member.title ? `**Title:** ${member.title}` : ""
25535
- ].filter(Boolean);
25536
- return { content: [{ type: "text", text: lines.join("\n") }] };
25537
- } catch (err) {
25538
- const message = err instanceof Error ? err.message : String(err);
25539
- return { content: [{ type: "text", text: `Failed to add member: ${message}` }], isError: true };
25540
- }
25541
- }
25542
- );
25543
- server.tool(
25544
- "update_member_role",
25545
- "Change a member's role within an organization.",
25546
- {
25547
- orgId: orgIdArg,
25548
- userId: external_exports.string().describe("User ID (email address) of the member to update"),
25549
- role: external_exports.enum(["member", "admin", "client"]).describe("New organization role")
25550
- },
25551
- async ({ orgId, userId, role }) => {
25552
- try {
25553
- const member = await client.updateOrgMember(orgId, userId, { role });
25554
- if (!member) {
25555
- return { content: [{ type: "text", text: `Member not found: "${userId}"` }], isError: true };
25556
- }
25557
- const lines = [
25558
- "Member updated successfully.",
25559
- "",
25560
- `**User ID:** ${member.userId}`,
25561
- `**Email:** ${member.email}`,
25562
- `**Name:** ${member.name}`,
25563
- `**Role:** ${member.role}`,
25564
- member.title ? `**Title:** ${member.title}` : "",
25565
- member.responsibility ? `**Responsibility:** ${member.responsibility}` : ""
25566
- ].filter(Boolean);
25567
- return { content: [{ type: "text", text: lines.join("\n") }] };
25568
- } catch (err) {
25569
- const message = err instanceof Error ? err.message : String(err);
25570
- return { content: [{ type: "text", text: `Failed to update member: ${message}` }], isError: true };
25571
- }
25572
- }
25573
- );
25574
- server.tool(
25575
- "remove_member",
25576
- "Remove a user from an organization.",
25577
- {
25578
- orgId: orgIdArg,
25579
- userId: external_exports.string().describe("User ID (email address) of the member to remove")
25580
- },
25581
- async ({ orgId, userId }) => {
25582
- try {
25583
- await client.removeOrgMember(orgId, userId);
25584
- return { content: [{ type: "text", text: `Member "${userId}" has been removed from the organization.` }] };
25585
- } catch (err) {
25586
- const message = err instanceof Error ? err.message : String(err);
25587
- return { content: [{ type: "text", text: `Failed to remove member: ${message}` }], isError: true };
25588
- }
25589
- }
25590
- );
25591
- }
25066
+ // ../../libs/harmonica-schemas/src/drop-quality.ts
25067
+ var DROP_QUALITY_DIMENSIONS = [
25068
+ "substantial",
25069
+ "harmonious",
25070
+ "clear",
25071
+ "focused",
25072
+ "strategicallyAligned"
25073
+ ];
25074
+ var DropDimensionScoreSchema = external_exports.object({
25075
+ dimension: external_exports.enum(DROP_QUALITY_DIMENSIONS),
25076
+ label: external_exports.string(),
25077
+ score: external_exports.number().min(1).max(5),
25078
+ rationale: external_exports.string(),
25079
+ suggestions: external_exports.array(external_exports.string())
25080
+ });
25081
+ var DropQualityLLMResponseSchema = external_exports.object({
25082
+ dimensions: external_exports.array(DropDimensionScoreSchema).length(DROP_QUALITY_DIMENSIONS.length).refine(
25083
+ (arr) => new Set(arr.map((d) => d.dimension)).size === arr.length,
25084
+ { message: "Each drop quality dimension must appear exactly once" }
25085
+ ),
25086
+ summary: external_exports.string(),
25087
+ topSuggestion: external_exports.string()
25088
+ });
25592
25089
 
25593
- // ../../libs/harmonica-services/src/mcp/tools/movement-tools.ts
25594
- function registerMovementTools(server, _ctx, client) {
25595
- server.tool(
25596
- "list_movements",
25597
- 'List all Movements for a Teamspace. A Movement is a Teamspace-scoped client-SOW container for an engagement. Recognition cues: "this SOW", "this engagement", "what contracts are active" \u2192 list_movements.',
25598
- {
25599
- teamspaceId: external_exports.string().describe("The teamspace ID")
25600
- },
25601
- async ({ teamspaceId }) => {
25602
- const movements = await client.listTeamspaceMovements(teamspaceId);
25603
- const text = movements.length === 0 ? "No movements found." : movements.map((m) => `[${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})`).join("\n");
25604
- return { content: [{ type: "text", text }] };
25605
- }
25606
- );
25607
- server.tool(
25608
- "get_movement",
25609
- "Get a Movement by ID. Returns full details including status, approval timestamps, and audit stamps.",
25610
- {
25611
- movementId: external_exports.string().describe("The movement ID (e.g., mv-abc123)")
25612
- },
25613
- async ({ movementId }) => {
25614
- const m = await client.getMovement(movementId);
25615
- if (!m) {
25616
- return { content: [{ type: "text", text: `Movement not found: ${movementId}` }] };
25617
- }
25618
- const lines = [
25619
- `ID: ${m.movementId}`,
25620
- `Code: ${m.movementCode}`,
25621
- `Title: ${m.title}`,
25622
- `Teamspace: ${m.teamspaceId}`,
25623
- `Status: ${m.status}`,
25624
- m.description ? `Description: ${m.description}` : null,
25625
- m.proposedAt ? `Proposed: ${m.proposedAt}` : null,
25626
- m.approvedAt ? `Approved: ${m.approvedAt}` : null,
25627
- m.approvalNoteId ? `Approval Note: ${m.approvalNoteId}` : null,
25628
- m.createdBy ? `Created By: ${m.createdBy}` : null,
25629
- `Version: ${m.version}`,
25630
- `Created: ${m.createdAt}`,
25631
- `Updated: ${m.updatedAt}`
25632
- ].filter(Boolean).join("\n");
25633
- return { content: [{ type: "text", text: lines }] };
25634
- }
25635
- );
25636
- server.tool(
25637
- "create_movement",
25638
- '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.',
25639
- {
25640
- teamspaceId: external_exports.string().describe("The teamspace ID"),
25641
- title: external_exports.string().min(1).max(300).describe('Movement title (e.g. "Q3 Platform Modernisation SOW")'),
25642
- description: external_exports.string().max(2e3).optional().describe("Optional description"),
25643
- 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.")
25644
- },
25645
- async ({ teamspaceId, title, description, accountId }) => {
25646
- const m = await client.createMovement({
25647
- teamspaceId,
25648
- title,
25649
- ...description !== void 0 && { description },
25650
- ...accountId !== void 0 && { accountId }
25651
- });
25652
- return {
25653
- content: [
25654
- {
25655
- type: "text",
25656
- text: `Created Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})`
25657
- }
25658
- ]
25659
- };
25660
- }
25661
- );
25662
- server.tool(
25663
- "update_movement",
25664
- "Update mutable content fields on a Movement (title, description). To change status use transition_movement_status instead.",
25665
- {
25666
- movementId: external_exports.string().describe("The movement ID"),
25667
- title: external_exports.string().min(1).max(300).optional().describe("New title"),
25668
- description: external_exports.string().max(2e3).optional().describe("New description"),
25669
- 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.")
25670
- },
25671
- async ({ movementId, title, description, accountId }) => {
25672
- const hasUpdates = title !== void 0 || description !== void 0 || accountId !== void 0;
25673
- if (!hasUpdates) {
25674
- return {
25675
- content: [{ type: "text", text: `No updates provided for Movement: ${movementId}` }],
25676
- isError: true
25677
- };
25678
- }
25679
- const resolvedAccountId = accountId === "" ? null : accountId;
25680
- try {
25681
- const m = await client.updateMovement(movementId, {
25682
- ...title !== void 0 && { title },
25683
- ...description !== void 0 && { description },
25684
- ...accountId !== void 0 && { accountId: resolvedAccountId }
25685
- });
25686
- if (!m) {
25687
- return {
25688
- content: [{ type: "text", text: `Movement not found: ${movementId}` }],
25689
- isError: true
25690
- };
25691
- }
25692
- const account = accountId !== void 0 ? resolvedAccountId === null ? " \u2014 Account: (unlinked)" : ` \u2014 Account: ${resolvedAccountId}` : m.accountId ? ` \u2014 Account: ${m.accountId}` : "";
25693
- return {
25694
- content: [
25695
- {
25696
- type: "text",
25697
- text: `Updated Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})${account}`
25698
- }
25699
- ]
25700
- };
25701
- } catch (err) {
25702
- const message = err instanceof Error ? err.message : String(err);
25703
- return {
25704
- content: [{ type: "text", text: `Failed to update Movement: ${message}` }],
25705
- isError: true
25706
- };
25707
- }
25708
- }
25709
- );
25710
- server.tool(
25711
- "transition_movement_status",
25712
- 'Transition a Movement through its commercial lifecycle: draft \u2192 proposed \u2192 approved \u2192 active \u2192 fulfilled. Cancelled is reachable from any non-terminal state and is how a superseded Movement is retired. Advancing to "approved" requires an approvalNoteId. Use confirm=false (default) to preview the transition without writing; confirm=true to execute.',
25713
- {
25714
- movementId: external_exports.string().describe("The movement ID"),
25715
- targetState: external_exports.enum(["proposed", "approved", "active", "fulfilled", "cancelled"]).describe("Target lifecycle state"),
25716
- approvalNoteId: external_exports.string().optional().describe('Required when transitioning to "approved" \u2014 the Note ID capturing the approval decision'),
25717
- reason: external_exports.string().optional().describe("Reason for the transition. Strongly recommended for cancellation \u2014 retiring a Movement with no recorded reason leaves no audit trail"),
25718
- confirm: external_exports.boolean().optional().default(false).describe("false (default) = dry-run preview; true = execute the transition")
25719
- },
25720
- async ({ movementId, targetState, approvalNoteId, reason, confirm }) => {
25721
- if (!confirm) {
25722
- return {
25723
- content: [
25724
- {
25725
- type: "text",
25726
- text: `Dry-run: would transition Movement ${movementId} \u2192 ${targetState}${approvalNoteId ? ` (approvalNoteId: ${approvalNoteId})` : ""}${reason ? ` \u2014 ${reason}` : ""}. Call again with confirm=true to execute.`
25727
- }
25728
- ]
25729
- };
25730
- }
25731
- try {
25732
- const m = await client.transitionMovementStatus(movementId, targetState, {
25733
- ...approvalNoteId !== void 0 && { approvalNoteId },
25734
- ...reason !== void 0 && { reason }
25735
- });
25736
- if (!m) {
25737
- return {
25738
- content: [{ type: "text", text: `Movement not found: ${movementId}` }],
25739
- isError: true
25740
- };
25741
- }
25742
- return {
25743
- content: [
25744
- {
25745
- type: "text",
25746
- text: `Transitioned Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} \u2192 ${m.status}`
25747
- }
25748
- ]
25749
- };
25750
- } catch (err) {
25751
- const message = err instanceof Error ? err.message : String(err);
25752
- return {
25753
- content: [{ type: "text", text: `Failed to transition Movement: ${message}` }],
25754
- isError: true
25755
- };
25756
- }
25757
- }
25758
- );
25759
- }
25760
-
25761
- // ../../libs/harmonica-services/src/mcp/tools/next-actions-tools.ts
25762
- var PRIORITY_ICONS = {
25763
- P0: "\u{1F6A8}",
25764
- P1: "\u{1F680}",
25765
- P2: "\u{1F528}",
25766
- P3: "\u{1F4CB}",
25767
- P4: "\u{1F4A1}",
25768
- P5: "\u2728"
25769
- };
25770
- function formatNextActions(actions) {
25771
- if (actions.length === 0) {
25772
- return "No actions found. Project is up to date.";
25773
- }
25774
- const lines = ["# Next Actions\n"];
25775
- for (const action of actions) {
25776
- const icon = PRIORITY_ICONS[action.priority] ?? "\u2022";
25777
- lines.push(`## ${icon} ${action.priority} \u2014 ${action.category}`);
25778
- lines.push(`**${action.title}**`);
25779
- lines.push(action.description);
25780
- if (action.entityId) {
25781
- const parts = [`${action.entityType}: ${action.entityId}`];
25782
- if (action.beatId) parts.push(`Beat: ${action.beatId}`);
25783
- lines.push(parts.join(" | "));
25784
- }
25785
- if (action.count) {
25786
- lines.push(`Count: ${action.count}`);
25787
- }
25788
- lines.push("");
25789
- }
25790
- return lines.join("\n");
25791
- }
25792
- function registerNextActionsTools(server, ctx, client) {
25793
- server.tool(
25794
- "get_next_actions",
25795
- "Get prioritized list of recommended next actions for a system. Analyzes Beats, Revisions, and Notes to surface the highest-priority work: P0 (urgent bugs) \u2192 P1 (deploy) \u2192 P2 (build) \u2192 P3 (plan) \u2192 P4 (compose) \u2192 P5 (maintenance).",
25796
- {
25797
- systemId: external_exports.string().describe("The system ID"),
25798
- limit: external_exports.coerce.number().optional().default(10).describe("Max actions to return")
25799
- },
25800
- async ({ systemId, limit }) => {
25801
- try {
25802
- await assertSystemExists(client, systemId);
25803
- const actions = await client.getNextActions(systemId, { limit });
25804
- return { content: [{ type: "text", text: formatNextActions(actions) }] };
25805
- } catch (err) {
25806
- const message = err instanceof Error ? err.message : String(err);
25807
- return { content: [{ type: "text", text: `Failed to get next actions: ${message}` }], isError: true };
25808
- }
25809
- }
25810
- );
25811
- server.tool(
25812
- "list_triage_actions",
25813
- "List active triage actions from the ledger (open + acted). This is the stateful view of what needs attention \u2014 includes status tracking, task progress, and escalation state. Use this instead of get_next_actions when triage has been run.",
25814
- {
25815
- systemId: external_exports.string().describe("The system ID")
25816
- },
25817
- async ({ systemId }) => {
25818
- try {
25819
- await assertSystemExists(client, systemId);
25820
- const actions = await client.listTriageActions(systemId);
25821
- return { content: [{ type: "text", text: formatTriageActions(actions) }] };
25822
- } catch (err) {
25823
- const message = err instanceof Error ? err.message : String(err);
25824
- return { content: [{ type: "text", text: `Failed to list triage actions: ${message}` }], isError: true };
25825
- }
25826
- }
25827
- );
25828
- server.tool(
25829
- "dismiss_triage_action",
25830
- 'Dismiss a triage action (human decides "not now"). The action will not be re-escalated until the underlying state changes.',
25831
- {
25832
- systemId: external_exports.string().describe("The system ID"),
25833
- actionKey: external_exports.string().describe("The action key to dismiss (from list_triage_actions)")
25834
- },
25835
- async ({ systemId, actionKey }) => {
25836
- try {
25837
- await assertSystemExists(client, systemId);
25838
- await client.dismissTriageAction(systemId, actionKey, ctx.user?.userId ?? "mcp");
25839
- return { content: [{ type: "text", text: `Action ${actionKey} dismissed.` }] };
25840
- } catch (err) {
25841
- const message = err instanceof Error ? err.message : String(err);
25842
- return { content: [{ type: "text", text: `Failed to dismiss action: ${message}` }], isError: true };
25843
- }
25844
- }
25845
- );
25846
- }
25847
- var STATUS_ICONS = {
25848
- open: "\u2B1A",
25849
- acted: "\u25D0",
25850
- dismissed: "\u2715",
25851
- resolved: "\u2713"
25852
- };
25853
- function formatTriageActions(actions) {
25854
- if (actions.length === 0) {
25855
- return "No active triage actions. Run triage to populate the ledger.";
25856
- }
25857
- const lines = [`# Triage Actions (${actions.length})
25858
- `];
25859
- for (const action of actions) {
25860
- const pIcon = PRIORITY_ICONS[action.priority] ?? "\u2022";
25861
- const sIcon = STATUS_ICONS[action.status] ?? "?";
25862
- lines.push(`## ${pIcon} ${action.priority} \u2014 ${action.category} ${sIcon} ${action.status.toUpperCase()}`);
25863
- lines.push(`**${action.title}**`);
25864
- lines.push(action.description);
25865
- const meta = [];
25866
- if (action.entityId) meta.push(`Entity: ${action.entityId}`);
25867
- if (action.taskId) meta.push(`Task: ${action.taskId}${action.taskStatus ? ` (${action.taskStatus})` : ""}`);
25868
- if (action.decision) meta.push(`Decision: ${action.decision}`);
25869
- if (meta.length > 0) lines.push(meta.join(" | "));
25870
- lines.push(`Action key: \`${action.actionKey}\` | First seen: ${action.firstSeenAt}`);
25871
- lines.push("");
25872
- }
25873
- return lines.join("\n");
25874
- }
25875
-
25876
- // ../../libs/harmonica-services/src/mcp/tools/note-element-tools.ts
25877
- var import_crypto3 = require("crypto");
25090
+ // ../../libs/harmonica-schemas/src/build-quality.ts
25091
+ var BUILD_DIMENSIONS = [
25092
+ "testability",
25093
+ "observability",
25094
+ "architecturalFitness",
25095
+ "modernization",
25096
+ "resilience",
25097
+ "maintainability",
25098
+ "securityPosture",
25099
+ "operability",
25100
+ "completeness"
25101
+ ];
25102
+ var BuildDimensionScoreSchema = external_exports.object({
25103
+ dimension: external_exports.enum(BUILD_DIMENSIONS),
25104
+ label: external_exports.string(),
25105
+ score: external_exports.number().min(1).max(5),
25106
+ rationale: external_exports.string(),
25107
+ suggestions: external_exports.array(external_exports.string()),
25108
+ evidenceFiles: external_exports.array(external_exports.string()).optional()
25109
+ });
25110
+ var BuildGapSchema = external_exports.object({
25111
+ dimension: external_exports.enum(BUILD_DIMENSIONS),
25112
+ severity: external_exports.enum(["blocker", "major", "minor"]),
25113
+ finding: external_exports.string(),
25114
+ impact: external_exports.string(),
25115
+ recommendation: external_exports.string(),
25116
+ effort: external_exports.enum(["small", "medium", "large"]),
25117
+ incrementalPath: external_exports.string().optional(),
25118
+ relevantFiles: external_exports.array(external_exports.string()).optional(),
25119
+ evidenceFile: external_exports.string().optional(),
25120
+ evidenceLine: external_exports.number().int().positive().optional(),
25121
+ evidenceQuote: external_exports.string().optional()
25122
+ // evidenceVerified is intentionally absent — it is set ONLY by verifyEvidenceQuotes(),
25123
+ // never by the LLM. Accepting it from the LLM would undermine the entire trust model.
25124
+ });
25125
+ var BuildQualityLLMResponseSchema = external_exports.object({
25126
+ dimensions: external_exports.array(BuildDimensionScoreSchema).min(1),
25127
+ gaps: external_exports.array(BuildGapSchema),
25128
+ summary: external_exports.string(),
25129
+ topSuggestion: external_exports.string(),
25130
+ filesExamined: external_exports.array(external_exports.string())
25131
+ });
25878
25132
 
25879
- // ../../libs/harmonica-schemas/src/azure-marketplace.ts
25880
- var AzureMarketplaceSubscriptionSchema = external_exports.object({
25881
- id: external_exports.string(),
25882
- publisherId: external_exports.string().optional(),
25883
- offerId: external_exports.string().optional(),
25133
+ // ../../libs/harmonica-schemas/src/onboarding-analysis.ts
25134
+ var ConfidenceTierSchema = external_exports.enum(["confirmed", "inferred", "assumed"]);
25135
+ var DiscoveryItemSchema = external_exports.object({
25136
+ content: external_exports.string().min(1),
25137
+ confidence: ConfidenceTierSchema,
25138
+ source: external_exports.string().min(1)
25139
+ // e.g. "documents", "your input", "research"
25140
+ });
25141
+ var StakeholderGapActionSchema = external_exports.enum([
25142
+ "confirm_name",
25143
+ "ask_pm",
25144
+ "flag_for_followup",
25145
+ "suggest_from_roster"
25146
+ ]);
25147
+ var ClientStakeholderSchema = external_exports.object({
25148
+ role: external_exports.string().min(1),
25884
25149
  name: external_exports.string().optional(),
25885
- saasSubscriptionStatus: external_exports.string().optional(),
25886
- planId: external_exports.string().optional(),
25887
- quantity: external_exports.number().optional(),
25888
- beneficiary: external_exports.object({
25889
- emailId: external_exports.string().optional(),
25890
- objectId: external_exports.string().optional(),
25891
- tenantId: external_exports.string().optional()
25892
- }).optional(),
25893
- purchaser: external_exports.object({
25894
- emailId: external_exports.string().optional(),
25895
- objectId: external_exports.string().optional(),
25896
- tenantId: external_exports.string().optional()
25897
- }).optional()
25150
+ confidence: ConfidenceTierSchema,
25151
+ whyNeeded: external_exports.string().min(1),
25152
+ proposedAction: StakeholderGapActionSchema
25898
25153
  });
25899
- var AzureSubscribedSchema = external_exports.object({
25900
- action: external_exports.literal("Subscribe"),
25901
- // Non-blank, not merely non-empty. A bare z.string() accepts "", which would
25902
- // derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
25903
- // slug invariant. And .min(1) alone still admits " ", which normalizes to
25904
- // nothing and falls through to the hash fallback, quietly provisioning an org
25905
- // under a hashed slug for input that is plainly garbage. Reject it at the
25906
- // boundary instead.
25907
- subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
25908
- message: "subscriptionId must not be blank"
25909
- }),
25910
- offerId: external_exports.string(),
25911
- planId: external_exports.string(),
25912
- quantity: external_exports.number().optional(),
25913
- timeStamp: external_exports.string(),
25914
- operationId: external_exports.string(),
25915
- subscription: AzureMarketplaceSubscriptionSchema
25154
+ var CodazenStakeholderSchema = external_exports.object({
25155
+ role: external_exports.string().min(1),
25156
+ name: external_exports.string().optional(),
25157
+ confidence: ConfidenceTierSchema,
25158
+ basis: external_exports.string().min(1)
25916
25159
  });
25917
- var AzureUnsubscribedSchema = external_exports.object({
25918
- action: external_exports.literal("Unsubscribe"),
25919
- // Non-blank, not merely non-empty. A bare z.string() accepts "", which would
25920
- // derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
25921
- // slug invariant. And .min(1) alone still admits " ", which normalizes to
25922
- // nothing and falls through to the hash fallback, quietly provisioning an org
25923
- // under a hashed slug for input that is plainly garbage. Reject it at the
25924
- // boundary instead.
25925
- subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
25926
- message: "subscriptionId must not be blank"
25927
- }),
25928
- offerId: external_exports.string(),
25929
- planId: external_exports.string(),
25930
- timeStamp: external_exports.string(),
25931
- operationId: external_exports.string(),
25932
- subscription: AzureMarketplaceSubscriptionSchema
25933
- });
25934
- var AzureChangePlanSchema = external_exports.object({
25935
- action: external_exports.literal("ChangePlan"),
25936
- // Non-blank, not merely non-empty. A bare z.string() accepts "", which would
25937
- // derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
25938
- // slug invariant. And .min(1) alone still admits " ", which normalizes to
25939
- // nothing and falls through to the hash fallback, quietly provisioning an org
25940
- // under a hashed slug for input that is plainly garbage. Reject it at the
25941
- // boundary instead.
25942
- subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
25943
- message: "subscriptionId must not be blank"
25944
- }),
25945
- offerId: external_exports.string(),
25946
- planId: external_exports.string(),
25947
- timeStamp: external_exports.string(),
25948
- operationId: external_exports.string(),
25949
- subscription: AzureMarketplaceSubscriptionSchema
25950
- });
25951
- var AzureSuspendSchema = external_exports.object({
25952
- action: external_exports.literal("Suspend"),
25953
- // Non-blank, not merely non-empty. A bare z.string() accepts "", which would
25954
- // derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
25955
- // slug invariant. And .min(1) alone still admits " ", which normalizes to
25956
- // nothing and falls through to the hash fallback, quietly provisioning an org
25957
- // under a hashed slug for input that is plainly garbage. Reject it at the
25958
- // boundary instead.
25959
- subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
25960
- message: "subscriptionId must not be blank"
25961
- }),
25962
- offerId: external_exports.string(),
25963
- planId: external_exports.string(),
25964
- timeStamp: external_exports.string(),
25965
- operationId: external_exports.string(),
25966
- subscription: AzureMarketplaceSubscriptionSchema
25967
- });
25968
- var AzureReinstateSchema = external_exports.object({
25969
- action: external_exports.literal("Reinstate"),
25970
- // Non-blank, not merely non-empty. A bare z.string() accepts "", which would
25971
- // derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
25972
- // slug invariant. And .min(1) alone still admits " ", which normalizes to
25973
- // nothing and falls through to the hash fallback, quietly provisioning an org
25974
- // under a hashed slug for input that is plainly garbage. Reject it at the
25975
- // boundary instead.
25976
- subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
25977
- message: "subscriptionId must not be blank"
25160
+ var DiscoveryAnalysisSchema = external_exports.object({
25161
+ organization: external_exports.object({
25162
+ name: external_exports.string().min(1),
25163
+ description: external_exports.string().min(1),
25164
+ confidence: ConfidenceTierSchema,
25165
+ source: external_exports.string().min(1)
25978
25166
  }),
25979
- offerId: external_exports.string(),
25980
- planId: external_exports.string(),
25981
- timeStamp: external_exports.string(),
25982
- operationId: external_exports.string(),
25983
- subscription: AzureMarketplaceSubscriptionSchema
25167
+ painPoints: external_exports.array(DiscoveryItemSchema),
25168
+ goals: external_exports.array(DiscoveryItemSchema),
25169
+ constraints: external_exports.array(DiscoveryItemSchema),
25170
+ commitments: external_exports.array(
25171
+ DiscoveryItemSchema.extend({
25172
+ suggestedDeliverableName: external_exports.string().optional(),
25173
+ suggestedDueDate: external_exports.string().optional()
25174
+ })
25175
+ ),
25176
+ people: external_exports.object({
25177
+ client: external_exports.array(ClientStakeholderSchema),
25178
+ codazen: external_exports.array(CodazenStakeholderSchema)
25179
+ })
25984
25180
  });
25985
- var AzureMarketplaceWebhookEventSchema = external_exports.discriminatedUnion("action", [
25986
- AzureSubscribedSchema,
25987
- AzureUnsubscribedSchema,
25988
- AzureChangePlanSchema,
25989
- AzureSuspendSchema,
25990
- AzureReinstateSchema
25181
+ var GapNoteTypeSchema = external_exports.enum([
25182
+ "constraint",
25183
+ "context",
25184
+ "guidance",
25185
+ "decision"
25991
25186
  ]);
25992
- var AZURE_MARKETPLACE_WEBHOOK_ACTIONS = AzureMarketplaceWebhookEventSchema.options.map(
25993
- (o) => o.shape.action.value
25994
- );
25995
- var AzureMarketplaceActivateBodySchema = external_exports.object({
25996
- token: external_exports.string().min(1)
25997
- });
25998
-
25999
- // ../../libs/harmonica-schemas/src/compose-beat-versions.ts
26000
- var BeatVersionDraftSchema = external_exports.object({
26001
- title: external_exports.string().min(1).max(300),
26002
- description: external_exports.string().min(1).max(2e3),
26003
- scope: external_exports.string().max(1e3).optional(),
26004
- changeSummary: external_exports.string().min(1).max(500),
26005
- changeImportance: external_exports.enum(["minor", "moderate", "major"]),
26006
- priority: external_exports.number().int().min(1).optional(),
26007
- tags: external_exports.array(external_exports.string()).optional(),
26008
- estimatedEffort: external_exports.enum(["XS", "S", "M", "L", "XL"]).optional()
26009
- });
26010
- var LLMBeatVersionsCompositionOutputSchema = external_exports.object({
26011
- drafts: external_exports.array(BeatVersionDraftSchema).min(2).max(5)
26012
- });
26013
-
26014
- // ../../libs/harmonica-schemas/src/note-extraction.ts
26015
- var EXTRACTABLE_NOTE_TYPES = ["context", "constraint", "guidance", "decision"];
26016
- var DECISION_SIGNIFICANCE_VALUES = ["strategic", "structural", "implementation"];
26017
- var ExtractedNoteRawSchema = external_exports.object({
26018
- noteType: external_exports.enum(EXTRACTABLE_NOTE_TYPES),
25187
+ var GapToFileSchema = external_exports.object({
25188
+ action: external_exports.literal("file"),
25189
+ noteType: GapNoteTypeSchema,
26019
25190
  content: external_exports.string().min(1),
26020
25191
  rationale: external_exports.string().optional(),
26021
- confidence: external_exports.number().min(0).max(1),
26022
- significance: external_exports.enum(DECISION_SIGNIFICANCE_VALUES).optional()
26023
- });
26024
- var NoteExtractionResponseSchema = external_exports.array(ExtractedNoteRawSchema);
26025
-
26026
- // ../../libs/harmonica-schemas/src/plan-quality.ts
26027
- var PLAN_QUALITY_DIMENSIONS = [
26028
- "scoped",
26029
- "directional",
26030
- "testable",
26031
- "traceable",
26032
- "assignable",
26033
- "riskAware",
26034
- "completeness"
26035
- ];
26036
- var PlanDimensionScoreSchema = external_exports.object({
26037
- dimension: external_exports.enum(PLAN_QUALITY_DIMENSIONS),
26038
- label: external_exports.string(),
26039
- score: external_exports.number().min(1).max(5),
26040
- rationale: external_exports.string(),
26041
- suggestions: external_exports.array(external_exports.string())
26042
- });
26043
- var PlanQualityLLMResponseSchema = external_exports.object({
26044
- dimensions: external_exports.array(PlanDimensionScoreSchema).min(1),
26045
- summary: external_exports.string(),
26046
- topSuggestion: external_exports.string()
26047
- });
26048
-
26049
- // ../../libs/harmonica-schemas/src/revision-quality.ts
26050
- var REVISION_QUALITY_DIMENSIONS = [
26051
- "scoped",
26052
- "traceable",
26053
- "testable",
26054
- "reviewReady",
26055
- "nonDuplicating"
26056
- ];
26057
- var RevisionDimensionScoreSchema = external_exports.object({
26058
- dimension: external_exports.enum(REVISION_QUALITY_DIMENSIONS),
26059
- label: external_exports.string(),
26060
- score: external_exports.number().min(1).max(5),
26061
- rationale: external_exports.string(),
26062
- suggestions: external_exports.array(external_exports.string())
26063
- });
26064
- var RevisionQualityLLMResponseSchema = external_exports.object({
26065
- dimensions: external_exports.array(RevisionDimensionScoreSchema).min(1),
26066
- summary: external_exports.string(),
26067
- topSuggestion: external_exports.string()
26068
- });
26069
-
26070
- // ../../libs/harmonica-schemas/src/beat-quality.ts
26071
- var BEAT_QUALITY_DIMENSIONS = [
26072
- "distinctive",
26073
- "harmonious",
26074
- "substantial",
26075
- "durable",
26076
- "clear",
26077
- "strategicallyAligned",
26078
- "outcomeOriented"
26079
- ];
26080
- var BeatDimensionScoreSchema = external_exports.object({
26081
- dimension: external_exports.enum(BEAT_QUALITY_DIMENSIONS),
26082
- label: external_exports.string(),
26083
- score: external_exports.number().min(1).max(5),
26084
- rationale: external_exports.string(),
26085
- suggestions: external_exports.array(external_exports.string())
26086
- });
26087
- var BeatQualityLLMResponseSchema = external_exports.object({
26088
- dimensions: external_exports.array(BeatDimensionScoreSchema).length(BEAT_QUALITY_DIMENSIONS.length).refine(
26089
- (arr) => new Set(arr.map((d) => d.dimension)).size === arr.length,
26090
- { message: "Each quality dimension must appear exactly once" }
26091
- ),
26092
- summary: external_exports.string(),
26093
- topSuggestion: external_exports.string()
25192
+ confidence: external_exports.number().min(0).max(1).optional()
26094
25193
  });
26095
-
26096
- // ../../libs/harmonica-schemas/src/drop-quality.ts
26097
- var DROP_QUALITY_DIMENSIONS = [
26098
- "substantial",
26099
- "harmonious",
26100
- "clear",
26101
- "focused",
26102
- "strategicallyAligned"
26103
- ];
26104
- var DropDimensionScoreSchema = external_exports.object({
26105
- dimension: external_exports.enum(DROP_QUALITY_DIMENSIONS),
26106
- label: external_exports.string(),
26107
- score: external_exports.number().min(1).max(5),
26108
- rationale: external_exports.string(),
26109
- suggestions: external_exports.array(external_exports.string())
25194
+ var GapToEscalateSchema = external_exports.object({
25195
+ action: external_exports.literal("escalate"),
25196
+ category: external_exports.string().min(1),
25197
+ question: external_exports.string().min(1),
25198
+ mayaRecommendation: external_exports.string().min(1)
25199
+ // Maya's best guess so PM is correcting, not composing
26110
25200
  });
26111
- var DropQualityLLMResponseSchema = external_exports.object({
26112
- dimensions: external_exports.array(DropDimensionScoreSchema).length(DROP_QUALITY_DIMENSIONS.length).refine(
26113
- (arr) => new Set(arr.map((d) => d.dimension)).size === arr.length,
26114
- { message: "Each drop quality dimension must appear exactly once" }
26115
- ),
26116
- summary: external_exports.string(),
26117
- topSuggestion: external_exports.string()
25201
+ var GapClassificationItemSchema = external_exports.discriminatedUnion("action", [
25202
+ GapToFileSchema,
25203
+ GapToEscalateSchema
25204
+ ]);
25205
+ var GapClassificationResponseSchema = external_exports.array(GapClassificationItemSchema);
25206
+ var BeatDraftSuggestionSchema = external_exports.object({
25207
+ title: external_exports.string().min(1).max(200),
25208
+ description: external_exports.string().min(1).max(2e3),
25209
+ rationale: external_exports.string().min(1).max(1e3),
25210
+ recommendConvertToNote: external_exports.boolean(),
25211
+ noteConversionReason: external_exports.string().max(1e3).optional()
26118
25212
  });
26119
-
26120
- // ../../libs/harmonica-schemas/src/build-quality.ts
26121
- var BUILD_DIMENSIONS = [
26122
- "testability",
26123
- "observability",
26124
- "architecturalFitness",
26125
- "modernization",
26126
- "resilience",
26127
- "maintainability",
26128
- "securityPosture",
26129
- "operability",
26130
- "completeness"
26131
- ];
26132
- var BuildDimensionScoreSchema = external_exports.object({
26133
- dimension: external_exports.enum(BUILD_DIMENSIONS),
26134
- label: external_exports.string(),
26135
- score: external_exports.number().min(1).max(5),
26136
- rationale: external_exports.string(),
26137
- suggestions: external_exports.array(external_exports.string()),
26138
- evidenceFiles: external_exports.array(external_exports.string()).optional()
26139
- });
26140
- var BuildGapSchema = external_exports.object({
26141
- dimension: external_exports.enum(BUILD_DIMENSIONS),
26142
- severity: external_exports.enum(["blocker", "major", "minor"]),
26143
- finding: external_exports.string(),
26144
- impact: external_exports.string(),
26145
- recommendation: external_exports.string(),
26146
- effort: external_exports.enum(["small", "medium", "large"]),
26147
- incrementalPath: external_exports.string().optional(),
26148
- relevantFiles: external_exports.array(external_exports.string()).optional(),
26149
- evidenceFile: external_exports.string().optional(),
26150
- evidenceLine: external_exports.number().int().positive().optional(),
26151
- evidenceQuote: external_exports.string().optional()
26152
- // evidenceVerified is intentionally absent — it is set ONLY by verifyEvidenceQuotes(),
26153
- // never by the LLM. Accepting it from the LLM would undermine the entire trust model.
26154
- });
26155
- var BuildQualityLLMResponseSchema = external_exports.object({
26156
- dimensions: external_exports.array(BuildDimensionScoreSchema).min(1),
26157
- gaps: external_exports.array(BuildGapSchema),
26158
- summary: external_exports.string(),
26159
- topSuggestion: external_exports.string(),
26160
- filesExamined: external_exports.array(external_exports.string())
26161
- });
26162
-
26163
- // ../../libs/harmonica-schemas/src/onboarding-analysis.ts
26164
- var ConfidenceTierSchema = external_exports.enum(["confirmed", "inferred", "assumed"]);
26165
- var DiscoveryItemSchema = external_exports.object({
26166
- content: external_exports.string().min(1),
26167
- confidence: ConfidenceTierSchema,
26168
- source: external_exports.string().min(1)
26169
- // e.g. "documents", "your input", "research"
26170
- });
26171
- var StakeholderGapActionSchema = external_exports.enum([
26172
- "confirm_name",
26173
- "ask_pm",
26174
- "flag_for_followup",
26175
- "suggest_from_roster"
26176
- ]);
26177
- var ClientStakeholderSchema = external_exports.object({
26178
- role: external_exports.string().min(1),
26179
- name: external_exports.string().optional(),
26180
- confidence: ConfidenceTierSchema,
26181
- whyNeeded: external_exports.string().min(1),
26182
- proposedAction: StakeholderGapActionSchema
26183
- });
26184
- var CodazenStakeholderSchema = external_exports.object({
26185
- role: external_exports.string().min(1),
26186
- name: external_exports.string().optional(),
26187
- confidence: ConfidenceTierSchema,
26188
- basis: external_exports.string().min(1)
26189
- });
26190
- var DiscoveryAnalysisSchema = external_exports.object({
26191
- organization: external_exports.object({
26192
- name: external_exports.string().min(1),
26193
- description: external_exports.string().min(1),
26194
- confidence: ConfidenceTierSchema,
26195
- source: external_exports.string().min(1)
26196
- }),
26197
- painPoints: external_exports.array(DiscoveryItemSchema),
26198
- goals: external_exports.array(DiscoveryItemSchema),
26199
- constraints: external_exports.array(DiscoveryItemSchema),
26200
- commitments: external_exports.array(
26201
- DiscoveryItemSchema.extend({
26202
- suggestedDeliverableName: external_exports.string().optional(),
26203
- suggestedDueDate: external_exports.string().optional()
26204
- })
26205
- ),
26206
- people: external_exports.object({
26207
- client: external_exports.array(ClientStakeholderSchema),
26208
- codazen: external_exports.array(CodazenStakeholderSchema)
26209
- })
26210
- });
26211
- var GapNoteTypeSchema = external_exports.enum([
26212
- "constraint",
26213
- "context",
26214
- "guidance",
26215
- "decision"
26216
- ]);
26217
- var GapToFileSchema = external_exports.object({
26218
- action: external_exports.literal("file"),
26219
- noteType: GapNoteTypeSchema,
26220
- content: external_exports.string().min(1),
26221
- rationale: external_exports.string().optional(),
26222
- confidence: external_exports.number().min(0).max(1).optional()
26223
- });
26224
- var GapToEscalateSchema = external_exports.object({
26225
- action: external_exports.literal("escalate"),
26226
- category: external_exports.string().min(1),
26227
- question: external_exports.string().min(1),
26228
- mayaRecommendation: external_exports.string().min(1)
26229
- // Maya's best guess so PM is correcting, not composing
26230
- });
26231
- var GapClassificationItemSchema = external_exports.discriminatedUnion("action", [
26232
- GapToFileSchema,
26233
- GapToEscalateSchema
26234
- ]);
26235
- var GapClassificationResponseSchema = external_exports.array(GapClassificationItemSchema);
26236
- var BeatDraftSuggestionSchema = external_exports.object({
26237
- title: external_exports.string().min(1).max(200),
26238
- description: external_exports.string().min(1).max(2e3),
26239
- rationale: external_exports.string().min(1).max(1e3),
26240
- recommendConvertToNote: external_exports.boolean(),
26241
- noteConversionReason: external_exports.string().max(1e3).optional()
26242
- });
26243
- var BeatDraftSuggestionsSchema = external_exports.array(BeatDraftSuggestionSchema);
25213
+ var BeatDraftSuggestionsSchema = external_exports.array(BeatDraftSuggestionSchema);
26244
25214
 
26245
25215
  // ../../libs/harmonica-schemas/src/api-responses.ts
26246
25216
  var HumanAssigneeSchema = external_exports.object({ email: external_exports.string(), name: external_exports.string() });
@@ -26627,6 +25597,7 @@ var OrganizationSummarySchema = external_exports.object({
26627
25597
  name: external_exports.string(),
26628
25598
  slug: external_exports.string(),
26629
25599
  logoUrl: external_exports.string().optional(),
25600
+ capabilityProfile: external_exports.enum(["azure-core"]).optional(),
26630
25601
  status: external_exports.string(),
26631
25602
  role: external_exports.string(),
26632
25603
  systemCount: external_exports.number(),
@@ -26967,6 +25938,15 @@ var WorkItemApiSchema = external_exports.object({
26967
25938
  * named group, not hide them.
26968
25939
  */
26969
25940
  workOrderId: external_exports.string().min(1).optional(),
25941
+ /**
25942
+ * Optional link to the Bar this item is planned into (B-298). Absent means
25943
+ * unplanned, which is a supported state — a reader grouping a queue by Bar
25944
+ * must render those as their own named group, not hide them. An id that
25945
+ * doesn't resolve within the Account in view is work the reader cannot
25946
+ * place, not work that is unplanned: a Bar is Account-scoped but a Track is
25947
+ * not, so a move on either side can separate the two.
25948
+ */
25949
+ barId: external_exports.string().min(1).optional(),
26970
25950
  createdBy: external_exports.string().optional(),
26971
25951
  version: external_exports.number().optional(),
26972
25952
  createdAt: external_exports.string(),
@@ -26976,8 +25956,68 @@ var WorkItemEstimateHintApiSchema = external_exports.object({
26976
25956
  hours: external_exports.number(),
26977
25957
  source: external_exports.literal("beat_version_value_velocity")
26978
25958
  });
25959
+ var BAR_STATUSES2 = ["open", "closed"];
25960
+ var BarApiSchema = external_exports.object({
25961
+ barId: external_exports.string(),
25962
+ accountId: external_exports.string(),
25963
+ /** ISO 8601 date the Bar opens. */
25964
+ startDate: external_exports.string(),
25965
+ /** ISO 8601 date the Bar closes. */
25966
+ endDate: external_exports.string(),
25967
+ status: external_exports.enum(BAR_STATUSES2),
25968
+ createdAt: external_exports.string(),
25969
+ updatedAt: external_exports.string()
25970
+ }).passthrough();
25971
+ var BarsResponseSchema = external_exports.object({ bars: external_exports.array(BarApiSchema) }).passthrough();
26979
25972
  var WorkItemsResponseSchema = external_exports.object({ workItems: external_exports.array(WorkItemApiSchema) }).passthrough();
26980
25973
  var WorkItemResponseSchema = external_exports.object({ workItem: WorkItemApiSchema, estimateHint: WorkItemEstimateHintApiSchema.optional() }).passthrough();
25974
+ var EXTERNAL_MAPPING_SUBJECT_TYPES = [
25975
+ "movement",
25976
+ "work_order",
25977
+ "work_item"
25978
+ ];
25979
+ var ExternalMappingSubjectTypeApiSchema = external_exports.enum(EXTERNAL_MAPPING_SUBJECT_TYPES);
25980
+ var EXTERNAL_MAPPING_PROVIDERS = [
25981
+ "jira",
25982
+ "linear",
25983
+ "github",
25984
+ "asana",
25985
+ "other"
25986
+ ];
25987
+ var ExternalMappingProviderApiSchema = external_exports.enum(EXTERNAL_MAPPING_PROVIDERS);
25988
+ var EXTERNAL_MAPPING_SYNC_DIRECTIONS = [
25989
+ "none",
25990
+ "inbound",
25991
+ "outbound",
25992
+ "bidirectional"
25993
+ ];
25994
+ var ExternalMappingSyncDirectionApiSchema = external_exports.enum(EXTERNAL_MAPPING_SYNC_DIRECTIONS);
25995
+ var EXTERNAL_MAPPING_SYNC_STATUSES = [
25996
+ "ok",
25997
+ "stale",
25998
+ "error",
25999
+ "unlinked"
26000
+ ];
26001
+ var ExternalMappingSyncStatusApiSchema = external_exports.enum(EXTERNAL_MAPPING_SYNC_STATUSES);
26002
+ var ExternalMappingApiSchema = external_exports.object({
26003
+ externalMappingId: external_exports.string(),
26004
+ subjectType: ExternalMappingSubjectTypeApiSchema,
26005
+ subjectId: external_exports.string(),
26006
+ teamspaceId: external_exports.string(),
26007
+ orgId: external_exports.string(),
26008
+ provider: ExternalMappingProviderApiSchema,
26009
+ externalKey: external_exports.string(),
26010
+ url: external_exports.string().optional(),
26011
+ syncDirection: ExternalMappingSyncDirectionApiSchema,
26012
+ lastSyncedAt: external_exports.string().optional(),
26013
+ syncStatus: ExternalMappingSyncStatusApiSchema,
26014
+ createdBy: external_exports.string().optional(),
26015
+ version: external_exports.number().optional(),
26016
+ createdAt: external_exports.string(),
26017
+ updatedAt: external_exports.string()
26018
+ }).passthrough();
26019
+ var ExternalMappingsResponseSchema = external_exports.object({ externalMappings: external_exports.array(ExternalMappingApiSchema) }).passthrough();
26020
+ var EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE = "SUBJECT_NOT_FOUND";
26981
26021
  var WORK_INTAKE_STATUSES = [
26982
26022
  "requested",
26983
26023
  "needs_decision",
@@ -27024,8 +26064,14 @@ var WorkOrderApiSchema = external_exports.object({
27024
26064
  workOrderId: external_exports.string(),
27025
26065
  /** Immutable lineage back to the ask this Order was split from. */
27026
26066
  workRequestId: external_exports.string(),
27027
- /** Mutable — an Order moves Tracks when an early split guess turns out wrong. */
27028
- trackId: external_exports.string(),
26067
+ /**
26068
+ * Optional: an Order may carry no Track at all. Under N-4E09-7470 a Track is
26069
+ * attribution, not an Order's parent, and an Order can be raised against a
26070
+ * Request before any placement exists. Requiring it here made the API reject
26071
+ * its own valid responses — a Movement holding one unplaced Order failed to
26072
+ * parse, so the whole list threw.
26073
+ */
26074
+ trackId: external_exports.string().optional(),
27029
26075
  teamspaceId: external_exports.string(),
27030
26076
  orgId: external_exports.string(),
27031
26077
  title: external_exports.string(),
@@ -27141,7 +26187,7 @@ var LayerRubricTemplateApiSchema = external_exports.object({
27141
26187
  var LayerRubricTemplatesResponseSchema = external_exports.object({
27142
26188
  layerRubricTemplates: external_exports.array(LayerRubricTemplateApiSchema)
27143
26189
  }).passthrough();
27144
- var DELIVERABLE_GROUP_STATUSES = [
26190
+ var TRACK_STATUSES = [
27145
26191
  "draft",
27146
26192
  "proposed",
27147
26193
  "approved",
@@ -27149,19 +26195,18 @@ var DELIVERABLE_GROUP_STATUSES = [
27149
26195
  "fulfilled",
27150
26196
  "cancelled"
27151
26197
  ];
27152
- var DeliverableGroupStatusSchema = external_exports.enum(DELIVERABLE_GROUP_STATUSES);
27153
- var DeliverableGroupApiSchema = external_exports.object({
27154
- deliverableGroupId: external_exports.string(),
27155
- groupCode: external_exports.string(),
26198
+ var TrackStatusSchema = external_exports.enum(TRACK_STATUSES);
26199
+ var TrackApiSchema = external_exports.object({
26200
+ trackId: external_exports.string(),
26201
+ trackCode: external_exports.string(),
27156
26202
  teamspaceId: external_exports.string(),
27157
- systemId: external_exports.string().nullish(),
27158
26203
  movementId: external_exports.string().nullish(),
27159
26204
  orgId: external_exports.string(),
27160
26205
  title: external_exports.string(),
27161
26206
  description: external_exports.string().nullish(),
27162
26207
  deliverables: external_exports.string().nullish(),
27163
26208
  acceptanceCriteria: external_exports.string().nullish(),
27164
- status: DeliverableGroupStatusSchema,
26209
+ status: TrackStatusSchema,
27165
26210
  vuRate: external_exports.number().nullish(),
27166
26211
  proposedAt: external_exports.string().nullish(),
27167
26212
  approvedAt: external_exports.string().nullish(),
@@ -27172,8 +26217,8 @@ var DeliverableGroupApiSchema = external_exports.object({
27172
26217
  createdAt: external_exports.string(),
27173
26218
  updatedAt: external_exports.string()
27174
26219
  }).passthrough();
27175
- var DeliverableGroupsResponseSchema = external_exports.object({
27176
- deliverableGroups: external_exports.array(DeliverableGroupApiSchema)
26220
+ var TracksResponseSchema = external_exports.object({
26221
+ tracks: external_exports.array(TrackApiSchema)
27177
26222
  }).passthrough();
27178
26223
  var MovementDropSummaryApiSchema = external_exports.object({
27179
26224
  dropId: external_exports.string(),
@@ -27203,7 +26248,7 @@ var WorkPlanBeatVersionApiSchema = external_exports.object({
27203
26248
  createdAt: external_exports.string(),
27204
26249
  updatedAt: external_exports.string()
27205
26250
  }).passthrough();
27206
- var DeliverableGroupBeatVersionsResponseSchema = external_exports.object({
26251
+ var TrackBeatVersionsResponseSchema = external_exports.object({
27207
26252
  beatVersions: external_exports.array(WorkPlanBeatVersionApiSchema)
27208
26253
  }).passthrough();
27209
26254
  var RevisionClaimActorSchema = external_exports.union([
@@ -27300,7 +26345,918 @@ var DropProposalApiSchema = external_exports.object({
27300
26345
  scoredCandidates: external_exports.array(ScoredDropCandidateSchema).optional()
27301
26346
  }).passthrough();
27302
26347
 
26348
+ // ../../libs/harmonica-services/src/mcp/tools/external-mapping-tools.ts
26349
+ var SUBJECT_TYPE_ENUM = ["movement", "work_order", "work_item"];
26350
+ var PROVIDER_ENUM = ["jira", "linear", "github", "asana", "other"];
26351
+ var SYNC_DIRECTION_ENUM = ["none", "inbound", "outbound", "bidirectional"];
26352
+ var SYNC_STATUS_ENUM = ["ok", "stale", "error", "unlinked"];
26353
+ function formatMapping(m) {
26354
+ const parts = [
26355
+ `[${m.externalMappingId}] ${m.provider}:${m.externalKey}`,
26356
+ `status: ${m.syncStatus}`,
26357
+ `direction: ${m.syncDirection}`
26358
+ ];
26359
+ if (m.url) parts.push(`url: ${m.url}`);
26360
+ if (m.lastSyncedAt) parts.push(`last synced: ${m.lastSyncedAt}`);
26361
+ return parts.join(" \u2014 ");
26362
+ }
26363
+ function registerExternalMappingTools(server, _ctx, client) {
26364
+ server.tool(
26365
+ "add_external_mapping",
26366
+ "Add an External Mapping \u2014 a reference-only link from a Project (Movement), Work Order, or Work Item to an external tracker such as Jira, Linear, GitHub, or Asana. Nothing talks to the external system; sync direction is recorded intent only (N-4E09-6311). Zero to many mappings per subject; creating one checks the subject exists and is in the caller's scope.",
26367
+ {
26368
+ subjectType: external_exports.enum(SUBJECT_TYPE_ENUM).describe("The type of Harmonica entity being linked: movement (project), work_order, or work_item"),
26369
+ subjectId: external_exports.string().min(1).max(128).describe("The ID of the subject entity"),
26370
+ provider: external_exports.enum(PROVIDER_ENUM).describe("The external tracker provider"),
26371
+ externalKey: external_exports.string().min(1).max(500).describe('Provider-specific key, e.g. a Jira issue key "PROJ-123" or a Linear issue id'),
26372
+ url: external_exports.string().url().optional().describe("Optional human-readable URL to the external object"),
26373
+ syncDirection: external_exports.enum(SYNC_DIRECTION_ENUM).optional().describe('Recorded intent for a later sync increment. Defaults to "none". Nothing syncs in this version.')
26374
+ },
26375
+ async ({ subjectType, subjectId, provider, externalKey, url, syncDirection }) => {
26376
+ try {
26377
+ const em = await client.addExternalMapping({
26378
+ subjectType,
26379
+ subjectId,
26380
+ provider,
26381
+ externalKey,
26382
+ ...url !== void 0 && { url },
26383
+ ...syncDirection !== void 0 && { syncDirection }
26384
+ });
26385
+ return {
26386
+ content: [
26387
+ {
26388
+ type: "text",
26389
+ text: `Added external mapping: ${formatMapping(em)}`
26390
+ }
26391
+ ]
26392
+ };
26393
+ } catch (err) {
26394
+ const message = err instanceof Error ? err.message : String(err);
26395
+ const isSubjectMissing = err instanceof Error && err.code === EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE;
26396
+ return {
26397
+ content: [{
26398
+ type: "text",
26399
+ text: isSubjectMissing ? `**Field:** subjectId
26400
+ **Detail:** ${subjectType} ${subjectId} was not found, or is outside your scope.
26401
+ **Action:** Check the ID, or list the subject first to confirm it exists.` : `Failed to add external mapping: ${message}`
26402
+ }],
26403
+ isError: true
26404
+ };
26405
+ }
26406
+ }
26407
+ );
26408
+ server.tool(
26409
+ "update_external_mapping",
26410
+ 'Update an External Mapping \u2014 change the external key, URL, sync direction, or sync status. Use syncStatus "unlinked" when the external object no longer exists (the mapping is kept as evidence, not deleted).',
26411
+ {
26412
+ externalMappingId: external_exports.string().describe("The external mapping ID (e.g., em-abc123)"),
26413
+ externalKey: external_exports.string().min(1).max(500).optional().describe("New provider-specific key"),
26414
+ url: external_exports.string().url().nullable().optional().describe("New URL. Pass null to clear; omit to leave untouched."),
26415
+ syncDirection: external_exports.enum(SYNC_DIRECTION_ENUM).optional().describe("New recorded sync intent"),
26416
+ syncStatus: external_exports.enum(SYNC_STATUS_ENUM).optional().describe('New sync status. Use "unlinked" when the external object is gone \u2014 the mapping is kept as evidence.')
26417
+ },
26418
+ async ({ externalMappingId, externalKey, url, syncDirection, syncStatus }) => {
26419
+ const hasUpdates = externalKey !== void 0 || url !== void 0 || syncDirection !== void 0 || syncStatus !== void 0;
26420
+ if (!hasUpdates) {
26421
+ return {
26422
+ content: [{ type: "text", text: `No updates provided for external mapping: ${externalMappingId}` }],
26423
+ isError: true
26424
+ };
26425
+ }
26426
+ try {
26427
+ const em = await client.updateExternalMapping(externalMappingId, {
26428
+ ...externalKey !== void 0 && { externalKey },
26429
+ ...url !== void 0 && { url },
26430
+ ...syncDirection !== void 0 && { syncDirection },
26431
+ ...syncStatus !== void 0 && { syncStatus }
26432
+ });
26433
+ if (!em) {
26434
+ return {
26435
+ content: [{ type: "text", text: `External mapping not found: ${externalMappingId}` }],
26436
+ isError: true
26437
+ };
26438
+ }
26439
+ return {
26440
+ content: [
26441
+ {
26442
+ type: "text",
26443
+ text: `Updated external mapping: ${formatMapping(em)}`
26444
+ }
26445
+ ]
26446
+ };
26447
+ } catch (err) {
26448
+ const message = err instanceof Error ? err.message : String(err);
26449
+ return {
26450
+ content: [{ type: "text", text: `Failed to update external mapping: ${message}` }],
26451
+ isError: true
26452
+ };
26453
+ }
26454
+ }
26455
+ );
26456
+ server.tool(
26457
+ "list_external_mappings",
26458
+ "List all External Mappings for a subject (Project/Movement, Work Order, or Work Item). Shows where this entity is tracked in external systems. Use subjectType and subjectId from the entity you are inspecting.",
26459
+ {
26460
+ subjectType: external_exports.enum(SUBJECT_TYPE_ENUM).describe("The type of Harmonica entity: movement, work_order, or work_item"),
26461
+ subjectId: external_exports.string().min(1).max(128).describe("The ID of the subject entity")
26462
+ },
26463
+ async ({ subjectType, subjectId }) => {
26464
+ try {
26465
+ const mappings = await client.listExternalMappings(subjectType, subjectId);
26466
+ const text = mappings.length === 0 ? `No external mappings found for ${subjectType} ${subjectId}.` : mappings.map(formatMapping).join("\n");
26467
+ return { content: [{ type: "text", text }] };
26468
+ } catch (err) {
26469
+ const message = err instanceof Error ? err.message : String(err);
26470
+ const isSubjectMissing = err instanceof Error && err.code === EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE;
26471
+ return {
26472
+ content: [{
26473
+ type: "text",
26474
+ text: isSubjectMissing ? `**Field:** subjectId
26475
+ **Detail:** ${subjectType} ${subjectId} was not found, or is outside your scope.
26476
+ **Action:** Check the ID, or list the subject first to confirm it exists.` : `Failed to list external mappings: ${message}`
26477
+ }],
26478
+ isError: true
26479
+ };
26480
+ }
26481
+ }
26482
+ );
26483
+ }
26484
+
26485
+ // ../../libs/harmonica-services/src/mcp/tools/feature-flag-tools.ts
26486
+ var STAGE_ENUM = external_exports.enum([
26487
+ "local",
26488
+ "staging",
26489
+ "staging-alpha",
26490
+ "staging-beta",
26491
+ "alpha",
26492
+ "beta",
26493
+ "prod"
26494
+ ]);
26495
+ function registerFeatureFlagTools(server, ctx, client) {
26496
+ server.tool(
26497
+ "list_flags",
26498
+ "List feature flags and their enabled stages. Use check_flag to test a single flag against a specific stage, or get_flag for full detail on one flag.",
26499
+ {
26500
+ includeArchived: external_exports.boolean().optional().describe("Include archived flags. Default false"),
26501
+ search: external_exports.string().optional().describe("Case-insensitive search against flag name and description"),
26502
+ cursor: external_exports.string().optional().describe("Pagination cursor from a prior list_flags result"),
26503
+ limit: external_exports.number().int().min(1).max(100).optional().describe("Flags per page. Default 20")
26504
+ },
26505
+ async ({ includeArchived, search, cursor, limit }) => {
26506
+ try {
26507
+ const page = await client.listFlags({ includeArchived, search, cursor, limit });
26508
+ if (page.flags.length === 0) {
26509
+ return { content: [{ type: "text", text: "No flags found." }] };
26510
+ }
26511
+ return { content: [{ type: "text", text: formatFlagList(page.flags, page.total, page.nextCursor) }] };
26512
+ } catch (err) {
26513
+ const message = err instanceof Error ? err.message : String(err);
26514
+ return { content: [{ type: "text", text: `Failed to list flags: ${message}` }], isError: true };
26515
+ }
26516
+ }
26517
+ );
26518
+ server.tool(
26519
+ "get_flag",
26520
+ "Get full detail for a single feature flag by name, including its enabled stages and optional change history.",
26521
+ {
26522
+ name: external_exports.string().describe("Flag name"),
26523
+ includeHistory: external_exports.boolean().optional().describe("Include change history entries. Default false")
26524
+ },
26525
+ async ({ name, includeHistory }) => {
26526
+ try {
26527
+ const flag = await client.getFlag(name, { includeHistory });
26528
+ if (!flag) {
26529
+ return { content: [{ type: "text", text: `Flag "${name}" not found.` }] };
26530
+ }
26531
+ return { content: [{ type: "text", text: formatFlag(flag, flag.history) }] };
26532
+ } catch (err) {
26533
+ const message = err instanceof Error ? err.message : String(err);
26534
+ return { content: [{ type: "text", text: `Failed to get flag: ${message}` }], isError: true };
26535
+ }
26536
+ }
26537
+ );
26538
+ server.tool(
26539
+ "check_flag",
26540
+ "Check whether a feature flag is enabled in a specific stage. Returns a simple enabled/disabled answer.",
26541
+ {
26542
+ name: external_exports.string().describe("Flag name"),
26543
+ stage: STAGE_ENUM.describe("Deployment stage to check against")
26544
+ },
26545
+ async ({ name, stage }) => {
26546
+ try {
26547
+ const enabled = await client.checkFlag(name, stage);
26548
+ const status = enabled ? "enabled" : "disabled";
26549
+ return {
26550
+ content: [{
26551
+ type: "text",
26552
+ text: `Flag "${name}" is **${status}** in stage "${stage}".`
26553
+ }]
26554
+ };
26555
+ } catch (err) {
26556
+ const message = err instanceof Error ? err.message : String(err);
26557
+ return { content: [{ type: "text", text: `Failed to check flag: ${message}` }], isError: true };
26558
+ }
26559
+ }
26560
+ );
26561
+ server.tool(
26562
+ "create_flag",
26563
+ "Create or update a feature flag with the specified enabled stages. Idempotent \u2014 calling with an existing flag name replaces its enabledIn list.",
26564
+ {
26565
+ name: external_exports.string().min(1).describe('Flag name (e.g. "rbac:enforcement")'),
26566
+ enabledIn: external_exports.array(STAGE_ENUM).describe("Stages to enable the flag in"),
26567
+ description: external_exports.string().optional().describe("Human-readable description of what this flag gates"),
26568
+ note: external_exports.string().optional().describe("Reason for this change (recorded in change history)")
26569
+ },
26570
+ async ({ name, enabledIn, description, note }) => {
26571
+ if (!ctx.user.email) {
26572
+ return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
26573
+ }
26574
+ try {
26575
+ const flag = await client.createFlag(name, enabledIn, { description, note, updatedBy: ctx.user.email });
26576
+ return { content: [{ type: "text", text: formatFlag(flag) }] };
26577
+ } catch (err) {
26578
+ const message = err instanceof Error ? err.message : String(err);
26579
+ return { content: [{ type: "text", text: `Failed to create flag: ${message}` }], isError: true };
26580
+ }
26581
+ }
26582
+ );
26583
+ server.tool(
26584
+ "set_flag",
26585
+ "Update which stages a feature flag is enabled in. Replaces the current enabledIn list entirely.",
26586
+ {
26587
+ name: external_exports.string().min(1).describe("Flag name"),
26588
+ enabledIn: external_exports.array(STAGE_ENUM).describe("New set of stages to enable the flag in. Pass [] to disable everywhere without archiving."),
26589
+ note: external_exports.string().optional().describe("Reason for this change (recorded in change history)")
26590
+ },
26591
+ async ({ name, enabledIn, note }) => {
26592
+ if (!ctx.user.email) {
26593
+ return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
26594
+ }
26595
+ try {
26596
+ const flag = await client.setFlag(name, enabledIn, { note, updatedBy: ctx.user.email });
26597
+ return { content: [{ type: "text", text: formatFlag(flag) }] };
26598
+ } catch (err) {
26599
+ const message = err instanceof Error ? err.message : String(err);
26600
+ return { content: [{ type: "text", text: `Failed to set flag: ${message}` }], isError: true };
26601
+ }
26602
+ }
26603
+ );
26604
+ server.tool(
26605
+ "archive_flag",
26606
+ "Archive a feature flag. Clears its enabledIn list and marks it archived so it cannot be re-enabled via set_flag. Use unarchive_flag to restore it.",
26607
+ {
26608
+ name: external_exports.string().min(1).describe("Flag name"),
26609
+ note: external_exports.string().optional().describe("Reason for archiving (recorded in change history)")
26610
+ },
26611
+ async ({ name, note }) => {
26612
+ if (!ctx.user.email) {
26613
+ return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
26614
+ }
26615
+ try {
26616
+ const flag = await client.archiveFlag(name, { note, updatedBy: ctx.user.email });
26617
+ return { content: [{ type: "text", text: formatFlag(flag) }] };
26618
+ } catch (err) {
26619
+ const message = err instanceof Error ? err.message : String(err);
26620
+ return { content: [{ type: "text", text: `Failed to archive flag: ${message}` }], isError: true };
26621
+ }
26622
+ }
26623
+ );
26624
+ server.tool(
26625
+ "unarchive_flag",
26626
+ "Restore an archived feature flag to operable state. The flag is re-enabled with an empty enabledIn list \u2014 use set_flag afterward to enable it in specific stages.",
26627
+ {
26628
+ name: external_exports.string().min(1).describe("Flag name"),
26629
+ note: external_exports.string().optional().describe("Reason for restoring (recorded in change history)")
26630
+ },
26631
+ async ({ name, note }) => {
26632
+ if (!ctx.user.email) {
26633
+ return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
26634
+ }
26635
+ try {
26636
+ const flag = await client.unarchiveFlag(name, { note, updatedBy: ctx.user.email });
26637
+ return { content: [{ type: "text", text: formatFlag(flag) }] };
26638
+ } catch (err) {
26639
+ const message = err instanceof Error ? err.message : String(err);
26640
+ return { content: [{ type: "text", text: `Failed to unarchive flag: ${message}` }], isError: true };
26641
+ }
26642
+ }
26643
+ );
26644
+ }
26645
+ function formatFlag(flag, history) {
26646
+ const archived = flag.archivedAt ? " **(archived)**" : "";
26647
+ const enabled = flag.enabledIn.length > 0 ? flag.enabledIn.join(", ") : "(none)";
26648
+ const parts = [
26649
+ `# Flag: ${flag.name}${archived}`,
26650
+ `**Scope:** ${flag.scope}`,
26651
+ `**Enabled in:** ${enabled}`,
26652
+ flag.description ? `**Description:** ${flag.description}` : null,
26653
+ `**Last updated:** ${flag.updatedAt.slice(0, 10)} by ${flag.updatedBy}`,
26654
+ flag.archivedAt ? `**Archived:** ${flag.archivedAt.slice(0, 10)} by ${flag.archivedBy}` : null
26655
+ ].filter(Boolean).join("\n");
26656
+ if (!history?.length) return parts;
26657
+ const historyLines = history.map((h) => {
26658
+ const prev = h.previousEnabledIn.length > 0 ? h.previousEnabledIn.join(", ") : "(none)";
26659
+ const next = h.newEnabledIn.length > 0 ? h.newEnabledIn.join(", ") : "(none)";
26660
+ const note = h.note ? ` \u2014 ${h.note}` : "";
26661
+ return `- ${h.updatedAt.slice(0, 10)} ${h.updatedBy}: ${prev} \u2192 ${next}${note}`;
26662
+ }).join("\n");
26663
+ return `${parts}
26664
+
26665
+ ## Change History
26666
+ ${historyLines}`;
26667
+ }
26668
+ function formatFlagList(flags, total, nextCursor) {
26669
+ const rows = flags.map((f) => {
26670
+ const enabled = f.enabledIn.length > 0 ? f.enabledIn.join(", ") : "(none)";
26671
+ const archived = f.archivedAt ? " *(archived)*" : "";
26672
+ return `| ${f.name}${archived} | ${enabled} | ${f.updatedAt.slice(0, 10)} |`;
26673
+ });
26674
+ const table = [
26675
+ `# Feature Flags (${total} total, showing ${flags.length})`,
26676
+ "",
26677
+ "| Name | Enabled In | Updated |",
26678
+ "|---|---|---|",
26679
+ ...rows
26680
+ ].join("\n");
26681
+ if (!nextCursor) return table;
26682
+ return `${table}
26683
+
26684
+ More flags available. Pass \`cursor: "${nextCursor}"\` to list_flags to continue.`;
26685
+ }
26686
+
26687
+ // ../../libs/harmonica-services/src/mcp/tools/layer-tools.ts
26688
+ function registerLayerTools(server, ctx, client) {
26689
+ server.tool(
26690
+ "list_system_layers",
26691
+ "List the architectural Layers declared on a System (e.g. frontend, backend, data). Layers are an evaluation lens for plan/build quality checks, not a work container.",
26692
+ {
26693
+ systemId: external_exports.string().describe("The System ID")
26694
+ },
26695
+ async ({ systemId }) => {
26696
+ try {
26697
+ await assertSystemExists(client, systemId);
26698
+ } catch (err) {
26699
+ const message = err instanceof Error ? err.message : String(err);
26700
+ return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
26701
+ }
26702
+ let layers;
26703
+ try {
26704
+ layers = await client.listSystemLayers(systemId);
26705
+ } catch (err) {
26706
+ const message = err instanceof Error ? err.message : String(err);
26707
+ return { content: [{ type: "text", text: `Failed to list Layers: ${message}` }], isError: true };
26708
+ }
26709
+ const text = formatLayerList(layers);
26710
+ return { content: [{ type: "text", text }] };
26711
+ }
26712
+ );
26713
+ server.tool(
26714
+ "get_layer",
26715
+ "Get full details of a single Layer by ID, including its repo mapping and parent Layer (if it was split from one).",
26716
+ {
26717
+ systemId: external_exports.string().describe("The System ID (for access control)"),
26718
+ layerId: external_exports.string().describe("The Layer ID")
26719
+ },
26720
+ async ({ systemId, layerId }) => {
26721
+ try {
26722
+ await assertSystemExists(client, systemId);
26723
+ } catch (err) {
26724
+ const message = err instanceof Error ? err.message : String(err);
26725
+ return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
26726
+ }
26727
+ let layer;
26728
+ try {
26729
+ layer = await client.getLayer(layerId);
26730
+ } catch (err) {
26731
+ const message = err instanceof Error ? err.message : String(err);
26732
+ return { content: [{ type: "text", text: `Failed to get Layer: ${message}` }], isError: true };
26733
+ }
26734
+ if (!layer) {
26735
+ return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
26736
+ }
26737
+ if (layer.systemId !== systemId) {
26738
+ return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
26739
+ }
26740
+ const text = formatLayerDetail(layer);
26741
+ return { content: [{ type: "text", text }] };
26742
+ }
26743
+ );
26744
+ server.tool(
26745
+ "create_layer",
26746
+ "Declare a new architectural Layer on a System (e.g. frontend, backend, data). Layers are an evaluation lens for plan/build quality checks. layerKey must be a lowercase slug and unique per System.",
26747
+ {
26748
+ systemId: external_exports.string().describe("The System ID"),
26749
+ layerKey: external_exports.string().regex(/^[a-z]([a-z0-9-]*[a-z0-9])?$/, "layerKey must be a lowercase slug (letters, digits, hyphens; start with a letter, no trailing hyphen)").describe('Stable lowercase slug for the Layer role, e.g. "frontend" or "agent-workers"'),
26750
+ name: external_exports.string().describe('Display name, e.g. "Frontend"'),
26751
+ description: external_exports.string().optional().describe("Optional description of the Layer"),
26752
+ parentLayerId: external_exports.string().optional().describe("Optional parent Layer to nest under (must be a Layer on the same System)"),
26753
+ repoPatterns: external_exports.array(external_exports.string()).optional().describe("Optional repo path patterns owned by this Layer")
26754
+ },
26755
+ async ({ systemId, layerKey, name, description, parentLayerId, repoPatterns }) => {
26756
+ try {
26757
+ await assertSystemExists(client, systemId);
26758
+ } catch (err) {
26759
+ const message = err instanceof Error ? err.message : String(err);
26760
+ return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
26761
+ }
26762
+ let layer;
26763
+ try {
26764
+ layer = await client.createSystemLayer({
26765
+ systemId,
26766
+ layerKey,
26767
+ name,
26768
+ ...description !== void 0 && { description },
26769
+ ...parentLayerId !== void 0 && { parentLayerId },
26770
+ ...repoPatterns !== void 0 && { repoPatterns }
26771
+ });
26772
+ } catch (err) {
26773
+ const message = err instanceof Error ? err.message : String(err);
26774
+ return { content: [{ type: "text", text: `Failed to create Layer: ${message}` }], isError: true };
26775
+ }
26776
+ if (!layer) {
26777
+ return { content: [{ type: "text", text: `System "${systemId}" not found or not writable.` }], isError: true };
26778
+ }
26779
+ return { content: [{ type: "text", text: `Created Layer.
26780
+
26781
+ ${formatLayerDetail(layer)}` }] };
26782
+ }
26783
+ );
26784
+ server.tool(
26785
+ "update_layer",
26786
+ "Update a Layer's editable fields (name, description, repo mapping). Omitted fields are left unchanged. Archiving/reviving a Layer goes through transition_layer, not this tool. Requires write access to the System.",
26787
+ {
26788
+ systemId: external_exports.string().describe("The System ID the Layer belongs to (for access control)"),
26789
+ layerId: external_exports.string().describe("The Layer ID to update"),
26790
+ name: external_exports.string().optional().describe("New display name"),
26791
+ description: external_exports.string().optional().describe("New description"),
26792
+ repoPatterns: external_exports.array(external_exports.string()).optional().describe("Replacement repo mapping (replaces the existing list)")
26793
+ },
26794
+ async ({ systemId, layerId, name, description, repoPatterns }) => {
26795
+ try {
26796
+ await assertSystemExists(client, systemId);
26797
+ } catch (err) {
26798
+ const message = err instanceof Error ? err.message : String(err);
26799
+ return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
26800
+ }
26801
+ let existing;
26802
+ try {
26803
+ existing = await client.getLayer(layerId);
26804
+ } catch (err) {
26805
+ const message = err instanceof Error ? err.message : String(err);
26806
+ return { content: [{ type: "text", text: `Failed to load Layer: ${message}` }], isError: true };
26807
+ }
26808
+ if (!existing) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
26809
+ if (existing.systemId !== systemId) {
26810
+ return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
26811
+ }
26812
+ let layer;
26813
+ try {
26814
+ layer = await client.updateSystemLayer(layerId, {
26815
+ ...name !== void 0 && { name },
26816
+ ...description !== void 0 && { description },
26817
+ ...repoPatterns !== void 0 && { repoPatterns }
26818
+ });
26819
+ } catch (err) {
26820
+ const message = err instanceof Error ? err.message : String(err);
26821
+ return { content: [{ type: "text", text: `Failed to update Layer: ${message}` }], isError: true };
26822
+ }
26823
+ if (!layer) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
26824
+ return { content: [{ type: "text", text: `Updated Layer.
26825
+
26826
+ ${formatLayerDetail(layer)}` }] };
26827
+ }
26828
+ );
26829
+ server.tool(
26830
+ "transition_layer",
26831
+ "Archive or revive a Layer. Archiving requires a reason (audit trail); reviving sets it back to active. Requires write access to the System.",
26832
+ {
26833
+ systemId: external_exports.string().describe("The System ID the Layer belongs to (for access control)"),
26834
+ layerId: external_exports.string().describe("The Layer ID to transition"),
26835
+ status: external_exports.enum(["active", "archived"]).describe('Target status: "archived" to retire, "active" to revive'),
26836
+ reason: external_exports.string().optional().describe('Why the Layer is being archived (required when status is "archived")')
26837
+ },
26838
+ async ({ systemId, layerId, status, reason }) => {
26839
+ try {
26840
+ await assertSystemExists(client, systemId);
26841
+ } catch (err) {
26842
+ const message = err instanceof Error ? err.message : String(err);
26843
+ return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
26844
+ }
26845
+ let existing;
26846
+ try {
26847
+ existing = await client.getLayer(layerId);
26848
+ } catch (err) {
26849
+ const message = err instanceof Error ? err.message : String(err);
26850
+ return { content: [{ type: "text", text: `Failed to load Layer: ${message}` }], isError: true };
26851
+ }
26852
+ if (!existing) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
26853
+ if (existing.systemId !== systemId) {
26854
+ return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
26855
+ }
26856
+ let layer;
26857
+ try {
26858
+ layer = await client.setSystemLayerStatus(layerId, status, reason);
26859
+ } catch (err) {
26860
+ const message = err instanceof Error ? err.message : String(err);
26861
+ return { content: [{ type: "text", text: `Failed to transition Layer: ${message}` }], isError: true };
26862
+ }
26863
+ if (!layer) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
26864
+ return { content: [{ type: "text", text: `Layer ${status === "archived" ? "archived" : "revived"}.
26865
+
26866
+ ${formatLayerDetail(layer)}` }] };
26867
+ }
26868
+ );
26869
+ }
26870
+
26871
+ // ../../libs/harmonica-services/src/mcp/tools/membership-tools.ts
26872
+ var orgIdArg = external_exports.string().describe("The organization ID \u2014 see list_organizations");
26873
+ function registerMembershipTools(server, ctx, client) {
26874
+ server.tool(
26875
+ "list_org_members",
26876
+ "List all members of an organization",
26877
+ {
26878
+ orgId: orgIdArg
26879
+ },
26880
+ async ({ orgId }) => {
26881
+ try {
26882
+ const members = await client.listOrgMembers(orgId);
26883
+ if (members.length === 0) {
26884
+ return { content: [{ type: "text", text: "No members found." }] };
26885
+ }
26886
+ const header = "| User ID | Name | Role | Title |";
26887
+ const divider = "|---------|------|------|-------|";
26888
+ const rows = members.map(
26889
+ (m) => `| ${m.userId} | ${m.name} | ${m.role} | ${m.title ?? "\u2014"} |`
26890
+ );
26891
+ return { content: [{ type: "text", text: [header, divider, ...rows].join("\n") }] };
26892
+ } catch (err) {
26893
+ const message = err instanceof Error ? err.message : String(err);
26894
+ return { content: [{ type: "text", text: `Failed to list members: ${message}` }], isError: true };
26895
+ }
26896
+ }
26897
+ );
26898
+ server.tool(
26899
+ "add_member",
26900
+ "Grant a user access to an organization. Their name and avatar are filled in when they first sign in.",
26901
+ {
26902
+ orgId: orgIdArg,
26903
+ email: external_exports.string().email().describe("Email address (used as the user ID)"),
26904
+ role: external_exports.enum(["member", "admin", "client"]).optional().describe('Organization role (defaults to "member")')
26905
+ },
26906
+ async ({ orgId, email: email2, role }) => {
26907
+ try {
26908
+ const member = await client.addOrgMember(orgId, { email: email2, role });
26909
+ const lines = [
26910
+ "Member added successfully.",
26911
+ "",
26912
+ `**User ID:** ${member.userId}`,
26913
+ `**Email:** ${member.email}`,
26914
+ `**Name:** ${member.name}`,
26915
+ `**Role:** ${member.role}`,
26916
+ member.title ? `**Title:** ${member.title}` : ""
26917
+ ].filter(Boolean);
26918
+ return { content: [{ type: "text", text: lines.join("\n") }] };
26919
+ } catch (err) {
26920
+ const message = err instanceof Error ? err.message : String(err);
26921
+ return { content: [{ type: "text", text: `Failed to add member: ${message}` }], isError: true };
26922
+ }
26923
+ }
26924
+ );
26925
+ server.tool(
26926
+ "update_member_role",
26927
+ "Change a member's role within an organization.",
26928
+ {
26929
+ orgId: orgIdArg,
26930
+ userId: external_exports.string().describe("User ID (email address) of the member to update"),
26931
+ role: external_exports.enum(["member", "admin", "client"]).describe("New organization role")
26932
+ },
26933
+ async ({ orgId, userId, role }) => {
26934
+ try {
26935
+ const member = await client.updateOrgMember(orgId, userId, { role });
26936
+ if (!member) {
26937
+ return { content: [{ type: "text", text: `Member not found: "${userId}"` }], isError: true };
26938
+ }
26939
+ const lines = [
26940
+ "Member updated successfully.",
26941
+ "",
26942
+ `**User ID:** ${member.userId}`,
26943
+ `**Email:** ${member.email}`,
26944
+ `**Name:** ${member.name}`,
26945
+ `**Role:** ${member.role}`,
26946
+ member.title ? `**Title:** ${member.title}` : "",
26947
+ member.responsibility ? `**Responsibility:** ${member.responsibility}` : ""
26948
+ ].filter(Boolean);
26949
+ return { content: [{ type: "text", text: lines.join("\n") }] };
26950
+ } catch (err) {
26951
+ const message = err instanceof Error ? err.message : String(err);
26952
+ return { content: [{ type: "text", text: `Failed to update member: ${message}` }], isError: true };
26953
+ }
26954
+ }
26955
+ );
26956
+ server.tool(
26957
+ "remove_member",
26958
+ "Remove a user from an organization.",
26959
+ {
26960
+ orgId: orgIdArg,
26961
+ userId: external_exports.string().describe("User ID (email address) of the member to remove")
26962
+ },
26963
+ async ({ orgId, userId }) => {
26964
+ try {
26965
+ await client.removeOrgMember(orgId, userId);
26966
+ return { content: [{ type: "text", text: `Member "${userId}" has been removed from the organization.` }] };
26967
+ } catch (err) {
26968
+ const message = err instanceof Error ? err.message : String(err);
26969
+ return { content: [{ type: "text", text: `Failed to remove member: ${message}` }], isError: true };
26970
+ }
26971
+ }
26972
+ );
26973
+ }
26974
+
26975
+ // ../../libs/harmonica-services/src/mcp/tools/movement-tools.ts
26976
+ function registerMovementTools(server, _ctx, client) {
26977
+ server.tool(
26978
+ "list_movements",
26979
+ 'List all Movements for a Teamspace. A Movement is a Teamspace-scoped client-SOW container for an engagement. Recognition cues: "this SOW", "this engagement", "what contracts are active" \u2192 list_movements.',
26980
+ {
26981
+ teamspaceId: external_exports.string().describe("The teamspace ID")
26982
+ },
26983
+ async ({ teamspaceId }) => {
26984
+ const movements = await client.listTeamspaceMovements(teamspaceId);
26985
+ const text = movements.length === 0 ? "No movements found." : movements.map((m) => `[${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})`).join("\n");
26986
+ return { content: [{ type: "text", text }] };
26987
+ }
26988
+ );
26989
+ server.tool(
26990
+ "get_movement",
26991
+ "Get a Movement by ID. Returns full details including status, approval timestamps, and audit stamps.",
26992
+ {
26993
+ movementId: external_exports.string().describe("The movement ID (e.g., mv-abc123)")
26994
+ },
26995
+ async ({ movementId }) => {
26996
+ const m = await client.getMovement(movementId);
26997
+ if (!m) {
26998
+ return { content: [{ type: "text", text: `Movement not found: ${movementId}` }] };
26999
+ }
27000
+ const lines = [
27001
+ `ID: ${m.movementId}`,
27002
+ `Code: ${m.movementCode}`,
27003
+ `Title: ${m.title}`,
27004
+ `Teamspace: ${m.teamspaceId}`,
27005
+ `Status: ${m.status}`,
27006
+ m.description ? `Description: ${m.description}` : null,
27007
+ m.proposedAt ? `Proposed: ${m.proposedAt}` : null,
27008
+ m.approvedAt ? `Approved: ${m.approvedAt}` : null,
27009
+ m.approvalNoteId ? `Approval Note: ${m.approvalNoteId}` : null,
27010
+ m.createdBy ? `Created By: ${m.createdBy}` : null,
27011
+ `Version: ${m.version}`,
27012
+ `Created: ${m.createdAt}`,
27013
+ `Updated: ${m.updatedAt}`
27014
+ ].filter(Boolean).join("\n");
27015
+ return { content: [{ type: "text", text: lines }] };
27016
+ }
27017
+ );
27018
+ server.tool(
27019
+ "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.',
27021
+ {
27022
+ teamspaceId: external_exports.string().describe("The teamspace ID"),
27023
+ title: external_exports.string().min(1).max(300).describe('Movement title (e.g. "Q3 Platform Modernisation SOW")'),
27024
+ 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.")
27026
+ },
27027
+ async ({ teamspaceId, title, description, accountId }) => {
27028
+ const m = await client.createMovement({
27029
+ teamspaceId,
27030
+ title,
27031
+ ...description !== void 0 && { description },
27032
+ ...accountId !== void 0 && { accountId }
27033
+ });
27034
+ return {
27035
+ content: [
27036
+ {
27037
+ type: "text",
27038
+ text: `Created Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})`
27039
+ }
27040
+ ]
27041
+ };
27042
+ }
27043
+ );
27044
+ server.tool(
27045
+ "update_movement",
27046
+ "Update mutable content fields on a Movement (title, description). To change status use transition_movement_status instead.",
27047
+ {
27048
+ movementId: external_exports.string().describe("The movement ID"),
27049
+ title: external_exports.string().min(1).max(300).optional().describe("New title"),
27050
+ 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.")
27052
+ },
27053
+ async ({ movementId, title, description, accountId }) => {
27054
+ const hasUpdates = title !== void 0 || description !== void 0 || accountId !== void 0;
27055
+ if (!hasUpdates) {
27056
+ return {
27057
+ content: [{ type: "text", text: `No updates provided for Movement: ${movementId}` }],
27058
+ isError: true
27059
+ };
27060
+ }
27061
+ const resolvedAccountId = accountId === "" ? null : accountId;
27062
+ try {
27063
+ const m = await client.updateMovement(movementId, {
27064
+ ...title !== void 0 && { title },
27065
+ ...description !== void 0 && { description },
27066
+ ...accountId !== void 0 && { accountId: resolvedAccountId }
27067
+ });
27068
+ if (!m) {
27069
+ return {
27070
+ content: [{ type: "text", text: `Movement not found: ${movementId}` }],
27071
+ isError: true
27072
+ };
27073
+ }
27074
+ const account = accountId !== void 0 ? resolvedAccountId === null ? " \u2014 Account: (unlinked)" : ` \u2014 Account: ${resolvedAccountId}` : m.accountId ? ` \u2014 Account: ${m.accountId}` : "";
27075
+ return {
27076
+ content: [
27077
+ {
27078
+ type: "text",
27079
+ text: `Updated Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})${account}`
27080
+ }
27081
+ ]
27082
+ };
27083
+ } catch (err) {
27084
+ const message = err instanceof Error ? err.message : String(err);
27085
+ return {
27086
+ content: [{ type: "text", text: `Failed to update Movement: ${message}` }],
27087
+ isError: true
27088
+ };
27089
+ }
27090
+ }
27091
+ );
27092
+ server.tool(
27093
+ "transition_movement_status",
27094
+ 'Transition a Movement through its commercial lifecycle: draft \u2192 proposed \u2192 approved \u2192 active \u2192 fulfilled. Cancelled is reachable from any non-terminal state and is how a superseded Movement is retired. Advancing to "approved" requires an approvalNoteId. Use confirm=false (default) to preview the transition without writing; confirm=true to execute.',
27095
+ {
27096
+ movementId: external_exports.string().describe("The movement ID"),
27097
+ targetState: external_exports.enum(["proposed", "approved", "active", "fulfilled", "cancelled"]).describe("Target lifecycle state"),
27098
+ approvalNoteId: external_exports.string().optional().describe('Required when transitioning to "approved" \u2014 the Note ID capturing the approval decision'),
27099
+ reason: external_exports.string().optional().describe("Reason for the transition. Strongly recommended for cancellation \u2014 retiring a Movement with no recorded reason leaves no audit trail"),
27100
+ confirm: external_exports.boolean().optional().default(false).describe("false (default) = dry-run preview; true = execute the transition")
27101
+ },
27102
+ async ({ movementId, targetState, approvalNoteId, reason, confirm }) => {
27103
+ if (!confirm) {
27104
+ return {
27105
+ content: [
27106
+ {
27107
+ type: "text",
27108
+ text: `Dry-run: would transition Movement ${movementId} \u2192 ${targetState}${approvalNoteId ? ` (approvalNoteId: ${approvalNoteId})` : ""}${reason ? ` \u2014 ${reason}` : ""}. Call again with confirm=true to execute.`
27109
+ }
27110
+ ]
27111
+ };
27112
+ }
27113
+ try {
27114
+ const m = await client.transitionMovementStatus(movementId, targetState, {
27115
+ ...approvalNoteId !== void 0 && { approvalNoteId },
27116
+ ...reason !== void 0 && { reason }
27117
+ });
27118
+ if (!m) {
27119
+ return {
27120
+ content: [{ type: "text", text: `Movement not found: ${movementId}` }],
27121
+ isError: true
27122
+ };
27123
+ }
27124
+ return {
27125
+ content: [
27126
+ {
27127
+ type: "text",
27128
+ text: `Transitioned Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} \u2192 ${m.status}`
27129
+ }
27130
+ ]
27131
+ };
27132
+ } catch (err) {
27133
+ const message = err instanceof Error ? err.message : String(err);
27134
+ return {
27135
+ content: [{ type: "text", text: `Failed to transition Movement: ${message}` }],
27136
+ isError: true
27137
+ };
27138
+ }
27139
+ }
27140
+ );
27141
+ }
27142
+
27143
+ // ../../libs/harmonica-services/src/mcp/tools/next-actions-tools.ts
27144
+ var PRIORITY_ICONS = {
27145
+ P0: "\u{1F6A8}",
27146
+ P1: "\u{1F680}",
27147
+ P2: "\u{1F528}",
27148
+ P3: "\u{1F4CB}",
27149
+ P4: "\u{1F4A1}",
27150
+ P5: "\u2728"
27151
+ };
27152
+ function formatNextActions(actions) {
27153
+ if (actions.length === 0) {
27154
+ return "No actions found. Project is up to date.";
27155
+ }
27156
+ const lines = ["# Next Actions\n"];
27157
+ for (const action of actions) {
27158
+ const icon = PRIORITY_ICONS[action.priority] ?? "\u2022";
27159
+ lines.push(`## ${icon} ${action.priority} \u2014 ${action.category}`);
27160
+ lines.push(`**${action.title}**`);
27161
+ lines.push(action.description);
27162
+ if (action.entityId) {
27163
+ const parts = [`${action.entityType}: ${action.entityId}`];
27164
+ if (action.beatId) parts.push(`Beat: ${action.beatId}`);
27165
+ lines.push(parts.join(" | "));
27166
+ }
27167
+ if (action.count) {
27168
+ lines.push(`Count: ${action.count}`);
27169
+ }
27170
+ lines.push("");
27171
+ }
27172
+ return lines.join("\n");
27173
+ }
27174
+ function registerNextActionsTools(server, ctx, client) {
27175
+ server.tool(
27176
+ "get_next_actions",
27177
+ "Get prioritized list of recommended next actions for a system. Analyzes Beats, Revisions, and Notes to surface the highest-priority work: P0 (urgent bugs) \u2192 P1 (deploy) \u2192 P2 (build) \u2192 P3 (plan) \u2192 P4 (compose) \u2192 P5 (maintenance).",
27178
+ {
27179
+ systemId: external_exports.string().describe("The system ID"),
27180
+ limit: external_exports.coerce.number().optional().default(10).describe("Max actions to return")
27181
+ },
27182
+ async ({ systemId, limit }) => {
27183
+ try {
27184
+ await assertSystemExists(client, systemId);
27185
+ const actions = await client.getNextActions(systemId, { limit });
27186
+ return { content: [{ type: "text", text: formatNextActions(actions) }] };
27187
+ } catch (err) {
27188
+ const message = err instanceof Error ? err.message : String(err);
27189
+ return { content: [{ type: "text", text: `Failed to get next actions: ${message}` }], isError: true };
27190
+ }
27191
+ }
27192
+ );
27193
+ server.tool(
27194
+ "list_triage_actions",
27195
+ "List active triage actions from the ledger (open + acted). This is the stateful view of what needs attention \u2014 includes status tracking, task progress, and escalation state. Use this instead of get_next_actions when triage has been run.",
27196
+ {
27197
+ systemId: external_exports.string().describe("The system ID")
27198
+ },
27199
+ async ({ systemId }) => {
27200
+ try {
27201
+ await assertSystemExists(client, systemId);
27202
+ const actions = await client.listTriageActions(systemId);
27203
+ return { content: [{ type: "text", text: formatTriageActions(actions) }] };
27204
+ } catch (err) {
27205
+ const message = err instanceof Error ? err.message : String(err);
27206
+ return { content: [{ type: "text", text: `Failed to list triage actions: ${message}` }], isError: true };
27207
+ }
27208
+ }
27209
+ );
27210
+ server.tool(
27211
+ "dismiss_triage_action",
27212
+ 'Dismiss a triage action (human decides "not now"). The action will not be re-escalated until the underlying state changes.',
27213
+ {
27214
+ systemId: external_exports.string().describe("The system ID"),
27215
+ actionKey: external_exports.string().describe("The action key to dismiss (from list_triage_actions)")
27216
+ },
27217
+ async ({ systemId, actionKey }) => {
27218
+ try {
27219
+ await assertSystemExists(client, systemId);
27220
+ await client.dismissTriageAction(systemId, actionKey, ctx.user?.userId ?? "mcp");
27221
+ return { content: [{ type: "text", text: `Action ${actionKey} dismissed.` }] };
27222
+ } catch (err) {
27223
+ const message = err instanceof Error ? err.message : String(err);
27224
+ return { content: [{ type: "text", text: `Failed to dismiss action: ${message}` }], isError: true };
27225
+ }
27226
+ }
27227
+ );
27228
+ }
27229
+ var STATUS_ICONS = {
27230
+ open: "\u2B1A",
27231
+ acted: "\u25D0",
27232
+ dismissed: "\u2715",
27233
+ resolved: "\u2713"
27234
+ };
27235
+ function formatTriageActions(actions) {
27236
+ if (actions.length === 0) {
27237
+ return "No active triage actions. Run triage to populate the ledger.";
27238
+ }
27239
+ const lines = [`# Triage Actions (${actions.length})
27240
+ `];
27241
+ for (const action of actions) {
27242
+ const pIcon = PRIORITY_ICONS[action.priority] ?? "\u2022";
27243
+ const sIcon = STATUS_ICONS[action.status] ?? "?";
27244
+ lines.push(`## ${pIcon} ${action.priority} \u2014 ${action.category} ${sIcon} ${action.status.toUpperCase()}`);
27245
+ lines.push(`**${action.title}**`);
27246
+ lines.push(action.description);
27247
+ const meta = [];
27248
+ if (action.entityId) meta.push(`Entity: ${action.entityId}`);
27249
+ if (action.taskId) meta.push(`Task: ${action.taskId}${action.taskStatus ? ` (${action.taskStatus})` : ""}`);
27250
+ if (action.decision) meta.push(`Decision: ${action.decision}`);
27251
+ if (meta.length > 0) lines.push(meta.join(" | "));
27252
+ lines.push(`Action key: \`${action.actionKey}\` | First seen: ${action.firstSeenAt}`);
27253
+ lines.push("");
27254
+ }
27255
+ return lines.join("\n");
27256
+ }
27257
+
27303
27258
  // ../../libs/harmonica-services/src/mcp/tools/note-element-tools.ts
27259
+ var import_crypto3 = require("crypto");
27304
27260
  var parentKindSchema = external_exports.enum(["notebook", "note"]);
27305
27261
  function formatEl(e) {
27306
27262
  const binding = e.query ? `**Live collection:** ${e.query.noteType} @ ${e.query.scope} (limit ${e.query.limit}${e.query.sortBy ? `, ${e.query.sortBy}` : ""})` : `**Child Note:** ${e.childNoteId ?? "(none)"}`;
@@ -27955,7 +27911,13 @@ ${text}` }] };
27955
27911
  try {
27956
27912
  await assertSystemExists(client, systemId);
27957
27913
  const note = await client.getNote(noteId);
27958
- if (!note || (note.systemId ?? note.projectId) !== systemId) {
27914
+ if (!note) {
27915
+ return { content: [{ type: "text", text: `Note not found: "${noteId}"` }], isError: true };
27916
+ }
27917
+ if (!note.systemId) {
27918
+ return { content: [{ type: "text", text: `Note "${noteId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
27919
+ }
27920
+ if (note.systemId !== systemId) {
27959
27921
  return { content: [{ type: "text", text: `Note not found: "${noteId}"` }], isError: true };
27960
27922
  }
27961
27923
  const removeReason = reason ?? "Removed via remove_note";
@@ -27982,7 +27944,13 @@ ${text}` }] };
27982
27944
  try {
27983
27945
  await assertSystemExists(client, systemId);
27984
27946
  const note = await client.getNote(noteId);
27985
- if (!note || (note.systemId ?? note.projectId) !== systemId) {
27947
+ if (!note) {
27948
+ return { content: [{ type: "text", text: `Document not found: "${noteId}"` }], isError: true };
27949
+ }
27950
+ if (!note.systemId) {
27951
+ return { content: [{ type: "text", text: `Document "${noteId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
27952
+ }
27953
+ if (note.systemId !== systemId) {
27986
27954
  return { content: [{ type: "text", text: `Document not found: "${noteId}"` }], isError: true };
27987
27955
  }
27988
27956
  if (note.noteType !== "document") {
@@ -28045,7 +28013,7 @@ ${trimmed}` : `# ${title}`;
28045
28013
  }
28046
28014
 
28047
28015
  // ../../libs/harmonica-services/src/mcp/tools/notebook-tools.ts
28048
- var SCOPE_TYPE_VALUES = ["org", "teamspace", "system", "account", "project"];
28016
+ var SCOPE_TYPE_VALUES = ["org", "teamspace", "system", "account"];
28049
28017
  var scopeRefSchema = external_exports.object({
28050
28018
  scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, system, or account"),
28051
28019
  scopeId: external_exports.string().min(1).describe("ID of the scope entity")
@@ -28271,14 +28239,23 @@ ${formatTree(tree.root)}` }] };
28271
28239
  }
28272
28240
  let scopeSystemId = systemId;
28273
28241
  if (!scopeSystemId) {
28242
+ let bindings;
28243
+ let bindingsFetchFailed = false;
28274
28244
  try {
28275
- const bindings = await client.getNotebookScopeBindings(notebookId);
28276
- scopeSystemId = bindings?.find((b) => b.scopeType === "system" || b.scopeType === "project")?.scopeId;
28245
+ bindings = await client.getNotebookScopeBindings(notebookId);
28246
+ scopeSystemId = bindings?.find((b) => b.scopeType === "system")?.scopeId;
28277
28247
  } catch {
28248
+ bindingsFetchFailed = true;
28249
+ }
28250
+ if (!scopeSystemId) {
28251
+ if (!bindingsFetchFailed) {
28252
+ const hasLegacyProjectBinding = bindings?.some((b) => b.scopeType === "project");
28253
+ if (hasLegacyProjectBinding) {
28254
+ 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 };
28255
+ }
28256
+ }
28257
+ return { content: [{ type: "text", text: "No system scope: pass systemId or bind the notebook to a system first." }], isError: true };
28278
28258
  }
28279
- }
28280
- if (!scopeSystemId) {
28281
- return { content: [{ type: "text", text: "No system scope: pass systemId or bind the notebook to a system first." }], isError: true };
28282
28259
  }
28283
28260
  let tree;
28284
28261
  try {
@@ -28892,8 +28869,7 @@ Error: ${msg}`);
28892
28869
  const createdTracks = [];
28893
28870
  for (const trackInput of tracksToCreate) {
28894
28871
  try {
28895
- const track = await client.createDeliverableGroup({
28896
- orgId,
28872
+ const track = await client.createTrack({
28897
28873
  teamspaceId: teamspace.teamspaceId,
28898
28874
  movementId: createdMovementId,
28899
28875
  title: trackInput.title,
@@ -28901,7 +28877,7 @@ Error: ${msg}`);
28901
28877
  vuRate: 0,
28902
28878
  createdBy: ctx.user?.userId
28903
28879
  });
28904
- createdTracks.push({ id: track.deliverableGroupId, title: track.title });
28880
+ createdTracks.push({ id: track.trackId, title: track.title });
28905
28881
  } catch (err) {
28906
28882
  const msg = err instanceof Error ? err.message : String(err);
28907
28883
  lines.unshift(`\u26A0\uFE0F Onboarding batch partially created \u2014 stopped at: track:${trackInput.title}
@@ -28997,7 +28973,7 @@ ${d.rationale}${d.suggestions?.length ? `
28997
28973
  function registerOrganizationTools(server, ctx, client) {
28998
28974
  server.tool(
28999
28975
  "get_organization",
29000
- "Get details for an organization by ID \u2014 returns name, slug, status, industry, and description",
28976
+ "Get details for an organization by ID \u2014 returns name, slug, status, industry, description, and coda",
29001
28977
  {
29002
28978
  orgId: external_exports.string().describe("The organization ID")
29003
28979
  },
@@ -29012,7 +28988,9 @@ function registerOrganizationTools(server, ctx, client) {
29012
28988
  `**Slug:** ${org.slug}`,
29013
28989
  `**Status:** ${org.status}`,
29014
28990
  org.industry ? `**Industry:** ${org.industry}` : "",
29015
- org.description ? `**Description:** ${org.description}` : ""
28991
+ org.description ? `**Description:** ${org.description}` : "",
28992
+ org.coda ? `**Coda:**
28993
+ ${org.coda}` : ""
29016
28994
  ].filter(Boolean);
29017
28995
  return { content: [{ type: "text", text: lines.join("\n") }] };
29018
28996
  }
@@ -29651,9 +29629,9 @@ function registerRevisionLifecycleTools(server, ctx, client) {
29651
29629
  if (!beat) {
29652
29630
  return { content: [{ type: "text", text: `Beat not found: "${beatId}"` }], isError: true };
29653
29631
  }
29654
- const beatSystemId = beat.systemId ?? beat.projectId;
29632
+ const beatSystemId = beat.systemId;
29655
29633
  if (!beatSystemId) {
29656
- return { content: [{ type: "text", text: `Beat "${beatId}" has no system identifier \u2014 record may be corrupt.` }], isError: true };
29634
+ return { content: [{ type: "text", text: `Beat "${beatId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
29657
29635
  }
29658
29636
  let parentBv;
29659
29637
  if (beatVersionId !== void 0) {
@@ -29661,9 +29639,9 @@ function registerRevisionLifecycleTools(server, ctx, client) {
29661
29639
  if (!parentBv) {
29662
29640
  return { content: [{ type: "text", text: `Beat Version not found: "${beatVersionId}"` }], isError: true };
29663
29641
  }
29664
- const bvSystemId = parentBv.systemId ?? parentBv.projectId;
29642
+ const bvSystemId = parentBv.systemId;
29665
29643
  if (!bvSystemId) {
29666
- return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" has no system identifier \u2014 record may be corrupt.` }], isError: true };
29644
+ return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
29667
29645
  }
29668
29646
  if (parentBv.beatId !== beat.beatId || bvSystemId !== beatSystemId) {
29669
29647
  return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" belongs to a different Beat.` }], isError: true };
@@ -29772,13 +29750,13 @@ function registerRevisionLifecycleTools(server, ctx, client) {
29772
29750
  if (!bv) {
29773
29751
  return { content: [{ type: "text", text: `Beat Version not found: "${beatVersionId}"` }], isError: true };
29774
29752
  }
29775
- const bvSystemId = bv.systemId ?? bv.projectId;
29753
+ const bvSystemId = bv.systemId;
29776
29754
  if (!bvSystemId) {
29777
- return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" has no system identifier \u2014 record may be corrupt.` }], isError: true };
29755
+ return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
29778
29756
  }
29779
- const revSystemId = revision.systemId ?? revision.projectId;
29757
+ const revSystemId = revision.systemId;
29780
29758
  if (!revSystemId) {
29781
- return { content: [{ type: "text", text: `Revision "${revisionId}" has no system identifier \u2014 record may be corrupt.` }], isError: true };
29759
+ return { content: [{ type: "text", text: `Revision "${revisionId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
29782
29760
  }
29783
29761
  if (bv.beatId !== revision.beatId || bvSystemId !== revSystemId) {
29784
29762
  return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" belongs to a different Beat.` }], isError: true };
@@ -30667,17 +30645,13 @@ function normalizeSnapshot(raw) {
30667
30645
  accountDropIndex: raw.accountDropIndex ?? [],
30668
30646
  deliverables: raw.deliverables ?? [],
30669
30647
  teamspaceDeliverableIndex: raw.teamspaceDeliverableIndex ?? [],
30670
- // Compat: v3 and earlier snapshots have 'projectDeliverableIndex'
30671
- systemDeliverableIndex: raw.systemDeliverableIndex ?? anyRaw["projectDeliverableIndex"] ?? [],
30648
+ systemDeliverableIndex: raw.systemDeliverableIndex ?? [],
30672
30649
  teamspaces: raw.teamspaces ?? [],
30673
30650
  teamspaceAccountIndex: raw.teamspaceAccountIndex ?? [],
30674
30651
  accountTeamspaceIndex: raw.accountTeamspaceIndex ?? [],
30675
30652
  layers: raw.layers ?? [],
30676
30653
  layerIndex: raw.layerIndex ?? [],
30677
30654
  tracks: raw.tracks ?? [],
30678
- // Compat: early v4 snapshots (before field rename) have 'trackProjectIndex'
30679
- trackSystemIndex: raw.trackSystemIndex ?? anyRaw["trackProjectIndex"] ?? [],
30680
- trackTeamspaceIndex: raw.trackTeamspaceIndex ?? [],
30681
30655
  workItems: raw.workItems ?? [],
30682
30656
  workItemIndex: raw.workItemIndex ?? [],
30683
30657
  accounts: raw.accounts ?? []
@@ -30769,10 +30743,7 @@ function assertSnapshotShape(val, sourceHint) {
30769
30743
 
30770
30744
  // ../../libs/harmonica-services/src/mcp/tools/subscription-tools.ts
30771
30745
  var ENTITY_TYPES = ["system", "beat", "revision"];
30772
- var entityTypeField = external_exports.preprocess(
30773
- (v) => v === "project" ? "system" : v,
30774
- external_exports.enum(ENTITY_TYPES)
30775
- );
30746
+ var entityTypeField = external_exports.enum(ENTITY_TYPES);
30776
30747
  function registerSubscriptionTools(server, ctx, client) {
30777
30748
  server.tool(
30778
30749
  "subscribe_to_entity",
@@ -31264,6 +31235,222 @@ function registerTeamspaceTools(server, ctx, client) {
31264
31235
  );
31265
31236
  }
31266
31237
 
31238
+ // ../../libs/harmonica-services/src/mcp/tools/track-tools.ts
31239
+ function registerTrackTools(server, ctx, client) {
31240
+ server.tool(
31241
+ "list_movement_tracks",
31242
+ `List the Tracks scoped to a Movement (client SOW). Returns the commitments grouped under that engagement. Recognition cues: "what's in this SOW", "this engagement's tracks" \u2192 list_movement_tracks.`,
31243
+ {
31244
+ movementId: external_exports.string().describe("The Movement ID (e.g., mv-abc123)")
31245
+ },
31246
+ async ({ movementId }) => {
31247
+ await assertMovementExists(client, movementId);
31248
+ const tracks = await client.listMovementTracks(movementId);
31249
+ const text = tracks.length === 0 ? "No tracks found for this Movement." : tracks.map(
31250
+ (t) => `[${t.trackId}] ${t.trackCode} \u2014 ${t.title} (${t.status})`
31251
+ ).join("\n");
31252
+ return { content: [{ type: "text", text }] };
31253
+ }
31254
+ );
31255
+ server.tool(
31256
+ "get_track",
31257
+ "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
+ },
31261
+ async ({ trackId }) => {
31262
+ const t = await client.getTrack(trackId);
31263
+ if (!t) {
31264
+ return {
31265
+ content: [{ type: "text", text: `Track not found: ${trackId}` }]
31266
+ };
31267
+ }
31268
+ const lines = [
31269
+ `ID: ${t.trackId}`,
31270
+ `Code: ${t.trackCode}`,
31271
+ `Title: ${t.title}`,
31272
+ `Teamspace: ${t.teamspaceId}`,
31273
+ t.movementId ? `Movement: ${t.movementId}` : null,
31274
+ `Status: ${t.status}`,
31275
+ `VU Rate: ${t.vuRate}`,
31276
+ t.description ? `Description: ${t.description}` : null,
31277
+ t.deliverables ? `Deliverables:
31278
+ ${t.deliverables}` : null,
31279
+ t.acceptanceCriteria ? `Acceptance Criteria:
31280
+ ${t.acceptanceCriteria}` : null,
31281
+ t.parentGroupId ? `Parent Track: ${t.parentGroupId}` : null,
31282
+ t.proposedAt ? `Proposed: ${t.proposedAt}` : null,
31283
+ t.approvedAt ? `Approved: ${t.approvedAt}` : null,
31284
+ `Version: ${t.version}`,
31285
+ `Created: ${t.createdAt}`,
31286
+ `Updated: ${t.updatedAt}`
31287
+ ].filter(Boolean).join("\n");
31288
+ return { content: [{ type: "text", text: lines }] };
31289
+ }
31290
+ );
31291
+ const createTrackSchema = {
31292
+ teamspaceId: external_exports.string().describe("The teamspace ID"),
31293
+ title: external_exports.string().min(1).max(300).describe('Track title (e.g. "SOW-2026-Q2 / Platform Modernisation")'),
31294
+ description: external_exports.string().max(2e3).optional().describe("Optional description"),
31295
+ deliverables: external_exports.string().max(2e4).optional().describe("The Track's deliverables as Markdown bullet points (parallel to acceptanceCriteria)"),
31296
+ acceptanceCriteria: external_exports.string().max(2e4).optional().describe("The Track's acceptance criteria (Markdown), parallel to its deliverables"),
31297
+ vuRate: external_exports.number().nonnegative().describe("Value-Unit rate (billing rate for this engagement)"),
31298
+ movementId: external_exports.string().describe("The Movement (client SOW) this Track belongs to. Required: a Track is created on its Movement, which supplies the Teamspace (rev-1880d2f0)."),
31299
+ parentGroupId: external_exports.string().optional().describe("Optional parent Track ID for nested grouping (Phase 0 scaffolding)")
31300
+ };
31301
+ const createTrackHandler = async ({ teamspaceId, title, description, deliverables, acceptanceCriteria, vuRate, movementId, parentGroupId }) => {
31302
+ if (movementId !== void 0) {
31303
+ await assertMovementExists(client, movementId);
31304
+ }
31305
+ const t = await client.createTrack({
31306
+ teamspaceId,
31307
+ title,
31308
+ ...description !== void 0 && { description },
31309
+ ...deliverables !== void 0 && { deliverables },
31310
+ ...acceptanceCriteria !== void 0 && { acceptanceCriteria },
31311
+ vuRate,
31312
+ ...movementId !== void 0 && { movementId },
31313
+ ...parentGroupId !== void 0 && { parentGroupId },
31314
+ createdBy: ctx.user.userId
31315
+ });
31316
+ return {
31317
+ content: [
31318
+ {
31319
+ type: "text",
31320
+ text: `Created Track: [${t.trackId}] ${t.trackCode} \u2014 ${t.title} (${t.status})`
31321
+ }
31322
+ ]
31323
+ };
31324
+ };
31325
+ server.tool(
31326
+ "create_track",
31327
+ '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.',
31328
+ createTrackSchema,
31329
+ createTrackHandler
31330
+ );
31331
+ server.tool(
31332
+ "update_track",
31333
+ "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
+ },
31342
+ async ({ trackId, title, description, deliverables, acceptanceCriteria, vuRate }) => {
31343
+ const t = await client.updateTrack(trackId, {
31344
+ ...title !== void 0 && { title },
31345
+ ...description !== void 0 && { description },
31346
+ ...deliverables !== void 0 && { deliverables },
31347
+ ...acceptanceCriteria !== void 0 && { acceptanceCriteria },
31348
+ ...vuRate !== void 0 && { vuRate }
31349
+ });
31350
+ if (!t) {
31351
+ return {
31352
+ content: [{ type: "text", text: `Track not found: ${trackId}` }]
31353
+ };
31354
+ }
31355
+ return {
31356
+ content: [
31357
+ {
31358
+ type: "text",
31359
+ text: `Updated Track: [${t.trackId}] ${t.trackCode} \u2014 ${t.title} (${t.status})`
31360
+ }
31361
+ ]
31362
+ };
31363
+ }
31364
+ );
31365
+ server.tool(
31366
+ "transition_track",
31367
+ '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
+ },
31375
+ async ({ trackId, targetState, approvalNoteId, reason, confirm }) => {
31376
+ if (!confirm) {
31377
+ return {
31378
+ content: [
31379
+ {
31380
+ type: "text",
31381
+ text: `Dry-run: would transition Track ${trackId} \u2192 ${targetState}${approvalNoteId ? ` (approvalNoteId: ${approvalNoteId})` : ""}${reason ? ` \u2014 ${reason}` : ""}. Call again with confirm=true to execute.`
31382
+ }
31383
+ ]
31384
+ };
31385
+ }
31386
+ const t = await client.transitionTrackStatus(
31387
+ trackId,
31388
+ targetState,
31389
+ {
31390
+ ...approvalNoteId !== void 0 && { approvalNoteId },
31391
+ ...reason !== void 0 && { reason }
31392
+ }
31393
+ );
31394
+ if (!t) {
31395
+ return {
31396
+ content: [{ type: "text", text: `Track not found: ${trackId}` }]
31397
+ };
31398
+ }
31399
+ return {
31400
+ content: [
31401
+ {
31402
+ type: "text",
31403
+ text: `Transitioned Track [${t.trackId}] ${t.trackCode} \u2192 ${t.status}`
31404
+ }
31405
+ ]
31406
+ };
31407
+ }
31408
+ );
31409
+ server.tool(
31410
+ "assign_drop_to_track",
31411
+ `Link a Drop to a Track so it appears on the parent Movement's timed-release axis (the Drops shown on the Movement overview). A Drop belongs to at most one group; linking moves it if it was in another group. Use to answer "put this release under this SOW/phase" or "add a Drop to this Movement".`,
31412
+ {
31413
+ trackId: external_exports.string().describe("The Track ID"),
31414
+ dropId: external_exports.string().describe("The Drop ID to link to the group")
31415
+ },
31416
+ async ({ trackId, dropId }) => {
31417
+ await fetchTrack(client, trackId);
31418
+ const drop = await client.getDrop(dropId);
31419
+ if (!drop) {
31420
+ return { content: [{ type: "text", text: `Drop not found: ${dropId}` }] };
31421
+ }
31422
+ await assertAccountExists(client, drop.accountId);
31423
+ const ok = await client.setDropTrack(trackId, dropId);
31424
+ if (!ok) {
31425
+ return { content: [{ type: "text", text: `Drop not found: ${dropId}` }] };
31426
+ }
31427
+ return {
31428
+ content: [{ type: "text", text: `Linked Drop ${dropId} to Track ${trackId}.` }]
31429
+ };
31430
+ }
31431
+ );
31432
+ server.tool(
31433
+ "list_track_beat_versions",
31434
+ 'List the Beat Versions linked to a Track via its work items (Work Plan). Use to answer "what feature increments are scoped under this SOW/phase?"',
31435
+ {
31436
+ trackId: external_exports.string().describe("The Track ID")
31437
+ },
31438
+ async ({ trackId }) => {
31439
+ await fetchTrack(client, trackId);
31440
+ const workItems = await client.listTrackWorkItems(trackId);
31441
+ const bvIds = [...new Set(
31442
+ workItems.map((wi) => wi.beatVersionId).filter((id) => id !== void 0)
31443
+ )];
31444
+ if (bvIds.length === 0) {
31445
+ return { content: [{ type: "text", text: "No beat versions linked via work items in this track." }] };
31446
+ }
31447
+ const hydrated = await Promise.all(bvIds.map((id) => client.getBeatVersion(id)));
31448
+ const text = hydrated.filter((bv) => bv !== void 0).map((bv) => `[${bv.beatVersionId}] v${bv.versionNumber} \u2014 ${bv.title} (${bv.status})`).join("\n");
31449
+ return { content: [{ type: "text", text }] };
31450
+ }
31451
+ );
31452
+ }
31453
+
31267
31454
  // ../../libs/harmonica-services/src/mcp/tools/value-velocity-tools.ts
31268
31455
  var lensSchema = external_exports.enum(["velocity", "roi", "valuePrimary"]).default("velocity");
31269
31456
  function resolveActorId(ctx) {
@@ -31642,6 +31829,7 @@ function registerWorkItemTools(server, _ctx, client) {
31642
31829
  wi.costPerValueUnit !== void 0 ? `Cost per Value Unit: ${wi.costPerValueUnit}` : null,
31643
31830
  wi.beatVersionId ? `Linked Beat Version: ${wi.beatVersionId}` : null,
31644
31831
  wi.workOrderId ? `Work Order: ${wi.workOrderId}` : "Work Order: none (unattached)",
31832
+ wi.barId ? `Bar: ${wi.barId}` : "Bar: none (unplanned)",
31645
31833
  estimateHint ? `Estimate Hint: ${estimateHint.hours}h (source: ${estimateHint.source})` : null,
31646
31834
  wi.createdBy ? `Created By: ${wi.createdBy}` : null,
31647
31835
  `Created: ${wi.createdAt}`,
@@ -31652,7 +31840,7 @@ function registerWorkItemTools(server, _ctx, client) {
31652
31840
  );
31653
31841
  server.tool(
31654
31842
  "create_work_item",
31655
- '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).',
31843
+ '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).',
31656
31844
  {
31657
31845
  trackId: external_exports.string().describe("The Track (DeliverableGroup) ID this work item belongs to"),
31658
31846
  title: external_exports.string().min(1).max(300).describe("Work item title"),
@@ -31664,9 +31852,10 @@ function registerWorkItemTools(server, _ctx, client) {
31664
31852
  costPerValueUnit: external_exports.number().nonnegative().optional().describe("Optional price per Value Unit"),
31665
31853
  createdBy: external_exports.string().optional().describe("Optional creator identity for audit"),
31666
31854
  beatVersionId: external_exports.string().min(1).optional().describe("Optional Beat Version to link for capability lineage. Must exist."),
31667
- workOrderId: external_exports.string().min(1).max(128).optional().describe("Optional Work Order this item delivers part of. Must exist. Omit when there is no Order \u2014 unattached work is legitimate, so do not invent one.")
31855
+ workOrderId: external_exports.string().min(1).max(128).optional().describe("Optional Work Order this item delivers part of. Must exist. Omit when there is no Order \u2014 unattached work is legitimate, so do not invent one."),
31856
+ barId: external_exports.string().min(1).max(128).optional().describe("Optional Bar this item is planned into. Must exist, in this org. Omit when the work is not planned into a Bar yet \u2014 unplanned work is a real state, so do not reach for the current Bar to fill the field.")
31668
31857
  },
31669
- async ({ trackId, title, ownerName, ownerEmail, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId, workOrderId }) => {
31858
+ async ({ trackId, title, ownerName, ownerEmail, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId, workOrderId, barId }) => {
31670
31859
  if (ownerName === void 0 !== (ownerEmail === void 0)) {
31671
31860
  return {
31672
31861
  content: [{ type: "text", text: "Setting the owner requires both ownerName and ownerEmail. Omit both to create an unowned work item." }],
@@ -31683,7 +31872,8 @@ function registerWorkItemTools(server, _ctx, client) {
31683
31872
  ...costPerValueUnit !== void 0 && { costPerValueUnit },
31684
31873
  ...createdBy !== void 0 && { createdBy },
31685
31874
  ...beatVersionId !== void 0 && { beatVersionId },
31686
- ...workOrderId !== void 0 && { workOrderId }
31875
+ ...workOrderId !== void 0 && { workOrderId },
31876
+ ...barId !== void 0 && { barId }
31687
31877
  });
31688
31878
  return {
31689
31879
  content: [
@@ -31697,7 +31887,7 @@ function registerWorkItemTools(server, _ctx, client) {
31697
31887
  );
31698
31888
  server.tool(
31699
31889
  "update_work_item",
31700
- "Update mutable content fields on a work item (title, owner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId, workOrderId). To move it between Tracks use move_work_item; to change its status use transition_work_item_status.",
31890
+ "Update mutable content fields on a work item (title, owner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId, workOrderId, barId). To move it between Tracks use move_work_item; to change its status use transition_work_item_status.",
31701
31891
  {
31702
31892
  workItemId: external_exports.string().describe("The work item ID"),
31703
31893
  title: external_exports.string().min(1).max(300).optional().describe("New title"),
@@ -31710,9 +31900,10 @@ function registerWorkItemTools(server, _ctx, client) {
31710
31900
  valueUnits: external_exports.number().nonnegative().nullable().optional().describe("Value Units (commercial lens). Pass null to clear; omit to leave untouched."),
31711
31901
  costPerValueUnit: external_exports.number().nonnegative().nullable().optional().describe("Price per Value Unit. Pass null to clear; omit to leave untouched."),
31712
31902
  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."),
31713
- workOrderId: external_exports.string().min(1).max(128).nullable().optional().describe("Link to the Work Order this item delivers part of. Pass null to unlink, leaving the item unattached; omit to leave the current link untouched.")
31903
+ workOrderId: external_exports.string().min(1).max(128).nullable().optional().describe("Link to the Work Order this item delivers part of. Pass null to unlink, leaving the item unattached; omit to leave the current link untouched."),
31904
+ barId: external_exports.string().min(1).max(128).nullable().optional().describe("Link to the Bar this item is planned into. Pass null to unlink, leaving the item unplanned; omit to leave the current link untouched.")
31714
31905
  },
31715
- async ({ workItemId, title, ownerName, ownerEmail, unassignOwner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId, workOrderId }) => {
31906
+ async ({ workItemId, title, ownerName, ownerEmail, unassignOwner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId, workOrderId, barId }) => {
31716
31907
  const hasOwnerUpdate = ownerName !== void 0 || ownerEmail !== void 0;
31717
31908
  if (hasOwnerUpdate && (ownerName === void 0 || ownerEmail === void 0)) {
31718
31909
  return {
@@ -31728,7 +31919,7 @@ function registerWorkItemTools(server, _ctx, client) {
31728
31919
  }
31729
31920
  const hasUpdates = title !== void 0 || hasOwnerUpdate || unassignOwner === true || committedEstimateHours !== void 0 || actualHours !== void 0 || effortSize !== void 0 || valueUnits !== void 0 || costPerValueUnit !== void 0 || beatVersionId !== void 0 || // `!== undefined`, never truthiness: an unlink arrives as `null`, and
31730
31921
  // a truthy check here would report "no updates provided" and drop it.
31731
- workOrderId !== void 0;
31922
+ workOrderId !== void 0 || barId !== void 0;
31732
31923
  if (!hasUpdates) {
31733
31924
  return {
31734
31925
  content: [{ type: "text", text: `No updates provided for work item: ${workItemId}` }],
@@ -31745,7 +31936,8 @@ function registerWorkItemTools(server, _ctx, client) {
31745
31936
  ...valueUnits !== void 0 && { valueUnits },
31746
31937
  ...costPerValueUnit !== void 0 && { costPerValueUnit },
31747
31938
  ...beatVersionId !== void 0 && { beatVersionId },
31748
- ...workOrderId !== void 0 && { workOrderId }
31939
+ ...workOrderId !== void 0 && { workOrderId },
31940
+ ...barId !== void 0 && { barId }
31749
31941
  });
31750
31942
  if (!wi) {
31751
31943
  return {
@@ -31834,19 +32026,33 @@ function oneLine(value) {
31834
32026
  }
31835
32027
  function formatOrderLine(o) {
31836
32028
  const reason = o.needsDecisionReason ? ` [${o.needsDecisionReason}]` : "";
31837
- return `[${o.workOrderId}] ${oneLine(o.title)} \u2014 ${o.status}${reason}, ${o.size ?? "unsized"}, on ${o.trackId}`;
32029
+ const track = o.trackId ? `on ${o.trackId}` : "unplaced";
32030
+ return `[${o.workOrderId}] ${oneLine(o.title)} \u2014 ${o.status}${reason}, ${o.size ?? "unsized"}, ${track}`;
31838
32031
  }
31839
32032
  function registerWorkOrderTools(server, _ctx, client) {
31840
32033
  server.tool(
31841
32034
  "list_track_work_orders",
31842
- 'List the Work Orders placed on a Track (DeliverableGroup). A Work Order is the internal unit of agreed work. Recognition cues: "what is committed on this track", "list the orders" \u2192 list_track_work_orders.',
32035
+ "DEPRECATED \u2014 use list_movement_work_orders instead. A Work Order now belongs to its Request and Movement, not its Track. This alias returns a migration message for one minor release rather than failing outright.",
32036
+ // Optional on purpose: a caller that has already dropped the argument is
32037
+ // exactly the caller this alias exists to greet. Requiring it would answer
32038
+ // the migrated caller with a schema error instead of the migration message.
31843
32039
  {
31844
- trackId: external_exports.string().describe("The Track (DeliverableGroup) ID")
32040
+ trackId: external_exports.string().optional().describe("Ignored \u2014 the Track (DeliverableGroup) ID is no longer meaningful for Order scoping")
31845
32041
  },
31846
- async ({ trackId }) => {
31847
- const orders = await client.listTrackWorkOrders(trackId);
31848
- const text = orders.length === 0 ? "No work orders found." : orders.map(formatOrderLine).join("\n");
31849
- return { content: [{ type: "text", text }] };
32042
+ async () => {
32043
+ return {
32044
+ content: [
32045
+ {
32046
+ type: "text",
32047
+ 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."
32048
+ }
32049
+ ],
32050
+ // The caller asked for a list and is getting none. A result without
32051
+ // this flag reads as a successful empty listing to any client that
32052
+ // checks the field rather than the prose — the same reason every other
32053
+ // "your call did nothing" return in this file sets it.
32054
+ isError: true
32055
+ };
31850
32056
  }
31851
32057
  );
31852
32058
  server.tool(
@@ -31890,18 +32096,16 @@ function registerWorkOrderTools(server, _ctx, client) {
31890
32096
  );
31891
32097
  server.tool(
31892
32098
  "create_work_order",
31893
- 'Split a Work Request into an agreed Work Order on a Track. 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 Track must belong to the same teamspace and Account as the Request; the service refuses a placement that crosses either boundary.',
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.`,
31894
32100
  {
31895
- trackId: external_exports.string().describe("The Track (DeliverableGroup) this order is placed on"),
31896
32101
  workRequestId: external_exports.string().min(1).describe("The Work Request this order was split from \u2014 immutable lineage"),
31897
32102
  title: external_exports.string().min(1).max(300).describe("Work order title"),
31898
32103
  description: external_exports.string().max(1e4).optional().describe("Optional detail beyond the title"),
31899
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."),
31900
32105
  createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
31901
32106
  },
31902
- async ({ trackId, workRequestId, title, description, size, createdBy }) => {
32107
+ async ({ workRequestId, title, description, size, createdBy }) => {
31903
32108
  const wo = await client.createWorkOrder({
31904
- trackId,
31905
32109
  workRequestId,
31906
32110
  title,
31907
32111
  ...description !== void 0 && { description },
@@ -31913,9 +32117,21 @@ function registerWorkOrderTools(server, _ctx, client) {
31913
32117
  };
31914
32118
  }
31915
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
+ );
31916
32132
  server.tool(
31917
32133
  "update_work_order",
31918
- "Update content fields on a Work Order (title, description, size). To move it between Tracks use move_work_order; to change its status use transition_work_order_status. The Work Request it came from cannot be changed \u2014 that lineage is immutable.",
32134
+ "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.",
31919
32135
  {
31920
32136
  workOrderId: external_exports.string().describe("The Work Order ID"),
31921
32137
  title: external_exports.string().min(1).max(300).optional().describe("New title"),
@@ -31944,18 +32160,26 @@ function registerWorkOrderTools(server, _ctx, client) {
31944
32160
  );
31945
32161
  server.tool(
31946
32162
  "move_work_order",
31947
- "Move a Work Order to a different Track \u2014 the reversal path for a bad early split guess. The destination must belong to the same teamspace and Account as the order's Work Request; the service refuses a placement that crosses either boundary.",
32163
+ "DEPRECATED \u2014 this tool still responds but no longer moves anything. Work Orders no longer live on a Track; a Track is now attribution on the Work Item. To associate work with a Track, set workOrderId on the relevant Work Items via update_work_item instead.",
32164
+ // Both optional for the same reason as list_track_work_orders: the tool's
32165
+ // only job now is to deliver the migration message, so it must not refuse
32166
+ // the callers who have already stopped sending these.
31948
32167
  {
31949
- workOrderId: external_exports.string().describe("The Work Order ID"),
31950
- newTrackId: external_exports.string().describe("The destination Track (DeliverableGroup) ID")
32168
+ workOrderId: external_exports.string().optional().describe("Ignored \u2014 a Work Order is no longer moveable between Tracks"),
32169
+ newTrackId: external_exports.string().optional().describe("Ignored \u2014 a Track (DeliverableGroup) is no longer a valid parent for Orders")
31951
32170
  },
31952
- async ({ workOrderId, newTrackId }) => {
31953
- const wo = await client.moveWorkOrder(workOrderId, newTrackId);
31954
- if (!wo) {
31955
- return { content: [{ type: "text", text: `Work order not found: ${workOrderId}` }] };
31956
- }
32171
+ async () => {
31957
32172
  return {
31958
- content: [{ type: "text", text: `Moved work order to Track ${wo.trackId}: ${formatOrderLine(wo)}` }]
32173
+ content: [
32174
+ {
32175
+ type: "text",
32176
+ text: "move_work_order is deprecated and has no effect. Work Orders are no longer scoped to a Track. To assign work to a Track, set workOrderId on the relevant Work Items via update_work_item."
32177
+ }
32178
+ ],
32179
+ // "Has no effect" must be machine-readable, not only prose: without
32180
+ // this, a client that moved an Order and checked the result field
32181
+ // records the move as done.
32182
+ isError: true
31959
32183
  };
31960
32184
  }
31961
32185
  );
@@ -32640,7 +32864,7 @@ function registerAllTools(server, ctx, client, profile) {
32640
32864
  registerSubscriptionTools(server, ctx, client);
32641
32865
  registerBaselineTools(server, ctx, client);
32642
32866
  registerTeamspaceTools(server, ctx, client);
32643
- registerDeliverableGroupTools(server, ctx, client);
32867
+ registerTrackTools(server, ctx, client);
32644
32868
  registerDropTools(server, ctx, client);
32645
32869
  registerSessionTools(server, ctx, client);
32646
32870
  registerOnboardingTools(server, ctx, client);
@@ -32648,6 +32872,7 @@ function registerAllTools(server, ctx, client, profile) {
32648
32872
  registerNotebookTools(server, ctx, client);
32649
32873
  registerAccountTools(server, ctx, client);
32650
32874
  registerMovementTools(server, ctx, client);
32875
+ registerExternalMappingTools(server, ctx, client);
32651
32876
  registerWorkItemTools(server, ctx, client);
32652
32877
  registerWorkRequestTools(server, ctx, client);
32653
32878
  registerWorkOrderTools(server, ctx, client);
@@ -32762,6 +32987,13 @@ var ApiError = class extends Error {
32762
32987
  this.name = "ApiError";
32763
32988
  }
32764
32989
  };
32990
+ var ExternalMappingSubjectNotFoundError = class extends Error {
32991
+ code = EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE;
32992
+ constructor(subjectType, subjectId) {
32993
+ super(`Subject ${subjectType} ${subjectId} not found`);
32994
+ this.name = "ExternalMappingSubjectNotFoundError";
32995
+ }
32996
+ };
32765
32997
  function bodyCode(body) {
32766
32998
  try {
32767
32999
  const parsed = JSON.parse(body);
@@ -34244,60 +34476,46 @@ function createHttpClient(config2) {
34244
34476
  updateTeamspace: async (teamspaceId, updates) => {
34245
34477
  return request("PATCH", `/api/teamspaces/${encodeURIComponent(teamspaceId)}`, updates);
34246
34478
  },
34247
- // DeliverableGroups
34248
- listTeamspaceDeliverableGroups: async (teamspaceId) => {
34249
- const res = await request("GET", `/api/teamspaces/${encodeURIComponent(teamspaceId)}/deliverable-groups`);
34250
- if (res === void 0) throw new Error(`Teamspace ${teamspaceId} not found`);
34251
- return res.deliverableGroups;
34252
- },
34253
- listSystemDeliverableGroups: async (systemId) => {
34254
- const res = await request("GET", `/api/systems/${encodeURIComponent(systemId)}/deliverable-groups`);
34255
- if (res === void 0) throw new Error(`System ${systemId} not found`);
34256
- return res.deliverableGroups;
34257
- },
34258
- listMovementDeliverableGroups: async (movementId) => {
34259
- const res = await request("GET", `/api/movements/${encodeURIComponent(movementId)}/deliverable-groups`);
34479
+ // Tracks
34480
+ listMovementTracks: async (movementId) => {
34481
+ const res = await request("GET", `/api/movements/${encodeURIComponent(movementId)}/tracks`);
34260
34482
  if (res === void 0) throw new Error(`Movement ${movementId} not found`);
34261
- return res.deliverableGroups;
34483
+ return res.tracks;
34262
34484
  },
34263
- getDeliverableGroup: async (deliverableGroupId) => {
34264
- return request("GET", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}`);
34485
+ getTrack: async (trackId) => {
34486
+ return request("GET", `/api/tracks/${encodeURIComponent(trackId)}`);
34265
34487
  },
34266
- createDeliverableGroup: async (input) => {
34267
- const { title, description, deliverables, acceptanceCriteria, vuRate, movementId, parentGroupId, createdBy } = input;
34488
+ createTrack: async (input) => {
34489
+ const { title, description, deliverables, acceptanceCriteria, vuRate, movementId, parentGroupId, createdBy, teamspaceId } = input;
34268
34490
  const body = {
34269
34491
  title,
34270
34492
  ...description !== void 0 && { description },
34271
34493
  ...deliverables !== void 0 && { deliverables },
34272
34494
  ...acceptanceCriteria !== void 0 && { acceptanceCriteria },
34273
34495
  vuRate,
34274
- // movementId rides in the body (not the route) so it composes with the
34275
- // project route when a group is both project- and movement-scoped.
34276
34496
  ...movementId !== void 0 && { movementId },
34277
34497
  ...parentGroupId !== void 0 && { parentGroupId },
34278
34498
  ...createdBy !== void 0 && { createdBy }
34279
34499
  };
34280
- const effectiveSystemId = input.systemId;
34281
- const url2 = effectiveSystemId ? `/api/systems/${encodeURIComponent(effectiveSystemId)}/deliverable-groups` : `/api/teamspaces/${encodeURIComponent(input.teamspaceId)}/deliverable-groups`;
34500
+ if (!movementId) {
34501
+ throw new Error(`Cannot create Track "${title}": a movementId is required (teamspaceId ${teamspaceId} alone has no create route).`);
34502
+ }
34503
+ const url2 = `/api/movements/${encodeURIComponent(movementId)}/tracks`;
34282
34504
  const result = await request("POST", url2, body);
34283
- if (result === void 0) throw new Error(`Resource not found when creating DeliverableGroup`);
34505
+ if (result === void 0) throw new Error(`Resource not found when creating Track`);
34284
34506
  return result;
34285
34507
  },
34286
- updateDeliverableGroup: async (deliverableGroupId, updates) => {
34287
- return request("PATCH", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}`, updates);
34508
+ updateTrack: async (trackId, updates) => {
34509
+ return request("PATCH", `/api/tracks/${encodeURIComponent(trackId)}`, updates);
34288
34510
  },
34289
- transitionDeliverableGroupStatus: async (deliverableGroupId, targetState, opts) => {
34290
- return request("PATCH", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/status`, {
34511
+ transitionTrackStatus: async (trackId, targetState, opts) => {
34512
+ return request("PATCH", `/api/tracks/${encodeURIComponent(trackId)}/status`, {
34291
34513
  targetState,
34292
34514
  ...opts ?? {}
34293
34515
  });
34294
34516
  },
34295
- listDeliverableGroupDrops: async (deliverableGroupId) => {
34296
- const res = await request("GET", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/drops`);
34297
- return res?.drops ?? [];
34298
- },
34299
- setDropDeliverableGroup: async (deliverableGroupId, dropId) => {
34300
- const res = await request("POST", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/drops`, { dropId });
34517
+ setDropTrack: async (trackId, dropId) => {
34518
+ const res = await request("POST", `/api/tracks/${encodeURIComponent(trackId)}/drops`, { dropId });
34301
34519
  return res?.success === true;
34302
34520
  },
34303
34521
  // Movements (B-242 / teamspace-scoped SOW container)
@@ -34345,9 +34563,55 @@ function createHttpClient(config2) {
34345
34563
  if (!parsed.success) throw new Error(`Unexpected movements list shape from API: ${parsed.error.message}`);
34346
34564
  return parsed.data.movements;
34347
34565
  },
34566
+ // ExternalMappings (reference-only links to external trackers — B-301 / N-4E09-6311)
34567
+ addExternalMapping: async (input) => {
34568
+ const { subjectType, subjectId, provider, externalKey, url: url2, syncDirection } = input;
34569
+ const body = {
34570
+ subjectType,
34571
+ subjectId,
34572
+ provider,
34573
+ externalKey,
34574
+ ...url2 !== void 0 && { url: url2 },
34575
+ ...syncDirection !== void 0 && { syncDirection }
34576
+ };
34577
+ let res;
34578
+ try {
34579
+ res = await request("POST", `/api/external-mappings`, body);
34580
+ } catch (err) {
34581
+ if (err instanceof ApiError && err.status === 400 && bodyCode(err.body) === EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE) {
34582
+ throw new ExternalMappingSubjectNotFoundError(subjectType, subjectId);
34583
+ }
34584
+ throw err;
34585
+ }
34586
+ if (res === void 0) throw new ExternalMappingSubjectNotFoundError(subjectType, subjectId);
34587
+ const parsed = ExternalMappingApiSchema.safeParse(res.externalMapping);
34588
+ if (!parsed.success) throw new Error(`Unexpected ExternalMapping shape from API: ${parsed.error.message}`);
34589
+ return parsed.data;
34590
+ },
34591
+ updateExternalMapping: async (externalMappingId, updates) => {
34592
+ const res = await request(
34593
+ "PATCH",
34594
+ `/api/external-mappings/${encodeURIComponent(externalMappingId)}`,
34595
+ updates
34596
+ );
34597
+ if (res === void 0) return void 0;
34598
+ const parsed = ExternalMappingApiSchema.safeParse(res.externalMapping);
34599
+ if (!parsed.success) throw new Error(`Unexpected ExternalMapping shape from API: ${parsed.error.message}`);
34600
+ return parsed.data;
34601
+ },
34602
+ listExternalMappings: async (subjectType, subjectId) => {
34603
+ const res = await request(
34604
+ "GET",
34605
+ `/api/external-mappings?subjectType=${encodeURIComponent(subjectType)}&subjectId=${encodeURIComponent(subjectId)}`
34606
+ );
34607
+ if (res === void 0) throw new ExternalMappingSubjectNotFoundError(subjectType, subjectId);
34608
+ const parsed = ExternalMappingsResponseSchema.safeParse(res);
34609
+ if (!parsed.success) throw new Error(`Unexpected ExternalMappings list shape from API: ${parsed.error.message}`);
34610
+ return parsed.data.externalMappings;
34611
+ },
34348
34612
  // WorkItems (WorkItem exposure layer — owned, statused unit of operational work under a Track)
34349
34613
  createWorkItem: async (input) => {
34350
- const { trackId, title, owner, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId, workOrderId } = input;
34614
+ const { trackId, title, owner, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId, workOrderId, barId } = input;
34351
34615
  const body = {
34352
34616
  title,
34353
34617
  ...owner !== void 0 && { owner },
@@ -34357,11 +34621,12 @@ function createHttpClient(config2) {
34357
34621
  ...costPerValueUnit !== void 0 && { costPerValueUnit },
34358
34622
  ...createdBy !== void 0 && { createdBy },
34359
34623
  ...beatVersionId !== void 0 && { beatVersionId },
34360
- ...workOrderId !== void 0 && { workOrderId }
34624
+ ...workOrderId !== void 0 && { workOrderId },
34625
+ ...barId !== void 0 && { barId }
34361
34626
  };
34362
34627
  const res = await request(
34363
34628
  "POST",
34364
- `/api/deliverable-groups/${encodeURIComponent(trackId)}/work-items`,
34629
+ `/api/tracks/${encodeURIComponent(trackId)}/work-items`,
34365
34630
  body
34366
34631
  );
34367
34632
  if (res === void 0) throw new Error(`Track ${trackId} not found`);
@@ -34391,7 +34656,7 @@ function createHttpClient(config2) {
34391
34656
  return parsed.data;
34392
34657
  },
34393
34658
  listTrackWorkItems: async (trackId) => {
34394
- const res = await request("GET", `/api/deliverable-groups/${encodeURIComponent(trackId)}/work-items`);
34659
+ const res = await request("GET", `/api/tracks/${encodeURIComponent(trackId)}/work-items`);
34395
34660
  if (res === void 0) throw new Error(`Track ${trackId} not found`);
34396
34661
  const parsed = WorkItemsResponseSchema.safeParse(res);
34397
34662
  if (!parsed.success) throw new Error(`Unexpected work items list shape from API: ${parsed.error.message}`);
@@ -34475,9 +34740,8 @@ function createHttpClient(config2) {
34475
34740
  return parsed.data.workRequests;
34476
34741
  },
34477
34742
  createWorkOrder: async (input) => {
34478
- const { trackId, workRequestId, title, description, size, createdBy } = input;
34743
+ const { workRequestId, title, description, size, createdBy } = input;
34479
34744
  const body = {
34480
- workRequestId,
34481
34745
  title,
34482
34746
  ...description !== void 0 && { description },
34483
34747
  ...size !== void 0 && { size },
@@ -34485,10 +34749,10 @@ function createHttpClient(config2) {
34485
34749
  };
34486
34750
  const res = await request(
34487
34751
  "POST",
34488
- `/api/deliverable-groups/${encodeURIComponent(trackId)}/work-orders`,
34752
+ `/api/work-requests/${encodeURIComponent(workRequestId)}/work-orders`,
34489
34753
  body
34490
34754
  );
34491
- if (res === void 0) throw new Error(`Track ${trackId} not found`);
34755
+ if (res === void 0) throw new Error(`Work request ${workRequestId} not found`);
34492
34756
  const parsed = WorkOrderApiSchema.safeParse(res.workOrder);
34493
34757
  if (!parsed.success) throw new Error(`Unexpected WorkOrder shape from API: ${parsed.error.message}`);
34494
34758
  return parsed.data;
@@ -34511,8 +34775,15 @@ function createHttpClient(config2) {
34511
34775
  if (!parsed.success) throw new Error(`Unexpected WorkOrder shape from API: ${parsed.error.message}`);
34512
34776
  return parsed.data;
34513
34777
  },
34778
+ listMovementWorkOrders: async (movementId) => {
34779
+ const res = await request("GET", `/api/movements/${encodeURIComponent(movementId)}/work-orders`);
34780
+ if (res === void 0) throw new Error(`Movement ${movementId} not found`);
34781
+ const parsed = WorkOrdersResponseSchema.safeParse(res);
34782
+ if (!parsed.success) throw new Error(`Unexpected work orders list shape from API: ${parsed.error.message}`);
34783
+ return parsed.data.workOrders;
34784
+ },
34514
34785
  listTrackWorkOrders: async (trackId) => {
34515
- const res = await request("GET", `/api/deliverable-groups/${encodeURIComponent(trackId)}/work-orders`);
34786
+ const res = await request("GET", `/api/tracks/${encodeURIComponent(trackId)}/work-orders`);
34516
34787
  if (res === void 0) throw new Error(`Track ${trackId} not found`);
34517
34788
  const parsed = WorkOrdersResponseSchema.safeParse(res);
34518
34789
  if (!parsed.success) throw new Error(`Unexpected work orders list shape from API: ${parsed.error.message}`);
@@ -35098,7 +35369,7 @@ function loadConfig() {
35098
35369
  };
35099
35370
  }
35100
35371
  async function main() {
35101
- console.error(`[harmonica-mcp] v${"4.4.2"} starting\u2026`);
35372
+ console.error(`[harmonica-mcp] v${"5.0.0"} starting\u2026`);
35102
35373
  const config2 = loadConfig();
35103
35374
  const client = createHttpClient({
35104
35375
  apiBaseUrl: config2.apiBaseUrl,