@teammates/cli 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -19,9 +19,10 @@ import { DAILY_LOG_BUDGET_TOKENS, syncRecallIndex } from "./adapter.js";
19
19
  import { AnimatedBanner, } from "./banner.js";
20
20
  import { findTeammatesDir, PKG_VERSION, parseCliArgs, printUsage, resolveAdapter, } from "./cli-args.js";
21
21
  import { buildConversationContext as buildConvCtx, buildSummarizationPrompt, cleanResponseBody, findAtMention, findSummarizationSplit, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
22
- import { autoCompactForBudget, buildWisdomPrompt, compactEpisodic, purgeStaleDailies, } from "./compact.js";
22
+ import { autoCompactForBudget, buildDailyCompressionPrompt, buildMigrationCompressionPrompt, buildWisdomPrompt, compactEpisodic, findUncompressedDailies, purgeStaleDailies, } from "./compact.js";
23
23
  import { PromptInput } from "./console/prompt-input.js";
24
24
  import { buildTitle } from "./console/startup.js";
25
+ import { buildConversationLog } from "./log-parser.js";
25
26
  import { buildImportAdaptationPrompt, copyTemplateFiles, getOnboardingPrompt, importTeammates, } from "./onboard.js";
26
27
  import { Orchestrator } from "./orchestrator.js";
27
28
  import { loadPersonas, scaffoldFromPersona } from "./personas.js";
@@ -154,8 +155,20 @@ class TeammatesREPL {
154
155
  }
155
156
  /** Token budget for recent conversation history (24k tokens ≈ 96k chars). */
156
157
  static CONV_HISTORY_CHARS = 24_000 * 4;
158
+ /** Threshold (chars) above which conversation history is written to a temp file. */
159
+ static CONV_FILE_THRESHOLD = 8_000;
157
160
  buildConversationContext() {
158
- return buildConvCtx(this.conversationHistory, this.conversationSummary, TeammatesREPL.CONV_HISTORY_CHARS);
161
+ const context = buildConvCtx(this.conversationHistory, this.conversationSummary, TeammatesREPL.CONV_HISTORY_CHARS);
162
+ // If the conversation context is very long, write it to a temp file
163
+ // and return a pointer instead — reduces inline prompt noise.
164
+ if (context.length > TeammatesREPL.CONV_FILE_THRESHOLD) {
165
+ const tmpDir = join(this.teammatesDir, ".tmp");
166
+ mkdirSync(tmpDir, { recursive: true });
167
+ const convFile = join(tmpDir, "conversation.md");
168
+ writeFileSync(convFile, context, "utf-8");
169
+ return `## Conversation History\n\nThe full conversation history is in \`.teammates/.tmp/conversation.md\`. Read it before responding to understand prior context.`;
170
+ }
171
+ return context;
159
172
  }
160
173
  /**
161
174
  * Check if conversation history exceeds the 24k token budget.
@@ -187,6 +200,8 @@ class TeammatesREPL {
187
200
  systemActive = new Map();
188
201
  /** Agents currently in a silent retry — suppress all events. */
189
202
  silentAgents = new Set();
203
+ /** Counter for pending migration compression tasks — triggers re-index when it hits 0. */
204
+ pendingMigrationSyncs = 0;
190
205
  /** Per-agent drain locks — prevents double-draining a single agent. */
191
206
  agentDrainLocks = new Map();
192
207
  /** Stored pasted text keyed by paste number, expanded on Enter. */
@@ -208,6 +223,8 @@ class TeammatesREPL {
208
223
  pendingHandoffs = [];
209
224
  /** Pending retro proposals awaiting user approval. */
210
225
  pendingRetroProposals = [];
226
+ /** Pending cross-folder violations awaiting user decision. */
227
+ pendingViolations = [];
211
228
  /** Maps reply action IDs to their context (teammate + message). */
212
229
  _replyContexts = new Map();
213
230
  /** Quoted reply text to expand on next submit. */
@@ -727,6 +744,135 @@ class TeammatesREPL {
727
744
  return;
728
745
  }
729
746
  }
747
+ /**
748
+ * Audit a task result for cross-folder writes.
749
+ * AI teammates must not write to another teammate's folder.
750
+ * Returns violating file paths (relative), or empty array if clean.
751
+ */
752
+ auditCrossFolderWrites(teammate, changedFiles) {
753
+ // Normalize .teammates/ prefix for comparison
754
+ const tmPrefix = ".teammates/";
755
+ const ownPrefix = `${tmPrefix}${teammate}/`;
756
+ return changedFiles.filter((f) => {
757
+ const normalized = f.replace(/\\/g, "/");
758
+ // Only care about files inside .teammates/
759
+ if (!normalized.startsWith(tmPrefix))
760
+ return false;
761
+ // Own folder is fine
762
+ if (normalized.startsWith(ownPrefix))
763
+ return false;
764
+ // Shared folders (_prefix) are fine
765
+ const subPath = normalized.slice(tmPrefix.length);
766
+ if (subPath.startsWith("_"))
767
+ return false;
768
+ // Ephemeral folders (.prefix) are fine
769
+ if (subPath.startsWith("."))
770
+ return false;
771
+ // Root-level shared files (USER.md, settings.json, CROSS-TEAM.md, etc.)
772
+ if (!subPath.includes("/"))
773
+ return false;
774
+ // Everything else is a violation
775
+ return true;
776
+ });
777
+ }
778
+ /**
779
+ * Show cross-folder violation warning with [revert] / [allow] actions.
780
+ */
781
+ showViolationWarning(teammate, violations) {
782
+ const t = theme();
783
+ this.feedLine(tp.warning(` ⚠ @${teammate} wrote to another teammate's folder:`));
784
+ for (const f of violations) {
785
+ this.feedLine(tp.muted(` ${f}`));
786
+ }
787
+ if (this.chatView) {
788
+ const violationId = `violation-${Date.now()}`;
789
+ const actionIdx = this.chatView.feedLineCount;
790
+ this.chatView.appendActionList([
791
+ {
792
+ id: `revert-${violationId}`,
793
+ normalStyle: this.makeSpan({
794
+ text: " [revert]",
795
+ style: { fg: t.error },
796
+ }),
797
+ hoverStyle: this.makeSpan({
798
+ text: " [revert]",
799
+ style: { fg: t.accent },
800
+ }),
801
+ },
802
+ {
803
+ id: `allow-${violationId}`,
804
+ normalStyle: this.makeSpan({
805
+ text: " [allow]",
806
+ style: { fg: t.textDim },
807
+ }),
808
+ hoverStyle: this.makeSpan({
809
+ text: " [allow]",
810
+ style: { fg: t.accent },
811
+ }),
812
+ },
813
+ ]);
814
+ this.pendingViolations.push({
815
+ id: violationId,
816
+ teammate,
817
+ files: violations,
818
+ actionIdx,
819
+ });
820
+ }
821
+ }
822
+ /**
823
+ * Handle revert/allow actions for cross-folder violations.
824
+ */
825
+ handleViolationAction(actionId) {
826
+ const revertMatch = actionId.match(/^revert-(violation-.+)$/);
827
+ if (revertMatch) {
828
+ const vId = revertMatch[1];
829
+ const idx = this.pendingViolations.findIndex((v) => v.id === vId);
830
+ if (idx >= 0 && this.chatView) {
831
+ const v = this.pendingViolations.splice(idx, 1)[0];
832
+ // Revert violating files via git checkout
833
+ for (const f of v.files) {
834
+ try {
835
+ execSync(`git checkout -- "${f}"`, {
836
+ cwd: resolve(this.teammatesDir, ".."),
837
+ stdio: "pipe",
838
+ });
839
+ }
840
+ catch {
841
+ // File might be untracked — try git rm
842
+ try {
843
+ execSync(`git rm -f "${f}"`, {
844
+ cwd: resolve(this.teammatesDir, ".."),
845
+ stdio: "pipe",
846
+ });
847
+ }
848
+ catch {
849
+ // Best effort — file may already be clean
850
+ }
851
+ }
852
+ }
853
+ this.chatView.updateFeedLine(v.actionIdx, this.makeSpan({
854
+ text: ` reverted ${v.files.length} file(s)`,
855
+ style: { fg: theme().success },
856
+ }));
857
+ this.refreshView();
858
+ }
859
+ return;
860
+ }
861
+ const allowMatch = actionId.match(/^allow-(violation-.+)$/);
862
+ if (allowMatch) {
863
+ const vId = allowMatch[1];
864
+ const idx = this.pendingViolations.findIndex((v) => v.id === vId);
865
+ if (idx >= 0 && this.chatView) {
866
+ const v = this.pendingViolations.splice(idx, 1)[0];
867
+ this.chatView.updateFeedLine(v.actionIdx, this.makeSpan({
868
+ text: " allowed",
869
+ style: { fg: theme().textDim },
870
+ }));
871
+ this.refreshView();
872
+ }
873
+ return;
874
+ }
875
+ }
730
876
  /** Handle bulk handoff actions. */
731
877
  handleBulkHandoff(action) {
732
878
  if (!this.chatView)
@@ -1113,6 +1259,20 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1113
1259
  }
1114
1260
  finally {
1115
1261
  this.systemActive.delete(taskId);
1262
+ // Migration tasks: decrement counter and re-index when all are done
1263
+ if (entry.type === "agent" && entry.migration) {
1264
+ this.pendingMigrationSyncs--;
1265
+ if (this.pendingMigrationSyncs <= 0) {
1266
+ try {
1267
+ await syncRecallIndex(this.teammatesDir);
1268
+ this.feedLine(tp.success(" ✔ v0.6.0 migration complete — indexes rebuilt"));
1269
+ this.refreshView();
1270
+ }
1271
+ catch {
1272
+ /* re-index failed — non-fatal, next startup will retry */
1273
+ }
1274
+ }
1275
+ }
1116
1276
  }
1117
1277
  }
1118
1278
  // ─── Onboarding ───────────────────────────────────────────────────
@@ -2550,6 +2710,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2550
2710
  id.startsWith("retro-reject-")) {
2551
2711
  this.handleRetroAction(id);
2552
2712
  }
2713
+ else if (id.startsWith("revert-") || id.startsWith("allow-")) {
2714
+ this.handleViolationAction(id);
2715
+ }
2553
2716
  else if (id.startsWith("approve-") || id.startsWith("reject-")) {
2554
2717
  this.handleHandoffAction(id);
2555
2718
  }
@@ -3046,6 +3209,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3046
3209
  description: "Cancel a queued task by number",
3047
3210
  run: (args) => this.cmdCancel(args),
3048
3211
  },
3212
+ {
3213
+ name: "interrupt",
3214
+ aliases: ["int"],
3215
+ usage: "/interrupt <teammate> [message]",
3216
+ description: "Interrupt a running agent and resume with a steering message",
3217
+ run: (args) => this.cmdInterrupt(args),
3218
+ },
3049
3219
  {
3050
3220
  name: "init",
3051
3221
  aliases: ["onboard", "setup"],
@@ -3365,6 +3535,114 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3365
3535
  this.feedLine(concat(tp.muted(" Cancelled: "), tp.accent(`@${cancelDisplay}`), tp.muted(" — "), tp.text(removed.task.slice(0, 60))));
3366
3536
  this.refreshView();
3367
3537
  }
3538
+ /**
3539
+ * /interrupt <teammate> [message] — Kill a running agent and resume with context.
3540
+ */
3541
+ async cmdInterrupt(argsStr) {
3542
+ const parts = argsStr.trim().split(/\s+/);
3543
+ const teammateName = parts[0]?.replace(/^@/, "").toLowerCase();
3544
+ const steeringMessage = parts.slice(1).join(" ").trim() ||
3545
+ "Wrap up your current work and report what you've done so far.";
3546
+ if (!teammateName) {
3547
+ this.feedLine(tp.warning(" Usage: /interrupt <teammate> [message]"));
3548
+ this.refreshView();
3549
+ return;
3550
+ }
3551
+ // Resolve display name → internal name
3552
+ const resolvedName = teammateName === this.adapterName ? this.selfName : teammateName;
3553
+ // Check if the teammate has an active task
3554
+ const activeEntry = this.agentActive.get(resolvedName);
3555
+ if (!activeEntry) {
3556
+ this.feedLine(tp.warning(` @${teammateName} has no active task to interrupt.`));
3557
+ this.refreshView();
3558
+ return;
3559
+ }
3560
+ // Check if the adapter supports killing
3561
+ const adapter = this.orchestrator.getAdapter();
3562
+ if (!adapter?.killAgent) {
3563
+ this.feedLine(tp.warning(" This adapter does not support interruption."));
3564
+ this.refreshView();
3565
+ return;
3566
+ }
3567
+ // Show interruption status
3568
+ const displayName = resolvedName === this.selfName ? this.adapterName : resolvedName;
3569
+ this.feedLine(concat(tp.warning(" ⚡ Interrupting "), tp.accent(`@${displayName}`), tp.warning("...")));
3570
+ this.refreshView();
3571
+ try {
3572
+ // Kill the agent process and capture its output
3573
+ const spawnResult = await adapter.killAgent(resolvedName);
3574
+ if (!spawnResult) {
3575
+ this.feedLine(tp.warning(` @${displayName} process already exited.`));
3576
+ this.refreshView();
3577
+ return;
3578
+ }
3579
+ // Get the original full prompt for this agent
3580
+ const _originalFullPrompt = this.lastTaskPrompts.get(resolvedName) ?? "";
3581
+ const originalTask = activeEntry.task;
3582
+ // Parse the conversation log from available sources
3583
+ const presetName = adapter.name ?? "unknown";
3584
+ const { log, toolCallCount, filesChanged } = buildConversationLog(spawnResult.debugFile, spawnResult.stdout, presetName);
3585
+ // Build the resume prompt
3586
+ const resumePrompt = this.buildResumePrompt(originalTask, log, steeringMessage, toolCallCount, filesChanged);
3587
+ // Report what happened
3588
+ const elapsed = this.activeTasks.get(resolvedName)?.startTime
3589
+ ? `${((Date.now() - this.activeTasks.get(resolvedName).startTime) / 1000).toFixed(0)}s`
3590
+ : "unknown";
3591
+ this.feedLine(concat(tp.success(" ⚡ Interrupted "), tp.accent(`@${displayName}`), tp.muted(` (${elapsed}, ${toolCallCount} tool calls, ${filesChanged.length} files changed)`)));
3592
+ this.feedLine(concat(tp.muted(" Resuming with: "), tp.text(steeringMessage.slice(0, 70))));
3593
+ this.refreshView();
3594
+ // Clean up the active task state — the drainAgentQueue loop will see
3595
+ // the agent as inactive and the queue entry was already removed
3596
+ this.activeTasks.delete(resolvedName);
3597
+ this.agentActive.delete(resolvedName);
3598
+ if (this.activeTasks.size === 0)
3599
+ this.stopStatusAnimation();
3600
+ // Queue the resumed task
3601
+ this.taskQueue.push({
3602
+ type: "agent",
3603
+ teammate: resolvedName,
3604
+ task: resumePrompt,
3605
+ });
3606
+ this.kickDrain();
3607
+ }
3608
+ catch (err) {
3609
+ this.feedLine(tp.error(` ✖ Failed to interrupt @${displayName}: ${err?.message ?? String(err)}`));
3610
+ this.refreshView();
3611
+ }
3612
+ }
3613
+ /**
3614
+ * Build a resume prompt from the original task, conversation log, and steering message.
3615
+ */
3616
+ buildResumePrompt(originalTask, conversationLog, steeringMessage, toolCallCount, filesChanged) {
3617
+ const parts = [];
3618
+ parts.push("<RESUME_CONTEXT>");
3619
+ parts.push("This is a resumed task. You were previously working on this task but were interrupted.");
3620
+ parts.push("Below is the log of what you accomplished before the interruption.");
3621
+ parts.push("");
3622
+ parts.push("DO NOT repeat work that is already done. Check the filesystem for files you already wrote.");
3623
+ parts.push("Continue from where you left off.");
3624
+ parts.push("");
3625
+ parts.push("## What You Did Before Interruption");
3626
+ parts.push("");
3627
+ parts.push(`Tool calls: ${toolCallCount}`);
3628
+ if (filesChanged.length > 0) {
3629
+ parts.push(`Files changed: ${filesChanged.slice(0, 20).join(", ")}${filesChanged.length > 20 ? ` (+${filesChanged.length - 20} more)` : ""}`);
3630
+ }
3631
+ parts.push("");
3632
+ parts.push(conversationLog);
3633
+ parts.push("");
3634
+ parts.push("## Interruption");
3635
+ parts.push("");
3636
+ parts.push(steeringMessage);
3637
+ parts.push("");
3638
+ parts.push("## Your Task Now");
3639
+ parts.push("");
3640
+ parts.push("Continue the original task from where you left off. The original task was:");
3641
+ parts.push("");
3642
+ parts.push(originalTask);
3643
+ parts.push("</RESUME_CONTEXT>");
3644
+ return parts.join("\n");
3645
+ }
3368
3646
  /** Drain user tasks for a single agent — runs in parallel with other agents.
3369
3647
  * System tasks are handled separately by runSystemTask(). */
3370
3648
  async drainAgentQueue(agent) {
@@ -3429,6 +3707,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3429
3707
  }
3430
3708
  // Display the (possibly retried) result to the user
3431
3709
  this.displayTaskResult(result, entry.type);
3710
+ // Audit cross-folder writes for AI teammates
3711
+ const tmConfig = this.orchestrator.getRegistry().get(entry.teammate);
3712
+ if (tmConfig?.type === "ai" && result.changedFiles.length > 0) {
3713
+ const violations = this.auditCrossFolderWrites(entry.teammate, result.changedFiles);
3714
+ if (violations.length > 0) {
3715
+ this.showViolationWarning(entry.teammate, violations);
3716
+ }
3717
+ }
3432
3718
  // Write debug entry — skip for debug analysis tasks (avoid recursion)
3433
3719
  if (entry.type !== "debug") {
3434
3720
  this.writeDebugEntry(entry.teammate, entry.task, result, startTime);
@@ -3933,7 +4219,21 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3933
4219
  }
3934
4220
  }
3935
4221
  }
4222
+ /** Compare two semver strings. Returns true if `a` is less than `b`. */
4223
+ static semverLessThan(a, b) {
4224
+ const pa = a.split(".").map(Number);
4225
+ const pb = b.split(".").map(Number);
4226
+ for (let i = 0; i < 3; i++) {
4227
+ if ((pa[i] ?? 0) < (pb[i] ?? 0))
4228
+ return true;
4229
+ if ((pa[i] ?? 0) > (pb[i] ?? 0))
4230
+ return false;
4231
+ }
4232
+ return false;
4233
+ }
3936
4234
  async startupMaintenance() {
4235
+ // Check and update installed CLI version
4236
+ const versionUpdate = this.checkVersionUpdate();
3937
4237
  const tmpDir = join(this.teammatesDir, ".tmp");
3938
4238
  // Clean up debug log files older than 1 day
3939
4239
  const debugDir = join(tmpDir, "debug");
@@ -3960,7 +4260,56 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3960
4260
  for (const name of teammates) {
3961
4261
  await this.runCompact(name, true);
3962
4262
  }
3963
- // 2. Purge daily logs older than 30 days (disk + Vectra)
4263
+ // 2. Compress previous day's log for each teammate (queued as system tasks)
4264
+ for (const name of teammates) {
4265
+ try {
4266
+ const compression = await buildDailyCompressionPrompt(join(this.teammatesDir, name));
4267
+ if (compression) {
4268
+ this.taskQueue.push({
4269
+ type: "agent",
4270
+ teammate: name,
4271
+ task: compression.prompt,
4272
+ system: true,
4273
+ });
4274
+ }
4275
+ }
4276
+ catch {
4277
+ /* compression check failed — non-fatal */
4278
+ }
4279
+ }
4280
+ // 2b. v0.6.0 migration — compress ALL uncompressed daily logs + re-index
4281
+ const needsMigration = versionUpdate &&
4282
+ (versionUpdate.previous === "" ||
4283
+ TeammatesREPL.semverLessThan(versionUpdate.previous, "0.6.0"));
4284
+ if (needsMigration) {
4285
+ this.feedLine(tp.accent(" ℹ Migrating to v0.6.0 — compressing daily logs..."));
4286
+ this.refreshView();
4287
+ let migrationCount = 0;
4288
+ for (const name of teammates) {
4289
+ try {
4290
+ const uncompressed = await findUncompressedDailies(join(this.teammatesDir, name));
4291
+ if (uncompressed.length === 0)
4292
+ continue;
4293
+ const prompt = await buildMigrationCompressionPrompt(join(this.teammatesDir, name), name, uncompressed);
4294
+ if (prompt) {
4295
+ migrationCount++;
4296
+ this.taskQueue.push({
4297
+ type: "agent",
4298
+ teammate: name,
4299
+ task: prompt,
4300
+ system: true,
4301
+ migration: true,
4302
+ });
4303
+ }
4304
+ }
4305
+ catch {
4306
+ /* migration compression failed — non-fatal */
4307
+ }
4308
+ }
4309
+ this.pendingMigrationSyncs = migrationCount;
4310
+ }
4311
+ this.kickDrain();
4312
+ // 3. Purge daily logs older than 30 days (disk + Vectra)
3964
4313
  const { Indexer } = await import("@teammates/recall");
3965
4314
  const indexer = new Indexer({ teammatesDir: this.teammatesDir });
3966
4315
  for (const name of teammates) {
@@ -3975,7 +4324,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3975
4324
  /* purge failed — non-fatal */
3976
4325
  }
3977
4326
  }
3978
- // 3. Sync recall indexes (bundled library call)
4327
+ // 4. Sync recall indexes (bundled library call)
3979
4328
  try {
3980
4329
  await syncRecallIndex(this.teammatesDir);
3981
4330
  }
@@ -3983,6 +4332,45 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3983
4332
  /* sync failed — non-fatal */
3984
4333
  }
3985
4334
  }
4335
+ /**
4336
+ * Check if the CLI version has changed since last run.
4337
+ * Updates settings.json with the current version.
4338
+ * Returns the previous version if it changed, null otherwise.
4339
+ */
4340
+ checkVersionUpdate() {
4341
+ const settingsPath = join(this.teammatesDir, "settings.json");
4342
+ let settings = {};
4343
+ try {
4344
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
4345
+ }
4346
+ catch {
4347
+ // No settings file or invalid JSON — create one
4348
+ }
4349
+ const previous = settings.cliVersion ?? "";
4350
+ const current = PKG_VERSION;
4351
+ if (previous === current)
4352
+ return null;
4353
+ // Detect major/minor version change (not just patch)
4354
+ const [prevMajor, prevMinor] = previous.split(".").map(Number);
4355
+ const [curMajor, curMinor] = current.split(".").map(Number);
4356
+ const isMajorMinor = previous !== "" && (prevMajor !== curMajor || prevMinor !== curMinor);
4357
+ // Update the stored version
4358
+ settings.cliVersion = current;
4359
+ if (!settings.version)
4360
+ settings.version = 1;
4361
+ try {
4362
+ writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
4363
+ }
4364
+ catch {
4365
+ /* write failed — non-fatal */
4366
+ }
4367
+ if (isMajorMinor) {
4368
+ this.feedLine(tp.accent(` ✔ Updated from v${previous} → v${current}`));
4369
+ this.feedLine();
4370
+ this.refreshView();
4371
+ }
4372
+ return { previous, current };
4373
+ }
3986
4374
  async cmdCopy() {
3987
4375
  this.doCopy(); // copies entire session
3988
4376
  }
package/dist/compact.d.ts CHANGED
@@ -67,3 +67,32 @@ export declare function buildWisdomPrompt(teammateDir: string, teammateName: str
67
67
  * Returns the list of deleted filenames.
68
68
  */
69
69
  export declare function purgeStaleDailies(teammateDir: string): Promise<string[]>;
70
+ /**
71
+ * Find all daily logs that are not yet compressed (no `compressed: true`
72
+ * frontmatter). Returns an array of { date, file } for each uncompressed log.
73
+ */
74
+ export declare function findUncompressedDailies(teammateDir: string): Promise<{
75
+ date: string;
76
+ file: string;
77
+ }[]>;
78
+ /**
79
+ * Build a prompt for an agent to compress multiple daily logs in bulk.
80
+ * Used during version migrations to compress all historical daily logs.
81
+ * Returns null if there are no uncompressed dailies.
82
+ */
83
+ export declare function buildMigrationCompressionPrompt(_teammateDir: string, teammateName: string, dailies: {
84
+ date: string;
85
+ file: string;
86
+ }[]): Promise<string | null>;
87
+ /**
88
+ * Check if the previous day's log needs compression and return a prompt
89
+ * to compress it. Returns null if no compression is needed.
90
+ *
91
+ * A daily log is eligible for compression when:
92
+ * - Today's log does not yet exist (new day boundary)
93
+ * - Yesterday's log exists and is not already compressed (no `compressed: true` frontmatter)
94
+ */
95
+ export declare function buildDailyCompressionPrompt(teammateDir: string): Promise<{
96
+ date: string;
97
+ prompt: string;
98
+ } | null>;
package/dist/compact.js CHANGED
@@ -534,3 +534,128 @@ export async function purgeStaleDailies(teammateDir) {
534
534
  }
535
535
  return purged;
536
536
  }
537
+ /**
538
+ * Find all daily logs that are not yet compressed (no `compressed: true`
539
+ * frontmatter). Returns an array of { date, file } for each uncompressed log.
540
+ */
541
+ export async function findUncompressedDailies(teammateDir) {
542
+ const memoryDir = join(teammateDir, "memory");
543
+ const entries = await readdir(memoryDir).catch(() => []);
544
+ const results = [];
545
+ for (const entry of entries) {
546
+ if (!entry.endsWith(".md"))
547
+ continue;
548
+ const stem = basename(entry, ".md");
549
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(stem))
550
+ continue;
551
+ const content = await readFile(join(memoryDir, entry), "utf-8");
552
+ if (content.startsWith("---") && /compressed:\s*true/.test(content)) {
553
+ continue; // Already compressed
554
+ }
555
+ results.push({ date: stem, file: entry });
556
+ }
557
+ return results.sort((a, b) => a.date.localeCompare(b.date));
558
+ }
559
+ /**
560
+ * Build a prompt for an agent to compress multiple daily logs in bulk.
561
+ * Used during version migrations to compress all historical daily logs.
562
+ * Returns null if there are no uncompressed dailies.
563
+ */
564
+ export async function buildMigrationCompressionPrompt(_teammateDir, teammateName, dailies) {
565
+ if (dailies.length === 0)
566
+ return null;
567
+ const filePaths = dailies
568
+ .map((d) => `.teammates/${teammateName}/memory/${d.file}`)
569
+ .join("\n- ");
570
+ return `You are compressing daily work logs to save context window space. There are ${dailies.length} uncompressed daily logs that need compression.
571
+
572
+ ## Rules
573
+
574
+ For EACH file listed below:
575
+ 1. Read the file
576
+ 2. Rewrite it into a shorter version that preserves:
577
+ - Task names and one-line summaries of what was done
578
+ - Key decisions and their rationale
579
+ - Files changed (as a flat list per task, not grouped subsections)
580
+ - Important context for future tasks
581
+ 3. Remove:
582
+ - Detailed "What was done" step-by-step breakdowns
583
+ - Build/test status lines (unless something failed)
584
+ - Redundant section headers
585
+ 4. Keep the same markdown structure (# date header, ## Task headers) but make each task entry 3-5 lines max
586
+ 5. Start the file with this frontmatter:
587
+ \`\`\`
588
+ ---
589
+ compressed: true
590
+ ---
591
+ \`\`\`
592
+
593
+ ## Files to Compress
594
+
595
+ - ${filePaths}
596
+
597
+ Process each file one at a time. Read it, compress it, write it back. Do NOT skip any files.`;
598
+ }
599
+ /**
600
+ * Check if the previous day's log needs compression and return a prompt
601
+ * to compress it. Returns null if no compression is needed.
602
+ *
603
+ * A daily log is eligible for compression when:
604
+ * - Today's log does not yet exist (new day boundary)
605
+ * - Yesterday's log exists and is not already compressed (no `compressed: true` frontmatter)
606
+ */
607
+ export async function buildDailyCompressionPrompt(teammateDir) {
608
+ const memoryDir = join(teammateDir, "memory");
609
+ const today = new Date().toISOString().slice(0, 10);
610
+ // Find yesterday's date
611
+ const yesterday = new Date();
612
+ yesterday.setDate(yesterday.getDate() - 1);
613
+ const yesterdayStr = yesterday.toISOString().slice(0, 10);
614
+ // Check if yesterday's log exists
615
+ const yesterdayFile = join(memoryDir, `${yesterdayStr}.md`);
616
+ let content;
617
+ try {
618
+ content = await readFile(yesterdayFile, "utf-8");
619
+ }
620
+ catch {
621
+ return null; // No yesterday log
622
+ }
623
+ // Skip if already compressed
624
+ if (content.startsWith("---") && /compressed:\s*true/.test(content)) {
625
+ return null;
626
+ }
627
+ // Skip if today's log already exists (we already passed the day boundary)
628
+ const todayFile = join(memoryDir, `${today}.md`);
629
+ try {
630
+ await readFile(todayFile, "utf-8");
631
+ // Today's log exists — this isn't a fresh day boundary, skip
632
+ return null;
633
+ }
634
+ catch {
635
+ // Today's log doesn't exist — this is a new day, compress yesterday
636
+ }
637
+ const prompt = `You are compressing a daily work log to save context window space. Rewrite the log below into a shorter version that preserves:
638
+ - Task names and one-line summaries of what was done
639
+ - Key decisions and their rationale
640
+ - Files changed (as a flat list per task, not grouped subsections)
641
+ - Important context for future tasks
642
+
643
+ Remove:
644
+ - Detailed "What was done" step-by-step breakdowns
645
+ - Build/test status lines (unless something failed)
646
+ - Redundant section headers
647
+
648
+ Keep the same markdown structure (# date header, ## Task headers) but make each task entry 3-5 lines max.
649
+
650
+ Write the compressed version to \`.teammates/${basename(teammateDir)}/memory/${yesterdayStr}.md\`. Start the file with this frontmatter:
651
+ \`\`\`
652
+ ---
653
+ compressed: true
654
+ ---
655
+ \`\`\`
656
+
657
+ ## Original Log
658
+
659
+ ${content}`;
660
+ return { date: yesterdayStr, prompt };
661
+ }
package/dist/index.d.ts CHANGED
@@ -1,15 +1,17 @@
1
1
  export type { AgentAdapter, InstalledService, RecallContext, RosterEntry, } from "./adapter.js";
2
2
  export { buildTeammatePrompt, DAILY_LOG_BUDGET_TOKENS, formatHandoffContext, queryRecallContext, syncRecallIndex, } from "./adapter.js";
3
- export { autoCompactForBudget } from "./compact.js";
4
3
  export { type AgentPreset, CliProxyAdapter, type CliProxyOptions, PRESETS, } from "./adapters/cli-proxy.js";
5
4
  export { EchoAdapter } from "./adapters/echo.js";
6
5
  export type { BannerInfo, ServiceInfo, ServiceStatus } from "./banner.js";
7
6
  export { AnimatedBanner } from "./banner.js";
8
7
  export type { CliArgs } from "./cli-args.js";
9
8
  export { findTeammatesDir, PKG_VERSION, parseCliArgs } from "./cli-args.js";
9
+ export { autoCompactForBudget, buildDailyCompressionPrompt, buildMigrationCompressionPrompt, findUncompressedDailies, } from "./compact.js";
10
+ export type { LogEntry } from "./log-parser.js";
11
+ export { buildConversationLog, formatLogTimeline, parseClaudeDebugLog, parseCodexOutput, parseRawOutput, } from "./log-parser.js";
10
12
  export { Orchestrator, type OrchestratorConfig, type TeammateStatus, } from "./orchestrator.js";
11
13
  export type { Persona } from "./personas.js";
12
14
  export { loadPersonas, scaffoldFromPersona } from "./personas.js";
13
15
  export { Registry } from "./registry.js";
14
16
  export { tp } from "./theme.js";
15
- export type { DailyLog, HandoffEnvelope, OrchestratorEvent, OwnershipRules, PresenceState, QueueEntry, SandboxLevel, SlashCommand, TaskAssignment, TaskResult, TeammateConfig, TeammateType, } from "./types.js";
17
+ export type { DailyLog, HandoffEnvelope, InterruptState, OrchestratorEvent, OwnershipRules, PresenceState, QueueEntry, SandboxLevel, SlashCommand, TaskAssignment, TaskResult, TeammateConfig, TeammateType, } from "./types.js";