@codazen/harmonica-mcp 2.0.0 → 2.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.
Files changed (2) hide show
  1. package/dist/index.js +495 -352
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22598,7 +22598,7 @@ ${beat.description}`);
22598
22598
  return sections.join("\n");
22599
22599
  }
22600
22600
 
22601
- // ../../libs/harmonica-services/src/mcp/formatters/project-formatter.ts
22601
+ // ../../libs/harmonica-services/src/mcp/formatters/system-formatter.ts
22602
22602
  function formatProjectSummaryTable(projects) {
22603
22603
  if (projects.length === 0) return "_No projects found._";
22604
22604
  const header = "| Project ID | Title | Status |\n|------------|-------|--------|";
@@ -23018,7 +23018,7 @@ function registerBeatTools(server, ctx, client) {
23018
23018
  },
23019
23019
  async ({ projectId, offset = 0, limit = 100, includeArchived }) => {
23020
23020
  await assertProjectInOrg(client, projectId, ctx.orgId);
23021
- let beats = await client.listProjectBeats(projectId);
23021
+ let beats = await client.listSystemBeats(projectId);
23022
23022
  if (!includeArchived) {
23023
23023
  beats = beats.filter((b) => b.beatStatus !== "archived");
23024
23024
  }
@@ -23803,7 +23803,7 @@ function registerCheckTools(server, ctx, client) {
23803
23803
  checks = await client.listBeatChecks(beatId, projectId, checkType);
23804
23804
  scope = `beat ${beatId}`;
23805
23805
  } else {
23806
- const result = await client.listProjectChecks(projectId, checkType, { limit: 200 });
23806
+ const result = await client.listSystemChecks(projectId, checkType, { limit: 200 });
23807
23807
  checks = result.checks;
23808
23808
  scope = `project ${projectId}`;
23809
23809
  }
@@ -26759,6 +26759,14 @@ var MeasureApiSchema = external_exports.object({
26759
26759
  var MeasuresResponseSchema = external_exports.object({ measures: external_exports.array(MeasureApiSchema) }).passthrough();
26760
26760
  var MeasureResponseSchema = external_exports.object({ measure: MeasureApiSchema }).passthrough();
26761
26761
  var WorkItemStatusSchema = external_exports.enum(["not_started", "in_progress", "blocked", "done"]);
26762
+ var WORK_ITEM_EFFORT_SIZES = [
26763
+ "XS",
26764
+ "S",
26765
+ "M",
26766
+ "L",
26767
+ "XL"
26768
+ ];
26769
+ var WorkItemEffortSizeSchema = external_exports.enum(WORK_ITEM_EFFORT_SIZES);
26762
26770
  var HumanAssigneeApiSchema = external_exports.object({
26763
26771
  name: external_exports.string(),
26764
26772
  email: external_exports.string()
@@ -26769,10 +26777,16 @@ var WorkItemApiSchema = external_exports.object({
26769
26777
  teamspaceId: external_exports.string(),
26770
26778
  orgId: external_exports.string(),
26771
26779
  title: external_exports.string(),
26772
- owner: HumanAssigneeApiSchema,
26780
+ /** Optional since N-4E09-6672 — a work item may be unowned. */
26781
+ owner: HumanAssigneeApiSchema.optional(),
26773
26782
  status: WorkItemStatusSchema,
26774
26783
  committedEstimateHours: external_exports.number().optional(),
26775
26784
  actualHours: external_exports.number().optional(),
26785
+ /** Delivery lens — rough t-shirt effort sizing (N-4E09-6672). */
26786
+ effortSize: WorkItemEffortSizeSchema.optional(),
26787
+ /** Commercial lens — deliberately unblended with `effortSize` (N-4E09-6672). */
26788
+ valueUnits: external_exports.number().optional(),
26789
+ costPerValueUnit: external_exports.number().optional(),
26776
26790
  /** Optional capability-lineage link to a Beat Version (v2, N-4E09-6574). */
26777
26791
  beatVersionId: external_exports.string().optional(),
26778
26792
  createdBy: external_exports.string().optional(),
@@ -28882,7 +28896,7 @@ function registerPortfolioCoherenceTools(server, ctx, client) {
28882
28896
  isError: true
28883
28897
  };
28884
28898
  }
28885
- const allProjectBeats = await client.listProjectBeats(projectId);
28899
+ const allProjectBeats = await client.listSystemBeats(projectId);
28886
28900
  const beatMap = new Map(allProjectBeats.map((b) => [b.beatId, b]));
28887
28901
  const primaryBeat = beatMap.get(resolvedPrimaryId);
28888
28902
  if (!primaryBeat) {
@@ -28967,269 +28981,6 @@ function registerPortfolioCoherenceTools(server, ctx, client) {
28967
28981
  );
28968
28982
  }
28969
28983
 
28970
- // ../../libs/harmonica-services/src/mcp/tools/project-lifecycle-tools.ts
28971
- function registerProjectLifecycleTools(server, ctx, client) {
28972
- server.tool(
28973
- "transition_system_lifecycle",
28974
- "Advance a System's capability-maturity lifecycle state. Allowed transitions follow the state machine (Concept \u2192 Incubating \u2192 Piloting \u2192 Activated \u2192 Commercializing \u2192 Scaled, plus paused/killed/sunset/archived exits). The activated \u2192 commercializing edge requires a `decisionNoteId` pointing to a system-scoped Decision Note (the governance review). Other gates are advisory.",
28975
- {
28976
- systemId: external_exports.string().describe("The system ID"),
28977
- targetState: external_exports.enum([
28978
- "concept",
28979
- "incubating",
28980
- "piloting",
28981
- "activated",
28982
- "commercializing",
28983
- "scaled",
28984
- "paused",
28985
- "killed",
28986
- "sunset",
28987
- "archived"
28988
- ]).describe("Target lifecycle state. Active: concept, incubating, piloting, activated, commercializing, scaled. Exits: paused, killed, sunset, archived."),
28989
- reason: external_exports.string().optional().describe("Why this transition is being made (recorded in audit metadata)"),
28990
- decisionNoteId: external_exports.string().optional().describe("Note ID of the governance Decision Note. Required for the activated \u2192 commercializing transition; optional otherwise.")
28991
- },
28992
- async ({ systemId, targetState, reason, decisionNoteId }) => {
28993
- try {
28994
- const system = await client.getSystem(systemId);
28995
- if (!system) {
28996
- return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
28997
- }
28998
- if (system.orgId !== ctx.orgId) {
28999
- return { content: [{ type: "text", text: `System "${systemId}" is not in this organization` }], isError: true };
29000
- }
29001
- const result = await client.transitionProjectLifecycleState(systemId, targetState, {
29002
- actor: { type: "human", id: ctx.user.userId, name: ctx.user.name },
29003
- reason,
29004
- decisionNoteId
29005
- });
29006
- if (!result.success) {
29007
- const failedGates = result.error?.failedGates?.length ? ` (failed gates: ${result.error.failedGates.join(", ")})` : "";
29008
- return {
29009
- content: [{ type: "text", text: `Transition failed: ${result.error?.message ?? "Unknown error"}${failedGates}` }],
29010
- isError: true
29011
- };
29012
- }
29013
- const lines = [
29014
- `System lifecycle transitioned successfully.`,
29015
- "",
29016
- `**System:** ${systemId}`,
29017
- `**Title:** ${system.title}`,
29018
- `**Transition:** ${result.previousState} \u2192 ${result.newState}`
29019
- ];
29020
- if (reason) lines.push(`**Reason:** ${reason}`);
29021
- if (decisionNoteId) lines.push(`**Decision Note:** ${decisionNoteId}`);
29022
- return { content: [{ type: "text", text: lines.join("\n") }] };
29023
- } catch (err) {
29024
- const message = err instanceof Error ? err.message : String(err);
29025
- return { content: [{ type: "text", text: `Failed to transition system lifecycle: ${message}` }], isError: true };
29026
- }
29027
- }
29028
- );
29029
- }
29030
-
29031
- // ../../libs/harmonica-services/src/mcp/tools/project-tools.ts
29032
- var import_crypto5 = require("crypto");
29033
- function accountLine(request, resolved) {
29034
- const requested = request["accountId"];
29035
- if (requested === null || requested === "") return "**Account:** (unlinked)";
29036
- return resolved ? `**Account:** ${resolved}` : "";
29037
- }
29038
- var PROJECT_EMBEDDING_FIELDS = ["title", "description", "strategy"];
29039
- function registerProjectTools(server, ctx, client) {
29040
- const listSystemsHandler = async ({ teamspaceId }) => {
29041
- try {
29042
- const projects = await client.listOrgSystems(ctx.orgId);
29043
- const trimmed = teamspaceId?.trim();
29044
- const filtered = trimmed ? projects.filter((p) => p.teamspaceId === trimmed) : projects;
29045
- const text = formatProjectSummaryTable(filtered);
29046
- return { content: [{ type: "text", text }] };
29047
- } catch (err) {
29048
- const message = err instanceof Error ? err.message : String(err);
29049
- return { content: [{ type: "text", text: `Failed to list systems: ${message}` }], isError: true };
29050
- }
29051
- };
29052
- server.tool(
29053
- "list_systems",
29054
- "List all systems (projects) in the configured organization",
29055
- {
29056
- teamspaceId: external_exports.string().optional().describe("Filter to systems belonging to a specific teamspace. Empty or whitespace-only treated as no filter.")
29057
- },
29058
- listSystemsHandler
29059
- );
29060
- const getSystemContextSchema = {
29061
- systemId: external_exports.string().describe("The system ID"),
29062
- noteLimit: external_exports.number().int().min(1).max(MAX_CONTEXT_NOTE_LIMIT).optional().describe(
29063
- `Max Notes to include, prioritised by note type (default ${DEFAULT_CONTEXT_NOTE_LIMIT}, max ${MAX_CONTEXT_NOTE_LIMIT}). Use list_notes or search for the full set.`
29064
- )
29065
- };
29066
- const getSystemContextHandler = async ({
29067
- systemId,
29068
- noteLimit
29069
- }) => {
29070
- try {
29071
- const [project, org] = await Promise.all([
29072
- fetchProjectInOrg(client, systemId, ctx.orgId),
29073
- client.getOrg(ctx.orgId)
29074
- ]);
29075
- const notes = await client.listProjectNotes(systemId, {
29076
- limit: noteLimit ?? DEFAULT_CONTEXT_NOTE_LIMIT
29077
- });
29078
- const text = formatProjectContext(project, notes, org?.coda);
29079
- return { content: [{ type: "text", text }] };
29080
- } catch (err) {
29081
- const message = err instanceof Error ? err.message : String(err);
29082
- return { content: [{ type: "text", text: `Failed to get system context: ${message}` }], isError: true };
29083
- }
29084
- };
29085
- server.tool(
29086
- "get_system_context",
29087
- "Get system metadata, description, and notes in one view",
29088
- getSystemContextSchema,
29089
- getSystemContextHandler
29090
- );
29091
- const updateSystemSchema = {
29092
- systemId: external_exports.string().describe("The system ID"),
29093
- title: external_exports.string().optional().describe("New system title"),
29094
- description: external_exports.string().optional().describe("New system description"),
29095
- strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
29096
- teamspaceId: external_exports.string().nullable().optional().describe("Teamspace ID to associate this system with; pass null to remove the association"),
29097
- accountId: external_exports.string().nullable().optional().describe("Account that owns this System \u2014 the client or internal org unit. Pass null (or an empty string) to unlink. Reassigning moves the System so it is listed under exactly one Account."),
29098
- repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
29099
- repoName: external_exports.string().optional().describe("GitHub repository name"),
29100
- repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")'),
29101
- rateLimitOverrides: external_exports.record(external_exports.string(), external_exports.object({ maxPerHour: external_exports.number().min(0) })).optional().describe('Per-task-type rate limit overrides, e.g. {"agent_chat":{"maxPerHour":100}}. Overrides env var and compiled defaults.')
29102
- };
29103
- const updateSystemHandler = async ({ systemId, ...updates }) => {
29104
- try {
29105
- const nonEmpty = Object.fromEntries(
29106
- Object.entries(updates).filter(([, v]) => v !== void 0)
29107
- );
29108
- if (Object.keys(nonEmpty).length === 0) {
29109
- return { content: [{ type: "text", text: "No updates provided." }], isError: true };
29110
- }
29111
- await assertProjectInOrg(client, systemId, ctx.orgId);
29112
- if (nonEmpty["accountId"] === "") nonEmpty["accountId"] = null;
29113
- if (typeof updates.teamspaceId === "string") {
29114
- const teamspace = await client.getTeamspace(updates.teamspaceId);
29115
- if (!teamspace) {
29116
- return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
29117
- }
29118
- if (teamspace.orgId !== ctx.orgId) {
29119
- return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
29120
- }
29121
- }
29122
- const updated = await client.updateSystem(systemId, nonEmpty);
29123
- if (!updated) {
29124
- return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
29125
- }
29126
- if (PROJECT_EMBEDDING_FIELDS.some((f) => f in nonEmpty)) {
29127
- void client.triggerProjectEmbedding(systemId);
29128
- }
29129
- const lines = [
29130
- `System updated successfully.`,
29131
- "",
29132
- `**ID:** ${updated.projectId}`,
29133
- `**Title:** ${updated.title}`,
29134
- accountLine(nonEmpty, updated.accountId),
29135
- updated.strategy ? `**Strategy:** (updated)` : "",
29136
- updated.repoOwner ? `**Repo:** ${updated.repoOwner}/${updated.repoName}` : "",
29137
- updated.repoDefaultBranch ? `**Default Branch:** ${updated.repoDefaultBranch}` : ""
29138
- ].filter(Boolean);
29139
- return { content: [{ type: "text", text: lines.join("\n") }] };
29140
- } catch (err) {
29141
- const message = err instanceof Error ? err.message : String(err);
29142
- return { content: [{ type: "text", text: `Failed to update system: ${message}` }], isError: true };
29143
- }
29144
- };
29145
- server.tool("update_system", "Update system settings such as title, description, repository configuration, or the Account that owns it", updateSystemSchema, updateSystemHandler);
29146
- server.tool(
29147
- "archive_system",
29148
- "Archive a system, hiding it from the system dropdown and all active system views. Use this when a system is no longer active and should be removed from navigation.",
29149
- { systemId: external_exports.string().describe("The system ID to archive") },
29150
- async ({ systemId }) => {
29151
- const system = await client.getSystem(systemId);
29152
- if (!system) {
29153
- return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
29154
- }
29155
- if (system.orgId !== ctx.orgId) {
29156
- return { content: [{ type: "text", text: `System "${systemId}" is not in this organization` }], isError: true };
29157
- }
29158
- try {
29159
- const updated = await client.archiveSystem(systemId);
29160
- if (!updated) {
29161
- return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
29162
- }
29163
- return {
29164
- content: [{
29165
- type: "text",
29166
- text: [
29167
- "System archived successfully.",
29168
- "",
29169
- `**ID:** ${updated.projectId}`,
29170
- `**Title:** ${updated.title}`,
29171
- `**Status:** ${updated.status}`
29172
- ].join("\n")
29173
- }]
29174
- };
29175
- } catch (err) {
29176
- const message = err instanceof Error ? err.message : String(err);
29177
- return { content: [{ type: "text", text: `Failed to archive system: ${message}` }], isError: true };
29178
- }
29179
- }
29180
- );
29181
- const createSystemSchema = {
29182
- title: external_exports.string().describe("System title"),
29183
- description: external_exports.string().optional().describe("System description"),
29184
- strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
29185
- teamspaceId: external_exports.string().optional().describe("Teamspace ID to associate this system with"),
29186
- repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
29187
- repoName: external_exports.string().optional().describe("GitHub repository name"),
29188
- repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")')
29189
- };
29190
- const createSystemHandler = async ({ title, description, strategy, teamspaceId, repoOwner, repoName, repoDefaultBranch }) => {
29191
- if (teamspaceId) {
29192
- const teamspace = await client.getTeamspace(teamspaceId);
29193
- if (!teamspace) {
29194
- return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
29195
- }
29196
- if (teamspace.orgId !== ctx.orgId) {
29197
- return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
29198
- }
29199
- }
29200
- try {
29201
- const project = await client.createSystem({
29202
- projectId: (0, import_crypto5.randomUUID)(),
29203
- orgId: ctx.orgId,
29204
- ownerUserId: ctx.user.userId,
29205
- title,
29206
- description,
29207
- strategy,
29208
- status: "active",
29209
- teamspaceId,
29210
- repoOwner,
29211
- repoName,
29212
- repoDefaultBranch
29213
- });
29214
- const text = [
29215
- `System created successfully.`,
29216
- "",
29217
- `**ID:** ${project.projectId}`,
29218
- `**Title:** ${project.title}`,
29219
- `**Status:** ${project.status}`,
29220
- project.description ? `**Description:** ${project.description}` : "",
29221
- project.repoOwner ? `**Repo:** ${project.repoOwner}/${project.repoName}` : "",
29222
- project.repoDefaultBranch ? `**Default Branch:** ${project.repoDefaultBranch}` : ""
29223
- ].filter(Boolean).join("\n");
29224
- return { content: [{ type: "text", text }] };
29225
- } catch (err) {
29226
- const message = err instanceof Error ? err.message : String(err);
29227
- return { content: [{ type: "text", text: `Failed to create system: ${message}` }], isError: true };
29228
- }
29229
- };
29230
- server.tool("create_system", "Create a new system in the configured organization", createSystemSchema, createSystemHandler);
29231
- }
29232
-
29233
28984
  // ../../libs/harmonica-services/src/mcp/tools/pulse-report-tools.ts
29234
28985
  function formatBeatRow(r) {
29235
28986
  const downbeatState = r.hasDownbeat ? "" : " \u2014 no Downbeat ever set";
@@ -30018,8 +29769,8 @@ var import_promises = require("node:fs/promises");
30018
29769
  var import_node_os = require("node:os");
30019
29770
  var import_node_path = require("node:path");
30020
29771
 
30021
- // ../../libs/harmonica-services/src/project-snapshot.constants.ts
30022
- var SNAPSHOT_VERSION = 2;
29772
+ // ../../libs/harmonica-services/src/system-snapshot.constants.ts
29773
+ var SNAPSHOT_VERSION = 4;
30023
29774
 
30024
29775
  // ../../libs/harmonica-services/src/mcp/tools/snapshot-tools.ts
30025
29776
  function registerSnapshotTools(server, ctx, client) {
@@ -30051,7 +29802,7 @@ function registerSnapshotTools(server, ctx, client) {
30051
29802
  const filename = `harmonica-snapshot-${systemId}-${Date.now()}.json`;
30052
29803
  const filePath = (0, import_node_path.join)((0, import_node_os.tmpdir)(), filename);
30053
29804
  await (0, import_promises.writeFile)(filePath, JSON.stringify(snapshot), "utf-8");
30054
- const title = snapshot.project?.title ?? systemId;
29805
+ const title = snapshot.system?.title ?? systemId;
30055
29806
  const versionNote = snapshot.version !== SNAPSHOT_VERSION ? [`Warning: snapshot version ${snapshot.version} (local expects ${SNAPSHOT_VERSION}) \u2014 some counts may be zero.`, ""] : [];
30056
29807
  const summary = [
30057
29808
  `Exported "${title}" to: ${filePath}`,
@@ -30066,6 +29817,10 @@ function registerSnapshotTools(server, ctx, client) {
30066
29817
  ` PromptLogs: ${snapshot.promptLogs.length}`,
30067
29818
  ` Drops: ${snapshot.drops?.length ?? 0}`,
30068
29819
  ` Deliverables: ${snapshot.deliverables?.length ?? 0}`,
29820
+ ` Teamspaces: ${snapshot.teamspaces?.length ?? 0}`,
29821
+ ` Layers: ${snapshot.layers?.length ?? 0}`,
29822
+ ` Tracks: ${snapshot.tracks?.length ?? 0}`,
29823
+ ` Work Items: ${snapshot.workItems?.length ?? 0}`,
30069
29824
  "",
30070
29825
  "Use import_system_snapshot with this file path to import into another environment."
30071
29826
  ];
@@ -30099,7 +29854,7 @@ function registerSnapshotTools(server, ctx, client) {
30099
29854
  if (isPrivateHost(trimmed)) {
30100
29855
  throw new Error("Snapshot URL must point to a public host \u2014 private, link-local, and loopback addresses are not permitted.");
30101
29856
  }
30102
- const result2 = await client.importProjectSnapshotFromUrl(trimmed, importOptions);
29857
+ const result2 = await client.importSystemSnapshotFromUrl(trimmed, importOptions);
30103
29858
  return { content: [{ type: "text", text: formatImportSummary(result2, targetTeamspaceId) }] };
30104
29859
  }
30105
29860
  if (trimmed.startsWith("http://")) {
@@ -30114,21 +29869,25 @@ function registerSnapshotTools(server, ctx, client) {
30114
29869
  }
30115
29870
  assertSnapshotShape(parsed, "");
30116
29871
  const normalized = normalizeSnapshot(parsed);
30117
- const result = await client.importProjectSnapshot(normalized, importOptions);
29872
+ const result = await client.importSystemSnapshot(normalized, importOptions);
30118
29873
  return { content: [{ type: "text", text: formatImportSummary(result, targetTeamspaceId) }] };
30119
29874
  }
30120
29875
  );
30121
29876
  }
30122
29877
  function normalizeSnapshot(raw) {
29878
+ const anyRaw = raw;
30123
29879
  return {
30124
29880
  ...raw,
29881
+ // Compat: v3 and earlier snapshots have 'project' instead of 'system'
29882
+ system: raw.system ?? anyRaw["project"],
30125
29883
  beats: raw.beats ?? [],
30126
29884
  beatIndex: raw.beatIndex ?? [],
30127
29885
  proposals: raw.proposals ?? [],
30128
29886
  proposalIndex: raw.proposalIndex ?? [],
30129
29887
  revisions: raw.revisions ?? [],
30130
29888
  revisionIndex: raw.revisionIndex ?? [],
30131
- projectRevisionIndex: raw.projectRevisionIndex ?? [],
29889
+ // Compat: v3 and earlier snapshots have 'projectRevisionIndex'
29890
+ systemRevisionIndex: raw.systemRevisionIndex ?? anyRaw["projectRevisionIndex"] ?? [],
30132
29891
  activities: raw.activities ?? [],
30133
29892
  activityIndex: raw.activityIndex ?? [],
30134
29893
  notes: raw.notes ?? [],
@@ -30141,13 +29900,26 @@ function normalizeSnapshot(raw) {
30141
29900
  promptLogs: raw.promptLogs ?? [],
30142
29901
  beatVersions: raw.beatVersions ?? [],
30143
29902
  beatBeatVersionIndex: raw.beatBeatVersionIndex ?? [],
30144
- projectBeatVersionIndex: raw.projectBeatVersionIndex ?? [],
29903
+ // Compat: v3 and earlier snapshots have 'projectBeatVersionIndex'
29904
+ systemBeatVersionIndex: raw.systemBeatVersionIndex ?? anyRaw["projectBeatVersionIndex"] ?? [],
30145
29905
  beatVersionDropAssignments: raw.beatVersionDropAssignments ?? [],
30146
29906
  drops: raw.drops ?? [],
30147
29907
  accountDropIndex: raw.accountDropIndex ?? [],
30148
29908
  deliverables: raw.deliverables ?? [],
30149
29909
  teamspaceDeliverableIndex: raw.teamspaceDeliverableIndex ?? [],
30150
- projectDeliverableIndex: raw.projectDeliverableIndex ?? []
29910
+ // Compat: v3 and earlier snapshots have 'projectDeliverableIndex'
29911
+ systemDeliverableIndex: raw.systemDeliverableIndex ?? anyRaw["projectDeliverableIndex"] ?? [],
29912
+ teamspaces: raw.teamspaces ?? [],
29913
+ teamspaceAccountIndex: raw.teamspaceAccountIndex ?? [],
29914
+ accountTeamspaceIndex: raw.accountTeamspaceIndex ?? [],
29915
+ layers: raw.layers ?? [],
29916
+ layerIndex: raw.layerIndex ?? [],
29917
+ tracks: raw.tracks ?? [],
29918
+ // Compat: early v4 snapshots (before field rename) have 'trackProjectIndex'
29919
+ trackSystemIndex: raw.trackSystemIndex ?? anyRaw["trackProjectIndex"] ?? [],
29920
+ trackTeamspaceIndex: raw.trackTeamspaceIndex ?? [],
29921
+ workItems: raw.workItems ?? [],
29922
+ workItemIndex: raw.workItemIndex ?? []
30151
29923
  };
30152
29924
  }
30153
29925
  async function resolveSnapshotInput(input) {
@@ -30170,7 +29942,11 @@ function formatImportSummary(result, targetTeamspaceId) {
30170
29942
  ` PromptLogs: ${result.counts.promptLogs}`,
30171
29943
  ` Beat Versions: ${result.counts.beatVersions ?? 0}`,
30172
29944
  ` Drops: ${result.counts.drops ?? 0}`,
30173
- ` Deliverables: ${result.counts.deliverables ?? 0}`
29945
+ ` Deliverables: ${result.counts.deliverables ?? 0}`,
29946
+ ` Teamspaces: ${result.counts.teamspaces ?? 0}`,
29947
+ ` Layers: ${result.counts.layers ?? 0}`,
29948
+ ` Tracks: ${result.counts.tracks ?? 0}`,
29949
+ ` Work Items: ${result.counts.workItems ?? 0}`
30174
29950
  ];
30175
29951
  if (result.errors.length > 0) {
30176
29952
  lines.push("", `Errors (${result.errors.length}):`);
@@ -30217,9 +29993,9 @@ function assertSnapshotShape(val, sourceHint) {
30217
29993
  if (typeof obj["version"] !== "number" || obj["version"] <= 0) {
30218
29994
  throw new Error(`Snapshot${sourceHint} has an invalid or missing 'version' field \u2014 it may not be a valid project snapshot.`);
30219
29995
  }
30220
- const project = obj["project"];
30221
- if (typeof project !== "object" || project === null || typeof project["projectId"] !== "string") {
30222
- throw new Error(`Snapshot${sourceHint} is missing 'project.projectId' \u2014 it may not be a valid project snapshot.`);
29996
+ const systemOrProject = obj["system"] ?? obj["project"];
29997
+ if (typeof systemOrProject !== "object" || systemOrProject === null || typeof systemOrProject["projectId"] !== "string") {
29998
+ throw new Error(`Snapshot${sourceHint} is missing 'system.projectId' \u2014 it may not be a valid project snapshot.`);
30223
29999
  }
30224
30000
  }
30225
30001
 
@@ -30227,76 +30003,339 @@ function assertSnapshotShape(val, sourceHint) {
30227
30003
  var ENTITY_TYPES = ["project", "beat", "revision"];
30228
30004
  function registerSubscriptionTools(server, ctx, client) {
30229
30005
  server.tool(
30230
- "subscribe_to_entity",
30231
- "Follow a Project, Beat, or Revision to receive notifications when it changes. You are auto-subscribed to things you own \u2014 use this to follow additional entities.",
30232
- {
30233
- entityType: external_exports.enum(ENTITY_TYPES).describe("Type of entity to follow"),
30234
- entityId: external_exports.string().describe("The entity ID (project ID, beat ID, or revision ID)"),
30235
- projectId: external_exports.string().describe("The project ID (for access control)")
30236
- },
30237
- async ({ entityType, entityId, projectId }) => {
30238
- try {
30239
- await assertProjectInOrg(client, projectId, ctx.orgId);
30240
- await client.subscribe(ctx.user.userId, entityType, entityId, projectId, "manual");
30241
- return {
30242
- content: [{
30243
- type: "text",
30244
- text: `Now following ${entityType} ${entityId}. You'll receive notifications when it changes.`
30245
- }]
30246
- };
30247
- } catch (err) {
30248
- const message = err instanceof Error ? err.message : String(err);
30249
- return { content: [{ type: "text", text: `Failed to subscribe: ${message}` }], isError: true };
30006
+ "subscribe_to_entity",
30007
+ "Follow a Project, Beat, or Revision to receive notifications when it changes. You are auto-subscribed to things you own \u2014 use this to follow additional entities.",
30008
+ {
30009
+ entityType: external_exports.enum(ENTITY_TYPES).describe("Type of entity to follow"),
30010
+ entityId: external_exports.string().describe("The entity ID (project ID, beat ID, or revision ID)"),
30011
+ projectId: external_exports.string().describe("The project ID (for access control)")
30012
+ },
30013
+ async ({ entityType, entityId, projectId }) => {
30014
+ try {
30015
+ await assertProjectInOrg(client, projectId, ctx.orgId);
30016
+ await client.subscribe(ctx.user.userId, entityType, entityId, projectId, "manual");
30017
+ return {
30018
+ content: [{
30019
+ type: "text",
30020
+ text: `Now following ${entityType} ${entityId}. You'll receive notifications when it changes.`
30021
+ }]
30022
+ };
30023
+ } catch (err) {
30024
+ const message = err instanceof Error ? err.message : String(err);
30025
+ return { content: [{ type: "text", text: `Failed to subscribe: ${message}` }], isError: true };
30026
+ }
30027
+ }
30028
+ );
30029
+ server.tool(
30030
+ "unsubscribe_from_entity",
30031
+ "Stop following a Project, Beat, or Revision. Use durable=true to prevent auto-resubscribe if you are later re-assigned.",
30032
+ {
30033
+ entityType: external_exports.enum(ENTITY_TYPES).describe("Type of entity to unfollow"),
30034
+ entityId: external_exports.string().describe("The entity ID"),
30035
+ durable: external_exports.boolean().optional().default(false).describe("If true, prevents auto-resubscribe on reassignment")
30036
+ },
30037
+ async ({ entityType, entityId, durable }) => {
30038
+ try {
30039
+ await client.unsubscribe(ctx.user.userId, entityType, entityId, durable);
30040
+ const durableMsg = durable ? " (durable \u2014 will not auto-resubscribe on reassignment)" : "";
30041
+ return {
30042
+ content: [{
30043
+ type: "text",
30044
+ text: `Unfollowed ${entityType} ${entityId}${durableMsg}.`
30045
+ }]
30046
+ };
30047
+ } catch (err) {
30048
+ const message = err instanceof Error ? err.message : String(err);
30049
+ return { content: [{ type: "text", text: `Failed to unsubscribe: ${message}` }], isError: true };
30050
+ }
30051
+ }
30052
+ );
30053
+ server.tool(
30054
+ "list_subscriptions",
30055
+ "List entities you are currently following. Optionally filter by entity type (project, beat, revision).",
30056
+ {
30057
+ entityType: external_exports.enum(ENTITY_TYPES).optional().describe("Filter by entity type")
30058
+ },
30059
+ async ({ entityType }) => {
30060
+ try {
30061
+ const subs = await client.listSubscriptions(ctx.user.userId, entityType);
30062
+ if (subs.length === 0) {
30063
+ return { content: [{ type: "text", text: "You are not following any entities." }] };
30064
+ }
30065
+ const lines = [`# Your Subscriptions (${subs.length})`, ""];
30066
+ for (const sub of subs) {
30067
+ lines.push(`- **${sub.entityType}** ${sub.entityId} (${sub.source}, since ${sub.createdAt?.split("T")[0] ?? "unknown"})`);
30068
+ }
30069
+ return { content: [{ type: "text", text: lines.join("\n") }] };
30070
+ } catch (err) {
30071
+ const message = err instanceof Error ? err.message : String(err);
30072
+ return { content: [{ type: "text", text: `Failed to list subscriptions: ${message}` }], isError: true };
30073
+ }
30074
+ }
30075
+ );
30076
+ }
30077
+
30078
+ // ../../libs/harmonica-services/src/mcp/tools/system-lifecycle-tools.ts
30079
+ function registerProjectLifecycleTools(server, ctx, client) {
30080
+ server.tool(
30081
+ "transition_system_lifecycle",
30082
+ "Advance a System's capability-maturity lifecycle state. Allowed transitions follow the state machine (Concept \u2192 Incubating \u2192 Piloting \u2192 Activated \u2192 Commercializing \u2192 Scaled, plus paused/killed/sunset/archived exits). The activated \u2192 commercializing edge requires a `decisionNoteId` pointing to a system-scoped Decision Note (the governance review). Other gates are advisory.",
30083
+ {
30084
+ systemId: external_exports.string().describe("The system ID"),
30085
+ targetState: external_exports.enum([
30086
+ "concept",
30087
+ "incubating",
30088
+ "piloting",
30089
+ "activated",
30090
+ "commercializing",
30091
+ "scaled",
30092
+ "paused",
30093
+ "killed",
30094
+ "sunset",
30095
+ "archived"
30096
+ ]).describe("Target lifecycle state. Active: concept, incubating, piloting, activated, commercializing, scaled. Exits: paused, killed, sunset, archived."),
30097
+ reason: external_exports.string().optional().describe("Why this transition is being made (recorded in audit metadata)"),
30098
+ decisionNoteId: external_exports.string().optional().describe("Note ID of the governance Decision Note. Required for the activated \u2192 commercializing transition; optional otherwise.")
30099
+ },
30100
+ async ({ systemId, targetState, reason, decisionNoteId }) => {
30101
+ try {
30102
+ const system = await client.getSystem(systemId);
30103
+ if (!system) {
30104
+ return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
30105
+ }
30106
+ if (system.orgId !== ctx.orgId) {
30107
+ return { content: [{ type: "text", text: `System "${systemId}" is not in this organization` }], isError: true };
30108
+ }
30109
+ const result = await client.transitionProjectLifecycleState(systemId, targetState, {
30110
+ actor: { type: "human", id: ctx.user.userId, name: ctx.user.name },
30111
+ reason,
30112
+ decisionNoteId
30113
+ });
30114
+ if (!result.success) {
30115
+ const failedGates = result.error?.failedGates?.length ? ` (failed gates: ${result.error.failedGates.join(", ")})` : "";
30116
+ return {
30117
+ content: [{ type: "text", text: `Transition failed: ${result.error?.message ?? "Unknown error"}${failedGates}` }],
30118
+ isError: true
30119
+ };
30120
+ }
30121
+ const lines = [
30122
+ `System lifecycle transitioned successfully.`,
30123
+ "",
30124
+ `**System:** ${systemId}`,
30125
+ `**Title:** ${system.title}`,
30126
+ `**Transition:** ${result.previousState} \u2192 ${result.newState}`
30127
+ ];
30128
+ if (reason) lines.push(`**Reason:** ${reason}`);
30129
+ if (decisionNoteId) lines.push(`**Decision Note:** ${decisionNoteId}`);
30130
+ return { content: [{ type: "text", text: lines.join("\n") }] };
30131
+ } catch (err) {
30132
+ const message = err instanceof Error ? err.message : String(err);
30133
+ return { content: [{ type: "text", text: `Failed to transition system lifecycle: ${message}` }], isError: true };
30134
+ }
30135
+ }
30136
+ );
30137
+ }
30138
+
30139
+ // ../../libs/harmonica-services/src/mcp/tools/system-tools.ts
30140
+ var import_crypto5 = require("crypto");
30141
+ function accountLine(request, resolved) {
30142
+ const requested = request["accountId"];
30143
+ if (requested === null || requested === "") return "**Account:** (unlinked)";
30144
+ return resolved ? `**Account:** ${resolved}` : "";
30145
+ }
30146
+ var PROJECT_EMBEDDING_FIELDS = ["title", "description", "strategy"];
30147
+ function registerProjectTools(server, ctx, client) {
30148
+ const listSystemsHandler = async ({ teamspaceId }) => {
30149
+ try {
30150
+ const projects = await client.listOrgSystems(ctx.orgId);
30151
+ const trimmed = teamspaceId?.trim();
30152
+ const filtered = trimmed ? projects.filter((p) => p.teamspaceId === trimmed) : projects;
30153
+ const text = formatProjectSummaryTable(filtered);
30154
+ return { content: [{ type: "text", text }] };
30155
+ } catch (err) {
30156
+ const message = err instanceof Error ? err.message : String(err);
30157
+ return { content: [{ type: "text", text: `Failed to list systems: ${message}` }], isError: true };
30158
+ }
30159
+ };
30160
+ server.tool(
30161
+ "list_systems",
30162
+ "List all systems (projects) in the configured organization",
30163
+ {
30164
+ teamspaceId: external_exports.string().optional().describe("Filter to systems belonging to a specific teamspace. Empty or whitespace-only treated as no filter.")
30165
+ },
30166
+ listSystemsHandler
30167
+ );
30168
+ const getSystemContextSchema = {
30169
+ systemId: external_exports.string().describe("The system ID"),
30170
+ noteLimit: external_exports.number().int().min(1).max(MAX_CONTEXT_NOTE_LIMIT).optional().describe(
30171
+ `Max Notes to include, prioritised by note type (default ${DEFAULT_CONTEXT_NOTE_LIMIT}, max ${MAX_CONTEXT_NOTE_LIMIT}). Use list_notes or search for the full set.`
30172
+ )
30173
+ };
30174
+ const getSystemContextHandler = async ({
30175
+ systemId,
30176
+ noteLimit
30177
+ }) => {
30178
+ try {
30179
+ const [project, org] = await Promise.all([
30180
+ fetchProjectInOrg(client, systemId, ctx.orgId),
30181
+ client.getOrg(ctx.orgId)
30182
+ ]);
30183
+ const notes = await client.listProjectNotes(systemId, {
30184
+ limit: noteLimit ?? DEFAULT_CONTEXT_NOTE_LIMIT
30185
+ });
30186
+ const text = formatProjectContext(project, notes, org?.coda);
30187
+ return { content: [{ type: "text", text }] };
30188
+ } catch (err) {
30189
+ const message = err instanceof Error ? err.message : String(err);
30190
+ return { content: [{ type: "text", text: `Failed to get system context: ${message}` }], isError: true };
30191
+ }
30192
+ };
30193
+ server.tool(
30194
+ "get_system_context",
30195
+ "Get system metadata, description, and notes in one view",
30196
+ getSystemContextSchema,
30197
+ getSystemContextHandler
30198
+ );
30199
+ const updateSystemSchema = {
30200
+ systemId: external_exports.string().describe("The system ID"),
30201
+ title: external_exports.string().optional().describe("New system title"),
30202
+ description: external_exports.string().optional().describe("New system description"),
30203
+ strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
30204
+ teamspaceId: external_exports.string().nullable().optional().describe("Teamspace ID to associate this system with; pass null to remove the association"),
30205
+ accountId: external_exports.string().nullable().optional().describe("Account that owns this System \u2014 the client or internal org unit. Pass null (or an empty string) to unlink. Reassigning moves the System so it is listed under exactly one Account."),
30206
+ repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
30207
+ repoName: external_exports.string().optional().describe("GitHub repository name"),
30208
+ repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")'),
30209
+ rateLimitOverrides: external_exports.record(external_exports.string(), external_exports.object({ maxPerHour: external_exports.number().min(0) })).optional().describe('Per-task-type rate limit overrides, e.g. {"agent_chat":{"maxPerHour":100}}. Overrides env var and compiled defaults.')
30210
+ };
30211
+ const updateSystemHandler = async ({ systemId, ...updates }) => {
30212
+ try {
30213
+ const nonEmpty = Object.fromEntries(
30214
+ Object.entries(updates).filter(([, v]) => v !== void 0)
30215
+ );
30216
+ if (Object.keys(nonEmpty).length === 0) {
30217
+ return { content: [{ type: "text", text: "No updates provided." }], isError: true };
30218
+ }
30219
+ await assertProjectInOrg(client, systemId, ctx.orgId);
30220
+ if (nonEmpty["accountId"] === "") nonEmpty["accountId"] = null;
30221
+ if (typeof updates.teamspaceId === "string") {
30222
+ const teamspace = await client.getTeamspace(updates.teamspaceId);
30223
+ if (!teamspace) {
30224
+ return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
30225
+ }
30226
+ if (teamspace.orgId !== ctx.orgId) {
30227
+ return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
30228
+ }
30229
+ }
30230
+ const updated = await client.updateSystem(systemId, nonEmpty);
30231
+ if (!updated) {
30232
+ return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
30250
30233
  }
30234
+ if (PROJECT_EMBEDDING_FIELDS.some((f) => f in nonEmpty)) {
30235
+ void client.triggerProjectEmbedding(systemId);
30236
+ }
30237
+ const lines = [
30238
+ `System updated successfully.`,
30239
+ "",
30240
+ `**ID:** ${updated.projectId}`,
30241
+ `**Title:** ${updated.title}`,
30242
+ accountLine(nonEmpty, updated.accountId),
30243
+ updated.strategy ? `**Strategy:** (updated)` : "",
30244
+ updated.repoOwner ? `**Repo:** ${updated.repoOwner}/${updated.repoName}` : "",
30245
+ updated.repoDefaultBranch ? `**Default Branch:** ${updated.repoDefaultBranch}` : ""
30246
+ ].filter(Boolean);
30247
+ return { content: [{ type: "text", text: lines.join("\n") }] };
30248
+ } catch (err) {
30249
+ const message = err instanceof Error ? err.message : String(err);
30250
+ return { content: [{ type: "text", text: `Failed to update system: ${message}` }], isError: true };
30251
30251
  }
30252
- );
30252
+ };
30253
+ server.tool("update_system", "Update system settings such as title, description, repository configuration, or the Account that owns it", updateSystemSchema, updateSystemHandler);
30253
30254
  server.tool(
30254
- "unsubscribe_from_entity",
30255
- "Stop following a Project, Beat, or Revision. Use durable=true to prevent auto-resubscribe if you are later re-assigned.",
30256
- {
30257
- entityType: external_exports.enum(ENTITY_TYPES).describe("Type of entity to unfollow"),
30258
- entityId: external_exports.string().describe("The entity ID"),
30259
- durable: external_exports.boolean().optional().default(false).describe("If true, prevents auto-resubscribe on reassignment")
30260
- },
30261
- async ({ entityType, entityId, durable }) => {
30255
+ "archive_system",
30256
+ "Archive a system, hiding it from the system dropdown and all active system views. Use this when a system is no longer active and should be removed from navigation.",
30257
+ { systemId: external_exports.string().describe("The system ID to archive") },
30258
+ async ({ systemId }) => {
30259
+ const system = await client.getSystem(systemId);
30260
+ if (!system) {
30261
+ return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
30262
+ }
30263
+ if (system.orgId !== ctx.orgId) {
30264
+ return { content: [{ type: "text", text: `System "${systemId}" is not in this organization` }], isError: true };
30265
+ }
30262
30266
  try {
30263
- await client.unsubscribe(ctx.user.userId, entityType, entityId, durable);
30264
- const durableMsg = durable ? " (durable \u2014 will not auto-resubscribe on reassignment)" : "";
30267
+ const updated = await client.archiveSystem(systemId);
30268
+ if (!updated) {
30269
+ return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
30270
+ }
30265
30271
  return {
30266
30272
  content: [{
30267
30273
  type: "text",
30268
- text: `Unfollowed ${entityType} ${entityId}${durableMsg}.`
30274
+ text: [
30275
+ "System archived successfully.",
30276
+ "",
30277
+ `**ID:** ${updated.projectId}`,
30278
+ `**Title:** ${updated.title}`,
30279
+ `**Status:** ${updated.status}`
30280
+ ].join("\n")
30269
30281
  }]
30270
30282
  };
30271
30283
  } catch (err) {
30272
30284
  const message = err instanceof Error ? err.message : String(err);
30273
- return { content: [{ type: "text", text: `Failed to unsubscribe: ${message}` }], isError: true };
30285
+ return { content: [{ type: "text", text: `Failed to archive system: ${message}` }], isError: true };
30274
30286
  }
30275
30287
  }
30276
30288
  );
30277
- server.tool(
30278
- "list_subscriptions",
30279
- "List entities you are currently following. Optionally filter by entity type (project, beat, revision).",
30280
- {
30281
- entityType: external_exports.enum(ENTITY_TYPES).optional().describe("Filter by entity type")
30282
- },
30283
- async ({ entityType }) => {
30284
- try {
30285
- const subs = await client.listSubscriptions(ctx.user.userId, entityType);
30286
- if (subs.length === 0) {
30287
- return { content: [{ type: "text", text: "You are not following any entities." }] };
30288
- }
30289
- const lines = [`# Your Subscriptions (${subs.length})`, ""];
30290
- for (const sub of subs) {
30291
- lines.push(`- **${sub.entityType}** ${sub.entityId} (${sub.source}, since ${sub.createdAt?.split("T")[0] ?? "unknown"})`);
30292
- }
30293
- return { content: [{ type: "text", text: lines.join("\n") }] };
30294
- } catch (err) {
30295
- const message = err instanceof Error ? err.message : String(err);
30296
- return { content: [{ type: "text", text: `Failed to list subscriptions: ${message}` }], isError: true };
30289
+ const createSystemSchema = {
30290
+ title: external_exports.string().describe("System title"),
30291
+ description: external_exports.string().optional().describe("System description"),
30292
+ strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
30293
+ teamspaceId: external_exports.string().optional().describe("Teamspace ID to associate this system with"),
30294
+ repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
30295
+ repoName: external_exports.string().optional().describe("GitHub repository name"),
30296
+ repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")')
30297
+ };
30298
+ const createSystemHandler = async ({ title, description, strategy, teamspaceId, repoOwner, repoName, repoDefaultBranch }) => {
30299
+ if (teamspaceId) {
30300
+ const teamspace = await client.getTeamspace(teamspaceId);
30301
+ if (!teamspace) {
30302
+ return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
30303
+ }
30304
+ if (teamspace.orgId !== ctx.orgId) {
30305
+ return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
30297
30306
  }
30298
30307
  }
30299
- );
30308
+ try {
30309
+ const project = await client.createSystem({
30310
+ projectId: (0, import_crypto5.randomUUID)(),
30311
+ orgId: ctx.orgId,
30312
+ ownerUserId: ctx.user.userId,
30313
+ title,
30314
+ description,
30315
+ strategy,
30316
+ status: "active",
30317
+ teamspaceId,
30318
+ repoOwner,
30319
+ repoName,
30320
+ repoDefaultBranch
30321
+ });
30322
+ const text = [
30323
+ `System created successfully.`,
30324
+ "",
30325
+ `**ID:** ${project.projectId}`,
30326
+ `**Title:** ${project.title}`,
30327
+ `**Status:** ${project.status}`,
30328
+ project.description ? `**Description:** ${project.description}` : "",
30329
+ project.repoOwner ? `**Repo:** ${project.repoOwner}/${project.repoName}` : "",
30330
+ project.repoDefaultBranch ? `**Default Branch:** ${project.repoDefaultBranch}` : ""
30331
+ ].filter(Boolean).join("\n");
30332
+ return { content: [{ type: "text", text }] };
30333
+ } catch (err) {
30334
+ const message = err instanceof Error ? err.message : String(err);
30335
+ return { content: [{ type: "text", text: `Failed to create system: ${message}` }], isError: true };
30336
+ }
30337
+ };
30338
+ server.tool("create_system", "Create a new system in the configured organization", createSystemSchema, createSystemHandler);
30300
30339
  }
30301
30340
 
30302
30341
  // ../../libs/harmonica-services/src/mcp/tools/teamspace-tools.ts
@@ -30754,8 +30793,15 @@ function registerValueVelocityTools(server, ctx, client) {
30754
30793
  // ../../libs/harmonica-services/src/mcp/tools/work-item-tools.ts
30755
30794
  var WORK_ITEM_STATUS_ENUM = ["not_started", "in_progress", "blocked", "done"];
30756
30795
  function formatOwner(owner) {
30757
- return `${owner.name} <${owner.email}>`;
30758
- }
30796
+ return owner ? `${owner.name} <${owner.email}>` : "unassigned";
30797
+ }
30798
+ var WORK_ITEM_EFFORT_SIZE_ENUM = [
30799
+ "XS",
30800
+ "S",
30801
+ "M",
30802
+ "L",
30803
+ "XL"
30804
+ ];
30759
30805
  function registerWorkItemTools(server, _ctx, client) {
30760
30806
  server.tool(
30761
30807
  "list_track_work_items",
@@ -30791,6 +30837,9 @@ function registerWorkItemTools(server, _ctx, client) {
30791
30837
  `Status: ${wi.status}`,
30792
30838
  wi.committedEstimateHours !== void 0 ? `Committed Estimate: ${wi.committedEstimateHours}h` : null,
30793
30839
  wi.actualHours !== void 0 ? `Actual Hours: ${wi.actualHours}h` : null,
30840
+ wi.effortSize !== void 0 ? `Effort Size: ${wi.effortSize}` : null,
30841
+ wi.valueUnits !== void 0 ? `Value Units: ${wi.valueUnits}` : null,
30842
+ wi.costPerValueUnit !== void 0 ? `Cost per Value Unit: ${wi.costPerValueUnit}` : null,
30794
30843
  wi.beatVersionId ? `Linked Beat Version: ${wi.beatVersionId}` : null,
30795
30844
  estimateHint ? `Estimate Hint: ${estimateHint.hours}h (source: ${estimateHint.source})` : null,
30796
30845
  wi.createdBy ? `Created By: ${wi.createdBy}` : null,
@@ -30806,18 +30855,30 @@ function registerWorkItemTools(server, _ctx, client) {
30806
30855
  {
30807
30856
  trackId: external_exports.string().describe("The Track (DeliverableGroup) ID this work item belongs to"),
30808
30857
  title: external_exports.string().min(1).max(300).describe("Work item title"),
30809
- ownerName: external_exports.string().min(1).describe("Owner display name"),
30810
- ownerEmail: external_exports.string().email().describe("Owner email \u2014 exactly one owner at all times"),
30858
+ ownerName: external_exports.string().min(1).optional().describe("Optional owner display name (requires ownerEmail too)"),
30859
+ ownerEmail: external_exports.string().email().optional().describe("Optional owner email (requires ownerName too). A work item may be left unowned."),
30811
30860
  committedEstimateHours: external_exports.number().nonnegative().optional().describe("Optional committed estimate in hours"),
30861
+ effortSize: external_exports.enum(WORK_ITEM_EFFORT_SIZE_ENUM).optional().describe("Optional rough t-shirt effort size (delivery lens) \u2014 independent of valueUnits"),
30862
+ valueUnits: external_exports.number().nonnegative().optional().describe("Optional Value Units (commercial lens) \u2014 independent of effortSize"),
30863
+ costPerValueUnit: external_exports.number().nonnegative().optional().describe("Optional price per Value Unit"),
30812
30864
  createdBy: external_exports.string().optional().describe("Optional creator identity for audit"),
30813
30865
  beatVersionId: external_exports.string().min(1).optional().describe("Optional Beat Version to link for capability lineage. Must exist.")
30814
30866
  },
30815
- async ({ trackId, title, ownerName, ownerEmail, committedEstimateHours, createdBy, beatVersionId }) => {
30867
+ async ({ trackId, title, ownerName, ownerEmail, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId }) => {
30868
+ if (ownerName === void 0 !== (ownerEmail === void 0)) {
30869
+ return {
30870
+ content: [{ type: "text", text: "Setting the owner requires both ownerName and ownerEmail. Omit both to create an unowned work item." }],
30871
+ isError: true
30872
+ };
30873
+ }
30816
30874
  const wi = await client.createWorkItem({
30817
30875
  trackId,
30818
30876
  title,
30819
- owner: { name: ownerName, email: ownerEmail },
30877
+ ...ownerName !== void 0 && ownerEmail !== void 0 && { owner: { name: ownerName, email: ownerEmail } },
30820
30878
  ...committedEstimateHours !== void 0 && { committedEstimateHours },
30879
+ ...effortSize !== void 0 && { effortSize },
30880
+ ...valueUnits !== void 0 && { valueUnits },
30881
+ ...costPerValueUnit !== void 0 && { costPerValueUnit },
30821
30882
  ...createdBy !== void 0 && { createdBy },
30822
30883
  ...beatVersionId !== void 0 && { beatVersionId }
30823
30884
  });
@@ -30833,17 +30894,21 @@ function registerWorkItemTools(server, _ctx, client) {
30833
30894
  );
30834
30895
  server.tool(
30835
30896
  "update_work_item",
30836
- "Update mutable content fields on a work item (title, owner, committedEstimateHours, actualHours, beatVersionId). To move it between Tracks use move_work_item; to change its status use transition_work_item_status.",
30897
+ "Update mutable content fields on a work item (title, owner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId). To move it between Tracks use move_work_item; to change its status use transition_work_item_status.",
30837
30898
  {
30838
30899
  workItemId: external_exports.string().describe("The work item ID"),
30839
30900
  title: external_exports.string().min(1).max(300).optional().describe("New title"),
30840
30901
  ownerName: external_exports.string().min(1).optional().describe("New owner display name (requires ownerEmail too)"),
30841
30902
  ownerEmail: external_exports.string().email().optional().describe("New owner email (requires ownerName too)"),
30903
+ unassignOwner: external_exports.boolean().optional().describe("Pass true to clear the owner, leaving the work item unowned. Cannot be combined with ownerName/ownerEmail."),
30842
30904
  committedEstimateHours: external_exports.number().nonnegative().optional().describe("New committed estimate in hours"),
30843
30905
  actualHours: external_exports.number().nonnegative().optional().describe("New actual hours logged"),
30906
+ effortSize: external_exports.enum(WORK_ITEM_EFFORT_SIZE_ENUM).nullable().optional().describe("Rough t-shirt effort size (delivery lens). Pass null to clear; omit to leave untouched."),
30907
+ valueUnits: external_exports.number().nonnegative().nullable().optional().describe("Value Units (commercial lens). Pass null to clear; omit to leave untouched."),
30908
+ costPerValueUnit: external_exports.number().nonnegative().nullable().optional().describe("Price per Value Unit. Pass null to clear; omit to leave untouched."),
30844
30909
  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.")
30845
30910
  },
30846
- async ({ workItemId, title, ownerName, ownerEmail, committedEstimateHours, actualHours, beatVersionId }) => {
30911
+ async ({ workItemId, title, ownerName, ownerEmail, unassignOwner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId }) => {
30847
30912
  const hasOwnerUpdate = ownerName !== void 0 || ownerEmail !== void 0;
30848
30913
  if (hasOwnerUpdate && (ownerName === void 0 || ownerEmail === void 0)) {
30849
30914
  return {
@@ -30851,7 +30916,13 @@ function registerWorkItemTools(server, _ctx, client) {
30851
30916
  isError: true
30852
30917
  };
30853
30918
  }
30854
- const hasUpdates = title !== void 0 || hasOwnerUpdate || committedEstimateHours !== void 0 || actualHours !== void 0 || beatVersionId !== void 0;
30919
+ if (unassignOwner && hasOwnerUpdate) {
30920
+ return {
30921
+ content: [{ type: "text", text: "Cannot set and clear the owner in one update \u2014 pass either unassignOwner or ownerName/ownerEmail." }],
30922
+ isError: true
30923
+ };
30924
+ }
30925
+ 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;
30855
30926
  if (!hasUpdates) {
30856
30927
  return {
30857
30928
  content: [{ type: "text", text: `No updates provided for work item: ${workItemId}` }],
@@ -30861,8 +30932,12 @@ function registerWorkItemTools(server, _ctx, client) {
30861
30932
  const wi = await client.updateWorkItem(workItemId, {
30862
30933
  ...title !== void 0 && { title },
30863
30934
  ...hasOwnerUpdate && { owner: { name: ownerName, email: ownerEmail } },
30935
+ ...unassignOwner === true && { owner: null },
30864
30936
  ...committedEstimateHours !== void 0 && { committedEstimateHours },
30865
30937
  ...actualHours !== void 0 && { actualHours },
30938
+ ...effortSize !== void 0 && { effortSize },
30939
+ ...valueUnits !== void 0 && { valueUnits },
30940
+ ...costPerValueUnit !== void 0 && { costPerValueUnit },
30866
30941
  ...beatVersionId !== void 0 && { beatVersionId }
30867
30942
  });
30868
30943
  if (!wi) {
@@ -31408,7 +31483,7 @@ function registerBeatResources(server, ctx, client) {
31408
31483
  async (uri, { projectId }) => {
31409
31484
  const pid = String(projectId);
31410
31485
  await assertProjectInOrg(client, pid, ctx.orgId);
31411
- const beats = await client.listProjectBeats(pid);
31486
+ const beats = await client.listSystemBeats(pid);
31412
31487
  const text = formatBeatSummaryTable(beats);
31413
31488
  return { contents: [{ uri: uri.href, text }] };
31414
31489
  }
@@ -31442,7 +31517,7 @@ function registerGuidelinesResources(server, client) {
31442
31517
  );
31443
31518
  }
31444
31519
 
31445
- // ../../libs/harmonica-services/src/mcp/resources/project-resources.ts
31520
+ // ../../libs/harmonica-services/src/mcp/resources/system-resources.ts
31446
31521
  function registerProjectResources(server, ctx, client) {
31447
31522
  server.resource(
31448
31523
  "project-context",
@@ -31450,7 +31525,7 @@ function registerProjectResources(server, ctx, client) {
31450
31525
  async (uri, { projectId }) => {
31451
31526
  const pid = String(projectId);
31452
31527
  const project = await fetchProjectInOrg(client, pid, ctx.orgId);
31453
- const notes = await client.listProjectNotes(pid, { limit: DEFAULT_CONTEXT_NOTE_LIMIT });
31528
+ const notes = await client.listSystemNotes(pid, { limit: DEFAULT_CONTEXT_NOTE_LIMIT });
31454
31529
  const text = formatProjectContext(project, notes);
31455
31530
  return { contents: [{ uri: uri.href, text }] };
31456
31531
  }
@@ -31771,7 +31846,7 @@ function createHttpClient(config2) {
31771
31846
  const result = await request("POST", `/api/systems/${encodeURIComponent(projectId)}/beats/next-id`);
31772
31847
  return result.beatId;
31773
31848
  },
31774
- listProjectBeats: async (projectId) => {
31849
+ listSystemBeats: async (projectId) => {
31775
31850
  const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/beats`);
31776
31851
  return (result?.beats ?? []).map((b) => ({
31777
31852
  ...b,
@@ -31927,6 +32002,21 @@ function createHttpClient(config2) {
31927
32002
  const result = await request("POST", `/api/layers/${encodeURIComponent(layerId)}/status`, { status, reason });
31928
32003
  return result?.layer;
31929
32004
  },
32005
+ listSystemNotes: async (projectId, filters) => {
32006
+ const params = new URLSearchParams();
32007
+ if (filters?.noteType) {
32008
+ const types = Array.isArray(filters.noteType) ? filters.noteType : [filters.noteType];
32009
+ types.forEach((t) => params.append("type", t));
32010
+ }
32011
+ if (filters?.status) params.set("status", filters.status);
32012
+ if (filters?.revisionId) params.set("revisionId", filters.revisionId);
32013
+ if (filters?.excludeChildren) params.set("excludeChildren", "true");
32014
+ if (filters?.significance) params.set("significance", filters.significance);
32015
+ if (filters?.limit !== void 0) params.set("limit", String(filters.limit));
32016
+ const qs = params.toString() ? `?${params.toString()}` : "";
32017
+ const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/notes${qs}`);
32018
+ return result?.notes ?? [];
32019
+ },
31930
32020
  listProjectNotes: async (projectId, filters) => {
31931
32021
  const params = new URLSearchParams();
31932
32022
  if (filters?.noteType) {
@@ -32066,6 +32156,14 @@ function createHttpClient(config2) {
32066
32156
  const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/revisions/pending${qs}`);
32067
32157
  return result ?? [];
32068
32158
  },
32159
+ listSystemRevisions: async (projectId, options) => {
32160
+ const params = new URLSearchParams();
32161
+ if (options?.status) params.set("status", options.status);
32162
+ if (options?.includeArchived) params.set("includeArchived", "true");
32163
+ const qs = params.toString() ? `?${params.toString()}` : "";
32164
+ const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/revisions${qs}`);
32165
+ return result ?? [];
32166
+ },
32069
32167
  listProjectRevisions: async (projectId, options) => {
32070
32168
  const params = new URLSearchParams();
32071
32169
  if (options?.status) params.set("status", options.status);
@@ -32295,6 +32393,14 @@ function createHttpClient(config2) {
32295
32393
  // partitions, but the REST surface is unified per the project-level
32296
32394
  // "avoid nested/duplicate routes" guidance.
32297
32395
  getSession: (sessionId) => request("GET", `/api/sessions/${encodeURIComponent(sessionId)}`),
32396
+ listSystemSessions: async (projectId, options) => {
32397
+ const params = new URLSearchParams();
32398
+ params.set("projectId", projectId);
32399
+ if (options?.status) params.set("status", options.status);
32400
+ if (options?.includeTerminal) params.set("includeTerminal", "true");
32401
+ const result = await request("GET", `/api/sessions?${params.toString()}`);
32402
+ return result ?? [];
32403
+ },
32298
32404
  listProjectSessions: async (projectId, options) => {
32299
32405
  const params = new URLSearchParams();
32300
32406
  params.set("projectId", projectId);
@@ -32482,6 +32588,14 @@ function createHttpClient(config2) {
32482
32588
  getTask: async (taskId) => {
32483
32589
  return request("GET", `/api/tasks/${encodeURIComponent(taskId)}`);
32484
32590
  },
32591
+ listSystemTasks: async (projectId, options) => {
32592
+ const params = new URLSearchParams();
32593
+ if (options?.status) params.set("status", options.status);
32594
+ if (options?.limit) params.set("limit", String(options.limit));
32595
+ const qs = params.toString() ? `?${params.toString()}` : "";
32596
+ const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/tasks${qs}`);
32597
+ return result?.tasks ?? [];
32598
+ },
32485
32599
  listProjectTasks: async (projectId, options) => {
32486
32600
  const params = new URLSearchParams();
32487
32601
  if (options?.status) params.set("status", options.status);
@@ -32513,6 +32627,15 @@ function createHttpClient(config2) {
32513
32627
  }, LONG_RUNNING_TIMEOUT_MS);
32514
32628
  },
32515
32629
  // Activity — forward to API endpoint
32630
+ listSystemActivities: async (projectId, options) => {
32631
+ const params = new URLSearchParams();
32632
+ if (options?.limit) params.set("limit", String(options.limit));
32633
+ if (options?.minImportance) params.set("importance", options.minImportance);
32634
+ const qs = params.toString();
32635
+ const path = `/api/systems/${encodeURIComponent(projectId)}/activities${qs ? `?${qs}` : ""}`;
32636
+ const result = await request("GET", path);
32637
+ return { entries: result.activities ?? [], hasMore: false };
32638
+ },
32516
32639
  listProjectActivities: async (projectId, options) => {
32517
32640
  const params = new URLSearchParams();
32518
32641
  if (options?.limit) params.set("limit", String(options.limit));
@@ -32794,8 +32917,8 @@ function createHttpClient(config2) {
32794
32917
  }
32795
32918
  return download;
32796
32919
  },
32797
- importProjectSnapshot: (snapshot, options) => request("POST", "/api/systems/import", { ...snapshot, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
32798
- importProjectSnapshotFromUrl: (url2, options) => request("POST", "/api/systems/import", { snapshotUrl: url2, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
32920
+ importSystemSnapshot: (snapshot, options) => request("POST", "/api/systems/import", { ...snapshot, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
32921
+ importSystemSnapshotFromUrl: (url2, options) => request("POST", "/api/systems/import", { snapshotUrl: url2, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
32799
32922
  // Embedding similarity
32800
32923
  embedProjectEntities: (projectId) => request("POST", `/api/systems/${encodeURIComponent(projectId)}/embeddings/generate`),
32801
32924
  findSimilarNotes: async (noteId, projectId, options) => {
@@ -32861,6 +32984,18 @@ function createHttpClient(config2) {
32861
32984
  const res = await request("GET", `/api/checks/${encodeURIComponent(checkId)}`);
32862
32985
  return res.check;
32863
32986
  },
32987
+ listSystemChecks: async (projectId, checkType, options) => {
32988
+ const params = new URLSearchParams();
32989
+ if (checkType) params.set("type", checkType);
32990
+ if (options?.limit !== void 0) params.set("limit", String(options.limit));
32991
+ if (options?.cursor) params.set("cursor", options.cursor);
32992
+ const qs = params.toString() ? `?${params.toString()}` : "";
32993
+ const res = await request(
32994
+ "GET",
32995
+ `/api/systems/${encodeURIComponent(projectId)}/checks${qs}`
32996
+ );
32997
+ return { checks: res?.checks ?? [], nextCursor: res?.nextCursor };
32998
+ },
32864
32999
  listProjectChecks: async (projectId, checkType, options) => {
32865
33000
  const params = new URLSearchParams();
32866
33001
  if (checkType) params.set("type", checkType);
@@ -32959,6 +33094,11 @@ function createHttpClient(config2) {
32959
33094
  if (res === void 0) throw new Error(`Teamspace ${teamspaceId} not found`);
32960
33095
  return res.deliverableGroups;
32961
33096
  },
33097
+ listSystemDeliverableGroups: async (projectId) => {
33098
+ const res = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/deliverable-groups`);
33099
+ if (res === void 0) throw new Error(`Project ${projectId} not found`);
33100
+ return res.deliverableGroups;
33101
+ },
32962
33102
  listProjectDeliverableGroups: async (projectId) => {
32963
33103
  const res = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/deliverable-groups`);
32964
33104
  if (res === void 0) throw new Error(`Project ${projectId} not found`);
@@ -33138,11 +33278,14 @@ function createHttpClient(config2) {
33138
33278
  },
33139
33279
  // WorkItems (WorkItem exposure layer — owned, statused unit of operational work under a Track)
33140
33280
  createWorkItem: async (input) => {
33141
- const { trackId, title, owner, committedEstimateHours, createdBy, beatVersionId } = input;
33281
+ const { trackId, title, owner, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId } = input;
33142
33282
  const body = {
33143
33283
  title,
33144
- owner,
33284
+ ...owner !== void 0 && { owner },
33145
33285
  ...committedEstimateHours !== void 0 && { committedEstimateHours },
33286
+ ...effortSize !== void 0 && { effortSize },
33287
+ ...valueUnits !== void 0 && { valueUnits },
33288
+ ...costPerValueUnit !== void 0 && { costPerValueUnit },
33146
33289
  ...createdBy !== void 0 && { createdBy },
33147
33290
  ...beatVersionId !== void 0 && { beatVersionId }
33148
33291
  };
@@ -33685,7 +33828,7 @@ function loadConfig() {
33685
33828
  };
33686
33829
  }
33687
33830
  async function main() {
33688
- console.error(`[harmonica-mcp] v${"2.0.0"} starting\u2026`);
33831
+ console.error(`[harmonica-mcp] v${"2.1.0"} starting\u2026`);
33689
33832
  const config2 = loadConfig();
33690
33833
  const client = createHttpClient({
33691
33834
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "MCP server for Harmonica — connect any MCP-compatible AI assistant to Harmonica",
5
5
  "license": "MIT",
6
6
  "bin": {