@aabadin/project-memory-context 0.2.0 → 0.2.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.
@@ -29,6 +29,8 @@ let _symbolIndex = {};
29
29
  let _worklistFile = '';
30
30
  let _symbolIndexFile = '';
31
31
  let _enrichmentDir = '';
32
+ let _queueStateFile = '';
33
+ let _startedAt = '';
32
34
 
33
35
  async function loadJson(path) {
34
36
  return JSON.parse(await readFile(path, 'utf8'));
@@ -320,6 +322,30 @@ export async function runQueueSymbolEnrichment({
320
322
  throw createQueueEnrichmentError(failure);
321
323
  }
322
324
 
325
+ export function buildQueueState({ status, pid, startedAt, heartbeatAt, finishedAt = null, lastError = null, summary }) {
326
+ return {
327
+ status,
328
+ pid,
329
+ startedAt,
330
+ heartbeatAt,
331
+ finishedAt,
332
+ lastError,
333
+ summary: {
334
+ pending: summary?.pending ?? 0,
335
+ enriched: summary?.enriched ?? 0,
336
+ errors: summary?.errors ?? 0,
337
+ },
338
+ };
339
+ }
340
+
341
+ export async function writeQueueState(input) {
342
+ await saveJson(input.queueStateFile, buildQueueState(input));
343
+ }
344
+
345
+ export async function finalizeQueueState(input) {
346
+ await writeQueueState(input);
347
+ }
348
+
323
349
  async function checkpointSave() {
324
350
  if (!_worklistFile) return;
325
351
  console.error('\n[checkpoint] Saving progress...');
@@ -371,6 +397,9 @@ async function main() {
371
397
  _worklist = worklist;
372
398
  _symbolIndex = symbolIndex;
373
399
 
400
+ const queueStateFile = resolve(enrichmentDir, 'queue-state.json');
401
+ _queueStateFile = queueStateFile;
402
+
374
403
  for (const entry of worklist) {
375
404
  if (entry.status === 'enriched' && entry.memoryId) {
376
405
  entry.status = 'already_enriched';
@@ -385,10 +414,29 @@ async function main() {
385
414
 
386
415
  if (pending.length === 0) {
387
416
  const summary = buildQueueSummary(worklist);
417
+ await finalizeQueueState({
418
+ queueStateFile,
419
+ status: 'finished',
420
+ pid: process.pid,
421
+ startedAt: new Date().toISOString(),
422
+ heartbeatAt: new Date().toISOString(),
423
+ finishedAt: new Date().toISOString(),
424
+ summary,
425
+ });
388
426
  console.log(JSON.stringify({ complete: true, total, enriched: summary.enriched, errors: summary.errors }));
389
427
  return;
390
428
  }
391
429
 
430
+ _startedAt = new Date().toISOString();
431
+ await writeQueueState({
432
+ queueStateFile,
433
+ status: 'running',
434
+ pid: process.pid,
435
+ startedAt: _startedAt,
436
+ heartbeatAt: _startedAt,
437
+ summary: buildQueueSummary(worklist),
438
+ });
439
+
392
440
  console.error(`[queue] Starting continuous enrichment: ${pending.length} pending (${staleCount} stale), ${alreadyEnriched} already enriched, ${PMC_CONCURRENCY} parallel slots`);
393
441
  console.error(`[queue] Modes: ${enrichmentConfig.preferredModes.join(', ')} | Local: ${enrichmentConfig.localModel.baseUrl} | Model: ${enrichmentConfig.localModel.model} | Timeout: ${TIMEOUT_MS}ms per symbol\n`);
394
442
 
@@ -458,7 +506,7 @@ async function main() {
458
506
  await Promise.all(initPromises);
459
507
 
460
508
  await new Promise((resolve) => {
461
- const checkInterval = setInterval(() => {
509
+ const checkInterval = setInterval(async () => {
462
510
  if (!tracker.hasActiveSlots() && queue.length === 0) {
463
511
  clearInterval(checkInterval);
464
512
  resolve();
@@ -468,6 +516,14 @@ async function main() {
468
516
  const active = tracker.activeCount();
469
517
  const done = results.length + errors.length;
470
518
  console.error(`[queue status] done=${done} active=${active} queued=${remaining} elapsed=${Math.round(elapsedTotal / 1000)}s`);
519
+ await writeQueueState({
520
+ queueStateFile,
521
+ status: 'running',
522
+ pid: process.pid,
523
+ startedAt: _startedAt,
524
+ heartbeatAt: new Date().toISOString(),
525
+ summary: buildQueueSummary(worklist),
526
+ }).catch(() => {});
471
527
  }
472
528
  }, REPORT_INTERVAL_MS);
473
529
  });
@@ -504,6 +560,17 @@ async function main() {
504
560
  const summary = buildQueueSummary(worklist);
505
561
  const avgTime = results.length > 0 ? results.reduce((a, r) => a + r.elapsed, 0) / results.length : 0;
506
562
 
563
+ const finishedAt = new Date().toISOString();
564
+ await finalizeQueueState({
565
+ queueStateFile,
566
+ status: 'finished',
567
+ pid: process.pid,
568
+ startedAt: _startedAt,
569
+ heartbeatAt: finishedAt,
570
+ finishedAt,
571
+ summary,
572
+ });
573
+
507
574
  console.log(JSON.stringify({
508
575
  complete: summary.pending === 0,
509
576
  total: worklist.length,
@@ -518,7 +585,21 @@ async function main() {
518
585
  }
519
586
 
520
587
  if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
521
- main().catch(err => {
588
+ main().catch(async (err) => {
589
+ try {
590
+ const summary = Array.isArray(_worklist) ? buildQueueSummary(_worklist) : { pending: 0, enriched: 0, errors: 0 };
591
+ const enrichmentDir = _enrichmentDir || resolve(PROJECT_ROOT, '.planning/project-memory-context/enrichment');
592
+ await finalizeQueueState({
593
+ queueStateFile: resolve(enrichmentDir, 'queue-state.json'),
594
+ status: 'failed',
595
+ pid: process.pid,
596
+ startedAt: _startedAt || new Date().toISOString(),
597
+ heartbeatAt: new Date().toISOString(),
598
+ finishedAt: new Date().toISOString(),
599
+ lastError: err.message,
600
+ summary,
601
+ });
602
+ } catch {}
522
603
  console.error('[fatal]', err.message);
523
604
  process.exit(1);
524
605
  });
@@ -0,0 +1,359 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
4
+ import { resolve, dirname, basename } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ import { appendProviderEvent, withRecordedAttempt } from '../src/enrichment-attempts.mjs';
8
+ import { resolveEnrichmentConfig, PMC_ENRICHMENT_CONFIG_FILE } from '../src/enrichment-config.mjs';
9
+ import { runEnrichmentWithFallback } from '../src/enrichment-driver.mjs';
10
+ import { createLocalModelProvider } from '../src/providers/local-model-provider.mjs';
11
+ import { appendSyncEntry, createSyncEntry } from '../src/sync-manifest.mjs';
12
+ import { homedir } from 'node:os';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+
16
+ async function loadJson(path) {
17
+ return JSON.parse(await readFile(path, 'utf8'));
18
+ }
19
+
20
+ async function loadOptionalJson(path, fallback = null) {
21
+ try {
22
+ return await loadJson(path);
23
+ } catch {
24
+ return fallback;
25
+ }
26
+ }
27
+
28
+ async function saveJson(path, data) {
29
+ await mkdir(dirname(path), { recursive: true });
30
+ await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
31
+ }
32
+
33
+ function safeKey(key) {
34
+ return key.replace(/[^a-zA-Z0-9_-]+/g, '_');
35
+ }
36
+
37
+ async function buildSymbolPrompt(symbol, projectRoot, maxCodeLines = 80) {
38
+ const absoluteFile = resolve(projectRoot, symbol.filePath);
39
+ const content = await readFile(absoluteFile, 'utf8');
40
+ const lines = content.split('\n');
41
+ const totalSymbolLines = symbol.range.endLine - symbol.range.startLine + 1;
42
+ let codeSection = lines.slice(symbol.range.startLine - 1, symbol.range.endLine).join('\n');
43
+ let truncated = false;
44
+ if (totalSymbolLines > maxCodeLines) {
45
+ const header = lines.slice(symbol.range.startLine - 1, symbol.range.startLine - 1 + Math.floor(maxCodeLines * 0.6)).join('\n');
46
+ const footer = lines.slice(symbol.range.endLine - Math.floor(maxCodeLines * 0.4), symbol.range.endLine).join('\n');
47
+ codeSection = header + '\n // ... truncated ...\n' + footer;
48
+ truncated = true;
49
+ }
50
+ const importsSection = lines.slice(0, symbol.range.startLine - 1).filter(l => /^\s*import\b/.test(l) || /^\s*using\b/.test(l)).join('\n');
51
+ return `Symbol: ${symbol.name}\nKind: ${symbol.kind}\nLanguage: ${symbol.language}\nLocation: ${symbol.filePath}:${symbol.range.startLine}-${symbol.range.endLine}${truncated ? ` (truncated from ${totalSymbolLines} lines)` : ''}\n\nContext (imports):\n${importsSection || '(none)'}\n\nCode:\n${codeSection}\n\nReturn a compact structured explanation (max 150 words) with:\n- responsibility\n- primary inputs\n- output\n- immediate dependencies\n- role in module`;
52
+ }
53
+
54
+ function parseArgs(argv) {
55
+ const args = { concurrency: 4, timeoutMs: 600000, model: null, baseUrl: null, reportFile: null, limit: null };
56
+ for (let i = 2; i < argv.length; i++) {
57
+ const arg = argv[i];
58
+ if (arg === '--concurrency' && argv[i + 1]) { args.concurrency = parseInt(argv[++i], 10); }
59
+ else if (arg === '--timeout' && argv[i + 1]) { args.timeoutMs = parseInt(argv[++i], 10); }
60
+ else if (arg === '--model' && argv[i + 1]) { args.model = argv[++i]; }
61
+ else if (arg === '--base-url' && argv[i + 1]) { args.baseUrl = argv[++i]; }
62
+ else if (arg === '--report' && argv[i + 1]) { args.reportFile = argv[++i]; }
63
+ else if (arg === '--limit' && argv[i + 1]) { args.limit = parseInt(argv[++i], 10); }
64
+ else if (arg === '--help' || arg === '-h') { args.help = true; }
65
+ }
66
+ return args;
67
+ }
68
+
69
+ function printHelp() {
70
+ console.log(`
71
+ PMC Retry Errors — Re-enriches symbols that failed using Ollama directly
72
+
73
+ Usage: node tools/project-memory-context/cli/retry-errors.mjs [options]
74
+
75
+ Options:
76
+ --concurrency N Parallel slots (default: 4)
77
+ --timeout MS Timeout per symbol in ms (default: 600000 = 10min)
78
+ --model NAME Ollama model name (default: from config)
79
+ --base-url URL Ollama base URL (default: http://localhost:11434)
80
+ --limit N Only retry first N error entries (default: all)
81
+ --report PATH Save JSON report to file (default: .planning/.../retry-report.json)
82
+ -h, --help Show this help
83
+ `);
84
+ }
85
+
86
+ async function main() {
87
+ const rawArgs = process.argv.slice(2);
88
+ const projectRoot = rawArgs.find(a => !a.startsWith('-')) || process.cwd();
89
+ const args = parseArgs(process.argv);
90
+ if (args.help) { printHelp(); return 0; }
91
+
92
+ const enrichmentDir = resolve(projectRoot, '.planning/project-memory-context/enrichment');
93
+ await mkdir(enrichmentDir, { recursive: true });
94
+
95
+ const worklistFile = resolve(enrichmentDir, 'worklist.json');
96
+ const symbolIndexFile = resolve(enrichmentDir, 'symbol-index.json');
97
+
98
+ let worklist;
99
+ try {
100
+ worklist = await loadJson(worklistFile);
101
+ } catch {
102
+ console.error('[retry] No worklist found.');
103
+ process.exit(1);
104
+ }
105
+
106
+ const errorEntries = worklist.filter(e => e.status === 'error');
107
+ if (errorEntries.length === 0) {
108
+ console.log('[retry] No error entries found in worklist. Nothing to retry.');
109
+ return;
110
+ }
111
+
112
+ if (args.limit && args.limit < errorEntries.length) {
113
+ console.error(`[retry] Limiting to first ${args.limit} of ${errorEntries.length} error entries`);
114
+ errorEntries.splice(args.limit);
115
+ }
116
+
117
+ let symbolIndex = {};
118
+ try { symbolIndex = await loadJson(symbolIndexFile); } catch {}
119
+
120
+ const projectConfig = await loadOptionalJson(resolve(projectRoot, '.opencode', PMC_ENRICHMENT_CONFIG_FILE));
121
+ const globalConfig = await loadOptionalJson(resolve(homedir(), '.config', 'opencode', PMC_ENRICHMENT_CONFIG_FILE));
122
+ let config = resolveEnrichmentConfig({ projectConfig, globalConfig, env: process.env });
123
+
124
+ config.preferredModes = ['local-model'];
125
+
126
+ if (args.model) {
127
+ config.localModel.model = args.model;
128
+ }
129
+ if (args.baseUrl) {
130
+ config.localModel.baseUrl = args.baseUrl;
131
+ }
132
+
133
+ const projectSlug = process.env.PMC_PROJECT_SLUG || basename(projectRoot);
134
+
135
+ const providers = [createLocalModelProvider()];
136
+
137
+ console.error(`\n[retry] ═════════════════════════════════════════════════════`);
138
+ console.error(`[retry] Retrying ${errorEntries.length} error symbols`);
139
+ console.error(`[retry] Provider: ${config.localModel.provider} | Model: ${config.localModel.model}`);
140
+ console.error(`[retry] Base URL: ${config.localModel.baseUrl}`);
141
+ console.error(`[retry] Concurrency: ${args.concurrency} | Timeout: ${args.timeoutMs / 1000}s`);
142
+ console.error(`[retry] ═════════════════════════════════════════════════════\n`);
143
+
144
+ const report = {
145
+ startedAt: new Date().toISOString(),
146
+ config: { model: config.localModel.model, baseUrl: config.localModel.baseUrl, concurrency: args.concurrency, timeoutMs: args.timeoutMs },
147
+ previousErrors: {},
148
+ results: [],
149
+ summary: { succeeded: 0, failed: 0, skipped: 0 },
150
+ };
151
+
152
+ for (const entry of errorEntries) {
153
+ const lastLocalAttempt = (entry.attempts ?? []).find(a => a.mode === 'local-model');
154
+ report.previousErrors[entry.symbolKey] = {
155
+ name: entry.name,
156
+ filePath: entry.filePath,
157
+ kind: entry.kind,
158
+ previousError: entry.error,
159
+ localModelError: lastLocalAttempt?.errorMessage ?? null,
160
+ localModelErrorType: lastLocalAttempt?.errorType ?? null,
161
+ attemptCount: entry.attempts?.length ?? 0,
162
+ };
163
+ }
164
+
165
+ const queue = [...errorEntries];
166
+ const activeSlots = new Map();
167
+ const results = [];
168
+ let slotIdCounter = 0;
169
+
170
+ async function dispatchSlot() {
171
+ while (queue.length > 0) {
172
+ const entry = queue.shift();
173
+
174
+ entry.status = 'pending';
175
+ delete entry.error;
176
+ delete entry.failedAt;
177
+
178
+ const slotId = slotIdCounter++;
179
+ const startTime = Date.now();
180
+
181
+ activeSlots.set(slotId, { entry, startTime });
182
+
183
+ try {
184
+ const prompt = await buildSymbolPrompt(entry, projectRoot);
185
+ const result = await runEnrichmentWithFallback({
186
+ request: { prompt, timeoutMs: args.timeoutMs },
187
+ config,
188
+ providers,
189
+ env: process.env,
190
+ });
191
+
192
+ const elapsed = Date.now() - startTime;
193
+ activeSlots.delete(slotId);
194
+
195
+ for (const attempt of result.attempts ?? []) {
196
+ await appendProviderEvent(enrichmentDir, { symbolKey: entry.symbolKey, name: entry.name, ...attempt });
197
+ }
198
+
199
+ const reportEntry = {
200
+ symbolKey: entry.symbolKey,
201
+ name: entry.name,
202
+ filePath: entry.filePath,
203
+ kind: entry.kind,
204
+ language: entry.language,
205
+ elapsedMs: elapsed,
206
+ attempts: result.attempts ?? [],
207
+ };
208
+
209
+ if (result.status === 'succeeded') {
210
+ const memoryId = `queue-${safeKey(entry.symbolKey)}`;
211
+ const enrichedAt = new Date().toISOString();
212
+ const memoryFile = resolve(enrichmentDir, `${safeKey(entry.symbolKey)}.memory.json`);
213
+
214
+ await saveJson(memoryFile, {
215
+ content: result.content,
216
+ category: 'architecture',
217
+ tags: ['symbol', entry.language, entry.kind, `project:${projectSlug}`, `file:${entry.filePath}`],
218
+ });
219
+
220
+ const newAttempts = [...(entry.attempts ?? []), ...(result.attempts ?? [])];
221
+ Object.assign(entry, {
222
+ status: 'enriched',
223
+ memoryId,
224
+ enrichedAt,
225
+ error: undefined,
226
+ failedAt: undefined,
227
+ attempts: newAttempts,
228
+ lastModeUsed: 'local-model',
229
+ });
230
+
231
+ symbolIndex[entry.symbolKey] = {
232
+ memoryId,
233
+ graphNodeId: entry.graphNodeId ?? null,
234
+ codeHash: entry.codeHash,
235
+ status: 'enriched',
236
+ lastEnrichedAt: enrichedAt,
237
+ };
238
+
239
+ try {
240
+ await appendSyncEntry(enrichmentDir, createSyncEntry({
241
+ action: 'upsert',
242
+ keyTag: `key:symbol:${safeKey(entry.symbolKey)}`,
243
+ content: `## ${entry.name}\n\n${result.content}`,
244
+ category: 'architecture',
245
+ tags: ['symbol', entry.language, entry.kind, `project:${projectSlug}`, `file:${entry.filePath}`, 'enriched-by-retry'],
246
+ source: 'retry-errors',
247
+ symbolKey: entry.symbolKey,
248
+ }));
249
+ } catch (syncErr) {
250
+ console.error(`[retry] WARN: sync append failed for ${entry.name}: ${syncErr.message}`);
251
+ }
252
+
253
+ reportEntry.status = 'succeeded';
254
+ reportEntry.contentPreview = result.content?.substring(0, 200);
255
+ reportEntry.memoryId = memoryId;
256
+ report.summary.succeeded++;
257
+
258
+ const done = report.summary.succeeded + report.summary.failed;
259
+ console.error(` [OK] ${entry.name} (${entry.filePath}:${entry.range.startLine}) — ${Math.round(elapsed / 1000)}s [${done}/${errorEntries.length}]`);
260
+ } else {
261
+ const lastError = result.attempts?.find(a => a.errorMessage)?.errorMessage ?? 'all providers failed';
262
+
263
+ const newAttempts = [...(entry.attempts ?? []), ...(result.attempts ?? [])];
264
+ Object.assign(entry, {
265
+ status: 'error',
266
+ error: lastError,
267
+ failedAt: new Date().toISOString(),
268
+ attempts: newAttempts,
269
+ });
270
+
271
+ symbolIndex[entry.symbolKey] = {
272
+ memoryId: null,
273
+ graphNodeId: entry.graphNodeId ?? null,
274
+ codeHash: entry.codeHash,
275
+ status: 'error',
276
+ lastEnrichedAt: new Date().toISOString(),
277
+ };
278
+
279
+ reportEntry.status = 'failed';
280
+ reportEntry.failureReason = lastError;
281
+ report.summary.failed++;
282
+
283
+ const done = report.summary.succeeded + report.summary.failed;
284
+ console.error(` [FAIL] ${entry.name} (${entry.filePath}:${entry.range.startLine}) — ${lastError} — ${Math.round(elapsed / 1000)}s [${done}/${errorEntries.length}]`);
285
+ }
286
+
287
+ results.push(reportEntry);
288
+ } catch (err) {
289
+ const elapsed = Date.now() - startTime;
290
+ activeSlots.delete(slotId);
291
+
292
+ entry.status = 'error';
293
+ entry.error = err.message;
294
+ entry.failedAt = new Date().toISOString();
295
+
296
+ const reportEntry = {
297
+ symbolKey: entry.symbolKey,
298
+ name: entry.name,
299
+ filePath: entry.filePath,
300
+ kind: entry.kind,
301
+ language: entry.language,
302
+ elapsedMs: elapsed,
303
+ status: 'failed',
304
+ failureReason: err.message,
305
+ attempts: [],
306
+ };
307
+
308
+ results.push(reportEntry);
309
+ report.summary.failed++;
310
+
311
+ const done = report.summary.succeeded + report.summary.failed;
312
+ console.error(` [ERR] ${entry.name} — ${err.message} — ${Math.round(elapsed / 1000)}s [${done}/${errorEntries.length}]`);
313
+ }
314
+
315
+ await saveJson(worklistFile, worklist);
316
+ await saveJson(symbolIndexFile, symbolIndex);
317
+ }
318
+ }
319
+
320
+ const slots = [];
321
+ for (let i = 0; i < args.concurrency; i++) {
322
+ slots.push(dispatchSlot());
323
+ }
324
+ await Promise.all(slots);
325
+
326
+ report.results = results;
327
+ report.finishedAt = new Date().toISOString();
328
+ report.totalElapsedMs = results.reduce((a, r) => a + (r.elapsedMs ?? 0), 0);
329
+
330
+ const reportPath = args.reportFile || resolve(enrichmentDir, 'retry-report.json');
331
+ await saveJson(reportPath, report);
332
+
333
+ console.error(`\n[retry] ═════════════════════════════════════════════════════`);
334
+ console.error(`[retry] DONE — ${report.summary.succeeded} succeeded, ${report.summary.failed} failed, ${report.summary.skipped} skipped`);
335
+ console.error(`[retry] Report: ${reportPath}`);
336
+ console.error(`[retry] ═════════════════════════════════════════════════════\n`);
337
+
338
+ console.log(JSON.stringify({
339
+ ok: true,
340
+ command: 'retry-errors',
341
+ total: errorEntries.length,
342
+ succeeded: report.summary.succeeded,
343
+ failed: report.summary.failed,
344
+ reportPath,
345
+ }, null, 2));
346
+
347
+ return report.summary.failed > 0 ? 1 : 0;
348
+ }
349
+
350
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
351
+ const exitCode = await main().catch(err => {
352
+ console.error('[retry] FATAL:', err.message);
353
+ return 1;
354
+ });
355
+
356
+ if (exitCode !== 0) {
357
+ process.exit(exitCode);
358
+ }
359
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aabadin/project-memory-context",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Portable project memory context CLI — bootstraps semantic enrichment workflows for any AI coding agent.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -15,6 +15,7 @@ const COMMANDS = new Map([
15
15
  ['new-project', 'cli/new-project.mjs'],
16
16
  ['project-context', 'cli/project-context.mjs'],
17
17
  ['query', 'cli/query.mjs'],
18
+ ['retry-errors', 'cli/retry-errors.mjs'],
18
19
  ['sanitize', 'cli/sanitize.mjs'],
19
20
  ['setup', 'cli/setup.mjs'],
20
21
  ['status', 'cli/status.mjs'],
@@ -53,7 +53,14 @@ When the user types `/sanitize`, run:
53
53
  When starting a session in a project with `.planning/project-memory-context/`:
54
54
 
55
55
  1. Check `.planning/project-memory-context/enrichment/worklist.json` for pending or stale entries.
56
- 2. If pending/stale entries exist, launch background enrichment: `{{PMC_BIN}} enrich .`
56
+ 2. If pending/stale entries exist, launch background enrichment:
57
+ ```powershell
58
+ if (Test-Path "tools/project-memory-context/cli/enrich-queue.mjs") {
59
+ Start-Process -FilePath "node" -ArgumentList "tools/project-memory-context/cli/enrich-queue.mjs" -WindowStyle Hidden
60
+ } else {
61
+ Start-Process -FilePath "npx" -ArgumentList "--yes","--package","@aabadin/project-memory-context","pmc-enrich","." -WindowStyle Hidden
62
+ }
63
+ ```
57
64
  3. Check `.planning/project-memory-context/enrichment/sync-manifest.json` for pending entries.
58
65
  4. If sync-manifest has pending entries, inform the user: "PMC has {n} pending sync operations. Run `/sync-context` to apply them."
59
66
  5. Search agent-memory with `query: "project context overview"` and `tags: ["project-context"]` to fetch base context.
@@ -53,7 +53,14 @@ When the user types "/sanitize", run:
53
53
  When starting a session in a project with `.planning/project-memory-context/`:
54
54
 
55
55
  1. Check `.planning/project-memory-context/enrichment/worklist.json` for pending or stale entries.
56
- 2. If pending/stale entries exist, launch background enrichment: `{{PMC_BIN}} enrich .`
56
+ 2. If pending/stale entries exist, launch background enrichment:
57
+ ```powershell
58
+ if (Test-Path "tools/project-memory-context/cli/enrich-queue.mjs") {
59
+ Start-Process -FilePath "node" -ArgumentList "tools/project-memory-context/cli/enrich-queue.mjs" -WindowStyle Hidden
60
+ } else {
61
+ Start-Process -FilePath "npx" -ArgumentList "--yes","--package","@aabadin/project-memory-context","pmc-enrich","." -WindowStyle Hidden
62
+ }
63
+ ```
57
64
  3. Check `.planning/project-memory-context/enrichment/sync-manifest.json` for pending entries.
58
65
  4. If sync-manifest has pending entries, inform the user: "PMC has {n} pending sync operations. Run /sync-context to apply them."
59
66
  5. Search agent-memory with query "project context overview" and tags ["project-context"] to fetch base context.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: retry-errors
3
+ description: Re-enrich symbols that previously failed using Ollama directly, with a detailed per-symbol report.
4
+ argument-hint: "[--limit N] [--model MODEL] [--concurrency N] [--timeout MS]"
5
+ allowed-tools:
6
+ - Bash
7
+ ---
8
+
9
+ <objective>
10
+ Retry enrichment for all symbols currently in error status, using only the local Ollama provider. Generates a per-symbol report showing what each previous error was and whether the retry succeeded.
11
+ </objective>
12
+
13
+ <execution>
14
+ Run:
15
+
16
+ ```bash
17
+ {{PMC_BIN}} retry-errors . --model "qwen2.5-coder:14b" --concurrency 1 --timeout 300000
18
+ ```
19
+
20
+ Options:
21
+ - `--limit N` — Only retry the first N error entries (useful for testing)
22
+ - `--model MODEL` — Ollama model to use (default: from config)
23
+ - `--concurrency N` — Parallel slots (recommended: 1 for Ollama)
24
+ - `--timeout MS` — Timeout per symbol in ms (default: 600000 = 10min)
25
+ - `--report PATH` — Custom path for the JSON report
26
+
27
+ The command reads the worklist, finds all entries with `status: "error"`, and re-enriches them using only the local-model (Ollama) provider. A JSON report is saved to `.planning/project-memory-context/enrichment/retry-report.json` with:
28
+ - Previous error details for each symbol (error type, message, provider)
29
+ - Retry result (succeeded/failed, elapsed time, content preview)
30
+ - Overall summary counts
31
+ </execution>