@teammates/cli 0.4.1 → 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.
Files changed (47) hide show
  1. package/README.md +36 -4
  2. package/dist/adapter.d.ts +19 -3
  3. package/dist/adapter.js +168 -96
  4. package/dist/adapter.test.js +29 -16
  5. package/dist/adapters/cli-proxy.d.ts +3 -1
  6. package/dist/adapters/cli-proxy.js +65 -6
  7. package/dist/adapters/copilot.d.ts +3 -1
  8. package/dist/adapters/copilot.js +16 -3
  9. package/dist/adapters/echo.d.ts +3 -1
  10. package/dist/adapters/echo.js +4 -2
  11. package/dist/banner.js +5 -1
  12. package/dist/cli-args.js +23 -23
  13. package/dist/cli-args.test.d.ts +1 -0
  14. package/dist/cli-args.test.js +125 -0
  15. package/dist/cli.js +486 -220
  16. package/dist/compact.d.ts +23 -0
  17. package/dist/compact.js +181 -11
  18. package/dist/compact.test.js +323 -7
  19. package/dist/index.d.ts +4 -1
  20. package/dist/index.js +3 -1
  21. package/dist/onboard.js +165 -165
  22. package/dist/orchestrator.js +7 -2
  23. package/dist/personas.d.ts +42 -0
  24. package/dist/personas.js +108 -0
  25. package/dist/personas.test.d.ts +1 -0
  26. package/dist/personas.test.js +88 -0
  27. package/dist/registry.test.js +23 -23
  28. package/dist/theme.test.d.ts +1 -0
  29. package/dist/theme.test.js +113 -0
  30. package/dist/types.d.ts +2 -0
  31. package/package.json +4 -3
  32. package/personas/architect.md +95 -0
  33. package/personas/backend.md +97 -0
  34. package/personas/data-engineer.md +96 -0
  35. package/personas/designer.md +96 -0
  36. package/personas/devops.md +97 -0
  37. package/personas/frontend.md +98 -0
  38. package/personas/ml-ai.md +100 -0
  39. package/personas/mobile.md +97 -0
  40. package/personas/performance.md +96 -0
  41. package/personas/pm.md +93 -0
  42. package/personas/prompt-engineer.md +122 -0
  43. package/personas/qa.md +96 -0
  44. package/personas/security.md +96 -0
  45. package/personas/sre.md +97 -0
  46. package/personas/swe.md +92 -0
  47. package/personas/tech-writer.md +97 -0
package/README.md CHANGED
@@ -164,19 +164,51 @@ class MyAdapter implements AgentAdapter {
164
164
 
165
165
  Or add a preset to `cli-proxy.ts` for any CLI agent that accepts a prompt and runs to completion.
166
166
 
167
+ ## Startup Lifecycle
168
+
169
+ The CLI startup runs in two phases:
170
+
171
+ **Phase 1 — Pre-TUI (console I/O)**
172
+ 1. **User profile setup** — Prompts for alias (required), name, role, experience, preferences, context. Creates `USER.md` and a user avatar folder at `.teammates/<alias>/` with `SOUL.md` (`**Type:** human`).
173
+ 2. **Team onboarding** (if no `.teammates/` exists) — Offers New team / Import / Solo / Exit. Onboarding agents run non-interactively to completion.
174
+ 3. **Orchestrator init** — Loads existing teammates from `.teammates/`, registers user avatar with `type: "human"` and `presence: "online"`.
175
+
176
+ **Phase 2 — TUI (Consolonia)**
177
+ 4. Animated startup banner with roster
178
+ 5. REPL starts — routing, slash commands, handoff approval
179
+
180
+ 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
+
182
+ ## Personas
183
+
184
+ The CLI ships with 15 built-in persona templates that serve as starting points when creating new teammates. Each persona file (`personas/*.md`) contains YAML frontmatter (name, default alias, tier, description) and a complete SOUL.md scaffold pre-filled with the role's identity, principles, quality bar, and ownership structure.
185
+
186
+ ### Tiers
187
+
188
+ | Tier | Personas |
189
+ |---|---|
190
+ | **1 — Core** | PM (`scribe`), SWE (`beacon`), DevOps (`pipeline`), QA (`sentinel`) |
191
+ | **2 — Specialist** | Security (`shield`), Designer (`canvas`), Tech Writer (`quill`), Data Engineer (`forge`), SRE (`watchtower`), Architect (`blueprint`) |
192
+ | **3 — Niche** | Frontend (`pixel`), Backend (`engine`), Mobile (`orbit`), ML/AI (`neuron`), Performance (`tempo`) |
193
+
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.
195
+
167
196
  ## Architecture
168
197
 
169
198
  ```
170
199
  cli/src/
171
- cli.ts # Entry point, REPL, slash commands, wordwheel UI
172
- orchestrator.ts # Task routing, session management
200
+ cli.ts # Entry point, startup lifecycle, REPL, slash commands, wordwheel UI
201
+ orchestrator.ts # Task routing, session management, presence tracking
173
202
  adapter.ts # AgentAdapter interface, prompt builder, handoff formatting
174
- registry.ts # Discovers teammates from .teammates/, loads SOUL.md + memory
175
- types.ts # Core types (TeammateConfig, TaskResult, HandoffEnvelope)
203
+ registry.ts # Discovers teammates from .teammates/, loads SOUL.md + memory, type detection
204
+ personas.ts # Persona loader reads and parses bundled persona templates
205
+ types.ts # Core types (TeammateConfig, TaskResult, HandoffEnvelope, TeammateType, PresenceState)
206
+ onboard.ts # Template copying, team import, onboarding/adaptation prompts
176
207
  dropdown.ts # Terminal dropdown/wordwheel widget
177
208
  adapters/
178
209
  cli-proxy.ts # Generic subprocess adapter with agent presets
179
210
  echo.ts # Test adapter (no-op)
211
+ cli/personas/ # 15 persona template files (pm.md, swe.md, devops.md, etc.)
180
212
  ```
181
213
 
182
214
  ### Output Protocol
package/dist/adapter.d.ts CHANGED
@@ -19,7 +19,9 @@ export interface AgentAdapter {
19
19
  * Send a task prompt to a teammate's session.
20
20
  * The adapter hydrates the prompt with identity, memory, and handoff context.
21
21
  */
22
- executeTask(sessionId: string, teammate: TeammateConfig, prompt: string): Promise<TaskResult>;
22
+ executeTask(sessionId: string, teammate: TeammateConfig, prompt: string, options?: {
23
+ raw?: boolean;
24
+ }): Promise<TaskResult>;
23
25
  /**
24
26
  * Resume an existing session (for agents that support continuity).
25
27
  * Falls back to startSession if not implemented.
@@ -58,15 +60,29 @@ export interface RecallContext {
58
60
  }
59
61
  /**
60
62
  * Query the recall index for context relevant to the task prompt.
61
- * Returns search results that should be injected into the teammate prompt.
63
+ *
64
+ * Uses a multi-query strategy (Pass 1 from the recall query architecture):
65
+ * 1. Keyword extraction — generates focused queries from the task prompt
66
+ * 2. Conversation-aware queries — extracts recent topic from conversation history
67
+ * 3. Memory index scanning — text-matches frontmatter against the task prompt
68
+ * 4. Multi-query fusion — fires 2-3 queries and deduplicates by URI
69
+ *
62
70
  * Skips auto-sync (sync happens after tasks, not before — keeps pre-task fast).
63
71
  */
64
- export declare function queryRecallContext(teammatesDir: string, teammateName: string, taskPrompt: string): Promise<RecallContext>;
72
+ export declare function queryRecallContext(teammatesDir: string, teammateName: string, taskPrompt: string, conversationContext?: string): Promise<RecallContext>;
65
73
  /**
66
74
  * Sync the recall index for a teammate (or all teammates).
67
75
  * Wrapper around the recall library's Indexer.
68
76
  */
69
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;
70
86
  /**
71
87
  * Build the full prompt for a teammate session.
72
88
  * Includes identity, memory, roster, output protocol, and the task.
package/dist/adapter.js CHANGED
@@ -5,21 +5,39 @@
5
5
  * Each adapter wraps a specific agent backend (Codex, Claude Code, Cursor, etc.)
6
6
  * and translates between the orchestrator's protocol and the agent's native API.
7
7
  */
8
- import { Indexer, search } from "@teammates/recall";
8
+ import { platform } from "node:os";
9
+ import { buildQueryVariations, Indexer, matchMemoryCatalog, multiSearch, } from "@teammates/recall";
9
10
  /**
10
11
  * Query the recall index for context relevant to the task prompt.
11
- * Returns search results that should be injected into the teammate prompt.
12
+ *
13
+ * Uses a multi-query strategy (Pass 1 from the recall query architecture):
14
+ * 1. Keyword extraction — generates focused queries from the task prompt
15
+ * 2. Conversation-aware queries — extracts recent topic from conversation history
16
+ * 3. Memory index scanning — text-matches frontmatter against the task prompt
17
+ * 4. Multi-query fusion — fires 2-3 queries and deduplicates by URI
18
+ *
12
19
  * Skips auto-sync (sync happens after tasks, not before — keeps pre-task fast).
13
20
  */
14
- export async function queryRecallContext(teammatesDir, teammateName, taskPrompt) {
21
+ export async function queryRecallContext(teammatesDir, teammateName, taskPrompt, conversationContext) {
15
22
  try {
16
- const results = await search(taskPrompt, {
23
+ // Build query variations: original + keywords + conversation topic
24
+ // If no separate conversation context provided, use the task prompt itself
25
+ // (which may contain prepended conversation history from the orchestrator)
26
+ const queries = buildQueryVariations(taskPrompt, conversationContext ?? taskPrompt);
27
+ const primaryQuery = queries[0];
28
+ const additionalQueries = queries.slice(1);
29
+ // Scan memory frontmatter for text-matched results (no embeddings needed)
30
+ const catalogMatches = await matchMemoryCatalog(teammatesDir, teammateName, taskPrompt);
31
+ // Fire multi-query search with deduplication
32
+ const results = await multiSearch(primaryQuery, {
17
33
  teammatesDir,
18
34
  teammate: teammateName,
19
35
  maxResults: 5,
20
36
  maxChunks: 3,
21
37
  maxTokens: 500,
22
38
  skipSync: true,
39
+ additionalQueries,
40
+ catalogMatches,
23
41
  });
24
42
  return { results, ok: true };
25
43
  }
@@ -40,23 +58,16 @@ export async function syncRecallIndex(teammatesDir, teammate) {
40
58
  await indexer.syncAll();
41
59
  }
42
60
  }
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;
61
+ /** Approximate chars per token for budget estimation. */
50
62
  const CHARS_PER_TOKEN = 4;
51
63
  /**
52
64
  * Context budget allocation:
53
65
  * - Days 2-7 get up to DAILY_LOG_BUDGET tokens (whole entries)
54
66
  * - 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)
67
+ * - Last recall entry can push total up to budget + RECALL_OVERFLOW (4k grace)
56
68
  * - Weekly summaries are excluded (already indexed by recall)
57
69
  */
58
- const CONTEXT_BUDGET_TOKENS = 32_000;
59
- const DAILY_LOG_BUDGET_TOKENS = 24_000;
70
+ export const DAILY_LOG_BUDGET_TOKENS = 24_000;
60
71
  const RECALL_MIN_BUDGET_TOKENS = 8_000;
61
72
  const RECALL_OVERFLOW_TOKENS = 4_000;
62
73
  /** Estimate tokens from character count. */
@@ -79,57 +90,98 @@ function estimateTokens(text) {
79
90
  */
80
91
  export function buildTeammatePrompt(teammate, taskPrompt, options) {
81
92
  const parts = [];
82
- // ── Identity (required) ─────────────────────────────────────────
83
- parts.push(`# You are ${teammate.name}\n\n${teammate.soul}\n\n---\n`);
84
- // ── Wisdom (required) ───────────────────────────────────────────
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
85
97
  if (teammate.wisdom.trim()) {
86
- parts.push(`## Your Wisdom\n\n${teammate.wisdom}\n\n---\n`);
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`);
87
116
  }
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
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`);
129
+ }
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)
92
147
  const todayLog = teammate.dailyLogs.slice(0, 1);
93
148
  const pastLogs = teammate.dailyLogs.slice(1, 7); // days 2-7
94
149
  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"];
150
+ if (todayLog.length > 0 || pastLogs.length > 0) {
151
+ const logLines = ["<DAILY_LOGS>"];
152
+ // Current daily log (today) never trimmed, always included
98
153
  for (const log of todayLog) {
99
- todayLines.push(`### ${log.date}\n${log.content}\n`);
154
+ logLines.push(`### ${log.date}\n${log.content}`);
100
155
  }
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 = [];
156
+ // Days 2-7 — whole entries, up to 24k tokens
106
157
  for (const log of pastLogs) {
107
- const entry = `### ${log.date}\n${log.content}\n`;
158
+ const entry = `### ${log.date}\n${log.content}`;
108
159
  const cost = estimateTokens(entry);
109
160
  if (cost > dailyBudget)
110
161
  break;
111
- lines.push(entry);
162
+ logLines.push(entry);
112
163
  dailyBudget -= cost;
113
164
  }
114
- if (lines.length > 0)
115
- parts.push(lines.join("\n"));
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`);
116
170
  }
117
- // Recall results gets at least 8k tokens, plus unused daily budget
118
- // Last entry may overflow by up to 4k tokens
171
+ // ── Task-adjacent context (close to task for maximum relevance) ───
172
+ // <RECALL_RESULTS> budget-allocated, adjacent to task
119
173
  const recallBudget = Math.max(RECALL_MIN_BUDGET_TOKENS, RECALL_MIN_BUDGET_TOKENS + dailyBudget);
120
174
  const recallResults = options?.recallResults ?? [];
121
175
  if (recallResults.length > 0) {
122
176
  const lines = [
123
- "## Relevant Memories (from recall search)\n",
177
+ "<RECALL_RESULTS>",
124
178
  "These memories were retrieved based on relevance to the current task:\n",
125
179
  ];
126
180
  const headerCost = estimateTokens(lines.join("\n"));
127
181
  let recallUsed = headerCost;
128
182
  for (const r of recallResults) {
129
- const label = r.contentType
130
- ? `[${r.contentType}] ${r.uri}`
131
- : r.uri;
132
- 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}`;
133
185
  const cost = estimateTokens(entry);
134
186
  if (recallUsed + cost > recallBudget + RECALL_OVERFLOW_TOKENS)
135
187
  break;
@@ -140,70 +192,90 @@ export function buildTeammatePrompt(teammate, taskPrompt, options) {
140
192
  break;
141
193
  }
142
194
  if (lines.length > 2) {
143
- lines.push("\n---\n");
144
- parts.push(lines.join("\n"));
195
+ parts.push(`${lines.join("\n")}\n`);
145
196
  }
146
197
  }
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");
152
- }
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.");
153
257
  }
154
- // ── Team roster (required, small) ───────────────────────────────
155
258
  if (options?.roster && options.roster.length > 0) {
156
- const lines = [
157
- "## Your Team\n",
158
- "These are the other teammates you can hand off work to:\n",
159
- ];
160
- for (const t of options.roster) {
161
- if (t.name === teammate.name)
162
- continue;
163
- const owns = t.ownership.primary.length > 0
164
- ? ` — owns: ${t.ownership.primary.join(", ")}`
165
- : "";
166
- lines.push(`- **@${t.name}**: ${t.role}${owns}`);
167
- }
168
- lines.push("\n---\n");
169
- parts.push(lines.join("\n"));
259
+ instrLines.push("- Only hand off to teammates listed in `<TEAM>` using the handoff block format above.");
170
260
  }
171
- // ── Installed services (required, small) ────────────────────────
172
261
  if (options?.services && options.services.length > 0) {
173
- const lines = [
174
- "## Available Services\n",
175
- "These services are installed and available for you to use:\n",
176
- ];
177
- for (const svc of options.services) {
178
- lines.push(`### ${svc.name}\n`);
179
- lines.push(svc.description);
180
- lines.push(`\n**Usage:** \`${svc.usage}\`\n`);
181
- }
182
- lines.push("\n---\n");
183
- 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.");
184
263
  }
185
- // ── Handoff context (required when present) ─────────────────────
186
- if (options?.handoffContext) {
187
- parts.push(`## Handoff Context\n\n${options.handoffContext}\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.");
188
267
  }
189
- // ── Session state (required) ────────────────────────────────────
190
- if (options?.sessionFile) {
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) ───────────────────────────────────
194
- const today = new Date().toISOString().slice(0, 10);
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) ─────────────────────────
199
- const now = new Date();
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
268
  if (options?.userProfile?.trim()) {
203
- parts.push(`## User Profile\n\n${options.userProfile.trim()}\n\n---\n`);
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.");
204
276
  }
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.**`);
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"));
207
279
  return parts.join("\n");
208
280
  }
209
281
  /**
@@ -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("## Task");
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("## Output Protocol");
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("## Memory Updates");
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("## Your Wisdom");
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("## Your Wisdom");
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("## Recent Daily Logs");
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("## Your Team");
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("## Handoff Context");
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("## Session State");
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
- { teammate: "beacon", uri: "memory/decision_foo.md", text: recallText, score: 0.9, contentType: "typed_memory" },
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("## Relevant Memories");
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
- { teammate: "beacon", uri: "memory/big.md", text: recallText, score: 0.9, contentType: "typed_memory" },
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("## Relevant Memories");
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("## Recent Daily Logs");
164
- expect(prompt).not.toContain("## Recent Weekly Summaries");
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("## Recent Daily Logs");
187
+ expect(prompt).toContain("<DAILY_LOGS>");
175
188
  expect(prompt).toContain("small log");
176
189
  });
177
190
  });
@@ -88,7 +88,9 @@ export declare class CliProxyAdapter implements AgentAdapter {
88
88
  private pendingTempFiles;
89
89
  constructor(options: CliProxyOptions);
90
90
  startSession(teammate: TeammateConfig): Promise<string>;
91
- executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string): Promise<TaskResult>;
91
+ executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string, options?: {
92
+ raw?: boolean;
93
+ }): Promise<TaskResult>;
92
94
  routeTask(task: string, roster: RosterEntry[]): Promise<string | null>;
93
95
  getSessionFile(teammateName: string): string | undefined;
94
96
  destroySession(_sessionId: string): Promise<void>;