@fyso/awareness-framework 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/docs/cli.md +40 -3
- package/docs/evaluation-loop.md +2 -0
- package/docs/lifecycle.md +4 -0
- package/docs/memory.md +20 -0
- package/docs/superpowers/plans/2026-06-19-local-memory-operations.md +1026 -0
- package/package.json +1 -1
- package/src/cli.js +459 -18
- package/src/memory-candidates.js +82 -0
- package/src/text.js +35 -0
- 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
|
@@ -3,9 +3,21 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import {
|
|
7
|
+
activeMemoryCandidates,
|
|
8
|
+
isPrunedMemoryText,
|
|
9
|
+
memoryCandidateExists,
|
|
10
|
+
memoryCandidateTextExists,
|
|
11
|
+
repeatedMemoryCandidateSuggestions,
|
|
12
|
+
} from './memory-candidates.js';
|
|
13
|
+
import { normalizeSearchText, recallTermGroups } from './text.js';
|
|
6
14
|
|
|
7
15
|
const VALID_STATES = new Set(['started', 'in-progress', 'paused', 'blocked', 'waiting', 'done', 'in-review', 'ready']);
|
|
8
16
|
const DEFAULT_STATE = 'in-progress';
|
|
17
|
+
const STATE_ALIASES = {
|
|
18
|
+
in_progress: 'in-progress',
|
|
19
|
+
in_review: 'in-review',
|
|
20
|
+
};
|
|
9
21
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
22
|
const repoRoot = path.resolve(__dirname, '..');
|
|
11
23
|
|
|
@@ -18,7 +30,7 @@ export function runCli(argv, options = {}) {
|
|
|
18
30
|
|
|
19
31
|
try {
|
|
20
32
|
const parsed = parseArgs(argv);
|
|
21
|
-
const [command, subcommand] = parsed.positionals;
|
|
33
|
+
const [command, subcommand, ...positionRest] = parsed.positionals;
|
|
22
34
|
|
|
23
35
|
if (!command || command === 'help' || parsed.opts.help) {
|
|
24
36
|
printHelp(ctx);
|
|
@@ -42,6 +54,16 @@ export function runCli(argv, options = {}) {
|
|
|
42
54
|
return handoffCommand(ctx, parsed.opts);
|
|
43
55
|
case 'evaluate':
|
|
44
56
|
return evaluateCommand(ctx, parsed.opts);
|
|
57
|
+
case 'memory':
|
|
58
|
+
return memoryCommand(ctx, subcommand, parsed.opts);
|
|
59
|
+
case 'remember':
|
|
60
|
+
return rememberCommand(ctx, parsed.opts);
|
|
61
|
+
case 'recall':
|
|
62
|
+
return recallCommand(ctx, [subcommand, ...positionRest].filter(Boolean).join(' '), parsed.opts);
|
|
63
|
+
case 'forget':
|
|
64
|
+
return forgetCommand(ctx, parsed.opts);
|
|
65
|
+
case 'improve':
|
|
66
|
+
return improveCommand(ctx, parsed.opts);
|
|
45
67
|
case 'hook':
|
|
46
68
|
return hookCommand(ctx, subcommand, parsed.opts);
|
|
47
69
|
case 'schedule':
|
|
@@ -108,6 +130,14 @@ Usage:
|
|
|
108
130
|
awareness log --task ID --summary TEXT --changes TEXT [--context TEXT] [--state STATE] [--evidence TEXT] [--next TEXT] [--home PATH]
|
|
109
131
|
awareness handoff [--home PATH]
|
|
110
132
|
awareness evaluate [--home PATH] [--force] [--print]
|
|
133
|
+
awareness memory candidates [--home PATH]
|
|
134
|
+
awareness memory review [--min-count N] [--home PATH]
|
|
135
|
+
awareness memory note --text TEXT [--evidence TEXT] [--home PATH]
|
|
136
|
+
awareness memory promote --kind preference|pattern|project|review --text TEXT --evidence TEXT [--home PATH]
|
|
137
|
+
awareness remember --text TEXT --evidence TEXT [--home PATH]
|
|
138
|
+
awareness recall QUERY [--limit N] [--home PATH]
|
|
139
|
+
awareness forget --text TEXT --reason TEXT --evidence TEXT [--home PATH]
|
|
140
|
+
awareness improve [--force] [--min-count N] [--home PATH]
|
|
111
141
|
awareness hook run --event EVENT [--tool TOOL] [--quiet] [--home PATH]
|
|
112
142
|
awareness hook install --tool codex|claude|opencode|all [--command CMD] [--home PATH] [--user-home PATH] [--config-home PATH] [--overwrite]
|
|
113
143
|
awareness schedule run --cadence hourly|daily [--home PATH]
|
|
@@ -124,6 +154,9 @@ Scope options:
|
|
|
124
154
|
--channel NAME Store state under <folder>/channels/<safe-name>.
|
|
125
155
|
--user ID Select a user memory file for user commands.
|
|
126
156
|
|
|
157
|
+
State values:
|
|
158
|
+
${[...VALID_STATES].join(', ')}
|
|
159
|
+
|
|
127
160
|
The CLI maintains private files under ~/.agents by default. It does not post to Jira, GitHub, or any external system.`);
|
|
128
161
|
}
|
|
129
162
|
|
|
@@ -146,7 +179,7 @@ function initCommand(ctx, opts) {
|
|
|
146
179
|
writeIfMissing(path.join(home, 'memory', 'personality.md'), readTemplate('personality.md'), created, existing);
|
|
147
180
|
writeIfMissing(path.join(home, 'memory', 'preferences.md'), privateMemorySeed('Preferences'), created, existing);
|
|
148
181
|
writeIfMissing(path.join(home, 'memory', 'patterns.md'), privateMemorySeed('Patterns'), created, existing);
|
|
149
|
-
writeIfMissing(
|
|
182
|
+
writeIfMissing(longTermMemoryPath(home), readTemplate('memory-long-term.md'), created, existing);
|
|
150
183
|
|
|
151
184
|
if (opts.wrappers) {
|
|
152
185
|
writeWrappers({
|
|
@@ -191,7 +224,7 @@ function statusCommand(ctx, opts) {
|
|
|
191
224
|
for (const warning of warnings) {
|
|
192
225
|
out(ctx, `- ${warning}`);
|
|
193
226
|
}
|
|
194
|
-
return
|
|
227
|
+
return 0;
|
|
195
228
|
}
|
|
196
229
|
|
|
197
230
|
function checkCommand(ctx, opts) {
|
|
@@ -312,7 +345,7 @@ function handoffCommand(ctx, opts) {
|
|
|
312
345
|
}
|
|
313
346
|
}
|
|
314
347
|
|
|
315
|
-
return
|
|
348
|
+
return 0;
|
|
316
349
|
}
|
|
317
350
|
|
|
318
351
|
function evaluateCommand(ctx, opts) {
|
|
@@ -335,7 +368,220 @@ function evaluateCommand(ctx, opts) {
|
|
|
335
368
|
|
|
336
369
|
ensureDir(path.dirname(evaluationPath));
|
|
337
370
|
fs.writeFileSync(evaluationPath, content);
|
|
371
|
+
const candidates = recordEvaluationMemoryCandidates(home, today);
|
|
372
|
+
const candidateSummary = candidates.length ? `${candidates.length} recorded` : 'none';
|
|
338
373
|
out(ctx, `Evaluation written: ${evaluationPath}`);
|
|
374
|
+
out(ctx, `Memory candidates: ${candidateSummary}`);
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function memoryCommand(ctx, subcommand, opts) {
|
|
379
|
+
const home = agentsHome(ctx, opts);
|
|
380
|
+
ensurePrivateState(home, ctx);
|
|
381
|
+
|
|
382
|
+
switch (subcommand) {
|
|
383
|
+
case 'candidates':
|
|
384
|
+
case undefined:
|
|
385
|
+
return memoryCandidatesCommand(ctx, home);
|
|
386
|
+
case 'review':
|
|
387
|
+
return memoryReviewCommand(ctx, home, opts);
|
|
388
|
+
case 'note':
|
|
389
|
+
return memoryNoteCommand(ctx, home, opts);
|
|
390
|
+
case 'promote':
|
|
391
|
+
return memoryPromoteCommand(ctx, home, opts);
|
|
392
|
+
default:
|
|
393
|
+
err(ctx, `Unknown memory command: ${subcommand}`);
|
|
394
|
+
err(ctx, 'Use: candidates, review, note, or promote.');
|
|
395
|
+
return 1;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function memoryCandidatesCommand(ctx, home) {
|
|
400
|
+
const content = fs.readFileSync(longTermMemoryPath(home), 'utf8');
|
|
401
|
+
const activeCandidates = activeMemoryCandidates(content)
|
|
402
|
+
.map((candidate) => candidate.line)
|
|
403
|
+
.join('\n');
|
|
404
|
+
out(ctx, 'Promotion Candidates');
|
|
405
|
+
out(ctx, activeCandidates || '- None yet.');
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function memoryReviewCommand(ctx, home, opts) {
|
|
410
|
+
const minCount = Number.parseInt(opts.minCount || '2', 10);
|
|
411
|
+
if (!Number.isInteger(minCount) || minCount < 2) {
|
|
412
|
+
throw new Error('Invalid --min-count. Use an integer >= 2.');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const content = fs.readFileSync(longTermMemoryPath(home), 'utf8');
|
|
416
|
+
const suggestions = repeatedMemoryCandidateSuggestions(content, minCount);
|
|
417
|
+
out(ctx, 'Memory Review');
|
|
418
|
+
|
|
419
|
+
if (!suggestions.length) {
|
|
420
|
+
out(ctx, `- No repeated candidates found with min-count ${minCount}.`);
|
|
421
|
+
return 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
for (const suggestion of suggestions) {
|
|
425
|
+
out(ctx, `- Suggested pattern (${suggestion.count} observations): ${suggestion.text}`);
|
|
426
|
+
out(ctx, ` Evidence: ${suggestion.evidence}`);
|
|
427
|
+
out(ctx, ` Promote: awareness memory promote --kind pattern --text "${shellQuoteText(suggestion.text)}" --evidence "${shellQuoteText(suggestion.evidence)}"`);
|
|
428
|
+
}
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function memoryNoteCommand(ctx, home, opts) {
|
|
433
|
+
const text = required(opts, 'text');
|
|
434
|
+
const evidence = opts.evidence || 'Manual observation';
|
|
435
|
+
const today = todayParts(ctx);
|
|
436
|
+
const added = appendMemoryCandidate(home, today, text, evidence);
|
|
437
|
+
out(ctx, added ? `Memory candidate recorded: ${text}` : `Memory candidate already exists: ${text}`);
|
|
438
|
+
return 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function memoryPromoteCommand(ctx, home, opts) {
|
|
442
|
+
const kind = required(opts, 'kind');
|
|
443
|
+
const text = required(opts, 'text');
|
|
444
|
+
const evidence = required(opts, 'evidence');
|
|
445
|
+
const section = memoryPromotionSection(kind);
|
|
446
|
+
const today = todayParts(ctx);
|
|
447
|
+
const file = longTermMemoryPath(home);
|
|
448
|
+
let content = fs.readFileSync(file, 'utf8');
|
|
449
|
+
if (isPrunedMemoryText(content, text)) {
|
|
450
|
+
throw new Error(`Cannot promote pruned or revised memory: ${text}`);
|
|
451
|
+
}
|
|
452
|
+
content = replaceMetadata(content, 'Updated', formatTimestamp(today));
|
|
453
|
+
content = appendToSection(content, section, `- ${today.date}: ${text} (evidence: ${evidence})\n`);
|
|
454
|
+
fs.writeFileSync(file, content);
|
|
455
|
+
appendMemoryEvent(home, today, {
|
|
456
|
+
type: 'memory.promoted',
|
|
457
|
+
kind,
|
|
458
|
+
section,
|
|
459
|
+
text,
|
|
460
|
+
evidence,
|
|
461
|
+
});
|
|
462
|
+
out(ctx, `Memory promoted to ${section}: ${text}`);
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function memoryPromotionSection(kind) {
|
|
467
|
+
const sections = {
|
|
468
|
+
preference: 'Preferences',
|
|
469
|
+
pattern: 'Patterns',
|
|
470
|
+
project: 'Project Conventions',
|
|
471
|
+
review: 'Review Guidance',
|
|
472
|
+
};
|
|
473
|
+
if (!sections[kind]) {
|
|
474
|
+
throw new Error(`Invalid memory kind: ${kind}. Valid kinds: ${Object.keys(sections).join(', ')}`);
|
|
475
|
+
}
|
|
476
|
+
return sections[kind];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function rememberCommand(ctx, opts) {
|
|
480
|
+
const home = agentsHome(ctx, opts);
|
|
481
|
+
ensurePrivateState(home, ctx);
|
|
482
|
+
const text = required(opts, 'text');
|
|
483
|
+
const evidence = required(opts, 'evidence');
|
|
484
|
+
const today = todayParts(ctx);
|
|
485
|
+
const added = appendMemoryCandidate(home, today, text, evidence, 'remember');
|
|
486
|
+
out(ctx, added ? `Remembered candidate: ${text}` : `Memory candidate already exists: ${text}`);
|
|
487
|
+
return 0;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function recallCommand(ctx, query, opts) {
|
|
491
|
+
const home = agentsHome(ctx, opts);
|
|
492
|
+
ensurePrivateState(home, ctx);
|
|
493
|
+
const search = opts.query || query;
|
|
494
|
+
if (!search || search === true) {
|
|
495
|
+
throw new Error('Missing recall query. Use: awareness recall QUERY');
|
|
496
|
+
}
|
|
497
|
+
const limit = Number.parseInt(opts.limit || '10', 10);
|
|
498
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
499
|
+
throw new Error('Invalid --limit. Use an integer >= 1.');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const results = recallMatches(home, search, limit);
|
|
503
|
+
out(ctx, `Recall Results (${results.length})`);
|
|
504
|
+
if (!results.length) {
|
|
505
|
+
out(ctx, '- No matches.');
|
|
506
|
+
return 0;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
for (const result of results) {
|
|
510
|
+
out(ctx, `- ${displayPath(home, result.file)}:${result.line}: ${result.text}`);
|
|
511
|
+
}
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function forgetCommand(ctx, opts) {
|
|
516
|
+
const home = agentsHome(ctx, opts);
|
|
517
|
+
ensurePrivateState(home, ctx);
|
|
518
|
+
const text = required(opts, 'text');
|
|
519
|
+
const reason = required(opts, 'reason');
|
|
520
|
+
const evidence = required(opts, 'evidence');
|
|
521
|
+
const today = todayParts(ctx);
|
|
522
|
+
const file = longTermMemoryPath(home);
|
|
523
|
+
let content = fs.readFileSync(file, 'utf8');
|
|
524
|
+
content = replaceMetadata(content, 'Updated', formatTimestamp(today));
|
|
525
|
+
content = appendToSection(content, 'Pruned Or Revised', `- ${today.date}: ${text} (reason: ${reason}; evidence: ${evidence})\n`);
|
|
526
|
+
fs.writeFileSync(file, content);
|
|
527
|
+
appendMemoryEvent(home, today, {
|
|
528
|
+
type: 'memory.pruned',
|
|
529
|
+
text,
|
|
530
|
+
reason,
|
|
531
|
+
evidence,
|
|
532
|
+
});
|
|
533
|
+
out(ctx, `Memory pruned or revised: ${text}`);
|
|
534
|
+
return 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function improveCommand(ctx, opts) {
|
|
538
|
+
const home = agentsHome(ctx, opts);
|
|
539
|
+
ensurePrivateState(home, ctx);
|
|
540
|
+
const today = todayParts(ctx);
|
|
541
|
+
const evaluationPath = path.join(home, 'evaluations', `${today.date}.md`);
|
|
542
|
+
const force = Boolean(opts.force);
|
|
543
|
+
const minCount = Number.parseInt(opts.minCount || '2', 10);
|
|
544
|
+
|
|
545
|
+
if (!Number.isInteger(minCount) || minCount < 2) {
|
|
546
|
+
throw new Error('Invalid --min-count. Use an integer >= 2.');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
let evaluation;
|
|
550
|
+
if (force && fs.existsSync(evaluationPath)) {
|
|
551
|
+
fs.writeFileSync(evaluationPath, buildEvaluation(home, today));
|
|
552
|
+
const candidates = recordEvaluationMemoryCandidates(home, today);
|
|
553
|
+
evaluation = { file: evaluationPath, status: 'rewritten', candidates };
|
|
554
|
+
} else {
|
|
555
|
+
evaluation = writeEvaluationIfMissing(home, today);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (evaluation.status === 'written' || evaluation.status === 'rewritten') {
|
|
559
|
+
appendMemoryEvent(home, today, {
|
|
560
|
+
type: 'evaluation.created',
|
|
561
|
+
file: evaluation.file,
|
|
562
|
+
status: evaluation.status,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const content = fs.readFileSync(longTermMemoryPath(home), 'utf8');
|
|
567
|
+
const suggestions = repeatedMemoryCandidateSuggestions(content, minCount);
|
|
568
|
+
for (const suggestion of suggestions) {
|
|
569
|
+
appendMemoryEvent(home, today, {
|
|
570
|
+
type: 'pattern.suggested',
|
|
571
|
+
text: suggestion.text,
|
|
572
|
+
count: suggestion.count,
|
|
573
|
+
evidence: suggestion.evidence,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
out(ctx, `Evaluation: ${evaluation.status} (${evaluation.file})`);
|
|
578
|
+
const candidateSummary = evaluation.candidates ? `${evaluation.candidates.length} (from evaluation diagnostics)` : 'not changed';
|
|
579
|
+
out(ctx, `Auto-generated candidates: ${candidateSummary}`);
|
|
580
|
+
out(ctx, `Pattern suggestions: ${suggestions.length}`);
|
|
581
|
+
for (const suggestion of suggestions) {
|
|
582
|
+
out(ctx, `- ${suggestion.text} (${suggestion.count} observations)`);
|
|
583
|
+
out(ctx, ` Promote: awareness memory promote --kind pattern --text "${shellQuoteText(suggestion.text)}" --evidence "${shellQuoteText(suggestion.evidence)}"`);
|
|
584
|
+
}
|
|
339
585
|
return 0;
|
|
340
586
|
}
|
|
341
587
|
|
|
@@ -544,6 +790,10 @@ function scheduleRunCommand(ctx, opts) {
|
|
|
544
790
|
out(ctx, `Schedule run complete: ${cadence}`);
|
|
545
791
|
out(ctx, `Runtime log: ${eventFile}`);
|
|
546
792
|
if (evaluation) out(ctx, `Evaluation: ${evaluation.status} (${evaluation.file})`);
|
|
793
|
+
if (evaluation?.candidates) {
|
|
794
|
+
const candidateSummary = evaluation.candidates.length ? `${evaluation.candidates.length} recorded` : 'none';
|
|
795
|
+
out(ctx, `Memory candidates: ${candidateSummary}`);
|
|
796
|
+
}
|
|
547
797
|
out(ctx, warnings.length ? `Warnings: ${warnings.length}` : 'Warnings: none');
|
|
548
798
|
return 0;
|
|
549
799
|
}
|
|
@@ -573,6 +823,7 @@ function scheduleInstallCommand(ctx, opts) {
|
|
|
573
823
|
label,
|
|
574
824
|
args,
|
|
575
825
|
interval,
|
|
826
|
+
environmentPath: launchAgentPath(command),
|
|
576
827
|
stdoutPath: path.join(launchdLogDir, `${target}.out.log`),
|
|
577
828
|
stderrPath: path.join(launchdLogDir, `${target}.err.log`),
|
|
578
829
|
}));
|
|
@@ -581,7 +832,8 @@ function scheduleInstallCommand(ctx, opts) {
|
|
|
581
832
|
if (opts.load) {
|
|
582
833
|
const result = loadLaunchAgent(file, label);
|
|
583
834
|
if (result.status !== 0) {
|
|
584
|
-
|
|
835
|
+
const failure = result.stderr || result.stdout || `exit ${result.status}`;
|
|
836
|
+
throw new Error(`launchctl failed for ${file}: ${failure}`);
|
|
585
837
|
}
|
|
586
838
|
loaded.push(file);
|
|
587
839
|
}
|
|
@@ -615,7 +867,8 @@ function writeEvaluationIfMissing(home, today) {
|
|
|
615
867
|
|
|
616
868
|
ensureDir(path.dirname(evaluationPath));
|
|
617
869
|
fs.writeFileSync(evaluationPath, buildEvaluation(home, today));
|
|
618
|
-
|
|
870
|
+
const candidates = recordEvaluationMemoryCandidates(home, today);
|
|
871
|
+
return { file: evaluationPath, status: 'written', candidates };
|
|
619
872
|
}
|
|
620
873
|
|
|
621
874
|
function installCodexHooks(userHome, command, home) {
|
|
@@ -731,7 +984,16 @@ export const AwarenessFramework = async () => ({
|
|
|
731
984
|
`;
|
|
732
985
|
}
|
|
733
986
|
|
|
734
|
-
function
|
|
987
|
+
function launchAgentPath(command) {
|
|
988
|
+
const defaultPath = ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
|
|
989
|
+
if (path.isAbsolute(command)) {
|
|
990
|
+
const commandDir = path.dirname(command);
|
|
991
|
+
return [commandDir, ...defaultPath.filter((dir) => dir !== commandDir)].join(':');
|
|
992
|
+
}
|
|
993
|
+
return defaultPath.join(':');
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function launchAgentPlist({ label, args, interval, environmentPath, stdoutPath, stderrPath }) {
|
|
735
997
|
const argItems = args.map((arg) => ` <string>${escapeXml(arg)}</string>`).join('\n');
|
|
736
998
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
737
999
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -739,6 +1001,11 @@ function launchAgentPlist({ label, args, interval, stdoutPath, stderrPath }) {
|
|
|
739
1001
|
<dict>
|
|
740
1002
|
<key>Label</key>
|
|
741
1003
|
<string>${escapeXml(label)}</string>
|
|
1004
|
+
<key>EnvironmentVariables</key>
|
|
1005
|
+
<dict>
|
|
1006
|
+
<key>PATH</key>
|
|
1007
|
+
<string>${escapeXml(environmentPath)}</string>
|
|
1008
|
+
</dict>
|
|
742
1009
|
<key>ProgramArguments</key>
|
|
743
1010
|
<array>
|
|
744
1011
|
${argItems}
|
|
@@ -845,7 +1112,8 @@ function buildEvaluation(home, today) {
|
|
|
845
1112
|
const traceability = !entries.length ? 0 : assignedEntries / entries.length >= 0.8 ? 2 : 1;
|
|
846
1113
|
const handoff = /- Next:\s+(?!The next concrete action)\S+/.test(extractSection(current, 'Current Focus')) ? 2 : current ? 1 : 0;
|
|
847
1114
|
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')
|
|
1115
|
+
const reporting = sectionHasMeaningfulContent(extractSection(current, 'End-of-Day Candidates')) ? 2 : entries.length ? 1 : 0;
|
|
1116
|
+
const warningsMarkdown = warnings.length ? warnings.map((warning) => `- ${warning}`).join('\n') : '- None.';
|
|
849
1117
|
|
|
850
1118
|
return `# Awareness Evaluation - ${today.date}
|
|
851
1119
|
|
|
@@ -861,7 +1129,7 @@ function buildEvaluation(home, today) {
|
|
|
861
1129
|
|
|
862
1130
|
## Warnings
|
|
863
1131
|
|
|
864
|
-
${
|
|
1132
|
+
${warningsMarkdown}
|
|
865
1133
|
|
|
866
1134
|
## Proposed Changes
|
|
867
1135
|
|
|
@@ -870,6 +1138,87 @@ ${warnings.length ? warnings.map((warning) => `- ${warning}`).join('\n') : '- No
|
|
|
870
1138
|
`;
|
|
871
1139
|
}
|
|
872
1140
|
|
|
1141
|
+
function recordEvaluationMemoryCandidates(home, today) {
|
|
1142
|
+
const candidates = buildEvaluationMemoryCandidates(home, today);
|
|
1143
|
+
return candidates.filter((candidate) => appendMemoryCandidate(home, today, candidate.text, candidate.evidence, 'evaluation', {
|
|
1144
|
+
dedupeByText: true,
|
|
1145
|
+
}));
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function buildEvaluationMemoryCandidates(home, today) {
|
|
1149
|
+
const currentPath = awarenessPath(home);
|
|
1150
|
+
const worklogPath = path.join(home, 'worklog', `${today.date}.md`);
|
|
1151
|
+
const current = fs.existsSync(currentPath) ? fs.readFileSync(currentPath, 'utf8') : '';
|
|
1152
|
+
const worklog = fs.existsSync(worklogPath) ? fs.readFileSync(worklogPath, 'utf8') : '';
|
|
1153
|
+
const warnings = collectWarnings(home, today);
|
|
1154
|
+
const entries = parseWorklogEntries(worklog);
|
|
1155
|
+
const assignedEntries = entries.filter((entry) => entry.task && entry.task !== 'Unassigned').length;
|
|
1156
|
+
const candidates = warnings.map((warning) => ({
|
|
1157
|
+
text: `Review recurring awareness warning: ${warning}`,
|
|
1158
|
+
evidence: `daily evaluation ${today.date}`,
|
|
1159
|
+
}));
|
|
1160
|
+
|
|
1161
|
+
if (entries.length && assignedEntries / entries.length < 0.8) {
|
|
1162
|
+
candidates.push({
|
|
1163
|
+
text: `Improve task traceability: ${assignedEntries}/${entries.length} worklog entries had explicit task IDs.`,
|
|
1164
|
+
evidence: `worklog/${today.date}.md`,
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (current && !/- Next:\s+(?!The next concrete action)\S+/.test(extractSection(current, 'Current Focus'))) {
|
|
1169
|
+
candidates.push({
|
|
1170
|
+
text: 'Tighten handoff habit: Current Focus should keep a concrete Next action before yielding control.',
|
|
1171
|
+
evidence: awarenessPath(home),
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (current && (current.split('\n').length > 180 || /YYYY-MM-DD|branch-name/.test(current))) {
|
|
1176
|
+
candidates.push({
|
|
1177
|
+
text: 'Review awareness noise: current board is too long or still contains template placeholders.',
|
|
1178
|
+
evidence: awarenessPath(home),
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (!sectionHasMeaningfulContent(extractSection(current, 'End-of-Day Candidates')) && entries.length) {
|
|
1183
|
+
candidates.push({
|
|
1184
|
+
text: 'Improve reporting readiness: capture end-of-day candidates while work is fresh.',
|
|
1185
|
+
evidence: awarenessPath(home),
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
return candidates;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function appendMemoryCandidate(home, today, text, evidence, source = 'memory.note', opts = {}) {
|
|
1193
|
+
const file = longTermMemoryPath(home);
|
|
1194
|
+
let content = fs.readFileSync(file, 'utf8');
|
|
1195
|
+
if (memoryCandidateExists(content, text, evidence)) return false;
|
|
1196
|
+
if (isPrunedMemoryText(content, text)) return false;
|
|
1197
|
+
if (opts.dedupeByText && memoryCandidateTextExists(content, text)) return false;
|
|
1198
|
+
|
|
1199
|
+
content = replaceMetadata(content, 'Updated', formatTimestamp(today));
|
|
1200
|
+
content = appendToSection(content, 'Promotion Candidates', `- ${today.date}: ${text} (evidence: ${evidence})\n`);
|
|
1201
|
+
fs.writeFileSync(file, content);
|
|
1202
|
+
appendMemoryEvent(home, today, {
|
|
1203
|
+
type: 'memory.candidate.created',
|
|
1204
|
+
source,
|
|
1205
|
+
text,
|
|
1206
|
+
evidence,
|
|
1207
|
+
});
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function shellQuoteText(text) {
|
|
1212
|
+
return text.replace(/["\\$`]/g, '\\$&');
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function sectionHasMeaningfulContent(section) {
|
|
1216
|
+
return section
|
|
1217
|
+
.split('\n')
|
|
1218
|
+
.map((line) => line.trim())
|
|
1219
|
+
.some((line) => line && line !== '- None.' && line !== '- None yet.');
|
|
1220
|
+
}
|
|
1221
|
+
|
|
873
1222
|
function appendWorklog(home, today, entry) {
|
|
874
1223
|
const worklogPath = path.join(home, 'worklog', `${today.date}.md`);
|
|
875
1224
|
ensureDir(path.dirname(worklogPath));
|
|
@@ -900,7 +1249,7 @@ function parseWorklogEntries(worklog) {
|
|
|
900
1249
|
const headingText = heading[0];
|
|
901
1250
|
const headingParts = headingText.replace(/^#{2,3} \d{2}:\d{2} - /, '').split(' - ');
|
|
902
1251
|
const headingTask = headingParts.length > 1 && isTaskId(headingParts[0]) ? headingParts[0] : null;
|
|
903
|
-
const jiraTask = block
|
|
1252
|
+
const jiraTask = metadataValue(block, 'Jira');
|
|
904
1253
|
return {
|
|
905
1254
|
block,
|
|
906
1255
|
task: headingTask || jiraTask || null,
|
|
@@ -955,7 +1304,7 @@ function ensurePrivateState(home, ctx) {
|
|
|
955
1304
|
if (!fs.existsSync(awarenessPath(home))) fs.writeFileSync(awarenessPath(home), initialAwareness(today));
|
|
956
1305
|
if (!fs.existsSync(path.join(home, 'worklog', `${today.date}.md`))) fs.writeFileSync(path.join(home, 'worklog', `${today.date}.md`), dailyWorklog(today.date));
|
|
957
1306
|
if (!fs.existsSync(personalityPath(home))) fs.writeFileSync(personalityPath(home), readTemplate('personality.md'));
|
|
958
|
-
if (!fs.existsSync(
|
|
1307
|
+
if (!fs.existsSync(longTermMemoryPath(home))) fs.writeFileSync(longTermMemoryPath(home), readTemplate('memory-long-term.md'));
|
|
959
1308
|
}
|
|
960
1309
|
|
|
961
1310
|
function replaceSection(content, section, body) {
|
|
@@ -971,12 +1320,13 @@ function replaceSection(content, section, body) {
|
|
|
971
1320
|
function appendToSection(content, section, addition) {
|
|
972
1321
|
const current = extractSection(content, section);
|
|
973
1322
|
const cleaned = current.replace(/^- None yet\.\n?/m, '').trimEnd();
|
|
974
|
-
|
|
1323
|
+
const prefix = cleaned ? `${cleaned}\n\n` : '';
|
|
1324
|
+
return replaceSection(content, section, `${prefix}${addition.trimEnd()}\n`);
|
|
975
1325
|
}
|
|
976
1326
|
|
|
977
1327
|
function extractSection(content, section) {
|
|
978
1328
|
const pattern = new RegExp(`(?:^|\\n)## ${escapeRegExp(section)}\\n\\n?([\\s\\S]*?)(?=\\n## |$)`);
|
|
979
|
-
const match =
|
|
1329
|
+
const match = pattern.exec(content);
|
|
980
1330
|
return match ? match[1] : '';
|
|
981
1331
|
}
|
|
982
1332
|
|
|
@@ -988,12 +1338,18 @@ function replaceMetadata(content, key, value) {
|
|
|
988
1338
|
return content.replace(/^# .+$/m, (heading) => `${heading}\n\n- ${key}: ${value}`);
|
|
989
1339
|
}
|
|
990
1340
|
|
|
1341
|
+
function metadataValue(content, key) {
|
|
1342
|
+
const prefix = `- ${key}:`;
|
|
1343
|
+
const line = content.split('\n').find((candidate) => candidate.startsWith(prefix));
|
|
1344
|
+
return line?.slice(prefix.length).trim() || null;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
991
1347
|
function currentContext(home) {
|
|
992
1348
|
const file = awarenessPath(home);
|
|
993
1349
|
if (!fs.existsSync(file)) return 'Not specified';
|
|
994
1350
|
const focus = extractSection(fs.readFileSync(file, 'utf8'), 'Current Focus');
|
|
995
|
-
const repo = focus
|
|
996
|
-
const branch = focus
|
|
1351
|
+
const repo = metadataValue(focus, 'Repository') || 'Not specified';
|
|
1352
|
+
const branch = metadataValue(focus, 'Branch') || 'Not specified';
|
|
997
1353
|
return `${repo} / ${branch}`;
|
|
998
1354
|
}
|
|
999
1355
|
|
|
@@ -1171,10 +1527,11 @@ function initialUserMemory(user, timestamp) {
|
|
|
1171
1527
|
}
|
|
1172
1528
|
|
|
1173
1529
|
function normalizeState(state) {
|
|
1174
|
-
|
|
1530
|
+
const normalized = STATE_ALIASES[state] || String(state).replaceAll('_', '-');
|
|
1531
|
+
if (!VALID_STATES.has(normalized)) {
|
|
1175
1532
|
throw new Error(`Invalid state: ${state}. Valid states: ${[...VALID_STATES].join(', ')}`);
|
|
1176
1533
|
}
|
|
1177
|
-
return
|
|
1534
|
+
return normalized;
|
|
1178
1535
|
}
|
|
1179
1536
|
|
|
1180
1537
|
function expandTargets(value, allowed) {
|
|
@@ -1191,11 +1548,15 @@ function expandTargets(value, allowed) {
|
|
|
1191
1548
|
|
|
1192
1549
|
function required(opts, key) {
|
|
1193
1550
|
if (!opts[key] || opts[key] === true) {
|
|
1194
|
-
throw new Error(`Missing required option: --${key
|
|
1551
|
+
throw new Error(`Missing required option: --${optionName(key)}`);
|
|
1195
1552
|
}
|
|
1196
1553
|
return opts[key];
|
|
1197
1554
|
}
|
|
1198
1555
|
|
|
1556
|
+
function optionName(key) {
|
|
1557
|
+
return key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1199
1560
|
function agentsHome(ctx, opts) {
|
|
1200
1561
|
const raw = opts.home
|
|
1201
1562
|
|| opts.agentFolder
|
|
@@ -1277,6 +1638,86 @@ function personalityPath(home) {
|
|
|
1277
1638
|
return path.join(home, 'memory', 'personality.md');
|
|
1278
1639
|
}
|
|
1279
1640
|
|
|
1641
|
+
function longTermMemoryPath(home) {
|
|
1642
|
+
return path.join(home, 'memory', 'long-term.md');
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function memoryEventPath(home) {
|
|
1646
|
+
return path.join(home, 'memory', 'events.jsonl');
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function appendMemoryEvent(home, today, event) {
|
|
1650
|
+
const file = memoryEventPath(home);
|
|
1651
|
+
ensureDir(path.dirname(file));
|
|
1652
|
+
fs.appendFileSync(file, `${JSON.stringify({
|
|
1653
|
+
timestamp: formatTimestamp(today),
|
|
1654
|
+
...event,
|
|
1655
|
+
})}\n`);
|
|
1656
|
+
return file;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function readMemoryEvents(home) {
|
|
1660
|
+
const file = memoryEventPath(home);
|
|
1661
|
+
if (!fs.existsSync(file)) return [];
|
|
1662
|
+
return fs.readFileSync(file, 'utf8')
|
|
1663
|
+
.split('\n')
|
|
1664
|
+
.map((line) => line.trim())
|
|
1665
|
+
.filter(Boolean)
|
|
1666
|
+
.map((line) => JSON.parse(line));
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function collectRecallSources(home) {
|
|
1670
|
+
return [
|
|
1671
|
+
...markdownFilesRecursive(path.join(home, 'memory')),
|
|
1672
|
+
memoryEventPath(home),
|
|
1673
|
+
...markdownFiles(path.join(home, 'worklog')),
|
|
1674
|
+
...markdownFiles(path.join(home, 'evaluations')),
|
|
1675
|
+
].filter((file, index, files) => fs.existsSync(file) && files.indexOf(file) === index);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function markdownFiles(dir) {
|
|
1679
|
+
if (!fs.existsSync(dir)) return [];
|
|
1680
|
+
return fs.readdirSync(dir)
|
|
1681
|
+
.filter((name) => name.endsWith('.md'))
|
|
1682
|
+
.sort()
|
|
1683
|
+
.map((name) => path.join(dir, name));
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
function markdownFilesRecursive(dir) {
|
|
1687
|
+
if (!fs.existsSync(dir)) return [];
|
|
1688
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
1689
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
1690
|
+
.flatMap((entry) => {
|
|
1691
|
+
const file = path.join(dir, entry.name);
|
|
1692
|
+
if (entry.isDirectory()) return markdownFilesRecursive(file);
|
|
1693
|
+
if (entry.isFile() && entry.name.endsWith('.md')) return [file];
|
|
1694
|
+
return [];
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function recallMatches(home, query, limit) {
|
|
1699
|
+
const termGroups = recallTermGroups(query);
|
|
1700
|
+
const results = [];
|
|
1701
|
+
for (const file of collectRecallSources(home)) {
|
|
1702
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
1703
|
+
const lines = content.split('\n');
|
|
1704
|
+
lines.forEach((line, index) => {
|
|
1705
|
+
const haystack = normalizeSearchText(line);
|
|
1706
|
+
const score = termGroups.filter((terms) => terms.some((term) => haystack.includes(term))).length;
|
|
1707
|
+
if (score > 0) {
|
|
1708
|
+
results.push({
|
|
1709
|
+
file,
|
|
1710
|
+
line: index + 1,
|
|
1711
|
+
score,
|
|
1712
|
+
text: line.trim(),
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
results.sort((left, right) => right.score - left.score || left.file.localeCompare(right.file) || left.line - right.line);
|
|
1718
|
+
return results.slice(0, limit);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1280
1721
|
function userMemoryPath(home, userSlug) {
|
|
1281
1722
|
return path.join(home, 'memory', 'users', `${userSlug}.md`);
|
|
1282
1723
|
}
|