@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.
Files changed (47) hide show
  1. package/README.md +36 -4
  2. package/dist/adapter.d.ts +19 -3
  3. package/dist/adapter.js +168 -96
  4. package/dist/adapter.test.js +29 -16
  5. package/dist/adapters/cli-proxy.d.ts +3 -1
  6. package/dist/adapters/cli-proxy.js +65 -6
  7. package/dist/adapters/copilot.d.ts +3 -1
  8. package/dist/adapters/copilot.js +16 -3
  9. package/dist/adapters/echo.d.ts +3 -1
  10. package/dist/adapters/echo.js +4 -2
  11. package/dist/banner.js +5 -1
  12. package/dist/cli-args.js +23 -23
  13. package/dist/cli-args.test.d.ts +1 -0
  14. package/dist/cli-args.test.js +125 -0
  15. package/dist/cli.js +486 -220
  16. package/dist/compact.d.ts +23 -0
  17. package/dist/compact.js +181 -11
  18. package/dist/compact.test.js +323 -7
  19. package/dist/index.d.ts +4 -1
  20. package/dist/index.js +3 -1
  21. package/dist/onboard.js +165 -165
  22. package/dist/orchestrator.js +7 -2
  23. package/dist/personas.d.ts +42 -0
  24. package/dist/personas.js +108 -0
  25. package/dist/personas.test.d.ts +1 -0
  26. package/dist/personas.test.js +88 -0
  27. package/dist/registry.test.js +23 -23
  28. package/dist/theme.test.d.ts +1 -0
  29. package/dist/theme.test.js +113 -0
  30. package/dist/types.d.ts +2 -0
  31. package/package.json +4 -3
  32. package/personas/architect.md +95 -0
  33. package/personas/backend.md +97 -0
  34. package/personas/data-engineer.md +96 -0
  35. package/personas/designer.md +96 -0
  36. package/personas/devops.md +97 -0
  37. package/personas/frontend.md +98 -0
  38. package/personas/ml-ai.md +100 -0
  39. package/personas/mobile.md +97 -0
  40. package/personas/performance.md +96 -0
  41. package/personas/pm.md +93 -0
  42. package/personas/prompt-engineer.md +122 -0
  43. package/personas/qa.md +96 -0
  44. package/personas/security.md +96 -0
  45. package/personas/sre.md +97 -0
  46. package/personas/swe.md +92 -0
  47. 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:00Z`);
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
- existingWeeklies.add(basename(e, ".md"));
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
- // Skip if weekly summary already exists
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
- // Delete the daily logs that were compacted
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
+ }
@@ -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
- expect(result.removed).toHaveLength(3);
28
- expect(result.removed).toContain("2024-03-05.md");
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
- expect(result.removed).toHaveLength(4);
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
- expect(result.dailiesRemoved).toHaveLength(2);
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
+ });