@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.
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
@@ -1,13 +1,15 @@
1
- import { describe, it, expect, vi } from "vitest";
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
- memories: "",
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.", ["recall/src/**", "cli/src/**"]),
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(["task_assigned", "task_completed"]);
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("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 () => {
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
- 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" },
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.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");
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
+ });
@@ -2,7 +2,7 @@
2
2
  * Teammate registry.
3
3
  *
4
4
  * Discovers teammates from .teammates/ and loads their configs
5
- * (SOUL.md, MEMORIES.md, daily logs, ownership rules).
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, MEMORIES.md, daily logs, ownership rules).
5
+ * (SOUL.md, WISDOM.md, daily logs, ownership rules).
6
6
  */
7
7
  import { readdir, readFile, stat } from "node:fs/promises";
8
- import { join, basename } from "node:path";
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 memories = await readFileSafe(join(dir, "MEMORIES.md"));
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
- memories,
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
- const date = basename(entry, ".md");
87
- const content = await readFile(join(memoryDir, entry), "utf-8");
88
- logs.push({ date, content });
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 = [];
@@ -1,8 +1,8 @@
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";
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?.memories) {
18
- await writeFile(join(dir, "MEMORIES.md"), options.memories);
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 memories", async () => {
68
+ it("loads wisdom", async () => {
61
69
  await createTeammate("beacon", "# Beacon", {
62
- memories: "Important decision made",
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?.memories).toBe("Important decision made");
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 memories when MEMORIES.md is missing", async () => {
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?.memories).toBe("");
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(["recall/src/**", "recall/package.json"]);
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
- memories: "",
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.");
@@ -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 MEMORIES.md content */
15
- memories: string;
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
- /** Optional handoff request to another teammate */
54
- handoff?: HandoffEnvelope;
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;