pmem-ai 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +11 -9
  3. package/dist/commands/distill.js +1 -1
  4. package/dist/commands/distill.js.map +1 -1
  5. package/dist/commands/doctor.d.ts.map +1 -1
  6. package/dist/commands/doctor.js +15 -1
  7. package/dist/commands/doctor.js.map +1 -1
  8. package/dist/commands/integration.d.ts.map +1 -1
  9. package/dist/commands/integration.js +57 -3
  10. package/dist/commands/integration.js.map +1 -1
  11. package/dist/commands/new.d.ts +2 -0
  12. package/dist/commands/new.d.ts.map +1 -0
  13. package/dist/commands/new.js +117 -0
  14. package/dist/commands/new.js.map +1 -0
  15. package/dist/commands/rebuild.d.ts.map +1 -1
  16. package/dist/commands/rebuild.js +27 -1
  17. package/dist/commands/rebuild.js.map +1 -1
  18. package/dist/commands/recall.d.ts +1 -1
  19. package/dist/commands/recall.d.ts.map +1 -1
  20. package/dist/commands/recall.js +34 -3
  21. package/dist/commands/recall.js.map +1 -1
  22. package/dist/commands/rename.d.ts +6 -0
  23. package/dist/commands/rename.d.ts.map +1 -0
  24. package/dist/commands/rename.js +177 -0
  25. package/dist/commands/rename.js.map +1 -0
  26. package/dist/commands/status.d.ts.map +1 -1
  27. package/dist/commands/status.js +3 -4
  28. package/dist/commands/status.js.map +1 -1
  29. package/dist/commands/update.d.ts +1 -0
  30. package/dist/commands/update.d.ts.map +1 -1
  31. package/dist/commands/update.js +335 -133
  32. package/dist/commands/update.js.map +1 -1
  33. package/dist/commands/verify.d.ts +2 -0
  34. package/dist/commands/verify.d.ts.map +1 -1
  35. package/dist/commands/verify.js +61 -39
  36. package/dist/commands/verify.js.map +1 -1
  37. package/dist/core/consistency.d.ts +10 -0
  38. package/dist/core/consistency.d.ts.map +1 -0
  39. package/dist/core/consistency.js +97 -0
  40. package/dist/core/consistency.js.map +1 -0
  41. package/dist/core/db.d.ts +9 -0
  42. package/dist/core/db.d.ts.map +1 -1
  43. package/dist/core/db.js +4 -0
  44. package/dist/core/db.js.map +1 -1
  45. package/dist/core/fs.d.ts +6 -0
  46. package/dist/core/fs.d.ts.map +1 -1
  47. package/dist/core/fs.js +32 -2
  48. package/dist/core/fs.js.map +1 -1
  49. package/dist/core/manifest.js +1 -1
  50. package/dist/core/manifest.js.map +1 -1
  51. package/dist/index.js +24 -3
  52. package/dist/index.js.map +1 -1
  53. package/dist/types.d.ts +40 -0
  54. package/dist/types.d.ts.map +1 -1
  55. package/docs/handover-v0.4.md +2 -0
  56. package/docs/handover-v0.6.md +152 -0
  57. package/docs/project-roadmap.md +42 -5
  58. package/docs/usage.md +13 -14
  59. package/docs/v0.4 pre-design.md +2 -0
  60. package/docs/v0.5 pre-design.md +2 -0
  61. package/docs/v0.6 pre-design.md +2 -0
  62. package/docs/v0.6.1 pre-design.md +566 -0
  63. package/docs/v0.6.2 pre-design.md +657 -0
  64. package/package.json +3 -2
  65. package/skills/pmem/SKILL.md +12 -10
@@ -42,6 +42,7 @@ const manifest_1 = require("../core/manifest");
42
42
  const rebuild_1 = require("./rebuild");
43
43
  const db_1 = require("../core/db");
44
44
  const git_1 = require("../core/git");
45
+ const consistency_1 = require("../core/consistency");
45
46
  const PMEM_DIR = '.pmem';
46
47
  function updateCommand(options) {
47
48
  const cwd = process.cwd();
@@ -57,7 +58,7 @@ function updateCommand(options) {
57
58
  }
58
59
  // --suggest: show intelligent update suggestions
59
60
  if (options.suggest) {
60
- suggestActions(pmemPath, options.format);
61
+ suggestActions(pmemPath, options.format, options.includeHistory);
61
62
  return;
62
63
  }
63
64
  // --apply-suggestion: apply a specific suggestion
@@ -246,8 +247,12 @@ function autoUpdate(pmemPath, manifest) {
246
247
  function confirmUpdate(pmemPath, summary, next) {
247
248
  const lockPath = path.join(pmemPath, '.lock');
248
249
  if (!(0, fs_1.acquireLock)(lockPath)) {
249
- console.log('Failed to acquire pmem lock. Another pmem update may be running.');
250
- console.log('No memory was written. Try again later.');
250
+ console.log('Failed to acquire pmem lock after 3s.');
251
+ console.log(' The lock at .pmem/.lock may be held by another pmem process, or a stale lock from a previous crash.');
252
+ console.log(' → Run: pmem verify --fix-locks (to check and clean stale locks)');
253
+ console.log(' → Or: pmem doctor (to diagnose lock status)');
254
+ console.log(' If no other pmem process is running, delete .pmem/.lock manually.');
255
+ console.log('No memory was written. Try again after resolving the lock.');
251
256
  return;
252
257
  }
253
258
  try {
@@ -358,132 +363,299 @@ function listSourceFiles(root) {
358
363
  walk(root);
359
364
  return results;
360
365
  }
361
- function generateSuggestions(pmemPath) {
366
+ /**
367
+ * Extract the matched file path from a dirty flag reason string.
368
+ * Handles formats like "file_changed: path/to/file" or plain text.
369
+ */
370
+ function extractMatchedFile(reason) {
371
+ const match = reason.match(/^file_changed:\s*(.+)/);
372
+ if (match) {
373
+ return match[1].trim();
374
+ }
375
+ return null;
376
+ }
377
+ /**
378
+ * Build the aggregation key for a dirty flag: target + reason + matched_file.
379
+ */
380
+ function aggregationKey(flag) {
381
+ const mf = extractMatchedFile(flag.reason);
382
+ return `${flag.target}||${flag.reason}||${mf ?? ''}`;
383
+ }
384
+ /**
385
+ * Find the most recent session end time.
386
+ * Returns null if no ended session exists.
387
+ */
388
+ function getLatestSessionEnd(pmemPath) {
389
+ try {
390
+ const db = (0, db_1.openDatabase)(pmemPath);
391
+ const row = db.prepare("SELECT ended_at FROM sessions WHERE ended_at IS NOT NULL ORDER BY ended_at DESC LIMIT 1").get();
392
+ return row?.ended_at ?? null;
393
+ }
394
+ catch {
395
+ return null;
396
+ }
397
+ }
398
+ /**
399
+ * Get the active (un-ended) session if one exists.
400
+ */
401
+ function getActiveSessionStart(pmemPath) {
402
+ try {
403
+ const db = (0, db_1.openDatabase)(pmemPath);
404
+ const row = db.prepare("SELECT started_at FROM sessions WHERE ended_at IS NULL ORDER BY started_at DESC LIMIT 1").get();
405
+ return row?.started_at ?? null;
406
+ }
407
+ catch {
408
+ return null;
409
+ }
410
+ }
411
+ function generateSuggestions(pmemPath, includeHistory = false) {
362
412
  const dbPath = path.join(pmemPath, 'pmem.db');
363
413
  if (!(0, fs_1.fileExists)(dbPath)) {
364
- console.log('No SQLite database. Run pmem rebuild first.');
365
- process.exit(2);
414
+ return {
415
+ summary: { affected_cards: 0, blocking: 0, warning: 0, info: 0, duplicates_hidden: 0, historical_hidden: 0, verify_blocking: false },
416
+ message: 'No SQLite database. Run pmem rebuild first.',
417
+ next_steps: ['Run `pmem rebuild` to create the database index.'],
418
+ groups: { blocking_for_verify: [], current_suggestions: [], historical_dirty_flags: [] },
419
+ error: true,
420
+ };
366
421
  }
367
422
  let db;
368
423
  try {
369
424
  db = (0, db_1.openDatabase)(pmemPath);
370
425
  }
371
426
  catch {
372
- console.log('No SQLite database. Run pmem rebuild first.');
373
- process.exit(2);
427
+ return {
428
+ summary: { affected_cards: 0, blocking: 0, warning: 0, info: 0, duplicates_hidden: 0, historical_hidden: 0, verify_blocking: false },
429
+ message: 'Cannot open database. Run pmem rebuild first.',
430
+ next_steps: ['Run `pmem rebuild` to recreate the database.'],
431
+ groups: { blocking_for_verify: [], current_suggestions: [], historical_dirty_flags: [] },
432
+ error: true,
433
+ };
374
434
  }
375
- const unresolved = (0, db_1.getUnresolvedDirtyFlags)(db);
376
- const suggestions = [];
377
- // Card-level dirty flags update_card suggestions
378
- for (const flag of unresolved) {
379
- if (flag.scope === 'card') {
380
- suggestions.push({
381
- id: `suggest-${suggestions.length + 1}`,
382
- action: 'update_card',
383
- target: flag.target,
384
- reason: flag.reason,
385
- priority: 'high',
386
- });
387
- }
388
- }
389
- // Project-level dirty → create_trace or confirm suggestions
390
- for (const flag of unresolved) {
391
- if (flag.scope === 'project') {
392
- suggestions.push({
393
- id: `suggest-${suggestions.length + 1}`,
394
- action: 'create_trace',
395
- target: flag.target,
396
- reason: flag.reason,
397
- priority: 'medium',
398
- });
435
+ // 1. Get raw dirty flags with full details
436
+ const allFlags = (0, db_1.getUnresolvedDirtyFlagsDetailed)(db);
437
+ // 2. Run shared stale-memory consistency check
438
+ const staleIssues = (0, consistency_1.checkStaleMemory)(pmemPath);
439
+ // Build lookup: card_id → set of stale file paths
440
+ const staleByCard = new Map();
441
+ for (const issue of staleIssues) {
442
+ if (issue.card_id) {
443
+ if (!staleByCard.has(issue.card_id)) {
444
+ staleByCard.set(issue.card_id, new Set());
445
+ }
446
+ if (issue.file_path) {
447
+ staleByCard.get(issue.card_id).add(issue.file_path);
448
+ }
399
449
  }
400
450
  }
401
- // Check state.md freshness
402
- let stateFreshness = 'fresh';
403
- const statePath = path.join(pmemPath, 'state.md');
404
- if ((0, fs_1.fileExists)(statePath)) {
405
- const fs = require('fs');
406
- const stat = fs.statSync(statePath);
407
- const hoursSinceUpdate = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60);
408
- if (hoursSinceUpdate > 24) {
409
- stateFreshness = 'stale';
451
+ // 3. Aggregate dirty flags by target + reason + matched_file
452
+ const groups = new Map();
453
+ for (const flag of allFlags) {
454
+ const key = aggregationKey(flag);
455
+ if (!groups.has(key)) {
456
+ groups.set(key, []);
410
457
  }
458
+ groups.get(key).push(flag);
411
459
  }
412
- if (stateFreshness === 'stale') {
413
- suggestions.push({
414
- id: `suggest-${suggestions.length + 1}`,
415
- action: 'update_state',
416
- target: 'state.md',
417
- reason: 'state.md has not been updated in over 24 hours',
418
- priority: 'medium',
419
- });
460
+ // 4. Get session boundaries for historical classification
461
+ const latestSessionEnd = getLatestSessionEnd(pmemPath);
462
+ const activeSessionStart = getActiveSessionStart(pmemPath);
463
+ const sessionBoundary = latestSessionEnd || activeSessionStart;
464
+ // 5. Get total card count for affected_cards
465
+ const cardCount = getCardCount(pmemPath);
466
+ // 6. Classify each aggregated group
467
+ const blockingForVerify = [];
468
+ const currentSuggestions = [];
469
+ const historicalDirtyFlags = [];
470
+ for (const [key, flags] of groups) {
471
+ const representative = flags[0];
472
+ const matchedFile = extractMatchedFile(representative.reason);
473
+ // Determine if this group blocks verify
474
+ let blocksVerify = false;
475
+ if (representative.scope === 'card' && staleByCard.has(representative.target)) {
476
+ const staleFiles = staleByCard.get(representative.target);
477
+ if (matchedFile && staleFiles.has(matchedFile)) {
478
+ blocksVerify = true;
479
+ }
480
+ else if (!matchedFile) {
481
+ // Card is in stale list, even if we can't match the specific file
482
+ blocksVerify = true;
483
+ }
484
+ }
485
+ // Determine severity
486
+ let severity;
487
+ if (blocksVerify) {
488
+ severity = 'blocking';
489
+ }
490
+ else if (representative.scope === 'card') {
491
+ severity = 'warning';
492
+ }
493
+ else {
494
+ severity = 'info';
495
+ }
496
+ // Historical classification
497
+ const allCreatedAts = flags.map(f => f.created_at).sort();
498
+ const latestCreated = allCreatedAts[allCreatedAts.length - 1];
499
+ const earliestCreated = allCreatedAts[0];
500
+ const isMulti = flags.length > 1;
501
+ let isHistorical = false;
502
+ let isDuplicate = false;
503
+ if (blocksVerify) {
504
+ // Blocking items are never historical
505
+ isHistorical = false;
506
+ isDuplicate = isMulti;
507
+ }
508
+ else if (sessionBoundary && latestCreated < sessionBoundary) {
509
+ // All flags are from before the session boundary → historical
510
+ isHistorical = true;
511
+ isDuplicate = isMulti;
512
+ }
513
+ else if (isMulti && sessionBoundary && latestCreated < sessionBoundary) {
514
+ // Multiple flags, all old → historical duplicate
515
+ isHistorical = true;
516
+ isDuplicate = true;
517
+ }
518
+ else {
519
+ // Default: keep in current
520
+ isHistorical = false;
521
+ isDuplicate = isMulti;
522
+ }
523
+ const aggregated = {
524
+ target: representative.target,
525
+ reason: representative.reason,
526
+ matched_file: matchedFile,
527
+ count: flags.length,
528
+ severity,
529
+ blocks_verify: blocksVerify,
530
+ is_duplicate: isDuplicate,
531
+ is_historical: isHistorical,
532
+ created_at_first: earliestCreated,
533
+ created_at_last: latestCreated,
534
+ sources: flags.map(f => ({
535
+ scope: f.scope,
536
+ target: f.target,
537
+ reason: f.reason,
538
+ created_at: f.created_at,
539
+ session_id: f.session_id,
540
+ })),
541
+ };
542
+ if (blocksVerify) {
543
+ blockingForVerify.push(aggregated);
544
+ }
545
+ else if (isHistorical) {
546
+ historicalDirtyFlags.push(aggregated);
547
+ }
548
+ else {
549
+ currentSuggestions.push(aggregated);
550
+ }
420
551
  }
421
- // Check next.md content
422
- const nextPath = path.join(pmemPath, 'next.md');
423
- let nextEmpty = false;
424
- if ((0, fs_1.fileExists)(nextPath)) {
425
- const content = (0, fs_1.readFile)(nextPath) || '';
426
- if (content.replace(/#.*\n/g, '').trim().length < 50) {
427
- nextEmpty = true;
428
- }
429
- }
430
- if (nextEmpty) {
431
- suggestions.push({
432
- id: `suggest-${suggestions.length + 1}`,
433
- action: 'update_next',
434
- target: 'next.md',
435
- reason: 'next.md appears to have minimal content',
436
- priority: 'low',
437
- });
438
- }
439
- (0, db_1.closeDatabase)();
440
- return { dirtyFlags: unresolved, stateFreshness, suggestions };
552
+ // 7. Compute summary
553
+ const uniqueAffectedCards = new Set();
554
+ for (const item of [...blockingForVerify, ...currentSuggestions]) {
555
+ uniqueAffectedCards.add(item.target);
556
+ }
557
+ const duplicatesHidden = [...blockingForVerify, ...currentSuggestions, ...historicalDirtyFlags]
558
+ .filter(g => g.count > 1)
559
+ .reduce((sum, g) => sum + (g.count - 1), 0);
560
+ const summary = {
561
+ affected_cards: uniqueAffectedCards.size,
562
+ blocking: blockingForVerify.length,
563
+ warning: currentSuggestions.filter(g => g.severity === 'warning').length,
564
+ info: currentSuggestions.filter(g => g.severity === 'info').length,
565
+ duplicates_hidden: duplicatesHidden,
566
+ historical_hidden: includeHistory ? 0 : historicalDirtyFlags.length,
567
+ verify_blocking: blockingForVerify.length > 0,
568
+ };
569
+ // 8. Build message and next steps
570
+ const message = buildSuggestMessage(summary, cardCount);
571
+ const nextSteps = buildSuggestNextSteps(summary, cardCount);
572
+ return {
573
+ summary,
574
+ message,
575
+ next_steps: nextSteps,
576
+ groups: {
577
+ blocking_for_verify: blockingForVerify,
578
+ current_suggestions: currentSuggestions,
579
+ historical_dirty_flags: includeHistory ? historicalDirtyFlags : [],
580
+ },
581
+ };
441
582
  }
442
- function suggestActions(pmemPath, format) {
443
- const { dirtyFlags, stateFreshness, suggestions } = generateSuggestions(pmemPath);
444
- // Build contextual message and next steps for empty results
445
- const cardCount = getCardCount(pmemPath);
446
- const message = buildSuggestMessage(suggestions, dirtyFlags, cardCount);
447
- const nextSteps = buildSuggestNextSteps(suggestions, dirtyFlags, cardCount);
583
+ function suggestActions(pmemPath, format, includeHistory) {
584
+ const report = generateSuggestions(pmemPath, includeHistory);
448
585
  if (format === 'json') {
449
586
  console.log(JSON.stringify({
450
- dirty_flags: dirtyFlags,
451
- state_freshness: stateFreshness,
452
- suggestions,
453
- message,
454
- next_steps: nextSteps,
587
+ summary: report.summary,
588
+ message: report.message,
589
+ next_steps: report.next_steps,
590
+ groups: report.groups,
455
591
  }, null, 2));
456
592
  }
457
593
  else {
458
- if (dirtyFlags.length > 0) {
459
- console.log(`Dirty flags: ${dirtyFlags.length}`);
460
- for (const flag of dirtyFlags) {
461
- console.log(` [${flag.scope}] ${flag.target}: ${flag.reason}`);
594
+ // Compact output
595
+ console.log('Memory update suggestions');
596
+ console.log('');
597
+ console.log(`Affected cards: ${report.summary.affected_cards}`);
598
+ console.log(`Blocking for verify: ${report.summary.blocking}`);
599
+ console.log(`Current suggestions: ${report.summary.warning + report.summary.info}`);
600
+ console.log(`Historical hidden: ${report.summary.historical_hidden}`);
601
+ console.log(`Duplicate flags hidden: ${report.summary.duplicates_hidden}`);
602
+ // Blocking section
603
+ if (report.groups.blocking_for_verify.length > 0) {
604
+ console.log('');
605
+ console.log('Blocking:');
606
+ for (const item of report.groups.blocking_for_verify) {
607
+ const filePart = (item.matched_file && !item.reason.includes(item.matched_file)) ? `, ${item.matched_file}` : '';
608
+ const countPart = item.count > 1 ? `, count ${item.count}` : '';
609
+ console.log(` - ${item.target} (${item.reason}${filePart}${countPart})`);
462
610
  }
463
611
  }
464
- else {
465
- console.log('No unresolved dirty flags.');
466
- }
467
- console.log(`State freshness: ${stateFreshness}`);
468
- if (suggestions.length > 0) {
469
- console.log(`\nSuggestions (${suggestions.length}):`);
470
- for (const s of suggestions) {
471
- console.log(` ${s.id}: [${s.priority}] ${s.action} ${s.target}`);
472
- console.log(` ${s.reason}`);
612
+ // Current section
613
+ if (report.groups.current_suggestions.length > 0) {
614
+ console.log('');
615
+ console.log('Current:');
616
+ for (const item of report.groups.current_suggestions) {
617
+ const filePart = (item.matched_file && !item.reason.includes(item.matched_file)) ? `, ${item.matched_file}` : '';
618
+ const countPart = item.count > 1 ? `, count ${item.count}` : '';
619
+ console.log(` - ${item.target} (${item.reason}${filePart}${countPart})`);
473
620
  }
474
621
  }
622
+ // Historical section (only when --include-history)
623
+ if (includeHistory && report.groups.historical_dirty_flags.length > 0) {
624
+ console.log('');
625
+ console.log('Historical:');
626
+ for (const item of report.groups.historical_dirty_flags) {
627
+ const filePart = (item.matched_file && !item.reason.includes(item.matched_file)) ? `, ${item.matched_file}` : '';
628
+ const countPart = item.count > 1 ? `, count ${item.count}` : '';
629
+ console.log(` - ${item.target} (${item.reason}${filePart}${countPart})`);
630
+ }
631
+ }
632
+ // Message
633
+ console.log('');
634
+ if (report.summary.blocking > 0) {
635
+ console.log(report.message);
636
+ }
475
637
  else {
476
- console.log(`\n${message}`);
477
- if (nextSteps.length > 0) {
478
- console.log('\nNext steps:');
479
- for (const step of nextSteps) {
480
- console.log(` ${step}`);
481
- }
638
+ console.log('No blocking memory consistency issues.');
639
+ if (report.summary.historical_hidden > 0) {
640
+ console.log('Historical suggestions available with --include-history.');
482
641
  }
483
642
  }
643
+ // Next steps
644
+ if (report.next_steps.length > 0) {
645
+ console.log('');
646
+ console.log('Next:');
647
+ for (const step of report.next_steps) {
648
+ console.log(` - ${step}`);
649
+ }
650
+ }
651
+ }
652
+ // Exit code: 2 for runtime errors (missing DB, etc.)
653
+ if (report.error) {
654
+ process.exit(2);
484
655
  }
485
- const hasSuggestions = suggestions.length > 0;
486
- process.exit(hasSuggestions ? 1 : 0);
656
+ // v0.6.2: Exit 0 regardless of whether suggestions were found.
657
+ // Exit 1 is no longer used as "actionable suggestions exist" workflow signal.
658
+ // Agents should check JSON output summary fields instead of exit code.
487
659
  }
488
660
  function getCardCount(pmemPath) {
489
661
  try {
@@ -495,45 +667,70 @@ function getCardCount(pmemPath) {
495
667
  return 0;
496
668
  }
497
669
  }
498
- function buildSuggestMessage(suggestions, dirtyFlags, cardCount) {
499
- if (suggestions.length > 0) {
500
- return `${suggestions.length} suggestion(s) found.`;
501
- }
502
- if (dirtyFlags.length > 0) {
503
- return 'Dirty flags exist but no matching suggestions could be generated.';
504
- }
670
+ function buildSuggestMessage(summary, cardCount) {
505
671
  if (cardCount === 0) {
506
672
  return 'No memory cards found. Create a first module, decision, or task card to start building project memory.';
507
673
  }
674
+ if (summary.blocking > 0 || summary.warning > 0 || summary.info > 0) {
675
+ const parts = [];
676
+ if (summary.blocking > 0)
677
+ parts.push(`${summary.blocking} blocking memory consistency issue(s)`);
678
+ if (summary.warning > 0)
679
+ parts.push(`${summary.warning} current suggestion(s)`);
680
+ if (summary.info > 0)
681
+ parts.push(`${summary.info} informational item(s)`);
682
+ return parts.join(' and ') + '.';
683
+ }
508
684
  return 'No suggestions. Memory is up to date.';
509
685
  }
510
- function buildSuggestNextSteps(suggestions, dirtyFlags, cardCount) {
511
- if (suggestions.length > 0)
512
- return [];
686
+ function buildSuggestNextSteps(summary, cardCount) {
513
687
  const steps = [];
514
688
  if (cardCount === 0) {
515
689
  steps.push('Create a module card with source_files pointing to your code');
516
690
  steps.push('Run `pmem rebuild` after creating cards');
517
691
  steps.push('Then try `pmem status` and `pmem mark-dirty --auto`');
518
692
  }
519
- else if (dirtyFlags.length === 0) {
520
- steps.push('Edit some source files, then run `pmem status` and `pmem mark-dirty --auto`');
521
- steps.push('Run `pmem verify` to check overall memory consistency');
693
+ else if (summary.blocking > 0 || summary.warning > 0) {
694
+ steps.push('Update or confirm affected cards with pmem update --confirm -s "<summary>" -n "<next step>"');
695
+ steps.push('Use --include-history to inspect older dirty flags.');
696
+ }
697
+ else if (summary.historical_hidden > 0) {
698
+ steps.push('Use --include-history to inspect older dirty flags.');
699
+ steps.push('Run `pmem verify` to check overall memory consistency.');
522
700
  }
523
701
  else {
524
- steps.push('Run `pmem update --confirm` to manually record changes');
525
- steps.push('Check that dirty cards have source_files defined in their frontmatter');
702
+ steps.push('Edit some source files, then run `pmem status` and `pmem mark-dirty --auto`');
703
+ steps.push('Run `pmem verify` to check overall memory consistency');
526
704
  }
527
705
  return steps;
528
706
  }
529
707
  function applySuggestionAction(pmemPath, suggestionId) {
530
- // Re-derive suggestions to find the matching one
531
- const { suggestions, dirtyFlags } = generateSuggestions(pmemPath);
532
- const match = suggestions.find(s => s.id === suggestionId);
708
+ // Re-derive suggestions to find the matching one (with history included for full search)
709
+ const report = generateSuggestions(pmemPath, true);
710
+ // Flatten all groups into a single searchable list with generated IDs
711
+ const flatList = [];
712
+ let idx = 1;
713
+ for (const item of report.groups.blocking_for_verify) {
714
+ const action = item.reason.startsWith('file_changed') ? 'update_card' : 'create_trace';
715
+ flatList.push({ id: `suggest-${idx}`, item, action });
716
+ idx++;
717
+ }
718
+ for (const item of report.groups.current_suggestions) {
719
+ const action = item.reason.startsWith('file_changed') ? 'update_card' : 'create_trace';
720
+ flatList.push({ id: `suggest-${idx}`, item, action });
721
+ idx++;
722
+ }
723
+ for (const item of report.groups.historical_dirty_flags) {
724
+ const action = item.reason.startsWith('file_changed') ? 'update_card' : 'create_trace';
725
+ flatList.push({ id: `suggest-${idx}`, item, action });
726
+ idx++;
727
+ }
728
+ const match = flatList.find(s => s.id === suggestionId);
533
729
  if (!match) {
534
730
  console.log(`Suggestion "${suggestionId}" not found. Available suggestions:`);
535
- for (const s of suggestions) {
536
- console.log(` ${s.id}: ${s.action} ${s.target}`);
731
+ for (const s of flatList) {
732
+ const filePart = s.item.matched_file ? `, ${s.item.matched_file}` : '';
733
+ console.log(` ${s.id}: ${s.action} ${s.item.target} (${s.item.reason}${filePart})`);
537
734
  }
538
735
  process.exit(2);
539
736
  }
@@ -542,14 +739,17 @@ function applySuggestionAction(pmemPath, suggestionId) {
542
739
  console.log('No SQLite database. Run pmem rebuild first.');
543
740
  process.exit(2);
544
741
  }
545
- switch (match.action) {
742
+ const action = match.action;
743
+ const target = match.item.target;
744
+ const reason = match.item.reason;
745
+ switch (action) {
546
746
  case 'update_card': {
547
747
  const db = (0, db_1.openDatabase)(pmemPath);
548
748
  // Mark the card's last_verified_at as expired
549
- db.prepare("UPDATE cards SET last_verified_at = ? WHERE id = ?").run(new Date(0).toISOString(), match.target);
749
+ db.prepare("UPDATE cards SET last_verified_at = ? WHERE id = ?").run(new Date(0).toISOString(), target);
550
750
  (0, db_1.closeDatabase)();
551
- console.log(`Marked card "${match.target}" as needing verification.`);
552
- console.log(` Reason: ${match.reason}`);
751
+ console.log(`Marked card "${target}" as needing verification.`);
752
+ console.log(` Reason: ${reason}`);
553
753
  break;
554
754
  }
555
755
  case 'create_trace': {
@@ -568,35 +768,37 @@ type: trace
568
768
  created: ${today}
569
769
  ---
570
770
 
571
- # Trace: ${match.reason}
771
+ # Trace: ${reason}
572
772
 
573
773
  ## What Changed
574
- ${match.reason}
774
+ ${reason}
575
775
 
576
776
  ## Next
577
777
  Continue as planned.
578
778
  `);
579
779
  console.log(`Auto-created trace: traces/${today}-${traceNum}.md`);
580
- console.log(` Reason: ${match.reason}`);
581
- // Log in SQLite
780
+ console.log(` Reason: ${reason}`);
781
+ // Resolve the associated dirty flags
582
782
  const db = (0, db_1.openDatabase)(pmemPath);
583
783
  const activeSession = (0, db_1.getActiveSession)(db);
584
- (0, db_1.insertUpdateLog)(db, 'auto_trace', match.reason, activeSession?.id, [`trace.${today}-${traceNum}`], true);
784
+ // Resolve all dirty flags matching this target+reason
785
+ (0, db_1.resolveDirtyFlags)(db, 'card', target);
786
+ (0, db_1.insertUpdateLog)(db, 'auto_trace', reason, activeSession?.id, [`trace.${today}-${traceNum}`], true);
585
787
  (0, db_1.closeDatabase)();
586
788
  break;
587
789
  }
588
790
  case 'update_state': {
589
- console.log(`Action required: ${match.reason}`);
791
+ console.log(`Action required: ${reason}`);
590
792
  console.log(' Please run `pmem update --confirm` to update state.md.');
591
793
  break;
592
794
  }
593
795
  case 'update_next': {
594
- console.log(`Action required: ${match.reason}`);
796
+ console.log(`Action required: ${reason}`);
595
797
  console.log(' Please run `pmem update --confirm --next "<next step>"` to update next.md.');
596
798
  break;
597
799
  }
598
800
  default: {
599
- console.log(`Unknown action "${match.action}" for suggestion ${suggestionId}.`);
801
+ console.log(`Unknown action "${action}" for suggestion ${suggestionId}.`);
600
802
  process.exit(2);
601
803
  }
602
804
  }