@teammates/cli 0.5.0 → 0.5.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/dist/adapter.d.ts +8 -0
- package/dist/adapter.js +147 -108
- package/dist/adapter.test.js +29 -16
- package/dist/adapters/cli-proxy.js +58 -2
- package/dist/adapters/copilot.js +11 -1
- package/dist/adapters/echo.js +3 -1
- package/dist/banner.js +5 -1
- package/dist/cli-args.js +23 -23
- package/dist/cli-args.test.d.ts +1 -0
- package/dist/cli-args.test.js +125 -0
- package/dist/cli.js +135 -102
- package/dist/compact.d.ts +23 -0
- package/dist/compact.js +181 -11
- package/dist/compact.test.js +323 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/onboard.js +165 -165
- package/dist/orchestrator.js +4 -1
- package/dist/personas.test.d.ts +1 -0
- package/dist/personas.test.js +88 -0
- package/dist/registry.test.js +23 -23
- package/dist/theme.test.d.ts +1 -0
- package/dist/theme.test.js +113 -0
- package/package.json +3 -3
- package/personas/architect.md +4 -0
- package/personas/backend.md +4 -0
- package/personas/data-engineer.md +4 -0
- package/personas/designer.md +4 -0
- package/personas/devops.md +4 -0
- package/personas/frontend.md +4 -0
- package/personas/ml-ai.md +4 -0
- package/personas/mobile.md +4 -0
- package/personas/performance.md +4 -0
- package/personas/pm.md +4 -0
- package/personas/prompt-engineer.md +122 -0
- package/personas/qa.md +4 -0
- package/personas/security.md +4 -0
- package/personas/sre.md +4 -0
- package/personas/swe.md +4 -0
- package/personas/tech-writer.md +4 -0
package/dist/adapter.d.ts
CHANGED
|
@@ -75,6 +75,14 @@ export declare function queryRecallContext(teammatesDir: string, teammateName: s
|
|
|
75
75
|
* Wrapper around the recall library's Indexer.
|
|
76
76
|
*/
|
|
77
77
|
export declare function syncRecallIndex(teammatesDir: string, teammate?: string): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Context budget allocation:
|
|
80
|
+
* - Days 2-7 get up to DAILY_LOG_BUDGET tokens (whole entries)
|
|
81
|
+
* - Recall gets at least RECALL_MIN_BUDGET, plus whatever daily logs didn't use
|
|
82
|
+
* - Last recall entry can push total up to budget + RECALL_OVERFLOW (4k grace)
|
|
83
|
+
* - Weekly summaries are excluded (already indexed by recall)
|
|
84
|
+
*/
|
|
85
|
+
export declare const DAILY_LOG_BUDGET_TOKENS = 24000;
|
|
78
86
|
/**
|
|
79
87
|
* Build the full prompt for a teammate session.
|
|
80
88
|
* Includes identity, memory, roster, output protocol, and the task.
|
package/dist/adapter.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* and translates between the orchestrator's protocol and the agent's native API.
|
|
7
7
|
*/
|
|
8
8
|
import { platform } from "node:os";
|
|
9
|
-
import {
|
|
9
|
+
import { buildQueryVariations, Indexer, matchMemoryCatalog, multiSearch, } from "@teammates/recall";
|
|
10
10
|
/**
|
|
11
11
|
* Query the recall index for context relevant to the task prompt.
|
|
12
12
|
*
|
|
@@ -58,23 +58,16 @@ export async function syncRecallIndex(teammatesDir, teammate) {
|
|
|
58
58
|
await indexer.syncAll();
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Default token budget for the prompt wrapper (everything except the task).
|
|
63
|
-
* ~64k tokens ≈ 256k chars at ~4 chars/token.
|
|
64
|
-
* The task prompt itself is excluded from this budget — if a user pastes
|
|
65
|
-
* a large input, that's intentional and we don't trim it.
|
|
66
|
-
*/
|
|
67
|
-
const DEFAULT_TOKEN_BUDGET = 64_000;
|
|
61
|
+
/** Approximate chars per token for budget estimation. */
|
|
68
62
|
const CHARS_PER_TOKEN = 4;
|
|
69
63
|
/**
|
|
70
64
|
* Context budget allocation:
|
|
71
65
|
* - Days 2-7 get up to DAILY_LOG_BUDGET tokens (whole entries)
|
|
72
66
|
* - Recall gets at least RECALL_MIN_BUDGET, plus whatever daily logs didn't use
|
|
73
|
-
* - Last recall entry can push total up to
|
|
67
|
+
* - Last recall entry can push total up to budget + RECALL_OVERFLOW (4k grace)
|
|
74
68
|
* - Weekly summaries are excluded (already indexed by recall)
|
|
75
69
|
*/
|
|
76
|
-
const
|
|
77
|
-
const DAILY_LOG_BUDGET_TOKENS = 24_000;
|
|
70
|
+
export const DAILY_LOG_BUDGET_TOKENS = 24_000;
|
|
78
71
|
const RECALL_MIN_BUDGET_TOKENS = 8_000;
|
|
79
72
|
const RECALL_OVERFLOW_TOKENS = 4_000;
|
|
80
73
|
/** Estimate tokens from character count. */
|
|
@@ -97,57 +90,98 @@ function estimateTokens(text) {
|
|
|
97
90
|
*/
|
|
98
91
|
export function buildTeammatePrompt(teammate, taskPrompt, options) {
|
|
99
92
|
const parts = [];
|
|
100
|
-
// ──
|
|
101
|
-
|
|
102
|
-
|
|
93
|
+
// ── Top edge (high attention) ─────────────────────────────────────
|
|
94
|
+
// <IDENTITY> — anchors persona
|
|
95
|
+
parts.push(`<IDENTITY>\n# You are ${teammate.name}\n\n${teammate.soul}\n`);
|
|
96
|
+
// <WISDOM> — stable knowledge
|
|
103
97
|
if (teammate.wisdom.trim()) {
|
|
104
|
-
parts.push(
|
|
98
|
+
parts.push(`<WISDOM>\n${teammate.wisdom}\n`);
|
|
99
|
+
}
|
|
100
|
+
// ── Reference data (middle — acceptable for "lost in the middle") ──
|
|
101
|
+
// <TEAM> — roster for handoffs
|
|
102
|
+
if (options?.roster && options.roster.length > 0) {
|
|
103
|
+
const lines = [
|
|
104
|
+
"<TEAM>",
|
|
105
|
+
"These are the other teammates you can hand off work to:\n",
|
|
106
|
+
];
|
|
107
|
+
for (const t of options.roster) {
|
|
108
|
+
if (t.name === teammate.name)
|
|
109
|
+
continue;
|
|
110
|
+
const owns = t.ownership.primary.length > 0
|
|
111
|
+
? ` — owns: ${t.ownership.primary.join(", ")}`
|
|
112
|
+
: "";
|
|
113
|
+
lines.push(`- **@${t.name}**: ${t.role}${owns}`);
|
|
114
|
+
}
|
|
115
|
+
parts.push(`${lines.join("\n")}\n`);
|
|
116
|
+
}
|
|
117
|
+
// <SERVICES> — installed services
|
|
118
|
+
if (options?.services && options.services.length > 0) {
|
|
119
|
+
const lines = [
|
|
120
|
+
"<SERVICES>",
|
|
121
|
+
"These services are installed and available for you to use:\n",
|
|
122
|
+
];
|
|
123
|
+
for (const svc of options.services) {
|
|
124
|
+
lines.push(`### ${svc.name}\n`);
|
|
125
|
+
lines.push(svc.description);
|
|
126
|
+
lines.push(`\n**Usage:** \`${svc.usage}\`\n`);
|
|
127
|
+
}
|
|
128
|
+
parts.push(`${lines.join("\n")}\n`);
|
|
105
129
|
}
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
|
|
130
|
+
// <RECALL_TOOL> — Pass 2: agent-driven search
|
|
131
|
+
parts.push(`<RECALL_TOOL>\nYou can search your own memories mid-task for additional context. This is useful when the pre-loaded memories don't cover what you need.\n\n**Usage:** Run this command via your shell/terminal tool:\n\`\`\`\nteammates-recall search "<your query>" --dir .teammates --teammate ${teammate.name} --no-sync --json\n\`\`\`\n\n**Tips:**\n- Use specific, descriptive queries ("hooks lifecycle event naming decision" not "hooks")\n- Search iteratively: query → read result → refine query\n- The \`--json\` flag returns structured results for easier parsing\n- Results include a \`score\` field (0-1) — higher is more relevant\n- You can omit \`--teammate\` to search across all teammates' memories\n`);
|
|
132
|
+
// <ENVIRONMENT> — date/time + platform
|
|
133
|
+
const now = new Date();
|
|
134
|
+
const today = now.toISOString().slice(0, 10);
|
|
135
|
+
const os = platform();
|
|
136
|
+
const osLabel = os === "win32" ? "Windows" : os === "darwin" ? "macOS" : "Linux";
|
|
137
|
+
const slashNote = os === "win32"
|
|
138
|
+
? "Use backslashes (`\\`) in file paths."
|
|
139
|
+
: "Use forward slashes (`/`) in file paths.";
|
|
140
|
+
// Extract timezone from USER.md if available
|
|
141
|
+
const tzMatch = options?.userProfile?.match(/\*\*Primary Timezone:\*\*\s*(.+)/);
|
|
142
|
+
const userTimezone = tzMatch?.[1]?.trim();
|
|
143
|
+
const tzLine = userTimezone ? `\n**Timezone:** ${userTimezone}` : "";
|
|
144
|
+
parts.push(`<ENVIRONMENT>\n**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" })}${tzLine}\n**Environment:** ${osLabel} — ${slashNote}\n`);
|
|
145
|
+
// ── Session context (middle-to-lower) ─────────────────────────────
|
|
146
|
+
// <DAILY_LOGS> — today's log (never trimmed) + days 2-7 (budget-allocated)
|
|
110
147
|
const todayLog = teammate.dailyLogs.slice(0, 1);
|
|
111
148
|
const pastLogs = teammate.dailyLogs.slice(1, 7); // days 2-7
|
|
112
149
|
let dailyBudget = DAILY_LOG_BUDGET_TOKENS;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
150
|
+
if (todayLog.length > 0 || pastLogs.length > 0) {
|
|
151
|
+
const logLines = ["<DAILY_LOGS>"];
|
|
152
|
+
// Current daily log (today) — never trimmed, always included
|
|
116
153
|
for (const log of todayLog) {
|
|
117
|
-
|
|
154
|
+
logLines.push(`### ${log.date}\n${log.content}`);
|
|
118
155
|
}
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
// Days 2-7 — whole entries, up to 24k tokens
|
|
122
|
-
if (pastLogs.length > 0) {
|
|
123
|
-
const lines = [];
|
|
156
|
+
// Days 2-7 — whole entries, up to 24k tokens
|
|
124
157
|
for (const log of pastLogs) {
|
|
125
|
-
const entry = `### ${log.date}\n${log.content}
|
|
158
|
+
const entry = `### ${log.date}\n${log.content}`;
|
|
126
159
|
const cost = estimateTokens(entry);
|
|
127
160
|
if (cost > dailyBudget)
|
|
128
161
|
break;
|
|
129
|
-
|
|
162
|
+
logLines.push(entry);
|
|
130
163
|
dailyBudget -= cost;
|
|
131
164
|
}
|
|
132
|
-
|
|
133
|
-
|
|
165
|
+
parts.push(`${logLines.join("\n")}\n`);
|
|
166
|
+
}
|
|
167
|
+
// <USER_PROFILE> — always included when present
|
|
168
|
+
if (options?.userProfile?.trim()) {
|
|
169
|
+
parts.push(`<USER_PROFILE>\n${options.userProfile.trim()}\n`);
|
|
134
170
|
}
|
|
135
|
-
//
|
|
136
|
-
//
|
|
171
|
+
// ── Task-adjacent context (close to task for maximum relevance) ───
|
|
172
|
+
// <RECALL_RESULTS> — budget-allocated, adjacent to task
|
|
137
173
|
const recallBudget = Math.max(RECALL_MIN_BUDGET_TOKENS, RECALL_MIN_BUDGET_TOKENS + dailyBudget);
|
|
138
174
|
const recallResults = options?.recallResults ?? [];
|
|
139
175
|
if (recallResults.length > 0) {
|
|
140
176
|
const lines = [
|
|
141
|
-
"
|
|
177
|
+
"<RECALL_RESULTS>",
|
|
142
178
|
"These memories were retrieved based on relevance to the current task:\n",
|
|
143
179
|
];
|
|
144
180
|
const headerCost = estimateTokens(lines.join("\n"));
|
|
145
181
|
let recallUsed = headerCost;
|
|
146
182
|
for (const r of recallResults) {
|
|
147
|
-
const label = r.contentType
|
|
148
|
-
|
|
149
|
-
: r.uri;
|
|
150
|
-
const entry = `### ${label}\n${r.text}\n`;
|
|
183
|
+
const label = r.contentType ? `[${r.contentType}] ${r.uri}` : r.uri;
|
|
184
|
+
const entry = `### ${label}\n${r.text}`;
|
|
151
185
|
const cost = estimateTokens(entry);
|
|
152
186
|
if (recallUsed + cost > recallBudget + RECALL_OVERFLOW_TOKENS)
|
|
153
187
|
break;
|
|
@@ -158,85 +192,90 @@ export function buildTeammatePrompt(teammate, taskPrompt, options) {
|
|
|
158
192
|
break;
|
|
159
193
|
}
|
|
160
194
|
if (lines.length > 2) {
|
|
161
|
-
|
|
162
|
-
parts.push(lines.join("\n"));
|
|
195
|
+
parts.push(`${lines.join("\n")}\n`);
|
|
163
196
|
}
|
|
164
197
|
}
|
|
165
|
-
//
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
198
|
+
// <HANDOFF_CONTEXT> — directly task-relevant when present
|
|
199
|
+
if (options?.handoffContext) {
|
|
200
|
+
parts.push(`<HANDOFF_CONTEXT>\n${options.handoffContext}\n`);
|
|
201
|
+
}
|
|
202
|
+
// ── The question ──────────────────────────────────────────────────
|
|
203
|
+
// <TASK> — always included, excluded from budget
|
|
204
|
+
parts.push(`<TASK>\n${taskPrompt}\n`);
|
|
205
|
+
// ── Bottom edge (high attention) — all instructions merged ────────
|
|
206
|
+
// <INSTRUCTIONS> — output protocol, handoffs, session state, memory updates
|
|
207
|
+
const instrLines = [
|
|
208
|
+
"<INSTRUCTIONS>",
|
|
209
|
+
"",
|
|
210
|
+
"### Output Protocol (CRITICAL)",
|
|
211
|
+
"",
|
|
212
|
+
"**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.",
|
|
213
|
+
"",
|
|
214
|
+
"Format your response as:",
|
|
215
|
+
"",
|
|
216
|
+
"```",
|
|
217
|
+
"TO: user",
|
|
218
|
+
"# <Subject line>",
|
|
219
|
+
"",
|
|
220
|
+
"<Body — full markdown response>",
|
|
221
|
+
"```",
|
|
222
|
+
"",
|
|
223
|
+
"**Rules:**",
|
|
224
|
+
"- **You MUST end your turn with visible text output.** A turn that ends with only tool calls and no text is a failed turn.",
|
|
225
|
+
"- The `# Subject` line is REQUIRED — it becomes the message title.",
|
|
226
|
+
"- Always write a substantive body. Never return just the subject.",
|
|
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
|
+
"",
|
|
230
|
+
"### Handoffs",
|
|
231
|
+
"",
|
|
232
|
+
"To delegate work to a teammate, you MUST include a fenced code block with the language tag `handoff` in your text output. **This is the ONLY way to trigger a handoff.** Mentioning a handoff in plain English does NOT work — the system parses the fenced block, not your prose.",
|
|
233
|
+
"",
|
|
234
|
+
"Exact format (include the triple backticks exactly as shown):",
|
|
235
|
+
"",
|
|
236
|
+
" ```handoff",
|
|
237
|
+
" @<teammate-name>",
|
|
238
|
+
" <task description with full context>",
|
|
239
|
+
" ```",
|
|
240
|
+
"",
|
|
241
|
+
"Rules:",
|
|
242
|
+
`- Only hand off to teammates listed in \`<TEAM>\`.`,
|
|
243
|
+
"- Do as much work as you can BEFORE handing off.",
|
|
244
|
+
'- Do NOT just say "I\'ll hand this off" in prose — that does nothing. You MUST use the fenced block.',
|
|
245
|
+
];
|
|
246
|
+
// Session state (conditional)
|
|
247
|
+
if (options?.sessionFile) {
|
|
248
|
+
instrLines.push("", "### Session State", "", `Your session file is at: \`${options.sessionFile}\``, "", "**After writing your text response**, 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
|
+
}
|
|
250
|
+
// Memory updates
|
|
251
|
+
instrLines.push("", "### Memory Updates", "", "**After writing your text response**, 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
|
+
// Section Reinforcement — back-references from high-attention bottom edge to each section tag
|
|
253
|
+
instrLines.push("", "### Section Reinforcement", "");
|
|
254
|
+
instrLines.push("- Stay in character as defined in `<IDENTITY>` — never break persona or speak as a generic assistant.");
|
|
255
|
+
if (teammate.wisdom.trim()) {
|
|
256
|
+
instrLines.push("- Apply lessons from `<WISDOM>` before proposing solutions — do not repeat past mistakes.");
|
|
171
257
|
}
|
|
172
|
-
// ── Team roster (required, small) ───────────────────────────────
|
|
173
258
|
if (options?.roster && options.roster.length > 0) {
|
|
174
|
-
|
|
175
|
-
"## Your Team\n",
|
|
176
|
-
"These are the other teammates you can hand off work to:\n",
|
|
177
|
-
];
|
|
178
|
-
for (const t of options.roster) {
|
|
179
|
-
if (t.name === teammate.name)
|
|
180
|
-
continue;
|
|
181
|
-
const owns = t.ownership.primary.length > 0
|
|
182
|
-
? ` — owns: ${t.ownership.primary.join(", ")}`
|
|
183
|
-
: "";
|
|
184
|
-
lines.push(`- **@${t.name}**: ${t.role}${owns}`);
|
|
185
|
-
}
|
|
186
|
-
lines.push("\n---\n");
|
|
187
|
-
parts.push(lines.join("\n"));
|
|
259
|
+
instrLines.push("- Only hand off to teammates listed in `<TEAM>` using the handoff block format above.");
|
|
188
260
|
}
|
|
189
|
-
// ── Installed services (required, small) ────────────────────────
|
|
190
261
|
if (options?.services && options.services.length > 0) {
|
|
191
|
-
|
|
192
|
-
"## Available Services\n",
|
|
193
|
-
"These services are installed and available for you to use:\n",
|
|
194
|
-
];
|
|
195
|
-
for (const svc of options.services) {
|
|
196
|
-
lines.push(`### ${svc.name}\n`);
|
|
197
|
-
lines.push(svc.description);
|
|
198
|
-
lines.push(`\n**Usage:** \`${svc.usage}\`\n`);
|
|
199
|
-
}
|
|
200
|
-
lines.push("\n---\n");
|
|
201
|
-
parts.push(lines.join("\n"));
|
|
262
|
+
instrLines.push("- Use tools and services from `<SERVICES>` when they fit the task — do not reinvent what is already available.");
|
|
202
263
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// ── Handoff context (required when present) ─────────────────────
|
|
207
|
-
if (options?.handoffContext) {
|
|
208
|
-
parts.push(`## Handoff Context\n\n${options.handoffContext}\n\n---\n`);
|
|
209
|
-
}
|
|
210
|
-
// ── Output protocol (required — BEFORE session/memory updates) ──
|
|
211
|
-
// Placed first so agents produce text response before doing housekeeping.
|
|
212
|
-
// When output protocol is after memory updates, agents often end their turn
|
|
213
|
-
// with file edits and produce no visible text output.
|
|
214
|
-
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- **Write your text response FIRST, then update session/memory files.** This ensures visible output even if the agent turn ends early.\n\n---\n`);
|
|
215
|
-
// ── Session state (required) ────────────────────────────────────
|
|
216
|
-
if (options?.sessionFile) {
|
|
217
|
-
parts.push(`## Session State\n\nYour session file is at: \`${options.sessionFile}\`\n\n**After writing your text response**, 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`);
|
|
264
|
+
instrLines.push("- If pre-loaded context is insufficient, use `<RECALL_TOOL>` to search for additional memories before giving up.", "- Respect platform, date, and path conventions from `<ENVIRONMENT>`.");
|
|
265
|
+
if (todayLog.length > 0 || pastLogs.length > 0) {
|
|
266
|
+
instrLines.push("- Check `<DAILY_LOGS>` for prior work on this topic before starting — avoid duplicating what was already done today.");
|
|
218
267
|
}
|
|
219
|
-
// ── Memory updates (required) ───────────────────────────────────
|
|
220
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
221
|
-
parts.push(`## Memory Updates\n\n**After writing your text response**, 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`);
|
|
222
|
-
// ── Current date/time + environment (required, small) ───────────
|
|
223
|
-
const now = new Date();
|
|
224
|
-
const os = platform();
|
|
225
|
-
const osLabel = os === "win32" ? "Windows" : os === "darwin" ? "macOS" : "Linux";
|
|
226
|
-
const slashNote = os === "win32"
|
|
227
|
-
? "Use backslashes (`\\`) in file paths."
|
|
228
|
-
: "Use forward slashes (`/`) in file paths.";
|
|
229
|
-
// Extract timezone from USER.md if available
|
|
230
|
-
const tzMatch = options?.userProfile?.match(/\*\*Primary Timezone:\*\*\s*(.+)/);
|
|
231
|
-
const userTimezone = tzMatch?.[1]?.trim();
|
|
232
|
-
const tzLine = userTimezone ? `\n**Timezone:** ${userTimezone}` : "";
|
|
233
|
-
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" })}${tzLine}\n**Environment:** ${osLabel} — ${slashNote}\n\n---\n`);
|
|
234
|
-
// ── User profile (always included when present) ────────────────
|
|
235
268
|
if (options?.userProfile?.trim()) {
|
|
236
|
-
|
|
269
|
+
instrLines.push("- Honor the user's role, preferences, and communication style from `<USER_PROFILE>`.");
|
|
270
|
+
}
|
|
271
|
+
if (recallResults.length > 0) {
|
|
272
|
+
instrLines.push("- Incorporate relevant context from `<RECALL_RESULTS>` into your response — these memories were retrieved for a reason.");
|
|
273
|
+
}
|
|
274
|
+
if (options?.handoffContext) {
|
|
275
|
+
instrLines.push("- When `<HANDOFF_CONTEXT>` is present, address its requirements and open questions directly.");
|
|
237
276
|
}
|
|
238
|
-
|
|
239
|
-
parts.push(
|
|
277
|
+
instrLines.push("- Your response must answer `<TASK>` — everything else is supporting context.", "", "**REMINDER: Write your text response (TO: user) FIRST, then update session/memory files. A turn with only file edits and no text output is a failed turn.**");
|
|
278
|
+
parts.push(instrLines.join("\n"));
|
|
240
279
|
return parts.join("\n");
|
|
241
280
|
}
|
|
242
281
|
/**
|
package/dist/adapter.test.js
CHANGED
|
@@ -25,27 +25,28 @@ describe("buildTeammatePrompt", () => {
|
|
|
25
25
|
});
|
|
26
26
|
it("includes the task", () => {
|
|
27
27
|
const prompt = buildTeammatePrompt(makeConfig(), "fix the bug");
|
|
28
|
-
expect(prompt).toContain("
|
|
28
|
+
expect(prompt).toContain("<TASK>");
|
|
29
29
|
expect(prompt).toContain("fix the bug");
|
|
30
30
|
});
|
|
31
31
|
it("includes output protocol", () => {
|
|
32
32
|
const prompt = buildTeammatePrompt(makeConfig(), "task");
|
|
33
|
-
expect(prompt).toContain("
|
|
33
|
+
expect(prompt).toContain("<INSTRUCTIONS>");
|
|
34
|
+
expect(prompt).toContain("Output Protocol");
|
|
34
35
|
expect(prompt).toContain("TO: user");
|
|
35
36
|
expect(prompt).toContain("```handoff");
|
|
36
37
|
});
|
|
37
38
|
it("includes memory updates section", () => {
|
|
38
39
|
const prompt = buildTeammatePrompt(makeConfig(), "task");
|
|
39
|
-
expect(prompt).toContain("
|
|
40
|
+
expect(prompt).toContain("### Memory Updates");
|
|
40
41
|
expect(prompt).toContain(".teammates/beacon/memory/");
|
|
41
42
|
});
|
|
42
43
|
it("skips wisdom section when empty", () => {
|
|
43
44
|
const prompt = buildTeammatePrompt(makeConfig({ wisdom: "" }), "task");
|
|
44
|
-
expect(prompt).not.toContain("
|
|
45
|
+
expect(prompt).not.toContain("<WISDOM>");
|
|
45
46
|
});
|
|
46
47
|
it("includes wisdom when present", () => {
|
|
47
48
|
const prompt = buildTeammatePrompt(makeConfig({ wisdom: "Some important wisdom" }), "task");
|
|
48
|
-
expect(prompt).toContain("
|
|
49
|
+
expect(prompt).toContain("<WISDOM>");
|
|
49
50
|
expect(prompt).toContain("Some important wisdom");
|
|
50
51
|
});
|
|
51
52
|
it("includes daily logs (up to 7)", () => {
|
|
@@ -60,7 +61,7 @@ describe("buildTeammatePrompt", () => {
|
|
|
60
61
|
{ date: "2026-03-06", content: "Should be excluded" },
|
|
61
62
|
];
|
|
62
63
|
const prompt = buildTeammatePrompt(makeConfig({ dailyLogs: logs }), "task");
|
|
63
|
-
expect(prompt).toContain("
|
|
64
|
+
expect(prompt).toContain("<DAILY_LOGS>");
|
|
64
65
|
expect(prompt).toContain("2026-03-13");
|
|
65
66
|
expect(prompt).toContain("2026-03-07");
|
|
66
67
|
expect(prompt).not.toContain("2026-03-06");
|
|
@@ -80,7 +81,7 @@ describe("buildTeammatePrompt", () => {
|
|
|
80
81
|
},
|
|
81
82
|
];
|
|
82
83
|
const prompt = buildTeammatePrompt(makeConfig(), "task", { roster });
|
|
83
|
-
expect(prompt).toContain("
|
|
84
|
+
expect(prompt).toContain("<TEAM>");
|
|
84
85
|
expect(prompt).toContain("@scribe");
|
|
85
86
|
expect(prompt).toContain("Documentation writer.");
|
|
86
87
|
// Should not list self in roster
|
|
@@ -90,14 +91,14 @@ describe("buildTeammatePrompt", () => {
|
|
|
90
91
|
const prompt = buildTeammatePrompt(makeConfig(), "task", {
|
|
91
92
|
handoffContext: "Handed off from scribe with files changed",
|
|
92
93
|
});
|
|
93
|
-
expect(prompt).toContain("
|
|
94
|
+
expect(prompt).toContain("<HANDOFF_CONTEXT>");
|
|
94
95
|
expect(prompt).toContain("Handed off from scribe");
|
|
95
96
|
});
|
|
96
97
|
it("includes session file when provided", () => {
|
|
97
98
|
const prompt = buildTeammatePrompt(makeConfig(), "task", {
|
|
98
99
|
sessionFile: "/tmp/beacon-session.md",
|
|
99
100
|
});
|
|
100
|
-
expect(prompt).toContain("
|
|
101
|
+
expect(prompt).toContain("### Session State");
|
|
101
102
|
expect(prompt).toContain("/tmp/beacon-session.md");
|
|
102
103
|
});
|
|
103
104
|
it("drops daily logs that exceed the 24k daily budget", () => {
|
|
@@ -130,11 +131,17 @@ describe("buildTeammatePrompt", () => {
|
|
|
130
131
|
const recallText = "R".repeat(20_000); // ~5k tokens — fits in 8k min
|
|
131
132
|
const prompt = buildTeammatePrompt(config, "task", {
|
|
132
133
|
recallResults: [
|
|
133
|
-
{
|
|
134
|
+
{
|
|
135
|
+
teammate: "beacon",
|
|
136
|
+
uri: "memory/decision_foo.md",
|
|
137
|
+
text: recallText,
|
|
138
|
+
score: 0.9,
|
|
139
|
+
contentType: "typed_memory",
|
|
140
|
+
},
|
|
134
141
|
],
|
|
135
142
|
});
|
|
136
143
|
expect(prompt).toContain("2026-03-17");
|
|
137
|
-
expect(prompt).toContain("
|
|
144
|
+
expect(prompt).toContain("<RECALL_RESULTS>");
|
|
138
145
|
});
|
|
139
146
|
it("recall gets unused daily log budget", () => {
|
|
140
147
|
// Small daily logs leave most of 24k unused — recall gets the surplus.
|
|
@@ -148,10 +155,16 @@ describe("buildTeammatePrompt", () => {
|
|
|
148
155
|
const recallText = "R".repeat(80_000); // ~20k tokens — fits in (8k + ~24k unused)
|
|
149
156
|
const prompt = buildTeammatePrompt(config, "task", {
|
|
150
157
|
recallResults: [
|
|
151
|
-
{
|
|
158
|
+
{
|
|
159
|
+
teammate: "beacon",
|
|
160
|
+
uri: "memory/big.md",
|
|
161
|
+
text: recallText,
|
|
162
|
+
score: 0.9,
|
|
163
|
+
contentType: "typed_memory",
|
|
164
|
+
},
|
|
152
165
|
],
|
|
153
166
|
});
|
|
154
|
-
expect(prompt).toContain("
|
|
167
|
+
expect(prompt).toContain("<RECALL_RESULTS>");
|
|
155
168
|
expect(prompt).toContain("memory/big.md");
|
|
156
169
|
});
|
|
157
170
|
it("weekly summaries are excluded (indexed by recall)", () => {
|
|
@@ -160,8 +173,8 @@ describe("buildTeammatePrompt", () => {
|
|
|
160
173
|
weeklyLogs: [{ week: "2026-W11", content: "short summary" }],
|
|
161
174
|
});
|
|
162
175
|
const prompt = buildTeammatePrompt(config, "task");
|
|
163
|
-
expect(prompt).toContain("
|
|
164
|
-
expect(prompt).not.toContain("
|
|
176
|
+
expect(prompt).toContain("<DAILY_LOGS>");
|
|
177
|
+
expect(prompt).not.toContain("Weekly Summaries");
|
|
165
178
|
});
|
|
166
179
|
it("excludes task prompt from budget calculation", () => {
|
|
167
180
|
// Large task prompt should not trigger trimming of wrapper sections
|
|
@@ -171,7 +184,7 @@ describe("buildTeammatePrompt", () => {
|
|
|
171
184
|
});
|
|
172
185
|
const prompt = buildTeammatePrompt(config, bigTask);
|
|
173
186
|
// Daily logs should still be included despite the huge task
|
|
174
|
-
expect(prompt).toContain("
|
|
187
|
+
expect(prompt).toContain("<DAILY_LOGS>");
|
|
175
188
|
expect(prompt).toContain("small log");
|
|
176
189
|
});
|
|
177
190
|
});
|
|
@@ -20,7 +20,8 @@ import { mkdirSync } from "node:fs";
|
|
|
20
20
|
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
21
21
|
import { tmpdir } from "node:os";
|
|
22
22
|
import { join } from "node:path";
|
|
23
|
-
import { buildTeammatePrompt, queryRecallContext } from "../adapter.js";
|
|
23
|
+
import { DAILY_LOG_BUDGET_TOKENS, buildTeammatePrompt, queryRecallContext, } from "../adapter.js";
|
|
24
|
+
import { autoCompactForBudget } from "../compact.js";
|
|
24
25
|
export const PRESETS = {
|
|
25
26
|
claude: {
|
|
26
27
|
name: "claude",
|
|
@@ -152,6 +153,16 @@ export class CliProxyAdapter {
|
|
|
152
153
|
const recall = teammatesDir
|
|
153
154
|
? await queryRecallContext(teammatesDir, teammate.name, prompt)
|
|
154
155
|
: undefined;
|
|
156
|
+
// Auto-compact daily logs if they exceed the token budget
|
|
157
|
+
if (teammatesDir) {
|
|
158
|
+
const teammateDir = join(teammatesDir, teammate.name);
|
|
159
|
+
const compacted = await autoCompactForBudget(teammateDir, DAILY_LOG_BUDGET_TOKENS);
|
|
160
|
+
if (compacted) {
|
|
161
|
+
// Filter compacted dates out of in-memory daily logs
|
|
162
|
+
const compactedSet = new Set(compacted.compactedDates);
|
|
163
|
+
teammate.dailyLogs = teammate.dailyLogs.filter((log) => !compactedSet.has(log.date));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
155
166
|
// Read USER.md for injection into the prompt
|
|
156
167
|
let userProfile;
|
|
157
168
|
if (teammatesDir) {
|
|
@@ -481,8 +492,12 @@ function parseMessageProtocol(output, teammateName, _teammateNames) {
|
|
|
481
492
|
break;
|
|
482
493
|
}
|
|
483
494
|
}
|
|
484
|
-
// Find all ```handoff blocks
|
|
495
|
+
// Find all ```handoff blocks (primary) + natural-language fallback
|
|
485
496
|
const handoffBlocks = findHandoffBlocks(output);
|
|
497
|
+
if (handoffBlocks.length === 0) {
|
|
498
|
+
// Fallback: detect natural-language handoff patterns mentioning known teammates
|
|
499
|
+
handoffBlocks.push(...findNaturalLanguageHandoffs(output, _teammateNames));
|
|
500
|
+
}
|
|
486
501
|
const handoffs = handoffBlocks.map((h) => ({
|
|
487
502
|
from: teammateName,
|
|
488
503
|
to: h.target,
|
|
@@ -523,6 +538,47 @@ function findHandoffBlocks(output) {
|
|
|
523
538
|
}
|
|
524
539
|
return results;
|
|
525
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* Fallback handoff detector: catches natural-language handoff patterns when
|
|
543
|
+
* the agent fails to use the ```handoff fenced block format.
|
|
544
|
+
*
|
|
545
|
+
* Looks for sentences like:
|
|
546
|
+
* - "hand off to @beacon: implement the feature"
|
|
547
|
+
* - "handing this to @scribe for documentation"
|
|
548
|
+
* - "I'll delegate to @pipeline"
|
|
549
|
+
* - "queued a handoff to @beacon"
|
|
550
|
+
*
|
|
551
|
+
* Only triggers if the @mentioned name is in the known teammate list.
|
|
552
|
+
* Extracts the surrounding sentence as the task description.
|
|
553
|
+
*/
|
|
554
|
+
function findNaturalLanguageHandoffs(output, teammateNames) {
|
|
555
|
+
if (teammateNames.length === 0)
|
|
556
|
+
return [];
|
|
557
|
+
const results = [];
|
|
558
|
+
const seen = new Set();
|
|
559
|
+
// Pattern: handoff-related verb/noun near @teammate
|
|
560
|
+
const pattern = /(?:hand(?:off|ing off| off| this off)|delegat(?:e|ing)|pass(?:ing)? (?:this |it )?(?:to|off to)|queued? (?:a )?handoff (?:to|for))\s+@(\w+)\b[.:,]?\s*(.*)/gi;
|
|
561
|
+
let match;
|
|
562
|
+
while ((match = pattern.exec(output)) !== null) {
|
|
563
|
+
const target = match[1].toLowerCase();
|
|
564
|
+
if (!teammateNames.includes(target))
|
|
565
|
+
continue;
|
|
566
|
+
if (seen.has(target))
|
|
567
|
+
continue;
|
|
568
|
+
seen.add(target);
|
|
569
|
+
// Use the rest of the sentence as the task, or a generic description
|
|
570
|
+
let task = match[2]
|
|
571
|
+
.replace(/\n.*/s, "") // first line only
|
|
572
|
+
.replace(/[.!]+$/, "") // strip trailing punctuation
|
|
573
|
+
.trim();
|
|
574
|
+
if (!task || task.length < 5) {
|
|
575
|
+
task =
|
|
576
|
+
"(handoff detected from natural language — no task details provided)";
|
|
577
|
+
}
|
|
578
|
+
results.push({ target, task });
|
|
579
|
+
}
|
|
580
|
+
return results;
|
|
581
|
+
}
|
|
526
582
|
/** Extract file paths from agent output. */
|
|
527
583
|
export function parseChangedFiles(output) {
|
|
528
584
|
const files = new Set();
|
package/dist/adapters/copilot.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { approveAll, CopilotClient, } from "@github/copilot-sdk";
|
|
15
|
-
import { buildTeammatePrompt, queryRecallContext } from "../adapter.js";
|
|
15
|
+
import { DAILY_LOG_BUDGET_TOKENS, buildTeammatePrompt, queryRecallContext, } from "../adapter.js";
|
|
16
|
+
import { autoCompactForBudget } from "../compact.js";
|
|
16
17
|
import { parseResult } from "./cli-proxy.js";
|
|
17
18
|
// ─── Adapter ─────────────────────────────────────────────────────────
|
|
18
19
|
let nextId = 1;
|
|
@@ -72,6 +73,15 @@ export class CopilotAdapter {
|
|
|
72
73
|
const recall = teammatesDir
|
|
73
74
|
? await queryRecallContext(teammatesDir, teammate.name, prompt)
|
|
74
75
|
: undefined;
|
|
76
|
+
// Auto-compact daily logs if they exceed the token budget
|
|
77
|
+
if (teammatesDir) {
|
|
78
|
+
const teammateDir = join(teammatesDir, teammate.name);
|
|
79
|
+
const compacted = await autoCompactForBudget(teammateDir, DAILY_LOG_BUDGET_TOKENS);
|
|
80
|
+
if (compacted) {
|
|
81
|
+
const compactedSet = new Set(compacted.compactedDates);
|
|
82
|
+
teammate.dailyLogs = teammate.dailyLogs.filter((log) => !compactedSet.has(log.date));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
75
85
|
// Read USER.md for injection into the prompt
|
|
76
86
|
let userProfile;
|
|
77
87
|
if (teammatesDir) {
|
package/dist/adapters/echo.js
CHANGED
|
@@ -12,7 +12,9 @@ export class EchoAdapter {
|
|
|
12
12
|
return `echo-${teammate.name}-${nextId++}`;
|
|
13
13
|
}
|
|
14
14
|
async executeTask(_sessionId, teammate, prompt, options) {
|
|
15
|
-
const fullPrompt = options?.raw
|
|
15
|
+
const fullPrompt = options?.raw
|
|
16
|
+
? prompt
|
|
17
|
+
: buildTeammatePrompt(teammate, prompt);
|
|
16
18
|
return {
|
|
17
19
|
teammate: teammate.name,
|
|
18
20
|
success: true,
|
package/dist/banner.js
CHANGED
|
@@ -78,7 +78,11 @@ export class AnimatedBanner extends Control {
|
|
|
78
78
|
// Service status rows
|
|
79
79
|
for (const svc of info.services) {
|
|
80
80
|
const isBundledOrConfigured = svc.status === "bundled" || svc.status === "configured";
|
|
81
|
-
const icon = isBundledOrConfigured
|
|
81
|
+
const icon = isBundledOrConfigured
|
|
82
|
+
? "● "
|
|
83
|
+
: svc.status === "not-configured"
|
|
84
|
+
? "◐ "
|
|
85
|
+
: "○ ";
|
|
82
86
|
const color = isBundledOrConfigured ? tp.success : tp.warning;
|
|
83
87
|
const label = svc.status === "bundled"
|
|
84
88
|
? "bundled"
|