@teamclaws/teamclaw 2026.3.21 → 2026.3.25
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 +19 -1
- package/api.ts +2 -2
- package/cli.mjs +1185 -0
- package/index.ts +24 -7
- package/openclaw.plugin.json +326 -2
- package/package.json +6 -9
- package/src/config.ts +29 -1
- package/src/controller/controller-service.ts +1 -0
- package/src/controller/controller-tools.ts +12 -1
- package/src/controller/http-server.ts +355 -10
- package/src/controller/local-worker-manager.ts +5 -3
- package/src/controller/prompt-injector.ts +6 -1
- package/src/controller/websocket.ts +1 -0
- package/src/controller/worker-provisioning.ts +93 -4
- package/src/install-defaults.ts +1 -0
- package/src/openclaw-workspace.ts +57 -1
- package/src/roles.ts +42 -7
- package/src/state.ts +6 -0
- package/src/task-executor.ts +1 -0
- package/src/types.ts +53 -1
- package/src/ui/app.js +138 -2
- package/src/ui/index.html +10 -0
- package/src/ui/style.css +148 -0
- package/src/worker/http-handler.ts +4 -0
- package/src/worker/prompt-injector.ts +1 -0
- package/src/worker/skill-installer.ts +302 -0
|
@@ -5,6 +5,8 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
5
5
|
import type { OpenClawPluginApi, PluginLogger } from "../../api.js";
|
|
6
6
|
import type {
|
|
7
7
|
ClarificationRequest,
|
|
8
|
+
ControllerRunInfo,
|
|
9
|
+
ControllerRunSource,
|
|
8
10
|
GitRepoState,
|
|
9
11
|
PluginConfig,
|
|
10
12
|
RepoSyncInfo,
|
|
@@ -29,7 +31,7 @@ import {
|
|
|
29
31
|
generateId,
|
|
30
32
|
} from "../protocol.js";
|
|
31
33
|
import { listWorkspaceTree, readWorkspaceFile, readWorkspaceRawFile } from "../workspace-browser.js";
|
|
32
|
-
import { ROLES } from "../roles.js";
|
|
34
|
+
import { ROLES, normalizeRecommendedSkills, resolveRecommendedSkillsForRole } from "../roles.js";
|
|
33
35
|
import { buildRepoSyncInfo, ensureControllerGitRepo, exportControllerGitBundle, importControllerGitBundle } from "../git-collaboration.js";
|
|
34
36
|
import type { LocalWorkerManager } from "./local-worker-manager.js";
|
|
35
37
|
import { TaskRouter } from "./task-router.js";
|
|
@@ -51,6 +53,7 @@ export type ControllerHttpDeps = {
|
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
const MAX_TASK_EXECUTION_EVENTS = 250;
|
|
56
|
+
const MAX_CONTROLLER_RUNS = 40;
|
|
54
57
|
const MAX_RECENT_TASK_CONTEXT = 3;
|
|
55
58
|
const MAX_TASK_CONTEXT_SUMMARY_CHARS = 500;
|
|
56
59
|
const CONTROLLER_INTAKE_TIMEOUT_CAP_MS = 180_000;
|
|
@@ -167,6 +170,196 @@ function buildTaskExecutionSummary(execution?: TaskExecution): TaskExecutionSumm
|
|
|
167
170
|
};
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
function ensureControllerRunExecution(run: ControllerRunInfo): TaskExecution {
|
|
174
|
+
if (!run.execution) {
|
|
175
|
+
run.execution = {
|
|
176
|
+
status: run.status,
|
|
177
|
+
runId: run.runId,
|
|
178
|
+
startedAt: run.startedAt,
|
|
179
|
+
endedAt: run.completedAt,
|
|
180
|
+
lastUpdatedAt: run.updatedAt,
|
|
181
|
+
events: [],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!Array.isArray(run.execution.events)) {
|
|
186
|
+
run.execution.events = [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
run.execution.status = run.status;
|
|
190
|
+
run.execution.runId = run.runId ?? run.execution.runId;
|
|
191
|
+
run.execution.startedAt = run.startedAt ?? run.execution.startedAt;
|
|
192
|
+
run.execution.endedAt = run.completedAt ?? run.execution.endedAt;
|
|
193
|
+
run.execution.lastUpdatedAt = run.updatedAt ?? run.execution.lastUpdatedAt;
|
|
194
|
+
return run.execution;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function appendControllerRunEvent(run: ControllerRunInfo, input: TaskExecutionEventInput): TaskExecutionEvent {
|
|
198
|
+
const now = input.createdAt ?? Date.now();
|
|
199
|
+
const execution = ensureControllerRunExecution(run);
|
|
200
|
+
|
|
201
|
+
if (input.runId) {
|
|
202
|
+
run.runId = input.runId;
|
|
203
|
+
execution.runId = input.runId;
|
|
204
|
+
}
|
|
205
|
+
if (input.sessionKey) {
|
|
206
|
+
execution.sessionKey = input.sessionKey;
|
|
207
|
+
}
|
|
208
|
+
if (input.status) {
|
|
209
|
+
run.status = input.status;
|
|
210
|
+
execution.status = input.status;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if ((input.status === "running" || input.phase === "run_started") && !run.startedAt) {
|
|
214
|
+
run.startedAt = now;
|
|
215
|
+
execution.startedAt = now;
|
|
216
|
+
}
|
|
217
|
+
if (run.status === "completed" || run.status === "failed") {
|
|
218
|
+
run.completedAt = run.completedAt ?? now;
|
|
219
|
+
execution.endedAt = execution.endedAt ?? now;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
run.updatedAt = now;
|
|
223
|
+
execution.lastUpdatedAt = now;
|
|
224
|
+
|
|
225
|
+
const event: TaskExecutionEvent = {
|
|
226
|
+
id: generateId(),
|
|
227
|
+
type: input.type,
|
|
228
|
+
createdAt: now,
|
|
229
|
+
message: input.message,
|
|
230
|
+
phase: input.phase,
|
|
231
|
+
source: input.source,
|
|
232
|
+
stream: input.stream,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
execution.events.push(event);
|
|
236
|
+
if (execution.events.length > MAX_TASK_EXECUTION_EVENTS) {
|
|
237
|
+
execution.events = execution.events.slice(-MAX_TASK_EXECUTION_EVENTS);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return event;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function trimControllerRuns(state: TeamState): void {
|
|
244
|
+
const runs = Object.values(state.controllerRuns)
|
|
245
|
+
.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
246
|
+
if (runs.length <= MAX_CONTROLLER_RUNS) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
for (const run of runs.slice(0, runs.length - MAX_CONTROLLER_RUNS)) {
|
|
250
|
+
delete state.controllerRuns[run.id];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function serializeControllerRun(run?: ControllerRunInfo, includeExecutionEvents = true): Record<string, unknown> | undefined {
|
|
255
|
+
if (!run) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const payload: Record<string, unknown> = { ...run };
|
|
260
|
+
if (!run.execution) {
|
|
261
|
+
return payload;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
payload.execution = includeExecutionEvents
|
|
265
|
+
? {
|
|
266
|
+
status: run.execution.status,
|
|
267
|
+
runId: run.execution.runId,
|
|
268
|
+
startedAt: run.execution.startedAt,
|
|
269
|
+
endedAt: run.execution.endedAt,
|
|
270
|
+
lastUpdatedAt: run.execution.lastUpdatedAt,
|
|
271
|
+
events: run.execution.events.map((event) => ({ ...event })),
|
|
272
|
+
}
|
|
273
|
+
: buildTaskExecutionSummary(run.execution);
|
|
274
|
+
|
|
275
|
+
return payload;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildControllerRunTitle(
|
|
279
|
+
message: string,
|
|
280
|
+
source: ControllerRunSource,
|
|
281
|
+
sourceTaskTitle?: string,
|
|
282
|
+
): string {
|
|
283
|
+
if (source === "task_follow_up") {
|
|
284
|
+
return sourceTaskTitle
|
|
285
|
+
? `Controller follow-up after ${sourceTaskTitle}`
|
|
286
|
+
: "Controller workflow follow-up";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const normalized = message.replace(/\s+/g, " ").trim();
|
|
290
|
+
if (!normalized) {
|
|
291
|
+
return "Controller intake";
|
|
292
|
+
}
|
|
293
|
+
if (normalized.length <= 100) {
|
|
294
|
+
return normalized;
|
|
295
|
+
}
|
|
296
|
+
return `${normalized.slice(0, 100).trimEnd()}…`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function createControllerRun(
|
|
300
|
+
message: string,
|
|
301
|
+
sessionKey: string,
|
|
302
|
+
deps: ControllerHttpDeps,
|
|
303
|
+
options?: {
|
|
304
|
+
source?: ControllerRunSource;
|
|
305
|
+
sourceTaskId?: string;
|
|
306
|
+
sourceTaskTitle?: string;
|
|
307
|
+
},
|
|
308
|
+
): ControllerRunInfo {
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
const run: ControllerRunInfo = {
|
|
311
|
+
id: generateId(),
|
|
312
|
+
title: buildControllerRunTitle(message, options?.source ?? "human", options?.sourceTaskTitle),
|
|
313
|
+
sessionKey,
|
|
314
|
+
source: options?.source ?? "human",
|
|
315
|
+
sourceTaskId: options?.sourceTaskId,
|
|
316
|
+
sourceTaskTitle: options?.sourceTaskTitle,
|
|
317
|
+
request: message,
|
|
318
|
+
createdTaskIds: [],
|
|
319
|
+
status: "pending",
|
|
320
|
+
createdAt: now,
|
|
321
|
+
updatedAt: now,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const state = deps.updateTeamState((teamState) => {
|
|
325
|
+
teamState.controllerRuns[run.id] = run;
|
|
326
|
+
trimControllerRuns(teamState);
|
|
327
|
+
});
|
|
328
|
+
const createdRun = state.controllerRuns[run.id] ?? run;
|
|
329
|
+
deps.wsServer.broadcastUpdate({ type: "controller:run", data: serializeControllerRun(createdRun) });
|
|
330
|
+
return createdRun;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function updateControllerRun(
|
|
334
|
+
runId: string,
|
|
335
|
+
deps: ControllerHttpDeps,
|
|
336
|
+
updater: (run: ControllerRunInfo) => void,
|
|
337
|
+
): ControllerRunInfo | undefined {
|
|
338
|
+
const state = deps.updateTeamState((teamState) => {
|
|
339
|
+
const run = teamState.controllerRuns[runId];
|
|
340
|
+
if (!run) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
updater(run);
|
|
344
|
+
trimControllerRuns(teamState);
|
|
345
|
+
});
|
|
346
|
+
const updatedRun = state.controllerRuns[runId];
|
|
347
|
+
if (updatedRun) {
|
|
348
|
+
deps.wsServer.broadcastUpdate({ type: "controller:run", data: serializeControllerRun(updatedRun) });
|
|
349
|
+
}
|
|
350
|
+
return updatedRun;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function recordControllerRunEvent(
|
|
354
|
+
runId: string,
|
|
355
|
+
input: TaskExecutionEventInput,
|
|
356
|
+
deps: ControllerHttpDeps,
|
|
357
|
+
): ControllerRunInfo | undefined {
|
|
358
|
+
return updateControllerRun(runId, deps, (run) => {
|
|
359
|
+
appendControllerRunEvent(run, input);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
170
363
|
function serializeTask(task?: TaskInfo, includeExecutionEvents = false): Record<string, unknown> | undefined {
|
|
171
364
|
if (!task) {
|
|
172
365
|
return undefined;
|
|
@@ -248,7 +441,8 @@ function tagControllerCreatedTasks(
|
|
|
248
441
|
taskIdsBeforeRun: Set<string>,
|
|
249
442
|
sessionKey: string,
|
|
250
443
|
deps: ControllerHttpDeps,
|
|
251
|
-
):
|
|
444
|
+
): string[] {
|
|
445
|
+
const taggedTaskIds: string[] = [];
|
|
252
446
|
deps.updateTeamState((state) => {
|
|
253
447
|
for (const task of Object.values(state.tasks)) {
|
|
254
448
|
if (taskIdsBeforeRun.has(task.id)) {
|
|
@@ -258,8 +452,10 @@ function tagControllerCreatedTasks(
|
|
|
258
452
|
continue;
|
|
259
453
|
}
|
|
260
454
|
task.controllerSessionKey = sessionKey;
|
|
455
|
+
taggedTaskIds.push(task.id);
|
|
261
456
|
}
|
|
262
457
|
});
|
|
458
|
+
return taggedTaskIds;
|
|
263
459
|
}
|
|
264
460
|
|
|
265
461
|
function buildControllerFollowUpMessage(task: TaskInfo): string {
|
|
@@ -297,20 +493,48 @@ async function continueControllerWorkflow(task: TaskInfo, deps: ControllerHttpDe
|
|
|
297
493
|
if (task.createdBy !== "controller" || !task.controllerSessionKey) {
|
|
298
494
|
return;
|
|
299
495
|
}
|
|
300
|
-
await runControllerIntake(buildControllerFollowUpMessage(task), task.controllerSessionKey, deps
|
|
496
|
+
await runControllerIntake(buildControllerFollowUpMessage(task), task.controllerSessionKey, deps, {
|
|
497
|
+
source: "task_follow_up",
|
|
498
|
+
sourceTaskId: task.id,
|
|
499
|
+
sourceTaskTitle: task.title,
|
|
500
|
+
});
|
|
301
501
|
}
|
|
302
502
|
|
|
303
503
|
async function runControllerIntake(
|
|
304
504
|
message: string,
|
|
305
505
|
sessionKey: string,
|
|
306
506
|
deps: ControllerHttpDeps,
|
|
307
|
-
|
|
507
|
+
options?: {
|
|
508
|
+
source?: ControllerRunSource;
|
|
509
|
+
sourceTaskId?: string;
|
|
510
|
+
sourceTaskTitle?: string;
|
|
511
|
+
},
|
|
512
|
+
): Promise<{ sessionKey: string; runId: string; reply: string; controllerRunId: string }> {
|
|
308
513
|
const taskIdsBeforeRun = collectTaskIds(deps.getTeamState());
|
|
514
|
+
const controllerRun = createControllerRun(message, sessionKey, deps, options);
|
|
515
|
+
recordControllerRunEvent(controllerRun.id, {
|
|
516
|
+
type: "lifecycle",
|
|
517
|
+
phase: "queued",
|
|
518
|
+
source: "controller",
|
|
519
|
+
status: "pending",
|
|
520
|
+
sessionKey,
|
|
521
|
+
message: "Controller intake queued.",
|
|
522
|
+
}, deps);
|
|
523
|
+
|
|
309
524
|
const runResult = await deps.runtime.subagent.run({
|
|
310
525
|
sessionKey,
|
|
311
526
|
message,
|
|
312
527
|
idempotencyKey: `controller-intake-${generateId()}`,
|
|
313
528
|
});
|
|
529
|
+
recordControllerRunEvent(controllerRun.id, {
|
|
530
|
+
type: "lifecycle",
|
|
531
|
+
phase: "run_started",
|
|
532
|
+
source: "controller",
|
|
533
|
+
status: "running",
|
|
534
|
+
sessionKey,
|
|
535
|
+
runId: runResult.runId,
|
|
536
|
+
message: `Controller intake started (${runResult.runId}).`,
|
|
537
|
+
}, deps);
|
|
314
538
|
|
|
315
539
|
const waitResult = await deps.runtime.subagent.waitForRun({
|
|
316
540
|
runId: runResult.runId,
|
|
@@ -318,26 +542,90 @@ async function runControllerIntake(
|
|
|
318
542
|
});
|
|
319
543
|
|
|
320
544
|
if (waitResult.status === "timeout") {
|
|
321
|
-
tagControllerCreatedTasks(taskIdsBeforeRun, sessionKey, deps);
|
|
545
|
+
const createdTaskIds = tagControllerCreatedTasks(taskIdsBeforeRun, sessionKey, deps);
|
|
546
|
+
updateControllerRun(controllerRun.id, deps, (run) => {
|
|
547
|
+
run.createdTaskIds = createdTaskIds;
|
|
548
|
+
run.error = "Controller intake timed out";
|
|
549
|
+
appendControllerRunEvent(run, {
|
|
550
|
+
type: "error",
|
|
551
|
+
phase: "timeout",
|
|
552
|
+
source: "controller",
|
|
553
|
+
status: "failed",
|
|
554
|
+
sessionKey,
|
|
555
|
+
runId: runResult.runId,
|
|
556
|
+
message: "Controller intake timed out.",
|
|
557
|
+
});
|
|
558
|
+
});
|
|
322
559
|
throw new Error("Controller intake timed out");
|
|
323
560
|
}
|
|
324
561
|
if (waitResult.status !== "ok") {
|
|
325
|
-
|
|
326
|
-
|
|
562
|
+
const errorMessage = waitResult.error || "Controller intake failed";
|
|
563
|
+
const createdTaskIds = tagControllerCreatedTasks(taskIdsBeforeRun, sessionKey, deps);
|
|
564
|
+
updateControllerRun(controllerRun.id, deps, (run) => {
|
|
565
|
+
run.createdTaskIds = createdTaskIds;
|
|
566
|
+
run.error = errorMessage;
|
|
567
|
+
appendControllerRunEvent(run, {
|
|
568
|
+
type: "error",
|
|
569
|
+
phase: "run_failed",
|
|
570
|
+
source: "controller",
|
|
571
|
+
status: "failed",
|
|
572
|
+
sessionKey,
|
|
573
|
+
runId: runResult.runId,
|
|
574
|
+
message: errorMessage,
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
throw new Error(errorMessage);
|
|
327
578
|
}
|
|
328
579
|
|
|
329
|
-
tagControllerCreatedTasks(taskIdsBeforeRun, sessionKey, deps);
|
|
580
|
+
const createdTaskIds = tagControllerCreatedTasks(taskIdsBeforeRun, sessionKey, deps);
|
|
330
581
|
|
|
331
582
|
const sessionMessages = await deps.runtime.subagent.getSessionMessages({
|
|
332
583
|
sessionKey,
|
|
333
584
|
limit: 100,
|
|
334
585
|
});
|
|
335
|
-
const reply = extractLastAssistantText(sessionMessages.messages)
|
|
586
|
+
const reply = extractLastAssistantText(sessionMessages.messages)
|
|
587
|
+
|| "Controller completed the intake run but did not return any text.";
|
|
588
|
+
|
|
589
|
+
updateControllerRun(controllerRun.id, deps, (run) => {
|
|
590
|
+
run.reply = reply;
|
|
591
|
+
run.error = undefined;
|
|
592
|
+
run.createdTaskIds = createdTaskIds;
|
|
593
|
+
appendControllerRunEvent(run, {
|
|
594
|
+
type: "output",
|
|
595
|
+
phase: "final_reply",
|
|
596
|
+
source: "subagent",
|
|
597
|
+
status: "running",
|
|
598
|
+
sessionKey,
|
|
599
|
+
runId: runResult.runId,
|
|
600
|
+
message: reply,
|
|
601
|
+
});
|
|
602
|
+
if (createdTaskIds.length > 0) {
|
|
603
|
+
appendControllerRunEvent(run, {
|
|
604
|
+
type: "lifecycle",
|
|
605
|
+
phase: "tasks_created",
|
|
606
|
+
source: "controller",
|
|
607
|
+
status: "running",
|
|
608
|
+
sessionKey,
|
|
609
|
+
runId: runResult.runId,
|
|
610
|
+
message: `Controller created ${createdTaskIds.length} task(s): ${createdTaskIds.join(", ")}`,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
appendControllerRunEvent(run, {
|
|
614
|
+
type: "lifecycle",
|
|
615
|
+
phase: "run_completed",
|
|
616
|
+
source: "controller",
|
|
617
|
+
status: "completed",
|
|
618
|
+
sessionKey,
|
|
619
|
+
runId: runResult.runId,
|
|
620
|
+
message: "Controller intake completed.",
|
|
621
|
+
});
|
|
622
|
+
});
|
|
336
623
|
|
|
337
624
|
return {
|
|
338
625
|
sessionKey,
|
|
339
626
|
runId: runResult.runId,
|
|
340
|
-
reply
|
|
627
|
+
reply,
|
|
628
|
+
controllerRunId: controllerRun.id,
|
|
341
629
|
};
|
|
342
630
|
}
|
|
343
631
|
|
|
@@ -384,8 +672,27 @@ function buildRecentCompletedTaskContext(task: TaskInfo, state: TeamState | null
|
|
|
384
672
|
].join("\n");
|
|
385
673
|
}
|
|
386
674
|
|
|
675
|
+
function buildRecommendedSkillsContext(task: TaskInfo): string {
|
|
676
|
+
const recommendedSkills = resolveRecommendedSkillsForRole(task.assignedRole, task.recommendedSkills ?? []);
|
|
677
|
+
if (recommendedSkills.length === 0) {
|
|
678
|
+
return "";
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return [
|
|
682
|
+
"## Recommended Skills",
|
|
683
|
+
"- Prefer these skill slugs for this task when relevant:",
|
|
684
|
+
...recommendedSkills.map((skill) => ` - ${skill}`),
|
|
685
|
+
"- Before starting, search/install missing recommended skills in the current workspace when the runtime supports it.",
|
|
686
|
+
"- Prefer exact ClawHub/OpenClaw skill slugs over vague descriptions whenever possible.",
|
|
687
|
+
].join("\n");
|
|
688
|
+
}
|
|
689
|
+
|
|
387
690
|
function buildTaskAssignmentDescription(task: TaskInfo, state: TeamState | null, repoInfo?: RepoSyncInfo): string {
|
|
388
691
|
const parts = [task.description];
|
|
692
|
+
const recommendedSkillsContext = buildRecommendedSkillsContext(task);
|
|
693
|
+
if (recommendedSkillsContext) {
|
|
694
|
+
parts.push("", recommendedSkillsContext);
|
|
695
|
+
}
|
|
389
696
|
const recentContext = buildRecentCompletedTaskContext(task, state);
|
|
390
697
|
if (recentContext) {
|
|
391
698
|
parts.push("", recentContext);
|
|
@@ -746,11 +1053,13 @@ async function dispatchTaskToWorker(
|
|
|
746
1053
|
const repoState = await refreshControllerRepoState(deps);
|
|
747
1054
|
const repoInfo = buildRepoSyncInfo(repoState, sharedWorkspace);
|
|
748
1055
|
const description = buildTaskAssignmentDescription(task, state ?? null, repoInfo);
|
|
1056
|
+
const recommendedSkills = resolveRecommendedSkillsForRole(task.assignedRole, task.recommendedSkills ?? []);
|
|
749
1057
|
const assignment: TaskAssignmentPayload = {
|
|
750
1058
|
taskId: task.id,
|
|
751
1059
|
title: task.title,
|
|
752
1060
|
description,
|
|
753
1061
|
priority: task.priority,
|
|
1062
|
+
recommendedSkills,
|
|
754
1063
|
repo: repoInfo,
|
|
755
1064
|
};
|
|
756
1065
|
|
|
@@ -1139,6 +1448,9 @@ async function handleRequest(
|
|
|
1139
1448
|
const priority = typeof body.priority === "string" ? body.priority as TaskPriority : "medium";
|
|
1140
1449
|
const assignedRole = typeof body.assignedRole === "string" ? body.assignedRole as RoleId : undefined;
|
|
1141
1450
|
const createdBy = typeof body.createdBy === "string" ? body.createdBy : "boss";
|
|
1451
|
+
const recommendedSkills = normalizeRecommendedSkills(
|
|
1452
|
+
Array.isArray(body.recommendedSkills) ? body.recommendedSkills.map((entry) => String(entry ?? "")) : [],
|
|
1453
|
+
);
|
|
1142
1454
|
|
|
1143
1455
|
if (!title) {
|
|
1144
1456
|
sendError(res, 400, "title is required");
|
|
@@ -1157,6 +1469,7 @@ async function handleRequest(
|
|
|
1157
1469
|
priority,
|
|
1158
1470
|
assignedRole,
|
|
1159
1471
|
createdBy,
|
|
1472
|
+
recommendedSkills: recommendedSkills.length > 0 ? recommendedSkills : undefined,
|
|
1160
1473
|
createdAt: now,
|
|
1161
1474
|
updatedAt: now,
|
|
1162
1475
|
};
|
|
@@ -1184,6 +1497,16 @@ async function handleRequest(
|
|
|
1184
1497
|
role: assignedRole,
|
|
1185
1498
|
}, deps);
|
|
1186
1499
|
}
|
|
1500
|
+
if (recommendedSkills.length > 0) {
|
|
1501
|
+
recordTaskExecutionEvent(taskId, {
|
|
1502
|
+
type: "lifecycle",
|
|
1503
|
+
phase: "skills_recommended",
|
|
1504
|
+
source: "controller",
|
|
1505
|
+
status: "pending",
|
|
1506
|
+
message: `Recommended skills: ${recommendedSkills.join(", ")}`,
|
|
1507
|
+
role: assignedRole,
|
|
1508
|
+
}, deps);
|
|
1509
|
+
}
|
|
1187
1510
|
|
|
1188
1511
|
await autoAssignPendingTasks(deps);
|
|
1189
1512
|
|
|
@@ -1259,6 +1582,12 @@ async function handleRequest(
|
|
|
1259
1582
|
if (typeof body.progress === "string") task.progress = body.progress as string;
|
|
1260
1583
|
if (typeof body.priority === "string") task.priority = body.priority as TaskPriority;
|
|
1261
1584
|
if (typeof body.assignedRole === "string") task.assignedRole = body.assignedRole as RoleId;
|
|
1585
|
+
if (Array.isArray(body.recommendedSkills)) {
|
|
1586
|
+
const recommendedSkills = normalizeRecommendedSkills(
|
|
1587
|
+
body.recommendedSkills.map((entry: unknown) => String(entry ?? "")),
|
|
1588
|
+
);
|
|
1589
|
+
task.recommendedSkills = recommendedSkills.length > 0 ? recommendedSkills : undefined;
|
|
1590
|
+
}
|
|
1262
1591
|
task.updatedAt = Date.now();
|
|
1263
1592
|
|
|
1264
1593
|
if (typeof body.status === "string" && body.status !== previousStatus) {
|
|
@@ -1463,6 +1792,18 @@ async function handleRequest(
|
|
|
1463
1792
|
|
|
1464
1793
|
// ==================== Message Routing ====================
|
|
1465
1794
|
|
|
1795
|
+
// GET /api/v1/controller/runs
|
|
1796
|
+
if (req.method === "GET" && pathname === "/api/v1/controller/runs") {
|
|
1797
|
+
const state = getTeamState();
|
|
1798
|
+
const controllerRuns = state
|
|
1799
|
+
? Object.values(state.controllerRuns)
|
|
1800
|
+
.sort((left, right) => right.updatedAt - left.updatedAt)
|
|
1801
|
+
.map((run) => serializeControllerRun(run))
|
|
1802
|
+
: [];
|
|
1803
|
+
sendJson(res, 200, { controllerRuns });
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1466
1807
|
// POST /api/v1/controller/intake
|
|
1467
1808
|
if (req.method === "POST" && pathname === "/api/v1/controller/intake") {
|
|
1468
1809
|
const body = await parseJsonBody(req);
|
|
@@ -1907,6 +2248,7 @@ async function handleRequest(
|
|
|
1907
2248
|
teamName: config.teamName,
|
|
1908
2249
|
workers: [],
|
|
1909
2250
|
tasks: [],
|
|
2251
|
+
controllerRuns: [],
|
|
1910
2252
|
messages: [],
|
|
1911
2253
|
clarifications: [],
|
|
1912
2254
|
repo: null,
|
|
@@ -1920,6 +2262,9 @@ async function handleRequest(
|
|
|
1920
2262
|
teamName: state.teamName,
|
|
1921
2263
|
workers: Object.values(state.workers),
|
|
1922
2264
|
tasks: Object.values(state.tasks).map((task) => serializeTask(task)),
|
|
2265
|
+
controllerRuns: Object.values(state.controllerRuns)
|
|
2266
|
+
.sort((left, right) => right.updatedAt - left.updatedAt)
|
|
2267
|
+
.map((run) => serializeControllerRun(run)),
|
|
1923
2268
|
messages: state.messages,
|
|
1924
2269
|
clarifications,
|
|
1925
2270
|
repo: state.repo ?? null,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import net from "node:net";
|
|
5
4
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
@@ -19,6 +18,7 @@ import type {
|
|
|
19
18
|
import {
|
|
20
19
|
resolveDefaultOpenClawConfigPath,
|
|
21
20
|
resolveDefaultOpenClawStateDir,
|
|
21
|
+
resolveDefaultTeamClawRuntimeRootDir,
|
|
22
22
|
resolveDefaultOpenClawWorkspaceDir,
|
|
23
23
|
} from "../openclaw-workspace.js";
|
|
24
24
|
|
|
@@ -125,8 +125,10 @@ export class LocalWorkerManager {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
this.stoppingAll = false;
|
|
128
|
+
const workerBaseRoot = path.join(resolveDefaultTeamClawRuntimeRootDir(), "local-workers");
|
|
129
|
+
await fs.mkdir(workerBaseRoot, { recursive: true });
|
|
128
130
|
this.workerBaseDir = await fs.mkdtemp(
|
|
129
|
-
path.join(
|
|
131
|
+
path.join(workerBaseRoot, `${sanitizePathSegment(this.deps.config.teamName)}-`),
|
|
130
132
|
);
|
|
131
133
|
|
|
132
134
|
const sourceStateDir = resolveDefaultOpenClawStateDir();
|
|
@@ -153,7 +155,7 @@ export class LocalWorkerManager {
|
|
|
153
155
|
|
|
154
156
|
if (this.workerBaseDir) {
|
|
155
157
|
await fs.rm(this.workerBaseDir, { recursive: true, force: true }).catch(() => {
|
|
156
|
-
// Best-effort cleanup;
|
|
158
|
+
// Best-effort cleanup; managed runtime dirs are safe to leave behind.
|
|
157
159
|
});
|
|
158
160
|
this.workerBaseDir = null;
|
|
159
161
|
}
|
|
@@ -80,7 +80,10 @@ export function createControllerPromptInjector(deps: ControllerPromptDeps) {
|
|
|
80
80
|
parts.push("");
|
|
81
81
|
parts.push("### Available Roles");
|
|
82
82
|
for (const role of ROLES) {
|
|
83
|
-
|
|
83
|
+
const skillLine = role.recommendedSkills.length > 0
|
|
84
|
+
? ` Recommended skills: ${role.recommendedSkills.join(", ")}.`
|
|
85
|
+
: "";
|
|
86
|
+
parts.push(`- ${role.icon} ${role.label}: ${role.description}.${skillLine}`);
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
parts.push("");
|
|
@@ -89,6 +92,8 @@ export function createControllerPromptInjector(deps: ControllerPromptDeps) {
|
|
|
89
92
|
parts.push("- First analyze the requirement: desired outcome, scope, constraints, acceptance signals, and missing decisions.");
|
|
90
93
|
parts.push("- If critical information is missing, ask the human a concrete clarification question before creating execution tasks.");
|
|
91
94
|
parts.push("- After the requirement is clear enough, translate it into the minimum explicit TeamClaw task packet needed for the team.");
|
|
95
|
+
parts.push("- When creating a task, include a recommendedSkills array whenever you know a useful OpenClaw/ClawHub skill slug (or a short search query if you do not know the exact slug).");
|
|
96
|
+
parts.push("- Prefer exact skill slugs over vague labels so the assigned worker can auto-search/install them before starting.");
|
|
92
97
|
parts.push("- 'Minimum task packet' means only tasks that can start immediately with the currently available information and already-satisfied prerequisites.");
|
|
93
98
|
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
99
|
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.");
|
|
@@ -5,6 +5,7 @@ import type { PluginLogger } from "../../api.js";
|
|
|
5
5
|
export type WsEvent =
|
|
6
6
|
| { type: "worker:online"; data: unknown }
|
|
7
7
|
| { type: "worker:offline"; data: unknown }
|
|
8
|
+
| { type: "controller:run"; data: unknown }
|
|
8
9
|
| { type: "task:created"; data: unknown }
|
|
9
10
|
| { type: "task:updated"; data: unknown }
|
|
10
11
|
| { type: "task:completed"; data: unknown }
|