@teammates/cli 0.5.2 → 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/README.md +46 -29
- 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-utils.d.ts +29 -0
- package/dist/cli-utils.js +74 -0
- package/dist/cli-utils.test.js +185 -1
- package/dist/cli.js +401 -47
- 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
|
@@ -18,10 +18,11 @@ import ora from "ora";
|
|
|
18
18
|
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
|
-
import { findAtMention, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
|
|
22
|
-
import { autoCompactForBudget, buildWisdomPrompt, compactEpisodic, purgeStaleDailies, } from "./compact.js";
|
|
21
|
+
import { buildConversationContext as buildConvCtx, buildSummarizationPrompt, cleanResponseBody, findAtMention, findSummarizationSplit, isImagePath, relativeTime, wrapLine, } from "./cli-utils.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";
|
|
@@ -44,9 +45,12 @@ class TeammatesREPL {
|
|
|
44
45
|
storeResult(result) {
|
|
45
46
|
this.lastResult = result;
|
|
46
47
|
this.lastResults.set(result.teammate, result);
|
|
48
|
+
// Store the full response body in conversation history — not just the
|
|
49
|
+
// subject line. The 24k-token budget + auto-summarization handle size.
|
|
50
|
+
const body = cleanResponseBody(result.rawOutput ?? "");
|
|
47
51
|
this.conversationHistory.push({
|
|
48
52
|
role: result.teammate,
|
|
49
|
-
text: result.summary,
|
|
53
|
+
text: body || result.summary,
|
|
50
54
|
});
|
|
51
55
|
}
|
|
52
56
|
/**
|
|
@@ -151,28 +155,20 @@ class TeammatesREPL {
|
|
|
151
155
|
}
|
|
152
156
|
/** Token budget for recent conversation history (24k tokens ≈ 96k chars). */
|
|
153
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;
|
|
154
160
|
buildConversationContext() {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
for (let i = this.conversationHistory.length - 1; i >= 0; i--) {
|
|
167
|
-
const line = `**${this.conversationHistory[i].role}:** ${this.conversationHistory[i].text}\n`;
|
|
168
|
-
if (used + line.length > budget && entries.length > 0)
|
|
169
|
-
break;
|
|
170
|
-
entries.unshift(line);
|
|
171
|
-
used += line.length;
|
|
172
|
-
}
|
|
173
|
-
if (entries.length > 0)
|
|
174
|
-
parts.push(entries.join("\n"));
|
|
175
|
-
return parts.join("\n");
|
|
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;
|
|
176
172
|
}
|
|
177
173
|
/**
|
|
178
174
|
* Check if conversation history exceeds the 24k token budget.
|
|
@@ -180,28 +176,11 @@ class TeammatesREPL {
|
|
|
180
176
|
* and queue a summarization task to the coding agent.
|
|
181
177
|
*/
|
|
182
178
|
maybeQueueSummarization() {
|
|
183
|
-
const
|
|
184
|
-
// Calculate how many recent entries fit in the budget (newest first)
|
|
185
|
-
let recentChars = 0;
|
|
186
|
-
let splitIdx = this.conversationHistory.length;
|
|
187
|
-
for (let i = this.conversationHistory.length - 1; i >= 0; i--) {
|
|
188
|
-
const line = `**${this.conversationHistory[i].role}:** ${this.conversationHistory[i].text}\n`;
|
|
189
|
-
if (recentChars + line.length > budget)
|
|
190
|
-
break;
|
|
191
|
-
recentChars += line.length;
|
|
192
|
-
splitIdx = i;
|
|
193
|
-
}
|
|
179
|
+
const splitIdx = findSummarizationSplit(this.conversationHistory, TeammatesREPL.CONV_HISTORY_CHARS);
|
|
194
180
|
if (splitIdx === 0)
|
|
195
181
|
return; // everything fits — nothing to summarize
|
|
196
|
-
// Collect entries that are being pushed out
|
|
197
182
|
const toSummarize = this.conversationHistory.slice(0, splitIdx);
|
|
198
|
-
const
|
|
199
|
-
.map((e) => `**${e.role}:** ${e.text}`)
|
|
200
|
-
.join("\n");
|
|
201
|
-
// Build the summarization prompt
|
|
202
|
-
const prompt = this.conversationSummary
|
|
203
|
-
? `You are maintaining a running summary of an ongoing conversation between a user and their AI teammates. Update the existing summary to incorporate the new conversation entries below.\n\n## Current Summary\n\n${this.conversationSummary}\n\n## New Entries to Incorporate\n\n${entriesText}\n\n## Instructions\n\nReturn ONLY the updated summary — no preamble, no explanation. The summary should:\n- Be a concise bulleted list of key topics discussed, decisions made, and work completed\n- Preserve important context that future messages might reference\n- Drop trivial or redundant details\n- Stay under 2000 characters\n- Do NOT include any output protocol (no TO:, no # Subject, no handoff blocks)`
|
|
204
|
-
: `You are maintaining a running summary of an ongoing conversation between a user and their AI teammates. Summarize the conversation entries below.\n\n## Entries to Summarize\n\n${entriesText}\n\n## Instructions\n\nReturn ONLY the summary — no preamble, no explanation. The summary should:\n- Be a concise bulleted list of key topics discussed, decisions made, and work completed\n- Preserve important context that future messages might reference\n- Drop trivial or redundant details\n- Stay under 2000 characters\n- Do NOT include any output protocol (no TO:, no # Subject, no handoff blocks)`;
|
|
183
|
+
const prompt = buildSummarizationPrompt(toSummarize, this.conversationSummary);
|
|
205
184
|
// Remove the summarized entries — they'll be captured in the summary
|
|
206
185
|
this.conversationHistory.splice(0, splitIdx);
|
|
207
186
|
// Queue the summarization task through the user's agent
|
|
@@ -221,6 +200,8 @@ class TeammatesREPL {
|
|
|
221
200
|
systemActive = new Map();
|
|
222
201
|
/** Agents currently in a silent retry — suppress all events. */
|
|
223
202
|
silentAgents = new Set();
|
|
203
|
+
/** Counter for pending migration compression tasks — triggers re-index when it hits 0. */
|
|
204
|
+
pendingMigrationSyncs = 0;
|
|
224
205
|
/** Per-agent drain locks — prevents double-draining a single agent. */
|
|
225
206
|
agentDrainLocks = new Map();
|
|
226
207
|
/** Stored pasted text keyed by paste number, expanded on Enter. */
|
|
@@ -242,6 +223,8 @@ class TeammatesREPL {
|
|
|
242
223
|
pendingHandoffs = [];
|
|
243
224
|
/** Pending retro proposals awaiting user approval. */
|
|
244
225
|
pendingRetroProposals = [];
|
|
226
|
+
/** Pending cross-folder violations awaiting user decision. */
|
|
227
|
+
pendingViolations = [];
|
|
245
228
|
/** Maps reply action IDs to their context (teammate + message). */
|
|
246
229
|
_replyContexts = new Map();
|
|
247
230
|
/** Quoted reply text to expand on next submit. */
|
|
@@ -346,12 +329,12 @@ class TeammatesREPL {
|
|
|
346
329
|
// Keep adding segments from the front until we'd exceed maxLen
|
|
347
330
|
let front = parts[0];
|
|
348
331
|
for (let i = 1; i < parts.length - 1; i++) {
|
|
349
|
-
const candidate = front + sep + parts[i] + sep
|
|
332
|
+
const candidate = `${front + sep + parts[i] + sep}...${sep}${last}`;
|
|
350
333
|
if (candidate.length > maxLen)
|
|
351
334
|
break;
|
|
352
335
|
front += sep + parts[i];
|
|
353
336
|
}
|
|
354
|
-
return front + sep
|
|
337
|
+
return `${front + sep}...${sep}${last}`;
|
|
355
338
|
}
|
|
356
339
|
/** Format elapsed seconds as (Ns), (Nm Ns), or (Nh Nm Ns). */
|
|
357
340
|
static formatElapsed(totalSeconds) {
|
|
@@ -761,6 +744,135 @@ class TeammatesREPL {
|
|
|
761
744
|
return;
|
|
762
745
|
}
|
|
763
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
|
+
}
|
|
764
876
|
/** Handle bulk handoff actions. */
|
|
765
877
|
handleBulkHandoff(action) {
|
|
766
878
|
if (!this.chatView)
|
|
@@ -1147,6 +1259,20 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
1147
1259
|
}
|
|
1148
1260
|
finally {
|
|
1149
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
|
+
}
|
|
1150
1276
|
}
|
|
1151
1277
|
}
|
|
1152
1278
|
// ─── Onboarding ───────────────────────────────────────────────────
|
|
@@ -2584,6 +2710,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
2584
2710
|
id.startsWith("retro-reject-")) {
|
|
2585
2711
|
this.handleRetroAction(id);
|
|
2586
2712
|
}
|
|
2713
|
+
else if (id.startsWith("revert-") || id.startsWith("allow-")) {
|
|
2714
|
+
this.handleViolationAction(id);
|
|
2715
|
+
}
|
|
2587
2716
|
else if (id.startsWith("approve-") || id.startsWith("reject-")) {
|
|
2588
2717
|
this.handleHandoffAction(id);
|
|
2589
2718
|
}
|
|
@@ -3080,6 +3209,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3080
3209
|
description: "Cancel a queued task by number",
|
|
3081
3210
|
run: (args) => this.cmdCancel(args),
|
|
3082
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
|
+
},
|
|
3083
3219
|
{
|
|
3084
3220
|
name: "init",
|
|
3085
3221
|
aliases: ["onboard", "setup"],
|
|
@@ -3399,6 +3535,114 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3399
3535
|
this.feedLine(concat(tp.muted(" Cancelled: "), tp.accent(`@${cancelDisplay}`), tp.muted(" — "), tp.text(removed.task.slice(0, 60))));
|
|
3400
3536
|
this.refreshView();
|
|
3401
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
|
+
}
|
|
3402
3646
|
/** Drain user tasks for a single agent — runs in parallel with other agents.
|
|
3403
3647
|
* System tasks are handled separately by runSystemTask(). */
|
|
3404
3648
|
async drainAgentQueue(agent) {
|
|
@@ -3463,6 +3707,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3463
3707
|
}
|
|
3464
3708
|
// Display the (possibly retried) result to the user
|
|
3465
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
|
+
}
|
|
3466
3718
|
// Write debug entry — skip for debug analysis tasks (avoid recursion)
|
|
3467
3719
|
if (entry.type !== "debug") {
|
|
3468
3720
|
this.writeDebugEntry(entry.teammate, entry.task, result, startTime);
|
|
@@ -3967,7 +4219,21 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
3967
4219
|
}
|
|
3968
4220
|
}
|
|
3969
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
|
+
}
|
|
3970
4234
|
async startupMaintenance() {
|
|
4235
|
+
// Check and update installed CLI version
|
|
4236
|
+
const versionUpdate = this.checkVersionUpdate();
|
|
3971
4237
|
const tmpDir = join(this.teammatesDir, ".tmp");
|
|
3972
4238
|
// Clean up debug log files older than 1 day
|
|
3973
4239
|
const debugDir = join(tmpDir, "debug");
|
|
@@ -3994,7 +4260,56 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
3994
4260
|
for (const name of teammates) {
|
|
3995
4261
|
await this.runCompact(name, true);
|
|
3996
4262
|
}
|
|
3997
|
-
// 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)
|
|
3998
4313
|
const { Indexer } = await import("@teammates/recall");
|
|
3999
4314
|
const indexer = new Indexer({ teammatesDir: this.teammatesDir });
|
|
4000
4315
|
for (const name of teammates) {
|
|
@@ -4009,7 +4324,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
4009
4324
|
/* purge failed — non-fatal */
|
|
4010
4325
|
}
|
|
4011
4326
|
}
|
|
4012
|
-
//
|
|
4327
|
+
// 4. Sync recall indexes (bundled library call)
|
|
4013
4328
|
try {
|
|
4014
4329
|
await syncRecallIndex(this.teammatesDir);
|
|
4015
4330
|
}
|
|
@@ -4017,6 +4332,45 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
4017
4332
|
/* sync failed — non-fatal */
|
|
4018
4333
|
}
|
|
4019
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
|
+
}
|
|
4020
4374
|
async cmdCopy() {
|
|
4021
4375
|
this.doCopy(); // copies entire session
|
|
4022
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>;
|