@teamclaws/teamclaw 2026.3.26-2 → 2026.4.2-1
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 +52 -8
- package/cli.mjs +538 -224
- package/index.ts +76 -27
- package/openclaw.plugin.json +53 -28
- package/package.json +5 -2
- package/skills/teamclaw/SKILL.md +213 -0
- package/skills/teamclaw/references/api-quick-ref.md +117 -0
- package/skills/teamclaw-setup/SKILL.md +81 -0
- package/skills/teamclaw-setup/references/install-modes.md +136 -0
- package/skills/teamclaw-setup/references/validation-checklist.md +73 -0
- package/src/config.ts +44 -16
- package/src/controller/controller-capacity.ts +2 -2
- package/src/controller/controller-service.ts +193 -47
- package/src/controller/controller-tools.ts +102 -2
- package/src/controller/delivery-report.ts +563 -0
- package/src/controller/http-server.ts +1907 -172
- package/src/controller/kickoff-orchestrator.ts +292 -0
- package/src/controller/managed-gateway-process.ts +330 -0
- package/src/controller/orchestration-manifest.ts +69 -1
- package/src/controller/preview-manager.ts +676 -0
- package/src/controller/prompt-injector.ts +116 -67
- package/src/controller/role-inference.ts +41 -0
- package/src/controller/websocket.ts +3 -1
- package/src/controller/worker-provisioning.ts +429 -74
- package/src/discovery.ts +1 -1
- package/src/git-collaboration.ts +198 -47
- package/src/identity.ts +12 -2
- package/src/interaction-contracts.ts +179 -3
- package/src/networking.ts +99 -0
- package/src/openclaw-workspace.ts +478 -11
- package/src/prompt-policy.ts +381 -0
- package/src/roles.ts +37 -36
- package/src/state.ts +40 -1
- package/src/task-executor.ts +282 -78
- package/src/types.ts +150 -7
- package/src/ui/app.js +1403 -175
- package/src/ui/assets/teamclaw-app-icon.png +0 -0
- package/src/ui/index.html +122 -40
- package/src/ui/style.css +829 -143
- package/src/worker/http-handler.ts +40 -4
- package/src/worker/prompt-injector.ts +9 -38
- package/src/worker/skill-installer.ts +2 -2
- package/src/worker/tools.ts +31 -5
- package/src/worker/worker-service.ts +49 -8
- package/src/workspace-browser.ts +20 -7
- package/src/controller/local-worker-manager.ts +0 -533
|
@@ -1,46 +1,48 @@
|
|
|
1
1
|
import type { OpenClawPluginApi, OpenClawPluginService, OpenClawPluginServiceContext, PluginLogger } from "../../api.js";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { exec } from "node:child_process";
|
|
4
|
+
import type { KickoffAssessment, PluginConfig, RoleId, TeamState } from "../types.js";
|
|
4
5
|
import { loadTeamState, saveTeamState } from "../state.js";
|
|
5
6
|
import { MDnsAdvertiser } from "../discovery.js";
|
|
6
7
|
import { WORKER_TIMEOUT_MS } from "../protocol.js";
|
|
7
8
|
import { createControllerHttpServer } from "./http-server.js";
|
|
8
|
-
import type { LocalWorkerManager } from "./local-worker-manager.js";
|
|
9
9
|
import { TaskRouter } from "./task-router.js";
|
|
10
10
|
import { MessageRouter } from "./message-router.js";
|
|
11
11
|
import { TeamWebSocketServer } from "./websocket.js";
|
|
12
12
|
import { ensureOpenClawWorkspaceMemoryDir } from "../openclaw-workspace.js";
|
|
13
13
|
import { ensureControllerGitRepo } from "../git-collaboration.js";
|
|
14
14
|
import { WorkerProvisioningManager } from "./worker-provisioning.js";
|
|
15
|
+
import { PreviewManager } from "./preview-manager.js";
|
|
16
|
+
import { runKickoffMeeting, buildKickoffAssessmentPrompt, ASSESSMENT_TIMEOUT_MS } from "./kickoff-orchestrator.js";
|
|
17
|
+
import { resolvePreferredLanAddress } from "../networking.js";
|
|
18
|
+
|
|
19
|
+
export type KickoffHandler = (
|
|
20
|
+
candidateRoles: RoleId[],
|
|
21
|
+
complexity: "simple" | "medium" | "complex",
|
|
22
|
+
requirement: string,
|
|
23
|
+
) => Promise<{ assessments: KickoffAssessment[]; summary: string }>;
|
|
15
24
|
|
|
16
25
|
export type ControllerServiceDeps = {
|
|
17
26
|
config: PluginConfig;
|
|
18
27
|
logger: PluginLogger;
|
|
19
28
|
runtime: OpenClawPluginApi["runtime"];
|
|
20
|
-
localWorkerManager?: LocalWorkerManager;
|
|
21
29
|
onTeamStateAvailable?: (getter: () => TeamState | null) => void;
|
|
30
|
+
/** Called once the HTTP server has bound to an actual port. */
|
|
31
|
+
onActualPort?: (port: number) => void;
|
|
32
|
+
/** Called once the kickoff handler is ready. */
|
|
33
|
+
onKickoffHandlerAvailable?: (handler: KickoffHandler) => void;
|
|
22
34
|
};
|
|
23
35
|
|
|
24
36
|
function getPreferredLanUiUrl(port: number): string | null {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
for (const records of Object.values(interfaces)) {
|
|
28
|
-
for (const record of records ?? []) {
|
|
29
|
-
if (!record || record.internal || record.family !== "IPv4") {
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
candidates.push(record.address);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
candidates.sort((left, right) => left.localeCompare(right));
|
|
36
|
-
if (candidates.length === 0) {
|
|
37
|
+
const preferredLanAddress = resolvePreferredLanAddress();
|
|
38
|
+
if (!preferredLanAddress) {
|
|
37
39
|
return null;
|
|
38
40
|
}
|
|
39
|
-
return `http://${
|
|
41
|
+
return `http://${preferredLanAddress}:${port}/ui`;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export function createControllerService(deps: ControllerServiceDeps): OpenClawPluginService {
|
|
43
|
-
const { config, logger
|
|
45
|
+
const { config, logger } = deps;
|
|
44
46
|
let teamState: TeamState | null = null;
|
|
45
47
|
let mdnsAdvertiser: MDnsAdvertiser;
|
|
46
48
|
let taskRouter: TaskRouter;
|
|
@@ -48,6 +50,7 @@ export function createControllerService(deps: ControllerServiceDeps): OpenClawPl
|
|
|
48
50
|
let wsServer: TeamWebSocketServer;
|
|
49
51
|
let timeoutTimer: ReturnType<typeof setInterval> | null = null;
|
|
50
52
|
let workerProvisioningManager: WorkerProvisioningManager | null = null;
|
|
53
|
+
let previewManager: PreviewManager;
|
|
51
54
|
|
|
52
55
|
return {
|
|
53
56
|
id: "teamclaw-controller",
|
|
@@ -95,24 +98,62 @@ export function createControllerService(deps: ControllerServiceDeps): OpenClawPl
|
|
|
95
98
|
getTeamState: () => teamState,
|
|
96
99
|
updateTeamState: updateState,
|
|
97
100
|
});
|
|
101
|
+
if (workerProvisioningManager.isEnabled()) {
|
|
102
|
+
workerProvisioningManager.primeStartupReadiness();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
previewManager = new PreviewManager({
|
|
106
|
+
logger,
|
|
107
|
+
getTeamState: () => teamState,
|
|
108
|
+
updateTeamState: updateState,
|
|
109
|
+
});
|
|
98
110
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
workerProvisioningManager.syncState(teamState)
|
|
103
|
-
) {
|
|
111
|
+
// Run ALL syncState calls (avoid || short-circuit skipping some).
|
|
112
|
+
const syncC = workerProvisioningManager.syncState(teamState);
|
|
113
|
+
if (repoStateChanged || syncC) {
|
|
104
114
|
await saveTeamState(teamState);
|
|
105
115
|
}
|
|
106
116
|
|
|
117
|
+
// Clean up orphaned tasks — tasks assigned to workers that no longer
|
|
118
|
+
// exist OR that are offline (stale entries surviving from a previous run).
|
|
119
|
+
{
|
|
120
|
+
let orphanCleaned = false;
|
|
121
|
+
for (const task of Object.values(teamState.tasks)) {
|
|
122
|
+
if (
|
|
123
|
+
task.assignedWorkerId &&
|
|
124
|
+
(task.status === "assigned" || task.status === "in_progress")
|
|
125
|
+
) {
|
|
126
|
+
const worker = teamState.workers[task.assignedWorkerId];
|
|
127
|
+
if (!worker || worker.status === "offline") {
|
|
128
|
+
logger.info(`Controller: resetting orphaned task ${task.id} (worker ${task.assignedWorkerId} ${worker ? "offline" : "missing"})`);
|
|
129
|
+
task.status = "pending";
|
|
130
|
+
task.assignedWorkerId = undefined;
|
|
131
|
+
task.updatedAt = Date.now();
|
|
132
|
+
orphanCleaned = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (orphanCleaned) {
|
|
137
|
+
await saveTeamState(teamState);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
107
141
|
mdnsAdvertiser = new MDnsAdvertiser(logger);
|
|
108
142
|
taskRouter = new TaskRouter(logger);
|
|
109
143
|
messageRouter = new MessageRouter(logger);
|
|
110
144
|
wsServer = new TeamWebSocketServer(logger);
|
|
111
145
|
|
|
112
|
-
//
|
|
113
|
-
|
|
146
|
+
// When running inside a container (Docker or K8s), bind to 0.0.0.0
|
|
147
|
+
// so that port mapping, service networking, and health probes work.
|
|
148
|
+
// When running locally (host machine), bind to 127.0.0.1 for safety.
|
|
149
|
+
const isContainer = fs.existsSync("/.dockerenv") ||
|
|
150
|
+
fs.existsSync("/run/.containerenv") ||
|
|
151
|
+
process.env.KUBERNETES_SERVICE_HOST !== undefined;
|
|
152
|
+
const listenPort = config.port;
|
|
153
|
+
const listenHost = isContainer ? "0.0.0.0" : "127.0.0.1";
|
|
154
|
+
|
|
155
|
+
let serviceKickoffHandler: KickoffHandler | undefined;
|
|
114
156
|
|
|
115
|
-
// Start HTTP server
|
|
116
157
|
const server = createControllerHttpServer({
|
|
117
158
|
config,
|
|
118
159
|
logger,
|
|
@@ -122,31 +163,90 @@ export function createControllerService(deps: ControllerServiceDeps): OpenClawPl
|
|
|
122
163
|
taskRouter,
|
|
123
164
|
messageRouter,
|
|
124
165
|
wsServer,
|
|
125
|
-
localWorkerManager,
|
|
126
166
|
workerProvisioningManager,
|
|
167
|
+
previewManager,
|
|
168
|
+
getKickoffHandler: () => serviceKickoffHandler,
|
|
127
169
|
});
|
|
128
170
|
|
|
171
|
+
const PORT_RETRY_STEP = 10;
|
|
172
|
+
const PORT_MAX_RETRIES = 10;
|
|
173
|
+
|
|
174
|
+
let actualPort = 0;
|
|
129
175
|
await new Promise<void>((resolve, reject) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
176
|
+
let attempt = 0;
|
|
177
|
+
const tryListen = (port: number) => {
|
|
178
|
+
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
179
|
+
if (err.code === "EADDRINUSE" && attempt < PORT_MAX_RETRIES) {
|
|
180
|
+
attempt++;
|
|
181
|
+
const nextPort = config.port + attempt * PORT_RETRY_STEP;
|
|
182
|
+
logger.warn(`Controller: port ${port} in use, retrying on ${nextPort}`);
|
|
183
|
+
tryListen(nextPort);
|
|
184
|
+
} else {
|
|
185
|
+
reject(err);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
server.listen(port, listenHost, () => {
|
|
189
|
+
const addr = server.address();
|
|
190
|
+
actualPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
191
|
+
if (actualPort !== config.port) {
|
|
192
|
+
logger.info(`Controller: configured port ${config.port} unavailable, bound to ${actualPort} instead`);
|
|
193
|
+
}
|
|
194
|
+
logger.info(`Controller: HTTP server listening on port ${actualPort}`);
|
|
195
|
+
const uiUrl = `http://127.0.0.1:${actualPort}/ui`;
|
|
196
|
+
logger.info(`Controller: Web UI available at ${uiUrl}`);
|
|
197
|
+
const lanUiUrl = getPreferredLanUiUrl(actualPort);
|
|
198
|
+
if (lanUiUrl) {
|
|
199
|
+
logger.info(`Controller: Web UI available on LAN at ${lanUiUrl}`);
|
|
200
|
+
}
|
|
201
|
+
deps.onActualPort?.(actualPort);
|
|
202
|
+
openBrowser(uiUrl, logger);
|
|
203
|
+
resolve();
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
tryListen(listenPort);
|
|
140
207
|
});
|
|
141
208
|
|
|
142
|
-
|
|
143
|
-
|
|
209
|
+
// Propagate the actual port to worker provisioning (created earlier, before port was known)
|
|
210
|
+
if (workerProvisioningManager.isEnabled()) {
|
|
211
|
+
workerProvisioningManager.setActualPort(actualPort);
|
|
144
212
|
}
|
|
145
213
|
|
|
214
|
+
// Start mDNS advertising with the actual port
|
|
215
|
+
await mdnsAdvertiser.start(actualPort, config.teamName);
|
|
216
|
+
|
|
146
217
|
if (workerProvisioningManager.isEnabled()) {
|
|
147
|
-
void workerProvisioningManager.
|
|
218
|
+
void workerProvisioningManager.runStartupReadinessCheck();
|
|
148
219
|
}
|
|
149
220
|
|
|
221
|
+
// ── Kickoff handler ───────────────────────────────────────────────
|
|
222
|
+
const kickoffHandler: KickoffHandler = async (candidateRoles, complexity, requirement) => {
|
|
223
|
+
const result = await runKickoffMeeting(
|
|
224
|
+
{ requirement, candidateRoles, complexity },
|
|
225
|
+
{
|
|
226
|
+
logger,
|
|
227
|
+
getTeamState: () => teamState,
|
|
228
|
+
ensureRoleProvisioned: async (role) => {
|
|
229
|
+
if (workerProvisioningManager?.isEnabled()) {
|
|
230
|
+
await workerProvisioningManager.requestReconcile(`kickoff-provision-${role}`);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
requestWorkerAssessment: async (worker, req) => {
|
|
234
|
+
return await requestKickoffAssessment(worker, req);
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
return { assessments: result.plan.assessments, summary: result.summary };
|
|
239
|
+
};
|
|
240
|
+
serviceKickoffHandler = kickoffHandler;
|
|
241
|
+
deps.onKickoffHandlerAvailable?.(kickoffHandler);
|
|
242
|
+
|
|
243
|
+
logger.info(`Controller: starting preview restoration...`);
|
|
244
|
+
void previewManager.restorePreviewsOnStartup().then(() => {
|
|
245
|
+
logger.info(`Controller: preview restoration completed`);
|
|
246
|
+
}).catch((err) => {
|
|
247
|
+
logger.warn(`Controller: failed to restore previews on startup: ${String(err)}`);
|
|
248
|
+
});
|
|
249
|
+
|
|
150
250
|
// Start timeout monitoring
|
|
151
251
|
timeoutTimer = setInterval(() => {
|
|
152
252
|
if (!teamState) return;
|
|
@@ -156,10 +256,6 @@ export function createControllerService(deps: ControllerServiceDeps): OpenClawPl
|
|
|
156
256
|
|
|
157
257
|
for (const [workerId, worker] of Object.entries(teamState.workers)) {
|
|
158
258
|
if (worker.status === "offline") continue;
|
|
159
|
-
if (localWorkerManager?.isLocalWorker(worker)) {
|
|
160
|
-
worker.lastHeartbeat = now;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
259
|
if (now - worker.lastHeartbeat > WORKER_TIMEOUT_MS) {
|
|
164
260
|
logger.info(`Controller: worker ${workerId} timed out`);
|
|
165
261
|
const activeTaskId = worker.currentTaskId;
|
|
@@ -210,15 +306,65 @@ export function createControllerService(deps: ControllerServiceDeps): OpenClawPl
|
|
|
210
306
|
clearInterval(timeoutTimer);
|
|
211
307
|
timeoutTimer = null;
|
|
212
308
|
}
|
|
213
|
-
if (localWorkerManager?.hasLocalWorkers()) {
|
|
214
|
-
await localWorkerManager.stop();
|
|
215
|
-
}
|
|
216
309
|
if (workerProvisioningManager) {
|
|
217
310
|
await workerProvisioningManager.stop();
|
|
218
311
|
}
|
|
312
|
+
await previewManager.stopAll();
|
|
219
313
|
wsServer.close();
|
|
220
314
|
mdnsAdvertiser.stop();
|
|
221
315
|
logger.info("Controller: stopped");
|
|
222
316
|
},
|
|
223
317
|
};
|
|
224
318
|
}
|
|
319
|
+
|
|
320
|
+
function openBrowser(url: string, logger: PluginLogger): void {
|
|
321
|
+
const cmd = process.platform === "darwin"
|
|
322
|
+
? `open "${url}"`
|
|
323
|
+
: process.platform === "win32"
|
|
324
|
+
? `start "" "${url}"`
|
|
325
|
+
: `xdg-open "${url}"`;
|
|
326
|
+
exec(cmd, (err) => {
|
|
327
|
+
if (err) {
|
|
328
|
+
logger.warn(`Controller: failed to open browser: ${err.message}`);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Request a kickoff assessment from a worker.
|
|
335
|
+
*
|
|
336
|
+
* Request kickoff assessment from a worker over HTTP.
|
|
337
|
+
*/
|
|
338
|
+
async function requestKickoffAssessment(
|
|
339
|
+
worker: import("../types.js").WorkerInfo,
|
|
340
|
+
requirement: string,
|
|
341
|
+
): Promise<import("../types.js").KickoffAssessment> {
|
|
342
|
+
const role = worker.role;
|
|
343
|
+
const prompt = buildKickoffAssessmentPrompt(role, requirement);
|
|
344
|
+
|
|
345
|
+
// External worker — POST to kickoff assess endpoint
|
|
346
|
+
if (!worker.url) {
|
|
347
|
+
throw new Error(`Worker ${worker.id} has no URL for kickoff assessment`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const controller = new AbortController();
|
|
351
|
+
const timeout = setTimeout(() => controller.abort(), ASSESSMENT_TIMEOUT_MS);
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const res = await fetch(`${worker.url}/api/v1/kickoff/assess`, {
|
|
355
|
+
method: "POST",
|
|
356
|
+
headers: { "Content-Type": "application/json" },
|
|
357
|
+
body: JSON.stringify({ requirement, role }),
|
|
358
|
+
signal: controller.signal,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
if (!res.ok) {
|
|
362
|
+
throw new Error(`Worker ${worker.id} returned ${res.status} for kickoff assessment`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const data = await res.json() as { assessment: import("../types.js").KickoffAssessment };
|
|
366
|
+
return data.assessment;
|
|
367
|
+
} finally {
|
|
368
|
+
clearTimeout(timeout);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type {
|
|
3
3
|
ControllerOrchestrationManifest,
|
|
4
|
+
KickoffAssessment,
|
|
4
5
|
PluginConfig,
|
|
6
|
+
RoleId,
|
|
5
7
|
TaskInfo,
|
|
6
8
|
TeamState,
|
|
7
9
|
} from "../types.js";
|
|
8
10
|
import { buildControllerNoWorkersMessage, hasOnDemandWorkerProvisioning, shouldBlockControllerWithoutWorkers } from "./controller-capacity.js";
|
|
9
11
|
import {
|
|
12
|
+
normalizeClarificationQuestionSchemas,
|
|
10
13
|
normalizeManifestCreatedTasks,
|
|
11
14
|
normalizeManifestDeferredTasks,
|
|
12
15
|
normalizeManifestRoleList,
|
|
@@ -24,6 +27,8 @@ export type ControllerToolsDeps = {
|
|
|
24
27
|
controllerUrl: string;
|
|
25
28
|
getTeamState: () => TeamState | null;
|
|
26
29
|
sessionKey?: string | null;
|
|
30
|
+
/** Handler for kickoff meeting requests. Use a getter for late-binding (service may start after tool registration). */
|
|
31
|
+
getKickoffHandler?: () => ((candidateRoles: RoleId[], complexity: "simple" | "medium" | "complex", requirement: string) => Promise<{ assessments: KickoffAssessment[]; summary: string }>) | undefined;
|
|
27
32
|
};
|
|
28
33
|
|
|
29
34
|
const EXECUTION_READY_BLOCKERS: Array<{ pattern: RegExp; reason: string }> = [
|
|
@@ -34,6 +39,11 @@ const EXECUTION_READY_BLOCKERS: Array<{ pattern: RegExp; reason: string }> = [
|
|
|
34
39
|
{ pattern: /待.*完成|等待.*完成/u, reason: "it is described as work for a later phase" },
|
|
35
40
|
];
|
|
36
41
|
|
|
42
|
+
const VALID_ROLE_IDS = new Set([
|
|
43
|
+
"pm", "architect", "developer", "qa", "release-engineer",
|
|
44
|
+
"infra-engineer", "devops", "security-engineer", "designer", "marketing",
|
|
45
|
+
]);
|
|
46
|
+
|
|
37
47
|
const ENGLISH_LATER_PHASE_CLAUSE_RE = /\b(?:after|once)\b(.+?)\b(complete|completed|ready|available|exists?)\b/i;
|
|
38
48
|
const ENGLISH_LATER_PHASE_DEPENDENCY_RE = /\b(?:task|tasks|service|services|module|modules|phase|phases|api|apis|interface|interfaces|review|qa|design|developer|architect|skeleton|backend|frontend|deliverable|artifact|handoff)\b/i;
|
|
39
49
|
const CHINESE_LATER_PHASE_CLAUSE_RE = /(.+?)(完成后|就绪后)/u;
|
|
@@ -49,6 +59,68 @@ export function createControllerTools(deps: ControllerToolsDeps) {
|
|
|
49
59
|
const baseUrl = controllerUrl;
|
|
50
60
|
|
|
51
61
|
return [
|
|
62
|
+
{
|
|
63
|
+
name: "teamclaw_request_kickoff",
|
|
64
|
+
label: "Request Team Kickoff Meeting",
|
|
65
|
+
description: "Provision candidate role workers and collect structured assessments from each before creating execution tasks. Use for medium/complex projects where multiple roles need to collaborate.",
|
|
66
|
+
parameters: Type.Object({
|
|
67
|
+
requirement: Type.String({ description: "The full user requirement to present to the team for assessment" }),
|
|
68
|
+
candidateRoles: Type.Array(
|
|
69
|
+
Type.String({ description: "Role IDs to invite to the kickoff meeting (e.g. architect, developer, qa)" }),
|
|
70
|
+
),
|
|
71
|
+
complexity: Type.Union([
|
|
72
|
+
Type.Literal("simple"),
|
|
73
|
+
Type.Literal("medium"),
|
|
74
|
+
Type.Literal("complex"),
|
|
75
|
+
], { description: "Project complexity: simple (skip kickoff, 1-2 roles), medium (partial kickoff, 2-3 roles), complex (full team kickoff, 4+ roles)" }),
|
|
76
|
+
}),
|
|
77
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
78
|
+
const kickoffHandler = deps.getKickoffHandler?.();
|
|
79
|
+
if (!kickoffHandler) {
|
|
80
|
+
return {
|
|
81
|
+
content: [{
|
|
82
|
+
type: "text" as const,
|
|
83
|
+
text: "Kickoff meeting is not available in this deployment. Proceed with direct task creation.",
|
|
84
|
+
}],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const requirement = String(params.requirement ?? "").trim();
|
|
89
|
+
if (!requirement) {
|
|
90
|
+
return { content: [{ type: "text" as const, text: "requirement is required." }] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const rawRoles = Array.isArray(params.candidateRoles) ? params.candidateRoles : [];
|
|
94
|
+
const candidateRoles = rawRoles
|
|
95
|
+
.map((r) => String(r ?? "").trim().toLowerCase())
|
|
96
|
+
.filter((r): r is RoleId => VALID_ROLE_IDS.has(r));
|
|
97
|
+
|
|
98
|
+
if (candidateRoles.length === 0) {
|
|
99
|
+
return { content: [{ type: "text" as const, text: "At least one valid candidate role is required." }] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const complexity = (params.complexity === "simple" || params.complexity === "medium" || params.complexity === "complex")
|
|
103
|
+
? params.complexity
|
|
104
|
+
: "medium";
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const result = await kickoffHandler(candidateRoles, complexity, requirement);
|
|
108
|
+
return {
|
|
109
|
+
content: [{
|
|
110
|
+
type: "text" as const,
|
|
111
|
+
text: result.summary,
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return {
|
|
116
|
+
content: [{
|
|
117
|
+
type: "text" as const,
|
|
118
|
+
text: `Kickoff meeting failed: ${err instanceof Error ? err.message : String(err)}. Proceed with controller-only planning.`,
|
|
119
|
+
}],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
},
|
|
52
124
|
{
|
|
53
125
|
name: "teamclaw_create_task",
|
|
54
126
|
label: "Create Team Task",
|
|
@@ -152,6 +224,7 @@ export function createControllerTools(deps: ControllerToolsDeps) {
|
|
|
152
224
|
label: "Submit Controller Manifest",
|
|
153
225
|
description: "Record the structured orchestration manifest for this intake run after role selection and task creation decisions are complete",
|
|
154
226
|
parameters: Type.Object({
|
|
227
|
+
projectName: Type.Optional(Type.String({ description: "Short, lowercase, kebab-case project name for the workspace directory (e.g. 'todo-rest-api', 'stripe-payment-integration'). 2-5 words max, no random suffixes." })),
|
|
155
228
|
requirementSummary: Type.String({ description: "Brief summary of the requirement the controller is orchestrating" }),
|
|
156
229
|
requiredRoles: Type.Array(
|
|
157
230
|
Type.String({
|
|
@@ -162,6 +235,27 @@ export function createControllerTools(deps: ControllerToolsDeps) {
|
|
|
162
235
|
clarificationQuestions: Type.Optional(
|
|
163
236
|
Type.Array(Type.String({ description: "Concrete clarification questions still waiting on the human" })),
|
|
164
237
|
),
|
|
238
|
+
clarificationSchemas: Type.Optional(
|
|
239
|
+
Type.Array(
|
|
240
|
+
Type.Object({
|
|
241
|
+
kind: Type.String({ description: "Question kind: single-select, multi-select, number, or text" }),
|
|
242
|
+
title: Type.String({ description: "Question title shown to the human" }),
|
|
243
|
+
description: Type.Optional(Type.String({ description: "Optional supporting context for the question" })),
|
|
244
|
+
required: Type.Optional(Type.Boolean({ description: "Whether the human must answer this question before continuing" })),
|
|
245
|
+
options: Type.Optional(Type.Array(Type.Object({
|
|
246
|
+
value: Type.String({ description: "Stable option value" }),
|
|
247
|
+
label: Type.String({ description: "Human-visible option label" }),
|
|
248
|
+
hint: Type.Optional(Type.String({ description: "Optional helper text for this option" })),
|
|
249
|
+
}))),
|
|
250
|
+
allowOther: Type.Optional(Type.Boolean({ description: "Whether freeform 'other' text is allowed alongside options" })),
|
|
251
|
+
placeholder: Type.Optional(Type.String({ description: "Optional placeholder or hint for text/number input" })),
|
|
252
|
+
unit: Type.Optional(Type.String({ description: "Optional unit label for number questions" })),
|
|
253
|
+
min: Type.Optional(Type.Number({ description: "Optional minimum numeric value" })),
|
|
254
|
+
max: Type.Optional(Type.Number({ description: "Optional maximum numeric value" })),
|
|
255
|
+
step: Type.Optional(Type.Number({ description: "Optional numeric step size" })),
|
|
256
|
+
}),
|
|
257
|
+
),
|
|
258
|
+
),
|
|
165
259
|
createdTasks: Type.Optional(
|
|
166
260
|
Type.Array(
|
|
167
261
|
Type.Object({
|
|
@@ -183,6 +277,7 @@ export function createControllerTools(deps: ControllerToolsDeps) {
|
|
|
183
277
|
),
|
|
184
278
|
handoffPlan: Type.Optional(Type.String({ description: "Brief note about how workers should report progress/handoffs across this flow" })),
|
|
185
279
|
notes: Type.Optional(Type.String({ description: "Additional orchestration notes for the human/controller log" })),
|
|
280
|
+
requirementFullyComplete: Type.Optional(Type.Boolean({ description: "Set to true when the entire human requirement is fully satisfied — all tasks completed, no deferred tasks remaining, no follow-ups needed" })),
|
|
186
281
|
}),
|
|
187
282
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
188
283
|
const normalizedSessionKey = typeof sessionKey === "string" ? sessionKey.trim() : "";
|
|
@@ -200,16 +295,21 @@ export function createControllerTools(deps: ControllerToolsDeps) {
|
|
|
200
295
|
return { content: [{ type: "text" as const, text: "requirementSummary is required." }] };
|
|
201
296
|
}
|
|
202
297
|
|
|
298
|
+
const clarificationSchemas = normalizeClarificationQuestionSchemas(params.clarificationSchemas);
|
|
299
|
+
const clarificationQuestions = normalizeManifestStringList(params.clarificationQuestions);
|
|
203
300
|
const manifest: ControllerOrchestrationManifest = {
|
|
204
301
|
version: "1.0",
|
|
302
|
+
projectName: normalizeOptionalManifestText(params.projectName) || undefined,
|
|
205
303
|
requirementSummary,
|
|
206
304
|
requiredRoles: normalizeManifestRoleList(params.requiredRoles),
|
|
207
305
|
clarificationsNeeded: Boolean(params.clarificationsNeeded),
|
|
208
|
-
clarificationQuestions:
|
|
306
|
+
clarificationQuestions: clarificationQuestions.length > 0 ? clarificationQuestions : clarificationSchemas.map((entry) => entry.title),
|
|
307
|
+
clarificationSchemas,
|
|
209
308
|
createdTasks: normalizeManifestCreatedTasks(params.createdTasks),
|
|
210
309
|
deferredTasks: normalizeManifestDeferredTasks(params.deferredTasks),
|
|
211
310
|
handoffPlan: normalizeOptionalManifestText(params.handoffPlan),
|
|
212
311
|
notes: normalizeOptionalManifestText(params.notes),
|
|
312
|
+
requirementFullyComplete: Boolean(params.requirementFullyComplete),
|
|
213
313
|
};
|
|
214
314
|
|
|
215
315
|
try {
|
|
@@ -230,7 +330,7 @@ export function createControllerTools(deps: ControllerToolsDeps) {
|
|
|
230
330
|
return {
|
|
231
331
|
content: [{
|
|
232
332
|
type: "text" as const,
|
|
233
|
-
text: `Controller manifest recorded: roles=${manifest.requiredRoles.join(", ") || "none"} created=${manifest.createdTasks.length} deferred=${manifest.deferredTasks.length}`,
|
|
333
|
+
text: `Controller manifest recorded: roles=${manifest.requiredRoles.join(", ") || "none"} created=${manifest.createdTasks.length} deferred=${manifest.deferredTasks.length}${manifest.requirementFullyComplete ? " requirementFullyComplete=true" : ""}`,
|
|
234
334
|
}],
|
|
235
335
|
};
|
|
236
336
|
} catch (err) {
|