@teamclaws/teamclaw 2026.3.21

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,116 @@
1
+ import type { PluginConfig, TeamState } from "../types.js";
2
+ import { ROLES } from "../roles.js";
3
+
4
+ const TEAMCLAW_ROLE_IDS_TEXT = [
5
+ "pm",
6
+ "architect",
7
+ "developer",
8
+ "qa",
9
+ "release-engineer",
10
+ "infra-engineer",
11
+ "devops",
12
+ "security-engineer",
13
+ "designer",
14
+ "marketing",
15
+ ].join(", ");
16
+
17
+ export type ControllerPromptDeps = {
18
+ config: PluginConfig;
19
+ getTeamState: () => TeamState | null;
20
+ };
21
+
22
+ export function createControllerPromptInjector(deps: ControllerPromptDeps) {
23
+ return () => {
24
+ const state = deps.getTeamState();
25
+ if (!state) return null;
26
+
27
+ const workers = Object.values(state.workers);
28
+ const tasks = Object.values(state.tasks);
29
+ const pendingTasks = tasks.filter((t) => t.status === "pending");
30
+ const activeTasks = tasks.filter((t) => t.status === "in_progress" || t.status === "assigned");
31
+ const blockedTasks = tasks.filter((t) => t.status === "blocked");
32
+ const completedTasks = tasks.filter((t) => t.status === "completed");
33
+ const pendingClarifications = Object.values(state.clarifications).filter((c) => c.status === "pending");
34
+
35
+ const parts: string[] = [
36
+ "## TeamClaw Controller Mode",
37
+ "You are the Team Controller and the first-pass requirements analyst for the human.",
38
+ "Treat human input as raw requirements unless it is already explicitly phrased as an execution-ready TeamClaw task.",
39
+ "",
40
+ "### Available Tools",
41
+ "- teamclaw_create_task: Create a new task with role assignment",
42
+ "- teamclaw_list_tasks: List all tasks with status filtering",
43
+ "- teamclaw_assign_task: Assign a task to a specific worker",
44
+ "- teamclaw_send_message: Send messages between team members",
45
+ "",
46
+ "### Current Team Status",
47
+ ];
48
+
49
+ if (workers.length === 0) {
50
+ parts.push("- No workers registered yet");
51
+ } else {
52
+ for (const w of workers) {
53
+ const roleDef = ROLES.find((r) => r.id === w.role);
54
+ const statusIcon = w.status === "idle" ? "[idle]" : w.status === "busy" ? "[busy]" : "[offline]";
55
+ const currentTask = w.currentTaskId ? ` (task: ${w.currentTaskId})` : "";
56
+ parts.push(`- ${roleDef?.icon ?? ""} ${w.label} (${w.id}) ${statusIcon}${currentTask}`);
57
+ }
58
+ }
59
+
60
+ parts.push("");
61
+ parts.push(`### Tasks Summary`);
62
+ parts.push(`- Pending: ${pendingTasks.length} | Active: ${activeTasks.length} | Blocked: ${blockedTasks.length} | Completed: ${completedTasks.length}`);
63
+
64
+ if (pendingClarifications.length > 0) {
65
+ parts.push("");
66
+ parts.push("Pending clarification requests:");
67
+ for (const clarification of pendingClarifications.slice(0, 10)) {
68
+ parts.push(`- Task ${clarification.taskId}: ${clarification.question}`);
69
+ }
70
+ }
71
+
72
+ if (pendingTasks.length > 0) {
73
+ parts.push("");
74
+ parts.push("Pending tasks:");
75
+ for (const t of pendingTasks.slice(0, 10)) {
76
+ parts.push(`- [${t.priority}] ${t.title} (role: ${t.assignedRole ?? "any"})`);
77
+ }
78
+ }
79
+
80
+ parts.push("");
81
+ parts.push("### Available Roles");
82
+ for (const role of ROLES) {
83
+ parts.push(`- ${role.icon} ${role.label}: ${role.description}`);
84
+ }
85
+
86
+ parts.push("");
87
+ parts.push("## Requirement Intake Rules");
88
+ parts.push("- Human messages are the initial requirement, not an already-decomposed task tree.");
89
+ parts.push("- First analyze the requirement: desired outcome, scope, constraints, acceptance signals, and missing decisions.");
90
+ parts.push("- If critical information is missing, ask the human a concrete clarification question before creating execution tasks.");
91
+ parts.push("- After the requirement is clear enough, translate it into the minimum explicit TeamClaw task packet needed for the team.");
92
+ parts.push("- 'Minimum task packet' means only tasks that can start immediately with the currently available information and already-satisfied prerequisites.");
93
+ parts.push("- If later phases depend on outputs that do not exist yet, describe them to the human as the plan, but do not create those TeamClaw tasks yet.");
94
+ parts.push("- Downstream QA/review/release/README/integration tasks must stay in the plan until the upstream code or artifacts already exist in the workspace.");
95
+ parts.push("- Do not dump raw user wording directly onto workers when the requirement still needs controller-side analysis.");
96
+ parts.push("- TeamClaw uses git as the default file collaboration mechanism. Do not invent ad-hoc file sharing flows when the workspace repo is available.");
97
+
98
+ parts.push("");
99
+ parts.push("## Controller Discipline");
100
+ parts.push("- Stay within the user's current requirement/request.");
101
+ parts.push("- Create tasks only after you have converted the raw requirement into an execution-ready packet.");
102
+ parts.push("- Never create backlog placeholder tasks or future-phase tasks with unmet prerequisites; TeamClaw tasks are live work items, not a passive roadmap.");
103
+ parts.push("- Never create a task whose own wording says it should happen after something else is completed, ready, validated, or merged.");
104
+ parts.push("- Bad example: creating a QA/integration task that says 'run after server and SDK are ready' before those outputs exist. Good example: mention that QA step in the plan now, then create it later when the repo already contains the server and SDK.");
105
+ parts.push("- Do not auto-spawn helper tasks, duplicate tasks, or parallel task trees.");
106
+ parts.push("- Do not let a worker task turn itself into a controller/coordinator workflow.");
107
+ parts.push("- If the correct role is busy, prefer waiting, messaging, or explicit reassignment over routing core work to an unrelated role.");
108
+ parts.push("- If a task is blocked by missing information, keep it in the clarification queue until the human answers; do not guess on the user's behalf.");
109
+ parts.push("- Use the controller itself for requirement analysis; use the PM role only for PM-owned deliverables after intake is clear.");
110
+ parts.push(`- Use exact TeamClaw role IDs only: ${TEAMCLAW_ROLE_IDS_TEXT}.`);
111
+
112
+ return {
113
+ prependSystemContext: parts.join("\n"),
114
+ };
115
+ };
116
+ }
@@ -0,0 +1,97 @@
1
+ import type { PluginLogger } from "../../api.js";
2
+ import type { TaskInfo, WorkerInfo } from "../types.js";
3
+ import { getRole } from "../roles.js";
4
+
5
+ export class TaskRouter {
6
+ private logger: PluginLogger;
7
+
8
+ constructor(logger: PluginLogger) {
9
+ this.logger = logger;
10
+ }
11
+
12
+ routeTask(
13
+ task: TaskInfo,
14
+ workers: Record<string, WorkerInfo>,
15
+ ): WorkerInfo | null {
16
+ // First try exact role match with idle workers
17
+ if (task.assignedRole) {
18
+ const candidates = Object.values(workers).filter(
19
+ (w) => w.role === task.assignedRole && w.status === "idle",
20
+ );
21
+ if (candidates.length > 0) {
22
+ return candidates[0]!;
23
+ }
24
+
25
+ this.logger.info(
26
+ `TaskRouter: no idle worker for explicitly assigned role ${task.assignedRole} on task ${task.id}; keeping task pending`,
27
+ );
28
+ return null;
29
+ }
30
+
31
+ // Try keyword matching on capabilities
32
+ const keywords = this.extractKeywords(task.description + " " + task.title);
33
+ const scored = Object.values(workers)
34
+ .filter((w) => w.status === "idle")
35
+ .map((w) => {
36
+ const roleDef = getRole(w.role);
37
+ const capabilities = roleDef?.capabilities ?? [];
38
+ const matchCount = keywords.filter((k) =>
39
+ capabilities.some((c) => c.includes(k) || k.includes(c)),
40
+ ).length;
41
+ return { worker: w, score: matchCount };
42
+ })
43
+ .filter((s) => s.score > 0)
44
+ .sort((a, b) => b.score - a.score);
45
+
46
+ if (scored.length > 0) {
47
+ return scored[0]!.worker;
48
+ }
49
+
50
+ // Fallback: any idle worker
51
+ const anyIdle = Object.values(workers).find((w) => w.status === "idle");
52
+ if (anyIdle) {
53
+ return anyIdle;
54
+ }
55
+
56
+ return null;
57
+ }
58
+
59
+ autoAssignPendingTasks(
60
+ tasks: Record<string, TaskInfo>,
61
+ workers: Record<string, WorkerInfo>,
62
+ ): Array<{ task: TaskInfo; worker: WorkerInfo }> {
63
+ const pendingTasks = Object.values(tasks).filter(
64
+ (t) => t.status === "pending" || t.status === "assigned",
65
+ );
66
+
67
+ const assignments: Array<{ task: TaskInfo; worker: WorkerInfo }> = [];
68
+
69
+ for (const task of pendingTasks) {
70
+ if (task.assignedWorkerId && workers[task.assignedWorkerId]) {
71
+ continue; // Already assigned to a valid worker
72
+ }
73
+
74
+ const worker = this.routeTask(task, workers);
75
+ if (worker) {
76
+ assignments.push({ task, worker });
77
+ }
78
+ }
79
+
80
+ return assignments;
81
+ }
82
+
83
+ private extractKeywords(text: string): string[] {
84
+ const common = new Set([
85
+ "the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for",
86
+ "of", "with", "by", "is", "are", "was", "were", "be", "been", "being",
87
+ "have", "has", "had", "do", "does", "did", "will", "would", "could",
88
+ "should", "may", "might", "must", "shall", "can", "this", "that",
89
+ "these", "those", "it", "its", "we", "our", "you", "your", "they",
90
+ "their", "create", "implement", "build", "make", "add", "update", "fix",
91
+ ]);
92
+ return text
93
+ .toLowerCase()
94
+ .split(/[^a-z0-9]+/)
95
+ .filter((w) => w.length > 2 && !common.has(w));
96
+ }
97
+ }
@@ -0,0 +1,63 @@
1
+ import { WebSocketServer, WebSocket } from "ws";
2
+ import type { Server } from "node:http";
3
+ import type { PluginLogger } from "../../api.js";
4
+
5
+ export type WsEvent =
6
+ | { type: "worker:online"; data: unknown }
7
+ | { type: "worker:offline"; data: unknown }
8
+ | { type: "task:created"; data: unknown }
9
+ | { type: "task:updated"; data: unknown }
10
+ | { type: "task:completed"; data: unknown }
11
+ | { type: "task:execution"; data: unknown }
12
+ | { type: "message:new"; data: unknown }
13
+ | { type: "clarification:requested"; data: unknown }
14
+ | { type: "clarification:answered"; data: unknown };
15
+
16
+ export class TeamWebSocketServer {
17
+ private wss: WebSocketServer | null = null;
18
+ private logger: PluginLogger;
19
+
20
+ constructor(logger: PluginLogger) {
21
+ this.logger = logger;
22
+ }
23
+
24
+ attach(server: Server): void {
25
+ this.wss = new WebSocketServer({ server, path: "/ws" });
26
+
27
+ this.wss.on("connection", (ws) => {
28
+ this.logger.info("WebSocket: client connected");
29
+ ws.on("close", () => {
30
+ this.logger.info("WebSocket: client disconnected");
31
+ });
32
+ });
33
+
34
+ this.wss.on("error", (err) => {
35
+ this.logger.warn(`WebSocket: error: ${String(err)}`);
36
+ });
37
+ }
38
+
39
+ broadcastUpdate(event: WsEvent): void {
40
+ if (!this.wss) return;
41
+
42
+ const data = JSON.stringify(event);
43
+ for (const client of this.wss.clients) {
44
+ if (client.readyState === WebSocket.OPEN) {
45
+ client.send(data);
46
+ }
47
+ }
48
+ }
49
+
50
+ close(): void {
51
+ if (this.wss) {
52
+ for (const client of this.wss.clients) {
53
+ client.close();
54
+ }
55
+ this.wss.close();
56
+ this.wss = null;
57
+ }
58
+ }
59
+
60
+ getClientCount(): number {
61
+ return this.wss ? this.wss.clients.size : 0;
62
+ }
63
+ }