@fyso/awareness-framework 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fyso/awareness-framework",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Methodology and helper CLI for agent awareness, initialization, daily worklogs, handoffs, and evaluation loops.",
5
5
  "type": "module",
6
6
  "author": "Fyso",
package/src/cli.js CHANGED
@@ -18,7 +18,7 @@ export function runCli(argv, options = {}) {
18
18
 
19
19
  try {
20
20
  const parsed = parseArgs(argv);
21
- const [command, subcommand] = parsed.positionals;
21
+ const [command, subcommand, ...positionRest] = parsed.positionals;
22
22
 
23
23
  if (!command || command === 'help' || parsed.opts.help) {
24
24
  printHelp(ctx);
@@ -42,6 +42,16 @@ export function runCli(argv, options = {}) {
42
42
  return handoffCommand(ctx, parsed.opts);
43
43
  case 'evaluate':
44
44
  return evaluateCommand(ctx, parsed.opts);
45
+ case 'memory':
46
+ return memoryCommand(ctx, subcommand, parsed.opts);
47
+ case 'remember':
48
+ return rememberCommand(ctx, parsed.opts);
49
+ case 'recall':
50
+ return recallCommand(ctx, [subcommand, ...positionRest].filter(Boolean).join(' '), parsed.opts);
51
+ case 'forget':
52
+ return forgetCommand(ctx, parsed.opts);
53
+ case 'improve':
54
+ return improveCommand(ctx, parsed.opts);
45
55
  case 'hook':
46
56
  return hookCommand(ctx, subcommand, parsed.opts);
47
57
  case 'schedule':
@@ -108,6 +118,14 @@ Usage:
108
118
  awareness log --task ID --summary TEXT --changes TEXT [--context TEXT] [--state STATE] [--evidence TEXT] [--next TEXT] [--home PATH]
109
119
  awareness handoff [--home PATH]
110
120
  awareness evaluate [--home PATH] [--force] [--print]
121
+ awareness memory candidates [--home PATH]
122
+ awareness memory review [--min-count N] [--home PATH]
123
+ awareness memory note --text TEXT [--evidence TEXT] [--home PATH]
124
+ awareness memory promote --kind preference|pattern|project|review --text TEXT --evidence TEXT [--home PATH]
125
+ awareness remember --text TEXT --evidence TEXT [--home PATH]
126
+ awareness recall QUERY [--limit N] [--home PATH]
127
+ awareness forget --text TEXT --reason TEXT --evidence TEXT [--home PATH]
128
+ awareness improve [--force] [--min-count N] [--home PATH]
111
129
  awareness hook run --event EVENT [--tool TOOL] [--quiet] [--home PATH]
112
130
  awareness hook install --tool codex|claude|opencode|all [--command CMD] [--home PATH] [--user-home PATH] [--config-home PATH] [--overwrite]
113
131
  awareness schedule run --cadence hourly|daily [--home PATH]
@@ -146,7 +164,7 @@ function initCommand(ctx, opts) {
146
164
  writeIfMissing(path.join(home, 'memory', 'personality.md'), readTemplate('personality.md'), created, existing);
147
165
  writeIfMissing(path.join(home, 'memory', 'preferences.md'), privateMemorySeed('Preferences'), created, existing);
148
166
  writeIfMissing(path.join(home, 'memory', 'patterns.md'), privateMemorySeed('Patterns'), created, existing);
149
- writeIfMissing(path.join(home, 'memory', 'long-term.md'), readTemplate('memory-long-term.md'), created, existing);
167
+ writeIfMissing(longTermMemoryPath(home), readTemplate('memory-long-term.md'), created, existing);
150
168
 
151
169
  if (opts.wrappers) {
152
170
  writeWrappers({
@@ -335,7 +353,212 @@ function evaluateCommand(ctx, opts) {
335
353
 
336
354
  ensureDir(path.dirname(evaluationPath));
337
355
  fs.writeFileSync(evaluationPath, content);
356
+ const candidates = recordEvaluationMemoryCandidates(home, today);
338
357
  out(ctx, `Evaluation written: ${evaluationPath}`);
358
+ out(ctx, `Memory candidates: ${candidates.length ? `${candidates.length} recorded` : 'none'}`);
359
+ return 0;
360
+ }
361
+
362
+ function memoryCommand(ctx, subcommand, opts) {
363
+ const home = agentsHome(ctx, opts);
364
+ ensurePrivateState(home, ctx);
365
+
366
+ switch (subcommand) {
367
+ case 'candidates':
368
+ case undefined:
369
+ return memoryCandidatesCommand(ctx, home);
370
+ case 'review':
371
+ return memoryReviewCommand(ctx, home, opts);
372
+ case 'note':
373
+ return memoryNoteCommand(ctx, home, opts);
374
+ case 'promote':
375
+ return memoryPromoteCommand(ctx, home, opts);
376
+ default:
377
+ err(ctx, `Unknown memory command: ${subcommand}`);
378
+ err(ctx, 'Use: candidates, review, note, or promote.');
379
+ return 1;
380
+ }
381
+ }
382
+
383
+ function memoryCandidatesCommand(ctx, home) {
384
+ const content = fs.readFileSync(longTermMemoryPath(home), 'utf8');
385
+ out(ctx, 'Promotion Candidates');
386
+ out(ctx, extractSection(content, 'Promotion Candidates').trim() || '- None yet.');
387
+ return 0;
388
+ }
389
+
390
+ function memoryReviewCommand(ctx, home, opts) {
391
+ const minCount = Number.parseInt(opts.minCount || '2', 10);
392
+ if (!Number.isInteger(minCount) || minCount < 2) {
393
+ throw new Error('Invalid --min-count. Use an integer >= 2.');
394
+ }
395
+
396
+ const content = fs.readFileSync(longTermMemoryPath(home), 'utf8');
397
+ const suggestions = repeatedMemoryCandidateSuggestions(content, minCount);
398
+ out(ctx, 'Memory Review');
399
+
400
+ if (!suggestions.length) {
401
+ out(ctx, `- No repeated candidates found with min-count ${minCount}.`);
402
+ return 0;
403
+ }
404
+
405
+ for (const suggestion of suggestions) {
406
+ out(ctx, `- Suggested pattern (${suggestion.count} observations): ${suggestion.text}`);
407
+ out(ctx, ` Evidence: ${suggestion.evidence}`);
408
+ out(ctx, ` Promote: awareness memory promote --kind pattern --text "${shellQuoteText(suggestion.text)}" --evidence "${shellQuoteText(suggestion.evidence)}"`);
409
+ }
410
+ return 0;
411
+ }
412
+
413
+ function memoryNoteCommand(ctx, home, opts) {
414
+ const text = required(opts, 'text');
415
+ const evidence = opts.evidence || 'Manual observation';
416
+ const today = todayParts(ctx);
417
+ const added = appendMemoryCandidate(home, today, text, evidence);
418
+ out(ctx, added ? `Memory candidate recorded: ${text}` : `Memory candidate already exists: ${text}`);
419
+ return 0;
420
+ }
421
+
422
+ function memoryPromoteCommand(ctx, home, opts) {
423
+ const kind = required(opts, 'kind');
424
+ const text = required(opts, 'text');
425
+ const evidence = required(opts, 'evidence');
426
+ const section = memoryPromotionSection(kind);
427
+ const today = todayParts(ctx);
428
+ const file = longTermMemoryPath(home);
429
+ let content = fs.readFileSync(file, 'utf8');
430
+ content = replaceMetadata(content, 'Updated', formatTimestamp(today));
431
+ content = appendToSection(content, section, `- ${today.date}: ${text} (evidence: ${evidence})\n`);
432
+ fs.writeFileSync(file, content);
433
+ appendMemoryEvent(home, today, {
434
+ type: 'memory.promoted',
435
+ kind,
436
+ section,
437
+ text,
438
+ evidence,
439
+ });
440
+ out(ctx, `Memory promoted to ${section}: ${text}`);
441
+ return 0;
442
+ }
443
+
444
+ function memoryPromotionSection(kind) {
445
+ const sections = {
446
+ preference: 'Preferences',
447
+ pattern: 'Patterns',
448
+ project: 'Project Conventions',
449
+ review: 'Review Guidance',
450
+ };
451
+ if (!sections[kind]) {
452
+ throw new Error(`Invalid memory kind: ${kind}. Valid kinds: ${Object.keys(sections).join(', ')}`);
453
+ }
454
+ return sections[kind];
455
+ }
456
+
457
+ function rememberCommand(ctx, opts) {
458
+ const home = agentsHome(ctx, opts);
459
+ ensurePrivateState(home, ctx);
460
+ const text = required(opts, 'text');
461
+ const evidence = required(opts, 'evidence');
462
+ const today = todayParts(ctx);
463
+ const added = appendMemoryCandidate(home, today, text, evidence, 'remember');
464
+ out(ctx, added ? `Remembered candidate: ${text}` : `Memory candidate already exists: ${text}`);
465
+ return 0;
466
+ }
467
+
468
+ function recallCommand(ctx, query, opts) {
469
+ const home = agentsHome(ctx, opts);
470
+ ensurePrivateState(home, ctx);
471
+ const search = opts.query || query;
472
+ if (!search || search === true) {
473
+ throw new Error('Missing recall query. Use: awareness recall QUERY');
474
+ }
475
+ const limit = Number.parseInt(opts.limit || '10', 10);
476
+ if (!Number.isInteger(limit) || limit < 1) {
477
+ throw new Error('Invalid --limit. Use an integer >= 1.');
478
+ }
479
+
480
+ const results = recallMatches(home, search, limit);
481
+ out(ctx, `Recall Results (${results.length})`);
482
+ if (!results.length) {
483
+ out(ctx, '- No matches.');
484
+ return 0;
485
+ }
486
+
487
+ for (const result of results) {
488
+ out(ctx, `- ${displayPath(home, result.file)}:${result.line}: ${result.text}`);
489
+ }
490
+ return 0;
491
+ }
492
+
493
+ function forgetCommand(ctx, opts) {
494
+ const home = agentsHome(ctx, opts);
495
+ ensurePrivateState(home, ctx);
496
+ const text = required(opts, 'text');
497
+ const reason = required(opts, 'reason');
498
+ const evidence = required(opts, 'evidence');
499
+ const today = todayParts(ctx);
500
+ const file = longTermMemoryPath(home);
501
+ let content = fs.readFileSync(file, 'utf8');
502
+ content = replaceMetadata(content, 'Updated', formatTimestamp(today));
503
+ content = appendToSection(content, 'Pruned Or Revised', `- ${today.date}: ${text} (reason: ${reason}; evidence: ${evidence})\n`);
504
+ fs.writeFileSync(file, content);
505
+ appendMemoryEvent(home, today, {
506
+ type: 'memory.pruned',
507
+ text,
508
+ reason,
509
+ evidence,
510
+ });
511
+ out(ctx, `Memory pruned or revised: ${text}`);
512
+ return 0;
513
+ }
514
+
515
+ function improveCommand(ctx, opts) {
516
+ const home = agentsHome(ctx, opts);
517
+ ensurePrivateState(home, ctx);
518
+ const today = todayParts(ctx);
519
+ const evaluationPath = path.join(home, 'evaluations', `${today.date}.md`);
520
+ const force = Boolean(opts.force);
521
+ const minCount = Number.parseInt(opts.minCount || '2', 10);
522
+
523
+ if (!Number.isInteger(minCount) || minCount < 2) {
524
+ throw new Error('Invalid --min-count. Use an integer >= 2.');
525
+ }
526
+
527
+ let evaluation;
528
+ if (force && fs.existsSync(evaluationPath)) {
529
+ fs.writeFileSync(evaluationPath, buildEvaluation(home, today));
530
+ const candidates = recordEvaluationMemoryCandidates(home, today);
531
+ evaluation = { file: evaluationPath, status: 'rewritten', candidates };
532
+ } else {
533
+ evaluation = writeEvaluationIfMissing(home, today);
534
+ }
535
+
536
+ if (evaluation.status === 'written' || evaluation.status === 'rewritten') {
537
+ appendMemoryEvent(home, today, {
538
+ type: 'evaluation.created',
539
+ file: evaluation.file,
540
+ status: evaluation.status,
541
+ });
542
+ }
543
+
544
+ const content = fs.readFileSync(longTermMemoryPath(home), 'utf8');
545
+ const suggestions = repeatedMemoryCandidateSuggestions(content, minCount);
546
+ for (const suggestion of suggestions) {
547
+ appendMemoryEvent(home, today, {
548
+ type: 'pattern.suggested',
549
+ text: suggestion.text,
550
+ count: suggestion.count,
551
+ evidence: suggestion.evidence,
552
+ });
553
+ }
554
+
555
+ out(ctx, `Evaluation: ${evaluation.status} (${evaluation.file})`);
556
+ out(ctx, `Memory candidates: ${evaluation.candidates ? evaluation.candidates.length : 'not changed'}`);
557
+ out(ctx, `Pattern suggestions: ${suggestions.length}`);
558
+ for (const suggestion of suggestions) {
559
+ out(ctx, `- ${suggestion.text} (${suggestion.count} observations)`);
560
+ out(ctx, ` Promote: awareness memory promote --kind pattern --text "${shellQuoteText(suggestion.text)}" --evidence "${shellQuoteText(suggestion.evidence)}"`);
561
+ }
339
562
  return 0;
340
563
  }
341
564
 
@@ -434,6 +657,11 @@ function hookCommand(ctx, subcommand, opts) {
434
657
  }
435
658
  }
436
659
 
660
+ // Events whose stdout the host agent injects into its context. For these we
661
+ // always emit the Current Focus so the agent actually loads the protocol state,
662
+ // even under --quiet (which only suppresses diagnostic noise, not the payload).
663
+ const CONTEXT_INJECTION_EVENTS = new Set(['session-start', 'post-compact']);
664
+
437
665
  function hookRunCommand(ctx, opts) {
438
666
  const home = agentsHome(ctx, opts);
439
667
  const today = todayParts(ctx);
@@ -455,9 +683,27 @@ function hookRunCommand(ctx, opts) {
455
683
  out(ctx, warnings.length ? `Warnings: ${warnings.length}` : 'Warnings: none');
456
684
  }
457
685
 
686
+ if (CONTEXT_INJECTION_EVENTS.has(event)) {
687
+ emitFocusContext(ctx, home);
688
+ }
689
+
458
690
  return 0;
459
691
  }
460
692
 
693
+ // Print the Current Focus as injectable context for the host agent. Framed as
694
+ // an instruction so the agent treats it as actionable, not background noise.
695
+ function emitFocusContext(ctx, home) {
696
+ const currentPath = awarenessPath(home);
697
+ if (!fs.existsSync(currentPath)) return;
698
+ const focus = extractSection(fs.readFileSync(currentPath, 'utf8'), 'Current Focus').trim();
699
+ if (!focus) return;
700
+ out(ctx, '[awareness] Load this before doing work — current focus:');
701
+ out(ctx, '');
702
+ out(ctx, focus);
703
+ out(ctx, '');
704
+ out(ctx, 'Follow the awareness protocol; run `awareness handoff` before yielding control.');
705
+ }
706
+
461
707
  function hookInstallCommand(ctx, opts) {
462
708
  const tool = opts.tool || 'all';
463
709
  const userHome = userHomePath(ctx, opts);
@@ -521,6 +767,7 @@ function scheduleRunCommand(ctx, opts) {
521
767
  out(ctx, `Schedule run complete: ${cadence}`);
522
768
  out(ctx, `Runtime log: ${eventFile}`);
523
769
  if (evaluation) out(ctx, `Evaluation: ${evaluation.status} (${evaluation.file})`);
770
+ if (evaluation?.candidates) out(ctx, `Memory candidates: ${evaluation.candidates.length ? `${evaluation.candidates.length} recorded` : 'none'}`);
524
771
  out(ctx, warnings.length ? `Warnings: ${warnings.length}` : 'Warnings: none');
525
772
  return 0;
526
773
  }
@@ -550,6 +797,7 @@ function scheduleInstallCommand(ctx, opts) {
550
797
  label,
551
798
  args,
552
799
  interval,
800
+ environmentPath: launchAgentPath(command),
553
801
  stdoutPath: path.join(launchdLogDir, `${target}.out.log`),
554
802
  stderrPath: path.join(launchdLogDir, `${target}.err.log`),
555
803
  }));
@@ -592,7 +840,8 @@ function writeEvaluationIfMissing(home, today) {
592
840
 
593
841
  ensureDir(path.dirname(evaluationPath));
594
842
  fs.writeFileSync(evaluationPath, buildEvaluation(home, today));
595
- return { file: evaluationPath, status: 'written' };
843
+ const candidates = recordEvaluationMemoryCandidates(home, today);
844
+ return { file: evaluationPath, status: 'written', candidates };
596
845
  }
597
846
 
598
847
  function installCodexHooks(userHome, command, home) {
@@ -708,7 +957,16 @@ export const AwarenessFramework = async () => ({
708
957
  `;
709
958
  }
710
959
 
711
- function launchAgentPlist({ label, args, interval, stdoutPath, stderrPath }) {
960
+ function launchAgentPath(command) {
961
+ const defaultPath = ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
962
+ if (path.isAbsolute(command)) {
963
+ const commandDir = path.dirname(command);
964
+ return [commandDir, ...defaultPath.filter((dir) => dir !== commandDir)].join(':');
965
+ }
966
+ return defaultPath.join(':');
967
+ }
968
+
969
+ function launchAgentPlist({ label, args, interval, environmentPath, stdoutPath, stderrPath }) {
712
970
  const argItems = args.map((arg) => ` <string>${escapeXml(arg)}</string>`).join('\n');
713
971
  return `<?xml version="1.0" encoding="UTF-8"?>
714
972
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -716,6 +974,11 @@ function launchAgentPlist({ label, args, interval, stdoutPath, stderrPath }) {
716
974
  <dict>
717
975
  <key>Label</key>
718
976
  <string>${escapeXml(label)}</string>
977
+ <key>EnvironmentVariables</key>
978
+ <dict>
979
+ <key>PATH</key>
980
+ <string>${escapeXml(environmentPath)}</string>
981
+ </dict>
719
982
  <key>ProgramArguments</key>
720
983
  <array>
721
984
  ${argItems}
@@ -822,7 +1085,7 @@ function buildEvaluation(home, today) {
822
1085
  const traceability = !entries.length ? 0 : assignedEntries / entries.length >= 0.8 ? 2 : 1;
823
1086
  const handoff = /- Next:\s+(?!The next concrete action)\S+/.test(extractSection(current, 'Current Focus')) ? 2 : current ? 1 : 0;
824
1087
  const noise = current.split('\n').length <= 180 && !/YYYY-MM-DD|branch-name/.test(current) ? 2 : 1;
825
- const reporting = extractSection(current, 'End-of-Day Candidates').trim() ? 2 : entries.length ? 1 : 0;
1088
+ const reporting = sectionHasMeaningfulContent(extractSection(current, 'End-of-Day Candidates')) ? 2 : entries.length ? 1 : 0;
826
1089
 
827
1090
  return `# Awareness Evaluation - ${today.date}
828
1091
 
@@ -847,6 +1110,135 @@ ${warnings.length ? warnings.map((warning) => `- ${warning}`).join('\n') : '- No
847
1110
  `;
848
1111
  }
849
1112
 
1113
+ function recordEvaluationMemoryCandidates(home, today) {
1114
+ const candidates = buildEvaluationMemoryCandidates(home, today);
1115
+ return candidates.filter((candidate) => appendMemoryCandidate(home, today, candidate.text, candidate.evidence, 'evaluation'));
1116
+ }
1117
+
1118
+ function buildEvaluationMemoryCandidates(home, today) {
1119
+ const currentPath = awarenessPath(home);
1120
+ const worklogPath = path.join(home, 'worklog', `${today.date}.md`);
1121
+ const current = fs.existsSync(currentPath) ? fs.readFileSync(currentPath, 'utf8') : '';
1122
+ const worklog = fs.existsSync(worklogPath) ? fs.readFileSync(worklogPath, 'utf8') : '';
1123
+ const warnings = collectWarnings(home, today);
1124
+ const entries = parseWorklogEntries(worklog);
1125
+ const assignedEntries = entries.filter((entry) => entry.task && entry.task !== 'Unassigned').length;
1126
+ const candidates = warnings.map((warning) => ({
1127
+ text: `Review recurring awareness warning: ${warning}`,
1128
+ evidence: `daily evaluation ${today.date}`,
1129
+ }));
1130
+
1131
+ if (entries.length && assignedEntries / entries.length < 0.8) {
1132
+ candidates.push({
1133
+ text: `Improve task traceability: ${assignedEntries}/${entries.length} worklog entries had explicit task IDs.`,
1134
+ evidence: `worklog/${today.date}.md`,
1135
+ });
1136
+ }
1137
+
1138
+ if (current && !/- Next:\s+(?!The next concrete action)\S+/.test(extractSection(current, 'Current Focus'))) {
1139
+ candidates.push({
1140
+ text: 'Tighten handoff habit: Current Focus should keep a concrete Next action before yielding control.',
1141
+ evidence: awarenessPath(home),
1142
+ });
1143
+ }
1144
+
1145
+ if (current && (current.split('\n').length > 180 || /YYYY-MM-DD|branch-name/.test(current))) {
1146
+ candidates.push({
1147
+ text: 'Review awareness noise: current board is too long or still contains template placeholders.',
1148
+ evidence: awarenessPath(home),
1149
+ });
1150
+ }
1151
+
1152
+ if (!sectionHasMeaningfulContent(extractSection(current, 'End-of-Day Candidates')) && entries.length) {
1153
+ candidates.push({
1154
+ text: 'Improve reporting readiness: capture end-of-day candidates while work is fresh.',
1155
+ evidence: awarenessPath(home),
1156
+ });
1157
+ }
1158
+
1159
+ return candidates;
1160
+ }
1161
+
1162
+ function appendMemoryCandidate(home, today, text, evidence, source = 'memory.note') {
1163
+ const file = longTermMemoryPath(home);
1164
+ let content = fs.readFileSync(file, 'utf8');
1165
+ if (memoryCandidateExists(content, text, evidence)) return false;
1166
+
1167
+ content = replaceMetadata(content, 'Updated', formatTimestamp(today));
1168
+ content = appendToSection(content, 'Promotion Candidates', `- ${today.date}: ${text} (evidence: ${evidence})\n`);
1169
+ fs.writeFileSync(file, content);
1170
+ appendMemoryEvent(home, today, {
1171
+ type: 'memory.candidate.created',
1172
+ source,
1173
+ text,
1174
+ evidence,
1175
+ });
1176
+ return true;
1177
+ }
1178
+
1179
+ function memoryCandidateExists(content, text, evidence) {
1180
+ const candidates = extractSection(content, 'Promotion Candidates');
1181
+ return candidates.split('\n').some((line) => line.includes(`: ${text} (evidence: ${evidence})`));
1182
+ }
1183
+
1184
+ function repeatedMemoryCandidateSuggestions(content, minCount) {
1185
+ const grouped = new Map();
1186
+ const prunedTexts = prunedMemoryCandidateTexts(content);
1187
+ for (const candidate of parseMemoryCandidates(content)) {
1188
+ const key = normalizeMemoryCandidateText(candidate.text);
1189
+ if (prunedTexts.has(key)) continue;
1190
+ const group = grouped.get(key) || { text: candidate.text, count: 0, evidence: [] };
1191
+ group.count += 1;
1192
+ group.evidence.push(candidate.evidence);
1193
+ grouped.set(key, group);
1194
+ }
1195
+
1196
+ return [...grouped.values()]
1197
+ .filter((group) => group.count >= minCount)
1198
+ .map((group) => ({
1199
+ text: group.text,
1200
+ count: group.count,
1201
+ evidence: [...new Set(group.evidence)].join('; '),
1202
+ }))
1203
+ .sort((left, right) => right.count - left.count || left.text.localeCompare(right.text));
1204
+ }
1205
+
1206
+ function parseMemoryCandidates(content) {
1207
+ return extractSection(content, 'Promotion Candidates')
1208
+ .split('\n')
1209
+ .map((line) => line.trim())
1210
+ .map((line) => line.match(/^- \d{4}-\d{2}-\d{2}: (.+) \(evidence: (.+)\)$/))
1211
+ .filter(Boolean)
1212
+ .map((match) => ({
1213
+ text: match[1],
1214
+ evidence: match[2],
1215
+ }));
1216
+ }
1217
+
1218
+ function prunedMemoryCandidateTexts(content) {
1219
+ return new Set(extractSection(content, 'Pruned Or Revised')
1220
+ .split('\n')
1221
+ .map((line) => line.trim())
1222
+ .map((line) => line.match(/^- \d{4}-\d{2}-\d{2}: (.+) \(reason: .+; evidence: .+\)$/))
1223
+ .filter(Boolean)
1224
+ .map((match) => normalizeMemoryCandidateText(match[1])));
1225
+ }
1226
+
1227
+ function normalizeMemoryCandidateText(text) {
1228
+ return text.toLowerCase().replace(/\s+/g, ' ').trim();
1229
+ }
1230
+
1231
+ function shellQuoteText(text) {
1232
+ return text.replace(/["\\$`]/g, '\\$&');
1233
+ }
1234
+
1235
+ function sectionHasMeaningfulContent(section) {
1236
+ return section
1237
+ .split('\n')
1238
+ .map((line) => line.trim())
1239
+ .some((line) => line && line !== '- None.' && line !== '- None yet.');
1240
+ }
1241
+
850
1242
  function appendWorklog(home, today, entry) {
851
1243
  const worklogPath = path.join(home, 'worklog', `${today.date}.md`);
852
1244
  ensureDir(path.dirname(worklogPath));
@@ -932,7 +1324,7 @@ function ensurePrivateState(home, ctx) {
932
1324
  if (!fs.existsSync(awarenessPath(home))) fs.writeFileSync(awarenessPath(home), initialAwareness(today));
933
1325
  if (!fs.existsSync(path.join(home, 'worklog', `${today.date}.md`))) fs.writeFileSync(path.join(home, 'worklog', `${today.date}.md`), dailyWorklog(today.date));
934
1326
  if (!fs.existsSync(personalityPath(home))) fs.writeFileSync(personalityPath(home), readTemplate('personality.md'));
935
- if (!fs.existsSync(path.join(home, 'memory', 'long-term.md'))) fs.writeFileSync(path.join(home, 'memory', 'long-term.md'), readTemplate('memory-long-term.md'));
1327
+ if (!fs.existsSync(longTermMemoryPath(home))) fs.writeFileSync(longTermMemoryPath(home), readTemplate('memory-long-term.md'));
936
1328
  }
937
1329
 
938
1330
  function replaceSection(content, section, body) {
@@ -1254,6 +1646,86 @@ function personalityPath(home) {
1254
1646
  return path.join(home, 'memory', 'personality.md');
1255
1647
  }
1256
1648
 
1649
+ function longTermMemoryPath(home) {
1650
+ return path.join(home, 'memory', 'long-term.md');
1651
+ }
1652
+
1653
+ function memoryEventPath(home) {
1654
+ return path.join(home, 'memory', 'events.jsonl');
1655
+ }
1656
+
1657
+ function appendMemoryEvent(home, today, event) {
1658
+ const file = memoryEventPath(home);
1659
+ ensureDir(path.dirname(file));
1660
+ fs.appendFileSync(file, `${JSON.stringify({
1661
+ timestamp: formatTimestamp(today),
1662
+ ...event,
1663
+ })}\n`);
1664
+ return file;
1665
+ }
1666
+
1667
+ function readMemoryEvents(home) {
1668
+ const file = memoryEventPath(home);
1669
+ if (!fs.existsSync(file)) return [];
1670
+ return fs.readFileSync(file, 'utf8')
1671
+ .split('\n')
1672
+ .map((line) => line.trim())
1673
+ .filter(Boolean)
1674
+ .map((line) => JSON.parse(line));
1675
+ }
1676
+
1677
+ function collectRecallSources(home) {
1678
+ return [
1679
+ ...markdownFilesRecursive(path.join(home, 'memory')),
1680
+ memoryEventPath(home),
1681
+ ...markdownFiles(path.join(home, 'worklog')),
1682
+ ...markdownFiles(path.join(home, 'evaluations')),
1683
+ ].filter((file, index, files) => fs.existsSync(file) && files.indexOf(file) === index);
1684
+ }
1685
+
1686
+ function markdownFiles(dir) {
1687
+ if (!fs.existsSync(dir)) return [];
1688
+ return fs.readdirSync(dir)
1689
+ .filter((name) => name.endsWith('.md'))
1690
+ .sort()
1691
+ .map((name) => path.join(dir, name));
1692
+ }
1693
+
1694
+ function markdownFilesRecursive(dir) {
1695
+ if (!fs.existsSync(dir)) return [];
1696
+ return fs.readdirSync(dir, { withFileTypes: true })
1697
+ .sort((left, right) => left.name.localeCompare(right.name))
1698
+ .flatMap((entry) => {
1699
+ const file = path.join(dir, entry.name);
1700
+ if (entry.isDirectory()) return markdownFilesRecursive(file);
1701
+ if (entry.isFile() && entry.name.endsWith('.md')) return [file];
1702
+ return [];
1703
+ });
1704
+ }
1705
+
1706
+ function recallMatches(home, query, limit) {
1707
+ const terms = [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))];
1708
+ const results = [];
1709
+ for (const file of collectRecallSources(home)) {
1710
+ const content = fs.readFileSync(file, 'utf8');
1711
+ const lines = content.split('\n');
1712
+ lines.forEach((line, index) => {
1713
+ const haystack = line.toLowerCase();
1714
+ const score = terms.filter((term) => haystack.includes(term)).length;
1715
+ if (score > 0) {
1716
+ results.push({
1717
+ file,
1718
+ line: index + 1,
1719
+ score,
1720
+ text: line.trim(),
1721
+ });
1722
+ }
1723
+ });
1724
+ }
1725
+ results.sort((left, right) => right.score - left.score || left.file.localeCompare(right.file) || left.line - right.line);
1726
+ return results.slice(0, limit);
1727
+ }
1728
+
1257
1729
  function userMemoryPath(home, userSlug) {
1258
1730
  return path.join(home, 'memory', 'users', `${userSlug}.md`);
1259
1731
  }
@@ -6,7 +6,7 @@ You operate in a multi-task, multi-agent environment. Before doing work, load th
6
6
 
7
7
  - Awareness board: `~/.agents/awareness/current.md`
8
8
  - Daily worklog: `~/.agents/worklog/YYYY-MM-DD.md`
9
- - Optional durable memory: `~/.agents/memory/`
9
+ - Durable memory and review candidates: `~/.agents/memory/`
10
10
  - Optional narrow user memory: `~/.agents/memory/users/<user>.md` or scoped channel equivalent
11
11
  - Optional evaluation notes: `~/.agents/evaluations/YYYY-MM-DD.md`
12
12
  - Runtime hook and scheduler events: `~/.agents/runtime/`
@@ -20,18 +20,27 @@ You operate in a multi-task, multi-agent environment. Before doing work, load th
20
20
  5. When concrete progress happens, append to the daily worklog.
21
21
  6. When state changes, update the awareness board.
22
22
  7. Before handoff, run `awareness handoff` if available; otherwise make the awareness board reflect the exact current state and append a final worklog entry.
23
- 8. At end of day, prepare a task-grouped summary for human review.
24
- 9. Treat hook and scheduler runtime events as diagnostics only; they do not replace task worklog entries.
25
- 10. For multi-user channels, keep context scoped by channel and store only narrow user facts in `memory/users/<user>.md`.
23
+ 8. When evaluation or handoff exposes repeated friction, review memory candidates with `awareness memory candidates` or `awareness memory review`.
24
+ 9. At end of day, prepare a task-grouped summary for human review, including memory candidates and pattern suggestions.
25
+ 10. Treat hook and scheduler runtime events as diagnostics only; they do not replace task worklog entries.
26
+ 11. For multi-user channels, keep context scoped by channel and store only narrow user facts in `memory/users/<user>.md`.
26
27
 
27
28
  ## Rules
28
29
 
29
30
  - Keep the worklog append-only.
30
31
  - Do not invent task IDs.
31
32
  - Record evidence: paths, commands, test results, commits, PRs, deployments, blockers.
32
- - Prefer CLI maintenance commands (`awareness focus`, `awareness log`, `awareness handoff`, `awareness evaluate`) when available.
33
+ - Prefer CLI maintenance commands (`awareness focus`, `awareness log`, `awareness handoff`, `awareness evaluate`, `awareness memory candidates`, `awareness memory review`) when available.
34
+ - Let evaluations and schedules record promotion candidates, but promote durable memory only with explicit evidence through `awareness memory promote`.
35
+ - Promote repeated candidates as `pattern` only after `awareness memory review` or equivalent evidence shows repetition.
36
+ - Promote direct user preferences promptly when they affect future collaboration.
33
37
  - Use `awareness user note` only for short, evidence-backed participant facts such as nicknames, repeated questions, topics, or explicit preferences.
34
- - Use `awareness hook run` and `awareness schedule run` only for low-noise maintenance; do not let them post externally or promote long-term memory silently.
38
+ - Use `awareness hook run` and `awareness schedule run` only for low-noise maintenance; do not let them post externally or silently promote long-term memory.
35
39
  - Keep private state out of version control.
36
40
  - Ask before posting worklogs, comments, status changes, or summaries to external systems.
37
41
  - Propose framework improvements through reviewed changes, not hidden local edits.
42
+
43
+ - Use `awareness remember` for explicit observations that should enter memory review.
44
+ - Use `awareness recall QUERY` before repeating uncertain or previously solved work.
45
+ - Use `awareness forget --text TEXT --reason REASON --evidence EVIDENCE` when memory is stale, wrong, or superseded.
46
+ - Use `awareness improve` after material work or process friction to run evaluation plus memory review.
@@ -6,6 +6,6 @@ Read and follow the canonical private protocol at:
6
6
 
7
7
  If your CLI does not expand `@` imports automatically, open that file explicitly before starting work.
8
8
 
9
- Treat imported awareness files as session-start snapshots. If the Awareness CLI is available, run `awareness status` or `awareness check` at session start, `awareness refresh` when parallel work may have changed state, and `awareness handoff` before returning control.
9
+ Treat imported awareness files as session-start snapshots. If the Awareness CLI is available, run `awareness status` or `awareness check` at session start, `awareness refresh` when parallel work may have changed state, `awareness memory review` when evaluating repeated candidates, and `awareness handoff` before returning control.
10
10
 
11
11
  Keep this wrapper small. The framework should live in versioned methodology docs, and live operational state should stay private.
@@ -25,3 +25,11 @@
25
25
  ## Methodology Observations
26
26
 
27
27
  - Anything that made the awareness or worklog process better or worse
28
+
29
+ ## Memory Review
30
+
31
+ - New promotion candidates:
32
+ - Repeated candidates or suggested patterns:
33
+ - Promotions made:
34
+ - Needs user confirmation:
35
+ - Candidates to prune or leave short-term: