@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.
Files changed (47) hide show
  1. package/README.md +36 -4
  2. package/dist/adapter.d.ts +19 -3
  3. package/dist/adapter.js +168 -96
  4. package/dist/adapter.test.js +29 -16
  5. package/dist/adapters/cli-proxy.d.ts +3 -1
  6. package/dist/adapters/cli-proxy.js +65 -6
  7. package/dist/adapters/copilot.d.ts +3 -1
  8. package/dist/adapters/copilot.js +16 -3
  9. package/dist/adapters/echo.d.ts +3 -1
  10. package/dist/adapters/echo.js +4 -2
  11. package/dist/banner.js +5 -1
  12. package/dist/cli-args.js +23 -23
  13. package/dist/cli-args.test.d.ts +1 -0
  14. package/dist/cli-args.test.js +125 -0
  15. package/dist/cli.js +486 -220
  16. package/dist/compact.d.ts +23 -0
  17. package/dist/compact.js +181 -11
  18. package/dist/compact.test.js +323 -7
  19. package/dist/index.d.ts +4 -1
  20. package/dist/index.js +3 -1
  21. package/dist/onboard.js +165 -165
  22. package/dist/orchestrator.js +7 -2
  23. package/dist/personas.d.ts +42 -0
  24. package/dist/personas.js +108 -0
  25. package/dist/personas.test.d.ts +1 -0
  26. package/dist/personas.test.js +88 -0
  27. package/dist/registry.test.js +23 -23
  28. package/dist/theme.test.d.ts +1 -0
  29. package/dist/theme.test.js +113 -0
  30. package/dist/types.d.ts +2 -0
  31. package/package.json +4 -3
  32. package/personas/architect.md +95 -0
  33. package/personas/backend.md +97 -0
  34. package/personas/data-engineer.md +96 -0
  35. package/personas/designer.md +96 -0
  36. package/personas/devops.md +97 -0
  37. package/personas/frontend.md +98 -0
  38. package/personas/ml-ai.md +100 -0
  39. package/personas/mobile.md +97 -0
  40. package/personas/performance.md +96 -0
  41. package/personas/pm.md +93 -0
  42. package/personas/prompt-engineer.md +122 -0
  43. package/personas/qa.md +96 -0
  44. package/personas/security.md +96 -0
  45. package/personas/sre.md +97 -0
  46. package/personas/swe.md +92 -0
  47. package/personas/tech-writer.md +97 -0
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type { AgentAdapter, InstalledService, RecallContext, RosterEntry, } from "./adapter.js";
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 { type AgentPreset, CliProxyAdapter, type CliProxyOptions, PRESETS, } from "./adapters/cli-proxy.js";
4
5
  export { EchoAdapter } from "./adapters/echo.js";
5
6
  export type { BannerInfo, ServiceInfo, ServiceStatus } from "./banner.js";
@@ -7,6 +8,8 @@ export { AnimatedBanner } from "./banner.js";
7
8
  export type { CliArgs } from "./cli-args.js";
8
9
  export { findTeammatesDir, PKG_VERSION, parseCliArgs } from "./cli-args.js";
9
10
  export { Orchestrator, type OrchestratorConfig, type TeammateStatus, } from "./orchestrator.js";
11
+ export type { Persona } from "./personas.js";
12
+ export { loadPersonas, scaffoldFromPersona } from "./personas.js";
10
13
  export { Registry } from "./registry.js";
11
14
  export { tp } from "./theme.js";
12
15
  export type { DailyLog, HandoffEnvelope, OrchestratorEvent, OwnershipRules, PresenceState, QueueEntry, SandboxLevel, SlashCommand, TaskAssignment, TaskResult, TeammateConfig, TeammateType, } from "./types.js";
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
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";
6
7
  export { findTeammatesDir, PKG_VERSION, parseCliArgs } from "./cli-args.js";
7
8
  export { Orchestrator, } from "./orchestrator.js";
9
+ export { loadPersonas, scaffoldFromPersona } from "./personas.js";
8
10
  export { Registry } from "./registry.js";
9
11
  export { tp } from "./theme.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) {
@@ -76,7 +79,9 @@ export class Orchestrator {
76
79
  prompt = `${assignment.extraContext}\n\n---\n\n${prompt}`;
77
80
  }
78
81
  // Execute
79
- const result = await this.adapter.executeTask(sessionId, teammate, prompt);
82
+ const result = await this.adapter.executeTask(sessionId, teammate, prompt, {
83
+ raw: assignment.raw,
84
+ });
80
85
  this.onEvent({ type: "task_completed", result });
81
86
  // Update status (preserve presence)
82
87
  const postPresence = this.statuses.get(assignment.teammate)?.presence ?? "online";
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Persona loader — reads bundled persona templates from the personas/ directory.
3
+ *
4
+ * Each persona file is a markdown file with YAML frontmatter:
5
+ * ---
6
+ * persona: Software Engineer
7
+ * alias: beacon
8
+ * tier: 1
9
+ * description: Architecture, implementation, and code quality
10
+ * ---
11
+ * # <Name> — Software Engineer
12
+ * ...body (SOUL.md scaffold)...
13
+ *
14
+ * The `<Name>` placeholder in the body is replaced with the user's chosen
15
+ * teammate name during scaffolding.
16
+ */
17
+ export interface Persona {
18
+ /** Display name, e.g. "Software Engineer" */
19
+ persona: string;
20
+ /** Suggested alias, e.g. "beacon" */
21
+ alias: string;
22
+ /** Tier for ordering: 1 = core, 2 = specialized */
23
+ tier: number;
24
+ /** One-line description shown in selection UI */
25
+ description: string;
26
+ /** Raw SOUL.md body (everything after the closing ---) */
27
+ body: string;
28
+ }
29
+ /**
30
+ * Load all personas from the bundled personas/ directory.
31
+ * Returns sorted by tier (ascending), then alphabetically.
32
+ */
33
+ export declare function loadPersonas(): Promise<Persona[]>;
34
+ /**
35
+ * Scaffold a teammate folder from a persona template.
36
+ *
37
+ * @param teammatesDir - The .teammates/ directory
38
+ * @param name - The teammate name (used as folder name and replaces <Name>)
39
+ * @param persona - The persona to scaffold from
40
+ * @returns The path to the created teammate folder
41
+ */
42
+ export declare function scaffoldFromPersona(teammatesDir: string, name: string, persona: Persona): Promise<string>;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Persona loader — reads bundled persona templates from the personas/ directory.
3
+ *
4
+ * Each persona file is a markdown file with YAML frontmatter:
5
+ * ---
6
+ * persona: Software Engineer
7
+ * alias: beacon
8
+ * tier: 1
9
+ * description: Architecture, implementation, and code quality
10
+ * ---
11
+ * # <Name> — Software Engineer
12
+ * ...body (SOUL.md scaffold)...
13
+ *
14
+ * The `<Name>` placeholder in the body is replaced with the user's chosen
15
+ * teammate name during scaffolding.
16
+ */
17
+ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
18
+ import { dirname, join, resolve } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ /**
22
+ * Resolve the bundled personas/ directory.
23
+ * Works from both dist/ (compiled) and src/ (dev).
24
+ */
25
+ function getPersonasDir() {
26
+ const candidates = [
27
+ resolve(__dirname, "../personas"), // dist/ → cli/personas
28
+ resolve(__dirname, "../../personas"), // src/ → cli/personas (dev)
29
+ ];
30
+ return candidates[0]; // both resolve to cli/personas
31
+ }
32
+ /**
33
+ * Parse a persona file's frontmatter and body.
34
+ */
35
+ function parsePersonaFile(content) {
36
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
37
+ if (!match)
38
+ return null;
39
+ const frontmatter = match[1];
40
+ const body = match[2].trim();
41
+ const persona = extractField(frontmatter, "persona");
42
+ const alias = extractField(frontmatter, "alias");
43
+ const tierStr = extractField(frontmatter, "tier");
44
+ const description = extractField(frontmatter, "description");
45
+ if (!persona || !alias || !description)
46
+ return null;
47
+ return {
48
+ persona,
49
+ alias,
50
+ tier: tierStr ? parseInt(tierStr, 10) : 2,
51
+ description,
52
+ body,
53
+ };
54
+ }
55
+ function extractField(frontmatter, field) {
56
+ const re = new RegExp(`^${field}:\\s*(.+)$`, "m");
57
+ const m = frontmatter.match(re);
58
+ return m?.[1]?.trim();
59
+ }
60
+ /**
61
+ * Load all personas from the bundled personas/ directory.
62
+ * Returns sorted by tier (ascending), then alphabetically.
63
+ */
64
+ export async function loadPersonas() {
65
+ const dir = getPersonasDir();
66
+ const personas = [];
67
+ try {
68
+ const files = await readdir(dir);
69
+ for (const file of files) {
70
+ if (!file.endsWith(".md"))
71
+ continue;
72
+ try {
73
+ const content = await readFile(join(dir, file), "utf-8");
74
+ const persona = parsePersonaFile(content);
75
+ if (persona)
76
+ personas.push(persona);
77
+ }
78
+ catch {
79
+ /* skip unreadable files */
80
+ }
81
+ }
82
+ }
83
+ catch {
84
+ /* personas dir missing — return empty */
85
+ }
86
+ personas.sort((a, b) => a.tier - b.tier || a.persona.localeCompare(b.persona));
87
+ return personas;
88
+ }
89
+ /**
90
+ * Scaffold a teammate folder from a persona template.
91
+ *
92
+ * @param teammatesDir - The .teammates/ directory
93
+ * @param name - The teammate name (used as folder name and replaces <Name>)
94
+ * @param persona - The persona to scaffold from
95
+ * @returns The path to the created teammate folder
96
+ */
97
+ export async function scaffoldFromPersona(teammatesDir, name, persona) {
98
+ const folderName = name.toLowerCase().replace(/[^a-z0-9_-]/g, "");
99
+ const teamDir = join(teammatesDir, folderName);
100
+ await mkdir(teamDir, { recursive: true });
101
+ await mkdir(join(teamDir, "memory"), { recursive: true });
102
+ // Replace <Name> placeholder with the chosen name (capitalize first letter)
103
+ const displayName = name.charAt(0).toUpperCase() + name.slice(1);
104
+ const soulContent = persona.body.replace(/<Name>/g, displayName);
105
+ await writeFile(join(teamDir, "SOUL.md"), soulContent, "utf-8");
106
+ await writeFile(join(teamDir, "WISDOM.md"), `# ${displayName} — Wisdom\n\nDistilled principles. Read this first every session (after SOUL.md).\n\nLast compacted: never\n\n---\n\n*No entries yet — wisdom is distilled from experience.*\n`, "utf-8");
107
+ return teamDir;
108
+ }
@@ -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
+ });