@teammates/cli 0.4.1 → 0.5.0

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
@@ -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,10 +60,16 @@ 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.
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 { Indexer, buildQueryVariations, 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
  }
@@ -182,28 +200,43 @@ export function buildTeammatePrompt(teammate, taskPrompt, options) {
182
200
  lines.push("\n---\n");
183
201
  parts.push(lines.join("\n"));
184
202
  }
203
+ // ── Recall tool (Pass 2 — agent-driven search) ─────────────────
204
+ // Tell the agent it can search memories mid-task via the CLI tool
205
+ parts.push(`## Recall — Memory Search Tool\n\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\n---\n`);
185
206
  // ── Handoff context (required when present) ─────────────────────
186
207
  if (options?.handoffContext) {
187
208
  parts.push(`## Handoff Context\n\n${options.handoffContext}\n\n---\n`);
188
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`);
189
215
  // ── Session state (required) ────────────────────────────────────
190
216
  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`);
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`);
192
218
  }
193
219
  // ── Memory updates (required) ───────────────────────────────────
194
220
  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) ─────────────────────────
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) ───────────
199
223
  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`);
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`);
201
234
  // ── User profile (always included when present) ────────────────
202
235
  if (options?.userProfile?.trim()) {
203
236
  parts.push(`## User Profile\n\n${options.userProfile.trim()}\n\n---\n`);
204
237
  }
205
238
  // ── 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.**`);
239
+ parts.push(`## Task\n\n${taskPrompt}\n\n---\n\n**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.**`);
207
240
  return parts.join("\n");
208
241
  }
209
242
  /**
@@ -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>;
@@ -136,12 +136,15 @@ export class CliProxyAdapter {
136
136
  this.sessionFiles.set(teammate.name, sessionFile);
137
137
  return id;
138
138
  }
139
- async executeTask(_sessionId, teammate, prompt) {
140
- // If the teammate has no soul (e.g. the raw agent), skip identity/memory
141
- // wrapping but include handoff instructions so it can delegate to teammates
139
+ async executeTask(_sessionId, teammate, prompt, options) {
140
+ // If raw mode is set, skip all prompt wrapping send prompt as-is
141
+ // Used for defensive retries where the full prompt template is counterproductive
142
142
  const sessionFile = this.sessionFiles.get(teammate.name);
143
143
  let fullPrompt;
144
- if (teammate.soul) {
144
+ if (options?.raw) {
145
+ fullPrompt = prompt;
146
+ }
147
+ else if (teammate.soul) {
145
148
  // Query recall for relevant memories before building prompt
146
149
  const teammatesDir = teammate.cwd
147
150
  ? join(teammate.cwd, ".teammates")
@@ -40,7 +40,9 @@ export declare class CopilotAdapter implements AgentAdapter {
40
40
  private sessionsDir;
41
41
  constructor(options?: CopilotAdapterOptions);
42
42
  startSession(teammate: TeammateConfig): Promise<string>;
43
- executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string): Promise<TaskResult>;
43
+ executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string, options?: {
44
+ raw?: boolean;
45
+ }): Promise<TaskResult>;
44
46
  routeTask(task: string, roster: RosterEntry[]): Promise<string | null>;
45
47
  getSessionFile(teammateName: string): string | undefined;
46
48
  destroySession(_sessionId: string): Promise<void>;
@@ -56,12 +56,15 @@ export class CopilotAdapter {
56
56
  this.sessionFiles.set(teammate.name, sessionFile);
57
57
  return id;
58
58
  }
59
- async executeTask(_sessionId, teammate, prompt) {
59
+ async executeTask(_sessionId, teammate, prompt, options) {
60
60
  await this.ensureClient(teammate.cwd);
61
61
  const sessionFile = this.sessionFiles.get(teammate.name);
62
62
  // Build the full teammate prompt (identity + memory + task)
63
63
  let fullPrompt;
64
- if (teammate.soul) {
64
+ if (options?.raw) {
65
+ fullPrompt = prompt;
66
+ }
67
+ else if (teammate.soul) {
65
68
  // Query recall for relevant memories before building prompt
66
69
  const teammatesDir = teammate.cwd
67
70
  ? join(teammate.cwd, ".teammates")
@@ -9,5 +9,7 @@ import type { TaskResult, TeammateConfig } from "../types.js";
9
9
  export declare class EchoAdapter implements AgentAdapter {
10
10
  readonly name = "echo";
11
11
  startSession(teammate: TeammateConfig): Promise<string>;
12
- executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string): Promise<TaskResult>;
12
+ executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string, options?: {
13
+ raw?: boolean;
14
+ }): Promise<TaskResult>;
13
15
  }
@@ -11,8 +11,8 @@ export class EchoAdapter {
11
11
  async startSession(teammate) {
12
12
  return `echo-${teammate.name}-${nextId++}`;
13
13
  }
14
- async executeTask(_sessionId, teammate, prompt) {
15
- const fullPrompt = buildTeammatePrompt(teammate, prompt);
14
+ async executeTask(_sessionId, teammate, prompt, options) {
15
+ const fullPrompt = options?.raw ? prompt : buildTeammatePrompt(teammate, prompt);
16
16
  return {
17
17
  teammate: teammate.name,
18
18
  success: true,