@teammates/cli 0.1.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.
@@ -0,0 +1,245 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { Orchestrator } from "./orchestrator.js";
3
+ function makeTeammate(name, role = "Test role.", primary = []) {
4
+ return {
5
+ name,
6
+ role,
7
+ soul: `# ${name}\n\n${role}`,
8
+ memories: "",
9
+ dailyLogs: [],
10
+ ownership: { primary, secondary: [] },
11
+ };
12
+ }
13
+ function makeMockAdapter(results) {
14
+ let sessionCounter = 0;
15
+ return {
16
+ name: "mock",
17
+ startSession: vi.fn(async (t) => `mock-${t.name}-${++sessionCounter}`),
18
+ executeTask: vi.fn(async (_sid, t, _prompt) => {
19
+ if (results?.has(t.name))
20
+ return results.get(t.name);
21
+ return {
22
+ teammate: t.name,
23
+ success: true,
24
+ summary: `${t.name} completed task`,
25
+ changedFiles: [],
26
+ };
27
+ }),
28
+ destroySession: vi.fn(async () => { }),
29
+ };
30
+ }
31
+ function createOrchestrator(teammates, adapter, onEvent) {
32
+ const mockAdapter = adapter ?? makeMockAdapter();
33
+ const orch = new Orchestrator({
34
+ teammatesDir: "/fake/.teammates",
35
+ adapter: mockAdapter,
36
+ onEvent,
37
+ });
38
+ // Register teammates directly instead of loading from disk
39
+ const registry = orch.getRegistry();
40
+ for (const t of teammates) {
41
+ registry.register(t);
42
+ }
43
+ // Initialize statuses
44
+ for (const t of teammates) {
45
+ orch.getAllStatuses().set(t.name, { state: "idle" });
46
+ }
47
+ return { orch, adapter: mockAdapter };
48
+ }
49
+ describe("Orchestrator.route", () => {
50
+ it("routes based on ownership keywords", () => {
51
+ const { orch } = createOrchestrator([
52
+ makeTeammate("beacon", "Platform engineer.", ["recall/src/**", "cli/src/**"]),
53
+ makeTeammate("scribe", "Documentation writer.", ["docs/**", "README.md"]),
54
+ ]);
55
+ expect(orch.route("fix the recall search")).toBe("beacon");
56
+ expect(orch.route("update the docs README")).toBe("scribe");
57
+ });
58
+ it("returns null when no keywords match", () => {
59
+ const { orch } = createOrchestrator([
60
+ makeTeammate("beacon", "Platform engineer.", ["recall/src/**"]),
61
+ ]);
62
+ expect(orch.route("deploy to production")).toBeNull();
63
+ });
64
+ it("scores primary ownership higher than secondary", () => {
65
+ const t1 = makeTeammate("beacon", "Platform engineer.");
66
+ t1.ownership = { primary: ["cli/src/**"], secondary: [] };
67
+ const t2 = makeTeammate("helper", "General helper.");
68
+ t2.ownership = { primary: [], secondary: ["cli/src/**"] };
69
+ const { orch } = createOrchestrator([t1, t2]);
70
+ expect(orch.route("fix the cli")).toBe("beacon");
71
+ });
72
+ it("considers role keywords", () => {
73
+ const { orch } = createOrchestrator([
74
+ makeTeammate("beacon", "Platform engineer with search expertise."),
75
+ makeTeammate("scribe", "Documentation writer.", ["docs/**"]),
76
+ ]);
77
+ // "write documentation" matches role word "documentation" (1pt) + ownership keyword "docs" (2pt)
78
+ expect(orch.route("write docs and documentation")).toBe("scribe");
79
+ });
80
+ it("returns null for weak matches (score < 2)", () => {
81
+ const { orch } = createOrchestrator([
82
+ makeTeammate("beacon", "Platform engineer."),
83
+ makeTeammate("scribe", "Documentation writer."),
84
+ ]);
85
+ // "documentation" only matches a role word (1pt) — too weak to route confidently
86
+ expect(orch.route("write documentation")).toBeNull();
87
+ });
88
+ });
89
+ describe("Orchestrator.assign", () => {
90
+ it("assigns a task and returns result", async () => {
91
+ const { orch } = createOrchestrator([makeTeammate("beacon")]);
92
+ const result = await orch.assign({ teammate: "beacon", task: "do stuff" });
93
+ expect(result.success).toBe(true);
94
+ expect(result.teammate).toBe("beacon");
95
+ });
96
+ it("strips @ from teammate name", async () => {
97
+ const { orch } = createOrchestrator([makeTeammate("beacon")]);
98
+ const result = await orch.assign({ teammate: "@beacon", task: "do stuff" });
99
+ expect(result.success).toBe(true);
100
+ expect(result.teammate).toBe("beacon");
101
+ });
102
+ it("returns error for unknown teammate", async () => {
103
+ const { orch } = createOrchestrator([makeTeammate("beacon")]);
104
+ const result = await orch.assign({ teammate: "unknown", task: "do stuff" });
105
+ expect(result.success).toBe(false);
106
+ expect(result.summary).toContain("Unknown teammate");
107
+ });
108
+ it("updates status to working during task", async () => {
109
+ const statuses = [];
110
+ const adapter = makeMockAdapter();
111
+ const origExecute = adapter.executeTask;
112
+ adapter.executeTask = async (sid, t, p) => {
113
+ const { orch } = { orch: orchRef };
114
+ statuses.push(orch.getStatus(t.name)?.state ?? "none");
115
+ return origExecute(sid, t, p);
116
+ };
117
+ const { orch } = createOrchestrator([makeTeammate("beacon")], adapter);
118
+ const orchRef = orch;
119
+ await orch.assign({ teammate: "beacon", task: "do stuff" });
120
+ expect(statuses).toContain("working");
121
+ expect(orch.getStatus("beacon")?.state).toBe("idle");
122
+ });
123
+ it("emits task_assigned and task_completed events", async () => {
124
+ const events = [];
125
+ const { orch } = createOrchestrator([makeTeammate("beacon")], undefined, (e) => events.push(e));
126
+ await orch.assign({ teammate: "beacon", task: "do stuff" });
127
+ expect(events.map((e) => e.type)).toEqual(["task_assigned", "task_completed"]);
128
+ });
129
+ it("reuses existing session", async () => {
130
+ const adapter = makeMockAdapter();
131
+ const { orch } = createOrchestrator([makeTeammate("beacon")], adapter);
132
+ await orch.assign({ teammate: "beacon", task: "task 1" });
133
+ await orch.assign({ teammate: "beacon", task: "task 2" });
134
+ expect(adapter.startSession).toHaveBeenCalledTimes(1);
135
+ expect(adapter.executeTask).toHaveBeenCalledTimes(2);
136
+ });
137
+ });
138
+ describe("Orchestrator handoffs", () => {
139
+ it("parks handoff when requireApproval is true", async () => {
140
+ const results = new Map();
141
+ results.set("beacon", {
142
+ teammate: "beacon",
143
+ success: true,
144
+ summary: "done",
145
+ changedFiles: [],
146
+ handoff: { from: "beacon", to: "scribe", task: "update docs" },
147
+ });
148
+ const adapter = makeMockAdapter(results);
149
+ const { orch } = createOrchestrator([makeTeammate("beacon"), makeTeammate("scribe")], adapter);
150
+ orch.requireApproval = true;
151
+ await orch.assign({ teammate: "beacon", task: "do stuff" });
152
+ expect(orch.getStatus("beacon")?.state).toBe("pending-handoff");
153
+ expect(orch.getPendingHandoff()?.to).toBe("scribe");
154
+ });
155
+ it("auto-follows handoff when requireApproval is false", async () => {
156
+ const results = new Map();
157
+ results.set("beacon", {
158
+ teammate: "beacon",
159
+ success: true,
160
+ summary: "done",
161
+ changedFiles: [],
162
+ handoff: { from: "beacon", to: "scribe", task: "update docs" },
163
+ });
164
+ const adapter = makeMockAdapter(results);
165
+ const { orch } = createOrchestrator([makeTeammate("beacon"), makeTeammate("scribe")], adapter);
166
+ orch.requireApproval = false;
167
+ const result = await orch.assign({ teammate: "beacon", task: "do stuff" });
168
+ // Final result should be from scribe (the handoff target)
169
+ expect(result.teammate).toBe("scribe");
170
+ });
171
+ it("detects handoff cycles", async () => {
172
+ const results = new Map();
173
+ results.set("beacon", {
174
+ teammate: "beacon",
175
+ success: true,
176
+ summary: "done",
177
+ changedFiles: [],
178
+ handoff: { from: "beacon", to: "scribe", task: "your turn" },
179
+ });
180
+ results.set("scribe", {
181
+ teammate: "scribe",
182
+ success: true,
183
+ summary: "done",
184
+ changedFiles: [],
185
+ handoff: { from: "scribe", to: "beacon", task: "back to you" },
186
+ });
187
+ const adapter = makeMockAdapter(results);
188
+ const { orch } = createOrchestrator([makeTeammate("beacon"), makeTeammate("scribe")], adapter);
189
+ orch.requireApproval = false;
190
+ const result = await orch.assign({ teammate: "beacon", task: "do stuff" });
191
+ expect(result.success).toBe(false);
192
+ expect(result.summary).toContain("cycle");
193
+ });
194
+ it("respects max handoff depth", async () => {
195
+ // Create a chain: a -> b -> c -> d -> e -> f (depth 5 should stop)
196
+ const teammates = ["a", "b", "c", "d", "e", "f"].map((n) => makeTeammate(n));
197
+ const results = new Map();
198
+ for (let i = 0; i < 5; i++) {
199
+ const from = teammates[i].name;
200
+ const to = teammates[i + 1].name;
201
+ results.set(from, {
202
+ teammate: from,
203
+ success: true,
204
+ summary: "done",
205
+ changedFiles: [],
206
+ handoff: { from, to, task: "next" },
207
+ });
208
+ }
209
+ const adapter = makeMockAdapter(results);
210
+ const { orch } = createOrchestrator(teammates, adapter);
211
+ orch.requireApproval = false;
212
+ const result = await orch.assign({ teammate: "a", task: "start" });
213
+ // Should stop at max depth, returning result from the teammate at depth limit
214
+ expect(adapter.executeTask).toHaveBeenCalled();
215
+ });
216
+ it("clears pending handoff on reject", () => {
217
+ const { orch } = createOrchestrator([makeTeammate("beacon")]);
218
+ orch.getAllStatuses().set("beacon", {
219
+ state: "pending-handoff",
220
+ pendingHandoff: { from: "beacon", to: "scribe", task: "docs" },
221
+ });
222
+ orch.clearPendingHandoff("beacon");
223
+ expect(orch.getStatus("beacon")?.state).toBe("idle");
224
+ expect(orch.getStatus("beacon")?.pendingHandoff).toBeUndefined();
225
+ });
226
+ });
227
+ describe("Orchestrator.reset", () => {
228
+ it("resets all statuses to idle and clears sessions", async () => {
229
+ const adapter = makeMockAdapter();
230
+ const { orch } = createOrchestrator([makeTeammate("beacon"), makeTeammate("scribe")], adapter);
231
+ await orch.assign({ teammate: "beacon", task: "task" });
232
+ await orch.reset();
233
+ expect(orch.getStatus("beacon")?.state).toBe("idle");
234
+ expect(adapter.destroySession).toHaveBeenCalled();
235
+ });
236
+ });
237
+ describe("Orchestrator.shutdown", () => {
238
+ it("destroys all sessions", async () => {
239
+ const adapter = makeMockAdapter();
240
+ const { orch } = createOrchestrator([makeTeammate("beacon")], adapter);
241
+ await orch.assign({ teammate: "beacon", task: "task" });
242
+ await orch.shutdown();
243
+ expect(adapter.destroySession).toHaveBeenCalled();
244
+ });
245
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Teammate registry.
3
+ *
4
+ * Discovers teammates from .teammates/ and loads their configs
5
+ * (SOUL.md, MEMORIES.md, daily logs, ownership rules).
6
+ */
7
+ import type { TeammateConfig } from "./types.js";
8
+ export declare class Registry {
9
+ private teammatesDir;
10
+ private teammates;
11
+ constructor(teammatesDir: string);
12
+ /** Discover and load all teammates from .teammates/ */
13
+ loadAll(): Promise<Map<string, TeammateConfig>>;
14
+ /** Load a single teammate by name */
15
+ loadTeammate(name: string): Promise<TeammateConfig | null>;
16
+ /** Register a teammate programmatically (e.g. the agent itself) */
17
+ register(config: TeammateConfig): void;
18
+ /** Get a loaded teammate by name */
19
+ get(name: string): TeammateConfig | undefined;
20
+ /** List all loaded teammate names */
21
+ list(): string[];
22
+ /** Get the full roster */
23
+ all(): Map<string, TeammateConfig>;
24
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Teammate registry.
3
+ *
4
+ * Discovers teammates from .teammates/ and loads their configs
5
+ * (SOUL.md, MEMORIES.md, daily logs, ownership rules).
6
+ */
7
+ import { readdir, readFile, stat } from "node:fs/promises";
8
+ import { join, basename } from "node:path";
9
+ export class Registry {
10
+ teammatesDir;
11
+ teammates = new Map();
12
+ constructor(teammatesDir) {
13
+ this.teammatesDir = teammatesDir;
14
+ }
15
+ /** Discover and load all teammates from .teammates/ */
16
+ async loadAll() {
17
+ const entries = await readdir(this.teammatesDir, { withFileTypes: true });
18
+ const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith("."));
19
+ for (const dir of dirs) {
20
+ const config = await this.loadTeammate(dir.name);
21
+ if (config) {
22
+ this.teammates.set(dir.name, config);
23
+ }
24
+ }
25
+ return this.teammates;
26
+ }
27
+ /** Load a single teammate by name */
28
+ async loadTeammate(name) {
29
+ const dir = join(this.teammatesDir, name);
30
+ const soulPath = join(dir, "SOUL.md");
31
+ try {
32
+ await stat(soulPath);
33
+ }
34
+ catch {
35
+ return null; // Not a teammate folder
36
+ }
37
+ const soul = await readFile(soulPath, "utf-8");
38
+ const memories = await readFileSafe(join(dir, "MEMORIES.md"));
39
+ const dailyLogs = await loadDailyLogs(join(dir, "memory"));
40
+ const ownership = parseOwnership(soul);
41
+ const role = parseRole(soul);
42
+ const config = {
43
+ name,
44
+ role,
45
+ soul,
46
+ memories,
47
+ dailyLogs,
48
+ ownership,
49
+ };
50
+ this.teammates.set(name, config);
51
+ return config;
52
+ }
53
+ /** Register a teammate programmatically (e.g. the agent itself) */
54
+ register(config) {
55
+ this.teammates.set(config.name, config);
56
+ }
57
+ /** Get a loaded teammate by name */
58
+ get(name) {
59
+ return this.teammates.get(name);
60
+ }
61
+ /** List all loaded teammate names */
62
+ list() {
63
+ return Array.from(this.teammates.keys());
64
+ }
65
+ /** Get the full roster */
66
+ all() {
67
+ return this.teammates;
68
+ }
69
+ }
70
+ /** Read a file, return empty string if missing */
71
+ async function readFileSafe(path) {
72
+ try {
73
+ return await readFile(path, "utf-8");
74
+ }
75
+ catch {
76
+ return "";
77
+ }
78
+ }
79
+ /** Load daily logs from memory/ directory, most recent first */
80
+ async function loadDailyLogs(memoryDir) {
81
+ try {
82
+ const entries = await readdir(memoryDir);
83
+ const logs = [];
84
+ for (const entry of entries) {
85
+ if (entry.endsWith(".md")) {
86
+ const date = basename(entry, ".md");
87
+ const content = await readFile(join(memoryDir, entry), "utf-8");
88
+ logs.push({ date, content });
89
+ }
90
+ }
91
+ // Most recent first
92
+ logs.sort((a, b) => b.date.localeCompare(a.date));
93
+ return logs;
94
+ }
95
+ catch {
96
+ return [];
97
+ }
98
+ }
99
+ /** Extract role from SOUL.md — uses ## Identity paragraph or **Persona:** line */
100
+ function parseRole(soul) {
101
+ // Look for a line like "**Persona:** Some role description"
102
+ const personaMatch = soul.match(/\*\*Persona:\*\*\s*(.+)/);
103
+ if (personaMatch)
104
+ return personaMatch[1].trim();
105
+ // Look for the paragraph under ## Identity
106
+ const identityMatch = soul.match(/## Identity\s*\n\s*\n(.+)/);
107
+ if (identityMatch) {
108
+ // Return just the first sentence
109
+ const firstSentence = identityMatch[1].split(/\.\s/)[0];
110
+ return firstSentence.endsWith(".") ? firstSentence : firstSentence + ".";
111
+ }
112
+ // Fallback: first non-heading, non-empty line
113
+ const lines = soul.split("\n");
114
+ for (const line of lines) {
115
+ const trimmed = line.trim();
116
+ if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("---")) {
117
+ const firstSentence = trimmed.split(/\.\s/)[0];
118
+ return firstSentence.endsWith(".") ? firstSentence : firstSentence + ".";
119
+ }
120
+ }
121
+ return "teammate";
122
+ }
123
+ /** Parse ownership patterns from SOUL.md */
124
+ function parseOwnership(soul) {
125
+ const rules = { primary: [], secondary: [] };
126
+ const primaryMatch = soul.match(/### Primary[\s\S]*?(?=###|## |$)/);
127
+ if (primaryMatch) {
128
+ rules.primary = extractPatterns(primaryMatch[0]);
129
+ }
130
+ const secondaryMatch = soul.match(/### Secondary[\s\S]*?(?=###|## |$)/);
131
+ if (secondaryMatch) {
132
+ rules.secondary = extractPatterns(secondaryMatch[0]);
133
+ }
134
+ return rules;
135
+ }
136
+ /** Extract file patterns (backtick-wrapped) from a markdown section */
137
+ function extractPatterns(section) {
138
+ const patterns = [];
139
+ const regex = /`([^`]+)`/g;
140
+ let match;
141
+ while ((match = regex.exec(section)) !== null) {
142
+ patterns.push(match[1]);
143
+ }
144
+ return patterns;
145
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,171 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { Registry } from "./registry.js";
3
+ import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ let tempDir;
7
+ beforeEach(async () => {
8
+ tempDir = await mkdtemp(join(tmpdir(), "teammates-test-"));
9
+ });
10
+ afterEach(async () => {
11
+ await rm(tempDir, { recursive: true, force: true });
12
+ });
13
+ async function createTeammate(name, soul, options) {
14
+ const dir = join(tempDir, name);
15
+ await mkdir(dir, { recursive: true });
16
+ await writeFile(join(dir, "SOUL.md"), soul);
17
+ if (options?.memories) {
18
+ await writeFile(join(dir, "MEMORIES.md"), options.memories);
19
+ }
20
+ if (options?.dailyLogs) {
21
+ const memDir = join(dir, "memory");
22
+ await mkdir(memDir, { recursive: true });
23
+ for (const log of options.dailyLogs) {
24
+ await writeFile(join(memDir, `${log.date}.md`), log.content);
25
+ }
26
+ }
27
+ }
28
+ describe("Registry.loadAll", () => {
29
+ it("discovers teammates with SOUL.md", async () => {
30
+ await createTeammate("beacon", "# Beacon\n\nPlatform engineer.");
31
+ await createTeammate("scribe", "# Scribe\n\nDocumentation writer.");
32
+ const registry = new Registry(tempDir);
33
+ await registry.loadAll();
34
+ expect(registry.list().sort()).toEqual(["beacon", "scribe"]);
35
+ });
36
+ it("skips directories without SOUL.md", async () => {
37
+ await createTeammate("beacon", "# Beacon\n\nPlatform engineer.");
38
+ await mkdir(join(tempDir, "not-a-teammate"), { recursive: true });
39
+ const registry = new Registry(tempDir);
40
+ await registry.loadAll();
41
+ expect(registry.list()).toEqual(["beacon"]);
42
+ });
43
+ it("skips dot directories", async () => {
44
+ await createTeammate("beacon", "# Beacon\n\nPlatform engineer.");
45
+ await mkdir(join(tempDir, ".hidden"), { recursive: true });
46
+ await writeFile(join(tempDir, ".hidden", "SOUL.md"), "hidden");
47
+ const registry = new Registry(tempDir);
48
+ await registry.loadAll();
49
+ expect(registry.list()).toEqual(["beacon"]);
50
+ });
51
+ });
52
+ describe("Registry.loadTeammate", () => {
53
+ it("loads soul content", async () => {
54
+ const soul = "# Beacon\n\nBeacon owns the recall package.";
55
+ await createTeammate("beacon", soul);
56
+ const registry = new Registry(tempDir);
57
+ const config = await registry.loadTeammate("beacon");
58
+ expect(config?.soul).toBe(soul);
59
+ });
60
+ it("loads memories", async () => {
61
+ await createTeammate("beacon", "# Beacon", {
62
+ memories: "Important decision made",
63
+ });
64
+ const registry = new Registry(tempDir);
65
+ const config = await registry.loadTeammate("beacon");
66
+ expect(config?.memories).toBe("Important decision made");
67
+ });
68
+ it("loads daily logs sorted most recent first", async () => {
69
+ await createTeammate("beacon", "# Beacon", {
70
+ dailyLogs: [
71
+ { date: "2026-03-11", content: "Day 1" },
72
+ { date: "2026-03-13", content: "Day 3" },
73
+ { date: "2026-03-12", content: "Day 2" },
74
+ ],
75
+ });
76
+ const registry = new Registry(tempDir);
77
+ const config = await registry.loadTeammate("beacon");
78
+ expect(config?.dailyLogs.map((l) => l.date)).toEqual([
79
+ "2026-03-13",
80
+ "2026-03-12",
81
+ "2026-03-11",
82
+ ]);
83
+ });
84
+ it("returns null for missing teammate", async () => {
85
+ const registry = new Registry(tempDir);
86
+ const config = await registry.loadTeammate("nonexistent");
87
+ expect(config).toBeNull();
88
+ });
89
+ it("returns empty memories when MEMORIES.md is missing", async () => {
90
+ await createTeammate("beacon", "# Beacon");
91
+ const registry = new Registry(tempDir);
92
+ const config = await registry.loadTeammate("beacon");
93
+ expect(config?.memories).toBe("");
94
+ });
95
+ it("returns empty daily logs when memory/ is missing", async () => {
96
+ await createTeammate("beacon", "# Beacon");
97
+ const registry = new Registry(tempDir);
98
+ const config = await registry.loadTeammate("beacon");
99
+ expect(config?.dailyLogs).toEqual([]);
100
+ });
101
+ });
102
+ describe("Registry role parsing", () => {
103
+ it("parses role from ## Identity paragraph", async () => {
104
+ await createTeammate("beacon", "# Beacon\n\n## Identity\n\nBeacon owns the recall package. It does stuff.");
105
+ const registry = new Registry(tempDir);
106
+ const config = await registry.loadTeammate("beacon");
107
+ expect(config?.role).toBe("Beacon owns the recall package.");
108
+ });
109
+ it("parses role from **Persona:** line", async () => {
110
+ await createTeammate("beacon", "# Beacon\n\n**Persona:** The platform engineer.");
111
+ const registry = new Registry(tempDir);
112
+ const config = await registry.loadTeammate("beacon");
113
+ expect(config?.role).toBe("The platform engineer.");
114
+ });
115
+ it("falls back to first non-heading line", async () => {
116
+ await createTeammate("beacon", "# Beacon\n\nSome role description here.");
117
+ const registry = new Registry(tempDir);
118
+ const config = await registry.loadTeammate("beacon");
119
+ expect(config?.role).toBe("Some role description here.");
120
+ });
121
+ it("returns 'teammate' when no role found", async () => {
122
+ await createTeammate("beacon", "# Beacon\n\n---");
123
+ const registry = new Registry(tempDir);
124
+ const config = await registry.loadTeammate("beacon");
125
+ expect(config?.role).toBe("teammate");
126
+ });
127
+ });
128
+ describe("Registry ownership parsing", () => {
129
+ it("parses primary ownership patterns", async () => {
130
+ const soul = `# Beacon
131
+
132
+ ## Ownership
133
+
134
+ ### Primary
135
+
136
+ - \`recall/src/**\` — All source files
137
+ - \`recall/package.json\` — Package manifest
138
+
139
+ ### Secondary
140
+
141
+ - \`.teammates/.index/**\` — Vector indexes
142
+ `;
143
+ await createTeammate("beacon", soul);
144
+ const registry = new Registry(tempDir);
145
+ const config = await registry.loadTeammate("beacon");
146
+ expect(config?.ownership.primary).toEqual(["recall/src/**", "recall/package.json"]);
147
+ expect(config?.ownership.secondary).toEqual([".teammates/.index/**"]);
148
+ });
149
+ it("returns empty arrays when no ownership section", async () => {
150
+ await createTeammate("beacon", "# Beacon\n\nJust a description.");
151
+ const registry = new Registry(tempDir);
152
+ const config = await registry.loadTeammate("beacon");
153
+ expect(config?.ownership.primary).toEqual([]);
154
+ expect(config?.ownership.secondary).toEqual([]);
155
+ });
156
+ });
157
+ describe("Registry.register", () => {
158
+ it("registers a teammate programmatically", () => {
159
+ const registry = new Registry(tempDir);
160
+ registry.register({
161
+ name: "test",
162
+ role: "Test role.",
163
+ soul: "# Test",
164
+ memories: "",
165
+ dailyLogs: [],
166
+ ownership: { primary: [], secondary: [] },
167
+ });
168
+ expect(registry.list()).toEqual(["test"]);
169
+ expect(registry.get("test")?.role).toBe("Test role.");
170
+ });
171
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Core types for @teammates/cli.
3
+ */
4
+ /** Sandbox level controlling what a teammate can do */
5
+ export type SandboxLevel = "read-only" | "workspace-write" | "danger-full-access";
6
+ /** A teammate's loaded configuration */
7
+ export interface TeammateConfig {
8
+ /** Teammate name (folder name under .teammates/) */
9
+ name: string;
10
+ /** Role description from SOUL.md */
11
+ role: string;
12
+ /** Full SOUL.md content */
13
+ soul: string;
14
+ /** Full MEMORIES.md content */
15
+ memories: string;
16
+ /** Daily log entries (most recent first) */
17
+ dailyLogs: DailyLog[];
18
+ /** File ownership patterns from SOUL.md */
19
+ ownership: OwnershipRules;
20
+ /** Working directory scope (defaults to repo root) */
21
+ cwd?: string;
22
+ /** Sandbox level (defaults to workspace-write) */
23
+ sandbox?: SandboxLevel;
24
+ }
25
+ export interface DailyLog {
26
+ date: string;
27
+ content: string;
28
+ }
29
+ export interface OwnershipRules {
30
+ primary: string[];
31
+ secondary: string[];
32
+ }
33
+ /** Structured handoff envelope passed between teammates */
34
+ export interface HandoffEnvelope {
35
+ from: string;
36
+ to: string;
37
+ task: string;
38
+ changedFiles?: string[];
39
+ acceptanceCriteria?: string[];
40
+ openQuestions?: string[];
41
+ context?: string;
42
+ }
43
+ /** Result from an agent completing a task */
44
+ export interface TaskResult {
45
+ /** The teammate that executed the task */
46
+ teammate: string;
47
+ /** Whether the task completed successfully */
48
+ success: boolean;
49
+ /** Summary of what was done */
50
+ summary: string;
51
+ /** Files that were changed */
52
+ changedFiles: string[];
53
+ /** Optional handoff request to another teammate */
54
+ handoff?: HandoffEnvelope;
55
+ /** Raw output from the agent */
56
+ rawOutput?: string;
57
+ }
58
+ /** Task assignment to a teammate */
59
+ export interface TaskAssignment {
60
+ /** Target teammate name */
61
+ teammate: string;
62
+ /** Task description / prompt */
63
+ task: string;
64
+ /** Optional handoff envelope if this came from another teammate */
65
+ handoff?: HandoffEnvelope;
66
+ /** Extra context to include in the prompt */
67
+ extraContext?: string;
68
+ }
69
+ /** Orchestrator event for logging/hooks */
70
+ export type OrchestratorEvent = {
71
+ type: "task_assigned";
72
+ assignment: TaskAssignment;
73
+ } | {
74
+ type: "task_completed";
75
+ result: TaskResult;
76
+ } | {
77
+ type: "handoff_initiated";
78
+ envelope: HandoffEnvelope;
79
+ } | {
80
+ type: "handoff_completed";
81
+ envelope: HandoffEnvelope;
82
+ result: TaskResult;
83
+ } | {
84
+ type: "error";
85
+ teammate: string;
86
+ error: string;
87
+ };
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Core types for @teammates/cli.
3
+ */
4
+ export {};