@codazen/harmonica-mcp 0.27.0 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +439 -122
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21518,63 +21518,6 @@ Poll progress with: get_task_status({ taskId: "${task.taskId}" })`
|
|
|
21518
21518
|
|
|
21519
21519
|
// ../../libs/harmonica-services/src/mcp/tools/beat-composer-tools.ts
|
|
21520
21520
|
function registerBeatComposerTools(server, ctx, client) {
|
|
21521
|
-
server.tool(
|
|
21522
|
-
"compose_revisions",
|
|
21523
|
-
"Compose delivery Revisions from a Beat's Coda (description) \u2014 creates concept-status Revisions for progressive delivery. IMPORTANT: If the Beat already has active revisions, this returns the existing ones instead of creating duplicates. Use list_revisions first to check current state. Use transition_revision to move revisions forward.",
|
|
21524
|
-
{
|
|
21525
|
-
beatId: external_exports.string().describe("The beat ID"),
|
|
21526
|
-
drafts: external_exports.array(external_exports.object({
|
|
21527
|
-
title: external_exports.string().describe("Title for this revision"),
|
|
21528
|
-
description: external_exports.string().describe("What this revision delivers"),
|
|
21529
|
-
changeSummary: external_exports.string().describe("Brief summary of the change"),
|
|
21530
|
-
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
21531
|
-
priority: external_exports.coerce.number().optional().describe("Priority (lower = higher priority)"),
|
|
21532
|
-
estimatedEffort: external_exports.string().optional().describe('Estimated effort (e.g., "S", "M", "L")')
|
|
21533
|
-
})).min(1).describe("Revision drafts to scaffold"),
|
|
21534
|
-
force: external_exports.boolean().optional().describe("Bypass idempotency guard \u2014 create new revisions even if active ones exist. Use when intentionally adding revisions alongside existing ones.")
|
|
21535
|
-
},
|
|
21536
|
-
async ({ beatId, drafts, force }) => {
|
|
21537
|
-
try {
|
|
21538
|
-
console.error("[MCP:ScaffoldRevisions] beatId=%s draftCount=%d actor=%s force=%s", beatId, drafts.length, ctx.user.userId, force ?? false);
|
|
21539
|
-
await assertBeatInOrg(client, beatId, ctx.orgId);
|
|
21540
|
-
const result = await client.composeRevisions(beatId, drafts, ctx.user.userId, { force });
|
|
21541
|
-
const { revisions, alreadyExisted } = result;
|
|
21542
|
-
if (alreadyExisted) {
|
|
21543
|
-
console.error("[MCP:ScaffoldRevisions] returning %d existing revisions beatId=%s", revisions.length, beatId);
|
|
21544
|
-
const lines2 = [
|
|
21545
|
-
`# Beat ${beatId} already has ${revisions.length} active Revision(s) \u2014 no new ones created`,
|
|
21546
|
-
"",
|
|
21547
|
-
"Use update_revision or transition_revision to modify existing revisions, or archive/reject them first if you need to start fresh.",
|
|
21548
|
-
""
|
|
21549
|
-
];
|
|
21550
|
-
for (const r of revisions) {
|
|
21551
|
-
lines2.push(`## ${r.revisionId} \u2014 ${r.title} [${r.status}]`);
|
|
21552
|
-
lines2.push(`- **Description:** ${r.description}`);
|
|
21553
|
-
lines2.push("");
|
|
21554
|
-
}
|
|
21555
|
-
return { content: [{ type: "text", text: lines2.join("\n") }] };
|
|
21556
|
-
}
|
|
21557
|
-
console.error(
|
|
21558
|
-
"[MCP:ScaffoldRevisions] created %d revisions beatId=%s ids=%s",
|
|
21559
|
-
revisions.length,
|
|
21560
|
-
beatId,
|
|
21561
|
-
revisions.map((r) => r.revisionId).join(",")
|
|
21562
|
-
);
|
|
21563
|
-
const lines = [`# Scaffolded ${revisions.length} Revisions for ${beatId}`, ""];
|
|
21564
|
-
for (const r of revisions) {
|
|
21565
|
-
lines.push(`## ${r.revisionId} \u2014 ${r.title} [${r.status}]`);
|
|
21566
|
-
lines.push(`- **Description:** ${r.description}`);
|
|
21567
|
-
lines.push(`- **Change:** ${r.changeSummary}`);
|
|
21568
|
-
lines.push("");
|
|
21569
|
-
}
|
|
21570
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
21571
|
-
} catch (err) {
|
|
21572
|
-
console.error("[MCP:ScaffoldRevisions] beatId=%s error=%s", beatId, err instanceof Error ? err.message : String(err));
|
|
21573
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
21574
|
-
return { content: [{ type: "text", text: `Failed to scaffold revisions: ${message}` }], isError: true };
|
|
21575
|
-
}
|
|
21576
|
-
}
|
|
21577
|
-
);
|
|
21578
21521
|
server.tool(
|
|
21579
21522
|
"record_beat_merge",
|
|
21580
21523
|
"Record that Beats were merged \u2014 the surviving Beat absorbs others. Creates an Activity record for audit trail.",
|
|
@@ -22446,9 +22389,24 @@ function registerBeatTools(server, ctx, client) {
|
|
|
22446
22389
|
reason: `Beat "${title}" created via MCP`,
|
|
22447
22390
|
actorId: ctx.user.email
|
|
22448
22391
|
});
|
|
22392
|
+
let similarityWarning = "";
|
|
22393
|
+
try {
|
|
22394
|
+
const similar = await client.findSimilarBeats(beat.beatId, projectId, { threshold: 0.7, limit: 5 });
|
|
22395
|
+
if (similar.length > 0) {
|
|
22396
|
+
const rows = similar.map((r) => `- **${r.entityId}** (score: ${r.score.toFixed(2)}): ${r.snippet.replace(/[\n\r]/g, " ")}`).join("\n");
|
|
22397
|
+
similarityWarning = `
|
|
22398
|
+
|
|
22399
|
+
---
|
|
22400
|
+
|
|
22401
|
+
\u26A0 **Similar Beats detected** \u2014 consider adding a Beat Version to an existing Beat instead of creating a new one:
|
|
22402
|
+
|
|
22403
|
+
${rows}`;
|
|
22404
|
+
}
|
|
22405
|
+
} catch {
|
|
22406
|
+
}
|
|
22449
22407
|
return { content: [{ type: "text", text: `Beat created.
|
|
22450
22408
|
|
|
22451
|
-
${formatBeatDetail(beat)}` }] };
|
|
22409
|
+
${formatBeatDetail(beat)}${similarityWarning}` }] };
|
|
22452
22410
|
} catch (err) {
|
|
22453
22411
|
const message = err instanceof Error ? err.message : String(err);
|
|
22454
22412
|
return { content: [{ type: "text", text: `Failed to create beat: ${message}` }], isError: true };
|
|
@@ -22604,6 +22562,26 @@ var LIFECYCLE_STATES = [
|
|
|
22604
22562
|
];
|
|
22605
22563
|
var PR_LIFECYCLE_STATES = ["draft", "open"];
|
|
22606
22564
|
|
|
22565
|
+
// ../../libs/harmonica-services/src/mcp/errors.ts
|
|
22566
|
+
var BeatQualityGateError = class extends Error {
|
|
22567
|
+
code = "beat_quality_gate_failed";
|
|
22568
|
+
failure;
|
|
22569
|
+
constructor(failure) {
|
|
22570
|
+
super(failure.message);
|
|
22571
|
+
this.failure = failure;
|
|
22572
|
+
}
|
|
22573
|
+
};
|
|
22574
|
+
var PlanVersionsIdempotencyError = class extends Error {
|
|
22575
|
+
code = "plan_versions_already_exist";
|
|
22576
|
+
existingBeatVersionIds;
|
|
22577
|
+
constructor(existingBeatVersionIds) {
|
|
22578
|
+
super(
|
|
22579
|
+
`Beat already has ${existingBeatVersionIds.length} planned Beat Version(s). Pass force=true to plan again.`
|
|
22580
|
+
);
|
|
22581
|
+
this.existingBeatVersionIds = existingBeatVersionIds;
|
|
22582
|
+
}
|
|
22583
|
+
};
|
|
22584
|
+
|
|
22607
22585
|
// ../../libs/harmonica-services/src/mcp/tools/beat-version-tools.ts
|
|
22608
22586
|
function registerBeatVersionTools(server, ctx, client) {
|
|
22609
22587
|
server.tool(
|
|
@@ -22774,6 +22752,61 @@ function registerBeatVersionTools(server, ctx, client) {
|
|
|
22774
22752
|
}
|
|
22775
22753
|
}
|
|
22776
22754
|
);
|
|
22755
|
+
server.tool(
|
|
22756
|
+
"plan_beat_versions",
|
|
22757
|
+
"Agent-decompose a composed Beat into 2-5 planning-state Beat Versions on the path to the Coda. Uses the Beat's Coda, project strategy, sibling Beats, Notes, and codebase grep as context. Requires a recent passing beat_quality check (run check_beat_quality first if needed). Idempotent: refuses when planned BVs already exist on the Beat unless force=true is set.",
|
|
22758
|
+
{
|
|
22759
|
+
beatId: external_exports.string().describe("The Beat ID to decompose (must have a Coda and a passing beat_quality check)"),
|
|
22760
|
+
force: external_exports.boolean().optional().describe("Bypass the idempotency guard and plan additional Beat Versions even when prior planned BVs are still active")
|
|
22761
|
+
},
|
|
22762
|
+
async ({ beatId, force }) => {
|
|
22763
|
+
try {
|
|
22764
|
+
await assertBeatInOrg(client, beatId, ctx.orgId);
|
|
22765
|
+
const actorId = ctx.user.email ?? ctx.user.userId;
|
|
22766
|
+
const beatVersions = await client.planBeatVersions(beatId, actorId, { force });
|
|
22767
|
+
const lines = [
|
|
22768
|
+
`Planned ${beatVersions.length} Beat Version${beatVersions.length === 1 ? "" : "s"} for ${beatId}:`,
|
|
22769
|
+
""
|
|
22770
|
+
];
|
|
22771
|
+
for (const bv of beatVersions) {
|
|
22772
|
+
lines.push(`- **${bv.beatVersionId}** v${bv.versionNumber} [${bv.status}]: ${bv.title}`);
|
|
22773
|
+
if (bv.changeSummary) lines.push(` Change: ${bv.changeSummary}`);
|
|
22774
|
+
}
|
|
22775
|
+
lines.push("", "Run check_plan_quality on each Beat Version before transitioning to building.");
|
|
22776
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
22777
|
+
} catch (err) {
|
|
22778
|
+
if (err instanceof BeatQualityGateError) {
|
|
22779
|
+
const remedy = err.failure.remedy ? `
|
|
22780
|
+
|
|
22781
|
+
**Remedy:** ${err.failure.remedy}` : "";
|
|
22782
|
+
return {
|
|
22783
|
+
content: [{
|
|
22784
|
+
type: "text",
|
|
22785
|
+
text: `Beat Quality gate failed (${err.failure.reason}): ${err.failure.message}${remedy}`
|
|
22786
|
+
}],
|
|
22787
|
+
isError: true
|
|
22788
|
+
};
|
|
22789
|
+
}
|
|
22790
|
+
if (err instanceof PlanVersionsIdempotencyError) {
|
|
22791
|
+
return {
|
|
22792
|
+
content: [{
|
|
22793
|
+
type: "text",
|
|
22794
|
+
text: [
|
|
22795
|
+
`${err.message}`,
|
|
22796
|
+
"",
|
|
22797
|
+
`Existing planned Beat Versions: ${err.existingBeatVersionIds.join(", ")}`,
|
|
22798
|
+
"",
|
|
22799
|
+
"Pass `force: true` if you intentionally want to plan additional Beat Versions on top of the existing ones."
|
|
22800
|
+
].join("\n")
|
|
22801
|
+
}],
|
|
22802
|
+
isError: true
|
|
22803
|
+
};
|
|
22804
|
+
}
|
|
22805
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
22806
|
+
return { content: [{ type: "text", text: `Failed to plan Beat Versions: ${message}` }], isError: true };
|
|
22807
|
+
}
|
|
22808
|
+
}
|
|
22809
|
+
);
|
|
22777
22810
|
server.tool(
|
|
22778
22811
|
"transition_beat_version",
|
|
22779
22812
|
"Transition a Beat Version to a new lifecycle state. The planning \u2192 building transition requires a passing plan_quality check (score \u2265 3, not stale). Use skip_quality_check only for reconciling already-shipped work.",
|
|
@@ -22825,7 +22858,8 @@ var CHECK_MAX_SCORES = {
|
|
|
22825
22858
|
beat_quality: 5,
|
|
22826
22859
|
plan_quality: 5,
|
|
22827
22860
|
build_quality: 5,
|
|
22828
|
-
pii_scan: 100
|
|
22861
|
+
pii_scan: 100,
|
|
22862
|
+
portfolio_coherence: 5
|
|
22829
22863
|
};
|
|
22830
22864
|
|
|
22831
22865
|
// ../../libs/harmonica-services/src/mcp/tools/check-tools.ts
|
|
@@ -22856,7 +22890,7 @@ function registerCheckTools(server, ctx, client) {
|
|
|
22856
22890
|
projectId: external_exports.string().describe("The project ID"),
|
|
22857
22891
|
beatId: external_exports.string().optional().describe("List checks for this specific beat"),
|
|
22858
22892
|
revisionId: external_exports.string().optional().describe("List checks for this specific revision"),
|
|
22859
|
-
checkType: external_exports.enum(["beat_quality", "plan_quality", "build_quality", "pii_scan"]).optional().describe("Filter by check type")
|
|
22893
|
+
checkType: external_exports.enum(["beat_quality", "plan_quality", "build_quality", "pii_scan", "portfolio_coherence"]).optional().describe("Filter by check type")
|
|
22860
22894
|
},
|
|
22861
22895
|
async ({ projectId, beatId, revisionId, checkType }) => {
|
|
22862
22896
|
try {
|
|
@@ -22869,7 +22903,8 @@ function registerCheckTools(server, ctx, client) {
|
|
|
22869
22903
|
checks = await client.listBeatChecks(beatId, projectId, checkType);
|
|
22870
22904
|
scope = `beat ${beatId}`;
|
|
22871
22905
|
} else {
|
|
22872
|
-
|
|
22906
|
+
const result = await client.listProjectChecks(projectId, checkType, { limit: 200 });
|
|
22907
|
+
checks = result.checks;
|
|
22873
22908
|
scope = `project ${projectId}`;
|
|
22874
22909
|
}
|
|
22875
22910
|
if (checks.length === 0) {
|
|
@@ -22918,7 +22953,8 @@ var CHECK_TYPE_DISPLAY = {
|
|
|
22918
22953
|
beat_quality: "Quality",
|
|
22919
22954
|
plan_quality: "Plan Quality",
|
|
22920
22955
|
build_quality: "Build Quality",
|
|
22921
|
-
pii_scan: "PII Scan"
|
|
22956
|
+
pii_scan: "PII Scan",
|
|
22957
|
+
portfolio_coherence: "Portfolio Coherence"
|
|
22922
22958
|
};
|
|
22923
22959
|
function formatCheck(a) {
|
|
22924
22960
|
const typeLabel = CHECK_TYPE_DISPLAY[a.checkType] ?? a.checkType;
|
|
@@ -23235,7 +23271,7 @@ function registerDropTools(server, ctx, client) {
|
|
|
23235
23271
|
);
|
|
23236
23272
|
server.tool(
|
|
23237
23273
|
"add_revision_to_drop",
|
|
23238
|
-
"Assign a Beat Revision to a Drop. Single-membership rule: a Revision can only belong to one *active* Drop (draft/scheduled/in_progress) at a time. Re-assignment from a released or rolled_back Drop is allowed. The target Drop must be
|
|
23274
|
+
"Assign a Beat Revision to a Drop. Single-membership rule: a Revision can only belong to one *active* Drop (draft/scheduled/in_progress) at a time. Re-assignment from a released or rolled_back Drop is allowed. The target Drop must not be `released` or `rolled_back`.",
|
|
23239
23275
|
{
|
|
23240
23276
|
dropId: external_exports.string().describe("The Drop ID"),
|
|
23241
23277
|
revisionId: external_exports.string().describe("The Revision ID (e.g. rev-abc123)")
|
|
@@ -23284,7 +23320,7 @@ function registerDropTools(server, ctx, client) {
|
|
|
23284
23320
|
);
|
|
23285
23321
|
server.tool(
|
|
23286
23322
|
"remove_revision_from_drop",
|
|
23287
|
-
"Remove a Beat Revision from a Drop.
|
|
23323
|
+
"Remove a Beat Revision from a Drop. Not allowed once the Drop is `released` or `rolled_back`.",
|
|
23288
23324
|
{
|
|
23289
23325
|
dropId: external_exports.string().describe("The Drop ID"),
|
|
23290
23326
|
revisionId: external_exports.string().describe("The Revision ID")
|
|
@@ -23307,7 +23343,7 @@ function registerDropTools(server, ctx, client) {
|
|
|
23307
23343
|
);
|
|
23308
23344
|
server.tool(
|
|
23309
23345
|
"add_beat_version_to_drop",
|
|
23310
|
-
"Assign a Beat Version to a Drop. Single-membership rule: a Beat Version can only belong to one *active* Drop (draft/scheduled/in_progress) at a time. Re-assignment from a released or rolled_back Drop is allowed. The target Drop must be
|
|
23346
|
+
"Assign a Beat Version to a Drop. Single-membership rule: a Beat Version can only belong to one *active* Drop (draft/scheduled/in_progress) at a time. Re-assignment from a released or rolled_back Drop is allowed. The target Drop must not be `released` or `rolled_back`.",
|
|
23311
23347
|
{
|
|
23312
23348
|
dropId: external_exports.string().describe("The Drop ID"),
|
|
23313
23349
|
beatVersionId: external_exports.string().describe("The Beat Version ID (e.g. bv-abc123)")
|
|
@@ -23331,7 +23367,7 @@ function registerDropTools(server, ctx, client) {
|
|
|
23331
23367
|
);
|
|
23332
23368
|
server.tool(
|
|
23333
23369
|
"remove_beat_version_from_drop",
|
|
23334
|
-
"Remove a Beat Version from a Drop.
|
|
23370
|
+
"Remove a Beat Version from a Drop. Not allowed once the Drop is `released` or `rolled_back`.",
|
|
23335
23371
|
{
|
|
23336
23372
|
dropId: external_exports.string().describe("The Drop ID"),
|
|
23337
23373
|
beatVersionId: external_exports.string().describe("The Beat Version ID")
|
|
@@ -24582,6 +24618,253 @@ Use get_task_status with taskId="${task.taskId}" to poll for results, or list_ch
|
|
|
24582
24618
|
);
|
|
24583
24619
|
}
|
|
24584
24620
|
|
|
24621
|
+
// ../../libs/harmonica-services/src/mcp/tools/portfolio-coherence-tools.ts
|
|
24622
|
+
var POLL_INTERVAL_MS3 = 4e3;
|
|
24623
|
+
var POLL_TIMEOUT_MS3 = 18e4;
|
|
24624
|
+
var POLL_MAX_CONSECUTIVE_ERRORS3 = 3;
|
|
24625
|
+
async function pollForCheck3(client, taskId) {
|
|
24626
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS3;
|
|
24627
|
+
let consecutiveErrors = 0;
|
|
24628
|
+
while (Date.now() < deadline) {
|
|
24629
|
+
const [task, err] = await client.getTask(taskId).then(
|
|
24630
|
+
(t) => [t, null],
|
|
24631
|
+
(e) => [null, e]
|
|
24632
|
+
);
|
|
24633
|
+
if (err) {
|
|
24634
|
+
consecutiveErrors++;
|
|
24635
|
+
if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS3) {
|
|
24636
|
+
throw new Error(`getTask failed ${consecutiveErrors} times: ${err instanceof Error ? err.message : String(err)}`);
|
|
24637
|
+
}
|
|
24638
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS3));
|
|
24639
|
+
continue;
|
|
24640
|
+
}
|
|
24641
|
+
consecutiveErrors = 0;
|
|
24642
|
+
if (!task) throw new Error(`Task ${taskId} not found`);
|
|
24643
|
+
if (task.status === "completed") {
|
|
24644
|
+
if (!task.result) throw new Error(`Task ${taskId} completed with no result`);
|
|
24645
|
+
return task.result;
|
|
24646
|
+
}
|
|
24647
|
+
if (task.status === "failed") throw new Error(`Portfolio coherence check failed: ${task.error ?? "unknown error"}`);
|
|
24648
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS3));
|
|
24649
|
+
}
|
|
24650
|
+
throw new Error(`Portfolio coherence check timed out after ${POLL_TIMEOUT_MS3 / 1e3}s`);
|
|
24651
|
+
}
|
|
24652
|
+
function formatReport(check2) {
|
|
24653
|
+
const bar = (score, max) => "\u2588".repeat(score) + "\u2591".repeat(max - score);
|
|
24654
|
+
const dims = check2.dimensions.map(
|
|
24655
|
+
(d) => `**${d.label}** ${bar(d.score, d.maxScore)} ${d.score}/${d.maxScore}
|
|
24656
|
+
${d.rationale}${d.suggestions?.length ? `
|
|
24657
|
+
\u2022 ${d.suggestions.join("\n\u2022 ")}` : ""}`
|
|
24658
|
+
).join("\n\n");
|
|
24659
|
+
const snapshot = check2.targetSnapshot;
|
|
24660
|
+
const totalBeats = snapshot?.totalBeats;
|
|
24661
|
+
const mergeCandidates = snapshot?.mergeCandidateGroups;
|
|
24662
|
+
const parts = [
|
|
24663
|
+
`## Portfolio Coherence \u2014 ${check2.targetId}`,
|
|
24664
|
+
`**Overall Score:** ${check2.overallScore}/5${totalBeats !== void 0 ? ` | **Beats analyzed:** ${totalBeats}` : ""}`,
|
|
24665
|
+
`**Summary:** ${check2.summary}`,
|
|
24666
|
+
check2.topSuggestion ? `**Top Suggestion:** ${check2.topSuggestion}` : "",
|
|
24667
|
+
"",
|
|
24668
|
+
dims
|
|
24669
|
+
].filter(Boolean);
|
|
24670
|
+
if (mergeCandidates?.length) {
|
|
24671
|
+
const groupLines = mergeCandidates.map((g, i) => `${i + 1}. [${(g.beatIds ?? []).join(", ")}] \u2014 ${Math.round((g.avgSimilarity ?? 0) * 100)}% similarity`).join("\n");
|
|
24672
|
+
parts.push(`
|
|
24673
|
+
## Merge Candidate Groups (${mergeCandidates.length})
|
|
24674
|
+
${groupLines}`);
|
|
24675
|
+
}
|
|
24676
|
+
return parts.join("\n");
|
|
24677
|
+
}
|
|
24678
|
+
function formatConsolidationPreview(primaryId, primary, duplicates, rationale) {
|
|
24679
|
+
const archiveLines = duplicates.map((d) => `- \`${d.beat.beatId}\` \u2014 "${d.beat.title}" [${d.beat.beatStatus ?? "composing"}]`).join("\n");
|
|
24680
|
+
const inflightBvs = duplicates.flatMap((d) => d.inflightBvs);
|
|
24681
|
+
const bvWarning = inflightBvs.length > 0 ? [
|
|
24682
|
+
"",
|
|
24683
|
+
`**\u26A0 In-flight Beat Versions (reassign manually after merge):**`,
|
|
24684
|
+
...inflightBvs.map((bv) => `- \`${bv.beatVersionId}\` \u2014 "${bv.title}" [${bv.status}]`)
|
|
24685
|
+
].join("\n") : "";
|
|
24686
|
+
return [
|
|
24687
|
+
`## Consolidation Preview \u2014 Dry Run`,
|
|
24688
|
+
"",
|
|
24689
|
+
`**Primary (survives):** \`${primaryId}\` \u2014 "${primary.title}" [${primary.beatStatus ?? "composing"}]`,
|
|
24690
|
+
"",
|
|
24691
|
+
`**To archive (${duplicates.length}):**`,
|
|
24692
|
+
archiveLines,
|
|
24693
|
+
"",
|
|
24694
|
+
`**Rationale:** ${rationale}`,
|
|
24695
|
+
...bvWarning ? [bvWarning] : [],
|
|
24696
|
+
"",
|
|
24697
|
+
"---",
|
|
24698
|
+
"Re-call `consolidate_beats` with `confirm: true` to execute this merge."
|
|
24699
|
+
].join("\n");
|
|
24700
|
+
}
|
|
24701
|
+
function formatConsolidationResult(primaryId, primary, archivedBeatIds, notesReassigned, inflightBvs) {
|
|
24702
|
+
const bvSection = inflightBvs.length > 0 ? [
|
|
24703
|
+
"",
|
|
24704
|
+
`**In-flight Beat Versions \u2014 reassign manually:**`,
|
|
24705
|
+
...inflightBvs.map(
|
|
24706
|
+
(bv) => `- \`${bv.beatVersionId}\` \u2014 "${bv.title}" [${bv.status}] \u2192 use \`update_beat_version\` to move to \`${primaryId}\``
|
|
24707
|
+
)
|
|
24708
|
+
].join("\n") : "";
|
|
24709
|
+
return [
|
|
24710
|
+
`## Merge Complete`,
|
|
24711
|
+
"",
|
|
24712
|
+
`**Primary:** \`${primaryId}\` \u2014 "${primary.title}"`,
|
|
24713
|
+
`**Archived:** ${archivedBeatIds.length} Beat${archivedBeatIds.length !== 1 ? "s" : ""}${archivedBeatIds.length > 0 ? ` \u2014 [${archivedBeatIds.join(", ")}]` : ""}`,
|
|
24714
|
+
`**Notes reassigned:** ${notesReassigned}`,
|
|
24715
|
+
...bvSection ? [bvSection] : [],
|
|
24716
|
+
"",
|
|
24717
|
+
archivedBeatIds.length > 0 ? "Merge event recorded. Run `check_portfolio_coherence` to verify the cluster is resolved." : "\u26A0 No beats were archived \u2014 check logs for archive failures. Re-run after resolving."
|
|
24718
|
+
].join("\n");
|
|
24719
|
+
}
|
|
24720
|
+
function registerPortfolioCoherenceTools(server, ctx, client) {
|
|
24721
|
+
server.tool(
|
|
24722
|
+
"check_portfolio_coherence",
|
|
24723
|
+
"Run a portfolio coherence check on a project. Clusters Beats by semantic similarity to surface fragmentation (merge candidates) and checks what fraction of Beats have a VP-readable buyer abstraction score. By default returns a taskId immediately \u2014 set wait=true to block and receive the full report inline.",
|
|
24724
|
+
{
|
|
24725
|
+
projectId: external_exports.string().min(1).describe("The project ID"),
|
|
24726
|
+
wait: external_exports.boolean().optional().describe("Block until the check completes and return the report inline. Default: false.")
|
|
24727
|
+
},
|
|
24728
|
+
async ({ projectId, wait }) => {
|
|
24729
|
+
try {
|
|
24730
|
+
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
24731
|
+
const { taskId } = await client.runCheck(projectId, "portfolio_coherence", projectId);
|
|
24732
|
+
if (!wait) {
|
|
24733
|
+
return {
|
|
24734
|
+
content: [{
|
|
24735
|
+
type: "text",
|
|
24736
|
+
text: [
|
|
24737
|
+
`Portfolio coherence check enqueued for ${projectId}.`,
|
|
24738
|
+
`Task ID: ${taskId}`,
|
|
24739
|
+
"",
|
|
24740
|
+
`Use get_task_status with taskId="${taskId}" to poll, or list_checks with projectId="${projectId}" and checkType="portfolio_coherence" to view results.`
|
|
24741
|
+
].join("\n")
|
|
24742
|
+
}]
|
|
24743
|
+
};
|
|
24744
|
+
}
|
|
24745
|
+
const check2 = await pollForCheck3(client, taskId);
|
|
24746
|
+
return { content: [{ type: "text", text: formatReport(check2) }] };
|
|
24747
|
+
} catch (err) {
|
|
24748
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24749
|
+
return { content: [{ type: "text", text: `Portfolio coherence check failed: ${message}` }], isError: true };
|
|
24750
|
+
}
|
|
24751
|
+
}
|
|
24752
|
+
);
|
|
24753
|
+
server.tool(
|
|
24754
|
+
"consolidate_beats",
|
|
24755
|
+
"Consolidate a fragmented Beat cluster \u2014 identified by check_portfolio_coherence \u2014 into a single VP-level capability. By default (confirm omitted or false) returns a dry-run preview: surviving Beat, Beats to archive, Notes to reassign, and any in-flight Beat Versions. Set confirm=true to execute the merge.",
|
|
24756
|
+
{
|
|
24757
|
+
projectId: external_exports.string().min(1).describe("The project ID \u2014 all beatIds must belong to this project"),
|
|
24758
|
+
beatIds: external_exports.array(external_exports.string().min(1)).min(2).max(50).describe("All Beat IDs in the cluster (including the one that will survive)"),
|
|
24759
|
+
primaryBeatId: external_exports.string().min(1).optional().describe("Beat ID that survives \u2014 defaults to beatIds[0] if omitted"),
|
|
24760
|
+
rationale: external_exports.string().max(2e3).optional().describe("Why these Beats are being merged \u2014 auto-generated if omitted"),
|
|
24761
|
+
confirm: external_exports.boolean().optional().describe("false/omitted = preview; true = execute the merge")
|
|
24762
|
+
},
|
|
24763
|
+
async ({ projectId, beatIds, primaryBeatId, rationale, confirm }) => {
|
|
24764
|
+
let archivedBeatIds = [];
|
|
24765
|
+
let totalNotesReassigned = 0;
|
|
24766
|
+
try {
|
|
24767
|
+
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
24768
|
+
const uniqueBeatIds = [...new Set(beatIds)];
|
|
24769
|
+
const resolvedPrimaryId = primaryBeatId ?? uniqueBeatIds[0];
|
|
24770
|
+
if (!uniqueBeatIds.includes(resolvedPrimaryId)) {
|
|
24771
|
+
return {
|
|
24772
|
+
content: [{ type: "text", text: `primaryBeatId "${resolvedPrimaryId}" is not in beatIds \u2014 include it in the cluster or omit to use beatIds[0].` }],
|
|
24773
|
+
isError: true
|
|
24774
|
+
};
|
|
24775
|
+
}
|
|
24776
|
+
const duplicateBeatIds = uniqueBeatIds.filter((id) => id !== resolvedPrimaryId);
|
|
24777
|
+
if (duplicateBeatIds.length === 0) {
|
|
24778
|
+
return {
|
|
24779
|
+
content: [{ type: "text", text: `No duplicates \u2014 all beatIds resolve to "${resolvedPrimaryId}" after deduplication. Include at least one distinct duplicate Beat ID.` }],
|
|
24780
|
+
isError: true
|
|
24781
|
+
};
|
|
24782
|
+
}
|
|
24783
|
+
const allProjectBeats = await client.listProjectBeats(projectId);
|
|
24784
|
+
const beatMap = new Map(allProjectBeats.map((b) => [b.beatId, b]));
|
|
24785
|
+
const primaryBeat = beatMap.get(resolvedPrimaryId);
|
|
24786
|
+
if (!primaryBeat) {
|
|
24787
|
+
return { content: [{ type: "text", text: `Primary Beat "${resolvedPrimaryId}" not found in project "${projectId}".` }], isError: true };
|
|
24788
|
+
}
|
|
24789
|
+
const missingIds = duplicateBeatIds.filter((id) => !beatMap.has(id));
|
|
24790
|
+
if (missingIds.length > 0) {
|
|
24791
|
+
return { content: [{ type: "text", text: `Beat(s) not found in project "${projectId}": ${missingIds.join(", ")}` }], isError: true };
|
|
24792
|
+
}
|
|
24793
|
+
const dupBvResults = await Promise.allSettled(
|
|
24794
|
+
duplicateBeatIds.map(
|
|
24795
|
+
(id) => client.listBeatVersions(projectId, { beatId: id, includeTerminal: false })
|
|
24796
|
+
)
|
|
24797
|
+
);
|
|
24798
|
+
const bvRejected = dupBvResults.filter((r) => r.status === "rejected");
|
|
24799
|
+
if (bvRejected.length > 0) {
|
|
24800
|
+
console.error("[consolidate_beats] listBeatVersions partial failure:", bvRejected.map((r) => r.reason instanceof Error ? r.reason.message : r.reason));
|
|
24801
|
+
}
|
|
24802
|
+
const duplicates = duplicateBeatIds.map((id, i) => ({
|
|
24803
|
+
beat: beatMap.get(id),
|
|
24804
|
+
inflightBvs: dupBvResults[i]?.status === "fulfilled" ? dupBvResults[i].value : []
|
|
24805
|
+
}));
|
|
24806
|
+
const resolvedRationale = rationale ?? `Merge ${duplicateBeatIds.length} overlapping Beat${duplicateBeatIds.length !== 1 ? "s" : ""} into "${primaryBeat.title}" \u2014 consolidating duplicate value proposition.`;
|
|
24807
|
+
if (!confirm) {
|
|
24808
|
+
return { content: [{ type: "text", text: formatConsolidationPreview(resolvedPrimaryId, primaryBeat, duplicates, resolvedRationale) }] };
|
|
24809
|
+
}
|
|
24810
|
+
for (const { beat } of duplicates) {
|
|
24811
|
+
const archiveResult = await client.updateBeatStatus(beat.beatId, "archived", {
|
|
24812
|
+
actor: { type: "human", id: ctx.user.userId, name: ctx.user.name, email: ctx.user.email },
|
|
24813
|
+
reason: `Merged into ${resolvedPrimaryId}: ${resolvedRationale}`
|
|
24814
|
+
});
|
|
24815
|
+
if (!archiveResult.success) {
|
|
24816
|
+
console.error("[consolidate_beats] archive failed for %s: %s", beat.beatId, archiveResult.error?.message ?? "unknown");
|
|
24817
|
+
continue;
|
|
24818
|
+
}
|
|
24819
|
+
archivedBeatIds.push(beat.beatId);
|
|
24820
|
+
try {
|
|
24821
|
+
const notes = await client.listBeatNotes(beat.beatId);
|
|
24822
|
+
for (const note of notes) {
|
|
24823
|
+
try {
|
|
24824
|
+
const reassigned = await client.reassignNote(note.noteId, resolvedPrimaryId, resolvedRationale, ctx.user.userId);
|
|
24825
|
+
if (reassigned !== void 0) {
|
|
24826
|
+
totalNotesReassigned++;
|
|
24827
|
+
} else {
|
|
24828
|
+
console.error("[consolidate_beats] reassignNote returned undefined for %s", note.noteId);
|
|
24829
|
+
}
|
|
24830
|
+
} catch (err) {
|
|
24831
|
+
console.error("[consolidate_beats] note reassign failed %s: %s", note.noteId, err instanceof Error ? err.message : err);
|
|
24832
|
+
}
|
|
24833
|
+
}
|
|
24834
|
+
} catch (err) {
|
|
24835
|
+
console.error("[consolidate_beats] listBeatNotes failed for %s: %s", beat.beatId, err instanceof Error ? err.message : err);
|
|
24836
|
+
}
|
|
24837
|
+
}
|
|
24838
|
+
if (archivedBeatIds.length > 0) {
|
|
24839
|
+
await client.recordMergeEvent(resolvedPrimaryId, archivedBeatIds, resolvedRationale, ctx.user.userId);
|
|
24840
|
+
}
|
|
24841
|
+
const inflightBvs = duplicates.flatMap((d) => d.inflightBvs);
|
|
24842
|
+
return { content: [{ type: "text", text: formatConsolidationResult(resolvedPrimaryId, primaryBeat, archivedBeatIds, totalNotesReassigned, inflightBvs) }] };
|
|
24843
|
+
} catch (err) {
|
|
24844
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24845
|
+
if (archivedBeatIds.length > 0) {
|
|
24846
|
+
return {
|
|
24847
|
+
content: [{
|
|
24848
|
+
type: "text",
|
|
24849
|
+
text: [
|
|
24850
|
+
`## Merge Partially Complete`,
|
|
24851
|
+
"",
|
|
24852
|
+
`**Archived:** ${archivedBeatIds.length} Beat${archivedBeatIds.length !== 1 ? "s" : ""} \u2014 [${archivedBeatIds.join(", ")}]`,
|
|
24853
|
+
`**Notes reassigned:** ${totalNotesReassigned}`,
|
|
24854
|
+
"",
|
|
24855
|
+
`\u26A0 Merge interrupted before completion: ${message}`,
|
|
24856
|
+
"Run `check_portfolio_coherence` to verify current state."
|
|
24857
|
+
].join("\n")
|
|
24858
|
+
}],
|
|
24859
|
+
isError: true
|
|
24860
|
+
};
|
|
24861
|
+
}
|
|
24862
|
+
return { content: [{ type: "text", text: `consolidate_beats failed: ${message}` }], isError: true };
|
|
24863
|
+
}
|
|
24864
|
+
}
|
|
24865
|
+
);
|
|
24866
|
+
}
|
|
24867
|
+
|
|
24585
24868
|
// ../../libs/harmonica-services/src/mcp/tools/project-lifecycle-tools.ts
|
|
24586
24869
|
function registerProjectLifecycleTools(server, ctx, client) {
|
|
24587
24870
|
server.tool(
|
|
@@ -25655,7 +25938,7 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
25655
25938
|
);
|
|
25656
25939
|
server.tool(
|
|
25657
25940
|
"draft_coda",
|
|
25658
|
-
"Generate and apply a Coda (description) for a Beat that has a title but no description. Uses project context, sibling Beats, related Notes, and codebase grep to write a resolved expression of the capability. The Coda is applied directly to the Beat, making it eligible for
|
|
25941
|
+
"Generate and apply a Coda (description) for a Beat that has a title but no description. Uses project context, sibling Beats, related Notes, and codebase grep to write a resolved expression of the capability. The Coda is applied directly to the Beat, making it eligible for plan_beat_versions once a beat_quality check passes. An activity is logged so the Beat owner can review and edit. Returns a task ID \u2014 use get_task_status to track progress.",
|
|
25659
25942
|
{
|
|
25660
25943
|
projectId: external_exports.string().describe("The project ID"),
|
|
25661
25944
|
beatId: external_exports.string().describe("The beat ID (must have a title but no description)")
|
|
@@ -25756,107 +26039,109 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
25756
26039
|
}
|
|
25757
26040
|
);
|
|
25758
26041
|
server.tool(
|
|
25759
|
-
"
|
|
25760
|
-
"
|
|
26042
|
+
"implement_revision",
|
|
26043
|
+
"Trigger an agent session to implement a Revision. The agent reads the Revision spec and beat-level Notes, writes code, commits, pushes a branch named agent/rev-{id}-{title}, and creates a draft PR with the Revision ID pre-populated in the PR body. Returns a task ID \u2014 use get_task_status to track progress. Requires agentCapabilities.implementation enabled on the project.",
|
|
25761
26044
|
{
|
|
25762
|
-
|
|
25763
|
-
|
|
26045
|
+
revisionId: external_exports.string().min(1).describe("The Revision ID to implement (e.g., rev-abc123)"),
|
|
26046
|
+
projectId: external_exports.string().min(1).describe("The project ID")
|
|
25764
26047
|
},
|
|
25765
|
-
async ({
|
|
26048
|
+
async ({ revisionId, projectId }) => {
|
|
25766
26049
|
try {
|
|
25767
26050
|
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
25768
|
-
const task = await client.
|
|
26051
|
+
const task = await client.implementRevision(revisionId, projectId, {
|
|
26052
|
+
name: ctx.user.name,
|
|
26053
|
+
email: ctx.user.email
|
|
26054
|
+
});
|
|
25769
26055
|
return {
|
|
25770
26056
|
content: [{
|
|
25771
26057
|
type: "text",
|
|
25772
26058
|
text: [
|
|
25773
|
-
"
|
|
26059
|
+
"Revision implementation session queued.",
|
|
25774
26060
|
"",
|
|
25775
|
-
`**
|
|
25776
|
-
`**Scope:** ${noteIds ? `${noteIds.length} specific items` : "All active work items"}`,
|
|
26061
|
+
`**Revision:** ${revisionId}`,
|
|
25777
26062
|
`**Task ID:** ${task.taskId}`,
|
|
25778
26063
|
`**Status:** ${task.status}`,
|
|
25779
26064
|
"",
|
|
25780
|
-
"The
|
|
25781
|
-
"Use `get_task_status` with the task ID to check progress."
|
|
26065
|
+
"The agent will read the Revision spec and beat-level Notes, implement the code, and create a draft PR.",
|
|
26066
|
+
"Use `get_task_status` with the task ID to check progress and see the result (branch, PR URL, tool count, duration)."
|
|
25782
26067
|
].join("\n")
|
|
25783
26068
|
}]
|
|
25784
26069
|
};
|
|
25785
26070
|
} catch (err) {
|
|
25786
26071
|
const message = err instanceof Error ? err.message : String(err);
|
|
25787
|
-
return { content: [{ type: "text", text: `Failed to start
|
|
26072
|
+
return { content: [{ type: "text", text: `Failed to start revision implementation: ${message}` }], isError: true };
|
|
25788
26073
|
}
|
|
25789
26074
|
}
|
|
25790
26075
|
);
|
|
25791
26076
|
server.tool(
|
|
25792
|
-
"
|
|
25793
|
-
"
|
|
26077
|
+
"validate_work_items",
|
|
26078
|
+
"Validate active workItem Notes against the codebase. Checks if work items are already implemented (resolved), no longer relevant (dismissed), or still needed (active). Active items get a solution plan for the implementation agent. Returns a task ID \u2014 use get_task_status to track progress.",
|
|
25794
26079
|
{
|
|
25795
26080
|
projectId: external_exports.string().describe("The project ID"),
|
|
25796
|
-
|
|
26081
|
+
noteIds: external_exports.array(external_exports.string()).optional().describe("Specific note IDs to validate. If omitted, validates all active work items.")
|
|
25797
26082
|
},
|
|
25798
|
-
async ({ projectId,
|
|
26083
|
+
async ({ projectId, noteIds }) => {
|
|
25799
26084
|
try {
|
|
25800
26085
|
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
25801
|
-
const task = await client.
|
|
26086
|
+
const task = await client.validateWorkItems(projectId, noteIds);
|
|
25802
26087
|
return {
|
|
25803
26088
|
content: [{
|
|
25804
26089
|
type: "text",
|
|
25805
26090
|
text: [
|
|
25806
|
-
"
|
|
26091
|
+
"Work item validation queued.",
|
|
25807
26092
|
"",
|
|
25808
26093
|
`**Project:** ${projectId}`,
|
|
25809
|
-
`**Scope:** ${
|
|
26094
|
+
`**Scope:** ${noteIds ? `${noteIds.length} specific items` : "All active work items"}`,
|
|
25810
26095
|
`**Task ID:** ${task.taskId}`,
|
|
25811
26096
|
`**Status:** ${task.status}`,
|
|
25812
26097
|
"",
|
|
25813
|
-
"The
|
|
26098
|
+
"The validator will grep the codebase for evidence, then classify each item.",
|
|
25814
26099
|
"Use `get_task_status` with the task ID to check progress."
|
|
25815
26100
|
].join("\n")
|
|
25816
26101
|
}]
|
|
25817
26102
|
};
|
|
25818
26103
|
} catch (err) {
|
|
25819
26104
|
const message = err instanceof Error ? err.message : String(err);
|
|
25820
|
-
return { content: [{ type: "text", text: `Failed to start
|
|
26105
|
+
return { content: [{ type: "text", text: `Failed to start validation: ${message}` }], isError: true };
|
|
25821
26106
|
}
|
|
25822
26107
|
}
|
|
25823
26108
|
);
|
|
25824
26109
|
server.tool(
|
|
25825
|
-
"
|
|
25826
|
-
"Autonomously
|
|
26110
|
+
"plan_revision_batch",
|
|
26111
|
+
"Autonomously generate work items for planning Revisions. If revisionIds are omitted, discovers all planning Revisions with no scoped work items and plans them all. Uses codebase-aware agent sessions when available, LLM-only fallback otherwise. Returns a task ID \u2014 use get_task_status to track progress.",
|
|
25827
26112
|
{
|
|
25828
26113
|
projectId: external_exports.string().describe("The project ID"),
|
|
25829
|
-
|
|
26114
|
+
revisionIds: external_exports.array(external_exports.string()).optional().describe("Specific revision IDs to plan. If omitted, auto-discovers all unplanned planning revisions.")
|
|
25830
26115
|
},
|
|
25831
|
-
async ({ projectId,
|
|
26116
|
+
async ({ projectId, revisionIds }) => {
|
|
25832
26117
|
try {
|
|
25833
26118
|
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
25834
|
-
const task = await client.
|
|
26119
|
+
const task = await client.planRevisionBatch(projectId, revisionIds);
|
|
25835
26120
|
return {
|
|
25836
26121
|
content: [{
|
|
25837
26122
|
type: "text",
|
|
25838
26123
|
text: [
|
|
25839
|
-
"
|
|
26124
|
+
"Revision batch planning queued.",
|
|
25840
26125
|
"",
|
|
25841
|
-
`**Beat:** ${beatId}`,
|
|
25842
26126
|
`**Project:** ${projectId}`,
|
|
26127
|
+
`**Scope:** ${revisionIds ? `${revisionIds.length} specific revision(s)` : "All unplanned planning revisions"}`,
|
|
25843
26128
|
`**Task ID:** ${task.taskId}`,
|
|
25844
26129
|
`**Status:** ${task.status}`,
|
|
25845
26130
|
"",
|
|
25846
|
-
"The agent will
|
|
25847
|
-
"Use `get_task_status` with the task ID to check progress
|
|
26131
|
+
"The agent will generate work items for each revision. Revisions stay in planning \u2014 review work items before advancing to building.",
|
|
26132
|
+
"Use `get_task_status` with the task ID to check progress."
|
|
25848
26133
|
].join("\n")
|
|
25849
26134
|
}]
|
|
25850
26135
|
};
|
|
25851
26136
|
} catch (err) {
|
|
25852
26137
|
const message = err instanceof Error ? err.message : String(err);
|
|
25853
|
-
return { content: [{ type: "text", text: `Failed to start
|
|
26138
|
+
return { content: [{ type: "text", text: `Failed to start revision batch planning: ${message}` }], isError: true };
|
|
25854
26139
|
}
|
|
25855
26140
|
}
|
|
25856
26141
|
);
|
|
25857
26142
|
server.tool(
|
|
25858
26143
|
"run_triage",
|
|
25859
|
-
"Run the autonomous triage loop for a project. Analyzes next actions, validates work items against the codebase, auto-executes safe actions (draft_coda,
|
|
26144
|
+
"Run the autonomous triage loop for a project. Analyzes next actions, validates work items against the codebase, auto-executes safe actions (draft_coda, plan_revision_batch, validate_work_items), and escalates items that need human judgment. Requires agentCapabilities.triage enabled on the project. Returns a task ID \u2014 use get_task_status to track progress.",
|
|
25860
26145
|
{
|
|
25861
26146
|
projectId: external_exports.string().describe("The project ID")
|
|
25862
26147
|
},
|
|
@@ -25945,6 +26230,7 @@ function registerAllTools(server, ctx, client) {
|
|
|
25945
26230
|
registerSnapshotTools(server, ctx, client);
|
|
25946
26231
|
registerBeatQualityTools(server, ctx, client);
|
|
25947
26232
|
registerPlanQualityTools(server, ctx, client);
|
|
26233
|
+
registerPortfolioCoherenceTools(server, ctx, client);
|
|
25948
26234
|
registerBeatReframeTools(server, ctx, client);
|
|
25949
26235
|
registerBeatPlanningTools(server, ctx, client);
|
|
25950
26236
|
registerRevisionLifecycleTools(server, ctx, client);
|
|
@@ -26043,6 +26329,13 @@ var ApiError = class extends Error {
|
|
|
26043
26329
|
this.name = "ApiError";
|
|
26044
26330
|
}
|
|
26045
26331
|
};
|
|
26332
|
+
function safeParseErrorBody(body) {
|
|
26333
|
+
try {
|
|
26334
|
+
return JSON.parse(body);
|
|
26335
|
+
} catch {
|
|
26336
|
+
return void 0;
|
|
26337
|
+
}
|
|
26338
|
+
}
|
|
26046
26339
|
function createHttpClient(config2) {
|
|
26047
26340
|
const url = new URL(config2.apiBaseUrl);
|
|
26048
26341
|
const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
@@ -26084,7 +26377,7 @@ function createHttpClient(config2) {
|
|
|
26084
26377
|
}
|
|
26085
26378
|
return void 0;
|
|
26086
26379
|
}
|
|
26087
|
-
const
|
|
26380
|
+
const POLL_INTERVAL_MS4 = 4e3;
|
|
26088
26381
|
async function pollTaskResult(taskId) {
|
|
26089
26382
|
const deadline = Date.now() + LONG_RUNNING_TIMEOUT_MS;
|
|
26090
26383
|
while (Date.now() < deadline) {
|
|
@@ -26094,7 +26387,7 @@ function createHttpClient(config2) {
|
|
|
26094
26387
|
);
|
|
26095
26388
|
if (task?.status === "completed") return task.result;
|
|
26096
26389
|
if (task?.status === "failed") throw new Error(`Task failed: ${task.error ?? "unknown error"}`);
|
|
26097
|
-
await new Promise((r) => setTimeout(r,
|
|
26390
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS4));
|
|
26098
26391
|
}
|
|
26099
26392
|
throw new Error(`Task ${taskId} timed out after ${LONG_RUNNING_TIMEOUT_MS}ms`);
|
|
26100
26393
|
}
|
|
@@ -26596,14 +26889,31 @@ function createHttpClient(config2) {
|
|
|
26596
26889
|
const result = await request("GET", `/api/projects/${encodeURIComponent(projectId)}/beats/${encodeURIComponent(beatId)}/history`);
|
|
26597
26890
|
return result?.timeline ?? [];
|
|
26598
26891
|
},
|
|
26599
|
-
// Beat
|
|
26600
|
-
|
|
26601
|
-
const
|
|
26602
|
-
|
|
26603
|
-
|
|
26604
|
-
|
|
26605
|
-
|
|
26892
|
+
// Plan Beat Versions (agent decomposition of a Beat into waypoint BVs)
|
|
26893
|
+
planBeatVersions: async (beatId, _actorId, options) => {
|
|
26894
|
+
const force = options?.force ? "?force=true" : "";
|
|
26895
|
+
try {
|
|
26896
|
+
const result = await request(
|
|
26897
|
+
"POST",
|
|
26898
|
+
`/api/beats/${encodeURIComponent(beatId)}/plan-versions${force}`,
|
|
26899
|
+
{}
|
|
26900
|
+
);
|
|
26901
|
+
if (!result) throw new Error(`Beat not found: ${beatId}`);
|
|
26902
|
+
return result.beatVersions;
|
|
26903
|
+
} catch (err) {
|
|
26904
|
+
if (err instanceof ApiError) {
|
|
26905
|
+
const parsed = safeParseErrorBody(err.body);
|
|
26906
|
+
if (err.status === 422 && parsed?.code === "beat_quality_gate_failed" && parsed.gate) {
|
|
26907
|
+
throw new BeatQualityGateError(parsed.gate);
|
|
26908
|
+
}
|
|
26909
|
+
if (err.status === 409 && parsed?.code === "plan_versions_already_exist" && Array.isArray(parsed.existingBeatVersionIds)) {
|
|
26910
|
+
throw new PlanVersionsIdempotencyError(parsed.existingBeatVersionIds);
|
|
26911
|
+
}
|
|
26912
|
+
}
|
|
26913
|
+
throw err;
|
|
26914
|
+
}
|
|
26606
26915
|
},
|
|
26916
|
+
// Beat Composer
|
|
26607
26917
|
recordMergeEvent: async () => {
|
|
26608
26918
|
throw new Error("Beat merge not supported via HTTP client yet");
|
|
26609
26919
|
},
|
|
@@ -26664,6 +26974,15 @@ function createHttpClient(config2) {
|
|
|
26664
26974
|
if (!result?.taskId) throw new Error("Implementation enqueue failed: no taskId returned");
|
|
26665
26975
|
return { taskId: result.taskId, taskType: "work_item_implementation", status: result.status ?? "pending", projectId, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
26666
26976
|
},
|
|
26977
|
+
implementRevision: async (revisionId, projectId, triggeredBy) => {
|
|
26978
|
+
const result = await request(
|
|
26979
|
+
"POST",
|
|
26980
|
+
`/api/projects/${encodeURIComponent(projectId)}/revisions/${encodeURIComponent(revisionId)}/implement`,
|
|
26981
|
+
triggeredBy ? { triggeredBy } : {}
|
|
26982
|
+
);
|
|
26983
|
+
if (!result?.taskId) throw new Error("Revision implementation enqueue failed: no taskId returned");
|
|
26984
|
+
return { taskId: result.taskId, taskType: "revision_implementation", status: result.status ?? "pending", projectId, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
26985
|
+
},
|
|
26667
26986
|
validateWorkItems: async (projectId, noteIds) => {
|
|
26668
26987
|
const result = await request(
|
|
26669
26988
|
"POST",
|
|
@@ -26673,15 +26992,6 @@ function createHttpClient(config2) {
|
|
|
26673
26992
|
if (!result?.taskId) throw new Error("Validation enqueue failed: no taskId returned");
|
|
26674
26993
|
return { taskId: result.taskId, taskType: "work_item_validation", status: result.status ?? "pending", projectId, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
26675
26994
|
},
|
|
26676
|
-
composeBeat: async (projectId, beatId) => {
|
|
26677
|
-
const result = await request(
|
|
26678
|
-
"POST",
|
|
26679
|
-
`/api/projects/${encodeURIComponent(projectId)}/beats/${encodeURIComponent(beatId)}/compose`,
|
|
26680
|
-
{}
|
|
26681
|
-
);
|
|
26682
|
-
if (!result?.taskId) throw new Error("Compose beat enqueue failed: no taskId returned");
|
|
26683
|
-
return { taskId: result.taskId, taskType: "compose_beat", status: result.status ?? "pending", projectId, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
26684
|
-
},
|
|
26685
26995
|
planRevisionBatch: async (projectId, revisionIds) => {
|
|
26686
26996
|
const result = await request(
|
|
26687
26997
|
"POST",
|
|
@@ -26833,10 +27143,17 @@ function createHttpClient(config2) {
|
|
|
26833
27143
|
const res = await request("GET", `/api/checks/${encodeURIComponent(checkId)}`);
|
|
26834
27144
|
return res.check;
|
|
26835
27145
|
},
|
|
26836
|
-
listProjectChecks: async (projectId, checkType) => {
|
|
26837
|
-
const
|
|
26838
|
-
|
|
26839
|
-
|
|
27146
|
+
listProjectChecks: async (projectId, checkType, options) => {
|
|
27147
|
+
const params = new URLSearchParams();
|
|
27148
|
+
if (checkType) params.set("type", checkType);
|
|
27149
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
27150
|
+
if (options?.cursor) params.set("cursor", options.cursor);
|
|
27151
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
27152
|
+
const res = await request(
|
|
27153
|
+
"GET",
|
|
27154
|
+
`/api/projects/${encodeURIComponent(projectId)}/checks${qs}`
|
|
27155
|
+
);
|
|
27156
|
+
return { checks: res?.checks ?? [], nextCursor: res?.nextCursor };
|
|
26840
27157
|
},
|
|
26841
27158
|
listBeatChecks: async (beatId, projectId, checkType) => {
|
|
26842
27159
|
const qs = checkType ? `?type=${encodeURIComponent(checkType)}` : "";
|
|
@@ -27089,7 +27406,7 @@ function loadConfig() {
|
|
|
27089
27406
|
};
|
|
27090
27407
|
}
|
|
27091
27408
|
async function main() {
|
|
27092
|
-
console.error(`[harmonica-mcp] v${"0.
|
|
27409
|
+
console.error(`[harmonica-mcp] v${"0.28.1"} starting\u2026`);
|
|
27093
27410
|
const config2 = loadConfig();
|
|
27094
27411
|
const client = createHttpClient({
|
|
27095
27412
|
apiBaseUrl: config2.apiBaseUrl,
|