@fyso/awareness-framework 0.2.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.2.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
 
@@ -544,6 +767,7 @@ function scheduleRunCommand(ctx, opts) {
544
767
  out(ctx, `Schedule run complete: ${cadence}`);
545
768
  out(ctx, `Runtime log: ${eventFile}`);
546
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'}`);
547
771
  out(ctx, warnings.length ? `Warnings: ${warnings.length}` : 'Warnings: none');
548
772
  return 0;
549
773
  }
@@ -573,6 +797,7 @@ function scheduleInstallCommand(ctx, opts) {
573
797
  label,
574
798
  args,
575
799
  interval,
800
+ environmentPath: launchAgentPath(command),
576
801
  stdoutPath: path.join(launchdLogDir, `${target}.out.log`),
577
802
  stderrPath: path.join(launchdLogDir, `${target}.err.log`),
578
803
  }));
@@ -615,7 +840,8 @@ function writeEvaluationIfMissing(home, today) {
615
840
 
616
841
  ensureDir(path.dirname(evaluationPath));
617
842
  fs.writeFileSync(evaluationPath, buildEvaluation(home, today));
618
- return { file: evaluationPath, status: 'written' };
843
+ const candidates = recordEvaluationMemoryCandidates(home, today);
844
+ return { file: evaluationPath, status: 'written', candidates };
619
845
  }
620
846
 
621
847
  function installCodexHooks(userHome, command, home) {
@@ -731,7 +957,16 @@ export const AwarenessFramework = async () => ({
731
957
  `;
732
958
  }
733
959
 
734
- 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 }) {
735
970
  const argItems = args.map((arg) => ` <string>${escapeXml(arg)}</string>`).join('\n');
736
971
  return `<?xml version="1.0" encoding="UTF-8"?>
737
972
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -739,6 +974,11 @@ function launchAgentPlist({ label, args, interval, stdoutPath, stderrPath }) {
739
974
  <dict>
740
975
  <key>Label</key>
741
976
  <string>${escapeXml(label)}</string>
977
+ <key>EnvironmentVariables</key>
978
+ <dict>
979
+ <key>PATH</key>
980
+ <string>${escapeXml(environmentPath)}</string>
981
+ </dict>
742
982
  <key>ProgramArguments</key>
743
983
  <array>
744
984
  ${argItems}
@@ -845,7 +1085,7 @@ function buildEvaluation(home, today) {
845
1085
  const traceability = !entries.length ? 0 : assignedEntries / entries.length >= 0.8 ? 2 : 1;
846
1086
  const handoff = /- Next:\s+(?!The next concrete action)\S+/.test(extractSection(current, 'Current Focus')) ? 2 : current ? 1 : 0;
847
1087
  const noise = current.split('\n').length <= 180 && !/YYYY-MM-DD|branch-name/.test(current) ? 2 : 1;
848
- 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;
849
1089
 
850
1090
  return `# Awareness Evaluation - ${today.date}
851
1091
 
@@ -870,6 +1110,135 @@ ${warnings.length ? warnings.map((warning) => `- ${warning}`).join('\n') : '- No
870
1110
  `;
871
1111
  }
872
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
+
873
1242
  function appendWorklog(home, today, entry) {
874
1243
  const worklogPath = path.join(home, 'worklog', `${today.date}.md`);
875
1244
  ensureDir(path.dirname(worklogPath));
@@ -955,7 +1324,7 @@ function ensurePrivateState(home, ctx) {
955
1324
  if (!fs.existsSync(awarenessPath(home))) fs.writeFileSync(awarenessPath(home), initialAwareness(today));
956
1325
  if (!fs.existsSync(path.join(home, 'worklog', `${today.date}.md`))) fs.writeFileSync(path.join(home, 'worklog', `${today.date}.md`), dailyWorklog(today.date));
957
1326
  if (!fs.existsSync(personalityPath(home))) fs.writeFileSync(personalityPath(home), readTemplate('personality.md'));
958
- 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'));
959
1328
  }
960
1329
 
961
1330
  function replaceSection(content, section, body) {
@@ -1277,6 +1646,86 @@ function personalityPath(home) {
1277
1646
  return path.join(home, 'memory', 'personality.md');
1278
1647
  }
1279
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
+
1280
1729
  function userMemoryPath(home, userSlug) {
1281
1730
  return path.join(home, 'memory', 'users', `${userSlug}.md`);
1282
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:
@@ -22,6 +22,9 @@
22
22
 
23
23
  - Short-term observations to keep short-term:
24
24
  - Long-term memory candidates:
25
+ - Repeated candidates from `awareness memory review`:
26
+ - Suggested pattern promotions:
27
+ - Promotion command and evidence:
25
28
  - Memory to prune or revise:
26
29
  - User confirmation needed:
27
30
 
@@ -29,7 +32,7 @@
29
32
 
30
33
  - Awareness cleanup:
31
34
  - Worklog correction:
32
- - Long-term memory promotion:
35
+ - Long-term memory promotion, candidate, or prune decision:
33
36
  - Personality update:
34
37
  - Framework PR candidate:
35
38
 
@@ -3,7 +3,7 @@
3
3
  - Updated: never
4
4
  - Scope: Local private state; do not commit
5
5
 
6
- This file stores durable, curated memory that improves future collaboration. Add entries only when they are user-confirmed, repeated, or operationally important.
6
+ This file stores durable, curated memory that improves future collaboration. Evaluations may add promotion candidates automatically, but durable entries should be promoted only when they are user-confirmed, repeated, or operationally important.
7
7
 
8
8
  ## Preferences
9
9
 
@@ -25,6 +25,19 @@ This file stores durable, curated memory that improves future collaboration. Add
25
25
 
26
26
  - None yet.
27
27
 
28
+ ## Review Notes
29
+
30
+ - Use `awareness memory candidates` to inspect raw candidates.
31
+ - Use `awareness memory review` to surface repeated candidates that may deserve promotion as `Patterns`.
32
+ - Use `awareness memory promote --kind preference|pattern|project|review --text TEXT --evidence EVIDENCE` after review.
33
+ - Repeated candidates may share the same text with distinct evidence; do not collapse them before review.
34
+
35
+ ## Event Log
36
+
37
+ - Append-only audit history: `memory/events.jsonl`
38
+ - Markdown sections are readable projections.
39
+ - Do not hand-edit event history.
40
+
28
41
  ## Pruned Or Revised
29
42
 
30
43
  - None yet.
@@ -35,3 +48,4 @@ This file stores durable, curated memory that improves future collaboration. Add
35
48
  - Do not promote one-off guesses without repeated evidence.
36
49
  - Direct user instructions override memory.
37
50
  - Remove or soften stale memory.
51
+ - Keep promotion evidence concise and linkable.