@teammates/cli 0.4.1 → 0.5.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 +36 -4
- package/dist/adapter.d.ts +19 -3
- package/dist/adapter.js +168 -96
- package/dist/adapter.test.js +29 -16
- package/dist/adapters/cli-proxy.d.ts +3 -1
- package/dist/adapters/cli-proxy.js +65 -6
- package/dist/adapters/copilot.d.ts +3 -1
- package/dist/adapters/copilot.js +16 -3
- package/dist/adapters/echo.d.ts +3 -1
- package/dist/adapters/echo.js +4 -2
- package/dist/banner.js +5 -1
- package/dist/cli-args.js +23 -23
- package/dist/cli-args.test.d.ts +1 -0
- package/dist/cli-args.test.js +125 -0
- package/dist/cli.js +486 -220
- package/dist/compact.d.ts +23 -0
- package/dist/compact.js +181 -11
- package/dist/compact.test.js +323 -7
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/onboard.js +165 -165
- package/dist/orchestrator.js +7 -2
- package/dist/personas.d.ts +42 -0
- package/dist/personas.js +108 -0
- package/dist/personas.test.d.ts +1 -0
- package/dist/personas.test.js +88 -0
- package/dist/registry.test.js +23 -23
- package/dist/theme.test.d.ts +1 -0
- package/dist/theme.test.js +113 -0
- package/dist/types.d.ts +2 -0
- package/package.json +4 -3
- package/personas/architect.md +95 -0
- package/personas/backend.md +97 -0
- package/personas/data-engineer.md +96 -0
- package/personas/designer.md +96 -0
- package/personas/devops.md +97 -0
- package/personas/frontend.md +98 -0
- package/personas/ml-ai.md +100 -0
- package/personas/mobile.md +97 -0
- package/personas/performance.md +96 -0
- package/personas/pm.md +93 -0
- package/personas/prompt-engineer.md +122 -0
- package/personas/qa.md +96 -0
- package/personas/security.md +96 -0
- package/personas/sre.md +97 -0
- package/personas/swe.md +92 -0
- package/personas/tech-writer.md +97 -0
package/dist/compact.d.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* daily logs (7 days) → weekly summary (kept 52 weeks)
|
|
10
10
|
* weekly summaries (>52 weeks) → monthly summary (kept permanently)
|
|
11
11
|
*/
|
|
12
|
+
/** How long daily logs are kept on disk before purging (30 days). */
|
|
13
|
+
export declare const DAILY_LOG_RETENTION_DAYS = 30;
|
|
12
14
|
export interface CompactionResult {
|
|
13
15
|
teammate: string;
|
|
14
16
|
weekliesCreated: string[];
|
|
@@ -19,11 +21,27 @@ export interface CompactionResult {
|
|
|
19
21
|
/**
|
|
20
22
|
* Compact daily logs into weekly summaries for a single teammate.
|
|
21
23
|
* Only compacts complete weeks (not the current week).
|
|
24
|
+
* If a partial weekly exists for a week, merges new dailies into it.
|
|
22
25
|
*/
|
|
23
26
|
export declare function compactDailies(teammateDir: string): Promise<{
|
|
24
27
|
created: string[];
|
|
25
28
|
removed: string[];
|
|
26
29
|
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Auto-compact oldest daily logs into weekly summaries when the total
|
|
32
|
+
* daily log token count exceeds the budget. Unlike `compactDailies()`,
|
|
33
|
+
* this WILL compact the current week if needed, marking the result as
|
|
34
|
+
* `partial: true` in frontmatter. Partial weeklies are later merged
|
|
35
|
+
* by `compactDailies()` when more dailies arrive.
|
|
36
|
+
*
|
|
37
|
+
* @param teammateDir - Path to the teammate directory
|
|
38
|
+
* @param budgetTokens - Maximum token budget for daily logs
|
|
39
|
+
* @returns What was compacted, or null if budget was not exceeded
|
|
40
|
+
*/
|
|
41
|
+
export declare function autoCompactForBudget(teammateDir: string, budgetTokens: number): Promise<{
|
|
42
|
+
created: string[];
|
|
43
|
+
compactedDates: string[];
|
|
44
|
+
} | null>;
|
|
27
45
|
/**
|
|
28
46
|
* Compact weekly summaries older than 52 weeks into monthly summaries.
|
|
29
47
|
*/
|
|
@@ -44,3 +62,8 @@ export declare function compactEpisodic(teammateDir: string, teammateName: strin
|
|
|
44
62
|
* Returns null if there are no typed memories to distill from.
|
|
45
63
|
*/
|
|
46
64
|
export declare function buildWisdomPrompt(teammateDir: string, teammateName: string): Promise<string | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Purge daily logs older than DAILY_LOG_RETENTION_DAYS from disk.
|
|
67
|
+
* Returns the list of deleted filenames.
|
|
68
|
+
*/
|
|
69
|
+
export declare function purgeStaleDailies(teammateDir: string): Promise<string[]>;
|
package/dist/compact.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
13
13
|
import { basename, join } from "node:path";
|
|
14
|
+
/** How long daily logs are kept on disk before purging (30 days). */
|
|
15
|
+
export const DAILY_LOG_RETENTION_DAYS = 30;
|
|
14
16
|
/**
|
|
15
17
|
* Get ISO week number and year for a date.
|
|
16
18
|
* Returns { year, week } where week is 1-53.
|
|
@@ -34,7 +36,7 @@ function formatWeek(year, week) {
|
|
|
34
36
|
function groupDailiesByWeek(dailies) {
|
|
35
37
|
const groups = new Map();
|
|
36
38
|
for (const daily of dailies) {
|
|
37
|
-
const d = new Date(`${daily.date}T00:00:
|
|
39
|
+
const d = new Date(`${daily.date}T00:00:00`);
|
|
38
40
|
const { year, week } = getISOWeek(d);
|
|
39
41
|
const key = formatWeek(year, week);
|
|
40
42
|
const group = groups.get(key) ?? [];
|
|
@@ -72,8 +74,10 @@ function groupWeekliesByMonth(weeklies) {
|
|
|
72
74
|
/**
|
|
73
75
|
* Build a weekly summary from daily logs.
|
|
74
76
|
* This is a structural concatenation — the agent can refine it afterward.
|
|
77
|
+
* When `partial` is true, adds `partial: true` to frontmatter to indicate
|
|
78
|
+
* the week is incomplete and may be merged with later dailies.
|
|
75
79
|
*/
|
|
76
|
-
function buildWeeklySummary(weekKey, dailies) {
|
|
80
|
+
function buildWeeklySummary(weekKey, dailies, partial = false) {
|
|
77
81
|
// Sort chronologically
|
|
78
82
|
const sorted = [...dailies].sort((a, b) => a.date.localeCompare(b.date));
|
|
79
83
|
const firstDate = sorted[0].date;
|
|
@@ -83,6 +87,8 @@ function buildWeeklySummary(weekKey, dailies) {
|
|
|
83
87
|
lines.push(`type: weekly`);
|
|
84
88
|
lines.push(`week: ${weekKey}`);
|
|
85
89
|
lines.push(`period: ${firstDate} to ${lastDate}`);
|
|
90
|
+
if (partial)
|
|
91
|
+
lines.push("partial: true");
|
|
86
92
|
lines.push("---");
|
|
87
93
|
lines.push("");
|
|
88
94
|
lines.push(`# Week ${weekKey}`);
|
|
@@ -122,6 +128,7 @@ function buildMonthlySummary(monthKey, weeklies) {
|
|
|
122
128
|
/**
|
|
123
129
|
* Compact daily logs into weekly summaries for a single teammate.
|
|
124
130
|
* Only compacts complete weeks (not the current week).
|
|
131
|
+
* If a partial weekly exists for a week, merges new dailies into it.
|
|
125
132
|
*/
|
|
126
133
|
export async function compactDailies(teammateDir) {
|
|
127
134
|
const memoryDir = join(teammateDir, "memory");
|
|
@@ -146,13 +153,21 @@ export async function compactDailies(teammateDir) {
|
|
|
146
153
|
const now = new Date();
|
|
147
154
|
const { year: curYear, week: curWeek } = getISOWeek(now);
|
|
148
155
|
const currentWeek = formatWeek(curYear, curWeek);
|
|
149
|
-
// Check which weekly summaries already exist
|
|
156
|
+
// Check which weekly summaries already exist and which are partial
|
|
150
157
|
const existingWeeklies = new Set();
|
|
158
|
+
const partialWeeklies = new Set();
|
|
151
159
|
try {
|
|
152
160
|
const wEntries = await readdir(weeklyDir);
|
|
153
161
|
for (const e of wEntries) {
|
|
154
|
-
if (e.endsWith(".md"))
|
|
155
|
-
|
|
162
|
+
if (!e.endsWith(".md"))
|
|
163
|
+
continue;
|
|
164
|
+
const stem = basename(e, ".md");
|
|
165
|
+
existingWeeklies.add(stem);
|
|
166
|
+
// Check if this weekly is partial
|
|
167
|
+
const content = await readFile(join(weeklyDir, e), "utf-8");
|
|
168
|
+
if (/^partial:\s*true/m.test(content)) {
|
|
169
|
+
partialWeeklies.add(stem);
|
|
170
|
+
}
|
|
156
171
|
}
|
|
157
172
|
}
|
|
158
173
|
catch {
|
|
@@ -164,7 +179,25 @@ export async function compactDailies(teammateDir) {
|
|
|
164
179
|
// Skip current week
|
|
165
180
|
if (weekKey === currentWeek)
|
|
166
181
|
continue;
|
|
167
|
-
//
|
|
182
|
+
// If a partial weekly exists for this week, merge new dailies into it
|
|
183
|
+
if (partialWeeklies.has(weekKey)) {
|
|
184
|
+
const existingDailies = await extractDailiesFromWeekly(join(weeklyDir, `${weekKey}.md`));
|
|
185
|
+
// Merge: combine existing + new, dedup by date
|
|
186
|
+
const dateSet = new Set(existingDailies.map((d) => d.date));
|
|
187
|
+
const merged = [...existingDailies];
|
|
188
|
+
for (const d of weekDailies) {
|
|
189
|
+
if (!dateSet.has(d.date)) {
|
|
190
|
+
merged.push({ date: d.date, content: d.content });
|
|
191
|
+
dateSet.add(d.date);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Rewrite as non-partial (complete week now, since current week is excluded)
|
|
195
|
+
const summary = buildWeeklySummary(weekKey, merged, false);
|
|
196
|
+
await writeFile(join(weeklyDir, `${weekKey}.md`), summary, "utf-8");
|
|
197
|
+
created.push(`${weekKey}.md (merged)`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
// Skip if weekly summary already exists (non-partial)
|
|
168
201
|
if (existingWeeklies.has(weekKey))
|
|
169
202
|
continue;
|
|
170
203
|
// Create weekly dir if needed
|
|
@@ -174,14 +207,127 @@ export async function compactDailies(teammateDir) {
|
|
|
174
207
|
const weeklyFile = join(weeklyDir, `${weekKey}.md`);
|
|
175
208
|
await writeFile(weeklyFile, summary, "utf-8");
|
|
176
209
|
created.push(`${weekKey}.md`);
|
|
177
|
-
//
|
|
178
|
-
for (const daily of weekDailies) {
|
|
179
|
-
await unlink(join(memoryDir, daily.file)).catch(() => { });
|
|
180
|
-
removed.push(daily.file);
|
|
181
|
-
}
|
|
210
|
+
// Daily logs are kept on disk for DAILY_LOG_RETENTION_DAYS (purged separately)
|
|
182
211
|
}
|
|
183
212
|
return { created, removed };
|
|
184
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Extract daily log entries from a weekly summary file.
|
|
216
|
+
* Parses `## YYYY-MM-DD` sections back into individual entries.
|
|
217
|
+
*/
|
|
218
|
+
async function extractDailiesFromWeekly(weeklyPath) {
|
|
219
|
+
let raw;
|
|
220
|
+
try {
|
|
221
|
+
raw = await readFile(weeklyPath, "utf-8");
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
// Strip frontmatter
|
|
227
|
+
raw = raw.replace(/^---[\s\S]*?---\s*\n/, "");
|
|
228
|
+
// Split on ## YYYY-MM-DD headers
|
|
229
|
+
const entries = [];
|
|
230
|
+
const parts = raw.split(/^## (\d{4}-\d{2}-\d{2})\s*$/m);
|
|
231
|
+
// parts[0] = preamble (# Week header), then alternating: date, content, date, content...
|
|
232
|
+
for (let i = 1; i < parts.length; i += 2) {
|
|
233
|
+
const date = parts[i];
|
|
234
|
+
const content = (parts[i + 1] ?? "").trim();
|
|
235
|
+
if (date && content) {
|
|
236
|
+
entries.push({ date, content });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return entries;
|
|
240
|
+
}
|
|
241
|
+
/** Approximate chars per token for budget estimation. */
|
|
242
|
+
const CHARS_PER_TOKEN = 4;
|
|
243
|
+
/** Estimate tokens from character count. */
|
|
244
|
+
function estimateTokens(text) {
|
|
245
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Auto-compact oldest daily logs into weekly summaries when the total
|
|
249
|
+
* daily log token count exceeds the budget. Unlike `compactDailies()`,
|
|
250
|
+
* this WILL compact the current week if needed, marking the result as
|
|
251
|
+
* `partial: true` in frontmatter. Partial weeklies are later merged
|
|
252
|
+
* by `compactDailies()` when more dailies arrive.
|
|
253
|
+
*
|
|
254
|
+
* @param teammateDir - Path to the teammate directory
|
|
255
|
+
* @param budgetTokens - Maximum token budget for daily logs
|
|
256
|
+
* @returns What was compacted, or null if budget was not exceeded
|
|
257
|
+
*/
|
|
258
|
+
export async function autoCompactForBudget(teammateDir, budgetTokens) {
|
|
259
|
+
const memoryDir = join(teammateDir, "memory");
|
|
260
|
+
const weeklyDir = join(memoryDir, "weekly");
|
|
261
|
+
// Read all daily logs
|
|
262
|
+
const entries = await readdir(memoryDir).catch(() => []);
|
|
263
|
+
const dailies = [];
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
if (!entry.endsWith(".md"))
|
|
266
|
+
continue;
|
|
267
|
+
const stem = basename(entry, ".md");
|
|
268
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(stem))
|
|
269
|
+
continue;
|
|
270
|
+
const content = await readFile(join(memoryDir, entry), "utf-8");
|
|
271
|
+
dailies.push({ date: stem, content, file: entry });
|
|
272
|
+
}
|
|
273
|
+
if (dailies.length === 0)
|
|
274
|
+
return null;
|
|
275
|
+
// Sort chronologically (oldest first)
|
|
276
|
+
dailies.sort((a, b) => a.date.localeCompare(b.date));
|
|
277
|
+
// Estimate total token cost (excluding today — today is always in prompt)
|
|
278
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
279
|
+
const pastDailies = dailies.filter((d) => d.date !== today);
|
|
280
|
+
const totalTokens = pastDailies.reduce((sum, d) => sum + estimateTokens(`### ${d.date}\n${d.content}`), 0);
|
|
281
|
+
// If under budget, nothing to do
|
|
282
|
+
if (totalTokens <= budgetTokens)
|
|
283
|
+
return null;
|
|
284
|
+
// Group by ISO week
|
|
285
|
+
const groups = groupDailiesByWeek(pastDailies);
|
|
286
|
+
// Sort week keys chronologically (oldest first)
|
|
287
|
+
const sortedWeeks = [...groups.keys()].sort();
|
|
288
|
+
// Determine current week
|
|
289
|
+
const now = new Date();
|
|
290
|
+
const { year: curYear, week: curWeek } = getISOWeek(now);
|
|
291
|
+
const currentWeek = formatWeek(curYear, curWeek);
|
|
292
|
+
// Check existing weeklies
|
|
293
|
+
const existingWeeklies = new Set();
|
|
294
|
+
try {
|
|
295
|
+
const wEntries = await readdir(weeklyDir);
|
|
296
|
+
for (const e of wEntries) {
|
|
297
|
+
if (e.endsWith(".md"))
|
|
298
|
+
existingWeeklies.add(basename(e, ".md"));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// No weekly dir yet
|
|
303
|
+
}
|
|
304
|
+
// Compact oldest weeks first until remaining dailies fit in budget
|
|
305
|
+
let remainingTokens = totalTokens;
|
|
306
|
+
const created = [];
|
|
307
|
+
const compactedDates = [];
|
|
308
|
+
for (const weekKey of sortedWeeks) {
|
|
309
|
+
if (remainingTokens <= budgetTokens)
|
|
310
|
+
break;
|
|
311
|
+
// Skip weeks that already have a (non-partial) weekly summary
|
|
312
|
+
if (existingWeeklies.has(weekKey))
|
|
313
|
+
continue;
|
|
314
|
+
const weekDailies = groups.get(weekKey);
|
|
315
|
+
const weekTokens = weekDailies.reduce((sum, d) => sum + estimateTokens(`### ${d.date}\n${d.content}`), 0);
|
|
316
|
+
// Mark as partial if this is the current week
|
|
317
|
+
const isPartial = weekKey === currentWeek;
|
|
318
|
+
await mkdir(weeklyDir, { recursive: true });
|
|
319
|
+
const summary = buildWeeklySummary(weekKey, weekDailies, isPartial);
|
|
320
|
+
await writeFile(join(weeklyDir, `${weekKey}.md`), summary, "utf-8");
|
|
321
|
+
created.push(`${weekKey}.md${isPartial ? " (partial)" : ""}`);
|
|
322
|
+
for (const d of weekDailies) {
|
|
323
|
+
compactedDates.push(d.date);
|
|
324
|
+
}
|
|
325
|
+
remainingTokens -= weekTokens;
|
|
326
|
+
}
|
|
327
|
+
if (created.length === 0)
|
|
328
|
+
return null;
|
|
329
|
+
return { created, compactedDates };
|
|
330
|
+
}
|
|
185
331
|
/**
|
|
186
332
|
* Compact weekly summaries older than 52 weeks into monthly summaries.
|
|
187
333
|
*/
|
|
@@ -364,3 +510,27 @@ export async function buildWisdomPrompt(teammateDir, teammateName) {
|
|
|
364
510
|
parts.push(`Read your current WISDOM.md at \`.teammates/${teammateName}/WISDOM.md\` and rewrite it with updated, distilled entries. Write the file directly — this is the one time you are allowed to edit WISDOM.md.`);
|
|
365
511
|
return parts.join("\n");
|
|
366
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Purge daily logs older than DAILY_LOG_RETENTION_DAYS from disk.
|
|
515
|
+
* Returns the list of deleted filenames.
|
|
516
|
+
*/
|
|
517
|
+
export async function purgeStaleDailies(teammateDir) {
|
|
518
|
+
const memoryDir = join(teammateDir, "memory");
|
|
519
|
+
const cutoff = new Date();
|
|
520
|
+
cutoff.setDate(cutoff.getDate() - DAILY_LOG_RETENTION_DAYS);
|
|
521
|
+
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
|
522
|
+
const entries = await readdir(memoryDir).catch(() => []);
|
|
523
|
+
const purged = [];
|
|
524
|
+
for (const entry of entries) {
|
|
525
|
+
if (!entry.endsWith(".md"))
|
|
526
|
+
continue;
|
|
527
|
+
const stem = basename(entry, ".md");
|
|
528
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(stem))
|
|
529
|
+
continue;
|
|
530
|
+
if (stem < cutoffStr) {
|
|
531
|
+
await unlink(join(memoryDir, entry)).catch(() => { });
|
|
532
|
+
purged.push(entry);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return purged;
|
|
536
|
+
}
|
package/dist/compact.test.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { compactDailies, compactEpisodic, compactWeeklies } from "./compact.js";
|
|
5
|
+
import { DAILY_LOG_RETENTION_DAYS, autoCompactForBudget, buildWisdomPrompt, compactDailies, compactEpisodic, compactWeeklies, purgeStaleDailies, } from "./compact.js";
|
|
6
6
|
let testDir;
|
|
7
7
|
beforeEach(async () => {
|
|
8
8
|
testDir = join(tmpdir(), `compact-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
@@ -24,10 +24,8 @@ describe("compactDailies", () => {
|
|
|
24
24
|
const result = await compactDailies(testDir);
|
|
25
25
|
expect(result.created).toHaveLength(1);
|
|
26
26
|
expect(result.created[0]).toBe("2024-W10.md");
|
|
27
|
-
|
|
28
|
-
expect(result.removed).
|
|
29
|
-
expect(result.removed).toContain("2024-03-06.md");
|
|
30
|
-
expect(result.removed).toContain("2024-03-07.md");
|
|
27
|
+
// Daily logs are no longer deleted during compaction (kept 30 days)
|
|
28
|
+
expect(result.removed).toHaveLength(0);
|
|
31
29
|
// Verify the weekly summary was actually written
|
|
32
30
|
const weeklyDir = join(memDir, "weekly");
|
|
33
31
|
const weeklyFiles = await readdir(weeklyDir);
|
|
@@ -57,7 +55,8 @@ describe("compactDailies", () => {
|
|
|
57
55
|
await writeFile(join(memDir, "2024-03-13.md"), "# W11 day2");
|
|
58
56
|
const result = await compactDailies(testDir);
|
|
59
57
|
expect(result.created).toHaveLength(2);
|
|
60
|
-
|
|
58
|
+
// Daily logs are no longer deleted during compaction (kept 30 days)
|
|
59
|
+
expect(result.removed).toHaveLength(0);
|
|
61
60
|
});
|
|
62
61
|
it("skips weeks that already have a weekly summary", async () => {
|
|
63
62
|
const memDir = join(testDir, "memory");
|
|
@@ -181,7 +180,8 @@ describe("compactEpisodic", () => {
|
|
|
181
180
|
const result = await compactEpisodic(testDir, "test");
|
|
182
181
|
expect(result.teammate).toBe("test");
|
|
183
182
|
expect(result.weekliesCreated).toHaveLength(1);
|
|
184
|
-
|
|
183
|
+
// Daily logs are no longer deleted during compaction (kept 30 days)
|
|
184
|
+
expect(result.dailiesRemoved).toHaveLength(0);
|
|
185
185
|
// The newly created weekly (2024-W10) is >52 weeks old, so compactWeeklies
|
|
186
186
|
// immediately compacts it into a monthly summary
|
|
187
187
|
expect(result.monthliesCreated).toHaveLength(1);
|
|
@@ -196,3 +196,319 @@ describe("compactEpisodic", () => {
|
|
|
196
196
|
expect(result.weekliesRemoved).toEqual([]);
|
|
197
197
|
});
|
|
198
198
|
});
|
|
199
|
+
describe("buildWisdomPrompt", () => {
|
|
200
|
+
it("returns null when no typed memories or daily logs exist", async () => {
|
|
201
|
+
const memDir = join(testDir, "memory");
|
|
202
|
+
await mkdir(memDir, { recursive: true });
|
|
203
|
+
const result = await buildWisdomPrompt(testDir, "test");
|
|
204
|
+
expect(result).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
it("returns null when memory dir does not exist", async () => {
|
|
207
|
+
const result = await buildWisdomPrompt(testDir, "test");
|
|
208
|
+
expect(result).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
it("includes typed memory files in the prompt", async () => {
|
|
211
|
+
const memDir = join(testDir, "memory");
|
|
212
|
+
await mkdir(memDir, { recursive: true });
|
|
213
|
+
await writeFile(join(memDir, "feedback_testing.md"), "---\nname: Testing feedback\ntype: feedback\n---\nAlways run tests before committing.");
|
|
214
|
+
const result = await buildWisdomPrompt(testDir, "beacon");
|
|
215
|
+
expect(result).not.toBeNull();
|
|
216
|
+
expect(result).toContain("## Typed Memories");
|
|
217
|
+
expect(result).toContain("feedback_testing.md");
|
|
218
|
+
expect(result).toContain("Always run tests before committing.");
|
|
219
|
+
});
|
|
220
|
+
it("includes recent daily logs in the prompt", async () => {
|
|
221
|
+
const memDir = join(testDir, "memory");
|
|
222
|
+
await mkdir(memDir, { recursive: true });
|
|
223
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
224
|
+
await writeFile(join(memDir, `${today}.md`), "# Today\n\nDid some work.");
|
|
225
|
+
const result = await buildWisdomPrompt(testDir, "beacon");
|
|
226
|
+
expect(result).not.toBeNull();
|
|
227
|
+
expect(result).toContain("## Recent Daily Logs");
|
|
228
|
+
expect(result).toContain("Did some work.");
|
|
229
|
+
});
|
|
230
|
+
it("includes current WISDOM.md in the prompt when it exists", async () => {
|
|
231
|
+
const memDir = join(testDir, "memory");
|
|
232
|
+
await mkdir(memDir, { recursive: true });
|
|
233
|
+
await writeFile(join(testDir, "WISDOM.md"), "# Beacon — Wisdom\n\n### Important pattern\nAlways check types.");
|
|
234
|
+
await writeFile(join(memDir, "decision_types.md"), "---\nname: Type checking\ntype: decision\n---\nUse strict mode.");
|
|
235
|
+
const result = await buildWisdomPrompt(testDir, "beacon");
|
|
236
|
+
expect(result).not.toBeNull();
|
|
237
|
+
expect(result).toContain("## Current WISDOM.md");
|
|
238
|
+
expect(result).toContain("Important pattern");
|
|
239
|
+
});
|
|
240
|
+
it("skips daily log files from typed memories", async () => {
|
|
241
|
+
const memDir = join(testDir, "memory");
|
|
242
|
+
await mkdir(memDir, { recursive: true });
|
|
243
|
+
await writeFile(join(memDir, "2026-03-20.md"), "# Daily log content");
|
|
244
|
+
await writeFile(join(memDir, "feedback_test.md"), "---\nname: Test\ntype: feedback\n---\nSome feedback.");
|
|
245
|
+
const result = await buildWisdomPrompt(testDir, "beacon");
|
|
246
|
+
expect(result).not.toBeNull();
|
|
247
|
+
// Typed memories should NOT include the daily log
|
|
248
|
+
const typedSection = result.indexOf("## Typed Memories");
|
|
249
|
+
const dailySection = result.indexOf("## Recent Daily Logs");
|
|
250
|
+
if (typedSection >= 0) {
|
|
251
|
+
const typedContent = result.slice(typedSection, dailySection > typedSection ? dailySection : undefined);
|
|
252
|
+
expect(typedContent).not.toContain("2026-03-20.md");
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
it("includes distillation rules", async () => {
|
|
256
|
+
const memDir = join(testDir, "memory");
|
|
257
|
+
await mkdir(memDir, { recursive: true });
|
|
258
|
+
await writeFile(join(memDir, "ref_test.md"), "---\nname: Ref\ntype: reference\n---\nA reference.");
|
|
259
|
+
const result = await buildWisdomPrompt(testDir, "beacon");
|
|
260
|
+
expect(result).not.toBeNull();
|
|
261
|
+
expect(result).toContain("## Rules");
|
|
262
|
+
expect(result).toContain("distilled principles");
|
|
263
|
+
expect(result).toContain("Last compacted:");
|
|
264
|
+
});
|
|
265
|
+
it("limits daily logs to 7 most recent", async () => {
|
|
266
|
+
const memDir = join(testDir, "memory");
|
|
267
|
+
await mkdir(memDir, { recursive: true });
|
|
268
|
+
// Create 10 daily logs
|
|
269
|
+
for (let i = 1; i <= 10; i++) {
|
|
270
|
+
const day = i.toString().padStart(2, "0");
|
|
271
|
+
await writeFile(join(memDir, `2026-03-${day}.md`), `# Day ${i}`);
|
|
272
|
+
}
|
|
273
|
+
const result = await buildWisdomPrompt(testDir, "beacon");
|
|
274
|
+
expect(result).not.toBeNull();
|
|
275
|
+
// Should include most recent 7, not all 10
|
|
276
|
+
// Count the day headers in the Recent Daily Logs section
|
|
277
|
+
const dailySection = result.slice(result.indexOf("## Recent Daily Logs"));
|
|
278
|
+
const dayHeaders = dailySection.match(/### 2026-03-\d{2}/g);
|
|
279
|
+
expect(dayHeaders).toHaveLength(7);
|
|
280
|
+
});
|
|
281
|
+
it("includes teammate name in instructions", async () => {
|
|
282
|
+
const memDir = join(testDir, "memory");
|
|
283
|
+
await mkdir(memDir, { recursive: true });
|
|
284
|
+
await writeFile(join(memDir, "ref_x.md"), "---\nname: X\ntype: reference\n---\nContent.");
|
|
285
|
+
const result = await buildWisdomPrompt(testDir, "mybot");
|
|
286
|
+
expect(result).not.toBeNull();
|
|
287
|
+
expect(result).toContain(".teammates/mybot/WISDOM.md");
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
describe("autoCompactForBudget", () => {
|
|
291
|
+
it("returns null when daily logs are under budget", async () => {
|
|
292
|
+
const memDir = join(testDir, "memory");
|
|
293
|
+
await mkdir(memDir, { recursive: true });
|
|
294
|
+
// Write a small daily log from a past week
|
|
295
|
+
await writeFile(join(memDir, "2024-03-05.md"), "# Short log");
|
|
296
|
+
const result = await autoCompactForBudget(testDir, 100_000);
|
|
297
|
+
expect(result).toBeNull();
|
|
298
|
+
});
|
|
299
|
+
it("compacts oldest weeks when over budget", async () => {
|
|
300
|
+
const memDir = join(testDir, "memory");
|
|
301
|
+
await mkdir(memDir, { recursive: true });
|
|
302
|
+
// Create large daily logs in two past weeks (W10 and W11)
|
|
303
|
+
const bigContent = "x".repeat(50_000); // ~12,500 tokens each
|
|
304
|
+
await writeFile(join(memDir, "2024-03-05.md"), bigContent); // W10
|
|
305
|
+
await writeFile(join(memDir, "2024-03-06.md"), bigContent); // W10
|
|
306
|
+
await writeFile(join(memDir, "2024-03-12.md"), bigContent); // W11
|
|
307
|
+
await writeFile(join(memDir, "2024-03-13.md"), bigContent); // W11
|
|
308
|
+
// Budget that fits ~2 logs but not 4
|
|
309
|
+
const result = await autoCompactForBudget(testDir, 30_000);
|
|
310
|
+
expect(result).not.toBeNull();
|
|
311
|
+
expect(result.created.length).toBeGreaterThanOrEqual(1);
|
|
312
|
+
// Oldest week (W10) should be compacted first
|
|
313
|
+
expect(result.created[0]).toContain("2024-W10");
|
|
314
|
+
expect(result.compactedDates).toContain("2024-03-05");
|
|
315
|
+
expect(result.compactedDates).toContain("2024-03-06");
|
|
316
|
+
// Verify weekly file was written
|
|
317
|
+
const weeklyDir = join(memDir, "weekly");
|
|
318
|
+
const files = await readdir(weeklyDir);
|
|
319
|
+
expect(files).toContain("2024-W10.md");
|
|
320
|
+
});
|
|
321
|
+
it("marks current week compaction as partial", async () => {
|
|
322
|
+
const memDir = join(testDir, "memory");
|
|
323
|
+
await mkdir(memDir, { recursive: true });
|
|
324
|
+
// Create a large daily log in a past week and one for today's week
|
|
325
|
+
const bigContent = "x".repeat(100_000); // ~25,000 tokens
|
|
326
|
+
await writeFile(join(memDir, "2024-03-05.md"), bigContent); // W10
|
|
327
|
+
// Create a log in current week (not today specifically)
|
|
328
|
+
const now = new Date();
|
|
329
|
+
const yesterday = new Date(now);
|
|
330
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
331
|
+
const yesterdayStr = yesterday.toISOString().slice(0, 10);
|
|
332
|
+
await writeFile(join(memDir, `${yesterdayStr}.md`), bigContent);
|
|
333
|
+
// Very tight budget forces compacting both weeks
|
|
334
|
+
const result = await autoCompactForBudget(testDir, 1_000);
|
|
335
|
+
expect(result).not.toBeNull();
|
|
336
|
+
// Check if any entry is marked partial (current week)
|
|
337
|
+
const weeklyDir = join(memDir, "weekly");
|
|
338
|
+
const files = await readdir(weeklyDir);
|
|
339
|
+
let hasPartial = false;
|
|
340
|
+
for (const f of files) {
|
|
341
|
+
const content = await readFile(join(weeklyDir, f), "utf-8");
|
|
342
|
+
if (content.includes("partial: true")) {
|
|
343
|
+
hasPartial = true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// If yesterday is in a different week than today, there may not be a partial
|
|
347
|
+
// But W10 should always be compacted
|
|
348
|
+
expect(result.created.some((c) => c.includes("2024-W10"))).toBe(true);
|
|
349
|
+
// If the current week was compacted, it should be partial
|
|
350
|
+
const currentWeekCompacted = result.created.find((c) => c.includes("(partial)"));
|
|
351
|
+
if (currentWeekCompacted) {
|
|
352
|
+
expect(hasPartial).toBe(true);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
it("skips today's log when calculating budget", async () => {
|
|
356
|
+
const memDir = join(testDir, "memory");
|
|
357
|
+
await mkdir(memDir, { recursive: true });
|
|
358
|
+
// Write today's log (should not be compacted)
|
|
359
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
360
|
+
const bigContent = "x".repeat(200_000);
|
|
361
|
+
await writeFile(join(memDir, `${today}.md`), bigContent);
|
|
362
|
+
// Even with a tiny budget, today should not trigger compaction
|
|
363
|
+
const result = await autoCompactForBudget(testDir, 1_000);
|
|
364
|
+
// No past dailies to compact, so result should be null
|
|
365
|
+
expect(result).toBeNull();
|
|
366
|
+
});
|
|
367
|
+
it("returns null when memory dir does not exist", async () => {
|
|
368
|
+
const result = await autoCompactForBudget(testDir, 24_000);
|
|
369
|
+
expect(result).toBeNull();
|
|
370
|
+
});
|
|
371
|
+
it("does not compact weeks that already have a weekly summary", async () => {
|
|
372
|
+
const memDir = join(testDir, "memory");
|
|
373
|
+
const weeklyDir = join(memDir, "weekly");
|
|
374
|
+
await mkdir(weeklyDir, { recursive: true });
|
|
375
|
+
const bigContent = "x".repeat(100_000);
|
|
376
|
+
await writeFile(join(memDir, "2024-03-05.md"), bigContent); // W10
|
|
377
|
+
await writeFile(join(memDir, "2024-03-12.md"), bigContent); // W11
|
|
378
|
+
// Pre-existing weekly for W10
|
|
379
|
+
await writeFile(join(weeklyDir, "2024-W10.md"), "# Already compacted");
|
|
380
|
+
const result = await autoCompactForBudget(testDir, 1_000);
|
|
381
|
+
expect(result).not.toBeNull();
|
|
382
|
+
// Should compact W11, not W10
|
|
383
|
+
expect(result.created.some((c) => c.includes("2024-W11"))).toBe(true);
|
|
384
|
+
expect(result.created.some((c) => c.includes("2024-W10"))).toBe(false);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
describe("compactDailies — partial merge", () => {
|
|
388
|
+
it("merges new dailies into a partial weekly", async () => {
|
|
389
|
+
const memDir = join(testDir, "memory");
|
|
390
|
+
const weeklyDir = join(memDir, "weekly");
|
|
391
|
+
await mkdir(weeklyDir, { recursive: true });
|
|
392
|
+
// Create a partial weekly for W10 with only 2024-03-05
|
|
393
|
+
const partialContent = [
|
|
394
|
+
"---",
|
|
395
|
+
"type: weekly",
|
|
396
|
+
"week: 2024-W10",
|
|
397
|
+
"period: 2024-03-05 to 2024-03-05",
|
|
398
|
+
"partial: true",
|
|
399
|
+
"---",
|
|
400
|
+
"",
|
|
401
|
+
"# Week 2024-W10",
|
|
402
|
+
"",
|
|
403
|
+
"## 2024-03-05",
|
|
404
|
+
"",
|
|
405
|
+
"# Tuesday work",
|
|
406
|
+
"",
|
|
407
|
+
].join("\n");
|
|
408
|
+
await writeFile(join(weeklyDir, "2024-W10.md"), partialContent);
|
|
409
|
+
// Add new dailies for the same week
|
|
410
|
+
await writeFile(join(memDir, "2024-03-06.md"), "# Wednesday work");
|
|
411
|
+
await writeFile(join(memDir, "2024-03-07.md"), "# Thursday work");
|
|
412
|
+
const result = await compactDailies(testDir);
|
|
413
|
+
expect(result.created).toHaveLength(1);
|
|
414
|
+
expect(result.created[0]).toBe("2024-W10.md (merged)");
|
|
415
|
+
// Verify the merged weekly has all 3 days and no partial flag
|
|
416
|
+
const content = await readFile(join(weeklyDir, "2024-W10.md"), "utf-8");
|
|
417
|
+
expect(content).toContain("2024-03-05");
|
|
418
|
+
expect(content).toContain("2024-03-06");
|
|
419
|
+
expect(content).toContain("2024-03-07");
|
|
420
|
+
expect(content).toContain("Tuesday work");
|
|
421
|
+
expect(content).toContain("Wednesday work");
|
|
422
|
+
expect(content).toContain("Thursday work");
|
|
423
|
+
expect(content).not.toContain("partial: true");
|
|
424
|
+
});
|
|
425
|
+
it("does not duplicate dates when merging", async () => {
|
|
426
|
+
const memDir = join(testDir, "memory");
|
|
427
|
+
const weeklyDir = join(memDir, "weekly");
|
|
428
|
+
await mkdir(weeklyDir, { recursive: true });
|
|
429
|
+
// Partial weekly with 2024-03-05
|
|
430
|
+
const partialContent = [
|
|
431
|
+
"---",
|
|
432
|
+
"type: weekly",
|
|
433
|
+
"week: 2024-W10",
|
|
434
|
+
"period: 2024-03-05 to 2024-03-05",
|
|
435
|
+
"partial: true",
|
|
436
|
+
"---",
|
|
437
|
+
"",
|
|
438
|
+
"# Week 2024-W10",
|
|
439
|
+
"",
|
|
440
|
+
"## 2024-03-05",
|
|
441
|
+
"",
|
|
442
|
+
"# Original Tuesday",
|
|
443
|
+
"",
|
|
444
|
+
].join("\n");
|
|
445
|
+
await writeFile(join(weeklyDir, "2024-W10.md"), partialContent);
|
|
446
|
+
// Same date exists as a daily log (shouldn't duplicate)
|
|
447
|
+
await writeFile(join(memDir, "2024-03-05.md"), "# Updated Tuesday");
|
|
448
|
+
await writeFile(join(memDir, "2024-03-06.md"), "# Wednesday");
|
|
449
|
+
const result = await compactDailies(testDir);
|
|
450
|
+
const content = await readFile(join(weeklyDir, "2024-W10.md"), "utf-8");
|
|
451
|
+
// Should contain the original (from partial), not duplicated
|
|
452
|
+
const tuesdayMatches = content.match(/## 2024-03-05/g);
|
|
453
|
+
expect(tuesdayMatches).toHaveLength(1);
|
|
454
|
+
});
|
|
455
|
+
it("skips non-partial existing weeklies", async () => {
|
|
456
|
+
const memDir = join(testDir, "memory");
|
|
457
|
+
const weeklyDir = join(memDir, "weekly");
|
|
458
|
+
await mkdir(weeklyDir, { recursive: true });
|
|
459
|
+
// Complete (non-partial) weekly
|
|
460
|
+
const completeContent = [
|
|
461
|
+
"---",
|
|
462
|
+
"type: weekly",
|
|
463
|
+
"week: 2024-W10",
|
|
464
|
+
"period: 2024-03-05 to 2024-03-07",
|
|
465
|
+
"---",
|
|
466
|
+
"",
|
|
467
|
+
"# Week 2024-W10",
|
|
468
|
+
"",
|
|
469
|
+
"## 2024-03-05",
|
|
470
|
+
"",
|
|
471
|
+
"# Content",
|
|
472
|
+
"",
|
|
473
|
+
].join("\n");
|
|
474
|
+
await writeFile(join(weeklyDir, "2024-W10.md"), completeContent);
|
|
475
|
+
await writeFile(join(memDir, "2024-03-06.md"), "# New daily");
|
|
476
|
+
const result = await compactDailies(testDir);
|
|
477
|
+
expect(result.created).toHaveLength(0);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
describe("purgeStaleDailies", () => {
|
|
481
|
+
it("deletes daily logs older than retention period", async () => {
|
|
482
|
+
const memDir = join(testDir, "memory");
|
|
483
|
+
await mkdir(memDir, { recursive: true });
|
|
484
|
+
// Create a daily log well past the retention period
|
|
485
|
+
const old = new Date();
|
|
486
|
+
old.setDate(old.getDate() - DAILY_LOG_RETENTION_DAYS - 5);
|
|
487
|
+
const oldDate = old.toISOString().slice(0, 10);
|
|
488
|
+
await writeFile(join(memDir, `${oldDate}.md`), "# Old log");
|
|
489
|
+
// Create a recent daily log
|
|
490
|
+
const recent = new Date();
|
|
491
|
+
recent.setDate(recent.getDate() - 2);
|
|
492
|
+
const recentDate = recent.toISOString().slice(0, 10);
|
|
493
|
+
await writeFile(join(memDir, `${recentDate}.md`), "# Recent log");
|
|
494
|
+
// Create a typed memory (should not be touched)
|
|
495
|
+
await writeFile(join(memDir, "feedback_test.md"), "# Feedback");
|
|
496
|
+
const purged = await purgeStaleDailies(testDir);
|
|
497
|
+
expect(purged).toContain(`${oldDate}.md`);
|
|
498
|
+
expect(purged).not.toContain(`${recentDate}.md`);
|
|
499
|
+
expect(purged).not.toContain("feedback_test.md");
|
|
500
|
+
// Verify the old file is gone and others remain
|
|
501
|
+
const remaining = await readdir(memDir);
|
|
502
|
+
expect(remaining).not.toContain(`${oldDate}.md`);
|
|
503
|
+
expect(remaining).toContain(`${recentDate}.md`);
|
|
504
|
+
expect(remaining).toContain("feedback_test.md");
|
|
505
|
+
});
|
|
506
|
+
it("returns empty array when no stale logs exist", async () => {
|
|
507
|
+
const memDir = join(testDir, "memory");
|
|
508
|
+
await mkdir(memDir, { recursive: true });
|
|
509
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
510
|
+
await writeFile(join(memDir, `${today}.md`), "# Today");
|
|
511
|
+
const purged = await purgeStaleDailies(testDir);
|
|
512
|
+
expect(purged).toHaveLength(0);
|
|
513
|
+
});
|
|
514
|
+
});
|