pmem-ai 0.6.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  All notable changes to pmem are documented here.
4
4
 
5
+ ## 0.6.1 - Actionable Update Suggestions
6
+
7
+ ### Added
8
+
9
+ - Aggregate duplicate `update --suggest` items by `target + reason + matched_file`, with counts and `sources` arrays.
10
+ - Split suggestion output into `blocking_for_verify`, `current_suggestions`, and `historical_dirty_flags` groups.
11
+ - Add machine-readable severity metadata per suggestion: `severity`, `blocks_verify`, `is_duplicate`, `is_historical`.
12
+ - Compact output summarizing affected cards, blocking issues, hidden duplicates, and hidden history.
13
+ - `--include-history` flag to inspect historical dirty flags that are hidden by default.
14
+ - Shared `checkStaleMemory()` in `src/core/consistency.ts`, aligning `update --suggest` with `pmem verify`.
15
+ - Structured JSON output with `summary`, `message`, `next_steps`, and `groups` for agent decision-making.
16
+ - v0.6.1 E2E test suite covering duplicate aggregation, historical hiding, include-history, missing DB, and blocking groups.
17
+
18
+ ### Changed
19
+
20
+ - `update --suggest --format json` output restructured from flat arrays to `summary` + `groups`.
21
+ - `pmem verify` stale-memory check now delegates to shared `checkStaleMemory()` from `consistency.ts`.
22
+ - `update --suggest` exit code: only hidden historical items return 0; missing/corrupt DB returns 2 (runtime error).
23
+
24
+ ### Fixed
25
+
26
+ - Long-term projects no longer see repeated dirty flags and historical suggestions flooding `update --suggest` output.
27
+ - `pmem verify` 100/100 and `update --suggest` now semantically aligned — verify-clean projects see "No blocking memory consistency issues."
28
+
5
29
  ## 0.6.0 - Agent-native Workflow Polish
6
30
 
7
31
  ### Added
@@ -7,6 +7,7 @@ export declare function updateCommand(options: {
7
7
  suggest?: boolean;
8
8
  applySuggestion?: string;
9
9
  format?: string;
10
+ includeHistory?: boolean;
10
11
  }): void;
11
12
  export declare function markDirtyCommand(reason: string, options?: {
12
13
  auto?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/commands/update.ts"],"names":[],"mappings":"AAUA,wBAAgB,aAAa,CAAC,OAAO,EAAE;IACrC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,IAAI,CAyCP;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,IAAI,CA8FvF"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/commands/update.ts"],"names":[],"mappings":"AAaA,wBAAgB,aAAa,CAAC,OAAO,EAAE;IACrC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,GAAG,IAAI,CAyCP;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,IAAI,CA8FvF"}
@@ -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
@@ -358,132 +359,301 @@ function listSourceFiles(root) {
358
359
  walk(root);
359
360
  return results;
360
361
  }
361
- function generateSuggestions(pmemPath) {
362
+ /**
363
+ * Extract the matched file path from a dirty flag reason string.
364
+ * Handles formats like "file_changed: path/to/file" or plain text.
365
+ */
366
+ function extractMatchedFile(reason) {
367
+ const match = reason.match(/^file_changed:\s*(.+)/);
368
+ if (match) {
369
+ return match[1].trim();
370
+ }
371
+ return null;
372
+ }
373
+ /**
374
+ * Build the aggregation key for a dirty flag: target + reason + matched_file.
375
+ */
376
+ function aggregationKey(flag) {
377
+ const mf = extractMatchedFile(flag.reason);
378
+ return `${flag.target}||${flag.reason}||${mf ?? ''}`;
379
+ }
380
+ /**
381
+ * Find the most recent session end time.
382
+ * Returns null if no ended session exists.
383
+ */
384
+ function getLatestSessionEnd(pmemPath) {
385
+ try {
386
+ const db = (0, db_1.openDatabase)(pmemPath);
387
+ const row = db.prepare("SELECT ended_at FROM sessions WHERE ended_at IS NOT NULL ORDER BY ended_at DESC LIMIT 1").get();
388
+ return row?.ended_at ?? null;
389
+ }
390
+ catch {
391
+ return null;
392
+ }
393
+ }
394
+ /**
395
+ * Get the active (un-ended) session if one exists.
396
+ */
397
+ function getActiveSessionStart(pmemPath) {
398
+ try {
399
+ const db = (0, db_1.openDatabase)(pmemPath);
400
+ const row = db.prepare("SELECT started_at FROM sessions WHERE ended_at IS NULL ORDER BY started_at DESC LIMIT 1").get();
401
+ return row?.started_at ?? null;
402
+ }
403
+ catch {
404
+ return null;
405
+ }
406
+ }
407
+ function generateSuggestions(pmemPath, includeHistory = false) {
362
408
  const dbPath = path.join(pmemPath, 'pmem.db');
363
409
  if (!(0, fs_1.fileExists)(dbPath)) {
364
- console.log('No SQLite database. Run pmem rebuild first.');
365
- process.exit(2);
410
+ return {
411
+ summary: { affected_cards: 0, blocking: 0, warning: 0, info: 0, duplicates_hidden: 0, historical_hidden: 0, verify_blocking: false },
412
+ message: 'No SQLite database. Run pmem rebuild first.',
413
+ next_steps: ['Run `pmem rebuild` to create the database index.'],
414
+ groups: { blocking_for_verify: [], current_suggestions: [], historical_dirty_flags: [] },
415
+ error: true,
416
+ };
366
417
  }
367
418
  let db;
368
419
  try {
369
420
  db = (0, db_1.openDatabase)(pmemPath);
370
421
  }
371
422
  catch {
372
- console.log('No SQLite database. Run pmem rebuild first.');
373
- process.exit(2);
423
+ return {
424
+ summary: { affected_cards: 0, blocking: 0, warning: 0, info: 0, duplicates_hidden: 0, historical_hidden: 0, verify_blocking: false },
425
+ message: 'Cannot open database. Run pmem rebuild first.',
426
+ next_steps: ['Run `pmem rebuild` to recreate the database.'],
427
+ groups: { blocking_for_verify: [], current_suggestions: [], historical_dirty_flags: [] },
428
+ error: true,
429
+ };
374
430
  }
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
- });
431
+ // 1. Get raw dirty flags with full details
432
+ const allFlags = (0, db_1.getUnresolvedDirtyFlagsDetailed)(db);
433
+ // 2. Run shared stale-memory consistency check
434
+ const staleIssues = (0, consistency_1.checkStaleMemory)(pmemPath);
435
+ // Build lookup: card_id → set of stale file paths
436
+ const staleByCard = new Map();
437
+ for (const issue of staleIssues) {
438
+ if (issue.card_id) {
439
+ if (!staleByCard.has(issue.card_id)) {
440
+ staleByCard.set(issue.card_id, new Set());
441
+ }
442
+ if (issue.file_path) {
443
+ staleByCard.get(issue.card_id).add(issue.file_path);
444
+ }
399
445
  }
400
446
  }
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';
447
+ // 3. Aggregate dirty flags by target + reason + matched_file
448
+ const groups = new Map();
449
+ for (const flag of allFlags) {
450
+ const key = aggregationKey(flag);
451
+ if (!groups.has(key)) {
452
+ groups.set(key, []);
410
453
  }
454
+ groups.get(key).push(flag);
411
455
  }
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
- });
456
+ // 4. Get session boundaries for historical classification
457
+ const latestSessionEnd = getLatestSessionEnd(pmemPath);
458
+ const activeSessionStart = getActiveSessionStart(pmemPath);
459
+ const sessionBoundary = latestSessionEnd || activeSessionStart;
460
+ // 5. Get total card count for affected_cards
461
+ const cardCount = getCardCount(pmemPath);
462
+ // 6. Classify each aggregated group
463
+ const blockingForVerify = [];
464
+ const currentSuggestions = [];
465
+ const historicalDirtyFlags = [];
466
+ for (const [key, flags] of groups) {
467
+ const representative = flags[0];
468
+ const matchedFile = extractMatchedFile(representative.reason);
469
+ // Determine if this group blocks verify
470
+ let blocksVerify = false;
471
+ if (representative.scope === 'card' && staleByCard.has(representative.target)) {
472
+ const staleFiles = staleByCard.get(representative.target);
473
+ if (matchedFile && staleFiles.has(matchedFile)) {
474
+ blocksVerify = true;
475
+ }
476
+ else if (!matchedFile) {
477
+ // Card is in stale list, even if we can't match the specific file
478
+ blocksVerify = true;
479
+ }
480
+ }
481
+ // Determine severity
482
+ let severity;
483
+ if (blocksVerify) {
484
+ severity = 'blocking';
485
+ }
486
+ else if (representative.scope === 'card') {
487
+ severity = 'warning';
488
+ }
489
+ else {
490
+ severity = 'info';
491
+ }
492
+ // Historical classification
493
+ const allCreatedAts = flags.map(f => f.created_at).sort();
494
+ const latestCreated = allCreatedAts[allCreatedAts.length - 1];
495
+ const earliestCreated = allCreatedAts[0];
496
+ const isMulti = flags.length > 1;
497
+ let isHistorical = false;
498
+ let isDuplicate = false;
499
+ if (blocksVerify) {
500
+ // Blocking items are never historical
501
+ isHistorical = false;
502
+ isDuplicate = isMulti;
503
+ }
504
+ else if (sessionBoundary && latestCreated < sessionBoundary) {
505
+ // All flags are from before the session boundary → historical
506
+ isHistorical = true;
507
+ isDuplicate = isMulti;
508
+ }
509
+ else if (isMulti && sessionBoundary && latestCreated < sessionBoundary) {
510
+ // Multiple flags, all old → historical duplicate
511
+ isHistorical = true;
512
+ isDuplicate = true;
513
+ }
514
+ else {
515
+ // Default: keep in current
516
+ isHistorical = false;
517
+ isDuplicate = isMulti;
518
+ }
519
+ const aggregated = {
520
+ target: representative.target,
521
+ reason: representative.reason,
522
+ matched_file: matchedFile,
523
+ count: flags.length,
524
+ severity,
525
+ blocks_verify: blocksVerify,
526
+ is_duplicate: isDuplicate,
527
+ is_historical: isHistorical,
528
+ created_at_first: earliestCreated,
529
+ created_at_last: latestCreated,
530
+ sources: flags.map(f => ({
531
+ scope: f.scope,
532
+ target: f.target,
533
+ reason: f.reason,
534
+ created_at: f.created_at,
535
+ session_id: f.session_id,
536
+ })),
537
+ };
538
+ if (blocksVerify) {
539
+ blockingForVerify.push(aggregated);
540
+ }
541
+ else if (isHistorical) {
542
+ historicalDirtyFlags.push(aggregated);
543
+ }
544
+ else {
545
+ currentSuggestions.push(aggregated);
546
+ }
420
547
  }
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 };
548
+ // 7. Compute summary
549
+ const uniqueAffectedCards = new Set();
550
+ for (const item of [...blockingForVerify, ...currentSuggestions]) {
551
+ uniqueAffectedCards.add(item.target);
552
+ }
553
+ const duplicatesHidden = [...blockingForVerify, ...currentSuggestions, ...historicalDirtyFlags]
554
+ .filter(g => g.count > 1)
555
+ .reduce((sum, g) => sum + (g.count - 1), 0);
556
+ const summary = {
557
+ affected_cards: uniqueAffectedCards.size,
558
+ blocking: blockingForVerify.length,
559
+ warning: currentSuggestions.filter(g => g.severity === 'warning').length,
560
+ info: currentSuggestions.filter(g => g.severity === 'info').length,
561
+ duplicates_hidden: duplicatesHidden,
562
+ historical_hidden: includeHistory ? 0 : historicalDirtyFlags.length,
563
+ verify_blocking: blockingForVerify.length > 0,
564
+ };
565
+ // 8. Build message and next steps
566
+ const message = buildSuggestMessage(summary, cardCount);
567
+ const nextSteps = buildSuggestNextSteps(summary, cardCount);
568
+ return {
569
+ summary,
570
+ message,
571
+ next_steps: nextSteps,
572
+ groups: {
573
+ blocking_for_verify: blockingForVerify,
574
+ current_suggestions: currentSuggestions,
575
+ historical_dirty_flags: includeHistory ? historicalDirtyFlags : [],
576
+ },
577
+ };
441
578
  }
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);
579
+ function suggestActions(pmemPath, format, includeHistory) {
580
+ const report = generateSuggestions(pmemPath, includeHistory);
448
581
  if (format === 'json') {
449
582
  console.log(JSON.stringify({
450
- dirty_flags: dirtyFlags,
451
- state_freshness: stateFreshness,
452
- suggestions,
453
- message,
454
- next_steps: nextSteps,
583
+ summary: report.summary,
584
+ message: report.message,
585
+ next_steps: report.next_steps,
586
+ groups: report.groups,
455
587
  }, null, 2));
456
588
  }
457
589
  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}`);
590
+ // Compact output
591
+ console.log('Memory update suggestions');
592
+ console.log('');
593
+ console.log(`Affected cards: ${report.summary.affected_cards}`);
594
+ console.log(`Blocking for verify: ${report.summary.blocking}`);
595
+ console.log(`Current suggestions: ${report.summary.warning + report.summary.info}`);
596
+ console.log(`Historical hidden: ${report.summary.historical_hidden}`);
597
+ console.log(`Duplicate flags hidden: ${report.summary.duplicates_hidden}`);
598
+ // Blocking section
599
+ if (report.groups.blocking_for_verify.length > 0) {
600
+ console.log('');
601
+ console.log('Blocking:');
602
+ for (const item of report.groups.blocking_for_verify) {
603
+ const filePart = (item.matched_file && !item.reason.includes(item.matched_file)) ? `, ${item.matched_file}` : '';
604
+ const countPart = item.count > 1 ? `, count ${item.count}` : '';
605
+ console.log(` - ${item.target} (${item.reason}${filePart}${countPart})`);
462
606
  }
463
607
  }
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}`);
608
+ // Current section
609
+ if (report.groups.current_suggestions.length > 0) {
610
+ console.log('');
611
+ console.log('Current:');
612
+ for (const item of report.groups.current_suggestions) {
613
+ const filePart = (item.matched_file && !item.reason.includes(item.matched_file)) ? `, ${item.matched_file}` : '';
614
+ const countPart = item.count > 1 ? `, count ${item.count}` : '';
615
+ console.log(` - ${item.target} (${item.reason}${filePart}${countPart})`);
473
616
  }
474
617
  }
618
+ // Historical section (only when --include-history)
619
+ if (includeHistory && report.groups.historical_dirty_flags.length > 0) {
620
+ console.log('');
621
+ console.log('Historical:');
622
+ for (const item of report.groups.historical_dirty_flags) {
623
+ const filePart = (item.matched_file && !item.reason.includes(item.matched_file)) ? `, ${item.matched_file}` : '';
624
+ const countPart = item.count > 1 ? `, count ${item.count}` : '';
625
+ console.log(` - ${item.target} (${item.reason}${filePart}${countPart})`);
626
+ }
627
+ }
628
+ // Message
629
+ console.log('');
630
+ if (report.summary.blocking > 0) {
631
+ console.log(report.message);
632
+ }
475
633
  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
- }
634
+ console.log('No blocking memory consistency issues.');
635
+ if (report.summary.historical_hidden > 0) {
636
+ console.log('Historical suggestions available with --include-history.');
482
637
  }
483
638
  }
639
+ // Next steps
640
+ if (report.next_steps.length > 0) {
641
+ console.log('');
642
+ console.log('Next:');
643
+ for (const step of report.next_steps) {
644
+ console.log(` - ${step}`);
645
+ }
646
+ }
647
+ }
648
+ // Exit code: 2 for runtime errors (missing DB, etc.)
649
+ if (report.error) {
650
+ process.exit(2);
484
651
  }
485
- const hasSuggestions = suggestions.length > 0;
486
- process.exit(hasSuggestions ? 1 : 0);
652
+ // Exit code: 1 if there are blocking or current suggestions (actionable items)
653
+ // Exit 0 if only hidden history or nothing at all
654
+ const hasActionable = report.summary.blocking > 0 ||
655
+ (report.summary.warning + report.summary.info) > 0;
656
+ process.exit(hasActionable ? 1 : 0);
487
657
  }
488
658
  function getCardCount(pmemPath) {
489
659
  try {
@@ -495,45 +665,70 @@ function getCardCount(pmemPath) {
495
665
  return 0;
496
666
  }
497
667
  }
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
- }
668
+ function buildSuggestMessage(summary, cardCount) {
505
669
  if (cardCount === 0) {
506
670
  return 'No memory cards found. Create a first module, decision, or task card to start building project memory.';
507
671
  }
672
+ if (summary.blocking > 0 || summary.warning > 0 || summary.info > 0) {
673
+ const parts = [];
674
+ if (summary.blocking > 0)
675
+ parts.push(`${summary.blocking} blocking memory consistency issue(s)`);
676
+ if (summary.warning > 0)
677
+ parts.push(`${summary.warning} current suggestion(s)`);
678
+ if (summary.info > 0)
679
+ parts.push(`${summary.info} informational item(s)`);
680
+ return parts.join(' and ') + '.';
681
+ }
508
682
  return 'No suggestions. Memory is up to date.';
509
683
  }
510
- function buildSuggestNextSteps(suggestions, dirtyFlags, cardCount) {
511
- if (suggestions.length > 0)
512
- return [];
684
+ function buildSuggestNextSteps(summary, cardCount) {
513
685
  const steps = [];
514
686
  if (cardCount === 0) {
515
687
  steps.push('Create a module card with source_files pointing to your code');
516
688
  steps.push('Run `pmem rebuild` after creating cards');
517
689
  steps.push('Then try `pmem status` and `pmem mark-dirty --auto`');
518
690
  }
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');
691
+ else if (summary.blocking > 0 || summary.warning > 0) {
692
+ steps.push('Update or confirm affected cards with pmem update --confirm -s "<summary>" -n "<next step>"');
693
+ steps.push('Use --include-history to inspect older dirty flags.');
694
+ }
695
+ else if (summary.historical_hidden > 0) {
696
+ steps.push('Use --include-history to inspect older dirty flags.');
697
+ steps.push('Run `pmem verify` to check overall memory consistency.');
522
698
  }
523
699
  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');
700
+ steps.push('Edit some source files, then run `pmem status` and `pmem mark-dirty --auto`');
701
+ steps.push('Run `pmem verify` to check overall memory consistency');
526
702
  }
527
703
  return steps;
528
704
  }
529
705
  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);
706
+ // Re-derive suggestions to find the matching one (with history included for full search)
707
+ const report = generateSuggestions(pmemPath, true);
708
+ // Flatten all groups into a single searchable list with generated IDs
709
+ const flatList = [];
710
+ let idx = 1;
711
+ for (const item of report.groups.blocking_for_verify) {
712
+ const action = item.reason.startsWith('file_changed') ? 'update_card' : 'create_trace';
713
+ flatList.push({ id: `suggest-${idx}`, item, action });
714
+ idx++;
715
+ }
716
+ for (const item of report.groups.current_suggestions) {
717
+ const action = item.reason.startsWith('file_changed') ? 'update_card' : 'create_trace';
718
+ flatList.push({ id: `suggest-${idx}`, item, action });
719
+ idx++;
720
+ }
721
+ for (const item of report.groups.historical_dirty_flags) {
722
+ const action = item.reason.startsWith('file_changed') ? 'update_card' : 'create_trace';
723
+ flatList.push({ id: `suggest-${idx}`, item, action });
724
+ idx++;
725
+ }
726
+ const match = flatList.find(s => s.id === suggestionId);
533
727
  if (!match) {
534
728
  console.log(`Suggestion "${suggestionId}" not found. Available suggestions:`);
535
- for (const s of suggestions) {
536
- console.log(` ${s.id}: ${s.action} ${s.target}`);
729
+ for (const s of flatList) {
730
+ const filePart = s.item.matched_file ? `, ${s.item.matched_file}` : '';
731
+ console.log(` ${s.id}: ${s.action} ${s.item.target} (${s.item.reason}${filePart})`);
537
732
  }
538
733
  process.exit(2);
539
734
  }
@@ -542,14 +737,17 @@ function applySuggestionAction(pmemPath, suggestionId) {
542
737
  console.log('No SQLite database. Run pmem rebuild first.');
543
738
  process.exit(2);
544
739
  }
545
- switch (match.action) {
740
+ const action = match.action;
741
+ const target = match.item.target;
742
+ const reason = match.item.reason;
743
+ switch (action) {
546
744
  case 'update_card': {
547
745
  const db = (0, db_1.openDatabase)(pmemPath);
548
746
  // 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);
747
+ db.prepare("UPDATE cards SET last_verified_at = ? WHERE id = ?").run(new Date(0).toISOString(), target);
550
748
  (0, db_1.closeDatabase)();
551
- console.log(`Marked card "${match.target}" as needing verification.`);
552
- console.log(` Reason: ${match.reason}`);
749
+ console.log(`Marked card "${target}" as needing verification.`);
750
+ console.log(` Reason: ${reason}`);
553
751
  break;
554
752
  }
555
753
  case 'create_trace': {
@@ -568,35 +766,37 @@ type: trace
568
766
  created: ${today}
569
767
  ---
570
768
 
571
- # Trace: ${match.reason}
769
+ # Trace: ${reason}
572
770
 
573
771
  ## What Changed
574
- ${match.reason}
772
+ ${reason}
575
773
 
576
774
  ## Next
577
775
  Continue as planned.
578
776
  `);
579
777
  console.log(`Auto-created trace: traces/${today}-${traceNum}.md`);
580
- console.log(` Reason: ${match.reason}`);
581
- // Log in SQLite
778
+ console.log(` Reason: ${reason}`);
779
+ // Resolve the associated dirty flags
582
780
  const db = (0, db_1.openDatabase)(pmemPath);
583
781
  const activeSession = (0, db_1.getActiveSession)(db);
584
- (0, db_1.insertUpdateLog)(db, 'auto_trace', match.reason, activeSession?.id, [`trace.${today}-${traceNum}`], true);
782
+ // Resolve all dirty flags matching this target+reason
783
+ (0, db_1.resolveDirtyFlags)(db, 'card', target);
784
+ (0, db_1.insertUpdateLog)(db, 'auto_trace', reason, activeSession?.id, [`trace.${today}-${traceNum}`], true);
585
785
  (0, db_1.closeDatabase)();
586
786
  break;
587
787
  }
588
788
  case 'update_state': {
589
- console.log(`Action required: ${match.reason}`);
789
+ console.log(`Action required: ${reason}`);
590
790
  console.log(' Please run `pmem update --confirm` to update state.md.');
591
791
  break;
592
792
  }
593
793
  case 'update_next': {
594
- console.log(`Action required: ${match.reason}`);
794
+ console.log(`Action required: ${reason}`);
595
795
  console.log(' Please run `pmem update --confirm --next "<next step>"` to update next.md.');
596
796
  break;
597
797
  }
598
798
  default: {
599
- console.log(`Unknown action "${match.action}" for suggestion ${suggestionId}.`);
799
+ console.log(`Unknown action "${action}" for suggestion ${suggestionId}.`);
600
800
  process.exit(2);
601
801
  }
602
802
  }