@codazen/harmonica-mcp 0.28.1 → 0.30.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 +553 -262
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21503,9 +21503,9 @@ function registerBaselineTools(server, ctx, client) {
|
|
|
21503
21503
|
return {
|
|
21504
21504
|
content: [{
|
|
21505
21505
|
type: "text",
|
|
21506
|
-
text: `Project baseline started.
|
|
21506
|
+
text: `Project baseline started. Job ID: ${task.taskId}
|
|
21507
21507
|
|
|
21508
|
-
Poll progress with:
|
|
21508
|
+
Poll progress with: get_job_status({ jobId: "${task.taskId}" })`
|
|
21509
21509
|
}]
|
|
21510
21510
|
};
|
|
21511
21511
|
} catch (err) {
|
|
@@ -21779,7 +21779,7 @@ ${d.rationale}${d.suggestions?.length ? `
|
|
|
21779
21779
|
function registerBeatQualityTools(server, ctx, client) {
|
|
21780
21780
|
server.tool(
|
|
21781
21781
|
"check_beat_quality",
|
|
21782
|
-
"Enqueue a Beat quality check across 6 dimensions (Distinctive, Harmonious, Substantial, Durable, Clear, Strategically Aligned). By default returns a
|
|
21782
|
+
"Enqueue a Beat quality check across 6 dimensions (Distinctive, Harmonious, Substantial, Durable, Clear, Strategically Aligned). By default returns a jobId immediately (fire-and-forget) \u2014 use get_job_status or list_checks to retrieve results. Set wait=true to block until the check completes and receive the scorecard inline. WARNING: wait=true holds the MCP session open for 30\u201360s per check \u2014 for bulk operations (more than one Beat), omit wait and poll get_job_status separately to avoid session exhaustion.",
|
|
21783
21783
|
{
|
|
21784
21784
|
beatId: external_exports.string().describe("The beat ID (e.g., TF-B-001)"),
|
|
21785
21785
|
projectId: external_exports.string().describe("The project ID"),
|
|
@@ -21794,9 +21794,9 @@ function registerBeatQualityTools(server, ctx, client) {
|
|
|
21794
21794
|
content: [{
|
|
21795
21795
|
type: "text",
|
|
21796
21796
|
text: `Quality check enqueued for ${beatId}.
|
|
21797
|
-
|
|
21797
|
+
Job ID: ${taskId}
|
|
21798
21798
|
|
|
21799
|
-
Use
|
|
21799
|
+
Use get_job_status with jobId="${taskId}" to poll for results, or list_checks to retrieve the completed scorecard once done.`
|
|
21800
21800
|
}]
|
|
21801
21801
|
};
|
|
21802
21802
|
}
|
|
@@ -21877,7 +21877,7 @@ function formatBeatSummaryTable(beats) {
|
|
|
21877
21877
|
});
|
|
21878
21878
|
return [header, ...rows].join("\n");
|
|
21879
21879
|
}
|
|
21880
|
-
function formatBeatDetail(beat
|
|
21880
|
+
function formatBeatDetail(beat) {
|
|
21881
21881
|
const sections = [];
|
|
21882
21882
|
sections.push(`# ${beat.beatId}: ${beat.title}`);
|
|
21883
21883
|
sections.push(`**Status:** ${resolveStatus(beat).replace("_", " ")}`);
|
|
@@ -21896,38 +21896,8 @@ ${beat.description}`);
|
|
|
21896
21896
|
if (beat.priority != null) {
|
|
21897
21897
|
sections.push(`**Priority:** ${beat.priority}`);
|
|
21898
21898
|
}
|
|
21899
|
-
if (workItemNotes && workItemNotes.length > 0) {
|
|
21900
|
-
formatWorkItemNotes(sections, workItemNotes);
|
|
21901
|
-
}
|
|
21902
21899
|
return sections.join("\n");
|
|
21903
21900
|
}
|
|
21904
|
-
function formatWorkItemNotes(sections, notes) {
|
|
21905
|
-
const essentials = notes.filter((n) => n.workItemMeta?.scope === "essential");
|
|
21906
|
-
const niceToHaves = notes.filter((n) => n.workItemMeta?.scope === "nice_to_have");
|
|
21907
|
-
if (essentials.length > 0) {
|
|
21908
|
-
const lines = essentials.filter((n) => n.workItemMeta).map((n) => {
|
|
21909
|
-
const meta = n.workItemMeta;
|
|
21910
|
-
const hours = meta.estimatedHoursRange ? ` [${meta.estimatedHoursRange}]` : "";
|
|
21911
|
-
const check2 = meta.acceptanceCheck ? `
|
|
21912
|
-
- _Check:_ ${meta.acceptanceCheck}` : "";
|
|
21913
|
-
return `- **${n.content}** (${meta.category}, ${meta.complexity})${hours}${check2}`;
|
|
21914
|
-
}).join("\n");
|
|
21915
|
-
sections.push(`
|
|
21916
|
-
## Essentials (${essentials.length})
|
|
21917
|
-
${lines}`);
|
|
21918
|
-
}
|
|
21919
|
-
if (niceToHaves.length > 0) {
|
|
21920
|
-
const lines = niceToHaves.filter((n) => n.workItemMeta).map((n) => {
|
|
21921
|
-
const meta = n.workItemMeta;
|
|
21922
|
-
const why = meta.whyOutOfScope ? `
|
|
21923
|
-
- _Why out of scope:_ ${meta.whyOutOfScope}` : "";
|
|
21924
|
-
return `- **${n.content}** (${meta.category}, ${meta.complexity})${why}`;
|
|
21925
|
-
}).join("\n");
|
|
21926
|
-
sections.push(`
|
|
21927
|
-
## Nice-to-Haves \u2014 Out of Scope (${niceToHaves.length})
|
|
21928
|
-
${lines}`);
|
|
21929
|
-
}
|
|
21930
|
-
}
|
|
21931
21901
|
|
|
21932
21902
|
// ../../libs/harmonica-services/src/mcp/formatters/project-formatter.ts
|
|
21933
21903
|
function formatProjectSummaryTable(projects) {
|
|
@@ -21945,7 +21915,6 @@ var NOTE_TYPE_LABELS = {
|
|
|
21945
21915
|
constraint: "Constraints",
|
|
21946
21916
|
guidance: "Guidance",
|
|
21947
21917
|
decision: "Decisions",
|
|
21948
|
-
workItem: "Work Items",
|
|
21949
21918
|
document: "Documents"
|
|
21950
21919
|
};
|
|
21951
21920
|
function formatProjectContext(project, notes, orgCoda) {
|
|
@@ -22042,7 +22011,6 @@ var TYPE_LABELS = {
|
|
|
22042
22011
|
constraint: "Constraint",
|
|
22043
22012
|
guidance: "Guidance",
|
|
22044
22013
|
decision: "Decision",
|
|
22045
|
-
workItem: "Work Item",
|
|
22046
22014
|
document: "Document"
|
|
22047
22015
|
};
|
|
22048
22016
|
function formatNoteSummary(note) {
|
|
@@ -22127,15 +22095,6 @@ function formatNoteDetail(note) {
|
|
|
22127
22095
|
parts.push(`- Criticality: ${note.criticality.toFixed(2)}`);
|
|
22128
22096
|
}
|
|
22129
22097
|
}
|
|
22130
|
-
if (note.workItemMeta) {
|
|
22131
|
-
parts.push("", "### Work Item Details");
|
|
22132
|
-
parts.push(`- Category: ${note.workItemMeta.category}`);
|
|
22133
|
-
parts.push(`- Complexity: ${note.workItemMeta.complexity}`);
|
|
22134
|
-
parts.push(`- Scope: ${note.workItemMeta.scope}`);
|
|
22135
|
-
if (note.workItemMeta.estimatedHoursRange) {
|
|
22136
|
-
parts.push(`- Estimated Hours: ${note.workItemMeta.estimatedHoursRange}`);
|
|
22137
|
-
}
|
|
22138
|
-
}
|
|
22139
22098
|
if (note.documentMeta) {
|
|
22140
22099
|
parts.push("", "### Document Details");
|
|
22141
22100
|
parts.push(`- Filename: ${note.documentMeta.originalFilename}`);
|
|
@@ -22168,6 +22127,7 @@ var TYPE_LABELS2 = {
|
|
|
22168
22127
|
};
|
|
22169
22128
|
var STATUS_LABELS = {
|
|
22170
22129
|
running: "Running",
|
|
22130
|
+
held: "Held",
|
|
22171
22131
|
succeeded: "Succeeded",
|
|
22172
22132
|
failed: "Failed",
|
|
22173
22133
|
timed_out: "Timed Out",
|
|
@@ -22310,7 +22270,7 @@ function registerBeatTools(server, ctx, client) {
|
|
|
22310
22270
|
);
|
|
22311
22271
|
server.tool(
|
|
22312
22272
|
"get_beat",
|
|
22313
|
-
"Get full beat detail including description
|
|
22273
|
+
"Get full beat detail including description",
|
|
22314
22274
|
{ beatId: external_exports.string().describe("The beat ID (e.g., TF-B-001)") },
|
|
22315
22275
|
async ({ beatId }) => {
|
|
22316
22276
|
await assertBeatInOrg(client, beatId, ctx.orgId);
|
|
@@ -22318,12 +22278,7 @@ function registerBeatTools(server, ctx, client) {
|
|
|
22318
22278
|
if (!beat) {
|
|
22319
22279
|
return { content: [{ type: "text", text: `Beat not found: "${beatId}"` }], isError: true };
|
|
22320
22280
|
}
|
|
22321
|
-
const
|
|
22322
|
-
const workItemNotes = await Promise.all(
|
|
22323
|
-
workItemIndex.map((n) => client.getNote(n.noteId))
|
|
22324
|
-
);
|
|
22325
|
-
const validNotes = workItemNotes.filter((n) => n !== void 0);
|
|
22326
|
-
const text = formatBeatDetail(beat, validNotes);
|
|
22281
|
+
const text = formatBeatDetail(beat);
|
|
22327
22282
|
return { content: [{ type: "text", text }] };
|
|
22328
22283
|
}
|
|
22329
22284
|
);
|
|
@@ -22681,6 +22636,15 @@ function registerBeatVersionTools(server, ctx, client) {
|
|
|
22681
22636
|
createdBy: ctx.user.email,
|
|
22682
22637
|
sourceType: "mcp_tool"
|
|
22683
22638
|
});
|
|
22639
|
+
if (!beatVersion?.beatVersionId || !beatVersion.title) {
|
|
22640
|
+
return {
|
|
22641
|
+
content: [{
|
|
22642
|
+
type: "text",
|
|
22643
|
+
text: `Beat Version was stored but the response is missing required fields (beatVersionId or title). Use list_beat_versions on beat "${beatId}" to find the newly created version.`
|
|
22644
|
+
}],
|
|
22645
|
+
isError: true
|
|
22646
|
+
};
|
|
22647
|
+
}
|
|
22684
22648
|
const lines = [
|
|
22685
22649
|
`Beat Version created successfully.`,
|
|
22686
22650
|
"",
|
|
@@ -22844,6 +22808,34 @@ function registerBeatVersionTools(server, ctx, client) {
|
|
|
22844
22808
|
`**Transition:** ${bv.status} \u2192 ${targetState}`
|
|
22845
22809
|
];
|
|
22846
22810
|
if (reason) lines.push(`**Reason:** ${reason}`);
|
|
22811
|
+
if (result.fanOutWarning) {
|
|
22812
|
+
lines.push("");
|
|
22813
|
+
lines.push(`\u26A0 Fan-out warning: ${result.fanOutWarning.message}`);
|
|
22814
|
+
}
|
|
22815
|
+
if (targetState === "building" && !bv.targetDropId) {
|
|
22816
|
+
try {
|
|
22817
|
+
const project = await client.getProject(bv.projectId);
|
|
22818
|
+
const teamspaceId = project?.teamspaceId;
|
|
22819
|
+
if (teamspaceId) {
|
|
22820
|
+
const draftDrops = await client.listTeamspaceDrops(teamspaceId, { state: "draft" });
|
|
22821
|
+
lines.push("");
|
|
22822
|
+
if (draftDrops.length > 0) {
|
|
22823
|
+
lines.push("\u26A0 **No Drop assigned.** This Beat Version has no target Drop. Assign it to track which release this work belongs to.");
|
|
22824
|
+
lines.push("");
|
|
22825
|
+
lines.push("Available draft Drops:");
|
|
22826
|
+
for (const drop of draftDrops) {
|
|
22827
|
+
lines.push(` \u2022 ${drop.dropCode} \u2014 ${drop.name} (${drop.dropId})`);
|
|
22828
|
+
}
|
|
22829
|
+
lines.push("");
|
|
22830
|
+
lines.push("Use `update_beat_version` to set `targetDropId`, or call `add_beat_version_to_drop`.");
|
|
22831
|
+
} else {
|
|
22832
|
+
lines.push(`\u26A0 **No Drop assigned and no draft Drops exist in Teamspace \`${teamspaceId}\`.** Create a Drop first with \`create_drop\`, then assign this Beat Version to it.`);
|
|
22833
|
+
}
|
|
22834
|
+
}
|
|
22835
|
+
} catch (err) {
|
|
22836
|
+
console.warn("[drop-warning] drop lookup failed for Beat Version", beatVersionId, err);
|
|
22837
|
+
}
|
|
22838
|
+
}
|
|
22847
22839
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
22848
22840
|
} catch (err) {
|
|
22849
22841
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -22939,7 +22931,7 @@ function registerCheckTools(server, ctx, client) {
|
|
|
22939
22931
|
`**Check Type:** ${checkType}`,
|
|
22940
22932
|
`**Target:** ${targetId}`,
|
|
22941
22933
|
``,
|
|
22942
|
-
`Poll with \`
|
|
22934
|
+
`Poll with \`get_job_status\` (jobId: \`${task.taskId}\`), then \`list_checks\` to view results.`
|
|
22943
22935
|
].join("\n");
|
|
22944
22936
|
return { content: [{ type: "text", text }] };
|
|
22945
22937
|
} catch (err) {
|
|
@@ -23108,7 +23100,7 @@ var DROP_STATES = ["draft", "scheduled", "in_progress", "released", "rolled_back
|
|
|
23108
23100
|
function registerDropTools(server, ctx, client) {
|
|
23109
23101
|
server.tool(
|
|
23110
23102
|
"list_drops",
|
|
23111
|
-
"List Drops in a Teamspace, optionally filtered by lifecycle state and sorted. A Drop is a release container \u2014 different from a Beat (capability) or Revision (
|
|
23103
|
+
"List Drops in a Teamspace, optionally filtered by lifecycle state and sorted. A Drop is a release container \u2014 different from a Beat (capability) or Revision (PR-level increment). Use this to see what releases are planned, in flight, or shipped.",
|
|
23112
23104
|
{
|
|
23113
23105
|
teamspaceId: external_exports.string().describe("The teamspace ID"),
|
|
23114
23106
|
state: external_exports.enum(DROP_STATES).optional().describe("Filter by a single Drop state (use status for multi-state OR filtering)"),
|
|
@@ -23433,7 +23425,7 @@ function registerEmbeddingTools(server, ctx, client) {
|
|
|
23433
23425
|
limit: external_exports.coerce.number().int().min(1).max(50).optional().default(10).describe("Max results to return. Default 10"),
|
|
23434
23426
|
includeArchived: external_exports.boolean().optional().default(false).describe("Include archived/deprecated entities in results (excluded by default)"),
|
|
23435
23427
|
entityType: external_exports.enum(["note", "beat", "revision"]).optional().describe("Restrict results to a specific entity type"),
|
|
23436
|
-
noteType: external_exports.enum(["context", "assumption", "constraint", "guidance", "decision", "
|
|
23428
|
+
noteType: external_exports.enum(["context", "assumption", "constraint", "guidance", "decision", "document"]).optional().describe("Restrict results to a specific note type (only applies when entityType is note or unset). Requires embeddings generated after this filter was introduced."),
|
|
23437
23429
|
noteStatus: external_exports.enum(["active", "unvalidated", "validated", "invalidated", "inProgress", "resolved", "superseded", "dismissed"]).optional().describe("Restrict note results to a specific status (e.g. unvalidated). DynamoDB post-filter \u2014 works without re-embedding."),
|
|
23438
23430
|
beatId: external_exports.string().optional().describe("Restrict note results to those belonging to a specific beat (DynamoDB post-filter, no re-embedding needed)")
|
|
23439
23431
|
},
|
|
@@ -23807,7 +23799,7 @@ function formatTriageActions(actions) {
|
|
|
23807
23799
|
}
|
|
23808
23800
|
|
|
23809
23801
|
// ../../libs/harmonica-services/src/mcp/tools/note-tools.ts
|
|
23810
|
-
var NOTE_TYPE_VALUES = ["context", "assumption", "constraint", "guidance", "decision", "
|
|
23802
|
+
var NOTE_TYPE_VALUES = ["context", "assumption", "constraint", "guidance", "decision", "document"];
|
|
23811
23803
|
var NOTE_STATUS_VALUES = ["active", "unvalidated", "validated", "invalidated", "inProgress", "resolved", "superseded", "dismissed"];
|
|
23812
23804
|
function registerNoteTools(server, ctx, client) {
|
|
23813
23805
|
server.tool(
|
|
@@ -23940,7 +23932,6 @@ function registerNoteTools(server, ctx, client) {
|
|
|
23940
23932
|
rationale,
|
|
23941
23933
|
createdBy: ctx.user.userId,
|
|
23942
23934
|
aiGenerated: false,
|
|
23943
|
-
humanAssignee: noteType === "workItem" ? { email: ctx.user.email, name: ctx.user.name } : void 0,
|
|
23944
23935
|
affectsBeats,
|
|
23945
23936
|
dependsOnNotes,
|
|
23946
23937
|
assumptionMeta
|
|
@@ -23992,7 +23983,7 @@ ${text}` }] };
|
|
|
23992
23983
|
content: external_exports.string().optional().describe("Updated content"),
|
|
23993
23984
|
response: external_exports.string().optional().describe("User response (for resolving assumptions)"),
|
|
23994
23985
|
beatId: external_exports.string().optional().describe("Move this note to a different beat (updates all index items transactionally)"),
|
|
23995
|
-
revisionId: external_exports.string().optional().describe("Revision ID to associate this note with (
|
|
23986
|
+
revisionId: external_exports.string().optional().describe("Revision ID to associate this note with (revision-scoped assumptions, decisions, or guidance)"),
|
|
23996
23987
|
dependsOnNotes: external_exports.array(external_exports.string()).optional().describe("Note IDs this note depends on (child \u2192 parent links for consolidation)"),
|
|
23997
23988
|
humanAssignee: humanAssigneeSchema.nullable().optional().describe("Assign or reassign this note to a person (null to clear)"),
|
|
23998
23989
|
agentAssignee: agentAssigneeSchema.nullable().optional().describe("Assign or reassign this note to an agent (null to clear)")
|
|
@@ -24252,7 +24243,7 @@ function registerOnboardingTools(server, ctx, client) {
|
|
|
24252
24243
|
server.tool(
|
|
24253
24244
|
"create_teamspace_onboarding_batch",
|
|
24254
24245
|
[
|
|
24255
|
-
"Create a Teamspace, Projects, Notes,
|
|
24246
|
+
"Create a Teamspace, Projects, Notes, and Deliverables",
|
|
24256
24247
|
"from a PM-approved onboarding payload in a single batch operation.",
|
|
24257
24248
|
"Call this after the PM approves the pre-flight review card from initiate_teamspace_onboarding.",
|
|
24258
24249
|
"Returns a creation receipt with IDs and counts for all created entities.",
|
|
@@ -24262,15 +24253,13 @@ function registerOnboardingTools(server, ctx, client) {
|
|
|
24262
24253
|
teamspaceName: external_exports.string().min(1).max(200).describe("Teamspace display name"),
|
|
24263
24254
|
projects: external_exports.array(OnboardingProjectInputSchema).min(1).describe("Phase 1 projects to create, each linked to the Teamspace"),
|
|
24264
24255
|
teamspaceNotes: external_exports.array(OnboardingNoteInputSchema).describe("Constraints, assumptions, and context notes at the Teamspace/project level"),
|
|
24265
|
-
deliverables: external_exports.array(OnboardingDeliverableInputSchema).describe("Deliverables detected from commitment language in documents")
|
|
24266
|
-
stakeholderWorkItems: external_exports.array(external_exports.string().min(1)).describe("Work item content strings for missing client stakeholder roles")
|
|
24256
|
+
deliverables: external_exports.array(OnboardingDeliverableInputSchema).describe("Deliverables detected from commitment language in documents")
|
|
24267
24257
|
},
|
|
24268
24258
|
async ({
|
|
24269
24259
|
teamspaceName,
|
|
24270
24260
|
projects,
|
|
24271
24261
|
teamspaceNotes,
|
|
24272
|
-
deliverables
|
|
24273
|
-
stakeholderWorkItems
|
|
24262
|
+
deliverables
|
|
24274
24263
|
}) => {
|
|
24275
24264
|
const lines = [];
|
|
24276
24265
|
let teamspace;
|
|
@@ -24373,27 +24362,6 @@ Notes filed: ${createdNotes.length}`);
|
|
|
24373
24362
|
}
|
|
24374
24363
|
}
|
|
24375
24364
|
lines.push(`Deliverables: ${createdDeliverables.length}`);
|
|
24376
|
-
let workItemCount = 0;
|
|
24377
|
-
if (stakeholderWorkItems.length > 0 && createdProjects.length > 0) {
|
|
24378
|
-
const firstProjectId = createdProjects[0].projectId;
|
|
24379
|
-
for (const content of stakeholderWorkItems) {
|
|
24380
|
-
try {
|
|
24381
|
-
await client.createNote({
|
|
24382
|
-
noteId: `N-TEMP-${Date.now()}`,
|
|
24383
|
-
// overridden server-side
|
|
24384
|
-
projectId: firstProjectId,
|
|
24385
|
-
noteType: "workItem",
|
|
24386
|
-
content,
|
|
24387
|
-
status: "active",
|
|
24388
|
-
createdBy: ctx.user?.userId ?? "mcp",
|
|
24389
|
-
aiGenerated: false
|
|
24390
|
-
});
|
|
24391
|
-
workItemCount++;
|
|
24392
|
-
} catch {
|
|
24393
|
-
}
|
|
24394
|
-
}
|
|
24395
|
-
}
|
|
24396
|
-
lines.push(`Stakeholder workItems: ${workItemCount}`);
|
|
24397
24365
|
lines.unshift(`\u2705 Onboarding batch created successfully.`);
|
|
24398
24366
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
24399
24367
|
}
|
|
@@ -24587,7 +24555,7 @@ ${d.rationale}${d.suggestions?.length ? `
|
|
|
24587
24555
|
function registerPlanQualityTools(server, ctx, client) {
|
|
24588
24556
|
server.tool(
|
|
24589
24557
|
"check_plan_quality",
|
|
24590
|
-
"Enqueue a Revision plan quality check across 6 dimensions (Scoped, Directional, Testable, Traceable, Assignable, Risk-aware). By default returns a
|
|
24558
|
+
"Enqueue a Revision plan quality check across 6 dimensions (Scoped, Directional, Testable, Traceable, Assignable, Risk-aware). 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 revision), omit wait and poll get_job_status separately to avoid session exhaustion.",
|
|
24591
24559
|
{
|
|
24592
24560
|
revisionId: external_exports.string().describe("The revision or Beat Version ID (e.g., rev-abc123 or bv-abc123)"),
|
|
24593
24561
|
projectId: external_exports.string().describe("The project ID"),
|
|
@@ -24602,9 +24570,9 @@ function registerPlanQualityTools(server, ctx, client) {
|
|
|
24602
24570
|
content: [{
|
|
24603
24571
|
type: "text",
|
|
24604
24572
|
text: `Plan quality check enqueued for ${revisionId}.
|
|
24605
|
-
|
|
24573
|
+
Job ID: ${task.taskId}
|
|
24606
24574
|
|
|
24607
|
-
Use
|
|
24575
|
+
Use get_job_status with jobId="${task.taskId}" to poll for results, or list_checks to retrieve the completed scorecard once done.`
|
|
24608
24576
|
}]
|
|
24609
24577
|
};
|
|
24610
24578
|
}
|
|
@@ -24737,7 +24705,7 @@ function registerPortfolioCoherenceTools(server, ctx, client) {
|
|
|
24737
24705
|
`Portfolio coherence check enqueued for ${projectId}.`,
|
|
24738
24706
|
`Task ID: ${taskId}`,
|
|
24739
24707
|
"",
|
|
24740
|
-
`Use
|
|
24708
|
+
`Use get_job_status with jobId="${taskId}" to poll, or list_checks with projectId="${projectId}" and checkType="portfolio_coherence" to view results.`
|
|
24741
24709
|
].join("\n")
|
|
24742
24710
|
}]
|
|
24743
24711
|
};
|
|
@@ -24954,17 +24922,18 @@ function registerProjectTools(server, ctx, client) {
|
|
|
24954
24922
|
"Get project metadata, description, and notes in one view",
|
|
24955
24923
|
{ projectId: external_exports.string().describe("The project ID") },
|
|
24956
24924
|
async ({ projectId }) => {
|
|
24957
|
-
|
|
24958
|
-
|
|
24959
|
-
|
|
24960
|
-
|
|
24961
|
-
|
|
24962
|
-
|
|
24963
|
-
|
|
24925
|
+
try {
|
|
24926
|
+
const [project, org] = await Promise.all([
|
|
24927
|
+
fetchProjectInOrg(client, projectId, ctx.orgId),
|
|
24928
|
+
client.getOrg(ctx.orgId)
|
|
24929
|
+
]);
|
|
24930
|
+
const notes = await client.listProjectNotes(projectId);
|
|
24931
|
+
const text = formatProjectContext(project, notes, org?.coda);
|
|
24932
|
+
return { content: [{ type: "text", text }] };
|
|
24933
|
+
} catch (err) {
|
|
24934
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24935
|
+
return { content: [{ type: "text", text: `Failed to get project context: ${message}` }], isError: true };
|
|
24964
24936
|
}
|
|
24965
|
-
const notes = await client.listProjectNotes(projectId);
|
|
24966
|
-
const text = formatProjectContext(project, notes, org?.coda);
|
|
24967
|
-
return { content: [{ type: "text", text }] };
|
|
24968
24937
|
}
|
|
24969
24938
|
);
|
|
24970
24939
|
server.tool(
|
|
@@ -24982,39 +24951,44 @@ function registerProjectTools(server, ctx, client) {
|
|
|
24982
24951
|
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.')
|
|
24983
24952
|
},
|
|
24984
24953
|
async ({ projectId, ...updates }) => {
|
|
24985
|
-
|
|
24986
|
-
|
|
24987
|
-
|
|
24988
|
-
|
|
24989
|
-
|
|
24954
|
+
try {
|
|
24955
|
+
const nonEmpty = Object.fromEntries(
|
|
24956
|
+
Object.entries(updates).filter(([, v]) => v !== void 0)
|
|
24957
|
+
);
|
|
24958
|
+
if (Object.keys(nonEmpty).length === 0) {
|
|
24959
|
+
return { content: [{ type: "text", text: "No updates provided." }], isError: true };
|
|
24990
24960
|
}
|
|
24991
|
-
|
|
24992
|
-
|
|
24961
|
+
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
24962
|
+
if (updates.teamspaceId != null) {
|
|
24963
|
+
const teamspace = await client.getTeamspace(updates.teamspaceId);
|
|
24964
|
+
if (!teamspace) {
|
|
24965
|
+
return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
|
|
24966
|
+
}
|
|
24967
|
+
if (teamspace.orgId !== ctx.orgId) {
|
|
24968
|
+
return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
|
|
24969
|
+
}
|
|
24993
24970
|
}
|
|
24971
|
+
const updated = await client.updateProject(projectId, nonEmpty);
|
|
24972
|
+
if (!updated) {
|
|
24973
|
+
return { content: [{ type: "text", text: `Project not found: "${projectId}"` }], isError: true };
|
|
24974
|
+
}
|
|
24975
|
+
if (PROJECT_EMBEDDING_FIELDS.some((f) => f in nonEmpty)) {
|
|
24976
|
+
void client.triggerProjectEmbedding(projectId);
|
|
24977
|
+
}
|
|
24978
|
+
const lines = [
|
|
24979
|
+
`Project updated successfully.`,
|
|
24980
|
+
"",
|
|
24981
|
+
`**ID:** ${updated.projectId}`,
|
|
24982
|
+
`**Title:** ${updated.title}`,
|
|
24983
|
+
updated.strategy ? `**Strategy:** (updated)` : "",
|
|
24984
|
+
updated.repoOwner ? `**Repo:** ${updated.repoOwner}/${updated.repoName}` : "",
|
|
24985
|
+
updated.repoDefaultBranch ? `**Default Branch:** ${updated.repoDefaultBranch}` : ""
|
|
24986
|
+
].filter(Boolean);
|
|
24987
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
24988
|
+
} catch (err) {
|
|
24989
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24990
|
+
return { content: [{ type: "text", text: `Failed to update project: ${message}` }], isError: true };
|
|
24994
24991
|
}
|
|
24995
|
-
const nonEmpty = Object.fromEntries(
|
|
24996
|
-
Object.entries(updates).filter(([, v]) => v !== void 0)
|
|
24997
|
-
);
|
|
24998
|
-
if (Object.keys(nonEmpty).length === 0) {
|
|
24999
|
-
return { content: [{ type: "text", text: "No updates provided." }], isError: true };
|
|
25000
|
-
}
|
|
25001
|
-
const updated = await client.updateProject(projectId, nonEmpty);
|
|
25002
|
-
if (!updated) {
|
|
25003
|
-
return { content: [{ type: "text", text: `Project not found: "${projectId}"` }], isError: true };
|
|
25004
|
-
}
|
|
25005
|
-
if (PROJECT_EMBEDDING_FIELDS.some((f) => f in nonEmpty)) {
|
|
25006
|
-
void client.triggerProjectEmbedding(projectId);
|
|
25007
|
-
}
|
|
25008
|
-
const lines = [
|
|
25009
|
-
`Project updated successfully.`,
|
|
25010
|
-
"",
|
|
25011
|
-
`**ID:** ${updated.projectId}`,
|
|
25012
|
-
`**Title:** ${updated.title}`,
|
|
25013
|
-
updated.strategy ? `**Strategy:** (updated)` : "",
|
|
25014
|
-
updated.repoOwner ? `**Repo:** ${updated.repoOwner}/${updated.repoName}` : "",
|
|
25015
|
-
updated.repoDefaultBranch ? `**Default Branch:** ${updated.repoDefaultBranch}` : ""
|
|
25016
|
-
].filter(Boolean);
|
|
25017
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
25018
24992
|
}
|
|
25019
24993
|
);
|
|
25020
24994
|
server.tool(
|
|
@@ -25111,13 +25085,13 @@ function registerProjectTools(server, ctx, client) {
|
|
|
25111
25085
|
function registerRevisionLifecycleTools(server, ctx, client) {
|
|
25112
25086
|
server.tool(
|
|
25113
25087
|
"create_revision",
|
|
25114
|
-
"Create a new revision on a Beat. Starts in `draft` state under the PR workflow (draft \u2192 open \u2192 merged) \u2014 NOT the standard Beat Version lifecycle. Transition to `open` when the PR is ready for review; transition to `merged` when the PR is merged. Environment progression (in_staging, ready_for_production, live) is tracked on the parent Beat Version, not the Revision. Use this to add revisions alongside existing ones \u2014 no idempotency guard.",
|
|
25088
|
+
"Create a new revision on a Beat. Starts in `draft` state under the PR workflow (draft \u2192 open \u2192 merged) \u2014 NOT the standard Beat Version lifecycle. Transition to `open` when the PR is ready for review; transition to `merged` when the PR is merged. Environment progression (in_staging, ready_for_production, live) is tracked on the parent Beat Version, not the Revision. `beatVersionId` is optional \u2014 omit it for hotfixes, dep bumps, or other operational fixes that don't belong to a planning increment. Orphan Revisions (no BV) must carry their own `description` since there's no BV context to inherit. Use this to add revisions alongside existing ones \u2014 no idempotency guard.",
|
|
25115
25089
|
{
|
|
25116
25090
|
beatId: external_exports.string().describe("The beat ID to create the revision on"),
|
|
25117
25091
|
title: external_exports.string().describe("Title for this revision"),
|
|
25118
|
-
description: external_exports.string().describe("What this revision delivers"),
|
|
25092
|
+
description: external_exports.string().describe("What this revision delivers. Required when beatVersionId is omitted (the description is the audit-trail floor for orphan Revisions)."),
|
|
25119
25093
|
changeSummary: external_exports.string().describe("Brief summary of the change"),
|
|
25120
|
-
beatVersionId: external_exports.string().min(1).optional().describe("
|
|
25094
|
+
beatVersionId: external_exports.string().min(1).optional().describe("Beat Version ID to associate this revision with. Omit for hotfixes / orphan changes that don't fit a planning increment."),
|
|
25121
25095
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
25122
25096
|
priority: external_exports.coerce.number().optional().describe("Priority (lower = higher priority)"),
|
|
25123
25097
|
estimatedEffort: external_exports.string().optional().describe("Estimated effort (e.g., S, M, L, XL)"),
|
|
@@ -25131,12 +25105,13 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
25131
25105
|
if (!beat) {
|
|
25132
25106
|
return { content: [{ type: "text", text: `Beat not found: "${beatId}"` }], isError: true };
|
|
25133
25107
|
}
|
|
25108
|
+
let parentBv;
|
|
25134
25109
|
if (beatVersionId !== void 0) {
|
|
25135
|
-
|
|
25136
|
-
if (!
|
|
25110
|
+
parentBv = await client.getBeatVersion(beatVersionId);
|
|
25111
|
+
if (!parentBv) {
|
|
25137
25112
|
return { content: [{ type: "text", text: `Beat Version not found: "${beatVersionId}"` }], isError: true };
|
|
25138
25113
|
}
|
|
25139
|
-
if (
|
|
25114
|
+
if (parentBv.beatId !== beatId || parentBv.projectId !== beat.projectId) {
|
|
25140
25115
|
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" belongs to a different Beat.` }], isError: true };
|
|
25141
25116
|
}
|
|
25142
25117
|
}
|
|
@@ -25167,6 +25142,30 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
25167
25142
|
for (const w of warnings) {
|
|
25168
25143
|
lines.push("", `**Warning (${w.code}):** ${w.message}`);
|
|
25169
25144
|
}
|
|
25145
|
+
if (parentBv && !parentBv.targetDropId) {
|
|
25146
|
+
try {
|
|
25147
|
+
const project = await client.getProject(parentBv.projectId);
|
|
25148
|
+
const teamspaceId = project?.teamspaceId;
|
|
25149
|
+
if (teamspaceId) {
|
|
25150
|
+
const draftDrops = await client.listTeamspaceDrops(teamspaceId, { state: "draft" });
|
|
25151
|
+
lines.push("");
|
|
25152
|
+
if (draftDrops.length > 0) {
|
|
25153
|
+
lines.push("\u26A0 **No Drop assigned.** The parent Beat Version has no target Drop. Assign it to track which release this work belongs to.");
|
|
25154
|
+
lines.push("");
|
|
25155
|
+
lines.push("Available draft Drops:");
|
|
25156
|
+
for (const drop of draftDrops) {
|
|
25157
|
+
lines.push(` \u2022 ${drop.dropCode} \u2014 ${drop.name} (${drop.dropId})`);
|
|
25158
|
+
}
|
|
25159
|
+
lines.push("");
|
|
25160
|
+
lines.push("Use `update_beat_version` to set `targetDropId`, or call `add_beat_version_to_drop`.");
|
|
25161
|
+
} else {
|
|
25162
|
+
lines.push(`\u26A0 **No Drop assigned and no draft Drops exist in Teamspace \`${teamspaceId}\`.** Create a Drop first with \`create_drop\`, then assign the parent Beat Version to it.`);
|
|
25163
|
+
}
|
|
25164
|
+
}
|
|
25165
|
+
} catch (err) {
|
|
25166
|
+
console.warn("[drop-warning] drop lookup failed for parent Beat Version", beatVersionId, err);
|
|
25167
|
+
}
|
|
25168
|
+
}
|
|
25170
25169
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
25171
25170
|
} catch (err) {
|
|
25172
25171
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -25240,9 +25239,10 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
25240
25239
|
targetState: external_exports.enum([...LIFECYCLE_STATES, ...PR_LIFECYCLE_STATES]).describe("The target lifecycle state"),
|
|
25241
25240
|
reason: external_exports.string().optional().describe("Why this transition is being made"),
|
|
25242
25241
|
skipQualityCheck: external_exports.boolean().optional().describe("Skip the quality check guard (for reconciling already-shipped code)"),
|
|
25243
|
-
reconcile: external_exports.boolean().optional().describe("Walk through all intermediate forward states automatically. Use for already-shipped work that needs state machine catch-up.")
|
|
25242
|
+
reconcile: external_exports.boolean().optional().describe("Walk through all intermediate forward states automatically. Use for already-shipped work that needs state machine catch-up."),
|
|
25243
|
+
mergeCommitSha: external_exports.string().regex(/^[0-9a-f]{7,40}$/).optional().describe("SHA of the merge commit (from the GitHub webhook payload or CI). Stored at the open \u2192 merged transition to enable precise deploy correlation.")
|
|
25244
25244
|
},
|
|
25245
|
-
async ({ revisionId, targetState, reason, skipQualityCheck, reconcile }) => {
|
|
25245
|
+
async ({ revisionId, targetState, reason, skipQualityCheck, reconcile, mergeCommitSha }) => {
|
|
25246
25246
|
try {
|
|
25247
25247
|
const revision = await client.getRevision(revisionId);
|
|
25248
25248
|
if (!revision) {
|
|
@@ -25259,7 +25259,8 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
25259
25259
|
actor: { type: "human", id: ctx.user.userId, name: ctx.user.name, email: ctx.user.email },
|
|
25260
25260
|
trigger: "human_action",
|
|
25261
25261
|
reason,
|
|
25262
|
-
metadata: { source: "mcp", ...skipQualityCheck ? { skipQualityCheck: true } : {} }
|
|
25262
|
+
metadata: { source: "mcp", ...skipQualityCheck ? { skipQualityCheck: true } : {} },
|
|
25263
|
+
...mergeCommitSha !== void 0 && { mergeCommitSha }
|
|
25263
25264
|
};
|
|
25264
25265
|
const result = reconcile ? await client.reconcileRevisionToState(revisionId, targetState, transitionOptions) : await client.transitionRevisionStatus(revisionId, targetState, transitionOptions);
|
|
25265
25266
|
if (!result.success) {
|
|
@@ -25286,6 +25287,11 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
25286
25287
|
// ../../libs/harmonica-services/src/mcp/tools/session-tools.ts
|
|
25287
25288
|
var SESSION_STATUSES = ["running", "succeeded", "failed", "timed_out", "cancelled"];
|
|
25288
25289
|
var SESSION_TYPES = ["planning", "implementation", "triage", "assessment", "composition", "baseline"];
|
|
25290
|
+
var SESSION_MESSAGE_MAX_LENGTH = 8e3;
|
|
25291
|
+
var STREAM_MAX_EVENTS_DEFAULT = 50;
|
|
25292
|
+
var STREAM_MAX_EVENTS_CEILING = 200;
|
|
25293
|
+
var STREAM_MAX_WAIT_MS_DEFAULT = 5e3;
|
|
25294
|
+
var STREAM_MAX_WAIT_MS_CEILING = 25e3;
|
|
25289
25295
|
function registerSessionTools(server, ctx, client) {
|
|
25290
25296
|
server.tool(
|
|
25291
25297
|
"get_session",
|
|
@@ -25370,6 +25376,147 @@ function registerSessionTools(server, ctx, client) {
|
|
|
25370
25376
|
};
|
|
25371
25377
|
}
|
|
25372
25378
|
);
|
|
25379
|
+
server.tool(
|
|
25380
|
+
"send_session_message",
|
|
25381
|
+
'Send a user-turn message into a running (or held) agent Session. The message is queued for the worker turn loop to inject as the next user input. Returns immediately once queued \u2014 the agent\'s response will appear in the session\'s event stream. Use this to course-correct a running agent without cancelling and restarting (e.g., "switch to React Testing Library", "scope this PR to just X"). Terminal sessions (succeeded, failed, timed_out, cancelled) cannot accept messages \u2014 call this on a `running` or `held` session only.',
|
|
25382
|
+
{
|
|
25383
|
+
sessionId: external_exports.string().min(1).describe("The Session ID (sess-<uuid>) to deliver the message to"),
|
|
25384
|
+
body: external_exports.string().min(1).max(SESSION_MESSAGE_MAX_LENGTH).describe("The user-turn content. Plain text \u2014 no formatting required.")
|
|
25385
|
+
},
|
|
25386
|
+
async ({ sessionId, body }) => {
|
|
25387
|
+
try {
|
|
25388
|
+
const result = await client.sendSessionMessage(sessionId, {
|
|
25389
|
+
body,
|
|
25390
|
+
enqueuedBy: {
|
|
25391
|
+
email: ctx.user.email ?? ctx.user.userId,
|
|
25392
|
+
...ctx.user.name && { name: ctx.user.name }
|
|
25393
|
+
}
|
|
25394
|
+
});
|
|
25395
|
+
if (result.status === "not_found") {
|
|
25396
|
+
return {
|
|
25397
|
+
content: [{ type: "text", text: `Session not found: ${sessionId}` }],
|
|
25398
|
+
isError: true
|
|
25399
|
+
};
|
|
25400
|
+
}
|
|
25401
|
+
if (result.status === "not_accepting") {
|
|
25402
|
+
return {
|
|
25403
|
+
content: [
|
|
25404
|
+
{
|
|
25405
|
+
type: "text",
|
|
25406
|
+
text: `Session ${sessionId} cannot accept new messages \u2014 current status: ${result.sessionStatus}. Send messages only to sessions in 'running' or 'held' status.`
|
|
25407
|
+
}
|
|
25408
|
+
],
|
|
25409
|
+
isError: true
|
|
25410
|
+
};
|
|
25411
|
+
}
|
|
25412
|
+
return {
|
|
25413
|
+
content: [
|
|
25414
|
+
{
|
|
25415
|
+
type: "text",
|
|
25416
|
+
text: `Message queued for session ${sessionId} (messageId: ${result.message.messageId}). It will be delivered as the next user-turn input.`
|
|
25417
|
+
}
|
|
25418
|
+
]
|
|
25419
|
+
};
|
|
25420
|
+
} catch (err) {
|
|
25421
|
+
return {
|
|
25422
|
+
content: [
|
|
25423
|
+
{
|
|
25424
|
+
type: "text",
|
|
25425
|
+
text: `Failed to send message: ${err instanceof Error ? err.message : String(err)}`
|
|
25426
|
+
}
|
|
25427
|
+
],
|
|
25428
|
+
isError: true
|
|
25429
|
+
};
|
|
25430
|
+
}
|
|
25431
|
+
}
|
|
25432
|
+
);
|
|
25433
|
+
server.tool(
|
|
25434
|
+
"resume_session",
|
|
25435
|
+
"Resume a held agent Session (B-205 R2b). Use this when a Session is in the `held` state \u2014 paused by a timeout, harness compaction, or manual hold \u2014 and you want it to continue where it left off without losing the SDK conversation context. The call validates the Session is eligible (still `held`, has the captured SDK session id, has resume budget remaining) and enqueues a resume Task; the worker re-attaches the SDK conversation. Returns the enqueued Task ID \u2014 poll `get_job_status` (or stream the Session via `stream_session_events`) to watch progress. Sessions that are not held (running / terminal / out of budget / origin task missing / lost the atomic claim to a sibling resume call) return a structured `not_resumable` reason \u2014 surface it; don't retry blindly.",
|
|
25436
|
+
{
|
|
25437
|
+
sessionId: external_exports.string().min(1).describe("The Session ID (sess-<uuid>) to resume")
|
|
25438
|
+
},
|
|
25439
|
+
async ({ sessionId }) => {
|
|
25440
|
+
try {
|
|
25441
|
+
const result = await client.resumeSession(sessionId);
|
|
25442
|
+
if (result.status === "not_found") {
|
|
25443
|
+
return {
|
|
25444
|
+
content: [{ type: "text", text: `Session not found: ${sessionId}` }],
|
|
25445
|
+
isError: true
|
|
25446
|
+
};
|
|
25447
|
+
}
|
|
25448
|
+
if (result.status === "not_resumable") {
|
|
25449
|
+
return {
|
|
25450
|
+
content: [
|
|
25451
|
+
{
|
|
25452
|
+
type: "text",
|
|
25453
|
+
text: `Session ${sessionId} cannot be resumed \u2014 current status: ${result.sessionStatus}, reason: ${result.reason}. Resume requires a session in 'held' state with an SDK session captured and resume budget remaining.`
|
|
25454
|
+
}
|
|
25455
|
+
],
|
|
25456
|
+
isError: true
|
|
25457
|
+
};
|
|
25458
|
+
}
|
|
25459
|
+
return {
|
|
25460
|
+
content: [
|
|
25461
|
+
{
|
|
25462
|
+
type: "text",
|
|
25463
|
+
text: `Resume queued for session ${sessionId} (taskId: ${result.result.taskId}). Poll \`get_task ${result.result.taskId}\` or stream the session for progress.`
|
|
25464
|
+
}
|
|
25465
|
+
]
|
|
25466
|
+
};
|
|
25467
|
+
} catch (err) {
|
|
25468
|
+
return {
|
|
25469
|
+
content: [
|
|
25470
|
+
{
|
|
25471
|
+
type: "text",
|
|
25472
|
+
text: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
|
|
25473
|
+
}
|
|
25474
|
+
],
|
|
25475
|
+
isError: true
|
|
25476
|
+
};
|
|
25477
|
+
}
|
|
25478
|
+
}
|
|
25479
|
+
);
|
|
25480
|
+
server.tool(
|
|
25481
|
+
"stream_session_events",
|
|
25482
|
+
'Long-poll an agent Session\'s event stream. Subscribes to the SSE endpoint and returns a bounded batch of tool/text events plus current status/metrics \u2014 closes N-4E09-2818 by keeping each MCP RPC short while the upstream SSE connection is the real transport. Iterate by passing the returned `nextSinceEventIndex` as `since_event_index` on the next call; stop when `isTerminal: true`. Use this for "watch this session in real time" workflows (agent supervising another agent, live UI tail).',
|
|
25483
|
+
{
|
|
25484
|
+
sessionId: external_exports.string().min(1).describe("The Session ID (sess-<uuid>) to subscribe to"),
|
|
25485
|
+
since_event_index: external_exports.number().int().nonnegative().optional().describe("Resume cursor from the previous batch's `nextSinceEventIndex`. Omit on the first call to receive a `snapshot` record. Server replays events with `index > since_event_index`."),
|
|
25486
|
+
max_events: external_exports.number().int().min(1).max(STREAM_MAX_EVENTS_CEILING).optional().describe(`Cap on records per batch. Default ${STREAM_MAX_EVENTS_DEFAULT}, max ${STREAM_MAX_EVENTS_CEILING}.`),
|
|
25487
|
+
max_wait_ms: external_exports.number().int().min(100).max(STREAM_MAX_WAIT_MS_CEILING).optional().describe(`Cap on batch duration in milliseconds. Default ${STREAM_MAX_WAIT_MS_DEFAULT}, max ${STREAM_MAX_WAIT_MS_CEILING}.`)
|
|
25488
|
+
},
|
|
25489
|
+
async ({ sessionId, since_event_index, max_events, max_wait_ms }) => {
|
|
25490
|
+
try {
|
|
25491
|
+
const result = await client.streamSessionEvents(sessionId, {
|
|
25492
|
+
...since_event_index !== void 0 && { sinceEventIndex: since_event_index },
|
|
25493
|
+
...max_events !== void 0 && { maxEvents: max_events },
|
|
25494
|
+
...max_wait_ms !== void 0 && { maxWaitMs: max_wait_ms }
|
|
25495
|
+
});
|
|
25496
|
+
if (result.status === "not_found") {
|
|
25497
|
+
return {
|
|
25498
|
+
content: [{ type: "text", text: `Session not found: ${sessionId}` }],
|
|
25499
|
+
isError: true
|
|
25500
|
+
};
|
|
25501
|
+
}
|
|
25502
|
+
const header = `Batch: ${result.events.length} record(s) [reason=${result.reason}, isTerminal=${result.isTerminal}${result.nextSinceEventIndex !== void 0 ? `, nextSinceEventIndex=${result.nextSinceEventIndex}` : ""}]`;
|
|
25503
|
+
const lines = result.events.map((e, i) => {
|
|
25504
|
+
const payloadJson = JSON.stringify(e.payload);
|
|
25505
|
+
return `${i + 1}. ${e.kind} ${payloadJson}`;
|
|
25506
|
+
});
|
|
25507
|
+
const body = lines.length > 0 ? `
|
|
25508
|
+
${lines.join("\n")}` : "";
|
|
25509
|
+
return { content: [{ type: "text", text: `${header}${body}` }] };
|
|
25510
|
+
} catch (err) {
|
|
25511
|
+
return {
|
|
25512
|
+
content: [
|
|
25513
|
+
{ type: "text", text: `Failed to stream session events: ${err instanceof Error ? err.message : String(err)}` }
|
|
25514
|
+
],
|
|
25515
|
+
isError: true
|
|
25516
|
+
};
|
|
25517
|
+
}
|
|
25518
|
+
}
|
|
25519
|
+
);
|
|
25373
25520
|
server.tool(
|
|
25374
25521
|
"cancel_session",
|
|
25375
25522
|
"Cancel a running agent Session. Idempotent \u2014 calling cancel on a session that is already terminal (succeeded, failed, timed_out, cancelled) returns the existing record unchanged. Use this to stop a runaway agent or free up budget.",
|
|
@@ -25938,7 +26085,7 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
25938
26085
|
);
|
|
25939
26086
|
server.tool(
|
|
25940
26087
|
"draft_coda",
|
|
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
|
|
26088
|
+
"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 job ID \u2014 use get_job_status to track progress.",
|
|
25942
26089
|
{
|
|
25943
26090
|
projectId: external_exports.string().describe("The project ID"),
|
|
25944
26091
|
beatId: external_exports.string().describe("The beat ID (must have a title but no description)")
|
|
@@ -25959,7 +26106,7 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
25959
26106
|
`**Status:** ${task.status}`,
|
|
25960
26107
|
"",
|
|
25961
26108
|
"The agent will gather context and apply a Coda directly to the Beat. An activity will be logged \u2014 review and edit if needed.",
|
|
25962
|
-
"Use `
|
|
26109
|
+
"Use `get_job_status` with the job ID to check progress."
|
|
25963
26110
|
].join("\n")
|
|
25964
26111
|
}]
|
|
25965
26112
|
};
|
|
@@ -25971,7 +26118,7 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
25971
26118
|
);
|
|
25972
26119
|
server.tool(
|
|
25973
26120
|
"validate_assumptions",
|
|
25974
|
-
"Validate unvalidated assumption Notes against the codebase. Checks if assumptions are confirmed (validated), contradicted (invalidated), or have insufficient evidence (unvalidated). Low-criticality assumptions are auto-resolved; high-criticality are flagged for human review. Returns a
|
|
26121
|
+
"Validate unvalidated assumption Notes against the codebase. Checks if assumptions are confirmed (validated), contradicted (invalidated), or have insufficient evidence (unvalidated). Low-criticality assumptions are auto-resolved; high-criticality are flagged for human review. Returns a job ID \u2014 use get_job_status to track progress.",
|
|
25975
26122
|
{
|
|
25976
26123
|
projectId: external_exports.string().describe("The project ID"),
|
|
25977
26124
|
noteIds: external_exports.array(external_exports.string()).optional().describe("Specific assumption note IDs to validate. If omitted, validates all unvalidated assumptions.")
|
|
@@ -25993,7 +26140,7 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
25993
26140
|
"",
|
|
25994
26141
|
"The validator will grep the codebase for evidence, then classify each assumption.",
|
|
25995
26142
|
"Low-criticality assumptions are auto-resolved. High-criticality are flagged for your review.",
|
|
25996
|
-
"Use `
|
|
26143
|
+
"Use `get_job_status` with the job ID to check progress."
|
|
25997
26144
|
].join("\n")
|
|
25998
26145
|
}]
|
|
25999
26146
|
};
|
|
@@ -26003,44 +26150,9 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
26003
26150
|
}
|
|
26004
26151
|
}
|
|
26005
26152
|
);
|
|
26006
|
-
server.tool(
|
|
26007
|
-
"implement_work_item",
|
|
26008
|
-
"Trigger an agent session to implement a workItem Note. The agent explores the codebase, writes code, commits, pushes a branch, and creates a PR. Returns a task ID \u2014 use get_task_status to track progress. Requires agentCapabilities.implementation enabled on the project.",
|
|
26009
|
-
{
|
|
26010
|
-
noteId: external_exports.string().describe("The workItem note ID to implement (e.g., N-TF-001)"),
|
|
26011
|
-
projectId: external_exports.string().describe("The project ID")
|
|
26012
|
-
},
|
|
26013
|
-
async ({ noteId, projectId }) => {
|
|
26014
|
-
try {
|
|
26015
|
-
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
26016
|
-
const task = await client.implementWorkItem(noteId, projectId, {
|
|
26017
|
-
name: ctx.user.name,
|
|
26018
|
-
email: ctx.user.email
|
|
26019
|
-
});
|
|
26020
|
-
return {
|
|
26021
|
-
content: [{
|
|
26022
|
-
type: "text",
|
|
26023
|
-
text: [
|
|
26024
|
-
"Work item implementation session queued.",
|
|
26025
|
-
"",
|
|
26026
|
-
`**Note:** ${noteId}`,
|
|
26027
|
-
`**Task ID:** ${task.taskId}`,
|
|
26028
|
-
`**Status:** ${task.status}`,
|
|
26029
|
-
"",
|
|
26030
|
-
"The agent will explore the codebase, implement the work item, and create a PR.",
|
|
26031
|
-
"Use `get_task_status` with the task ID to check progress and see the result (branch, PR URL, tool count, duration)."
|
|
26032
|
-
].join("\n")
|
|
26033
|
-
}]
|
|
26034
|
-
};
|
|
26035
|
-
} catch (err) {
|
|
26036
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
26037
|
-
return { content: [{ type: "text", text: `Failed to start implementation: ${message}` }], isError: true };
|
|
26038
|
-
}
|
|
26039
|
-
}
|
|
26040
|
-
);
|
|
26041
26153
|
server.tool(
|
|
26042
26154
|
"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
|
|
26155
|
+
"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 job ID \u2014 use get_job_status to track progress. Requires agentCapabilities.implementation enabled on the project.",
|
|
26044
26156
|
{
|
|
26045
26157
|
revisionId: external_exports.string().min(1).describe("The Revision ID to implement (e.g., rev-abc123)"),
|
|
26046
26158
|
projectId: external_exports.string().min(1).describe("The project ID")
|
|
@@ -26063,7 +26175,7 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
26063
26175
|
`**Status:** ${task.status}`,
|
|
26064
26176
|
"",
|
|
26065
26177
|
"The agent will read the Revision spec and beat-level Notes, implement the code, and create a draft PR.",
|
|
26066
|
-
"Use `
|
|
26178
|
+
"Use `get_job_status` with the job ID to check progress and see the result (branch, PR URL, tool count, duration)."
|
|
26067
26179
|
].join("\n")
|
|
26068
26180
|
}]
|
|
26069
26181
|
};
|
|
@@ -26073,42 +26185,9 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
26073
26185
|
}
|
|
26074
26186
|
}
|
|
26075
26187
|
);
|
|
26076
|
-
server.tool(
|
|
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.",
|
|
26079
|
-
{
|
|
26080
|
-
projectId: external_exports.string().describe("The project ID"),
|
|
26081
|
-
noteIds: external_exports.array(external_exports.string()).optional().describe("Specific note IDs to validate. If omitted, validates all active work items.")
|
|
26082
|
-
},
|
|
26083
|
-
async ({ projectId, noteIds }) => {
|
|
26084
|
-
try {
|
|
26085
|
-
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
26086
|
-
const task = await client.validateWorkItems(projectId, noteIds);
|
|
26087
|
-
return {
|
|
26088
|
-
content: [{
|
|
26089
|
-
type: "text",
|
|
26090
|
-
text: [
|
|
26091
|
-
"Work item validation queued.",
|
|
26092
|
-
"",
|
|
26093
|
-
`**Project:** ${projectId}`,
|
|
26094
|
-
`**Scope:** ${noteIds ? `${noteIds.length} specific items` : "All active work items"}`,
|
|
26095
|
-
`**Task ID:** ${task.taskId}`,
|
|
26096
|
-
`**Status:** ${task.status}`,
|
|
26097
|
-
"",
|
|
26098
|
-
"The validator will grep the codebase for evidence, then classify each item.",
|
|
26099
|
-
"Use `get_task_status` with the task ID to check progress."
|
|
26100
|
-
].join("\n")
|
|
26101
|
-
}]
|
|
26102
|
-
};
|
|
26103
|
-
} catch (err) {
|
|
26104
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
26105
|
-
return { content: [{ type: "text", text: `Failed to start validation: ${message}` }], isError: true };
|
|
26106
|
-
}
|
|
26107
|
-
}
|
|
26108
|
-
);
|
|
26109
26188
|
server.tool(
|
|
26110
26189
|
"plan_revision_batch",
|
|
26111
|
-
"Autonomously generate
|
|
26190
|
+
"Autonomously generate an implementation plan for planning Revisions. If revisionIds are omitted, discovers all unplanned planning Revisions and plans them all. Uses codebase-aware agent sessions when available, LLM-only fallback otherwise. Returns a job ID \u2014 use get_job_status to track progress.",
|
|
26112
26191
|
{
|
|
26113
26192
|
projectId: external_exports.string().describe("The project ID"),
|
|
26114
26193
|
revisionIds: external_exports.array(external_exports.string()).optional().describe("Specific revision IDs to plan. If omitted, auto-discovers all unplanned planning revisions.")
|
|
@@ -26128,8 +26207,8 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
26128
26207
|
`**Task ID:** ${task.taskId}`,
|
|
26129
26208
|
`**Status:** ${task.status}`,
|
|
26130
26209
|
"",
|
|
26131
|
-
"The agent will generate
|
|
26132
|
-
"Use `
|
|
26210
|
+
"The agent will generate an implementation plan for each revision. Revisions stay in planning \u2014 review the plan before advancing to building.",
|
|
26211
|
+
"Use `get_job_status` with the job ID to check progress."
|
|
26133
26212
|
].join("\n")
|
|
26134
26213
|
}]
|
|
26135
26214
|
};
|
|
@@ -26141,7 +26220,7 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
26141
26220
|
);
|
|
26142
26221
|
server.tool(
|
|
26143
26222
|
"run_triage",
|
|
26144
|
-
"Run the autonomous triage loop for a project. Analyzes next actions,
|
|
26223
|
+
"Run the autonomous triage loop for a project. Analyzes next actions, auto-executes safe actions (draft_coda, plan_revision_batch), and escalates items that need human judgment. Requires agentCapabilities.triage enabled on the project. Returns a job ID \u2014 use get_job_status to track progress.",
|
|
26145
26224
|
{
|
|
26146
26225
|
projectId: external_exports.string().describe("The project ID")
|
|
26147
26226
|
},
|
|
@@ -26161,7 +26240,7 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
26161
26240
|
"",
|
|
26162
26241
|
"The triage engine will analyze next actions, filter by Beat lifecycle eligibility,",
|
|
26163
26242
|
"auto-execute safe tasks, and escalate items needing human input.",
|
|
26164
|
-
"Use `
|
|
26243
|
+
"Use `get_job_status` with the job ID to see the results."
|
|
26165
26244
|
].join("\n")
|
|
26166
26245
|
}]
|
|
26167
26246
|
};
|
|
@@ -26183,7 +26262,7 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
26183
26262
|
"Use this for .docx, .xlsx, .pptx, or .pdf files \u2014 the server parses them automatically.",
|
|
26184
26263
|
"Example: run `base64 < report.docx` to get the base64 string.",
|
|
26185
26264
|
"",
|
|
26186
|
-
"Returns a
|
|
26265
|
+
"Returns a job ID \u2014 use `get_job_status` to track progress."
|
|
26187
26266
|
].join("\n"),
|
|
26188
26267
|
{
|
|
26189
26268
|
projectId: external_exports.string().describe("The project ID"),
|
|
@@ -26206,7 +26285,7 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
26206
26285
|
`**Task ID:** ${result.taskId}`,
|
|
26207
26286
|
`**Status:** ${result.status}`,
|
|
26208
26287
|
"",
|
|
26209
|
-
"Use `
|
|
26288
|
+
"Use `get_job_status` with the job ID to check when extraction is complete."
|
|
26210
26289
|
].join("\n")
|
|
26211
26290
|
}]
|
|
26212
26291
|
};
|
|
@@ -26219,7 +26298,58 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
26219
26298
|
}
|
|
26220
26299
|
|
|
26221
26300
|
// ../../libs/harmonica-services/src/mcp/tools/index.ts
|
|
26222
|
-
|
|
26301
|
+
var TOOL_PROFILES = Object.freeze({
|
|
26302
|
+
"beat-planning": Object.freeze([
|
|
26303
|
+
registerBeatTools,
|
|
26304
|
+
registerBeatVersionTools,
|
|
26305
|
+
registerNoteTools,
|
|
26306
|
+
registerBeatQualityTools,
|
|
26307
|
+
registerPlanQualityTools,
|
|
26308
|
+
registerBeatPlanningTools,
|
|
26309
|
+
registerCheckTools
|
|
26310
|
+
]),
|
|
26311
|
+
"execution": Object.freeze([
|
|
26312
|
+
registerRevisionLifecycleTools,
|
|
26313
|
+
registerBeatLifecycleTools,
|
|
26314
|
+
registerBeatVersionTools,
|
|
26315
|
+
registerDropTools,
|
|
26316
|
+
registerWorkflowTools,
|
|
26317
|
+
registerCheckTools
|
|
26318
|
+
]),
|
|
26319
|
+
"discovery": Object.freeze([
|
|
26320
|
+
registerEmbeddingTools,
|
|
26321
|
+
registerNextActionsTools,
|
|
26322
|
+
registerBeatTools,
|
|
26323
|
+
registerNoteTools,
|
|
26324
|
+
registerProjectTools,
|
|
26325
|
+
registerAnalysisTools
|
|
26326
|
+
]),
|
|
26327
|
+
"admin": Object.freeze([
|
|
26328
|
+
registerOrganizationTools,
|
|
26329
|
+
registerTeamspaceTools,
|
|
26330
|
+
registerMembershipTools,
|
|
26331
|
+
registerProjectTools,
|
|
26332
|
+
registerProjectLifecycleTools
|
|
26333
|
+
]),
|
|
26334
|
+
"onboarding": Object.freeze([
|
|
26335
|
+
registerOnboardingTools,
|
|
26336
|
+
registerBaselineTools,
|
|
26337
|
+
registerSnapshotTools,
|
|
26338
|
+
registerBeatComposerTools,
|
|
26339
|
+
registerProjectTools
|
|
26340
|
+
])
|
|
26341
|
+
});
|
|
26342
|
+
function registerAllTools(server, ctx, client, profile) {
|
|
26343
|
+
if (profile && profile !== "all") {
|
|
26344
|
+
if (!Object.hasOwn(TOOL_PROFILES, profile)) {
|
|
26345
|
+
const valid = Object.keys(TOOL_PROFILES).join(", ");
|
|
26346
|
+
throw new Error(`Unknown tool profile: "${profile}". Valid profiles: ${valid}`);
|
|
26347
|
+
}
|
|
26348
|
+
for (const register of TOOL_PROFILES[profile]) {
|
|
26349
|
+
register(server, ctx, client);
|
|
26350
|
+
}
|
|
26351
|
+
return;
|
|
26352
|
+
}
|
|
26223
26353
|
registerProjectTools(server, ctx, client);
|
|
26224
26354
|
registerProjectLifecycleTools(server, ctx, client);
|
|
26225
26355
|
registerMembershipTools(server, ctx, client);
|
|
@@ -26321,6 +26451,34 @@ function registerAllResources(server, ctx, client) {
|
|
|
26321
26451
|
}
|
|
26322
26452
|
|
|
26323
26453
|
// ../../apps/harmonica-mcp-server/src/data-client/http-client.ts
|
|
26454
|
+
var STREAM_EVENT_KINDS = /* @__PURE__ */ new Set([
|
|
26455
|
+
"snapshot",
|
|
26456
|
+
"tool_event",
|
|
26457
|
+
"text_event",
|
|
26458
|
+
"metrics_update",
|
|
26459
|
+
"status_change",
|
|
26460
|
+
"terminal"
|
|
26461
|
+
]);
|
|
26462
|
+
function parseSseBlock(block) {
|
|
26463
|
+
let event = "message";
|
|
26464
|
+
let data = "";
|
|
26465
|
+
for (const line of block.split("\n")) {
|
|
26466
|
+
if (line.startsWith("event: ")) event = line.slice("event: ".length).trim();
|
|
26467
|
+
else if (line.startsWith("data: ")) data += (data ? "\n" : "") + line.slice("data: ".length);
|
|
26468
|
+
}
|
|
26469
|
+
if (!data) return void 0;
|
|
26470
|
+
return { event, data };
|
|
26471
|
+
}
|
|
26472
|
+
function toStreamRecord(event, dataStr) {
|
|
26473
|
+
if (!STREAM_EVENT_KINDS.has(event)) return void 0;
|
|
26474
|
+
let payload;
|
|
26475
|
+
try {
|
|
26476
|
+
payload = JSON.parse(dataStr);
|
|
26477
|
+
} catch {
|
|
26478
|
+
return void 0;
|
|
26479
|
+
}
|
|
26480
|
+
return { kind: event, payload };
|
|
26481
|
+
}
|
|
26324
26482
|
var ApiError = class extends Error {
|
|
26325
26483
|
constructor(status, body) {
|
|
26326
26484
|
super(`API ${status}: ${body}`);
|
|
@@ -26658,26 +26816,6 @@ function createHttpClient(config2) {
|
|
|
26658
26816
|
getNextNoteId: async (projectCode) => {
|
|
26659
26817
|
return `N-${projectCode}-${Date.now()}`;
|
|
26660
26818
|
},
|
|
26661
|
-
saveTasksAsWorkItemNotes: async (projectId, beatId, tasks) => {
|
|
26662
|
-
const results = await Promise.all(
|
|
26663
|
-
tasks.map(
|
|
26664
|
-
(task) => request("POST", `/api/projects/${encodeURIComponent(projectId)}/notes`, {
|
|
26665
|
-
noteType: "workItem",
|
|
26666
|
-
content: `**${task.title}** \u2014 ${task.description}`,
|
|
26667
|
-
beatId,
|
|
26668
|
-
workItemMeta: {
|
|
26669
|
-
category: task.category,
|
|
26670
|
-
complexity: task.complexity,
|
|
26671
|
-
scope: task.scope,
|
|
26672
|
-
estimatedHoursRange: task.estimatedHoursRange,
|
|
26673
|
-
acceptanceCheck: task.acceptanceCheck,
|
|
26674
|
-
whyOutOfScope: task.whyOutOfScope
|
|
26675
|
-
}
|
|
26676
|
-
})
|
|
26677
|
-
)
|
|
26678
|
-
);
|
|
26679
|
-
return results.map((r) => r?.note ?? r);
|
|
26680
|
-
},
|
|
26681
26819
|
reassignNote: async (noteId, targetBeatId, reason, _actorId, targetRevisionId) => {
|
|
26682
26820
|
const note = await request("GET", `/api/notes/${encodeURIComponent(noteId)}`);
|
|
26683
26821
|
if (!note?.note) return void 0;
|
|
@@ -26758,7 +26896,8 @@ function createHttpClient(config2) {
|
|
|
26758
26896
|
actorName: options.actor.name,
|
|
26759
26897
|
trigger: options.trigger,
|
|
26760
26898
|
reason: options.reason,
|
|
26761
|
-
metadata: options.metadata
|
|
26899
|
+
metadata: options.metadata,
|
|
26900
|
+
...options.mergeCommitSha !== void 0 && { mergeCommitSha: options.mergeCommitSha }
|
|
26762
26901
|
}
|
|
26763
26902
|
);
|
|
26764
26903
|
if (raw === void 0) {
|
|
@@ -26827,6 +26966,164 @@ function createHttpClient(config2) {
|
|
|
26827
26966
|
return result ?? [];
|
|
26828
26967
|
},
|
|
26829
26968
|
cancelSession: (sessionId, options) => request("POST", `/api/sessions/${encodeURIComponent(sessionId)}/cancel`, { reason: options.reason }),
|
|
26969
|
+
sendSessionMessage: async (sessionId, options) => {
|
|
26970
|
+
try {
|
|
26971
|
+
const result = await request("POST", `/api/sessions/${encodeURIComponent(sessionId)}/messages`, {
|
|
26972
|
+
body: options.body,
|
|
26973
|
+
enqueuedBy: options.enqueuedBy
|
|
26974
|
+
});
|
|
26975
|
+
if (!result) return { status: "not_found" };
|
|
26976
|
+
return { status: "enqueued", message: result };
|
|
26977
|
+
} catch (err) {
|
|
26978
|
+
if (err instanceof ApiError && err.status === 409) {
|
|
26979
|
+
const parsed = safeParseErrorBody(err.body);
|
|
26980
|
+
if (parsed?.sessionStatus) {
|
|
26981
|
+
return { status: "not_accepting", sessionStatus: parsed.sessionStatus };
|
|
26982
|
+
}
|
|
26983
|
+
}
|
|
26984
|
+
throw err;
|
|
26985
|
+
}
|
|
26986
|
+
},
|
|
26987
|
+
resumeSession: async (sessionId) => {
|
|
26988
|
+
try {
|
|
26989
|
+
const result = await request("POST", `/api/sessions/${encodeURIComponent(sessionId)}/resume`);
|
|
26990
|
+
if (!result) return { status: "not_found" };
|
|
26991
|
+
return { status: "resumed", result };
|
|
26992
|
+
} catch (err) {
|
|
26993
|
+
if (err instanceof ApiError && err.status === 409) {
|
|
26994
|
+
const parsed = safeParseErrorBody(err.body);
|
|
26995
|
+
if (parsed?.code === "SESSION_NOT_RESUMABLE") {
|
|
26996
|
+
return {
|
|
26997
|
+
status: "not_resumable",
|
|
26998
|
+
sessionStatus: parsed.sessionStatus ?? "held",
|
|
26999
|
+
reason: parsed.reason ?? "not_held"
|
|
27000
|
+
};
|
|
27001
|
+
}
|
|
27002
|
+
}
|
|
27003
|
+
throw err;
|
|
27004
|
+
}
|
|
27005
|
+
},
|
|
27006
|
+
streamSessionEvents: async (sessionId, options) => {
|
|
27007
|
+
const maxEvents = Math.min(Math.max(options?.maxEvents ?? 50, 1), 200);
|
|
27008
|
+
const maxWaitMs = Math.min(Math.max(options?.maxWaitMs ?? 5e3, 100), 25e3);
|
|
27009
|
+
const params = new URLSearchParams();
|
|
27010
|
+
if (options?.sinceEventIndex !== void 0) {
|
|
27011
|
+
params.set("since_event_index", String(options.sinceEventIndex));
|
|
27012
|
+
}
|
|
27013
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
27014
|
+
const url2 = `${config2.apiBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}/events${qs}`;
|
|
27015
|
+
const controller = new AbortController();
|
|
27016
|
+
const deadlineTimer = setTimeout(() => controller.abort(), maxWaitMs);
|
|
27017
|
+
let res;
|
|
27018
|
+
try {
|
|
27019
|
+
res = await fetch(url2, {
|
|
27020
|
+
method: "GET",
|
|
27021
|
+
headers: {
|
|
27022
|
+
Authorization: `Bearer ${config2.apiKey}`,
|
|
27023
|
+
Accept: "text/event-stream"
|
|
27024
|
+
},
|
|
27025
|
+
signal: controller.signal
|
|
27026
|
+
});
|
|
27027
|
+
} catch (err) {
|
|
27028
|
+
clearTimeout(deadlineTimer);
|
|
27029
|
+
if (err.name === "AbortError" && controller.signal.aborted) {
|
|
27030
|
+
return { status: "ok", events: [], isTerminal: false, reason: "max_wait", ...options?.sinceEventIndex !== void 0 && { nextSinceEventIndex: options.sinceEventIndex } };
|
|
27031
|
+
}
|
|
27032
|
+
throw err;
|
|
27033
|
+
}
|
|
27034
|
+
if (res.status === 404) {
|
|
27035
|
+
clearTimeout(deadlineTimer);
|
|
27036
|
+
return { status: "not_found" };
|
|
27037
|
+
}
|
|
27038
|
+
if (!res.ok) {
|
|
27039
|
+
clearTimeout(deadlineTimer);
|
|
27040
|
+
const text = await res.text();
|
|
27041
|
+
throw new ApiError(res.status, text);
|
|
27042
|
+
}
|
|
27043
|
+
if (!res.body) {
|
|
27044
|
+
clearTimeout(deadlineTimer);
|
|
27045
|
+
return { status: "ok", events: [], isTerminal: false, reason: "max_wait", ...options?.sinceEventIndex !== void 0 && { nextSinceEventIndex: options.sinceEventIndex } };
|
|
27046
|
+
}
|
|
27047
|
+
const reader = res.body.getReader();
|
|
27048
|
+
const decoder = new TextDecoder("utf-8");
|
|
27049
|
+
let buffer = "";
|
|
27050
|
+
const events = [];
|
|
27051
|
+
let nextSinceEventIndex = options?.sinceEventIndex;
|
|
27052
|
+
let isTerminal2 = false;
|
|
27053
|
+
let reason = "max_wait";
|
|
27054
|
+
const deadline = Date.now() + maxWaitMs;
|
|
27055
|
+
const TIMEOUT = Symbol("stream-timeout");
|
|
27056
|
+
try {
|
|
27057
|
+
outer: while (true) {
|
|
27058
|
+
const remaining = deadline - Date.now();
|
|
27059
|
+
if (remaining <= 0) {
|
|
27060
|
+
reason = "max_wait";
|
|
27061
|
+
break outer;
|
|
27062
|
+
}
|
|
27063
|
+
let timerHandle;
|
|
27064
|
+
const raced = await Promise.race([
|
|
27065
|
+
reader.read(),
|
|
27066
|
+
new Promise((resolve) => {
|
|
27067
|
+
timerHandle = setTimeout(() => resolve(TIMEOUT), remaining);
|
|
27068
|
+
})
|
|
27069
|
+
]);
|
|
27070
|
+
if (timerHandle) clearTimeout(timerHandle);
|
|
27071
|
+
if (raced === TIMEOUT) {
|
|
27072
|
+
reason = "max_wait";
|
|
27073
|
+
break outer;
|
|
27074
|
+
}
|
|
27075
|
+
const { done, value } = raced;
|
|
27076
|
+
if (done) break;
|
|
27077
|
+
buffer += decoder.decode(value, { stream: true });
|
|
27078
|
+
let sepIdx;
|
|
27079
|
+
while ((sepIdx = buffer.indexOf("\n\n")) !== -1) {
|
|
27080
|
+
const block = buffer.slice(0, sepIdx);
|
|
27081
|
+
buffer = buffer.slice(sepIdx + 2);
|
|
27082
|
+
const parsed = parseSseBlock(block);
|
|
27083
|
+
if (!parsed) continue;
|
|
27084
|
+
if (parsed.event === "heartbeat") continue;
|
|
27085
|
+
const record2 = toStreamRecord(parsed.event, parsed.data);
|
|
27086
|
+
if (!record2) continue;
|
|
27087
|
+
events.push(record2);
|
|
27088
|
+
if (record2.kind === "tool_event" || record2.kind === "text_event") {
|
|
27089
|
+
const idx = record2.payload.index;
|
|
27090
|
+
if (typeof idx === "number" && (nextSinceEventIndex === void 0 || idx > nextSinceEventIndex)) {
|
|
27091
|
+
nextSinceEventIndex = idx;
|
|
27092
|
+
}
|
|
27093
|
+
}
|
|
27094
|
+
if (record2.kind === "terminal") {
|
|
27095
|
+
isTerminal2 = true;
|
|
27096
|
+
reason = "terminal";
|
|
27097
|
+
break outer;
|
|
27098
|
+
}
|
|
27099
|
+
if (events.length >= maxEvents) {
|
|
27100
|
+
reason = "max_events";
|
|
27101
|
+
break outer;
|
|
27102
|
+
}
|
|
27103
|
+
}
|
|
27104
|
+
}
|
|
27105
|
+
} catch (err) {
|
|
27106
|
+
if (!controller.signal.aborted) throw err;
|
|
27107
|
+
reason = "max_wait";
|
|
27108
|
+
} finally {
|
|
27109
|
+
clearTimeout(deadlineTimer);
|
|
27110
|
+
try {
|
|
27111
|
+
await reader.cancel();
|
|
27112
|
+
} catch {
|
|
27113
|
+
}
|
|
27114
|
+
try {
|
|
27115
|
+
controller.abort();
|
|
27116
|
+
} catch {
|
|
27117
|
+
}
|
|
27118
|
+
}
|
|
27119
|
+
return {
|
|
27120
|
+
status: "ok",
|
|
27121
|
+
events,
|
|
27122
|
+
isTerminal: isTerminal2,
|
|
27123
|
+
reason,
|
|
27124
|
+
...nextSinceEventIndex !== void 0 && { nextSinceEventIndex }
|
|
27125
|
+
};
|
|
27126
|
+
},
|
|
26830
27127
|
// Tasks
|
|
26831
27128
|
getTask: async (taskId) => {
|
|
26832
27129
|
return request("GET", `/api/tasks/${encodeURIComponent(taskId)}`);
|
|
@@ -26965,15 +27262,6 @@ function createHttpClient(config2) {
|
|
|
26965
27262
|
return pollTaskResult(enqueued.taskId);
|
|
26966
27263
|
},
|
|
26967
27264
|
// Agent sessions
|
|
26968
|
-
implementWorkItem: async (noteId, projectId, triggeredBy) => {
|
|
26969
|
-
const result = await request(
|
|
26970
|
-
"POST",
|
|
26971
|
-
`/api/projects/${encodeURIComponent(projectId)}/notes/${encodeURIComponent(noteId)}/implement`,
|
|
26972
|
-
triggeredBy ? { triggeredBy } : {}
|
|
26973
|
-
);
|
|
26974
|
-
if (!result?.taskId) throw new Error("Implementation enqueue failed: no taskId returned");
|
|
26975
|
-
return { taskId: result.taskId, taskType: "work_item_implementation", status: result.status ?? "pending", projectId, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
26976
|
-
},
|
|
26977
27265
|
implementRevision: async (revisionId, projectId, triggeredBy) => {
|
|
26978
27266
|
const result = await request(
|
|
26979
27267
|
"POST",
|
|
@@ -26983,15 +27271,6 @@ function createHttpClient(config2) {
|
|
|
26983
27271
|
if (!result?.taskId) throw new Error("Revision implementation enqueue failed: no taskId returned");
|
|
26984
27272
|
return { taskId: result.taskId, taskType: "revision_implementation", status: result.status ?? "pending", projectId, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
26985
27273
|
},
|
|
26986
|
-
validateWorkItems: async (projectId, noteIds) => {
|
|
26987
|
-
const result = await request(
|
|
26988
|
-
"POST",
|
|
26989
|
-
`/api/projects/${encodeURIComponent(projectId)}/work-items/validate`,
|
|
26990
|
-
noteIds ? { noteIds } : {}
|
|
26991
|
-
);
|
|
26992
|
-
if (!result?.taskId) throw new Error("Validation enqueue failed: no taskId returned");
|
|
26993
|
-
return { taskId: result.taskId, taskType: "work_item_validation", status: result.status ?? "pending", projectId, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
26994
|
-
},
|
|
26995
27274
|
planRevisionBatch: async (projectId, revisionIds) => {
|
|
26996
27275
|
const result = await request(
|
|
26997
27276
|
"POST",
|
|
@@ -27171,6 +27450,14 @@ function createHttpClient(config2) {
|
|
|
27171
27450
|
);
|
|
27172
27451
|
return res.checks;
|
|
27173
27452
|
},
|
|
27453
|
+
listDropChecks: async (dropId, _projectId, checkType) => {
|
|
27454
|
+
const qs = checkType ? `?type=${encodeURIComponent(checkType)}` : "";
|
|
27455
|
+
const res = await request(
|
|
27456
|
+
"GET",
|
|
27457
|
+
`/api/drops/${encodeURIComponent(dropId)}/checks${qs}`
|
|
27458
|
+
);
|
|
27459
|
+
return res.checks;
|
|
27460
|
+
},
|
|
27174
27461
|
addCheckFeedback: async (checkId, feedback) => {
|
|
27175
27462
|
const res = await request(
|
|
27176
27463
|
"POST",
|
|
@@ -27305,7 +27592,11 @@ function createHttpClient(config2) {
|
|
|
27305
27592
|
if (res === void 0) {
|
|
27306
27593
|
return { success: false, error: { code: "BEAT_VERSION_NOT_FOUND", message: "Beat version not found" } };
|
|
27307
27594
|
}
|
|
27308
|
-
return {
|
|
27595
|
+
return {
|
|
27596
|
+
success: true,
|
|
27597
|
+
...res.fanOutCount !== void 0 && { fanOutCount: res.fanOutCount },
|
|
27598
|
+
...res.fanOutWarning && { fanOutWarning: res.fanOutWarning }
|
|
27599
|
+
};
|
|
27309
27600
|
} catch (err) {
|
|
27310
27601
|
const message = err instanceof Error ? err.message : String(err);
|
|
27311
27602
|
return { success: false, error: { code: "TRANSITION_FAILED", message } };
|
|
@@ -27406,7 +27697,7 @@ function loadConfig() {
|
|
|
27406
27697
|
};
|
|
27407
27698
|
}
|
|
27408
27699
|
async function main() {
|
|
27409
|
-
console.error(`[harmonica-mcp] v${"0.
|
|
27700
|
+
console.error(`[harmonica-mcp] v${"0.30.0"} starting\u2026`);
|
|
27410
27701
|
const config2 = loadConfig();
|
|
27411
27702
|
const client = createHttpClient({
|
|
27412
27703
|
apiBaseUrl: config2.apiBaseUrl,
|