@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 +36 -4
- package/dist/adapter.d.ts +11 -3
- package/dist/adapter.js +44 -11
- package/dist/adapters/cli-proxy.d.ts +3 -1
- package/dist/adapters/cli-proxy.js +7 -4
- package/dist/adapters/copilot.d.ts +3 -1
- package/dist/adapters/copilot.js +5 -2
- package/dist/adapters/echo.d.ts +3 -1
- package/dist/adapters/echo.js +2 -2
- package/dist/cli.js +365 -132
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/orchestrator.js +3 -1
- package/dist/personas.d.ts +42 -0
- package/dist/personas.js +108 -0
- package/dist/types.d.ts +2 -0
- package/package.json +4 -3
- package/personas/architect.md +91 -0
- package/personas/backend.md +93 -0
- package/personas/data-engineer.md +92 -0
- package/personas/designer.md +92 -0
- package/personas/devops.md +93 -0
- package/personas/frontend.md +94 -0
- package/personas/ml-ai.md +96 -0
- package/personas/mobile.md +93 -0
- package/personas/performance.md +92 -0
- package/personas/pm.md +89 -0
- package/personas/qa.md +92 -0
- package/personas/security.md +92 -0
- package/personas/sre.md +93 -0
- package/personas/swe.md +88 -0
- package/personas/tech-writer.md +93 -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
|
-
|
|
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
|
|
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
|
-
*
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
|
|
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**
|
|
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**
|
|
196
|
-
// ──
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
141
|
-
//
|
|
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 (
|
|
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
|
|
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>;
|
package/dist/adapters/copilot.js
CHANGED
|
@@ -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 (
|
|
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")
|
package/dist/adapters/echo.d.ts
CHANGED
|
@@ -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
|
|
12
|
+
executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string, options?: {
|
|
13
|
+
raw?: boolean;
|
|
14
|
+
}): Promise<TaskResult>;
|
|
13
15
|
}
|
package/dist/adapters/echo.js
CHANGED
|
@@ -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,
|