harmony-mcp 1.14.0 → 1.14.2

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.
Files changed (4) hide show
  1. package/README.md +34 -4
  2. package/dist/cli.js +429 -129
  3. package/dist/index.js +429 -129
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -5,7 +5,7 @@ Enables AI coding agents (Claude Code, OpenAI Codex, Cursor, Windsurf) to intera
5
5
 
6
6
  ## Features
7
7
 
8
- - **43+ MCP Tools** for full board control, knowledge graph, and workflow plans
8
+ - **61+ MCP Tools** for full board control, knowledge graph, and workflow plans
9
9
  - **Knowledge Graph Memory** - persistent memory with entity types, tiers, scopes, and typed relations
10
10
  - **Active Learning** - auto-extracts lessons, solutions, and error patterns from completed work sessions
11
11
  - **Context Assembly** - token-budget-aware memory injection into AI prompts
@@ -13,6 +13,7 @@ Enables AI coding agents (Claude Code, OpenAI Codex, Cursor, Windsurf) to intera
13
13
  - **Card Linking** - create relationships between cards (blocks, relates_to, duplicates, is_part_of)
14
14
  - **Prompt Builder** - generate AI-ready prompts from cards with context
15
15
  - **Agent Session Tracking** - track work progress with timer badges
16
+ - **Auto-Session Detection** - automatically starts/ends sessions when agents interact with cards
16
17
  - **Auto-Assignment** - automatically assign cards to you when starting agent sessions
17
18
  - **Memory Sync** - bidirectional sync between local markdown files and remote database
18
19
  - **Multi-Agent Support** - works with Claude Code, Codex, Cursor, Windsurf
@@ -89,7 +90,7 @@ If you prefer to configure manually (e.g., in Claude.ai's UI):
89
90
  1. Get an API key from [Harmony](https://gethmy.com/user/keys)
90
91
  2. In Claude.ai, add a remote MCP server with URL `https://mcp.gethmy.com/mcp`
91
92
  3. Set the Authorization header to `Bearer hmy_your_key_here`
92
- 4. All 43+ Harmony tools become available in your conversation
93
+ 4. All 61+ Harmony tools become available in your conversation
93
94
 
94
95
  **Session management** is automatic - sessions have a 1-hour TTL and are created/renewed transparently.
95
96
 
@@ -167,6 +168,8 @@ When you start working on a card (e.g., `/hmy #42`):
167
168
  - `harmony_create_card` - Create a new card
168
169
  - `harmony_update_card` - Update card properties
169
170
  - `harmony_move_card` - Move card to different column (auto-ends agent session on move to done/review)
171
+ - `harmony_archive_card` - Archive a card (soft-delete, can be restored)
172
+ - `harmony_unarchive_card` - Restore an archived card back to the board
170
173
  - `harmony_delete_card` - Delete a card
171
174
  - `harmony_assign_card` - Assign to team member
172
175
  - `harmony_search_cards` - Search by title/description
@@ -209,7 +212,7 @@ When you start working on a card (e.g., `/hmy #42`):
209
212
 
210
213
  - `harmony_list_workspaces` - List workspaces
211
214
  - `harmony_list_projects` - List projects
212
- - `harmony_get_board` - Get board state (supports pagination via `limit`/`offset`, `summary` mode, `columnId` filter)
215
+ - `harmony_get_board` - Get board state (supports pagination via `limit`/`offset`, `summary` mode, `columnId` filter, `includeArchived`)
213
216
  - `harmony_get_workspace_members` - Get team members
214
217
  - `harmony_set_workspace_context` - Set active workspace
215
218
  - `harmony_set_project_context` - Set active project
@@ -225,6 +228,20 @@ When you start working on a card (e.g., `/hmy #42`):
225
228
  - `harmony_update_agent_progress` - Update progress, status, blockers
226
229
  - `harmony_end_agent_session` - End session (completed/paused); triggers active learning extraction
227
230
  - `harmony_get_agent_session` - Get current session state
231
+ - `harmony_get_agent_profile` - Get aggregate performance profile for an agent
232
+
233
+ ### Auto-Session Detection
234
+
235
+ Sessions are automatically started when agents call card-mutating tools without an explicit `harmony_start_agent_session`. This ensures work is always tracked, even when agents don't explicitly manage sessions.
236
+
237
+ **Trigger tools:** `harmony_generate_prompt`, `harmony_update_card`, `harmony_move_card`, `harmony_create_subtask`, `harmony_toggle_subtask`, `harmony_add_label_to_card`, `harmony_remove_label_from_card`
238
+
239
+ **Behavior:**
240
+ - Auto-starts a session when any trigger tool is called on a card without an active session
241
+ - Auto-ends after 10 minutes of inactivity
242
+ - Switching to a different card auto-ends the previous auto-session
243
+ - Explicitly started sessions (via `harmony_start_agent_session`) are never auto-ended
244
+ - Auto-ended sessions trigger the full active learning pipeline
228
245
 
229
246
  ### Prompt Generation
230
247
 
@@ -256,7 +273,7 @@ Store and retrieve persistent knowledge across sessions. Memories have types, ti
256
273
  - `harmony_memory_search` - Full-text search across the knowledge base
257
274
  - `harmony_relate` - Create a typed relationship between two memory entities
258
275
 
259
- **Entity Types:** `agent`, `task`, `decision`, `context`, `pattern`, `error`, `solution`, `preference`, `relationship`, `commitment`, `lesson`, `project`, `handoff`
276
+ **Entity Types:** `agent`, `task`, `decision`, `context`, `pattern`, `error`, `solution`, `preference`, `relationship`, `commitment`, `lesson`, `project`, `handoff`, `procedure`
260
277
 
261
278
  **Memory Tiers:** `draft` (working notes) → `episode` (session summaries) → `reference` (durable knowledge)
262
279
 
@@ -274,6 +291,9 @@ Store and retrieve persistent knowledge across sessions. Memories have types, ti
274
291
 
275
292
  - `harmony_promote_memory` - Promote entity tier: `draft` → `episode` or `episode` → `reference`
276
293
  - `harmony_prune_draft` - Remove stale draft memories (dry-run mode by default)
294
+ - `harmony_consolidate_memories` - Cluster similar draft/episode memories and merge into reference entities (dry-run by default)
295
+ - `harmony_backfill_embeddings` - Generate vector embeddings for entities missing them
296
+ - `harmony_backfill_relations` - Retroactively create semantic relations across existing entities
277
297
 
278
298
  ### Context Debugging
279
299
 
@@ -285,6 +305,7 @@ Store and retrieve persistent knowledge across sessions. Memories have types, ti
285
305
 
286
306
  Create and manage project plans with a phased workflow: **plan** → **execute** → **verify** → **done**.
287
307
 
308
+ - `harmony_list_plans` - List plans in a project (filter by status or workflow phase)
288
309
  - `harmony_create_plan` - Create a new plan with embedded tasks
289
310
  - `harmony_get_plan` - Get plan by ID or card ID
290
311
  - `harmony_update_plan` - Update plan title, content, status, or phase
@@ -293,6 +314,15 @@ Create and manage project plans with a phased workflow: **plan** → **execute**
293
314
  - **execute → verify:** checks card completion status
294
315
  - **verify → done:** archives plan, creates memory entities
295
316
 
317
+ ### Onboarding & Account
318
+
319
+ - `harmony_onboard` - Complete end-to-end onboarding: signup → workspace → project → API key
320
+ - `harmony_signup` - Create a new user account
321
+ - `harmony_create_workspace` - Create a new workspace
322
+ - `harmony_create_project` - Create a new project with template columns (kanban, scrum, or simple)
323
+ - `harmony_send_invitations` - Send workspace invitations to team members
324
+ - `harmony_generate_api_key` - Generate an API key for the authenticated user
325
+
296
326
  ## Direct API Access
297
327
 
298
328
  You can also call the Harmony API directly from any HTTP client:
package/dist/cli.js CHANGED
@@ -25315,7 +25315,8 @@ async function findSimilarEntities(client2, title, content, workspaceId, options
25315
25315
  try {
25316
25316
  const { entities } = await client2.searchMemoryEntities(workspaceId, query, {
25317
25317
  project_id: options?.projectId,
25318
- limit: options?.limit ?? 20
25318
+ limit: options?.limit ?? 20,
25319
+ type: options?.type
25319
25320
  });
25320
25321
  const minScore = options?.minRrfScore ?? 0;
25321
25322
  const excludeSet = new Set(options?.excludeIds || []);
@@ -25330,6 +25331,57 @@ async function findSimilarEntities(client2, title, content, workspaceId, options
25330
25331
  return [];
25331
25332
  }
25332
25333
  }
25334
+ var CAUSAL_LOOKUP = [
25335
+ {
25336
+ sourceType: "error",
25337
+ targetType: "solution",
25338
+ relation: "resolved_by",
25339
+ direction: "forward"
25340
+ },
25341
+ {
25342
+ sourceType: "solution",
25343
+ targetType: "error",
25344
+ relation: "resolved_by",
25345
+ direction: "reverse"
25346
+ },
25347
+ {
25348
+ sourceType: "lesson",
25349
+ targetType: "error",
25350
+ relation: "learned_from",
25351
+ direction: "forward"
25352
+ }
25353
+ ];
25354
+ async function linkCrossTypeNeighbors(client2, entityId, entityType, title, content, workspaceId, projectId) {
25355
+ const rules = CAUSAL_LOOKUP.filter((r) => r.sourceType === entityType);
25356
+ if (rules.length === 0)
25357
+ return { relationsCreated: 0 };
25358
+ let relationsCreated = 0;
25359
+ for (const rule of rules) {
25360
+ try {
25361
+ const matches = await findSimilarEntities(client2, title, content, workspaceId, {
25362
+ projectId,
25363
+ limit: 10,
25364
+ minRrfScore: 0.04,
25365
+ excludeIds: [entityId],
25366
+ type: rule.targetType
25367
+ });
25368
+ for (const match of matches.slice(0, 3)) {
25369
+ const sourceId = rule.direction === "forward" ? entityId : match.id;
25370
+ const targetId = rule.direction === "forward" ? match.id : entityId;
25371
+ try {
25372
+ await client2.createMemoryRelation({
25373
+ source_id: sourceId,
25374
+ target_id: targetId,
25375
+ relation_type: rule.relation,
25376
+ confidence: 0.65
25377
+ });
25378
+ relationsCreated++;
25379
+ } catch {}
25380
+ }
25381
+ } catch {}
25382
+ }
25383
+ return { relationsCreated };
25384
+ }
25333
25385
 
25334
25386
  // src/active-learning.ts
25335
25387
  var CONTRADICTION_TYPES = new Set([
@@ -25602,6 +25654,31 @@ Agent: ${session.agentName} | ${new Date().toISOString().split("T")[0]}`;
25602
25654
  }
25603
25655
  };
25604
25656
  }
25657
+ var SESSION_CAUSAL_RULES = [
25658
+ { sourceType: "error", targetType: "solution", relation: "resolved_by", confidence: 0.8 },
25659
+ { sourceType: "lesson", targetType: "error", relation: "learned_from", confidence: 0.75 }
25660
+ ];
25661
+ async function linkSessionEntities(client2, createdPairs, _workspaceId, _projectId) {
25662
+ let relationsCreated = 0;
25663
+ for (const rule of SESSION_CAUSAL_RULES) {
25664
+ const sources = createdPairs.filter((p) => p.learning.type === rule.sourceType);
25665
+ const targets = createdPairs.filter((p) => p.learning.type === rule.targetType);
25666
+ for (const source of sources) {
25667
+ for (const target of targets) {
25668
+ try {
25669
+ await client2.createMemoryRelation({
25670
+ source_id: source.id,
25671
+ target_id: target.id,
25672
+ relation_type: rule.relation,
25673
+ confidence: rule.confidence
25674
+ });
25675
+ relationsCreated++;
25676
+ } catch {}
25677
+ }
25678
+ }
25679
+ }
25680
+ return { relationsCreated };
25681
+ }
25605
25682
  async function extractLearnings(client2, session) {
25606
25683
  const workspaceId = getActiveWorkspaceId();
25607
25684
  if (!workspaceId) {
@@ -25743,10 +25820,17 @@ Agent: ${session.agentName}`,
25743
25820
  for (const { id, learning } of createdPairs) {
25744
25821
  autoExpandGraph(client2, id, learning.title, learning.content, learning.tags, workspaceId, projectId).catch(() => {});
25745
25822
  detectContradictions(client2, id, learning.type, learning.title, learning.content, learning.tags, workspaceId, projectId).catch(() => {});
25823
+ linkCrossTypeNeighbors(client2, id, learning.type, learning.title, learning.content, workspaceId, projectId).catch(() => {});
25824
+ }
25825
+ if (createdPairs.length >= 2) {
25826
+ linkSessionEntities(client2, createdPairs, workspaceId, projectId).catch(() => {});
25746
25827
  }
25747
25828
  if (entityIds.length > 0) {
25748
25829
  detectAndCreatePatterns(client2, entityIds, session, workspaceId, projectId).catch(() => {});
25749
25830
  }
25831
+ if (createdPairs.length > 0) {
25832
+ detectCausalPatterns(client2, createdPairs, session, workspaceId, projectId).catch(() => {});
25833
+ }
25750
25834
  clearMidSessionTracking(session.cardId);
25751
25835
  return { count: entityIds.length, entityIds };
25752
25836
  }
@@ -25837,6 +25921,122 @@ ${memberTitles.map((t) => `- ${t}`).join(`
25837
25921
  }
25838
25922
  return patternEntityIds;
25839
25923
  }
25924
+ var CAUSAL_PATTERN_THRESHOLD = 3;
25925
+ async function detectCausalPatterns(client2, createdPairs, session, workspaceId, projectId) {
25926
+ const patternIds = [];
25927
+ const errors3 = createdPairs.filter((p) => p.learning.type === "error");
25928
+ const solutions = createdPairs.filter((p) => p.learning.type === "solution");
25929
+ if (errors3.length === 0 || solutions.length === 0)
25930
+ return patternIds;
25931
+ for (const errorPair of errors3) {
25932
+ try {
25933
+ const similarErrors = await findSimilarEntities(client2, errorPair.learning.title, errorPair.learning.content, workspaceId, {
25934
+ projectId,
25935
+ limit: 20,
25936
+ minRrfScore: 0.03,
25937
+ excludeIds: createdPairs.map((p) => p.id),
25938
+ type: "error"
25939
+ });
25940
+ const resolvedErrors = [];
25941
+ for (const similar of similarErrors.slice(0, 10)) {
25942
+ try {
25943
+ const { outgoing } = await client2.getRelatedEntities(similar.id);
25944
+ const resolvedByRel = outgoing.find((r) => r.relation_type === "resolved_by");
25945
+ if (resolvedByRel) {
25946
+ resolvedErrors.push({
25947
+ errorId: similar.id,
25948
+ errorTitle: similar.title,
25949
+ solutionTitle: resolvedByRel.target_title || "unknown"
25950
+ });
25951
+ }
25952
+ } catch {}
25953
+ }
25954
+ if (resolvedErrors.length + 1 < CAUSAL_PATTERN_THRESHOLD)
25955
+ continue;
25956
+ const { entities: existingPatterns } = await client2.listMemoryEntities({
25957
+ workspace_id: workspaceId,
25958
+ project_id: projectId,
25959
+ type: "pattern",
25960
+ limit: 10
25961
+ });
25962
+ const matchingPattern = existingPatterns.find((p) => p.metadata?.pattern_chain_type === "error_resolved_by_solution");
25963
+ if (matchingPattern) {
25964
+ await client2.updateMemoryEntity(matchingPattern.id, {
25965
+ content: [
25966
+ `Recurring error→solution chain detected (${resolvedErrors.length + 1} instances).`,
25967
+ "",
25968
+ "## Error→Solution Pairs",
25969
+ `- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
25970
+ ...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`),
25971
+ "",
25972
+ `Last updated: ${new Date().toISOString()}`
25973
+ ].join(`
25974
+ `),
25975
+ metadata: {
25976
+ pattern_chain_type: "error_resolved_by_solution",
25977
+ pattern_count: resolvedErrors.length + 1,
25978
+ last_updated: new Date().toISOString()
25979
+ }
25980
+ });
25981
+ for (const pair of [errorPair, solutions[0]]) {
25982
+ try {
25983
+ await client2.createMemoryRelation({
25984
+ source_id: pair.id,
25985
+ target_id: matchingPattern.id,
25986
+ relation_type: "part_of",
25987
+ confidence: 0.75
25988
+ });
25989
+ } catch {}
25990
+ }
25991
+ } else {
25992
+ const result = await client2.createMemoryEntity({
25993
+ workspace_id: workspaceId,
25994
+ project_id: projectId,
25995
+ type: "pattern",
25996
+ scope: "project",
25997
+ memory_tier: "reference",
25998
+ title: `Pattern: recurring error→solution chain (${resolvedErrors.length + 1} instances)`,
25999
+ content: [
26000
+ `Recurring error→solution chain detected across ${resolvedErrors.length + 1} sessions.`,
26001
+ "",
26002
+ "## Error→Solution Pairs",
26003
+ `- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
26004
+ ...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`)
26005
+ ].join(`
26006
+ `),
26007
+ confidence: 0.8,
26008
+ tags: ["auto-extracted", "pattern", "causal-chain"],
26009
+ metadata: {
26010
+ source: "causal_pattern_detection",
26011
+ pattern_chain_type: "error_resolved_by_solution",
26012
+ pattern_count: resolvedErrors.length + 1
26013
+ },
26014
+ agent_identifier: session.agentIdentifier
26015
+ });
26016
+ const created = result.entity;
26017
+ if (created?.id) {
26018
+ patternIds.push(created.id);
26019
+ const memberIds = [
26020
+ errorPair.id,
26021
+ solutions[0].id,
26022
+ ...resolvedErrors.slice(0, 4).map((r) => r.errorId)
26023
+ ];
26024
+ for (const memberId of memberIds) {
26025
+ try {
26026
+ await client2.createMemoryRelation({
26027
+ source_id: memberId,
26028
+ target_id: created.id,
26029
+ relation_type: "part_of",
26030
+ confidence: 0.75
26031
+ });
26032
+ } catch {}
26033
+ }
26034
+ }
26035
+ }
26036
+ } catch {}
26037
+ }
26038
+ return patternIds;
26039
+ }
25840
26040
  async function detectContradictions(client2, entityId, entityType, title, content, tags, workspaceId, projectId) {
25841
26041
  if (!CONTRADICTION_TYPES.has(entityType))
25842
26042
  return [];
@@ -25883,125 +26083,6 @@ async function detectContradictions(client2, entityId, entityType, title, conten
25883
26083
  }
25884
26084
  }
25885
26085
 
25886
- // src/auto-session.ts
25887
- var AUTO_START_TRIGGERS = new Set([
25888
- "harmony_generate_prompt",
25889
- "harmony_update_card",
25890
- "harmony_move_card",
25891
- "harmony_create_subtask",
25892
- "harmony_toggle_subtask",
25893
- "harmony_add_label_to_card",
25894
- "harmony_remove_label_from_card"
25895
- ]);
25896
- var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
25897
- var CHECK_INTERVAL_MS = 60 * 1000;
25898
- var activeSessions = new Map;
25899
- var inactivityTimer = null;
25900
- var endCallback = null;
25901
- var clientGetter = null;
25902
- function initAutoSession(callback, getClient) {
25903
- endCallback = callback;
25904
- clientGetter = getClient;
25905
- if (inactivityTimer)
25906
- clearInterval(inactivityTimer);
25907
- inactivityTimer = setInterval(checkInactivity, CHECK_INTERVAL_MS);
25908
- }
25909
- async function trackActivity(cardId, options) {
25910
- const now = Date.now();
25911
- const existing = activeSessions.get(cardId);
25912
- if (existing) {
25913
- existing.lastActivityAt = now;
25914
- return;
25915
- }
25916
- if (!options?.autoStart)
25917
- return;
25918
- const client2 = options?.client ?? clientGetter?.();
25919
- if (!client2)
25920
- return;
25921
- const toEnd = [];
25922
- for (const [otherCardId, session] of activeSessions) {
25923
- if (otherCardId !== cardId && !session.isExplicit) {
25924
- toEnd.push(otherCardId);
25925
- }
25926
- }
25927
- for (const otherCardId of toEnd) {
25928
- await autoEndSession(client2, otherCardId, "completed");
25929
- }
25930
- try {
25931
- await client2.startAgentSession(cardId, {
25932
- agentIdentifier: "auto",
25933
- agentName: "Auto-detected Agent",
25934
- status: "working"
25935
- });
25936
- } catch {}
25937
- activeSessions.set(cardId, {
25938
- cardId,
25939
- startedAt: now,
25940
- lastActivityAt: now,
25941
- isExplicit: false,
25942
- agentIdentifier: "auto",
25943
- agentName: "Auto-detected Agent"
25944
- });
25945
- }
25946
- function markExplicit(cardId) {
25947
- const existing = activeSessions.get(cardId);
25948
- if (existing) {
25949
- existing.isExplicit = true;
25950
- } else {
25951
- activeSessions.set(cardId, {
25952
- cardId,
25953
- startedAt: Date.now(),
25954
- lastActivityAt: Date.now(),
25955
- isExplicit: true,
25956
- agentIdentifier: "explicit",
25957
- agentName: "Explicit Agent"
25958
- });
25959
- }
25960
- }
25961
- function untrack(cardId) {
25962
- activeSessions.delete(cardId);
25963
- }
25964
- async function shutdownAllSessions() {
25965
- const client2 = clientGetter?.();
25966
- if (!client2)
25967
- return;
25968
- const cardIds = [...activeSessions.keys()];
25969
- const promises = cardIds.map((cardId) => autoEndSession(client2, cardId, "paused"));
25970
- await Promise.allSettled(promises);
25971
- }
25972
- function destroyAutoSession() {
25973
- if (inactivityTimer) {
25974
- clearInterval(inactivityTimer);
25975
- inactivityTimer = null;
25976
- }
25977
- activeSessions.clear();
25978
- endCallback = null;
25979
- clientGetter = null;
25980
- }
25981
- function checkInactivity() {
25982
- const now = Date.now();
25983
- const client2 = clientGetter?.();
25984
- if (!client2)
25985
- return;
25986
- const entries = [...activeSessions.entries()];
25987
- for (const [cardId, session] of entries) {
25988
- if (session.isExplicit)
25989
- continue;
25990
- if (now - session.lastActivityAt > INACTIVITY_TIMEOUT_MS) {
25991
- autoEndSession(client2, cardId, "completed").catch(() => {});
25992
- }
25993
- }
25994
- }
25995
- async function autoEndSession(client2, cardId, status) {
25996
- activeSessions.delete(cardId);
25997
- try {
25998
- await client2.endAgentSession(cardId, { status });
25999
- } catch {}
26000
- try {
26001
- await endCallback?.(client2, cardId, status);
26002
- } catch {}
26003
- }
26004
-
26005
26086
  // src/api-client.ts
26006
26087
  var RETRY_CONFIG = {
26007
26088
  maxRetries: 3,
@@ -26302,6 +26383,22 @@ class HarmonyApiClient {
26302
26383
  const query = params.toString() ? `?${params.toString()}` : "";
26303
26384
  return this.request("GET", `/cards/${cardId}/agent-context${query}`);
26304
26385
  }
26386
+ async getAgentProfile(workspaceId, agentIdentifier) {
26387
+ const params = new URLSearchParams({
26388
+ workspace_id: workspaceId,
26389
+ agent_identifier: agentIdentifier
26390
+ });
26391
+ return this.request("GET", `/agent-profiles?${params.toString()}`);
26392
+ }
26393
+ async listAgentProfiles(workspaceId) {
26394
+ const params = new URLSearchParams({ workspace_id: workspaceId });
26395
+ return this.request("GET", `/agent-profiles?${params.toString()}`);
26396
+ }
26397
+ async refreshAgentProfiles(workspaceId) {
26398
+ return this.request("POST", "/agent-profiles/refresh", {
26399
+ workspace_id: workspaceId
26400
+ });
26401
+ }
26305
26402
  async createMemoryEntity(data) {
26306
26403
  return this.request("POST", "/memory/entities", data);
26307
26404
  }
@@ -26505,6 +26602,125 @@ function resetClient() {
26505
26602
  client2 = null;
26506
26603
  }
26507
26604
 
26605
+ // src/auto-session.ts
26606
+ var AUTO_START_TRIGGERS = new Set([
26607
+ "harmony_generate_prompt",
26608
+ "harmony_update_card",
26609
+ "harmony_move_card",
26610
+ "harmony_create_subtask",
26611
+ "harmony_toggle_subtask",
26612
+ "harmony_add_label_to_card",
26613
+ "harmony_remove_label_from_card"
26614
+ ]);
26615
+ var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
26616
+ var CHECK_INTERVAL_MS = 60 * 1000;
26617
+ var activeSessions = new Map;
26618
+ var inactivityTimer = null;
26619
+ var endCallback = null;
26620
+ var clientGetter = null;
26621
+ function initAutoSession(callback, getClient2) {
26622
+ endCallback = callback;
26623
+ clientGetter = getClient2;
26624
+ if (inactivityTimer)
26625
+ clearInterval(inactivityTimer);
26626
+ inactivityTimer = setInterval(checkInactivity, CHECK_INTERVAL_MS);
26627
+ }
26628
+ async function trackActivity(cardId, options) {
26629
+ const now = Date.now();
26630
+ const existing = activeSessions.get(cardId);
26631
+ if (existing) {
26632
+ existing.lastActivityAt = now;
26633
+ return;
26634
+ }
26635
+ if (!options?.autoStart)
26636
+ return;
26637
+ const client3 = options?.client ?? clientGetter?.();
26638
+ if (!client3)
26639
+ return;
26640
+ const toEnd = [];
26641
+ for (const [otherCardId, session] of activeSessions) {
26642
+ if (otherCardId !== cardId && !session.isExplicit) {
26643
+ toEnd.push(otherCardId);
26644
+ }
26645
+ }
26646
+ for (const otherCardId of toEnd) {
26647
+ await autoEndSession(client3, otherCardId, "completed");
26648
+ }
26649
+ try {
26650
+ await client3.startAgentSession(cardId, {
26651
+ agentIdentifier: "auto",
26652
+ agentName: "Auto-detected Agent",
26653
+ status: "working"
26654
+ });
26655
+ } catch {}
26656
+ activeSessions.set(cardId, {
26657
+ cardId,
26658
+ startedAt: now,
26659
+ lastActivityAt: now,
26660
+ isExplicit: false,
26661
+ agentIdentifier: "auto",
26662
+ agentName: "Auto-detected Agent"
26663
+ });
26664
+ }
26665
+ function markExplicit(cardId) {
26666
+ const existing = activeSessions.get(cardId);
26667
+ if (existing) {
26668
+ existing.isExplicit = true;
26669
+ } else {
26670
+ activeSessions.set(cardId, {
26671
+ cardId,
26672
+ startedAt: Date.now(),
26673
+ lastActivityAt: Date.now(),
26674
+ isExplicit: true,
26675
+ agentIdentifier: "explicit",
26676
+ agentName: "Explicit Agent"
26677
+ });
26678
+ }
26679
+ }
26680
+ function untrack(cardId) {
26681
+ activeSessions.delete(cardId);
26682
+ }
26683
+ async function shutdownAllSessions() {
26684
+ const client3 = clientGetter?.();
26685
+ if (!client3)
26686
+ return;
26687
+ const cardIds = [...activeSessions.keys()];
26688
+ const promises = cardIds.map((cardId) => autoEndSession(client3, cardId, "paused"));
26689
+ await Promise.allSettled(promises);
26690
+ }
26691
+ function destroyAutoSession() {
26692
+ if (inactivityTimer) {
26693
+ clearInterval(inactivityTimer);
26694
+ inactivityTimer = null;
26695
+ }
26696
+ activeSessions.clear();
26697
+ endCallback = null;
26698
+ clientGetter = null;
26699
+ }
26700
+ function checkInactivity() {
26701
+ const now = Date.now();
26702
+ const client3 = clientGetter?.();
26703
+ if (!client3)
26704
+ return;
26705
+ const entries = [...activeSessions.entries()];
26706
+ for (const [cardId, session] of entries) {
26707
+ if (session.isExplicit)
26708
+ continue;
26709
+ if (now - session.lastActivityAt > INACTIVITY_TIMEOUT_MS) {
26710
+ autoEndSession(client3, cardId, "completed").catch(() => {});
26711
+ }
26712
+ }
26713
+ }
26714
+ async function autoEndSession(client3, cardId, status) {
26715
+ activeSessions.delete(cardId);
26716
+ try {
26717
+ await client3.endAgentSession(cardId, { status });
26718
+ } catch {}
26719
+ try {
26720
+ await endCallback?.(client3, cardId, status);
26721
+ } catch {}
26722
+ }
26723
+
26508
26724
  // src/consolidation.ts
26509
26725
  async function consolidateMemories(client3, workspaceId, projectId, options) {
26510
26726
  const dryRun = options?.dryRun !== false;
@@ -28166,6 +28382,22 @@ var TOOLS = {
28166
28382
  required: ["cardId"]
28167
28383
  }
28168
28384
  },
28385
+ harmony_get_agent_profile: {
28386
+ description: "Get aggregate performance profile for an agent. Shows total sessions, completion rate, average duration, and more. Defaults to the current agent if no identifier provided.",
28387
+ inputSchema: {
28388
+ type: "object",
28389
+ properties: {
28390
+ agentIdentifier: {
28391
+ type: "string",
28392
+ description: "Agent identifier (e.g., 'claude-code'). Defaults to current agent."
28393
+ },
28394
+ workspaceId: {
28395
+ type: "string",
28396
+ description: "Workspace ID (optional if context set)"
28397
+ }
28398
+ }
28399
+ }
28400
+ },
28169
28401
  harmony_generate_prompt: {
28170
28402
  description: "Generate an AI-ready prompt from a card. Automatically infers role and focus based on labels (bug, feature, design, etc.). Use this to create context-rich prompts for working on cards.",
28171
28403
  inputSchema: {
@@ -29023,6 +29255,62 @@ async function runEndSessionPipeline(client3, deps, cardId, sessionStatus, endPr
29023
29255
  const learningResult = await extractLearnings(client3, sessionContext);
29024
29256
  learningsExtracted = learningResult.count;
29025
29257
  } catch {}
29258
+ try {
29259
+ const workspaceId = deps.getActiveWorkspaceId();
29260
+ const agentId = sessionData?.agent_identifier || "unknown";
29261
+ if (workspaceId && agentId !== "unknown") {
29262
+ (async () => {
29263
+ try {
29264
+ await client3.refreshAgentProfiles(workspaceId);
29265
+ const { profile } = await client3.getAgentProfile(workspaceId, agentId);
29266
+ if (profile) {
29267
+ const p = profile;
29268
+ const title = `Agent Profile: ${agentId}`;
29269
+ const content = [
29270
+ `## ${agentId} Performance Profile`,
29271
+ "",
29272
+ `- **Total sessions:** ${p.total_sessions}`,
29273
+ `- **Completed:** ${p.completed_sessions} (${p.completion_rate_pct}%)`,
29274
+ `- **Paused:** ${p.paused_sessions}`,
29275
+ `- **Blocked:** ${p.blocked_sessions}`,
29276
+ `- **Avg duration:** ${Math.round(Number(p.avg_active_duration_ms || 0) / 1000)}s`,
29277
+ `- **Avg progress:** ${p.avg_completion_progress}%`,
29278
+ `- **First session:** ${p.first_session_at}`,
29279
+ `- **Last session:** ${p.last_session_at}`
29280
+ ].join(`
29281
+ `);
29282
+ const existing = await client3.listMemoryEntities({
29283
+ workspace_id: workspaceId,
29284
+ type: "agent",
29285
+ limit: 50
29286
+ });
29287
+ const entities = existing.entities || [];
29288
+ const match = entities.find((e) => e.title === title || e.agent_identifier === agentId);
29289
+ if (match) {
29290
+ await client3.updateMemoryEntity(match.id, {
29291
+ content,
29292
+ confidence: 1,
29293
+ metadata: p
29294
+ });
29295
+ } else {
29296
+ await client3.createMemoryEntity({
29297
+ workspace_id: workspaceId,
29298
+ type: "agent",
29299
+ scope: "workspace",
29300
+ memory_tier: "reference",
29301
+ title,
29302
+ content,
29303
+ confidence: 1,
29304
+ tags: ["agent-profile", agentId],
29305
+ agent_identifier: agentId,
29306
+ metadata: p
29307
+ });
29308
+ }
29309
+ }
29310
+ } catch {}
29311
+ })();
29312
+ }
29313
+ } catch {}
29026
29314
  try {
29027
29315
  const feedbackResult = await recordContextFeedback(client3, cardId, sessionStatus, endProgressPercent, (sessionData?.blockers?.length ?? 0) > 0);
29028
29316
  feedbackAdjusted = feedbackResult.adjusted;
@@ -29517,18 +29805,18 @@ async function handleToolCall(name, args, deps) {
29517
29805
  });
29518
29806
  untrack(cardId);
29519
29807
  let movedTo = null;
29520
- let learningsExtracted = 0;
29521
- let cardTitle = "";
29522
- let cardLabels = [];
29523
- let cardDescription = "";
29524
- let cardSubtasks = [];
29808
+ const _learningsExtracted = 0;
29809
+ let _cardTitle = "";
29810
+ let _cardLabels = [];
29811
+ let _cardDescription = "";
29812
+ let _cardSubtasks = [];
29525
29813
  try {
29526
29814
  const { card } = await client3.getCard(cardId);
29527
29815
  const typedCard = card;
29528
- cardTitle = typedCard.title || "";
29529
- cardLabels = (typedCard.labels || []).map((l) => l.name);
29530
- cardDescription = typedCard.description || "";
29531
- cardSubtasks = (typedCard.subtasks || []).map((s) => ({
29816
+ _cardTitle = typedCard.title || "";
29817
+ _cardLabels = (typedCard.labels || []).map((l) => l.name);
29818
+ _cardDescription = typedCard.description || "";
29819
+ _cardSubtasks = (typedCard.subtasks || []).map((s) => ({
29532
29820
  title: s.title,
29533
29821
  done: s.done
29534
29822
  }));
@@ -29571,6 +29859,18 @@ async function handleToolCall(name, args, deps) {
29571
29859
  });
29572
29860
  return { success: true, ...result };
29573
29861
  }
29862
+ case "harmony_get_agent_profile": {
29863
+ const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
29864
+ if (!workspaceId) {
29865
+ return {
29866
+ success: false,
29867
+ error: "No workspace context. Provide workspaceId or set context."
29868
+ };
29869
+ }
29870
+ const agentIdentifier = args.agentIdentifier || "claude-code";
29871
+ const result = await client3.getAgentProfile(workspaceId, agentIdentifier);
29872
+ return { success: true, ...result };
29873
+ }
29574
29874
  case "harmony_generate_prompt": {
29575
29875
  let cardData;
29576
29876
  let columnData = null;
package/dist/index.js CHANGED
@@ -23075,7 +23075,8 @@ async function findSimilarEntities(client2, title, content, workspaceId, options
23075
23075
  try {
23076
23076
  const { entities } = await client2.searchMemoryEntities(workspaceId, query, {
23077
23077
  project_id: options?.projectId,
23078
- limit: options?.limit ?? 20
23078
+ limit: options?.limit ?? 20,
23079
+ type: options?.type
23079
23080
  });
23080
23081
  const minScore = options?.minRrfScore ?? 0;
23081
23082
  const excludeSet = new Set(options?.excludeIds || []);
@@ -23090,6 +23091,57 @@ async function findSimilarEntities(client2, title, content, workspaceId, options
23090
23091
  return [];
23091
23092
  }
23092
23093
  }
23094
+ var CAUSAL_LOOKUP = [
23095
+ {
23096
+ sourceType: "error",
23097
+ targetType: "solution",
23098
+ relation: "resolved_by",
23099
+ direction: "forward"
23100
+ },
23101
+ {
23102
+ sourceType: "solution",
23103
+ targetType: "error",
23104
+ relation: "resolved_by",
23105
+ direction: "reverse"
23106
+ },
23107
+ {
23108
+ sourceType: "lesson",
23109
+ targetType: "error",
23110
+ relation: "learned_from",
23111
+ direction: "forward"
23112
+ }
23113
+ ];
23114
+ async function linkCrossTypeNeighbors(client2, entityId, entityType, title, content, workspaceId, projectId) {
23115
+ const rules = CAUSAL_LOOKUP.filter((r) => r.sourceType === entityType);
23116
+ if (rules.length === 0)
23117
+ return { relationsCreated: 0 };
23118
+ let relationsCreated = 0;
23119
+ for (const rule of rules) {
23120
+ try {
23121
+ const matches = await findSimilarEntities(client2, title, content, workspaceId, {
23122
+ projectId,
23123
+ limit: 10,
23124
+ minRrfScore: 0.04,
23125
+ excludeIds: [entityId],
23126
+ type: rule.targetType
23127
+ });
23128
+ for (const match of matches.slice(0, 3)) {
23129
+ const sourceId = rule.direction === "forward" ? entityId : match.id;
23130
+ const targetId = rule.direction === "forward" ? match.id : entityId;
23131
+ try {
23132
+ await client2.createMemoryRelation({
23133
+ source_id: sourceId,
23134
+ target_id: targetId,
23135
+ relation_type: rule.relation,
23136
+ confidence: 0.65
23137
+ });
23138
+ relationsCreated++;
23139
+ } catch {}
23140
+ }
23141
+ } catch {}
23142
+ }
23143
+ return { relationsCreated };
23144
+ }
23093
23145
 
23094
23146
  // src/active-learning.ts
23095
23147
  var CONTRADICTION_TYPES = new Set([
@@ -23362,6 +23414,31 @@ Agent: ${session.agentName} | ${new Date().toISOString().split("T")[0]}`;
23362
23414
  }
23363
23415
  };
23364
23416
  }
23417
+ var SESSION_CAUSAL_RULES = [
23418
+ { sourceType: "error", targetType: "solution", relation: "resolved_by", confidence: 0.8 },
23419
+ { sourceType: "lesson", targetType: "error", relation: "learned_from", confidence: 0.75 }
23420
+ ];
23421
+ async function linkSessionEntities(client2, createdPairs, _workspaceId, _projectId) {
23422
+ let relationsCreated = 0;
23423
+ for (const rule of SESSION_CAUSAL_RULES) {
23424
+ const sources = createdPairs.filter((p) => p.learning.type === rule.sourceType);
23425
+ const targets = createdPairs.filter((p) => p.learning.type === rule.targetType);
23426
+ for (const source of sources) {
23427
+ for (const target of targets) {
23428
+ try {
23429
+ await client2.createMemoryRelation({
23430
+ source_id: source.id,
23431
+ target_id: target.id,
23432
+ relation_type: rule.relation,
23433
+ confidence: rule.confidence
23434
+ });
23435
+ relationsCreated++;
23436
+ } catch {}
23437
+ }
23438
+ }
23439
+ }
23440
+ return { relationsCreated };
23441
+ }
23365
23442
  async function extractLearnings(client2, session) {
23366
23443
  const workspaceId = getActiveWorkspaceId();
23367
23444
  if (!workspaceId) {
@@ -23503,10 +23580,17 @@ Agent: ${session.agentName}`,
23503
23580
  for (const { id, learning } of createdPairs) {
23504
23581
  autoExpandGraph(client2, id, learning.title, learning.content, learning.tags, workspaceId, projectId).catch(() => {});
23505
23582
  detectContradictions(client2, id, learning.type, learning.title, learning.content, learning.tags, workspaceId, projectId).catch(() => {});
23583
+ linkCrossTypeNeighbors(client2, id, learning.type, learning.title, learning.content, workspaceId, projectId).catch(() => {});
23584
+ }
23585
+ if (createdPairs.length >= 2) {
23586
+ linkSessionEntities(client2, createdPairs, workspaceId, projectId).catch(() => {});
23506
23587
  }
23507
23588
  if (entityIds.length > 0) {
23508
23589
  detectAndCreatePatterns(client2, entityIds, session, workspaceId, projectId).catch(() => {});
23509
23590
  }
23591
+ if (createdPairs.length > 0) {
23592
+ detectCausalPatterns(client2, createdPairs, session, workspaceId, projectId).catch(() => {});
23593
+ }
23510
23594
  clearMidSessionTracking(session.cardId);
23511
23595
  return { count: entityIds.length, entityIds };
23512
23596
  }
@@ -23597,6 +23681,122 @@ ${memberTitles.map((t) => `- ${t}`).join(`
23597
23681
  }
23598
23682
  return patternEntityIds;
23599
23683
  }
23684
+ var CAUSAL_PATTERN_THRESHOLD = 3;
23685
+ async function detectCausalPatterns(client2, createdPairs, session, workspaceId, projectId) {
23686
+ const patternIds = [];
23687
+ const errors3 = createdPairs.filter((p) => p.learning.type === "error");
23688
+ const solutions = createdPairs.filter((p) => p.learning.type === "solution");
23689
+ if (errors3.length === 0 || solutions.length === 0)
23690
+ return patternIds;
23691
+ for (const errorPair of errors3) {
23692
+ try {
23693
+ const similarErrors = await findSimilarEntities(client2, errorPair.learning.title, errorPair.learning.content, workspaceId, {
23694
+ projectId,
23695
+ limit: 20,
23696
+ minRrfScore: 0.03,
23697
+ excludeIds: createdPairs.map((p) => p.id),
23698
+ type: "error"
23699
+ });
23700
+ const resolvedErrors = [];
23701
+ for (const similar of similarErrors.slice(0, 10)) {
23702
+ try {
23703
+ const { outgoing } = await client2.getRelatedEntities(similar.id);
23704
+ const resolvedByRel = outgoing.find((r) => r.relation_type === "resolved_by");
23705
+ if (resolvedByRel) {
23706
+ resolvedErrors.push({
23707
+ errorId: similar.id,
23708
+ errorTitle: similar.title,
23709
+ solutionTitle: resolvedByRel.target_title || "unknown"
23710
+ });
23711
+ }
23712
+ } catch {}
23713
+ }
23714
+ if (resolvedErrors.length + 1 < CAUSAL_PATTERN_THRESHOLD)
23715
+ continue;
23716
+ const { entities: existingPatterns } = await client2.listMemoryEntities({
23717
+ workspace_id: workspaceId,
23718
+ project_id: projectId,
23719
+ type: "pattern",
23720
+ limit: 10
23721
+ });
23722
+ const matchingPattern = existingPatterns.find((p) => p.metadata?.pattern_chain_type === "error_resolved_by_solution");
23723
+ if (matchingPattern) {
23724
+ await client2.updateMemoryEntity(matchingPattern.id, {
23725
+ content: [
23726
+ `Recurring error→solution chain detected (${resolvedErrors.length + 1} instances).`,
23727
+ "",
23728
+ "## Error→Solution Pairs",
23729
+ `- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
23730
+ ...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`),
23731
+ "",
23732
+ `Last updated: ${new Date().toISOString()}`
23733
+ ].join(`
23734
+ `),
23735
+ metadata: {
23736
+ pattern_chain_type: "error_resolved_by_solution",
23737
+ pattern_count: resolvedErrors.length + 1,
23738
+ last_updated: new Date().toISOString()
23739
+ }
23740
+ });
23741
+ for (const pair of [errorPair, solutions[0]]) {
23742
+ try {
23743
+ await client2.createMemoryRelation({
23744
+ source_id: pair.id,
23745
+ target_id: matchingPattern.id,
23746
+ relation_type: "part_of",
23747
+ confidence: 0.75
23748
+ });
23749
+ } catch {}
23750
+ }
23751
+ } else {
23752
+ const result = await client2.createMemoryEntity({
23753
+ workspace_id: workspaceId,
23754
+ project_id: projectId,
23755
+ type: "pattern",
23756
+ scope: "project",
23757
+ memory_tier: "reference",
23758
+ title: `Pattern: recurring error→solution chain (${resolvedErrors.length + 1} instances)`,
23759
+ content: [
23760
+ `Recurring error→solution chain detected across ${resolvedErrors.length + 1} sessions.`,
23761
+ "",
23762
+ "## Error→Solution Pairs",
23763
+ `- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
23764
+ ...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`)
23765
+ ].join(`
23766
+ `),
23767
+ confidence: 0.8,
23768
+ tags: ["auto-extracted", "pattern", "causal-chain"],
23769
+ metadata: {
23770
+ source: "causal_pattern_detection",
23771
+ pattern_chain_type: "error_resolved_by_solution",
23772
+ pattern_count: resolvedErrors.length + 1
23773
+ },
23774
+ agent_identifier: session.agentIdentifier
23775
+ });
23776
+ const created = result.entity;
23777
+ if (created?.id) {
23778
+ patternIds.push(created.id);
23779
+ const memberIds = [
23780
+ errorPair.id,
23781
+ solutions[0].id,
23782
+ ...resolvedErrors.slice(0, 4).map((r) => r.errorId)
23783
+ ];
23784
+ for (const memberId of memberIds) {
23785
+ try {
23786
+ await client2.createMemoryRelation({
23787
+ source_id: memberId,
23788
+ target_id: created.id,
23789
+ relation_type: "part_of",
23790
+ confidence: 0.75
23791
+ });
23792
+ } catch {}
23793
+ }
23794
+ }
23795
+ }
23796
+ } catch {}
23797
+ }
23798
+ return patternIds;
23799
+ }
23600
23800
  async function detectContradictions(client2, entityId, entityType, title, content, tags, workspaceId, projectId) {
23601
23801
  if (!CONTRADICTION_TYPES.has(entityType))
23602
23802
  return [];
@@ -23643,125 +23843,6 @@ async function detectContradictions(client2, entityId, entityType, title, conten
23643
23843
  }
23644
23844
  }
23645
23845
 
23646
- // src/auto-session.ts
23647
- var AUTO_START_TRIGGERS = new Set([
23648
- "harmony_generate_prompt",
23649
- "harmony_update_card",
23650
- "harmony_move_card",
23651
- "harmony_create_subtask",
23652
- "harmony_toggle_subtask",
23653
- "harmony_add_label_to_card",
23654
- "harmony_remove_label_from_card"
23655
- ]);
23656
- var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
23657
- var CHECK_INTERVAL_MS = 60 * 1000;
23658
- var activeSessions = new Map;
23659
- var inactivityTimer = null;
23660
- var endCallback = null;
23661
- var clientGetter = null;
23662
- function initAutoSession(callback, getClient) {
23663
- endCallback = callback;
23664
- clientGetter = getClient;
23665
- if (inactivityTimer)
23666
- clearInterval(inactivityTimer);
23667
- inactivityTimer = setInterval(checkInactivity, CHECK_INTERVAL_MS);
23668
- }
23669
- async function trackActivity(cardId, options) {
23670
- const now = Date.now();
23671
- const existing = activeSessions.get(cardId);
23672
- if (existing) {
23673
- existing.lastActivityAt = now;
23674
- return;
23675
- }
23676
- if (!options?.autoStart)
23677
- return;
23678
- const client2 = options?.client ?? clientGetter?.();
23679
- if (!client2)
23680
- return;
23681
- const toEnd = [];
23682
- for (const [otherCardId, session] of activeSessions) {
23683
- if (otherCardId !== cardId && !session.isExplicit) {
23684
- toEnd.push(otherCardId);
23685
- }
23686
- }
23687
- for (const otherCardId of toEnd) {
23688
- await autoEndSession(client2, otherCardId, "completed");
23689
- }
23690
- try {
23691
- await client2.startAgentSession(cardId, {
23692
- agentIdentifier: "auto",
23693
- agentName: "Auto-detected Agent",
23694
- status: "working"
23695
- });
23696
- } catch {}
23697
- activeSessions.set(cardId, {
23698
- cardId,
23699
- startedAt: now,
23700
- lastActivityAt: now,
23701
- isExplicit: false,
23702
- agentIdentifier: "auto",
23703
- agentName: "Auto-detected Agent"
23704
- });
23705
- }
23706
- function markExplicit(cardId) {
23707
- const existing = activeSessions.get(cardId);
23708
- if (existing) {
23709
- existing.isExplicit = true;
23710
- } else {
23711
- activeSessions.set(cardId, {
23712
- cardId,
23713
- startedAt: Date.now(),
23714
- lastActivityAt: Date.now(),
23715
- isExplicit: true,
23716
- agentIdentifier: "explicit",
23717
- agentName: "Explicit Agent"
23718
- });
23719
- }
23720
- }
23721
- function untrack(cardId) {
23722
- activeSessions.delete(cardId);
23723
- }
23724
- async function shutdownAllSessions() {
23725
- const client2 = clientGetter?.();
23726
- if (!client2)
23727
- return;
23728
- const cardIds = [...activeSessions.keys()];
23729
- const promises = cardIds.map((cardId) => autoEndSession(client2, cardId, "paused"));
23730
- await Promise.allSettled(promises);
23731
- }
23732
- function destroyAutoSession() {
23733
- if (inactivityTimer) {
23734
- clearInterval(inactivityTimer);
23735
- inactivityTimer = null;
23736
- }
23737
- activeSessions.clear();
23738
- endCallback = null;
23739
- clientGetter = null;
23740
- }
23741
- function checkInactivity() {
23742
- const now = Date.now();
23743
- const client2 = clientGetter?.();
23744
- if (!client2)
23745
- return;
23746
- const entries = [...activeSessions.entries()];
23747
- for (const [cardId, session] of entries) {
23748
- if (session.isExplicit)
23749
- continue;
23750
- if (now - session.lastActivityAt > INACTIVITY_TIMEOUT_MS) {
23751
- autoEndSession(client2, cardId, "completed").catch(() => {});
23752
- }
23753
- }
23754
- }
23755
- async function autoEndSession(client2, cardId, status) {
23756
- activeSessions.delete(cardId);
23757
- try {
23758
- await client2.endAgentSession(cardId, { status });
23759
- } catch {}
23760
- try {
23761
- await endCallback?.(client2, cardId, status);
23762
- } catch {}
23763
- }
23764
-
23765
23846
  // src/api-client.ts
23766
23847
  var RETRY_CONFIG = {
23767
23848
  maxRetries: 3,
@@ -24062,6 +24143,22 @@ class HarmonyApiClient {
24062
24143
  const query = params.toString() ? `?${params.toString()}` : "";
24063
24144
  return this.request("GET", `/cards/${cardId}/agent-context${query}`);
24064
24145
  }
24146
+ async getAgentProfile(workspaceId, agentIdentifier) {
24147
+ const params = new URLSearchParams({
24148
+ workspace_id: workspaceId,
24149
+ agent_identifier: agentIdentifier
24150
+ });
24151
+ return this.request("GET", `/agent-profiles?${params.toString()}`);
24152
+ }
24153
+ async listAgentProfiles(workspaceId) {
24154
+ const params = new URLSearchParams({ workspace_id: workspaceId });
24155
+ return this.request("GET", `/agent-profiles?${params.toString()}`);
24156
+ }
24157
+ async refreshAgentProfiles(workspaceId) {
24158
+ return this.request("POST", "/agent-profiles/refresh", {
24159
+ workspace_id: workspaceId
24160
+ });
24161
+ }
24065
24162
  async createMemoryEntity(data) {
24066
24163
  return this.request("POST", "/memory/entities", data);
24067
24164
  }
@@ -24265,6 +24362,125 @@ function resetClient() {
24265
24362
  client2 = null;
24266
24363
  }
24267
24364
 
24365
+ // src/auto-session.ts
24366
+ var AUTO_START_TRIGGERS = new Set([
24367
+ "harmony_generate_prompt",
24368
+ "harmony_update_card",
24369
+ "harmony_move_card",
24370
+ "harmony_create_subtask",
24371
+ "harmony_toggle_subtask",
24372
+ "harmony_add_label_to_card",
24373
+ "harmony_remove_label_from_card"
24374
+ ]);
24375
+ var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
24376
+ var CHECK_INTERVAL_MS = 60 * 1000;
24377
+ var activeSessions = new Map;
24378
+ var inactivityTimer = null;
24379
+ var endCallback = null;
24380
+ var clientGetter = null;
24381
+ function initAutoSession(callback, getClient2) {
24382
+ endCallback = callback;
24383
+ clientGetter = getClient2;
24384
+ if (inactivityTimer)
24385
+ clearInterval(inactivityTimer);
24386
+ inactivityTimer = setInterval(checkInactivity, CHECK_INTERVAL_MS);
24387
+ }
24388
+ async function trackActivity(cardId, options) {
24389
+ const now = Date.now();
24390
+ const existing = activeSessions.get(cardId);
24391
+ if (existing) {
24392
+ existing.lastActivityAt = now;
24393
+ return;
24394
+ }
24395
+ if (!options?.autoStart)
24396
+ return;
24397
+ const client3 = options?.client ?? clientGetter?.();
24398
+ if (!client3)
24399
+ return;
24400
+ const toEnd = [];
24401
+ for (const [otherCardId, session] of activeSessions) {
24402
+ if (otherCardId !== cardId && !session.isExplicit) {
24403
+ toEnd.push(otherCardId);
24404
+ }
24405
+ }
24406
+ for (const otherCardId of toEnd) {
24407
+ await autoEndSession(client3, otherCardId, "completed");
24408
+ }
24409
+ try {
24410
+ await client3.startAgentSession(cardId, {
24411
+ agentIdentifier: "auto",
24412
+ agentName: "Auto-detected Agent",
24413
+ status: "working"
24414
+ });
24415
+ } catch {}
24416
+ activeSessions.set(cardId, {
24417
+ cardId,
24418
+ startedAt: now,
24419
+ lastActivityAt: now,
24420
+ isExplicit: false,
24421
+ agentIdentifier: "auto",
24422
+ agentName: "Auto-detected Agent"
24423
+ });
24424
+ }
24425
+ function markExplicit(cardId) {
24426
+ const existing = activeSessions.get(cardId);
24427
+ if (existing) {
24428
+ existing.isExplicit = true;
24429
+ } else {
24430
+ activeSessions.set(cardId, {
24431
+ cardId,
24432
+ startedAt: Date.now(),
24433
+ lastActivityAt: Date.now(),
24434
+ isExplicit: true,
24435
+ agentIdentifier: "explicit",
24436
+ agentName: "Explicit Agent"
24437
+ });
24438
+ }
24439
+ }
24440
+ function untrack(cardId) {
24441
+ activeSessions.delete(cardId);
24442
+ }
24443
+ async function shutdownAllSessions() {
24444
+ const client3 = clientGetter?.();
24445
+ if (!client3)
24446
+ return;
24447
+ const cardIds = [...activeSessions.keys()];
24448
+ const promises = cardIds.map((cardId) => autoEndSession(client3, cardId, "paused"));
24449
+ await Promise.allSettled(promises);
24450
+ }
24451
+ function destroyAutoSession() {
24452
+ if (inactivityTimer) {
24453
+ clearInterval(inactivityTimer);
24454
+ inactivityTimer = null;
24455
+ }
24456
+ activeSessions.clear();
24457
+ endCallback = null;
24458
+ clientGetter = null;
24459
+ }
24460
+ function checkInactivity() {
24461
+ const now = Date.now();
24462
+ const client3 = clientGetter?.();
24463
+ if (!client3)
24464
+ return;
24465
+ const entries = [...activeSessions.entries()];
24466
+ for (const [cardId, session] of entries) {
24467
+ if (session.isExplicit)
24468
+ continue;
24469
+ if (now - session.lastActivityAt > INACTIVITY_TIMEOUT_MS) {
24470
+ autoEndSession(client3, cardId, "completed").catch(() => {});
24471
+ }
24472
+ }
24473
+ }
24474
+ async function autoEndSession(client3, cardId, status) {
24475
+ activeSessions.delete(cardId);
24476
+ try {
24477
+ await client3.endAgentSession(cardId, { status });
24478
+ } catch {}
24479
+ try {
24480
+ await endCallback?.(client3, cardId, status);
24481
+ } catch {}
24482
+ }
24483
+
24268
24484
  // src/consolidation.ts
24269
24485
  async function consolidateMemories(client3, workspaceId, projectId, options) {
24270
24486
  const dryRun = options?.dryRun !== false;
@@ -25926,6 +26142,22 @@ var TOOLS = {
25926
26142
  required: ["cardId"]
25927
26143
  }
25928
26144
  },
26145
+ harmony_get_agent_profile: {
26146
+ description: "Get aggregate performance profile for an agent. Shows total sessions, completion rate, average duration, and more. Defaults to the current agent if no identifier provided.",
26147
+ inputSchema: {
26148
+ type: "object",
26149
+ properties: {
26150
+ agentIdentifier: {
26151
+ type: "string",
26152
+ description: "Agent identifier (e.g., 'claude-code'). Defaults to current agent."
26153
+ },
26154
+ workspaceId: {
26155
+ type: "string",
26156
+ description: "Workspace ID (optional if context set)"
26157
+ }
26158
+ }
26159
+ }
26160
+ },
25929
26161
  harmony_generate_prompt: {
25930
26162
  description: "Generate an AI-ready prompt from a card. Automatically infers role and focus based on labels (bug, feature, design, etc.). Use this to create context-rich prompts for working on cards.",
25931
26163
  inputSchema: {
@@ -26783,6 +27015,62 @@ async function runEndSessionPipeline(client3, deps, cardId, sessionStatus, endPr
26783
27015
  const learningResult = await extractLearnings(client3, sessionContext);
26784
27016
  learningsExtracted = learningResult.count;
26785
27017
  } catch {}
27018
+ try {
27019
+ const workspaceId = deps.getActiveWorkspaceId();
27020
+ const agentId = sessionData?.agent_identifier || "unknown";
27021
+ if (workspaceId && agentId !== "unknown") {
27022
+ (async () => {
27023
+ try {
27024
+ await client3.refreshAgentProfiles(workspaceId);
27025
+ const { profile } = await client3.getAgentProfile(workspaceId, agentId);
27026
+ if (profile) {
27027
+ const p = profile;
27028
+ const title = `Agent Profile: ${agentId}`;
27029
+ const content = [
27030
+ `## ${agentId} Performance Profile`,
27031
+ "",
27032
+ `- **Total sessions:** ${p.total_sessions}`,
27033
+ `- **Completed:** ${p.completed_sessions} (${p.completion_rate_pct}%)`,
27034
+ `- **Paused:** ${p.paused_sessions}`,
27035
+ `- **Blocked:** ${p.blocked_sessions}`,
27036
+ `- **Avg duration:** ${Math.round(Number(p.avg_active_duration_ms || 0) / 1000)}s`,
27037
+ `- **Avg progress:** ${p.avg_completion_progress}%`,
27038
+ `- **First session:** ${p.first_session_at}`,
27039
+ `- **Last session:** ${p.last_session_at}`
27040
+ ].join(`
27041
+ `);
27042
+ const existing = await client3.listMemoryEntities({
27043
+ workspace_id: workspaceId,
27044
+ type: "agent",
27045
+ limit: 50
27046
+ });
27047
+ const entities = existing.entities || [];
27048
+ const match = entities.find((e) => e.title === title || e.agent_identifier === agentId);
27049
+ if (match) {
27050
+ await client3.updateMemoryEntity(match.id, {
27051
+ content,
27052
+ confidence: 1,
27053
+ metadata: p
27054
+ });
27055
+ } else {
27056
+ await client3.createMemoryEntity({
27057
+ workspace_id: workspaceId,
27058
+ type: "agent",
27059
+ scope: "workspace",
27060
+ memory_tier: "reference",
27061
+ title,
27062
+ content,
27063
+ confidence: 1,
27064
+ tags: ["agent-profile", agentId],
27065
+ agent_identifier: agentId,
27066
+ metadata: p
27067
+ });
27068
+ }
27069
+ }
27070
+ } catch {}
27071
+ })();
27072
+ }
27073
+ } catch {}
26786
27074
  try {
26787
27075
  const feedbackResult = await recordContextFeedback(client3, cardId, sessionStatus, endProgressPercent, (sessionData?.blockers?.length ?? 0) > 0);
26788
27076
  feedbackAdjusted = feedbackResult.adjusted;
@@ -27277,18 +27565,18 @@ async function handleToolCall(name, args, deps) {
27277
27565
  });
27278
27566
  untrack(cardId);
27279
27567
  let movedTo = null;
27280
- let learningsExtracted = 0;
27281
- let cardTitle = "";
27282
- let cardLabels = [];
27283
- let cardDescription = "";
27284
- let cardSubtasks = [];
27568
+ const _learningsExtracted = 0;
27569
+ let _cardTitle = "";
27570
+ let _cardLabels = [];
27571
+ let _cardDescription = "";
27572
+ let _cardSubtasks = [];
27285
27573
  try {
27286
27574
  const { card } = await client3.getCard(cardId);
27287
27575
  const typedCard = card;
27288
- cardTitle = typedCard.title || "";
27289
- cardLabels = (typedCard.labels || []).map((l) => l.name);
27290
- cardDescription = typedCard.description || "";
27291
- cardSubtasks = (typedCard.subtasks || []).map((s) => ({
27576
+ _cardTitle = typedCard.title || "";
27577
+ _cardLabels = (typedCard.labels || []).map((l) => l.name);
27578
+ _cardDescription = typedCard.description || "";
27579
+ _cardSubtasks = (typedCard.subtasks || []).map((s) => ({
27292
27580
  title: s.title,
27293
27581
  done: s.done
27294
27582
  }));
@@ -27331,6 +27619,18 @@ async function handleToolCall(name, args, deps) {
27331
27619
  });
27332
27620
  return { success: true, ...result };
27333
27621
  }
27622
+ case "harmony_get_agent_profile": {
27623
+ const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
27624
+ if (!workspaceId) {
27625
+ return {
27626
+ success: false,
27627
+ error: "No workspace context. Provide workspaceId or set context."
27628
+ };
27629
+ }
27630
+ const agentIdentifier = args.agentIdentifier || "claude-code";
27631
+ const result = await client3.getAgentProfile(workspaceId, agentIdentifier);
27632
+ return { success: true, ...result };
27633
+ }
27334
27634
  case "harmony_generate_prompt": {
27335
27635
  let cardData;
27336
27636
  let columnData = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harmony-mcp",
3
- "version": "1.14.0",
3
+ "version": "1.14.2",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"