@teammates/cli 0.5.1 → 0.5.3
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.js +3 -4
- package/dist/adapters/cli-proxy.js +1 -0
- package/dist/adapters/copilot.js +3 -1
- 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 +183 -112
- package/dist/orchestrator.js +3 -0
- package/dist/types.d.ts +7 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -7,18 +7,17 @@ Agent-agnostic CLI orchestrator for teammates. Routes tasks to teammates, manage
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
cd cli
|
|
10
|
+
cd packages/cli
|
|
11
11
|
npm install
|
|
12
12
|
npm run build
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Then launch a session
|
|
15
|
+
Then launch a session:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
teammates
|
|
19
|
-
teammates
|
|
20
|
-
teammates
|
|
21
|
-
teammates echo # Test adapter (no external agent)
|
|
18
|
+
teammates # Uses default adapter (claude)
|
|
19
|
+
teammates --model sonnet # Override the agent model
|
|
20
|
+
teammates echo # Use the test adapter (no external agent)
|
|
22
21
|
```
|
|
23
22
|
|
|
24
23
|
The CLI auto-discovers your `.teammates/` directory by walking up from the current working directory.
|
|
@@ -26,7 +25,7 @@ The CLI auto-discovers your `.teammates/` directory by walking up from the curre
|
|
|
26
25
|
## Usage
|
|
27
26
|
|
|
28
27
|
```
|
|
29
|
-
teammates
|
|
28
|
+
teammates [options] [-- agent-flags...]
|
|
30
29
|
```
|
|
31
30
|
|
|
32
31
|
### Options
|
|
@@ -37,7 +36,7 @@ teammates <agent> [options] [-- agent-flags...]
|
|
|
37
36
|
| `--dir <path>` | Override `.teammates/` directory location |
|
|
38
37
|
| `--help` | Show usage information |
|
|
39
38
|
|
|
40
|
-
Any arguments after
|
|
39
|
+
Any arguments after `--` are passed through to the underlying agent CLI.
|
|
41
40
|
|
|
42
41
|
## In-Session Commands
|
|
43
42
|
|
|
@@ -55,16 +54,18 @@ Once inside the REPL, you can interact with teammates using `@mentions`, `/comma
|
|
|
55
54
|
|
|
56
55
|
| Command | Aliases | Description |
|
|
57
56
|
|---|---|---|
|
|
58
|
-
| `/status` | `/s`, `/queue
|
|
59
|
-
| `/
|
|
60
|
-
| `/
|
|
61
|
-
| `/
|
|
62
|
-
| `/init` |
|
|
63
|
-
| `/install <service>` | | Install a teammates service (e.g. `recall`) |
|
|
57
|
+
| `/status` | `/s`, `/queue` | Show teammates, active tasks, and queue |
|
|
58
|
+
| `/debug [teammate] [focus]` | | Analyze the last agent task with optional focus text |
|
|
59
|
+
| `/cancel [n]` | | Cancel a queued task by number |
|
|
60
|
+
| `/init` | `/onboard`, `/setup` | Run onboarding to set up teammates |
|
|
61
|
+
| `/init pick` | | Pick teammates from persona templates (in-TUI) |
|
|
64
62
|
| `/compact [teammate]` | | Compact daily logs into weekly/monthly summaries |
|
|
65
63
|
| `/retro [teammate]` | | Run a structured self-retrospective for a teammate |
|
|
64
|
+
| `/user [change]` | | View or update USER.md |
|
|
65
|
+
| `/btw [question]` | | Ask a quick side question without interrupting |
|
|
66
66
|
| `/copy` | `/cp` | Copy the last response to clipboard |
|
|
67
67
|
| `/theme` | | Show current theme colors |
|
|
68
|
+
| `/configure [service]` | `/config` | Configure external services (e.g. GitHub) |
|
|
68
69
|
| `/clear` | `/cls`, `/reset` | Clear history and reset the session |
|
|
69
70
|
| `/help` | `/h`, `/?` | Show available commands |
|
|
70
71
|
| `/exit` | `/q`, `/quit` | Exit the session |
|
|
@@ -121,21 +122,24 @@ The CLI uses a generic adapter interface to support any coding agent. Each adapt
|
|
|
121
122
|
|
|
122
123
|
| Preset | Command | Notes |
|
|
123
124
|
|---|---|---|
|
|
124
|
-
| `claude` | `claude -p --verbose` | Requires `claude` on PATH |
|
|
125
|
+
| `claude` | `claude -p --verbose` | Default adapter. Requires `claude` on PATH |
|
|
125
126
|
| `codex` | `codex exec` | Requires `codex` on PATH |
|
|
126
127
|
| `aider` | `aider --message-file` | Requires `aider` on PATH |
|
|
128
|
+
| `copilot` | GitHub Copilot SDK | Requires `@anthropic-ai/copilot-sdk` |
|
|
127
129
|
| `echo` | (in-process) | Test adapter — echoes prompts, no external agent |
|
|
128
130
|
|
|
129
131
|
### How Adapters Work
|
|
130
132
|
|
|
131
|
-
1.
|
|
132
|
-
2.
|
|
133
|
-
3. The prompt
|
|
134
|
-
4. The
|
|
135
|
-
5.
|
|
136
|
-
6.
|
|
137
|
-
7.
|
|
138
|
-
8.
|
|
133
|
+
1. **Auto-compaction** — If daily logs exceed the 24k token budget, oldest weeks are compacted into weekly summaries
|
|
134
|
+
2. **Two-pass recall** — Pass 1: keyword extraction → query variation generation → frontmatter catalog matching → multi-query fusion with dedup. Pass 2: agents can search mid-task via `teammates-recall search`
|
|
135
|
+
3. The orchestrator builds a full prompt with token-budgeted sections (identity → wisdom → recall → daily logs → roster → services → date/time/environment → user profile → task → output protocol → session/memory instructions)
|
|
136
|
+
4. The prompt is written to a temp file
|
|
137
|
+
5. The agent CLI is spawned with the prompt
|
|
138
|
+
6. stdout/stderr are captured for result parsing
|
|
139
|
+
7. **Empty response defense** — If the agent returns no text: retry with raw mode (no prompt wrapping), then minimal prompt, then synthetic fallback from metadata
|
|
140
|
+
8. The output is parsed for embedded handoff blocks (with natural-language fallback)
|
|
141
|
+
9. The recall index is synced to pick up any files the agent created/modified
|
|
142
|
+
10. Temp files are cleaned up
|
|
139
143
|
|
|
140
144
|
### Writing a Custom Adapter
|
|
141
145
|
|
|
@@ -169,13 +173,15 @@ Or add a preset to `cli-proxy.ts` for any CLI agent that accepts a prompt and ru
|
|
|
169
173
|
The CLI startup runs in two phases:
|
|
170
174
|
|
|
171
175
|
**Phase 1 — Pre-TUI (console I/O)**
|
|
172
|
-
1. **User profile setup** —
|
|
173
|
-
2. **Team onboarding** (if
|
|
176
|
+
1. **User profile setup** — If `USER.md` is missing, offers three paths: **GitHub** (imports name/alias via `gh api user`), **Manual** (prompts for alias, name, role, experience, preferences), or **Skip**. Creates `USER.md` and a user avatar folder at `.teammates/<alias>/` with `SOUL.md` (`**Type:** human`). Auto-detects the user's timezone.
|
|
177
|
+
2. **Team onboarding** (if `.teammates/` was just created) — Offers **Pick teammates** (persona templates), **Auto-generate** (agent-driven), **Import** (from another project), **Solo mode**, or **Exit**.
|
|
174
178
|
3. **Orchestrator init** — Loads existing teammates from `.teammates/`, registers user avatar with `type: "human"` and `presence: "online"`.
|
|
179
|
+
4. **Startup maintenance** — Runs auto-compaction and recall sync for all teammates (silent — progress bar only, no feed output unless actual work was done).
|
|
175
180
|
|
|
176
181
|
**Phase 2 — TUI (Consolonia)**
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
5. Animated startup banner with presence-colored roster
|
|
183
|
+
6. REPL starts — routing, slash commands, handoff approval
|
|
184
|
+
7. System tasks (compaction, summarization, wisdom distillation) run in the background without blocking user tasks
|
|
179
185
|
|
|
180
186
|
All user interaction during Phase 1 uses plain console I/O (readline + ora spinners), avoiding mouse tracking issues that would occur inside the TUI.
|
|
181
187
|
|
|
@@ -191,7 +197,7 @@ The CLI ships with 15 built-in persona templates that serve as starting points w
|
|
|
191
197
|
| **2 — Specialist** | Security (`shield`), Designer (`canvas`), Tech Writer (`quill`), Data Engineer (`forge`), SRE (`watchtower`), Architect (`blueprint`) |
|
|
192
198
|
| **3 — Niche** | Frontend (`pixel`), Backend (`engine`), Mobile (`orbit`), ML/AI (`neuron`), Performance (`tempo`) |
|
|
193
199
|
|
|
194
|
-
During onboarding, the CLI uses these personas to scaffold teammates. The user picks roles, optionally renames them, and the persona's SOUL.md body becomes the starting template — project-specific sections (commands, file patterns, technologies) are filled in by the onboarding agent.
|
|
200
|
+
During onboarding, the CLI uses these personas to scaffold teammates. Use **Pick teammates** during initial onboarding or `/init pick` in-session to choose from the list. The user picks roles, optionally renames them, and the persona's SOUL.md body becomes the starting template — project-specific sections (commands, file patterns, technologies) are filled in by the onboarding agent.
|
|
195
201
|
|
|
196
202
|
## Architecture
|
|
197
203
|
|
|
@@ -201,12 +207,18 @@ cli/src/
|
|
|
201
207
|
orchestrator.ts # Task routing, session management, presence tracking
|
|
202
208
|
adapter.ts # AgentAdapter interface, prompt builder, handoff formatting
|
|
203
209
|
registry.ts # Discovers teammates from .teammates/, loads SOUL.md + memory, type detection
|
|
210
|
+
compact.ts # Episodic memory compaction (daily→weekly→monthly) + auto-compaction
|
|
211
|
+
banner.ts # Animated startup banner with presence roster and segmented footer
|
|
204
212
|
personas.ts # Persona loader — reads and parses bundled persona templates
|
|
213
|
+
theme.ts # Theme configuration, color palette, styled text shortcuts
|
|
214
|
+
cli-args.ts # CLI argument parsing, .teammates/ directory discovery
|
|
215
|
+
cli-utils.ts # Pure utility functions (relativeTime, wrapLine, findAtMention, etc.)
|
|
205
216
|
types.ts # Core types (TeammateConfig, TaskResult, HandoffEnvelope, TeammateType, PresenceState)
|
|
206
217
|
onboard.ts # Template copying, team import, onboarding/adaptation prompts
|
|
207
218
|
dropdown.ts # Terminal dropdown/wordwheel widget
|
|
208
219
|
adapters/
|
|
209
|
-
cli-proxy.ts # Generic subprocess adapter with agent presets
|
|
220
|
+
cli-proxy.ts # Generic subprocess adapter with agent presets (claude, codex, aider)
|
|
221
|
+
copilot.ts # GitHub Copilot adapter
|
|
210
222
|
echo.ts # Test adapter (no-op)
|
|
211
223
|
cli/personas/ # 15 persona template files (pm.md, swe.md, devops.md, etc.)
|
|
212
224
|
```
|
|
@@ -246,6 +258,11 @@ Tests use [Vitest](https://vitest.dev/) and cover the core modules:
|
|
|
246
258
|
| `src/adapter.test.ts` | `buildTeammatePrompt`, `formatHandoffContext` |
|
|
247
259
|
| `src/orchestrator.test.ts` | Task routing, assignment, reset |
|
|
248
260
|
| `src/registry.test.ts` | Teammate discovery, SOUL.md parsing (role, ownership), daily logs |
|
|
261
|
+
| `src/compact.test.ts` | Daily→weekly compaction, auto-compaction, partial merge |
|
|
262
|
+
| `src/personas.test.ts` | Persona loading and scaffolding |
|
|
263
|
+
| `src/theme.test.ts` | Theme configuration |
|
|
264
|
+
| `src/cli-args.test.ts` | Argument parsing, directory discovery |
|
|
265
|
+
| `src/cli-utils.test.ts` | Utility functions |
|
|
249
266
|
| `src/adapters/echo.test.ts` | Echo adapter session and task execution |
|
|
250
267
|
|
|
251
268
|
## Dependencies
|
package/dist/adapter.js
CHANGED
|
@@ -225,7 +225,6 @@ export function buildTeammatePrompt(teammate, taskPrompt, options) {
|
|
|
225
225
|
"- The `# Subject` line is REQUIRED — it becomes the message title.",
|
|
226
226
|
"- Always write a substantive body. Never return just the subject.",
|
|
227
227
|
"- Use markdown: headings, lists, code blocks, bold, etc.",
|
|
228
|
-
"- **Write your text response FIRST, then update session/memory files.** This ensures visible output even if the agent turn ends early.",
|
|
229
228
|
"",
|
|
230
229
|
"### Handoffs",
|
|
231
230
|
"",
|
|
@@ -245,10 +244,10 @@ export function buildTeammatePrompt(teammate, taskPrompt, options) {
|
|
|
245
244
|
];
|
|
246
245
|
// Session state (conditional)
|
|
247
246
|
if (options?.sessionFile) {
|
|
248
|
-
instrLines.push("", "### Session State", "", `Your session file is at: \`${options.sessionFile}\``, "", "**After
|
|
247
|
+
instrLines.push("", "### Session State", "", `Your session file is at: \`${options.sessionFile}\``, "", "**After completing the task**, append a brief entry to this file with:", "- What you did", "- Key decisions made", "- Files changed", "- Anything the next task should know", "", "This is how you maintain continuity across tasks. Always read it, always update it.");
|
|
249
248
|
}
|
|
250
249
|
// Memory updates
|
|
251
|
-
instrLines.push("", "### Memory Updates", "", "**After
|
|
250
|
+
instrLines.push("", "### Memory Updates", "", "**After completing the task**, update your memory files:", "", `1. **Daily log** — Read \`.teammates/${teammate.name}/memory/${today}.md\` first (it may have entries from earlier tasks today), then write it back with your entry added. Create the file if it doesn't exist.`, " - What you did", " - Key decisions made", " - Files changed", " - Anything the next task should know", "", `2. **Typed memories** — If you learned something durable (a decision, pattern, feedback, or reference), create a typed memory file at \`.teammates/${teammate.name}/memory/<type>_<topic>.md\` with frontmatter (\`name\`, \`description\`, \`type\`). Update existing memory files if the topic already has one.`, "", "3. **WISDOM.md** — Do not edit directly. Wisdom entries are distilled from typed memories during compaction.", "", "These files are your persistent memory. Without them, your next session starts from scratch.");
|
|
252
251
|
// Section Reinforcement — back-references from high-attention bottom edge to each section tag
|
|
253
252
|
instrLines.push("", "### Section Reinforcement", "");
|
|
254
253
|
instrLines.push("- Stay in character as defined in `<IDENTITY>` — never break persona or speak as a generic assistant.");
|
|
@@ -274,7 +273,7 @@ export function buildTeammatePrompt(teammate, taskPrompt, options) {
|
|
|
274
273
|
if (options?.handoffContext) {
|
|
275
274
|
instrLines.push("- When `<HANDOFF_CONTEXT>` is present, address its requirements and open questions directly.");
|
|
276
275
|
}
|
|
277
|
-
instrLines.push("- Your response must answer `<TASK>` — everything else is supporting context.", "", "**REMINDER:
|
|
276
|
+
instrLines.push("- Your response must answer `<TASK>` — everything else is supporting context.", "", "**REMINDER: You MUST end your turn with visible text output. A turn with only file edits and no text is a failed turn.**");
|
|
278
277
|
parts.push(instrLines.join("\n"));
|
|
279
278
|
return parts.join("\n");
|
|
280
279
|
}
|
|
@@ -210,6 +210,7 @@ export class CliProxyAdapter {
|
|
|
210
210
|
: spawn.output;
|
|
211
211
|
const teammateNames = this.roster.map((r) => r.name);
|
|
212
212
|
const result = parseResult(teammate.name, output, teammateNames, prompt);
|
|
213
|
+
result.fullPrompt = fullPrompt;
|
|
213
214
|
result.diagnostics = {
|
|
214
215
|
exitCode: spawn.exitCode,
|
|
215
216
|
signal: spawn.signal,
|
package/dist/adapters/copilot.js
CHANGED
|
@@ -152,7 +152,9 @@ export class CopilotAdapter {
|
|
|
152
152
|
// Use the final assistant message content, fall back to collected deltas
|
|
153
153
|
const output = reply?.data?.content ?? outputParts.join("");
|
|
154
154
|
const teammateNames = this.roster.map((r) => r.name);
|
|
155
|
-
|
|
155
|
+
const result = parseResult(teammate.name, output, teammateNames, prompt);
|
|
156
|
+
result.fullPrompt = fullPrompt;
|
|
157
|
+
return result;
|
|
156
158
|
}
|
|
157
159
|
finally {
|
|
158
160
|
// Disconnect the session (preserves data for potential resume)
|
package/dist/cli-utils.d.ts
CHANGED
|
@@ -17,5 +17,34 @@ export declare function findAtMention(line: string, cursor: number): {
|
|
|
17
17
|
} | null;
|
|
18
18
|
/** Set of recognized image file extensions. */
|
|
19
19
|
export declare const IMAGE_EXTS: Set<string>;
|
|
20
|
+
/** A single entry in the conversation history. */
|
|
21
|
+
export interface ConversationEntry {
|
|
22
|
+
role: string;
|
|
23
|
+
text: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Strip protocol artifacts (TO: header, handoff blocks, trailing JSON) from
|
|
27
|
+
* an agent's raw output, returning just the message body.
|
|
28
|
+
*/
|
|
29
|
+
export declare function cleanResponseBody(rawOutput: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Format a conversation entry for inclusion in a prompt.
|
|
32
|
+
* Single-line text stays inline; multi-line text gets the body on the next line.
|
|
33
|
+
*/
|
|
34
|
+
export declare function formatConversationEntry(role: string, text: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Build the conversation context section for a teammate prompt.
|
|
37
|
+
* Works backwards from newest entries, including whole entries up to the budget.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildConversationContext(history: ConversationEntry[], summary: string, budget: number): string;
|
|
40
|
+
/**
|
|
41
|
+
* Find the split index where older conversation entries should be summarized.
|
|
42
|
+
* Returns 0 if everything fits within the budget (nothing to summarize).
|
|
43
|
+
*/
|
|
44
|
+
export declare function findSummarizationSplit(history: ConversationEntry[], budget: number): number;
|
|
45
|
+
/**
|
|
46
|
+
* Build the summarization prompt text from entries being pushed out of the budget.
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildSummarizationPrompt(entries: ConversationEntry[], existingSummary: string): string;
|
|
20
49
|
/** Check if a string looks like an image file path. */
|
|
21
50
|
export declare function isImagePath(text: string): boolean;
|
package/dist/cli-utils.js
CHANGED
|
@@ -61,6 +61,80 @@ export const IMAGE_EXTS = new Set([
|
|
|
61
61
|
".svg",
|
|
62
62
|
".ico",
|
|
63
63
|
]);
|
|
64
|
+
/**
|
|
65
|
+
* Strip protocol artifacts (TO: header, handoff blocks, trailing JSON) from
|
|
66
|
+
* an agent's raw output, returning just the message body.
|
|
67
|
+
*/
|
|
68
|
+
export function cleanResponseBody(rawOutput) {
|
|
69
|
+
return rawOutput
|
|
70
|
+
.replace(/^TO:\s*\S+\s*\n/im, "")
|
|
71
|
+
.replace(/```handoff\s*\n@\w+\s*\n[\s\S]*?```/g, "")
|
|
72
|
+
.replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/g, "")
|
|
73
|
+
.trim();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Format a conversation entry for inclusion in a prompt.
|
|
77
|
+
* Single-line text stays inline; multi-line text gets the body on the next line.
|
|
78
|
+
*/
|
|
79
|
+
export function formatConversationEntry(role, text) {
|
|
80
|
+
return text.includes("\n")
|
|
81
|
+
? `**${role}:**\n${text}\n`
|
|
82
|
+
: `**${role}:** ${text}\n`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build the conversation context section for a teammate prompt.
|
|
86
|
+
* Works backwards from newest entries, including whole entries up to the budget.
|
|
87
|
+
*/
|
|
88
|
+
export function buildConversationContext(history, summary, budget) {
|
|
89
|
+
if (history.length === 0 && !summary)
|
|
90
|
+
return "";
|
|
91
|
+
const parts = ["## Conversation History\n"];
|
|
92
|
+
if (summary) {
|
|
93
|
+
parts.push(`### Previous Conversation Summary\n\n${summary}\n`);
|
|
94
|
+
}
|
|
95
|
+
const entries = [];
|
|
96
|
+
let used = 0;
|
|
97
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
98
|
+
const entry = formatConversationEntry(history[i].role, history[i].text);
|
|
99
|
+
if (used + entry.length > budget && entries.length > 0)
|
|
100
|
+
break;
|
|
101
|
+
entries.unshift(entry);
|
|
102
|
+
used += entry.length;
|
|
103
|
+
}
|
|
104
|
+
if (entries.length > 0)
|
|
105
|
+
parts.push(entries.join("\n"));
|
|
106
|
+
return parts.join("\n");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Find the split index where older conversation entries should be summarized.
|
|
110
|
+
* Returns 0 if everything fits within the budget (nothing to summarize).
|
|
111
|
+
*/
|
|
112
|
+
export function findSummarizationSplit(history, budget) {
|
|
113
|
+
let recentChars = 0;
|
|
114
|
+
let splitIdx = history.length;
|
|
115
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
116
|
+
const entry = formatConversationEntry(history[i].role, history[i].text);
|
|
117
|
+
if (recentChars + entry.length > budget)
|
|
118
|
+
break;
|
|
119
|
+
recentChars += entry.length;
|
|
120
|
+
splitIdx = i;
|
|
121
|
+
}
|
|
122
|
+
return splitIdx === history.length ? 0 : splitIdx;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Build the summarization prompt text from entries being pushed out of the budget.
|
|
126
|
+
*/
|
|
127
|
+
export function buildSummarizationPrompt(entries, existingSummary) {
|
|
128
|
+
const entriesText = entries
|
|
129
|
+
.map((e) => e.text.includes("\n")
|
|
130
|
+
? `**${e.role}:**\n${e.text}`
|
|
131
|
+
: `**${e.role}:** ${e.text}`)
|
|
132
|
+
.join("\n\n");
|
|
133
|
+
const instructions = `## Instructions\n\nReturn ONLY the ${existingSummary ? "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)`;
|
|
134
|
+
return existingSummary
|
|
135
|
+
? `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${existingSummary}\n\n## New Entries to Incorporate\n\n${entriesText}\n\n${instructions}`
|
|
136
|
+
: `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}`;
|
|
137
|
+
}
|
|
64
138
|
/** Check if a string looks like an image file path. */
|
|
65
139
|
export function isImagePath(text) {
|
|
66
140
|
// Must look like a file path (contains slash or backslash, or starts with drive letter)
|
package/dist/cli-utils.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { findAtMention, IMAGE_EXTS, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
|
|
2
|
+
import { buildConversationContext, buildSummarizationPrompt, cleanResponseBody, findAtMention, findSummarizationSplit, formatConversationEntry, IMAGE_EXTS, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
|
|
3
3
|
// ── relativeTime ────────────────────────────────────────────────────
|
|
4
4
|
describe("relativeTime", () => {
|
|
5
5
|
beforeEach(() => {
|
|
@@ -177,3 +177,187 @@ describe("IMAGE_EXTS", () => {
|
|
|
177
177
|
expect(IMAGE_EXTS.size).toBe(8);
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
|
+
// ── cleanResponseBody ──────────────────────────────────────────────
|
|
181
|
+
describe("cleanResponseBody", () => {
|
|
182
|
+
it("strips TO: header", () => {
|
|
183
|
+
const raw = "TO: user\n# Subject\n\nBody text here";
|
|
184
|
+
expect(cleanResponseBody(raw)).toBe("# Subject\n\nBody text here");
|
|
185
|
+
});
|
|
186
|
+
it("strips TO: header case-insensitively", () => {
|
|
187
|
+
const raw = "to: User\n# Subject\n\nBody text here";
|
|
188
|
+
expect(cleanResponseBody(raw)).toBe("# Subject\n\nBody text here");
|
|
189
|
+
});
|
|
190
|
+
it("strips handoff blocks", () => {
|
|
191
|
+
const raw = "Some text\n\n```handoff\n@scribe\nDo this task\n```\n\nMore text";
|
|
192
|
+
expect(cleanResponseBody(raw)).toContain("Some text");
|
|
193
|
+
expect(cleanResponseBody(raw)).toContain("More text");
|
|
194
|
+
expect(cleanResponseBody(raw)).not.toContain("handoff");
|
|
195
|
+
expect(cleanResponseBody(raw)).not.toContain("@scribe");
|
|
196
|
+
});
|
|
197
|
+
it("strips multiple handoff blocks", () => {
|
|
198
|
+
const raw = "Text\n```handoff\n@scribe\ntask1\n```\nmiddle\n```handoff\n@beacon\ntask2\n```\nend";
|
|
199
|
+
expect(cleanResponseBody(raw)).toBe("Text\n\nmiddle\n\nend");
|
|
200
|
+
});
|
|
201
|
+
it("strips trailing JSON blocks", () => {
|
|
202
|
+
const raw = 'Body text\n\n```json\n{ "summary": "done" }\n```';
|
|
203
|
+
expect(cleanResponseBody(raw)).toBe("Body text");
|
|
204
|
+
});
|
|
205
|
+
it("strips all protocol artifacts together", () => {
|
|
206
|
+
const raw = 'TO: user\n# Done\n\nI finished the task.\n\n```handoff\n@pipeline\nDeploy it\n```\n\n```json\n{ "summary": "Done" }\n```';
|
|
207
|
+
expect(cleanResponseBody(raw)).toBe("# Done\n\nI finished the task.");
|
|
208
|
+
});
|
|
209
|
+
it("returns empty string for empty input", () => {
|
|
210
|
+
expect(cleanResponseBody("")).toBe("");
|
|
211
|
+
});
|
|
212
|
+
it("returns body unchanged when no protocol artifacts exist", () => {
|
|
213
|
+
expect(cleanResponseBody("Just a plain message")).toBe("Just a plain message");
|
|
214
|
+
});
|
|
215
|
+
it("trims surrounding whitespace", () => {
|
|
216
|
+
expect(cleanResponseBody(" \n Hello \n ")).toBe("Hello");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
// ── formatConversationEntry ────────────────────────────────────────
|
|
220
|
+
describe("formatConversationEntry", () => {
|
|
221
|
+
it("formats single-line text inline", () => {
|
|
222
|
+
expect(formatConversationEntry("scribe", "Task completed")).toBe("**scribe:** Task completed\n");
|
|
223
|
+
});
|
|
224
|
+
it("formats multi-line text with body on next line", () => {
|
|
225
|
+
expect(formatConversationEntry("beacon", "Line 1\nLine 2")).toBe("**beacon:**\nLine 1\nLine 2\n");
|
|
226
|
+
});
|
|
227
|
+
it("treats single line with no newline as inline", () => {
|
|
228
|
+
expect(formatConversationEntry("user", "hello")).toBe("**user:** hello\n");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
// ── buildConversationContext ────────────────────────────────────────
|
|
232
|
+
describe("buildConversationContext", () => {
|
|
233
|
+
it("returns empty string for empty history and no summary", () => {
|
|
234
|
+
expect(buildConversationContext([], "", 1000)).toBe("");
|
|
235
|
+
});
|
|
236
|
+
it("includes summary when present", () => {
|
|
237
|
+
const result = buildConversationContext([], "Previous topics discussed", 1000);
|
|
238
|
+
expect(result).toContain("## Conversation History");
|
|
239
|
+
expect(result).toContain("### Previous Conversation Summary");
|
|
240
|
+
expect(result).toContain("Previous topics discussed");
|
|
241
|
+
});
|
|
242
|
+
it("includes all entries when within budget", () => {
|
|
243
|
+
const history = [
|
|
244
|
+
{ role: "stevenic", text: "Hello" },
|
|
245
|
+
{ role: "scribe", text: "Hi there" },
|
|
246
|
+
{ role: "stevenic", text: "Do the thing" },
|
|
247
|
+
];
|
|
248
|
+
const result = buildConversationContext(history, "", 10_000);
|
|
249
|
+
expect(result).toContain("**stevenic:** Hello");
|
|
250
|
+
expect(result).toContain("**scribe:** Hi there");
|
|
251
|
+
expect(result).toContain("**stevenic:** Do the thing");
|
|
252
|
+
});
|
|
253
|
+
it("drops oldest entries when over budget", () => {
|
|
254
|
+
const history = [
|
|
255
|
+
{ role: "old", text: "A".repeat(500) },
|
|
256
|
+
{ role: "mid", text: "B".repeat(500) },
|
|
257
|
+
{ role: "new", text: "C".repeat(500) },
|
|
258
|
+
];
|
|
259
|
+
// Budget enough for ~2 entries but not 3
|
|
260
|
+
const budget = 1100;
|
|
261
|
+
const result = buildConversationContext(history, "", budget);
|
|
262
|
+
expect(result).not.toContain("**old:**");
|
|
263
|
+
expect(result).toContain("**new:**");
|
|
264
|
+
});
|
|
265
|
+
it("always includes at least the newest entry even if over budget", () => {
|
|
266
|
+
const history = [
|
|
267
|
+
{ role: "beacon", text: "A".repeat(2000) },
|
|
268
|
+
];
|
|
269
|
+
const result = buildConversationContext(history, "", 100);
|
|
270
|
+
expect(result).toContain("**beacon:**");
|
|
271
|
+
});
|
|
272
|
+
it("formats multi-line entries with body on next line", () => {
|
|
273
|
+
const history = [
|
|
274
|
+
{ role: "scribe", text: "Line 1\nLine 2\nLine 3" },
|
|
275
|
+
];
|
|
276
|
+
const result = buildConversationContext(history, "", 10_000);
|
|
277
|
+
expect(result).toContain("**scribe:**\nLine 1\nLine 2\nLine 3");
|
|
278
|
+
});
|
|
279
|
+
it("includes both summary and entries", () => {
|
|
280
|
+
const history = [
|
|
281
|
+
{ role: "stevenic", text: "Latest message" },
|
|
282
|
+
];
|
|
283
|
+
const result = buildConversationContext(history, "Earlier we discussed X", 10_000);
|
|
284
|
+
expect(result).toContain("### Previous Conversation Summary");
|
|
285
|
+
expect(result).toContain("Earlier we discussed X");
|
|
286
|
+
expect(result).toContain("**stevenic:** Latest message");
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
// ── findSummarizationSplit ─────────────────────────────────────────
|
|
290
|
+
describe("findSummarizationSplit", () => {
|
|
291
|
+
it("returns 0 when everything fits in budget", () => {
|
|
292
|
+
const history = [
|
|
293
|
+
{ role: "a", text: "short" },
|
|
294
|
+
{ role: "b", text: "also short" },
|
|
295
|
+
];
|
|
296
|
+
expect(findSummarizationSplit(history, 10_000)).toBe(0);
|
|
297
|
+
});
|
|
298
|
+
it("returns 0 for empty history", () => {
|
|
299
|
+
expect(findSummarizationSplit([], 1000)).toBe(0);
|
|
300
|
+
});
|
|
301
|
+
it("returns split index when history exceeds budget", () => {
|
|
302
|
+
const history = [
|
|
303
|
+
{ role: "old1", text: "A".repeat(400) },
|
|
304
|
+
{ role: "old2", text: "B".repeat(400) },
|
|
305
|
+
{ role: "new1", text: "C".repeat(400) },
|
|
306
|
+
{ role: "new2", text: "D".repeat(400) },
|
|
307
|
+
];
|
|
308
|
+
// Budget fits ~2 entries (~430 chars each with formatting)
|
|
309
|
+
const budget = 900;
|
|
310
|
+
const split = findSummarizationSplit(history, budget);
|
|
311
|
+
expect(split).toBeGreaterThan(0);
|
|
312
|
+
expect(split).toBeLessThan(history.length);
|
|
313
|
+
});
|
|
314
|
+
it("keeps newest entries and pushes oldest out", () => {
|
|
315
|
+
const history = [
|
|
316
|
+
{ role: "oldest", text: "X".repeat(300) },
|
|
317
|
+
{ role: "middle", text: "Y".repeat(300) },
|
|
318
|
+
{ role: "newest", text: "Z".repeat(300) },
|
|
319
|
+
];
|
|
320
|
+
// Budget fits 1 entry
|
|
321
|
+
const budget = 350;
|
|
322
|
+
const split = findSummarizationSplit(history, budget);
|
|
323
|
+
// Split should be 2 — entries 0 and 1 get summarized, entry 2 (newest) stays
|
|
324
|
+
expect(split).toBe(2);
|
|
325
|
+
});
|
|
326
|
+
it("returns 0 when single entry fits", () => {
|
|
327
|
+
const history = [{ role: "a", text: "hello" }];
|
|
328
|
+
expect(findSummarizationSplit(history, 10_000)).toBe(0);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
// ── buildSummarizationPrompt ───────────────────────────────────────
|
|
332
|
+
describe("buildSummarizationPrompt", () => {
|
|
333
|
+
const entries = [
|
|
334
|
+
{ role: "stevenic", text: "Build the feature" },
|
|
335
|
+
{ role: "beacon", text: "Done, here's what I did" },
|
|
336
|
+
];
|
|
337
|
+
it("builds a fresh summarization prompt when no existing summary", () => {
|
|
338
|
+
const prompt = buildSummarizationPrompt(entries, "");
|
|
339
|
+
expect(prompt).toContain("Summarize the conversation entries below");
|
|
340
|
+
expect(prompt).toContain("**stevenic:** Build the feature");
|
|
341
|
+
expect(prompt).toContain("**beacon:** Done, here's what I did");
|
|
342
|
+
expect(prompt).not.toContain("Current Summary");
|
|
343
|
+
});
|
|
344
|
+
it("builds an update prompt when existing summary is present", () => {
|
|
345
|
+
const prompt = buildSummarizationPrompt(entries, "Previously discussed X");
|
|
346
|
+
expect(prompt).toContain("Update the existing summary");
|
|
347
|
+
expect(prompt).toContain("## Current Summary");
|
|
348
|
+
expect(prompt).toContain("Previously discussed X");
|
|
349
|
+
expect(prompt).toContain("## New Entries to Incorporate");
|
|
350
|
+
});
|
|
351
|
+
it("includes instruction constraints", () => {
|
|
352
|
+
const prompt = buildSummarizationPrompt(entries, "");
|
|
353
|
+
expect(prompt).toContain("Stay under 2000 characters");
|
|
354
|
+
expect(prompt).toContain("Do NOT include any output protocol");
|
|
355
|
+
});
|
|
356
|
+
it("formats multi-line entries correctly", () => {
|
|
357
|
+
const multiLine = [
|
|
358
|
+
{ role: "scribe", text: "Line 1\nLine 2" },
|
|
359
|
+
];
|
|
360
|
+
const prompt = buildSummarizationPrompt(multiLine, "");
|
|
361
|
+
expect(prompt).toContain("**scribe:**\nLine 1\nLine 2");
|
|
362
|
+
});
|
|
363
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { exec as execCb, execSync, spawnSync } from "node:child_process";
|
|
11
11
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
|
|
13
|
-
import { dirname, join, resolve } from "node:path";
|
|
13
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
14
14
|
import { createInterface } from "node:readline";
|
|
15
15
|
import { App, ChatView, concat, esc, pen, renderMarkdown, stripAnsi, } from "@teammates/consolonia";
|
|
16
16
|
import chalk from "chalk";
|
|
@@ -18,7 +18,7 @@ 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";
|
|
21
|
+
import { buildConversationContext as buildConvCtx, buildSummarizationPrompt, cleanResponseBody, findAtMention, findSummarizationSplit, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
|
|
22
22
|
import { autoCompactForBudget, buildWisdomPrompt, compactEpisodic, purgeStaleDailies, } from "./compact.js";
|
|
23
23
|
import { PromptInput } from "./console/prompt-input.js";
|
|
24
24
|
import { buildTitle } from "./console/startup.js";
|
|
@@ -44,9 +44,12 @@ class TeammatesREPL {
|
|
|
44
44
|
storeResult(result) {
|
|
45
45
|
this.lastResult = result;
|
|
46
46
|
this.lastResults.set(result.teammate, result);
|
|
47
|
+
// Store the full response body in conversation history — not just the
|
|
48
|
+
// subject line. The 24k-token budget + auto-summarization handle size.
|
|
49
|
+
const body = cleanResponseBody(result.rawOutput ?? "");
|
|
47
50
|
this.conversationHistory.push({
|
|
48
51
|
role: result.teammate,
|
|
49
|
-
text: result.summary,
|
|
52
|
+
text: body || result.summary,
|
|
50
53
|
});
|
|
51
54
|
}
|
|
52
55
|
/**
|
|
@@ -152,27 +155,7 @@ class TeammatesREPL {
|
|
|
152
155
|
/** Token budget for recent conversation history (24k tokens ≈ 96k chars). */
|
|
153
156
|
static CONV_HISTORY_CHARS = 24_000 * 4;
|
|
154
157
|
buildConversationContext() {
|
|
155
|
-
|
|
156
|
-
return "";
|
|
157
|
-
const budget = TeammatesREPL.CONV_HISTORY_CHARS;
|
|
158
|
-
const parts = ["## Conversation History\n"];
|
|
159
|
-
// Include running summary of older conversation if present
|
|
160
|
-
if (this.conversationSummary) {
|
|
161
|
-
parts.push(`### Previous Conversation Summary\n\n${this.conversationSummary}\n`);
|
|
162
|
-
}
|
|
163
|
-
// Work backwards from newest — include whole entries up to 24k tokens
|
|
164
|
-
const entries = [];
|
|
165
|
-
let used = 0;
|
|
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");
|
|
158
|
+
return buildConvCtx(this.conversationHistory, this.conversationSummary, TeammatesREPL.CONV_HISTORY_CHARS);
|
|
176
159
|
}
|
|
177
160
|
/**
|
|
178
161
|
* Check if conversation history exceeds the 24k token budget.
|
|
@@ -180,28 +163,11 @@ class TeammatesREPL {
|
|
|
180
163
|
* and queue a summarization task to the coding agent.
|
|
181
164
|
*/
|
|
182
165
|
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
|
-
}
|
|
166
|
+
const splitIdx = findSummarizationSplit(this.conversationHistory, TeammatesREPL.CONV_HISTORY_CHARS);
|
|
194
167
|
if (splitIdx === 0)
|
|
195
168
|
return; // everything fits — nothing to summarize
|
|
196
|
-
// Collect entries that are being pushed out
|
|
197
169
|
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)`;
|
|
170
|
+
const prompt = buildSummarizationPrompt(toSummarize, this.conversationSummary);
|
|
205
171
|
// Remove the summarized entries — they'll be captured in the summary
|
|
206
172
|
this.conversationHistory.splice(0, splitIdx);
|
|
207
173
|
// Queue the summarization task through the user's agent
|
|
@@ -217,6 +183,8 @@ class TeammatesREPL {
|
|
|
217
183
|
taskQueue = [];
|
|
218
184
|
/** Per-agent active tasks — one per agent running in parallel. */
|
|
219
185
|
agentActive = new Map();
|
|
186
|
+
/** Active system tasks — multiple can run concurrently per agent. */
|
|
187
|
+
systemActive = new Map();
|
|
220
188
|
/** Agents currently in a silent retry — suppress all events. */
|
|
221
189
|
silentAgents = new Set();
|
|
222
190
|
/** Per-agent drain locks — prevents double-draining a single agent. */
|
|
@@ -330,37 +298,74 @@ class TeammatesREPL {
|
|
|
330
298
|
this.input.setStatus(null);
|
|
331
299
|
}
|
|
332
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Truncate a path for display, collapsing middle segments if too long.
|
|
303
|
+
* E.g. C:\source\some\deep\project → C:\source\...\project
|
|
304
|
+
*/
|
|
305
|
+
static truncatePath(fullPath, maxLen = 30) {
|
|
306
|
+
if (fullPath.length <= maxLen)
|
|
307
|
+
return fullPath;
|
|
308
|
+
const parts = fullPath.split(sep);
|
|
309
|
+
if (parts.length <= 2)
|
|
310
|
+
return fullPath;
|
|
311
|
+
const last = parts[parts.length - 1];
|
|
312
|
+
// Keep adding segments from the front until we'd exceed maxLen
|
|
313
|
+
let front = parts[0];
|
|
314
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
315
|
+
const candidate = `${front + sep + parts[i] + sep}...${sep}${last}`;
|
|
316
|
+
if (candidate.length > maxLen)
|
|
317
|
+
break;
|
|
318
|
+
front += sep + parts[i];
|
|
319
|
+
}
|
|
320
|
+
return `${front + sep}...${sep}${last}`;
|
|
321
|
+
}
|
|
322
|
+
/** Format elapsed seconds as (Ns), (Nm Ns), or (Nh Nm Ns). */
|
|
323
|
+
static formatElapsed(totalSeconds) {
|
|
324
|
+
const s = totalSeconds % 60;
|
|
325
|
+
const m = Math.floor(totalSeconds / 60) % 60;
|
|
326
|
+
const h = Math.floor(totalSeconds / 3600);
|
|
327
|
+
if (h > 0)
|
|
328
|
+
return `(${h}h ${m}m ${s}s)`;
|
|
329
|
+
if (m > 0)
|
|
330
|
+
return `(${m}m ${s}s)`;
|
|
331
|
+
return `(${s}s)`;
|
|
332
|
+
}
|
|
333
333
|
/** Render one frame of the status animation. */
|
|
334
334
|
renderStatusFrame() {
|
|
335
335
|
if (this.activeTasks.size === 0)
|
|
336
336
|
return;
|
|
337
337
|
const entries = Array.from(this.activeTasks.values());
|
|
338
|
-
const
|
|
339
|
-
const
|
|
338
|
+
const total = entries.length;
|
|
339
|
+
const idx = this.statusRotateIndex % total;
|
|
340
|
+
const { teammate, task, startTime } = entries[idx];
|
|
340
341
|
const displayName = teammate === this.selfName ? this.adapterName : teammate;
|
|
341
342
|
const spinChar = TeammatesREPL.SPINNER[this.statusFrame % TeammatesREPL.SPINNER.length];
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
343
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
344
|
+
const elapsedStr = TeammatesREPL.formatElapsed(elapsed);
|
|
345
|
+
// Build the tag: (1/3 - 2m 5s) when multiple, (2m 5s) when single
|
|
346
|
+
const tag = total > 1
|
|
347
|
+
? `(${idx + 1}/${total} - ${elapsedStr.slice(1, -1)})`
|
|
348
|
+
: elapsedStr;
|
|
349
|
+
// Target 80 chars total: "<spinner> <name>... <task> <tag>"
|
|
350
|
+
const prefix = `${spinChar} ${displayName}... `;
|
|
351
|
+
const suffix = ` ${tag}`;
|
|
352
|
+
const maxTask = 80 - prefix.length - suffix.length;
|
|
353
|
+
const cleanTask = task.replace(/[\r\n]+/g, " ").trim();
|
|
354
|
+
const taskText = maxTask <= 3
|
|
355
|
+
? ""
|
|
356
|
+
: cleanTask.length > maxTask
|
|
357
|
+
? `${cleanTask.slice(0, maxTask - 1)}…`
|
|
350
358
|
: cleanTask;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
: "";
|
|
354
|
-
this.chatView.setProgress(concat(tp.accent(`${spinChar} ${displayName}… `), tp.muted(taskText + queueTag)));
|
|
359
|
+
if (this.chatView) {
|
|
360
|
+
this.chatView.setProgress(concat(tp.accent(`${spinChar} ${displayName}... `), tp.muted(`${taskText}${suffix}`)));
|
|
355
361
|
this.app.refresh();
|
|
356
362
|
}
|
|
357
363
|
else {
|
|
358
|
-
// Mostly bright blue, periodically flicker to dark blue
|
|
359
364
|
const spinColor = this.statusFrame % 8 === 0 ? chalk.blue : chalk.blueBright;
|
|
360
365
|
const line = ` ${spinColor(spinChar)} ` +
|
|
361
366
|
chalk.bold(displayName) +
|
|
362
|
-
chalk.gray(
|
|
363
|
-
|
|
367
|
+
chalk.gray(`... ${taskText}`) +
|
|
368
|
+
chalk.gray(suffix);
|
|
364
369
|
this.input.setStatus(line);
|
|
365
370
|
}
|
|
366
371
|
}
|
|
@@ -1032,9 +1037,24 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
1032
1037
|
this.taskQueue.push({ type: "agent", teammate: match, task: input });
|
|
1033
1038
|
this.kickDrain();
|
|
1034
1039
|
}
|
|
1035
|
-
/**
|
|
1040
|
+
/** Returns true if the queue entry is a system-initiated (non-blocking) task. */
|
|
1041
|
+
isSystemTask(entry) {
|
|
1042
|
+
return (entry.type === "compact" ||
|
|
1043
|
+
entry.type === "summarize" ||
|
|
1044
|
+
(entry.type === "agent" && entry.system === true));
|
|
1045
|
+
}
|
|
1046
|
+
/** Start draining per-agent queues in parallel. Each agent gets its own drain loop.
|
|
1047
|
+
* System tasks are extracted and run concurrently without blocking user tasks. */
|
|
1036
1048
|
kickDrain() {
|
|
1037
|
-
//
|
|
1049
|
+
// Extract system tasks and fire them concurrently (non-blocking)
|
|
1050
|
+
for (let i = this.taskQueue.length - 1; i >= 0; i--) {
|
|
1051
|
+
const entry = this.taskQueue[i];
|
|
1052
|
+
if (this.isSystemTask(entry)) {
|
|
1053
|
+
this.taskQueue.splice(i, 1);
|
|
1054
|
+
this.runSystemTask(entry);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// Find agents that have user tasks but no active drain
|
|
1038
1058
|
const agentsWithWork = new Set();
|
|
1039
1059
|
for (const entry of this.taskQueue) {
|
|
1040
1060
|
agentsWithWork.add(entry.teammate);
|
|
@@ -1048,6 +1068,53 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
1048
1068
|
}
|
|
1049
1069
|
}
|
|
1050
1070
|
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Run a system-initiated task concurrently without blocking user tasks.
|
|
1073
|
+
* Purely background — no progress bar, no /status. Only reports errors.
|
|
1074
|
+
*/
|
|
1075
|
+
async runSystemTask(entry) {
|
|
1076
|
+
const taskId = `sys-${entry.teammate}-${Date.now()}`;
|
|
1077
|
+
this.systemActive.set(taskId, entry);
|
|
1078
|
+
const startTime = Date.now();
|
|
1079
|
+
try {
|
|
1080
|
+
if (entry.type === "compact") {
|
|
1081
|
+
await this.runCompact(entry.teammate, true);
|
|
1082
|
+
}
|
|
1083
|
+
else if (entry.type === "summarize") {
|
|
1084
|
+
const result = await this.orchestrator.assign({
|
|
1085
|
+
teammate: entry.teammate,
|
|
1086
|
+
task: entry.task,
|
|
1087
|
+
system: true,
|
|
1088
|
+
});
|
|
1089
|
+
const raw = result.rawOutput ?? "";
|
|
1090
|
+
this.conversationSummary = raw
|
|
1091
|
+
.replace(/^TO:\s*\S+\s*\n/im, "")
|
|
1092
|
+
.replace(/^#\s+.+\n*/m, "")
|
|
1093
|
+
.replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/g, "")
|
|
1094
|
+
.trim();
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
// System agent tasks (e.g. wisdom distillation)
|
|
1098
|
+
const result = await this.orchestrator.assign({
|
|
1099
|
+
teammate: entry.teammate,
|
|
1100
|
+
task: entry.task,
|
|
1101
|
+
system: true,
|
|
1102
|
+
});
|
|
1103
|
+
// Write debug entry for system tasks too
|
|
1104
|
+
this.writeDebugEntry(entry.teammate, entry.task, result, startTime);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
catch (err) {
|
|
1108
|
+
// System task errors always show in feed
|
|
1109
|
+
const msg = err?.message ?? String(err);
|
|
1110
|
+
const displayName = entry.teammate === this.selfName ? this.adapterName : entry.teammate;
|
|
1111
|
+
this.feedLine(tp.error(` ✖ @${displayName} (system): ${msg}`));
|
|
1112
|
+
this.refreshView();
|
|
1113
|
+
}
|
|
1114
|
+
finally {
|
|
1115
|
+
this.systemActive.delete(taskId);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1051
1118
|
// ─── Onboarding ───────────────────────────────────────────────────
|
|
1052
1119
|
/**
|
|
1053
1120
|
* Interactive prompt for team onboarding after user profile is set up.
|
|
@@ -2355,11 +2422,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
2355
2422
|
progressStyle: { fg: t.progress, italic: true },
|
|
2356
2423
|
dropdownHighlightStyle: { fg: t.accent },
|
|
2357
2424
|
dropdownStyle: { fg: t.textMuted },
|
|
2358
|
-
footer: concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`), tp.muted(" "), tp.text(this.adapterName)),
|
|
2425
|
+
footer: concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`), tp.muted(" "), tp.text(this.adapterName), tp.muted(" "), tp.dim(TeammatesREPL.truncatePath(dirname(this.teammatesDir)))),
|
|
2359
2426
|
footerRight: tp.muted("? /help "),
|
|
2360
2427
|
footerStyle: { fg: t.textDim },
|
|
2361
2428
|
});
|
|
2362
|
-
this.defaultFooter = concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`), tp.muted(" "), tp.text(this.adapterName));
|
|
2429
|
+
this.defaultFooter = concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`), tp.muted(" "), tp.text(this.adapterName), tp.muted(" "), tp.dim(TeammatesREPL.truncatePath(dirname(this.teammatesDir))));
|
|
2363
2430
|
this.defaultFooterRight = tp.muted("? /help ");
|
|
2364
2431
|
// Wire ChatView events for input handling
|
|
2365
2432
|
this.chatView.on("submit", (rawLine) => {
|
|
@@ -2968,7 +3035,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
2968
3035
|
{
|
|
2969
3036
|
name: "debug",
|
|
2970
3037
|
aliases: ["raw"],
|
|
2971
|
-
usage: "/debug [teammate]",
|
|
3038
|
+
usage: "/debug [teammate] [focus]",
|
|
2972
3039
|
description: "Analyze the last agent task with the coding agent",
|
|
2973
3040
|
run: (args) => this.cmdDebug(args),
|
|
2974
3041
|
},
|
|
@@ -3089,16 +3156,24 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3089
3156
|
return;
|
|
3090
3157
|
switch (event.type) {
|
|
3091
3158
|
case "task_assigned": {
|
|
3159
|
+
// System tasks (compaction, summarization, wisdom distillation) are
|
|
3160
|
+
// invisible — don't track them in the progress bar.
|
|
3161
|
+
if (event.assignment.system)
|
|
3162
|
+
break;
|
|
3092
3163
|
// Track this task and start the animated status bar
|
|
3093
3164
|
const key = event.assignment.teammate;
|
|
3094
3165
|
this.activeTasks.set(key, {
|
|
3095
3166
|
teammate: event.assignment.teammate,
|
|
3096
3167
|
task: event.assignment.task,
|
|
3168
|
+
startTime: Date.now(),
|
|
3097
3169
|
});
|
|
3098
3170
|
this.startStatusAnimation();
|
|
3099
3171
|
break;
|
|
3100
3172
|
}
|
|
3101
3173
|
case "task_completed": {
|
|
3174
|
+
// System task completions — don't touch activeTasks (was never added)
|
|
3175
|
+
if (event.result.system)
|
|
3176
|
+
break;
|
|
3102
3177
|
// Remove from active tasks and stop spinner.
|
|
3103
3178
|
// Result display is deferred to drainAgentQueue() so the defensive
|
|
3104
3179
|
// retry can update rawOutput before anything is shown to the user.
|
|
@@ -3186,10 +3261,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3186
3261
|
this.refreshView();
|
|
3187
3262
|
}
|
|
3188
3263
|
async cmdDebug(argsStr) {
|
|
3189
|
-
const
|
|
3264
|
+
const parts = argsStr.trim().split(/\s+/);
|
|
3265
|
+
const firstArg = (parts[0] ?? "").replace(/^@/, "");
|
|
3266
|
+
// Everything after the teammate name is the debug focus
|
|
3267
|
+
const debugFocus = parts.slice(1).join(" ").trim() || undefined;
|
|
3190
3268
|
// Resolve which teammate to debug
|
|
3191
3269
|
let targetName;
|
|
3192
|
-
if (
|
|
3270
|
+
if (firstArg === "everyone") {
|
|
3193
3271
|
// Pick all teammates with debug files, queue one analysis per teammate
|
|
3194
3272
|
const names = [];
|
|
3195
3273
|
for (const [name] of this.lastDebugFiles) {
|
|
@@ -3202,28 +3280,29 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3202
3280
|
return;
|
|
3203
3281
|
}
|
|
3204
3282
|
for (const name of names) {
|
|
3205
|
-
this.queueDebugAnalysis(name);
|
|
3283
|
+
this.queueDebugAnalysis(name, debugFocus);
|
|
3206
3284
|
}
|
|
3207
3285
|
return;
|
|
3208
3286
|
}
|
|
3209
|
-
else if (
|
|
3210
|
-
targetName =
|
|
3287
|
+
else if (firstArg) {
|
|
3288
|
+
targetName = firstArg;
|
|
3211
3289
|
}
|
|
3212
3290
|
else if (this.lastResult) {
|
|
3213
3291
|
targetName = this.lastResult.teammate;
|
|
3214
3292
|
}
|
|
3215
3293
|
else {
|
|
3216
|
-
this.feedLine(tp.muted(" No debug info available. Try: /debug [teammate]"));
|
|
3294
|
+
this.feedLine(tp.muted(" No debug info available. Try: /debug [teammate] [focus]"));
|
|
3217
3295
|
this.refreshView();
|
|
3218
3296
|
return;
|
|
3219
3297
|
}
|
|
3220
|
-
this.queueDebugAnalysis(targetName);
|
|
3298
|
+
this.queueDebugAnalysis(targetName, debugFocus);
|
|
3221
3299
|
}
|
|
3222
3300
|
/**
|
|
3223
3301
|
* Queue a debug analysis task — sends the last request + debug log
|
|
3224
3302
|
* to the base coding agent for analysis.
|
|
3303
|
+
* @param debugFocus Optional focus area the user wants to investigate
|
|
3225
3304
|
*/
|
|
3226
|
-
queueDebugAnalysis(teammate) {
|
|
3305
|
+
queueDebugAnalysis(teammate, debugFocus) {
|
|
3227
3306
|
const debugFile = this.lastDebugFiles.get(teammate);
|
|
3228
3307
|
const lastPrompt = this.lastTaskPrompts.get(teammate);
|
|
3229
3308
|
if (!debugFile) {
|
|
@@ -3241,8 +3320,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3241
3320
|
this.refreshView();
|
|
3242
3321
|
return;
|
|
3243
3322
|
}
|
|
3323
|
+
const focusLine = debugFocus
|
|
3324
|
+
? `\n\n**Focus your analysis on:** ${debugFocus}`
|
|
3325
|
+
: "";
|
|
3244
3326
|
const analysisPrompt = [
|
|
3245
|
-
`Analyze the following debug log from @${teammate}'s last task execution. Identify any issues, errors, or anomalies. If the response was empty, explain likely causes. Provide a concise diagnosis and suggest fixes if applicable
|
|
3327
|
+
`Analyze the following debug log from @${teammate}'s last task execution. Identify any issues, errors, or anomalies. If the response was empty, explain likely causes. Provide a concise diagnosis and suggest fixes if applicable.${focusLine}`,
|
|
3246
3328
|
"",
|
|
3247
3329
|
"## Last Request Sent to Agent",
|
|
3248
3330
|
"",
|
|
@@ -3254,6 +3336,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3254
3336
|
].join("\n");
|
|
3255
3337
|
// Show the debug log path — ctrl+click to open
|
|
3256
3338
|
this.feedLine(concat(tp.muted(" Debug log: "), tp.accent(debugFile)));
|
|
3339
|
+
if (debugFocus) {
|
|
3340
|
+
this.feedLine(tp.muted(` Focus: ${debugFocus}`));
|
|
3341
|
+
}
|
|
3257
3342
|
this.feedLine(tp.muted(" Queuing analysis…"));
|
|
3258
3343
|
this.refreshView();
|
|
3259
3344
|
this.taskQueue.push({
|
|
@@ -3280,34 +3365,18 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3280
3365
|
this.feedLine(concat(tp.muted(" Cancelled: "), tp.accent(`@${cancelDisplay}`), tp.muted(" — "), tp.text(removed.task.slice(0, 60))));
|
|
3281
3366
|
this.refreshView();
|
|
3282
3367
|
}
|
|
3283
|
-
/** Drain tasks for a single agent — runs in parallel with other agents.
|
|
3368
|
+
/** Drain user tasks for a single agent — runs in parallel with other agents.
|
|
3369
|
+
* System tasks are handled separately by runSystemTask(). */
|
|
3284
3370
|
async drainAgentQueue(agent) {
|
|
3285
3371
|
while (true) {
|
|
3286
|
-
const idx = this.taskQueue.findIndex((e) => e.teammate === agent);
|
|
3372
|
+
const idx = this.taskQueue.findIndex((e) => e.teammate === agent && !this.isSystemTask(e));
|
|
3287
3373
|
if (idx < 0)
|
|
3288
3374
|
break;
|
|
3289
3375
|
const entry = this.taskQueue.splice(idx, 1)[0];
|
|
3290
3376
|
this.agentActive.set(agent, entry);
|
|
3291
3377
|
const startTime = Date.now();
|
|
3292
3378
|
try {
|
|
3293
|
-
|
|
3294
|
-
await this.runCompact(entry.teammate);
|
|
3295
|
-
}
|
|
3296
|
-
else if (entry.type === "summarize") {
|
|
3297
|
-
// Internal housekeeping — summarize older conversation history
|
|
3298
|
-
const result = await this.orchestrator.assign({
|
|
3299
|
-
teammate: entry.teammate,
|
|
3300
|
-
task: entry.task,
|
|
3301
|
-
});
|
|
3302
|
-
// Extract the summary from the agent's output (strip protocol artifacts)
|
|
3303
|
-
const raw = result.rawOutput ?? "";
|
|
3304
|
-
this.conversationSummary = raw
|
|
3305
|
-
.replace(/^TO:\s*\S+\s*\n/im, "")
|
|
3306
|
-
.replace(/^#\s+.+\n*/m, "")
|
|
3307
|
-
.replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/g, "")
|
|
3308
|
-
.trim();
|
|
3309
|
-
}
|
|
3310
|
-
else {
|
|
3379
|
+
{
|
|
3311
3380
|
// btw and debug tasks skip conversation context (not part of main thread)
|
|
3312
3381
|
const extraContext = entry.type === "btw" || entry.type === "debug"
|
|
3313
3382
|
? ""
|
|
@@ -3418,6 +3487,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3418
3487
|
task,
|
|
3419
3488
|
"",
|
|
3420
3489
|
];
|
|
3490
|
+
// Include the full prompt sent to the agent (with identity, memory, etc.)
|
|
3491
|
+
const fullPrompt = result?.fullPrompt;
|
|
3492
|
+
if (fullPrompt) {
|
|
3493
|
+
lines.push("## Full Prompt");
|
|
3494
|
+
lines.push("");
|
|
3495
|
+
lines.push(fullPrompt);
|
|
3496
|
+
lines.push("");
|
|
3497
|
+
}
|
|
3421
3498
|
if (error) {
|
|
3422
3499
|
lines.push("## Result");
|
|
3423
3500
|
lines.push("");
|
|
@@ -3474,7 +3551,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3474
3551
|
lines.push("");
|
|
3475
3552
|
writeFileSync(debugFile, lines.join("\n"), "utf-8");
|
|
3476
3553
|
this.lastDebugFiles.set(teammate, debugFile);
|
|
3477
|
-
this.lastTaskPrompts.set(teammate, task);
|
|
3554
|
+
this.lastTaskPrompts.set(teammate, fullPrompt ?? task);
|
|
3478
3555
|
}
|
|
3479
3556
|
catch {
|
|
3480
3557
|
// Don't let debug logging break task execution
|
|
@@ -3655,12 +3732,12 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3655
3732
|
*/
|
|
3656
3733
|
async runCompact(name, silent = false) {
|
|
3657
3734
|
const teammateDir = join(this.teammatesDir, name);
|
|
3658
|
-
if (this.chatView) {
|
|
3735
|
+
if (!silent && this.chatView) {
|
|
3659
3736
|
this.chatView.setProgress(`Compacting ${name}...`);
|
|
3660
3737
|
this.refreshView();
|
|
3661
3738
|
}
|
|
3662
3739
|
let spinner = null;
|
|
3663
|
-
if (!this.chatView) {
|
|
3740
|
+
if (!silent && !this.chatView) {
|
|
3664
3741
|
spinner = ora({ text: `Compacting ${name}...`, color: "cyan" }).start();
|
|
3665
3742
|
}
|
|
3666
3743
|
try {
|
|
@@ -3698,16 +3775,16 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3698
3775
|
if (this.chatView)
|
|
3699
3776
|
this.feedLine(tp.success(` ✔ ${name}: ${parts.join(", ")}`));
|
|
3700
3777
|
}
|
|
3701
|
-
if (this.chatView)
|
|
3778
|
+
if (!silent && this.chatView)
|
|
3702
3779
|
this.chatView.setProgress(null);
|
|
3703
3780
|
// Sync recall index for this teammate (bundled library call)
|
|
3704
3781
|
try {
|
|
3705
|
-
if (this.chatView) {
|
|
3782
|
+
if (!silent && this.chatView) {
|
|
3706
3783
|
this.chatView.setProgress(`Syncing ${name} index...`);
|
|
3707
3784
|
this.refreshView();
|
|
3708
3785
|
}
|
|
3709
3786
|
let syncSpinner = null;
|
|
3710
|
-
if (!this.chatView) {
|
|
3787
|
+
if (!silent && !this.chatView) {
|
|
3711
3788
|
syncSpinner = ora({
|
|
3712
3789
|
text: `Syncing ${name} index...`,
|
|
3713
3790
|
color: "cyan",
|
|
@@ -3717,7 +3794,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3717
3794
|
if (syncSpinner)
|
|
3718
3795
|
syncSpinner.succeed(`${name}: index synced`);
|
|
3719
3796
|
if (this.chatView) {
|
|
3720
|
-
|
|
3797
|
+
if (!silent)
|
|
3798
|
+
this.chatView.setProgress(null);
|
|
3721
3799
|
if (!silent)
|
|
3722
3800
|
this.feedLine(tp.success(` ✔ ${name}: index synced`));
|
|
3723
3801
|
}
|
|
@@ -3734,9 +3812,9 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3734
3812
|
type: "agent",
|
|
3735
3813
|
teammate: name,
|
|
3736
3814
|
task: wisdomPrompt,
|
|
3815
|
+
system: true,
|
|
3737
3816
|
});
|
|
3738
|
-
|
|
3739
|
-
this.feedLine(tp.muted(` ↻ ${name}: queued wisdom distillation`));
|
|
3817
|
+
this.kickDrain();
|
|
3740
3818
|
}
|
|
3741
3819
|
}
|
|
3742
3820
|
catch {
|
|
@@ -3748,7 +3826,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3748
3826
|
if (spinner)
|
|
3749
3827
|
spinner.fail(`${name}: ${msg}`);
|
|
3750
3828
|
if (this.chatView) {
|
|
3751
|
-
|
|
3829
|
+
if (!silent)
|
|
3830
|
+
this.chatView.setProgress(null);
|
|
3752
3831
|
// Errors always show in feed
|
|
3753
3832
|
this.feedLine(tp.error(` ✖ ${name}: ${msg}`));
|
|
3754
3833
|
}
|
|
@@ -3879,16 +3958,8 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
3879
3958
|
// 1. Run compaction for all teammates (auto-compact + episodic + sync + wisdom)
|
|
3880
3959
|
// Progress bar shows status; feed only shows lines when actual work is done
|
|
3881
3960
|
for (const name of teammates) {
|
|
3882
|
-
if (this.chatView) {
|
|
3883
|
-
this.chatView.setProgress(`Maintaining @${name}...`);
|
|
3884
|
-
this.refreshView();
|
|
3885
|
-
}
|
|
3886
3961
|
await this.runCompact(name, true);
|
|
3887
3962
|
}
|
|
3888
|
-
if (this.chatView) {
|
|
3889
|
-
this.chatView.setProgress(null);
|
|
3890
|
-
this.refreshView();
|
|
3891
|
-
}
|
|
3892
3963
|
// 2. Purge daily logs older than 30 days (disk + Vectra)
|
|
3893
3964
|
const { Indexer } = await import("@teammates/recall");
|
|
3894
3965
|
const indexer = new Indexer({ teammatesDir: this.teammatesDir });
|
package/dist/orchestrator.js
CHANGED
|
@@ -82,6 +82,9 @@ export class Orchestrator {
|
|
|
82
82
|
const result = await this.adapter.executeTask(sessionId, teammate, prompt, {
|
|
83
83
|
raw: assignment.raw,
|
|
84
84
|
});
|
|
85
|
+
// Propagate system flag so event handlers can distinguish system vs user tasks
|
|
86
|
+
if (assignment.system)
|
|
87
|
+
result.system = true;
|
|
85
88
|
this.onEvent({ type: "task_completed", result });
|
|
86
89
|
// Update status (preserve presence)
|
|
87
90
|
const postPresence = this.statuses.get(assignment.teammate)?.presence ?? "online";
|
package/dist/types.d.ts
CHANGED
|
@@ -64,6 +64,8 @@ export interface HandoffEnvelope {
|
|
|
64
64
|
export interface TaskResult {
|
|
65
65
|
/** The teammate that executed the task */
|
|
66
66
|
teammate: string;
|
|
67
|
+
/** Whether this was a system-initiated task */
|
|
68
|
+
system?: boolean;
|
|
67
69
|
/** Whether the task completed successfully */
|
|
68
70
|
success: boolean;
|
|
69
71
|
/** Summary of what was done */
|
|
@@ -74,6 +76,8 @@ export interface TaskResult {
|
|
|
74
76
|
handoffs: HandoffEnvelope[];
|
|
75
77
|
/** Raw output from the agent */
|
|
76
78
|
rawOutput?: string;
|
|
79
|
+
/** The full prompt sent to the agent (for debug logging) */
|
|
80
|
+
fullPrompt?: string;
|
|
77
81
|
/** Process diagnostics for debugging empty/failed responses */
|
|
78
82
|
diagnostics?: {
|
|
79
83
|
/** Process exit code (null if killed by signal) */
|
|
@@ -98,6 +102,8 @@ export interface TaskAssignment {
|
|
|
98
102
|
extraContext?: string;
|
|
99
103
|
/** When true, skip identity/memory prompt wrapping — send task as-is */
|
|
100
104
|
raw?: boolean;
|
|
105
|
+
/** When true, this is a system-initiated task — suppress progress bar */
|
|
106
|
+
system?: boolean;
|
|
101
107
|
}
|
|
102
108
|
/** Orchestrator event for logging/hooks */
|
|
103
109
|
export type OrchestratorEvent = {
|
|
@@ -116,6 +122,7 @@ export type QueueEntry = {
|
|
|
116
122
|
type: "agent";
|
|
117
123
|
teammate: string;
|
|
118
124
|
task: string;
|
|
125
|
+
system?: boolean;
|
|
119
126
|
} | {
|
|
120
127
|
type: "compact";
|
|
121
128
|
teammate: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teammates/cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Agent-agnostic CLI for teammates. Routes tasks, manages handoffs, and plugs into any coding agent backend.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@github/copilot-sdk": "^0.1.32",
|
|
37
|
-
"@teammates/consolonia": "0.5.
|
|
38
|
-
"@teammates/recall": "0.5.
|
|
37
|
+
"@teammates/consolonia": "0.5.3",
|
|
38
|
+
"@teammates/recall": "0.5.3",
|
|
39
39
|
"chalk": "^5.6.2",
|
|
40
40
|
"ora": "^9.3.0"
|
|
41
41
|
},
|