@teammates/cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -22
- package/dist/adapter.d.ts +1 -1
- package/dist/adapter.js +68 -56
- package/dist/adapter.test.js +34 -21
- package/dist/adapters/cli-proxy.d.ts +11 -4
- package/dist/adapters/cli-proxy.js +176 -162
- package/dist/adapters/copilot.d.ts +50 -0
- package/dist/adapters/copilot.js +210 -0
- package/dist/adapters/echo.d.ts +2 -2
- package/dist/adapters/echo.js +2 -1
- package/dist/adapters/echo.test.js +4 -2
- package/dist/cli-utils.d.ts +21 -0
- package/dist/cli-utils.js +74 -0
- package/dist/cli-utils.test.d.ts +1 -0
- package/dist/cli-utils.test.js +179 -0
- package/dist/cli.js +3160 -961
- package/dist/compact.d.ts +39 -0
- package/dist/compact.js +269 -0
- package/dist/compact.test.d.ts +1 -0
- package/dist/compact.test.js +198 -0
- package/dist/console/ansi.d.ts +18 -0
- package/dist/console/ansi.js +20 -0
- package/dist/console/ansi.test.d.ts +1 -0
- package/dist/console/ansi.test.js +50 -0
- package/dist/console/dropdown.d.ts +23 -0
- package/dist/console/dropdown.js +63 -0
- package/dist/console/file-drop.d.ts +59 -0
- package/dist/console/file-drop.js +186 -0
- package/dist/console/file-drop.test.d.ts +1 -0
- package/dist/console/file-drop.test.js +145 -0
- package/dist/console/index.d.ts +22 -0
- package/dist/console/index.js +23 -0
- package/dist/console/interactive-readline.d.ts +65 -0
- package/dist/console/interactive-readline.js +132 -0
- package/dist/console/markdown-table.d.ts +17 -0
- package/dist/console/markdown-table.js +270 -0
- package/dist/console/markdown-table.test.d.ts +1 -0
- package/dist/console/markdown-table.test.js +130 -0
- package/dist/console/mutable-output.d.ts +21 -0
- package/dist/console/mutable-output.js +51 -0
- package/dist/console/paste-handler.d.ts +63 -0
- package/dist/console/paste-handler.js +177 -0
- package/dist/console/prompt-box.d.ts +55 -0
- package/dist/console/prompt-box.js +120 -0
- package/dist/console/prompt-input.d.ts +136 -0
- package/dist/console/prompt-input.js +618 -0
- package/dist/console/startup.d.ts +20 -0
- package/dist/console/startup.js +138 -0
- package/dist/console/startup.test.d.ts +1 -0
- package/dist/console/startup.test.js +41 -0
- package/dist/console/wordwheel.d.ts +75 -0
- package/dist/console/wordwheel.js +123 -0
- package/dist/dropdown.js +4 -21
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/onboard.d.ts +24 -0
- package/dist/onboard.js +174 -11
- package/dist/orchestrator.d.ts +8 -11
- package/dist/orchestrator.js +33 -81
- package/dist/orchestrator.test.js +59 -79
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +56 -12
- package/dist/registry.test.js +57 -13
- package/dist/theme.d.ts +56 -0
- package/dist/theme.js +54 -0
- package/dist/types.d.ts +18 -13
- package/package.json +8 -3
- package/template/CROSS-TEAM.md +2 -2
- package/template/PROTOCOL.md +72 -15
- package/template/README.md +2 -2
- package/template/TEMPLATE.md +118 -15
- package/template/example/SOUL.md +2 -1
- package/template/example/WISDOM.md +9 -0
- package/dist/adapters/codex.d.ts +0 -50
- package/dist/adapters/codex.js +0 -213
- 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 {
|
|
10
|
-
import {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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
|
|
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
|
-
-
|
|
360
|
+
- WISDOM.md starts light — just one creation entry
|
|
198
361
|
- Prompt the user to fill in USER.md after setup
|
|
199
362
|
`;
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -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"
|
|
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
|
|
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 */
|
package/dist/orchestrator.js
CHANGED
|
@@ -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();
|
|
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
|
|
69
|
-
// Normalize: strip leading @ from teammate names
|
|
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
|
|
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
|
|
122
|
-
|
|
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(/[
|
|
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 [
|
|
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 [
|
|
185
|
+
for (const [_name, sessionId] of this.sessions) {
|
|
234
186
|
if (this.adapter.destroySession) {
|
|
235
187
|
await this.adapter.destroySession(sessionId);
|
|
236
188
|
}
|