@codazen/harmonica-mcp 1.1.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 +503 -404
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22147,22 +22147,22 @@ function registerBarTools(server, _ctx, client) {
22147
22147
  // ../../libs/harmonica-services/src/mcp/tools/baseline-tools.ts
22148
22148
  function registerBaselineTools(server, ctx, client) {
22149
22149
  server.tool(
22150
- "baseline_project",
22151
- "Create Rev 1 baseline revisions for all eligible Beats in a project, transition them to live, and run build_quality checks. Returns a taskId to poll for progress. Skips Beats that already have revisions or are archived/deprecated.",
22150
+ "baseline_system",
22151
+ "Create Rev 1 baseline revisions for all eligible Beats in a system, transition them to live, and run build_quality checks. Returns a taskId to poll for progress. Skips Beats that already have revisions or are archived/deprecated.",
22152
22152
  {
22153
- projectId: external_exports.string().describe("The project ID to baseline"),
22153
+ systemId: external_exports.string().describe("The system ID to baseline"),
22154
22154
  beatIds: external_exports.array(external_exports.string()).optional().describe("Optional: only baseline these specific beat IDs"),
22155
22155
  runChecks: external_exports.boolean().optional().default(true).describe("Whether to run build_quality checks (default: true)"),
22156
22156
  localPath: external_exports.string().optional().describe("Absolute local filesystem path to the repo \u2014 skips git clone and drives revision state from code analysis (requires HARMONICA_ALLOW_LOCAL_REPO=true)")
22157
22157
  },
22158
- async ({ projectId, beatIds, runChecks, localPath }) => {
22158
+ async ({ systemId, beatIds, runChecks, localPath }) => {
22159
22159
  try {
22160
- await assertProjectInOrg(client, projectId, ctx.orgId);
22161
- const task = await client.baselineProject(projectId, { beatIds, runChecks, localPath });
22160
+ await assertProjectInOrg(client, systemId, ctx.orgId);
22161
+ const task = await client.baselineProject(systemId, { beatIds, runChecks, localPath });
22162
22162
  return {
22163
22163
  content: [{
22164
22164
  type: "text",
22165
- text: `Project baseline started. Job ID: ${task.taskId}
22165
+ text: `System baseline started. Job ID: ${task.taskId}
22166
22166
 
22167
22167
  Poll progress with: get_job_status({ jobId: "${task.taskId}" })`
22168
22168
  }]
@@ -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
  }
@@ -24964,14 +24964,14 @@ ${rows.join("\n\n")}`;
24964
24964
  }
24965
24965
  function registerEmbeddingTools(server, ctx, client) {
24966
24966
  server.tool(
24967
- "embed_project_entities",
24968
- "Batch-embed all Notes and Beats in a project. Generates vector embeddings for similarity search. Run this to populate or refresh embeddings.",
24967
+ "embed_system_entities",
24968
+ "Batch-embed all Notes and Beats in a system. Generates vector embeddings for similarity search. Run this to populate or refresh embeddings.",
24969
24969
  {
24970
- projectId: external_exports.string().describe("The project ID to embed entities for")
24970
+ systemId: external_exports.string().describe("The system ID to embed entities for")
24971
24971
  },
24972
- async ({ projectId }) => {
24973
- await assertProjectInOrg(client, projectId, ctx.orgId);
24974
- const result = await client.embedProjectEntities(projectId);
24972
+ async ({ systemId }) => {
24973
+ await assertProjectInOrg(client, systemId, ctx.orgId);
24974
+ const result = await client.embedProjectEntities(systemId);
24975
24975
  const text = `## Embedding Results
24976
24976
 
24977
24977
  - Embedded: ${result.embedded}
@@ -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(),
@@ -28593,31 +28607,31 @@ function registerOrganizationTools(server, ctx, client) {
28593
28607
  }
28594
28608
  );
28595
28609
  server.tool(
28596
- "transfer_project",
28597
- "Transfer a project from its current organization to a different one. The project must belong to the configured organization.",
28610
+ "transfer_system",
28611
+ "Transfer a system from its current organization to a different one. The system must belong to the configured organization.",
28598
28612
  {
28599
- projectId: external_exports.string().describe("The project ID to transfer"),
28613
+ systemId: external_exports.string().describe("The system ID to transfer"),
28600
28614
  targetOrgId: external_exports.string().describe("The target organization ID")
28601
28615
  },
28602
- async ({ projectId, targetOrgId }) => {
28603
- const project = await client.getSystem(projectId);
28604
- if (!project) {
28605
- return { content: [{ type: "text", text: `Project not found: ${projectId}` }], isError: true };
28616
+ async ({ systemId, targetOrgId }) => {
28617
+ const system = await client.getSystem(systemId);
28618
+ if (!system) {
28619
+ return { content: [{ type: "text", text: `System not found: ${systemId}` }], isError: true };
28606
28620
  }
28607
- if (project.orgId !== ctx.orgId) {
28608
- return { content: [{ type: "text", text: `Project "${projectId}" does not belong to the configured organization.` }], isError: true };
28621
+ if (system.orgId !== ctx.orgId) {
28622
+ return { content: [{ type: "text", text: `System "${systemId}" does not belong to the configured organization.` }], isError: true };
28609
28623
  }
28610
- const result = await client.transferProject(ctx.orgId, projectId, targetOrgId);
28624
+ const result = await client.transferProject(ctx.orgId, systemId, targetOrgId);
28611
28625
  if (result.error) {
28612
28626
  return { content: [{ type: "text", text: result.error }], isError: true };
28613
28627
  }
28614
28628
  const transferred = result.project;
28615
28629
  if (!transferred) {
28616
- return { content: [{ type: "text", text: "Transfer succeeded but project data was not returned." }], isError: true };
28630
+ return { content: [{ type: "text", text: "Transfer succeeded but system data was not returned." }], isError: true };
28617
28631
  }
28618
- const text = `Project transferred.
28632
+ const text = `System transferred.
28619
28633
 
28620
- **ID:** ${transferred.projectId}
28634
+ **System ID:** ${transferred.projectId}
28621
28635
  **Title:** ${transferred.title}
28622
28636
  **New Org:** ${transferred.orgId}`;
28623
28637
  return { content: [{ type: "text", text }] };
@@ -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,313 +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_project_lifecycle",
28974
- "Advance a Project'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 project-scoped Decision Note (the governance review). Other gates are advisory.",
28975
- {
28976
- projectId: external_exports.string().describe("The project 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 ({ projectId, targetState, reason, decisionNoteId }) => {
28993
- try {
28994
- const project = await client.getSystem(projectId);
28995
- if (!project) {
28996
- return { content: [{ type: "text", text: `Project not found: "${projectId}"` }], isError: true };
28997
- }
28998
- if (project.orgId !== ctx.orgId) {
28999
- return { content: [{ type: "text", text: `Project "${projectId}" is not in this organization` }], isError: true };
29000
- }
29001
- const result = await client.transitionProjectLifecycleState(projectId, 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
- `Project lifecycle transitioned successfully.`,
29015
- "",
29016
- `**Project:** ${projectId}`,
29017
- `**Title:** ${project.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 project 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
- server.tool(
29061
- "list_projects",
29062
- "[Deprecated \u2014 use list_systems] List all projects in the configured organization",
29063
- {
29064
- teamspaceId: external_exports.string().optional().describe("Filter to projects belonging to a specific teamspace. Empty or whitespace-only treated as no filter.")
29065
- },
29066
- listSystemsHandler
29067
- );
29068
- const getSystemContextSchema = {
29069
- projectId: external_exports.string().describe("The system ID"),
29070
- noteLimit: external_exports.number().int().min(1).max(MAX_CONTEXT_NOTE_LIMIT).optional().describe(
29071
- `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.`
29072
- )
29073
- };
29074
- const getSystemContextHandler = async ({
29075
- projectId,
29076
- noteLimit
29077
- }) => {
29078
- try {
29079
- const [project, org] = await Promise.all([
29080
- fetchProjectInOrg(client, projectId, ctx.orgId),
29081
- client.getOrg(ctx.orgId)
29082
- ]);
29083
- const notes = await client.listProjectNotes(projectId, {
29084
- limit: noteLimit ?? DEFAULT_CONTEXT_NOTE_LIMIT
29085
- });
29086
- const text = formatProjectContext(project, notes, org?.coda);
29087
- return { content: [{ type: "text", text }] };
29088
- } catch (err) {
29089
- const message = err instanceof Error ? err.message : String(err);
29090
- return { content: [{ type: "text", text: `Failed to get system context: ${message}` }], isError: true };
29091
- }
29092
- };
29093
- server.tool(
29094
- "get_system_context",
29095
- "Get system metadata, description, and notes in one view",
29096
- getSystemContextSchema,
29097
- getSystemContextHandler
29098
- );
29099
- server.tool(
29100
- "get_project_context",
29101
- "[Deprecated \u2014 use get_system_context] Get project metadata, description, and notes in one view",
29102
- { projectId: external_exports.string().describe("The project ID") },
29103
- getSystemContextHandler
29104
- );
29105
- const updateSystemSchema = {
29106
- projectId: external_exports.string().describe("The system ID"),
29107
- title: external_exports.string().optional().describe("New system title"),
29108
- description: external_exports.string().optional().describe("New system description"),
29109
- strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
29110
- teamspaceId: external_exports.string().nullable().optional().describe("Teamspace ID to associate this system with; pass null to remove the association"),
29111
- 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."),
29112
- repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
29113
- repoName: external_exports.string().optional().describe("GitHub repository name"),
29114
- repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")'),
29115
- 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.')
29116
- };
29117
- const updateSystemHandler = async ({ projectId, ...updates }) => {
29118
- try {
29119
- const nonEmpty = Object.fromEntries(
29120
- Object.entries(updates).filter(([, v]) => v !== void 0)
29121
- );
29122
- if (Object.keys(nonEmpty).length === 0) {
29123
- return { content: [{ type: "text", text: "No updates provided." }], isError: true };
29124
- }
29125
- await assertProjectInOrg(client, projectId, ctx.orgId);
29126
- if (nonEmpty["accountId"] === "") nonEmpty["accountId"] = null;
29127
- if (typeof updates.teamspaceId === "string") {
29128
- const teamspace = await client.getTeamspace(updates.teamspaceId);
29129
- if (!teamspace) {
29130
- return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
29131
- }
29132
- if (teamspace.orgId !== ctx.orgId) {
29133
- return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
29134
- }
29135
- }
29136
- const updated = await client.updateSystem(projectId, nonEmpty);
29137
- if (!updated) {
29138
- return { content: [{ type: "text", text: `System not found: "${projectId}"` }], isError: true };
29139
- }
29140
- if (PROJECT_EMBEDDING_FIELDS.some((f) => f in nonEmpty)) {
29141
- void client.triggerProjectEmbedding(projectId);
29142
- }
29143
- const lines = [
29144
- `System updated successfully.`,
29145
- "",
29146
- `**ID:** ${updated.projectId}`,
29147
- `**Title:** ${updated.title}`,
29148
- accountLine(nonEmpty, updated.accountId),
29149
- updated.strategy ? `**Strategy:** (updated)` : "",
29150
- updated.repoOwner ? `**Repo:** ${updated.repoOwner}/${updated.repoName}` : "",
29151
- updated.repoDefaultBranch ? `**Default Branch:** ${updated.repoDefaultBranch}` : ""
29152
- ].filter(Boolean);
29153
- return { content: [{ type: "text", text: lines.join("\n") }] };
29154
- } catch (err) {
29155
- const message = err instanceof Error ? err.message : String(err);
29156
- return { content: [{ type: "text", text: `Failed to update system: ${message}` }], isError: true };
29157
- }
29158
- };
29159
- server.tool("update_system", "Update system settings such as title, description, repository configuration, or the Account that owns it", updateSystemSchema, updateSystemHandler);
29160
- server.tool(
29161
- "update_project",
29162
- "[Deprecated \u2014 use update_system] Update project settings such as title, description, or repository configuration",
29163
- {
29164
- projectId: external_exports.string().describe("The project ID"),
29165
- title: external_exports.string().optional().describe("New project title"),
29166
- description: external_exports.string().optional().describe("New project description"),
29167
- strategy: external_exports.string().optional().describe('Project strategy markdown \u2014 Org Strategy + Project Strategy ("Project Coda")'),
29168
- teamspaceId: external_exports.string().nullable().optional().describe("Teamspace ID to associate this project with; pass null to remove the association"),
29169
- repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
29170
- repoName: external_exports.string().optional().describe("GitHub repository name"),
29171
- repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")'),
29172
- 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.')
29173
- },
29174
- updateSystemHandler
29175
- );
29176
- server.tool(
29177
- "archive_project",
29178
- "Archive a project, hiding it from the project dropdown and all active project views. Use this when a project is no longer active and should be removed from navigation.",
29179
- { projectId: external_exports.string().describe("The project ID to archive") },
29180
- async ({ projectId }) => {
29181
- const project = await client.getSystem(projectId);
29182
- if (!project) {
29183
- return { content: [{ type: "text", text: `Project not found: "${projectId}"` }], isError: true };
29184
- }
29185
- if (project.orgId !== ctx.orgId) {
29186
- return { content: [{ type: "text", text: `Project "${projectId}" is not in this organization` }], isError: true };
29187
- }
29188
- try {
29189
- const updated = await client.archiveSystem(projectId);
29190
- if (!updated) {
29191
- return { content: [{ type: "text", text: `Project not found: "${projectId}"` }], isError: true };
29192
- }
29193
- return {
29194
- content: [{
29195
- type: "text",
29196
- text: [
29197
- "Project archived successfully.",
29198
- "",
29199
- `**ID:** ${updated.projectId}`,
29200
- `**Title:** ${updated.title}`,
29201
- `**Status:** ${updated.status}`
29202
- ].join("\n")
29203
- }]
29204
- };
29205
- } catch (err) {
29206
- const message = err instanceof Error ? err.message : String(err);
29207
- return { content: [{ type: "text", text: `Failed to archive project: ${message}` }], isError: true };
29208
- }
29209
- }
29210
- );
29211
- const createSystemSchema = {
29212
- title: external_exports.string().describe("System title"),
29213
- description: external_exports.string().optional().describe("System description"),
29214
- strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
29215
- teamspaceId: external_exports.string().optional().describe("Teamspace ID to associate this system with"),
29216
- repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
29217
- repoName: external_exports.string().optional().describe("GitHub repository name"),
29218
- repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")')
29219
- };
29220
- const createSystemHandler = async ({ title, description, strategy, teamspaceId, repoOwner, repoName, repoDefaultBranch }) => {
29221
- if (teamspaceId) {
29222
- const teamspace = await client.getTeamspace(teamspaceId);
29223
- if (!teamspace) {
29224
- return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
29225
- }
29226
- if (teamspace.orgId !== ctx.orgId) {
29227
- return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
29228
- }
29229
- }
29230
- try {
29231
- const project = await client.createSystem({
29232
- projectId: (0, import_crypto5.randomUUID)(),
29233
- orgId: ctx.orgId,
29234
- ownerUserId: ctx.user.userId,
29235
- title,
29236
- description,
29237
- strategy,
29238
- status: "active",
29239
- teamspaceId,
29240
- repoOwner,
29241
- repoName,
29242
- repoDefaultBranch
29243
- });
29244
- const text = [
29245
- `System created successfully.`,
29246
- "",
29247
- `**ID:** ${project.projectId}`,
29248
- `**Title:** ${project.title}`,
29249
- `**Status:** ${project.status}`,
29250
- project.description ? `**Description:** ${project.description}` : "",
29251
- project.repoOwner ? `**Repo:** ${project.repoOwner}/${project.repoName}` : "",
29252
- project.repoDefaultBranch ? `**Default Branch:** ${project.repoDefaultBranch}` : ""
29253
- ].filter(Boolean).join("\n");
29254
- return { content: [{ type: "text", text }] };
29255
- } catch (err) {
29256
- const message = err instanceof Error ? err.message : String(err);
29257
- return { content: [{ type: "text", text: `Failed to create system: ${message}` }], isError: true };
29258
- }
29259
- };
29260
- server.tool("create_system", "Create a new system in the configured organization", createSystemSchema, createSystemHandler);
29261
- server.tool(
29262
- "create_project",
29263
- "[Deprecated \u2014 use create_system] Create a new project in the configured organization",
29264
- {
29265
- title: external_exports.string().describe("Project title"),
29266
- description: external_exports.string().optional().describe("Project description"),
29267
- strategy: external_exports.string().optional().describe('Project strategy markdown \u2014 Org Strategy + Project Strategy ("Project Coda")'),
29268
- teamspaceId: external_exports.string().optional().describe("Teamspace ID to associate this project with"),
29269
- repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
29270
- repoName: external_exports.string().optional().describe("GitHub repository name"),
29271
- repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")')
29272
- },
29273
- createSystemHandler
29274
- );
29275
- }
29276
-
29277
28984
  // ../../libs/harmonica-services/src/mcp/tools/pulse-report-tools.ts
29278
28985
  function formatBeatRow(r) {
29279
28986
  const downbeatState = r.hasDownbeat ? "" : " \u2014 no Downbeat ever set";
@@ -29818,21 +29525,21 @@ function registerSessionTools(server, ctx, client) {
29818
29525
  }
29819
29526
  );
29820
29527
  server.tool(
29821
- "list_project_sessions",
29822
- 'List agent Sessions in a project, optionally filtered by status or whether terminal sessions are included. Use this to answer "show me sessions in this project" or to find recent runs across all beats.',
29528
+ "list_system_sessions",
29529
+ 'List agent Sessions in a system, optionally filtered by status or whether terminal sessions are included. Use this to answer "show me sessions in this system" or to find recent runs across all beats.',
29823
29530
  {
29824
- projectId: external_exports.string().min(1).describe("The project ID"),
29531
+ systemId: external_exports.string().min(1).describe("The system ID"),
29825
29532
  status: external_exports.enum(SESSION_STATUSES).optional().describe("Filter by status: active, idle, or closed"),
29826
29533
  includeTerminal: external_exports.boolean().optional().describe("Include terminal (closed) sessions. Default false.")
29827
29534
  },
29828
- async ({ projectId, status, includeTerminal }) => {
29829
- const sessions = await client.listProjectSessions(projectId, {
29535
+ async ({ systemId, status, includeTerminal }) => {
29536
+ const sessions = await client.listProjectSessions(systemId, {
29830
29537
  status,
29831
29538
  includeTerminal
29832
29539
  });
29833
29540
  return {
29834
29541
  content: [
29835
- { type: "text", text: formatSessionList(sessions, `project ${projectId}`) }
29542
+ { type: "text", text: formatSessionList(sessions, `system ${systemId}`) }
29836
29543
  ]
29837
29544
  };
29838
29545
  }
@@ -30062,18 +29769,18 @@ var import_promises = require("node:fs/promises");
30062
29769
  var import_node_os = require("node:os");
30063
29770
  var import_node_path = require("node:path");
30064
29771
 
30065
- // ../../libs/harmonica-services/src/project-snapshot.constants.ts
30066
- var SNAPSHOT_VERSION = 2;
29772
+ // ../../libs/harmonica-services/src/system-snapshot.constants.ts
29773
+ var SNAPSHOT_VERSION = 4;
30067
29774
 
30068
29775
  // ../../libs/harmonica-services/src/mcp/tools/snapshot-tools.ts
30069
29776
  function registerSnapshotTools(server, ctx, client) {
30070
29777
  server.tool(
30071
- "export_project_snapshot",
30072
- "Export a complete project snapshot for environment sync. On a remote server (production/staging) returns an S3 presigned URL; on a local server writes to a temp file and returns the path.",
30073
- { projectId: external_exports.string().describe("The project ID to export") },
30074
- async ({ projectId }) => {
30075
- await assertProjectInOrg(client, projectId, ctx.orgId);
30076
- const result = await client.exportProjectSnapshot(projectId);
29778
+ "export_system_snapshot",
29779
+ "Export a complete system snapshot for environment sync. On a remote server (production/staging) returns an S3 presigned URL; on a local server writes to a temp file and returns the path.",
29780
+ { systemId: external_exports.string().describe("The system ID to export") },
29781
+ async ({ systemId }) => {
29782
+ await assertProjectInOrg(client, systemId, ctx.orgId);
29783
+ const result = await client.exportProjectSnapshot(systemId);
30077
29784
  if (typeof result === "string") {
30078
29785
  if (!result.startsWith("https://")) {
30079
29786
  throw new Error(`exportProjectSnapshot returned an unexpected string value (expected an https:// presigned URL): ${result.slice(0, 80)}`);
@@ -30082,7 +29789,7 @@ function registerSnapshotTools(server, ctx, client) {
30082
29789
  content: [{
30083
29790
  type: "text",
30084
29791
  text: [
30085
- "Snapshot exported to S3. Pass this URL to import_project_snapshot on the target environment:",
29792
+ "Snapshot exported to S3. Pass this URL to import_system_snapshot on the target environment:",
30086
29793
  "",
30087
29794
  result,
30088
29795
  "",
@@ -30092,10 +29799,10 @@ function registerSnapshotTools(server, ctx, client) {
30092
29799
  };
30093
29800
  }
30094
29801
  const snapshot = normalizeSnapshot(result);
30095
- const filename = `harmonica-snapshot-${projectId}-${Date.now()}.json`;
29802
+ const filename = `harmonica-snapshot-${systemId}-${Date.now()}.json`;
30096
29803
  const filePath = (0, import_node_path.join)((0, import_node_os.tmpdir)(), filename);
30097
29804
  await (0, import_promises.writeFile)(filePath, JSON.stringify(snapshot), "utf-8");
30098
- const title = snapshot.project?.title ?? projectId;
29805
+ const title = snapshot.system?.title ?? systemId;
30099
29806
  const versionNote = snapshot.version !== SNAPSHOT_VERSION ? [`Warning: snapshot version ${snapshot.version} (local expects ${SNAPSHOT_VERSION}) \u2014 some counts may be zero.`, ""] : [];
30100
29807
  const summary = [
30101
29808
  `Exported "${title}" to: ${filePath}`,
@@ -30110,15 +29817,19 @@ function registerSnapshotTools(server, ctx, client) {
30110
29817
  ` PromptLogs: ${snapshot.promptLogs.length}`,
30111
29818
  ` Drops: ${snapshot.drops?.length ?? 0}`,
30112
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}`,
30113
29824
  "",
30114
- "Use import_project_snapshot with this file path to import into another environment."
29825
+ "Use import_system_snapshot with this file path to import into another environment."
30115
29826
  ];
30116
29827
  return { content: [{ type: "text", text: summary.join("\n") }] };
30117
29828
  }
30118
29829
  );
30119
29830
  server.tool(
30120
- "import_project_snapshot",
30121
- "Import a project snapshot into the current environment. Accepts an S3 presigned URL (https:// only), a file path, or inline JSON. Blocked in production by default.",
29831
+ "import_system_snapshot",
29832
+ "Import a system snapshot into the current environment. Accepts an S3 presigned URL (https:// only), a file path, or inline JSON. Blocked in production by default.",
30122
29833
  {
30123
29834
  snapshot: external_exports.string().describe("S3 presigned URL (https://...), file path, or inline JSON string"),
30124
29835
  targetTeamspaceId: external_exports.string().min(1).optional().describe(
@@ -30127,12 +29838,12 @@ function registerSnapshotTools(server, ctx, client) {
30127
29838
  },
30128
29839
  async ({ snapshot: snapshotInput, targetTeamspaceId }) => {
30129
29840
  const isProduction = process.env.NODE_ENV === "production";
30130
- const importAllowed = process.env.ALLOW_PROJECT_IMPORT === "true";
29841
+ const importAllowed = process.env.ALLOW_SYSTEM_IMPORT === "true";
30131
29842
  if (isProduction && !importAllowed) {
30132
29843
  return {
30133
29844
  content: [{
30134
29845
  type: "text",
30135
- text: "Import is disabled in production. Set ALLOW_PROJECT_IMPORT=true to override."
29846
+ text: "Import is disabled in production. Set ALLOW_SYSTEM_IMPORT=true to override."
30136
29847
  }],
30137
29848
  isError: true
30138
29849
  };
@@ -30143,7 +29854,7 @@ function registerSnapshotTools(server, ctx, client) {
30143
29854
  if (isPrivateHost(trimmed)) {
30144
29855
  throw new Error("Snapshot URL must point to a public host \u2014 private, link-local, and loopback addresses are not permitted.");
30145
29856
  }
30146
- const result2 = await client.importProjectSnapshotFromUrl(trimmed, importOptions);
29857
+ const result2 = await client.importSystemSnapshotFromUrl(trimmed, importOptions);
30147
29858
  return { content: [{ type: "text", text: formatImportSummary(result2, targetTeamspaceId) }] };
30148
29859
  }
30149
29860
  if (trimmed.startsWith("http://")) {
@@ -30158,21 +29869,25 @@ function registerSnapshotTools(server, ctx, client) {
30158
29869
  }
30159
29870
  assertSnapshotShape(parsed, "");
30160
29871
  const normalized = normalizeSnapshot(parsed);
30161
- const result = await client.importProjectSnapshot(normalized, importOptions);
29872
+ const result = await client.importSystemSnapshot(normalized, importOptions);
30162
29873
  return { content: [{ type: "text", text: formatImportSummary(result, targetTeamspaceId) }] };
30163
29874
  }
30164
29875
  );
30165
29876
  }
30166
29877
  function normalizeSnapshot(raw) {
29878
+ const anyRaw = raw;
30167
29879
  return {
30168
29880
  ...raw,
29881
+ // Compat: v3 and earlier snapshots have 'project' instead of 'system'
29882
+ system: raw.system ?? anyRaw["project"],
30169
29883
  beats: raw.beats ?? [],
30170
29884
  beatIndex: raw.beatIndex ?? [],
30171
29885
  proposals: raw.proposals ?? [],
30172
29886
  proposalIndex: raw.proposalIndex ?? [],
30173
29887
  revisions: raw.revisions ?? [],
30174
29888
  revisionIndex: raw.revisionIndex ?? [],
30175
- projectRevisionIndex: raw.projectRevisionIndex ?? [],
29889
+ // Compat: v3 and earlier snapshots have 'projectRevisionIndex'
29890
+ systemRevisionIndex: raw.systemRevisionIndex ?? anyRaw["projectRevisionIndex"] ?? [],
30176
29891
  activities: raw.activities ?? [],
30177
29892
  activityIndex: raw.activityIndex ?? [],
30178
29893
  notes: raw.notes ?? [],
@@ -30185,13 +29900,26 @@ function normalizeSnapshot(raw) {
30185
29900
  promptLogs: raw.promptLogs ?? [],
30186
29901
  beatVersions: raw.beatVersions ?? [],
30187
29902
  beatBeatVersionIndex: raw.beatBeatVersionIndex ?? [],
30188
- projectBeatVersionIndex: raw.projectBeatVersionIndex ?? [],
29903
+ // Compat: v3 and earlier snapshots have 'projectBeatVersionIndex'
29904
+ systemBeatVersionIndex: raw.systemBeatVersionIndex ?? anyRaw["projectBeatVersionIndex"] ?? [],
30189
29905
  beatVersionDropAssignments: raw.beatVersionDropAssignments ?? [],
30190
29906
  drops: raw.drops ?? [],
30191
29907
  accountDropIndex: raw.accountDropIndex ?? [],
30192
29908
  deliverables: raw.deliverables ?? [],
30193
29909
  teamspaceDeliverableIndex: raw.teamspaceDeliverableIndex ?? [],
30194
- 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 ?? []
30195
29923
  };
30196
29924
  }
30197
29925
  async function resolveSnapshotInput(input) {
@@ -30203,7 +29931,7 @@ async function resolveSnapshotInput(input) {
30203
29931
  }
30204
29932
  function formatImportSummary(result, targetTeamspaceId) {
30205
29933
  const lines = [
30206
- `Imported project: ${result.projectId}`,
29934
+ `Imported system: ${result.projectId}`,
30207
29935
  ` Beats: ${result.counts.beats}`,
30208
29936
  ` Proposals: ${result.counts.proposals}`,
30209
29937
  ` Revisions: ${result.counts.revisions}`,
@@ -30214,7 +29942,11 @@ function formatImportSummary(result, targetTeamspaceId) {
30214
29942
  ` PromptLogs: ${result.counts.promptLogs}`,
30215
29943
  ` Beat Versions: ${result.counts.beatVersions ?? 0}`,
30216
29944
  ` Drops: ${result.counts.drops ?? 0}`,
30217
- ` 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}`
30218
29950
  ];
30219
29951
  if (result.errors.length > 0) {
30220
29952
  lines.push("", `Errors (${result.errors.length}):`);
@@ -30261,9 +29993,9 @@ function assertSnapshotShape(val, sourceHint) {
30261
29993
  if (typeof obj["version"] !== "number" || obj["version"] <= 0) {
30262
29994
  throw new Error(`Snapshot${sourceHint} has an invalid or missing 'version' field \u2014 it may not be a valid project snapshot.`);
30263
29995
  }
30264
- const project = obj["project"];
30265
- if (typeof project !== "object" || project === null || typeof project["projectId"] !== "string") {
30266
- 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.`);
30267
29999
  }
30268
30000
  }
30269
30001
 
@@ -30343,13 +30075,276 @@ function registerSubscriptionTools(server, ctx, client) {
30343
30075
  );
30344
30076
  }
30345
30077
 
30346
- // ../../libs/harmonica-services/src/mcp/tools/teamspace-tools.ts
30347
- var import_node_crypto3 = require("node:crypto");
30348
- var EnsembleMemberSchema = external_exports.discriminatedUnion("type", [
30349
- external_exports.object({ type: external_exports.literal("human"), email: external_exports.string().email().max(254), name: external_exports.string().min(1).max(200) }),
30350
- external_exports.object({ type: external_exports.literal("agent"), agentId: external_exports.string().min(1).max(100), name: external_exports.string().min(1).max(200) })
30351
- ]);
30352
- function generateTeamspaceSlug(name) {
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 };
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
+ }
30252
+ };
30253
+ server.tool("update_system", "Update system settings such as title, description, repository configuration, or the Account that owns it", updateSystemSchema, updateSystemHandler);
30254
+ server.tool(
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
+ }
30266
+ try {
30267
+ const updated = await client.archiveSystem(systemId);
30268
+ if (!updated) {
30269
+ return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
30270
+ }
30271
+ return {
30272
+ content: [{
30273
+ type: "text",
30274
+ text: [
30275
+ "System archived successfully.",
30276
+ "",
30277
+ `**ID:** ${updated.projectId}`,
30278
+ `**Title:** ${updated.title}`,
30279
+ `**Status:** ${updated.status}`
30280
+ ].join("\n")
30281
+ }]
30282
+ };
30283
+ } catch (err) {
30284
+ const message = err instanceof Error ? err.message : String(err);
30285
+ return { content: [{ type: "text", text: `Failed to archive system: ${message}` }], isError: true };
30286
+ }
30287
+ }
30288
+ );
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 };
30306
+ }
30307
+ }
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);
30339
+ }
30340
+
30341
+ // ../../libs/harmonica-services/src/mcp/tools/teamspace-tools.ts
30342
+ var import_node_crypto3 = require("node:crypto");
30343
+ var EnsembleMemberSchema = external_exports.discriminatedUnion("type", [
30344
+ external_exports.object({ type: external_exports.literal("human"), email: external_exports.string().email().max(254), name: external_exports.string().min(1).max(200) }),
30345
+ external_exports.object({ type: external_exports.literal("agent"), agentId: external_exports.string().min(1).max(100), name: external_exports.string().min(1).max(200) })
30346
+ ]);
30347
+ function generateTeamspaceSlug(name) {
30353
30348
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
30354
30349
  }
30355
30350
  function formatEnsembleMember(member) {
@@ -30547,19 +30542,19 @@ function registerValueVelocityTools(server, ctx, client) {
30547
30542
  }
30548
30543
  );
30549
30544
  server.tool(
30550
- "list_project_beat_versions_ranked",
30551
- "Rank all Beat Versions in a project by Value Velocity score. Only Beat Versions with confirmed inputs appear. Results are sorted descending by chosen score. Use this to prioritize work or surface the highest-impact items first.",
30545
+ "list_system_beat_versions_ranked",
30546
+ "Rank all Beat Versions in a system by Value Velocity score. Only Beat Versions with confirmed inputs appear. Results are sorted descending by chosen score. Use this to prioritize work or surface the highest-impact items first.",
30552
30547
  {
30553
- projectId: external_exports.string().describe("The project ID"),
30548
+ systemId: external_exports.string().describe("The system ID"),
30554
30549
  lens: lensSchema.describe("Scoring lens: velocity | roi | valuePrimary (default: velocity)"),
30555
30550
  withLeverage: external_exports.boolean().optional().describe("Include cascade leverage scores (v2). Default: false.")
30556
30551
  },
30557
- async ({ projectId, lens, withLeverage }) => {
30552
+ async ({ systemId, lens, withLeverage }) => {
30558
30553
  try {
30559
- await assertProjectInOrg(client, projectId, ctx.orgId);
30554
+ await assertProjectInOrg(client, systemId, ctx.orgId);
30560
30555
  const [rankedResult, staleWResult] = await Promise.allSettled([
30561
- withLeverage ? client.rankProjectBeatVersionsWithLeverage(projectId, lens) : client.rankProjectBeatVersions(projectId, lens),
30562
- client.getStaleWBeatIds(projectId)
30556
+ withLeverage ? client.rankProjectBeatVersionsWithLeverage(systemId, lens) : client.rankProjectBeatVersions(systemId, lens),
30557
+ client.getStaleWBeatIds(systemId)
30563
30558
  ]);
30564
30559
  if (rankedResult.status === "rejected") {
30565
30560
  const msg = rankedResult.reason instanceof Error ? rankedResult.reason.message : String(rankedResult.reason);
@@ -30571,7 +30566,7 @@ function registerValueVelocityTools(server, ctx, client) {
30571
30566
  return { content: [{ type: "text", text: "No Beat Versions with confirmed Value Velocity inputs found." }] };
30572
30567
  }
30573
30568
  const lines = [
30574
- `Ranked Beat Versions for project ${projectId} (lens: ${lens}):`,
30569
+ `Ranked Beat Versions for system ${systemId} (lens: ${lens}):`,
30575
30570
  "",
30576
30571
  withLeverage ? "| Rank | Beat Version | Title | Chosen Score | Leverage Score |" : "| Rank | Beat Version | Title | Chosen Score |",
30577
30572
  withLeverage ? "|------|-------------|-------|-------------|----------------|" : "|------|-------------|-------|-------------|"
@@ -30798,8 +30793,15 @@ function registerValueVelocityTools(server, ctx, client) {
30798
30793
  // ../../libs/harmonica-services/src/mcp/tools/work-item-tools.ts
30799
30794
  var WORK_ITEM_STATUS_ENUM = ["not_started", "in_progress", "blocked", "done"];
30800
30795
  function formatOwner(owner) {
30801
- return `${owner.name} <${owner.email}>`;
30802
- }
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
+ ];
30803
30805
  function registerWorkItemTools(server, _ctx, client) {
30804
30806
  server.tool(
30805
30807
  "list_track_work_items",
@@ -30835,6 +30837,9 @@ function registerWorkItemTools(server, _ctx, client) {
30835
30837
  `Status: ${wi.status}`,
30836
30838
  wi.committedEstimateHours !== void 0 ? `Committed Estimate: ${wi.committedEstimateHours}h` : null,
30837
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,
30838
30843
  wi.beatVersionId ? `Linked Beat Version: ${wi.beatVersionId}` : null,
30839
30844
  estimateHint ? `Estimate Hint: ${estimateHint.hours}h (source: ${estimateHint.source})` : null,
30840
30845
  wi.createdBy ? `Created By: ${wi.createdBy}` : null,
@@ -30850,18 +30855,30 @@ function registerWorkItemTools(server, _ctx, client) {
30850
30855
  {
30851
30856
  trackId: external_exports.string().describe("The Track (DeliverableGroup) ID this work item belongs to"),
30852
30857
  title: external_exports.string().min(1).max(300).describe("Work item title"),
30853
- ownerName: external_exports.string().min(1).describe("Owner display name"),
30854
- 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."),
30855
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"),
30856
30864
  createdBy: external_exports.string().optional().describe("Optional creator identity for audit"),
30857
30865
  beatVersionId: external_exports.string().min(1).optional().describe("Optional Beat Version to link for capability lineage. Must exist.")
30858
30866
  },
30859
- 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
+ }
30860
30874
  const wi = await client.createWorkItem({
30861
30875
  trackId,
30862
30876
  title,
30863
- owner: { name: ownerName, email: ownerEmail },
30877
+ ...ownerName !== void 0 && ownerEmail !== void 0 && { owner: { name: ownerName, email: ownerEmail } },
30864
30878
  ...committedEstimateHours !== void 0 && { committedEstimateHours },
30879
+ ...effortSize !== void 0 && { effortSize },
30880
+ ...valueUnits !== void 0 && { valueUnits },
30881
+ ...costPerValueUnit !== void 0 && { costPerValueUnit },
30865
30882
  ...createdBy !== void 0 && { createdBy },
30866
30883
  ...beatVersionId !== void 0 && { beatVersionId }
30867
30884
  });
@@ -30877,17 +30894,21 @@ function registerWorkItemTools(server, _ctx, client) {
30877
30894
  );
30878
30895
  server.tool(
30879
30896
  "update_work_item",
30880
- "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.",
30881
30898
  {
30882
30899
  workItemId: external_exports.string().describe("The work item ID"),
30883
30900
  title: external_exports.string().min(1).max(300).optional().describe("New title"),
30884
30901
  ownerName: external_exports.string().min(1).optional().describe("New owner display name (requires ownerEmail too)"),
30885
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."),
30886
30904
  committedEstimateHours: external_exports.number().nonnegative().optional().describe("New committed estimate in hours"),
30887
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."),
30888
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.")
30889
30910
  },
30890
- async ({ workItemId, title, ownerName, ownerEmail, committedEstimateHours, actualHours, beatVersionId }) => {
30911
+ async ({ workItemId, title, ownerName, ownerEmail, unassignOwner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId }) => {
30891
30912
  const hasOwnerUpdate = ownerName !== void 0 || ownerEmail !== void 0;
30892
30913
  if (hasOwnerUpdate && (ownerName === void 0 || ownerEmail === void 0)) {
30893
30914
  return {
@@ -30895,7 +30916,13 @@ function registerWorkItemTools(server, _ctx, client) {
30895
30916
  isError: true
30896
30917
  };
30897
30918
  }
30898
- 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;
30899
30926
  if (!hasUpdates) {
30900
30927
  return {
30901
30928
  content: [{ type: "text", text: `No updates provided for work item: ${workItemId}` }],
@@ -30905,8 +30932,12 @@ function registerWorkItemTools(server, _ctx, client) {
30905
30932
  const wi = await client.updateWorkItem(workItemId, {
30906
30933
  ...title !== void 0 && { title },
30907
30934
  ...hasOwnerUpdate && { owner: { name: ownerName, email: ownerEmail } },
30935
+ ...unassignOwner === true && { owner: null },
30908
30936
  ...committedEstimateHours !== void 0 && { committedEstimateHours },
30909
30937
  ...actualHours !== void 0 && { actualHours },
30938
+ ...effortSize !== void 0 && { effortSize },
30939
+ ...valueUnits !== void 0 && { valueUnits },
30940
+ ...costPerValueUnit !== void 0 && { costPerValueUnit },
30910
30941
  ...beatVersionId !== void 0 && { beatVersionId }
30911
30942
  });
30912
30943
  if (!wi) {
@@ -31371,7 +31402,7 @@ var TOOL_PROFILES = Object.freeze({
31371
31402
  // Copilot's orchestrator degrades with large tool sets, so each module is trimmed
31372
31403
  // to its query tools; create_note is the single write action.
31373
31404
  "copilot": Object.freeze([
31374
- allow(registerProjectTools, ["list_systems", "get_system_context", "list_projects", "get_project_context"]),
31405
+ allow(registerProjectTools, ["list_systems", "get_system_context"]),
31375
31406
  allow(registerBeatTools, ["list_beats", "get_beat", "list_revisions", "get_revision"]),
31376
31407
  allow(registerBeatVersionTools, ["list_beat_versions", "get_beat_version"]),
31377
31408
  allow(registerNoteTools, ["list_notes", "get_note", "create_note", "list_documents"]),
@@ -31452,7 +31483,7 @@ function registerBeatResources(server, ctx, client) {
31452
31483
  async (uri, { projectId }) => {
31453
31484
  const pid = String(projectId);
31454
31485
  await assertProjectInOrg(client, pid, ctx.orgId);
31455
- const beats = await client.listProjectBeats(pid);
31486
+ const beats = await client.listSystemBeats(pid);
31456
31487
  const text = formatBeatSummaryTable(beats);
31457
31488
  return { contents: [{ uri: uri.href, text }] };
31458
31489
  }
@@ -31486,7 +31517,7 @@ function registerGuidelinesResources(server, client) {
31486
31517
  );
31487
31518
  }
31488
31519
 
31489
- // ../../libs/harmonica-services/src/mcp/resources/project-resources.ts
31520
+ // ../../libs/harmonica-services/src/mcp/resources/system-resources.ts
31490
31521
  function registerProjectResources(server, ctx, client) {
31491
31522
  server.resource(
31492
31523
  "project-context",
@@ -31494,7 +31525,7 @@ function registerProjectResources(server, ctx, client) {
31494
31525
  async (uri, { projectId }) => {
31495
31526
  const pid = String(projectId);
31496
31527
  const project = await fetchProjectInOrg(client, pid, ctx.orgId);
31497
- const notes = await client.listProjectNotes(pid, { limit: DEFAULT_CONTEXT_NOTE_LIMIT });
31528
+ const notes = await client.listSystemNotes(pid, { limit: DEFAULT_CONTEXT_NOTE_LIMIT });
31498
31529
  const text = formatProjectContext(project, notes);
31499
31530
  return { contents: [{ uri: uri.href, text }] };
31500
31531
  }
@@ -31815,7 +31846,7 @@ function createHttpClient(config2) {
31815
31846
  const result = await request("POST", `/api/systems/${encodeURIComponent(projectId)}/beats/next-id`);
31816
31847
  return result.beatId;
31817
31848
  },
31818
- listProjectBeats: async (projectId) => {
31849
+ listSystemBeats: async (projectId) => {
31819
31850
  const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/beats`);
31820
31851
  return (result?.beats ?? []).map((b) => ({
31821
31852
  ...b,
@@ -31971,6 +32002,21 @@ function createHttpClient(config2) {
31971
32002
  const result = await request("POST", `/api/layers/${encodeURIComponent(layerId)}/status`, { status, reason });
31972
32003
  return result?.layer;
31973
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
+ },
31974
32020
  listProjectNotes: async (projectId, filters) => {
31975
32021
  const params = new URLSearchParams();
31976
32022
  if (filters?.noteType) {
@@ -32110,6 +32156,14 @@ function createHttpClient(config2) {
32110
32156
  const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/revisions/pending${qs}`);
32111
32157
  return result ?? [];
32112
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
+ },
32113
32167
  listProjectRevisions: async (projectId, options) => {
32114
32168
  const params = new URLSearchParams();
32115
32169
  if (options?.status) params.set("status", options.status);
@@ -32339,6 +32393,14 @@ function createHttpClient(config2) {
32339
32393
  // partitions, but the REST surface is unified per the project-level
32340
32394
  // "avoid nested/duplicate routes" guidance.
32341
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
+ },
32342
32404
  listProjectSessions: async (projectId, options) => {
32343
32405
  const params = new URLSearchParams();
32344
32406
  params.set("projectId", projectId);
@@ -32526,6 +32588,14 @@ function createHttpClient(config2) {
32526
32588
  getTask: async (taskId) => {
32527
32589
  return request("GET", `/api/tasks/${encodeURIComponent(taskId)}`);
32528
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
+ },
32529
32599
  listProjectTasks: async (projectId, options) => {
32530
32600
  const params = new URLSearchParams();
32531
32601
  if (options?.status) params.set("status", options.status);
@@ -32557,6 +32627,15 @@ function createHttpClient(config2) {
32557
32627
  }, LONG_RUNNING_TIMEOUT_MS);
32558
32628
  },
32559
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
+ },
32560
32639
  listProjectActivities: async (projectId, options) => {
32561
32640
  const params = new URLSearchParams();
32562
32641
  if (options?.limit) params.set("limit", String(options.limit));
@@ -32838,8 +32917,8 @@ function createHttpClient(config2) {
32838
32917
  }
32839
32918
  return download;
32840
32919
  },
32841
- importProjectSnapshot: (snapshot, options) => request("POST", "/api/systems/import", { ...snapshot, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
32842
- 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),
32843
32922
  // Embedding similarity
32844
32923
  embedProjectEntities: (projectId) => request("POST", `/api/systems/${encodeURIComponent(projectId)}/embeddings/generate`),
32845
32924
  findSimilarNotes: async (noteId, projectId, options) => {
@@ -32905,6 +32984,18 @@ function createHttpClient(config2) {
32905
32984
  const res = await request("GET", `/api/checks/${encodeURIComponent(checkId)}`);
32906
32985
  return res.check;
32907
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
+ },
32908
32999
  listProjectChecks: async (projectId, checkType, options) => {
32909
33000
  const params = new URLSearchParams();
32910
33001
  if (checkType) params.set("type", checkType);
@@ -33003,6 +33094,11 @@ function createHttpClient(config2) {
33003
33094
  if (res === void 0) throw new Error(`Teamspace ${teamspaceId} not found`);
33004
33095
  return res.deliverableGroups;
33005
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
+ },
33006
33102
  listProjectDeliverableGroups: async (projectId) => {
33007
33103
  const res = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/deliverable-groups`);
33008
33104
  if (res === void 0) throw new Error(`Project ${projectId} not found`);
@@ -33182,11 +33278,14 @@ function createHttpClient(config2) {
33182
33278
  },
33183
33279
  // WorkItems (WorkItem exposure layer — owned, statused unit of operational work under a Track)
33184
33280
  createWorkItem: async (input) => {
33185
- const { trackId, title, owner, committedEstimateHours, createdBy, beatVersionId } = input;
33281
+ const { trackId, title, owner, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId } = input;
33186
33282
  const body = {
33187
33283
  title,
33188
- owner,
33284
+ ...owner !== void 0 && { owner },
33189
33285
  ...committedEstimateHours !== void 0 && { committedEstimateHours },
33286
+ ...effortSize !== void 0 && { effortSize },
33287
+ ...valueUnits !== void 0 && { valueUnits },
33288
+ ...costPerValueUnit !== void 0 && { costPerValueUnit },
33190
33289
  ...createdBy !== void 0 && { createdBy },
33191
33290
  ...beatVersionId !== void 0 && { beatVersionId }
33192
33291
  };
@@ -33729,7 +33828,7 @@ function loadConfig() {
33729
33828
  };
33730
33829
  }
33731
33830
  async function main() {
33732
- console.error(`[harmonica-mcp] v${"1.1.0"} starting\u2026`);
33831
+ console.error(`[harmonica-mcp] v${"2.1.0"} starting\u2026`);
33733
33832
  const config2 = loadConfig();
33734
33833
  const client = createHttpClient({
33735
33834
  apiBaseUrl: config2.apiBaseUrl,