@teammates/cli 0.1.0 → 0.2.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 (76) hide show
  1. package/README.md +31 -22
  2. package/dist/adapter.d.ts +1 -1
  3. package/dist/adapter.js +68 -56
  4. package/dist/adapter.test.js +34 -21
  5. package/dist/adapters/cli-proxy.d.ts +11 -4
  6. package/dist/adapters/cli-proxy.js +176 -162
  7. package/dist/adapters/copilot.d.ts +50 -0
  8. package/dist/adapters/copilot.js +210 -0
  9. package/dist/adapters/echo.d.ts +2 -2
  10. package/dist/adapters/echo.js +2 -1
  11. package/dist/adapters/echo.test.js +4 -2
  12. package/dist/cli-utils.d.ts +21 -0
  13. package/dist/cli-utils.js +74 -0
  14. package/dist/cli-utils.test.d.ts +1 -0
  15. package/dist/cli-utils.test.js +179 -0
  16. package/dist/cli.js +3160 -961
  17. package/dist/compact.d.ts +39 -0
  18. package/dist/compact.js +269 -0
  19. package/dist/compact.test.d.ts +1 -0
  20. package/dist/compact.test.js +198 -0
  21. package/dist/console/ansi.d.ts +18 -0
  22. package/dist/console/ansi.js +20 -0
  23. package/dist/console/ansi.test.d.ts +1 -0
  24. package/dist/console/ansi.test.js +50 -0
  25. package/dist/console/dropdown.d.ts +23 -0
  26. package/dist/console/dropdown.js +63 -0
  27. package/dist/console/file-drop.d.ts +59 -0
  28. package/dist/console/file-drop.js +186 -0
  29. package/dist/console/file-drop.test.d.ts +1 -0
  30. package/dist/console/file-drop.test.js +145 -0
  31. package/dist/console/index.d.ts +22 -0
  32. package/dist/console/index.js +23 -0
  33. package/dist/console/interactive-readline.d.ts +65 -0
  34. package/dist/console/interactive-readline.js +132 -0
  35. package/dist/console/markdown-table.d.ts +17 -0
  36. package/dist/console/markdown-table.js +270 -0
  37. package/dist/console/markdown-table.test.d.ts +1 -0
  38. package/dist/console/markdown-table.test.js +130 -0
  39. package/dist/console/mutable-output.d.ts +21 -0
  40. package/dist/console/mutable-output.js +51 -0
  41. package/dist/console/paste-handler.d.ts +63 -0
  42. package/dist/console/paste-handler.js +177 -0
  43. package/dist/console/prompt-box.d.ts +55 -0
  44. package/dist/console/prompt-box.js +120 -0
  45. package/dist/console/prompt-input.d.ts +136 -0
  46. package/dist/console/prompt-input.js +618 -0
  47. package/dist/console/startup.d.ts +20 -0
  48. package/dist/console/startup.js +138 -0
  49. package/dist/console/startup.test.d.ts +1 -0
  50. package/dist/console/startup.test.js +41 -0
  51. package/dist/console/wordwheel.d.ts +75 -0
  52. package/dist/console/wordwheel.js +123 -0
  53. package/dist/dropdown.js +4 -21
  54. package/dist/index.d.ts +5 -5
  55. package/dist/index.js +3 -3
  56. package/dist/onboard.d.ts +24 -0
  57. package/dist/onboard.js +174 -11
  58. package/dist/orchestrator.d.ts +8 -11
  59. package/dist/orchestrator.js +33 -81
  60. package/dist/orchestrator.test.js +59 -79
  61. package/dist/registry.d.ts +1 -1
  62. package/dist/registry.js +56 -12
  63. package/dist/registry.test.js +57 -13
  64. package/dist/theme.d.ts +56 -0
  65. package/dist/theme.js +54 -0
  66. package/dist/types.d.ts +18 -13
  67. package/package.json +8 -3
  68. package/template/CROSS-TEAM.md +2 -2
  69. package/template/PROTOCOL.md +72 -15
  70. package/template/README.md +2 -2
  71. package/template/TEMPLATE.md +118 -15
  72. package/template/example/SOUL.md +2 -1
  73. package/template/example/WISDOM.md +9 -0
  74. package/dist/adapters/codex.d.ts +0 -50
  75. package/dist/adapters/codex.js +0 -213
  76. package/template/example/MEMORIES.md +0 -26
package/dist/onboard.js CHANGED
@@ -6,8 +6,8 @@
6
6
  * target .teammates/ directory before the agent runs, so the agent only needs to
7
7
  * analyze the codebase and create teammate-specific folders.
8
8
  */
9
- import { readFile, readdir, copyFile, mkdir, stat } from "node:fs/promises";
10
- import { resolve, join, dirname } from "node:path";
9
+ import { copyFile, mkdir, readdir, readFile, stat, writeFile, } from "node:fs/promises";
10
+ import { dirname, join, resolve } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
13
  /**
@@ -49,7 +49,9 @@ export async function copyTemplateFiles(teammatesDir) {
49
49
  await copyFile(src, dest);
50
50
  copied.push(file);
51
51
  }
52
- catch { /* template file missing, skip */ }
52
+ catch {
53
+ /* template file missing, skip */
54
+ }
53
55
  }
54
56
  }
55
57
  // Create .gitignore if it doesn't exist
@@ -78,10 +80,169 @@ export async function copyTemplateFiles(teammatesDir) {
78
80
  copied.push(`example/${file}`);
79
81
  }
80
82
  }
81
- catch { /* template example dir missing, skip */ }
83
+ catch {
84
+ /* template example dir missing, skip */
85
+ }
82
86
  }
83
87
  return copied;
84
88
  }
89
+ /**
90
+ * Check if a directory entry name is a non-teammate entry.
91
+ * Directories starting with "." are local/ephemeral (gitignored).
92
+ * Directories starting with "_" are shared non-teammate folders.
93
+ * Files (non-directories) and special names are also excluded.
94
+ */
95
+ const NON_TEAMMATE_NAMES = new Set(["example", "services.json"]);
96
+ function isNonTeammateEntry(name) {
97
+ return (name.startsWith(".") || name.startsWith("_") || NON_TEAMMATE_NAMES.has(name));
98
+ }
99
+ /**
100
+ * Detect whether a directory entry is a teammate folder (has SOUL.md).
101
+ */
102
+ async function isTeammateFolder(dirPath) {
103
+ try {
104
+ await stat(join(dirPath, "SOUL.md"));
105
+ return true;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ /**
112
+ * Import teammates from another project's .teammates/ dir.
113
+ *
114
+ * Only copies SOUL.md and WISDOM.md per teammate (identity + wisdom carry over).
115
+ * Creates an empty memory/ dir for each (fresh start — no daily logs or typed memories).
116
+ * Also copies USER.md if present. Framework files (CROSS-TEAM.md, README.md, etc.)
117
+ * are project-specific and NOT copied — they get created fresh from the template.
118
+ *
119
+ * Skips teammates that already exist in the target (idempotent).
120
+ * Returns { teammates: string[], files: string[] }.
121
+ */
122
+ export async function importTeammates(sourceDir, targetDir) {
123
+ // Validate source exists and looks like a .teammates/ dir
124
+ try {
125
+ await stat(sourceDir);
126
+ }
127
+ catch {
128
+ throw new Error(`Source directory not found: ${sourceDir}`);
129
+ }
130
+ await mkdir(targetDir, { recursive: true });
131
+ const teammates = [];
132
+ const files = [];
133
+ const entries = await readdir(sourceDir, { withFileTypes: true });
134
+ for (const entry of entries) {
135
+ const srcPath = join(sourceDir, entry.name);
136
+ const destPath = join(targetDir, entry.name);
137
+ if (entry.isDirectory() && !isNonTeammateEntry(entry.name)) {
138
+ // Check if it's a teammate folder
139
+ if (await isTeammateFolder(srcPath)) {
140
+ // Skip if teammate already exists in target
141
+ try {
142
+ await stat(destPath);
143
+ continue;
144
+ }
145
+ catch {
146
+ /* doesn't exist, proceed */
147
+ }
148
+ // Create teammate dir and copy only SOUL.md + WISDOM.md
149
+ await mkdir(destPath, { recursive: true });
150
+ // SOUL.md (required — isTeammateFolder confirmed it exists)
151
+ await copyFile(join(srcPath, "SOUL.md"), join(destPath, "SOUL.md"));
152
+ files.push(`${entry.name}/SOUL.md`);
153
+ // WISDOM.md (optional)
154
+ try {
155
+ await copyFile(join(srcPath, "WISDOM.md"), join(destPath, "WISDOM.md"));
156
+ files.push(`${entry.name}/WISDOM.md`);
157
+ }
158
+ catch {
159
+ /* no WISDOM.md in source, skip */
160
+ }
161
+ // Create empty memory/ dir (fresh start)
162
+ await mkdir(join(destPath, "memory"), { recursive: true });
163
+ teammates.push(entry.name);
164
+ }
165
+ }
166
+ else if (entry.isFile() && entry.name === "USER.md") {
167
+ // Only USER.md transfers — framework files (CROSS-TEAM.md, README.md,
168
+ // PROTOCOL.md, TEMPLATE.md) are project-specific and get created fresh
169
+ // from the template by copyTemplateFiles().
170
+ try {
171
+ await stat(destPath);
172
+ }
173
+ catch {
174
+ await copyFile(srcPath, destPath);
175
+ files.push(entry.name);
176
+ }
177
+ }
178
+ }
179
+ // Ensure .gitignore exists
180
+ const gitignoreDest = join(targetDir, ".gitignore");
181
+ try {
182
+ await stat(gitignoreDest);
183
+ }
184
+ catch {
185
+ await writeFile(gitignoreDest, "USER.md\n.index/\n", "utf-8");
186
+ files.push(".gitignore");
187
+ }
188
+ return { teammates, files };
189
+ }
190
+ /**
191
+ * Build the adaptation prompt for a single imported teammate.
192
+ * Tells the agent to update ownership patterns, file paths, and boundaries
193
+ * for the new codebase while preserving identity, principles, and wisdom.
194
+ *
195
+ * @param teammatesDir - The .teammates/ directory in the target project
196
+ * @param teammateName - The name of the teammate to adapt
197
+ */
198
+ export async function buildAdaptationPrompt(teammatesDir, teammateName) {
199
+ const teammateDir = join(teammatesDir, teammateName);
200
+ // Read the teammate's current SOUL.md and WISDOM.md
201
+ let soulContent = "";
202
+ let wisdomContent = "";
203
+ try {
204
+ soulContent = await readFile(join(teammateDir, "SOUL.md"), "utf-8");
205
+ }
206
+ catch {
207
+ /* missing — agent will create from scratch */
208
+ }
209
+ try {
210
+ wisdomContent = await readFile(join(teammateDir, "WISDOM.md"), "utf-8");
211
+ }
212
+ catch {
213
+ /* missing — that's fine */
214
+ }
215
+ const soulSection = soulContent
216
+ ? `\n\n## Current SOUL.md\n\n\`\`\`markdown\n${soulContent}\n\`\`\``
217
+ : "\n\n*No SOUL.md found — create one from the template.*";
218
+ const wisdomSection = wisdomContent
219
+ ? `\n\n## Current WISDOM.md\n\n\`\`\`markdown\n${wisdomContent}\n\`\`\``
220
+ : "";
221
+ return `You are adapting the imported teammate **${teammateName}** to this new codebase.
222
+
223
+ **Teammate directory:** \`${teammateDir}\`
224
+
225
+ This teammate was imported from another project. Their SOUL.md and WISDOM.md contain identity, principles, and accumulated wisdom that should be preserved, but their **ownership patterns**, **file paths**, **boundaries**, **capabilities**, and **routing keywords** need to be updated for this codebase.
226
+ ${soulSection}${wisdomSection}
227
+
228
+ ## Your job:
229
+
230
+ 1. **Analyze this codebase** — read the project structure, entry points, package manifest, and key files to understand the architecture.
231
+
232
+ 2. **Update ${teammateName}'s SOUL.md**:
233
+ - **Preserve**: Identity, Core Principles, Ethics, personality, tone
234
+ - **Update**: Ownership patterns (primary/secondary file globs), Boundaries (reference correct teammate names), Capabilities (commands, file patterns, technologies), Routing keywords, Quality Bar
235
+ - **Adapt**: Any codebase-specific references (paths, package names, tools)
236
+
237
+ 3. **Update ${teammateName}'s WISDOM.md**:
238
+ - **Preserve**: Wisdom entries that are universal (principles, patterns, lessons)
239
+ - **Remove or update**: Entries referencing old project paths, file names, or architecture
240
+ - **Add**: A creation entry noting this teammate was imported and adapted
241
+
242
+ 4. **Verify** that ownership globs are valid for this codebase.
243
+
244
+ Present your proposed changes before applying them. Focus only on **${teammateName}** — other teammates will be adapted separately.`;
245
+ }
85
246
  /**
86
247
  * Load ONBOARDING.md from the project dir, package root, or built-in fallback.
87
248
  */
@@ -98,7 +259,9 @@ export async function getOnboardingPrompt(projectDir) {
98
259
  return wrapPrompt(content, projectDir);
99
260
  }
100
261
  }
101
- catch { /* not found, try next */ }
262
+ catch {
263
+ /* not found, try next */
264
+ }
102
265
  }
103
266
  return wrapPrompt(BUILTIN_ONBOARDING, projectDir);
104
267
  }
@@ -110,16 +273,16 @@ function wrapPrompt(onboardingContent, projectDir) {
110
273
  **Framework files have already been copied** into \`${projectDir}/.teammates/\` from the template. The following files are already in place:
111
274
  - CROSS-TEAM.md — fill in the Ownership Scopes table as you create teammates
112
275
  - PROTOCOL.md — team protocol (ready to use)
113
- - TEMPLATE.md — reference for creating teammate SOUL.md and MEMORIES.md files
276
+ - TEMPLATE.md — reference for creating teammate SOUL.md and WISDOM.md files
114
277
  - USER.md — user profile (gitignored, user fills in later)
115
278
  - README.md — update with project-specific roster and info
116
279
  - .gitignore — configured for USER.md and .index/
117
- - example/ — example SOUL.md and MEMORIES.md for reference
280
+ - example/ — example SOUL.md and WISDOM.md for reference
118
281
 
119
282
  **Your job is to:**
120
283
  1. Analyze the codebase (Step 1)
121
284
  2. Design the team roster (Step 2)
122
- 3. Create teammate folders with SOUL.md and MEMORIES.md (Step 3) — use TEMPLATE.md for the structure
285
+ 3. Create teammate folders with SOUL.md and WISDOM.md (Step 3) — use TEMPLATE.md for the structure
123
286
  4. Update README.md and CROSS-TEAM.md with the roster info (Step 3)
124
287
  5. Verify everything is in place (Step 4)
125
288
 
@@ -175,7 +338,7 @@ For each teammate, create \`.teammates/<name>/\` with:
175
338
 
176
339
  **SOUL.md** — Use the template from \`.teammates/TEMPLATE.md\`. Fill in identity, core principles, boundaries, capabilities, ownership, ethics.
177
340
 
178
- **MEMORIES.md** — Start with one entry recording creation and key decisions.
341
+ **WISDOM.md** — Start with one entry recording creation and key decisions.
179
342
 
180
343
  **memory/** — Empty directory for daily logs.
181
344
 
@@ -186,7 +349,7 @@ For each teammate, create \`.teammates/<name>/\` with:
186
349
  ## Step 4: Verify
187
350
 
188
351
  Check:
189
- - Every roster teammate has a folder with SOUL.md and MEMORIES.md
352
+ - Every roster teammate has a folder with SOUL.md and WISDOM.md
190
353
  - Ownership globs cover the codebase without major gaps
191
354
  - Boundaries reference the correct owning teammate
192
355
  - CROSS-TEAM.md Ownership Scopes table has one row per teammate with correct paths
@@ -194,6 +357,6 @@ Check:
194
357
 
195
358
  ## Tips
196
359
  - Small projects are fine with 2–3 teammates
197
- - MEMORIES.md starts light — just one creation entry
360
+ - WISDOM.md starts light — just one creation entry
198
361
  - Prompt the user to fill in USER.md after setup
199
362
  `;
@@ -4,9 +4,9 @@
4
4
  * Routes tasks to teammates, manages handoff chains,
5
5
  * and delegates execution to the plugged-in agent adapter.
6
6
  */
7
- import type { TaskAssignment, TaskResult, HandoffEnvelope, OrchestratorEvent } from "./types.js";
8
7
  import type { AgentAdapter } from "./adapter.js";
9
8
  import { Registry } from "./registry.js";
9
+ import type { OrchestratorEvent, TaskAssignment, TaskResult } from "./types.js";
10
10
  export interface OrchestratorConfig {
11
11
  /** Path to .teammates/ directory */
12
12
  teammatesDir: string;
@@ -18,21 +18,17 @@ export interface OrchestratorConfig {
18
18
  onEvent?: (event: OrchestratorEvent) => void;
19
19
  }
20
20
  export interface TeammateStatus {
21
- state: "idle" | "working" | "pending-handoff";
21
+ state: "idle" | "working";
22
22
  lastSummary?: string;
23
23
  lastChangedFiles?: string[];
24
24
  lastTimestamp?: Date;
25
- pendingHandoff?: HandoffEnvelope;
26
25
  }
27
26
  export declare class Orchestrator {
28
27
  private registry;
29
28
  private adapter;
30
29
  private sessions;
31
30
  private statuses;
32
- private maxHandoffDepth;
33
31
  private onEvent;
34
- /** When true, handoffs require explicit /approve */
35
- requireApproval: boolean;
36
32
  constructor(config: OrchestratorConfig);
37
33
  /** Initialize: load all teammates from disk */
38
34
  init(): Promise<void>;
@@ -40,10 +36,6 @@ export declare class Orchestrator {
40
36
  getStatus(name: string): TeammateStatus | undefined;
41
37
  /** Get all statuses */
42
38
  getAllStatuses(): Map<string, TeammateStatus>;
43
- /** Get the pending handoff if any teammate has one */
44
- getPendingHandoff(): HandoffEnvelope | null;
45
- /** Clear a pending handoff (on reject) */
46
- clearPendingHandoff(teammate: string): void;
47
39
  /** List available teammates */
48
40
  listTeammates(): string[];
49
41
  /** Get the registry for direct access */
@@ -52,7 +44,7 @@ export declare class Orchestrator {
52
44
  * Assign a task to a specific teammate and execute it.
53
45
  * If the result contains a handoff, follows the chain automatically.
54
46
  */
55
- assign(assignment: TaskAssignment, depth?: number, visited?: Set<string>): Promise<TaskResult>;
47
+ assign(assignment: TaskAssignment): Promise<TaskResult>;
56
48
  /**
57
49
  * Route a task to the best teammate based on keyword matching.
58
50
  * Uses the routing guide from .teammates/README.md ownership patterns.
@@ -63,6 +55,11 @@ export declare class Orchestrator {
63
55
  * Used as a fallback when keyword routing doesn't find a strong match.
64
56
  */
65
57
  agentRoute(task: string): Promise<string | null>;
58
+ /**
59
+ * Reload the registry from disk and detect new teammates.
60
+ * Returns the names of any newly discovered teammates.
61
+ */
62
+ refresh(): Promise<string[]>;
66
63
  /** Reset all teammate statuses to idle and clear sessions */
67
64
  reset(): Promise<void>;
68
65
  /** Destroy all sessions */
@@ -4,21 +4,16 @@
4
4
  * Routes tasks to teammates, manages handoff chains,
5
5
  * and delegates execution to the plugged-in agent adapter.
6
6
  */
7
- import { formatHandoffContext } from "./adapter.js";
8
7
  import { Registry } from "./registry.js";
9
8
  export class Orchestrator {
10
9
  registry;
11
10
  adapter;
12
- sessions = new Map(); // teammate -> sessionId
11
+ sessions = new Map();
13
12
  statuses = new Map();
14
- maxHandoffDepth;
15
13
  onEvent;
16
- /** When true, handoffs require explicit /approve */
17
- requireApproval = true;
18
14
  constructor(config) {
19
15
  this.registry = new Registry(config.teammatesDir);
20
16
  this.adapter = config.adapter;
21
- this.maxHandoffDepth = config.maxHandoffDepth ?? 5;
22
17
  this.onEvent = config.onEvent ?? (() => { });
23
18
  }
24
19
  /** Initialize: load all teammates from disk */
@@ -36,23 +31,6 @@ export class Orchestrator {
36
31
  getAllStatuses() {
37
32
  return this.statuses;
38
33
  }
39
- /** Get the pending handoff if any teammate has one */
40
- getPendingHandoff() {
41
- for (const [, status] of this.statuses) {
42
- if (status.state === "pending-handoff" && status.pendingHandoff) {
43
- return status.pendingHandoff;
44
- }
45
- }
46
- return null;
47
- }
48
- /** Clear a pending handoff (on reject) */
49
- clearPendingHandoff(teammate) {
50
- const status = this.statuses.get(teammate);
51
- if (status && status.state === "pending-handoff") {
52
- status.state = "idle";
53
- status.pendingHandoff = undefined;
54
- }
55
- }
56
34
  /** List available teammates */
57
35
  listTeammates() {
58
36
  return this.registry.list();
@@ -65,13 +43,9 @@ export class Orchestrator {
65
43
  * Assign a task to a specific teammate and execute it.
66
44
  * If the result contains a handoff, follows the chain automatically.
67
45
  */
68
- async assign(assignment, depth = 0, visited) {
69
- // Normalize: strip leading @ from teammate names (agents may use @mentions)
46
+ async assign(assignment) {
47
+ // Normalize: strip leading @ from teammate names
70
48
  assignment.teammate = assignment.teammate.replace(/^@/, "");
71
- if (assignment.handoff) {
72
- assignment.handoff.to = assignment.handoff.to.replace(/^@/, "");
73
- assignment.handoff.from = assignment.handoff.from.replace(/^@/, "");
74
- }
75
49
  const teammate = this.registry.get(assignment.teammate);
76
50
  if (!teammate) {
77
51
  const error = `Unknown teammate: ${assignment.teammate}`;
@@ -81,24 +55,10 @@ export class Orchestrator {
81
55
  success: false,
82
56
  summary: error,
83
57
  changedFiles: [],
58
+ handoffs: [],
84
59
  };
85
60
  }
86
- // ── Handoff cycle detection ──────────────────────────────────
87
- const chain = visited ?? new Set();
88
- if (chain.has(assignment.teammate)) {
89
- const cycle = [...chain, assignment.teammate].join(" → ");
90
- const error = `Handoff cycle detected: ${cycle}`;
91
- this.onEvent({ type: "error", teammate: assignment.teammate, error });
92
- return {
93
- teammate: assignment.teammate,
94
- success: false,
95
- summary: error,
96
- changedFiles: [],
97
- };
98
- }
99
- chain.add(assignment.teammate);
100
61
  this.onEvent({ type: "task_assigned", assignment });
101
- // Update status
102
62
  this.statuses.set(assignment.teammate, { state: "working" });
103
63
  // Get or create session
104
64
  let sessionId = this.sessions.get(assignment.teammate);
@@ -106,51 +66,21 @@ export class Orchestrator {
106
66
  sessionId = await this.adapter.startSession(teammate);
107
67
  this.sessions.set(assignment.teammate, sessionId);
108
68
  }
109
- // Build prompt with handoff context if present
69
+ // Build prompt
110
70
  let prompt = assignment.task;
111
- if (assignment.handoff) {
112
- const handoffCtx = formatHandoffContext(assignment.handoff);
113
- prompt = `${handoffCtx}\n\n---\n\n${prompt}`;
114
- }
115
71
  if (assignment.extraContext) {
116
72
  prompt = `${assignment.extraContext}\n\n---\n\n${prompt}`;
117
73
  }
118
74
  // Execute
119
75
  const result = await this.adapter.executeTask(sessionId, teammate, prompt);
120
76
  this.onEvent({ type: "task_completed", result });
121
- // Update status with result
122
- const newStatus = {
77
+ // Update status
78
+ this.statuses.set(assignment.teammate, {
123
79
  state: "idle",
124
80
  lastSummary: result.summary,
125
81
  lastChangedFiles: result.changedFiles,
126
82
  lastTimestamp: new Date(),
127
- };
128
- // Handle handoff
129
- if (result.handoff && depth < this.maxHandoffDepth) {
130
- this.onEvent({ type: "handoff_initiated", envelope: result.handoff });
131
- if (this.requireApproval) {
132
- // Park the handoff — user must /approve
133
- newStatus.state = "pending-handoff";
134
- newStatus.pendingHandoff = result.handoff;
135
- this.statuses.set(assignment.teammate, newStatus);
136
- return result;
137
- }
138
- // Auto-follow handoff
139
- this.statuses.set(assignment.teammate, newStatus);
140
- const nextAssignment = {
141
- teammate: result.handoff.to,
142
- task: result.handoff.task,
143
- handoff: result.handoff,
144
- };
145
- const handoffResult = await this.assign(nextAssignment, depth + 1, chain);
146
- this.onEvent({
147
- type: "handoff_completed",
148
- envelope: result.handoff,
149
- result: handoffResult,
150
- });
151
- return handoffResult;
152
- }
153
- this.statuses.set(assignment.teammate, newStatus);
83
+ });
154
84
  return result;
155
85
  }
156
86
  /**
@@ -163,6 +93,12 @@ export class Orchestrator {
163
93
  let bestScore = 0;
164
94
  for (const [name, config] of this.registry.all()) {
165
95
  let score = 0;
96
+ // Check explicit routing keywords (highest priority, same weight as primary)
97
+ for (const kw of config.routingKeywords) {
98
+ if (taskLower.includes(kw.toLowerCase())) {
99
+ score += 2;
100
+ }
101
+ }
166
102
  // Check ownership patterns against task text
167
103
  for (const pattern of [
168
104
  ...config.ownership.primary,
@@ -170,7 +106,7 @@ export class Orchestrator {
170
106
  ]) {
171
107
  // Extract meaningful keywords from glob patterns
172
108
  const keywords = pattern
173
- .replace(/[*\/{}]/g, " ")
109
+ .replace(/[*/{}]/g, " ")
174
110
  .split(/\s+/)
175
111
  .filter((w) => w.length > 2);
176
112
  for (const kw of keywords) {
@@ -216,9 +152,25 @@ export class Orchestrator {
216
152
  });
217
153
  return this.adapter.routeTask(task, roster);
218
154
  }
155
+ /**
156
+ * Reload the registry from disk and detect new teammates.
157
+ * Returns the names of any newly discovered teammates.
158
+ */
159
+ async refresh() {
160
+ const before = new Set(this.registry.list());
161
+ await this.registry.loadAll();
162
+ const added = [];
163
+ for (const name of this.registry.list()) {
164
+ if (!before.has(name)) {
165
+ this.statuses.set(name, { state: "idle" });
166
+ added.push(name);
167
+ }
168
+ }
169
+ return added;
170
+ }
219
171
  /** Reset all teammate statuses to idle and clear sessions */
220
172
  async reset() {
221
- for (const [name, sessionId] of this.sessions) {
173
+ for (const [_name, sessionId] of this.sessions) {
222
174
  if (this.adapter.destroySession) {
223
175
  await this.adapter.destroySession(sessionId);
224
176
  }
@@ -230,7 +182,7 @@ export class Orchestrator {
230
182
  }
231
183
  /** Destroy all sessions */
232
184
  async shutdown() {
233
- for (const [name, sessionId] of this.sessions) {
185
+ for (const [_name, sessionId] of this.sessions) {
234
186
  if (this.adapter.destroySession) {
235
187
  await this.adapter.destroySession(sessionId);
236
188
  }