@gethmy/mcp 2.24.0 → 2.26.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.
@@ -0,0 +1,56 @@
1
+ import {
2
+ declaredGateMetricsFromAgents,
3
+ referencedGateMetrics,
4
+ type WorkspaceAgent,
5
+ } from "@harmony/shared";
6
+
7
+ /**
8
+ * Non-blocking warnings for a playbook write whose `custom` gates name metrics
9
+ * no reporting agent in the workspace declares (card #922). The write always
10
+ * succeeds — a metric name can legitimately precede its declaration — but the
11
+ * author learns about the mismatch NOW, not after the first bound card burns a
12
+ * stage run on it. Inert until at least one agent has reported its declared
13
+ * names (an older daemon, or none running, must never raise a false alarm).
14
+ */
15
+ export function playbookMetricWarnings(
16
+ agents: ReadonlyArray<Pick<WorkspaceAgent, "declared_gate_metrics">>,
17
+ steps: unknown,
18
+ ): string[] {
19
+ if (!Array.isArray(steps)) return [];
20
+ const declared = declaredGateMetricsFromAgents(agents);
21
+ if (!declared.known) return [];
22
+
23
+ const warnings: string[] = [];
24
+ const seen = new Set<string>();
25
+ for (const ref of referencedGateMetrics({ steps, steps_version: 2 })) {
26
+ if (declared.names.has(ref.metric) || seen.has(ref.metric)) continue;
27
+ seen.add(ref.metric);
28
+ warnings.push(
29
+ `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}.`,
30
+ );
31
+ }
32
+ return warnings;
33
+ }
34
+
35
+ /**
36
+ * Fetch the workspace's agents and compute the warnings, swallowing every
37
+ * error: the playbook write already succeeded, and a warning lookup must never
38
+ * turn that success into a tool failure.
39
+ */
40
+ export async function collectPlaybookMetricWarnings(
41
+ client: {
42
+ listWorkspaceAgents(
43
+ workspaceId: string,
44
+ ): Promise<{ agents: WorkspaceAgent[] }>;
45
+ },
46
+ workspaceId: string | undefined,
47
+ steps: unknown,
48
+ ): Promise<string[]> {
49
+ if (!workspaceId || !Array.isArray(steps)) return [];
50
+ try {
51
+ const { agents } = await client.listWorkspaceAgents(workspaceId);
52
+ return playbookMetricWarnings(agents, steps);
53
+ } catch {
54
+ return [];
55
+ }
56
+ }
@@ -402,8 +402,6 @@ export interface GeneratePromptOptions {
402
402
  contextOptions?: Partial<PromptContextOptions>;
403
403
  customConstraints?: string;
404
404
  memories?: MemoryData[];
405
- /** Pre-assembled context string from context assembly engine */
406
- assembledContext?: string;
407
405
  /** Assembly ID for manifest tracking */
408
406
  assemblyId?: string;
409
407
  }
@@ -414,15 +412,8 @@ export interface GeneratePromptOptions {
414
412
  export function generatePrompt(
415
413
  options: GeneratePromptOptions,
416
414
  ): GeneratedPrompt {
417
- const {
418
- card,
419
- column,
420
- variant,
421
- customConstraints,
422
- memories,
423
- assembledContext,
424
- assemblyId,
425
- } = options;
415
+ const { card, column, variant, customConstraints, memories, assemblyId } =
416
+ options;
426
417
 
427
418
  // Merge context options with defaults
428
419
  const contextOpts: PromptContextOptions = {
@@ -537,11 +528,7 @@ export function generatePrompt(
537
528
  });
538
529
 
539
530
  // Relevant memories from knowledge graph
540
- if (assembledContext) {
541
- // Use pre-assembled context from context assembly engine
542
- sections.push(`\n${assembledContext}`);
543
- } else if (memories && memories.length > 0) {
544
- // Fallback: legacy memory format
531
+ if (memories && memories.length > 0) {
545
532
  sections.push(`\n## Relevant Memories`);
546
533
  sections.push(
547
534
  `*${memories.length} memories recalled from knowledge graph:*`,
@@ -556,12 +543,7 @@ export function generatePrompt(
556
543
  }
557
544
 
558
545
  // "One Thing" synthesis — highest-leverage next action
559
- const oneThingLine = synthesizeOneThing(
560
- card,
561
- subtasks,
562
- links,
563
- assembledContext,
564
- );
546
+ const oneThingLine = synthesizeOneThing(card, subtasks, links);
565
547
  if (oneThingLine) {
566
548
  sections.push(`\n## Recommended Next Step\n${oneThingLine}`);
567
549
  }
@@ -590,9 +572,7 @@ Keep \`currentTask\` specific (e.g., "Refactoring auth middleware" not "Working
590
572
 
591
573
  const prompt = sections.join("\n");
592
574
 
593
- const memoryCount = assembledContext
594
- ? (assembledContext.match(/^### /gm) || []).length
595
- : memories?.length || 0;
575
+ const memoryCount = memories?.length ?? 0;
596
576
 
597
577
  return {
598
578
  prompt,
@@ -616,69 +596,7 @@ Keep \`currentTask\` specific (e.g., "Refactoring auth middleware" not "Working
616
596
  }
617
597
 
618
598
  /**
619
- * Extract session insights from assembled context string.
620
- * Parses session summaries, blockers, and progress data.
621
- */
622
- function extractSessionInsights(assembledContext: string): {
623
- lastSessionStatus: "completed" | "paused" | null;
624
- lastSessionTask: string | null;
625
- lastSessionProgress: number | null;
626
- blockers: string[];
627
- procedureNextStep: string | null;
628
- } {
629
- const result = {
630
- lastSessionStatus: null as "completed" | "paused" | null,
631
- lastSessionTask: null as string | null,
632
- lastSessionProgress: null as number | null,
633
- blockers: [] as string[],
634
- procedureNextStep: null as string | null,
635
- };
636
-
637
- // Find the most recent session summary with status
638
- const sessionMatches = assembledContext.match(
639
- /### Session:.*?\n([\s\S]*?)(?=\n###|\n## |\n---|\n\*Assembly|$)/g,
640
- );
641
- if (sessionMatches && sessionMatches.length > 0) {
642
- const latest = sessionMatches[0];
643
- if (/Completed work on/i.test(latest)) {
644
- result.lastSessionStatus = "completed";
645
- } else if (/Paused work on|status:\s*paused/i.test(latest)) {
646
- result.lastSessionStatus = "paused";
647
- }
648
- const taskMatch = latest.match(/Final task:\s*(.+)/);
649
- if (taskMatch) result.lastSessionTask = taskMatch[1].trim();
650
- const progressMatch = latest.match(/Progress:\s*(\d+)%/);
651
- if (progressMatch)
652
- result.lastSessionProgress = parseInt(progressMatch[1], 10);
653
- }
654
-
655
- // Extract blockers from context
656
- const blockerMatches = assembledContext.match(
657
- /(?:blocker|blocked by|blocking):\s*(.+)/gi,
658
- );
659
- if (blockerMatches) {
660
- result.blockers = blockerMatches.map((m) =>
661
- m.replace(/(?:blocker|blocked by|blocking):\s*/i, "").trim(),
662
- );
663
- }
664
-
665
- // Extract next procedure step (first uncompleted step)
666
- const stepMatches = assembledContext.match(
667
- /^\d+\.\s+(?!.*\*\*\[key step\]\*\*.*✓)(.+?)(?:\s*\*\*\[key step\]\*\*)?$/gm,
668
- );
669
- if (stepMatches && stepMatches.length > 0) {
670
- result.procedureNextStep = stepMatches[0]
671
- .replace(/^\d+\.\s+/, "")
672
- .replace(/\s*\*\*\[key step\]\*\*.*$/, "")
673
- .trim();
674
- }
675
-
676
- return result;
677
- }
678
-
679
- /**
680
- * Synthesize the single highest-leverage next action from card state
681
- * and assembled context (session history, blockers, procedures).
599
+ * Synthesize the single highest-leverage next action from card state.
682
600
  * Inspired by ArtemXTech's "One Thing" pattern in /recall.
683
601
  */
684
602
  function synthesizeOneThing(
@@ -689,7 +607,6 @@ function synthesizeOneThing(
689
607
  display_type: string;
690
608
  direction: "outgoing" | "incoming";
691
609
  }>,
692
- assembledContext?: string,
693
610
  ): string | null {
694
611
  // Priority 1: Card is already done
695
612
  if (card.done) return null;
@@ -703,25 +620,7 @@ function synthesizeOneThing(
703
620
  return `Unblock first: resolve #${blocker.target_card.short_id} "${blocker.target_card.title}" which is blocking this card.`;
704
621
  }
705
622
 
706
- // Extract session insights from assembled context
707
- const session = assembledContext
708
- ? extractSessionInsights(assembledContext)
709
- : null;
710
-
711
- // Priority 3: Blockers detected in session context
712
- if (session?.blockers && session.blockers.length > 0) {
713
- return `Resolve blocker: ${session.blockers[0]}`;
714
- }
715
-
716
- // Priority 4: Previous session was paused — resume where it left off
717
- if (session?.lastSessionStatus === "paused" && session.lastSessionTask) {
718
- const progress = session.lastSessionProgress
719
- ? ` (was ${session.lastSessionProgress}% complete)`
720
- : "";
721
- return `Resume previous session${progress}: "${session.lastSessionTask}".`;
722
- }
723
-
724
- // Priority 5: Has subtasks — find the first incomplete one
623
+ // Priority 3: Has subtasks find the first incomplete one
725
624
  if (subtasks.length > 0) {
726
625
  const completed = subtasks.filter((s) => s.completed).length;
727
626
  if (completed === subtasks.length) {
@@ -733,17 +632,7 @@ function synthesizeOneThing(
733
632
  }
734
633
  }
735
634
 
736
- // Priority 6: Procedure has a next step
737
- if (session?.procedureNextStep) {
738
- return `Follow procedure: ${session.procedureNextStep}`;
739
- }
740
-
741
- // Priority 7: Previous session completed — build on it
742
- if (session?.lastSessionStatus === "completed" && session.lastSessionTask) {
743
- return `Previous session completed ("${session.lastSessionTask}"). Review results and continue with remaining work.`;
744
- }
745
-
746
- // Priority 8: High/urgent priority with due date
635
+ // Priority 4: High/urgent priority with due date
747
636
  if (
748
637
  card.due_date &&
749
638
  (card.priority === "urgent" || card.priority === "high")
@@ -751,7 +640,7 @@ function synthesizeOneThing(
751
640
  return `High-priority task with deadline ${card.due_date}. Start implementation immediately.`;
752
641
  }
753
642
 
754
- // Priority 9: Has description — start working
643
+ // Priority 5: Has description — start working
755
644
  if (card.description) {
756
645
  return "Analyze the description, identify the approach, and begin implementation.";
757
646
  }
@@ -0,0 +1,16 @@
1
+ // Read-consumer labels for `record_entity_reads` (migration
2
+ // 20260828120000_knowledge_entity_reads.sql).
3
+ //
4
+ // Duplicated in `supabase/functions/_shared/read-consumer.ts` rather than
5
+ // shared via `packages/harmony-shared`: this package has no dependency on
6
+ // `@harmony/shared` (so it could not import the type from there), and
7
+ // `packages/mobile` DOES depend on `@harmony/shared` (so adding it there
8
+ // would drag mobile's typecheck CI into a change mobile never uses). Both
9
+ // copies must match the SQL `CHECK` constraint exactly — six values, no more.
10
+ export type ReadConsumer =
11
+ | "agent-prompt"
12
+ | "assistant"
13
+ | "mcp-tool"
14
+ | "analyze"
15
+ | "search"
16
+ | "browse";
package/src/server.ts CHANGED
@@ -69,6 +69,7 @@ import {
69
69
  } from "./memory-session.js";
70
70
  import { lintTags, normalizeTags } from "./memory-tags.js";
71
71
  import { onboardNewUser } from "./onboard.js";
72
+ import { collectPlaybookMetricWarnings } from "./playbook-metric-warnings.js";
72
73
  import { stripSkillPreamble } from "./skills.js";
73
74
 
74
75
  // --- Signed-upload handshake (artifacts & card attachments) ---
@@ -1206,7 +1207,7 @@ export const TOOLS = {
1206
1207
  },
1207
1208
  harmony_classify_card: {
1208
1209
  description:
1209
- "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`.",
1210
+ "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`.",
1210
1211
  inputSchema: {
1211
1212
  type: "object",
1212
1213
  properties: {
@@ -4680,6 +4681,12 @@ async function handleToolCall(
4680
4681
  : undefined,
4681
4682
  include_superseded: includeSuperseded,
4682
4683
  include_episodes: includeEpisodes,
4684
+ // No consumer here on purpose (fix round 1, task 4): this fetches
4685
+ // an over-fetched candidate pool (up to `fetchLimit`, itself
4686
+ // padded above `requestedLimit`/`topK`) for Park to rescore, not
4687
+ // what the tool actually returns. Recording it would overstate
4688
+ // access_count by up to fetchLimit/finalCount per call. What we
4689
+ // actually deliver is recorded below, after trimming.
4683
4690
  },
4684
4691
  );
4685
4692
  entities = (searchResult.entities ?? []) as any[];
@@ -4741,19 +4748,19 @@ async function handleToolCall(
4741
4748
  trimmed = fitToBudget(trimmed, budgetTokens);
4742
4749
  }
4743
4750
 
4744
- // Touch access counters for the entities we returned (#273, task 4).
4745
- // Previously this wrote `metadata._last_recall` via updateMemoryEntity,
4746
- // which left `access_count` at 0 forever and never updated
4747
- // `last_accessed_at` the column the Park recency term actually reads.
4748
- // Route through batch-touch, which calls the atomic
4749
- // `batch_touch_knowledge_entities` RPC (access_count += 1, last_accessed_at
4750
- // = now) in a single round-trip. Non-blocking; best-effort.
4751
+ // Read accounting (task 4, fix round 1): record only the entities
4752
+ // actually delivered to the caller — after trimming and the budget
4753
+ // pass, not the larger over-fetched candidate pool the search above
4754
+ // deliberately left unrecorded. Routed through batch-touch, which
4755
+ // dispatches to the same recordEntityReads/record_entity_reads path
4756
+ // the search chokepoint uses when a consumer is given. Non-blocking;
4757
+ // best-effort.
4751
4758
  if (trimmed.length > 0) {
4752
4759
  const touchIds = trimmed
4753
4760
  .map(({ entity }: { entity: any }) => entity?.id)
4754
4761
  .filter((id: unknown): id is string => typeof id === "string");
4755
4762
  if (touchIds.length > 0) {
4756
- client.batchTouchMemoryEntities(touchIds).catch(() => {});
4763
+ client.batchTouchMemoryEntities(touchIds, "mcp-tool").catch(() => {});
4757
4764
  }
4758
4765
  }
4759
4766
 
@@ -5370,7 +5377,18 @@ async function handleToolCall(
5370
5377
  autoBind: args.autoBind,
5371
5378
  catalogId: args.catalogId as string | undefined,
5372
5379
  });
5373
- return { success: true, playbook: result.playbook };
5380
+ // #922: surface custom-gate metrics no agent declares — non-blocking,
5381
+ // the playbook is already created.
5382
+ const warnings = await collectPlaybookMetricWarnings(
5383
+ client,
5384
+ workspaceId,
5385
+ args.steps,
5386
+ );
5387
+ return {
5388
+ success: true,
5389
+ playbook: result.playbook,
5390
+ ...(warnings.length > 0 ? { warnings } : {}),
5391
+ };
5374
5392
  }
5375
5393
 
5376
5394
  case "harmony_update_playbook": {
@@ -5386,7 +5404,19 @@ async function handleToolCall(
5386
5404
  triggerType: args.triggerType as string | undefined,
5387
5405
  ...("autoBind" in args ? { autoBind: args.autoBind } : {}),
5388
5406
  });
5389
- return { success: true, playbook: result.playbook };
5407
+ // #922: same non-blocking metric check as create. The playbook's own
5408
+ // workspace scopes the lookup — the active context may point elsewhere.
5409
+ const warnings = await collectPlaybookMetricWarnings(
5410
+ client,
5411
+ (result.playbook as { workspace_id?: string } | undefined)
5412
+ ?.workspace_id,
5413
+ args.steps,
5414
+ );
5415
+ return {
5416
+ success: true,
5417
+ playbook: result.playbook,
5418
+ ...(warnings.length > 0 ? { warnings } : {}),
5419
+ };
5390
5420
  }
5391
5421
 
5392
5422
  // Deprecated (#612) — see harmony_run_playbook above.