@gethmy/mcp 2.2.3 → 2.2.4
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/cli.js +201 -18
- package/dist/index.js +165 -17
- package/dist/lib/context-assembly.js +218 -26
- package/dist/lib/skills.js +25 -1
- package/dist/lib/tui/setup.js +11 -0
- package/package.json +1 -1
- package/src/context-assembly.ts +267 -26
- package/src/skills.ts +25 -1
- package/src/tui/setup.ts +11 -0
package/src/context-assembly.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* for a given task, producing a manifest of what was included/excluded.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import type { GraphRelation } from "@harmony/memory";
|
|
9
|
+
import { checkPromotion, discoverRelatedContext } from "@harmony/memory";
|
|
9
10
|
import type { HarmonyApiClient } from "./api-client.js";
|
|
10
11
|
|
|
11
12
|
// Types
|
|
@@ -68,6 +69,13 @@ export interface AssembleContextOptions {
|
|
|
68
69
|
cardId?: string;
|
|
69
70
|
tokenBudget?: number; // Default: 4000 tokens
|
|
70
71
|
client: HarmonyApiClient;
|
|
72
|
+
graphWalkEnabled?: boolean; // Default: true — enrich candidates via knowledge graph relations
|
|
73
|
+
queryExpansionEnabled?: boolean; // Default: true — expand query with synonyms/variations
|
|
74
|
+
enableLlmReranking?: boolean; // Default: false — LLM re-ranking when scores are clustered
|
|
75
|
+
rerankFn?: (
|
|
76
|
+
taskContext: string,
|
|
77
|
+
candidates: Array<{ id: string; title: string; snippet: string }>,
|
|
78
|
+
) => Promise<string[]>; // Custom re-rank function
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
export interface AssembledContext {
|
|
@@ -101,6 +109,53 @@ const TIER_BUDGET_ALLOCATION: Record<MemoryTier, number> = {
|
|
|
101
109
|
// Minimum guaranteed slots per tier
|
|
102
110
|
const MIN_REFERENCE_SLOTS = 3;
|
|
103
111
|
|
|
112
|
+
// Graph walk configuration
|
|
113
|
+
const GRAPH_WALK_MAX_DEPTH = 1;
|
|
114
|
+
const GRAPH_WALK_MAX_ENTITIES = 10;
|
|
115
|
+
const GRAPH_WALK_MIN_CONFIDENCE = 0.5;
|
|
116
|
+
const GRAPH_WALK_SEED_COUNT = 5;
|
|
117
|
+
|
|
118
|
+
// Query expansion configuration
|
|
119
|
+
const MAX_QUERY_VARIATIONS = 4;
|
|
120
|
+
|
|
121
|
+
// LLM re-ranking configuration
|
|
122
|
+
const RERANK_CLUSTER_THRESHOLD = 0.05;
|
|
123
|
+
const RERANK_TOP_N = 10;
|
|
124
|
+
const RERANK_MIN_CANDIDATES = 5;
|
|
125
|
+
|
|
126
|
+
// Graph walk relation-type bonuses for relevance scoring
|
|
127
|
+
const RELATION_BONUSES: Record<string, number> = {
|
|
128
|
+
depends_on: 0.15,
|
|
129
|
+
resolved_by: 0.2,
|
|
130
|
+
relates_to: 0.1,
|
|
131
|
+
implements: 0.15,
|
|
132
|
+
blocks: 0.15,
|
|
133
|
+
references: 0.1,
|
|
134
|
+
extends: 0.1,
|
|
135
|
+
caused_by: 0.15,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Synonym map for query expansion (common dev term variations)
|
|
139
|
+
// NOTE: Avoid circular references (auth->login, login->auth) — first synonym
|
|
140
|
+
// is used for replacement, so each key should expand to non-overlapping terms.
|
|
141
|
+
const QUERY_SYNONYMS: Record<string, string[]> = {
|
|
142
|
+
auth: ["authentication", "authorization", "session"],
|
|
143
|
+
authentication: ["auth", "session", "sign-in"],
|
|
144
|
+
login: ["sign-in", "authentication", "session"],
|
|
145
|
+
bug: ["error", "issue", "defect", "problem"],
|
|
146
|
+
error: ["exception", "failure", "issue"],
|
|
147
|
+
fix: ["resolve", "patch", "repair", "correct"],
|
|
148
|
+
deploy: ["deployment", "release", "ship", "publish"],
|
|
149
|
+
test: ["testing", "spec", "assertion", "verify"],
|
|
150
|
+
config: ["configuration", "settings", "setup"],
|
|
151
|
+
db: ["database", "storage", "persistence"],
|
|
152
|
+
database: ["storage", "persistence", "data store"],
|
|
153
|
+
api: ["endpoint", "route", "service"],
|
|
154
|
+
ui: ["frontend", "component", "view"],
|
|
155
|
+
perf: ["performance", "speed", "latency"],
|
|
156
|
+
performance: ["speed", "latency", "optimization"],
|
|
157
|
+
};
|
|
158
|
+
|
|
104
159
|
/**
|
|
105
160
|
* Estimate token count (rough: 1 token per 4 chars)
|
|
106
161
|
*/
|
|
@@ -154,6 +209,73 @@ function truncateContent(
|
|
|
154
209
|
return { text: result, truncated: true };
|
|
155
210
|
}
|
|
156
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Escape regex metacharacters in a string for safe use in RegExp constructor.
|
|
214
|
+
*/
|
|
215
|
+
function escapeRegex(str: string): string {
|
|
216
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Expand a query into multiple search variations using synonym substitution.
|
|
221
|
+
* Returns the original query plus up to 3 additional variations (4 total).
|
|
222
|
+
*/
|
|
223
|
+
export function expandQuery(taskContext: string): string[] {
|
|
224
|
+
const queries = [taskContext];
|
|
225
|
+
const lowerQueries = [taskContext.toLowerCase()];
|
|
226
|
+
const words = taskContext
|
|
227
|
+
.toLowerCase()
|
|
228
|
+
.split(/\W+/)
|
|
229
|
+
.filter((w) => w.length > 2);
|
|
230
|
+
|
|
231
|
+
// Find words that have synonym expansions
|
|
232
|
+
const expandableWords = words.filter((w) => QUERY_SYNONYMS[w]);
|
|
233
|
+
|
|
234
|
+
for (const word of expandableWords) {
|
|
235
|
+
const synonyms = QUERY_SYNONYMS[word];
|
|
236
|
+
if (!synonyms) continue;
|
|
237
|
+
// Create a variation by replacing the word with its first synonym
|
|
238
|
+
const variation = taskContext.replace(
|
|
239
|
+
new RegExp(`\\b${escapeRegex(word)}\\b`, "gi"),
|
|
240
|
+
synonyms[0],
|
|
241
|
+
);
|
|
242
|
+
const lowerVariation = variation.toLowerCase();
|
|
243
|
+
if (
|
|
244
|
+
lowerVariation !== taskContext.toLowerCase() &&
|
|
245
|
+
!lowerQueries.includes(lowerVariation)
|
|
246
|
+
) {
|
|
247
|
+
queries.push(variation);
|
|
248
|
+
lowerQueries.push(lowerVariation);
|
|
249
|
+
}
|
|
250
|
+
if (queries.length >= MAX_QUERY_VARIATIONS) break;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Also extract key noun phrases as a compact query
|
|
254
|
+
if (words.length >= 3) {
|
|
255
|
+
const keyPhrases = words
|
|
256
|
+
.filter(
|
|
257
|
+
(w) =>
|
|
258
|
+
![
|
|
259
|
+
"the",
|
|
260
|
+
"and",
|
|
261
|
+
"for",
|
|
262
|
+
"with",
|
|
263
|
+
"this",
|
|
264
|
+
"that",
|
|
265
|
+
"from",
|
|
266
|
+
"into",
|
|
267
|
+
].includes(w),
|
|
268
|
+
)
|
|
269
|
+
.slice(0, 4)
|
|
270
|
+
.join(" ");
|
|
271
|
+
if (!lowerQueries.includes(keyPhrases)) {
|
|
272
|
+
queries.push(keyPhrases);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return queries.slice(0, MAX_QUERY_VARIATIONS);
|
|
277
|
+
}
|
|
278
|
+
|
|
157
279
|
/**
|
|
158
280
|
* Compute relevance score for an entity against task context.
|
|
159
281
|
*/
|
|
@@ -161,6 +283,7 @@ export function computeRelevanceScore(
|
|
|
161
283
|
entity: ContextEntity,
|
|
162
284
|
taskContext: string,
|
|
163
285
|
cardLabels: string[],
|
|
286
|
+
graphRelations?: GraphRelation[],
|
|
164
287
|
): { score: number; reasons: string[] } {
|
|
165
288
|
const reasons: string[] = [];
|
|
166
289
|
let score = 0;
|
|
@@ -255,8 +378,29 @@ export function computeRelevanceScore(
|
|
|
255
378
|
reasons.push("procedure_boost");
|
|
256
379
|
}
|
|
257
380
|
|
|
381
|
+
// 7. Graph walk relation bonus: boost entities discovered via knowledge graph
|
|
382
|
+
if (graphRelations && graphRelations.length > 0) {
|
|
383
|
+
const entityRelations = graphRelations.filter(
|
|
384
|
+
(r) => r.source_id === entity.id || r.target_id === entity.id,
|
|
385
|
+
);
|
|
386
|
+
if (entityRelations.length > 0) {
|
|
387
|
+
// Take the highest relation bonus (don't stack all of them)
|
|
388
|
+
let bestBonus = 0;
|
|
389
|
+
let bestRelType = "";
|
|
390
|
+
for (const rel of entityRelations) {
|
|
391
|
+
const bonus = RELATION_BONUSES[rel.relation_type] ?? 0.1;
|
|
392
|
+
if (bonus > bestBonus) {
|
|
393
|
+
bestBonus = bonus;
|
|
394
|
+
bestRelType = rel.relation_type;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
score += bestBonus;
|
|
398
|
+
reasons.push(`graph_walk(${bestRelType})`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
258
402
|
// Clamp raw score to 0-1 range before applying tier weight
|
|
259
|
-
score = Math.min(score, 1.0);
|
|
403
|
+
score = Math.max(0, Math.min(score, 1.0));
|
|
260
404
|
|
|
261
405
|
// Apply tier weight
|
|
262
406
|
const tierWeight = TIER_WEIGHTS[entity.memory_tier];
|
|
@@ -278,6 +422,10 @@ export async function assembleContext(
|
|
|
278
422
|
cardLabels = [],
|
|
279
423
|
tokenBudget = DEFAULT_TOKEN_BUDGET,
|
|
280
424
|
client,
|
|
425
|
+
graphWalkEnabled = true,
|
|
426
|
+
queryExpansionEnabled = true,
|
|
427
|
+
enableLlmReranking = false,
|
|
428
|
+
rerankFn,
|
|
281
429
|
} = options;
|
|
282
430
|
|
|
283
431
|
const assemblyId = generateAssemblyId();
|
|
@@ -295,21 +443,35 @@ export async function assembleContext(
|
|
|
295
443
|
},
|
|
296
444
|
};
|
|
297
445
|
|
|
298
|
-
// Fetch candidate entities: search by task context + list by project
|
|
299
|
-
|
|
446
|
+
// Fetch candidate entities: search by task context (with query expansion) + list by project
|
|
447
|
+
const candidates: ContextEntity[] = [];
|
|
300
448
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
|
|
449
|
+
// P1: Query expansion — search with multiple query variations to catch synonym mismatches
|
|
450
|
+
const queries = queryExpansionEnabled
|
|
451
|
+
? expandQuery(taskContext)
|
|
452
|
+
: [taskContext];
|
|
453
|
+
|
|
454
|
+
const searchResults = await Promise.allSettled(
|
|
455
|
+
queries.map((query) =>
|
|
456
|
+
client.searchMemoryEntities(workspaceId, query, {
|
|
457
|
+
project_id: projectId,
|
|
458
|
+
limit: 30,
|
|
459
|
+
}),
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
const candidateIds = new Set<string>();
|
|
464
|
+
for (const result of searchResults) {
|
|
465
|
+
if (result.status !== "fulfilled") continue;
|
|
466
|
+
if (result.value.entities?.length > 0) {
|
|
467
|
+
for (const raw of result.value.entities) {
|
|
468
|
+
const entity = mapToContextEntity(raw);
|
|
469
|
+
if (!candidateIds.has(entity.id)) {
|
|
470
|
+
candidateIds.add(entity.id);
|
|
471
|
+
candidates.push(entity);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
310
474
|
}
|
|
311
|
-
} catch {
|
|
312
|
-
// Search failed, fall back to listing
|
|
313
475
|
}
|
|
314
476
|
|
|
315
477
|
// Also fetch by project scope if we have few candidates
|
|
@@ -321,11 +483,13 @@ export async function assembleContext(
|
|
|
321
483
|
limit: 30,
|
|
322
484
|
});
|
|
323
485
|
if (listResult.entities?.length > 0) {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
.
|
|
327
|
-
|
|
328
|
-
|
|
486
|
+
for (const raw of listResult.entities) {
|
|
487
|
+
const entity = mapToContextEntity(raw);
|
|
488
|
+
if (!candidateIds.has(entity.id)) {
|
|
489
|
+
candidateIds.add(entity.id);
|
|
490
|
+
candidates.push(entity);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
329
493
|
}
|
|
330
494
|
} catch {
|
|
331
495
|
// List failed, continue with what we have
|
|
@@ -342,17 +506,61 @@ export async function assembleContext(
|
|
|
342
506
|
limit: 20,
|
|
343
507
|
});
|
|
344
508
|
if (wsResult.entities?.length > 0) {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
.
|
|
348
|
-
|
|
349
|
-
|
|
509
|
+
for (const raw of wsResult.entities) {
|
|
510
|
+
const entity = mapToContextEntity(raw);
|
|
511
|
+
if (!candidateIds.has(entity.id)) {
|
|
512
|
+
candidateIds.add(entity.id);
|
|
513
|
+
candidates.push(entity);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
350
516
|
}
|
|
351
517
|
} catch {
|
|
352
518
|
// Continue with what we have
|
|
353
519
|
}
|
|
354
520
|
}
|
|
355
521
|
|
|
522
|
+
// P0: Graph walk enrichment — discover related entities via knowledge graph
|
|
523
|
+
let graphRelations: GraphRelation[] = [];
|
|
524
|
+
if (graphWalkEnabled && candidates.length > 0) {
|
|
525
|
+
try {
|
|
526
|
+
// Take top candidates by RRF score (or first N if no RRF scores)
|
|
527
|
+
const seedCandidates = [...candidates]
|
|
528
|
+
.sort((a, b) => (b.rrf_score ?? 0) - (a.rrf_score ?? 0))
|
|
529
|
+
.slice(0, GRAPH_WALK_SEED_COUNT);
|
|
530
|
+
const seedIds = seedCandidates.map((c) => c.id);
|
|
531
|
+
|
|
532
|
+
const walkResult = await discoverRelatedContext(
|
|
533
|
+
client,
|
|
534
|
+
seedIds,
|
|
535
|
+
GRAPH_WALK_MAX_DEPTH,
|
|
536
|
+
GRAPH_WALK_MAX_ENTITIES,
|
|
537
|
+
GRAPH_WALK_MIN_CONFIDENCE,
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
graphRelations = walkResult.relations;
|
|
541
|
+
|
|
542
|
+
// Add discovered entities to candidate pool (skip those already present)
|
|
543
|
+
const newEntityIds = walkResult.entities
|
|
544
|
+
.filter((e) => !candidateIds.has(e.id))
|
|
545
|
+
.map((e) => e.id);
|
|
546
|
+
|
|
547
|
+
if (newEntityIds.length > 0) {
|
|
548
|
+
// Fetch full entity data in parallel (graph walk only returns summary fields)
|
|
549
|
+
const fetchResults = await Promise.allSettled(
|
|
550
|
+
newEntityIds.map((id) => client.getMemoryEntity(id)),
|
|
551
|
+
);
|
|
552
|
+
for (const result of fetchResults) {
|
|
553
|
+
if (result.status !== "fulfilled" || !result.value.entity) continue;
|
|
554
|
+
const mapped = mapToContextEntity(result.value.entity);
|
|
555
|
+
candidateIds.add(mapped.id);
|
|
556
|
+
candidates.push(mapped);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
// Graph walk failed, continue with search-only candidates
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
356
564
|
if (candidates.length === 0) {
|
|
357
565
|
return {
|
|
358
566
|
context: "",
|
|
@@ -361,12 +569,13 @@ export async function assembleContext(
|
|
|
361
569
|
};
|
|
362
570
|
}
|
|
363
571
|
|
|
364
|
-
// Score all candidates
|
|
572
|
+
// Score all candidates (pass graph relations for relation-type bonuses)
|
|
365
573
|
const scored = candidates.map((entity) => {
|
|
366
574
|
const { score, reasons } = computeRelevanceScore(
|
|
367
575
|
entity,
|
|
368
576
|
taskContext,
|
|
369
577
|
cardLabels,
|
|
578
|
+
graphRelations.length > 0 ? graphRelations : undefined,
|
|
370
579
|
);
|
|
371
580
|
return { entity, score, reasons };
|
|
372
581
|
});
|
|
@@ -374,6 +583,38 @@ export async function assembleContext(
|
|
|
374
583
|
// Sort by score descending
|
|
375
584
|
scored.sort((a, b) => b.score - a.score);
|
|
376
585
|
|
|
586
|
+
// P2: Optional LLM re-ranking when top scores are clustered
|
|
587
|
+
if (
|
|
588
|
+
enableLlmReranking &&
|
|
589
|
+
rerankFn &&
|
|
590
|
+
scored.length >= RERANK_MIN_CANDIDATES
|
|
591
|
+
) {
|
|
592
|
+
const topN = scored.slice(0, RERANK_TOP_N);
|
|
593
|
+
const scoreRange = topN[0].score - topN[topN.length - 1].score;
|
|
594
|
+
// Only re-rank when scores are tightly clustered
|
|
595
|
+
if (scoreRange <= RERANK_CLUSTER_THRESHOLD) {
|
|
596
|
+
try {
|
|
597
|
+
const rerankCandidates = topN.map((s) => ({
|
|
598
|
+
id: s.entity.id,
|
|
599
|
+
title: s.entity.title,
|
|
600
|
+
snippet: s.entity.content.slice(0, 200),
|
|
601
|
+
}));
|
|
602
|
+
const rerankedIds = await rerankFn(taskContext, rerankCandidates);
|
|
603
|
+
// Reorder based on LLM ranking
|
|
604
|
+
const idOrder = new Map(rerankedIds.map((id, i) => [id, i]));
|
|
605
|
+
topN.sort((a, b) => {
|
|
606
|
+
const aIdx = idOrder.get(a.entity.id) ?? 999;
|
|
607
|
+
const bIdx = idOrder.get(b.entity.id) ?? 999;
|
|
608
|
+
return aIdx - bIdx;
|
|
609
|
+
});
|
|
610
|
+
// Splice reranked items back in
|
|
611
|
+
scored.splice(0, topN.length, ...topN);
|
|
612
|
+
} catch {
|
|
613
|
+
// Re-ranking failed, continue with static ordering
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
377
618
|
// Reserve dedicated procedure budget, allocate remaining to tiers
|
|
378
619
|
const procedureBudget = Math.floor(tokenBudget * PROCEDURE_BUDGET_FRACTION);
|
|
379
620
|
const remainingBudget = tokenBudget - procedureBudget;
|
package/src/skills.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { areSkillsInstalled } from "./config.js";
|
|
4
4
|
|
|
5
|
-
export const SKILLS_VERSION = "
|
|
5
|
+
export const SKILLS_VERSION = "4";
|
|
6
6
|
|
|
7
7
|
const VERSION_MARKER_PREFIX = "<!-- skills-version:";
|
|
8
8
|
|
|
@@ -206,6 +206,30 @@ Do not start an agent session.
|
|
|
206
206
|
|
|
207
207
|
---
|
|
208
208
|
|
|
209
|
+
## Step E: Auto-Detect Card for Implementation Tasks
|
|
210
|
+
|
|
211
|
+
**IMPORTANT:** This step applies when you are about to implement a plan, feature, or fix
|
|
212
|
+
that was NOT started via /hmy. Before writing any code, check if the work maps to an
|
|
213
|
+
existing Harmony card.
|
|
214
|
+
|
|
215
|
+
### When to run this check
|
|
216
|
+
- User says "implement this plan", "build this feature", "fix this bug" (without /hmy)
|
|
217
|
+
- You are about to execute a plan file
|
|
218
|
+
- Skip if: the user already started with /hmy, or no Harmony MCP tools are available
|
|
219
|
+
|
|
220
|
+
### Detection steps
|
|
221
|
+
1. Call \`harmony_search_cards\` with keywords from the plan title or task description
|
|
222
|
+
2. If a card matches (same feature/fix described), tell the user:
|
|
223
|
+
"This maps to card #N — starting a session to track progress."
|
|
224
|
+
3. Call \`harmony_start_agent_session\` with \`moveToColumn: "In Progress"\`, \`addLabels: ["agent"]\`
|
|
225
|
+
4. Track progress with \`harmony_update_agent_progress\` at milestones
|
|
226
|
+
5. When done, call \`harmony_end_agent_session\` with \`status: "completed"\`, \`moveToColumn: "Review"\`
|
|
227
|
+
|
|
228
|
+
### If no match
|
|
229
|
+
Proceed normally without a session. No action needed.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
209
233
|
## Key Tools Reference
|
|
210
234
|
|
|
211
235
|
**Cards:** \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\`, \`harmony_create_card\`, \`harmony_update_card\`, \`harmony_move_card\`, \`harmony_delete_card\`, \`harmony_assign_card\`
|
package/src/tui/setup.ts
CHANGED
|
@@ -299,6 +299,17 @@ When given a card reference (e.g., #42 or a card name), follow this workflow:
|
|
|
299
299
|
7. Update progress periodically with \`harmony_update_agent_progress\`
|
|
300
300
|
8. When done, call \`harmony_end_agent_session\` and move to "Review"
|
|
301
301
|
|
|
302
|
+
## Auto-Detect Card for Implementation Tasks
|
|
303
|
+
|
|
304
|
+
Before implementing a plan or feature, check if it maps to an existing Harmony card:
|
|
305
|
+
|
|
306
|
+
1. Use \`harmony_search_cards\` with keywords from the task description
|
|
307
|
+
2. If a match is found, call \`harmony_start_agent_session\` (agentIdentifier: "claude-code", agentName: "Claude Code", moveToColumn: "In Progress", addLabels: ["agent"])
|
|
308
|
+
3. Update progress with \`harmony_update_agent_progress\` at milestones
|
|
309
|
+
4. When done, call \`harmony_end_agent_session\` with status: "completed", moveToColumn: "Review"
|
|
310
|
+
|
|
311
|
+
Skip if: work was already started with a card reference, or no matching card exists.
|
|
312
|
+
|
|
302
313
|
## Available Harmony Tools
|
|
303
314
|
|
|
304
315
|
- \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\` - Find cards
|