@gethmy/mcp 2.24.0 → 2.25.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 CHANGED
@@ -354,15 +354,7 @@ ${lines.join(`
354
354
  `)}`;
355
355
  }
356
356
  function generatePrompt(options) {
357
- const {
358
- card,
359
- column,
360
- variant,
361
- customConstraints,
362
- memories,
363
- assembledContext,
364
- assemblyId
365
- } = options;
357
+ const { card, column, variant, customConstraints, memories, assemblyId } = options;
366
358
  const contextOpts = {
367
359
  includeTitle: true,
368
360
  includeDescription: true,
@@ -440,10 +432,7 @@ ${card.description}`);
440
432
  roleFraming.outputSuggestions.forEach((s) => {
441
433
  sections.push(`- ${s}`);
442
434
  });
443
- if (assembledContext) {
444
- sections.push(`
445
- ${assembledContext}`);
446
- } else if (memories && memories.length > 0) {
435
+ if (memories && memories.length > 0) {
447
436
  sections.push(`
448
437
  ## Relevant Memories`);
449
438
  sections.push(`*${memories.length} memories recalled from knowledge graph:*`);
@@ -454,7 +443,7 @@ ${assembledContext}`);
454
443
  sections.push(memory.content);
455
444
  }
456
445
  }
457
- const oneThingLine = synthesizeOneThing(card, subtasks, links, assembledContext);
446
+ const oneThingLine = synthesizeOneThing(card, subtasks, links);
458
447
  if (oneThingLine) {
459
448
  sections.push(`
460
449
  ## Recommended Next Step
@@ -481,7 +470,7 @@ ${customConstraints}`);
481
470
  *Card #${card.short_id} | Generated for ${variant} mode*`);
482
471
  const prompt = sections.join(`
483
472
  `);
484
- const memoryCount = assembledContext ? (assembledContext.match(/^### /gm) || []).length : memories?.length || 0;
473
+ const memoryCount = memories?.length ?? 0;
485
474
  return {
486
475
  prompt,
487
476
  variant,
@@ -502,40 +491,7 @@ ${customConstraints}`);
502
491
  version: PROMPT_TEMPLATE_VERSION
503
492
  };
504
493
  }
505
- function extractSessionInsights(assembledContext) {
506
- const result = {
507
- lastSessionStatus: null,
508
- lastSessionTask: null,
509
- lastSessionProgress: null,
510
- blockers: [],
511
- procedureNextStep: null
512
- };
513
- const sessionMatches = assembledContext.match(/### Session:.*?\n([\s\S]*?)(?=\n###|\n## |\n---|\n\*Assembly|$)/g);
514
- if (sessionMatches && sessionMatches.length > 0) {
515
- const latest = sessionMatches[0];
516
- if (/Completed work on/i.test(latest)) {
517
- result.lastSessionStatus = "completed";
518
- } else if (/Paused work on|status:\s*paused/i.test(latest)) {
519
- result.lastSessionStatus = "paused";
520
- }
521
- const taskMatch = latest.match(/Final task:\s*(.+)/);
522
- if (taskMatch)
523
- result.lastSessionTask = taskMatch[1].trim();
524
- const progressMatch = latest.match(/Progress:\s*(\d+)%/);
525
- if (progressMatch)
526
- result.lastSessionProgress = parseInt(progressMatch[1], 10);
527
- }
528
- const blockerMatches = assembledContext.match(/(?:blocker|blocked by|blocking):\s*(.+)/gi);
529
- if (blockerMatches) {
530
- result.blockers = blockerMatches.map((m) => m.replace(/(?:blocker|blocked by|blocking):\s*/i, "").trim());
531
- }
532
- const stepMatches = assembledContext.match(/^\d+\.\s+(?!.*\*\*\[key step\]\*\*.*✓)(.+?)(?:\s*\*\*\[key step\]\*\*)?$/gm);
533
- if (stepMatches && stepMatches.length > 0) {
534
- result.procedureNextStep = stepMatches[0].replace(/^\d+\.\s+/, "").replace(/\s*\*\*\[key step\]\*\*.*$/, "").trim();
535
- }
536
- return result;
537
- }
538
- function synthesizeOneThing(card, subtasks, links, assembledContext) {
494
+ function synthesizeOneThing(card, subtasks, links) {
539
495
  if (card.done)
540
496
  return null;
541
497
  const blockers = links.filter((l) => l.display_type === "is_blocked_by" && l.direction === "incoming");
@@ -543,14 +499,6 @@ function synthesizeOneThing(card, subtasks, links, assembledContext) {
543
499
  const blocker = blockers[0];
544
500
  return `Unblock first: resolve #${blocker.target_card.short_id} "${blocker.target_card.title}" which is blocking this card.`;
545
501
  }
546
- const session = assembledContext ? extractSessionInsights(assembledContext) : null;
547
- if (session?.blockers && session.blockers.length > 0) {
548
- return `Resolve blocker: ${session.blockers[0]}`;
549
- }
550
- if (session?.lastSessionStatus === "paused" && session.lastSessionTask) {
551
- const progress = session.lastSessionProgress ? ` (was ${session.lastSessionProgress}% complete)` : "";
552
- return `Resume previous session${progress}: "${session.lastSessionTask}".`;
553
- }
554
502
  if (subtasks.length > 0) {
555
503
  const completed = subtasks.filter((s) => s.completed).length;
556
504
  if (completed === subtasks.length) {
@@ -561,12 +509,6 @@ function synthesizeOneThing(card, subtasks, links, assembledContext) {
561
509
  return `Work on next subtask: "${nextSubtask.title}" (${completed}/${subtasks.length} done).`;
562
510
  }
563
511
  }
564
- if (session?.procedureNextStep) {
565
- return `Follow procedure: ${session.procedureNextStep}`;
566
- }
567
- if (session?.lastSessionStatus === "completed" && session.lastSessionTask) {
568
- return `Previous session completed ("${session.lastSessionTask}"). Review results and continue with remaining work.`;
569
- }
570
512
  if (card.due_date && (card.priority === "urgent" || card.priority === "high")) {
571
513
  return `High-priority task with deadline ${card.due_date}. Start implementation immediately.`;
572
514
  }
@@ -1433,6 +1375,9 @@ import {
1433
1375
  ReadResourceRequestSchema
1434
1376
  } from "@modelcontextprotocol/sdk/types.js";
1435
1377
  import { z } from "zod";
1378
+
1379
+ // src/api-client.ts
1380
+ import { randomUUID as randomUUID2 } from "node:crypto";
1436
1381
  // ../harmony-shared/dist/agentStaleness.js
1437
1382
  var AGENT_HEARTBEAT_LIVENESS_MS = 5 * 60 * 1000;
1438
1383
  var AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
@@ -1554,12 +1499,97 @@ var TIMINGS = {
1554
1499
  QUERY_STALE_TIME: 1000 * 60 * 5,
1555
1500
  QUERY_GC_TIME: 1000 * 60 * 60 * 24
1556
1501
  };
1502
+ // ../harmony-shared/dist/declaredGateMetrics.js
1503
+ function declaredGateMetricsFromAgents(agents) {
1504
+ const names = new Set;
1505
+ let known = false;
1506
+ for (const agent of agents) {
1507
+ const declared = agent.declared_gate_metrics;
1508
+ if (!Array.isArray(declared))
1509
+ continue;
1510
+ known = true;
1511
+ for (const name of declared) {
1512
+ if (typeof name === "string" && name.trim())
1513
+ names.add(name.trim());
1514
+ }
1515
+ }
1516
+ return { names, known };
1517
+ }
1518
+ // ../harmony-shared/dist/gateConfigError.js
1519
+ var GATE_CONFIG_ERROR_KEY = "configError";
1520
+ var GATE_CONFIG_ERROR_MARK = Object.freeze({ [GATE_CONFIG_ERROR_KEY]: true });
1557
1521
  // ../harmony-shared/dist/playbookStage.js
1522
+ var DEFAULT_LOOP_MAX_ITERATIONS = 5;
1523
+ function normalizeLoopDef(raw) {
1524
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
1525
+ return null;
1526
+ const obj = raw;
1527
+ if (obj.mode !== "converge" && obj.mode !== "fanout")
1528
+ return null;
1529
+ const mode = obj.mode;
1530
+ const rawMax = obj.max_iterations;
1531
+ const maxInt = typeof rawMax === "number" && Number.isFinite(rawMax) && rawMax >= 1 ? Math.floor(rawMax) : DEFAULT_LOOP_MAX_ITERATIONS;
1532
+ const exitGate = obj.exit_gate && typeof obj.exit_gate === "object" && !Array.isArray(obj.exit_gate) ? obj.exit_gate : null;
1533
+ const def = { mode, max_iterations: maxInt };
1534
+ if (exitGate)
1535
+ def.exit_gate = exitGate;
1536
+ if (obj.item_source && typeof obj.item_source === "object" && !Array.isArray(obj.item_source)) {
1537
+ def.item_source = obj.item_source;
1538
+ }
1539
+ if (typeof obj.concurrency === "number" && obj.concurrency >= 1) {
1540
+ def.concurrency = Math.floor(obj.concurrency);
1541
+ }
1542
+ if (obj.on_item_fail === "continue" || obj.on_item_fail === "halt") {
1543
+ def.on_item_fail = obj.on_item_fail;
1544
+ }
1545
+ return def;
1546
+ }
1547
+ function readStageDefs(def) {
1548
+ if (def.steps_version !== 2)
1549
+ return [];
1550
+ return Array.isArray(def.steps) ? def.steps : [];
1551
+ }
1558
1552
  var STAGE_DAEMON_OWNED_TOOLS = [
1559
1553
  "mcp__harmony__harmony_end_agent_session",
1560
1554
  "mcp__harmony__harmony_start_agent_session",
1561
1555
  "mcp__harmony__harmony_move_card"
1562
1556
  ];
1557
+ function customGateMetric(gate) {
1558
+ if (gate === null || typeof gate !== "object" || Array.isArray(gate)) {
1559
+ return null;
1560
+ }
1561
+ const record = gate;
1562
+ if (record.kind !== "custom")
1563
+ return null;
1564
+ if (record.pendingEngine === true)
1565
+ return null;
1566
+ const metric = typeof record.metric === "string" ? record.metric.trim() : "";
1567
+ return metric ? metric : null;
1568
+ }
1569
+ function referencedGateMetrics(def) {
1570
+ const out = [];
1571
+ for (const stage of readStageDefs(def)) {
1572
+ if (!stage || typeof stage !== "object")
1573
+ continue;
1574
+ const stageId = typeof stage.id === "string" ? stage.id : "";
1575
+ const stageName = typeof stage.name === "string" ? stage.name : stageId;
1576
+ const gateMetric = customGateMetric(stage.gate);
1577
+ if (gateMetric) {
1578
+ out.push({ stageId, stageName, metric: gateMetric, source: "gate" });
1579
+ }
1580
+ const loop = normalizeLoopDef(stage.loop);
1581
+ const loopMetric = loop?.exit_gate ? customGateMetric(loop.exit_gate) : null;
1582
+ if (loopMetric) {
1583
+ out.push({
1584
+ stageId,
1585
+ stageName,
1586
+ metric: loopMetric,
1587
+ source: "loop_exit_gate"
1588
+ });
1589
+ }
1590
+ }
1591
+ return out;
1592
+ }
1563
1593
  // ../harmony-shared/dist/reviewTools.js
1564
1594
  var REVIEW_DISALLOWED_TOOLS = [
1565
1595
  ...STAGE_DAEMON_OWNED_TOOLS,
@@ -1594,6 +1624,19 @@ function getRetryDelay(attempt) {
1594
1624
  return Math.round(delay + delay * 0.25 * (Math.random() * 2 - 1));
1595
1625
  }
1596
1626
  var sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1627
+ function buildMemoryQuery(title, description) {
1628
+ const DESCRIPTION_CAP = 600;
1629
+ const trimmedTitle = title.trim();
1630
+ const trimmedBody = (description ?? "").trim();
1631
+ if (!trimmedTitle && !trimmedBody)
1632
+ return "";
1633
+ if (!trimmedTitle)
1634
+ return trimmedBody.slice(0, DESCRIPTION_CAP);
1635
+ if (!trimmedBody)
1636
+ return trimmedTitle;
1637
+ return `${trimmedTitle}
1638
+ ${trimmedBody.slice(0, DESCRIPTION_CAP)}`;
1639
+ }
1597
1640
 
1598
1641
  class Semaphore {
1599
1642
  permits;
@@ -2064,6 +2107,14 @@ class HarmonyApiClient {
2064
2107
  params.set("sinceSeq", String(sinceSeq));
2065
2108
  return this.request("GET", `/cards/${cardId}/agent-messages?${params.toString()}`);
2066
2109
  }
2110
+ async postBudgetDecision(cardId, data) {
2111
+ return this.request("POST", `/cards/${cardId}/budget-decisions`, data);
2112
+ }
2113
+ async getBudgetDecisions(cardId, sinceIso) {
2114
+ const params = new URLSearchParams;
2115
+ params.set("sinceIso", sinceIso);
2116
+ return this.request("GET", `/cards/${cardId}/budget-decisions?${params.toString()}`);
2117
+ }
2067
2118
  async updateAgentProgress(cardId, data) {
2068
2119
  return this.request("POST", `/cards/${cardId}/agent-context`, data);
2069
2120
  }
@@ -2109,6 +2160,8 @@ class HarmonyApiClient {
2109
2160
  params.set("offset", String(options.offset));
2110
2161
  if (options.include_superseded)
2111
2162
  params.set("include_superseded", "true");
2163
+ if (options.consumer)
2164
+ params.set("consumer", options.consumer);
2112
2165
  if (options.include_episodes)
2113
2166
  params.set("include_episodes", "true");
2114
2167
  return this.request("GET", `/memory/entities?${params.toString()}`);
@@ -2164,6 +2217,12 @@ class HarmonyApiClient {
2164
2217
  if (options.topK !== undefined) {
2165
2218
  entities = entities.slice(0, options.topK);
2166
2219
  }
2220
+ if (options.consumer) {
2221
+ const deliveredIds = entities.map((e) => e.id).filter((id) => typeof id === "string");
2222
+ if (deliveredIds.length > 0) {
2223
+ this.batchTouchMemoryEntities(deliveredIds, options.consumer).catch(() => {});
2224
+ }
2225
+ }
2167
2226
  return { entities };
2168
2227
  }
2169
2228
  async deleteMemoryEntity(entityId) {
@@ -2172,9 +2231,10 @@ class HarmonyApiClient {
2172
2231
  async touchMemoryEntity(entityId) {
2173
2232
  return this.request("POST", `/memory/entities/${entityId}/touch`);
2174
2233
  }
2175
- async batchTouchMemoryEntities(entityIds) {
2234
+ async batchTouchMemoryEntities(entityIds, consumer) {
2176
2235
  return this.request("POST", "/memory/entities/batch-touch", {
2177
- entity_ids: entityIds
2236
+ entity_ids: entityIds,
2237
+ ...consumer ? { consumer } : {}
2178
2238
  });
2179
2239
  }
2180
2240
  async createMemoryRelation(data) {
@@ -2200,8 +2260,12 @@ class HarmonyApiClient {
2200
2260
  params.append("tags", tag);
2201
2261
  if (options?.include_superseded)
2202
2262
  params.set("include_superseded", "true");
2263
+ if (options?.consumer)
2264
+ params.set("consumer", options.consumer);
2203
2265
  if (options?.include_episodes)
2204
2266
  params.set("include_episodes", "true");
2267
+ if (options?.assembly_id)
2268
+ params.set("assembly_id", options.assembly_id);
2205
2269
  return this.request("GET", `/memory/search?${params.toString()}`);
2206
2270
  }
2207
2271
  async getVaultIndex(options) {
@@ -2213,6 +2277,8 @@ class HarmonyApiClient {
2213
2277
  params.set("type", options.type);
2214
2278
  if (options.limit !== undefined)
2215
2279
  params.set("limit", String(options.limit));
2280
+ if (options.consumer)
2281
+ params.set("consumer", options.consumer);
2216
2282
  if (options.include_episodes)
2217
2283
  params.set("include_episodes", "true");
2218
2284
  return this.request("GET", `/memory/index?${params.toString()}`);
@@ -2226,6 +2292,8 @@ class HarmonyApiClient {
2226
2292
  params.set("type", options.type);
2227
2293
  if (options.limit !== undefined)
2228
2294
  params.set("limit", String(options.limit));
2295
+ if (options.consumer)
2296
+ params.set("consumer", options.consumer);
2229
2297
  if (options.include_episodes)
2230
2298
  params.set("include_episodes", "true");
2231
2299
  return this.requestRaw("GET", `/memory/index?${params.toString()}`, undefined, {
@@ -2285,6 +2353,8 @@ class HarmonyApiClient {
2285
2353
  params.set("type", options.type);
2286
2354
  if (options?.limit !== undefined)
2287
2355
  params.set("limit", String(options.limit));
2356
+ if (options?.consumer)
2357
+ params.set("consumer", options.consumer);
2288
2358
  if (options?.include_episodes)
2289
2359
  params.set("include_episodes", "true");
2290
2360
  return this.requestRaw("GET", `/memory/search?${params.toString()}`, undefined, {
@@ -2390,14 +2460,15 @@ class HarmonyApiClient {
2390
2460
  } catch {}
2391
2461
  }
2392
2462
  const variant = options.variant || "execute";
2393
- const assembledContextStr = undefined;
2394
- const assemblyId = undefined;
2463
+ const assemblyId = randomUUID2();
2395
2464
  let memories;
2396
2465
  try {
2397
2466
  if (options.workspaceId && cardData.title) {
2398
- const memoryResult = await this.searchMemoryEntities(options.workspaceId, cardData.title, {
2467
+ const memoryResult = await this.searchMemoryEntities(options.workspaceId, buildMemoryQuery(cardData.title, cardData.description), {
2399
2468
  project_id: options.projectId,
2400
- limit: 5
2469
+ limit: 5,
2470
+ consumer: "agent-prompt",
2471
+ assembly_id: assemblyId
2401
2472
  });
2402
2473
  if (memoryResult.entities?.length > 0) {
2403
2474
  memories = memoryResult.entities.map((e) => ({
@@ -2421,7 +2492,6 @@ class HarmonyApiClient {
2421
2492
  contextOptions: options.contextOptions,
2422
2493
  customConstraints: options.customConstraints,
2423
2494
  memories,
2424
- assembledContext: assembledContextStr,
2425
2495
  assemblyId
2426
2496
  });
2427
2497
  try {
@@ -3335,6 +3405,34 @@ async function onboardNewUser(params) {
3335
3405
  };
3336
3406
  }
3337
3407
 
3408
+ // src/playbook-metric-warnings.ts
3409
+ function playbookMetricWarnings(agents, steps) {
3410
+ if (!Array.isArray(steps))
3411
+ return [];
3412
+ const declared = declaredGateMetricsFromAgents(agents);
3413
+ if (!declared.known)
3414
+ return [];
3415
+ const warnings = [];
3416
+ const seen = new Set;
3417
+ for (const ref of referencedGateMetrics({ steps, steps_version: 2 })) {
3418
+ if (declared.names.has(ref.metric) || seen.has(ref.metric))
3419
+ continue;
3420
+ seen.add(ref.metric);
3421
+ warnings.push(`Gate metric "${ref.metric}" (stage "${ref.stageName}") is not declared by any agent in this workspace — a stage run gating on it will hold until a daemon declares it under agent.playbooks.metrics.${ref.metric}.`);
3422
+ }
3423
+ return warnings;
3424
+ }
3425
+ async function collectPlaybookMetricWarnings(client3, workspaceId, steps) {
3426
+ if (!workspaceId || !Array.isArray(steps))
3427
+ return [];
3428
+ try {
3429
+ const { agents } = await client3.listWorkspaceAgents(workspaceId);
3430
+ return playbookMetricWarnings(agents, steps);
3431
+ } catch {
3432
+ return [];
3433
+ }
3434
+ }
3435
+
3338
3436
  // src/skills.ts
3339
3437
  import {
3340
3438
  existsSync as existsSync4,
@@ -4379,7 +4477,7 @@ var TOOLS = {
4379
4477
  }
4380
4478
  },
4381
4479
  harmony_classify_card: {
4382
- description: "Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the type label (feature/bug/idea). Call right after creating a card to classify it in-flow. Idempotent; never touches the user-owned `model_override`.",
4480
+ description: "DEPRECATED run sizing now happens at daemon pickup and is run-scoped, so nothing reads `model_tier`, `intent` or `complexity_score` any more; this tool still writes them, but only the type label has an effect. Prefer letting card creation apply the type label. Sets `intent` (plan/think/implement/review), `complexity_score` (0-10), `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the type label (feature/bug/idea). Idempotent; never touches the user-owned `model_override`.",
4383
4481
  inputSchema: {
4384
4482
  type: "object",
4385
4483
  properties: {
@@ -6892,7 +6990,7 @@ ${options}
6892
6990
  if (trimmed.length > 0) {
6893
6991
  const touchIds = trimmed.map(({ entity }) => entity?.id).filter((id) => typeof id === "string");
6894
6992
  if (touchIds.length > 0) {
6895
- client3.batchTouchMemoryEntities(touchIds).catch(() => {});
6993
+ client3.batchTouchMemoryEntities(touchIds, "mcp-tool").catch(() => {});
6896
6994
  }
6897
6995
  }
6898
6996
  let sessionEntities = [];
@@ -7306,7 +7404,12 @@ ${options}
7306
7404
  autoBind: args.autoBind,
7307
7405
  catalogId: args.catalogId
7308
7406
  });
7309
- return { success: true, playbook: result.playbook };
7407
+ const warnings = await collectPlaybookMetricWarnings(client3, workspaceId, args.steps);
7408
+ return {
7409
+ success: true,
7410
+ playbook: result.playbook,
7411
+ ...warnings.length > 0 ? { warnings } : {}
7412
+ };
7310
7413
  }
7311
7414
  case "harmony_update_playbook": {
7312
7415
  const playbookId = z.string().uuid().parse(args.playbookId);
@@ -7319,7 +7422,12 @@ ${options}
7319
7422
  triggerType: args.triggerType,
7320
7423
  ..."autoBind" in args ? { autoBind: args.autoBind } : {}
7321
7424
  });
7322
- return { success: true, playbook: result.playbook };
7425
+ const warnings = await collectPlaybookMetricWarnings(client3, result.playbook?.workspace_id, args.steps);
7426
+ return {
7427
+ success: true,
7428
+ playbook: result.playbook,
7429
+ ...warnings.length > 0 ? { warnings } : {}
7430
+ };
7323
7431
  }
7324
7432
  case "harmony_save_card_as_playbook":
7325
7433
  return deprecatedRemovedToolResult("harmony_save_card_as_playbook");