@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/adapter.d.ts +7 -1
- package/dist/adapter.js +28 -3
- package/dist/adapter.test.js +10 -13
- package/dist/adapters/cli-proxy.d.ts +3 -0
- package/dist/adapters/cli-proxy.js +133 -108
- package/dist/cli.js +392 -4
- package/dist/compact.d.ts +29 -0
- package/dist/compact.js +125 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/log-parser.d.ts +53 -0
- package/dist/log-parser.js +228 -0
- package/dist/log-parser.test.d.ts +1 -0
- package/dist/log-parser.test.js +113 -0
- package/dist/orchestrator.d.ts +2 -0
- package/dist/orchestrator.js +4 -0
- package/dist/types.d.ts +18 -0
- package/package.json +3 -3
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
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";
|