@codazen/harmonica-mcp 2.1.0 → 3.0.1

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 +412 -412
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -21526,6 +21526,17 @@ async function assertProjectInOrg(client, projectId, orgId) {
21526
21526
  );
21527
21527
  }
21528
21528
  }
21529
+ async function assertRevisionInProject(client, revisionId, projectId) {
21530
+ const revision = await client.getRevision(revisionId);
21531
+ if (!revision) {
21532
+ throw new Error(`Revision not found: "${revisionId}"`);
21533
+ }
21534
+ if (revision.projectId !== projectId) {
21535
+ throw new Error(
21536
+ `Revision "${revisionId}" does not belong to project "${projectId}". Access denied.`
21537
+ );
21538
+ }
21539
+ }
21529
21540
  async function assertBeatInOrg(client, beatId, orgId) {
21530
21541
  const beat = await client.getBeat(beatId);
21531
21542
  if (!beat) {
@@ -22421,11 +22432,18 @@ function registerBeatPlanningTools(server, ctx, client) {
22421
22432
 
22422
22433
  // ../../libs/harmonica-services/src/mcp/check.constants.ts
22423
22434
  var CHECK_MAX_SCORES = {
22435
+ beat_coda_quality: 5,
22436
+ // Legacy name for beat_coda_quality — kept until every caller passes the new name (N-4E09-6779).
22424
22437
  beat_quality: 5,
22438
+ beat_version_quality: 5,
22439
+ // Legacy name for beat_version_quality — kept until every caller passes the new name.
22425
22440
  plan_quality: 5,
22426
22441
  build_quality: 5,
22442
+ // Retired from the taxonomy (B-308 v3) — kept so historical records still
22443
+ // render with the right denominator.
22427
22444
  pii_scan: 100,
22428
22445
  portfolio_coherence: 5,
22446
+ revision_quality: 5,
22429
22447
  drop_quality: 5
22430
22448
  };
22431
22449
 
@@ -22467,8 +22485,8 @@ ${d.rationale}${d.suggestions?.length ? `
22467
22485
  \u2022 ${d.suggestions.join("\n\u2022 ")}` : ""}`
22468
22486
  ).join("\n\n");
22469
22487
  return [
22470
- `## Beat Quality Check \u2014 ${check2.targetId}`,
22471
- `**Overall Score:** ${check2.overallScore}/${CHECK_MAX_SCORES.beat_quality}`,
22488
+ `## Beat Coda Quality Check \u2014 ${check2.targetId}`,
22489
+ `**Overall Score:** ${check2.overallScore}/${CHECK_MAX_SCORES.beat_coda_quality}`,
22472
22490
  `**Summary:** ${check2.summary}`,
22473
22491
  check2.topSuggestion ? `**Top Suggestion:** ${check2.topSuggestion}` : "",
22474
22492
  "",
@@ -22476,35 +22494,46 @@ ${d.rationale}${d.suggestions?.length ? `
22476
22494
  ].filter(Boolean).join("\n");
22477
22495
  }
22478
22496
  function registerBeatQualityTools(server, ctx, client) {
22479
- server.tool(
22480
- "check_beat_quality",
22481
- "Enqueue a Beat quality check across 6 dimensions (Distinctive, Harmonious, Substantial, Durable, Clear, Strategically Aligned). By default returns a jobId immediately (fire-and-forget) \u2014 use get_job_status or list_checks to retrieve results. Set wait=true to block until the check completes and receive the scorecard inline. WARNING: wait=true holds the MCP session open for 30\u201360s per check \u2014 for bulk operations (more than one Beat), omit wait and poll get_job_status separately to avoid session exhaustion.",
22482
- {
22483
- beatId: external_exports.string().describe("The beat ID (e.g., TF-B-001)"),
22484
- projectId: external_exports.string().describe("The project ID"),
22485
- wait: external_exports.boolean().optional().describe("If true, block until the check completes and return the scorecard inline. Default: false (returns taskId immediately).")
22486
- },
22487
- async ({ beatId, projectId, wait }) => {
22488
- try {
22489
- await assertBeatInOrg(client, beatId, ctx.orgId);
22490
- const { taskId } = await client.checkBeatQuality(projectId, beatId);
22491
- if (!wait) {
22492
- return {
22493
- content: [{
22494
- type: "text",
22495
- text: `Quality check enqueued for ${beatId}.
22497
+ const schema = {
22498
+ beatId: external_exports.string().describe("The beat ID (e.g., TF-B-001)"),
22499
+ projectId: external_exports.string().describe("The project ID"),
22500
+ wait: external_exports.boolean().optional().describe("If true, block until the check completes and return the scorecard inline. Default: false (returns taskId immediately).")
22501
+ };
22502
+ const handler = async ({ beatId, projectId, wait }) => {
22503
+ try {
22504
+ await assertBeatInOrg(client, beatId, ctx.orgId);
22505
+ const { taskId } = await client.checkBeatQuality(projectId, beatId);
22506
+ if (!wait) {
22507
+ return {
22508
+ content: [{
22509
+ type: "text",
22510
+ text: `Quality check enqueued for ${beatId}.
22496
22511
  Job ID: ${taskId}
22497
22512
 
22498
22513
  Use get_job_status with jobId="${taskId}" to poll for results, or list_checks to retrieve the completed scorecard once done.`
22499
- }]
22500
- };
22501
- }
22502
- const check2 = await pollForCheck(client, taskId);
22503
- return { content: [{ type: "text", text: formatScorecard(check2) }] };
22504
- } catch (err) {
22505
- const message = err instanceof Error ? err.message : String(err);
22506
- return { content: [{ type: "text", text: `Quality check failed: ${message}` }], isError: true };
22514
+ }]
22515
+ };
22507
22516
  }
22517
+ const check2 = await pollForCheck(client, taskId);
22518
+ return { content: [{ type: "text", text: formatScorecard(check2) }] };
22519
+ } catch (err) {
22520
+ const message = err instanceof Error ? err.message : String(err);
22521
+ return { content: [{ type: "text", text: `Quality check failed: ${message}` }], isError: true };
22522
+ }
22523
+ };
22524
+ server.tool(
22525
+ "check_beat_coda_quality",
22526
+ "Enqueue a Beat definition quality check across 7 dimensions (Distinctive, Harmonious, Substantial, Durable, Clear, Strategically Aligned, Business Outcome) \u2014 grades the Beat's Coda and directing context, persisted as beat_coda_quality. By default returns a jobId immediately (fire-and-forget) \u2014 use get_job_status or list_checks to retrieve results. Set wait=true to block until the check completes and receive the scorecard inline. WARNING: wait=true holds the MCP session open for 30\u201360s per check \u2014 for bulk operations (more than one Beat), omit wait and poll get_job_status separately to avoid session exhaustion.",
22527
+ schema,
22528
+ handler
22529
+ );
22530
+ server.tool(
22531
+ "check_beat_quality",
22532
+ "DEPRECATED \u2014 renamed to check_beat_coda_quality (the check grades the Beat's definition: its Coda and directing context). This alias runs the same check and persists it as beat_coda_quality. Prefer check_beat_coda_quality.",
22533
+ schema,
22534
+ async (args) => {
22535
+ console.warn("[deprecated-tool] check_beat_quality invoked \u2014 use check_beat_coda_quality (N-4E09-6779)");
22536
+ return handler(args);
22508
22537
  }
22509
22538
  );
22510
22539
  }
@@ -22747,7 +22776,8 @@ function formatNoteSummary(note) {
22747
22776
  const crit = note.criticality != null ? ` (criticality: ${note.criticality.toFixed(2)})` : "";
22748
22777
  const scope = note.beatId ? ` [Beat: ${note.beatId}]` : "";
22749
22778
  const rev = note.revisionId ? ` [Rev: ${note.revisionId}]` : "";
22750
- return `- **${type}** (${note.noteId})${status}${crit}${scope}${rev}: ${note.content}`;
22779
+ const bv = note.beatVersionId ? ` [BV: ${note.beatVersionId}]` : "";
22780
+ return `- **${type}** (${note.noteId})${status}${crit}${scope}${rev}${bv}: ${note.content}`;
22751
22781
  }
22752
22782
  function formatNoteList(notes) {
22753
22783
  if (notes.length === 0) {
@@ -22805,6 +22835,9 @@ function formatNoteDetail(note) {
22805
22835
  if (note.revisionId) {
22806
22836
  parts.push(`**Revision:** ${note.revisionId}`);
22807
22837
  }
22838
+ if (note.beatVersionId) {
22839
+ parts.push(`**Beat Version:** ${note.beatVersionId}`);
22840
+ }
22808
22841
  if (note.assumptionMeta) {
22809
22842
  parts.push("", "### Assumption Details");
22810
22843
  parts.push(`- Confidence: ${note.assumptionMeta.confidence}`);
@@ -23360,7 +23393,7 @@ function registerBeatVersionTools(server, ctx, client) {
23360
23393
  );
23361
23394
  server.tool(
23362
23395
  "create_beat_version",
23363
- "Create a new Beat Version for an existing Beat. A Beat Version is a shippable increment \u2014 one planned delivery of the Beat's capability. Starts at active status (binary: active | archived). Run check_plan_quality on the Beat Version BEFORE creating its first Revision \u2014 the first Revision is gated on a recent, passing plan_quality check.",
23396
+ "Create a new Beat Version for an existing Beat. A Beat Version is a shippable increment \u2014 one planned delivery of the Beat's capability. Starts at active status (binary: active | archived). Run check_beat_version_quality on the Beat Version BEFORE creating its first Revision \u2014 the first Revision is gated on a recent, passing beat_version_quality check.",
23364
23397
  {
23365
23398
  beatId: external_exports.string().describe("The Beat ID to create the Beat Version on"),
23366
23399
  title: external_exports.string().min(1).max(300).describe("What this Beat Version delivers (concise, outcome-focused)"),
@@ -23417,7 +23450,7 @@ function registerBeatVersionTools(server, ctx, client) {
23417
23450
  `**Status:** ${beatVersion.status}`,
23418
23451
  `**Version:** v${beatVersion.versionNumber}`,
23419
23452
  "",
23420
- `Next step: Run check_plan_quality on this Beat Version before creating its first Revision (the first Revision is gated on a passing plan_quality check).`
23453
+ `Next step: Run check_beat_version_quality on this Beat Version before creating its first Revision (the first Revision is gated on a passing beat_version_quality check).`
23421
23454
  ];
23422
23455
  return { content: [{ type: "text", text: lines.join("\n") }] };
23423
23456
  } catch (err) {
@@ -23428,7 +23461,7 @@ function registerBeatVersionTools(server, ctx, client) {
23428
23461
  );
23429
23462
  server.tool(
23430
23463
  "update_beat_version",
23431
- "Update a Beat Version's content fields (title, description, scope, assignees, etc.). Does NOT change lifecycle state \u2014 use transition_beat_version for that. Editing the Beat Version after a plan_quality check will invalidate the check \u2014 re-run before creating its first Revision.",
23464
+ "Update a Beat Version's content fields (title, description, scope, assignees, etc.). Does NOT change lifecycle state \u2014 use transition_beat_version for that. Editing the Beat Version after a beat_version_quality check will invalidate the check \u2014 re-run before creating its first Revision.",
23432
23465
  {
23433
23466
  beatVersionId: external_exports.string().describe("The Beat Version ID (e.g., bv-abc123)"),
23434
23467
  title: external_exports.string().min(1).max(300).optional().describe("New title"),
@@ -23481,9 +23514,9 @@ function registerBeatVersionTools(server, ctx, client) {
23481
23514
  );
23482
23515
  server.tool(
23483
23516
  "plan_beat_versions",
23484
- "Agent-decompose a composed Beat into 2-5 planning-state Beat Versions on the path to the Coda. Uses the Beat's Coda, project strategy, sibling Beats, Notes, and codebase grep as context. Requires a recent passing beat_quality check (run check_beat_quality first if needed). Idempotent: refuses when planned BVs already exist on the Beat unless force=true is set.",
23517
+ "Agent-decompose a composed Beat into 2-5 planning-state Beat Versions on the path to the Coda. Uses the Beat's Coda, project strategy, sibling Beats, Notes, and codebase grep as context. Requires a recent passing beat_coda_quality check (run check_beat_coda_quality first if needed). Idempotent: refuses when planned BVs already exist on the Beat unless force=true is set.",
23485
23518
  {
23486
- beatId: external_exports.string().describe("The Beat ID to decompose (must have a Coda and a passing beat_quality check)"),
23519
+ beatId: external_exports.string().describe("The Beat ID to decompose (must have a Coda and a passing beat_coda_quality check)"),
23487
23520
  force: external_exports.boolean().optional().describe("Bypass the idempotency guard and plan additional Beat Versions even when prior planned BVs are still active")
23488
23521
  },
23489
23522
  async ({ beatId, force }) => {
@@ -23499,7 +23532,7 @@ function registerBeatVersionTools(server, ctx, client) {
23499
23532
  lines.push(`- **${bv.beatVersionId}** v${bv.versionNumber} [${bv.status}]: ${bv.title}`);
23500
23533
  if (bv.changeSummary) lines.push(` Change: ${bv.changeSummary}`);
23501
23534
  }
23502
- lines.push("", "Run check_plan_quality on each Beat Version before creating its first Revision (the first Revision is gated on a passing plan_quality check).");
23535
+ lines.push("", "Run check_beat_version_quality on each Beat Version before creating its first Revision (the first Revision is gated on a passing beat_version_quality check).");
23503
23536
  return { content: [{ type: "text", text: lines.join("\n") }] };
23504
23537
  } catch (err) {
23505
23538
  if (err instanceof BeatQualityGateError) {
@@ -23536,7 +23569,7 @@ function registerBeatVersionTools(server, ctx, client) {
23536
23569
  );
23537
23570
  server.tool(
23538
23571
  "transition_beat_version",
23539
- 'Transition a Beat Version between binary statuses: active \u2194 archived. Archiving requires a reason; reviving an archived Beat Version back to active does not. Pipeline position ("where is this?") is derived from child Revisions and the Drop, not from status. plan_quality is now enforced at first-Revision creation, not on any Beat Version transition.',
23572
+ 'Transition a Beat Version between binary statuses: active \u2194 archived. Archiving requires a reason; reviving an archived Beat Version back to active does not. Pipeline position ("where is this?") is derived from child Revisions and the Drop, not from status. beat_version_quality is now enforced at first-Revision creation, not on any Beat Version transition.',
23540
23573
  {
23541
23574
  beatVersionId: external_exports.string().describe("The Beat Version ID (e.g., bv-abc123)"),
23542
23575
  targetState: external_exports.enum(BEAT_VERSION_STATUSES).describe("The target status (active | archived)"),
@@ -23673,12 +23706,41 @@ function registerCadenceScheduleTools(server, _ctx, client) {
23673
23706
  );
23674
23707
  }
23675
23708
 
23709
+ // ../../libs/harmonica-services/src/check-target-validation.ts
23710
+ function isBeatVersionTargetId(targetId) {
23711
+ return /^bv-\S/.test(targetId);
23712
+ }
23713
+ function beatVersionQualityTargetError(targetId) {
23714
+ if (typeof targetId !== "string") {
23715
+ return "beat_version_quality requires a Beat Version ID (bv-\u2026), but no targetId was provided.";
23716
+ }
23717
+ if (targetId.startsWith("rev-")) {
23718
+ return "beat_version_quality targets a Beat Version, not a Revision \u2014 pass a Beat Version ID (bv-\u2026). Revisions inherit their parent Beat Version's plan and grade; run beat_version_quality on the parent Beat Version, or build_quality for a build-readiness grade.";
23719
+ }
23720
+ if (!isBeatVersionTargetId(targetId)) {
23721
+ return `beat_version_quality requires a Beat Version ID (bv-\u2026), got '${targetId}'.`;
23722
+ }
23723
+ return null;
23724
+ }
23725
+ function isRevisionTargetId(targetId) {
23726
+ return /^rev-\S/.test(targetId);
23727
+ }
23728
+ function revisionQualityTargetError(targetId) {
23729
+ if (typeof targetId !== "string" || !isRevisionTargetId(targetId)) {
23730
+ return `revision_quality runs only on Revisions (rev-*); got "${targetId ?? ""}". For a Beat Version, run beat_version_quality on the Beat Version.`;
23731
+ }
23732
+ return null;
23733
+ }
23734
+
23676
23735
  // ../../libs/harmonica-services/src/build-quality/target-validation.ts
23677
23736
  function buildQualityTargetError(targetId) {
23737
+ if (typeof targetId !== "string") {
23738
+ return "build_quality requires a Beat Version ID (bv-\u2026), but no targetId was provided.";
23739
+ }
23678
23740
  if (targetId.startsWith("rev-")) {
23679
23741
  return "build_quality now targets a Beat Version, not a Revision \u2014 pass a Beat Version ID (bv-\u2026). The check moved to the Beat Version so it grades the planned capability against the codebase and runs even when there are no Revisions yet.";
23680
23742
  }
23681
- if (!/^bv-\S/.test(targetId)) {
23743
+ if (!isBeatVersionTargetId(targetId)) {
23682
23744
  return `build_quality requires a Beat Version ID (bv-\u2026), got '${targetId}'.`;
23683
23745
  }
23684
23746
  return null;
@@ -23713,7 +23775,7 @@ function registerCheckTools(server, ctx, client) {
23713
23775
  beatId: external_exports.string().optional().describe("List checks for this specific beat"),
23714
23776
  beatVersionId: external_exports.string().optional().describe("List checks for this specific beat version"),
23715
23777
  revisionId: external_exports.string().optional().describe("List checks for this specific revision"),
23716
- checkType: external_exports.enum(["beat_quality", "plan_quality", "build_quality", "pii_scan", "portfolio_coherence"]).optional().describe("Filter by check type")
23778
+ checkType: external_exports.enum(["beat_coda_quality", "beat_quality", "beat_version_quality", "plan_quality", "build_quality", "pii_scan", "portfolio_coherence"]).optional().describe("Filter by check type. beat_quality is the DEPRECATED alias of beat_coda_quality. plan_quality is the DEPRECATED alias of beat_version_quality. pii_scan is RETIRED from the taxonomy (PII detection moved to the Scan family, N-4E09-6809) \u2014 it is still accepted here so historical records stay readable, but no new pii_scan checks are written.")
23717
23779
  },
23718
23780
  async ({ projectId, beatId, beatVersionId, revisionId, checkType }) => {
23719
23781
  try {
@@ -23840,10 +23902,10 @@ function registerCheckTools(server, ctx, client) {
23840
23902
  );
23841
23903
  server.tool(
23842
23904
  "run_check",
23843
- "Run a check on a Beat or Beat Version. beat_quality: 6-dimension Beat quality (1-5), target a beatId. plan_quality: 6-dimension plan quality (1-5), target a Beat Version (bv-*). build_quality: 8-dimension engineering assessment (1-5), target a Beat Version (bv-*) \u2014 build_quality moved from the Revision to the Beat Version, so it grades the planned capability against the codebase and runs even when the Beat Version has no Revisions yet.",
23905
+ "Run a check on a Beat or Beat Version. beat_coda_quality: 7-dimension Beat definition quality (1-5) \u2014 grades the Coda and directing context, target a beatId (beat_quality is its DEPRECATED alias and persists under the new name). beat_version_quality: 7-dimension plan quality (1-5) including completeness, target a Beat Version (bv-*) \u2014 plan_quality is its DEPRECATED alias. build_quality: 9-dimension engineering assessment (1-5), target a Beat Version (bv-*) \u2014 build_quality moved from the Revision to the Beat Version, so it grades the planned capability against the codebase and runs even when the Beat Version has no Revisions yet.",
23844
23906
  {
23845
- checkType: external_exports.enum(["beat_quality", "plan_quality", "build_quality"]).describe("Type of check to run"),
23846
- targetId: external_exports.string().describe("The target entity ID \u2014 a beatId for beat_quality, or a Beat Version ID (bv-*) for plan_quality and build_quality"),
23907
+ checkType: external_exports.enum(["beat_coda_quality", "beat_quality", "beat_version_quality", "plan_quality", "build_quality"]).describe("Type of check to run. beat_quality is the deprecated alias of beat_coda_quality. plan_quality is the deprecated alias of beat_version_quality."),
23908
+ targetId: external_exports.string().describe("The target entity ID \u2014 a beatId for beat_coda_quality, or a Beat Version ID (bv-*) for beat_version_quality and build_quality"),
23847
23909
  projectId: external_exports.string().describe("The project ID"),
23848
23910
  branch: external_exports.string().optional().describe("Override the git branch for code checks (default: project default branch)"),
23849
23911
  localPath: external_exports.string().optional().describe("Absolute path to an already-cloned local copy of the repo \u2014 skips git clone entirely (useful for large repos or offline use)")
@@ -23851,11 +23913,9 @@ function registerCheckTools(server, ctx, client) {
23851
23913
  async ({ checkType, targetId, projectId, branch, localPath }) => {
23852
23914
  try {
23853
23915
  await assertProjectInOrg(client, projectId, ctx.orgId);
23854
- if (checkType === "build_quality") {
23855
- const targetError = buildQualityTargetError(targetId);
23856
- if (targetError) {
23857
- return { content: [{ type: "text", text: targetError }], isError: true };
23858
- }
23916
+ const targetError = checkType === "build_quality" ? buildQualityTargetError(targetId) : checkType === "beat_version_quality" || checkType === "plan_quality" ? beatVersionQualityTargetError(targetId) : null;
23917
+ if (targetError) {
23918
+ return { content: [{ type: "text", text: targetError }], isError: true };
23859
23919
  }
23860
23920
  const opts = branch || localPath ? { branch, localPath } : void 0;
23861
23921
  const task = await client.runCheck(projectId, checkType, targetId, opts);
@@ -23876,10 +23936,13 @@ function registerCheckTools(server, ctx, client) {
23876
23936
  );
23877
23937
  }
23878
23938
  var CHECK_TYPE_DISPLAY = {
23879
- beat_quality: "Quality",
23880
- plan_quality: "Plan Quality",
23939
+ beat_coda_quality: "Coda Quality",
23940
+ beat_quality: "Coda Quality",
23941
+ beat_version_quality: "Beat Version Quality",
23942
+ plan_quality: "Beat Version Quality",
23881
23943
  build_quality: "Build Quality",
23882
- pii_scan: "PII Scan",
23944
+ // Retired (B-308 v3) — label retained so historical records never render unlabeled.
23945
+ pii_scan: "PII Scan (retired)",
23883
23946
  portfolio_coherence: "Portfolio Coherence"
23884
23947
  };
23885
23948
  function formatCheck(a) {
@@ -24047,6 +24110,11 @@ function registerDeliverableGroupTools(server, ctx, client) {
24047
24110
  content: [{ type: "text", text: `DeliverableGroup not found: ${deliverableGroupId}` }]
24048
24111
  };
24049
24112
  }
24113
+ if (g.orgId !== ctx.orgId) {
24114
+ throw new Error(
24115
+ `DeliverableGroup "${deliverableGroupId}" does not belong to the configured organization. Access denied.`
24116
+ );
24117
+ }
24050
24118
  const lines = [
24051
24119
  `ID: ${g.deliverableGroupId}`,
24052
24120
  `Code: ${g.groupCode}`,
@@ -24055,6 +24123,10 @@ function registerDeliverableGroupTools(server, ctx, client) {
24055
24123
  `Status: ${g.status}`,
24056
24124
  `VU Rate: ${g.vuRate}`,
24057
24125
  g.description ? `Description: ${g.description}` : null,
24126
+ g.deliverables ? `Deliverables:
24127
+ ${g.deliverables}` : null,
24128
+ g.acceptanceCriteria ? `Acceptance Criteria:
24129
+ ${g.acceptanceCriteria}` : null,
24058
24130
  g.parentGroupId ? `Parent Group: ${g.parentGroupId}` : null,
24059
24131
  g.proposedAt ? `Proposed: ${g.proposedAt}` : null,
24060
24132
  g.approvedAt ? `Approved: ${g.approvedAt}` : null,
@@ -24220,51 +24292,6 @@ function registerDeliverableGroupTools(server, ctx, client) {
24220
24292
  return { content: [{ type: "text", text }] };
24221
24293
  }
24222
24294
  );
24223
- server.tool(
24224
- "assign_beat_version_to_deliverable_group",
24225
- 'Assign a Beat Version to a DeliverableGroup (the Work Plan / WBS link). A Beat Version belongs to at most one group; assigning moves it if it was in another group. Use to answer "put this feature increment under this SOW/phase".',
24226
- {
24227
- deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID"),
24228
- beatVersionId: external_exports.string().describe("The Beat Version ID to place in the group")
24229
- },
24230
- async ({ deliverableGroupId, beatVersionId }) => {
24231
- await assertDeliverableGroupInOrg(client, deliverableGroupId, ctx.orgId);
24232
- const bv = await client.getBeatVersion(beatVersionId);
24233
- if (!bv) {
24234
- return { content: [{ type: "text", text: `Beat Version not found: ${beatVersionId}` }] };
24235
- }
24236
- await assertProjectInOrg(client, bv.projectId, ctx.orgId);
24237
- const ok = await client.setBeatVersionDeliverableGroup(deliverableGroupId, beatVersionId);
24238
- if (!ok) {
24239
- return { content: [{ type: "text", text: `Beat Version not found: ${beatVersionId}` }] };
24240
- }
24241
- return {
24242
- content: [{ type: "text", text: `Assigned Beat Version ${beatVersionId} to DeliverableGroup ${deliverableGroupId}.` }]
24243
- };
24244
- }
24245
- );
24246
- server.tool(
24247
- "clear_beat_version_deliverable_group",
24248
- 'Remove a Beat Version from a DeliverableGroup (clears its Work Plan link). The Beat Version must currently belong to the named group. Use to answer "take this increment out of this SOW/phase".',
24249
- {
24250
- deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID the Beat Version currently belongs to"),
24251
- beatVersionId: external_exports.string().describe("The Beat Version ID to remove from the group")
24252
- },
24253
- async ({ deliverableGroupId, beatVersionId }) => {
24254
- await assertDeliverableGroupInOrg(client, deliverableGroupId, ctx.orgId);
24255
- const bv = await client.getBeatVersion(beatVersionId);
24256
- if (!bv) {
24257
- return { content: [{ type: "text", text: `Beat Version not found: ${beatVersionId}` }] };
24258
- }
24259
- if (bv.deliverableGroupId !== deliverableGroupId) {
24260
- return { content: [{ type: "text", text: `Beat Version ${beatVersionId} is not a member of DeliverableGroup ${deliverableGroupId}.` }] };
24261
- }
24262
- await client.clearBeatVersionDeliverableGroup(deliverableGroupId, beatVersionId);
24263
- return {
24264
- content: [{ type: "text", text: `Cleared Beat Version ${beatVersionId} from DeliverableGroup ${deliverableGroupId}.` }]
24265
- };
24266
- }
24267
- );
24268
24295
  server.tool(
24269
24296
  "assign_drop_to_deliverable_group",
24270
24297
  `Link a Drop to a DeliverableGroup so it appears on the parent Movement's timed-release axis (the Drops shown on the Movement overview). A Drop belongs to at most one group; linking moves it if it was in another group. Use to answer "put this release under this SOW/phase" or "add a Drop to this Movement".`,
@@ -24290,17 +24317,20 @@ function registerDeliverableGroupTools(server, ctx, client) {
24290
24317
  );
24291
24318
  server.tool(
24292
24319
  "list_deliverable_group_beat_versions",
24293
- 'List the Beat Versions assigned to a DeliverableGroup (its Work Plan). Use to answer "what feature increments are scoped under this SOW/phase?"',
24320
+ 'List the Beat Versions linked to a DeliverableGroup via its work items (Work Plan). Use to answer "what feature increments are scoped under this SOW/phase?"',
24294
24321
  {
24295
24322
  deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID")
24296
24323
  },
24297
24324
  async ({ deliverableGroupId }) => {
24298
24325
  await assertDeliverableGroupInOrg(client, deliverableGroupId, ctx.orgId);
24299
- const items = await client.listDeliverableGroupBeatVersions(deliverableGroupId);
24300
- if (items.length === 0) {
24301
- return { content: [{ type: "text", text: "No beat versions in this group." }] };
24302
- }
24303
- const hydrated = await Promise.all(items.map((b) => client.getBeatVersion(b.beatVersionId)));
24326
+ const workItems = await client.listTrackWorkItems(deliverableGroupId);
24327
+ const bvIds = [...new Set(
24328
+ workItems.map((wi) => wi.beatVersionId).filter((id) => id !== void 0)
24329
+ )];
24330
+ if (bvIds.length === 0) {
24331
+ return { content: [{ type: "text", text: "No beat versions linked via work items in this group." }] };
24332
+ }
24333
+ const hydrated = await Promise.all(bvIds.map((id) => client.getBeatVersion(id)));
24304
24334
  const text = hydrated.filter((bv) => bv !== void 0).map((bv) => `[${bv.beatVersionId}] v${bv.versionNumber} \u2014 ${bv.title} (${bv.status})`).join("\n");
24305
24335
  return { content: [{ type: "text", text }] };
24306
24336
  }
@@ -24421,7 +24451,7 @@ function formatDownbeatHarmonyReport(r) {
24421
24451
  function registerDownbeatHarmonyReportTools(server, _ctx, client) {
24422
24452
  server.tool(
24423
24453
  "get_downbeat_harmony_report",
24424
- "Get the Downbeat Harmony Report: a read-only qualification advisor that extends the Pulse report's per-Beat signals (increments since Downbeat, edited-goal drift, relational drift) with a Measure/convergence mismatch check, and returns which open Beats under an Account qualify for a Downbeat this cycle \u2014 citing exactly which signal(s) triggered each one. Never anchors a Downbeat; the caller decides whether to accept and calls set_downbeat per qualifying Beat themselves.",
24454
+ "Get the Downbeat Harmony Report: a read-only qualification advisor that extends the Pulse report's per-Beat signals (increments since Downbeat, edited-goal drift, relational drift) with a work-item/convergence mismatch check, and returns which open Beats under an Account qualify for a Downbeat this cycle \u2014 citing exactly which signal(s) triggered each one. Never anchors a Downbeat; the caller decides whether to accept and calls set_downbeat per qualifying Beat themselves.",
24425
24455
  {
24426
24456
  accountId: external_exports.string().describe("The Account ID"),
24427
24457
  asOf: external_exports.string().datetime().optional().describe("ISO 8601 timestamp used only to stamp the report header. The underlying signals always reflect current state. Defaults to the current time.")
@@ -24615,7 +24645,15 @@ function registerDropQualityTools(server, ctx, client) {
24615
24645
 
24616
24646
  // ../../libs/harmonica-services/src/mcp/tools/drop-tools.ts
24617
24647
  var DROP_STATES = ["draft", "open", "released", "rolled_back", "cancelled"];
24618
- var FEATURE_FLAG_STAGES = ["local", "staging", "prod"];
24648
+ var FEATURE_FLAG_STAGES = [
24649
+ "local",
24650
+ "staging",
24651
+ "staging-alpha",
24652
+ "staging-beta",
24653
+ "alpha",
24654
+ "beta",
24655
+ "prod"
24656
+ ];
24619
24657
  var featureFlagBindingSchema = external_exports.object({
24620
24658
  scope: external_exports.string().min(1).max(100).describe('Feature-flag scope, e.g. "harmonica"'),
24621
24659
  name: external_exports.string().min(1).max(200).describe("Feature-flag name within the scope"),
@@ -25329,166 +25367,6 @@ ${formatLayerDetail(layer)}` }] };
25329
25367
  );
25330
25368
  }
25331
25369
 
25332
- // ../../libs/harmonica-services/src/mcp/tools/measure-tools.ts
25333
- var VU_ENUM = ["XS", "S", "M", "L", "XL"];
25334
- function vu(valueUnit) {
25335
- return valueUnit ?? "(hidden)";
25336
- }
25337
- function registerMeasureTools(server, _ctx, client) {
25338
- server.tool(
25339
- "list_track_measures",
25340
- 'List all Measures under a Track (DeliverableGroup). A Measure is the atomic unit of planned work carrying a Value Unit estimate. Recognition cues: "what work is planned on this track", "list the measures" \u2192 list_track_measures.',
25341
- {
25342
- trackId: external_exports.string().describe("The Track (DeliverableGroup) ID")
25343
- },
25344
- async ({ trackId }) => {
25345
- const measures = await client.listTrackMeasures(trackId);
25346
- const text = measures.length === 0 ? "No measures found." : measures.map((m) => `[${m.measureId}] ${m.measureCode} \u2014 ${m.title} (VU: ${vu(m.valueUnit)})`).join("\n");
25347
- return { content: [{ type: "text", text }] };
25348
- }
25349
- );
25350
- server.tool(
25351
- "list_beat_version_measures",
25352
- 'List the Measures linked to a Beat Version \u2014 answers "which planned work advances this Beat Version".',
25353
- {
25354
- beatVersionId: external_exports.string().describe("The Beat Version ID (e.g., bv-abc123)")
25355
- },
25356
- async ({ beatVersionId }) => {
25357
- const measures = await client.listBeatVersionMeasures(beatVersionId);
25358
- const text = measures.length === 0 ? "No measures found." : measures.map((m) => `[${m.measureId}] ${m.measureCode} \u2014 ${m.title} (VU: ${vu(m.valueUnit)})`).join("\n");
25359
- return { content: [{ type: "text", text }] };
25360
- }
25361
- );
25362
- server.tool(
25363
- "get_measure",
25364
- "Get a Measure by ID. Returns title, Value Unit estimate (if visible), Track, and any linked Beat Version.",
25365
- {
25366
- measureId: external_exports.string().describe("The measure ID (e.g., me-abc123)")
25367
- },
25368
- async ({ measureId }) => {
25369
- const m = await client.getMeasure(measureId);
25370
- if (!m) {
25371
- return { content: [{ type: "text", text: `Measure not found: ${measureId}` }] };
25372
- }
25373
- const lines = [
25374
- `ID: ${m.measureId}`,
25375
- `Code: ${m.measureCode}`,
25376
- `Title: ${m.title}`,
25377
- `Track: ${m.trackId}`,
25378
- `Value Unit: ${vu(m.valueUnit)}`,
25379
- m.beatVersionId ? `Beat Version: ${m.beatVersionId}` : null,
25380
- m.createdBy ? `Created By: ${m.createdBy}` : null,
25381
- `Created: ${m.createdAt}`,
25382
- `Updated: ${m.updatedAt}`
25383
- ].filter(Boolean).join("\n");
25384
- return { content: [{ type: "text", text: lines }] };
25385
- }
25386
- );
25387
- server.tool(
25388
- "create_measure",
25389
- "Create a Measure under a Track (DeliverableGroup). A Measure is the atomic unit of planned work with a Value Unit estimate \u2014 it is NOT a Beat. Optionally link it to a Beat Version at creation.",
25390
- {
25391
- trackId: external_exports.string().describe("The Track (DeliverableGroup) ID this Measure belongs to"),
25392
- title: external_exports.string().min(1).max(300).describe('Measure title (e.g. "Login form validation")'),
25393
- valueUnit: external_exports.enum(VU_ENUM).describe("Value Unit label estimate (t-shirt size XS\u2013XL)"),
25394
- beatVersionId: external_exports.string().min(1).max(128).optional().describe("Optional Beat Version to link at creation. Must exist."),
25395
- createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
25396
- },
25397
- async ({ trackId, title, valueUnit, beatVersionId, createdBy }) => {
25398
- const m = await client.createMeasure({
25399
- trackId,
25400
- title,
25401
- valueUnit,
25402
- ...beatVersionId !== void 0 && { beatVersionId },
25403
- ...createdBy !== void 0 && { createdBy }
25404
- });
25405
- return {
25406
- content: [
25407
- {
25408
- type: "text",
25409
- text: `Created Measure: [${m.measureId}] ${m.measureCode} \u2014 ${m.title} (VU: ${vu(m.valueUnit)})`
25410
- }
25411
- ]
25412
- };
25413
- }
25414
- );
25415
- server.tool(
25416
- "update_measure",
25417
- "Update mutable content fields on a Measure (title, valueUnit). To change the Beat Version link use link_measure_to_beat_version / unlink_measure_from_beat_version instead.",
25418
- {
25419
- measureId: external_exports.string().describe("The measure ID"),
25420
- title: external_exports.string().min(1).max(300).optional().describe("New title"),
25421
- valueUnit: external_exports.enum(VU_ENUM).optional().describe("New Value Unit estimate")
25422
- },
25423
- async ({ measureId, title, valueUnit }) => {
25424
- const hasUpdates = title !== void 0 || valueUnit !== void 0;
25425
- if (!hasUpdates) {
25426
- return {
25427
- content: [{ type: "text", text: `No updates provided for Measure: ${measureId}` }],
25428
- isError: true
25429
- };
25430
- }
25431
- const m = await client.updateMeasure(measureId, {
25432
- ...title !== void 0 && { title },
25433
- ...valueUnit !== void 0 && { valueUnit }
25434
- });
25435
- if (!m) {
25436
- return {
25437
- content: [{ type: "text", text: `Measure not found: ${measureId}` }],
25438
- isError: true
25439
- };
25440
- }
25441
- return {
25442
- content: [
25443
- {
25444
- type: "text",
25445
- text: `Updated Measure: [${m.measureId}] ${m.measureCode} \u2014 ${m.title} (VU: ${vu(m.valueUnit)})`
25446
- }
25447
- ]
25448
- };
25449
- }
25450
- );
25451
- server.tool(
25452
- "link_measure_to_beat_version",
25453
- "Soft-link a Measure to a single Beat Version (the internal capability it advances). Replaces any existing link.",
25454
- {
25455
- measureId: external_exports.string().describe("The measure ID"),
25456
- beatVersionId: external_exports.string().describe("The Beat Version ID to link (e.g., bv-abc123). Must exist.")
25457
- },
25458
- async ({ measureId, beatVersionId }) => {
25459
- const m = await client.linkMeasureToBeatVersion(measureId, beatVersionId);
25460
- if (!m) {
25461
- return {
25462
- content: [{ type: "text", text: `Measure not found: ${measureId}` }],
25463
- isError: true
25464
- };
25465
- }
25466
- return {
25467
- content: [{ type: "text", text: `Linked Measure ${m.measureId} \u2192 Beat Version ${beatVersionId}` }]
25468
- };
25469
- }
25470
- );
25471
- server.tool(
25472
- "unlink_measure_from_beat_version",
25473
- "Remove a Measure's soft link to its Beat Version. No-op-safe: the Measure remains under its Track.",
25474
- {
25475
- measureId: external_exports.string().describe("The measure ID")
25476
- },
25477
- async ({ measureId }) => {
25478
- const m = await client.unlinkMeasureFromBeatVersion(measureId);
25479
- if (!m) {
25480
- return {
25481
- content: [{ type: "text", text: `Measure not found: ${measureId}` }],
25482
- isError: true
25483
- };
25484
- }
25485
- return {
25486
- content: [{ type: "text", text: `Unlinked Measure ${m.measureId} from its Beat Version` }]
25487
- };
25488
- }
25489
- );
25490
- }
25491
-
25492
25370
  // ../../libs/harmonica-services/src/mcp/tools/membership-tools.ts
25493
25371
  function registerMembershipTools(server, ctx, client) {
25494
25372
  server.tool(
@@ -25913,7 +25791,8 @@ var PLAN_QUALITY_DIMENSIONS = [
25913
25791
  "testable",
25914
25792
  "traceable",
25915
25793
  "assignable",
25916
- "riskAware"
25794
+ "riskAware",
25795
+ "completeness"
25917
25796
  ];
25918
25797
  var PlanDimensionScoreSchema = external_exports.object({
25919
25798
  dimension: external_exports.enum(PLAN_QUALITY_DIMENSIONS),
@@ -25928,6 +25807,27 @@ var PlanQualityLLMResponseSchema = external_exports.object({
25928
25807
  topSuggestion: external_exports.string()
25929
25808
  });
25930
25809
 
25810
+ // ../../libs/harmonica-schemas/src/revision-quality.ts
25811
+ var REVISION_QUALITY_DIMENSIONS = [
25812
+ "scoped",
25813
+ "traceable",
25814
+ "testable",
25815
+ "reviewReady",
25816
+ "nonDuplicating"
25817
+ ];
25818
+ var RevisionDimensionScoreSchema = external_exports.object({
25819
+ dimension: external_exports.enum(REVISION_QUALITY_DIMENSIONS),
25820
+ label: external_exports.string(),
25821
+ score: external_exports.number().min(1).max(5),
25822
+ rationale: external_exports.string(),
25823
+ suggestions: external_exports.array(external_exports.string())
25824
+ });
25825
+ var RevisionQualityLLMResponseSchema = external_exports.object({
25826
+ dimensions: external_exports.array(RevisionDimensionScoreSchema).min(1),
25827
+ summary: external_exports.string(),
25828
+ topSuggestion: external_exports.string()
25829
+ });
25830
+
25931
25831
  // ../../libs/harmonica-schemas/src/beat-quality.ts
25932
25832
  var BEAT_QUALITY_DIMENSIONS = [
25933
25833
  "distinctive",
@@ -26741,23 +26641,6 @@ var MovementApiSchema = external_exports.object({
26741
26641
  updatedAt: external_exports.string()
26742
26642
  }).passthrough();
26743
26643
  var MovementsResponseSchema = external_exports.object({ movements: external_exports.array(MovementApiSchema) }).passthrough();
26744
- var ValueUnitEstimateSchema = external_exports.enum(["XS", "S", "M", "L", "XL"]);
26745
- var MeasureApiSchema = external_exports.object({
26746
- measureId: external_exports.string(),
26747
- measureCode: external_exports.string(),
26748
- trackId: external_exports.string(),
26749
- teamspaceId: external_exports.string(),
26750
- orgId: external_exports.string(),
26751
- title: external_exports.string(),
26752
- valueUnit: ValueUnitEstimateSchema.optional(),
26753
- beatVersionId: external_exports.string().optional(),
26754
- createdBy: external_exports.string().optional(),
26755
- version: external_exports.number().optional(),
26756
- createdAt: external_exports.string(),
26757
- updatedAt: external_exports.string()
26758
- }).passthrough();
26759
- var MeasuresResponseSchema = external_exports.object({ measures: external_exports.array(MeasureApiSchema) }).passthrough();
26760
- var MeasureResponseSchema = external_exports.object({ measure: MeasureApiSchema }).passthrough();
26761
26644
  var WorkItemStatusSchema = external_exports.enum(["not_started", "in_progress", "blocked", "done"]);
26762
26645
  var WORK_ITEM_EFFORT_SIZES = [
26763
26646
  "XS",
@@ -26930,7 +26813,6 @@ var WorkPlanBeatVersionApiSchema = external_exports.object({
26930
26813
  versionNumber: external_exports.number(),
26931
26814
  title: external_exports.string(),
26932
26815
  status: BeatVersionStatusSchema,
26933
- deliverableGroupId: external_exports.string().nullish(),
26934
26816
  // Derived Value Units (VU = Δf × W), read-only. 0 when the BV has no confirmed
26935
26817
  // value-velocity inputs. Computed server-side from the parent Beat's weight.
26936
26818
  // `.default(0)` tolerates a rolling deploy where an older API omits the field —
@@ -26942,6 +26824,71 @@ var WorkPlanBeatVersionApiSchema = external_exports.object({
26942
26824
  var DeliverableGroupBeatVersionsResponseSchema = external_exports.object({
26943
26825
  beatVersions: external_exports.array(WorkPlanBeatVersionApiSchema)
26944
26826
  }).passthrough();
26827
+ var RevisionClaimActorSchema = external_exports.union([
26828
+ external_exports.object({ kind: external_exports.literal("user"), userId: external_exports.string() }),
26829
+ external_exports.object({ kind: external_exports.literal("agent"), agentId: external_exports.string(), projectId: external_exports.string() })
26830
+ ]);
26831
+ var RevisionClaimSchema = external_exports.object({
26832
+ claimId: external_exports.string(),
26833
+ status: external_exports.enum(["active", "archived"]),
26834
+ actor: RevisionClaimActorSchema,
26835
+ scope: external_exports.object({ type: external_exports.literal("revision"), revisionId: external_exports.string() })
26836
+ }).passthrough();
26837
+ var RevisionClaimsResponseSchema = external_exports.object({
26838
+ claims: external_exports.array(RevisionClaimSchema)
26839
+ }).passthrough();
26840
+ var ProposedRevisionSchema = external_exports.object({
26841
+ revisionId: external_exports.string(),
26842
+ title: external_exports.string(),
26843
+ state: external_exports.string(),
26844
+ beatId: external_exports.string(),
26845
+ beatTitle: external_exports.string().optional(),
26846
+ projectId: external_exports.string(),
26847
+ projectTitle: external_exports.string().optional()
26848
+ }).passthrough();
26849
+ var ReleaseIntervalSchema = external_exports.object({
26850
+ averageIntervalDays: external_exports.number(),
26851
+ sampleSize: external_exports.number(),
26852
+ isFallback: external_exports.boolean()
26853
+ }).passthrough();
26854
+ var EligibleBeatVersionForDropSchema = external_exports.object({
26855
+ beatVersionId: external_exports.string(),
26856
+ title: external_exports.string(),
26857
+ state: external_exports.string(),
26858
+ beatId: external_exports.string(),
26859
+ beatTitle: external_exports.string().optional(),
26860
+ projectId: external_exports.string(),
26861
+ projectTitle: external_exports.string().optional(),
26862
+ createdAt: external_exports.string(),
26863
+ updatedAt: external_exports.string(),
26864
+ humanAssignee: external_exports.object({ email: external_exports.string(), name: external_exports.string() }).optional(),
26865
+ currentDropAssignment: external_exports.object({ dropId: external_exports.string(), dropCode: external_exports.string(), state: external_exports.string() }).optional()
26866
+ }).passthrough();
26867
+ var ScoredDropCandidateSchema = external_exports.object({
26868
+ bv: EligibleBeatVersionForDropSchema,
26869
+ // Score fields are nullable — the ranking engine returns null when there is
26870
+ // insufficient historical data to compute a metric (e.g. no prior releases).
26871
+ score: external_exports.object({
26872
+ vu: external_exports.number().nullable(),
26873
+ slope: external_exports.number().nullable(),
26874
+ roi: external_exports.number().nullable(),
26875
+ valuePrimary: external_exports.number().nullable(),
26876
+ chosen: external_exports.number().nullable(),
26877
+ lens: external_exports.string()
26878
+ }).passthrough(),
26879
+ atRisk: external_exports.boolean(),
26880
+ requiredSlope: external_exports.number().nullable().optional()
26881
+ }).passthrough();
26882
+ var DropProposalApiSchema = external_exports.object({
26883
+ accountId: external_exports.string(),
26884
+ revisionIds: external_exports.array(external_exports.string()),
26885
+ revisions: external_exports.array(ProposedRevisionSchema),
26886
+ suggestedTargetDate: external_exports.string(),
26887
+ releaseInterval: ReleaseIntervalSchema,
26888
+ rationale: external_exports.string(),
26889
+ isEmpty: external_exports.boolean(),
26890
+ scoredCandidates: external_exports.array(ScoredDropCandidateSchema).optional()
26891
+ }).passthrough();
26945
26892
 
26946
26893
  // ../../libs/harmonica-services/src/mcp/tools/note-element-tools.ts
26947
26894
  var parentKindSchema = external_exports.enum(["notebook", "note"]);
@@ -27114,13 +27061,14 @@ var DECISION_SIGNIFICANCE_VALUES2 = ["strategic", "structural", "implementation"
27114
27061
  function registerNoteTools(server, ctx, client) {
27115
27062
  server.tool(
27116
27063
  "list_notes",
27117
- "List Notes for a project. Without beatId: returns all notes (project-level + beat-level). With beatId: returns only notes scoped to that beat. Filterable by type, status, revisionId, and significance. Use offset/limit to paginate beat-scoped results; use cursor for project-wide results.",
27064
+ "List Notes for a project. Without beatId: returns all notes (project-level + beat-level). With beatId: returns only notes scoped to that beat. Filterable by type, status, revisionId, beatVersionId, and significance. Use offset/limit to paginate beat-scoped results; use cursor for project-wide results.",
27118
27065
  {
27119
27066
  projectId: external_exports.string().describe("The project ID"),
27120
27067
  beatId: external_exports.string().optional().describe("Scope to a specific beat (omit for all project notes)"),
27121
27068
  noteType: external_exports.union([external_exports.enum(NOTE_TYPE_VALUES), external_exports.array(external_exports.enum(NOTE_TYPE_VALUES)).min(1)]).optional().describe('Filter by note type \u2014 single value or array for multi-select (e.g. ["constraint", "assumption"])'),
27122
27069
  status: external_exports.enum(NOTE_STATUS_VALUES).optional().describe("Filter by status"),
27123
27070
  revisionId: external_exports.string().optional().describe("Filter to notes scoped to a specific revision"),
27071
+ beatVersionId: external_exports.string().optional().describe("Filter to notes scoped to a specific Beat Version (planning increment)"),
27124
27072
  sourceDocumentNoteId: external_exports.string().optional().describe("Filter to notes extracted from a specific document (matches sourceSubmissionId)"),
27125
27073
  significance: external_exports.enum(DECISION_SIGNIFICANCE_VALUES2).optional().describe("Filter decision Notes by significance tier (strategic | structural | implementation). Notes without a significance value are treated as implementation."),
27126
27074
  offset: external_exports.coerce.number().int().min(0).default(0).describe("Notes to skip for pagination (applies to beat-scoped and source-document-filtered results only)"),
@@ -27128,7 +27076,7 @@ function registerNoteTools(server, ctx, client) {
27128
27076
  cursor: external_exports.string().optional().describe("Opaque pagination cursor from a previous response \u2014 use for project-wide note pagination (no beatId)"),
27129
27077
  orderBy: external_exports.enum(["newest", "oldest"]).optional().describe("Sort order \u2014 newest or oldest first. Applies to beat-scoped queries (beatId provided); omit to use DynamoDB scan order.")
27130
27078
  },
27131
- async ({ projectId, beatId, noteType, status, revisionId, sourceDocumentNoteId, significance, offset = 0, limit = 100, cursor, orderBy }) => {
27079
+ async ({ projectId, beatId, noteType, status, revisionId, beatVersionId, sourceDocumentNoteId, significance, offset = 0, limit = 100, cursor, orderBy }) => {
27132
27080
  if (Array.isArray(noteType) && noteType.length === 0) {
27133
27081
  return { content: [{ type: "text", text: "noteType must not be an empty array \u2014 omit it to return all types." }], isError: true };
27134
27082
  }
@@ -27149,6 +27097,7 @@ function registerNoteTools(server, ctx, client) {
27149
27097
  noteType,
27150
27098
  status,
27151
27099
  revisionId,
27100
+ beatVersionId,
27152
27101
  significance
27153
27102
  };
27154
27103
  let page;
@@ -27344,13 +27293,14 @@ function registerNoteTools(server, ctx, client) {
27344
27293
  content: external_exports.string().describe("The note content"),
27345
27294
  beatId: external_exports.string().optional().describe("Beat ID if this note is beat-scoped (requires projectId)"),
27346
27295
  revisionId: external_exports.string().optional().describe("Revision ID if this note is revision-scoped (requires projectId)"),
27296
+ beatVersionId: external_exports.string().optional().describe("Beat Version ID if this note governs a specific planning increment (requires beatId)"),
27347
27297
  rationale: external_exports.string().optional().describe("Why this note exists"),
27348
27298
  confidence: external_exports.coerce.number().min(0).max(1).optional().describe("Confidence level for assumptions (0-1)"),
27349
27299
  affectsBeats: external_exports.array(external_exports.string()).optional().describe("Beat IDs this note impacts"),
27350
27300
  dependsOnNotes: external_exports.array(external_exports.string()).optional().describe("Note IDs this note depends on (child \u2192 parent links for consolidation)"),
27351
27301
  significance: external_exports.enum(DECISION_SIGNIFICANCE_VALUES2).optional().describe("Significance tier for decision Notes (strategic | structural | implementation). Defaults to implementation when unset. Only meaningful on noteType=decision.")
27352
27302
  },
27353
- async ({ projectId, teamspaceId, movementId, noteType, content, beatId, revisionId, rationale, confidence, affectsBeats, dependsOnNotes, significance }) => {
27303
+ async ({ projectId, teamspaceId, movementId, noteType, content, beatId, revisionId, beatVersionId, rationale, confidence, affectsBeats, dependsOnNotes, significance }) => {
27354
27304
  try {
27355
27305
  if ([projectId, teamspaceId, movementId].filter(Boolean).length > 1) {
27356
27306
  return { content: [{ type: "text", text: "Specify at most one of projectId, teamspaceId, or movementId." }], isError: true };
@@ -27366,6 +27316,7 @@ function registerNoteTools(server, ctx, client) {
27366
27316
  projectId,
27367
27317
  beatId,
27368
27318
  revisionId,
27319
+ beatVersionId,
27369
27320
  noteType,
27370
27321
  content,
27371
27322
  rationale,
@@ -28670,10 +28621,10 @@ async function pollForCheck3(client, taskId) {
28670
28621
  if (!task.result) throw new Error(`Task ${taskId} completed with no result`);
28671
28622
  return task.result;
28672
28623
  }
28673
- if (task.status === "failed") throw new Error(`Plan quality check failed: ${task.error ?? "unknown error"}`);
28624
+ if (task.status === "failed") throw new Error(`Beat version quality check failed: ${task.error ?? "unknown error"}`);
28674
28625
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS3));
28675
28626
  }
28676
- throw new Error(`Plan quality check timed out after ${POLL_TIMEOUT_MS3 / 1e3}s`);
28627
+ throw new Error(`Beat version quality check timed out after ${POLL_TIMEOUT_MS3 / 1e3}s`);
28677
28628
  }
28678
28629
  function formatScorecard2(check2) {
28679
28630
  const bar = (score, max) => "\u2588".repeat(score) + "\u2591".repeat(max - score);
@@ -28683,8 +28634,8 @@ ${d.rationale}${d.suggestions?.length ? `
28683
28634
  \u2022 ${d.suggestions.join("\n\u2022 ")}` : ""}`
28684
28635
  ).join("\n\n");
28685
28636
  return [
28686
- `## Plan Quality Check \u2014 ${check2.targetId}`,
28687
- `**Overall Score:** ${check2.overallScore}/${CHECK_MAX_SCORES.plan_quality}`,
28637
+ `## Beat Version Quality Check \u2014 ${check2.targetId}`,
28638
+ `**Overall Score:** ${check2.overallScore}/${CHECK_MAX_SCORES.beat_version_quality}`,
28688
28639
  `**Summary:** ${check2.summary}`,
28689
28640
  check2.topSuggestion ? `**Top Suggestion:** ${check2.topSuggestion}` : "",
28690
28641
  "",
@@ -28692,44 +28643,59 @@ ${d.rationale}${d.suggestions?.length ? `
28692
28643
  ].filter(Boolean).join("\n");
28693
28644
  }
28694
28645
  function registerPlanQualityTools(server, ctx, client) {
28695
- server.tool(
28696
- "check_plan_quality",
28697
- "Enqueue a plan quality check across 6 dimensions (Scoped, Directional, Testable, Traceable, Assignable, Risk-aware) on a Beat Version. plan_quality is a Beat-Version-only check \u2014 run it on a Beat Version before creating its first Revision (the first Revision is gated on a passing BV plan_quality check). For a Revision, run check_build_quality instead. By default returns a jobId immediately (fire-and-forget) \u2014 use get_job_status or list_checks to retrieve results. Set wait=true to block until the check completes and receive the scorecard inline. WARNING: wait=true holds the MCP session open for 30\u201360s per check \u2014 for bulk operations, omit wait and poll get_job_status separately to avoid session exhaustion.",
28698
- {
28699
- beatVersionId: external_exports.string().describe("The Beat Version ID (e.g., bv-abc123). plan_quality targets Beat Versions only."),
28700
- projectId: external_exports.string().describe("The project ID"),
28701
- wait: external_exports.boolean().optional().describe("If true, block until the check completes and return the scorecard inline. Default: false (returns taskId immediately).")
28702
- },
28703
- async ({ beatVersionId, projectId, wait }) => {
28704
- try {
28705
- if (!beatVersionId?.startsWith("bv-")) {
28706
- return {
28707
- content: [{
28708
- type: "text",
28709
- text: `Plan quality check failed: plan_quality runs only on Beat Versions (bv-*); got "${beatVersionId}". For a Revision, run check_build_quality on the Revision, or run check_plan_quality on its parent Beat Version.`
28710
- }],
28711
- isError: true
28712
- };
28713
- }
28714
- await assertProjectInOrg(client, projectId, ctx.orgId);
28715
- const task = await client.runCheck(projectId, "plan_quality", beatVersionId);
28716
- if (!wait) {
28717
- return {
28718
- content: [{
28719
- type: "text",
28720
- text: `Plan quality check enqueued for ${beatVersionId}.
28646
+ const schema = {
28647
+ beatVersionId: external_exports.string().describe("The Beat Version ID (e.g., bv-abc123). beat_version_quality targets Beat Versions only."),
28648
+ projectId: external_exports.string().describe("The project ID"),
28649
+ // The completeness dimension probes the codebase, so this tool accepts the
28650
+ // same repo overrides as run_check without them the probe could only ever
28651
+ // read the default branch from this surface.
28652
+ branch: external_exports.string().optional().describe("Override the git branch the completeness dimension probes (default: the branch of an open child Revision, else the project default branch)"),
28653
+ localPath: external_exports.string().optional().describe("Absolute path to an already-cloned local copy of the repo \u2014 the completeness probe reads it instead of cloning"),
28654
+ wait: external_exports.boolean().optional().describe("If true, block until the check completes and return the scorecard inline. Default: false (returns taskId immediately).")
28655
+ };
28656
+ const handler = async ({ beatVersionId, projectId, branch, localPath, wait }) => {
28657
+ try {
28658
+ const targetError = beatVersionQualityTargetError(beatVersionId ?? "");
28659
+ if (targetError) {
28660
+ return {
28661
+ content: [{ type: "text", text: `Beat version quality check failed: ${targetError}` }],
28662
+ isError: true
28663
+ };
28664
+ }
28665
+ await assertProjectInOrg(client, projectId, ctx.orgId);
28666
+ const opts = branch || localPath ? { branch, localPath } : void 0;
28667
+ const task = await client.runCheck(projectId, "beat_version_quality", beatVersionId, opts);
28668
+ if (!wait) {
28669
+ return {
28670
+ content: [{
28671
+ type: "text",
28672
+ text: `Beat version quality check enqueued for ${beatVersionId}.
28721
28673
  Job ID: ${task.taskId}
28722
28674
 
28723
28675
  Use get_job_status with jobId="${task.taskId}" to poll for results, or list_checks to retrieve the completed scorecard once done.`
28724
- }]
28725
- };
28726
- }
28727
- const check2 = await pollForCheck3(client, task.taskId);
28728
- return { content: [{ type: "text", text: formatScorecard2(check2) }] };
28729
- } catch (err) {
28730
- const message = err instanceof Error ? err.message : String(err);
28731
- return { content: [{ type: "text", text: `Plan quality check failed: ${message}` }], isError: true };
28676
+ }]
28677
+ };
28732
28678
  }
28679
+ const check2 = await pollForCheck3(client, task.taskId);
28680
+ return { content: [{ type: "text", text: formatScorecard2(check2) }] };
28681
+ } catch (err) {
28682
+ const message = err instanceof Error ? err.message : String(err);
28683
+ return { content: [{ type: "text", text: `Beat version quality check failed: ${message}` }], isError: true };
28684
+ }
28685
+ };
28686
+ server.tool(
28687
+ "check_beat_version_quality",
28688
+ "Enqueue a Beat Version quality check across 7 dimensions (Scoped, Directional, Testable, Traceable, Assignable, Risk-aware, Completeness) on a Beat Version. beat_version_quality is a Beat-Version-only check \u2014 run it on a Beat Version before creating its first Revision (the first Revision is gated on a passing BV beat_version_quality check). For a Revision, run check_build_quality instead. By default returns a jobId immediately (fire-and-forget) \u2014 use get_job_status or list_checks to retrieve results. Set wait=true to block until the check completes and receive the scorecard inline. WARNING: wait=true holds the MCP session open for 30\u201360s per check \u2014 for bulk operations, omit wait and poll get_job_status separately to avoid session exhaustion.",
28689
+ schema,
28690
+ handler
28691
+ );
28692
+ server.tool(
28693
+ "check_plan_quality",
28694
+ "DEPRECATED \u2014 renamed to check_beat_version_quality (the check grades the Beat Version's planning increment: its plan dimensions and completeness against the codebase). This alias runs the same check and persists it as beat_version_quality. Prefer check_beat_version_quality.",
28695
+ schema,
28696
+ async (args) => {
28697
+ console.warn("[deprecated-tool] check_plan_quality invoked \u2014 use check_beat_version_quality");
28698
+ return handler(args);
28733
28699
  }
28734
28700
  );
28735
28701
  }
@@ -29499,6 +29465,103 @@ Error code: PR_NOT_DRAFT (the PR is already ready for review or was never a draf
29499
29465
  );
29500
29466
  }
29501
29467
 
29468
+ // ../../libs/harmonica-services/src/mcp/tools/revision-quality-tools.ts
29469
+ var POLL_INTERVAL_MS5 = 4e3;
29470
+ var POLL_TIMEOUT_MS5 = 18e4;
29471
+ var POLL_MAX_CONSECUTIVE_ERRORS5 = 3;
29472
+ async function pollForCheck5(client, taskId) {
29473
+ const deadline = Date.now() + POLL_TIMEOUT_MS5;
29474
+ let consecutiveErrors = 0;
29475
+ while (Date.now() < deadline) {
29476
+ const [task, err] = await client.getTask(taskId).then(
29477
+ (t) => [t, null],
29478
+ (e) => [null, e]
29479
+ );
29480
+ if (err) {
29481
+ consecutiveErrors++;
29482
+ if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS5)
29483
+ throw new Error(`getTask failed ${consecutiveErrors} consecutive times: ${err instanceof Error ? err.message : String(err)}`);
29484
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS5));
29485
+ continue;
29486
+ }
29487
+ if (!task) {
29488
+ consecutiveErrors++;
29489
+ if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS5)
29490
+ throw new Error(`Task ${taskId} not found after ${consecutiveErrors} consecutive attempts`);
29491
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS5));
29492
+ continue;
29493
+ }
29494
+ consecutiveErrors = 0;
29495
+ if (task.status === "completed") {
29496
+ if (!task.result) throw new Error(`Task ${taskId} completed with no result`);
29497
+ return task.result;
29498
+ }
29499
+ if (task.status === "failed") throw new Error(`Revision quality check failed: ${task.error ?? "unknown error"}`);
29500
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS5));
29501
+ }
29502
+ throw new Error(`Revision quality check timed out after ${POLL_TIMEOUT_MS5 / 1e3}s`);
29503
+ }
29504
+ function formatScorecard3(check2) {
29505
+ const bar = (score, max) => "\u2588".repeat(score) + "\u2591".repeat(max - score);
29506
+ const dims = check2.dimensions.map(
29507
+ (d) => `**${d.label}** ${bar(d.score, d.maxScore)} ${d.score}/${d.maxScore}
29508
+ ${d.rationale}${d.suggestions?.length ? `
29509
+ \u2022 ${d.suggestions.join("\n\u2022 ")}` : ""}`
29510
+ ).join("\n\n");
29511
+ return [
29512
+ `## Revision Quality Check \u2014 ${check2.targetId}`,
29513
+ `**Overall Score:** ${check2.overallScore}/${CHECK_MAX_SCORES.revision_quality}`,
29514
+ `*(Advisory \u2014 no lifecycle gate reads this score)*`,
29515
+ `**Summary:** ${check2.summary}`,
29516
+ check2.topSuggestion ? `**Top Suggestion:** ${check2.topSuggestion}` : "",
29517
+ "",
29518
+ dims
29519
+ ].filter(Boolean).join("\n");
29520
+ }
29521
+ function registerRevisionQualityTools(server, ctx, client) {
29522
+ server.tool(
29523
+ "check_revision_quality",
29524
+ "Run a five-dimension PR-readiness check on a Revision (Scoped, Traceable, Testable, Review-ready, Non-duplicating). Advisory only \u2014 no lifecycle transition or creation path gates on this score. By default returns a jobId immediately (fire-and-forget) \u2014 use get_job_status or list_checks to retrieve results. Set wait=true to block until the check completes and receive the scorecard inline. WARNING: wait=true holds the MCP session open for 30\u201360s \u2014 for bulk operations, omit wait and poll get_job_status separately.",
29525
+ {
29526
+ revisionId: external_exports.string().describe("The Revision ID (e.g., rev-abc123). revision_quality targets Revisions only."),
29527
+ projectId: external_exports.string().describe("The project ID"),
29528
+ wait: external_exports.boolean().optional().describe("If true, block until the check completes and return the scorecard inline. Default: false (returns taskId immediately).")
29529
+ },
29530
+ async ({ revisionId, projectId, wait }) => {
29531
+ try {
29532
+ const targetError = revisionQualityTargetError(revisionId);
29533
+ if (targetError) {
29534
+ return {
29535
+ content: [{ type: "text", text: `Revision quality check failed: ${targetError}` }],
29536
+ isError: true
29537
+ };
29538
+ }
29539
+ await assertProjectInOrg(client, projectId, ctx.orgId);
29540
+ await assertRevisionInProject(client, revisionId, projectId);
29541
+ const task = await client.runCheck(projectId, "revision_quality", revisionId);
29542
+ if (!wait) {
29543
+ return {
29544
+ content: [{
29545
+ type: "text",
29546
+ text: `Revision quality check enqueued for ${revisionId}.
29547
+ Job ID: ${task.taskId}
29548
+
29549
+ Use get_job_status with jobId="${task.taskId}" to poll for results, or list_checks to retrieve the completed scorecard once done.
29550
+
29551
+ *(Advisory check \u2014 score does not gate any lifecycle transition.)*`
29552
+ }]
29553
+ };
29554
+ }
29555
+ const check2 = await pollForCheck5(client, task.taskId);
29556
+ return { content: [{ type: "text", text: formatScorecard3(check2) }] };
29557
+ } catch (err) {
29558
+ const message = err instanceof Error ? err.message : String(err);
29559
+ return { content: [{ type: "text", text: `Revision quality check failed: ${message}` }], isError: true };
29560
+ }
29561
+ }
29562
+ );
29563
+ }
29564
+
29502
29565
  // ../../libs/harmonica-services/src/mcp/tools/session-tools.ts
29503
29566
  var SESSION_STATUSES = ["active", "idle", "closed"];
29504
29567
  var SESSION_MESSAGE_MAX_LENGTH = 8e3;
@@ -29770,7 +29833,7 @@ var import_node_os = require("node:os");
29770
29833
  var import_node_path = require("node:path");
29771
29834
 
29772
29835
  // ../../libs/harmonica-services/src/system-snapshot.constants.ts
29773
- var SNAPSHOT_VERSION = 4;
29836
+ var SNAPSHOT_VERSION = 6;
29774
29837
 
29775
29838
  // ../../libs/harmonica-services/src/mcp/tools/snapshot-tools.ts
29776
29839
  function registerSnapshotTools(server, ctx, client) {
@@ -29780,10 +29843,10 @@ function registerSnapshotTools(server, ctx, client) {
29780
29843
  { systemId: external_exports.string().describe("The system ID to export") },
29781
29844
  async ({ systemId }) => {
29782
29845
  await assertProjectInOrg(client, systemId, ctx.orgId);
29783
- const result = await client.exportProjectSnapshot(systemId);
29846
+ const result = await client.exportSystemSnapshot(systemId);
29784
29847
  if (typeof result === "string") {
29785
29848
  if (!result.startsWith("https://")) {
29786
- throw new Error(`exportProjectSnapshot returned an unexpected string value (expected an https:// presigned URL): ${result.slice(0, 80)}`);
29849
+ throw new Error(`exportSystemSnapshot returned an unexpected string value (expected an https:// presigned URL): ${result.slice(0, 80)}`);
29787
29850
  }
29788
29851
  return {
29789
29852
  content: [{
@@ -29821,6 +29884,10 @@ function registerSnapshotTools(server, ctx, client) {
29821
29884
  ` Layers: ${snapshot.layers?.length ?? 0}`,
29822
29885
  ` Tracks: ${snapshot.tracks?.length ?? 0}`,
29823
29886
  ` Work Items: ${snapshot.workItems?.length ?? 0}`,
29887
+ ` Movements: ${snapshot.movements?.length ?? 0}`,
29888
+ ` Bars: ${snapshot.bars?.length ?? 0}`,
29889
+ ` Notebooks: ${snapshot.notebooks?.length ?? 0}`,
29890
+ ` Accounts: ${(snapshot.account ? 1 : 0) + (snapshot.accounts?.length ?? 0)}`,
29824
29891
  "",
29825
29892
  "Use import_system_snapshot with this file path to import into another environment."
29826
29893
  ];
@@ -29834,9 +29901,12 @@ function registerSnapshotTools(server, ctx, client) {
29834
29901
  snapshot: external_exports.string().describe("S3 presigned URL (https://...), file path, or inline JSON string"),
29835
29902
  targetTeamspaceId: external_exports.string().min(1).optional().describe(
29836
29903
  "Remap all teamspace references (Drops, Deliverables, Project record) to this teamspace ID in the target environment. Required when the source and target environments use different teamspace UUIDs. When omitted and the snapshot contains Drops or Deliverables, a warning is included in the import summary."
29904
+ ),
29905
+ targetOrgId: external_exports.string().min(1).optional().describe(
29906
+ "Import all records under this org ID instead of the caller's org. Required when syncing from production to a local environment where the org UUID differs. When omitted, the caller's org ID is used."
29837
29907
  )
29838
29908
  },
29839
- async ({ snapshot: snapshotInput, targetTeamspaceId }) => {
29909
+ async ({ snapshot: snapshotInput, targetTeamspaceId, targetOrgId }) => {
29840
29910
  const isProduction = process.env.NODE_ENV === "production";
29841
29911
  const importAllowed = process.env.ALLOW_SYSTEM_IMPORT === "true";
29842
29912
  if (isProduction && !importAllowed) {
@@ -29849,7 +29919,7 @@ function registerSnapshotTools(server, ctx, client) {
29849
29919
  };
29850
29920
  }
29851
29921
  const trimmed = snapshotInput.trim();
29852
- const importOptions = { targetOrgId: ctx.orgId, targetUserId: ctx.user.userId, targetTeamspaceId };
29922
+ const importOptions = { targetOrgId: targetOrgId ?? ctx.orgId, targetUserId: ctx.user.userId, targetTeamspaceId };
29853
29923
  if (trimmed.startsWith("https://")) {
29854
29924
  if (isPrivateHost(trimmed)) {
29855
29925
  throw new Error("Snapshot URL must point to a public host \u2014 private, link-local, and loopback addresses are not permitted.");
@@ -29919,7 +29989,8 @@ function normalizeSnapshot(raw) {
29919
29989
  trackSystemIndex: raw.trackSystemIndex ?? anyRaw["trackProjectIndex"] ?? [],
29920
29990
  trackTeamspaceIndex: raw.trackTeamspaceIndex ?? [],
29921
29991
  workItems: raw.workItems ?? [],
29922
- workItemIndex: raw.workItemIndex ?? []
29992
+ workItemIndex: raw.workItemIndex ?? [],
29993
+ accounts: raw.accounts ?? []
29923
29994
  };
29924
29995
  }
29925
29996
  async function resolveSnapshotInput(input) {
@@ -29946,7 +30017,11 @@ function formatImportSummary(result, targetTeamspaceId) {
29946
30017
  ` Teamspaces: ${result.counts.teamspaces ?? 0}`,
29947
30018
  ` Layers: ${result.counts.layers ?? 0}`,
29948
30019
  ` Tracks: ${result.counts.tracks ?? 0}`,
29949
- ` Work Items: ${result.counts.workItems ?? 0}`
30020
+ ` Work Items: ${result.counts.workItems ?? 0}`,
30021
+ ` Movements: ${result.counts.movements ?? 0}`,
30022
+ ` Bars: ${result.counts.bars ?? 0}`,
30023
+ ` Notebooks: ${result.counts.notebooks ?? 0}`,
30024
+ ` Accounts: ${result.counts.accounts ?? 0}`
29950
30025
  ];
29951
30026
  if (result.errors.length > 0) {
29952
30027
  lines.push("", `Errors (${result.errors.length}):`);
@@ -31440,6 +31515,7 @@ function registerAllTools(server, ctx, client, profile) {
31440
31515
  registerSnapshotTools(server, ctx, client);
31441
31516
  registerBeatQualityTools(server, ctx, client);
31442
31517
  registerPlanQualityTools(server, ctx, client);
31518
+ registerRevisionQualityTools(server, ctx, client);
31443
31519
  registerPortfolioCoherenceTools(server, ctx, client);
31444
31520
  registerDropQualityTools(server, ctx, client);
31445
31521
  registerBeatReframeTools(server, ctx, client);
@@ -31467,7 +31543,6 @@ function registerAllTools(server, ctx, client, profile) {
31467
31543
  registerNotebookTools(server, ctx, client);
31468
31544
  registerAccountTools(server, ctx, client);
31469
31545
  registerMovementTools(server, ctx, client);
31470
- registerMeasureTools(server, ctx, client);
31471
31546
  registerWorkItemTools(server, ctx, client);
31472
31547
  registerBarTools(server, ctx, client);
31473
31548
  registerCadenceScheduleTools(server, ctx, client);
@@ -31674,7 +31749,7 @@ function createHttpClient(config2) {
31674
31749
  }
31675
31750
  return result.edge;
31676
31751
  }
31677
- const POLL_INTERVAL_MS5 = 4e3;
31752
+ const POLL_INTERVAL_MS6 = 4e3;
31678
31753
  async function pollTaskResult(taskId) {
31679
31754
  const deadline = Date.now() + LONG_RUNNING_TIMEOUT_MS;
31680
31755
  while (Date.now() < deadline) {
@@ -31684,7 +31759,7 @@ function createHttpClient(config2) {
31684
31759
  );
31685
31760
  if (task?.status === "completed") return task.result;
31686
31761
  if (task?.status === "failed") throw new Error(`Task failed: ${task.error ?? "unknown error"}`);
31687
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS5));
31762
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS6));
31688
31763
  }
31689
31764
  throw new Error(`Task ${taskId} timed out after ${LONG_RUNNING_TIMEOUT_MS}ms`);
31690
31765
  }
@@ -32010,6 +32085,7 @@ function createHttpClient(config2) {
32010
32085
  }
32011
32086
  if (filters?.status) params.set("status", filters.status);
32012
32087
  if (filters?.revisionId) params.set("revisionId", filters.revisionId);
32088
+ if (filters?.beatVersionId) params.set("beatVersionId", filters.beatVersionId);
32013
32089
  if (filters?.excludeChildren) params.set("excludeChildren", "true");
32014
32090
  if (filters?.significance) params.set("significance", filters.significance);
32015
32091
  if (filters?.limit !== void 0) params.set("limit", String(filters.limit));
@@ -32025,6 +32101,7 @@ function createHttpClient(config2) {
32025
32101
  }
32026
32102
  if (filters?.status) params.set("status", filters.status);
32027
32103
  if (filters?.revisionId) params.set("revisionId", filters.revisionId);
32104
+ if (filters?.beatVersionId) params.set("beatVersionId", filters.beatVersionId);
32028
32105
  if (filters?.excludeChildren) params.set("excludeChildren", "true");
32029
32106
  if (filters?.significance) params.set("significance", filters.significance);
32030
32107
  if (filters?.limit !== void 0) params.set("limit", String(filters.limit));
@@ -32041,6 +32118,7 @@ function createHttpClient(config2) {
32041
32118
  if (filters?.excludeNoteType) params.set("excludeType", filters.excludeNoteType);
32042
32119
  if (filters?.status) params.set("status", filters.status);
32043
32120
  if (filters?.revisionId) params.set("revisionId", filters.revisionId);
32121
+ if (filters?.beatVersionId) params.set("beatVersionId", filters.beatVersionId);
32044
32122
  if (filters?.excludeChildren) params.set("excludeChildren", "true");
32045
32123
  if (filters?.significance) params.set("significance", filters.significance);
32046
32124
  if (limit !== void 0) params.set("limit", String(limit));
@@ -32059,6 +32137,7 @@ function createHttpClient(config2) {
32059
32137
  }
32060
32138
  if (filters?.status) params.set("status", filters.status);
32061
32139
  if (filters?.revisionId) params.set("revisionId", filters.revisionId);
32140
+ if (filters?.beatVersionId) params.set("beatVersionId", filters.beatVersionId);
32062
32141
  if (filters?.excludeChildren) params.set("excludeChildren", "true");
32063
32142
  if (filters?.significance) params.set("significance", filters.significance);
32064
32143
  const qs = params.toString() ? `?${params.toString()}` : "";
@@ -32902,7 +32981,7 @@ function createHttpClient(config2) {
32902
32981
  return request("GET", `/api/subscriptions/entity/${encodeURIComponent(entityId)}`, void 0);
32903
32982
  },
32904
32983
  // Project Snapshot — export runs as a background task to avoid Lambda timeout
32905
- exportProjectSnapshot: async (projectId) => {
32984
+ exportSystemSnapshot: async (projectId) => {
32906
32985
  const pid = encodeURIComponent(projectId);
32907
32986
  const enqueued = await request("POST", `/api/systems/${pid}/export`, {});
32908
32987
  if (!enqueued?.taskId) throw new Error("Export enqueue failed: no taskId returned");
@@ -33148,22 +33227,10 @@ function createHttpClient(config2) {
33148
33227
  const res = await request("GET", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/drops`);
33149
33228
  return res?.drops ?? [];
33150
33229
  },
33151
- listDeliverableGroupBeatVersions: async (deliverableGroupId) => {
33152
- const res = await request("GET", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/beat-versions`);
33153
- return res?.beatVersions ?? [];
33154
- },
33155
- setBeatVersionDeliverableGroup: async (deliverableGroupId, beatVersionId) => {
33156
- const res = await request("POST", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/beat-versions`, { beatVersionId });
33157
- return res?.success === true;
33158
- },
33159
33230
  setDropDeliverableGroup: async (deliverableGroupId, dropId) => {
33160
33231
  const res = await request("POST", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/drops`, { dropId });
33161
33232
  return res?.success === true;
33162
33233
  },
33163
- clearBeatVersionDeliverableGroup: async (deliverableGroupId, beatVersionId) => {
33164
- const res = await request("DELETE", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/beat-versions/${encodeURIComponent(beatVersionId)}`);
33165
- return res?.success === true;
33166
- },
33167
33234
  // Movements (B-242 / teamspace-scoped SOW container)
33168
33235
  createMovement: async (input) => {
33169
33236
  const { teamspaceId, title, description, accountId } = input;
@@ -33209,73 +33276,6 @@ function createHttpClient(config2) {
33209
33276
  if (!parsed.success) throw new Error(`Unexpected movements list shape from API: ${parsed.error.message}`);
33210
33277
  return parsed.data.movements;
33211
33278
  },
33212
- // Measures (Measure exposure layer — atomic Work Plan unit under a Track).
33213
- // valueUnit is server-stripped for non-admins (N-4E09-6509) — the schema
33214
- // makes it optional; consumers must treat its absence as "not permitted".
33215
- createMeasure: async (input) => {
33216
- const { trackId, title, valueUnit, beatVersionId, createdBy } = input;
33217
- const body = {
33218
- title,
33219
- valueUnit,
33220
- ...beatVersionId !== void 0 && { beatVersionId },
33221
- ...createdBy !== void 0 && { createdBy }
33222
- };
33223
- const res = await request(
33224
- "POST",
33225
- `/api/deliverable-groups/${encodeURIComponent(trackId)}/measures`,
33226
- body
33227
- );
33228
- if (res === void 0) throw new Error(`Track ${trackId} not found`);
33229
- const parsed = MeasureApiSchema.safeParse(res.measure);
33230
- if (!parsed.success) throw new Error(`Unexpected Measure shape from API: ${parsed.error.message}`);
33231
- return parsed.data;
33232
- },
33233
- getMeasure: async (measureId) => {
33234
- const res = await request("GET", `/api/measures/${encodeURIComponent(measureId)}`);
33235
- if (res === void 0) return void 0;
33236
- const parsed = MeasureApiSchema.safeParse(res.measure);
33237
- if (!parsed.success) throw new Error(`Unexpected Measure shape from API: ${parsed.error.message}`);
33238
- return parsed.data;
33239
- },
33240
- updateMeasure: async (measureId, updates) => {
33241
- const res = await request("PATCH", `/api/measures/${encodeURIComponent(measureId)}`, updates);
33242
- if (res === void 0) return void 0;
33243
- const parsed = MeasureApiSchema.safeParse(res.measure);
33244
- if (!parsed.success) throw new Error(`Unexpected Measure shape from API: ${parsed.error.message}`);
33245
- return parsed.data;
33246
- },
33247
- listTrackMeasures: async (trackId) => {
33248
- const res = await request("GET", `/api/deliverable-groups/${encodeURIComponent(trackId)}/measures`);
33249
- if (res === void 0) throw new Error(`Track ${trackId} not found`);
33250
- const parsed = MeasuresResponseSchema.safeParse(res);
33251
- if (!parsed.success) throw new Error(`Unexpected measures list shape from API: ${parsed.error.message}`);
33252
- return parsed.data.measures;
33253
- },
33254
- listBeatVersionMeasures: async (beatVersionId) => {
33255
- const res = await request("GET", `/api/beat-versions/${encodeURIComponent(beatVersionId)}/measures`);
33256
- if (res === void 0) throw new Error(`Beat Version ${beatVersionId} not found`);
33257
- const parsed = MeasuresResponseSchema.safeParse(res);
33258
- if (!parsed.success) throw new Error(`Unexpected measures list shape from API: ${parsed.error.message}`);
33259
- return parsed.data.measures;
33260
- },
33261
- linkMeasureToBeatVersion: async (measureId, beatVersionId) => {
33262
- const res = await request(
33263
- "POST",
33264
- `/api/measures/${encodeURIComponent(measureId)}/beat-version`,
33265
- { beatVersionId }
33266
- );
33267
- if (res === void 0) return void 0;
33268
- const parsed = MeasureApiSchema.safeParse(res.measure);
33269
- if (!parsed.success) throw new Error(`Unexpected Measure shape from API: ${parsed.error.message}`);
33270
- return parsed.data;
33271
- },
33272
- unlinkMeasureFromBeatVersion: async (measureId) => {
33273
- const res = await request("DELETE", `/api/measures/${encodeURIComponent(measureId)}/beat-version`);
33274
- if (res === void 0) return void 0;
33275
- const parsed = MeasureApiSchema.safeParse(res.measure);
33276
- if (!parsed.success) throw new Error(`Unexpected Measure shape from API: ${parsed.error.message}`);
33277
- return parsed.data;
33278
- },
33279
33279
  // WorkItems (WorkItem exposure layer — owned, statused unit of operational work under a Track)
33280
33280
  createWorkItem: async (input) => {
33281
33281
  const { trackId, title, owner, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId } = input;
@@ -33828,7 +33828,7 @@ function loadConfig() {
33828
33828
  };
33829
33829
  }
33830
33830
  async function main() {
33831
- console.error(`[harmonica-mcp] v${"2.1.0"} starting\u2026`);
33831
+ console.error(`[harmonica-mcp] v${"3.0.1"} starting\u2026`);
33832
33832
  const config2 = loadConfig();
33833
33833
  const client = createHttpClient({
33834
33834
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "2.1.0",
3
+ "version": "3.0.1",
4
4
  "description": "MCP server for Harmonica — connect any MCP-compatible AI assistant to Harmonica",
5
5
  "license": "MIT",
6
6
  "bin": {