@teammates/cli 0.5.0 → 0.5.2

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/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Public API for @teammates/cli
2
- export { buildTeammatePrompt, formatHandoffContext, queryRecallContext, syncRecallIndex, } from "./adapter.js";
2
+ export { buildTeammatePrompt, DAILY_LOG_BUDGET_TOKENS, formatHandoffContext, queryRecallContext, syncRecallIndex, } from "./adapter.js";
3
+ export { autoCompactForBudget } from "./compact.js";
3
4
  export { CliProxyAdapter, PRESETS, } from "./adapters/cli-proxy.js";
4
5
  export { EchoAdapter } from "./adapters/echo.js";
5
6
  export { AnimatedBanner } from "./banner.js";
package/dist/onboard.js CHANGED
@@ -225,82 +225,82 @@ export async function buildImportAdaptationPrompt(teammatesDir, teammateNames, s
225
225
  teammateSections.push(`### @${name}\n${soulBlock}${wisdomBlock}`);
226
226
  }
227
227
  const projectDir = dirname(teammatesDir);
228
- return `You are adapting an imported team to a new project. This is a non-interactive session — complete ALL work without pausing. Do not ask for confirmation or wait for user input.
229
-
230
- **Source project:** \`${sourceProjectPath}\`
231
- **Target project:** \`${projectDir}\`
232
- **Target .teammates/ directory:** \`${teammatesDir}\`
233
- **Imported teammates:** ${teammateNames.map((n) => `@${n}`).join(", ")}
234
-
235
- > **IMPORTANT:** The \`example/\` directory inside \`.teammates/\` is a **template reference**, NOT a teammate. Do not adapt it, rename it, or treat it as a teammate. When creating new teammates, never use "example" as a folder name.
236
-
237
- ## Imported Teammates (from source project)
238
-
239
- ${teammateSections.join("\n\n---\n\n")}
240
-
241
- ## Instructions
242
-
243
- Complete these steps in order. Do NOT pause, ask questions, or wait for approval. Make all changes directly.
244
-
245
- ### Step 1: Scan This Project
246
-
247
- Read the project root to understand its structure:
248
- - Package manifest, README, config files
249
- - Major subsystems, languages, frameworks, file patterns
250
- - Dependency flow and architecture
251
-
252
- ### Step 2: Adapt EVERY Imported Teammate
253
-
254
- This is the most important step. For EACH imported teammate listed above, you MUST edit their SOUL.md and WISDOM.md to reflect THIS project, not the source project.
255
-
256
- For each teammate's **SOUL.md**:
257
-
258
- 1. **Add a "Previous Projects" section** (place it after Ethics). Compress what the teammate did in the source project:
259
- \`\`\`markdown
260
- ## Previous Projects
261
-
262
- ### <source-project-name>
263
- - **Role**: <one-line summary of what they did>
264
- - **Stack**: <key technologies they worked with>
265
- - **Domains**: <what they owned — file patterns or subsystem names>
266
- - **Key learnings**: <1-3 bullets of notable patterns, decisions, or lessons>
267
- \`\`\`
268
-
269
- 2. **Rewrite project-specific sections** for THIS project:
270
- - **Preserve**: Identity (name, personality), Core Principles, Ethics
271
- - **Rewrite completely**: Ownership (primary/secondary file globs for THIS project's actual files), Boundaries, Capabilities (commands, file patterns, technologies for THIS project), Routing keywords, Quality Bar
272
- - **Update**: All codebase-specific references — paths, package names, tools, teammate names must reference THIS project
273
-
274
- For each teammate's **WISDOM.md**:
275
- - Add a "Previous Projects" note at the top
276
- - Keep universal wisdom entries (general principles, patterns)
277
- - Remove entries that reference source project paths, architecture, or tools not used here
278
- - Adapt entries with transferable knowledge but old-project-specific details
279
-
280
- ### Step 3: Evaluate Gaps and Create New Teammates
281
-
282
- After adapting all existing teammates, check if THIS project has major subsystems that no teammate covers. If so, create new teammates:
283
- - Create \`${teammatesDir}/<name>/\` with SOUL.md, WISDOM.md, and \`memory/\`
284
- - Use the template at \`${teammatesDir}/TEMPLATE.md\` for structure
285
- - WISDOM.md starts with one creation entry
286
-
287
- If a teammate's domain doesn't exist at all in this project and their skills aren't transferable, delete their directory under \`${teammatesDir}\`.
288
-
289
- ### Step 4: Update Framework Files
290
-
291
- - Update \`${teammatesDir}/README.md\` with the final roster
292
- - Update \`${teammatesDir}/CROSS-TEAM.md\` ownership table
293
-
294
- ### Step 5: Verify
295
-
296
- - Every teammate has SOUL.md and WISDOM.md adapted to THIS project
297
- - Ownership globs reference actual files in THIS project
298
- - Boundaries reference correct teammate names
299
- - Previous Projects sections are present for all imported teammates
300
- - CROSS-TEAM.md has one row per teammate
301
-
302
- ## Critical Reminder
303
-
228
+ return `You are adapting an imported team to a new project. This is a non-interactive session — complete ALL work without pausing. Do not ask for confirmation or wait for user input.
229
+
230
+ **Source project:** \`${sourceProjectPath}\`
231
+ **Target project:** \`${projectDir}\`
232
+ **Target .teammates/ directory:** \`${teammatesDir}\`
233
+ **Imported teammates:** ${teammateNames.map((n) => `@${n}`).join(", ")}
234
+
235
+ > **IMPORTANT:** The \`example/\` directory inside \`.teammates/\` is a **template reference**, NOT a teammate. Do not adapt it, rename it, or treat it as a teammate. When creating new teammates, never use "example" as a folder name.
236
+
237
+ ## Imported Teammates (from source project)
238
+
239
+ ${teammateSections.join("\n\n---\n\n")}
240
+
241
+ ## Instructions
242
+
243
+ Complete these steps in order. Do NOT pause, ask questions, or wait for approval. Make all changes directly.
244
+
245
+ ### Step 1: Scan This Project
246
+
247
+ Read the project root to understand its structure:
248
+ - Package manifest, README, config files
249
+ - Major subsystems, languages, frameworks, file patterns
250
+ - Dependency flow and architecture
251
+
252
+ ### Step 2: Adapt EVERY Imported Teammate
253
+
254
+ This is the most important step. For EACH imported teammate listed above, you MUST edit their SOUL.md and WISDOM.md to reflect THIS project, not the source project.
255
+
256
+ For each teammate's **SOUL.md**:
257
+
258
+ 1. **Add a "Previous Projects" section** (place it after Ethics). Compress what the teammate did in the source project:
259
+ \`\`\`markdown
260
+ ## Previous Projects
261
+
262
+ ### <source-project-name>
263
+ - **Role**: <one-line summary of what they did>
264
+ - **Stack**: <key technologies they worked with>
265
+ - **Domains**: <what they owned — file patterns or subsystem names>
266
+ - **Key learnings**: <1-3 bullets of notable patterns, decisions, or lessons>
267
+ \`\`\`
268
+
269
+ 2. **Rewrite project-specific sections** for THIS project:
270
+ - **Preserve**: Identity (name, personality), Core Principles, Ethics
271
+ - **Rewrite completely**: Ownership (primary/secondary file globs for THIS project's actual files), Boundaries, Capabilities (commands, file patterns, technologies for THIS project), Routing keywords, Quality Bar
272
+ - **Update**: All codebase-specific references — paths, package names, tools, teammate names must reference THIS project
273
+
274
+ For each teammate's **WISDOM.md**:
275
+ - Add a "Previous Projects" note at the top
276
+ - Keep universal wisdom entries (general principles, patterns)
277
+ - Remove entries that reference source project paths, architecture, or tools not used here
278
+ - Adapt entries with transferable knowledge but old-project-specific details
279
+
280
+ ### Step 3: Evaluate Gaps and Create New Teammates
281
+
282
+ After adapting all existing teammates, check if THIS project has major subsystems that no teammate covers. If so, create new teammates:
283
+ - Create \`${teammatesDir}/<name>/\` with SOUL.md, WISDOM.md, and \`memory/\`
284
+ - Use the template at \`${teammatesDir}/TEMPLATE.md\` for structure
285
+ - WISDOM.md starts with one creation entry
286
+
287
+ If a teammate's domain doesn't exist at all in this project and their skills aren't transferable, delete their directory under \`${teammatesDir}\`.
288
+
289
+ ### Step 4: Update Framework Files
290
+
291
+ - Update \`${teammatesDir}/README.md\` with the final roster
292
+ - Update \`${teammatesDir}/CROSS-TEAM.md\` ownership table
293
+
294
+ ### Step 5: Verify
295
+
296
+ - Every teammate has SOUL.md and WISDOM.md adapted to THIS project
297
+ - Ownership globs reference actual files in THIS project
298
+ - Boundaries reference correct teammate names
299
+ - Previous Projects sections are present for all imported teammates
300
+ - CROSS-TEAM.md has one row per teammate
301
+
302
+ ## Critical Reminder
303
+
304
304
  The PRIMARY goal is adapting the imported teammates. Every SOUL.md must be rewritten so the teammate understands THIS project's codebase, not the source project's. If you only have time for one thing, adapt the existing teammates — that is more important than creating new ones.`;
305
305
  }
306
306
  /**
@@ -326,95 +326,95 @@ export async function getOnboardingPrompt(projectDir) {
326
326
  return wrapPrompt(BUILTIN_ONBOARDING, projectDir);
327
327
  }
328
328
  function wrapPrompt(onboardingContent, projectDir) {
329
- return `You are setting up the teammates framework for a project.
330
-
331
- **Target project directory:** ${projectDir}
332
-
333
- **Framework files have already been copied** into \`${projectDir}/.teammates/\` from the template. The following files are already in place:
334
- - CROSS-TEAM.md — fill in the Ownership Scopes table as you create teammates
335
- - PROTOCOL.md — team protocol (ready to use)
336
- - TEMPLATE.md — reference for creating teammate SOUL.md and WISDOM.md files
337
- - USER.md — user profile (gitignored, user fills in later)
338
- - README.md — update with project-specific roster and info
339
- - .gitignore — configured for USER.md and .index/
340
- - example/ — example SOUL.md and WISDOM.md for reference
341
-
342
- **Your job is to:**
343
- 1. Analyze the codebase (Step 1)
344
- 2. Design the team roster (Step 2)
345
- 3. Create teammate folders with SOUL.md and WISDOM.md (Step 3) — use TEMPLATE.md for the structure
346
- 4. Update README.md and CROSS-TEAM.md with the roster info (Step 3)
347
- 5. Verify everything is in place (Step 4)
348
-
349
- You do NOT need to create the framework files listed above — they're already there.
350
-
351
- > **IMPORTANT:** The \`example/\` directory is a **template reference**, NOT a teammate. Do not modify it or treat it as a teammate. Never name a new teammate "example".
352
-
353
- Follow the onboarding instructions below. This is a non-interactive session — complete ALL work without pausing. Do not ask for confirmation or wait for user input. Work through each step and make all changes directly.
354
-
355
- ---
356
-
329
+ return `You are setting up the teammates framework for a project.
330
+
331
+ **Target project directory:** ${projectDir}
332
+
333
+ **Framework files have already been copied** into \`${projectDir}/.teammates/\` from the template. The following files are already in place:
334
+ - CROSS-TEAM.md — fill in the Ownership Scopes table as you create teammates
335
+ - PROTOCOL.md — team protocol (ready to use)
336
+ - TEMPLATE.md — reference for creating teammate SOUL.md and WISDOM.md files
337
+ - USER.md — user profile (gitignored, user fills in later)
338
+ - README.md — update with project-specific roster and info
339
+ - .gitignore — configured for USER.md and .index/
340
+ - example/ — example SOUL.md and WISDOM.md for reference
341
+
342
+ **Your job is to:**
343
+ 1. Analyze the codebase (Step 1)
344
+ 2. Design the team roster (Step 2)
345
+ 3. Create teammate folders with SOUL.md and WISDOM.md (Step 3) — use TEMPLATE.md for the structure
346
+ 4. Update README.md and CROSS-TEAM.md with the roster info (Step 3)
347
+ 5. Verify everything is in place (Step 4)
348
+
349
+ You do NOT need to create the framework files listed above — they're already there.
350
+
351
+ > **IMPORTANT:** The \`example/\` directory is a **template reference**, NOT a teammate. Do not modify it or treat it as a teammate. Never name a new teammate "example".
352
+
353
+ Follow the onboarding instructions below. This is a non-interactive session — complete ALL work without pausing. Do not ask for confirmation or wait for user input. Work through each step and make all changes directly.
354
+
355
+ ---
356
+
357
357
  ${onboardingContent}`;
358
358
  }
359
- const BUILTIN_ONBOARDING = `# Teammates Onboarding
360
-
361
- You are going to analyze a codebase and create a set of AI teammates — persistent personas that each own a slice of the project. Follow these steps in order.
362
-
363
- ## Step 1: Analyze the Codebase
364
-
365
- Read the project's entry points to understand its structure:
366
- - README, CONTRIBUTING, or similar docs
367
- - Package manifest (package.json, Cargo.toml, pyproject.toml, go.mod, etc.)
368
- - Top-level directory structure
369
- - Key configuration files
370
-
371
- Identify:
372
- 1. **Major domains/subsystems** — distinct areas of the codebase
373
- 2. **Dependency flow** — which layers depend on which
374
- 3. **Key technologies** — languages, frameworks, tools per area
375
- 4. **File patterns** — glob patterns for each domain
376
-
377
- ## Step 2: Design the Team
378
-
379
- Based on your analysis, design a roster of teammates:
380
- - **Aim for 3–7 teammates.** Fewer for small projects, more for monorepos.
381
- - **Each teammate owns a distinct domain** with minimal overlap.
382
- - **Pick short, memorable names** — one word, evocative of the domain.
383
-
384
- For each teammate, define:
385
- - Name and one-line persona
386
- - Primary ownership (file patterns)
387
- - Key technologies
388
- - Boundaries (what they do NOT own)
389
-
390
- ## Step 3: Create the Directory Structure
391
-
392
- Create teammate folders under \`.teammates/\`:
393
-
394
- ### Teammate folders
395
- For each teammate, create \`.teammates/<name>/\` with:
396
-
397
- **SOUL.md** — Use the template from \`.teammates/TEMPLATE.md\`. Fill in identity, core principles, boundaries, capabilities, ownership, ethics.
398
-
399
- **WISDOM.md** — Start with one entry recording creation and key decisions.
400
-
401
- **memory/** — Empty directory for daily logs.
402
-
403
- ### Update framework files
404
- - Update \`.teammates/README.md\` with the roster table, dependency flow, and routing guide
405
- - Update \`.teammates/CROSS-TEAM.md\` Ownership Scopes table with one row per teammate
406
-
407
- ## Step 4: Verify
408
-
409
- Check:
410
- - Every roster teammate has a folder with SOUL.md and WISDOM.md
411
- - Ownership globs cover the codebase without major gaps
412
- - Boundaries reference the correct owning teammate
413
- - CROSS-TEAM.md Ownership Scopes table has one row per teammate with correct paths
414
- - .gitignore is in place (USER.md not committed)
415
-
416
- ## Tips
417
- - Small projects are fine with 2–3 teammates
418
- - WISDOM.md starts light — just one creation entry
419
- - Prompt the user to fill in USER.md after setup
359
+ const BUILTIN_ONBOARDING = `# Teammates Onboarding
360
+
361
+ You are going to analyze a codebase and create a set of AI teammates — persistent personas that each own a slice of the project. Follow these steps in order.
362
+
363
+ ## Step 1: Analyze the Codebase
364
+
365
+ Read the project's entry points to understand its structure:
366
+ - README, CONTRIBUTING, or similar docs
367
+ - Package manifest (package.json, Cargo.toml, pyproject.toml, go.mod, etc.)
368
+ - Top-level directory structure
369
+ - Key configuration files
370
+
371
+ Identify:
372
+ 1. **Major domains/subsystems** — distinct areas of the codebase
373
+ 2. **Dependency flow** — which layers depend on which
374
+ 3. **Key technologies** — languages, frameworks, tools per area
375
+ 4. **File patterns** — glob patterns for each domain
376
+
377
+ ## Step 2: Design the Team
378
+
379
+ Based on your analysis, design a roster of teammates:
380
+ - **Aim for 3–7 teammates.** Fewer for small projects, more for monorepos.
381
+ - **Each teammate owns a distinct domain** with minimal overlap.
382
+ - **Pick short, memorable names** — one word, evocative of the domain.
383
+
384
+ For each teammate, define:
385
+ - Name and one-line persona
386
+ - Primary ownership (file patterns)
387
+ - Key technologies
388
+ - Boundaries (what they do NOT own)
389
+
390
+ ## Step 3: Create the Directory Structure
391
+
392
+ Create teammate folders under \`.teammates/\`:
393
+
394
+ ### Teammate folders
395
+ For each teammate, create \`.teammates/<name>/\` with:
396
+
397
+ **SOUL.md** — Use the template from \`.teammates/TEMPLATE.md\`. Fill in identity, core principles, boundaries, capabilities, ownership, ethics.
398
+
399
+ **WISDOM.md** — Start with one entry recording creation and key decisions.
400
+
401
+ **memory/** — Empty directory for daily logs.
402
+
403
+ ### Update framework files
404
+ - Update \`.teammates/README.md\` with the roster table, dependency flow, and routing guide
405
+ - Update \`.teammates/CROSS-TEAM.md\` Ownership Scopes table with one row per teammate
406
+
407
+ ## Step 4: Verify
408
+
409
+ Check:
410
+ - Every roster teammate has a folder with SOUL.md and WISDOM.md
411
+ - Ownership globs cover the codebase without major gaps
412
+ - Boundaries reference the correct owning teammate
413
+ - CROSS-TEAM.md Ownership Scopes table has one row per teammate with correct paths
414
+ - .gitignore is in place (USER.md not committed)
415
+
416
+ ## Tips
417
+ - Small projects are fine with 2–3 teammates
418
+ - WISDOM.md starts light — just one creation entry
419
+ - Prompt the user to fill in USER.md after setup
420
420
  `;
@@ -63,7 +63,10 @@ export class Orchestrator {
63
63
  }
64
64
  this.onEvent({ type: "task_assigned", assignment });
65
65
  const prevPresence = this.statuses.get(assignment.teammate)?.presence ?? "online";
66
- this.statuses.set(assignment.teammate, { state: "working", presence: prevPresence });
66
+ this.statuses.set(assignment.teammate, {
67
+ state: "working",
68
+ presence: prevPresence,
69
+ });
67
70
  // Get or create session
68
71
  let sessionId = this.sessions.get(assignment.teammate);
69
72
  if (!sessionId) {
@@ -79,6 +82,9 @@ export class Orchestrator {
79
82
  const result = await this.adapter.executeTask(sessionId, teammate, prompt, {
80
83
  raw: assignment.raw,
81
84
  });
85
+ // Propagate system flag so event handlers can distinguish system vs user tasks
86
+ if (assignment.system)
87
+ result.system = true;
82
88
  this.onEvent({ type: "task_completed", result });
83
89
  // Update status (preserve presence)
84
90
  const postPresence = this.statuses.get(assignment.teammate)?.presence ?? "online";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ import { mkdir, readFile, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { loadPersonas, scaffoldFromPersona } from "./personas.js";
6
+ // ── scaffoldFromPersona ─────────────────────────────────────────────
7
+ let testDir;
8
+ beforeEach(async () => {
9
+ testDir = join(tmpdir(), `personas-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
10
+ await mkdir(testDir, { recursive: true });
11
+ });
12
+ afterEach(async () => {
13
+ await rm(testDir, { recursive: true, force: true });
14
+ });
15
+ const samplePersona = {
16
+ persona: "Software Engineer",
17
+ alias: "beacon",
18
+ tier: 1,
19
+ description: "Architecture and implementation",
20
+ body: "# <Name> — Software Engineer\n\n## Identity\n<Name> is the team's SWE.",
21
+ };
22
+ describe("scaffoldFromPersona", () => {
23
+ it("creates teammate folder with SOUL.md and WISDOM.md", async () => {
24
+ const teamDir = await scaffoldFromPersona(testDir, "beacon", samplePersona);
25
+ const soul = await readFile(join(teamDir, "SOUL.md"), "utf-8");
26
+ const wisdom = await readFile(join(teamDir, "WISDOM.md"), "utf-8");
27
+ expect(soul).toContain("# Beacon — Software Engineer");
28
+ expect(soul).toContain("Beacon is the team's SWE.");
29
+ expect(wisdom).toContain("# Beacon — Wisdom");
30
+ expect(wisdom).toContain("Last compacted: never");
31
+ });
32
+ it("normalizes folder name to lowercase with safe chars", async () => {
33
+ const teamDir = await scaffoldFromPersona(testDir, "My Cool Bot!", samplePersona);
34
+ // Should strip spaces, uppercase, and special chars
35
+ expect(teamDir).toContain("mycoolbot");
36
+ });
37
+ it("replaces all <Name> placeholders in SOUL.md", async () => {
38
+ const teamDir = await scaffoldFromPersona(testDir, "atlas", samplePersona);
39
+ const soul = await readFile(join(teamDir, "SOUL.md"), "utf-8");
40
+ expect(soul).not.toContain("<Name>");
41
+ expect(soul).toContain("Atlas");
42
+ });
43
+ it("creates memory subdirectory", async () => {
44
+ const teamDir = await scaffoldFromPersona(testDir, "test", samplePersona);
45
+ // Should not throw — directory exists
46
+ const memDir = join(teamDir, "memory");
47
+ await mkdir(memDir, { recursive: true }); // no-op if exists
48
+ });
49
+ it("capitalizes display name in WISDOM.md", async () => {
50
+ const teamDir = await scaffoldFromPersona(testDir, "forge", samplePersona);
51
+ const wisdom = await readFile(join(teamDir, "WISDOM.md"), "utf-8");
52
+ expect(wisdom).toContain("# Forge — Wisdom");
53
+ });
54
+ });
55
+ // ── loadPersonas ────────────────────────────────────────────────────
56
+ describe("loadPersonas", () => {
57
+ it("loads persona files from the bundled directory", async () => {
58
+ const personas = await loadPersonas();
59
+ // Should find at least the built-in personas
60
+ expect(personas.length).toBeGreaterThan(0);
61
+ });
62
+ it("sorts by tier then alphabetically", async () => {
63
+ const personas = await loadPersonas();
64
+ // Verify ordering: all tier 1 before tier 2
65
+ let lastTier = 0;
66
+ let lastName = "";
67
+ for (const p of personas) {
68
+ if (p.tier > lastTier) {
69
+ lastTier = p.tier;
70
+ lastName = "";
71
+ }
72
+ if (p.tier === lastTier && lastName) {
73
+ expect(p.persona.localeCompare(lastName)).toBeGreaterThanOrEqual(0);
74
+ }
75
+ lastName = p.persona;
76
+ }
77
+ });
78
+ it("each persona has required fields", async () => {
79
+ const personas = await loadPersonas();
80
+ for (const p of personas) {
81
+ expect(p.persona).toBeTruthy();
82
+ expect(p.alias).toBeTruthy();
83
+ expect(p.description).toBeTruthy();
84
+ expect(typeof p.tier).toBe("number");
85
+ expect(p.body.length).toBeGreaterThan(0);
86
+ }
87
+ });
88
+ });
@@ -142,18 +142,18 @@ describe("Registry role parsing", () => {
142
142
  });
143
143
  describe("Registry ownership parsing", () => {
144
144
  it("parses primary ownership patterns", async () => {
145
- const soul = `# Beacon
146
-
147
- ## Ownership
148
-
149
- ### Primary
150
-
151
- - \`recall/src/**\` — All source files
152
- - \`recall/package.json\` — Package manifest
153
-
154
- ### Secondary
155
-
156
- - \`.teammates/.index/**\` — Vector indexes
145
+ const soul = `# Beacon
146
+
147
+ ## Ownership
148
+
149
+ ### Primary
150
+
151
+ - \`recall/src/**\` — All source files
152
+ - \`recall/package.json\` — Package manifest
153
+
154
+ ### Secondary
155
+
156
+ - \`.teammates/.index/**\` — Vector indexes
157
157
  `;
158
158
  await createTeammate("beacon", soul);
159
159
  const registry = new Registry(tempDir);
@@ -174,17 +174,17 @@ describe("Registry ownership parsing", () => {
174
174
  });
175
175
  describe("Registry routing keyword parsing", () => {
176
176
  it("parses routing keywords from ### Routing section", async () => {
177
- const soul = `# Beacon
178
-
179
- ## Ownership
180
-
181
- ### Primary
182
-
183
- - \`recall/src/**\` — All source files
184
-
185
- ### Routing
186
-
187
- - \`search\`, \`embeddings\`, \`vector\`, \`semantic\`
177
+ const soul = `# Beacon
178
+
179
+ ## Ownership
180
+
181
+ ### Primary
182
+
183
+ - \`recall/src/**\` — All source files
184
+
185
+ ### Routing
186
+
187
+ - \`search\`, \`embeddings\`, \`vector\`, \`semantic\`
188
188
  `;
189
189
  await createTeammate("beacon", soul);
190
190
  const registry = new Registry(tempDir);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,113 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { colorToHex, DEFAULT_THEME, setTheme, theme, tp } from "./theme.js";
3
+ describe("theme", () => {
4
+ afterEach(() => {
5
+ // Restore default theme after each test
6
+ setTheme(DEFAULT_THEME);
7
+ });
8
+ it("returns the default theme initially", () => {
9
+ const t = theme();
10
+ expect(t.accent).toEqual(DEFAULT_THEME.accent);
11
+ expect(t.text).toEqual(DEFAULT_THEME.text);
12
+ expect(t.success).toEqual(DEFAULT_THEME.success);
13
+ });
14
+ it("setTheme replaces the active theme", () => {
15
+ const custom = {
16
+ ...DEFAULT_THEME,
17
+ accent: { r: 255, g: 0, b: 0, a: 255 },
18
+ };
19
+ setTheme(custom);
20
+ expect(theme().accent).toEqual({ r: 255, g: 0, b: 0, a: 255 });
21
+ });
22
+ it("setTheme creates a copy (not a reference)", () => {
23
+ const custom = { ...DEFAULT_THEME };
24
+ setTheme(custom);
25
+ custom.accent = { r: 0, g: 0, b: 0, a: 255 };
26
+ // Should not be affected by mutation
27
+ expect(theme().accent).toEqual(DEFAULT_THEME.accent);
28
+ });
29
+ });
30
+ describe("colorToHex", () => {
31
+ it("converts RGB to uppercase hex string", () => {
32
+ expect(colorToHex({ r: 58, g: 150, b: 221, a: 255 })).toBe("#3A96DD");
33
+ });
34
+ it("pads single-digit hex values with zero", () => {
35
+ expect(colorToHex({ r: 0, g: 0, b: 0, a: 255 })).toBe("#000000");
36
+ });
37
+ it("converts white correctly", () => {
38
+ expect(colorToHex({ r: 255, g: 255, b: 255, a: 255 })).toBe("#FFFFFF");
39
+ });
40
+ it("converts mid-range values", () => {
41
+ expect(colorToHex({ r: 128, g: 64, b: 32, a: 255 })).toBe("#804020");
42
+ });
43
+ });
44
+ describe("tp (themed pen shortcuts)", () => {
45
+ // tp functions return StyledSpan (StyledSegment[] with __brand)
46
+ // Each span contains segments with { text, style } entries
47
+ it("accent returns a styled span with correct text", () => {
48
+ const result = tp.accent("hello");
49
+ expect(Array.isArray(result)).toBe(true);
50
+ expect(result).toHaveLength(1);
51
+ expect(result[0].text).toBe("hello");
52
+ });
53
+ it("muted returns a styled span with correct text", () => {
54
+ const result = tp.muted("test");
55
+ expect(Array.isArray(result)).toBe(true);
56
+ expect(result[0].text).toBe("test");
57
+ });
58
+ it("success returns a styled span with correct text", () => {
59
+ const result = tp.success("ok");
60
+ expect(Array.isArray(result)).toBe(true);
61
+ expect(result[0].text).toBe("ok");
62
+ });
63
+ it("error returns a styled span with correct text", () => {
64
+ const result = tp.error("fail");
65
+ expect(Array.isArray(result)).toBe(true);
66
+ expect(result[0].text).toBe("fail");
67
+ });
68
+ it("bold returns a styled span with correct text", () => {
69
+ const result = tp.bold("strong");
70
+ expect(Array.isArray(result)).toBe(true);
71
+ expect(result[0].text).toBe("strong");
72
+ });
73
+ it("accent uses the current theme color", () => {
74
+ const result = tp.accent("x");
75
+ expect(result[0].style.fg).toEqual(DEFAULT_THEME.accent);
76
+ });
77
+ it("picks up theme changes", () => {
78
+ const custom = { ...DEFAULT_THEME, error: { r: 1, g: 2, b: 3, a: 255 } };
79
+ setTheme(custom);
80
+ const result = tp.error("x");
81
+ expect(result[0].style.fg).toEqual({ r: 1, g: 2, b: 3, a: 255 });
82
+ });
83
+ it("accentBright returns a styled span", () => {
84
+ const result = tp.accentBright("x");
85
+ expect(result[0].text).toBe("x");
86
+ expect(result[0].style.fg).toEqual(DEFAULT_THEME.accentBright);
87
+ });
88
+ it("accentDim returns a styled span", () => {
89
+ const result = tp.accentDim("x");
90
+ expect(result[0].text).toBe("x");
91
+ expect(result[0].style.fg).toEqual(DEFAULT_THEME.accentDim);
92
+ });
93
+ it("text returns a styled span", () => {
94
+ const result = tp.text("x");
95
+ expect(result[0].text).toBe("x");
96
+ expect(result[0].style.fg).toEqual(DEFAULT_THEME.text);
97
+ });
98
+ it("dim returns a styled span", () => {
99
+ const result = tp.dim("x");
100
+ expect(result[0].text).toBe("x");
101
+ expect(result[0].style.fg).toEqual(DEFAULT_THEME.textDim);
102
+ });
103
+ it("info returns a styled span", () => {
104
+ const result = tp.info("x");
105
+ expect(result[0].text).toBe("x");
106
+ expect(result[0].style.fg).toEqual(DEFAULT_THEME.info);
107
+ });
108
+ it("warning returns a styled span", () => {
109
+ const result = tp.warning("x");
110
+ expect(result[0].text).toBe("x");
111
+ expect(result[0].style.fg).toEqual(DEFAULT_THEME.warning);
112
+ });
113
+ });