@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/README.md +4 -0
- package/docs/cli.md +33 -1
- package/docs/evaluation-loop.md +2 -0
- package/docs/hooks-and-scheduling.md +8 -2
- package/docs/memory.md +17 -0
- package/docs/superpowers/plans/2026-06-19-local-memory-operations.md +1026 -0
- package/package.json +1 -1
- package/src/cli.js +478 -6
- package/templates/agent-instructions.md +15 -6
- package/templates/cli-wrapper.md +1 -1
- package/templates/end-of-day-summary.md +8 -0
- package/templates/evaluation-note.md +4 -1
- package/templates/memory-long-term.md +15 -1
package/package.json
CHANGED
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(
|
|
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
|
-
|
|
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
|
|
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')
|
|
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(
|
|
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
|
-
-
|
|
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.
|
|
24
|
-
9.
|
|
25
|
-
10.
|
|
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
|
|
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.
|
package/templates/cli-wrapper.md
CHANGED
|
@@ -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:
|