@codazen/harmonica-mcp 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +833 -742
- 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.
|
|
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
|
-
|
|
22480
|
-
"
|
|
22481
|
-
|
|
22482
|
-
|
|
22483
|
-
|
|
22484
|
-
|
|
22485
|
-
|
|
22486
|
-
|
|
22487
|
-
|
|
22488
|
-
|
|
22489
|
-
|
|
22490
|
-
|
|
22491
|
-
|
|
22492
|
-
|
|
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
|
}
|
|
@@ -22598,7 +22627,7 @@ ${beat.description}`);
|
|
|
22598
22627
|
return sections.join("\n");
|
|
22599
22628
|
}
|
|
22600
22629
|
|
|
22601
|
-
// ../../libs/harmonica-services/src/mcp/formatters/
|
|
22630
|
+
// ../../libs/harmonica-services/src/mcp/formatters/system-formatter.ts
|
|
22602
22631
|
function formatProjectSummaryTable(projects) {
|
|
22603
22632
|
if (projects.length === 0) return "_No projects found._";
|
|
22604
22633
|
const header = "| Project ID | Title | Status |\n|------------|-------|--------|";
|
|
@@ -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
|
-
|
|
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}`);
|
|
@@ -23018,7 +23051,7 @@ function registerBeatTools(server, ctx, client) {
|
|
|
23018
23051
|
},
|
|
23019
23052
|
async ({ projectId, offset = 0, limit = 100, includeArchived }) => {
|
|
23020
23053
|
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
23021
|
-
let beats = await client.
|
|
23054
|
+
let beats = await client.listSystemBeats(projectId);
|
|
23022
23055
|
if (!includeArchived) {
|
|
23023
23056
|
beats = beats.filter((b) => b.beatStatus !== "archived");
|
|
23024
23057
|
}
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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 (
|
|
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 {
|
|
@@ -23803,7 +23865,7 @@ function registerCheckTools(server, ctx, client) {
|
|
|
23803
23865
|
checks = await client.listBeatChecks(beatId, projectId, checkType);
|
|
23804
23866
|
scope = `beat ${beatId}`;
|
|
23805
23867
|
} else {
|
|
23806
|
-
const result = await client.
|
|
23868
|
+
const result = await client.listSystemChecks(projectId, checkType, { limit: 200 });
|
|
23807
23869
|
checks = result.checks;
|
|
23808
23870
|
scope = `project ${projectId}`;
|
|
23809
23871
|
}
|
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
23855
|
-
|
|
23856
|
-
|
|
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
|
-
|
|
23880
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
24300
|
-
|
|
24301
|
-
|
|
24302
|
-
|
|
24303
|
-
|
|
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
|
|
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 = [
|
|
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,24 +26641,15 @@ 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"]);
|
|
26645
|
+
var WORK_ITEM_EFFORT_SIZES = [
|
|
26646
|
+
"XS",
|
|
26647
|
+
"S",
|
|
26648
|
+
"M",
|
|
26649
|
+
"L",
|
|
26650
|
+
"XL"
|
|
26651
|
+
];
|
|
26652
|
+
var WorkItemEffortSizeSchema = external_exports.enum(WORK_ITEM_EFFORT_SIZES);
|
|
26762
26653
|
var HumanAssigneeApiSchema = external_exports.object({
|
|
26763
26654
|
name: external_exports.string(),
|
|
26764
26655
|
email: external_exports.string()
|
|
@@ -26769,10 +26660,16 @@ var WorkItemApiSchema = external_exports.object({
|
|
|
26769
26660
|
teamspaceId: external_exports.string(),
|
|
26770
26661
|
orgId: external_exports.string(),
|
|
26771
26662
|
title: external_exports.string(),
|
|
26772
|
-
|
|
26663
|
+
/** Optional since N-4E09-6672 — a work item may be unowned. */
|
|
26664
|
+
owner: HumanAssigneeApiSchema.optional(),
|
|
26773
26665
|
status: WorkItemStatusSchema,
|
|
26774
26666
|
committedEstimateHours: external_exports.number().optional(),
|
|
26775
26667
|
actualHours: external_exports.number().optional(),
|
|
26668
|
+
/** Delivery lens — rough t-shirt effort sizing (N-4E09-6672). */
|
|
26669
|
+
effortSize: WorkItemEffortSizeSchema.optional(),
|
|
26670
|
+
/** Commercial lens — deliberately unblended with `effortSize` (N-4E09-6672). */
|
|
26671
|
+
valueUnits: external_exports.number().optional(),
|
|
26672
|
+
costPerValueUnit: external_exports.number().optional(),
|
|
26776
26673
|
/** Optional capability-lineage link to a Beat Version (v2, N-4E09-6574). */
|
|
26777
26674
|
beatVersionId: external_exports.string().optional(),
|
|
26778
26675
|
createdBy: external_exports.string().optional(),
|
|
@@ -26916,7 +26813,6 @@ var WorkPlanBeatVersionApiSchema = external_exports.object({
|
|
|
26916
26813
|
versionNumber: external_exports.number(),
|
|
26917
26814
|
title: external_exports.string(),
|
|
26918
26815
|
status: BeatVersionStatusSchema,
|
|
26919
|
-
deliverableGroupId: external_exports.string().nullish(),
|
|
26920
26816
|
// Derived Value Units (VU = Δf × W), read-only. 0 when the BV has no confirmed
|
|
26921
26817
|
// value-velocity inputs. Computed server-side from the parent Beat's weight.
|
|
26922
26818
|
// `.default(0)` tolerates a rolling deploy where an older API omits the field —
|
|
@@ -26928,6 +26824,19 @@ var WorkPlanBeatVersionApiSchema = external_exports.object({
|
|
|
26928
26824
|
var DeliverableGroupBeatVersionsResponseSchema = external_exports.object({
|
|
26929
26825
|
beatVersions: external_exports.array(WorkPlanBeatVersionApiSchema)
|
|
26930
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();
|
|
26931
26840
|
|
|
26932
26841
|
// ../../libs/harmonica-services/src/mcp/tools/note-element-tools.ts
|
|
26933
26842
|
var parentKindSchema = external_exports.enum(["notebook", "note"]);
|
|
@@ -27100,13 +27009,14 @@ var DECISION_SIGNIFICANCE_VALUES2 = ["strategic", "structural", "implementation"
|
|
|
27100
27009
|
function registerNoteTools(server, ctx, client) {
|
|
27101
27010
|
server.tool(
|
|
27102
27011
|
"list_notes",
|
|
27103
|
-
"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.",
|
|
27012
|
+
"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.",
|
|
27104
27013
|
{
|
|
27105
27014
|
projectId: external_exports.string().describe("The project ID"),
|
|
27106
27015
|
beatId: external_exports.string().optional().describe("Scope to a specific beat (omit for all project notes)"),
|
|
27107
27016
|
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"])'),
|
|
27108
27017
|
status: external_exports.enum(NOTE_STATUS_VALUES).optional().describe("Filter by status"),
|
|
27109
27018
|
revisionId: external_exports.string().optional().describe("Filter to notes scoped to a specific revision"),
|
|
27019
|
+
beatVersionId: external_exports.string().optional().describe("Filter to notes scoped to a specific Beat Version (planning increment)"),
|
|
27110
27020
|
sourceDocumentNoteId: external_exports.string().optional().describe("Filter to notes extracted from a specific document (matches sourceSubmissionId)"),
|
|
27111
27021
|
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."),
|
|
27112
27022
|
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)"),
|
|
@@ -27114,7 +27024,7 @@ function registerNoteTools(server, ctx, client) {
|
|
|
27114
27024
|
cursor: external_exports.string().optional().describe("Opaque pagination cursor from a previous response \u2014 use for project-wide note pagination (no beatId)"),
|
|
27115
27025
|
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.")
|
|
27116
27026
|
},
|
|
27117
|
-
async ({ projectId, beatId, noteType, status, revisionId, sourceDocumentNoteId, significance, offset = 0, limit = 100, cursor, orderBy }) => {
|
|
27027
|
+
async ({ projectId, beatId, noteType, status, revisionId, beatVersionId, sourceDocumentNoteId, significance, offset = 0, limit = 100, cursor, orderBy }) => {
|
|
27118
27028
|
if (Array.isArray(noteType) && noteType.length === 0) {
|
|
27119
27029
|
return { content: [{ type: "text", text: "noteType must not be an empty array \u2014 omit it to return all types." }], isError: true };
|
|
27120
27030
|
}
|
|
@@ -27135,6 +27045,7 @@ function registerNoteTools(server, ctx, client) {
|
|
|
27135
27045
|
noteType,
|
|
27136
27046
|
status,
|
|
27137
27047
|
revisionId,
|
|
27048
|
+
beatVersionId,
|
|
27138
27049
|
significance
|
|
27139
27050
|
};
|
|
27140
27051
|
let page;
|
|
@@ -27330,13 +27241,14 @@ function registerNoteTools(server, ctx, client) {
|
|
|
27330
27241
|
content: external_exports.string().describe("The note content"),
|
|
27331
27242
|
beatId: external_exports.string().optional().describe("Beat ID if this note is beat-scoped (requires projectId)"),
|
|
27332
27243
|
revisionId: external_exports.string().optional().describe("Revision ID if this note is revision-scoped (requires projectId)"),
|
|
27244
|
+
beatVersionId: external_exports.string().optional().describe("Beat Version ID if this note governs a specific planning increment (requires beatId)"),
|
|
27333
27245
|
rationale: external_exports.string().optional().describe("Why this note exists"),
|
|
27334
27246
|
confidence: external_exports.coerce.number().min(0).max(1).optional().describe("Confidence level for assumptions (0-1)"),
|
|
27335
27247
|
affectsBeats: external_exports.array(external_exports.string()).optional().describe("Beat IDs this note impacts"),
|
|
27336
27248
|
dependsOnNotes: external_exports.array(external_exports.string()).optional().describe("Note IDs this note depends on (child \u2192 parent links for consolidation)"),
|
|
27337
27249
|
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.")
|
|
27338
27250
|
},
|
|
27339
|
-
async ({ projectId, teamspaceId, movementId, noteType, content, beatId, revisionId, rationale, confidence, affectsBeats, dependsOnNotes, significance }) => {
|
|
27251
|
+
async ({ projectId, teamspaceId, movementId, noteType, content, beatId, revisionId, beatVersionId, rationale, confidence, affectsBeats, dependsOnNotes, significance }) => {
|
|
27340
27252
|
try {
|
|
27341
27253
|
if ([projectId, teamspaceId, movementId].filter(Boolean).length > 1) {
|
|
27342
27254
|
return { content: [{ type: "text", text: "Specify at most one of projectId, teamspaceId, or movementId." }], isError: true };
|
|
@@ -27352,6 +27264,7 @@ function registerNoteTools(server, ctx, client) {
|
|
|
27352
27264
|
projectId,
|
|
27353
27265
|
beatId,
|
|
27354
27266
|
revisionId,
|
|
27267
|
+
beatVersionId,
|
|
27355
27268
|
noteType,
|
|
27356
27269
|
content,
|
|
27357
27270
|
rationale,
|
|
@@ -28656,10 +28569,10 @@ async function pollForCheck3(client, taskId) {
|
|
|
28656
28569
|
if (!task.result) throw new Error(`Task ${taskId} completed with no result`);
|
|
28657
28570
|
return task.result;
|
|
28658
28571
|
}
|
|
28659
|
-
if (task.status === "failed") throw new Error(`
|
|
28572
|
+
if (task.status === "failed") throw new Error(`Beat version quality check failed: ${task.error ?? "unknown error"}`);
|
|
28660
28573
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS3));
|
|
28661
28574
|
}
|
|
28662
|
-
throw new Error(`
|
|
28575
|
+
throw new Error(`Beat version quality check timed out after ${POLL_TIMEOUT_MS3 / 1e3}s`);
|
|
28663
28576
|
}
|
|
28664
28577
|
function formatScorecard2(check2) {
|
|
28665
28578
|
const bar = (score, max) => "\u2588".repeat(score) + "\u2591".repeat(max - score);
|
|
@@ -28669,8 +28582,8 @@ ${d.rationale}${d.suggestions?.length ? `
|
|
|
28669
28582
|
\u2022 ${d.suggestions.join("\n\u2022 ")}` : ""}`
|
|
28670
28583
|
).join("\n\n");
|
|
28671
28584
|
return [
|
|
28672
|
-
`##
|
|
28673
|
-
`**Overall Score:** ${check2.overallScore}/${CHECK_MAX_SCORES.
|
|
28585
|
+
`## Beat Version Quality Check \u2014 ${check2.targetId}`,
|
|
28586
|
+
`**Overall Score:** ${check2.overallScore}/${CHECK_MAX_SCORES.beat_version_quality}`,
|
|
28674
28587
|
`**Summary:** ${check2.summary}`,
|
|
28675
28588
|
check2.topSuggestion ? `**Top Suggestion:** ${check2.topSuggestion}` : "",
|
|
28676
28589
|
"",
|
|
@@ -28678,48 +28591,63 @@ ${d.rationale}${d.suggestions?.length ? `
|
|
|
28678
28591
|
].filter(Boolean).join("\n");
|
|
28679
28592
|
}
|
|
28680
28593
|
function registerPlanQualityTools(server, ctx, client) {
|
|
28681
|
-
|
|
28682
|
-
"
|
|
28683
|
-
|
|
28684
|
-
|
|
28685
|
-
|
|
28686
|
-
|
|
28687
|
-
|
|
28688
|
-
|
|
28689
|
-
|
|
28690
|
-
|
|
28691
|
-
|
|
28692
|
-
|
|
28693
|
-
|
|
28694
|
-
|
|
28695
|
-
|
|
28696
|
-
|
|
28697
|
-
|
|
28698
|
-
|
|
28699
|
-
|
|
28700
|
-
|
|
28701
|
-
|
|
28702
|
-
|
|
28703
|
-
|
|
28704
|
-
|
|
28705
|
-
|
|
28706
|
-
|
|
28594
|
+
const schema = {
|
|
28595
|
+
beatVersionId: external_exports.string().describe("The Beat Version ID (e.g., bv-abc123). beat_version_quality targets Beat Versions only."),
|
|
28596
|
+
projectId: external_exports.string().describe("The project ID"),
|
|
28597
|
+
// The completeness dimension probes the codebase, so this tool accepts the
|
|
28598
|
+
// same repo overrides as run_check — without them the probe could only ever
|
|
28599
|
+
// read the default branch from this surface.
|
|
28600
|
+
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)"),
|
|
28601
|
+
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"),
|
|
28602
|
+
wait: external_exports.boolean().optional().describe("If true, block until the check completes and return the scorecard inline. Default: false (returns taskId immediately).")
|
|
28603
|
+
};
|
|
28604
|
+
const handler = async ({ beatVersionId, projectId, branch, localPath, wait }) => {
|
|
28605
|
+
try {
|
|
28606
|
+
const targetError = beatVersionQualityTargetError(beatVersionId ?? "");
|
|
28607
|
+
if (targetError) {
|
|
28608
|
+
return {
|
|
28609
|
+
content: [{ type: "text", text: `Beat version quality check failed: ${targetError}` }],
|
|
28610
|
+
isError: true
|
|
28611
|
+
};
|
|
28612
|
+
}
|
|
28613
|
+
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
28614
|
+
const opts = branch || localPath ? { branch, localPath } : void 0;
|
|
28615
|
+
const task = await client.runCheck(projectId, "beat_version_quality", beatVersionId, opts);
|
|
28616
|
+
if (!wait) {
|
|
28617
|
+
return {
|
|
28618
|
+
content: [{
|
|
28619
|
+
type: "text",
|
|
28620
|
+
text: `Beat version quality check enqueued for ${beatVersionId}.
|
|
28707
28621
|
Job ID: ${task.taskId}
|
|
28708
28622
|
|
|
28709
28623
|
Use get_job_status with jobId="${task.taskId}" to poll for results, or list_checks to retrieve the completed scorecard once done.`
|
|
28710
|
-
|
|
28711
|
-
|
|
28712
|
-
}
|
|
28713
|
-
const check2 = await pollForCheck3(client, task.taskId);
|
|
28714
|
-
return { content: [{ type: "text", text: formatScorecard2(check2) }] };
|
|
28715
|
-
} catch (err) {
|
|
28716
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
28717
|
-
return { content: [{ type: "text", text: `Plan quality check failed: ${message}` }], isError: true };
|
|
28624
|
+
}]
|
|
28625
|
+
};
|
|
28718
28626
|
}
|
|
28627
|
+
const check2 = await pollForCheck3(client, task.taskId);
|
|
28628
|
+
return { content: [{ type: "text", text: formatScorecard2(check2) }] };
|
|
28629
|
+
} catch (err) {
|
|
28630
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28631
|
+
return { content: [{ type: "text", text: `Beat version quality check failed: ${message}` }], isError: true };
|
|
28719
28632
|
}
|
|
28633
|
+
};
|
|
28634
|
+
server.tool(
|
|
28635
|
+
"check_beat_version_quality",
|
|
28636
|
+
"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.",
|
|
28637
|
+
schema,
|
|
28638
|
+
handler
|
|
28720
28639
|
);
|
|
28721
|
-
|
|
28722
|
-
|
|
28640
|
+
server.tool(
|
|
28641
|
+
"check_plan_quality",
|
|
28642
|
+
"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.",
|
|
28643
|
+
schema,
|
|
28644
|
+
async (args) => {
|
|
28645
|
+
console.warn("[deprecated-tool] check_plan_quality invoked \u2014 use check_beat_version_quality");
|
|
28646
|
+
return handler(args);
|
|
28647
|
+
}
|
|
28648
|
+
);
|
|
28649
|
+
}
|
|
28650
|
+
|
|
28723
28651
|
// ../../libs/harmonica-services/src/mcp/tools/portfolio-coherence-tools.ts
|
|
28724
28652
|
var POLL_INTERVAL_MS4 = 4e3;
|
|
28725
28653
|
var POLL_TIMEOUT_MS4 = 18e4;
|
|
@@ -28882,7 +28810,7 @@ function registerPortfolioCoherenceTools(server, ctx, client) {
|
|
|
28882
28810
|
isError: true
|
|
28883
28811
|
};
|
|
28884
28812
|
}
|
|
28885
|
-
const allProjectBeats = await client.
|
|
28813
|
+
const allProjectBeats = await client.listSystemBeats(projectId);
|
|
28886
28814
|
const beatMap = new Map(allProjectBeats.map((b) => [b.beatId, b]));
|
|
28887
28815
|
const primaryBeat = beatMap.get(resolvedPrimaryId);
|
|
28888
28816
|
if (!primaryBeat) {
|
|
@@ -28967,269 +28895,6 @@ function registerPortfolioCoherenceTools(server, ctx, client) {
|
|
|
28967
28895
|
);
|
|
28968
28896
|
}
|
|
28969
28897
|
|
|
28970
|
-
// ../../libs/harmonica-services/src/mcp/tools/project-lifecycle-tools.ts
|
|
28971
|
-
function registerProjectLifecycleTools(server, ctx, client) {
|
|
28972
|
-
server.tool(
|
|
28973
|
-
"transition_system_lifecycle",
|
|
28974
|
-
"Advance a System's capability-maturity lifecycle state. Allowed transitions follow the state machine (Concept \u2192 Incubating \u2192 Piloting \u2192 Activated \u2192 Commercializing \u2192 Scaled, plus paused/killed/sunset/archived exits). The activated \u2192 commercializing edge requires a `decisionNoteId` pointing to a system-scoped Decision Note (the governance review). Other gates are advisory.",
|
|
28975
|
-
{
|
|
28976
|
-
systemId: external_exports.string().describe("The system ID"),
|
|
28977
|
-
targetState: external_exports.enum([
|
|
28978
|
-
"concept",
|
|
28979
|
-
"incubating",
|
|
28980
|
-
"piloting",
|
|
28981
|
-
"activated",
|
|
28982
|
-
"commercializing",
|
|
28983
|
-
"scaled",
|
|
28984
|
-
"paused",
|
|
28985
|
-
"killed",
|
|
28986
|
-
"sunset",
|
|
28987
|
-
"archived"
|
|
28988
|
-
]).describe("Target lifecycle state. Active: concept, incubating, piloting, activated, commercializing, scaled. Exits: paused, killed, sunset, archived."),
|
|
28989
|
-
reason: external_exports.string().optional().describe("Why this transition is being made (recorded in audit metadata)"),
|
|
28990
|
-
decisionNoteId: external_exports.string().optional().describe("Note ID of the governance Decision Note. Required for the activated \u2192 commercializing transition; optional otherwise.")
|
|
28991
|
-
},
|
|
28992
|
-
async ({ systemId, targetState, reason, decisionNoteId }) => {
|
|
28993
|
-
try {
|
|
28994
|
-
const system = await client.getSystem(systemId);
|
|
28995
|
-
if (!system) {
|
|
28996
|
-
return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
|
|
28997
|
-
}
|
|
28998
|
-
if (system.orgId !== ctx.orgId) {
|
|
28999
|
-
return { content: [{ type: "text", text: `System "${systemId}" is not in this organization` }], isError: true };
|
|
29000
|
-
}
|
|
29001
|
-
const result = await client.transitionProjectLifecycleState(systemId, targetState, {
|
|
29002
|
-
actor: { type: "human", id: ctx.user.userId, name: ctx.user.name },
|
|
29003
|
-
reason,
|
|
29004
|
-
decisionNoteId
|
|
29005
|
-
});
|
|
29006
|
-
if (!result.success) {
|
|
29007
|
-
const failedGates = result.error?.failedGates?.length ? ` (failed gates: ${result.error.failedGates.join(", ")})` : "";
|
|
29008
|
-
return {
|
|
29009
|
-
content: [{ type: "text", text: `Transition failed: ${result.error?.message ?? "Unknown error"}${failedGates}` }],
|
|
29010
|
-
isError: true
|
|
29011
|
-
};
|
|
29012
|
-
}
|
|
29013
|
-
const lines = [
|
|
29014
|
-
`System lifecycle transitioned successfully.`,
|
|
29015
|
-
"",
|
|
29016
|
-
`**System:** ${systemId}`,
|
|
29017
|
-
`**Title:** ${system.title}`,
|
|
29018
|
-
`**Transition:** ${result.previousState} \u2192 ${result.newState}`
|
|
29019
|
-
];
|
|
29020
|
-
if (reason) lines.push(`**Reason:** ${reason}`);
|
|
29021
|
-
if (decisionNoteId) lines.push(`**Decision Note:** ${decisionNoteId}`);
|
|
29022
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
29023
|
-
} catch (err) {
|
|
29024
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29025
|
-
return { content: [{ type: "text", text: `Failed to transition system lifecycle: ${message}` }], isError: true };
|
|
29026
|
-
}
|
|
29027
|
-
}
|
|
29028
|
-
);
|
|
29029
|
-
}
|
|
29030
|
-
|
|
29031
|
-
// ../../libs/harmonica-services/src/mcp/tools/project-tools.ts
|
|
29032
|
-
var import_crypto5 = require("crypto");
|
|
29033
|
-
function accountLine(request, resolved) {
|
|
29034
|
-
const requested = request["accountId"];
|
|
29035
|
-
if (requested === null || requested === "") return "**Account:** (unlinked)";
|
|
29036
|
-
return resolved ? `**Account:** ${resolved}` : "";
|
|
29037
|
-
}
|
|
29038
|
-
var PROJECT_EMBEDDING_FIELDS = ["title", "description", "strategy"];
|
|
29039
|
-
function registerProjectTools(server, ctx, client) {
|
|
29040
|
-
const listSystemsHandler = async ({ teamspaceId }) => {
|
|
29041
|
-
try {
|
|
29042
|
-
const projects = await client.listOrgSystems(ctx.orgId);
|
|
29043
|
-
const trimmed = teamspaceId?.trim();
|
|
29044
|
-
const filtered = trimmed ? projects.filter((p) => p.teamspaceId === trimmed) : projects;
|
|
29045
|
-
const text = formatProjectSummaryTable(filtered);
|
|
29046
|
-
return { content: [{ type: "text", text }] };
|
|
29047
|
-
} catch (err) {
|
|
29048
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29049
|
-
return { content: [{ type: "text", text: `Failed to list systems: ${message}` }], isError: true };
|
|
29050
|
-
}
|
|
29051
|
-
};
|
|
29052
|
-
server.tool(
|
|
29053
|
-
"list_systems",
|
|
29054
|
-
"List all systems (projects) in the configured organization",
|
|
29055
|
-
{
|
|
29056
|
-
teamspaceId: external_exports.string().optional().describe("Filter to systems belonging to a specific teamspace. Empty or whitespace-only treated as no filter.")
|
|
29057
|
-
},
|
|
29058
|
-
listSystemsHandler
|
|
29059
|
-
);
|
|
29060
|
-
const getSystemContextSchema = {
|
|
29061
|
-
systemId: external_exports.string().describe("The system ID"),
|
|
29062
|
-
noteLimit: external_exports.number().int().min(1).max(MAX_CONTEXT_NOTE_LIMIT).optional().describe(
|
|
29063
|
-
`Max Notes to include, prioritised by note type (default ${DEFAULT_CONTEXT_NOTE_LIMIT}, max ${MAX_CONTEXT_NOTE_LIMIT}). Use list_notes or search for the full set.`
|
|
29064
|
-
)
|
|
29065
|
-
};
|
|
29066
|
-
const getSystemContextHandler = async ({
|
|
29067
|
-
systemId,
|
|
29068
|
-
noteLimit
|
|
29069
|
-
}) => {
|
|
29070
|
-
try {
|
|
29071
|
-
const [project, org] = await Promise.all([
|
|
29072
|
-
fetchProjectInOrg(client, systemId, ctx.orgId),
|
|
29073
|
-
client.getOrg(ctx.orgId)
|
|
29074
|
-
]);
|
|
29075
|
-
const notes = await client.listProjectNotes(systemId, {
|
|
29076
|
-
limit: noteLimit ?? DEFAULT_CONTEXT_NOTE_LIMIT
|
|
29077
|
-
});
|
|
29078
|
-
const text = formatProjectContext(project, notes, org?.coda);
|
|
29079
|
-
return { content: [{ type: "text", text }] };
|
|
29080
|
-
} catch (err) {
|
|
29081
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29082
|
-
return { content: [{ type: "text", text: `Failed to get system context: ${message}` }], isError: true };
|
|
29083
|
-
}
|
|
29084
|
-
};
|
|
29085
|
-
server.tool(
|
|
29086
|
-
"get_system_context",
|
|
29087
|
-
"Get system metadata, description, and notes in one view",
|
|
29088
|
-
getSystemContextSchema,
|
|
29089
|
-
getSystemContextHandler
|
|
29090
|
-
);
|
|
29091
|
-
const updateSystemSchema = {
|
|
29092
|
-
systemId: external_exports.string().describe("The system ID"),
|
|
29093
|
-
title: external_exports.string().optional().describe("New system title"),
|
|
29094
|
-
description: external_exports.string().optional().describe("New system description"),
|
|
29095
|
-
strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
|
|
29096
|
-
teamspaceId: external_exports.string().nullable().optional().describe("Teamspace ID to associate this system with; pass null to remove the association"),
|
|
29097
|
-
accountId: external_exports.string().nullable().optional().describe("Account that owns this System \u2014 the client or internal org unit. Pass null (or an empty string) to unlink. Reassigning moves the System so it is listed under exactly one Account."),
|
|
29098
|
-
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
29099
|
-
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
29100
|
-
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")'),
|
|
29101
|
-
rateLimitOverrides: external_exports.record(external_exports.string(), external_exports.object({ maxPerHour: external_exports.number().min(0) })).optional().describe('Per-task-type rate limit overrides, e.g. {"agent_chat":{"maxPerHour":100}}. Overrides env var and compiled defaults.')
|
|
29102
|
-
};
|
|
29103
|
-
const updateSystemHandler = async ({ systemId, ...updates }) => {
|
|
29104
|
-
try {
|
|
29105
|
-
const nonEmpty = Object.fromEntries(
|
|
29106
|
-
Object.entries(updates).filter(([, v]) => v !== void 0)
|
|
29107
|
-
);
|
|
29108
|
-
if (Object.keys(nonEmpty).length === 0) {
|
|
29109
|
-
return { content: [{ type: "text", text: "No updates provided." }], isError: true };
|
|
29110
|
-
}
|
|
29111
|
-
await assertProjectInOrg(client, systemId, ctx.orgId);
|
|
29112
|
-
if (nonEmpty["accountId"] === "") nonEmpty["accountId"] = null;
|
|
29113
|
-
if (typeof updates.teamspaceId === "string") {
|
|
29114
|
-
const teamspace = await client.getTeamspace(updates.teamspaceId);
|
|
29115
|
-
if (!teamspace) {
|
|
29116
|
-
return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
|
|
29117
|
-
}
|
|
29118
|
-
if (teamspace.orgId !== ctx.orgId) {
|
|
29119
|
-
return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
|
|
29120
|
-
}
|
|
29121
|
-
}
|
|
29122
|
-
const updated = await client.updateSystem(systemId, nonEmpty);
|
|
29123
|
-
if (!updated) {
|
|
29124
|
-
return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
|
|
29125
|
-
}
|
|
29126
|
-
if (PROJECT_EMBEDDING_FIELDS.some((f) => f in nonEmpty)) {
|
|
29127
|
-
void client.triggerProjectEmbedding(systemId);
|
|
29128
|
-
}
|
|
29129
|
-
const lines = [
|
|
29130
|
-
`System updated successfully.`,
|
|
29131
|
-
"",
|
|
29132
|
-
`**ID:** ${updated.projectId}`,
|
|
29133
|
-
`**Title:** ${updated.title}`,
|
|
29134
|
-
accountLine(nonEmpty, updated.accountId),
|
|
29135
|
-
updated.strategy ? `**Strategy:** (updated)` : "",
|
|
29136
|
-
updated.repoOwner ? `**Repo:** ${updated.repoOwner}/${updated.repoName}` : "",
|
|
29137
|
-
updated.repoDefaultBranch ? `**Default Branch:** ${updated.repoDefaultBranch}` : ""
|
|
29138
|
-
].filter(Boolean);
|
|
29139
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
29140
|
-
} catch (err) {
|
|
29141
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29142
|
-
return { content: [{ type: "text", text: `Failed to update system: ${message}` }], isError: true };
|
|
29143
|
-
}
|
|
29144
|
-
};
|
|
29145
|
-
server.tool("update_system", "Update system settings such as title, description, repository configuration, or the Account that owns it", updateSystemSchema, updateSystemHandler);
|
|
29146
|
-
server.tool(
|
|
29147
|
-
"archive_system",
|
|
29148
|
-
"Archive a system, hiding it from the system dropdown and all active system views. Use this when a system is no longer active and should be removed from navigation.",
|
|
29149
|
-
{ systemId: external_exports.string().describe("The system ID to archive") },
|
|
29150
|
-
async ({ systemId }) => {
|
|
29151
|
-
const system = await client.getSystem(systemId);
|
|
29152
|
-
if (!system) {
|
|
29153
|
-
return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
|
|
29154
|
-
}
|
|
29155
|
-
if (system.orgId !== ctx.orgId) {
|
|
29156
|
-
return { content: [{ type: "text", text: `System "${systemId}" is not in this organization` }], isError: true };
|
|
29157
|
-
}
|
|
29158
|
-
try {
|
|
29159
|
-
const updated = await client.archiveSystem(systemId);
|
|
29160
|
-
if (!updated) {
|
|
29161
|
-
return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
|
|
29162
|
-
}
|
|
29163
|
-
return {
|
|
29164
|
-
content: [{
|
|
29165
|
-
type: "text",
|
|
29166
|
-
text: [
|
|
29167
|
-
"System archived successfully.",
|
|
29168
|
-
"",
|
|
29169
|
-
`**ID:** ${updated.projectId}`,
|
|
29170
|
-
`**Title:** ${updated.title}`,
|
|
29171
|
-
`**Status:** ${updated.status}`
|
|
29172
|
-
].join("\n")
|
|
29173
|
-
}]
|
|
29174
|
-
};
|
|
29175
|
-
} catch (err) {
|
|
29176
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29177
|
-
return { content: [{ type: "text", text: `Failed to archive system: ${message}` }], isError: true };
|
|
29178
|
-
}
|
|
29179
|
-
}
|
|
29180
|
-
);
|
|
29181
|
-
const createSystemSchema = {
|
|
29182
|
-
title: external_exports.string().describe("System title"),
|
|
29183
|
-
description: external_exports.string().optional().describe("System description"),
|
|
29184
|
-
strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
|
|
29185
|
-
teamspaceId: external_exports.string().optional().describe("Teamspace ID to associate this system with"),
|
|
29186
|
-
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
29187
|
-
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
29188
|
-
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")')
|
|
29189
|
-
};
|
|
29190
|
-
const createSystemHandler = async ({ title, description, strategy, teamspaceId, repoOwner, repoName, repoDefaultBranch }) => {
|
|
29191
|
-
if (teamspaceId) {
|
|
29192
|
-
const teamspace = await client.getTeamspace(teamspaceId);
|
|
29193
|
-
if (!teamspace) {
|
|
29194
|
-
return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
|
|
29195
|
-
}
|
|
29196
|
-
if (teamspace.orgId !== ctx.orgId) {
|
|
29197
|
-
return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
|
|
29198
|
-
}
|
|
29199
|
-
}
|
|
29200
|
-
try {
|
|
29201
|
-
const project = await client.createSystem({
|
|
29202
|
-
projectId: (0, import_crypto5.randomUUID)(),
|
|
29203
|
-
orgId: ctx.orgId,
|
|
29204
|
-
ownerUserId: ctx.user.userId,
|
|
29205
|
-
title,
|
|
29206
|
-
description,
|
|
29207
|
-
strategy,
|
|
29208
|
-
status: "active",
|
|
29209
|
-
teamspaceId,
|
|
29210
|
-
repoOwner,
|
|
29211
|
-
repoName,
|
|
29212
|
-
repoDefaultBranch
|
|
29213
|
-
});
|
|
29214
|
-
const text = [
|
|
29215
|
-
`System created successfully.`,
|
|
29216
|
-
"",
|
|
29217
|
-
`**ID:** ${project.projectId}`,
|
|
29218
|
-
`**Title:** ${project.title}`,
|
|
29219
|
-
`**Status:** ${project.status}`,
|
|
29220
|
-
project.description ? `**Description:** ${project.description}` : "",
|
|
29221
|
-
project.repoOwner ? `**Repo:** ${project.repoOwner}/${project.repoName}` : "",
|
|
29222
|
-
project.repoDefaultBranch ? `**Default Branch:** ${project.repoDefaultBranch}` : ""
|
|
29223
|
-
].filter(Boolean).join("\n");
|
|
29224
|
-
return { content: [{ type: "text", text }] };
|
|
29225
|
-
} catch (err) {
|
|
29226
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29227
|
-
return { content: [{ type: "text", text: `Failed to create system: ${message}` }], isError: true };
|
|
29228
|
-
}
|
|
29229
|
-
};
|
|
29230
|
-
server.tool("create_system", "Create a new system in the configured organization", createSystemSchema, createSystemHandler);
|
|
29231
|
-
}
|
|
29232
|
-
|
|
29233
28898
|
// ../../libs/harmonica-services/src/mcp/tools/pulse-report-tools.ts
|
|
29234
28899
|
function formatBeatRow(r) {
|
|
29235
28900
|
const downbeatState = r.hasDownbeat ? "" : " \u2014 no Downbeat ever set";
|
|
@@ -29748,6 +29413,103 @@ Error code: PR_NOT_DRAFT (the PR is already ready for review or was never a draf
|
|
|
29748
29413
|
);
|
|
29749
29414
|
}
|
|
29750
29415
|
|
|
29416
|
+
// ../../libs/harmonica-services/src/mcp/tools/revision-quality-tools.ts
|
|
29417
|
+
var POLL_INTERVAL_MS5 = 4e3;
|
|
29418
|
+
var POLL_TIMEOUT_MS5 = 18e4;
|
|
29419
|
+
var POLL_MAX_CONSECUTIVE_ERRORS5 = 3;
|
|
29420
|
+
async function pollForCheck5(client, taskId) {
|
|
29421
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS5;
|
|
29422
|
+
let consecutiveErrors = 0;
|
|
29423
|
+
while (Date.now() < deadline) {
|
|
29424
|
+
const [task, err] = await client.getTask(taskId).then(
|
|
29425
|
+
(t) => [t, null],
|
|
29426
|
+
(e) => [null, e]
|
|
29427
|
+
);
|
|
29428
|
+
if (err) {
|
|
29429
|
+
consecutiveErrors++;
|
|
29430
|
+
if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS5)
|
|
29431
|
+
throw new Error(`getTask failed ${consecutiveErrors} consecutive times: ${err instanceof Error ? err.message : String(err)}`);
|
|
29432
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS5));
|
|
29433
|
+
continue;
|
|
29434
|
+
}
|
|
29435
|
+
if (!task) {
|
|
29436
|
+
consecutiveErrors++;
|
|
29437
|
+
if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS5)
|
|
29438
|
+
throw new Error(`Task ${taskId} not found after ${consecutiveErrors} consecutive attempts`);
|
|
29439
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS5));
|
|
29440
|
+
continue;
|
|
29441
|
+
}
|
|
29442
|
+
consecutiveErrors = 0;
|
|
29443
|
+
if (task.status === "completed") {
|
|
29444
|
+
if (!task.result) throw new Error(`Task ${taskId} completed with no result`);
|
|
29445
|
+
return task.result;
|
|
29446
|
+
}
|
|
29447
|
+
if (task.status === "failed") throw new Error(`Revision quality check failed: ${task.error ?? "unknown error"}`);
|
|
29448
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS5));
|
|
29449
|
+
}
|
|
29450
|
+
throw new Error(`Revision quality check timed out after ${POLL_TIMEOUT_MS5 / 1e3}s`);
|
|
29451
|
+
}
|
|
29452
|
+
function formatScorecard3(check2) {
|
|
29453
|
+
const bar = (score, max) => "\u2588".repeat(score) + "\u2591".repeat(max - score);
|
|
29454
|
+
const dims = check2.dimensions.map(
|
|
29455
|
+
(d) => `**${d.label}** ${bar(d.score, d.maxScore)} ${d.score}/${d.maxScore}
|
|
29456
|
+
${d.rationale}${d.suggestions?.length ? `
|
|
29457
|
+
\u2022 ${d.suggestions.join("\n\u2022 ")}` : ""}`
|
|
29458
|
+
).join("\n\n");
|
|
29459
|
+
return [
|
|
29460
|
+
`## Revision Quality Check \u2014 ${check2.targetId}`,
|
|
29461
|
+
`**Overall Score:** ${check2.overallScore}/${CHECK_MAX_SCORES.revision_quality}`,
|
|
29462
|
+
`*(Advisory \u2014 no lifecycle gate reads this score)*`,
|
|
29463
|
+
`**Summary:** ${check2.summary}`,
|
|
29464
|
+
check2.topSuggestion ? `**Top Suggestion:** ${check2.topSuggestion}` : "",
|
|
29465
|
+
"",
|
|
29466
|
+
dims
|
|
29467
|
+
].filter(Boolean).join("\n");
|
|
29468
|
+
}
|
|
29469
|
+
function registerRevisionQualityTools(server, ctx, client) {
|
|
29470
|
+
server.tool(
|
|
29471
|
+
"check_revision_quality",
|
|
29472
|
+
"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.",
|
|
29473
|
+
{
|
|
29474
|
+
revisionId: external_exports.string().describe("The Revision ID (e.g., rev-abc123). revision_quality targets Revisions only."),
|
|
29475
|
+
projectId: external_exports.string().describe("The project ID"),
|
|
29476
|
+
wait: external_exports.boolean().optional().describe("If true, block until the check completes and return the scorecard inline. Default: false (returns taskId immediately).")
|
|
29477
|
+
},
|
|
29478
|
+
async ({ revisionId, projectId, wait }) => {
|
|
29479
|
+
try {
|
|
29480
|
+
const targetError = revisionQualityTargetError(revisionId);
|
|
29481
|
+
if (targetError) {
|
|
29482
|
+
return {
|
|
29483
|
+
content: [{ type: "text", text: `Revision quality check failed: ${targetError}` }],
|
|
29484
|
+
isError: true
|
|
29485
|
+
};
|
|
29486
|
+
}
|
|
29487
|
+
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
29488
|
+
await assertRevisionInProject(client, revisionId, projectId);
|
|
29489
|
+
const task = await client.runCheck(projectId, "revision_quality", revisionId);
|
|
29490
|
+
if (!wait) {
|
|
29491
|
+
return {
|
|
29492
|
+
content: [{
|
|
29493
|
+
type: "text",
|
|
29494
|
+
text: `Revision quality check enqueued for ${revisionId}.
|
|
29495
|
+
Job ID: ${task.taskId}
|
|
29496
|
+
|
|
29497
|
+
Use get_job_status with jobId="${task.taskId}" to poll for results, or list_checks to retrieve the completed scorecard once done.
|
|
29498
|
+
|
|
29499
|
+
*(Advisory check \u2014 score does not gate any lifecycle transition.)*`
|
|
29500
|
+
}]
|
|
29501
|
+
};
|
|
29502
|
+
}
|
|
29503
|
+
const check2 = await pollForCheck5(client, task.taskId);
|
|
29504
|
+
return { content: [{ type: "text", text: formatScorecard3(check2) }] };
|
|
29505
|
+
} catch (err) {
|
|
29506
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
29507
|
+
return { content: [{ type: "text", text: `Revision quality check failed: ${message}` }], isError: true };
|
|
29508
|
+
}
|
|
29509
|
+
}
|
|
29510
|
+
);
|
|
29511
|
+
}
|
|
29512
|
+
|
|
29751
29513
|
// ../../libs/harmonica-services/src/mcp/tools/session-tools.ts
|
|
29752
29514
|
var SESSION_STATUSES = ["active", "idle", "closed"];
|
|
29753
29515
|
var SESSION_MESSAGE_MAX_LENGTH = 8e3;
|
|
@@ -30018,8 +29780,8 @@ var import_promises = require("node:fs/promises");
|
|
|
30018
29780
|
var import_node_os = require("node:os");
|
|
30019
29781
|
var import_node_path = require("node:path");
|
|
30020
29782
|
|
|
30021
|
-
// ../../libs/harmonica-services/src/
|
|
30022
|
-
var SNAPSHOT_VERSION =
|
|
29783
|
+
// ../../libs/harmonica-services/src/system-snapshot.constants.ts
|
|
29784
|
+
var SNAPSHOT_VERSION = 6;
|
|
30023
29785
|
|
|
30024
29786
|
// ../../libs/harmonica-services/src/mcp/tools/snapshot-tools.ts
|
|
30025
29787
|
function registerSnapshotTools(server, ctx, client) {
|
|
@@ -30029,10 +29791,10 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
30029
29791
|
{ systemId: external_exports.string().describe("The system ID to export") },
|
|
30030
29792
|
async ({ systemId }) => {
|
|
30031
29793
|
await assertProjectInOrg(client, systemId, ctx.orgId);
|
|
30032
|
-
const result = await client.
|
|
29794
|
+
const result = await client.exportSystemSnapshot(systemId);
|
|
30033
29795
|
if (typeof result === "string") {
|
|
30034
29796
|
if (!result.startsWith("https://")) {
|
|
30035
|
-
throw new Error(`
|
|
29797
|
+
throw new Error(`exportSystemSnapshot returned an unexpected string value (expected an https:// presigned URL): ${result.slice(0, 80)}`);
|
|
30036
29798
|
}
|
|
30037
29799
|
return {
|
|
30038
29800
|
content: [{
|
|
@@ -30051,7 +29813,7 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
30051
29813
|
const filename = `harmonica-snapshot-${systemId}-${Date.now()}.json`;
|
|
30052
29814
|
const filePath = (0, import_node_path.join)((0, import_node_os.tmpdir)(), filename);
|
|
30053
29815
|
await (0, import_promises.writeFile)(filePath, JSON.stringify(snapshot), "utf-8");
|
|
30054
|
-
const title = snapshot.
|
|
29816
|
+
const title = snapshot.system?.title ?? systemId;
|
|
30055
29817
|
const versionNote = snapshot.version !== SNAPSHOT_VERSION ? [`Warning: snapshot version ${snapshot.version} (local expects ${SNAPSHOT_VERSION}) \u2014 some counts may be zero.`, ""] : [];
|
|
30056
29818
|
const summary = [
|
|
30057
29819
|
`Exported "${title}" to: ${filePath}`,
|
|
@@ -30066,6 +29828,14 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
30066
29828
|
` PromptLogs: ${snapshot.promptLogs.length}`,
|
|
30067
29829
|
` Drops: ${snapshot.drops?.length ?? 0}`,
|
|
30068
29830
|
` Deliverables: ${snapshot.deliverables?.length ?? 0}`,
|
|
29831
|
+
` Teamspaces: ${snapshot.teamspaces?.length ?? 0}`,
|
|
29832
|
+
` Layers: ${snapshot.layers?.length ?? 0}`,
|
|
29833
|
+
` Tracks: ${snapshot.tracks?.length ?? 0}`,
|
|
29834
|
+
` Work Items: ${snapshot.workItems?.length ?? 0}`,
|
|
29835
|
+
` Movements: ${snapshot.movements?.length ?? 0}`,
|
|
29836
|
+
` Bars: ${snapshot.bars?.length ?? 0}`,
|
|
29837
|
+
` Notebooks: ${snapshot.notebooks?.length ?? 0}`,
|
|
29838
|
+
` Accounts: ${(snapshot.account ? 1 : 0) + (snapshot.accounts?.length ?? 0)}`,
|
|
30069
29839
|
"",
|
|
30070
29840
|
"Use import_system_snapshot with this file path to import into another environment."
|
|
30071
29841
|
];
|
|
@@ -30079,9 +29849,12 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
30079
29849
|
snapshot: external_exports.string().describe("S3 presigned URL (https://...), file path, or inline JSON string"),
|
|
30080
29850
|
targetTeamspaceId: external_exports.string().min(1).optional().describe(
|
|
30081
29851
|
"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."
|
|
29852
|
+
),
|
|
29853
|
+
targetOrgId: external_exports.string().min(1).optional().describe(
|
|
29854
|
+
"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."
|
|
30082
29855
|
)
|
|
30083
29856
|
},
|
|
30084
|
-
async ({ snapshot: snapshotInput, targetTeamspaceId }) => {
|
|
29857
|
+
async ({ snapshot: snapshotInput, targetTeamspaceId, targetOrgId }) => {
|
|
30085
29858
|
const isProduction = process.env.NODE_ENV === "production";
|
|
30086
29859
|
const importAllowed = process.env.ALLOW_SYSTEM_IMPORT === "true";
|
|
30087
29860
|
if (isProduction && !importAllowed) {
|
|
@@ -30094,12 +29867,12 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
30094
29867
|
};
|
|
30095
29868
|
}
|
|
30096
29869
|
const trimmed = snapshotInput.trim();
|
|
30097
|
-
const importOptions = { targetOrgId: ctx.orgId, targetUserId: ctx.user.userId, targetTeamspaceId };
|
|
29870
|
+
const importOptions = { targetOrgId: targetOrgId ?? ctx.orgId, targetUserId: ctx.user.userId, targetTeamspaceId };
|
|
30098
29871
|
if (trimmed.startsWith("https://")) {
|
|
30099
29872
|
if (isPrivateHost(trimmed)) {
|
|
30100
29873
|
throw new Error("Snapshot URL must point to a public host \u2014 private, link-local, and loopback addresses are not permitted.");
|
|
30101
29874
|
}
|
|
30102
|
-
const result2 = await client.
|
|
29875
|
+
const result2 = await client.importSystemSnapshotFromUrl(trimmed, importOptions);
|
|
30103
29876
|
return { content: [{ type: "text", text: formatImportSummary(result2, targetTeamspaceId) }] };
|
|
30104
29877
|
}
|
|
30105
29878
|
if (trimmed.startsWith("http://")) {
|
|
@@ -30114,21 +29887,25 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
30114
29887
|
}
|
|
30115
29888
|
assertSnapshotShape(parsed, "");
|
|
30116
29889
|
const normalized = normalizeSnapshot(parsed);
|
|
30117
|
-
const result = await client.
|
|
29890
|
+
const result = await client.importSystemSnapshot(normalized, importOptions);
|
|
30118
29891
|
return { content: [{ type: "text", text: formatImportSummary(result, targetTeamspaceId) }] };
|
|
30119
29892
|
}
|
|
30120
29893
|
);
|
|
30121
29894
|
}
|
|
30122
29895
|
function normalizeSnapshot(raw) {
|
|
29896
|
+
const anyRaw = raw;
|
|
30123
29897
|
return {
|
|
30124
29898
|
...raw,
|
|
29899
|
+
// Compat: v3 and earlier snapshots have 'project' instead of 'system'
|
|
29900
|
+
system: raw.system ?? anyRaw["project"],
|
|
30125
29901
|
beats: raw.beats ?? [],
|
|
30126
29902
|
beatIndex: raw.beatIndex ?? [],
|
|
30127
29903
|
proposals: raw.proposals ?? [],
|
|
30128
29904
|
proposalIndex: raw.proposalIndex ?? [],
|
|
30129
29905
|
revisions: raw.revisions ?? [],
|
|
30130
29906
|
revisionIndex: raw.revisionIndex ?? [],
|
|
30131
|
-
|
|
29907
|
+
// Compat: v3 and earlier snapshots have 'projectRevisionIndex'
|
|
29908
|
+
systemRevisionIndex: raw.systemRevisionIndex ?? anyRaw["projectRevisionIndex"] ?? [],
|
|
30132
29909
|
activities: raw.activities ?? [],
|
|
30133
29910
|
activityIndex: raw.activityIndex ?? [],
|
|
30134
29911
|
notes: raw.notes ?? [],
|
|
@@ -30141,13 +29918,27 @@ function normalizeSnapshot(raw) {
|
|
|
30141
29918
|
promptLogs: raw.promptLogs ?? [],
|
|
30142
29919
|
beatVersions: raw.beatVersions ?? [],
|
|
30143
29920
|
beatBeatVersionIndex: raw.beatBeatVersionIndex ?? [],
|
|
30144
|
-
|
|
29921
|
+
// Compat: v3 and earlier snapshots have 'projectBeatVersionIndex'
|
|
29922
|
+
systemBeatVersionIndex: raw.systemBeatVersionIndex ?? anyRaw["projectBeatVersionIndex"] ?? [],
|
|
30145
29923
|
beatVersionDropAssignments: raw.beatVersionDropAssignments ?? [],
|
|
30146
29924
|
drops: raw.drops ?? [],
|
|
30147
29925
|
accountDropIndex: raw.accountDropIndex ?? [],
|
|
30148
29926
|
deliverables: raw.deliverables ?? [],
|
|
30149
29927
|
teamspaceDeliverableIndex: raw.teamspaceDeliverableIndex ?? [],
|
|
30150
|
-
|
|
29928
|
+
// Compat: v3 and earlier snapshots have 'projectDeliverableIndex'
|
|
29929
|
+
systemDeliverableIndex: raw.systemDeliverableIndex ?? anyRaw["projectDeliverableIndex"] ?? [],
|
|
29930
|
+
teamspaces: raw.teamspaces ?? [],
|
|
29931
|
+
teamspaceAccountIndex: raw.teamspaceAccountIndex ?? [],
|
|
29932
|
+
accountTeamspaceIndex: raw.accountTeamspaceIndex ?? [],
|
|
29933
|
+
layers: raw.layers ?? [],
|
|
29934
|
+
layerIndex: raw.layerIndex ?? [],
|
|
29935
|
+
tracks: raw.tracks ?? [],
|
|
29936
|
+
// Compat: early v4 snapshots (before field rename) have 'trackProjectIndex'
|
|
29937
|
+
trackSystemIndex: raw.trackSystemIndex ?? anyRaw["trackProjectIndex"] ?? [],
|
|
29938
|
+
trackTeamspaceIndex: raw.trackTeamspaceIndex ?? [],
|
|
29939
|
+
workItems: raw.workItems ?? [],
|
|
29940
|
+
workItemIndex: raw.workItemIndex ?? [],
|
|
29941
|
+
accounts: raw.accounts ?? []
|
|
30151
29942
|
};
|
|
30152
29943
|
}
|
|
30153
29944
|
async function resolveSnapshotInput(input) {
|
|
@@ -30170,7 +29961,15 @@ function formatImportSummary(result, targetTeamspaceId) {
|
|
|
30170
29961
|
` PromptLogs: ${result.counts.promptLogs}`,
|
|
30171
29962
|
` Beat Versions: ${result.counts.beatVersions ?? 0}`,
|
|
30172
29963
|
` Drops: ${result.counts.drops ?? 0}`,
|
|
30173
|
-
` Deliverables: ${result.counts.deliverables ?? 0}
|
|
29964
|
+
` Deliverables: ${result.counts.deliverables ?? 0}`,
|
|
29965
|
+
` Teamspaces: ${result.counts.teamspaces ?? 0}`,
|
|
29966
|
+
` Layers: ${result.counts.layers ?? 0}`,
|
|
29967
|
+
` Tracks: ${result.counts.tracks ?? 0}`,
|
|
29968
|
+
` Work Items: ${result.counts.workItems ?? 0}`,
|
|
29969
|
+
` Movements: ${result.counts.movements ?? 0}`,
|
|
29970
|
+
` Bars: ${result.counts.bars ?? 0}`,
|
|
29971
|
+
` Notebooks: ${result.counts.notebooks ?? 0}`,
|
|
29972
|
+
` Accounts: ${result.counts.accounts ?? 0}`
|
|
30174
29973
|
];
|
|
30175
29974
|
if (result.errors.length > 0) {
|
|
30176
29975
|
lines.push("", `Errors (${result.errors.length}):`);
|
|
@@ -30217,9 +30016,9 @@ function assertSnapshotShape(val, sourceHint) {
|
|
|
30217
30016
|
if (typeof obj["version"] !== "number" || obj["version"] <= 0) {
|
|
30218
30017
|
throw new Error(`Snapshot${sourceHint} has an invalid or missing 'version' field \u2014 it may not be a valid project snapshot.`);
|
|
30219
30018
|
}
|
|
30220
|
-
const
|
|
30221
|
-
if (typeof
|
|
30222
|
-
throw new Error(`Snapshot${sourceHint} is missing '
|
|
30019
|
+
const systemOrProject = obj["system"] ?? obj["project"];
|
|
30020
|
+
if (typeof systemOrProject !== "object" || systemOrProject === null || typeof systemOrProject["projectId"] !== "string") {
|
|
30021
|
+
throw new Error(`Snapshot${sourceHint} is missing 'system.projectId' \u2014 it may not be a valid project snapshot.`);
|
|
30223
30022
|
}
|
|
30224
30023
|
}
|
|
30225
30024
|
|
|
@@ -30249,54 +30048,317 @@ function registerSubscriptionTools(server, ctx, client) {
|
|
|
30249
30048
|
return { content: [{ type: "text", text: `Failed to subscribe: ${message}` }], isError: true };
|
|
30250
30049
|
}
|
|
30251
30050
|
}
|
|
30252
|
-
);
|
|
30051
|
+
);
|
|
30052
|
+
server.tool(
|
|
30053
|
+
"unsubscribe_from_entity",
|
|
30054
|
+
"Stop following a Project, Beat, or Revision. Use durable=true to prevent auto-resubscribe if you are later re-assigned.",
|
|
30055
|
+
{
|
|
30056
|
+
entityType: external_exports.enum(ENTITY_TYPES).describe("Type of entity to unfollow"),
|
|
30057
|
+
entityId: external_exports.string().describe("The entity ID"),
|
|
30058
|
+
durable: external_exports.boolean().optional().default(false).describe("If true, prevents auto-resubscribe on reassignment")
|
|
30059
|
+
},
|
|
30060
|
+
async ({ entityType, entityId, durable }) => {
|
|
30061
|
+
try {
|
|
30062
|
+
await client.unsubscribe(ctx.user.userId, entityType, entityId, durable);
|
|
30063
|
+
const durableMsg = durable ? " (durable \u2014 will not auto-resubscribe on reassignment)" : "";
|
|
30064
|
+
return {
|
|
30065
|
+
content: [{
|
|
30066
|
+
type: "text",
|
|
30067
|
+
text: `Unfollowed ${entityType} ${entityId}${durableMsg}.`
|
|
30068
|
+
}]
|
|
30069
|
+
};
|
|
30070
|
+
} catch (err) {
|
|
30071
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30072
|
+
return { content: [{ type: "text", text: `Failed to unsubscribe: ${message}` }], isError: true };
|
|
30073
|
+
}
|
|
30074
|
+
}
|
|
30075
|
+
);
|
|
30076
|
+
server.tool(
|
|
30077
|
+
"list_subscriptions",
|
|
30078
|
+
"List entities you are currently following. Optionally filter by entity type (project, beat, revision).",
|
|
30079
|
+
{
|
|
30080
|
+
entityType: external_exports.enum(ENTITY_TYPES).optional().describe("Filter by entity type")
|
|
30081
|
+
},
|
|
30082
|
+
async ({ entityType }) => {
|
|
30083
|
+
try {
|
|
30084
|
+
const subs = await client.listSubscriptions(ctx.user.userId, entityType);
|
|
30085
|
+
if (subs.length === 0) {
|
|
30086
|
+
return { content: [{ type: "text", text: "You are not following any entities." }] };
|
|
30087
|
+
}
|
|
30088
|
+
const lines = [`# Your Subscriptions (${subs.length})`, ""];
|
|
30089
|
+
for (const sub of subs) {
|
|
30090
|
+
lines.push(`- **${sub.entityType}** ${sub.entityId} (${sub.source}, since ${sub.createdAt?.split("T")[0] ?? "unknown"})`);
|
|
30091
|
+
}
|
|
30092
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
30093
|
+
} catch (err) {
|
|
30094
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30095
|
+
return { content: [{ type: "text", text: `Failed to list subscriptions: ${message}` }], isError: true };
|
|
30096
|
+
}
|
|
30097
|
+
}
|
|
30098
|
+
);
|
|
30099
|
+
}
|
|
30100
|
+
|
|
30101
|
+
// ../../libs/harmonica-services/src/mcp/tools/system-lifecycle-tools.ts
|
|
30102
|
+
function registerProjectLifecycleTools(server, ctx, client) {
|
|
30103
|
+
server.tool(
|
|
30104
|
+
"transition_system_lifecycle",
|
|
30105
|
+
"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.",
|
|
30106
|
+
{
|
|
30107
|
+
systemId: external_exports.string().describe("The system ID"),
|
|
30108
|
+
targetState: external_exports.enum([
|
|
30109
|
+
"concept",
|
|
30110
|
+
"incubating",
|
|
30111
|
+
"piloting",
|
|
30112
|
+
"activated",
|
|
30113
|
+
"commercializing",
|
|
30114
|
+
"scaled",
|
|
30115
|
+
"paused",
|
|
30116
|
+
"killed",
|
|
30117
|
+
"sunset",
|
|
30118
|
+
"archived"
|
|
30119
|
+
]).describe("Target lifecycle state. Active: concept, incubating, piloting, activated, commercializing, scaled. Exits: paused, killed, sunset, archived."),
|
|
30120
|
+
reason: external_exports.string().optional().describe("Why this transition is being made (recorded in audit metadata)"),
|
|
30121
|
+
decisionNoteId: external_exports.string().optional().describe("Note ID of the governance Decision Note. Required for the activated \u2192 commercializing transition; optional otherwise.")
|
|
30122
|
+
},
|
|
30123
|
+
async ({ systemId, targetState, reason, decisionNoteId }) => {
|
|
30124
|
+
try {
|
|
30125
|
+
const system = await client.getSystem(systemId);
|
|
30126
|
+
if (!system) {
|
|
30127
|
+
return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
|
|
30128
|
+
}
|
|
30129
|
+
if (system.orgId !== ctx.orgId) {
|
|
30130
|
+
return { content: [{ type: "text", text: `System "${systemId}" is not in this organization` }], isError: true };
|
|
30131
|
+
}
|
|
30132
|
+
const result = await client.transitionProjectLifecycleState(systemId, targetState, {
|
|
30133
|
+
actor: { type: "human", id: ctx.user.userId, name: ctx.user.name },
|
|
30134
|
+
reason,
|
|
30135
|
+
decisionNoteId
|
|
30136
|
+
});
|
|
30137
|
+
if (!result.success) {
|
|
30138
|
+
const failedGates = result.error?.failedGates?.length ? ` (failed gates: ${result.error.failedGates.join(", ")})` : "";
|
|
30139
|
+
return {
|
|
30140
|
+
content: [{ type: "text", text: `Transition failed: ${result.error?.message ?? "Unknown error"}${failedGates}` }],
|
|
30141
|
+
isError: true
|
|
30142
|
+
};
|
|
30143
|
+
}
|
|
30144
|
+
const lines = [
|
|
30145
|
+
`System lifecycle transitioned successfully.`,
|
|
30146
|
+
"",
|
|
30147
|
+
`**System:** ${systemId}`,
|
|
30148
|
+
`**Title:** ${system.title}`,
|
|
30149
|
+
`**Transition:** ${result.previousState} \u2192 ${result.newState}`
|
|
30150
|
+
];
|
|
30151
|
+
if (reason) lines.push(`**Reason:** ${reason}`);
|
|
30152
|
+
if (decisionNoteId) lines.push(`**Decision Note:** ${decisionNoteId}`);
|
|
30153
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
30154
|
+
} catch (err) {
|
|
30155
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30156
|
+
return { content: [{ type: "text", text: `Failed to transition system lifecycle: ${message}` }], isError: true };
|
|
30157
|
+
}
|
|
30158
|
+
}
|
|
30159
|
+
);
|
|
30160
|
+
}
|
|
30161
|
+
|
|
30162
|
+
// ../../libs/harmonica-services/src/mcp/tools/system-tools.ts
|
|
30163
|
+
var import_crypto5 = require("crypto");
|
|
30164
|
+
function accountLine(request, resolved) {
|
|
30165
|
+
const requested = request["accountId"];
|
|
30166
|
+
if (requested === null || requested === "") return "**Account:** (unlinked)";
|
|
30167
|
+
return resolved ? `**Account:** ${resolved}` : "";
|
|
30168
|
+
}
|
|
30169
|
+
var PROJECT_EMBEDDING_FIELDS = ["title", "description", "strategy"];
|
|
30170
|
+
function registerProjectTools(server, ctx, client) {
|
|
30171
|
+
const listSystemsHandler = async ({ teamspaceId }) => {
|
|
30172
|
+
try {
|
|
30173
|
+
const projects = await client.listOrgSystems(ctx.orgId);
|
|
30174
|
+
const trimmed = teamspaceId?.trim();
|
|
30175
|
+
const filtered = trimmed ? projects.filter((p) => p.teamspaceId === trimmed) : projects;
|
|
30176
|
+
const text = formatProjectSummaryTable(filtered);
|
|
30177
|
+
return { content: [{ type: "text", text }] };
|
|
30178
|
+
} catch (err) {
|
|
30179
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30180
|
+
return { content: [{ type: "text", text: `Failed to list systems: ${message}` }], isError: true };
|
|
30181
|
+
}
|
|
30182
|
+
};
|
|
30183
|
+
server.tool(
|
|
30184
|
+
"list_systems",
|
|
30185
|
+
"List all systems (projects) in the configured organization",
|
|
30186
|
+
{
|
|
30187
|
+
teamspaceId: external_exports.string().optional().describe("Filter to systems belonging to a specific teamspace. Empty or whitespace-only treated as no filter.")
|
|
30188
|
+
},
|
|
30189
|
+
listSystemsHandler
|
|
30190
|
+
);
|
|
30191
|
+
const getSystemContextSchema = {
|
|
30192
|
+
systemId: external_exports.string().describe("The system ID"),
|
|
30193
|
+
noteLimit: external_exports.number().int().min(1).max(MAX_CONTEXT_NOTE_LIMIT).optional().describe(
|
|
30194
|
+
`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.`
|
|
30195
|
+
)
|
|
30196
|
+
};
|
|
30197
|
+
const getSystemContextHandler = async ({
|
|
30198
|
+
systemId,
|
|
30199
|
+
noteLimit
|
|
30200
|
+
}) => {
|
|
30201
|
+
try {
|
|
30202
|
+
const [project, org] = await Promise.all([
|
|
30203
|
+
fetchProjectInOrg(client, systemId, ctx.orgId),
|
|
30204
|
+
client.getOrg(ctx.orgId)
|
|
30205
|
+
]);
|
|
30206
|
+
const notes = await client.listProjectNotes(systemId, {
|
|
30207
|
+
limit: noteLimit ?? DEFAULT_CONTEXT_NOTE_LIMIT
|
|
30208
|
+
});
|
|
30209
|
+
const text = formatProjectContext(project, notes, org?.coda);
|
|
30210
|
+
return { content: [{ type: "text", text }] };
|
|
30211
|
+
} catch (err) {
|
|
30212
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30213
|
+
return { content: [{ type: "text", text: `Failed to get system context: ${message}` }], isError: true };
|
|
30214
|
+
}
|
|
30215
|
+
};
|
|
30216
|
+
server.tool(
|
|
30217
|
+
"get_system_context",
|
|
30218
|
+
"Get system metadata, description, and notes in one view",
|
|
30219
|
+
getSystemContextSchema,
|
|
30220
|
+
getSystemContextHandler
|
|
30221
|
+
);
|
|
30222
|
+
const updateSystemSchema = {
|
|
30223
|
+
systemId: external_exports.string().describe("The system ID"),
|
|
30224
|
+
title: external_exports.string().optional().describe("New system title"),
|
|
30225
|
+
description: external_exports.string().optional().describe("New system description"),
|
|
30226
|
+
strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
|
|
30227
|
+
teamspaceId: external_exports.string().nullable().optional().describe("Teamspace ID to associate this system with; pass null to remove the association"),
|
|
30228
|
+
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."),
|
|
30229
|
+
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
30230
|
+
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
30231
|
+
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")'),
|
|
30232
|
+
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.')
|
|
30233
|
+
};
|
|
30234
|
+
const updateSystemHandler = async ({ systemId, ...updates }) => {
|
|
30235
|
+
try {
|
|
30236
|
+
const nonEmpty = Object.fromEntries(
|
|
30237
|
+
Object.entries(updates).filter(([, v]) => v !== void 0)
|
|
30238
|
+
);
|
|
30239
|
+
if (Object.keys(nonEmpty).length === 0) {
|
|
30240
|
+
return { content: [{ type: "text", text: "No updates provided." }], isError: true };
|
|
30241
|
+
}
|
|
30242
|
+
await assertProjectInOrg(client, systemId, ctx.orgId);
|
|
30243
|
+
if (nonEmpty["accountId"] === "") nonEmpty["accountId"] = null;
|
|
30244
|
+
if (typeof updates.teamspaceId === "string") {
|
|
30245
|
+
const teamspace = await client.getTeamspace(updates.teamspaceId);
|
|
30246
|
+
if (!teamspace) {
|
|
30247
|
+
return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
|
|
30248
|
+
}
|
|
30249
|
+
if (teamspace.orgId !== ctx.orgId) {
|
|
30250
|
+
return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
|
|
30251
|
+
}
|
|
30252
|
+
}
|
|
30253
|
+
const updated = await client.updateSystem(systemId, nonEmpty);
|
|
30254
|
+
if (!updated) {
|
|
30255
|
+
return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
|
|
30256
|
+
}
|
|
30257
|
+
if (PROJECT_EMBEDDING_FIELDS.some((f) => f in nonEmpty)) {
|
|
30258
|
+
void client.triggerProjectEmbedding(systemId);
|
|
30259
|
+
}
|
|
30260
|
+
const lines = [
|
|
30261
|
+
`System updated successfully.`,
|
|
30262
|
+
"",
|
|
30263
|
+
`**ID:** ${updated.projectId}`,
|
|
30264
|
+
`**Title:** ${updated.title}`,
|
|
30265
|
+
accountLine(nonEmpty, updated.accountId),
|
|
30266
|
+
updated.strategy ? `**Strategy:** (updated)` : "",
|
|
30267
|
+
updated.repoOwner ? `**Repo:** ${updated.repoOwner}/${updated.repoName}` : "",
|
|
30268
|
+
updated.repoDefaultBranch ? `**Default Branch:** ${updated.repoDefaultBranch}` : ""
|
|
30269
|
+
].filter(Boolean);
|
|
30270
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
30271
|
+
} catch (err) {
|
|
30272
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30273
|
+
return { content: [{ type: "text", text: `Failed to update system: ${message}` }], isError: true };
|
|
30274
|
+
}
|
|
30275
|
+
};
|
|
30276
|
+
server.tool("update_system", "Update system settings such as title, description, repository configuration, or the Account that owns it", updateSystemSchema, updateSystemHandler);
|
|
30253
30277
|
server.tool(
|
|
30254
|
-
"
|
|
30255
|
-
"
|
|
30256
|
-
{
|
|
30257
|
-
|
|
30258
|
-
|
|
30259
|
-
|
|
30260
|
-
|
|
30261
|
-
|
|
30278
|
+
"archive_system",
|
|
30279
|
+
"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.",
|
|
30280
|
+
{ systemId: external_exports.string().describe("The system ID to archive") },
|
|
30281
|
+
async ({ systemId }) => {
|
|
30282
|
+
const system = await client.getSystem(systemId);
|
|
30283
|
+
if (!system) {
|
|
30284
|
+
return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
|
|
30285
|
+
}
|
|
30286
|
+
if (system.orgId !== ctx.orgId) {
|
|
30287
|
+
return { content: [{ type: "text", text: `System "${systemId}" is not in this organization` }], isError: true };
|
|
30288
|
+
}
|
|
30262
30289
|
try {
|
|
30263
|
-
await client.
|
|
30264
|
-
|
|
30290
|
+
const updated = await client.archiveSystem(systemId);
|
|
30291
|
+
if (!updated) {
|
|
30292
|
+
return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
|
|
30293
|
+
}
|
|
30265
30294
|
return {
|
|
30266
30295
|
content: [{
|
|
30267
30296
|
type: "text",
|
|
30268
|
-
text:
|
|
30297
|
+
text: [
|
|
30298
|
+
"System archived successfully.",
|
|
30299
|
+
"",
|
|
30300
|
+
`**ID:** ${updated.projectId}`,
|
|
30301
|
+
`**Title:** ${updated.title}`,
|
|
30302
|
+
`**Status:** ${updated.status}`
|
|
30303
|
+
].join("\n")
|
|
30269
30304
|
}]
|
|
30270
30305
|
};
|
|
30271
30306
|
} catch (err) {
|
|
30272
30307
|
const message = err instanceof Error ? err.message : String(err);
|
|
30273
|
-
return { content: [{ type: "text", text: `Failed to
|
|
30308
|
+
return { content: [{ type: "text", text: `Failed to archive system: ${message}` }], isError: true };
|
|
30274
30309
|
}
|
|
30275
30310
|
}
|
|
30276
30311
|
);
|
|
30277
|
-
|
|
30278
|
-
"
|
|
30279
|
-
|
|
30280
|
-
|
|
30281
|
-
|
|
30282
|
-
|
|
30283
|
-
|
|
30284
|
-
|
|
30285
|
-
|
|
30286
|
-
|
|
30287
|
-
|
|
30288
|
-
|
|
30289
|
-
|
|
30290
|
-
|
|
30291
|
-
|
|
30292
|
-
|
|
30293
|
-
return { content: [{ type: "text", text:
|
|
30294
|
-
} catch (err) {
|
|
30295
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
30296
|
-
return { content: [{ type: "text", text: `Failed to list subscriptions: ${message}` }], isError: true };
|
|
30312
|
+
const createSystemSchema = {
|
|
30313
|
+
title: external_exports.string().describe("System title"),
|
|
30314
|
+
description: external_exports.string().optional().describe("System description"),
|
|
30315
|
+
strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
|
|
30316
|
+
teamspaceId: external_exports.string().optional().describe("Teamspace ID to associate this system with"),
|
|
30317
|
+
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
30318
|
+
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
30319
|
+
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")')
|
|
30320
|
+
};
|
|
30321
|
+
const createSystemHandler = async ({ title, description, strategy, teamspaceId, repoOwner, repoName, repoDefaultBranch }) => {
|
|
30322
|
+
if (teamspaceId) {
|
|
30323
|
+
const teamspace = await client.getTeamspace(teamspaceId);
|
|
30324
|
+
if (!teamspace) {
|
|
30325
|
+
return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
|
|
30326
|
+
}
|
|
30327
|
+
if (teamspace.orgId !== ctx.orgId) {
|
|
30328
|
+
return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
|
|
30297
30329
|
}
|
|
30298
30330
|
}
|
|
30299
|
-
|
|
30331
|
+
try {
|
|
30332
|
+
const project = await client.createSystem({
|
|
30333
|
+
projectId: (0, import_crypto5.randomUUID)(),
|
|
30334
|
+
orgId: ctx.orgId,
|
|
30335
|
+
ownerUserId: ctx.user.userId,
|
|
30336
|
+
title,
|
|
30337
|
+
description,
|
|
30338
|
+
strategy,
|
|
30339
|
+
status: "active",
|
|
30340
|
+
teamspaceId,
|
|
30341
|
+
repoOwner,
|
|
30342
|
+
repoName,
|
|
30343
|
+
repoDefaultBranch
|
|
30344
|
+
});
|
|
30345
|
+
const text = [
|
|
30346
|
+
`System created successfully.`,
|
|
30347
|
+
"",
|
|
30348
|
+
`**ID:** ${project.projectId}`,
|
|
30349
|
+
`**Title:** ${project.title}`,
|
|
30350
|
+
`**Status:** ${project.status}`,
|
|
30351
|
+
project.description ? `**Description:** ${project.description}` : "",
|
|
30352
|
+
project.repoOwner ? `**Repo:** ${project.repoOwner}/${project.repoName}` : "",
|
|
30353
|
+
project.repoDefaultBranch ? `**Default Branch:** ${project.repoDefaultBranch}` : ""
|
|
30354
|
+
].filter(Boolean).join("\n");
|
|
30355
|
+
return { content: [{ type: "text", text }] };
|
|
30356
|
+
} catch (err) {
|
|
30357
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30358
|
+
return { content: [{ type: "text", text: `Failed to create system: ${message}` }], isError: true };
|
|
30359
|
+
}
|
|
30360
|
+
};
|
|
30361
|
+
server.tool("create_system", "Create a new system in the configured organization", createSystemSchema, createSystemHandler);
|
|
30300
30362
|
}
|
|
30301
30363
|
|
|
30302
30364
|
// ../../libs/harmonica-services/src/mcp/tools/teamspace-tools.ts
|
|
@@ -30754,8 +30816,15 @@ function registerValueVelocityTools(server, ctx, client) {
|
|
|
30754
30816
|
// ../../libs/harmonica-services/src/mcp/tools/work-item-tools.ts
|
|
30755
30817
|
var WORK_ITEM_STATUS_ENUM = ["not_started", "in_progress", "blocked", "done"];
|
|
30756
30818
|
function formatOwner(owner) {
|
|
30757
|
-
return `${owner.name} <${owner.email}
|
|
30758
|
-
}
|
|
30819
|
+
return owner ? `${owner.name} <${owner.email}>` : "unassigned";
|
|
30820
|
+
}
|
|
30821
|
+
var WORK_ITEM_EFFORT_SIZE_ENUM = [
|
|
30822
|
+
"XS",
|
|
30823
|
+
"S",
|
|
30824
|
+
"M",
|
|
30825
|
+
"L",
|
|
30826
|
+
"XL"
|
|
30827
|
+
];
|
|
30759
30828
|
function registerWorkItemTools(server, _ctx, client) {
|
|
30760
30829
|
server.tool(
|
|
30761
30830
|
"list_track_work_items",
|
|
@@ -30791,6 +30860,9 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
30791
30860
|
`Status: ${wi.status}`,
|
|
30792
30861
|
wi.committedEstimateHours !== void 0 ? `Committed Estimate: ${wi.committedEstimateHours}h` : null,
|
|
30793
30862
|
wi.actualHours !== void 0 ? `Actual Hours: ${wi.actualHours}h` : null,
|
|
30863
|
+
wi.effortSize !== void 0 ? `Effort Size: ${wi.effortSize}` : null,
|
|
30864
|
+
wi.valueUnits !== void 0 ? `Value Units: ${wi.valueUnits}` : null,
|
|
30865
|
+
wi.costPerValueUnit !== void 0 ? `Cost per Value Unit: ${wi.costPerValueUnit}` : null,
|
|
30794
30866
|
wi.beatVersionId ? `Linked Beat Version: ${wi.beatVersionId}` : null,
|
|
30795
30867
|
estimateHint ? `Estimate Hint: ${estimateHint.hours}h (source: ${estimateHint.source})` : null,
|
|
30796
30868
|
wi.createdBy ? `Created By: ${wi.createdBy}` : null,
|
|
@@ -30806,18 +30878,30 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
30806
30878
|
{
|
|
30807
30879
|
trackId: external_exports.string().describe("The Track (DeliverableGroup) ID this work item belongs to"),
|
|
30808
30880
|
title: external_exports.string().min(1).max(300).describe("Work item title"),
|
|
30809
|
-
ownerName: external_exports.string().min(1).describe("
|
|
30810
|
-
ownerEmail: external_exports.string().email().describe("
|
|
30881
|
+
ownerName: external_exports.string().min(1).optional().describe("Optional owner display name (requires ownerEmail too)"),
|
|
30882
|
+
ownerEmail: external_exports.string().email().optional().describe("Optional owner email (requires ownerName too). A work item may be left unowned."),
|
|
30811
30883
|
committedEstimateHours: external_exports.number().nonnegative().optional().describe("Optional committed estimate in hours"),
|
|
30884
|
+
effortSize: external_exports.enum(WORK_ITEM_EFFORT_SIZE_ENUM).optional().describe("Optional rough t-shirt effort size (delivery lens) \u2014 independent of valueUnits"),
|
|
30885
|
+
valueUnits: external_exports.number().nonnegative().optional().describe("Optional Value Units (commercial lens) \u2014 independent of effortSize"),
|
|
30886
|
+
costPerValueUnit: external_exports.number().nonnegative().optional().describe("Optional price per Value Unit"),
|
|
30812
30887
|
createdBy: external_exports.string().optional().describe("Optional creator identity for audit"),
|
|
30813
30888
|
beatVersionId: external_exports.string().min(1).optional().describe("Optional Beat Version to link for capability lineage. Must exist.")
|
|
30814
30889
|
},
|
|
30815
|
-
async ({ trackId, title, ownerName, ownerEmail, committedEstimateHours, createdBy, beatVersionId }) => {
|
|
30890
|
+
async ({ trackId, title, ownerName, ownerEmail, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId }) => {
|
|
30891
|
+
if (ownerName === void 0 !== (ownerEmail === void 0)) {
|
|
30892
|
+
return {
|
|
30893
|
+
content: [{ type: "text", text: "Setting the owner requires both ownerName and ownerEmail. Omit both to create an unowned work item." }],
|
|
30894
|
+
isError: true
|
|
30895
|
+
};
|
|
30896
|
+
}
|
|
30816
30897
|
const wi = await client.createWorkItem({
|
|
30817
30898
|
trackId,
|
|
30818
30899
|
title,
|
|
30819
|
-
owner: { name: ownerName, email: ownerEmail },
|
|
30900
|
+
...ownerName !== void 0 && ownerEmail !== void 0 && { owner: { name: ownerName, email: ownerEmail } },
|
|
30820
30901
|
...committedEstimateHours !== void 0 && { committedEstimateHours },
|
|
30902
|
+
...effortSize !== void 0 && { effortSize },
|
|
30903
|
+
...valueUnits !== void 0 && { valueUnits },
|
|
30904
|
+
...costPerValueUnit !== void 0 && { costPerValueUnit },
|
|
30821
30905
|
...createdBy !== void 0 && { createdBy },
|
|
30822
30906
|
...beatVersionId !== void 0 && { beatVersionId }
|
|
30823
30907
|
});
|
|
@@ -30833,17 +30917,21 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
30833
30917
|
);
|
|
30834
30918
|
server.tool(
|
|
30835
30919
|
"update_work_item",
|
|
30836
|
-
"Update mutable content fields on a work item (title, owner, committedEstimateHours, actualHours, beatVersionId). To move it between Tracks use move_work_item; to change its status use transition_work_item_status.",
|
|
30920
|
+
"Update mutable content fields on a work item (title, owner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId). To move it between Tracks use move_work_item; to change its status use transition_work_item_status.",
|
|
30837
30921
|
{
|
|
30838
30922
|
workItemId: external_exports.string().describe("The work item ID"),
|
|
30839
30923
|
title: external_exports.string().min(1).max(300).optional().describe("New title"),
|
|
30840
30924
|
ownerName: external_exports.string().min(1).optional().describe("New owner display name (requires ownerEmail too)"),
|
|
30841
30925
|
ownerEmail: external_exports.string().email().optional().describe("New owner email (requires ownerName too)"),
|
|
30926
|
+
unassignOwner: external_exports.boolean().optional().describe("Pass true to clear the owner, leaving the work item unowned. Cannot be combined with ownerName/ownerEmail."),
|
|
30842
30927
|
committedEstimateHours: external_exports.number().nonnegative().optional().describe("New committed estimate in hours"),
|
|
30843
30928
|
actualHours: external_exports.number().nonnegative().optional().describe("New actual hours logged"),
|
|
30929
|
+
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."),
|
|
30930
|
+
valueUnits: external_exports.number().nonnegative().nullable().optional().describe("Value Units (commercial lens). Pass null to clear; omit to leave untouched."),
|
|
30931
|
+
costPerValueUnit: external_exports.number().nonnegative().nullable().optional().describe("Price per Value Unit. Pass null to clear; omit to leave untouched."),
|
|
30844
30932
|
beatVersionId: external_exports.string().min(1).nullable().optional().describe("Link to a Beat Version for capability lineage. Pass null to unlink; omit to leave the current link untouched.")
|
|
30845
30933
|
},
|
|
30846
|
-
async ({ workItemId, title, ownerName, ownerEmail, committedEstimateHours, actualHours, beatVersionId }) => {
|
|
30934
|
+
async ({ workItemId, title, ownerName, ownerEmail, unassignOwner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId }) => {
|
|
30847
30935
|
const hasOwnerUpdate = ownerName !== void 0 || ownerEmail !== void 0;
|
|
30848
30936
|
if (hasOwnerUpdate && (ownerName === void 0 || ownerEmail === void 0)) {
|
|
30849
30937
|
return {
|
|
@@ -30851,7 +30939,13 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
30851
30939
|
isError: true
|
|
30852
30940
|
};
|
|
30853
30941
|
}
|
|
30854
|
-
|
|
30942
|
+
if (unassignOwner && hasOwnerUpdate) {
|
|
30943
|
+
return {
|
|
30944
|
+
content: [{ type: "text", text: "Cannot set and clear the owner in one update \u2014 pass either unassignOwner or ownerName/ownerEmail." }],
|
|
30945
|
+
isError: true
|
|
30946
|
+
};
|
|
30947
|
+
}
|
|
30948
|
+
const hasUpdates = title !== void 0 || hasOwnerUpdate || unassignOwner === true || committedEstimateHours !== void 0 || actualHours !== void 0 || effortSize !== void 0 || valueUnits !== void 0 || costPerValueUnit !== void 0 || beatVersionId !== void 0;
|
|
30855
30949
|
if (!hasUpdates) {
|
|
30856
30950
|
return {
|
|
30857
30951
|
content: [{ type: "text", text: `No updates provided for work item: ${workItemId}` }],
|
|
@@ -30861,8 +30955,12 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
30861
30955
|
const wi = await client.updateWorkItem(workItemId, {
|
|
30862
30956
|
...title !== void 0 && { title },
|
|
30863
30957
|
...hasOwnerUpdate && { owner: { name: ownerName, email: ownerEmail } },
|
|
30958
|
+
...unassignOwner === true && { owner: null },
|
|
30864
30959
|
...committedEstimateHours !== void 0 && { committedEstimateHours },
|
|
30865
30960
|
...actualHours !== void 0 && { actualHours },
|
|
30961
|
+
...effortSize !== void 0 && { effortSize },
|
|
30962
|
+
...valueUnits !== void 0 && { valueUnits },
|
|
30963
|
+
...costPerValueUnit !== void 0 && { costPerValueUnit },
|
|
30866
30964
|
...beatVersionId !== void 0 && { beatVersionId }
|
|
30867
30965
|
});
|
|
30868
30966
|
if (!wi) {
|
|
@@ -31365,6 +31463,7 @@ function registerAllTools(server, ctx, client, profile) {
|
|
|
31365
31463
|
registerSnapshotTools(server, ctx, client);
|
|
31366
31464
|
registerBeatQualityTools(server, ctx, client);
|
|
31367
31465
|
registerPlanQualityTools(server, ctx, client);
|
|
31466
|
+
registerRevisionQualityTools(server, ctx, client);
|
|
31368
31467
|
registerPortfolioCoherenceTools(server, ctx, client);
|
|
31369
31468
|
registerDropQualityTools(server, ctx, client);
|
|
31370
31469
|
registerBeatReframeTools(server, ctx, client);
|
|
@@ -31392,7 +31491,6 @@ function registerAllTools(server, ctx, client, profile) {
|
|
|
31392
31491
|
registerNotebookTools(server, ctx, client);
|
|
31393
31492
|
registerAccountTools(server, ctx, client);
|
|
31394
31493
|
registerMovementTools(server, ctx, client);
|
|
31395
|
-
registerMeasureTools(server, ctx, client);
|
|
31396
31494
|
registerWorkItemTools(server, ctx, client);
|
|
31397
31495
|
registerBarTools(server, ctx, client);
|
|
31398
31496
|
registerCadenceScheduleTools(server, ctx, client);
|
|
@@ -31408,7 +31506,7 @@ function registerBeatResources(server, ctx, client) {
|
|
|
31408
31506
|
async (uri, { projectId }) => {
|
|
31409
31507
|
const pid = String(projectId);
|
|
31410
31508
|
await assertProjectInOrg(client, pid, ctx.orgId);
|
|
31411
|
-
const beats = await client.
|
|
31509
|
+
const beats = await client.listSystemBeats(pid);
|
|
31412
31510
|
const text = formatBeatSummaryTable(beats);
|
|
31413
31511
|
return { contents: [{ uri: uri.href, text }] };
|
|
31414
31512
|
}
|
|
@@ -31442,7 +31540,7 @@ function registerGuidelinesResources(server, client) {
|
|
|
31442
31540
|
);
|
|
31443
31541
|
}
|
|
31444
31542
|
|
|
31445
|
-
// ../../libs/harmonica-services/src/mcp/resources/
|
|
31543
|
+
// ../../libs/harmonica-services/src/mcp/resources/system-resources.ts
|
|
31446
31544
|
function registerProjectResources(server, ctx, client) {
|
|
31447
31545
|
server.resource(
|
|
31448
31546
|
"project-context",
|
|
@@ -31450,7 +31548,7 @@ function registerProjectResources(server, ctx, client) {
|
|
|
31450
31548
|
async (uri, { projectId }) => {
|
|
31451
31549
|
const pid = String(projectId);
|
|
31452
31550
|
const project = await fetchProjectInOrg(client, pid, ctx.orgId);
|
|
31453
|
-
const notes = await client.
|
|
31551
|
+
const notes = await client.listSystemNotes(pid, { limit: DEFAULT_CONTEXT_NOTE_LIMIT });
|
|
31454
31552
|
const text = formatProjectContext(project, notes);
|
|
31455
31553
|
return { contents: [{ uri: uri.href, text }] };
|
|
31456
31554
|
}
|
|
@@ -31599,7 +31697,7 @@ function createHttpClient(config2) {
|
|
|
31599
31697
|
}
|
|
31600
31698
|
return result.edge;
|
|
31601
31699
|
}
|
|
31602
|
-
const
|
|
31700
|
+
const POLL_INTERVAL_MS6 = 4e3;
|
|
31603
31701
|
async function pollTaskResult(taskId) {
|
|
31604
31702
|
const deadline = Date.now() + LONG_RUNNING_TIMEOUT_MS;
|
|
31605
31703
|
while (Date.now() < deadline) {
|
|
@@ -31609,7 +31707,7 @@ function createHttpClient(config2) {
|
|
|
31609
31707
|
);
|
|
31610
31708
|
if (task?.status === "completed") return task.result;
|
|
31611
31709
|
if (task?.status === "failed") throw new Error(`Task failed: ${task.error ?? "unknown error"}`);
|
|
31612
|
-
await new Promise((r) => setTimeout(r,
|
|
31710
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS6));
|
|
31613
31711
|
}
|
|
31614
31712
|
throw new Error(`Task ${taskId} timed out after ${LONG_RUNNING_TIMEOUT_MS}ms`);
|
|
31615
31713
|
}
|
|
@@ -31771,7 +31869,7 @@ function createHttpClient(config2) {
|
|
|
31771
31869
|
const result = await request("POST", `/api/systems/${encodeURIComponent(projectId)}/beats/next-id`);
|
|
31772
31870
|
return result.beatId;
|
|
31773
31871
|
},
|
|
31774
|
-
|
|
31872
|
+
listSystemBeats: async (projectId) => {
|
|
31775
31873
|
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/beats`);
|
|
31776
31874
|
return (result?.beats ?? []).map((b) => ({
|
|
31777
31875
|
...b,
|
|
@@ -31927,6 +32025,22 @@ function createHttpClient(config2) {
|
|
|
31927
32025
|
const result = await request("POST", `/api/layers/${encodeURIComponent(layerId)}/status`, { status, reason });
|
|
31928
32026
|
return result?.layer;
|
|
31929
32027
|
},
|
|
32028
|
+
listSystemNotes: async (projectId, filters) => {
|
|
32029
|
+
const params = new URLSearchParams();
|
|
32030
|
+
if (filters?.noteType) {
|
|
32031
|
+
const types = Array.isArray(filters.noteType) ? filters.noteType : [filters.noteType];
|
|
32032
|
+
types.forEach((t) => params.append("type", t));
|
|
32033
|
+
}
|
|
32034
|
+
if (filters?.status) params.set("status", filters.status);
|
|
32035
|
+
if (filters?.revisionId) params.set("revisionId", filters.revisionId);
|
|
32036
|
+
if (filters?.beatVersionId) params.set("beatVersionId", filters.beatVersionId);
|
|
32037
|
+
if (filters?.excludeChildren) params.set("excludeChildren", "true");
|
|
32038
|
+
if (filters?.significance) params.set("significance", filters.significance);
|
|
32039
|
+
if (filters?.limit !== void 0) params.set("limit", String(filters.limit));
|
|
32040
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
32041
|
+
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/notes${qs}`);
|
|
32042
|
+
return result?.notes ?? [];
|
|
32043
|
+
},
|
|
31930
32044
|
listProjectNotes: async (projectId, filters) => {
|
|
31931
32045
|
const params = new URLSearchParams();
|
|
31932
32046
|
if (filters?.noteType) {
|
|
@@ -31935,6 +32049,7 @@ function createHttpClient(config2) {
|
|
|
31935
32049
|
}
|
|
31936
32050
|
if (filters?.status) params.set("status", filters.status);
|
|
31937
32051
|
if (filters?.revisionId) params.set("revisionId", filters.revisionId);
|
|
32052
|
+
if (filters?.beatVersionId) params.set("beatVersionId", filters.beatVersionId);
|
|
31938
32053
|
if (filters?.excludeChildren) params.set("excludeChildren", "true");
|
|
31939
32054
|
if (filters?.significance) params.set("significance", filters.significance);
|
|
31940
32055
|
if (filters?.limit !== void 0) params.set("limit", String(filters.limit));
|
|
@@ -31951,6 +32066,7 @@ function createHttpClient(config2) {
|
|
|
31951
32066
|
if (filters?.excludeNoteType) params.set("excludeType", filters.excludeNoteType);
|
|
31952
32067
|
if (filters?.status) params.set("status", filters.status);
|
|
31953
32068
|
if (filters?.revisionId) params.set("revisionId", filters.revisionId);
|
|
32069
|
+
if (filters?.beatVersionId) params.set("beatVersionId", filters.beatVersionId);
|
|
31954
32070
|
if (filters?.excludeChildren) params.set("excludeChildren", "true");
|
|
31955
32071
|
if (filters?.significance) params.set("significance", filters.significance);
|
|
31956
32072
|
if (limit !== void 0) params.set("limit", String(limit));
|
|
@@ -31969,6 +32085,7 @@ function createHttpClient(config2) {
|
|
|
31969
32085
|
}
|
|
31970
32086
|
if (filters?.status) params.set("status", filters.status);
|
|
31971
32087
|
if (filters?.revisionId) params.set("revisionId", filters.revisionId);
|
|
32088
|
+
if (filters?.beatVersionId) params.set("beatVersionId", filters.beatVersionId);
|
|
31972
32089
|
if (filters?.excludeChildren) params.set("excludeChildren", "true");
|
|
31973
32090
|
if (filters?.significance) params.set("significance", filters.significance);
|
|
31974
32091
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
@@ -32066,6 +32183,14 @@ function createHttpClient(config2) {
|
|
|
32066
32183
|
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/revisions/pending${qs}`);
|
|
32067
32184
|
return result ?? [];
|
|
32068
32185
|
},
|
|
32186
|
+
listSystemRevisions: async (projectId, options) => {
|
|
32187
|
+
const params = new URLSearchParams();
|
|
32188
|
+
if (options?.status) params.set("status", options.status);
|
|
32189
|
+
if (options?.includeArchived) params.set("includeArchived", "true");
|
|
32190
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
32191
|
+
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/revisions${qs}`);
|
|
32192
|
+
return result ?? [];
|
|
32193
|
+
},
|
|
32069
32194
|
listProjectRevisions: async (projectId, options) => {
|
|
32070
32195
|
const params = new URLSearchParams();
|
|
32071
32196
|
if (options?.status) params.set("status", options.status);
|
|
@@ -32295,6 +32420,14 @@ function createHttpClient(config2) {
|
|
|
32295
32420
|
// partitions, but the REST surface is unified per the project-level
|
|
32296
32421
|
// "avoid nested/duplicate routes" guidance.
|
|
32297
32422
|
getSession: (sessionId) => request("GET", `/api/sessions/${encodeURIComponent(sessionId)}`),
|
|
32423
|
+
listSystemSessions: async (projectId, options) => {
|
|
32424
|
+
const params = new URLSearchParams();
|
|
32425
|
+
params.set("projectId", projectId);
|
|
32426
|
+
if (options?.status) params.set("status", options.status);
|
|
32427
|
+
if (options?.includeTerminal) params.set("includeTerminal", "true");
|
|
32428
|
+
const result = await request("GET", `/api/sessions?${params.toString()}`);
|
|
32429
|
+
return result ?? [];
|
|
32430
|
+
},
|
|
32298
32431
|
listProjectSessions: async (projectId, options) => {
|
|
32299
32432
|
const params = new URLSearchParams();
|
|
32300
32433
|
params.set("projectId", projectId);
|
|
@@ -32482,6 +32615,14 @@ function createHttpClient(config2) {
|
|
|
32482
32615
|
getTask: async (taskId) => {
|
|
32483
32616
|
return request("GET", `/api/tasks/${encodeURIComponent(taskId)}`);
|
|
32484
32617
|
},
|
|
32618
|
+
listSystemTasks: async (projectId, options) => {
|
|
32619
|
+
const params = new URLSearchParams();
|
|
32620
|
+
if (options?.status) params.set("status", options.status);
|
|
32621
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
32622
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
32623
|
+
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/tasks${qs}`);
|
|
32624
|
+
return result?.tasks ?? [];
|
|
32625
|
+
},
|
|
32485
32626
|
listProjectTasks: async (projectId, options) => {
|
|
32486
32627
|
const params = new URLSearchParams();
|
|
32487
32628
|
if (options?.status) params.set("status", options.status);
|
|
@@ -32513,6 +32654,15 @@ function createHttpClient(config2) {
|
|
|
32513
32654
|
}, LONG_RUNNING_TIMEOUT_MS);
|
|
32514
32655
|
},
|
|
32515
32656
|
// Activity — forward to API endpoint
|
|
32657
|
+
listSystemActivities: async (projectId, options) => {
|
|
32658
|
+
const params = new URLSearchParams();
|
|
32659
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
32660
|
+
if (options?.minImportance) params.set("importance", options.minImportance);
|
|
32661
|
+
const qs = params.toString();
|
|
32662
|
+
const path = `/api/systems/${encodeURIComponent(projectId)}/activities${qs ? `?${qs}` : ""}`;
|
|
32663
|
+
const result = await request("GET", path);
|
|
32664
|
+
return { entries: result.activities ?? [], hasMore: false };
|
|
32665
|
+
},
|
|
32516
32666
|
listProjectActivities: async (projectId, options) => {
|
|
32517
32667
|
const params = new URLSearchParams();
|
|
32518
32668
|
if (options?.limit) params.set("limit", String(options.limit));
|
|
@@ -32779,7 +32929,7 @@ function createHttpClient(config2) {
|
|
|
32779
32929
|
return request("GET", `/api/subscriptions/entity/${encodeURIComponent(entityId)}`, void 0);
|
|
32780
32930
|
},
|
|
32781
32931
|
// Project Snapshot — export runs as a background task to avoid Lambda timeout
|
|
32782
|
-
|
|
32932
|
+
exportSystemSnapshot: async (projectId) => {
|
|
32783
32933
|
const pid = encodeURIComponent(projectId);
|
|
32784
32934
|
const enqueued = await request("POST", `/api/systems/${pid}/export`, {});
|
|
32785
32935
|
if (!enqueued?.taskId) throw new Error("Export enqueue failed: no taskId returned");
|
|
@@ -32794,8 +32944,8 @@ function createHttpClient(config2) {
|
|
|
32794
32944
|
}
|
|
32795
32945
|
return download;
|
|
32796
32946
|
},
|
|
32797
|
-
|
|
32798
|
-
|
|
32947
|
+
importSystemSnapshot: (snapshot, options) => request("POST", "/api/systems/import", { ...snapshot, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
|
|
32948
|
+
importSystemSnapshotFromUrl: (url2, options) => request("POST", "/api/systems/import", { snapshotUrl: url2, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
|
|
32799
32949
|
// Embedding similarity
|
|
32800
32950
|
embedProjectEntities: (projectId) => request("POST", `/api/systems/${encodeURIComponent(projectId)}/embeddings/generate`),
|
|
32801
32951
|
findSimilarNotes: async (noteId, projectId, options) => {
|
|
@@ -32861,6 +33011,18 @@ function createHttpClient(config2) {
|
|
|
32861
33011
|
const res = await request("GET", `/api/checks/${encodeURIComponent(checkId)}`);
|
|
32862
33012
|
return res.check;
|
|
32863
33013
|
},
|
|
33014
|
+
listSystemChecks: async (projectId, checkType, options) => {
|
|
33015
|
+
const params = new URLSearchParams();
|
|
33016
|
+
if (checkType) params.set("type", checkType);
|
|
33017
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
33018
|
+
if (options?.cursor) params.set("cursor", options.cursor);
|
|
33019
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
33020
|
+
const res = await request(
|
|
33021
|
+
"GET",
|
|
33022
|
+
`/api/systems/${encodeURIComponent(projectId)}/checks${qs}`
|
|
33023
|
+
);
|
|
33024
|
+
return { checks: res?.checks ?? [], nextCursor: res?.nextCursor };
|
|
33025
|
+
},
|
|
32864
33026
|
listProjectChecks: async (projectId, checkType, options) => {
|
|
32865
33027
|
const params = new URLSearchParams();
|
|
32866
33028
|
if (checkType) params.set("type", checkType);
|
|
@@ -32959,6 +33121,11 @@ function createHttpClient(config2) {
|
|
|
32959
33121
|
if (res === void 0) throw new Error(`Teamspace ${teamspaceId} not found`);
|
|
32960
33122
|
return res.deliverableGroups;
|
|
32961
33123
|
},
|
|
33124
|
+
listSystemDeliverableGroups: async (projectId) => {
|
|
33125
|
+
const res = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/deliverable-groups`);
|
|
33126
|
+
if (res === void 0) throw new Error(`Project ${projectId} not found`);
|
|
33127
|
+
return res.deliverableGroups;
|
|
33128
|
+
},
|
|
32962
33129
|
listProjectDeliverableGroups: async (projectId) => {
|
|
32963
33130
|
const res = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/deliverable-groups`);
|
|
32964
33131
|
if (res === void 0) throw new Error(`Project ${projectId} not found`);
|
|
@@ -33008,22 +33175,10 @@ function createHttpClient(config2) {
|
|
|
33008
33175
|
const res = await request("GET", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/drops`);
|
|
33009
33176
|
return res?.drops ?? [];
|
|
33010
33177
|
},
|
|
33011
|
-
listDeliverableGroupBeatVersions: async (deliverableGroupId) => {
|
|
33012
|
-
const res = await request("GET", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/beat-versions`);
|
|
33013
|
-
return res?.beatVersions ?? [];
|
|
33014
|
-
},
|
|
33015
|
-
setBeatVersionDeliverableGroup: async (deliverableGroupId, beatVersionId) => {
|
|
33016
|
-
const res = await request("POST", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/beat-versions`, { beatVersionId });
|
|
33017
|
-
return res?.success === true;
|
|
33018
|
-
},
|
|
33019
33178
|
setDropDeliverableGroup: async (deliverableGroupId, dropId) => {
|
|
33020
33179
|
const res = await request("POST", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/drops`, { dropId });
|
|
33021
33180
|
return res?.success === true;
|
|
33022
33181
|
},
|
|
33023
|
-
clearBeatVersionDeliverableGroup: async (deliverableGroupId, beatVersionId) => {
|
|
33024
|
-
const res = await request("DELETE", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/beat-versions/${encodeURIComponent(beatVersionId)}`);
|
|
33025
|
-
return res?.success === true;
|
|
33026
|
-
},
|
|
33027
33182
|
// Movements (B-242 / teamspace-scoped SOW container)
|
|
33028
33183
|
createMovement: async (input) => {
|
|
33029
33184
|
const { teamspaceId, title, description, accountId } = input;
|
|
@@ -33069,80 +33224,16 @@ function createHttpClient(config2) {
|
|
|
33069
33224
|
if (!parsed.success) throw new Error(`Unexpected movements list shape from API: ${parsed.error.message}`);
|
|
33070
33225
|
return parsed.data.movements;
|
|
33071
33226
|
},
|
|
33072
|
-
// Measures (Measure exposure layer — atomic Work Plan unit under a Track).
|
|
33073
|
-
// valueUnit is server-stripped for non-admins (N-4E09-6509) — the schema
|
|
33074
|
-
// makes it optional; consumers must treat its absence as "not permitted".
|
|
33075
|
-
createMeasure: async (input) => {
|
|
33076
|
-
const { trackId, title, valueUnit, beatVersionId, createdBy } = input;
|
|
33077
|
-
const body = {
|
|
33078
|
-
title,
|
|
33079
|
-
valueUnit,
|
|
33080
|
-
...beatVersionId !== void 0 && { beatVersionId },
|
|
33081
|
-
...createdBy !== void 0 && { createdBy }
|
|
33082
|
-
};
|
|
33083
|
-
const res = await request(
|
|
33084
|
-
"POST",
|
|
33085
|
-
`/api/deliverable-groups/${encodeURIComponent(trackId)}/measures`,
|
|
33086
|
-
body
|
|
33087
|
-
);
|
|
33088
|
-
if (res === void 0) throw new Error(`Track ${trackId} not found`);
|
|
33089
|
-
const parsed = MeasureApiSchema.safeParse(res.measure);
|
|
33090
|
-
if (!parsed.success) throw new Error(`Unexpected Measure shape from API: ${parsed.error.message}`);
|
|
33091
|
-
return parsed.data;
|
|
33092
|
-
},
|
|
33093
|
-
getMeasure: async (measureId) => {
|
|
33094
|
-
const res = await request("GET", `/api/measures/${encodeURIComponent(measureId)}`);
|
|
33095
|
-
if (res === void 0) return void 0;
|
|
33096
|
-
const parsed = MeasureApiSchema.safeParse(res.measure);
|
|
33097
|
-
if (!parsed.success) throw new Error(`Unexpected Measure shape from API: ${parsed.error.message}`);
|
|
33098
|
-
return parsed.data;
|
|
33099
|
-
},
|
|
33100
|
-
updateMeasure: async (measureId, updates) => {
|
|
33101
|
-
const res = await request("PATCH", `/api/measures/${encodeURIComponent(measureId)}`, updates);
|
|
33102
|
-
if (res === void 0) return void 0;
|
|
33103
|
-
const parsed = MeasureApiSchema.safeParse(res.measure);
|
|
33104
|
-
if (!parsed.success) throw new Error(`Unexpected Measure shape from API: ${parsed.error.message}`);
|
|
33105
|
-
return parsed.data;
|
|
33106
|
-
},
|
|
33107
|
-
listTrackMeasures: async (trackId) => {
|
|
33108
|
-
const res = await request("GET", `/api/deliverable-groups/${encodeURIComponent(trackId)}/measures`);
|
|
33109
|
-
if (res === void 0) throw new Error(`Track ${trackId} not found`);
|
|
33110
|
-
const parsed = MeasuresResponseSchema.safeParse(res);
|
|
33111
|
-
if (!parsed.success) throw new Error(`Unexpected measures list shape from API: ${parsed.error.message}`);
|
|
33112
|
-
return parsed.data.measures;
|
|
33113
|
-
},
|
|
33114
|
-
listBeatVersionMeasures: async (beatVersionId) => {
|
|
33115
|
-
const res = await request("GET", `/api/beat-versions/${encodeURIComponent(beatVersionId)}/measures`);
|
|
33116
|
-
if (res === void 0) throw new Error(`Beat Version ${beatVersionId} not found`);
|
|
33117
|
-
const parsed = MeasuresResponseSchema.safeParse(res);
|
|
33118
|
-
if (!parsed.success) throw new Error(`Unexpected measures list shape from API: ${parsed.error.message}`);
|
|
33119
|
-
return parsed.data.measures;
|
|
33120
|
-
},
|
|
33121
|
-
linkMeasureToBeatVersion: async (measureId, beatVersionId) => {
|
|
33122
|
-
const res = await request(
|
|
33123
|
-
"POST",
|
|
33124
|
-
`/api/measures/${encodeURIComponent(measureId)}/beat-version`,
|
|
33125
|
-
{ beatVersionId }
|
|
33126
|
-
);
|
|
33127
|
-
if (res === void 0) return void 0;
|
|
33128
|
-
const parsed = MeasureApiSchema.safeParse(res.measure);
|
|
33129
|
-
if (!parsed.success) throw new Error(`Unexpected Measure shape from API: ${parsed.error.message}`);
|
|
33130
|
-
return parsed.data;
|
|
33131
|
-
},
|
|
33132
|
-
unlinkMeasureFromBeatVersion: async (measureId) => {
|
|
33133
|
-
const res = await request("DELETE", `/api/measures/${encodeURIComponent(measureId)}/beat-version`);
|
|
33134
|
-
if (res === void 0) return void 0;
|
|
33135
|
-
const parsed = MeasureApiSchema.safeParse(res.measure);
|
|
33136
|
-
if (!parsed.success) throw new Error(`Unexpected Measure shape from API: ${parsed.error.message}`);
|
|
33137
|
-
return parsed.data;
|
|
33138
|
-
},
|
|
33139
33227
|
// WorkItems (WorkItem exposure layer — owned, statused unit of operational work under a Track)
|
|
33140
33228
|
createWorkItem: async (input) => {
|
|
33141
|
-
const { trackId, title, owner, committedEstimateHours, createdBy, beatVersionId } = input;
|
|
33229
|
+
const { trackId, title, owner, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId } = input;
|
|
33142
33230
|
const body = {
|
|
33143
33231
|
title,
|
|
33144
|
-
owner,
|
|
33232
|
+
...owner !== void 0 && { owner },
|
|
33145
33233
|
...committedEstimateHours !== void 0 && { committedEstimateHours },
|
|
33234
|
+
...effortSize !== void 0 && { effortSize },
|
|
33235
|
+
...valueUnits !== void 0 && { valueUnits },
|
|
33236
|
+
...costPerValueUnit !== void 0 && { costPerValueUnit },
|
|
33146
33237
|
...createdBy !== void 0 && { createdBy },
|
|
33147
33238
|
...beatVersionId !== void 0 && { beatVersionId }
|
|
33148
33239
|
};
|
|
@@ -33685,7 +33776,7 @@ function loadConfig() {
|
|
|
33685
33776
|
};
|
|
33686
33777
|
}
|
|
33687
33778
|
async function main() {
|
|
33688
|
-
console.error(`[harmonica-mcp] v${"
|
|
33779
|
+
console.error(`[harmonica-mcp] v${"3.0.0"} starting\u2026`);
|
|
33689
33780
|
const config2 = loadConfig();
|
|
33690
33781
|
const client = createHttpClient({
|
|
33691
33782
|
apiBaseUrl: config2.apiBaseUrl,
|