@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.
- package/README.md +36 -4
- package/dist/adapter.d.ts +19 -3
- package/dist/adapter.js +168 -96
- package/dist/adapter.test.js +29 -16
- package/dist/adapters/cli-proxy.d.ts +3 -1
- package/dist/adapters/cli-proxy.js +65 -6
- package/dist/adapters/copilot.d.ts +3 -1
- package/dist/adapters/copilot.js +16 -3
- package/dist/adapters/echo.d.ts +3 -1
- package/dist/adapters/echo.js +4 -2
- package/dist/banner.js +5 -1
- package/dist/cli-args.js +23 -23
- package/dist/cli-args.test.d.ts +1 -0
- package/dist/cli-args.test.js +125 -0
- package/dist/cli.js +486 -220
- package/dist/compact.d.ts +23 -0
- package/dist/compact.js +181 -11
- package/dist/compact.test.js +323 -7
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/onboard.js +165 -165
- package/dist/orchestrator.js +7 -2
- package/dist/personas.d.ts +42 -0
- package/dist/personas.js +108 -0
- package/dist/personas.test.d.ts +1 -0
- package/dist/personas.test.js +88 -0
- package/dist/registry.test.js +23 -23
- package/dist/theme.test.d.ts +1 -0
- package/dist/theme.test.js +113 -0
- package/dist/types.d.ts +2 -0
- package/package.json +4 -3
- package/personas/architect.md +95 -0
- package/personas/backend.md +97 -0
- package/personas/data-engineer.md +96 -0
- package/personas/designer.md +96 -0
- package/personas/devops.md +97 -0
- package/personas/frontend.md +98 -0
- package/personas/ml-ai.md +100 -0
- package/personas/mobile.md +97 -0
- package/personas/performance.md +96 -0
- package/personas/pm.md +93 -0
- package/personas/prompt-engineer.md +122 -0
- package/personas/qa.md +96 -0
- package/personas/security.md +96 -0
- package/personas/sre.md +97 -0
- package/personas/swe.md +92 -0
- 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
|
-
|
|
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,15 +60,29 @@ 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.
|
|
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 {
|
|
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
|
-
*
|
|
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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
-
// ──
|
|
83
|
-
|
|
84
|
-
|
|
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(
|
|
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
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
154
|
+
logLines.push(`### ${log.date}\n${log.content}`);
|
|
100
155
|
}
|
|
101
|
-
|
|
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}
|
|
158
|
+
const entry = `### ${log.date}\n${log.content}`;
|
|
108
159
|
const cost = estimateTokens(entry);
|
|
109
160
|
if (cost > dailyBudget)
|
|
110
161
|
break;
|
|
111
|
-
|
|
162
|
+
logLines.push(entry);
|
|
112
163
|
dailyBudget -= cost;
|
|
113
164
|
}
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
//
|
|
118
|
-
//
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
parts.push(lines.join("\n"));
|
|
195
|
+
parts.push(`${lines.join("\n")}\n`);
|
|
145
196
|
}
|
|
146
197
|
}
|
|
147
|
-
//
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
if (
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
parts.push(
|
|
277
|
+
instrLines.push("- Your response must answer `<TASK>` — everything else is supporting context.", "", "**REMINDER: Write your text response (TO: user) FIRST, then update session/memory files. A turn with only file edits and no text output is a failed turn.**");
|
|
278
|
+
parts.push(instrLines.join("\n"));
|
|
207
279
|
return parts.join("\n");
|
|
208
280
|
}
|
|
209
281
|
/**
|
package/dist/adapter.test.js
CHANGED
|
@@ -25,27 +25,28 @@ describe("buildTeammatePrompt", () => {
|
|
|
25
25
|
});
|
|
26
26
|
it("includes the task", () => {
|
|
27
27
|
const prompt = buildTeammatePrompt(makeConfig(), "fix the bug");
|
|
28
|
-
expect(prompt).toContain("
|
|
28
|
+
expect(prompt).toContain("<TASK>");
|
|
29
29
|
expect(prompt).toContain("fix the bug");
|
|
30
30
|
});
|
|
31
31
|
it("includes output protocol", () => {
|
|
32
32
|
const prompt = buildTeammatePrompt(makeConfig(), "task");
|
|
33
|
-
expect(prompt).toContain("
|
|
33
|
+
expect(prompt).toContain("<INSTRUCTIONS>");
|
|
34
|
+
expect(prompt).toContain("Output Protocol");
|
|
34
35
|
expect(prompt).toContain("TO: user");
|
|
35
36
|
expect(prompt).toContain("```handoff");
|
|
36
37
|
});
|
|
37
38
|
it("includes memory updates section", () => {
|
|
38
39
|
const prompt = buildTeammatePrompt(makeConfig(), "task");
|
|
39
|
-
expect(prompt).toContain("
|
|
40
|
+
expect(prompt).toContain("### Memory Updates");
|
|
40
41
|
expect(prompt).toContain(".teammates/beacon/memory/");
|
|
41
42
|
});
|
|
42
43
|
it("skips wisdom section when empty", () => {
|
|
43
44
|
const prompt = buildTeammatePrompt(makeConfig({ wisdom: "" }), "task");
|
|
44
|
-
expect(prompt).not.toContain("
|
|
45
|
+
expect(prompt).not.toContain("<WISDOM>");
|
|
45
46
|
});
|
|
46
47
|
it("includes wisdom when present", () => {
|
|
47
48
|
const prompt = buildTeammatePrompt(makeConfig({ wisdom: "Some important wisdom" }), "task");
|
|
48
|
-
expect(prompt).toContain("
|
|
49
|
+
expect(prompt).toContain("<WISDOM>");
|
|
49
50
|
expect(prompt).toContain("Some important wisdom");
|
|
50
51
|
});
|
|
51
52
|
it("includes daily logs (up to 7)", () => {
|
|
@@ -60,7 +61,7 @@ describe("buildTeammatePrompt", () => {
|
|
|
60
61
|
{ date: "2026-03-06", content: "Should be excluded" },
|
|
61
62
|
];
|
|
62
63
|
const prompt = buildTeammatePrompt(makeConfig({ dailyLogs: logs }), "task");
|
|
63
|
-
expect(prompt).toContain("
|
|
64
|
+
expect(prompt).toContain("<DAILY_LOGS>");
|
|
64
65
|
expect(prompt).toContain("2026-03-13");
|
|
65
66
|
expect(prompt).toContain("2026-03-07");
|
|
66
67
|
expect(prompt).not.toContain("2026-03-06");
|
|
@@ -80,7 +81,7 @@ describe("buildTeammatePrompt", () => {
|
|
|
80
81
|
},
|
|
81
82
|
];
|
|
82
83
|
const prompt = buildTeammatePrompt(makeConfig(), "task", { roster });
|
|
83
|
-
expect(prompt).toContain("
|
|
84
|
+
expect(prompt).toContain("<TEAM>");
|
|
84
85
|
expect(prompt).toContain("@scribe");
|
|
85
86
|
expect(prompt).toContain("Documentation writer.");
|
|
86
87
|
// Should not list self in roster
|
|
@@ -90,14 +91,14 @@ describe("buildTeammatePrompt", () => {
|
|
|
90
91
|
const prompt = buildTeammatePrompt(makeConfig(), "task", {
|
|
91
92
|
handoffContext: "Handed off from scribe with files changed",
|
|
92
93
|
});
|
|
93
|
-
expect(prompt).toContain("
|
|
94
|
+
expect(prompt).toContain("<HANDOFF_CONTEXT>");
|
|
94
95
|
expect(prompt).toContain("Handed off from scribe");
|
|
95
96
|
});
|
|
96
97
|
it("includes session file when provided", () => {
|
|
97
98
|
const prompt = buildTeammatePrompt(makeConfig(), "task", {
|
|
98
99
|
sessionFile: "/tmp/beacon-session.md",
|
|
99
100
|
});
|
|
100
|
-
expect(prompt).toContain("
|
|
101
|
+
expect(prompt).toContain("### Session State");
|
|
101
102
|
expect(prompt).toContain("/tmp/beacon-session.md");
|
|
102
103
|
});
|
|
103
104
|
it("drops daily logs that exceed the 24k daily budget", () => {
|
|
@@ -130,11 +131,17 @@ describe("buildTeammatePrompt", () => {
|
|
|
130
131
|
const recallText = "R".repeat(20_000); // ~5k tokens — fits in 8k min
|
|
131
132
|
const prompt = buildTeammatePrompt(config, "task", {
|
|
132
133
|
recallResults: [
|
|
133
|
-
{
|
|
134
|
+
{
|
|
135
|
+
teammate: "beacon",
|
|
136
|
+
uri: "memory/decision_foo.md",
|
|
137
|
+
text: recallText,
|
|
138
|
+
score: 0.9,
|
|
139
|
+
contentType: "typed_memory",
|
|
140
|
+
},
|
|
134
141
|
],
|
|
135
142
|
});
|
|
136
143
|
expect(prompt).toContain("2026-03-17");
|
|
137
|
-
expect(prompt).toContain("
|
|
144
|
+
expect(prompt).toContain("<RECALL_RESULTS>");
|
|
138
145
|
});
|
|
139
146
|
it("recall gets unused daily log budget", () => {
|
|
140
147
|
// Small daily logs leave most of 24k unused — recall gets the surplus.
|
|
@@ -148,10 +155,16 @@ describe("buildTeammatePrompt", () => {
|
|
|
148
155
|
const recallText = "R".repeat(80_000); // ~20k tokens — fits in (8k + ~24k unused)
|
|
149
156
|
const prompt = buildTeammatePrompt(config, "task", {
|
|
150
157
|
recallResults: [
|
|
151
|
-
{
|
|
158
|
+
{
|
|
159
|
+
teammate: "beacon",
|
|
160
|
+
uri: "memory/big.md",
|
|
161
|
+
text: recallText,
|
|
162
|
+
score: 0.9,
|
|
163
|
+
contentType: "typed_memory",
|
|
164
|
+
},
|
|
152
165
|
],
|
|
153
166
|
});
|
|
154
|
-
expect(prompt).toContain("
|
|
167
|
+
expect(prompt).toContain("<RECALL_RESULTS>");
|
|
155
168
|
expect(prompt).toContain("memory/big.md");
|
|
156
169
|
});
|
|
157
170
|
it("weekly summaries are excluded (indexed by recall)", () => {
|
|
@@ -160,8 +173,8 @@ describe("buildTeammatePrompt", () => {
|
|
|
160
173
|
weeklyLogs: [{ week: "2026-W11", content: "short summary" }],
|
|
161
174
|
});
|
|
162
175
|
const prompt = buildTeammatePrompt(config, "task");
|
|
163
|
-
expect(prompt).toContain("
|
|
164
|
-
expect(prompt).not.toContain("
|
|
176
|
+
expect(prompt).toContain("<DAILY_LOGS>");
|
|
177
|
+
expect(prompt).not.toContain("Weekly Summaries");
|
|
165
178
|
});
|
|
166
179
|
it("excludes task prompt from budget calculation", () => {
|
|
167
180
|
// Large task prompt should not trigger trimming of wrapper sections
|
|
@@ -171,7 +184,7 @@ describe("buildTeammatePrompt", () => {
|
|
|
171
184
|
});
|
|
172
185
|
const prompt = buildTeammatePrompt(config, bigTask);
|
|
173
186
|
// Daily logs should still be included despite the huge task
|
|
174
|
-
expect(prompt).toContain("
|
|
187
|
+
expect(prompt).toContain("<DAILY_LOGS>");
|
|
175
188
|
expect(prompt).toContain("small log");
|
|
176
189
|
});
|
|
177
190
|
});
|
|
@@ -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>;
|