@fyso/awareness-framework 0.3.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/docs/cli.md +10 -5
- package/docs/evaluation-loop.md +1 -1
- package/docs/lifecycle.md +4 -0
- package/docs/memory.md +4 -1
- package/package.json +1 -1
- package/src/cli.js +65 -73
- package/src/memory-candidates.js +82 -0
- package/src/text.js +35 -0
package/docs/cli.md
CHANGED
|
@@ -118,7 +118,7 @@ awareness init --wrappers \
|
|
|
118
118
|
|
|
119
119
|
### `status`
|
|
120
120
|
|
|
121
|
-
Shows the current focus and warnings.
|
|
121
|
+
Shows the current focus and warnings. Warnings are printed but do not make the command fail; use `awareness check --strict` when automation should fail on warnings.
|
|
122
122
|
|
|
123
123
|
```bash
|
|
124
124
|
awareness status
|
|
@@ -153,9 +153,12 @@ awareness focus \
|
|
|
153
153
|
--summary "Agent awareness framework" \
|
|
154
154
|
--repo fyso-dev/awareness-framework \
|
|
155
155
|
--branch codex/cli-and-personality \
|
|
156
|
+
--state in-progress \
|
|
156
157
|
--next "Run tests and open a PR"
|
|
157
158
|
```
|
|
158
159
|
|
|
160
|
+
Valid states are `started`, `in-progress`, `paused`, `blocked`, `waiting`, `done`, `in-review`, and `ready`. Underscore aliases such as `in_progress` and `in_review` are accepted and normalized.
|
|
161
|
+
|
|
159
162
|
### `log`
|
|
160
163
|
|
|
161
164
|
Appends a concrete progress entry to the daily worklog.
|
|
@@ -170,7 +173,7 @@ awareness log \
|
|
|
170
173
|
|
|
171
174
|
### `handoff`
|
|
172
175
|
|
|
173
|
-
Prints a handoff snapshot from the awareness board.
|
|
176
|
+
Prints a handoff snapshot from the awareness board. Like `status`, warnings are informational and return exit code 0.
|
|
174
177
|
|
|
175
178
|
```bash
|
|
176
179
|
awareness handoff
|
|
@@ -202,6 +205,8 @@ awareness memory promote --kind preference --text "Surface memory candidates pro
|
|
|
202
205
|
|
|
203
206
|
`memory review` scans promotion candidates and suggests repeated candidates as `pattern` promotions once they appear at least twice by default.
|
|
204
207
|
|
|
208
|
+
`memory candidates` lists active promotion candidates only. Text that has been pruned or revised remains in the Markdown history but is hidden from active candidates, excluded from suggestions, and rejected by `memory promote`.
|
|
209
|
+
|
|
205
210
|
Valid promotion kinds are `preference`, `pattern`, `project`, and `review`.
|
|
206
211
|
|
|
207
212
|
### Local memory operations
|
|
@@ -216,9 +221,9 @@ awareness improve
|
|
|
216
221
|
```
|
|
217
222
|
|
|
218
223
|
`remember` records a promotion candidate and appends `memory.candidate.created` to `memory/events.jsonl`.
|
|
219
|
-
`recall` performs deterministic local text search across memory, memory events, worklogs, and evaluations.
|
|
220
|
-
`forget` records a prune/revision entry and appends `memory.pruned`; it does not destructively delete historical evidence.
|
|
221
|
-
`improve` runs the evaluation/review loop and appends `evaluation.created` and `pattern.suggested` events when applicable.
|
|
224
|
+
`recall` performs deterministic local text search across memory, memory events, worklogs, and evaluations. Matching is case- and accent-insensitive, includes simple singular/plural normalization, and includes a small English/Spanish alias set for memory/user terms.
|
|
225
|
+
`forget` records a prune/revision entry and appends `memory.pruned`; it does not destructively delete historical evidence, but pruned text is inactive for candidate review and promotion.
|
|
226
|
+
`improve` runs the evaluation/review loop and appends `evaluation.created` and `pattern.suggested` events when applicable. Auto-generated evaluation candidates are deduplicated by text across days so recurring diagnostics do not flood human-curated candidates.
|
|
222
227
|
|
|
223
228
|
### `hook run`
|
|
224
229
|
|
package/docs/evaluation-loop.md
CHANGED
|
@@ -115,7 +115,7 @@ Each evaluation should end with one of these outcomes:
|
|
|
115
115
|
- Propose framework PR.
|
|
116
116
|
- Ask user for confirmation.
|
|
117
117
|
|
|
118
|
-
Daily evaluation is active by default: when it writes an evaluation note, the CLI also records promotion candidates under `memory/long-term.md`. Candidates are intentionally reviewable. Use `awareness memory review` to surface repeated candidates as suggested `pattern` promotions, then promote them with `awareness memory promote` only after they are user-confirmed, repeated,
|
|
118
|
+
Daily evaluation is active by default: when it writes an evaluation note, the CLI also records promotion candidates under `memory/long-term.md`. Auto-generated candidates are deduplicated by text across days so repeated diagnostics do not crowd out human-curated observations. Candidates are intentionally reviewable. Use `awareness memory review` to surface repeated candidates as suggested `pattern` promotions, then promote them with `awareness memory promote` only after they are user-confirmed, repeated, operationally important, and not pruned or revised.
|
|
119
119
|
|
|
120
120
|
## Example Outcomes
|
|
121
121
|
|
package/docs/lifecycle.md
CHANGED
|
@@ -74,12 +74,16 @@ When switching tasks, the agent:
|
|
|
74
74
|
|
|
75
75
|
Valid states:
|
|
76
76
|
|
|
77
|
+
- `started`
|
|
77
78
|
- `in-progress`
|
|
78
79
|
- `paused`
|
|
79
80
|
- `blocked`
|
|
80
81
|
- `waiting`
|
|
81
82
|
- `done`
|
|
82
83
|
- `in-review`
|
|
84
|
+
- `ready`
|
|
85
|
+
|
|
86
|
+
The CLI also accepts underscore aliases such as `in_progress` and normalizes them to hyphenated state names.
|
|
83
87
|
|
|
84
88
|
## 5. Handoff
|
|
85
89
|
|
package/docs/memory.md
CHANGED
|
@@ -19,7 +19,7 @@ Do not load every layer into every prompt. Load the smallest layer that answers
|
|
|
19
19
|
Awareness uses a small local operation vocabulary:
|
|
20
20
|
|
|
21
21
|
- `remember`: capture an evidence-backed candidate.
|
|
22
|
-
- `recall`: search local memory, events, worklogs, and evaluations.
|
|
22
|
+
- `recall`: search local memory, events, worklogs, and evaluations with deterministic normalized text matching.
|
|
23
23
|
- `forget`: prune or revise stale memory without destructive deletion.
|
|
24
24
|
- `improve`: run evaluation plus memory review to surface repeated candidates.
|
|
25
25
|
|
|
@@ -113,10 +113,13 @@ Information should move from short-term to long-term only when it earns promotio
|
|
|
113
113
|
|
|
114
114
|
Hooks and scheduled maintenance may perform steps 1 and 2 by recording observations, warnings, or evaluation notes. They must not perform step 4 silently.
|
|
115
115
|
|
|
116
|
+
Pruned or revised text remains in the Markdown history for auditability, but it is inactive. It should not appear in active candidate listings, repeated-candidate suggestions, or promotion commands.
|
|
117
|
+
|
|
116
118
|
## Promotion Rules
|
|
117
119
|
|
|
118
120
|
- Promote explicit user preferences immediately when they affect future collaboration.
|
|
119
121
|
- Promote inferred preferences only after repeated evidence.
|
|
122
|
+
- Do not promote text that has been pruned or revised; record a new corrected candidate instead.
|
|
120
123
|
- Promote framework changes only through version control.
|
|
121
124
|
- Keep private memory private; do not copy private examples into public docs.
|
|
122
125
|
- Prefer small, operational statements over long stories.
|
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
|
|
|
@@ -142,6 +154,9 @@ Scope options:
|
|
|
142
154
|
--channel NAME Store state under <folder>/channels/<safe-name>.
|
|
143
155
|
--user ID Select a user memory file for user commands.
|
|
144
156
|
|
|
157
|
+
State values:
|
|
158
|
+
${[...VALID_STATES].join(', ')}
|
|
159
|
+
|
|
145
160
|
The CLI maintains private files under ~/.agents by default. It does not post to Jira, GitHub, or any external system.`);
|
|
146
161
|
}
|
|
147
162
|
|
|
@@ -209,7 +224,7 @@ function statusCommand(ctx, opts) {
|
|
|
209
224
|
for (const warning of warnings) {
|
|
210
225
|
out(ctx, `- ${warning}`);
|
|
211
226
|
}
|
|
212
|
-
return
|
|
227
|
+
return 0;
|
|
213
228
|
}
|
|
214
229
|
|
|
215
230
|
function checkCommand(ctx, opts) {
|
|
@@ -330,7 +345,7 @@ function handoffCommand(ctx, opts) {
|
|
|
330
345
|
}
|
|
331
346
|
}
|
|
332
347
|
|
|
333
|
-
return
|
|
348
|
+
return 0;
|
|
334
349
|
}
|
|
335
350
|
|
|
336
351
|
function evaluateCommand(ctx, opts) {
|
|
@@ -354,8 +369,9 @@ function evaluateCommand(ctx, opts) {
|
|
|
354
369
|
ensureDir(path.dirname(evaluationPath));
|
|
355
370
|
fs.writeFileSync(evaluationPath, content);
|
|
356
371
|
const candidates = recordEvaluationMemoryCandidates(home, today);
|
|
372
|
+
const candidateSummary = candidates.length ? `${candidates.length} recorded` : 'none';
|
|
357
373
|
out(ctx, `Evaluation written: ${evaluationPath}`);
|
|
358
|
-
out(ctx, `Memory candidates: ${
|
|
374
|
+
out(ctx, `Memory candidates: ${candidateSummary}`);
|
|
359
375
|
return 0;
|
|
360
376
|
}
|
|
361
377
|
|
|
@@ -382,8 +398,11 @@ function memoryCommand(ctx, subcommand, opts) {
|
|
|
382
398
|
|
|
383
399
|
function memoryCandidatesCommand(ctx, home) {
|
|
384
400
|
const content = fs.readFileSync(longTermMemoryPath(home), 'utf8');
|
|
401
|
+
const activeCandidates = activeMemoryCandidates(content)
|
|
402
|
+
.map((candidate) => candidate.line)
|
|
403
|
+
.join('\n');
|
|
385
404
|
out(ctx, 'Promotion Candidates');
|
|
386
|
-
out(ctx,
|
|
405
|
+
out(ctx, activeCandidates || '- None yet.');
|
|
387
406
|
return 0;
|
|
388
407
|
}
|
|
389
408
|
|
|
@@ -427,6 +446,9 @@ function memoryPromoteCommand(ctx, home, opts) {
|
|
|
427
446
|
const today = todayParts(ctx);
|
|
428
447
|
const file = longTermMemoryPath(home);
|
|
429
448
|
let content = fs.readFileSync(file, 'utf8');
|
|
449
|
+
if (isPrunedMemoryText(content, text)) {
|
|
450
|
+
throw new Error(`Cannot promote pruned or revised memory: ${text}`);
|
|
451
|
+
}
|
|
430
452
|
content = replaceMetadata(content, 'Updated', formatTimestamp(today));
|
|
431
453
|
content = appendToSection(content, section, `- ${today.date}: ${text} (evidence: ${evidence})\n`);
|
|
432
454
|
fs.writeFileSync(file, content);
|
|
@@ -553,7 +575,8 @@ function improveCommand(ctx, opts) {
|
|
|
553
575
|
}
|
|
554
576
|
|
|
555
577
|
out(ctx, `Evaluation: ${evaluation.status} (${evaluation.file})`);
|
|
556
|
-
|
|
578
|
+
const candidateSummary = evaluation.candidates ? `${evaluation.candidates.length} (from evaluation diagnostics)` : 'not changed';
|
|
579
|
+
out(ctx, `Auto-generated candidates: ${candidateSummary}`);
|
|
557
580
|
out(ctx, `Pattern suggestions: ${suggestions.length}`);
|
|
558
581
|
for (const suggestion of suggestions) {
|
|
559
582
|
out(ctx, `- ${suggestion.text} (${suggestion.count} observations)`);
|
|
@@ -767,7 +790,10 @@ function scheduleRunCommand(ctx, opts) {
|
|
|
767
790
|
out(ctx, `Schedule run complete: ${cadence}`);
|
|
768
791
|
out(ctx, `Runtime log: ${eventFile}`);
|
|
769
792
|
if (evaluation) out(ctx, `Evaluation: ${evaluation.status} (${evaluation.file})`);
|
|
770
|
-
if (evaluation?.candidates)
|
|
793
|
+
if (evaluation?.candidates) {
|
|
794
|
+
const candidateSummary = evaluation.candidates.length ? `${evaluation.candidates.length} recorded` : 'none';
|
|
795
|
+
out(ctx, `Memory candidates: ${candidateSummary}`);
|
|
796
|
+
}
|
|
771
797
|
out(ctx, warnings.length ? `Warnings: ${warnings.length}` : 'Warnings: none');
|
|
772
798
|
return 0;
|
|
773
799
|
}
|
|
@@ -806,7 +832,8 @@ function scheduleInstallCommand(ctx, opts) {
|
|
|
806
832
|
if (opts.load) {
|
|
807
833
|
const result = loadLaunchAgent(file, label);
|
|
808
834
|
if (result.status !== 0) {
|
|
809
|
-
|
|
835
|
+
const failure = result.stderr || result.stdout || `exit ${result.status}`;
|
|
836
|
+
throw new Error(`launchctl failed for ${file}: ${failure}`);
|
|
810
837
|
}
|
|
811
838
|
loaded.push(file);
|
|
812
839
|
}
|
|
@@ -1086,6 +1113,7 @@ function buildEvaluation(home, today) {
|
|
|
1086
1113
|
const handoff = /- Next:\s+(?!The next concrete action)\S+/.test(extractSection(current, 'Current Focus')) ? 2 : current ? 1 : 0;
|
|
1087
1114
|
const noise = current.split('\n').length <= 180 && !/YYYY-MM-DD|branch-name/.test(current) ? 2 : 1;
|
|
1088
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.';
|
|
1089
1117
|
|
|
1090
1118
|
return `# Awareness Evaluation - ${today.date}
|
|
1091
1119
|
|
|
@@ -1101,7 +1129,7 @@ function buildEvaluation(home, today) {
|
|
|
1101
1129
|
|
|
1102
1130
|
## Warnings
|
|
1103
1131
|
|
|
1104
|
-
${
|
|
1132
|
+
${warningsMarkdown}
|
|
1105
1133
|
|
|
1106
1134
|
## Proposed Changes
|
|
1107
1135
|
|
|
@@ -1112,7 +1140,9 @@ ${warnings.length ? warnings.map((warning) => `- ${warning}`).join('\n') : '- No
|
|
|
1112
1140
|
|
|
1113
1141
|
function recordEvaluationMemoryCandidates(home, today) {
|
|
1114
1142
|
const candidates = buildEvaluationMemoryCandidates(home, today);
|
|
1115
|
-
return candidates.filter((candidate) => appendMemoryCandidate(home, today, candidate.text, candidate.evidence, 'evaluation'
|
|
1143
|
+
return candidates.filter((candidate) => appendMemoryCandidate(home, today, candidate.text, candidate.evidence, 'evaluation', {
|
|
1144
|
+
dedupeByText: true,
|
|
1145
|
+
}));
|
|
1116
1146
|
}
|
|
1117
1147
|
|
|
1118
1148
|
function buildEvaluationMemoryCandidates(home, today) {
|
|
@@ -1159,10 +1189,12 @@ function buildEvaluationMemoryCandidates(home, today) {
|
|
|
1159
1189
|
return candidates;
|
|
1160
1190
|
}
|
|
1161
1191
|
|
|
1162
|
-
function appendMemoryCandidate(home, today, text, evidence, source = 'memory.note') {
|
|
1192
|
+
function appendMemoryCandidate(home, today, text, evidence, source = 'memory.note', opts = {}) {
|
|
1163
1193
|
const file = longTermMemoryPath(home);
|
|
1164
1194
|
let content = fs.readFileSync(file, 'utf8');
|
|
1165
1195
|
if (memoryCandidateExists(content, text, evidence)) return false;
|
|
1196
|
+
if (isPrunedMemoryText(content, text)) return false;
|
|
1197
|
+
if (opts.dedupeByText && memoryCandidateTextExists(content, text)) return false;
|
|
1166
1198
|
|
|
1167
1199
|
content = replaceMetadata(content, 'Updated', formatTimestamp(today));
|
|
1168
1200
|
content = appendToSection(content, 'Promotion Candidates', `- ${today.date}: ${text} (evidence: ${evidence})\n`);
|
|
@@ -1176,58 +1208,6 @@ function appendMemoryCandidate(home, today, text, evidence, source = 'memory.not
|
|
|
1176
1208
|
return true;
|
|
1177
1209
|
}
|
|
1178
1210
|
|
|
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
1211
|
function shellQuoteText(text) {
|
|
1232
1212
|
return text.replace(/["\\$`]/g, '\\$&');
|
|
1233
1213
|
}
|
|
@@ -1269,7 +1249,7 @@ function parseWorklogEntries(worklog) {
|
|
|
1269
1249
|
const headingText = heading[0];
|
|
1270
1250
|
const headingParts = headingText.replace(/^#{2,3} \d{2}:\d{2} - /, '').split(' - ');
|
|
1271
1251
|
const headingTask = headingParts.length > 1 && isTaskId(headingParts[0]) ? headingParts[0] : null;
|
|
1272
|
-
const jiraTask = block
|
|
1252
|
+
const jiraTask = metadataValue(block, 'Jira');
|
|
1273
1253
|
return {
|
|
1274
1254
|
block,
|
|
1275
1255
|
task: headingTask || jiraTask || null,
|
|
@@ -1340,12 +1320,13 @@ function replaceSection(content, section, body) {
|
|
|
1340
1320
|
function appendToSection(content, section, addition) {
|
|
1341
1321
|
const current = extractSection(content, section);
|
|
1342
1322
|
const cleaned = current.replace(/^- None yet\.\n?/m, '').trimEnd();
|
|
1343
|
-
|
|
1323
|
+
const prefix = cleaned ? `${cleaned}\n\n` : '';
|
|
1324
|
+
return replaceSection(content, section, `${prefix}${addition.trimEnd()}\n`);
|
|
1344
1325
|
}
|
|
1345
1326
|
|
|
1346
1327
|
function extractSection(content, section) {
|
|
1347
1328
|
const pattern = new RegExp(`(?:^|\\n)## ${escapeRegExp(section)}\\n\\n?([\\s\\S]*?)(?=\\n## |$)`);
|
|
1348
|
-
const match =
|
|
1329
|
+
const match = pattern.exec(content);
|
|
1349
1330
|
return match ? match[1] : '';
|
|
1350
1331
|
}
|
|
1351
1332
|
|
|
@@ -1357,12 +1338,18 @@ function replaceMetadata(content, key, value) {
|
|
|
1357
1338
|
return content.replace(/^# .+$/m, (heading) => `${heading}\n\n- ${key}: ${value}`);
|
|
1358
1339
|
}
|
|
1359
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
|
+
|
|
1360
1347
|
function currentContext(home) {
|
|
1361
1348
|
const file = awarenessPath(home);
|
|
1362
1349
|
if (!fs.existsSync(file)) return 'Not specified';
|
|
1363
1350
|
const focus = extractSection(fs.readFileSync(file, 'utf8'), 'Current Focus');
|
|
1364
|
-
const repo = focus
|
|
1365
|
-
const branch = focus
|
|
1351
|
+
const repo = metadataValue(focus, 'Repository') || 'Not specified';
|
|
1352
|
+
const branch = metadataValue(focus, 'Branch') || 'Not specified';
|
|
1366
1353
|
return `${repo} / ${branch}`;
|
|
1367
1354
|
}
|
|
1368
1355
|
|
|
@@ -1540,10 +1527,11 @@ function initialUserMemory(user, timestamp) {
|
|
|
1540
1527
|
}
|
|
1541
1528
|
|
|
1542
1529
|
function normalizeState(state) {
|
|
1543
|
-
|
|
1530
|
+
const normalized = STATE_ALIASES[state] || String(state).replaceAll('_', '-');
|
|
1531
|
+
if (!VALID_STATES.has(normalized)) {
|
|
1544
1532
|
throw new Error(`Invalid state: ${state}. Valid states: ${[...VALID_STATES].join(', ')}`);
|
|
1545
1533
|
}
|
|
1546
|
-
return
|
|
1534
|
+
return normalized;
|
|
1547
1535
|
}
|
|
1548
1536
|
|
|
1549
1537
|
function expandTargets(value, allowed) {
|
|
@@ -1560,11 +1548,15 @@ function expandTargets(value, allowed) {
|
|
|
1560
1548
|
|
|
1561
1549
|
function required(opts, key) {
|
|
1562
1550
|
if (!opts[key] || opts[key] === true) {
|
|
1563
|
-
throw new Error(`Missing required option: --${key
|
|
1551
|
+
throw new Error(`Missing required option: --${optionName(key)}`);
|
|
1564
1552
|
}
|
|
1565
1553
|
return opts[key];
|
|
1566
1554
|
}
|
|
1567
1555
|
|
|
1556
|
+
function optionName(key) {
|
|
1557
|
+
return key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1568
1560
|
function agentsHome(ctx, opts) {
|
|
1569
1561
|
const raw = opts.home
|
|
1570
1562
|
|| opts.agentFolder
|
|
@@ -1704,14 +1696,14 @@ function markdownFilesRecursive(dir) {
|
|
|
1704
1696
|
}
|
|
1705
1697
|
|
|
1706
1698
|
function recallMatches(home, query, limit) {
|
|
1707
|
-
const
|
|
1699
|
+
const termGroups = recallTermGroups(query);
|
|
1708
1700
|
const results = [];
|
|
1709
1701
|
for (const file of collectRecallSources(home)) {
|
|
1710
1702
|
const content = fs.readFileSync(file, 'utf8');
|
|
1711
1703
|
const lines = content.split('\n');
|
|
1712
1704
|
lines.forEach((line, index) => {
|
|
1713
|
-
const haystack = line
|
|
1714
|
-
const score =
|
|
1705
|
+
const haystack = normalizeSearchText(line);
|
|
1706
|
+
const score = termGroups.filter((terms) => terms.some((term) => haystack.includes(term))).length;
|
|
1715
1707
|
if (score > 0) {
|
|
1716
1708
|
results.push({
|
|
1717
1709
|
file,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { normalizeSearchText } from './text.js';
|
|
2
|
+
|
|
3
|
+
export function activeMemoryCandidates(content) {
|
|
4
|
+
const prunedTexts = prunedMemoryCandidateTexts(content);
|
|
5
|
+
return parseMemoryCandidates(content).filter((candidate) => !prunedTexts.has(normalizeMemoryCandidateText(candidate.text)));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isPrunedMemoryText(content, text) {
|
|
9
|
+
return prunedMemoryCandidateTexts(content).has(normalizeMemoryCandidateText(text));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function memoryCandidateExists(content, text, evidence) {
|
|
13
|
+
const candidates = extractMarkdownSection(content, 'Promotion Candidates');
|
|
14
|
+
return candidates.split('\n').some((line) => line.includes(`: ${text} (evidence: ${evidence})`));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function memoryCandidateTextExists(content, text) {
|
|
18
|
+
const key = normalizeMemoryCandidateText(text);
|
|
19
|
+
return parseMemoryCandidates(content).some((candidate) => normalizeMemoryCandidateText(candidate.text) === key);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function repeatedMemoryCandidateSuggestions(content, minCount) {
|
|
23
|
+
const grouped = new Map();
|
|
24
|
+
for (const candidate of activeMemoryCandidates(content)) {
|
|
25
|
+
const key = normalizeMemoryCandidateText(candidate.text);
|
|
26
|
+
const group = grouped.get(key) || { text: candidate.text, count: 0, evidence: [] };
|
|
27
|
+
group.count += 1;
|
|
28
|
+
group.evidence.push(candidate.evidence);
|
|
29
|
+
grouped.set(key, group);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return [...grouped.values()]
|
|
33
|
+
.filter((group) => group.count >= minCount)
|
|
34
|
+
.map((group) => ({
|
|
35
|
+
text: group.text,
|
|
36
|
+
count: group.count,
|
|
37
|
+
evidence: [...new Set(group.evidence)].join('; '),
|
|
38
|
+
}))
|
|
39
|
+
.sort((left, right) => right.count - left.count || left.text.localeCompare(right.text));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseMemoryCandidates(content) {
|
|
43
|
+
const candidates = [];
|
|
44
|
+
const candidatePattern = /^- \d{4}-\d{2}-\d{2}: (.+) \(evidence: (.+)\)$/;
|
|
45
|
+
for (const rawLine of extractMarkdownSection(content, 'Promotion Candidates').split('\n')) {
|
|
46
|
+
const match = candidatePattern.exec(rawLine.trim());
|
|
47
|
+
if (!match) continue;
|
|
48
|
+
candidates.push({
|
|
49
|
+
line: match[0],
|
|
50
|
+
text: match[1],
|
|
51
|
+
evidence: match[2],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return candidates;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function prunedMemoryCandidateTexts(content) {
|
|
58
|
+
const pruned = new Set();
|
|
59
|
+
const prunedPattern = /^- \d{4}-\d{2}-\d{2}: (.+) \(reason: .+; evidence: .+\)$/;
|
|
60
|
+
for (const rawLine of extractMarkdownSection(content, 'Pruned Or Revised').split('\n')) {
|
|
61
|
+
const match = prunedPattern.exec(rawLine.trim());
|
|
62
|
+
if (match) pruned.add(normalizeMemoryCandidateText(match[1]));
|
|
63
|
+
}
|
|
64
|
+
return pruned;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeMemoryCandidateText(text) {
|
|
68
|
+
return normalizeSearchText(text);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extractMarkdownSection(content, section) {
|
|
72
|
+
const lines = content.split('\n');
|
|
73
|
+
const start = lines.findIndex((line) => line.trimEnd() === `## ${section}`);
|
|
74
|
+
if (start === -1) return '';
|
|
75
|
+
|
|
76
|
+
const body = [];
|
|
77
|
+
for (const line of lines.slice(start + 1)) {
|
|
78
|
+
if (line.startsWith('## ')) break;
|
|
79
|
+
body.push(line);
|
|
80
|
+
}
|
|
81
|
+
return body.join('\n').replace(/^\n/, '');
|
|
82
|
+
}
|
package/src/text.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const RECALL_ALIASES = {
|
|
2
|
+
memoria: ['memory'],
|
|
3
|
+
memorias: ['memory'],
|
|
4
|
+
memory: ['memoria', 'memorias'],
|
|
5
|
+
user: ['usuario', 'usuarios'],
|
|
6
|
+
users: ['usuario', 'usuarios'],
|
|
7
|
+
usuario: ['user', 'users'],
|
|
8
|
+
usuarios: ['user', 'users'],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function normalizeSearchText(text) {
|
|
12
|
+
return String(text)
|
|
13
|
+
.normalize('NFD')
|
|
14
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/[^a-z0-9._-]+/g, ' ')
|
|
17
|
+
.replace(/\s+/g, ' ')
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function recallTermGroups(query) {
|
|
22
|
+
return normalizeSearchText(query)
|
|
23
|
+
.split(/\s+/)
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.map((term) => new Set([term, ...recallTokenVariants(term), ...(RECALL_ALIASES[term] || [])]))
|
|
26
|
+
.map((terms) => [...terms].filter(Boolean))
|
|
27
|
+
.filter((terms, index, groups) => groups.findIndex((group) => group[0] === terms[0]) === index);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function recallTokenVariants(term) {
|
|
31
|
+
const variants = [];
|
|
32
|
+
if (term.endsWith('es') && term.length > 4) variants.push(term.slice(0, -2));
|
|
33
|
+
if (term.endsWith('s') && term.length > 3) variants.push(term.slice(0, -1));
|
|
34
|
+
return variants;
|
|
35
|
+
}
|