@teammates/cli 0.3.4 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapter.d.ts +14 -1
- package/dist/adapter.js +131 -125
- package/dist/adapter.test.js +75 -0
- package/dist/adapters/cli-proxy.js +12 -11
- package/dist/adapters/copilot.js +11 -11
- package/dist/adapters/echo.test.js +1 -0
- package/dist/banner.d.ts +6 -1
- package/dist/banner.js +18 -3
- package/dist/cli-args.js +0 -1
- package/dist/cli.js +671 -270
- package/dist/console/startup.d.ts +2 -1
- package/dist/console/startup.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/orchestrator.d.ts +2 -0
- package/dist/orchestrator.js +15 -12
- package/dist/orchestrator.test.js +2 -1
- package/dist/registry.js +7 -0
- package/dist/registry.test.js +1 -0
- package/dist/types.d.ts +10 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -129,7 +129,7 @@ The CLI uses a generic adapter interface to support any coding agent. Each adapt
|
|
|
129
129
|
### How Adapters Work
|
|
130
130
|
|
|
131
131
|
1. The adapter queries the recall index for relevant memories (automatic, in-process)
|
|
132
|
-
2. The orchestrator builds a full prompt (SOUL → WISDOM → recall results → daily logs
|
|
132
|
+
2. The orchestrator builds a full prompt within a 32k token budget (SOUL → WISDOM → recall results → daily logs (budget-trimmed) → session state → roster → task)
|
|
133
133
|
3. The prompt is written to a temp file
|
|
134
134
|
4. The agent CLI is spawned with the prompt
|
|
135
135
|
5. stdout/stderr are captured for result parsing
|
package/dist/adapter.d.ts
CHANGED
|
@@ -70,14 +70,27 @@ export declare function syncRecallIndex(teammatesDir: string, teammate?: string)
|
|
|
70
70
|
/**
|
|
71
71
|
* Build the full prompt for a teammate session.
|
|
72
72
|
* Includes identity, memory, roster, output protocol, and the task.
|
|
73
|
+
*
|
|
74
|
+
* Context budget (32k tokens):
|
|
75
|
+
* - Current daily log (today): always included, outside budget
|
|
76
|
+
* - Days 2-7: up to 24k tokens (whole entries)
|
|
77
|
+
* - Recall results: at least 8k tokens + unused daily log budget
|
|
78
|
+
* (last entry may overflow by up to 4k tokens)
|
|
79
|
+
* - Weekly summaries: excluded (already indexed by recall)
|
|
80
|
+
*
|
|
81
|
+
* Identity, wisdom, roster, and protocol are never trimmed.
|
|
82
|
+
* The task prompt is never trimmed.
|
|
73
83
|
*/
|
|
74
84
|
export declare function buildTeammatePrompt(teammate: TeammateConfig, taskPrompt: string, options?: {
|
|
75
85
|
handoffContext?: string;
|
|
76
86
|
roster?: RosterEntry[];
|
|
77
87
|
services?: InstalledService[];
|
|
78
88
|
sessionFile?: string;
|
|
79
|
-
sessionContent?: string;
|
|
80
89
|
recallResults?: SearchResult[];
|
|
90
|
+
/** Contents of USER.md — injected just before the task. */
|
|
91
|
+
userProfile?: string;
|
|
92
|
+
/** Token budget for the prompt wrapper (default 64k). Task is excluded. */
|
|
93
|
+
tokenBudget?: number;
|
|
81
94
|
}): string;
|
|
82
95
|
/**
|
|
83
96
|
* Format a handoff envelope into a human-readable context string.
|
package/dist/adapter.js
CHANGED
|
@@ -40,164 +40,170 @@ export async function syncRecallIndex(teammatesDir, teammate) {
|
|
|
40
40
|
await indexer.syncAll();
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Default token budget for the prompt wrapper (everything except the task).
|
|
45
|
+
* ~64k tokens ≈ 256k chars at ~4 chars/token.
|
|
46
|
+
* The task prompt itself is excluded from this budget — if a user pastes
|
|
47
|
+
* a large input, that's intentional and we don't trim it.
|
|
48
|
+
*/
|
|
49
|
+
const DEFAULT_TOKEN_BUDGET = 64_000;
|
|
50
|
+
const CHARS_PER_TOKEN = 4;
|
|
51
|
+
/**
|
|
52
|
+
* Context budget allocation:
|
|
53
|
+
* - Days 2-7 get up to DAILY_LOG_BUDGET tokens (whole entries)
|
|
54
|
+
* - Recall gets at least RECALL_MIN_BUDGET, plus whatever daily logs didn't use
|
|
55
|
+
* - Last recall entry can push total up to CONTEXT_BUDGET + RECALL_OVERFLOW (36k)
|
|
56
|
+
* - Weekly summaries are excluded (already indexed by recall)
|
|
57
|
+
*/
|
|
58
|
+
const CONTEXT_BUDGET_TOKENS = 32_000;
|
|
59
|
+
const DAILY_LOG_BUDGET_TOKENS = 24_000;
|
|
60
|
+
const RECALL_MIN_BUDGET_TOKENS = 8_000;
|
|
61
|
+
const RECALL_OVERFLOW_TOKENS = 4_000;
|
|
62
|
+
/** Estimate tokens from character count. */
|
|
63
|
+
function estimateTokens(text) {
|
|
64
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
65
|
+
}
|
|
43
66
|
/**
|
|
44
67
|
* Build the full prompt for a teammate session.
|
|
45
68
|
* Includes identity, memory, roster, output protocol, and the task.
|
|
69
|
+
*
|
|
70
|
+
* Context budget (32k tokens):
|
|
71
|
+
* - Current daily log (today): always included, outside budget
|
|
72
|
+
* - Days 2-7: up to 24k tokens (whole entries)
|
|
73
|
+
* - Recall results: at least 8k tokens + unused daily log budget
|
|
74
|
+
* (last entry may overflow by up to 4k tokens)
|
|
75
|
+
* - Weekly summaries: excluded (already indexed by recall)
|
|
76
|
+
*
|
|
77
|
+
* Identity, wisdom, roster, and protocol are never trimmed.
|
|
78
|
+
* The task prompt is never trimmed.
|
|
46
79
|
*/
|
|
47
80
|
export function buildTeammatePrompt(teammate, taskPrompt, options) {
|
|
48
81
|
const parts = [];
|
|
49
|
-
// ── Identity
|
|
50
|
-
parts.push(`# You are ${teammate.name}\n`);
|
|
51
|
-
|
|
52
|
-
parts.push("\n---\n");
|
|
53
|
-
// ── Wisdom ───────────────────────────────────────────────────────
|
|
82
|
+
// ── Identity (required) ─────────────────────────────────────────
|
|
83
|
+
parts.push(`# You are ${teammate.name}\n\n${teammate.soul}\n\n---\n`);
|
|
84
|
+
// ── Wisdom (required) ───────────────────────────────────────────
|
|
54
85
|
if (teammate.wisdom.trim()) {
|
|
55
|
-
parts.push(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
86
|
+
parts.push(`## Your Wisdom\n\n${teammate.wisdom}\n\n---\n`);
|
|
87
|
+
}
|
|
88
|
+
// ── Budget-allocated context (daily logs → recall) ──────────────
|
|
89
|
+
// Today's log: always included, outside budget
|
|
90
|
+
// Days 2-7: up to 24k tokens (whole entries)
|
|
91
|
+
// Recall: at least 8k + unused daily budget, last entry may overflow by 4k
|
|
92
|
+
const todayLog = teammate.dailyLogs.slice(0, 1);
|
|
93
|
+
const pastLogs = teammate.dailyLogs.slice(1, 7); // days 2-7
|
|
94
|
+
let dailyBudget = DAILY_LOG_BUDGET_TOKENS;
|
|
95
|
+
// Current daily log (today) — never trimmed, always included
|
|
96
|
+
if (todayLog.length > 0) {
|
|
97
|
+
const todayLines = ["## Recent Daily Logs\n"];
|
|
98
|
+
for (const log of todayLog) {
|
|
99
|
+
todayLines.push(`### ${log.date}\n${log.content}\n`);
|
|
100
|
+
}
|
|
101
|
+
parts.push(todayLines.join("\n"));
|
|
102
|
+
}
|
|
103
|
+
// Days 2-7 — whole entries, up to 24k tokens
|
|
104
|
+
if (pastLogs.length > 0) {
|
|
105
|
+
const lines = [];
|
|
106
|
+
for (const log of pastLogs) {
|
|
107
|
+
const entry = `### ${log.date}\n${log.content}\n`;
|
|
108
|
+
const cost = estimateTokens(entry);
|
|
109
|
+
if (cost > dailyBudget)
|
|
110
|
+
break;
|
|
111
|
+
lines.push(entry);
|
|
112
|
+
dailyBudget -= cost;
|
|
113
|
+
}
|
|
114
|
+
if (lines.length > 0)
|
|
115
|
+
parts.push(lines.join("\n"));
|
|
116
|
+
}
|
|
117
|
+
// Recall results — gets at least 8k tokens, plus unused daily budget
|
|
118
|
+
// Last entry may overflow by up to 4k tokens
|
|
119
|
+
const recallBudget = Math.max(RECALL_MIN_BUDGET_TOKENS, RECALL_MIN_BUDGET_TOKENS + dailyBudget);
|
|
120
|
+
const recallResults = options?.recallResults ?? [];
|
|
121
|
+
if (recallResults.length > 0) {
|
|
122
|
+
const lines = [
|
|
123
|
+
"## Relevant Memories (from recall search)\n",
|
|
124
|
+
"These memories were retrieved based on relevance to the current task:\n",
|
|
125
|
+
];
|
|
126
|
+
const headerCost = estimateTokens(lines.join("\n"));
|
|
127
|
+
let recallUsed = headerCost;
|
|
128
|
+
for (const r of recallResults) {
|
|
64
129
|
const label = r.contentType
|
|
65
130
|
? `[${r.contentType}] ${r.uri}`
|
|
66
131
|
: r.uri;
|
|
67
|
-
|
|
132
|
+
const entry = `### ${label}\n${r.text}\n`;
|
|
133
|
+
const cost = estimateTokens(entry);
|
|
134
|
+
if (recallUsed + cost > recallBudget + RECALL_OVERFLOW_TOKENS)
|
|
135
|
+
break;
|
|
136
|
+
lines.push(entry);
|
|
137
|
+
recallUsed += cost;
|
|
138
|
+
// Stop cleanly at budget — but allow the current entry (overflow grace)
|
|
139
|
+
if (recallUsed >= recallBudget)
|
|
140
|
+
break;
|
|
68
141
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (teammate.dailyLogs.length > 0) {
|
|
73
|
-
parts.push("## Recent Daily Logs\n");
|
|
74
|
-
for (const log of teammate.dailyLogs.slice(0, 7)) {
|
|
75
|
-
parts.push(`### ${log.date}\n${log.content}\n`);
|
|
142
|
+
if (lines.length > 2) {
|
|
143
|
+
lines.push("\n---\n");
|
|
144
|
+
parts.push(lines.join("\n"));
|
|
76
145
|
}
|
|
77
|
-
parts.push("\n---\n");
|
|
78
146
|
}
|
|
79
|
-
//
|
|
80
|
-
if (
|
|
81
|
-
parts.
|
|
82
|
-
|
|
83
|
-
parts.push(
|
|
147
|
+
// Close context section with separator if needed
|
|
148
|
+
if (todayLog.length > 0 || pastLogs.length > 0) {
|
|
149
|
+
const lastPart = parts[parts.length - 1];
|
|
150
|
+
if (!lastPart.endsWith("---\n")) {
|
|
151
|
+
parts.push("\n---\n");
|
|
84
152
|
}
|
|
85
|
-
parts.push("\n---\n");
|
|
86
153
|
}
|
|
87
|
-
// ──
|
|
88
|
-
if (options?.sessionContent?.trim()) {
|
|
89
|
-
parts.push("## Session History\n");
|
|
90
|
-
parts.push("These are entries from your prior tasks in this session:\n");
|
|
91
|
-
parts.push(options.sessionContent);
|
|
92
|
-
parts.push("\n---\n");
|
|
93
|
-
}
|
|
94
|
-
// ── Team roster ───────────────────────────────────────────────────
|
|
154
|
+
// ── Team roster (required, small) ───────────────────────────────
|
|
95
155
|
if (options?.roster && options.roster.length > 0) {
|
|
96
|
-
|
|
97
|
-
|
|
156
|
+
const lines = [
|
|
157
|
+
"## Your Team\n",
|
|
158
|
+
"These are the other teammates you can hand off work to:\n",
|
|
159
|
+
];
|
|
98
160
|
for (const t of options.roster) {
|
|
99
161
|
if (t.name === teammate.name)
|
|
100
162
|
continue;
|
|
101
163
|
const owns = t.ownership.primary.length > 0
|
|
102
164
|
? ` — owns: ${t.ownership.primary.join(", ")}`
|
|
103
165
|
: "";
|
|
104
|
-
|
|
166
|
+
lines.push(`- **@${t.name}**: ${t.role}${owns}`);
|
|
105
167
|
}
|
|
106
|
-
|
|
168
|
+
lines.push("\n---\n");
|
|
169
|
+
parts.push(lines.join("\n"));
|
|
107
170
|
}
|
|
108
|
-
// ── Installed services
|
|
171
|
+
// ── Installed services (required, small) ────────────────────────
|
|
109
172
|
if (options?.services && options.services.length > 0) {
|
|
110
|
-
|
|
111
|
-
|
|
173
|
+
const lines = [
|
|
174
|
+
"## Available Services\n",
|
|
175
|
+
"These services are installed and available for you to use:\n",
|
|
176
|
+
];
|
|
112
177
|
for (const svc of options.services) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
178
|
+
lines.push(`### ${svc.name}\n`);
|
|
179
|
+
lines.push(svc.description);
|
|
180
|
+
lines.push(`\n**Usage:** \`${svc.usage}\`\n`);
|
|
116
181
|
}
|
|
117
|
-
|
|
182
|
+
lines.push("\n---\n");
|
|
183
|
+
parts.push(lines.join("\n"));
|
|
118
184
|
}
|
|
119
|
-
// ── Handoff context (
|
|
185
|
+
// ── Handoff context (required when present) ─────────────────────
|
|
120
186
|
if (options?.handoffContext) {
|
|
121
|
-
parts.push(
|
|
122
|
-
parts.push(options.handoffContext);
|
|
123
|
-
parts.push("\n---\n");
|
|
187
|
+
parts.push(`## Handoff Context\n\n${options.handoffContext}\n\n---\n`);
|
|
124
188
|
}
|
|
125
|
-
// ── Session state
|
|
189
|
+
// ── Session state (required) ────────────────────────────────────
|
|
126
190
|
if (options?.sessionFile) {
|
|
127
|
-
parts.push(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
**Before returning your result**, append a brief entry to this file with:
|
|
131
|
-
- What you did
|
|
132
|
-
- Key decisions made
|
|
133
|
-
- Files changed
|
|
134
|
-
- Anything the next task should know
|
|
135
|
-
|
|
136
|
-
This is how you maintain continuity across tasks. Always read it, always update it.
|
|
137
|
-
`);
|
|
138
|
-
parts.push("\n---\n");
|
|
139
|
-
}
|
|
140
|
-
// ── Memory updates ─────────────────────────────────────────────────
|
|
191
|
+
parts.push(`## Session State\n\nYour session file is at: \`${options.sessionFile}\`\n\n**Before returning your result**, append a brief entry to this file with:\n- What you did\n- Key decisions made\n- Files changed\n- Anything the next task should know\n\nThis is how you maintain continuity across tasks. Always read it, always update it.\n\n---\n`);
|
|
192
|
+
}
|
|
193
|
+
// ── Memory updates (required) ───────────────────────────────────
|
|
141
194
|
const today = new Date().toISOString().slice(0, 10);
|
|
142
|
-
parts.push(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
- What you did
|
|
147
|
-
- Key decisions made
|
|
148
|
-
- Files changed
|
|
149
|
-
- Anything the next task should know
|
|
150
|
-
|
|
151
|
-
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.
|
|
152
|
-
|
|
153
|
-
3. **WISDOM.md** — Do not edit directly. Wisdom entries are distilled from typed memories during compaction.
|
|
154
|
-
|
|
155
|
-
These files are your persistent memory. Without them, your next session starts from scratch.
|
|
156
|
-
`);
|
|
157
|
-
parts.push("\n---\n");
|
|
158
|
-
// ── Output protocol ───────────────────────────────────────────────
|
|
159
|
-
parts.push("## Output Protocol (CRITICAL)\n");
|
|
160
|
-
parts.push(`**Your #1 job is to produce a visible text response.** Session updates and memory writes are secondary — they support continuity but are not the deliverable. The user sees ONLY your text output. If you update files but return no text, the user sees an empty message and your work is invisible.
|
|
161
|
-
|
|
162
|
-
Format your response as:
|
|
163
|
-
|
|
164
|
-
\`\`\`
|
|
165
|
-
TO: user
|
|
166
|
-
# <Subject line>
|
|
167
|
-
|
|
168
|
-
<Body — full markdown response>
|
|
169
|
-
\`\`\`
|
|
170
|
-
|
|
171
|
-
**Handoffs:** To hand off work to a teammate, include a fenced handoff block anywhere in your response:
|
|
172
|
-
|
|
173
|
-
\`\`\`
|
|
174
|
-
\`\`\`handoff
|
|
175
|
-
@<teammate>
|
|
176
|
-
<task description — what you need them to do, with full context>
|
|
177
|
-
\`\`\`
|
|
178
|
-
\`\`\`
|
|
179
|
-
|
|
180
|
-
**Rules:**
|
|
181
|
-
- **You MUST end your turn with visible text output.** A turn that ends with only tool calls and no text is a failed turn.
|
|
182
|
-
- The \`# Subject\` line is REQUIRED — it becomes the message title.
|
|
183
|
-
- Always write a substantive body. Never return just the subject.
|
|
184
|
-
- Use markdown: headings, lists, code blocks, bold, etc.
|
|
185
|
-
- Do as much work as you can before handing off.
|
|
186
|
-
- Only hand off to teammates listed in "Your Team" above.
|
|
187
|
-
- The handoff block can appear anywhere in your response — it will be detected automatically.
|
|
188
|
-
`);
|
|
189
|
-
parts.push("\n---\n");
|
|
190
|
-
// ── Current date/time ────────────────────────────────────────────
|
|
195
|
+
parts.push(`## Memory Updates\n\n**Before returning your result**, update your memory files:\n\n1. **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.\n - What you did\n - Key decisions made\n - Files changed\n - Anything the next task should know\n\n2. **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.\n\n3. **WISDOM.md** — Do not edit directly. Wisdom entries are distilled from typed memories during compaction.\n\nThese files are your persistent memory. Without them, your next session starts from scratch.\n\n---\n`);
|
|
196
|
+
// ── Output protocol (required) ──────────────────────────────────
|
|
197
|
+
parts.push(`## Output Protocol (CRITICAL)\n\n**Your #1 job is to produce a visible text response.** Session updates and memory writes are secondary — they support continuity but are not the deliverable. The user sees ONLY your text output. If you update files but return no text, the user sees an empty message and your work is invisible.\n\nFormat your response as:\n\n\`\`\`\nTO: user\n# <Subject line>\n\n<Body — full markdown response>\n\`\`\`\n\n**Handoffs:** To hand off work to a teammate, include a fenced handoff block anywhere in your response:\n\n\`\`\`\n\`\`\`handoff\n@<teammate>\n<task description — what you need them to do, with full context>\n\`\`\`\n\`\`\`\n\n**Rules:**\n- **You MUST end your turn with visible text output.** A turn that ends with only tool calls and no text is a failed turn.\n- The \`# Subject\` line is REQUIRED — it becomes the message title.\n- Always write a substantive body. Never return just the subject.\n- Use markdown: headings, lists, code blocks, bold, etc.\n- Do as much work as you can before handing off.\n- Only hand off to teammates listed in "Your Team" above.\n- The handoff block can appear anywhere in your response — it will be detected automatically.\n\n---\n`);
|
|
198
|
+
// ── Current date/time (required, small) ─────────────────────────
|
|
191
199
|
const now = new Date();
|
|
192
|
-
parts.push(`**Current date:** ${now.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })} (${today})`);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
parts.push(
|
|
199
|
-
// ── Final reminder (last thing the agent reads) ─────────────────
|
|
200
|
-
parts.push("**REMINDER: After completing the task and updating session/memory files, you MUST produce a text response starting with `TO: user`. An empty response is a bug.**");
|
|
200
|
+
parts.push(`**Current date:** ${now.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })} (${today})\n**Current time:** ${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}\n\n---\n`);
|
|
201
|
+
// ── User profile (always included when present) ────────────────
|
|
202
|
+
if (options?.userProfile?.trim()) {
|
|
203
|
+
parts.push(`## User Profile\n\n${options.userProfile.trim()}\n\n---\n`);
|
|
204
|
+
}
|
|
205
|
+
// ── Task (always included, excluded from budget) ────────────────
|
|
206
|
+
parts.push(`## Task\n\n${taskPrompt}\n\n---\n\n**REMINDER: After completing the task and updating session/memory files, you MUST produce a text response starting with \`TO: user\`. An empty response is a bug.**`);
|
|
201
207
|
return parts.join("\n");
|
|
202
208
|
}
|
|
203
209
|
/**
|
package/dist/adapter.test.js
CHANGED
|
@@ -3,6 +3,7 @@ import { buildTeammatePrompt, formatHandoffContext } from "./adapter.js";
|
|
|
3
3
|
function makeConfig(overrides) {
|
|
4
4
|
return {
|
|
5
5
|
name: "beacon",
|
|
6
|
+
type: "ai",
|
|
6
7
|
role: "Platform engineer.",
|
|
7
8
|
soul: "# Beacon\n\nBeacon owns the recall package.",
|
|
8
9
|
wisdom: "",
|
|
@@ -99,6 +100,80 @@ describe("buildTeammatePrompt", () => {
|
|
|
99
100
|
expect(prompt).toContain("## Session State");
|
|
100
101
|
expect(prompt).toContain("/tmp/beacon-session.md");
|
|
101
102
|
});
|
|
103
|
+
it("drops daily logs that exceed the 24k daily budget", () => {
|
|
104
|
+
// Each log is ~50k chars = ~12.5k tokens. Only 1 fits in 24k daily budget.
|
|
105
|
+
const bigContent = "D".repeat(50_000);
|
|
106
|
+
const config = makeConfig({
|
|
107
|
+
dailyLogs: [
|
|
108
|
+
{ date: "2026-03-18", content: "Today's log — never trimmed" },
|
|
109
|
+
{ date: "2026-03-17", content: bigContent }, // day 2 — fits in 24k
|
|
110
|
+
{ date: "2026-03-16", content: bigContent }, // day 3 — exceeds 24k, dropped
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
const prompt = buildTeammatePrompt(config, "task");
|
|
114
|
+
// Today's log is always fully present (never trimmed)
|
|
115
|
+
expect(prompt).toContain("Today's log — never trimmed");
|
|
116
|
+
// Day 2 fits within 24k
|
|
117
|
+
expect(prompt).toContain("2026-03-17");
|
|
118
|
+
// Day 3 doesn't fit (12.5k + 12.5k > 24k)
|
|
119
|
+
expect(prompt).not.toContain("2026-03-16");
|
|
120
|
+
});
|
|
121
|
+
it("recall gets at least 8k tokens even when daily logs use full 24k", () => {
|
|
122
|
+
// Daily logs fill their 24k budget. Recall still gets its guaranteed 8k minimum.
|
|
123
|
+
const dailyContent = "D".repeat(90_000); // ~22.5k tokens — fits in 24k
|
|
124
|
+
const config = makeConfig({
|
|
125
|
+
dailyLogs: [
|
|
126
|
+
{ date: "2026-03-18", content: "today" },
|
|
127
|
+
{ date: "2026-03-17", content: dailyContent },
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
const recallText = "R".repeat(20_000); // ~5k tokens — fits in 8k min
|
|
131
|
+
const prompt = buildTeammatePrompt(config, "task", {
|
|
132
|
+
recallResults: [
|
|
133
|
+
{ teammate: "beacon", uri: "memory/decision_foo.md", text: recallText, score: 0.9, contentType: "typed_memory" },
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
expect(prompt).toContain("2026-03-17");
|
|
137
|
+
expect(prompt).toContain("## Relevant Memories");
|
|
138
|
+
});
|
|
139
|
+
it("recall gets unused daily log budget", () => {
|
|
140
|
+
// Small daily logs leave most of 24k unused — recall gets the surplus.
|
|
141
|
+
const config = makeConfig({
|
|
142
|
+
dailyLogs: [
|
|
143
|
+
{ date: "2026-03-18", content: "today" },
|
|
144
|
+
{ date: "2026-03-17", content: "short day 2" }, // ~3 tokens
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
// Large recall result — should fit because daily logs barely used any budget
|
|
148
|
+
const recallText = "R".repeat(80_000); // ~20k tokens — fits in (8k + ~24k unused)
|
|
149
|
+
const prompt = buildTeammatePrompt(config, "task", {
|
|
150
|
+
recallResults: [
|
|
151
|
+
{ teammate: "beacon", uri: "memory/big.md", text: recallText, score: 0.9, contentType: "typed_memory" },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
expect(prompt).toContain("## Relevant Memories");
|
|
155
|
+
expect(prompt).toContain("memory/big.md");
|
|
156
|
+
});
|
|
157
|
+
it("weekly summaries are excluded (indexed by recall)", () => {
|
|
158
|
+
const config = makeConfig({
|
|
159
|
+
dailyLogs: [{ date: "2026-03-13", content: "short log" }],
|
|
160
|
+
weeklyLogs: [{ week: "2026-W11", content: "short summary" }],
|
|
161
|
+
});
|
|
162
|
+
const prompt = buildTeammatePrompt(config, "task");
|
|
163
|
+
expect(prompt).toContain("## Recent Daily Logs");
|
|
164
|
+
expect(prompt).not.toContain("## Recent Weekly Summaries");
|
|
165
|
+
});
|
|
166
|
+
it("excludes task prompt from budget calculation", () => {
|
|
167
|
+
// Large task prompt should not trigger trimming of wrapper sections
|
|
168
|
+
const bigTask = "x".repeat(100_000);
|
|
169
|
+
const config = makeConfig({
|
|
170
|
+
dailyLogs: [{ date: "2026-03-13", content: "small log" }],
|
|
171
|
+
});
|
|
172
|
+
const prompt = buildTeammatePrompt(config, bigTask);
|
|
173
|
+
// Daily logs should still be included despite the huge task
|
|
174
|
+
expect(prompt).toContain("## Recent Daily Logs");
|
|
175
|
+
expect(prompt).toContain("small log");
|
|
176
|
+
});
|
|
102
177
|
});
|
|
103
178
|
describe("formatHandoffContext", () => {
|
|
104
179
|
it("formats basic handoff", () => {
|
|
@@ -140,16 +140,6 @@ export class CliProxyAdapter {
|
|
|
140
140
|
// If the teammate has no soul (e.g. the raw agent), skip identity/memory
|
|
141
141
|
// wrapping but include handoff instructions so it can delegate to teammates
|
|
142
142
|
const sessionFile = this.sessionFiles.get(teammate.name);
|
|
143
|
-
// Read session file content for injection into the prompt
|
|
144
|
-
let sessionContent;
|
|
145
|
-
if (sessionFile) {
|
|
146
|
-
try {
|
|
147
|
-
sessionContent = await readFile(sessionFile, "utf-8");
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
// Session file may not exist yet — that's fine
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
143
|
let fullPrompt;
|
|
154
144
|
if (teammate.soul) {
|
|
155
145
|
// Query recall for relevant memories before building prompt
|
|
@@ -159,12 +149,22 @@ export class CliProxyAdapter {
|
|
|
159
149
|
const recall = teammatesDir
|
|
160
150
|
? await queryRecallContext(teammatesDir, teammate.name, prompt)
|
|
161
151
|
: undefined;
|
|
152
|
+
// Read USER.md for injection into the prompt
|
|
153
|
+
let userProfile;
|
|
154
|
+
if (teammatesDir) {
|
|
155
|
+
try {
|
|
156
|
+
userProfile = await readFile(join(teammatesDir, "USER.md"), "utf-8");
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// USER.md may not exist yet — that's fine
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
162
|
fullPrompt = buildTeammatePrompt(teammate, prompt, {
|
|
163
163
|
roster: this.roster,
|
|
164
164
|
services: this.services,
|
|
165
165
|
sessionFile,
|
|
166
|
-
sessionContent,
|
|
167
166
|
recallResults: recall?.results,
|
|
167
|
+
userProfile,
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
170
|
else {
|
|
@@ -230,6 +230,7 @@ export class CliProxyAdapter {
|
|
|
230
230
|
const command = this.options.commandPath ?? this.preset.command;
|
|
231
231
|
const args = this.preset.buildArgs({ promptFile, prompt }, {
|
|
232
232
|
name: "_router",
|
|
233
|
+
type: "ai",
|
|
233
234
|
role: "",
|
|
234
235
|
soul: "",
|
|
235
236
|
wisdom: "",
|
package/dist/adapters/copilot.js
CHANGED
|
@@ -59,16 +59,6 @@ export class CopilotAdapter {
|
|
|
59
59
|
async executeTask(_sessionId, teammate, prompt) {
|
|
60
60
|
await this.ensureClient(teammate.cwd);
|
|
61
61
|
const sessionFile = this.sessionFiles.get(teammate.name);
|
|
62
|
-
// Read session file content for injection into the prompt
|
|
63
|
-
let sessionContent;
|
|
64
|
-
if (sessionFile) {
|
|
65
|
-
try {
|
|
66
|
-
sessionContent = await readFile(sessionFile, "utf-8");
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
// Session file may not exist yet — that's fine
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
62
|
// Build the full teammate prompt (identity + memory + task)
|
|
73
63
|
let fullPrompt;
|
|
74
64
|
if (teammate.soul) {
|
|
@@ -79,12 +69,22 @@ export class CopilotAdapter {
|
|
|
79
69
|
const recall = teammatesDir
|
|
80
70
|
? await queryRecallContext(teammatesDir, teammate.name, prompt)
|
|
81
71
|
: undefined;
|
|
72
|
+
// Read USER.md for injection into the prompt
|
|
73
|
+
let userProfile;
|
|
74
|
+
if (teammatesDir) {
|
|
75
|
+
try {
|
|
76
|
+
userProfile = await readFile(join(teammatesDir, "USER.md"), "utf-8");
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// USER.md may not exist yet — that's fine
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
82
|
fullPrompt = buildTeammatePrompt(teammate, prompt, {
|
|
83
83
|
roster: this.roster,
|
|
84
84
|
services: this.services,
|
|
85
85
|
sessionFile,
|
|
86
|
-
sessionContent,
|
|
87
86
|
recallResults: recall?.results,
|
|
87
|
+
userProfile,
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
else {
|
package/dist/banner.d.ts
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
* Animated startup banner for @teammates/cli.
|
|
3
3
|
*/
|
|
4
4
|
import { type Constraint, Control, type DrawingContext, type Rect, type Size } from "@teammates/consolonia";
|
|
5
|
+
import type { PresenceState } from "./types.js";
|
|
5
6
|
export type ServiceStatus = "bundled" | "missing" | "not-configured" | "configured";
|
|
6
7
|
export interface ServiceInfo {
|
|
7
8
|
name: string;
|
|
8
9
|
status: ServiceStatus;
|
|
9
10
|
}
|
|
10
11
|
export interface BannerInfo {
|
|
11
|
-
|
|
12
|
+
/** Display name shown in the banner (user alias or adapter name). */
|
|
13
|
+
displayName: string;
|
|
12
14
|
teammateCount: number;
|
|
13
15
|
cwd: string;
|
|
14
16
|
teammates: {
|
|
15
17
|
name: string;
|
|
16
18
|
role: string;
|
|
19
|
+
presence: PresenceState;
|
|
17
20
|
}[];
|
|
18
21
|
services: ServiceInfo[];
|
|
19
22
|
}
|
|
@@ -66,6 +69,8 @@ export declare class AnimatedBanner extends Control {
|
|
|
66
69
|
* If the animation already reached the hold point, it resumes immediately.
|
|
67
70
|
*/
|
|
68
71
|
releaseHold(): void;
|
|
72
|
+
/** Update service statuses and rebuild the banner lines. */
|
|
73
|
+
updateServices(services: ServiceInfo[]): void;
|
|
69
74
|
/** Cancel any pending animation timer. */
|
|
70
75
|
dispose(): void;
|
|
71
76
|
measure(constraint: Constraint): Size;
|
package/dist/banner.js
CHANGED
|
@@ -72,7 +72,7 @@ export class AnimatedBanner extends Control {
|
|
|
72
72
|
const gap = " ";
|
|
73
73
|
const lines = [];
|
|
74
74
|
// TM logo row 1 + adapter info
|
|
75
|
-
lines.push(concat(tp.accent(tmTop), tp.text(gap + info.
|
|
75
|
+
lines.push(concat(tp.accent(tmTop), tp.text(gap + info.displayName), tp.muted(` · ${info.teammateCount} teammate${info.teammateCount === 1 ? "" : "s"}`), tp.muted(` · v${PKG_VERSION}`)));
|
|
76
76
|
// TM logo row 2 + cwd
|
|
77
77
|
lines.push(concat(tp.accent(tmBot), tp.muted(gap + info.cwd)));
|
|
78
78
|
// Service status rows
|
|
@@ -92,9 +92,14 @@ export class AnimatedBanner extends Control {
|
|
|
92
92
|
// blank
|
|
93
93
|
lines.push("");
|
|
94
94
|
this._rosterStart = lines.length;
|
|
95
|
-
// Teammate roster
|
|
95
|
+
// Teammate roster (with presence indicators)
|
|
96
96
|
for (const t of info.teammates) {
|
|
97
|
-
|
|
97
|
+
const presenceDot = t.presence === "online"
|
|
98
|
+
? tp.success(" ● ")
|
|
99
|
+
: t.presence === "reachable"
|
|
100
|
+
? tp.warning(" ● ")
|
|
101
|
+
: tp.error(" ● ");
|
|
102
|
+
lines.push(concat(presenceDot, tp.accent(`@${t.name}`.padEnd(14)), tp.muted(t.role)));
|
|
98
103
|
}
|
|
99
104
|
// blank
|
|
100
105
|
lines.push("");
|
|
@@ -261,6 +266,16 @@ export class AnimatedBanner extends Control {
|
|
|
261
266
|
this._schedule(80);
|
|
262
267
|
}
|
|
263
268
|
}
|
|
269
|
+
/** Update service statuses and rebuild the banner lines. */
|
|
270
|
+
updateServices(services) {
|
|
271
|
+
this._info.services = services;
|
|
272
|
+
this._buildFinalLines();
|
|
273
|
+
// If animation is done, refresh immediately
|
|
274
|
+
if (this._phase === "done") {
|
|
275
|
+
this._lines = this._finalLines;
|
|
276
|
+
this._apply();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
264
279
|
/** Cancel any pending animation timer. */
|
|
265
280
|
dispose() {
|
|
266
281
|
if (this._timer) {
|
package/dist/cli-args.js
CHANGED
|
@@ -102,7 +102,6 @@ ${chalk.bold("@teammates/cli")} — Agent-agnostic teammate orchestrator
|
|
|
102
102
|
|
|
103
103
|
${chalk.bold("Usage:")}
|
|
104
104
|
teammates <agent> Launch session with an agent
|
|
105
|
-
teammates claude Use Claude Code
|
|
106
105
|
teammates codex Use OpenAI Codex
|
|
107
106
|
teammates aider Use Aider
|
|
108
107
|
|