@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
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { describe,
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { Orchestrator } from "./orchestrator.js";
|
|
3
3
|
function makeTeammate(name, role = "Test role.", primary = []) {
|
|
4
4
|
return {
|
|
5
5
|
name,
|
|
6
6
|
role,
|
|
7
7
|
soul: `# ${name}\n\n${role}`,
|
|
8
|
-
|
|
8
|
+
wisdom: "",
|
|
9
9
|
dailyLogs: [],
|
|
10
|
+
weeklyLogs: [],
|
|
10
11
|
ownership: { primary, secondary: [] },
|
|
12
|
+
routingKeywords: [],
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
15
|
function makeMockAdapter(results) {
|
|
@@ -23,6 +25,7 @@ function makeMockAdapter(results) {
|
|
|
23
25
|
success: true,
|
|
24
26
|
summary: `${t.name} completed task`,
|
|
25
27
|
changedFiles: [],
|
|
28
|
+
handoffs: [],
|
|
26
29
|
};
|
|
27
30
|
}),
|
|
28
31
|
destroySession: vi.fn(async () => { }),
|
|
@@ -49,7 +52,10 @@ function createOrchestrator(teammates, adapter, onEvent) {
|
|
|
49
52
|
describe("Orchestrator.route", () => {
|
|
50
53
|
it("routes based on ownership keywords", () => {
|
|
51
54
|
const { orch } = createOrchestrator([
|
|
52
|
-
makeTeammate("beacon", "Platform engineer.", [
|
|
55
|
+
makeTeammate("beacon", "Platform engineer.", [
|
|
56
|
+
"recall/src/**",
|
|
57
|
+
"cli/src/**",
|
|
58
|
+
]),
|
|
53
59
|
makeTeammate("scribe", "Documentation writer.", ["docs/**", "README.md"]),
|
|
54
60
|
]);
|
|
55
61
|
expect(orch.route("fix the recall search")).toBe("beacon");
|
|
@@ -77,6 +83,23 @@ describe("Orchestrator.route", () => {
|
|
|
77
83
|
// "write documentation" matches role word "documentation" (1pt) + ownership keyword "docs" (2pt)
|
|
78
84
|
expect(orch.route("write docs and documentation")).toBe("scribe");
|
|
79
85
|
});
|
|
86
|
+
it("routes based on explicit routing keywords", () => {
|
|
87
|
+
const t1 = makeTeammate("beacon", "Platform engineer.");
|
|
88
|
+
t1.routingKeywords = ["search", "embeddings", "vector"];
|
|
89
|
+
const t2 = makeTeammate("scribe", "Documentation writer.");
|
|
90
|
+
t2.routingKeywords = ["template", "onboarding"];
|
|
91
|
+
const { orch } = createOrchestrator([t1, t2]);
|
|
92
|
+
expect(orch.route("fix the search feature")).toBe("beacon");
|
|
93
|
+
expect(orch.route("update the onboarding flow")).toBe("scribe");
|
|
94
|
+
});
|
|
95
|
+
it("routing keywords beat role-only matches", () => {
|
|
96
|
+
const t1 = makeTeammate("beacon", "Platform engineer.");
|
|
97
|
+
t1.routingKeywords = ["search"];
|
|
98
|
+
const t2 = makeTeammate("finder", "Search specialist.");
|
|
99
|
+
const { orch } = createOrchestrator([t1, t2]);
|
|
100
|
+
// "search" matches beacon's routing keyword (2pt) and finder's role (1pt)
|
|
101
|
+
expect(orch.route("improve search results")).toBe("beacon");
|
|
102
|
+
});
|
|
80
103
|
it("returns null for weak matches (score < 2)", () => {
|
|
81
104
|
const { orch } = createOrchestrator([
|
|
82
105
|
makeTeammate("beacon", "Platform engineer."),
|
|
@@ -124,7 +147,10 @@ describe("Orchestrator.assign", () => {
|
|
|
124
147
|
const events = [];
|
|
125
148
|
const { orch } = createOrchestrator([makeTeammate("beacon")], undefined, (e) => events.push(e));
|
|
126
149
|
await orch.assign({ teammate: "beacon", task: "do stuff" });
|
|
127
|
-
expect(events.map((e) => e.type)).toEqual([
|
|
150
|
+
expect(events.map((e) => e.type)).toEqual([
|
|
151
|
+
"task_assigned",
|
|
152
|
+
"task_completed",
|
|
153
|
+
]);
|
|
128
154
|
});
|
|
129
155
|
it("reuses existing session", async () => {
|
|
130
156
|
const adapter = makeMockAdapter();
|
|
@@ -136,92 +162,22 @@ describe("Orchestrator.assign", () => {
|
|
|
136
162
|
});
|
|
137
163
|
});
|
|
138
164
|
describe("Orchestrator handoffs", () => {
|
|
139
|
-
it("
|
|
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 () => {
|
|
165
|
+
it("returns handoffs in the result for CLI to handle", async () => {
|
|
172
166
|
const results = new Map();
|
|
173
167
|
results.set("beacon", {
|
|
174
168
|
teammate: "beacon",
|
|
175
169
|
success: true,
|
|
176
170
|
summary: "done",
|
|
177
171
|
changedFiles: [],
|
|
178
|
-
|
|
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" },
|
|
172
|
+
handoffs: [{ from: "beacon", to: "scribe", task: "update docs" }],
|
|
186
173
|
});
|
|
187
174
|
const adapter = makeMockAdapter(results);
|
|
188
175
|
const { orch } = createOrchestrator([makeTeammate("beacon"), makeTeammate("scribe")], adapter);
|
|
189
|
-
orch.requireApproval = false;
|
|
190
176
|
const result = await orch.assign({ teammate: "beacon", task: "do stuff" });
|
|
191
|
-
expect(result.
|
|
192
|
-
expect(result.
|
|
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");
|
|
177
|
+
expect(result.handoffs).toHaveLength(1);
|
|
178
|
+
expect(result.handoffs[0].to).toBe("scribe");
|
|
179
|
+
// Orchestrator doesn't auto-follow — status goes to idle
|
|
223
180
|
expect(orch.getStatus("beacon")?.state).toBe("idle");
|
|
224
|
-
expect(orch.getStatus("beacon")?.pendingHandoff).toBeUndefined();
|
|
225
181
|
});
|
|
226
182
|
});
|
|
227
183
|
describe("Orchestrator.reset", () => {
|
|
@@ -243,3 +199,27 @@ describe("Orchestrator.shutdown", () => {
|
|
|
243
199
|
expect(adapter.destroySession).toHaveBeenCalled();
|
|
244
200
|
});
|
|
245
201
|
});
|
|
202
|
+
describe("Orchestrator.refresh", () => {
|
|
203
|
+
it("detects new teammates added to registry after init", async () => {
|
|
204
|
+
const { orch } = createOrchestrator([makeTeammate("beacon")]);
|
|
205
|
+
expect(orch.listTeammates()).toEqual(["beacon"]);
|
|
206
|
+
// Mock loadAll to simulate a new teammate appearing on disk
|
|
207
|
+
const registry = orch.getRegistry();
|
|
208
|
+
vi.spyOn(registry, "loadAll").mockImplementation(async () => {
|
|
209
|
+
registry.register(makeTeammate("pipeline", "DevOps engineer."));
|
|
210
|
+
return registry.all();
|
|
211
|
+
});
|
|
212
|
+
const added = await orch.refresh();
|
|
213
|
+
expect(added).toEqual(["pipeline"]);
|
|
214
|
+
expect(orch.listTeammates()).toContain("pipeline");
|
|
215
|
+
expect(orch.getStatus("pipeline")?.state).toBe("idle");
|
|
216
|
+
});
|
|
217
|
+
it("returns empty array when no new teammates", async () => {
|
|
218
|
+
const { orch } = createOrchestrator([makeTeammate("beacon")]);
|
|
219
|
+
// Mock loadAll to return existing teammates only
|
|
220
|
+
const registry = orch.getRegistry();
|
|
221
|
+
vi.spyOn(registry, "loadAll").mockImplementation(async () => registry.all());
|
|
222
|
+
const added = await orch.refresh();
|
|
223
|
+
expect(added).toEqual([]);
|
|
224
|
+
});
|
|
225
|
+
});
|
package/dist/registry.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Teammate registry.
|
|
3
3
|
*
|
|
4
4
|
* Discovers teammates from .teammates/ and loads their configs
|
|
5
|
-
* (SOUL.md,
|
|
5
|
+
* (SOUL.md, WISDOM.md, daily logs, ownership rules).
|
|
6
6
|
*/
|
|
7
7
|
import type { TeammateConfig } from "./types.js";
|
|
8
8
|
export declare class Registry {
|
package/dist/registry.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Teammate registry.
|
|
3
3
|
*
|
|
4
4
|
* Discovers teammates from .teammates/ and loads their configs
|
|
5
|
-
* (SOUL.md,
|
|
5
|
+
* (SOUL.md, WISDOM.md, daily logs, ownership rules).
|
|
6
6
|
*/
|
|
7
7
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
8
|
-
import {
|
|
8
|
+
import { basename, join } from "node:path";
|
|
9
9
|
export class Registry {
|
|
10
10
|
teammatesDir;
|
|
11
11
|
teammates = new Map();
|
|
@@ -15,7 +15,7 @@ export class Registry {
|
|
|
15
15
|
/** Discover and load all teammates from .teammates/ */
|
|
16
16
|
async loadAll() {
|
|
17
17
|
const entries = await readdir(this.teammatesDir, { withFileTypes: true });
|
|
18
|
-
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
18
|
+
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !e.name.startsWith("_"));
|
|
19
19
|
for (const dir of dirs) {
|
|
20
20
|
const config = await this.loadTeammate(dir.name);
|
|
21
21
|
if (config) {
|
|
@@ -35,17 +35,21 @@ export class Registry {
|
|
|
35
35
|
return null; // Not a teammate folder
|
|
36
36
|
}
|
|
37
37
|
const soul = await readFile(soulPath, "utf-8");
|
|
38
|
-
const
|
|
38
|
+
const wisdom = await readFileSafe(join(dir, "WISDOM.md"));
|
|
39
39
|
const dailyLogs = await loadDailyLogs(join(dir, "memory"));
|
|
40
|
+
const weeklyLogs = await loadWeeklyLogs(join(dir, "memory"));
|
|
40
41
|
const ownership = parseOwnership(soul);
|
|
41
42
|
const role = parseRole(soul);
|
|
43
|
+
const routingKeywords = parseRoutingKeywords(soul);
|
|
42
44
|
const config = {
|
|
43
45
|
name,
|
|
44
46
|
role,
|
|
45
47
|
soul,
|
|
46
|
-
|
|
48
|
+
wisdom,
|
|
47
49
|
dailyLogs,
|
|
50
|
+
weeklyLogs,
|
|
48
51
|
ownership,
|
|
52
|
+
routingKeywords,
|
|
49
53
|
};
|
|
50
54
|
this.teammates.set(name, config);
|
|
51
55
|
return config;
|
|
@@ -82,11 +86,14 @@ async function loadDailyLogs(memoryDir) {
|
|
|
82
86
|
const entries = await readdir(memoryDir);
|
|
83
87
|
const logs = [];
|
|
84
88
|
for (const entry of entries) {
|
|
85
|
-
if (entry.endsWith(".md"))
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
89
|
+
if (!entry.endsWith(".md"))
|
|
90
|
+
continue;
|
|
91
|
+
const stem = basename(entry, ".md");
|
|
92
|
+
// Only include daily logs (YYYY-MM-DD format), skip typed memory files
|
|
93
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(stem))
|
|
94
|
+
continue;
|
|
95
|
+
const content = await readFile(join(memoryDir, entry), "utf-8");
|
|
96
|
+
logs.push({ date: stem, content });
|
|
90
97
|
}
|
|
91
98
|
// Most recent first
|
|
92
99
|
logs.sort((a, b) => b.date.localeCompare(a.date));
|
|
@@ -96,6 +103,30 @@ async function loadDailyLogs(memoryDir) {
|
|
|
96
103
|
return [];
|
|
97
104
|
}
|
|
98
105
|
}
|
|
106
|
+
/** Load weekly summaries from memory/weekly/ directory, most recent first */
|
|
107
|
+
async function loadWeeklyLogs(memoryDir) {
|
|
108
|
+
const weeklyDir = join(memoryDir, "weekly");
|
|
109
|
+
try {
|
|
110
|
+
const entries = await readdir(weeklyDir);
|
|
111
|
+
const logs = [];
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (!entry.endsWith(".md"))
|
|
114
|
+
continue;
|
|
115
|
+
const stem = basename(entry, ".md");
|
|
116
|
+
// Match YYYY-Wnn format
|
|
117
|
+
if (!/^\d{4}-W\d{2}$/.test(stem))
|
|
118
|
+
continue;
|
|
119
|
+
const content = await readFile(join(weeklyDir, entry), "utf-8");
|
|
120
|
+
logs.push({ week: stem, content });
|
|
121
|
+
}
|
|
122
|
+
// Most recent first
|
|
123
|
+
logs.sort((a, b) => b.week.localeCompare(a.week));
|
|
124
|
+
return logs;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
99
130
|
/** Extract role from SOUL.md — uses ## Identity paragraph or **Persona:** line */
|
|
100
131
|
function parseRole(soul) {
|
|
101
132
|
// Look for a line like "**Persona:** Some role description"
|
|
@@ -107,7 +138,7 @@ function parseRole(soul) {
|
|
|
107
138
|
if (identityMatch) {
|
|
108
139
|
// Return just the first sentence
|
|
109
140
|
const firstSentence = identityMatch[1].split(/\.\s/)[0];
|
|
110
|
-
return firstSentence.endsWith(".") ? firstSentence : firstSentence
|
|
141
|
+
return firstSentence.endsWith(".") ? firstSentence : `${firstSentence}.`;
|
|
111
142
|
}
|
|
112
143
|
// Fallback: first non-heading, non-empty line
|
|
113
144
|
const lines = soul.split("\n");
|
|
@@ -115,7 +146,7 @@ function parseRole(soul) {
|
|
|
115
146
|
const trimmed = line.trim();
|
|
116
147
|
if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("---")) {
|
|
117
148
|
const firstSentence = trimmed.split(/\.\s/)[0];
|
|
118
|
-
return firstSentence.endsWith(".") ? firstSentence : firstSentence
|
|
149
|
+
return firstSentence.endsWith(".") ? firstSentence : `${firstSentence}.`;
|
|
119
150
|
}
|
|
120
151
|
}
|
|
121
152
|
return "teammate";
|
|
@@ -133,6 +164,19 @@ function parseOwnership(soul) {
|
|
|
133
164
|
}
|
|
134
165
|
return rules;
|
|
135
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Parse explicit routing keywords from a ### Routing section in SOUL.md.
|
|
169
|
+
* Keywords are backtick-wrapped, comma-separated. Example:
|
|
170
|
+
*
|
|
171
|
+
* ### Routing
|
|
172
|
+
* - `search`, `embeddings`, `vector`, `semantic`
|
|
173
|
+
*/
|
|
174
|
+
function parseRoutingKeywords(soul) {
|
|
175
|
+
const routingMatch = soul.match(/### Routing[\s\S]*?(?=###|## |$)/);
|
|
176
|
+
if (!routingMatch)
|
|
177
|
+
return [];
|
|
178
|
+
return extractPatterns(routingMatch[0]);
|
|
179
|
+
}
|
|
136
180
|
/** Extract file patterns (backtick-wrapped) from a markdown section */
|
|
137
181
|
function extractPatterns(section) {
|
|
138
182
|
const patterns = [];
|
package/dist/registry.test.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Registry } from "./registry.js";
|
|
3
|
-
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
5
2
|
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { Registry } from "./registry.js";
|
|
6
6
|
let tempDir;
|
|
7
7
|
beforeEach(async () => {
|
|
8
8
|
tempDir = await mkdtemp(join(tmpdir(), "teammates-test-"));
|
|
@@ -14,8 +14,8 @@ async function createTeammate(name, soul, options) {
|
|
|
14
14
|
const dir = join(tempDir, name);
|
|
15
15
|
await mkdir(dir, { recursive: true });
|
|
16
16
|
await writeFile(join(dir, "SOUL.md"), soul);
|
|
17
|
-
if (options?.
|
|
18
|
-
await writeFile(join(dir, "
|
|
17
|
+
if (options?.wisdom) {
|
|
18
|
+
await writeFile(join(dir, "WISDOM.md"), options.wisdom);
|
|
19
19
|
}
|
|
20
20
|
if (options?.dailyLogs) {
|
|
21
21
|
const memDir = join(dir, "memory");
|
|
@@ -48,6 +48,14 @@ describe("Registry.loadAll", () => {
|
|
|
48
48
|
await registry.loadAll();
|
|
49
49
|
expect(registry.list()).toEqual(["beacon"]);
|
|
50
50
|
});
|
|
51
|
+
it("skips underscore-prefixed directories", async () => {
|
|
52
|
+
await createTeammate("beacon", "# Beacon\n\nPlatform engineer.");
|
|
53
|
+
await mkdir(join(tempDir, "_standups"), { recursive: true });
|
|
54
|
+
await mkdir(join(tempDir, "_tasks"), { recursive: true });
|
|
55
|
+
const registry = new Registry(tempDir);
|
|
56
|
+
await registry.loadAll();
|
|
57
|
+
expect(registry.list()).toEqual(["beacon"]);
|
|
58
|
+
});
|
|
51
59
|
});
|
|
52
60
|
describe("Registry.loadTeammate", () => {
|
|
53
61
|
it("loads soul content", async () => {
|
|
@@ -57,13 +65,13 @@ describe("Registry.loadTeammate", () => {
|
|
|
57
65
|
const config = await registry.loadTeammate("beacon");
|
|
58
66
|
expect(config?.soul).toBe(soul);
|
|
59
67
|
});
|
|
60
|
-
it("loads
|
|
68
|
+
it("loads wisdom", async () => {
|
|
61
69
|
await createTeammate("beacon", "# Beacon", {
|
|
62
|
-
|
|
70
|
+
wisdom: "Important decision made",
|
|
63
71
|
});
|
|
64
72
|
const registry = new Registry(tempDir);
|
|
65
73
|
const config = await registry.loadTeammate("beacon");
|
|
66
|
-
expect(config?.
|
|
74
|
+
expect(config?.wisdom).toBe("Important decision made");
|
|
67
75
|
});
|
|
68
76
|
it("loads daily logs sorted most recent first", async () => {
|
|
69
77
|
await createTeammate("beacon", "# Beacon", {
|
|
@@ -86,11 +94,11 @@ describe("Registry.loadTeammate", () => {
|
|
|
86
94
|
const config = await registry.loadTeammate("nonexistent");
|
|
87
95
|
expect(config).toBeNull();
|
|
88
96
|
});
|
|
89
|
-
it("returns empty
|
|
97
|
+
it("returns empty wisdom when WISDOM.md is missing", async () => {
|
|
90
98
|
await createTeammate("beacon", "# Beacon");
|
|
91
99
|
const registry = new Registry(tempDir);
|
|
92
100
|
const config = await registry.loadTeammate("beacon");
|
|
93
|
-
expect(config?.
|
|
101
|
+
expect(config?.wisdom).toBe("");
|
|
94
102
|
});
|
|
95
103
|
it("returns empty daily logs when memory/ is missing", async () => {
|
|
96
104
|
await createTeammate("beacon", "# Beacon");
|
|
@@ -143,7 +151,10 @@ describe("Registry ownership parsing", () => {
|
|
|
143
151
|
await createTeammate("beacon", soul);
|
|
144
152
|
const registry = new Registry(tempDir);
|
|
145
153
|
const config = await registry.loadTeammate("beacon");
|
|
146
|
-
expect(config?.ownership.primary).toEqual([
|
|
154
|
+
expect(config?.ownership.primary).toEqual([
|
|
155
|
+
"recall/src/**",
|
|
156
|
+
"recall/package.json",
|
|
157
|
+
]);
|
|
147
158
|
expect(config?.ownership.secondary).toEqual([".teammates/.index/**"]);
|
|
148
159
|
});
|
|
149
160
|
it("returns empty arrays when no ownership section", async () => {
|
|
@@ -154,6 +165,37 @@ describe("Registry ownership parsing", () => {
|
|
|
154
165
|
expect(config?.ownership.secondary).toEqual([]);
|
|
155
166
|
});
|
|
156
167
|
});
|
|
168
|
+
describe("Registry routing keyword parsing", () => {
|
|
169
|
+
it("parses routing keywords from ### Routing section", async () => {
|
|
170
|
+
const soul = `# Beacon
|
|
171
|
+
|
|
172
|
+
## Ownership
|
|
173
|
+
|
|
174
|
+
### Primary
|
|
175
|
+
|
|
176
|
+
- \`recall/src/**\` — All source files
|
|
177
|
+
|
|
178
|
+
### Routing
|
|
179
|
+
|
|
180
|
+
- \`search\`, \`embeddings\`, \`vector\`, \`semantic\`
|
|
181
|
+
`;
|
|
182
|
+
await createTeammate("beacon", soul);
|
|
183
|
+
const registry = new Registry(tempDir);
|
|
184
|
+
const config = await registry.loadTeammate("beacon");
|
|
185
|
+
expect(config?.routingKeywords).toEqual([
|
|
186
|
+
"search",
|
|
187
|
+
"embeddings",
|
|
188
|
+
"vector",
|
|
189
|
+
"semantic",
|
|
190
|
+
]);
|
|
191
|
+
});
|
|
192
|
+
it("returns empty array when no ### Routing section", async () => {
|
|
193
|
+
await createTeammate("beacon", "# Beacon\n\nJust a description.");
|
|
194
|
+
const registry = new Registry(tempDir);
|
|
195
|
+
const config = await registry.loadTeammate("beacon");
|
|
196
|
+
expect(config?.routingKeywords).toEqual([]);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
157
199
|
describe("Registry.register", () => {
|
|
158
200
|
it("registers a teammate programmatically", () => {
|
|
159
201
|
const registry = new Registry(tempDir);
|
|
@@ -161,9 +203,11 @@ describe("Registry.register", () => {
|
|
|
161
203
|
name: "test",
|
|
162
204
|
role: "Test role.",
|
|
163
205
|
soul: "# Test",
|
|
164
|
-
|
|
206
|
+
wisdom: "",
|
|
165
207
|
dailyLogs: [],
|
|
208
|
+
weeklyLogs: [],
|
|
166
209
|
ownership: { primary: [], secondary: [] },
|
|
210
|
+
routingKeywords: [],
|
|
167
211
|
});
|
|
168
212
|
expect(registry.list()).toEqual(["test"]);
|
|
169
213
|
expect(registry.get("test")?.role).toBe("Test role.");
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme — centralized color palette for the teammates CLI.
|
|
3
|
+
*
|
|
4
|
+
* All semantic colors used throughout the UI are defined here.
|
|
5
|
+
* The default palette is derived from the base accent #3A96DD.
|
|
6
|
+
*
|
|
7
|
+
* To change the look of the entire CLI, modify the values in
|
|
8
|
+
* DEFAULT_THEME below. Run `/theme` inside a session to preview
|
|
9
|
+
* all variables with live examples.
|
|
10
|
+
*/
|
|
11
|
+
import { type Color } from "@teammates/consolonia";
|
|
12
|
+
export interface Theme {
|
|
13
|
+
/** Primary accent — used for the logo, commands, teammate names. */
|
|
14
|
+
accent: Color;
|
|
15
|
+
/** Brighter accent — used for highlights, selected items. */
|
|
16
|
+
accentBright: Color;
|
|
17
|
+
/** Dimmed accent — used for borders, subtle accents. */
|
|
18
|
+
accentDim: Color;
|
|
19
|
+
/** Primary text — most readable foreground. */
|
|
20
|
+
text: Color;
|
|
21
|
+
/** Secondary / muted text — descriptions, metadata. */
|
|
22
|
+
textMuted: Color;
|
|
23
|
+
/** Dimmed text — separators, subtle UI chrome. */
|
|
24
|
+
textDim: Color;
|
|
25
|
+
/** Success — checkmarks, "installed", completed tasks. */
|
|
26
|
+
success: Color;
|
|
27
|
+
/** Warning — pending items, caution indicators. */
|
|
28
|
+
warning: Color;
|
|
29
|
+
/** Error — failures, error messages. */
|
|
30
|
+
error: Color;
|
|
31
|
+
/** Info — progress messages, working state. */
|
|
32
|
+
info: Color;
|
|
33
|
+
/** Prompt symbol color. */
|
|
34
|
+
prompt: Color;
|
|
35
|
+
/** User input text. */
|
|
36
|
+
input: Color;
|
|
37
|
+
/** Cursor foreground. */
|
|
38
|
+
cursorFg: Color;
|
|
39
|
+
/** Cursor background. */
|
|
40
|
+
cursorBg: Color;
|
|
41
|
+
/** Separator lines between sections. */
|
|
42
|
+
separator: Color;
|
|
43
|
+
/** Progress/status messages. */
|
|
44
|
+
progress: Color;
|
|
45
|
+
/** Dropdown: normal items. */
|
|
46
|
+
dropdown: Color;
|
|
47
|
+
/** Dropdown: highlighted/selected item. */
|
|
48
|
+
dropdownHighlight: Color;
|
|
49
|
+
}
|
|
50
|
+
export declare const DEFAULT_THEME: Theme;
|
|
51
|
+
/** Get the current active theme. */
|
|
52
|
+
export declare function theme(): Theme;
|
|
53
|
+
/** Replace the active theme. */
|
|
54
|
+
export declare function setTheme(t: Theme): void;
|
|
55
|
+
/** Format a Color as a hex string for display. */
|
|
56
|
+
export declare function colorToHex(c: Color): string;
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme — centralized color palette for the teammates CLI.
|
|
3
|
+
*
|
|
4
|
+
* All semantic colors used throughout the UI are defined here.
|
|
5
|
+
* The default palette is derived from the base accent #3A96DD.
|
|
6
|
+
*
|
|
7
|
+
* To change the look of the entire CLI, modify the values in
|
|
8
|
+
* DEFAULT_THEME below. Run `/theme` inside a session to preview
|
|
9
|
+
* all variables with live examples.
|
|
10
|
+
*/
|
|
11
|
+
import { color } from "@teammates/consolonia";
|
|
12
|
+
// ── Default theme (based on #3A96DD) ─────────────────────────────
|
|
13
|
+
export const DEFAULT_THEME = {
|
|
14
|
+
// Brand / accent — derived from #3A96DD
|
|
15
|
+
accent: color(58, 150, 221), // #3A96DD — the base blue
|
|
16
|
+
accentBright: color(85, 187, 255), // #55BBFF — lighter for highlights
|
|
17
|
+
accentDim: color(40, 100, 150), // #286496 — muted for borders
|
|
18
|
+
// Foreground
|
|
19
|
+
text: color(230, 230, 230), // #E6E6E6 — near-white primary text
|
|
20
|
+
textMuted: color(150, 150, 150), // #969696 — gray for descriptions
|
|
21
|
+
textDim: color(100, 100, 100), // #646464 — dim for separators
|
|
22
|
+
// Status
|
|
23
|
+
success: color(80, 200, 120), // #50C878 — green
|
|
24
|
+
warning: color(230, 180, 50), // #E6B432 — amber
|
|
25
|
+
error: color(230, 70, 70), // #E64646 — red
|
|
26
|
+
info: color(58, 150, 221), // #3A96DD — same as accent
|
|
27
|
+
// Interactive
|
|
28
|
+
prompt: color(150, 150, 150), // #969696 — gray prompt
|
|
29
|
+
input: color(230, 230, 230), // #E6E6E6 — near-white
|
|
30
|
+
cursorFg: color(0, 0, 0), // #000000 — black on light cursor
|
|
31
|
+
cursorBg: color(230, 230, 230), // #E6E6E6 — light block cursor
|
|
32
|
+
separator: color(100, 100, 100), // #646464 — dim rule lines
|
|
33
|
+
progress: color(58, 150, 221), // #3A96DD — blue italic
|
|
34
|
+
dropdown: color(58, 150, 221), // #3A96DD — accent for items
|
|
35
|
+
dropdownHighlight: color(85, 187, 255), // #55BBFF — bright for selected
|
|
36
|
+
};
|
|
37
|
+
// ── Active theme (mutable singleton) ─────────────────────────────
|
|
38
|
+
let _active = { ...DEFAULT_THEME };
|
|
39
|
+
/** Get the current active theme. */
|
|
40
|
+
export function theme() {
|
|
41
|
+
return _active;
|
|
42
|
+
}
|
|
43
|
+
/** Replace the active theme. */
|
|
44
|
+
export function setTheme(t) {
|
|
45
|
+
_active = { ...t };
|
|
46
|
+
}
|
|
47
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
48
|
+
/** Format a Color as a hex string for display. */
|
|
49
|
+
export function colorToHex(c) {
|
|
50
|
+
const r = c.r.toString(16).padStart(2, "0");
|
|
51
|
+
const g = c.g.toString(16).padStart(2, "0");
|
|
52
|
+
const b = c.b.toString(16).padStart(2, "0");
|
|
53
|
+
return `#${r}${g}${b}`.toUpperCase();
|
|
54
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -11,12 +11,16 @@ export interface TeammateConfig {
|
|
|
11
11
|
role: string;
|
|
12
12
|
/** Full SOUL.md content */
|
|
13
13
|
soul: string;
|
|
14
|
-
/** Full
|
|
15
|
-
|
|
14
|
+
/** Full WISDOM.md content */
|
|
15
|
+
wisdom: string;
|
|
16
16
|
/** Daily log entries (most recent first) */
|
|
17
17
|
dailyLogs: DailyLog[];
|
|
18
|
+
/** Weekly summary entries (most recent first) */
|
|
19
|
+
weeklyLogs: WeeklyLog[];
|
|
18
20
|
/** File ownership patterns from SOUL.md */
|
|
19
21
|
ownership: OwnershipRules;
|
|
22
|
+
/** Explicit routing keywords from SOUL.md ### Routing section */
|
|
23
|
+
routingKeywords: string[];
|
|
20
24
|
/** Working directory scope (defaults to repo root) */
|
|
21
25
|
cwd?: string;
|
|
22
26
|
/** Sandbox level (defaults to workspace-write) */
|
|
@@ -26,6 +30,16 @@ export interface DailyLog {
|
|
|
26
30
|
date: string;
|
|
27
31
|
content: string;
|
|
28
32
|
}
|
|
33
|
+
export interface WeeklyLog {
|
|
34
|
+
/** ISO week string, e.g. "2026-W11" */
|
|
35
|
+
week: string;
|
|
36
|
+
content: string;
|
|
37
|
+
}
|
|
38
|
+
export interface MonthlyLog {
|
|
39
|
+
/** Month string, e.g. "2026-03" */
|
|
40
|
+
month: string;
|
|
41
|
+
content: string;
|
|
42
|
+
}
|
|
29
43
|
export interface OwnershipRules {
|
|
30
44
|
primary: string[];
|
|
31
45
|
secondary: string[];
|
|
@@ -50,8 +64,8 @@ export interface TaskResult {
|
|
|
50
64
|
summary: string;
|
|
51
65
|
/** Files that were changed */
|
|
52
66
|
changedFiles: string[];
|
|
53
|
-
/**
|
|
54
|
-
|
|
67
|
+
/** Handoff requests to other teammates */
|
|
68
|
+
handoffs: HandoffEnvelope[];
|
|
55
69
|
/** Raw output from the agent */
|
|
56
70
|
rawOutput?: string;
|
|
57
71
|
}
|
|
@@ -61,8 +75,6 @@ export interface TaskAssignment {
|
|
|
61
75
|
teammate: string;
|
|
62
76
|
/** Task description / prompt */
|
|
63
77
|
task: string;
|
|
64
|
-
/** Optional handoff envelope if this came from another teammate */
|
|
65
|
-
handoff?: HandoffEnvelope;
|
|
66
78
|
/** Extra context to include in the prompt */
|
|
67
79
|
extraContext?: string;
|
|
68
80
|
}
|
|
@@ -73,13 +85,6 @@ export type OrchestratorEvent = {
|
|
|
73
85
|
} | {
|
|
74
86
|
type: "task_completed";
|
|
75
87
|
result: TaskResult;
|
|
76
|
-
} | {
|
|
77
|
-
type: "handoff_initiated";
|
|
78
|
-
envelope: HandoffEnvelope;
|
|
79
|
-
} | {
|
|
80
|
-
type: "handoff_completed";
|
|
81
|
-
envelope: HandoffEnvelope;
|
|
82
|
-
result: TaskResult;
|
|
83
88
|
} | {
|
|
84
89
|
type: "error";
|
|
85
90
|
teammate: string;
|