@gethmy/mcp 2.2.3 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +780 -352
- package/dist/index.js +744 -351
- package/dist/lib/active-learning.js +73 -129
- package/dist/lib/consolidation.js +71 -11
- package/dist/lib/context-assembly.js +287 -30
- package/dist/lib/memory-cleanup.js +426 -0
- package/dist/lib/prompt-builder.js +5 -1
- package/dist/lib/server.js +63 -0
- package/dist/lib/skills.js +25 -1
- package/dist/lib/tui/setup.js +11 -0
- package/package.json +1 -1
- package/src/active-learning.ts +83 -145
- package/src/consolidation.ts +81 -12
- package/src/context-assembly.ts +342 -30
- package/src/memory-cleanup.ts +616 -0
- package/src/prompt-builder.ts +13 -1
- package/src/server.ts +74 -0
- package/src/skills.ts +25 -1
- package/src/tui/setup.ts +11 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Memory Cleanup
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates a 5-stage cleanup pipeline: prune stale drafts, consolidate
|
|
5
|
+
* similar memories, detect orphans, detect duplicates, and backfill embeddings.
|
|
6
|
+
*
|
|
7
|
+
* All stages are non-fatal — individual failures are collected but never block
|
|
8
|
+
* the remaining stages. Defaults to dry-run mode (preview only).
|
|
9
|
+
*/
|
|
10
|
+
import { evaluateLifecycle } from "@harmony/memory";
|
|
11
|
+
import { consolidateMemories, } from "./consolidation.js";
|
|
12
|
+
import { findSimilarEntities } from "./graph-expansion.js";
|
|
13
|
+
const ALL_STEPS = [
|
|
14
|
+
"prune",
|
|
15
|
+
"consolidate",
|
|
16
|
+
"orphans",
|
|
17
|
+
"duplicates",
|
|
18
|
+
"backfill",
|
|
19
|
+
];
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Main orchestrator
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export async function runMemoryCleanup(client, workspaceId, projectId, options) {
|
|
24
|
+
const dryRun = options?.dryRun !== false;
|
|
25
|
+
const steps = options?.steps ?? ALL_STEPS;
|
|
26
|
+
const maxAgeDays = options?.maxAgeDays ?? 30;
|
|
27
|
+
const minClusterSize = options?.minClusterSize ?? 3;
|
|
28
|
+
const orphanAgeDays = options?.orphanAgeDays ?? 14;
|
|
29
|
+
const report = {
|
|
30
|
+
success: true,
|
|
31
|
+
dryRun,
|
|
32
|
+
timestamp: new Date().toISOString(),
|
|
33
|
+
workspace: { id: workspaceId, projectId },
|
|
34
|
+
summary: { totalEntities: 0, issuesFound: 0, actionsTaken: 0 },
|
|
35
|
+
steps: {},
|
|
36
|
+
errors: [],
|
|
37
|
+
healthReport: "",
|
|
38
|
+
};
|
|
39
|
+
// Fetch all entities once (shared across steps)
|
|
40
|
+
let entities = [];
|
|
41
|
+
try {
|
|
42
|
+
const listResult = await client.listMemoryEntities({
|
|
43
|
+
workspace_id: workspaceId,
|
|
44
|
+
project_id: projectId,
|
|
45
|
+
limit: 200,
|
|
46
|
+
});
|
|
47
|
+
entities = (listResult.entities || []);
|
|
48
|
+
report.summary.totalEntities = entities.length;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
report.errors.push({
|
|
52
|
+
step: "init",
|
|
53
|
+
message: `Failed to fetch entities: ${err.message}`,
|
|
54
|
+
});
|
|
55
|
+
report.success = false;
|
|
56
|
+
report.healthReport = generateHealthReport(report);
|
|
57
|
+
return report;
|
|
58
|
+
}
|
|
59
|
+
// Stage 1: Prune stale drafts
|
|
60
|
+
if (steps.includes("prune")) {
|
|
61
|
+
try {
|
|
62
|
+
report.steps.prune = runPruneStep(entities, maxAgeDays);
|
|
63
|
+
if (!dryRun) {
|
|
64
|
+
for (const item of report.steps.prune.items) {
|
|
65
|
+
try {
|
|
66
|
+
await client.deleteMemoryEntity(item.id);
|
|
67
|
+
report.steps.prune.pruned++;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Non-fatal
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
report.summary.actionsTaken += report.steps.prune.pruned;
|
|
74
|
+
}
|
|
75
|
+
report.summary.issuesFound += report.steps.prune.staleDraftsFound;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
report.errors.push({
|
|
79
|
+
step: "prune",
|
|
80
|
+
message: err.message,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Stage 2: Consolidate similar memories
|
|
85
|
+
if (steps.includes("consolidate")) {
|
|
86
|
+
try {
|
|
87
|
+
const result = await consolidateMemories(client, workspaceId, projectId, {
|
|
88
|
+
dryRun,
|
|
89
|
+
minClusterSize,
|
|
90
|
+
});
|
|
91
|
+
report.steps.consolidate = {
|
|
92
|
+
clustersFound: result.clustersFound,
|
|
93
|
+
entitiesProcessed: result.entitiesProcessed,
|
|
94
|
+
consolidated: result.consolidated,
|
|
95
|
+
details: result.details,
|
|
96
|
+
};
|
|
97
|
+
report.summary.issuesFound += result.clustersFound;
|
|
98
|
+
if (!dryRun)
|
|
99
|
+
report.summary.actionsTaken += result.consolidated;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
report.errors.push({
|
|
103
|
+
step: "consolidate",
|
|
104
|
+
message: err.message,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Stage 3: Detect orphans
|
|
109
|
+
if (steps.includes("orphans")) {
|
|
110
|
+
try {
|
|
111
|
+
report.steps.orphans = await runOrphanStep(client, entities, orphanAgeDays);
|
|
112
|
+
if (!dryRun) {
|
|
113
|
+
for (const item of report.steps.orphans.items) {
|
|
114
|
+
try {
|
|
115
|
+
await client.deleteMemoryEntity(item.id);
|
|
116
|
+
report.steps.orphans.removed++;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Non-fatal
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
report.summary.actionsTaken += report.steps.orphans.removed;
|
|
123
|
+
}
|
|
124
|
+
report.summary.issuesFound += report.steps.orphans.orphansFound;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
report.errors.push({
|
|
128
|
+
step: "orphans",
|
|
129
|
+
message: err.message,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Stage 4: Detect duplicates
|
|
134
|
+
if (steps.includes("duplicates")) {
|
|
135
|
+
try {
|
|
136
|
+
report.steps.duplicates = await runDuplicateStep(client, entities, workspaceId, projectId);
|
|
137
|
+
if (!dryRun) {
|
|
138
|
+
for (const pair of report.steps.duplicates.pairs) {
|
|
139
|
+
try {
|
|
140
|
+
await client.deleteMemoryEntity(pair.removeId);
|
|
141
|
+
report.steps.duplicates.resolved++;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Non-fatal
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
report.summary.actionsTaken += report.steps.duplicates.resolved;
|
|
148
|
+
}
|
|
149
|
+
report.summary.issuesFound += report.steps.duplicates.duplicatePairsFound;
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
report.errors.push({
|
|
153
|
+
step: "duplicates",
|
|
154
|
+
message: err.message,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Stage 5: Backfill embeddings
|
|
159
|
+
if (steps.includes("backfill")) {
|
|
160
|
+
try {
|
|
161
|
+
if (dryRun) {
|
|
162
|
+
// In dry-run, just report that backfill would run
|
|
163
|
+
report.steps.backfill = {
|
|
164
|
+
processed: 0,
|
|
165
|
+
remaining: -1,
|
|
166
|
+
errors: [],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const result = await client.backfillEmbeddings(workspaceId);
|
|
171
|
+
report.steps.backfill = {
|
|
172
|
+
processed: result.processed,
|
|
173
|
+
remaining: result.remaining,
|
|
174
|
+
errors: result.errors || [],
|
|
175
|
+
};
|
|
176
|
+
report.summary.actionsTaken += result.processed;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
report.errors.push({
|
|
181
|
+
step: "backfill",
|
|
182
|
+
message: err.message,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
report.healthReport = generateHealthReport(report);
|
|
187
|
+
return report;
|
|
188
|
+
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Step implementations
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
function runPruneStep(entities, maxAgeDays) {
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
const drafts = entities.filter((e) => e.memory_tier === "draft");
|
|
195
|
+
const stale = [];
|
|
196
|
+
for (const entity of drafts) {
|
|
197
|
+
const ageDays = (now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
198
|
+
if (ageDays < maxAgeDays)
|
|
199
|
+
continue;
|
|
200
|
+
const lifecycle = evaluateLifecycle(entity);
|
|
201
|
+
stale.push({
|
|
202
|
+
id: entity.id,
|
|
203
|
+
title: entity.title,
|
|
204
|
+
ageDays: Math.round(ageDays),
|
|
205
|
+
decayScore: Math.round(lifecycle.decay.score * 100) / 100,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return { staleDraftsFound: stale.length, pruned: 0, items: stale };
|
|
209
|
+
}
|
|
210
|
+
async function runOrphanStep(client, entities, orphanAgeDays) {
|
|
211
|
+
const now = Date.now();
|
|
212
|
+
const result = { orphansFound: 0, removed: 0, items: [] };
|
|
213
|
+
// Pre-filter: only check entities that look like orphan candidates
|
|
214
|
+
const candidates = entities.filter((e) => {
|
|
215
|
+
if (e.memory_tier === "reference")
|
|
216
|
+
return false;
|
|
217
|
+
if (e.access_count >= 2)
|
|
218
|
+
return false;
|
|
219
|
+
const ageDays = (now - new Date(e.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
220
|
+
return ageDays >= orphanAgeDays;
|
|
221
|
+
});
|
|
222
|
+
for (const entity of candidates) {
|
|
223
|
+
try {
|
|
224
|
+
const related = await client.getRelatedEntities(entity.id);
|
|
225
|
+
const totalRelations = (related.outgoing?.length || 0) + (related.incoming?.length || 0);
|
|
226
|
+
if (totalRelations > 0)
|
|
227
|
+
continue;
|
|
228
|
+
const ageDays = (now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
229
|
+
result.items.push({
|
|
230
|
+
id: entity.id,
|
|
231
|
+
title: entity.title,
|
|
232
|
+
type: entity.type,
|
|
233
|
+
tier: entity.memory_tier,
|
|
234
|
+
ageDays: Math.round(ageDays),
|
|
235
|
+
accessCount: entity.access_count,
|
|
236
|
+
});
|
|
237
|
+
result.orphansFound++;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// Non-fatal: skip this entity
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
async function runDuplicateStep(client, entities, workspaceId, projectId) {
|
|
246
|
+
const result = {
|
|
247
|
+
duplicatePairsFound: 0,
|
|
248
|
+
resolved: 0,
|
|
249
|
+
pairs: [],
|
|
250
|
+
};
|
|
251
|
+
const seenPairs = new Set();
|
|
252
|
+
const flaggedForRemoval = new Set();
|
|
253
|
+
for (const entity of entities) {
|
|
254
|
+
if (flaggedForRemoval.has(entity.id))
|
|
255
|
+
continue;
|
|
256
|
+
let similar;
|
|
257
|
+
try {
|
|
258
|
+
similar = await findSimilarEntities(client, entity.title, entity.content, workspaceId, { projectId, limit: 5, minRrfScore: 0.05, excludeIds: [entity.id] });
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
for (const match of similar) {
|
|
264
|
+
if (flaggedForRemoval.has(match.id))
|
|
265
|
+
continue;
|
|
266
|
+
const pairKey = [entity.id, match.id].sort().join(":");
|
|
267
|
+
if (seenPairs.has(pairKey))
|
|
268
|
+
continue;
|
|
269
|
+
seenPairs.add(pairKey);
|
|
270
|
+
const sim = titleSimilarity(entity.title, match.title);
|
|
271
|
+
if (sim < 0.85)
|
|
272
|
+
continue;
|
|
273
|
+
// Keep the one with higher confidence, more accesses, or higher tier
|
|
274
|
+
const entityScore = entityQualityScore(entity);
|
|
275
|
+
const matchEntity = entities.find((e) => e.id === match.id);
|
|
276
|
+
const matchScore = matchEntity
|
|
277
|
+
? entityQualityScore(matchEntity)
|
|
278
|
+
: match.confidence;
|
|
279
|
+
const [keep, remove] = entityScore >= matchScore
|
|
280
|
+
? [entity, { id: match.id, title: match.title }]
|
|
281
|
+
: [{ id: match.id, title: match.title }, entity];
|
|
282
|
+
flaggedForRemoval.add(remove.id);
|
|
283
|
+
result.pairs.push({
|
|
284
|
+
keepId: keep.id,
|
|
285
|
+
keepTitle: keep.title,
|
|
286
|
+
removeId: remove.id,
|
|
287
|
+
removeTitle: remove.title,
|
|
288
|
+
similarity: Math.round(sim * 100) / 100,
|
|
289
|
+
});
|
|
290
|
+
result.duplicatePairsFound++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Helpers
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
const TIER_WEIGHTS = {
|
|
299
|
+
reference: 3,
|
|
300
|
+
episode: 2,
|
|
301
|
+
draft: 1,
|
|
302
|
+
};
|
|
303
|
+
function entityQualityScore(entity) {
|
|
304
|
+
return (entity.confidence +
|
|
305
|
+
(TIER_WEIGHTS[entity.memory_tier] || 0) +
|
|
306
|
+
Math.min(entity.access_count, 10) * 0.1);
|
|
307
|
+
}
|
|
308
|
+
function titleSimilarity(a, b) {
|
|
309
|
+
const na = a.toLowerCase().trim();
|
|
310
|
+
const nb = b.toLowerCase().trim();
|
|
311
|
+
if (na === nb)
|
|
312
|
+
return 1;
|
|
313
|
+
const wordsA = new Set(na.split(/\W+/).filter(Boolean));
|
|
314
|
+
const wordsB = new Set(nb.split(/\W+/).filter(Boolean));
|
|
315
|
+
if (wordsA.size === 0 || wordsB.size === 0)
|
|
316
|
+
return 0;
|
|
317
|
+
let intersection = 0;
|
|
318
|
+
for (const w of wordsA) {
|
|
319
|
+
if (wordsB.has(w))
|
|
320
|
+
intersection++;
|
|
321
|
+
}
|
|
322
|
+
// Jaccard similarity
|
|
323
|
+
const union = wordsA.size + wordsB.size - intersection;
|
|
324
|
+
return union > 0 ? intersection / union : 0;
|
|
325
|
+
}
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// Health report renderer
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
function generateHealthReport(report) {
|
|
330
|
+
const mode = report.dryRun ? "Dry Run (preview)" : "Executed";
|
|
331
|
+
const lines = [
|
|
332
|
+
"# Memory Health Report\n",
|
|
333
|
+
`**Mode:** ${mode} | **Entities:** ${report.summary.totalEntities} | **Issues:** ${report.summary.issuesFound} | **Actions:** ${report.summary.actionsTaken}`,
|
|
334
|
+
"",
|
|
335
|
+
];
|
|
336
|
+
// Prune
|
|
337
|
+
if (report.steps.prune) {
|
|
338
|
+
const p = report.steps.prune;
|
|
339
|
+
lines.push("## Stale Drafts");
|
|
340
|
+
if (p.staleDraftsFound === 0) {
|
|
341
|
+
lines.push("No stale drafts found.\n");
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
lines.push(`Found **${p.staleDraftsFound}** stale drafts${!report.dryRun ? ` (pruned ${p.pruned})` : ""}:`);
|
|
345
|
+
lines.push("| Title | Age | Decay |");
|
|
346
|
+
lines.push("|-------|-----|-------|");
|
|
347
|
+
for (const item of p.items.slice(0, 20)) {
|
|
348
|
+
lines.push(`| ${item.title} | ${item.ageDays}d | ${item.decayScore} |`);
|
|
349
|
+
}
|
|
350
|
+
lines.push("");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Consolidate
|
|
354
|
+
if (report.steps.consolidate) {
|
|
355
|
+
const c = report.steps.consolidate;
|
|
356
|
+
lines.push("## Consolidation");
|
|
357
|
+
if (c.clustersFound === 0) {
|
|
358
|
+
lines.push(`Scanned ${c.entitiesProcessed} draft/episode entities — no clusters found.\n`);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
lines.push(`Found **${c.clustersFound}** clusters across ${c.entitiesProcessed} entities:`);
|
|
362
|
+
for (const d of c.details.slice(0, 10)) {
|
|
363
|
+
lines.push(`- **${d.mergedTitle}** — ${d.clusterSize} entities`);
|
|
364
|
+
}
|
|
365
|
+
lines.push("");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Orphans
|
|
369
|
+
if (report.steps.orphans) {
|
|
370
|
+
const o = report.steps.orphans;
|
|
371
|
+
lines.push("## Orphaned Entities");
|
|
372
|
+
if (o.orphansFound === 0) {
|
|
373
|
+
lines.push("No orphans found.\n");
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
lines.push(`Found **${o.orphansFound}** orphans${!report.dryRun ? ` (removed ${o.removed})` : ""}:`);
|
|
377
|
+
lines.push("| Title | Type | Tier | Age | Accesses |");
|
|
378
|
+
lines.push("|-------|------|------|-----|----------|");
|
|
379
|
+
for (const item of o.items.slice(0, 20)) {
|
|
380
|
+
lines.push(`| ${item.title} | ${item.type} | ${item.tier} | ${item.ageDays}d | ${item.accessCount} |`);
|
|
381
|
+
}
|
|
382
|
+
lines.push("");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Duplicates
|
|
386
|
+
if (report.steps.duplicates) {
|
|
387
|
+
const d = report.steps.duplicates;
|
|
388
|
+
lines.push("## Near-Duplicates");
|
|
389
|
+
if (d.duplicatePairsFound === 0) {
|
|
390
|
+
lines.push("No duplicates found.\n");
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
lines.push(`Found **${d.duplicatePairsFound}** duplicate pairs${!report.dryRun ? ` (resolved ${d.resolved})` : ""}:`);
|
|
394
|
+
for (const pair of d.pairs.slice(0, 20)) {
|
|
395
|
+
lines.push(`- "${pair.keepTitle}" ~ "${pair.removeTitle}" (${Math.round(pair.similarity * 100)}% similar, keep first)`);
|
|
396
|
+
}
|
|
397
|
+
lines.push("");
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Backfill
|
|
401
|
+
if (report.steps.backfill) {
|
|
402
|
+
const b = report.steps.backfill;
|
|
403
|
+
lines.push("## Embedding Coverage");
|
|
404
|
+
if (report.dryRun) {
|
|
405
|
+
lines.push("Backfill will run when executed with `dryRun: false`.\n");
|
|
406
|
+
}
|
|
407
|
+
else if (b.remaining === 0) {
|
|
408
|
+
lines.push(`All embeddings up to date (processed ${b.processed}).\n`);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
lines.push(`Processed ${b.processed} entities. ${b.remaining} still need embeddings.\n`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Errors
|
|
415
|
+
if (report.errors.length > 0) {
|
|
416
|
+
lines.push("## Errors");
|
|
417
|
+
for (const e of report.errors) {
|
|
418
|
+
lines.push(`- **${e.step}:** ${e.message}`);
|
|
419
|
+
}
|
|
420
|
+
lines.push("");
|
|
421
|
+
}
|
|
422
|
+
if (report.dryRun) {
|
|
423
|
+
lines.push("---\n*Run with `dryRun: false` to execute cleanup.*");
|
|
424
|
+
}
|
|
425
|
+
return lines.join("\n");
|
|
426
|
+
}
|
|
@@ -301,7 +301,11 @@ export function generatePrompt(options) {
|
|
|
301
301
|
roleFraming.focus.forEach((f) => {
|
|
302
302
|
sections.push(`- ${f}`);
|
|
303
303
|
});
|
|
304
|
-
sections.push(`- **Memory:**
|
|
304
|
+
sections.push(`- **Memory:** Store reusable knowledge via \`harmony_remember\`. Only store what a future agent couldn't easily discover from the code itself, applies beyond this specific card, and includes a "because" (not just what, but why).`);
|
|
305
|
+
sections.push(` - GOOD: "BoardContext card state must use moveCard action, never direct setState — optimistic updates depend on action ordering"`);
|
|
306
|
+
sections.push(` - GOOD: "Mobile bottom bar is 64px, overlaps fixed-position drawers — always add pb-16 to drawer content"`);
|
|
307
|
+
sections.push(` - BAD: "Fixed the login button" (no reusable knowledge — the fix is in the code)`);
|
|
308
|
+
sections.push(` - BAD: "Completed card #42" (ephemeral, auto-tracked by session)`);
|
|
305
309
|
// Output suggestions
|
|
306
310
|
sections.push(`\n## Suggested Outputs`);
|
|
307
311
|
roleFraming.outputSuggestions.forEach((s) => {
|
package/dist/lib/server.js
CHANGED
|
@@ -11,6 +11,7 @@ import { consolidateMemories } from "./consolidation.js";
|
|
|
11
11
|
import { assembleContext, cacheManifest, computeRelevanceScore, getCachedManifest, mapToContextEntity, recordContextFeedback, trackSessionAssembly, } from "./context-assembly.js";
|
|
12
12
|
import { autoExpandGraph } from "./graph-expansion.js";
|
|
13
13
|
import { runLifecycleMaintenance } from "./lifecycle-maintenance.js";
|
|
14
|
+
import { runMemoryCleanup } from "./memory-cleanup.js";
|
|
14
15
|
import { onboardNewUser } from "./onboard.js";
|
|
15
16
|
const memorySessions = new Map();
|
|
16
17
|
function initMemorySession(cardId, agentIdentifier, agentName) {
|
|
@@ -1450,6 +1451,47 @@ const TOOLS = {
|
|
|
1450
1451
|
required: [],
|
|
1451
1452
|
},
|
|
1452
1453
|
},
|
|
1454
|
+
harmony_cleanup_memories: {
|
|
1455
|
+
description: "Run a unified memory cleanup: prune stale drafts, consolidate similar memories, detect orphans and duplicates, and backfill embeddings. Returns a health report. Dry-run by default — run with dryRun=false to execute.",
|
|
1456
|
+
inputSchema: {
|
|
1457
|
+
type: "object",
|
|
1458
|
+
properties: {
|
|
1459
|
+
workspaceId: {
|
|
1460
|
+
type: "string",
|
|
1461
|
+
description: "Workspace ID (optional if context set)",
|
|
1462
|
+
},
|
|
1463
|
+
projectId: {
|
|
1464
|
+
type: "string",
|
|
1465
|
+
description: "Project ID (optional)",
|
|
1466
|
+
},
|
|
1467
|
+
dryRun: {
|
|
1468
|
+
type: "boolean",
|
|
1469
|
+
description: "Preview cleanup without executing changes (default: true)",
|
|
1470
|
+
},
|
|
1471
|
+
steps: {
|
|
1472
|
+
type: "array",
|
|
1473
|
+
items: {
|
|
1474
|
+
type: "string",
|
|
1475
|
+
enum: ["prune", "consolidate", "orphans", "duplicates", "backfill"],
|
|
1476
|
+
},
|
|
1477
|
+
description: "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill.",
|
|
1478
|
+
},
|
|
1479
|
+
maxAgeDays: {
|
|
1480
|
+
type: "number",
|
|
1481
|
+
description: "Max age in days for stale draft pruning (default: 30)",
|
|
1482
|
+
},
|
|
1483
|
+
minClusterSize: {
|
|
1484
|
+
type: "number",
|
|
1485
|
+
description: "Min cluster size for consolidation (default: 3)",
|
|
1486
|
+
},
|
|
1487
|
+
orphanAgeDays: {
|
|
1488
|
+
type: "number",
|
|
1489
|
+
description: "Min age in days for orphan detection (default: 14)",
|
|
1490
|
+
},
|
|
1491
|
+
},
|
|
1492
|
+
required: [],
|
|
1493
|
+
},
|
|
1494
|
+
},
|
|
1453
1495
|
};
|
|
1454
1496
|
// Resource URIs
|
|
1455
1497
|
const RESOURCES = [
|
|
@@ -3215,6 +3257,27 @@ async function handleToolCall(name, args, deps) {
|
|
|
3215
3257
|
message: `Processed ${entitiesProcessed} entities, created ${relationsCreated} new relations.`,
|
|
3216
3258
|
};
|
|
3217
3259
|
}
|
|
3260
|
+
case "harmony_cleanup_memories": {
|
|
3261
|
+
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|
|
3262
|
+
if (!workspaceId) {
|
|
3263
|
+
throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
|
|
3264
|
+
}
|
|
3265
|
+
const projectId = args.projectId || deps.getActiveProjectId() || undefined;
|
|
3266
|
+
const report = await runMemoryCleanup(client, workspaceId, projectId, {
|
|
3267
|
+
dryRun: args.dryRun,
|
|
3268
|
+
steps: args.steps,
|
|
3269
|
+
maxAgeDays: args.maxAgeDays,
|
|
3270
|
+
minClusterSize: args.minClusterSize,
|
|
3271
|
+
orphanAgeDays: args.orphanAgeDays,
|
|
3272
|
+
});
|
|
3273
|
+
return {
|
|
3274
|
+
success: report.success,
|
|
3275
|
+
dryRun: report.dryRun,
|
|
3276
|
+
summary: report.summary,
|
|
3277
|
+
errors: report.errors,
|
|
3278
|
+
healthReport: report.healthReport,
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3218
3281
|
default:
|
|
3219
3282
|
throw new Error(`Unknown tool: ${name}`);
|
|
3220
3283
|
}
|
package/dist/lib/skills.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { areSkillsInstalled } from "./config.js";
|
|
4
|
-
export const SKILLS_VERSION = "
|
|
4
|
+
export const SKILLS_VERSION = "4";
|
|
5
5
|
const VERSION_MARKER_PREFIX = "<!-- skills-version:";
|
|
6
6
|
/**
|
|
7
7
|
* Legacy workflow prompt used by Codex, Cursor agents.
|
|
@@ -202,6 +202,30 @@ Do not start an agent session.
|
|
|
202
202
|
|
|
203
203
|
---
|
|
204
204
|
|
|
205
|
+
## Step E: Auto-Detect Card for Implementation Tasks
|
|
206
|
+
|
|
207
|
+
**IMPORTANT:** This step applies when you are about to implement a plan, feature, or fix
|
|
208
|
+
that was NOT started via /hmy. Before writing any code, check if the work maps to an
|
|
209
|
+
existing Harmony card.
|
|
210
|
+
|
|
211
|
+
### When to run this check
|
|
212
|
+
- User says "implement this plan", "build this feature", "fix this bug" (without /hmy)
|
|
213
|
+
- You are about to execute a plan file
|
|
214
|
+
- Skip if: the user already started with /hmy, or no Harmony MCP tools are available
|
|
215
|
+
|
|
216
|
+
### Detection steps
|
|
217
|
+
1. Call \`harmony_search_cards\` with keywords from the plan title or task description
|
|
218
|
+
2. If a card matches (same feature/fix described), tell the user:
|
|
219
|
+
"This maps to card #N — starting a session to track progress."
|
|
220
|
+
3. Call \`harmony_start_agent_session\` with \`moveToColumn: "In Progress"\`, \`addLabels: ["agent"]\`
|
|
221
|
+
4. Track progress with \`harmony_update_agent_progress\` at milestones
|
|
222
|
+
5. When done, call \`harmony_end_agent_session\` with \`status: "completed"\`, \`moveToColumn: "Review"\`
|
|
223
|
+
|
|
224
|
+
### If no match
|
|
225
|
+
Proceed normally without a session. No action needed.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
205
229
|
## Key Tools Reference
|
|
206
230
|
|
|
207
231
|
**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/dist/lib/tui/setup.js
CHANGED
|
@@ -204,6 +204,17 @@ When given a card reference (e.g., #42 or a card name), follow this workflow:
|
|
|
204
204
|
7. Update progress periodically with \`harmony_update_agent_progress\`
|
|
205
205
|
8. When done, call \`harmony_end_agent_session\` and move to "Review"
|
|
206
206
|
|
|
207
|
+
## Auto-Detect Card for Implementation Tasks
|
|
208
|
+
|
|
209
|
+
Before implementing a plan or feature, check if it maps to an existing Harmony card:
|
|
210
|
+
|
|
211
|
+
1. Use \`harmony_search_cards\` with keywords from the task description
|
|
212
|
+
2. If a match is found, call \`harmony_start_agent_session\` (agentIdentifier: "claude-code", agentName: "Claude Code", moveToColumn: "In Progress", addLabels: ["agent"])
|
|
213
|
+
3. Update progress with \`harmony_update_agent_progress\` at milestones
|
|
214
|
+
4. When done, call \`harmony_end_agent_session\` with status: "completed", moveToColumn: "Review"
|
|
215
|
+
|
|
216
|
+
Skip if: work was already started with a card reference, or no matching card exists.
|
|
217
|
+
|
|
207
218
|
## Available Harmony Tools
|
|
208
219
|
|
|
209
220
|
- \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\` - Find cards
|