@gethmy/mcp 2.4.0 → 2.4.1

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 CHANGED
@@ -28087,7 +28087,7 @@ function isBoilerplate(title, content) {
28087
28087
  }
28088
28088
  return false;
28089
28089
  }
28090
- function scoreEntity(entity, relationCount, archiveBelow, deleteBelow) {
28090
+ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraftAgeDays) {
28091
28091
  const now = Date.now();
28092
28092
  const ageDays = (now - Date.parse(entity.created_at)) / MS_PER_DAY;
28093
28093
  const effectiveLastAccess = entity.last_accessed_at ?? entity.created_at;
@@ -28172,6 +28172,7 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow) {
28172
28172
  bucket = "review";
28173
28173
  else
28174
28174
  bucket = "keep";
28175
+ const staleDraft = entity.memory_tier === "draft" && (entity.access_count || 0) === 0 && ageDays > staleDraftAgeDays;
28175
28176
  return {
28176
28177
  id: entity.id,
28177
28178
  title: entity.title,
@@ -28183,6 +28184,7 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow) {
28183
28184
  reasons,
28184
28185
  legacy,
28185
28186
  legacyReasons,
28187
+ staleDraft,
28186
28188
  subScores: {
28187
28189
  confidence: Math.round(confidence),
28188
28190
  decay: Math.round(decay),
@@ -28198,6 +28200,7 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
28198
28200
  const archiveBelow = options?.archiveBelow ?? 40;
28199
28201
  const deleteBelow = options?.deleteBelow ?? 20;
28200
28202
  const limit = options?.limit ?? 500;
28203
+ const staleDraftAgeDays = options?.staleDraftAgeDays ?? 7;
28201
28204
  const report = {
28202
28205
  success: true,
28203
28206
  dryRun,
@@ -28210,7 +28213,8 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
28210
28213
  review: 0,
28211
28214
  archive: 0,
28212
28215
  delete: 0,
28213
- legacyCount: 0
28216
+ legacyCount: 0,
28217
+ staleDraftCount: 0
28214
28218
  },
28215
28219
  actionsTaken: { flaggedReview: 0, archived: 0, deleted: 0 },
28216
28220
  distribution: { "0-20": 0, "20-40": 0, "40-70": 0, "70-100": 0 },
@@ -28221,6 +28225,7 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
28221
28225
  noGraphPresence: 0
28222
28226
  },
28223
28227
  lowest: [],
28228
+ staleDrafts: [],
28224
28229
  errors: [],
28225
28230
  healthReport: ""
28226
28231
  };
@@ -28270,12 +28275,14 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
28270
28275
  const audits = [];
28271
28276
  for (const entity of entities) {
28272
28277
  const relCount = relationCounts.get(entity.id) ?? 0;
28273
- const audit = scoreEntity(entity, relCount, archiveBelow, deleteBelow);
28278
+ const audit = scoreEntity(entity, relCount, archiveBelow, deleteBelow, staleDraftAgeDays);
28274
28279
  audits.push(audit);
28275
28280
  report.summary.scanned++;
28276
28281
  report.summary[audit.bucket]++;
28277
28282
  if (audit.legacy)
28278
28283
  report.summary.legacyCount++;
28284
+ if (audit.staleDraft)
28285
+ report.summary.staleDraftCount++;
28279
28286
  if (audit.score < 20)
28280
28287
  report.distribution["0-20"]++;
28281
28288
  else if (audit.score < 40)
@@ -28296,6 +28303,7 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
28296
28303
  }
28297
28304
  }
28298
28305
  report.lowest = [...audits].sort((a, b) => a.score - b.score).slice(0, 10);
28306
+ report.staleDrafts = audits.filter((a) => a.staleDraft).sort((a, b) => b.ageDays - a.ageDays);
28299
28307
  if (!dryRun) {
28300
28308
  for (const audit of audits) {
28301
28309
  try {
@@ -28341,7 +28349,7 @@ function renderReport(report) {
28341
28349
  const lines = [
28342
28350
  `# Memory Quality Audit
28343
28351
  `,
28344
- `**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount}`,
28352
+ `**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount} | **Stale drafts:** ${s.staleDraftCount}`,
28345
28353
  "",
28346
28354
  "## Distribution",
28347
28355
  `- 70-100 (keep): ${report.distribution["70-100"]}`,
@@ -28365,6 +28373,17 @@ function renderReport(report) {
28365
28373
  lines.push(`- No tags + no relations: ${l.noGraphPresence}`);
28366
28374
  lines.push("");
28367
28375
  }
28376
+ if (report.staleDrafts.length > 0) {
28377
+ lines.push("## Stale Drafts (promote-or-drop candidates)");
28378
+ lines.push("Filter: `tier=draft AND access=0 AND age>threshold`");
28379
+ lines.push("| Age | Score | Title |");
28380
+ lines.push("|-----|-------|-------|");
28381
+ for (const a of report.staleDrafts.slice(0, 20)) {
28382
+ const titleTrunc = a.title.length > 50 ? `${a.title.slice(0, 47)}...` : a.title;
28383
+ lines.push(`| ${a.ageDays}d | ${a.score} | ${titleTrunc} |`);
28384
+ }
28385
+ lines.push("");
28386
+ }
28368
28387
  if (report.lowest.length > 0) {
28369
28388
  lines.push("## Lowest-Scoring (top 10)");
28370
28389
  lines.push("| Score | Bucket | Tier | Age | Title | Reasons |");
@@ -30459,6 +30478,10 @@ var TOOLS = {
30459
30478
  limit: {
30460
30479
  type: "number",
30461
30480
  description: "Max number of entities to audit (default: 500). Paginated fetch."
30481
+ },
30482
+ staleDraftAgeDays: {
30483
+ type: "number",
30484
+ description: "Age threshold (days) for the stale-draft filter: flags drafts with 0 accesses older than this. Reported separately from bucket scoring — surfaces promote-or-drop candidates the thresholds miss. Default: 7."
30462
30485
  }
30463
30486
  },
30464
30487
  required: []
@@ -32038,7 +32061,8 @@ async function handleToolCall(name, args, deps) {
32038
32061
  dryRun: args.dryRun,
32039
32062
  archiveBelow: args.archiveBelow,
32040
32063
  deleteBelow: args.deleteBelow,
32041
- limit: args.limit
32064
+ limit: args.limit,
32065
+ staleDraftAgeDays: args.staleDraftAgeDays
32042
32066
  });
32043
32067
  return {
32044
32068
  success: report.success,
@@ -32048,6 +32072,7 @@ async function handleToolCall(name, args, deps) {
32048
32072
  legacyBreakdown: report.legacyBreakdown,
32049
32073
  actionsTaken: report.actionsTaken,
32050
32074
  lowest: report.lowest,
32075
+ staleDrafts: report.staleDrafts,
32051
32076
  errors: report.errors,
32052
32077
  healthReport: report.healthReport
32053
32078
  };
package/dist/index.js CHANGED
@@ -25847,7 +25847,7 @@ function isBoilerplate(title, content) {
25847
25847
  }
25848
25848
  return false;
25849
25849
  }
25850
- function scoreEntity(entity, relationCount, archiveBelow, deleteBelow) {
25850
+ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraftAgeDays) {
25851
25851
  const now = Date.now();
25852
25852
  const ageDays = (now - Date.parse(entity.created_at)) / MS_PER_DAY;
25853
25853
  const effectiveLastAccess = entity.last_accessed_at ?? entity.created_at;
@@ -25932,6 +25932,7 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow) {
25932
25932
  bucket = "review";
25933
25933
  else
25934
25934
  bucket = "keep";
25935
+ const staleDraft = entity.memory_tier === "draft" && (entity.access_count || 0) === 0 && ageDays > staleDraftAgeDays;
25935
25936
  return {
25936
25937
  id: entity.id,
25937
25938
  title: entity.title,
@@ -25943,6 +25944,7 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow) {
25943
25944
  reasons,
25944
25945
  legacy,
25945
25946
  legacyReasons,
25947
+ staleDraft,
25946
25948
  subScores: {
25947
25949
  confidence: Math.round(confidence),
25948
25950
  decay: Math.round(decay),
@@ -25958,6 +25960,7 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
25958
25960
  const archiveBelow = options?.archiveBelow ?? 40;
25959
25961
  const deleteBelow = options?.deleteBelow ?? 20;
25960
25962
  const limit = options?.limit ?? 500;
25963
+ const staleDraftAgeDays = options?.staleDraftAgeDays ?? 7;
25961
25964
  const report = {
25962
25965
  success: true,
25963
25966
  dryRun,
@@ -25970,7 +25973,8 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
25970
25973
  review: 0,
25971
25974
  archive: 0,
25972
25975
  delete: 0,
25973
- legacyCount: 0
25976
+ legacyCount: 0,
25977
+ staleDraftCount: 0
25974
25978
  },
25975
25979
  actionsTaken: { flaggedReview: 0, archived: 0, deleted: 0 },
25976
25980
  distribution: { "0-20": 0, "20-40": 0, "40-70": 0, "70-100": 0 },
@@ -25981,6 +25985,7 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
25981
25985
  noGraphPresence: 0
25982
25986
  },
25983
25987
  lowest: [],
25988
+ staleDrafts: [],
25984
25989
  errors: [],
25985
25990
  healthReport: ""
25986
25991
  };
@@ -26030,12 +26035,14 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
26030
26035
  const audits = [];
26031
26036
  for (const entity of entities) {
26032
26037
  const relCount = relationCounts.get(entity.id) ?? 0;
26033
- const audit = scoreEntity(entity, relCount, archiveBelow, deleteBelow);
26038
+ const audit = scoreEntity(entity, relCount, archiveBelow, deleteBelow, staleDraftAgeDays);
26034
26039
  audits.push(audit);
26035
26040
  report.summary.scanned++;
26036
26041
  report.summary[audit.bucket]++;
26037
26042
  if (audit.legacy)
26038
26043
  report.summary.legacyCount++;
26044
+ if (audit.staleDraft)
26045
+ report.summary.staleDraftCount++;
26039
26046
  if (audit.score < 20)
26040
26047
  report.distribution["0-20"]++;
26041
26048
  else if (audit.score < 40)
@@ -26056,6 +26063,7 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
26056
26063
  }
26057
26064
  }
26058
26065
  report.lowest = [...audits].sort((a, b) => a.score - b.score).slice(0, 10);
26066
+ report.staleDrafts = audits.filter((a) => a.staleDraft).sort((a, b) => b.ageDays - a.ageDays);
26059
26067
  if (!dryRun) {
26060
26068
  for (const audit of audits) {
26061
26069
  try {
@@ -26101,7 +26109,7 @@ function renderReport(report) {
26101
26109
  const lines = [
26102
26110
  `# Memory Quality Audit
26103
26111
  `,
26104
- `**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount}`,
26112
+ `**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount} | **Stale drafts:** ${s.staleDraftCount}`,
26105
26113
  "",
26106
26114
  "## Distribution",
26107
26115
  `- 70-100 (keep): ${report.distribution["70-100"]}`,
@@ -26125,6 +26133,17 @@ function renderReport(report) {
26125
26133
  lines.push(`- No tags + no relations: ${l.noGraphPresence}`);
26126
26134
  lines.push("");
26127
26135
  }
26136
+ if (report.staleDrafts.length > 0) {
26137
+ lines.push("## Stale Drafts (promote-or-drop candidates)");
26138
+ lines.push("Filter: `tier=draft AND access=0 AND age>threshold`");
26139
+ lines.push("| Age | Score | Title |");
26140
+ lines.push("|-----|-------|-------|");
26141
+ for (const a of report.staleDrafts.slice(0, 20)) {
26142
+ const titleTrunc = a.title.length > 50 ? `${a.title.slice(0, 47)}...` : a.title;
26143
+ lines.push(`| ${a.ageDays}d | ${a.score} | ${titleTrunc} |`);
26144
+ }
26145
+ lines.push("");
26146
+ }
26128
26147
  if (report.lowest.length > 0) {
26129
26148
  lines.push("## Lowest-Scoring (top 10)");
26130
26149
  lines.push("| Score | Bucket | Tier | Age | Title | Reasons |");
@@ -28219,6 +28238,10 @@ var TOOLS = {
28219
28238
  limit: {
28220
28239
  type: "number",
28221
28240
  description: "Max number of entities to audit (default: 500). Paginated fetch."
28241
+ },
28242
+ staleDraftAgeDays: {
28243
+ type: "number",
28244
+ description: "Age threshold (days) for the stale-draft filter: flags drafts with 0 accesses older than this. Reported separately from bucket scoring — surfaces promote-or-drop candidates the thresholds miss. Default: 7."
28222
28245
  }
28223
28246
  },
28224
28247
  required: []
@@ -29798,7 +29821,8 @@ async function handleToolCall(name, args, deps) {
29798
29821
  dryRun: args.dryRun,
29799
29822
  archiveBelow: args.archiveBelow,
29800
29823
  deleteBelow: args.deleteBelow,
29801
- limit: args.limit
29824
+ limit: args.limit,
29825
+ staleDraftAgeDays: args.staleDraftAgeDays
29802
29826
  });
29803
29827
  return {
29804
29828
  success: report.success,
@@ -29808,6 +29832,7 @@ async function handleToolCall(name, args, deps) {
29808
29832
  legacyBreakdown: report.legacyBreakdown,
29809
29833
  actionsTaken: report.actionsTaken,
29810
29834
  lowest: report.lowest,
29835
+ staleDrafts: report.staleDrafts,
29811
29836
  errors: report.errors,
29812
29837
  healthReport: report.healthReport
29813
29838
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -282,6 +282,96 @@ describe("runMemoryAudit", () => {
282
282
  ).toBe(0.25);
283
283
  });
284
284
 
285
+ test("stale-draft filter flags draft+0access+age>threshold separately from bucket", async () => {
286
+ const { client } = makeMockClient(
287
+ [
288
+ // Stale draft — should be flagged by the filter, but otherwise healthy
289
+ // enough to bucket as "review" (not archive).
290
+ {
291
+ id: "stale-draft",
292
+ type: "context",
293
+ title:
294
+ "Task transition: feature work started but never touched again",
295
+ content:
296
+ "This draft has enough content and tags to score reasonably, " +
297
+ "but nobody ever accessed it after creation — classic promote-or-drop candidate.",
298
+ confidence: 0.4,
299
+ memory_tier: "draft",
300
+ access_count: 0,
301
+ last_accessed_at: null,
302
+ created_at: daysAgo(10),
303
+ tags: ["task"],
304
+ embedding: [0.1],
305
+ },
306
+ // Fresh draft — same shape but under the age threshold, must NOT flag.
307
+ {
308
+ id: "fresh-draft",
309
+ type: "context",
310
+ title: "Task transition: a fresh draft still within the grace window",
311
+ content:
312
+ "Content long enough to not be thin at all, really properly sized.",
313
+ confidence: 0.4,
314
+ memory_tier: "draft",
315
+ access_count: 0,
316
+ last_accessed_at: null,
317
+ created_at: daysAgo(3),
318
+ tags: ["task"],
319
+ embedding: [0.1],
320
+ },
321
+ // Non-draft old zero-access — must NOT flag (filter is draft-only).
322
+ {
323
+ id: "old-episode",
324
+ type: "pattern",
325
+ title: "Episode entity that is old and unaccessed but not a draft",
326
+ content:
327
+ "Sometimes reference/episode tier entities sit unaccessed; " +
328
+ "they're not draft-promotion candidates so the filter should skip them.",
329
+ confidence: 0.8,
330
+ memory_tier: "episode",
331
+ access_count: 0,
332
+ last_accessed_at: null,
333
+ created_at: daysAgo(30),
334
+ tags: ["pat"],
335
+ embedding: [0.1],
336
+ },
337
+ ],
338
+ { "stale-draft": 1, "fresh-draft": 1, "old-episode": 2 },
339
+ );
340
+
341
+ const report = await runMemoryAudit(client, "ws-1");
342
+ expect(report.summary.staleDraftCount).toBe(1);
343
+ expect(report.staleDrafts).toHaveLength(1);
344
+ expect(report.staleDrafts[0].id).toBe("stale-draft");
345
+ expect(report.healthReport).toContain("Stale Drafts");
346
+ });
347
+
348
+ test("staleDraftAgeDays option tunes the filter threshold", async () => {
349
+ const { client } = makeMockClient([
350
+ {
351
+ id: "draft-5d",
352
+ type: "context",
353
+ title: "Five-day-old untouched draft",
354
+ content:
355
+ "Content long enough to pass the thin-content check, absolutely.",
356
+ confidence: 0.4,
357
+ memory_tier: "draft",
358
+ access_count: 0,
359
+ last_accessed_at: null,
360
+ created_at: daysAgo(5),
361
+ tags: ["x"],
362
+ embedding: [0.1],
363
+ },
364
+ ]);
365
+
366
+ const defaultRun = await runMemoryAudit(client, "ws-1");
367
+ expect(defaultRun.summary.staleDraftCount).toBe(0);
368
+
369
+ const tightRun = await runMemoryAudit(client, "ws-1", undefined, {
370
+ staleDraftAgeDays: 3,
371
+ });
372
+ expect(tightRun.summary.staleDraftCount).toBe(1);
373
+ });
374
+
285
375
  test("fetch error surfaces as report.success=false", async () => {
286
376
  const client = {
287
377
  listMemoryEntities: mock(async () => {
@@ -46,6 +46,14 @@ export interface AuditOptions {
46
46
  deleteBelow?: number;
47
47
  includeLegacyFlag?: boolean;
48
48
  limit?: number;
49
+ /**
50
+ * Age threshold (days) for the stale-draft filter. A memory is flagged
51
+ * stale when `tier=draft AND access_count=0 AND age > staleDraftAgeDays`.
52
+ * Stale drafts are reported separately — they don't change scoring or
53
+ * bucketing, just surface promote-or-drop candidates the thresholds miss.
54
+ * Default: 7.
55
+ */
56
+ staleDraftAgeDays?: number;
49
57
  }
50
58
 
51
59
  interface EntityAudit {
@@ -59,6 +67,7 @@ interface EntityAudit {
59
67
  reasons: string[];
60
68
  legacy: boolean;
61
69
  legacyReasons: string[];
70
+ staleDraft: boolean;
62
71
  subScores: {
63
72
  confidence: number;
64
73
  decay: number;
@@ -82,6 +91,7 @@ export interface AuditReport {
82
91
  archive: number;
83
92
  delete: number;
84
93
  legacyCount: number;
94
+ staleDraftCount: number;
85
95
  };
86
96
  actionsTaken: {
87
97
  flaggedReview: number;
@@ -101,6 +111,7 @@ export interface AuditReport {
101
111
  noGraphPresence: number;
102
112
  };
103
113
  lowest: EntityAudit[];
114
+ staleDrafts: EntityAudit[];
104
115
  errors: Array<{ entityId?: string; step: string; message: string }>;
105
116
  healthReport: string;
106
117
  }
@@ -128,6 +139,7 @@ function scoreEntity(
128
139
  relationCount: number,
129
140
  archiveBelow: number,
130
141
  deleteBelow: number,
142
+ staleDraftAgeDays: number,
131
143
  ): EntityAudit {
132
144
  const now = Date.now();
133
145
  const ageDays = (now - Date.parse(entity.created_at)) / MS_PER_DAY;
@@ -232,6 +244,14 @@ function scoreEntity(
232
244
  else if (score < 70) bucket = "review";
233
245
  else bucket = "keep";
234
246
 
247
+ // Stale-draft filter — orthogonal to bucketing. A draft that's aged past
248
+ // the threshold without a single access is a promote-or-drop candidate
249
+ // regardless of its composite score.
250
+ const staleDraft =
251
+ entity.memory_tier === "draft" &&
252
+ (entity.access_count || 0) === 0 &&
253
+ ageDays > staleDraftAgeDays;
254
+
235
255
  return {
236
256
  id: entity.id,
237
257
  title: entity.title,
@@ -243,6 +263,7 @@ function scoreEntity(
243
263
  reasons,
244
264
  legacy,
245
265
  legacyReasons,
266
+ staleDraft,
246
267
  subScores: {
247
268
  confidence: Math.round(confidence),
248
269
  decay: Math.round(decay),
@@ -264,6 +285,7 @@ export async function runMemoryAudit(
264
285
  const archiveBelow = options?.archiveBelow ?? 40;
265
286
  const deleteBelow = options?.deleteBelow ?? 20;
266
287
  const limit = options?.limit ?? 500;
288
+ const staleDraftAgeDays = options?.staleDraftAgeDays ?? 7;
267
289
 
268
290
  const report: AuditReport = {
269
291
  success: true,
@@ -278,6 +300,7 @@ export async function runMemoryAudit(
278
300
  archive: 0,
279
301
  delete: 0,
280
302
  legacyCount: 0,
303
+ staleDraftCount: 0,
281
304
  },
282
305
  actionsTaken: { flaggedReview: 0, archived: 0, deleted: 0 },
283
306
  distribution: { "0-20": 0, "20-40": 0, "40-70": 0, "70-100": 0 },
@@ -288,6 +311,7 @@ export async function runMemoryAudit(
288
311
  noGraphPresence: 0,
289
312
  },
290
313
  lowest: [],
314
+ staleDrafts: [],
291
315
  errors: [],
292
316
  healthReport: "",
293
317
  };
@@ -345,11 +369,18 @@ export async function runMemoryAudit(
345
369
  const audits: EntityAudit[] = [];
346
370
  for (const entity of entities) {
347
371
  const relCount = relationCounts.get(entity.id) ?? 0;
348
- const audit = scoreEntity(entity, relCount, archiveBelow, deleteBelow);
372
+ const audit = scoreEntity(
373
+ entity,
374
+ relCount,
375
+ archiveBelow,
376
+ deleteBelow,
377
+ staleDraftAgeDays,
378
+ );
349
379
  audits.push(audit);
350
380
  report.summary.scanned++;
351
381
  report.summary[audit.bucket]++;
352
382
  if (audit.legacy) report.summary.legacyCount++;
383
+ if (audit.staleDraft) report.summary.staleDraftCount++;
353
384
 
354
385
  // Distribution bin
355
386
  if (audit.score < 20) report.distribution["0-20"]++;
@@ -373,6 +404,11 @@ export async function runMemoryAudit(
373
404
  // Top 10 lowest-scoring
374
405
  report.lowest = [...audits].sort((a, b) => a.score - b.score).slice(0, 10);
375
406
 
407
+ // All stale drafts — oldest first (most overdue for promote-or-drop)
408
+ report.staleDrafts = audits
409
+ .filter((a) => a.staleDraft)
410
+ .sort((a, b) => b.ageDays - a.ageDays);
411
+
376
412
  // Execute actions
377
413
  if (!dryRun) {
378
414
  for (const audit of audits) {
@@ -420,7 +456,7 @@ function renderReport(report: AuditReport): string {
420
456
  const s = report.summary;
421
457
  const lines: string[] = [
422
458
  "# Memory Quality Audit\n",
423
- `**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount}`,
459
+ `**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount} | **Stale drafts:** ${s.staleDraftCount}`,
424
460
  "",
425
461
  "## Distribution",
426
462
  `- 70-100 (keep): ${report.distribution["70-100"]}`,
@@ -446,6 +482,19 @@ function renderReport(report: AuditReport): string {
446
482
  lines.push("");
447
483
  }
448
484
 
485
+ if (report.staleDrafts.length > 0) {
486
+ lines.push("## Stale Drafts (promote-or-drop candidates)");
487
+ lines.push("Filter: `tier=draft AND access=0 AND age>threshold`");
488
+ lines.push("| Age | Score | Title |");
489
+ lines.push("|-----|-------|-------|");
490
+ for (const a of report.staleDrafts.slice(0, 20)) {
491
+ const titleTrunc =
492
+ a.title.length > 50 ? `${a.title.slice(0, 47)}...` : a.title;
493
+ lines.push(`| ${a.ageDays}d | ${a.score} | ${titleTrunc} |`);
494
+ }
495
+ lines.push("");
496
+ }
497
+
449
498
  if (report.lowest.length > 0) {
450
499
  lines.push("## Lowest-Scoring (top 10)");
451
500
  lines.push("| Score | Bucket | Tier | Age | Title | Reasons |");
package/src/server.ts CHANGED
@@ -1709,11 +1709,13 @@ const TOOLS = {
1709
1709
  },
1710
1710
  auditArchiveBelow: {
1711
1711
  type: "number",
1712
- description: "Audit: archive entities scoring below this (default: 40)",
1712
+ description:
1713
+ "Audit: archive entities scoring below this (default: 40)",
1713
1714
  },
1714
1715
  auditDeleteBelow: {
1715
1716
  type: "number",
1716
- description: "Audit: delete entities scoring below this (default: 20)",
1717
+ description:
1718
+ "Audit: delete entities scoring below this (default: 20)",
1717
1719
  },
1718
1720
  },
1719
1721
  required: [],
@@ -1753,6 +1755,11 @@ const TOOLS = {
1753
1755
  description:
1754
1756
  "Max number of entities to audit (default: 500). Paginated fetch.",
1755
1757
  },
1758
+ staleDraftAgeDays: {
1759
+ type: "number",
1760
+ description:
1761
+ "Age threshold (days) for the stale-draft filter: flags drafts with 0 accesses older than this. Reported separately from bucket scoring — surfaces promote-or-drop candidates the thresholds miss. Default: 7.",
1762
+ },
1756
1763
  },
1757
1764
  required: [],
1758
1765
  },
@@ -4165,6 +4172,7 @@ async function handleToolCall(
4165
4172
  archiveBelow: args.archiveBelow as number | undefined,
4166
4173
  deleteBelow: args.deleteBelow as number | undefined,
4167
4174
  limit: args.limit as number | undefined,
4175
+ staleDraftAgeDays: args.staleDraftAgeDays as number | undefined,
4168
4176
  });
4169
4177
 
4170
4178
  return {
@@ -4175,6 +4183,7 @@ async function handleToolCall(
4175
4183
  legacyBreakdown: report.legacyBreakdown,
4176
4184
  actionsTaken: report.actionsTaken,
4177
4185
  lowest: report.lowest,
4186
+ staleDrafts: report.staleDrafts,
4178
4187
  errors: report.errors,
4179
4188
  healthReport: report.healthReport,
4180
4189
  };
@@ -4210,7 +4219,14 @@ async function handleToolCall(
4210
4219
  const report = await runMemoryCleanup(client, workspaceId, projectId, {
4211
4220
  dryRun: args.dryRun as boolean | undefined,
4212
4221
  steps: steps as
4213
- | ("prune" | "consolidate" | "orphans" | "duplicates" | "backfill" | "audit")[]
4222
+ | (
4223
+ | "prune"
4224
+ | "consolidate"
4225
+ | "orphans"
4226
+ | "duplicates"
4227
+ | "backfill"
4228
+ | "audit"
4229
+ )[]
4214
4230
  | undefined,
4215
4231
  maxAgeDays: args.maxAgeDays as number | undefined,
4216
4232
  minClusterSize: args.minClusterSize as number | undefined,