@codazen/harmonica-mcp 4.4.2 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1881 -1508
- 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
|
|
21588
|
-
const
|
|
21589
|
-
if (!
|
|
21590
|
-
throw new Error(`
|
|
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
|
|
21592
|
+
return track;
|
|
21593
21593
|
}
|
|
21594
21594
|
async function fetchNotebook(client, notebookId) {
|
|
21595
21595
|
const notebook = await client.getNotebook(notebookId);
|
|
@@ -22032,22 +22032,33 @@ function registerAuditIntegrityTools(server, ctx, client) {
|
|
|
22032
22032
|
);
|
|
22033
22033
|
}
|
|
22034
22034
|
|
|
22035
|
-
// ../../libs/harmonica-services/src/
|
|
22035
|
+
// ../../libs/harmonica-services/src/tool-schemas/bar.ts
|
|
22036
22036
|
var BAR_STATUSES = ["open", "closed"];
|
|
22037
22037
|
var idSegmentSchema = external_exports.string().min(1).max(128).refine((id) => !id.includes("#"), {
|
|
22038
22038
|
message: "must not contain '#' \u2014 it is a DynamoDB key delimiter"
|
|
22039
22039
|
});
|
|
22040
|
+
var create_bar = {
|
|
22041
|
+
accountId: idSegmentSchema.describe("The Account ID this Bar belongs to"),
|
|
22042
|
+
barId: idSegmentSchema.describe(
|
|
22043
|
+
'Caller-supplied Bar ID (must not contain "#")'
|
|
22044
|
+
),
|
|
22045
|
+
startDate: external_exports.string().datetime().describe("ISO 8601 start of the planning period"),
|
|
22046
|
+
endDate: external_exports.string().datetime().describe("ISO 8601 end of the planning period; must be after startDate"),
|
|
22047
|
+
status: external_exports.enum(BAR_STATUSES).optional().describe('Defaults to "open"')
|
|
22048
|
+
};
|
|
22049
|
+
var get_bar = {
|
|
22050
|
+
barId: idSegmentSchema.describe("The Bar ID")
|
|
22051
|
+
};
|
|
22052
|
+
var list_account_bars = {
|
|
22053
|
+
accountId: idSegmentSchema.describe("The Account ID")
|
|
22054
|
+
};
|
|
22055
|
+
|
|
22056
|
+
// ../../libs/harmonica-services/src/mcp/tools/bar-tools.ts
|
|
22040
22057
|
function registerBarTools(server, _ctx, client) {
|
|
22041
22058
|
server.tool(
|
|
22042
22059
|
"create_bar",
|
|
22043
22060
|
"Create a Bar \u2014 the recurring planning-period time-box a client Account plans and delivers within (e.g. one or two weeks). Bars are Account-scoped and independent of any CadenceConfig already set on the Account; the caller supplies explicit start/end dates.",
|
|
22044
|
-
|
|
22045
|
-
accountId: idSegmentSchema.describe("The Account ID this Bar belongs to"),
|
|
22046
|
-
barId: idSegmentSchema.describe('Caller-supplied Bar ID (must not contain "#")'),
|
|
22047
|
-
startDate: external_exports.string().datetime().describe("ISO 8601 start of the planning period"),
|
|
22048
|
-
endDate: external_exports.string().datetime().describe("ISO 8601 end of the planning period; must be after startDate"),
|
|
22049
|
-
status: external_exports.enum(BAR_STATUSES).optional().describe('Defaults to "open"')
|
|
22050
|
-
},
|
|
22061
|
+
create_bar,
|
|
22051
22062
|
async ({ accountId, barId, startDate, endDate, status }) => {
|
|
22052
22063
|
const startMs = new Date(startDate).getTime();
|
|
22053
22064
|
const endMs = new Date(endDate).getTime();
|
|
@@ -22077,9 +22088,7 @@ function registerBarTools(server, _ctx, client) {
|
|
|
22077
22088
|
server.tool(
|
|
22078
22089
|
"get_bar",
|
|
22079
22090
|
"Get a Bar by ID.",
|
|
22080
|
-
|
|
22081
|
-
barId: idSegmentSchema.describe("The Bar ID")
|
|
22082
|
-
},
|
|
22091
|
+
get_bar,
|
|
22083
22092
|
async ({ barId }) => {
|
|
22084
22093
|
try {
|
|
22085
22094
|
const bar = await client.getBar(barId);
|
|
@@ -22098,28 +22107,28 @@ function registerBarTools(server, _ctx, client) {
|
|
|
22098
22107
|
}
|
|
22099
22108
|
}
|
|
22100
22109
|
);
|
|
22101
|
-
|
|
22102
|
-
|
|
22103
|
-
|
|
22104
|
-
|
|
22105
|
-
|
|
22106
|
-
|
|
22107
|
-
|
|
22108
|
-
|
|
22109
|
-
|
|
22110
|
-
|
|
22111
|
-
|
|
22112
|
-
|
|
22113
|
-
|
|
22114
|
-
|
|
22110
|
+
for (const name of ["list_account_bars", "list_bars"]) {
|
|
22111
|
+
server.tool(
|
|
22112
|
+
name,
|
|
22113
|
+
"List an Account's Bars, chronological by start date.",
|
|
22114
|
+
list_account_bars,
|
|
22115
|
+
async ({ accountId }) => {
|
|
22116
|
+
try {
|
|
22117
|
+
const bars = await client.listAccountBars(accountId);
|
|
22118
|
+
if (bars === void 0) {
|
|
22119
|
+
return { content: [{ type: "text", text: `Account not found: ${accountId}` }], isError: true };
|
|
22120
|
+
}
|
|
22121
|
+
if (bars.length === 0) {
|
|
22122
|
+
return { content: [{ type: "text", text: "No bars found." }] };
|
|
22123
|
+
}
|
|
22124
|
+
const text = bars.map((b) => `${b.barId} ${b.startDate} \u2192 ${b.endDate} \u2014 ${b.status}`).join("\n");
|
|
22125
|
+
return { content: [{ type: "text", text }] };
|
|
22126
|
+
} catch (err) {
|
|
22127
|
+
return { content: [{ type: "text", text: `Failed to list bars: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
22115
22128
|
}
|
|
22116
|
-
const text = bars.map((b) => `${b.barId} ${b.startDate} \u2192 ${b.endDate} \u2014 ${b.status}`).join("\n");
|
|
22117
|
-
return { content: [{ type: "text", text }] };
|
|
22118
|
-
} catch (err) {
|
|
22119
|
-
return { content: [{ type: "text", text: `Failed to list bars: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
22120
22129
|
}
|
|
22121
|
-
|
|
22122
|
-
|
|
22130
|
+
);
|
|
22131
|
+
}
|
|
22123
22132
|
}
|
|
22124
22133
|
|
|
22125
22134
|
// ../../libs/harmonica-services/src/mcp/tools/baseline-tools.ts
|
|
@@ -24101,260 +24110,6 @@ function registerClaimTools(server, ctx, client) {
|
|
|
24101
24110
|
);
|
|
24102
24111
|
}
|
|
24103
24112
|
|
|
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
24113
|
// ../../libs/harmonica-services/src/mcp/tools/downbeat-harmony-report-tools.ts
|
|
24359
24114
|
function formatQualifyingBeat(b) {
|
|
24360
24115
|
return `- \`${b.beatId}\` ${b.title}: ${b.signals.join(", ")}`;
|
|
@@ -25100,1064 +24855,288 @@ ${members}${parent}`;
|
|
|
25100
24855
|
);
|
|
25101
24856
|
}
|
|
25102
24857
|
|
|
25103
|
-
// ../../libs/harmonica-
|
|
25104
|
-
var
|
|
25105
|
-
|
|
25106
|
-
|
|
25107
|
-
|
|
25108
|
-
|
|
25109
|
-
|
|
25110
|
-
|
|
25111
|
-
|
|
25112
|
-
|
|
25113
|
-
|
|
25114
|
-
|
|
25115
|
-
|
|
25116
|
-
|
|
25117
|
-
|
|
25118
|
-
|
|
25119
|
-
|
|
25120
|
-
|
|
25121
|
-
|
|
25122
|
-
|
|
25123
|
-
|
|
25124
|
-
|
|
25125
|
-
|
|
25126
|
-
|
|
25127
|
-
|
|
25128
|
-
|
|
25129
|
-
|
|
25130
|
-
|
|
25131
|
-
|
|
25132
|
-
|
|
25133
|
-
|
|
25134
|
-
|
|
25135
|
-
)
|
|
25136
|
-
|
|
25137
|
-
|
|
25138
|
-
|
|
25139
|
-
|
|
25140
|
-
|
|
25141
|
-
|
|
25142
|
-
|
|
25143
|
-
|
|
25144
|
-
|
|
25145
|
-
|
|
25146
|
-
|
|
25147
|
-
|
|
25148
|
-
|
|
25149
|
-
|
|
25150
|
-
|
|
25151
|
-
|
|
25152
|
-
|
|
25153
|
-
|
|
25154
|
-
|
|
25155
|
-
)
|
|
25156
|
-
|
|
25157
|
-
|
|
25158
|
-
|
|
25159
|
-
|
|
25160
|
-
|
|
25161
|
-
|
|
25162
|
-
|
|
25163
|
-
|
|
25164
|
-
|
|
25165
|
-
|
|
25166
|
-
|
|
25167
|
-
|
|
25168
|
-
|
|
25169
|
-
|
|
25170
|
-
|
|
25171
|
-
|
|
25172
|
-
|
|
25173
|
-
|
|
25174
|
-
|
|
25175
|
-
|
|
25176
|
-
|
|
25177
|
-
|
|
25178
|
-
|
|
25179
|
-
|
|
25180
|
-
|
|
25181
|
-
|
|
25182
|
-
|
|
25183
|
-
|
|
25184
|
-
|
|
25185
|
-
|
|
25186
|
-
|
|
25187
|
-
|
|
25188
|
-
|
|
25189
|
-
|
|
25190
|
-
|
|
25191
|
-
|
|
25192
|
-
|
|
25193
|
-
|
|
25194
|
-
|
|
25195
|
-
|
|
25196
|
-
|
|
25197
|
-
|
|
25198
|
-
|
|
25199
|
-
|
|
25200
|
-
)
|
|
25201
|
-
|
|
25202
|
-
|
|
25203
|
-
|
|
25204
|
-
|
|
25205
|
-
|
|
25206
|
-
|
|
25207
|
-
|
|
25208
|
-
|
|
25209
|
-
|
|
25210
|
-
|
|
25211
|
-
|
|
25212
|
-
|
|
25213
|
-
|
|
25214
|
-
|
|
25215
|
-
|
|
25216
|
-
|
|
25217
|
-
|
|
25218
|
-
|
|
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}
|
|
25282
|
-
|
|
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}
|
|
25301
|
-
|
|
25302
|
-
More flags available. Pass \`cursor: "${nextCursor}"\` to list_flags to continue.`;
|
|
25303
|
-
}
|
|
24858
|
+
// ../../libs/harmonica-schemas/src/azure-marketplace.ts
|
|
24859
|
+
var AzureMarketplaceSubscriptionSchema = external_exports.object({
|
|
24860
|
+
id: external_exports.string(),
|
|
24861
|
+
publisherId: external_exports.string().optional(),
|
|
24862
|
+
offerId: external_exports.string().optional(),
|
|
24863
|
+
name: external_exports.string().optional(),
|
|
24864
|
+
saasSubscriptionStatus: external_exports.string().optional(),
|
|
24865
|
+
planId: external_exports.string().optional(),
|
|
24866
|
+
quantity: external_exports.number().optional(),
|
|
24867
|
+
beneficiary: external_exports.object({
|
|
24868
|
+
emailId: external_exports.string().optional(),
|
|
24869
|
+
objectId: external_exports.string().optional(),
|
|
24870
|
+
tenantId: external_exports.string().optional()
|
|
24871
|
+
}).optional(),
|
|
24872
|
+
purchaser: external_exports.object({
|
|
24873
|
+
emailId: external_exports.string().optional(),
|
|
24874
|
+
objectId: external_exports.string().optional(),
|
|
24875
|
+
tenantId: external_exports.string().optional()
|
|
24876
|
+
}).optional()
|
|
24877
|
+
});
|
|
24878
|
+
var AzureSubscribedSchema = external_exports.object({
|
|
24879
|
+
action: external_exports.literal("Subscribe"),
|
|
24880
|
+
// Non-blank, not merely non-empty. A bare z.string() accepts "", which would
|
|
24881
|
+
// derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
|
|
24882
|
+
// slug invariant. And .min(1) alone still admits " ", which normalizes to
|
|
24883
|
+
// nothing and falls through to the hash fallback, quietly provisioning an org
|
|
24884
|
+
// under a hashed slug for input that is plainly garbage. Reject it at the
|
|
24885
|
+
// boundary instead.
|
|
24886
|
+
subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
|
|
24887
|
+
message: "subscriptionId must not be blank"
|
|
24888
|
+
}),
|
|
24889
|
+
offerId: external_exports.string(),
|
|
24890
|
+
planId: external_exports.string(),
|
|
24891
|
+
quantity: external_exports.number().optional(),
|
|
24892
|
+
timeStamp: external_exports.string(),
|
|
24893
|
+
operationId: external_exports.string(),
|
|
24894
|
+
subscription: AzureMarketplaceSubscriptionSchema
|
|
24895
|
+
});
|
|
24896
|
+
var AzureUnsubscribedSchema = external_exports.object({
|
|
24897
|
+
action: external_exports.literal("Unsubscribe"),
|
|
24898
|
+
// Non-blank, not merely non-empty. A bare z.string() accepts "", which would
|
|
24899
|
+
// derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
|
|
24900
|
+
// slug invariant. And .min(1) alone still admits " ", which normalizes to
|
|
24901
|
+
// nothing and falls through to the hash fallback, quietly provisioning an org
|
|
24902
|
+
// under a hashed slug for input that is plainly garbage. Reject it at the
|
|
24903
|
+
// boundary instead.
|
|
24904
|
+
subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
|
|
24905
|
+
message: "subscriptionId must not be blank"
|
|
24906
|
+
}),
|
|
24907
|
+
offerId: external_exports.string(),
|
|
24908
|
+
planId: external_exports.string(),
|
|
24909
|
+
timeStamp: external_exports.string(),
|
|
24910
|
+
operationId: external_exports.string(),
|
|
24911
|
+
subscription: AzureMarketplaceSubscriptionSchema
|
|
24912
|
+
});
|
|
24913
|
+
var AzureChangePlanSchema = external_exports.object({
|
|
24914
|
+
action: external_exports.literal("ChangePlan"),
|
|
24915
|
+
// Non-blank, not merely non-empty. A bare z.string() accepts "", which would
|
|
24916
|
+
// derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
|
|
24917
|
+
// slug invariant. And .min(1) alone still admits " ", which normalizes to
|
|
24918
|
+
// nothing and falls through to the hash fallback, quietly provisioning an org
|
|
24919
|
+
// under a hashed slug for input that is plainly garbage. Reject it at the
|
|
24920
|
+
// boundary instead.
|
|
24921
|
+
subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
|
|
24922
|
+
message: "subscriptionId must not be blank"
|
|
24923
|
+
}),
|
|
24924
|
+
offerId: external_exports.string(),
|
|
24925
|
+
planId: external_exports.string(),
|
|
24926
|
+
timeStamp: external_exports.string(),
|
|
24927
|
+
operationId: external_exports.string(),
|
|
24928
|
+
subscription: AzureMarketplaceSubscriptionSchema
|
|
24929
|
+
});
|
|
24930
|
+
var AzureSuspendSchema = external_exports.object({
|
|
24931
|
+
action: external_exports.literal("Suspend"),
|
|
24932
|
+
// Non-blank, not merely non-empty. A bare z.string() accepts "", which would
|
|
24933
|
+
// derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
|
|
24934
|
+
// slug invariant. And .min(1) alone still admits " ", which normalizes to
|
|
24935
|
+
// nothing and falls through to the hash fallback, quietly provisioning an org
|
|
24936
|
+
// under a hashed slug for input that is plainly garbage. Reject it at the
|
|
24937
|
+
// boundary instead.
|
|
24938
|
+
subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
|
|
24939
|
+
message: "subscriptionId must not be blank"
|
|
24940
|
+
}),
|
|
24941
|
+
offerId: external_exports.string(),
|
|
24942
|
+
planId: external_exports.string(),
|
|
24943
|
+
timeStamp: external_exports.string(),
|
|
24944
|
+
operationId: external_exports.string(),
|
|
24945
|
+
subscription: AzureMarketplaceSubscriptionSchema
|
|
24946
|
+
});
|
|
24947
|
+
var AzureReinstateSchema = external_exports.object({
|
|
24948
|
+
action: external_exports.literal("Reinstate"),
|
|
24949
|
+
// Non-blank, not merely non-empty. A bare z.string() accepts "", which would
|
|
24950
|
+
// derive the slug "az-" — 3 chars, trailing dash, invalid under the repository
|
|
24951
|
+
// slug invariant. And .min(1) alone still admits " ", which normalizes to
|
|
24952
|
+
// nothing and falls through to the hash fallback, quietly provisioning an org
|
|
24953
|
+
// under a hashed slug for input that is plainly garbage. Reject it at the
|
|
24954
|
+
// boundary instead.
|
|
24955
|
+
subscriptionId: external_exports.string().min(1).refine((v) => v.trim().length > 0, {
|
|
24956
|
+
message: "subscriptionId must not be blank"
|
|
24957
|
+
}),
|
|
24958
|
+
offerId: external_exports.string(),
|
|
24959
|
+
planId: external_exports.string(),
|
|
24960
|
+
timeStamp: external_exports.string(),
|
|
24961
|
+
operationId: external_exports.string(),
|
|
24962
|
+
subscription: AzureMarketplaceSubscriptionSchema
|
|
24963
|
+
});
|
|
24964
|
+
var AzureMarketplaceWebhookEventSchema = external_exports.discriminatedUnion("action", [
|
|
24965
|
+
AzureSubscribedSchema,
|
|
24966
|
+
AzureUnsubscribedSchema,
|
|
24967
|
+
AzureChangePlanSchema,
|
|
24968
|
+
AzureSuspendSchema,
|
|
24969
|
+
AzureReinstateSchema
|
|
24970
|
+
]);
|
|
24971
|
+
var AZURE_MARKETPLACE_WEBHOOK_ACTIONS = AzureMarketplaceWebhookEventSchema.options.map(
|
|
24972
|
+
(o) => o.shape.action.value
|
|
24973
|
+
);
|
|
24974
|
+
var AzureMarketplaceActivateBodySchema = external_exports.object({
|
|
24975
|
+
token: external_exports.string().min(1)
|
|
24976
|
+
});
|
|
25304
24977
|
|
|
25305
|
-
// ../../libs/harmonica-
|
|
25306
|
-
|
|
25307
|
-
|
|
25308
|
-
|
|
25309
|
-
|
|
25310
|
-
|
|
25311
|
-
|
|
25312
|
-
|
|
25313
|
-
|
|
25314
|
-
|
|
25315
|
-
|
|
25316
|
-
|
|
25317
|
-
|
|
25318
|
-
|
|
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.
|
|
24978
|
+
// ../../libs/harmonica-schemas/src/compose-beat-versions.ts
|
|
24979
|
+
var BeatVersionDraftSchema = external_exports.object({
|
|
24980
|
+
title: external_exports.string().min(1).max(300),
|
|
24981
|
+
description: external_exports.string().min(1).max(2e3),
|
|
24982
|
+
scope: external_exports.string().max(1e3).optional(),
|
|
24983
|
+
changeSummary: external_exports.string().min(1).max(500),
|
|
24984
|
+
changeImportance: external_exports.enum(["minor", "moderate", "major"]),
|
|
24985
|
+
priority: external_exports.number().int().min(1).optional(),
|
|
24986
|
+
tags: external_exports.array(external_exports.string()).optional(),
|
|
24987
|
+
estimatedEffort: external_exports.enum(["XS", "S", "M", "L", "XL"]).optional()
|
|
24988
|
+
});
|
|
24989
|
+
var LLMBeatVersionsCompositionOutputSchema = external_exports.object({
|
|
24990
|
+
drafts: external_exports.array(BeatVersionDraftSchema).min(2).max(5)
|
|
24991
|
+
});
|
|
25398
24992
|
|
|
25399
|
-
|
|
25400
|
-
|
|
25401
|
-
|
|
25402
|
-
|
|
25403
|
-
|
|
25404
|
-
|
|
25405
|
-
|
|
25406
|
-
|
|
25407
|
-
|
|
25408
|
-
|
|
25409
|
-
|
|
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.
|
|
24993
|
+
// ../../libs/harmonica-schemas/src/note-extraction.ts
|
|
24994
|
+
var EXTRACTABLE_NOTE_TYPES = ["context", "constraint", "guidance", "decision"];
|
|
24995
|
+
var DECISION_SIGNIFICANCE_VALUES = ["strategic", "structural", "implementation"];
|
|
24996
|
+
var ExtractedNoteRawSchema = external_exports.object({
|
|
24997
|
+
noteType: external_exports.enum(EXTRACTABLE_NOTE_TYPES),
|
|
24998
|
+
content: external_exports.string().min(1),
|
|
24999
|
+
rationale: external_exports.string().optional(),
|
|
25000
|
+
confidence: external_exports.number().min(0).max(1),
|
|
25001
|
+
significance: external_exports.enum(DECISION_SIGNIFICANCE_VALUES).optional()
|
|
25002
|
+
});
|
|
25003
|
+
var NoteExtractionResponseSchema = external_exports.array(ExtractedNoteRawSchema);
|
|
25443
25004
|
|
|
25444
|
-
|
|
25445
|
-
|
|
25446
|
-
|
|
25447
|
-
|
|
25448
|
-
|
|
25449
|
-
|
|
25450
|
-
|
|
25451
|
-
|
|
25452
|
-
|
|
25453
|
-
|
|
25454
|
-
|
|
25455
|
-
|
|
25456
|
-
|
|
25457
|
-
|
|
25458
|
-
|
|
25459
|
-
|
|
25460
|
-
|
|
25461
|
-
|
|
25462
|
-
|
|
25463
|
-
|
|
25464
|
-
|
|
25465
|
-
|
|
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"}.
|
|
25005
|
+
// ../../libs/harmonica-schemas/src/plan-quality.ts
|
|
25006
|
+
var PLAN_QUALITY_DIMENSIONS = [
|
|
25007
|
+
"scoped",
|
|
25008
|
+
"directional",
|
|
25009
|
+
"testable",
|
|
25010
|
+
"traceable",
|
|
25011
|
+
"assignable",
|
|
25012
|
+
"riskAware",
|
|
25013
|
+
"completeness"
|
|
25014
|
+
];
|
|
25015
|
+
var PlanDimensionScoreSchema = external_exports.object({
|
|
25016
|
+
dimension: external_exports.enum(PLAN_QUALITY_DIMENSIONS),
|
|
25017
|
+
label: external_exports.string(),
|
|
25018
|
+
score: external_exports.number().min(1).max(5),
|
|
25019
|
+
rationale: external_exports.string(),
|
|
25020
|
+
suggestions: external_exports.array(external_exports.string())
|
|
25021
|
+
});
|
|
25022
|
+
var PlanQualityLLMResponseSchema = external_exports.object({
|
|
25023
|
+
dimensions: external_exports.array(PlanDimensionScoreSchema).min(1),
|
|
25024
|
+
summary: external_exports.string(),
|
|
25025
|
+
topSuggestion: external_exports.string()
|
|
25026
|
+
});
|
|
25483
25027
|
|
|
25484
|
-
|
|
25485
|
-
|
|
25486
|
-
|
|
25487
|
-
|
|
25028
|
+
// ../../libs/harmonica-schemas/src/revision-quality.ts
|
|
25029
|
+
var REVISION_QUALITY_DIMENSIONS = [
|
|
25030
|
+
"scoped",
|
|
25031
|
+
"traceable",
|
|
25032
|
+
"testable",
|
|
25033
|
+
"reviewReady",
|
|
25034
|
+
"nonDuplicating"
|
|
25035
|
+
];
|
|
25036
|
+
var RevisionDimensionScoreSchema = external_exports.object({
|
|
25037
|
+
dimension: external_exports.enum(REVISION_QUALITY_DIMENSIONS),
|
|
25038
|
+
label: external_exports.string(),
|
|
25039
|
+
score: external_exports.number().min(1).max(5),
|
|
25040
|
+
rationale: external_exports.string(),
|
|
25041
|
+
suggestions: external_exports.array(external_exports.string())
|
|
25042
|
+
});
|
|
25043
|
+
var RevisionQualityLLMResponseSchema = external_exports.object({
|
|
25044
|
+
dimensions: external_exports.array(RevisionDimensionScoreSchema).min(1),
|
|
25045
|
+
summary: external_exports.string(),
|
|
25046
|
+
topSuggestion: external_exports.string()
|
|
25047
|
+
});
|
|
25488
25048
|
|
|
25489
|
-
// ../../libs/harmonica-
|
|
25490
|
-
var
|
|
25491
|
-
|
|
25492
|
-
|
|
25493
|
-
|
|
25494
|
-
|
|
25495
|
-
|
|
25496
|
-
|
|
25497
|
-
|
|
25498
|
-
|
|
25499
|
-
|
|
25500
|
-
|
|
25501
|
-
|
|
25502
|
-
|
|
25503
|
-
|
|
25504
|
-
|
|
25505
|
-
|
|
25506
|
-
|
|
25507
|
-
|
|
25508
|
-
|
|
25509
|
-
|
|
25510
|
-
|
|
25511
|
-
|
|
25512
|
-
|
|
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
|
-
}
|
|
25049
|
+
// ../../libs/harmonica-schemas/src/beat-quality.ts
|
|
25050
|
+
var BEAT_QUALITY_DIMENSIONS = [
|
|
25051
|
+
"distinctive",
|
|
25052
|
+
"harmonious",
|
|
25053
|
+
"substantial",
|
|
25054
|
+
"durable",
|
|
25055
|
+
"clear",
|
|
25056
|
+
"strategicallyAligned",
|
|
25057
|
+
"outcomeOriented"
|
|
25058
|
+
];
|
|
25059
|
+
var BeatDimensionScoreSchema = external_exports.object({
|
|
25060
|
+
dimension: external_exports.enum(BEAT_QUALITY_DIMENSIONS),
|
|
25061
|
+
label: external_exports.string(),
|
|
25062
|
+
score: external_exports.number().min(1).max(5),
|
|
25063
|
+
rationale: external_exports.string(),
|
|
25064
|
+
suggestions: external_exports.array(external_exports.string())
|
|
25065
|
+
});
|
|
25066
|
+
var BeatQualityLLMResponseSchema = external_exports.object({
|
|
25067
|
+
dimensions: external_exports.array(BeatDimensionScoreSchema).length(BEAT_QUALITY_DIMENSIONS.length).refine(
|
|
25068
|
+
(arr) => new Set(arr.map((d) => d.dimension)).size === arr.length,
|
|
25069
|
+
{ message: "Each quality dimension must appear exactly once" }
|
|
25070
|
+
),
|
|
25071
|
+
summary: external_exports.string(),
|
|
25072
|
+
topSuggestion: external_exports.string()
|
|
25073
|
+
});
|
|
25592
25074
|
|
|
25593
|
-
// ../../libs/harmonica-
|
|
25594
|
-
|
|
25595
|
-
|
|
25596
|
-
|
|
25597
|
-
|
|
25598
|
-
|
|
25599
|
-
|
|
25600
|
-
|
|
25601
|
-
|
|
25602
|
-
|
|
25603
|
-
|
|
25604
|
-
|
|
25605
|
-
|
|
25606
|
-
)
|
|
25607
|
-
|
|
25608
|
-
|
|
25609
|
-
|
|
25610
|
-
|
|
25611
|
-
|
|
25612
|
-
|
|
25613
|
-
|
|
25614
|
-
|
|
25615
|
-
|
|
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");
|
|
25075
|
+
// ../../libs/harmonica-schemas/src/drop-quality.ts
|
|
25076
|
+
var DROP_QUALITY_DIMENSIONS = [
|
|
25077
|
+
"substantial",
|
|
25078
|
+
"harmonious",
|
|
25079
|
+
"clear",
|
|
25080
|
+
"focused",
|
|
25081
|
+
"strategicallyAligned"
|
|
25082
|
+
];
|
|
25083
|
+
var DropDimensionScoreSchema = external_exports.object({
|
|
25084
|
+
dimension: external_exports.enum(DROP_QUALITY_DIMENSIONS),
|
|
25085
|
+
label: external_exports.string(),
|
|
25086
|
+
score: external_exports.number().min(1).max(5),
|
|
25087
|
+
rationale: external_exports.string(),
|
|
25088
|
+
suggestions: external_exports.array(external_exports.string())
|
|
25089
|
+
});
|
|
25090
|
+
var DropQualityLLMResponseSchema = external_exports.object({
|
|
25091
|
+
dimensions: external_exports.array(DropDimensionScoreSchema).length(DROP_QUALITY_DIMENSIONS.length).refine(
|
|
25092
|
+
(arr) => new Set(arr.map((d) => d.dimension)).size === arr.length,
|
|
25093
|
+
{ message: "Each drop quality dimension must appear exactly once" }
|
|
25094
|
+
),
|
|
25095
|
+
summary: external_exports.string(),
|
|
25096
|
+
topSuggestion: external_exports.string()
|
|
25097
|
+
});
|
|
25878
25098
|
|
|
25879
|
-
// ../../libs/harmonica-schemas/src/
|
|
25880
|
-
var
|
|
25881
|
-
|
|
25882
|
-
|
|
25883
|
-
|
|
25884
|
-
|
|
25885
|
-
|
|
25886
|
-
|
|
25887
|
-
|
|
25888
|
-
|
|
25889
|
-
|
|
25890
|
-
|
|
25891
|
-
|
|
25892
|
-
|
|
25893
|
-
|
|
25894
|
-
|
|
25895
|
-
|
|
25896
|
-
|
|
25897
|
-
|
|
25099
|
+
// ../../libs/harmonica-schemas/src/build-quality.ts
|
|
25100
|
+
var BUILD_DIMENSIONS = [
|
|
25101
|
+
"testability",
|
|
25102
|
+
"observability",
|
|
25103
|
+
"architecturalFitness",
|
|
25104
|
+
"modernization",
|
|
25105
|
+
"resilience",
|
|
25106
|
+
"maintainability",
|
|
25107
|
+
"securityPosture",
|
|
25108
|
+
"operability",
|
|
25109
|
+
"completeness"
|
|
25110
|
+
];
|
|
25111
|
+
var BuildDimensionScoreSchema = external_exports.object({
|
|
25112
|
+
dimension: external_exports.enum(BUILD_DIMENSIONS),
|
|
25113
|
+
label: external_exports.string(),
|
|
25114
|
+
score: external_exports.number().min(1).max(5),
|
|
25115
|
+
rationale: external_exports.string(),
|
|
25116
|
+
suggestions: external_exports.array(external_exports.string()),
|
|
25117
|
+
evidenceFiles: external_exports.array(external_exports.string()).optional()
|
|
25898
25118
|
});
|
|
25899
|
-
var
|
|
25900
|
-
|
|
25901
|
-
|
|
25902
|
-
|
|
25903
|
-
|
|
25904
|
-
|
|
25905
|
-
|
|
25906
|
-
|
|
25907
|
-
|
|
25908
|
-
|
|
25909
|
-
|
|
25910
|
-
|
|
25911
|
-
|
|
25912
|
-
|
|
25913
|
-
timeStamp: external_exports.string(),
|
|
25914
|
-
operationId: external_exports.string(),
|
|
25915
|
-
subscription: AzureMarketplaceSubscriptionSchema
|
|
25119
|
+
var BuildGapSchema = external_exports.object({
|
|
25120
|
+
dimension: external_exports.enum(BUILD_DIMENSIONS),
|
|
25121
|
+
severity: external_exports.enum(["blocker", "major", "minor"]),
|
|
25122
|
+
finding: external_exports.string(),
|
|
25123
|
+
impact: external_exports.string(),
|
|
25124
|
+
recommendation: external_exports.string(),
|
|
25125
|
+
effort: external_exports.enum(["small", "medium", "large"]),
|
|
25126
|
+
incrementalPath: external_exports.string().optional(),
|
|
25127
|
+
relevantFiles: external_exports.array(external_exports.string()).optional(),
|
|
25128
|
+
evidenceFile: external_exports.string().optional(),
|
|
25129
|
+
evidenceLine: external_exports.number().int().positive().optional(),
|
|
25130
|
+
evidenceQuote: external_exports.string().optional()
|
|
25131
|
+
// evidenceVerified is intentionally absent — it is set ONLY by verifyEvidenceQuotes(),
|
|
25132
|
+
// never by the LLM. Accepting it from the LLM would undermine the entire trust model.
|
|
25916
25133
|
});
|
|
25917
|
-
var
|
|
25918
|
-
|
|
25919
|
-
|
|
25920
|
-
|
|
25921
|
-
|
|
25922
|
-
|
|
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"
|
|
25978
|
-
}),
|
|
25979
|
-
offerId: external_exports.string(),
|
|
25980
|
-
planId: external_exports.string(),
|
|
25981
|
-
timeStamp: external_exports.string(),
|
|
25982
|
-
operationId: external_exports.string(),
|
|
25983
|
-
subscription: AzureMarketplaceSubscriptionSchema
|
|
25984
|
-
});
|
|
25985
|
-
var AzureMarketplaceWebhookEventSchema = external_exports.discriminatedUnion("action", [
|
|
25986
|
-
AzureSubscribedSchema,
|
|
25987
|
-
AzureUnsubscribedSchema,
|
|
25988
|
-
AzureChangePlanSchema,
|
|
25989
|
-
AzureSuspendSchema,
|
|
25990
|
-
AzureReinstateSchema
|
|
25991
|
-
]);
|
|
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),
|
|
26019
|
-
content: external_exports.string().min(1),
|
|
26020
|
-
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()
|
|
26094
|
-
});
|
|
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())
|
|
26110
|
-
});
|
|
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()
|
|
26118
|
-
});
|
|
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())
|
|
25134
|
+
var BuildQualityLLMResponseSchema = external_exports.object({
|
|
25135
|
+
dimensions: external_exports.array(BuildDimensionScoreSchema).min(1),
|
|
25136
|
+
gaps: external_exports.array(BuildGapSchema),
|
|
25137
|
+
summary: external_exports.string(),
|
|
25138
|
+
topSuggestion: external_exports.string(),
|
|
25139
|
+
filesExamined: external_exports.array(external_exports.string())
|
|
26161
25140
|
});
|
|
26162
25141
|
|
|
26163
25142
|
// ../../libs/harmonica-schemas/src/onboarding-analysis.ts
|
|
@@ -26242,6 +25221,41 @@ var BeatDraftSuggestionSchema = external_exports.object({
|
|
|
26242
25221
|
});
|
|
26243
25222
|
var BeatDraftSuggestionsSchema = external_exports.array(BeatDraftSuggestionSchema);
|
|
26244
25223
|
|
|
25224
|
+
// ../../libs/harmonica-schemas/src/progress-evidence.ts
|
|
25225
|
+
var ProgressEvidenceQuerySchema = external_exports.object({
|
|
25226
|
+
start: external_exports.string().min(1).max(40),
|
|
25227
|
+
end: external_exports.string().min(1).max(40),
|
|
25228
|
+
timezone: external_exports.string().min(1).max(100),
|
|
25229
|
+
contributor: external_exports.string().trim().min(1).max(256).optional(),
|
|
25230
|
+
limit: external_exports.coerce.number().int().min(1).max(100).optional(),
|
|
25231
|
+
cursor: external_exports.string().min(1).max(4096).optional()
|
|
25232
|
+
}).strict();
|
|
25233
|
+
var ProgressCitationSchema = external_exports.object({ kind: external_exports.string(), id: external_exports.string(), url: external_exports.string() });
|
|
25234
|
+
var ProgressEvidenceItemSchema = external_exports.object({
|
|
25235
|
+
id: external_exports.string(),
|
|
25236
|
+
source: external_exports.enum(["activity", "session"]),
|
|
25237
|
+
occurredAt: external_exports.string(),
|
|
25238
|
+
recordedAt: external_exports.string(),
|
|
25239
|
+
summary: external_exports.string(),
|
|
25240
|
+
classification: external_exports.enum(["planned", "progressed", "delivered"]),
|
|
25241
|
+
attribution: external_exports.object({ basis: external_exports.enum(["recorded_actor", "session_initiator", "unknown"]), actorId: external_exports.string().nullable(), agentId: external_exports.string().optional() }),
|
|
25242
|
+
citations: external_exports.array(ProgressCitationSchema),
|
|
25243
|
+
context: external_exports.array(external_exports.object({ kind: external_exports.string(), id: external_exports.string(), title: external_exports.string().optional(), asOf: external_exports.literal("current") })),
|
|
25244
|
+
deliveryProof: external_exports.object({ kind: external_exports.literal("recorded_release"), occurredAt: external_exports.string(), citation: ProgressCitationSchema }).nullable()
|
|
25245
|
+
});
|
|
25246
|
+
var ProgressCoverageGapSchema = external_exports.object({ source: external_exports.string(), code: external_exports.string(), message: external_exports.string(), entityId: external_exports.string().optional() });
|
|
25247
|
+
var ProgressEvidenceResponseSchema = external_exports.object({
|
|
25248
|
+
systemId: external_exports.string(),
|
|
25249
|
+
accountId: external_exports.string().nullable(),
|
|
25250
|
+
window: external_exports.object({ start: external_exports.string(), end: external_exports.string(), timezone: external_exports.string() }),
|
|
25251
|
+
contributor: external_exports.string().nullable(),
|
|
25252
|
+
watermark: external_exports.string(),
|
|
25253
|
+
items: external_exports.array(ProgressEvidenceItemSchema),
|
|
25254
|
+
nextCursor: external_exports.string().optional(),
|
|
25255
|
+
coverage: external_exports.object({ status: external_exports.literal("partial"), gaps: external_exports.array(ProgressCoverageGapSchema) }),
|
|
25256
|
+
metrics: external_exports.object({ queryCalls: external_exports.number(), batchCalls: external_exports.number(), scannedCount: external_exports.number(), referenceCount: external_exports.number(), elapsedMs: external_exports.number() })
|
|
25257
|
+
});
|
|
25258
|
+
|
|
26245
25259
|
// ../../libs/harmonica-schemas/src/api-responses.ts
|
|
26246
25260
|
var HumanAssigneeSchema = external_exports.object({ email: external_exports.string(), name: external_exports.string() });
|
|
26247
25261
|
var AgentAssigneeSchema = external_exports.object({ agentId: external_exports.string(), name: external_exports.string() });
|
|
@@ -26627,6 +25641,7 @@ var OrganizationSummarySchema = external_exports.object({
|
|
|
26627
25641
|
name: external_exports.string(),
|
|
26628
25642
|
slug: external_exports.string(),
|
|
26629
25643
|
logoUrl: external_exports.string().optional(),
|
|
25644
|
+
capabilityProfile: external_exports.enum(["azure-core"]).optional(),
|
|
26630
25645
|
status: external_exports.string(),
|
|
26631
25646
|
role: external_exports.string(),
|
|
26632
25647
|
systemCount: external_exports.number(),
|
|
@@ -26942,7 +25957,11 @@ var HumanAssigneeApiSchema = external_exports.object({
|
|
|
26942
25957
|
name: external_exports.string(),
|
|
26943
25958
|
email: external_exports.string()
|
|
26944
25959
|
});
|
|
25960
|
+
var ArchiveWorkItemRequestSchema = external_exports.object({ reason: external_exports.string().trim().min(1).max(1e3) });
|
|
26945
25961
|
var WorkItemApiSchema = external_exports.object({
|
|
25962
|
+
archived: external_exports.boolean().optional(),
|
|
25963
|
+
archivedAt: external_exports.string().optional(),
|
|
25964
|
+
archivedReason: external_exports.string().optional(),
|
|
26946
25965
|
workItemId: external_exports.string(),
|
|
26947
25966
|
trackId: external_exports.string(),
|
|
26948
25967
|
teamspaceId: external_exports.string(),
|
|
@@ -26967,6 +25986,21 @@ var WorkItemApiSchema = external_exports.object({
|
|
|
26967
25986
|
* named group, not hide them.
|
|
26968
25987
|
*/
|
|
26969
25988
|
workOrderId: external_exports.string().min(1).optional(),
|
|
25989
|
+
/**
|
|
25990
|
+
* Optional link to the Bar this item is planned into (B-298). Absent means
|
|
25991
|
+
* unplanned, which is a supported state — a reader grouping a queue by Bar
|
|
25992
|
+
* must render those as their own named group, not hide them. An id that
|
|
25993
|
+
* doesn't resolve within the Account in view is work the reader cannot
|
|
25994
|
+
* place, not work that is unplanned: a Bar is Account-scoped but a Track is
|
|
25995
|
+
* not, so a move on either side can separate the two.
|
|
25996
|
+
*/
|
|
25997
|
+
barId: external_exports.string().min(1).optional(),
|
|
25998
|
+
/**
|
|
25999
|
+
* Optional per-Track fractional ordering rank (N-4E09-7665). Absent when
|
|
26000
|
+
* the item is unranked — an unranked item is a first-class state. The
|
|
26001
|
+
* Work Queue uses this to order items within their displayed group.
|
|
26002
|
+
*/
|
|
26003
|
+
rank: external_exports.number().optional(),
|
|
26970
26004
|
createdBy: external_exports.string().optional(),
|
|
26971
26005
|
version: external_exports.number().optional(),
|
|
26972
26006
|
createdAt: external_exports.string(),
|
|
@@ -26976,8 +26010,68 @@ var WorkItemEstimateHintApiSchema = external_exports.object({
|
|
|
26976
26010
|
hours: external_exports.number(),
|
|
26977
26011
|
source: external_exports.literal("beat_version_value_velocity")
|
|
26978
26012
|
});
|
|
26013
|
+
var BAR_STATUSES2 = ["open", "closed"];
|
|
26014
|
+
var BarApiSchema = external_exports.object({
|
|
26015
|
+
barId: external_exports.string(),
|
|
26016
|
+
accountId: external_exports.string(),
|
|
26017
|
+
/** ISO 8601 date the Bar opens. */
|
|
26018
|
+
startDate: external_exports.string(),
|
|
26019
|
+
/** ISO 8601 date the Bar closes. */
|
|
26020
|
+
endDate: external_exports.string(),
|
|
26021
|
+
status: external_exports.enum(BAR_STATUSES2),
|
|
26022
|
+
createdAt: external_exports.string(),
|
|
26023
|
+
updatedAt: external_exports.string()
|
|
26024
|
+
}).passthrough();
|
|
26025
|
+
var BarsResponseSchema = external_exports.object({ bars: external_exports.array(BarApiSchema) }).passthrough();
|
|
26979
26026
|
var WorkItemsResponseSchema = external_exports.object({ workItems: external_exports.array(WorkItemApiSchema) }).passthrough();
|
|
26980
26027
|
var WorkItemResponseSchema = external_exports.object({ workItem: WorkItemApiSchema, estimateHint: WorkItemEstimateHintApiSchema.optional() }).passthrough();
|
|
26028
|
+
var EXTERNAL_MAPPING_SUBJECT_TYPES = [
|
|
26029
|
+
"movement",
|
|
26030
|
+
"work_order",
|
|
26031
|
+
"work_item"
|
|
26032
|
+
];
|
|
26033
|
+
var ExternalMappingSubjectTypeApiSchema = external_exports.enum(EXTERNAL_MAPPING_SUBJECT_TYPES);
|
|
26034
|
+
var EXTERNAL_MAPPING_PROVIDERS = [
|
|
26035
|
+
"jira",
|
|
26036
|
+
"linear",
|
|
26037
|
+
"github",
|
|
26038
|
+
"asana",
|
|
26039
|
+
"other"
|
|
26040
|
+
];
|
|
26041
|
+
var ExternalMappingProviderApiSchema = external_exports.enum(EXTERNAL_MAPPING_PROVIDERS);
|
|
26042
|
+
var EXTERNAL_MAPPING_SYNC_DIRECTIONS = [
|
|
26043
|
+
"none",
|
|
26044
|
+
"inbound",
|
|
26045
|
+
"outbound",
|
|
26046
|
+
"bidirectional"
|
|
26047
|
+
];
|
|
26048
|
+
var ExternalMappingSyncDirectionApiSchema = external_exports.enum(EXTERNAL_MAPPING_SYNC_DIRECTIONS);
|
|
26049
|
+
var EXTERNAL_MAPPING_SYNC_STATUSES = [
|
|
26050
|
+
"ok",
|
|
26051
|
+
"stale",
|
|
26052
|
+
"error",
|
|
26053
|
+
"unlinked"
|
|
26054
|
+
];
|
|
26055
|
+
var ExternalMappingSyncStatusApiSchema = external_exports.enum(EXTERNAL_MAPPING_SYNC_STATUSES);
|
|
26056
|
+
var ExternalMappingApiSchema = external_exports.object({
|
|
26057
|
+
externalMappingId: external_exports.string(),
|
|
26058
|
+
subjectType: ExternalMappingSubjectTypeApiSchema,
|
|
26059
|
+
subjectId: external_exports.string(),
|
|
26060
|
+
teamspaceId: external_exports.string(),
|
|
26061
|
+
orgId: external_exports.string(),
|
|
26062
|
+
provider: ExternalMappingProviderApiSchema,
|
|
26063
|
+
externalKey: external_exports.string(),
|
|
26064
|
+
url: external_exports.string().optional(),
|
|
26065
|
+
syncDirection: ExternalMappingSyncDirectionApiSchema,
|
|
26066
|
+
lastSyncedAt: external_exports.string().optional(),
|
|
26067
|
+
syncStatus: ExternalMappingSyncStatusApiSchema,
|
|
26068
|
+
createdBy: external_exports.string().optional(),
|
|
26069
|
+
version: external_exports.number().optional(),
|
|
26070
|
+
createdAt: external_exports.string(),
|
|
26071
|
+
updatedAt: external_exports.string()
|
|
26072
|
+
}).passthrough();
|
|
26073
|
+
var ExternalMappingsResponseSchema = external_exports.object({ externalMappings: external_exports.array(ExternalMappingApiSchema) }).passthrough();
|
|
26074
|
+
var EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE = "SUBJECT_NOT_FOUND";
|
|
26981
26075
|
var WORK_INTAKE_STATUSES = [
|
|
26982
26076
|
"requested",
|
|
26983
26077
|
"needs_decision",
|
|
@@ -27024,8 +26118,14 @@ var WorkOrderApiSchema = external_exports.object({
|
|
|
27024
26118
|
workOrderId: external_exports.string(),
|
|
27025
26119
|
/** Immutable lineage back to the ask this Order was split from. */
|
|
27026
26120
|
workRequestId: external_exports.string(),
|
|
27027
|
-
/**
|
|
27028
|
-
|
|
26121
|
+
/**
|
|
26122
|
+
* Optional: an Order may carry no Track at all. Under N-4E09-7470 a Track is
|
|
26123
|
+
* attribution, not an Order's parent, and an Order can be raised against a
|
|
26124
|
+
* Request before any placement exists. Requiring it here made the API reject
|
|
26125
|
+
* its own valid responses — a Movement holding one unplaced Order failed to
|
|
26126
|
+
* parse, so the whole list threw.
|
|
26127
|
+
*/
|
|
26128
|
+
trackId: external_exports.string().optional(),
|
|
27029
26129
|
teamspaceId: external_exports.string(),
|
|
27030
26130
|
orgId: external_exports.string(),
|
|
27031
26131
|
title: external_exports.string(),
|
|
@@ -27141,7 +26241,7 @@ var LayerRubricTemplateApiSchema = external_exports.object({
|
|
|
27141
26241
|
var LayerRubricTemplatesResponseSchema = external_exports.object({
|
|
27142
26242
|
layerRubricTemplates: external_exports.array(LayerRubricTemplateApiSchema)
|
|
27143
26243
|
}).passthrough();
|
|
27144
|
-
var
|
|
26244
|
+
var TRACK_STATUSES = [
|
|
27145
26245
|
"draft",
|
|
27146
26246
|
"proposed",
|
|
27147
26247
|
"approved",
|
|
@@ -27149,19 +26249,18 @@ var DELIVERABLE_GROUP_STATUSES = [
|
|
|
27149
26249
|
"fulfilled",
|
|
27150
26250
|
"cancelled"
|
|
27151
26251
|
];
|
|
27152
|
-
var
|
|
27153
|
-
var
|
|
27154
|
-
|
|
27155
|
-
|
|
26252
|
+
var TrackStatusSchema = external_exports.enum(TRACK_STATUSES);
|
|
26253
|
+
var TrackApiSchema = external_exports.object({
|
|
26254
|
+
trackId: external_exports.string(),
|
|
26255
|
+
trackCode: external_exports.string(),
|
|
27156
26256
|
teamspaceId: external_exports.string(),
|
|
27157
|
-
systemId: external_exports.string().nullish(),
|
|
27158
26257
|
movementId: external_exports.string().nullish(),
|
|
27159
26258
|
orgId: external_exports.string(),
|
|
27160
26259
|
title: external_exports.string(),
|
|
27161
26260
|
description: external_exports.string().nullish(),
|
|
27162
26261
|
deliverables: external_exports.string().nullish(),
|
|
27163
26262
|
acceptanceCriteria: external_exports.string().nullish(),
|
|
27164
|
-
status:
|
|
26263
|
+
status: TrackStatusSchema,
|
|
27165
26264
|
vuRate: external_exports.number().nullish(),
|
|
27166
26265
|
proposedAt: external_exports.string().nullish(),
|
|
27167
26266
|
approvedAt: external_exports.string().nullish(),
|
|
@@ -27172,8 +26271,8 @@ var DeliverableGroupApiSchema = external_exports.object({
|
|
|
27172
26271
|
createdAt: external_exports.string(),
|
|
27173
26272
|
updatedAt: external_exports.string()
|
|
27174
26273
|
}).passthrough();
|
|
27175
|
-
var
|
|
27176
|
-
|
|
26274
|
+
var TracksResponseSchema = external_exports.object({
|
|
26275
|
+
tracks: external_exports.array(TrackApiSchema)
|
|
27177
26276
|
}).passthrough();
|
|
27178
26277
|
var MovementDropSummaryApiSchema = external_exports.object({
|
|
27179
26278
|
dropId: external_exports.string(),
|
|
@@ -27203,7 +26302,7 @@ var WorkPlanBeatVersionApiSchema = external_exports.object({
|
|
|
27203
26302
|
createdAt: external_exports.string(),
|
|
27204
26303
|
updatedAt: external_exports.string()
|
|
27205
26304
|
}).passthrough();
|
|
27206
|
-
var
|
|
26305
|
+
var TrackBeatVersionsResponseSchema = external_exports.object({
|
|
27207
26306
|
beatVersions: external_exports.array(WorkPlanBeatVersionApiSchema)
|
|
27208
26307
|
}).passthrough();
|
|
27209
26308
|
var RevisionClaimActorSchema = external_exports.union([
|
|
@@ -27300,7 +26399,917 @@ var DropProposalApiSchema = external_exports.object({
|
|
|
27300
26399
|
scoredCandidates: external_exports.array(ScoredDropCandidateSchema).optional()
|
|
27301
26400
|
}).passthrough();
|
|
27302
26401
|
|
|
26402
|
+
// ../../libs/harmonica-services/src/mcp/tools/external-mapping-tools.ts
|
|
26403
|
+
var SUBJECT_TYPE_ENUM = ["movement", "work_order", "work_item"];
|
|
26404
|
+
var PROVIDER_ENUM = ["jira", "linear", "github", "asana", "other"];
|
|
26405
|
+
var SYNC_DIRECTION_ENUM = ["none", "inbound", "outbound", "bidirectional"];
|
|
26406
|
+
var SYNC_STATUS_ENUM = ["ok", "stale", "error", "unlinked"];
|
|
26407
|
+
function formatMapping(m) {
|
|
26408
|
+
const parts = [
|
|
26409
|
+
`[${m.externalMappingId}] ${m.provider}:${m.externalKey}`,
|
|
26410
|
+
`status: ${m.syncStatus}`,
|
|
26411
|
+
`direction: ${m.syncDirection}`
|
|
26412
|
+
];
|
|
26413
|
+
if (m.url) parts.push(`url: ${m.url}`);
|
|
26414
|
+
if (m.lastSyncedAt) parts.push(`last synced: ${m.lastSyncedAt}`);
|
|
26415
|
+
return parts.join(" \u2014 ");
|
|
26416
|
+
}
|
|
26417
|
+
function registerExternalMappingTools(server, _ctx, client) {
|
|
26418
|
+
server.tool(
|
|
26419
|
+
"add_external_mapping",
|
|
26420
|
+
"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.",
|
|
26421
|
+
{
|
|
26422
|
+
subjectType: external_exports.enum(SUBJECT_TYPE_ENUM).describe("The type of Harmonica entity being linked: movement (project), work_order, or work_item"),
|
|
26423
|
+
subjectId: external_exports.string().min(1).max(128).describe("The ID of the subject entity"),
|
|
26424
|
+
provider: external_exports.enum(PROVIDER_ENUM).describe("The external tracker provider"),
|
|
26425
|
+
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'),
|
|
26426
|
+
url: external_exports.string().url().optional().describe("Optional human-readable URL to the external object"),
|
|
26427
|
+
syncDirection: external_exports.enum(SYNC_DIRECTION_ENUM).optional().describe('Recorded intent for a later sync increment. Defaults to "none". Nothing syncs in this version.')
|
|
26428
|
+
},
|
|
26429
|
+
async ({ subjectType, subjectId, provider, externalKey, url, syncDirection }) => {
|
|
26430
|
+
try {
|
|
26431
|
+
const em = await client.addExternalMapping({
|
|
26432
|
+
subjectType,
|
|
26433
|
+
subjectId,
|
|
26434
|
+
provider,
|
|
26435
|
+
externalKey,
|
|
26436
|
+
...url !== void 0 && { url },
|
|
26437
|
+
...syncDirection !== void 0 && { syncDirection }
|
|
26438
|
+
});
|
|
26439
|
+
return {
|
|
26440
|
+
content: [
|
|
26441
|
+
{
|
|
26442
|
+
type: "text",
|
|
26443
|
+
text: `Added external mapping: ${formatMapping(em)}`
|
|
26444
|
+
}
|
|
26445
|
+
]
|
|
26446
|
+
};
|
|
26447
|
+
} catch (err) {
|
|
26448
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26449
|
+
const isSubjectMissing = err instanceof Error && err.code === EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE;
|
|
26450
|
+
return {
|
|
26451
|
+
content: [{
|
|
26452
|
+
type: "text",
|
|
26453
|
+
text: isSubjectMissing ? `**Field:** subjectId
|
|
26454
|
+
**Detail:** ${subjectType} ${subjectId} was not found, or is outside your scope.
|
|
26455
|
+
**Action:** Check the ID, or list the subject first to confirm it exists.` : `Failed to add external mapping: ${message}`
|
|
26456
|
+
}],
|
|
26457
|
+
isError: true
|
|
26458
|
+
};
|
|
26459
|
+
}
|
|
26460
|
+
}
|
|
26461
|
+
);
|
|
26462
|
+
server.tool(
|
|
26463
|
+
"update_external_mapping",
|
|
26464
|
+
'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).',
|
|
26465
|
+
{
|
|
26466
|
+
externalMappingId: external_exports.string().describe("The external mapping ID (e.g., em-abc123)"),
|
|
26467
|
+
externalKey: external_exports.string().min(1).max(500).optional().describe("New provider-specific key"),
|
|
26468
|
+
url: external_exports.string().url().nullable().optional().describe("New URL. Pass null to clear; omit to leave untouched."),
|
|
26469
|
+
syncDirection: external_exports.enum(SYNC_DIRECTION_ENUM).optional().describe("New recorded sync intent"),
|
|
26470
|
+
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.')
|
|
26471
|
+
},
|
|
26472
|
+
async ({ externalMappingId, externalKey, url, syncDirection, syncStatus }) => {
|
|
26473
|
+
const hasUpdates = externalKey !== void 0 || url !== void 0 || syncDirection !== void 0 || syncStatus !== void 0;
|
|
26474
|
+
if (!hasUpdates) {
|
|
26475
|
+
return {
|
|
26476
|
+
content: [{ type: "text", text: `No updates provided for external mapping: ${externalMappingId}` }],
|
|
26477
|
+
isError: true
|
|
26478
|
+
};
|
|
26479
|
+
}
|
|
26480
|
+
try {
|
|
26481
|
+
const em = await client.updateExternalMapping(externalMappingId, {
|
|
26482
|
+
...externalKey !== void 0 && { externalKey },
|
|
26483
|
+
...url !== void 0 && { url },
|
|
26484
|
+
...syncDirection !== void 0 && { syncDirection },
|
|
26485
|
+
...syncStatus !== void 0 && { syncStatus }
|
|
26486
|
+
});
|
|
26487
|
+
if (!em) {
|
|
26488
|
+
return {
|
|
26489
|
+
content: [{ type: "text", text: `External mapping not found: ${externalMappingId}` }],
|
|
26490
|
+
isError: true
|
|
26491
|
+
};
|
|
26492
|
+
}
|
|
26493
|
+
return {
|
|
26494
|
+
content: [
|
|
26495
|
+
{
|
|
26496
|
+
type: "text",
|
|
26497
|
+
text: `Updated external mapping: ${formatMapping(em)}`
|
|
26498
|
+
}
|
|
26499
|
+
]
|
|
26500
|
+
};
|
|
26501
|
+
} catch (err) {
|
|
26502
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26503
|
+
return {
|
|
26504
|
+
content: [{ type: "text", text: `Failed to update external mapping: ${message}` }],
|
|
26505
|
+
isError: true
|
|
26506
|
+
};
|
|
26507
|
+
}
|
|
26508
|
+
}
|
|
26509
|
+
);
|
|
26510
|
+
server.tool(
|
|
26511
|
+
"list_external_mappings",
|
|
26512
|
+
"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.",
|
|
26513
|
+
{
|
|
26514
|
+
subjectType: external_exports.enum(SUBJECT_TYPE_ENUM).describe("The type of Harmonica entity: movement, work_order, or work_item"),
|
|
26515
|
+
subjectId: external_exports.string().min(1).max(128).describe("The ID of the subject entity")
|
|
26516
|
+
},
|
|
26517
|
+
async ({ subjectType, subjectId }) => {
|
|
26518
|
+
try {
|
|
26519
|
+
const mappings = await client.listExternalMappings(subjectType, subjectId);
|
|
26520
|
+
const text = mappings.length === 0 ? `No external mappings found for ${subjectType} ${subjectId}.` : mappings.map(formatMapping).join("\n");
|
|
26521
|
+
return { content: [{ type: "text", text }] };
|
|
26522
|
+
} catch (err) {
|
|
26523
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26524
|
+
const isSubjectMissing = err instanceof Error && err.code === EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE;
|
|
26525
|
+
return {
|
|
26526
|
+
content: [{
|
|
26527
|
+
type: "text",
|
|
26528
|
+
text: isSubjectMissing ? `**Field:** subjectId
|
|
26529
|
+
**Detail:** ${subjectType} ${subjectId} was not found, or is outside your scope.
|
|
26530
|
+
**Action:** Check the ID, or list the subject first to confirm it exists.` : `Failed to list external mappings: ${message}`
|
|
26531
|
+
}],
|
|
26532
|
+
isError: true
|
|
26533
|
+
};
|
|
26534
|
+
}
|
|
26535
|
+
}
|
|
26536
|
+
);
|
|
26537
|
+
}
|
|
26538
|
+
|
|
26539
|
+
// ../../libs/harmonica-services/src/mcp/tools/feature-flag-tools.ts
|
|
26540
|
+
var STAGE_ENUM = external_exports.enum([
|
|
26541
|
+
"local",
|
|
26542
|
+
"staging",
|
|
26543
|
+
"staging-alpha",
|
|
26544
|
+
"staging-beta",
|
|
26545
|
+
"alpha",
|
|
26546
|
+
"beta",
|
|
26547
|
+
"prod"
|
|
26548
|
+
]);
|
|
26549
|
+
function registerFeatureFlagTools(server, ctx, client) {
|
|
26550
|
+
server.tool(
|
|
26551
|
+
"list_flags",
|
|
26552
|
+
"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.",
|
|
26553
|
+
{
|
|
26554
|
+
includeArchived: external_exports.boolean().optional().describe("Include archived flags. Default false"),
|
|
26555
|
+
search: external_exports.string().optional().describe("Case-insensitive search against flag name and description"),
|
|
26556
|
+
cursor: external_exports.string().optional().describe("Pagination cursor from a prior list_flags result"),
|
|
26557
|
+
limit: external_exports.number().int().min(1).max(100).optional().describe("Flags per page. Default 20")
|
|
26558
|
+
},
|
|
26559
|
+
async ({ includeArchived, search, cursor, limit }) => {
|
|
26560
|
+
try {
|
|
26561
|
+
const page = await client.listFlags({ includeArchived, search, cursor, limit });
|
|
26562
|
+
if (page.flags.length === 0) {
|
|
26563
|
+
return { content: [{ type: "text", text: "No flags found." }] };
|
|
26564
|
+
}
|
|
26565
|
+
return { content: [{ type: "text", text: formatFlagList(page.flags, page.total, page.nextCursor) }] };
|
|
26566
|
+
} catch (err) {
|
|
26567
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26568
|
+
return { content: [{ type: "text", text: `Failed to list flags: ${message}` }], isError: true };
|
|
26569
|
+
}
|
|
26570
|
+
}
|
|
26571
|
+
);
|
|
26572
|
+
server.tool(
|
|
26573
|
+
"get_flag",
|
|
26574
|
+
"Get full detail for a single feature flag by name, including its enabled stages and optional change history.",
|
|
26575
|
+
{
|
|
26576
|
+
name: external_exports.string().describe("Flag name"),
|
|
26577
|
+
includeHistory: external_exports.boolean().optional().describe("Include change history entries. Default false")
|
|
26578
|
+
},
|
|
26579
|
+
async ({ name, includeHistory }) => {
|
|
26580
|
+
try {
|
|
26581
|
+
const flag = await client.getFlag(name, { includeHistory });
|
|
26582
|
+
if (!flag) {
|
|
26583
|
+
return { content: [{ type: "text", text: `Flag "${name}" not found.` }] };
|
|
26584
|
+
}
|
|
26585
|
+
return { content: [{ type: "text", text: formatFlag(flag, flag.history) }] };
|
|
26586
|
+
} catch (err) {
|
|
26587
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26588
|
+
return { content: [{ type: "text", text: `Failed to get flag: ${message}` }], isError: true };
|
|
26589
|
+
}
|
|
26590
|
+
}
|
|
26591
|
+
);
|
|
26592
|
+
server.tool(
|
|
26593
|
+
"check_flag",
|
|
26594
|
+
"Check whether a feature flag is enabled in a specific stage. Returns a simple enabled/disabled answer.",
|
|
26595
|
+
{
|
|
26596
|
+
name: external_exports.string().describe("Flag name"),
|
|
26597
|
+
stage: STAGE_ENUM.describe("Deployment stage to check against")
|
|
26598
|
+
},
|
|
26599
|
+
async ({ name, stage }) => {
|
|
26600
|
+
try {
|
|
26601
|
+
const enabled = await client.checkFlag(name, stage);
|
|
26602
|
+
const status = enabled ? "enabled" : "disabled";
|
|
26603
|
+
return {
|
|
26604
|
+
content: [{
|
|
26605
|
+
type: "text",
|
|
26606
|
+
text: `Flag "${name}" is **${status}** in stage "${stage}".`
|
|
26607
|
+
}]
|
|
26608
|
+
};
|
|
26609
|
+
} catch (err) {
|
|
26610
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26611
|
+
return { content: [{ type: "text", text: `Failed to check flag: ${message}` }], isError: true };
|
|
26612
|
+
}
|
|
26613
|
+
}
|
|
26614
|
+
);
|
|
26615
|
+
server.tool(
|
|
26616
|
+
"create_flag",
|
|
26617
|
+
"Create or update a feature flag with the specified enabled stages. Idempotent \u2014 calling with an existing flag name replaces its enabledIn list.",
|
|
26618
|
+
{
|
|
26619
|
+
name: external_exports.string().min(1).describe('Flag name (e.g. "rbac:enforcement")'),
|
|
26620
|
+
enabledIn: external_exports.array(STAGE_ENUM).describe("Stages to enable the flag in"),
|
|
26621
|
+
description: external_exports.string().optional().describe("Human-readable description of what this flag gates"),
|
|
26622
|
+
note: external_exports.string().optional().describe("Reason for this change (recorded in change history)")
|
|
26623
|
+
},
|
|
26624
|
+
async ({ name, enabledIn, description, note }) => {
|
|
26625
|
+
if (!ctx.user.email) {
|
|
26626
|
+
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
26627
|
+
}
|
|
26628
|
+
try {
|
|
26629
|
+
const flag = await client.createFlag(name, enabledIn, { description, note, updatedBy: ctx.user.email });
|
|
26630
|
+
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
26631
|
+
} catch (err) {
|
|
26632
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26633
|
+
return { content: [{ type: "text", text: `Failed to create flag: ${message}` }], isError: true };
|
|
26634
|
+
}
|
|
26635
|
+
}
|
|
26636
|
+
);
|
|
26637
|
+
server.tool(
|
|
26638
|
+
"set_flag",
|
|
26639
|
+
"Update which stages a feature flag is enabled in. Replaces the current enabledIn list entirely.",
|
|
26640
|
+
{
|
|
26641
|
+
name: external_exports.string().min(1).describe("Flag name"),
|
|
26642
|
+
enabledIn: external_exports.array(STAGE_ENUM).describe("New set of stages to enable the flag in. Pass [] to disable everywhere without archiving."),
|
|
26643
|
+
note: external_exports.string().optional().describe("Reason for this change (recorded in change history)")
|
|
26644
|
+
},
|
|
26645
|
+
async ({ name, enabledIn, note }) => {
|
|
26646
|
+
if (!ctx.user.email) {
|
|
26647
|
+
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
26648
|
+
}
|
|
26649
|
+
try {
|
|
26650
|
+
const flag = await client.setFlag(name, enabledIn, { note, updatedBy: ctx.user.email });
|
|
26651
|
+
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
26652
|
+
} catch (err) {
|
|
26653
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26654
|
+
return { content: [{ type: "text", text: `Failed to set flag: ${message}` }], isError: true };
|
|
26655
|
+
}
|
|
26656
|
+
}
|
|
26657
|
+
);
|
|
26658
|
+
server.tool(
|
|
26659
|
+
"archive_flag",
|
|
26660
|
+
"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.",
|
|
26661
|
+
{
|
|
26662
|
+
name: external_exports.string().min(1).describe("Flag name"),
|
|
26663
|
+
note: external_exports.string().optional().describe("Reason for archiving (recorded in change history)")
|
|
26664
|
+
},
|
|
26665
|
+
async ({ name, note }) => {
|
|
26666
|
+
if (!ctx.user.email) {
|
|
26667
|
+
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
26668
|
+
}
|
|
26669
|
+
try {
|
|
26670
|
+
const flag = await client.archiveFlag(name, { note, updatedBy: ctx.user.email });
|
|
26671
|
+
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
26672
|
+
} catch (err) {
|
|
26673
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26674
|
+
return { content: [{ type: "text", text: `Failed to archive flag: ${message}` }], isError: true };
|
|
26675
|
+
}
|
|
26676
|
+
}
|
|
26677
|
+
);
|
|
26678
|
+
server.tool(
|
|
26679
|
+
"unarchive_flag",
|
|
26680
|
+
"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.",
|
|
26681
|
+
{
|
|
26682
|
+
name: external_exports.string().min(1).describe("Flag name"),
|
|
26683
|
+
note: external_exports.string().optional().describe("Reason for restoring (recorded in change history)")
|
|
26684
|
+
},
|
|
26685
|
+
async ({ name, note }) => {
|
|
26686
|
+
if (!ctx.user.email) {
|
|
26687
|
+
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
26688
|
+
}
|
|
26689
|
+
try {
|
|
26690
|
+
const flag = await client.unarchiveFlag(name, { note, updatedBy: ctx.user.email });
|
|
26691
|
+
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
26692
|
+
} catch (err) {
|
|
26693
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26694
|
+
return { content: [{ type: "text", text: `Failed to unarchive flag: ${message}` }], isError: true };
|
|
26695
|
+
}
|
|
26696
|
+
}
|
|
26697
|
+
);
|
|
26698
|
+
}
|
|
26699
|
+
function formatFlag(flag, history) {
|
|
26700
|
+
const archived = flag.archivedAt ? " **(archived)**" : "";
|
|
26701
|
+
const enabled = flag.enabledIn.length > 0 ? flag.enabledIn.join(", ") : "(none)";
|
|
26702
|
+
const parts = [
|
|
26703
|
+
`# Flag: ${flag.name}${archived}`,
|
|
26704
|
+
`**Scope:** ${flag.scope}`,
|
|
26705
|
+
`**Enabled in:** ${enabled}`,
|
|
26706
|
+
flag.description ? `**Description:** ${flag.description}` : null,
|
|
26707
|
+
`**Last updated:** ${flag.updatedAt.slice(0, 10)} by ${flag.updatedBy}`,
|
|
26708
|
+
flag.archivedAt ? `**Archived:** ${flag.archivedAt.slice(0, 10)} by ${flag.archivedBy}` : null
|
|
26709
|
+
].filter(Boolean).join("\n");
|
|
26710
|
+
if (!history?.length) return parts;
|
|
26711
|
+
const historyLines = history.map((h) => {
|
|
26712
|
+
const prev = h.previousEnabledIn.length > 0 ? h.previousEnabledIn.join(", ") : "(none)";
|
|
26713
|
+
const next = h.newEnabledIn.length > 0 ? h.newEnabledIn.join(", ") : "(none)";
|
|
26714
|
+
const note = h.note ? ` \u2014 ${h.note}` : "";
|
|
26715
|
+
return `- ${h.updatedAt.slice(0, 10)} ${h.updatedBy}: ${prev} \u2192 ${next}${note}`;
|
|
26716
|
+
}).join("\n");
|
|
26717
|
+
return `${parts}
|
|
26718
|
+
|
|
26719
|
+
## Change History
|
|
26720
|
+
${historyLines}`;
|
|
26721
|
+
}
|
|
26722
|
+
function formatFlagList(flags, total, nextCursor) {
|
|
26723
|
+
const rows = flags.map((f) => {
|
|
26724
|
+
const enabled = f.enabledIn.length > 0 ? f.enabledIn.join(", ") : "(none)";
|
|
26725
|
+
const archived = f.archivedAt ? " *(archived)*" : "";
|
|
26726
|
+
return `| ${f.name}${archived} | ${enabled} | ${f.updatedAt.slice(0, 10)} |`;
|
|
26727
|
+
});
|
|
26728
|
+
const table = [
|
|
26729
|
+
`# Feature Flags (${total} total, showing ${flags.length})`,
|
|
26730
|
+
"",
|
|
26731
|
+
"| Name | Enabled In | Updated |",
|
|
26732
|
+
"|---|---|---|",
|
|
26733
|
+
...rows
|
|
26734
|
+
].join("\n");
|
|
26735
|
+
if (!nextCursor) return table;
|
|
26736
|
+
return `${table}
|
|
26737
|
+
|
|
26738
|
+
More flags available. Pass \`cursor: "${nextCursor}"\` to list_flags to continue.`;
|
|
26739
|
+
}
|
|
26740
|
+
|
|
26741
|
+
// ../../libs/harmonica-services/src/mcp/tools/layer-tools.ts
|
|
26742
|
+
function registerLayerTools(server, ctx, client) {
|
|
26743
|
+
server.tool(
|
|
26744
|
+
"list_system_layers",
|
|
26745
|
+
"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.",
|
|
26746
|
+
{
|
|
26747
|
+
systemId: external_exports.string().describe("The System ID")
|
|
26748
|
+
},
|
|
26749
|
+
async ({ systemId }) => {
|
|
26750
|
+
try {
|
|
26751
|
+
await assertSystemExists(client, systemId);
|
|
26752
|
+
} catch (err) {
|
|
26753
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26754
|
+
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
26755
|
+
}
|
|
26756
|
+
let layers;
|
|
26757
|
+
try {
|
|
26758
|
+
layers = await client.listSystemLayers(systemId);
|
|
26759
|
+
} catch (err) {
|
|
26760
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26761
|
+
return { content: [{ type: "text", text: `Failed to list Layers: ${message}` }], isError: true };
|
|
26762
|
+
}
|
|
26763
|
+
const text = formatLayerList(layers);
|
|
26764
|
+
return { content: [{ type: "text", text }] };
|
|
26765
|
+
}
|
|
26766
|
+
);
|
|
26767
|
+
server.tool(
|
|
26768
|
+
"get_layer",
|
|
26769
|
+
"Get full details of a single Layer by ID, including its repo mapping and parent Layer (if it was split from one).",
|
|
26770
|
+
{
|
|
26771
|
+
systemId: external_exports.string().describe("The System ID (for access control)"),
|
|
26772
|
+
layerId: external_exports.string().describe("The Layer ID")
|
|
26773
|
+
},
|
|
26774
|
+
async ({ systemId, layerId }) => {
|
|
26775
|
+
try {
|
|
26776
|
+
await assertSystemExists(client, systemId);
|
|
26777
|
+
} catch (err) {
|
|
26778
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26779
|
+
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
26780
|
+
}
|
|
26781
|
+
let layer;
|
|
26782
|
+
try {
|
|
26783
|
+
layer = await client.getLayer(layerId);
|
|
26784
|
+
} catch (err) {
|
|
26785
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26786
|
+
return { content: [{ type: "text", text: `Failed to get Layer: ${message}` }], isError: true };
|
|
26787
|
+
}
|
|
26788
|
+
if (!layer) {
|
|
26789
|
+
return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
26790
|
+
}
|
|
26791
|
+
if (layer.systemId !== systemId) {
|
|
26792
|
+
return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
|
|
26793
|
+
}
|
|
26794
|
+
const text = formatLayerDetail(layer);
|
|
26795
|
+
return { content: [{ type: "text", text }] };
|
|
26796
|
+
}
|
|
26797
|
+
);
|
|
26798
|
+
server.tool(
|
|
26799
|
+
"create_layer",
|
|
26800
|
+
"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.",
|
|
26801
|
+
{
|
|
26802
|
+
systemId: external_exports.string().describe("The System ID"),
|
|
26803
|
+
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"'),
|
|
26804
|
+
name: external_exports.string().describe('Display name, e.g. "Frontend"'),
|
|
26805
|
+
description: external_exports.string().optional().describe("Optional description of the Layer"),
|
|
26806
|
+
parentLayerId: external_exports.string().optional().describe("Optional parent Layer to nest under (must be a Layer on the same System)"),
|
|
26807
|
+
repoPatterns: external_exports.array(external_exports.string()).optional().describe("Optional repo path patterns owned by this Layer")
|
|
26808
|
+
},
|
|
26809
|
+
async ({ systemId, layerKey, name, description, parentLayerId, repoPatterns }) => {
|
|
26810
|
+
try {
|
|
26811
|
+
await assertSystemExists(client, systemId);
|
|
26812
|
+
} catch (err) {
|
|
26813
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26814
|
+
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
26815
|
+
}
|
|
26816
|
+
let layer;
|
|
26817
|
+
try {
|
|
26818
|
+
layer = await client.createSystemLayer({
|
|
26819
|
+
systemId,
|
|
26820
|
+
layerKey,
|
|
26821
|
+
name,
|
|
26822
|
+
...description !== void 0 && { description },
|
|
26823
|
+
...parentLayerId !== void 0 && { parentLayerId },
|
|
26824
|
+
...repoPatterns !== void 0 && { repoPatterns }
|
|
26825
|
+
});
|
|
26826
|
+
} catch (err) {
|
|
26827
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26828
|
+
return { content: [{ type: "text", text: `Failed to create Layer: ${message}` }], isError: true };
|
|
26829
|
+
}
|
|
26830
|
+
if (!layer) {
|
|
26831
|
+
return { content: [{ type: "text", text: `System "${systemId}" not found or not writable.` }], isError: true };
|
|
26832
|
+
}
|
|
26833
|
+
return { content: [{ type: "text", text: `Created Layer.
|
|
26834
|
+
|
|
26835
|
+
${formatLayerDetail(layer)}` }] };
|
|
26836
|
+
}
|
|
26837
|
+
);
|
|
26838
|
+
server.tool(
|
|
26839
|
+
"update_layer",
|
|
26840
|
+
"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.",
|
|
26841
|
+
{
|
|
26842
|
+
systemId: external_exports.string().describe("The System ID the Layer belongs to (for access control)"),
|
|
26843
|
+
layerId: external_exports.string().describe("The Layer ID to update"),
|
|
26844
|
+
name: external_exports.string().optional().describe("New display name"),
|
|
26845
|
+
description: external_exports.string().optional().describe("New description"),
|
|
26846
|
+
repoPatterns: external_exports.array(external_exports.string()).optional().describe("Replacement repo mapping (replaces the existing list)")
|
|
26847
|
+
},
|
|
26848
|
+
async ({ systemId, layerId, name, description, repoPatterns }) => {
|
|
26849
|
+
try {
|
|
26850
|
+
await assertSystemExists(client, systemId);
|
|
26851
|
+
} catch (err) {
|
|
26852
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26853
|
+
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
26854
|
+
}
|
|
26855
|
+
let existing;
|
|
26856
|
+
try {
|
|
26857
|
+
existing = await client.getLayer(layerId);
|
|
26858
|
+
} catch (err) {
|
|
26859
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26860
|
+
return { content: [{ type: "text", text: `Failed to load Layer: ${message}` }], isError: true };
|
|
26861
|
+
}
|
|
26862
|
+
if (!existing) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
26863
|
+
if (existing.systemId !== systemId) {
|
|
26864
|
+
return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
|
|
26865
|
+
}
|
|
26866
|
+
let layer;
|
|
26867
|
+
try {
|
|
26868
|
+
layer = await client.updateSystemLayer(layerId, {
|
|
26869
|
+
...name !== void 0 && { name },
|
|
26870
|
+
...description !== void 0 && { description },
|
|
26871
|
+
...repoPatterns !== void 0 && { repoPatterns }
|
|
26872
|
+
});
|
|
26873
|
+
} catch (err) {
|
|
26874
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26875
|
+
return { content: [{ type: "text", text: `Failed to update Layer: ${message}` }], isError: true };
|
|
26876
|
+
}
|
|
26877
|
+
if (!layer) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
26878
|
+
return { content: [{ type: "text", text: `Updated Layer.
|
|
26879
|
+
|
|
26880
|
+
${formatLayerDetail(layer)}` }] };
|
|
26881
|
+
}
|
|
26882
|
+
);
|
|
26883
|
+
server.tool(
|
|
26884
|
+
"transition_layer",
|
|
26885
|
+
"Archive or revive a Layer. Archiving requires a reason (audit trail); reviving sets it back to active. Requires write access to the System.",
|
|
26886
|
+
{
|
|
26887
|
+
systemId: external_exports.string().describe("The System ID the Layer belongs to (for access control)"),
|
|
26888
|
+
layerId: external_exports.string().describe("The Layer ID to transition"),
|
|
26889
|
+
status: external_exports.enum(["active", "archived"]).describe('Target status: "archived" to retire, "active" to revive'),
|
|
26890
|
+
reason: external_exports.string().optional().describe('Why the Layer is being archived (required when status is "archived")')
|
|
26891
|
+
},
|
|
26892
|
+
async ({ systemId, layerId, status, reason }) => {
|
|
26893
|
+
try {
|
|
26894
|
+
await assertSystemExists(client, systemId);
|
|
26895
|
+
} catch (err) {
|
|
26896
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26897
|
+
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
26898
|
+
}
|
|
26899
|
+
let existing;
|
|
26900
|
+
try {
|
|
26901
|
+
existing = await client.getLayer(layerId);
|
|
26902
|
+
} catch (err) {
|
|
26903
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26904
|
+
return { content: [{ type: "text", text: `Failed to load Layer: ${message}` }], isError: true };
|
|
26905
|
+
}
|
|
26906
|
+
if (!existing) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
26907
|
+
if (existing.systemId !== systemId) {
|
|
26908
|
+
return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
|
|
26909
|
+
}
|
|
26910
|
+
let layer;
|
|
26911
|
+
try {
|
|
26912
|
+
layer = await client.setSystemLayerStatus(layerId, status, reason);
|
|
26913
|
+
} catch (err) {
|
|
26914
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26915
|
+
return { content: [{ type: "text", text: `Failed to transition Layer: ${message}` }], isError: true };
|
|
26916
|
+
}
|
|
26917
|
+
if (!layer) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
26918
|
+
return { content: [{ type: "text", text: `Layer ${status === "archived" ? "archived" : "revived"}.
|
|
26919
|
+
|
|
26920
|
+
${formatLayerDetail(layer)}` }] };
|
|
26921
|
+
}
|
|
26922
|
+
);
|
|
26923
|
+
}
|
|
26924
|
+
|
|
26925
|
+
// ../../libs/harmonica-services/src/mcp/tools/membership-tools.ts
|
|
26926
|
+
var orgIdArg = external_exports.string().describe("The organization ID \u2014 see list_organizations");
|
|
26927
|
+
function registerMembershipTools(server, ctx, client) {
|
|
26928
|
+
server.tool(
|
|
26929
|
+
"list_org_members",
|
|
26930
|
+
"List all members of an organization",
|
|
26931
|
+
{
|
|
26932
|
+
orgId: orgIdArg
|
|
26933
|
+
},
|
|
26934
|
+
async ({ orgId }) => {
|
|
26935
|
+
try {
|
|
26936
|
+
const members = await client.listOrgMembers(orgId);
|
|
26937
|
+
if (members.length === 0) {
|
|
26938
|
+
return { content: [{ type: "text", text: "No members found." }] };
|
|
26939
|
+
}
|
|
26940
|
+
const header = "| User ID | Name | Role | Title |";
|
|
26941
|
+
const divider = "|---------|------|------|-------|";
|
|
26942
|
+
const rows = members.map(
|
|
26943
|
+
(m) => `| ${m.userId} | ${m.name} | ${m.role} | ${m.title ?? "\u2014"} |`
|
|
26944
|
+
);
|
|
26945
|
+
return { content: [{ type: "text", text: [header, divider, ...rows].join("\n") }] };
|
|
26946
|
+
} catch (err) {
|
|
26947
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26948
|
+
return { content: [{ type: "text", text: `Failed to list members: ${message}` }], isError: true };
|
|
26949
|
+
}
|
|
26950
|
+
}
|
|
26951
|
+
);
|
|
26952
|
+
server.tool(
|
|
26953
|
+
"add_member",
|
|
26954
|
+
"Grant a user access to an organization. Their name and avatar are filled in when they first sign in.",
|
|
26955
|
+
{
|
|
26956
|
+
orgId: orgIdArg,
|
|
26957
|
+
email: external_exports.string().email().describe("Email address (used as the user ID)"),
|
|
26958
|
+
role: external_exports.enum(["member", "admin", "client"]).optional().describe('Organization role (defaults to "member")')
|
|
26959
|
+
},
|
|
26960
|
+
async ({ orgId, email: email2, role }) => {
|
|
26961
|
+
try {
|
|
26962
|
+
const member = await client.addOrgMember(orgId, { email: email2, role });
|
|
26963
|
+
const lines = [
|
|
26964
|
+
"Member added successfully.",
|
|
26965
|
+
"",
|
|
26966
|
+
`**User ID:** ${member.userId}`,
|
|
26967
|
+
`**Email:** ${member.email}`,
|
|
26968
|
+
`**Name:** ${member.name}`,
|
|
26969
|
+
`**Role:** ${member.role}`,
|
|
26970
|
+
member.title ? `**Title:** ${member.title}` : ""
|
|
26971
|
+
].filter(Boolean);
|
|
26972
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
26973
|
+
} catch (err) {
|
|
26974
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26975
|
+
return { content: [{ type: "text", text: `Failed to add member: ${message}` }], isError: true };
|
|
26976
|
+
}
|
|
26977
|
+
}
|
|
26978
|
+
);
|
|
26979
|
+
server.tool(
|
|
26980
|
+
"update_member_role",
|
|
26981
|
+
"Change a member's role within an organization.",
|
|
26982
|
+
{
|
|
26983
|
+
orgId: orgIdArg,
|
|
26984
|
+
userId: external_exports.string().describe("User ID (email address) of the member to update"),
|
|
26985
|
+
role: external_exports.enum(["member", "admin", "client"]).describe("New organization role")
|
|
26986
|
+
},
|
|
26987
|
+
async ({ orgId, userId, role }) => {
|
|
26988
|
+
try {
|
|
26989
|
+
const member = await client.updateOrgMember(orgId, userId, { role });
|
|
26990
|
+
if (!member) {
|
|
26991
|
+
return { content: [{ type: "text", text: `Member not found: "${userId}"` }], isError: true };
|
|
26992
|
+
}
|
|
26993
|
+
const lines = [
|
|
26994
|
+
"Member updated successfully.",
|
|
26995
|
+
"",
|
|
26996
|
+
`**User ID:** ${member.userId}`,
|
|
26997
|
+
`**Email:** ${member.email}`,
|
|
26998
|
+
`**Name:** ${member.name}`,
|
|
26999
|
+
`**Role:** ${member.role}`,
|
|
27000
|
+
member.title ? `**Title:** ${member.title}` : "",
|
|
27001
|
+
member.responsibility ? `**Responsibility:** ${member.responsibility}` : ""
|
|
27002
|
+
].filter(Boolean);
|
|
27003
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
27004
|
+
} catch (err) {
|
|
27005
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27006
|
+
return { content: [{ type: "text", text: `Failed to update member: ${message}` }], isError: true };
|
|
27007
|
+
}
|
|
27008
|
+
}
|
|
27009
|
+
);
|
|
27010
|
+
server.tool(
|
|
27011
|
+
"remove_member",
|
|
27012
|
+
"Remove a user from an organization.",
|
|
27013
|
+
{
|
|
27014
|
+
orgId: orgIdArg,
|
|
27015
|
+
userId: external_exports.string().describe("User ID (email address) of the member to remove")
|
|
27016
|
+
},
|
|
27017
|
+
async ({ orgId, userId }) => {
|
|
27018
|
+
try {
|
|
27019
|
+
await client.removeOrgMember(orgId, userId);
|
|
27020
|
+
return { content: [{ type: "text", text: `Member "${userId}" has been removed from the organization.` }] };
|
|
27021
|
+
} catch (err) {
|
|
27022
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27023
|
+
return { content: [{ type: "text", text: `Failed to remove member: ${message}` }], isError: true };
|
|
27024
|
+
}
|
|
27025
|
+
}
|
|
27026
|
+
);
|
|
27027
|
+
}
|
|
27028
|
+
|
|
27029
|
+
// ../../libs/harmonica-services/src/mcp/tools/movement-tools.ts
|
|
27030
|
+
function registerMovementTools(server, _ctx, client) {
|
|
27031
|
+
server.tool(
|
|
27032
|
+
"list_movements",
|
|
27033
|
+
'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.',
|
|
27034
|
+
{
|
|
27035
|
+
teamspaceId: external_exports.string().describe("The teamspace ID")
|
|
27036
|
+
},
|
|
27037
|
+
async ({ teamspaceId }) => {
|
|
27038
|
+
const movements = await client.listTeamspaceMovements(teamspaceId);
|
|
27039
|
+
const text = movements.length === 0 ? "No movements found." : movements.map((m) => `[${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})`).join("\n");
|
|
27040
|
+
return { content: [{ type: "text", text }] };
|
|
27041
|
+
}
|
|
27042
|
+
);
|
|
27043
|
+
server.tool(
|
|
27044
|
+
"get_movement",
|
|
27045
|
+
"Get a Movement by ID. Returns full details including status, approval timestamps, and audit stamps.",
|
|
27046
|
+
{
|
|
27047
|
+
movementId: external_exports.string().describe("The movement ID (e.g., mv-abc123)")
|
|
27048
|
+
},
|
|
27049
|
+
async ({ movementId }) => {
|
|
27050
|
+
const m = await client.getMovement(movementId);
|
|
27051
|
+
if (!m) {
|
|
27052
|
+
return { content: [{ type: "text", text: `Movement not found: ${movementId}` }] };
|
|
27053
|
+
}
|
|
27054
|
+
const lines = [
|
|
27055
|
+
`ID: ${m.movementId}`,
|
|
27056
|
+
`Code: ${m.movementCode}`,
|
|
27057
|
+
`Title: ${m.title}`,
|
|
27058
|
+
`Teamspace: ${m.teamspaceId}`,
|
|
27059
|
+
`Status: ${m.status}`,
|
|
27060
|
+
m.description ? `Description: ${m.description}` : null,
|
|
27061
|
+
m.proposedAt ? `Proposed: ${m.proposedAt}` : null,
|
|
27062
|
+
m.approvedAt ? `Approved: ${m.approvedAt}` : null,
|
|
27063
|
+
m.approvalNoteId ? `Approval Note: ${m.approvalNoteId}` : null,
|
|
27064
|
+
m.createdBy ? `Created By: ${m.createdBy}` : null,
|
|
27065
|
+
`Version: ${m.version}`,
|
|
27066
|
+
`Created: ${m.createdAt}`,
|
|
27067
|
+
`Updated: ${m.updatedAt}`
|
|
27068
|
+
].filter(Boolean).join("\n");
|
|
27069
|
+
return { content: [{ type: "text", text: lines }] };
|
|
27070
|
+
}
|
|
27071
|
+
);
|
|
27072
|
+
server.tool(
|
|
27073
|
+
"create_movement",
|
|
27074
|
+
`Create a Movement for an Account, in one of that Account's Teamspaces. A Movement is a client-SOW container for an engagement \u2014 it is NOT a Beat. Recognition cues: "new SOW", "create a contract scope", "start an engagement container" \u2192 create_movement.`,
|
|
27075
|
+
{
|
|
27076
|
+
teamspaceId: external_exports.string().describe("The teamspace ID"),
|
|
27077
|
+
title: external_exports.string().min(1).max(300).describe('Movement title (e.g. "Q3 Platform Modernisation SOW")'),
|
|
27078
|
+
description: external_exports.string().max(2e3).optional().describe("Optional description"),
|
|
27079
|
+
accountId: external_exports.string().min(1).max(128).describe("The Account this Movement belongs to. Required (rev-950e828e). Must be in the same org as the Teamspace and linked to it.")
|
|
27080
|
+
},
|
|
27081
|
+
async ({ teamspaceId, title, description, accountId }) => {
|
|
27082
|
+
const m = await client.createMovement({
|
|
27083
|
+
teamspaceId,
|
|
27084
|
+
title,
|
|
27085
|
+
...description !== void 0 && { description },
|
|
27086
|
+
accountId
|
|
27087
|
+
});
|
|
27088
|
+
return {
|
|
27089
|
+
content: [
|
|
27090
|
+
{
|
|
27091
|
+
type: "text",
|
|
27092
|
+
text: `Created Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})`
|
|
27093
|
+
}
|
|
27094
|
+
]
|
|
27095
|
+
};
|
|
27096
|
+
}
|
|
27097
|
+
);
|
|
27098
|
+
server.tool(
|
|
27099
|
+
"update_movement",
|
|
27100
|
+
"Update mutable content fields on a Movement (title, description). To change status use transition_movement_status instead.",
|
|
27101
|
+
{
|
|
27102
|
+
movementId: external_exports.string().describe("The movement ID"),
|
|
27103
|
+
title: external_exports.string().min(1).max(300).optional().describe("New title"),
|
|
27104
|
+
description: external_exports.string().max(2e3).optional().describe("New description"),
|
|
27105
|
+
accountId: external_exports.string().min(1).max(128).optional().describe("Reassign this Movement to a different Account. Must be in the same org and linked to the teamspace. Omit to leave unchanged \u2014 a Movement cannot be left without an Account (rev-950e828e).")
|
|
27106
|
+
},
|
|
27107
|
+
async ({ movementId, title, description, accountId }) => {
|
|
27108
|
+
const hasUpdates = title !== void 0 || description !== void 0 || accountId !== void 0;
|
|
27109
|
+
if (!hasUpdates) {
|
|
27110
|
+
return {
|
|
27111
|
+
content: [{ type: "text", text: `No updates provided for Movement: ${movementId}` }],
|
|
27112
|
+
isError: true
|
|
27113
|
+
};
|
|
27114
|
+
}
|
|
27115
|
+
try {
|
|
27116
|
+
const m = await client.updateMovement(movementId, {
|
|
27117
|
+
...title !== void 0 && { title },
|
|
27118
|
+
...description !== void 0 && { description },
|
|
27119
|
+
...accountId !== void 0 && { accountId }
|
|
27120
|
+
});
|
|
27121
|
+
if (!m) {
|
|
27122
|
+
return {
|
|
27123
|
+
content: [{ type: "text", text: `Movement not found: ${movementId}` }],
|
|
27124
|
+
isError: true
|
|
27125
|
+
};
|
|
27126
|
+
}
|
|
27127
|
+
const account = ` \u2014 Account: ${accountId ?? m.accountId}`;
|
|
27128
|
+
return {
|
|
27129
|
+
content: [
|
|
27130
|
+
{
|
|
27131
|
+
type: "text",
|
|
27132
|
+
text: `Updated Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})${account}`
|
|
27133
|
+
}
|
|
27134
|
+
]
|
|
27135
|
+
};
|
|
27136
|
+
} catch (err) {
|
|
27137
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27138
|
+
return {
|
|
27139
|
+
content: [{ type: "text", text: `Failed to update Movement: ${message}` }],
|
|
27140
|
+
isError: true
|
|
27141
|
+
};
|
|
27142
|
+
}
|
|
27143
|
+
}
|
|
27144
|
+
);
|
|
27145
|
+
server.tool(
|
|
27146
|
+
"transition_movement_status",
|
|
27147
|
+
'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.',
|
|
27148
|
+
{
|
|
27149
|
+
movementId: external_exports.string().describe("The movement ID"),
|
|
27150
|
+
targetState: external_exports.enum(["proposed", "approved", "active", "fulfilled", "cancelled"]).describe("Target lifecycle state"),
|
|
27151
|
+
approvalNoteId: external_exports.string().optional().describe('Required when transitioning to "approved" \u2014 the Note ID capturing the approval decision'),
|
|
27152
|
+
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"),
|
|
27153
|
+
confirm: external_exports.boolean().optional().default(false).describe("false (default) = dry-run preview; true = execute the transition")
|
|
27154
|
+
},
|
|
27155
|
+
async ({ movementId, targetState, approvalNoteId, reason, confirm }) => {
|
|
27156
|
+
if (!confirm) {
|
|
27157
|
+
return {
|
|
27158
|
+
content: [
|
|
27159
|
+
{
|
|
27160
|
+
type: "text",
|
|
27161
|
+
text: `Dry-run: would transition Movement ${movementId} \u2192 ${targetState}${approvalNoteId ? ` (approvalNoteId: ${approvalNoteId})` : ""}${reason ? ` \u2014 ${reason}` : ""}. Call again with confirm=true to execute.`
|
|
27162
|
+
}
|
|
27163
|
+
]
|
|
27164
|
+
};
|
|
27165
|
+
}
|
|
27166
|
+
try {
|
|
27167
|
+
const m = await client.transitionMovementStatus(movementId, targetState, {
|
|
27168
|
+
...approvalNoteId !== void 0 && { approvalNoteId },
|
|
27169
|
+
...reason !== void 0 && { reason }
|
|
27170
|
+
});
|
|
27171
|
+
if (!m) {
|
|
27172
|
+
return {
|
|
27173
|
+
content: [{ type: "text", text: `Movement not found: ${movementId}` }],
|
|
27174
|
+
isError: true
|
|
27175
|
+
};
|
|
27176
|
+
}
|
|
27177
|
+
return {
|
|
27178
|
+
content: [
|
|
27179
|
+
{
|
|
27180
|
+
type: "text",
|
|
27181
|
+
text: `Transitioned Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} \u2192 ${m.status}`
|
|
27182
|
+
}
|
|
27183
|
+
]
|
|
27184
|
+
};
|
|
27185
|
+
} catch (err) {
|
|
27186
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27187
|
+
return {
|
|
27188
|
+
content: [{ type: "text", text: `Failed to transition Movement: ${message}` }],
|
|
27189
|
+
isError: true
|
|
27190
|
+
};
|
|
27191
|
+
}
|
|
27192
|
+
}
|
|
27193
|
+
);
|
|
27194
|
+
}
|
|
27195
|
+
|
|
27196
|
+
// ../../libs/harmonica-services/src/mcp/tools/next-actions-tools.ts
|
|
27197
|
+
var PRIORITY_ICONS = {
|
|
27198
|
+
P0: "\u{1F6A8}",
|
|
27199
|
+
P1: "\u{1F680}",
|
|
27200
|
+
P2: "\u{1F528}",
|
|
27201
|
+
P3: "\u{1F4CB}",
|
|
27202
|
+
P4: "\u{1F4A1}",
|
|
27203
|
+
P5: "\u2728"
|
|
27204
|
+
};
|
|
27205
|
+
function formatNextActions(actions) {
|
|
27206
|
+
if (actions.length === 0) {
|
|
27207
|
+
return "No actions found. Project is up to date.";
|
|
27208
|
+
}
|
|
27209
|
+
const lines = ["# Next Actions\n"];
|
|
27210
|
+
for (const action of actions) {
|
|
27211
|
+
const icon = PRIORITY_ICONS[action.priority] ?? "\u2022";
|
|
27212
|
+
lines.push(`## ${icon} ${action.priority} \u2014 ${action.category}`);
|
|
27213
|
+
lines.push(`**${action.title}**`);
|
|
27214
|
+
lines.push(action.description);
|
|
27215
|
+
if (action.entityId) {
|
|
27216
|
+
const parts = [`${action.entityType}: ${action.entityId}`];
|
|
27217
|
+
if (action.beatId) parts.push(`Beat: ${action.beatId}`);
|
|
27218
|
+
lines.push(parts.join(" | "));
|
|
27219
|
+
}
|
|
27220
|
+
if (action.count) {
|
|
27221
|
+
lines.push(`Count: ${action.count}`);
|
|
27222
|
+
}
|
|
27223
|
+
lines.push("");
|
|
27224
|
+
}
|
|
27225
|
+
return lines.join("\n");
|
|
27226
|
+
}
|
|
27227
|
+
function registerNextActionsTools(server, ctx, client) {
|
|
27228
|
+
server.tool(
|
|
27229
|
+
"get_next_actions",
|
|
27230
|
+
"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).",
|
|
27231
|
+
{
|
|
27232
|
+
systemId: external_exports.string().describe("The system ID"),
|
|
27233
|
+
limit: external_exports.coerce.number().optional().default(10).describe("Max actions to return")
|
|
27234
|
+
},
|
|
27235
|
+
async ({ systemId, limit }) => {
|
|
27236
|
+
try {
|
|
27237
|
+
await assertSystemExists(client, systemId);
|
|
27238
|
+
const actions = await client.getNextActions(systemId, { limit });
|
|
27239
|
+
return { content: [{ type: "text", text: formatNextActions(actions) }] };
|
|
27240
|
+
} catch (err) {
|
|
27241
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27242
|
+
return { content: [{ type: "text", text: `Failed to get next actions: ${message}` }], isError: true };
|
|
27243
|
+
}
|
|
27244
|
+
}
|
|
27245
|
+
);
|
|
27246
|
+
server.tool(
|
|
27247
|
+
"list_triage_actions",
|
|
27248
|
+
"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.",
|
|
27249
|
+
{
|
|
27250
|
+
systemId: external_exports.string().describe("The system ID")
|
|
27251
|
+
},
|
|
27252
|
+
async ({ systemId }) => {
|
|
27253
|
+
try {
|
|
27254
|
+
await assertSystemExists(client, systemId);
|
|
27255
|
+
const actions = await client.listTriageActions(systemId);
|
|
27256
|
+
return { content: [{ type: "text", text: formatTriageActions(actions) }] };
|
|
27257
|
+
} catch (err) {
|
|
27258
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27259
|
+
return { content: [{ type: "text", text: `Failed to list triage actions: ${message}` }], isError: true };
|
|
27260
|
+
}
|
|
27261
|
+
}
|
|
27262
|
+
);
|
|
27263
|
+
server.tool(
|
|
27264
|
+
"dismiss_triage_action",
|
|
27265
|
+
'Dismiss a triage action (human decides "not now"). The action will not be re-escalated until the underlying state changes.',
|
|
27266
|
+
{
|
|
27267
|
+
systemId: external_exports.string().describe("The system ID"),
|
|
27268
|
+
actionKey: external_exports.string().describe("The action key to dismiss (from list_triage_actions)")
|
|
27269
|
+
},
|
|
27270
|
+
async ({ systemId, actionKey }) => {
|
|
27271
|
+
try {
|
|
27272
|
+
await assertSystemExists(client, systemId);
|
|
27273
|
+
await client.dismissTriageAction(systemId, actionKey, ctx.user?.userId ?? "mcp");
|
|
27274
|
+
return { content: [{ type: "text", text: `Action ${actionKey} dismissed.` }] };
|
|
27275
|
+
} catch (err) {
|
|
27276
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27277
|
+
return { content: [{ type: "text", text: `Failed to dismiss action: ${message}` }], isError: true };
|
|
27278
|
+
}
|
|
27279
|
+
}
|
|
27280
|
+
);
|
|
27281
|
+
}
|
|
27282
|
+
var STATUS_ICONS = {
|
|
27283
|
+
open: "\u2B1A",
|
|
27284
|
+
acted: "\u25D0",
|
|
27285
|
+
dismissed: "\u2715",
|
|
27286
|
+
resolved: "\u2713"
|
|
27287
|
+
};
|
|
27288
|
+
function formatTriageActions(actions) {
|
|
27289
|
+
if (actions.length === 0) {
|
|
27290
|
+
return "No active triage actions. Run triage to populate the ledger.";
|
|
27291
|
+
}
|
|
27292
|
+
const lines = [`# Triage Actions (${actions.length})
|
|
27293
|
+
`];
|
|
27294
|
+
for (const action of actions) {
|
|
27295
|
+
const pIcon = PRIORITY_ICONS[action.priority] ?? "\u2022";
|
|
27296
|
+
const sIcon = STATUS_ICONS[action.status] ?? "?";
|
|
27297
|
+
lines.push(`## ${pIcon} ${action.priority} \u2014 ${action.category} ${sIcon} ${action.status.toUpperCase()}`);
|
|
27298
|
+
lines.push(`**${action.title}**`);
|
|
27299
|
+
lines.push(action.description);
|
|
27300
|
+
const meta = [];
|
|
27301
|
+
if (action.entityId) meta.push(`Entity: ${action.entityId}`);
|
|
27302
|
+
if (action.taskId) meta.push(`Task: ${action.taskId}${action.taskStatus ? ` (${action.taskStatus})` : ""}`);
|
|
27303
|
+
if (action.decision) meta.push(`Decision: ${action.decision}`);
|
|
27304
|
+
if (meta.length > 0) lines.push(meta.join(" | "));
|
|
27305
|
+
lines.push(`Action key: \`${action.actionKey}\` | First seen: ${action.firstSeenAt}`);
|
|
27306
|
+
lines.push("");
|
|
27307
|
+
}
|
|
27308
|
+
return lines.join("\n");
|
|
27309
|
+
}
|
|
27310
|
+
|
|
27303
27311
|
// ../../libs/harmonica-services/src/mcp/tools/note-element-tools.ts
|
|
27312
|
+
var import_crypto3 = require("crypto");
|
|
27304
27313
|
var parentKindSchema = external_exports.enum(["notebook", "note"]);
|
|
27305
27314
|
function formatEl(e) {
|
|
27306
27315
|
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 +27964,13 @@ ${text}` }] };
|
|
|
27955
27964
|
try {
|
|
27956
27965
|
await assertSystemExists(client, systemId);
|
|
27957
27966
|
const note = await client.getNote(noteId);
|
|
27958
|
-
if (!note
|
|
27967
|
+
if (!note) {
|
|
27968
|
+
return { content: [{ type: "text", text: `Note not found: "${noteId}"` }], isError: true };
|
|
27969
|
+
}
|
|
27970
|
+
if (!note.systemId) {
|
|
27971
|
+
return { content: [{ type: "text", text: `Note "${noteId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
27972
|
+
}
|
|
27973
|
+
if (note.systemId !== systemId) {
|
|
27959
27974
|
return { content: [{ type: "text", text: `Note not found: "${noteId}"` }], isError: true };
|
|
27960
27975
|
}
|
|
27961
27976
|
const removeReason = reason ?? "Removed via remove_note";
|
|
@@ -27982,7 +27997,13 @@ ${text}` }] };
|
|
|
27982
27997
|
try {
|
|
27983
27998
|
await assertSystemExists(client, systemId);
|
|
27984
27999
|
const note = await client.getNote(noteId);
|
|
27985
|
-
if (!note
|
|
28000
|
+
if (!note) {
|
|
28001
|
+
return { content: [{ type: "text", text: `Document not found: "${noteId}"` }], isError: true };
|
|
28002
|
+
}
|
|
28003
|
+
if (!note.systemId) {
|
|
28004
|
+
return { content: [{ type: "text", text: `Document "${noteId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
28005
|
+
}
|
|
28006
|
+
if (note.systemId !== systemId) {
|
|
27986
28007
|
return { content: [{ type: "text", text: `Document not found: "${noteId}"` }], isError: true };
|
|
27987
28008
|
}
|
|
27988
28009
|
if (note.noteType !== "document") {
|
|
@@ -28045,7 +28066,7 @@ ${trimmed}` : `# ${title}`;
|
|
|
28045
28066
|
}
|
|
28046
28067
|
|
|
28047
28068
|
// ../../libs/harmonica-services/src/mcp/tools/notebook-tools.ts
|
|
28048
|
-
var SCOPE_TYPE_VALUES = ["org", "teamspace", "system", "account"
|
|
28069
|
+
var SCOPE_TYPE_VALUES = ["org", "teamspace", "system", "account"];
|
|
28049
28070
|
var scopeRefSchema = external_exports.object({
|
|
28050
28071
|
scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, system, or account"),
|
|
28051
28072
|
scopeId: external_exports.string().min(1).describe("ID of the scope entity")
|
|
@@ -28271,14 +28292,23 @@ ${formatTree(tree.root)}` }] };
|
|
|
28271
28292
|
}
|
|
28272
28293
|
let scopeSystemId = systemId;
|
|
28273
28294
|
if (!scopeSystemId) {
|
|
28295
|
+
let bindings;
|
|
28296
|
+
let bindingsFetchFailed = false;
|
|
28274
28297
|
try {
|
|
28275
|
-
|
|
28276
|
-
scopeSystemId = bindings?.find((b) => b.scopeType === "system"
|
|
28298
|
+
bindings = await client.getNotebookScopeBindings(notebookId);
|
|
28299
|
+
scopeSystemId = bindings?.find((b) => b.scopeType === "system")?.scopeId;
|
|
28277
28300
|
} catch {
|
|
28301
|
+
bindingsFetchFailed = true;
|
|
28302
|
+
}
|
|
28303
|
+
if (!scopeSystemId) {
|
|
28304
|
+
if (!bindingsFetchFailed) {
|
|
28305
|
+
const hasLegacyProjectBinding = bindings?.some((b) => b.scopeType === "project");
|
|
28306
|
+
if (hasLegacyProjectBinding) {
|
|
28307
|
+
return { content: [{ type: "text", text: `Notebook "${notebookId}" has only a legacy 'project' scope binding \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
28308
|
+
}
|
|
28309
|
+
}
|
|
28310
|
+
return { content: [{ type: "text", text: "No system scope: pass systemId or bind the notebook to a system first." }], isError: true };
|
|
28278
28311
|
}
|
|
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
28312
|
}
|
|
28283
28313
|
let tree;
|
|
28284
28314
|
try {
|
|
@@ -28892,8 +28922,7 @@ Error: ${msg}`);
|
|
|
28892
28922
|
const createdTracks = [];
|
|
28893
28923
|
for (const trackInput of tracksToCreate) {
|
|
28894
28924
|
try {
|
|
28895
|
-
const track = await client.
|
|
28896
|
-
orgId,
|
|
28925
|
+
const track = await client.createTrack({
|
|
28897
28926
|
teamspaceId: teamspace.teamspaceId,
|
|
28898
28927
|
movementId: createdMovementId,
|
|
28899
28928
|
title: trackInput.title,
|
|
@@ -28901,7 +28930,7 @@ Error: ${msg}`);
|
|
|
28901
28930
|
vuRate: 0,
|
|
28902
28931
|
createdBy: ctx.user?.userId
|
|
28903
28932
|
});
|
|
28904
|
-
createdTracks.push({ id: track.
|
|
28933
|
+
createdTracks.push({ id: track.trackId, title: track.title });
|
|
28905
28934
|
} catch (err) {
|
|
28906
28935
|
const msg = err instanceof Error ? err.message : String(err);
|
|
28907
28936
|
lines.unshift(`\u26A0\uFE0F Onboarding batch partially created \u2014 stopped at: track:${trackInput.title}
|
|
@@ -28997,7 +29026,7 @@ ${d.rationale}${d.suggestions?.length ? `
|
|
|
28997
29026
|
function registerOrganizationTools(server, ctx, client) {
|
|
28998
29027
|
server.tool(
|
|
28999
29028
|
"get_organization",
|
|
29000
|
-
"Get details for an organization by ID \u2014 returns name, slug, status, industry, and
|
|
29029
|
+
"Get details for an organization by ID \u2014 returns name, slug, status, industry, description, and coda",
|
|
29001
29030
|
{
|
|
29002
29031
|
orgId: external_exports.string().describe("The organization ID")
|
|
29003
29032
|
},
|
|
@@ -29012,7 +29041,9 @@ function registerOrganizationTools(server, ctx, client) {
|
|
|
29012
29041
|
`**Slug:** ${org.slug}`,
|
|
29013
29042
|
`**Status:** ${org.status}`,
|
|
29014
29043
|
org.industry ? `**Industry:** ${org.industry}` : "",
|
|
29015
|
-
org.description ? `**Description:** ${org.description}` : ""
|
|
29044
|
+
org.description ? `**Description:** ${org.description}` : "",
|
|
29045
|
+
org.coda ? `**Coda:**
|
|
29046
|
+
${org.coda}` : ""
|
|
29016
29047
|
].filter(Boolean);
|
|
29017
29048
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
29018
29049
|
}
|
|
@@ -29543,6 +29574,24 @@ function registerPortfolioCoherenceTools(server, ctx, client) {
|
|
|
29543
29574
|
);
|
|
29544
29575
|
}
|
|
29545
29576
|
|
|
29577
|
+
// ../../libs/harmonica-services/src/mcp/tools/progress-evidence-tools.ts
|
|
29578
|
+
function registerProgressEvidenceTools(server, _ctx, client) {
|
|
29579
|
+
server.tool(
|
|
29580
|
+
"get_progress_evidence",
|
|
29581
|
+
"Read cited progress evidence for a system and [start,end) reporting window. Calendar dates use the required IANA timezone; timestamps need an offset. Optional contributor matches recorded actors or explicitly labeled session initiators, never assignees. Returns planned/progressed/delivered evidence, source gaps, a watermark and continuation. Follow nextCursor even on empty pages. No writes.",
|
|
29582
|
+
{ systemId: external_exports.string().min(1).max(128).regex(/^[^#]+$/), ...ProgressEvidenceQuerySchema.shape },
|
|
29583
|
+
async ({ systemId, ...query }) => {
|
|
29584
|
+
try {
|
|
29585
|
+
const report = await client.getProgressEvidence(systemId, query);
|
|
29586
|
+
if (!report) return { isError: true, content: [{ type: "text", text: "System not found." }] };
|
|
29587
|
+
return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
|
|
29588
|
+
} catch (error2) {
|
|
29589
|
+
return { isError: true, content: [{ type: "text", text: `Progress evidence query failed: ${error2 instanceof Error ? error2.message : String(error2)}` }] };
|
|
29590
|
+
}
|
|
29591
|
+
}
|
|
29592
|
+
);
|
|
29593
|
+
}
|
|
29594
|
+
|
|
29546
29595
|
// ../../libs/harmonica-services/src/mcp/tools/pulse-report-tools.ts
|
|
29547
29596
|
function formatBeatRow(r) {
|
|
29548
29597
|
const downbeatState = r.hasDownbeat ? "" : " \u2014 no Downbeat ever set";
|
|
@@ -29651,9 +29700,9 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
29651
29700
|
if (!beat) {
|
|
29652
29701
|
return { content: [{ type: "text", text: `Beat not found: "${beatId}"` }], isError: true };
|
|
29653
29702
|
}
|
|
29654
|
-
const beatSystemId = beat.systemId
|
|
29703
|
+
const beatSystemId = beat.systemId;
|
|
29655
29704
|
if (!beatSystemId) {
|
|
29656
|
-
return { content: [{ type: "text", text: `Beat "${beatId}" has no system identifier \u2014 record may
|
|
29705
|
+
return { content: [{ type: "text", text: `Beat "${beatId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
29657
29706
|
}
|
|
29658
29707
|
let parentBv;
|
|
29659
29708
|
if (beatVersionId !== void 0) {
|
|
@@ -29661,9 +29710,9 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
29661
29710
|
if (!parentBv) {
|
|
29662
29711
|
return { content: [{ type: "text", text: `Beat Version not found: "${beatVersionId}"` }], isError: true };
|
|
29663
29712
|
}
|
|
29664
|
-
const bvSystemId = parentBv.systemId
|
|
29713
|
+
const bvSystemId = parentBv.systemId;
|
|
29665
29714
|
if (!bvSystemId) {
|
|
29666
|
-
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" has no system identifier \u2014 record may
|
|
29715
|
+
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
29667
29716
|
}
|
|
29668
29717
|
if (parentBv.beatId !== beat.beatId || bvSystemId !== beatSystemId) {
|
|
29669
29718
|
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" belongs to a different Beat.` }], isError: true };
|
|
@@ -29772,13 +29821,13 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
29772
29821
|
if (!bv) {
|
|
29773
29822
|
return { content: [{ type: "text", text: `Beat Version not found: "${beatVersionId}"` }], isError: true };
|
|
29774
29823
|
}
|
|
29775
|
-
const bvSystemId = bv.systemId
|
|
29824
|
+
const bvSystemId = bv.systemId;
|
|
29776
29825
|
if (!bvSystemId) {
|
|
29777
|
-
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" has no system identifier \u2014 record may
|
|
29826
|
+
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
29778
29827
|
}
|
|
29779
|
-
const revSystemId = revision.systemId
|
|
29828
|
+
const revSystemId = revision.systemId;
|
|
29780
29829
|
if (!revSystemId) {
|
|
29781
|
-
return { content: [{ type: "text", text: `Revision "${revisionId}" has no system identifier \u2014 record may
|
|
29830
|
+
return { content: [{ type: "text", text: `Revision "${revisionId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
29782
29831
|
}
|
|
29783
29832
|
if (bv.beatId !== revision.beatId || bvSystemId !== revSystemId) {
|
|
29784
29833
|
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" belongs to a different Beat.` }], isError: true };
|
|
@@ -30667,17 +30716,13 @@ function normalizeSnapshot(raw) {
|
|
|
30667
30716
|
accountDropIndex: raw.accountDropIndex ?? [],
|
|
30668
30717
|
deliverables: raw.deliverables ?? [],
|
|
30669
30718
|
teamspaceDeliverableIndex: raw.teamspaceDeliverableIndex ?? [],
|
|
30670
|
-
|
|
30671
|
-
systemDeliverableIndex: raw.systemDeliverableIndex ?? anyRaw["projectDeliverableIndex"] ?? [],
|
|
30719
|
+
systemDeliverableIndex: raw.systemDeliverableIndex ?? [],
|
|
30672
30720
|
teamspaces: raw.teamspaces ?? [],
|
|
30673
30721
|
teamspaceAccountIndex: raw.teamspaceAccountIndex ?? [],
|
|
30674
30722
|
accountTeamspaceIndex: raw.accountTeamspaceIndex ?? [],
|
|
30675
30723
|
layers: raw.layers ?? [],
|
|
30676
30724
|
layerIndex: raw.layerIndex ?? [],
|
|
30677
30725
|
tracks: raw.tracks ?? [],
|
|
30678
|
-
// Compat: early v4 snapshots (before field rename) have 'trackProjectIndex'
|
|
30679
|
-
trackSystemIndex: raw.trackSystemIndex ?? anyRaw["trackProjectIndex"] ?? [],
|
|
30680
|
-
trackTeamspaceIndex: raw.trackTeamspaceIndex ?? [],
|
|
30681
30726
|
workItems: raw.workItems ?? [],
|
|
30682
30727
|
workItemIndex: raw.workItemIndex ?? [],
|
|
30683
30728
|
accounts: raw.accounts ?? []
|
|
@@ -30769,10 +30814,7 @@ function assertSnapshotShape(val, sourceHint) {
|
|
|
30769
30814
|
|
|
30770
30815
|
// ../../libs/harmonica-services/src/mcp/tools/subscription-tools.ts
|
|
30771
30816
|
var ENTITY_TYPES = ["system", "beat", "revision"];
|
|
30772
|
-
var entityTypeField = external_exports.
|
|
30773
|
-
(v) => v === "project" ? "system" : v,
|
|
30774
|
-
external_exports.enum(ENTITY_TYPES)
|
|
30775
|
-
);
|
|
30817
|
+
var entityTypeField = external_exports.enum(ENTITY_TYPES);
|
|
30776
30818
|
function registerSubscriptionTools(server, ctx, client) {
|
|
30777
30819
|
server.tool(
|
|
30778
30820
|
"subscribe_to_entity",
|
|
@@ -31264,6 +31306,231 @@ function registerTeamspaceTools(server, ctx, client) {
|
|
|
31264
31306
|
);
|
|
31265
31307
|
}
|
|
31266
31308
|
|
|
31309
|
+
// ../../libs/harmonica-services/src/tool-schemas/track.ts
|
|
31310
|
+
var get_track = {
|
|
31311
|
+
trackId: external_exports.string().describe("The Track ID")
|
|
31312
|
+
};
|
|
31313
|
+
var update_track = {
|
|
31314
|
+
trackId: external_exports.string().describe("The Track ID"),
|
|
31315
|
+
title: external_exports.string().min(1).max(300).optional().describe("New title"),
|
|
31316
|
+
description: external_exports.string().max(2e3).optional().describe("New description"),
|
|
31317
|
+
deliverables: external_exports.string().max(2e4).optional().describe(`Replace the Track's deliverables Markdown ("" clears)`),
|
|
31318
|
+
acceptanceCriteria: external_exports.string().max(2e4).optional().describe(`Replace the Track's acceptance-criteria Markdown ("" clears)`),
|
|
31319
|
+
vuRate: external_exports.number().nonnegative().optional().describe("New VU rate")
|
|
31320
|
+
};
|
|
31321
|
+
var transition_track = {
|
|
31322
|
+
trackId: external_exports.string().describe("The Track ID"),
|
|
31323
|
+
targetState: external_exports.enum(["proposed", "approved", "active", "fulfilled", "cancelled"]).describe("Target lifecycle state"),
|
|
31324
|
+
approvalNoteId: external_exports.string().optional().describe(
|
|
31325
|
+
'Required when transitioning to "approved" \u2014 the Note ID capturing the approval decision'
|
|
31326
|
+
),
|
|
31327
|
+
reason: external_exports.string().optional().describe("Reason for the transition (especially useful for cancellation)"),
|
|
31328
|
+
confirm: external_exports.boolean().optional().default(false).describe(
|
|
31329
|
+
"false (default) = dry-run preview; true = execute the transition"
|
|
31330
|
+
)
|
|
31331
|
+
};
|
|
31332
|
+
|
|
31333
|
+
// ../../libs/harmonica-services/src/mcp/tools/track-tools.ts
|
|
31334
|
+
function registerTrackTools(server, ctx, client) {
|
|
31335
|
+
server.tool(
|
|
31336
|
+
"list_movement_tracks",
|
|
31337
|
+
`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.`,
|
|
31338
|
+
{
|
|
31339
|
+
movementId: external_exports.string().describe("The Movement ID (e.g., mv-abc123)")
|
|
31340
|
+
},
|
|
31341
|
+
async ({ movementId }) => {
|
|
31342
|
+
await assertMovementExists(client, movementId);
|
|
31343
|
+
const tracks = await client.listMovementTracks(movementId);
|
|
31344
|
+
const text = tracks.length === 0 ? "No tracks found for this Movement." : tracks.map(
|
|
31345
|
+
(t) => `[${t.trackId}] ${t.trackCode} \u2014 ${t.title} (${t.status})`
|
|
31346
|
+
).join("\n");
|
|
31347
|
+
return { content: [{ type: "text", text }] };
|
|
31348
|
+
}
|
|
31349
|
+
);
|
|
31350
|
+
server.tool(
|
|
31351
|
+
"get_track",
|
|
31352
|
+
"Get a Track by ID. Returns full details including commercial lifecycle status, VU rate, and audit stamps.",
|
|
31353
|
+
get_track,
|
|
31354
|
+
async ({ trackId }) => {
|
|
31355
|
+
const t = await client.getTrack(trackId);
|
|
31356
|
+
if (!t) {
|
|
31357
|
+
return {
|
|
31358
|
+
content: [{ type: "text", text: `Track not found: ${trackId}` }]
|
|
31359
|
+
};
|
|
31360
|
+
}
|
|
31361
|
+
const lines = [
|
|
31362
|
+
`ID: ${t.trackId}`,
|
|
31363
|
+
`Code: ${t.trackCode}`,
|
|
31364
|
+
`Title: ${t.title}`,
|
|
31365
|
+
`Teamspace: ${t.teamspaceId}`,
|
|
31366
|
+
t.movementId ? `Movement: ${t.movementId}` : null,
|
|
31367
|
+
`Status: ${t.status}`,
|
|
31368
|
+
`VU Rate: ${t.vuRate}`,
|
|
31369
|
+
t.description ? `Description: ${t.description}` : null,
|
|
31370
|
+
t.deliverables ? `Deliverables:
|
|
31371
|
+
${t.deliverables}` : null,
|
|
31372
|
+
t.acceptanceCriteria ? `Acceptance Criteria:
|
|
31373
|
+
${t.acceptanceCriteria}` : null,
|
|
31374
|
+
t.parentGroupId ? `Parent Track: ${t.parentGroupId}` : null,
|
|
31375
|
+
t.proposedAt ? `Proposed: ${t.proposedAt}` : null,
|
|
31376
|
+
t.approvedAt ? `Approved: ${t.approvedAt}` : null,
|
|
31377
|
+
`Version: ${t.version}`,
|
|
31378
|
+
`Created: ${t.createdAt}`,
|
|
31379
|
+
`Updated: ${t.updatedAt}`
|
|
31380
|
+
].filter(Boolean).join("\n");
|
|
31381
|
+
return { content: [{ type: "text", text: lines }] };
|
|
31382
|
+
}
|
|
31383
|
+
);
|
|
31384
|
+
const createTrackSchema = {
|
|
31385
|
+
teamspaceId: external_exports.string().describe("The teamspace ID"),
|
|
31386
|
+
title: external_exports.string().min(1).max(300).describe('Track title (e.g. "SOW-2026-Q2 / Platform Modernisation")'),
|
|
31387
|
+
description: external_exports.string().max(2e3).optional().describe("Optional description"),
|
|
31388
|
+
deliverables: external_exports.string().max(2e4).optional().describe("The Track's deliverables as Markdown bullet points (parallel to acceptanceCriteria)"),
|
|
31389
|
+
acceptanceCriteria: external_exports.string().max(2e4).optional().describe("The Track's acceptance criteria (Markdown), parallel to its deliverables"),
|
|
31390
|
+
vuRate: external_exports.number().nonnegative().describe("Value-Unit rate (billing rate for this engagement)"),
|
|
31391
|
+
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)."),
|
|
31392
|
+
parentGroupId: external_exports.string().optional().describe("Optional parent Track ID for nested grouping (Phase 0 scaffolding)")
|
|
31393
|
+
};
|
|
31394
|
+
const createTrackHandler = async ({ teamspaceId, title, description, deliverables, acceptanceCriteria, vuRate, movementId, parentGroupId }) => {
|
|
31395
|
+
if (movementId !== void 0) {
|
|
31396
|
+
await assertMovementExists(client, movementId);
|
|
31397
|
+
}
|
|
31398
|
+
const t = await client.createTrack({
|
|
31399
|
+
teamspaceId,
|
|
31400
|
+
title,
|
|
31401
|
+
...description !== void 0 && { description },
|
|
31402
|
+
...deliverables !== void 0 && { deliverables },
|
|
31403
|
+
...acceptanceCriteria !== void 0 && { acceptanceCriteria },
|
|
31404
|
+
vuRate,
|
|
31405
|
+
...movementId !== void 0 && { movementId },
|
|
31406
|
+
...parentGroupId !== void 0 && { parentGroupId },
|
|
31407
|
+
createdBy: ctx.user.userId
|
|
31408
|
+
});
|
|
31409
|
+
return {
|
|
31410
|
+
content: [
|
|
31411
|
+
{
|
|
31412
|
+
type: "text",
|
|
31413
|
+
text: `Created Track: [${t.trackId}] ${t.trackCode} \u2014 ${t.title} (${t.status})`
|
|
31414
|
+
}
|
|
31415
|
+
]
|
|
31416
|
+
};
|
|
31417
|
+
};
|
|
31418
|
+
server.tool(
|
|
31419
|
+
"create_track",
|
|
31420
|
+
'Create a Track under a Movement. Use for scoped initiatives \u2014 client SOWs, internal phases, engagement containers \u2014 that collect Deliverables and Drops. Do NOT use for software capabilities the product provides to end users (those are Beats). Recognition cues: "this engagement", "this SOW", "this phase of work", "the client project", "this track" \u2192 Track.',
|
|
31421
|
+
createTrackSchema,
|
|
31422
|
+
createTrackHandler
|
|
31423
|
+
);
|
|
31424
|
+
server.tool(
|
|
31425
|
+
"update_track",
|
|
31426
|
+
"Update mutable content fields on a Track (title, description, deliverables, acceptanceCriteria, vuRate). To change commercial status use transition_track instead.",
|
|
31427
|
+
update_track,
|
|
31428
|
+
async ({ trackId, title, description, deliverables, acceptanceCriteria, vuRate }) => {
|
|
31429
|
+
const t = await client.updateTrack(trackId, {
|
|
31430
|
+
...title !== void 0 && { title },
|
|
31431
|
+
...description !== void 0 && { description },
|
|
31432
|
+
...deliverables !== void 0 && { deliverables },
|
|
31433
|
+
...acceptanceCriteria !== void 0 && { acceptanceCriteria },
|
|
31434
|
+
...vuRate !== void 0 && { vuRate }
|
|
31435
|
+
});
|
|
31436
|
+
if (!t) {
|
|
31437
|
+
return {
|
|
31438
|
+
content: [{ type: "text", text: `Track not found: ${trackId}` }]
|
|
31439
|
+
};
|
|
31440
|
+
}
|
|
31441
|
+
return {
|
|
31442
|
+
content: [
|
|
31443
|
+
{
|
|
31444
|
+
type: "text",
|
|
31445
|
+
text: `Updated Track: [${t.trackId}] ${t.trackCode} \u2014 ${t.title} (${t.status})`
|
|
31446
|
+
}
|
|
31447
|
+
]
|
|
31448
|
+
};
|
|
31449
|
+
}
|
|
31450
|
+
);
|
|
31451
|
+
server.tool(
|
|
31452
|
+
"transition_track",
|
|
31453
|
+
'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.',
|
|
31454
|
+
transition_track,
|
|
31455
|
+
async ({ trackId, targetState, approvalNoteId, reason, confirm }) => {
|
|
31456
|
+
if (!confirm) {
|
|
31457
|
+
return {
|
|
31458
|
+
content: [
|
|
31459
|
+
{
|
|
31460
|
+
type: "text",
|
|
31461
|
+
text: `Dry-run: would transition Track ${trackId} \u2192 ${targetState}${approvalNoteId ? ` (approvalNoteId: ${approvalNoteId})` : ""}${reason ? ` \u2014 ${reason}` : ""}. Call again with confirm=true to execute.`
|
|
31462
|
+
}
|
|
31463
|
+
]
|
|
31464
|
+
};
|
|
31465
|
+
}
|
|
31466
|
+
const t = await client.transitionTrackStatus(
|
|
31467
|
+
trackId,
|
|
31468
|
+
targetState,
|
|
31469
|
+
{
|
|
31470
|
+
...approvalNoteId !== void 0 && { approvalNoteId },
|
|
31471
|
+
...reason !== void 0 && { reason }
|
|
31472
|
+
}
|
|
31473
|
+
);
|
|
31474
|
+
if (!t) {
|
|
31475
|
+
return {
|
|
31476
|
+
content: [{ type: "text", text: `Track not found: ${trackId}` }]
|
|
31477
|
+
};
|
|
31478
|
+
}
|
|
31479
|
+
return {
|
|
31480
|
+
content: [
|
|
31481
|
+
{
|
|
31482
|
+
type: "text",
|
|
31483
|
+
text: `Transitioned Track [${t.trackId}] ${t.trackCode} \u2192 ${t.status}`
|
|
31484
|
+
}
|
|
31485
|
+
]
|
|
31486
|
+
};
|
|
31487
|
+
}
|
|
31488
|
+
);
|
|
31489
|
+
server.tool(
|
|
31490
|
+
"assign_drop_to_track",
|
|
31491
|
+
`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".`,
|
|
31492
|
+
{
|
|
31493
|
+
trackId: external_exports.string().describe("The Track ID"),
|
|
31494
|
+
dropId: external_exports.string().describe("The Drop ID to link to the group")
|
|
31495
|
+
},
|
|
31496
|
+
async ({ trackId, dropId }) => {
|
|
31497
|
+
await fetchTrack(client, trackId);
|
|
31498
|
+
const drop = await client.getDrop(dropId);
|
|
31499
|
+
if (!drop) {
|
|
31500
|
+
return { content: [{ type: "text", text: `Drop not found: ${dropId}` }] };
|
|
31501
|
+
}
|
|
31502
|
+
await assertAccountExists(client, drop.accountId);
|
|
31503
|
+
const ok = await client.setDropTrack(trackId, dropId);
|
|
31504
|
+
if (!ok) {
|
|
31505
|
+
return { content: [{ type: "text", text: `Drop not found: ${dropId}` }] };
|
|
31506
|
+
}
|
|
31507
|
+
return {
|
|
31508
|
+
content: [{ type: "text", text: `Linked Drop ${dropId} to Track ${trackId}.` }]
|
|
31509
|
+
};
|
|
31510
|
+
}
|
|
31511
|
+
);
|
|
31512
|
+
server.tool(
|
|
31513
|
+
"list_track_beat_versions",
|
|
31514
|
+
'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?"',
|
|
31515
|
+
{
|
|
31516
|
+
trackId: external_exports.string().describe("The Track ID")
|
|
31517
|
+
},
|
|
31518
|
+
async ({ trackId }) => {
|
|
31519
|
+
await fetchTrack(client, trackId);
|
|
31520
|
+
const workItems = await client.listTrackWorkItems(trackId);
|
|
31521
|
+
const bvIds = [...new Set(
|
|
31522
|
+
workItems.map((wi) => wi.beatVersionId).filter((id) => id !== void 0)
|
|
31523
|
+
)];
|
|
31524
|
+
if (bvIds.length === 0) {
|
|
31525
|
+
return { content: [{ type: "text", text: "No beat versions linked via work items in this track." }] };
|
|
31526
|
+
}
|
|
31527
|
+
const hydrated = await Promise.all(bvIds.map((id) => client.getBeatVersion(id)));
|
|
31528
|
+
const text = hydrated.filter((bv) => bv !== void 0).map((bv) => `[${bv.beatVersionId}] v${bv.versionNumber} \u2014 ${bv.title} (${bv.status})`).join("\n");
|
|
31529
|
+
return { content: [{ type: "text", text }] };
|
|
31530
|
+
}
|
|
31531
|
+
);
|
|
31532
|
+
}
|
|
31533
|
+
|
|
31267
31534
|
// ../../libs/harmonica-services/src/mcp/tools/value-velocity-tools.ts
|
|
31268
31535
|
var lensSchema = external_exports.enum(["velocity", "roi", "valuePrimary"]).default("velocity");
|
|
31269
31536
|
function resolveActorId(ctx) {
|
|
@@ -31642,6 +31909,7 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31642
31909
|
wi.costPerValueUnit !== void 0 ? `Cost per Value Unit: ${wi.costPerValueUnit}` : null,
|
|
31643
31910
|
wi.beatVersionId ? `Linked Beat Version: ${wi.beatVersionId}` : null,
|
|
31644
31911
|
wi.workOrderId ? `Work Order: ${wi.workOrderId}` : "Work Order: none (unattached)",
|
|
31912
|
+
wi.barId ? `Bar: ${wi.barId}` : "Bar: none (unplanned)",
|
|
31645
31913
|
estimateHint ? `Estimate Hint: ${estimateHint.hours}h (source: ${estimateHint.source})` : null,
|
|
31646
31914
|
wi.createdBy ? `Created By: ${wi.createdBy}` : null,
|
|
31647
31915
|
`Created: ${wi.createdAt}`,
|
|
@@ -31652,7 +31920,7 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31652
31920
|
);
|
|
31653
31921
|
server.tool(
|
|
31654
31922
|
"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).',
|
|
31923
|
+
'Create a work item under a Track (DeliverableGroup). A work item is an owned, statused unit of operational work \u2014 it is NOT a Beat. Starts in "not_started" status. Optionally link it to a Beat Version for capability lineage (v2, N-4E09-6574) \u2014 the link is one-directional and never required. Optionally link it to a Work Order it delivers part of; leaving it unattached is a supported state, not a gap to fill (N-4E09-7160). Optionally link it to a Bar it is planned into; leaving it unplanned is likewise a real state, not a gap (B-298).',
|
|
31656
31924
|
{
|
|
31657
31925
|
trackId: external_exports.string().describe("The Track (DeliverableGroup) ID this work item belongs to"),
|
|
31658
31926
|
title: external_exports.string().min(1).max(300).describe("Work item title"),
|
|
@@ -31664,9 +31932,10 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31664
31932
|
costPerValueUnit: external_exports.number().nonnegative().optional().describe("Optional price per Value Unit"),
|
|
31665
31933
|
createdBy: external_exports.string().optional().describe("Optional creator identity for audit"),
|
|
31666
31934
|
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.")
|
|
31935
|
+
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."),
|
|
31936
|
+
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
31937
|
},
|
|
31669
|
-
async ({ trackId, title, ownerName, ownerEmail, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId, workOrderId }) => {
|
|
31938
|
+
async ({ trackId, title, ownerName, ownerEmail, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId, workOrderId, barId }) => {
|
|
31670
31939
|
if (ownerName === void 0 !== (ownerEmail === void 0)) {
|
|
31671
31940
|
return {
|
|
31672
31941
|
content: [{ type: "text", text: "Setting the owner requires both ownerName and ownerEmail. Omit both to create an unowned work item." }],
|
|
@@ -31683,7 +31952,8 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31683
31952
|
...costPerValueUnit !== void 0 && { costPerValueUnit },
|
|
31684
31953
|
...createdBy !== void 0 && { createdBy },
|
|
31685
31954
|
...beatVersionId !== void 0 && { beatVersionId },
|
|
31686
|
-
...workOrderId !== void 0 && { workOrderId }
|
|
31955
|
+
...workOrderId !== void 0 && { workOrderId },
|
|
31956
|
+
...barId !== void 0 && { barId }
|
|
31687
31957
|
});
|
|
31688
31958
|
return {
|
|
31689
31959
|
content: [
|
|
@@ -31697,7 +31967,7 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31697
31967
|
);
|
|
31698
31968
|
server.tool(
|
|
31699
31969
|
"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.",
|
|
31970
|
+
"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
31971
|
{
|
|
31702
31972
|
workItemId: external_exports.string().describe("The work item ID"),
|
|
31703
31973
|
title: external_exports.string().min(1).max(300).optional().describe("New title"),
|
|
@@ -31710,9 +31980,10 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31710
31980
|
valueUnits: external_exports.number().nonnegative().nullable().optional().describe("Value Units (commercial lens). Pass null to clear; omit to leave untouched."),
|
|
31711
31981
|
costPerValueUnit: external_exports.number().nonnegative().nullable().optional().describe("Price per Value Unit. Pass null to clear; omit to leave untouched."),
|
|
31712
31982
|
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.")
|
|
31983
|
+
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."),
|
|
31984
|
+
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
31985
|
},
|
|
31715
|
-
async ({ workItemId, title, ownerName, ownerEmail, unassignOwner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId, workOrderId }) => {
|
|
31986
|
+
async ({ workItemId, title, ownerName, ownerEmail, unassignOwner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId, workOrderId, barId }) => {
|
|
31716
31987
|
const hasOwnerUpdate = ownerName !== void 0 || ownerEmail !== void 0;
|
|
31717
31988
|
if (hasOwnerUpdate && (ownerName === void 0 || ownerEmail === void 0)) {
|
|
31718
31989
|
return {
|
|
@@ -31728,7 +31999,7 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31728
31999
|
}
|
|
31729
32000
|
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
32001
|
// a truthy check here would report "no updates provided" and drop it.
|
|
31731
|
-
workOrderId !== void 0;
|
|
32002
|
+
workOrderId !== void 0 || barId !== void 0;
|
|
31732
32003
|
if (!hasUpdates) {
|
|
31733
32004
|
return {
|
|
31734
32005
|
content: [{ type: "text", text: `No updates provided for work item: ${workItemId}` }],
|
|
@@ -31745,7 +32016,8 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31745
32016
|
...valueUnits !== void 0 && { valueUnits },
|
|
31746
32017
|
...costPerValueUnit !== void 0 && { costPerValueUnit },
|
|
31747
32018
|
...beatVersionId !== void 0 && { beatVersionId },
|
|
31748
|
-
...workOrderId !== void 0 && { workOrderId }
|
|
32019
|
+
...workOrderId !== void 0 && { workOrderId },
|
|
32020
|
+
...barId !== void 0 && { barId }
|
|
31749
32021
|
});
|
|
31750
32022
|
if (!wi) {
|
|
31751
32023
|
return {
|
|
@@ -31810,6 +32082,24 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31810
32082
|
);
|
|
31811
32083
|
}
|
|
31812
32084
|
|
|
32085
|
+
// ../../libs/harmonica-services/src/tool-schemas/work-order.ts
|
|
32086
|
+
var WORK_ORDER_SIZE_ENUM = ["XS", "S", "M", "L", "XL"];
|
|
32087
|
+
var create_work_order = {
|
|
32088
|
+
workRequestId: external_exports.string().min(1).describe("The Work Request this order was split from \u2014 immutable lineage"),
|
|
32089
|
+
title: external_exports.string().min(1).max(300).describe("Work order title"),
|
|
32090
|
+
description: external_exports.string().max(1e4).optional().describe("Optional detail beyond the title"),
|
|
32091
|
+
size: external_exports.enum(WORK_ORDER_SIZE_ENUM).optional().describe(
|
|
32092
|
+
"Optional t-shirt size. An order can arrive before it is sized; a request's composition counts unsized orders rather than hiding them."
|
|
32093
|
+
),
|
|
32094
|
+
createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
|
|
32095
|
+
};
|
|
32096
|
+
var get_work_order = {
|
|
32097
|
+
workOrderId: external_exports.string().describe("The Work Order ID (e.g., wo-abc123)")
|
|
32098
|
+
};
|
|
32099
|
+
var list_movement_work_orders = {
|
|
32100
|
+
movementId: external_exports.string().describe("The Movement (engagement) ID")
|
|
32101
|
+
};
|
|
32102
|
+
|
|
31813
32103
|
// ../../libs/harmonica-services/src/mcp/tools/work-order-tools.ts
|
|
31814
32104
|
var WORK_INTAKE_STATUS_ENUM = [
|
|
31815
32105
|
"requested",
|
|
@@ -31822,7 +32112,7 @@ var NEEDS_DECISION_REASON_ENUM = [
|
|
|
31822
32112
|
"exceeds_capacity",
|
|
31823
32113
|
"incoherent_with_decision"
|
|
31824
32114
|
];
|
|
31825
|
-
var
|
|
32115
|
+
var WORK_ORDER_SIZE_ENUM2 = [
|
|
31826
32116
|
"XS",
|
|
31827
32117
|
"S",
|
|
31828
32118
|
"M",
|
|
@@ -31834,24 +32124,37 @@ function oneLine(value) {
|
|
|
31834
32124
|
}
|
|
31835
32125
|
function formatOrderLine(o) {
|
|
31836
32126
|
const reason = o.needsDecisionReason ? ` [${o.needsDecisionReason}]` : "";
|
|
31837
|
-
return `[${o.workOrderId}] ${oneLine(o.title)} \u2014 ${o.status}${reason}, ${o.size ?? "unsized"}
|
|
32127
|
+
return `[${o.workOrderId}] ${oneLine(o.title)} \u2014 ${o.status}${reason}, ${o.size ?? "unsized"}`;
|
|
31838
32128
|
}
|
|
31839
32129
|
function registerWorkOrderTools(server, _ctx, client) {
|
|
31840
32130
|
server.tool(
|
|
31841
32131
|
"list_track_work_orders",
|
|
31842
|
-
|
|
32132
|
+
"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.",
|
|
32133
|
+
// Optional on purpose: a caller that has already dropped the argument is
|
|
32134
|
+
// exactly the caller this alias exists to greet. Requiring it would answer
|
|
32135
|
+
// the migrated caller with a schema error instead of the migration message.
|
|
31843
32136
|
{
|
|
31844
|
-
trackId: external_exports.string().describe("
|
|
32137
|
+
trackId: external_exports.string().optional().describe("Ignored \u2014 the Track (DeliverableGroup) ID is no longer meaningful for Order scoping")
|
|
31845
32138
|
},
|
|
31846
|
-
async (
|
|
31847
|
-
|
|
31848
|
-
|
|
31849
|
-
|
|
32139
|
+
async () => {
|
|
32140
|
+
return {
|
|
32141
|
+
content: [
|
|
32142
|
+
{
|
|
32143
|
+
type: "text",
|
|
32144
|
+
text: "list_track_work_orders is deprecated. Use list_movement_work_orders(movementId) to fetch all Orders for an engagement \u2014 Orders are now scoped to their Movement, not their Track. If you have only a Track id, its Movement is on the Track itself: get_deliverable_group(trackId) returns movementId."
|
|
32145
|
+
}
|
|
32146
|
+
],
|
|
32147
|
+
// The caller asked for a list and is getting none. A result without
|
|
32148
|
+
// this flag reads as a successful empty listing to any client that
|
|
32149
|
+
// checks the field rather than the prose — the same reason every other
|
|
32150
|
+
// "your call did nothing" return in this file sets it.
|
|
32151
|
+
isError: true
|
|
32152
|
+
};
|
|
31850
32153
|
}
|
|
31851
32154
|
);
|
|
31852
32155
|
server.tool(
|
|
31853
32156
|
"list_work_request_work_orders",
|
|
31854
|
-
"List the Work Orders one Work Request split into \u2014 the lineage view. A four-way split shows four orders here, each
|
|
32157
|
+
"List the Work Orders one Work Request split into \u2014 the lineage view. A four-way split shows four orders here, each with its own Items and independent state.",
|
|
31855
32158
|
{
|
|
31856
32159
|
workRequestId: external_exports.string().describe("The Work Request ID")
|
|
31857
32160
|
},
|
|
@@ -31863,10 +32166,8 @@ function registerWorkOrderTools(server, _ctx, client) {
|
|
|
31863
32166
|
);
|
|
31864
32167
|
server.tool(
|
|
31865
32168
|
"get_work_order",
|
|
31866
|
-
"Get a Work Order by ID. Returns its title, status (with the routing reason when it is awaiting a decision), t-shirt size,
|
|
31867
|
-
|
|
31868
|
-
workOrderId: external_exports.string().describe("The Work Order ID (e.g., wo-abc123)")
|
|
31869
|
-
},
|
|
32169
|
+
"Get a Work Order by ID. Returns its title, status (with the routing reason when it is awaiting a decision), t-shirt size, and the Work Request it was split from.",
|
|
32170
|
+
get_work_order,
|
|
31870
32171
|
async ({ workOrderId }) => {
|
|
31871
32172
|
const wo = await client.getWorkOrder(workOrderId);
|
|
31872
32173
|
if (!wo) {
|
|
@@ -31876,7 +32177,6 @@ function registerWorkOrderTools(server, _ctx, client) {
|
|
|
31876
32177
|
`ID: ${wo.workOrderId}`,
|
|
31877
32178
|
`Title: ${oneLine(wo.title)}`,
|
|
31878
32179
|
`From Work Request: ${wo.workRequestId}`,
|
|
31879
|
-
`Track: ${wo.trackId}`,
|
|
31880
32180
|
`Status: ${wo.status}`,
|
|
31881
32181
|
wo.needsDecisionReason ? `Routing Reason: ${wo.needsDecisionReason}` : null,
|
|
31882
32182
|
wo.size !== void 0 ? `Size: ${wo.size}` : "Size: unsized",
|
|
@@ -31888,39 +32188,45 @@ function registerWorkOrderTools(server, _ctx, client) {
|
|
|
31888
32188
|
return { content: [{ type: "text", text: lines }] };
|
|
31889
32189
|
}
|
|
31890
32190
|
);
|
|
31891
|
-
|
|
31892
|
-
|
|
31893
|
-
|
|
31894
|
-
|
|
31895
|
-
|
|
31896
|
-
|
|
31897
|
-
|
|
31898
|
-
|
|
31899
|
-
|
|
31900
|
-
|
|
31901
|
-
|
|
31902
|
-
|
|
31903
|
-
|
|
31904
|
-
|
|
31905
|
-
|
|
31906
|
-
|
|
31907
|
-
|
|
31908
|
-
|
|
31909
|
-
|
|
31910
|
-
|
|
31911
|
-
|
|
31912
|
-
|
|
31913
|
-
|
|
31914
|
-
|
|
31915
|
-
|
|
32191
|
+
for (const name of ["create_work_order", "promote_work_request"]) {
|
|
32192
|
+
server.tool(
|
|
32193
|
+
name,
|
|
32194
|
+
`Create (promote) a Work Order from a Work Request. Each call creates another Order; it does not accept the Order or change Request status. Called once per piece: a request covering two separate lines of work becomes two orders. Starts in "requested" status \u2014 moving it past the coherence gate is transition_work_order_status. The Order's scope is derived from the Work Request's Movement; Tracks are assigned only to Work Items.`,
|
|
32195
|
+
create_work_order,
|
|
32196
|
+
async ({ workRequestId, title, description, size, createdBy }) => {
|
|
32197
|
+
const wo = await client.createWorkOrder({
|
|
32198
|
+
workRequestId,
|
|
32199
|
+
title,
|
|
32200
|
+
...description !== void 0 && { description },
|
|
32201
|
+
...size !== void 0 && { size },
|
|
32202
|
+
...createdBy !== void 0 && { createdBy }
|
|
32203
|
+
});
|
|
32204
|
+
return {
|
|
32205
|
+
content: [{ type: "text", text: `Created work order: ${formatOrderLine(wo)}` }]
|
|
32206
|
+
};
|
|
32207
|
+
}
|
|
32208
|
+
);
|
|
32209
|
+
}
|
|
32210
|
+
for (const name of ["list_movement_work_orders", "list_work_orders"]) {
|
|
32211
|
+
server.tool(
|
|
32212
|
+
name,
|
|
32213
|
+
'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.',
|
|
32214
|
+
list_movement_work_orders,
|
|
32215
|
+
async ({ movementId }) => {
|
|
32216
|
+
const orders = await client.listMovementWorkOrders(movementId);
|
|
32217
|
+
const text = orders.length === 0 ? "No work orders found." : orders.map(formatOrderLine).join("\n");
|
|
32218
|
+
return { content: [{ type: "text", text }] };
|
|
32219
|
+
}
|
|
32220
|
+
);
|
|
32221
|
+
}
|
|
31916
32222
|
server.tool(
|
|
31917
32223
|
"update_work_order",
|
|
31918
|
-
"Update content fields on a Work Order (title, description, size). To
|
|
32224
|
+
"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
32225
|
{
|
|
31920
32226
|
workOrderId: external_exports.string().describe("The Work Order ID"),
|
|
31921
32227
|
title: external_exports.string().min(1).max(300).optional().describe("New title"),
|
|
31922
32228
|
description: external_exports.string().max(1e4).nullable().optional().describe("New description. Pass null to clear; omit to leave untouched."),
|
|
31923
|
-
size: external_exports.enum(
|
|
32229
|
+
size: external_exports.enum(WORK_ORDER_SIZE_ENUM2).nullable().optional().describe("New t-shirt size. Pass null to return it to unsized; omit to leave untouched.")
|
|
31924
32230
|
},
|
|
31925
32231
|
async ({ workOrderId, title, description, size }) => {
|
|
31926
32232
|
if (title === void 0 && description === void 0 && size === void 0) {
|
|
@@ -31944,18 +32250,26 @@ function registerWorkOrderTools(server, _ctx, client) {
|
|
|
31944
32250
|
);
|
|
31945
32251
|
server.tool(
|
|
31946
32252
|
"move_work_order",
|
|
31947
|
-
"
|
|
32253
|
+
"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.",
|
|
32254
|
+
// Both optional for the same reason as list_track_work_orders: the tool's
|
|
32255
|
+
// only job now is to deliver the migration message, so it must not refuse
|
|
32256
|
+
// the callers who have already stopped sending these.
|
|
31948
32257
|
{
|
|
31949
|
-
workOrderId: external_exports.string().describe("
|
|
31950
|
-
newTrackId: external_exports.string().describe("
|
|
32258
|
+
workOrderId: external_exports.string().optional().describe("Ignored \u2014 a Work Order is no longer moveable between Tracks"),
|
|
32259
|
+
newTrackId: external_exports.string().optional().describe("Ignored \u2014 a Track (DeliverableGroup) is no longer a valid parent for Orders")
|
|
31951
32260
|
},
|
|
31952
|
-
async (
|
|
31953
|
-
const wo = await client.moveWorkOrder(workOrderId, newTrackId);
|
|
31954
|
-
if (!wo) {
|
|
31955
|
-
return { content: [{ type: "text", text: `Work order not found: ${workOrderId}` }] };
|
|
31956
|
-
}
|
|
32261
|
+
async () => {
|
|
31957
32262
|
return {
|
|
31958
|
-
content: [
|
|
32263
|
+
content: [
|
|
32264
|
+
{
|
|
32265
|
+
type: "text",
|
|
32266
|
+
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."
|
|
32267
|
+
}
|
|
32268
|
+
],
|
|
32269
|
+
// "Has no effect" must be machine-readable, not only prose: without
|
|
32270
|
+
// this, a client that moved an Order and checked the result field
|
|
32271
|
+
// records the move as done.
|
|
32272
|
+
isError: true
|
|
31959
32273
|
};
|
|
31960
32274
|
}
|
|
31961
32275
|
);
|
|
@@ -32001,11 +32315,27 @@ function registerWorkOrderTools(server, _ctx, client) {
|
|
|
32001
32315
|
);
|
|
32002
32316
|
}
|
|
32003
32317
|
|
|
32318
|
+
// ../../libs/harmonica-services/src/tool-schemas/work-request.ts
|
|
32319
|
+
var WORK_REQUEST_ORIGIN_ENUM = ["client", "internal"];
|
|
32320
|
+
var create_work_request = {
|
|
32321
|
+
movementId: external_exports.string().describe("The Movement (engagement) this request is filed against"),
|
|
32322
|
+
title: external_exports.string().min(1).max(300).describe("Short title for the ask"),
|
|
32323
|
+
body: external_exports.string().max(1e4).optional().describe("The ask as written, in the requester's own words"),
|
|
32324
|
+
requesterName: external_exports.string().min(1).describe("Who is asking \u2014 display name"),
|
|
32325
|
+
requesterEmail: external_exports.string().email().describe("Who is asking \u2014 email"),
|
|
32326
|
+
origin: external_exports.enum(WORK_REQUEST_ORIGIN_ENUM).describe(
|
|
32327
|
+
'Where the ask came from: "client" or "internal". One entity covers both \u2014 only the entry point differs.'
|
|
32328
|
+
),
|
|
32329
|
+
createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
|
|
32330
|
+
};
|
|
32331
|
+
var get_work_request = {
|
|
32332
|
+
workRequestId: external_exports.string().describe("The Work Request ID (e.g., wr-abc123)")
|
|
32333
|
+
};
|
|
32334
|
+
var list_movement_work_requests = {
|
|
32335
|
+
movementId: external_exports.string().describe("The Movement (engagement) ID")
|
|
32336
|
+
};
|
|
32337
|
+
|
|
32004
32338
|
// ../../libs/harmonica-services/src/mcp/tools/work-request-tools.ts
|
|
32005
|
-
var WORK_REQUEST_ORIGIN_ENUM = [
|
|
32006
|
-
"client",
|
|
32007
|
-
"internal"
|
|
32008
|
-
];
|
|
32009
32339
|
function oneLine2(value) {
|
|
32010
32340
|
return value.replace(/[\r\n]+/g, " ");
|
|
32011
32341
|
}
|
|
@@ -32019,9 +32349,7 @@ function registerWorkRequestTools(server, _ctx, client) {
|
|
|
32019
32349
|
server.tool(
|
|
32020
32350
|
"list_movement_work_requests",
|
|
32021
32351
|
`List the Work Requests filed against a Movement. A Work Request is the customer's ask in their own language, before it is sized or agreed. Recognition cues: "what has the client asked for", "list the requests on this engagement" \u2192 list_movement_work_requests.`,
|
|
32022
|
-
|
|
32023
|
-
movementId: external_exports.string().describe("The Movement (engagement) ID")
|
|
32024
|
-
},
|
|
32352
|
+
list_movement_work_requests,
|
|
32025
32353
|
async ({ movementId }) => {
|
|
32026
32354
|
const requests = await client.listMovementWorkRequests(movementId);
|
|
32027
32355
|
const text = requests.length === 0 ? "No work requests found." : requests.map((wr) => `[${wr.workRequestId}] ${oneLine2(wr.title)} \u2014 from ${oneLine2(wr.requester.name)} (${wr.origin})`).join("\n");
|
|
@@ -32031,9 +32359,7 @@ function registerWorkRequestTools(server, _ctx, client) {
|
|
|
32031
32359
|
server.tool(
|
|
32032
32360
|
"get_work_request",
|
|
32033
32361
|
"Get a Work Request with the Work Orders it split into and their combined size composition. Note there is no request-level status: each Order carries its own, because a split whose halves sit in different states cannot be reduced to one word.",
|
|
32034
|
-
|
|
32035
|
-
workRequestId: external_exports.string().describe("The Work Request ID (e.g., wr-abc123)")
|
|
32036
|
-
},
|
|
32362
|
+
get_work_request,
|
|
32037
32363
|
async ({ workRequestId }) => {
|
|
32038
32364
|
const detail = await client.getWorkRequestDetail(workRequestId);
|
|
32039
32365
|
if (!detail) {
|
|
@@ -32043,10 +32369,9 @@ function registerWorkRequestTools(server, _ctx, client) {
|
|
|
32043
32369
|
const orderLines = orders.length === 0 ? ["Orders: none yet \u2014 this request has not been split"] : [
|
|
32044
32370
|
"Orders:",
|
|
32045
32371
|
...orders.map((o) => {
|
|
32046
|
-
const track = o.trackTitle ? `${oneLine2(o.trackTitle)} (${o.trackId})` : o.trackId;
|
|
32047
32372
|
const reason = o.needsDecisionReason ? ` [${o.needsDecisionReason}]` : "";
|
|
32048
32373
|
const sized = o.size ?? "unsized";
|
|
32049
|
-
return ` [${o.workOrderId}] ${oneLine2(o.title)} \u2014 ${o.status}${reason}, ${sized}
|
|
32374
|
+
return ` [${o.workOrderId}] ${oneLine2(o.title)} \u2014 ${o.status}${reason}, ${sized}`;
|
|
32050
32375
|
})
|
|
32051
32376
|
];
|
|
32052
32377
|
const lines = [
|
|
@@ -32068,15 +32393,7 @@ function registerWorkRequestTools(server, _ctx, client) {
|
|
|
32068
32393
|
server.tool(
|
|
32069
32394
|
"create_work_request",
|
|
32070
32395
|
"File a Work Request against a Movement \u2014 how work arrives, in the requester's own language, before it is sized or agreed. Splitting it into Work Orders is a separate step (create_work_order).",
|
|
32071
|
-
|
|
32072
|
-
movementId: external_exports.string().describe("The Movement (engagement) this request is filed against"),
|
|
32073
|
-
title: external_exports.string().min(1).max(300).describe("Short title for the ask"),
|
|
32074
|
-
body: external_exports.string().max(1e4).optional().describe("The ask as written, in the requester's own words"),
|
|
32075
|
-
requesterName: external_exports.string().min(1).describe("Who is asking \u2014 display name"),
|
|
32076
|
-
requesterEmail: external_exports.string().email().describe("Who is asking \u2014 email"),
|
|
32077
|
-
origin: external_exports.enum(WORK_REQUEST_ORIGIN_ENUM).describe('Where the ask came from: "client" or "internal". One entity covers both \u2014 only the entry point differs.'),
|
|
32078
|
-
createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
|
|
32079
|
-
},
|
|
32396
|
+
create_work_request,
|
|
32080
32397
|
async ({ movementId, title, body, requesterName, requesterEmail, origin, createdBy }) => {
|
|
32081
32398
|
const wr = await client.createWorkRequest({
|
|
32082
32399
|
movementId,
|
|
@@ -32634,13 +32951,14 @@ function registerAllTools(server, ctx, client, profile) {
|
|
|
32634
32951
|
registerNextActionsTools(server, ctx, client);
|
|
32635
32952
|
registerCheckTools(server, ctx, client);
|
|
32636
32953
|
registerActivityTools(server, ctx, client);
|
|
32954
|
+
registerProgressEvidenceTools(server, ctx, client);
|
|
32637
32955
|
registerAuditIntegrityTools(server, ctx, client);
|
|
32638
32956
|
registerAnalysisTools(server, ctx, client);
|
|
32639
32957
|
registerOrganizationTools(server, ctx, client);
|
|
32640
32958
|
registerSubscriptionTools(server, ctx, client);
|
|
32641
32959
|
registerBaselineTools(server, ctx, client);
|
|
32642
32960
|
registerTeamspaceTools(server, ctx, client);
|
|
32643
|
-
|
|
32961
|
+
registerTrackTools(server, ctx, client);
|
|
32644
32962
|
registerDropTools(server, ctx, client);
|
|
32645
32963
|
registerSessionTools(server, ctx, client);
|
|
32646
32964
|
registerOnboardingTools(server, ctx, client);
|
|
@@ -32648,6 +32966,7 @@ function registerAllTools(server, ctx, client, profile) {
|
|
|
32648
32966
|
registerNotebookTools(server, ctx, client);
|
|
32649
32967
|
registerAccountTools(server, ctx, client);
|
|
32650
32968
|
registerMovementTools(server, ctx, client);
|
|
32969
|
+
registerExternalMappingTools(server, ctx, client);
|
|
32651
32970
|
registerWorkItemTools(server, ctx, client);
|
|
32652
32971
|
registerWorkRequestTools(server, ctx, client);
|
|
32653
32972
|
registerWorkOrderTools(server, ctx, client);
|
|
@@ -32762,6 +33081,13 @@ var ApiError = class extends Error {
|
|
|
32762
33081
|
this.name = "ApiError";
|
|
32763
33082
|
}
|
|
32764
33083
|
};
|
|
33084
|
+
var ExternalMappingSubjectNotFoundError = class extends Error {
|
|
33085
|
+
code = EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE;
|
|
33086
|
+
constructor(subjectType, subjectId) {
|
|
33087
|
+
super(`Subject ${subjectType} ${subjectId} not found`);
|
|
33088
|
+
this.name = "ExternalMappingSubjectNotFoundError";
|
|
33089
|
+
}
|
|
33090
|
+
};
|
|
32765
33091
|
function bodyCode(body) {
|
|
32766
33092
|
try {
|
|
32767
33093
|
const parsed = JSON.parse(body);
|
|
@@ -34244,60 +34570,46 @@ function createHttpClient(config2) {
|
|
|
34244
34570
|
updateTeamspace: async (teamspaceId, updates) => {
|
|
34245
34571
|
return request("PATCH", `/api/teamspaces/${encodeURIComponent(teamspaceId)}`, updates);
|
|
34246
34572
|
},
|
|
34247
|
-
//
|
|
34248
|
-
|
|
34249
|
-
const res = await request("GET", `/api/
|
|
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`);
|
|
34573
|
+
// Tracks
|
|
34574
|
+
listMovementTracks: async (movementId) => {
|
|
34575
|
+
const res = await request("GET", `/api/movements/${encodeURIComponent(movementId)}/tracks`);
|
|
34260
34576
|
if (res === void 0) throw new Error(`Movement ${movementId} not found`);
|
|
34261
|
-
return res.
|
|
34577
|
+
return res.tracks;
|
|
34262
34578
|
},
|
|
34263
|
-
|
|
34264
|
-
return request("GET", `/api/
|
|
34579
|
+
getTrack: async (trackId) => {
|
|
34580
|
+
return request("GET", `/api/tracks/${encodeURIComponent(trackId)}`);
|
|
34265
34581
|
},
|
|
34266
|
-
|
|
34267
|
-
const { title, description, deliverables, acceptanceCriteria, vuRate, movementId, parentGroupId, createdBy } = input;
|
|
34582
|
+
createTrack: async (input) => {
|
|
34583
|
+
const { title, description, deliverables, acceptanceCriteria, vuRate, movementId, parentGroupId, createdBy, teamspaceId } = input;
|
|
34268
34584
|
const body = {
|
|
34269
34585
|
title,
|
|
34270
34586
|
...description !== void 0 && { description },
|
|
34271
34587
|
...deliverables !== void 0 && { deliverables },
|
|
34272
34588
|
...acceptanceCriteria !== void 0 && { acceptanceCriteria },
|
|
34273
34589
|
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
34590
|
...movementId !== void 0 && { movementId },
|
|
34277
34591
|
...parentGroupId !== void 0 && { parentGroupId },
|
|
34278
34592
|
...createdBy !== void 0 && { createdBy }
|
|
34279
34593
|
};
|
|
34280
|
-
|
|
34281
|
-
|
|
34594
|
+
if (!movementId) {
|
|
34595
|
+
throw new Error(`Cannot create Track "${title}": a movementId is required (teamspaceId ${teamspaceId} alone has no create route).`);
|
|
34596
|
+
}
|
|
34597
|
+
const url2 = `/api/movements/${encodeURIComponent(movementId)}/tracks`;
|
|
34282
34598
|
const result = await request("POST", url2, body);
|
|
34283
|
-
if (result === void 0) throw new Error(`Resource not found when creating
|
|
34599
|
+
if (result === void 0) throw new Error(`Resource not found when creating Track`);
|
|
34284
34600
|
return result;
|
|
34285
34601
|
},
|
|
34286
|
-
|
|
34287
|
-
return request("PATCH", `/api/
|
|
34602
|
+
updateTrack: async (trackId, updates) => {
|
|
34603
|
+
return request("PATCH", `/api/tracks/${encodeURIComponent(trackId)}`, updates);
|
|
34288
34604
|
},
|
|
34289
|
-
|
|
34290
|
-
return request("PATCH", `/api/
|
|
34605
|
+
transitionTrackStatus: async (trackId, targetState, opts) => {
|
|
34606
|
+
return request("PATCH", `/api/tracks/${encodeURIComponent(trackId)}/status`, {
|
|
34291
34607
|
targetState,
|
|
34292
34608
|
...opts ?? {}
|
|
34293
34609
|
});
|
|
34294
34610
|
},
|
|
34295
|
-
|
|
34296
|
-
const res = await request("
|
|
34297
|
-
return res?.drops ?? [];
|
|
34298
|
-
},
|
|
34299
|
-
setDropDeliverableGroup: async (deliverableGroupId, dropId) => {
|
|
34300
|
-
const res = await request("POST", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/drops`, { dropId });
|
|
34611
|
+
setDropTrack: async (trackId, dropId) => {
|
|
34612
|
+
const res = await request("POST", `/api/tracks/${encodeURIComponent(trackId)}/drops`, { dropId });
|
|
34301
34613
|
return res?.success === true;
|
|
34302
34614
|
},
|
|
34303
34615
|
// Movements (B-242 / teamspace-scoped SOW container)
|
|
@@ -34305,10 +34617,13 @@ function createHttpClient(config2) {
|
|
|
34305
34617
|
const { teamspaceId, title, description, accountId } = input;
|
|
34306
34618
|
const body = {
|
|
34307
34619
|
title,
|
|
34308
|
-
...description !== void 0 && { description }
|
|
34309
|
-
...accountId !== void 0 && { accountId }
|
|
34620
|
+
...description !== void 0 && { description }
|
|
34310
34621
|
};
|
|
34311
|
-
const result = await request(
|
|
34622
|
+
const result = await request(
|
|
34623
|
+
"POST",
|
|
34624
|
+
`/api/teamspaces/${encodeURIComponent(teamspaceId)}/accounts/${encodeURIComponent(accountId)}/movements`,
|
|
34625
|
+
body
|
|
34626
|
+
);
|
|
34312
34627
|
if (result === void 0) throw new Error("Resource not found when creating Movement");
|
|
34313
34628
|
const parsed = MovementApiSchema.safeParse(result);
|
|
34314
34629
|
if (!parsed.success) throw new Error(`Unexpected Movement shape from API: ${parsed.error.message}`);
|
|
@@ -34345,9 +34660,55 @@ function createHttpClient(config2) {
|
|
|
34345
34660
|
if (!parsed.success) throw new Error(`Unexpected movements list shape from API: ${parsed.error.message}`);
|
|
34346
34661
|
return parsed.data.movements;
|
|
34347
34662
|
},
|
|
34663
|
+
// ExternalMappings (reference-only links to external trackers — B-301 / N-4E09-6311)
|
|
34664
|
+
addExternalMapping: async (input) => {
|
|
34665
|
+
const { subjectType, subjectId, provider, externalKey, url: url2, syncDirection } = input;
|
|
34666
|
+
const body = {
|
|
34667
|
+
subjectType,
|
|
34668
|
+
subjectId,
|
|
34669
|
+
provider,
|
|
34670
|
+
externalKey,
|
|
34671
|
+
...url2 !== void 0 && { url: url2 },
|
|
34672
|
+
...syncDirection !== void 0 && { syncDirection }
|
|
34673
|
+
};
|
|
34674
|
+
let res;
|
|
34675
|
+
try {
|
|
34676
|
+
res = await request("POST", `/api/external-mappings`, body);
|
|
34677
|
+
} catch (err) {
|
|
34678
|
+
if (err instanceof ApiError && err.status === 400 && bodyCode(err.body) === EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE) {
|
|
34679
|
+
throw new ExternalMappingSubjectNotFoundError(subjectType, subjectId);
|
|
34680
|
+
}
|
|
34681
|
+
throw err;
|
|
34682
|
+
}
|
|
34683
|
+
if (res === void 0) throw new ExternalMappingSubjectNotFoundError(subjectType, subjectId);
|
|
34684
|
+
const parsed = ExternalMappingApiSchema.safeParse(res.externalMapping);
|
|
34685
|
+
if (!parsed.success) throw new Error(`Unexpected ExternalMapping shape from API: ${parsed.error.message}`);
|
|
34686
|
+
return parsed.data;
|
|
34687
|
+
},
|
|
34688
|
+
updateExternalMapping: async (externalMappingId, updates) => {
|
|
34689
|
+
const res = await request(
|
|
34690
|
+
"PATCH",
|
|
34691
|
+
`/api/external-mappings/${encodeURIComponent(externalMappingId)}`,
|
|
34692
|
+
updates
|
|
34693
|
+
);
|
|
34694
|
+
if (res === void 0) return void 0;
|
|
34695
|
+
const parsed = ExternalMappingApiSchema.safeParse(res.externalMapping);
|
|
34696
|
+
if (!parsed.success) throw new Error(`Unexpected ExternalMapping shape from API: ${parsed.error.message}`);
|
|
34697
|
+
return parsed.data;
|
|
34698
|
+
},
|
|
34699
|
+
listExternalMappings: async (subjectType, subjectId) => {
|
|
34700
|
+
const res = await request(
|
|
34701
|
+
"GET",
|
|
34702
|
+
`/api/external-mappings?subjectType=${encodeURIComponent(subjectType)}&subjectId=${encodeURIComponent(subjectId)}`
|
|
34703
|
+
);
|
|
34704
|
+
if (res === void 0) throw new ExternalMappingSubjectNotFoundError(subjectType, subjectId);
|
|
34705
|
+
const parsed = ExternalMappingsResponseSchema.safeParse(res);
|
|
34706
|
+
if (!parsed.success) throw new Error(`Unexpected ExternalMappings list shape from API: ${parsed.error.message}`);
|
|
34707
|
+
return parsed.data.externalMappings;
|
|
34708
|
+
},
|
|
34348
34709
|
// WorkItems (WorkItem exposure layer — owned, statused unit of operational work under a Track)
|
|
34349
34710
|
createWorkItem: async (input) => {
|
|
34350
|
-
const { trackId, title, owner, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId, workOrderId } = input;
|
|
34711
|
+
const { trackId, title, owner, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId, workOrderId, barId } = input;
|
|
34351
34712
|
const body = {
|
|
34352
34713
|
title,
|
|
34353
34714
|
...owner !== void 0 && { owner },
|
|
@@ -34357,11 +34718,12 @@ function createHttpClient(config2) {
|
|
|
34357
34718
|
...costPerValueUnit !== void 0 && { costPerValueUnit },
|
|
34358
34719
|
...createdBy !== void 0 && { createdBy },
|
|
34359
34720
|
...beatVersionId !== void 0 && { beatVersionId },
|
|
34360
|
-
...workOrderId !== void 0 && { workOrderId }
|
|
34721
|
+
...workOrderId !== void 0 && { workOrderId },
|
|
34722
|
+
...barId !== void 0 && { barId }
|
|
34361
34723
|
};
|
|
34362
34724
|
const res = await request(
|
|
34363
34725
|
"POST",
|
|
34364
|
-
`/api/
|
|
34726
|
+
`/api/tracks/${encodeURIComponent(trackId)}/work-items`,
|
|
34365
34727
|
body
|
|
34366
34728
|
);
|
|
34367
34729
|
if (res === void 0) throw new Error(`Track ${trackId} not found`);
|
|
@@ -34391,7 +34753,7 @@ function createHttpClient(config2) {
|
|
|
34391
34753
|
return parsed.data;
|
|
34392
34754
|
},
|
|
34393
34755
|
listTrackWorkItems: async (trackId) => {
|
|
34394
|
-
const res = await request("GET", `/api/
|
|
34756
|
+
const res = await request("GET", `/api/tracks/${encodeURIComponent(trackId)}/work-items`);
|
|
34395
34757
|
if (res === void 0) throw new Error(`Track ${trackId} not found`);
|
|
34396
34758
|
const parsed = WorkItemsResponseSchema.safeParse(res);
|
|
34397
34759
|
if (!parsed.success) throw new Error(`Unexpected work items list shape from API: ${parsed.error.message}`);
|
|
@@ -34475,9 +34837,8 @@ function createHttpClient(config2) {
|
|
|
34475
34837
|
return parsed.data.workRequests;
|
|
34476
34838
|
},
|
|
34477
34839
|
createWorkOrder: async (input) => {
|
|
34478
|
-
const {
|
|
34840
|
+
const { workRequestId, title, description, size, createdBy } = input;
|
|
34479
34841
|
const body = {
|
|
34480
|
-
workRequestId,
|
|
34481
34842
|
title,
|
|
34482
34843
|
...description !== void 0 && { description },
|
|
34483
34844
|
...size !== void 0 && { size },
|
|
@@ -34485,10 +34846,10 @@ function createHttpClient(config2) {
|
|
|
34485
34846
|
};
|
|
34486
34847
|
const res = await request(
|
|
34487
34848
|
"POST",
|
|
34488
|
-
`/api/
|
|
34849
|
+
`/api/work-requests/${encodeURIComponent(workRequestId)}/work-orders`,
|
|
34489
34850
|
body
|
|
34490
34851
|
);
|
|
34491
|
-
if (res === void 0) throw new Error(`
|
|
34852
|
+
if (res === void 0) throw new Error(`Work request ${workRequestId} not found`);
|
|
34492
34853
|
const parsed = WorkOrderApiSchema.safeParse(res.workOrder);
|
|
34493
34854
|
if (!parsed.success) throw new Error(`Unexpected WorkOrder shape from API: ${parsed.error.message}`);
|
|
34494
34855
|
return parsed.data;
|
|
@@ -34511,8 +34872,15 @@ function createHttpClient(config2) {
|
|
|
34511
34872
|
if (!parsed.success) throw new Error(`Unexpected WorkOrder shape from API: ${parsed.error.message}`);
|
|
34512
34873
|
return parsed.data;
|
|
34513
34874
|
},
|
|
34875
|
+
listMovementWorkOrders: async (movementId) => {
|
|
34876
|
+
const res = await request("GET", `/api/movements/${encodeURIComponent(movementId)}/work-orders`);
|
|
34877
|
+
if (res === void 0) throw new Error(`Movement ${movementId} not found`);
|
|
34878
|
+
const parsed = WorkOrdersResponseSchema.safeParse(res);
|
|
34879
|
+
if (!parsed.success) throw new Error(`Unexpected work orders list shape from API: ${parsed.error.message}`);
|
|
34880
|
+
return parsed.data.workOrders;
|
|
34881
|
+
},
|
|
34514
34882
|
listTrackWorkOrders: async (trackId) => {
|
|
34515
|
-
const res = await request("GET", `/api/
|
|
34883
|
+
const res = await request("GET", `/api/tracks/${encodeURIComponent(trackId)}/work-orders`);
|
|
34516
34884
|
if (res === void 0) throw new Error(`Track ${trackId} not found`);
|
|
34517
34885
|
const parsed = WorkOrdersResponseSchema.safeParse(res);
|
|
34518
34886
|
if (!parsed.success) throw new Error(`Unexpected work orders list shape from API: ${parsed.error.message}`);
|
|
@@ -34665,6 +35033,11 @@ function createHttpClient(config2) {
|
|
|
34665
35033
|
const qs = asOf ? `?asOf=${encodeURIComponent(asOf)}` : "";
|
|
34666
35034
|
return request("GET", `/api/accounts/${encodeURIComponent(accountId)}/cadence-schedule${qs}`);
|
|
34667
35035
|
},
|
|
35036
|
+
getProgressEvidence: async (systemId, query) => {
|
|
35037
|
+
const qs = new URLSearchParams();
|
|
35038
|
+
for (const [key, value] of Object.entries(query)) if (value !== void 0) qs.set(key, String(value));
|
|
35039
|
+
return request("GET", `/api/systems/${encodeURIComponent(systemId)}/progress-evidence?${qs}`);
|
|
35040
|
+
},
|
|
34668
35041
|
getPulseReport: async (accountId, asOf) => {
|
|
34669
35042
|
const qs = asOf ? `?asOf=${encodeURIComponent(asOf)}` : "";
|
|
34670
35043
|
return request("GET", `/api/accounts/${encodeURIComponent(accountId)}/pulse-report${qs}`);
|
|
@@ -35098,7 +35471,7 @@ function loadConfig() {
|
|
|
35098
35471
|
};
|
|
35099
35472
|
}
|
|
35100
35473
|
async function main() {
|
|
35101
|
-
console.error(`[harmonica-mcp] v${"
|
|
35474
|
+
console.error(`[harmonica-mcp] v${"5.1.0"} starting\u2026`);
|
|
35102
35475
|
const config2 = loadConfig();
|
|
35103
35476
|
const client = createHttpClient({
|
|
35104
35477
|
apiBaseUrl: config2.apiBaseUrl,
|