@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 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 weekly summaries → session history → roster → task)
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
- parts.push(teammate.soul);
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("## Your Wisdom\n");
56
- parts.push(teammate.wisdom);
57
- parts.push("\n---\n");
58
- }
59
- // ── Recall results (relevant episodic & semantic memories) ────────
60
- if (options?.recallResults && options.recallResults.length > 0) {
61
- parts.push("## Relevant Memories (from recall search)\n");
62
- parts.push("These memories were retrieved based on relevance to the current task:\n");
63
- for (const r of options.recallResults) {
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
- parts.push(`### ${label}\n${r.text}\n`);
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
- parts.push("\n---\n");
70
- }
71
- // ── Recent daily logs ──────────────────────────────────────────────
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
- // ── Weekly summaries (recent episodic context) ─────────────────────
80
- if (teammate.weeklyLogs.length > 0) {
81
- parts.push("## Recent Weekly Summaries\n");
82
- for (const log of teammate.weeklyLogs.slice(0, 2)) {
83
- parts.push(`### ${log.week}\n${log.content}\n`);
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
- // ── Session history (prior tasks in this session) ─────────────────
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
- parts.push("## Your Team\n");
97
- parts.push("These are the other teammates you can hand off work to:\n");
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
- parts.push(`- **@${t.name}**: ${t.role}${owns}`);
166
+ lines.push(`- **@${t.name}**: ${t.role}${owns}`);
105
167
  }
106
- parts.push("\n---\n");
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
- parts.push("## Available Services\n");
111
- parts.push("These services are installed and available for you to use:\n");
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
- parts.push(`### ${svc.name}\n`);
114
- parts.push(svc.description);
115
- parts.push(`\n**Usage:** \`${svc.usage}\`\n`);
178
+ lines.push(`### ${svc.name}\n`);
179
+ lines.push(svc.description);
180
+ lines.push(`\n**Usage:** \`${svc.usage}\`\n`);
116
181
  }
117
- parts.push("\n---\n");
182
+ lines.push("\n---\n");
183
+ parts.push(lines.join("\n"));
118
184
  }
119
- // ── Handoff context (if this task came from another teammate) ─────
185
+ // ── Handoff context (required when present) ─────────────────────
120
186
  if (options?.handoffContext) {
121
- parts.push("## Handoff Context\n");
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("## Session State\n");
128
- parts.push(`Your session file is at: \`${options.sessionFile}\`
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("## Memory Updates\n");
143
- parts.push(`**Before returning your result**, update your memory files:
144
-
145
- 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.
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
- parts.push(`**Current time:** ${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}\n`);
194
- parts.push("---\n");
195
- // ── Task ──────────────────────────────────────────────────────────
196
- parts.push("## Task\n");
197
- parts.push(taskPrompt);
198
- parts.push("\n---\n");
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
  /**
@@ -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: "",
@@ -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 {
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
2
2
  import { EchoAdapter } from "./echo.js";
3
3
  const teammate = {
4
4
  name: "beacon",
5
+ type: "ai",
5
6
  role: "Platform engineer.",
6
7
  soul: "# Beacon\n\nBeacon owns the recall package.",
7
8
  wisdom: "",
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
- adapterName: string;
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.adapterName), tp.muted(` · ${info.teammateCount} teammate${info.teammateCount === 1 ? "" : "s"}`), tp.muted(` · v${PKG_VERSION}`)));
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
- lines.push(concat(tp.accent(" ● "), tp.accent(`@${t.name}`.padEnd(14)), tp.muted(t.role)));
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