@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
|
@@ -4,10 +4,11 @@ import type { TaskAssignmentPayload, TeamMessage } from "../types.js";
|
|
|
4
4
|
import { parseJsonBody, sendJson, sendError } from "../protocol.js";
|
|
5
5
|
import { MessageQueue } from "./message-queue.js";
|
|
6
6
|
|
|
7
|
-
export type TaskExecutor = (assignment: TaskAssignmentPayload) => Promise<string>;
|
|
8
|
-
export type ResultReporter = (taskId: string, result: string, error: string | null) => void;
|
|
7
|
+
export type TaskExecutor = (assignment: TaskAssignmentPayload) => Promise<string | { text: string; contract?: Record<string, unknown> }>;
|
|
8
|
+
export type ResultReporter = (taskId: string, result: string, error: string | null, contract?: Record<string, unknown>) => void;
|
|
9
9
|
export type TaskCanceller = (taskId: string) => Promise<boolean> | boolean;
|
|
10
10
|
export type TaskCancelChecker = (taskId: string) => boolean;
|
|
11
|
+
export type KickoffAssessor = (requirement: string, role: string) => Promise<Record<string, unknown>>;
|
|
11
12
|
|
|
12
13
|
export function createWorkerHttpHandler(
|
|
13
14
|
config: { role: string; port: number },
|
|
@@ -18,6 +19,7 @@ export function createWorkerHttpHandler(
|
|
|
18
19
|
resultReporter?: ResultReporter,
|
|
19
20
|
cancelTaskExecution?: TaskCanceller,
|
|
20
21
|
isTaskCancelled?: TaskCancelChecker,
|
|
22
|
+
kickoffAssessor?: KickoffAssessor,
|
|
21
23
|
) {
|
|
22
24
|
return async (req: IncomingMessage, res: ServerResponse) => {
|
|
23
25
|
// CORS preflight
|
|
@@ -68,6 +70,9 @@ export function createWorkerHttpHandler(
|
|
|
68
70
|
const executionIdempotencyKey = typeof body.executionIdempotencyKey === "string"
|
|
69
71
|
? body.executionIdempotencyKey
|
|
70
72
|
: undefined;
|
|
73
|
+
const projectDir = typeof body.projectDir === "string"
|
|
74
|
+
? body.projectDir
|
|
75
|
+
: undefined;
|
|
71
76
|
const repo = body.repo && typeof body.repo === "object"
|
|
72
77
|
? body.repo as TaskAssignmentPayload["repo"]
|
|
73
78
|
: undefined;
|
|
@@ -85,16 +90,18 @@ export function createWorkerHttpHandler(
|
|
|
85
90
|
title,
|
|
86
91
|
description,
|
|
87
92
|
recommendedSkills,
|
|
93
|
+
projectDir,
|
|
88
94
|
executionSessionKey,
|
|
89
95
|
executionIdempotencyKey,
|
|
90
96
|
repo,
|
|
91
97
|
})
|
|
92
|
-
.then((
|
|
98
|
+
.then((raw) => {
|
|
93
99
|
if (isTaskCancelled?.(taskId)) {
|
|
94
100
|
logger.info(`Worker: skipping result report for cancelled task ${taskId}`);
|
|
95
101
|
return;
|
|
96
102
|
}
|
|
97
|
-
|
|
103
|
+
const result = typeof raw === "string" ? { text: raw } : raw;
|
|
104
|
+
resultReporter(taskId, result.text, null, result.contract);
|
|
98
105
|
})
|
|
99
106
|
.catch((err) => {
|
|
100
107
|
if (isTaskCancelled?.(taskId)) {
|
|
@@ -139,6 +146,35 @@ export function createWorkerHttpHandler(
|
|
|
139
146
|
return;
|
|
140
147
|
}
|
|
141
148
|
|
|
149
|
+
// POST /api/v1/kickoff/assess
|
|
150
|
+
if (req.method === "POST" && pathname === "/api/v1/kickoff/assess") {
|
|
151
|
+
if (!kickoffAssessor) {
|
|
152
|
+
sendError(res, 501, "Kickoff assessment is not supported by this worker");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const body = await parseJsonBody(req);
|
|
157
|
+
const requirement = typeof body.requirement === "string" ? body.requirement : "";
|
|
158
|
+
const role = typeof body.role === "string" ? body.role : config.role;
|
|
159
|
+
|
|
160
|
+
if (!requirement) {
|
|
161
|
+
sendError(res, 400, "requirement is required");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
logger.info(`Worker: received kickoff assessment request for role ${role}`);
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const assessment = await kickoffAssessor(requirement, role);
|
|
169
|
+
sendJson(res, 200, { assessment });
|
|
170
|
+
} catch (err) {
|
|
171
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
172
|
+
logger.error(`Worker: kickoff assessment failed: ${errorMsg}`);
|
|
173
|
+
sendError(res, 500, `Assessment failed: ${errorMsg}`);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
142
178
|
sendError(res, 404, "Not found");
|
|
143
179
|
} catch (err) {
|
|
144
180
|
logger.error(`Worker HTTP error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
import type { PluginConfig, WorkerIdentity } from "../types.js";
|
|
2
2
|
import { getRole } from "../roles.js";
|
|
3
|
+
import { buildWorkerSessionRules, composePrompt } from "../prompt-policy.js";
|
|
3
4
|
import { MessageQueue } from "./message-queue.js";
|
|
4
5
|
|
|
5
|
-
const TEAMCLAW_ROLE_IDS_TEXT = [
|
|
6
|
-
"pm",
|
|
7
|
-
"architect",
|
|
8
|
-
"developer",
|
|
9
|
-
"qa",
|
|
10
|
-
"release-engineer",
|
|
11
|
-
"infra-engineer",
|
|
12
|
-
"devops",
|
|
13
|
-
"security-engineer",
|
|
14
|
-
"designer",
|
|
15
|
-
"marketing",
|
|
16
|
-
].join(", ");
|
|
17
|
-
|
|
18
6
|
export function createWorkerPromptInjector(
|
|
19
7
|
config: PluginConfig,
|
|
20
8
|
getIdentity: () => WorkerIdentity | null,
|
|
@@ -31,30 +19,13 @@ export function createWorkerPromptInjector(
|
|
|
31
19
|
return null;
|
|
32
20
|
}
|
|
33
21
|
|
|
34
|
-
const parts: string[] = [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
parts.push("1. Complete only the task assigned to this session.");
|
|
42
|
-
parts.push("2. Pending team messages are context, not permission to widen scope.");
|
|
43
|
-
parts.push("3. Do NOT create new tasks, duplicate an existing task, or start a parallel task tree.");
|
|
44
|
-
parts.push("4. If you are blocked by missing information, raise a clarification request and stop instead of guessing.");
|
|
45
|
-
parts.push("5. If required infrastructure, credentials, or external tool access are unavailable in this runtime, raise a clarification request and stop instead of faking completion.");
|
|
46
|
-
parts.push("6. Respect the task's requested deliverable: briefs, plans, matrices, reviews, and design artifacts are not implementation requests unless the task explicitly asks you to build code.");
|
|
47
|
-
parts.push("7. If another role must continue later, use review/handoff tools on the current task instead of spawning work.");
|
|
48
|
-
parts.push("8. Other workers' OpenClaw sessions are isolated from this worker. Do not attempt cross-session inspection; use task context, the shared workspace, and queued team messages instead.");
|
|
49
|
-
parts.push("9. Do not mark the task completed or failed via progress updates. Return the final deliverable and let TeamClaw close the task.");
|
|
50
|
-
parts.push(`10. Valid TeamClaw role IDs: ${TEAMCLAW_ROLE_IDS_TEXT}.`);
|
|
51
|
-
parts.push("11. Treat file paths from documents, plans, and teammate messages as hints, not guarantees. Verify the real path exists in the current workspace before reading or editing it; if it does not exist, search for the closest real file and note the drift instead of repeatedly calling missing paths.");
|
|
52
|
-
parts.push("12. The workspace may be backed by a TeamClaw-managed git repository. Treat the current checkout as canonical project state; do not delete `.git` or replace the repo with ad-hoc archives.");
|
|
53
|
-
parts.push("13. If the assigned task includes recommended skills, use those exact skill slugs first. Missing skills should be searched/installed before execution when supported by the runtime.");
|
|
54
|
-
parts.push("14. Important: submit structured collaboration contracts, not only prose. Use teamclaw_submit_result_contract before your final reply, use structured fields on progress/handoff/review/message tools, and use clarification tools instead of hiding questions inside freeform output.");
|
|
55
|
-
parts.push("15. Do not use sessions_yield or end your turn while background work, coding agents, or process sessions are still running. A TeamClaw task is only done when you have the real final deliverable, not when a helper session is still working.");
|
|
56
|
-
parts.push(`Worker ID: ${identity.workerId}`);
|
|
57
|
-
parts.push(`Controller: ${identity.controllerUrl}`);
|
|
22
|
+
const parts: string[] = [
|
|
23
|
+
`## TeamClaw Role: ${roleDef.label} ${roleDef.icon}`,
|
|
24
|
+
roleDef.systemPrompt,
|
|
25
|
+
...buildWorkerSessionRules(),
|
|
26
|
+
`Worker ID: ${identity.workerId}`,
|
|
27
|
+
`Controller: ${identity.controllerUrl}`,
|
|
28
|
+
];
|
|
58
29
|
|
|
59
30
|
// Pending messages
|
|
60
31
|
const pendingMessages = messageQueue.peek();
|
|
@@ -69,7 +40,7 @@ export function createWorkerPromptInjector(
|
|
|
69
40
|
}
|
|
70
41
|
|
|
71
42
|
return {
|
|
72
|
-
prependSystemContext: parts
|
|
43
|
+
prependSystemContext: composePrompt(parts),
|
|
73
44
|
};
|
|
74
45
|
};
|
|
75
46
|
}
|
|
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import type { PluginLogger } from "../../api.js";
|
|
5
5
|
import { normalizeRecommendedSkills } from "../roles.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveTeamClawWorkspaceDir } from "../openclaw-workspace.js";
|
|
7
7
|
import type { TaskAssignmentPayload, TaskExecutionEventInput } from "../types.js";
|
|
8
8
|
|
|
9
9
|
type SkillCli = "openclaw" | "clawhub";
|
|
@@ -146,7 +146,7 @@ export async function installRecommendedSkills(
|
|
|
146
146
|
logger: PluginLogger,
|
|
147
147
|
): Promise<SkillInstallResult> {
|
|
148
148
|
const recommendedSkills = normalizeRecommendedSkills(assignment.recommendedSkills ?? []);
|
|
149
|
-
const workspaceDir =
|
|
149
|
+
const workspaceDir = resolveTeamClawWorkspaceDir();
|
|
150
150
|
const events: TaskExecutionEventInput[] = [];
|
|
151
151
|
const installed: string[] = [];
|
|
152
152
|
const skipped: string[] = [];
|
package/src/worker/tools.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
renderWorkerProgressText,
|
|
12
12
|
} from "../interaction-contracts.js";
|
|
13
13
|
import type { PluginConfig, WorkerIdentity } from "../types.js";
|
|
14
|
+
import { normalizeClarificationQuestionSchema } from "../controller/orchestration-manifest.js";
|
|
14
15
|
|
|
15
16
|
const ALLOWED_PROGRESS_STATUSES = new Set(["in_progress", "review"]);
|
|
16
17
|
|
|
@@ -274,10 +275,15 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
274
275
|
kind: Type.String({ description: "Deliverable kind: file, directory, command, artifact, or note" }),
|
|
275
276
|
value: Type.String({ description: "Deliverable identifier, path, or note" }),
|
|
276
277
|
summary: Type.Optional(Type.String({ description: "Optional short note about this deliverable" })),
|
|
278
|
+
artifactType: Type.Optional(Type.String({ description: "Artifact type: web-app, static-site, binary, or document" })),
|
|
279
|
+
previewCommand: Type.Optional(Type.String({ description: "Command to start a live preview server (for web-app artifacts)" })),
|
|
280
|
+
previewCwd: Type.Optional(Type.String({ description: "Working directory for the preview command" })),
|
|
281
|
+
previewReadyPath: Type.Optional(Type.String({ description: "Path to poll for health (default /)" })),
|
|
277
282
|
}),
|
|
278
283
|
),
|
|
279
284
|
),
|
|
280
285
|
keyPoints: Type.Optional(Type.Array(Type.String({ description: "Important decisions, findings, or implementation notes" }))),
|
|
286
|
+
discoveredPatterns: Type.Optional(Type.Array(Type.String({ description: "Reusable codebase patterns discovered during this task (e.g. 'This codebase uses X for Y', 'Always update Z when changing W'). These are consolidated into workspace memory for future tasks." }))),
|
|
281
287
|
blockers: Type.Optional(Type.Array(Type.String({ description: "Any unresolved blockers or risks" }))),
|
|
282
288
|
followUps: Type.Optional(
|
|
283
289
|
Type.Array(
|
|
@@ -308,6 +314,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
308
314
|
summary: params.summary,
|
|
309
315
|
deliverables: params.deliverables,
|
|
310
316
|
keyPoints: params.keyPoints,
|
|
317
|
+
discoveredPatterns: params.discoveredPatterns,
|
|
311
318
|
blockers: params.blockers,
|
|
312
319
|
followUps: params.followUps,
|
|
313
320
|
questions: params.questions,
|
|
@@ -346,6 +353,23 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
346
353
|
question: Type.String({ description: "The exact question that must be answered before work can continue" }),
|
|
347
354
|
blockingReason: Type.String({ description: "Why this task cannot proceed safely without clarification" }),
|
|
348
355
|
context: Type.Optional(Type.String({ description: "Optional brief context or the specific decision that is missing" })),
|
|
356
|
+
questionSchema: Type.Optional(Type.Object({
|
|
357
|
+
kind: Type.String({ description: "Question kind: single-select, multi-select, number, or text" }),
|
|
358
|
+
title: Type.String({ description: "Question title shown to the human" }),
|
|
359
|
+
description: Type.Optional(Type.String({ description: "Optional supporting context for the question" })),
|
|
360
|
+
required: Type.Optional(Type.Boolean({ description: "Whether the question must be answered" })),
|
|
361
|
+
options: Type.Optional(Type.Array(Type.Object({
|
|
362
|
+
value: Type.String({ description: "Stable option value" }),
|
|
363
|
+
label: Type.String({ description: "Human-visible option label" }),
|
|
364
|
+
hint: Type.Optional(Type.String({ description: "Optional helper text for this option" })),
|
|
365
|
+
}))),
|
|
366
|
+
allowOther: Type.Optional(Type.Boolean({ description: "Whether freeform 'other' text is allowed" })),
|
|
367
|
+
placeholder: Type.Optional(Type.String({ description: "Optional placeholder or hint for text/number input" })),
|
|
368
|
+
unit: Type.Optional(Type.String({ description: "Optional unit label for number questions" })),
|
|
369
|
+
min: Type.Optional(Type.Number({ description: "Optional minimum numeric value" })),
|
|
370
|
+
max: Type.Optional(Type.Number({ description: "Optional maximum numeric value" })),
|
|
371
|
+
step: Type.Optional(Type.Number({ description: "Optional numeric step size" })),
|
|
372
|
+
})),
|
|
349
373
|
}),
|
|
350
374
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
351
375
|
const identity = getIdentity();
|
|
@@ -357,6 +381,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
357
381
|
const question = String(params.question ?? "");
|
|
358
382
|
const blockingReason = String(params.blockingReason ?? "");
|
|
359
383
|
const context = typeof params.context === "string" ? params.context : undefined;
|
|
384
|
+
const questionSchema = normalizeClarificationQuestionSchema(params.questionSchema);
|
|
360
385
|
|
|
361
386
|
if (!taskId || !question || !blockingReason) {
|
|
362
387
|
return { content: [{ type: "text" as const, text: "taskId, question, and blockingReason are required." }] };
|
|
@@ -370,11 +395,12 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
370
395
|
taskId,
|
|
371
396
|
requestedBy: identity.workerId,
|
|
372
397
|
requestedByWorkerId: identity.workerId,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
398
|
+
requestedByRole: identity.role,
|
|
399
|
+
question,
|
|
400
|
+
questionSchema,
|
|
401
|
+
blockingReason,
|
|
402
|
+
context,
|
|
403
|
+
}),
|
|
378
404
|
});
|
|
379
405
|
|
|
380
406
|
if (!res.ok) {
|
|
@@ -5,13 +5,15 @@ import { createHeartbeatPayload } from "../protocol.js";
|
|
|
5
5
|
import { IdentityManager } from "../identity.js";
|
|
6
6
|
import { MessageQueue } from "./message-queue.js";
|
|
7
7
|
import { createWorkerHttpHandler } from "./http-handler.js";
|
|
8
|
-
import { ensureOpenClawWorkspaceMemoryDir } from "../openclaw-workspace.js";
|
|
8
|
+
import { buildTeamClawAgentSessionKey, ensureOpenClawWorkspaceMemoryDir } from "../openclaw-workspace.js";
|
|
9
|
+
|
|
10
|
+
export type TaskExecutorResultLike = string | { text: string; contract?: Record<string, unknown> };
|
|
9
11
|
|
|
10
12
|
export type WorkerServiceDeps = {
|
|
11
13
|
config: PluginConfig;
|
|
12
14
|
logger: PluginLogger;
|
|
13
15
|
onIdentityEstablished: (identity: WorkerIdentity) => void;
|
|
14
|
-
taskExecutor?: (taskDescription: string, assignment: TaskAssignmentPayload) => Promise<
|
|
16
|
+
taskExecutor?: (taskDescription: string, assignment: TaskAssignmentPayload) => Promise<TaskExecutorResultLike>;
|
|
15
17
|
prepareTaskAssignment?: (assignment: TaskAssignmentPayload) => Promise<void> | void;
|
|
16
18
|
publishTaskAssignment?: (assignment: TaskAssignmentPayload, result: string) => Promise<void> | void;
|
|
17
19
|
cancelTaskExecution?: (taskId: string, sessionKey?: string) => Promise<boolean> | boolean;
|
|
@@ -31,19 +33,20 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
|
|
|
31
33
|
const cancelledTaskIds = new Set<string>();
|
|
32
34
|
|
|
33
35
|
const taskExecutor = externalTaskExecutor
|
|
34
|
-
? async (assignment: TaskAssignmentPayload): Promise<string> => {
|
|
36
|
+
? async (assignment: TaskAssignmentPayload): Promise<{ text: string; contract?: Record<string, unknown> }> => {
|
|
35
37
|
const taskId = assignment.taskId;
|
|
36
38
|
cancelledTaskIds.delete(taskId);
|
|
37
39
|
activeTaskId = taskId;
|
|
38
|
-
activeTaskSessionKeys.set(taskId, assignment.executionSessionKey || `teamclaw-task-${taskId}`);
|
|
40
|
+
activeTaskSessionKeys.set(taskId, assignment.executionSessionKey || buildTeamClawAgentSessionKey(`teamclaw-task-${taskId}`));
|
|
39
41
|
try {
|
|
40
42
|
await deps.prepareTaskAssignment?.(assignment);
|
|
41
43
|
const taskPrompt = [assignment.title.trim(), assignment.description.trim()].filter(Boolean).join("\n\n");
|
|
42
|
-
const
|
|
44
|
+
const raw = await externalTaskExecutor(taskPrompt, assignment);
|
|
43
45
|
if (cancelledTaskIds.has(taskId)) {
|
|
44
46
|
throw new Error("Task execution cancelled by controller");
|
|
45
47
|
}
|
|
46
|
-
|
|
48
|
+
const result = typeof raw === "string" ? { text: raw } : raw;
|
|
49
|
+
await deps.publishTaskAssignment?.(assignment, result.text);
|
|
47
50
|
return result;
|
|
48
51
|
} finally {
|
|
49
52
|
activeTaskId = undefined;
|
|
@@ -55,16 +58,20 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
|
|
|
55
58
|
}
|
|
56
59
|
: undefined;
|
|
57
60
|
|
|
58
|
-
function reportTaskResult(taskId: string, result: string, error: string | null): void {
|
|
61
|
+
function reportTaskResult(taskId: string, result: string, error: string | null, contract?: Record<string, unknown>): void {
|
|
59
62
|
if (cancelledTaskIds.has(taskId)) {
|
|
60
63
|
logger.info(`Worker: suppressing result report for cancelled task ${taskId}`);
|
|
61
64
|
return;
|
|
62
65
|
}
|
|
63
66
|
if (!controllerUrl) return;
|
|
67
|
+
const body: Record<string, unknown> = { result, error, workerId };
|
|
68
|
+
if (contract) {
|
|
69
|
+
body.resultContract = contract;
|
|
70
|
+
}
|
|
64
71
|
fetch(`${controllerUrl}/api/v1/tasks/${taskId}/result`, {
|
|
65
72
|
method: "POST",
|
|
66
73
|
headers: { "Content-Type": "application/json" },
|
|
67
|
-
body: JSON.stringify(
|
|
74
|
+
body: JSON.stringify(body),
|
|
68
75
|
}).catch((err) => {
|
|
69
76
|
logger.error(`Worker: failed to report task result: ${String(err)}`);
|
|
70
77
|
});
|
|
@@ -90,6 +97,39 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
|
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
async function startServer(): Promise<void> {
|
|
100
|
+
// Kickoff assessor — uses the worker's subagent runtime for lightweight assessment
|
|
101
|
+
const kickoffAssessor = externalTaskExecutor
|
|
102
|
+
? async (requirement: string, role: string): Promise<Record<string, unknown>> => {
|
|
103
|
+
const { buildKickoffAssessmentPrompt } = await import("../controller/kickoff-orchestrator.js");
|
|
104
|
+
const prompt = buildKickoffAssessmentPrompt(role as import("../types.js").RoleId, requirement);
|
|
105
|
+
const sessionKey = buildTeamClawAgentSessionKey(`teamclaw-kickoff-${role}-${Date.now()}`);
|
|
106
|
+
const raw = await externalTaskExecutor(prompt, {
|
|
107
|
+
taskId: `kickoff-${role}`,
|
|
108
|
+
title: `Kickoff Assessment (${role})`,
|
|
109
|
+
description: prompt,
|
|
110
|
+
executionSessionKey: sessionKey,
|
|
111
|
+
});
|
|
112
|
+
const text = typeof raw === "string" ? raw : raw.text;
|
|
113
|
+
// Parse the JSON response
|
|
114
|
+
const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/) || text.match(/(\{[\s\S]*\})/);
|
|
115
|
+
const jsonStr = jsonMatch?.[1]?.trim() ?? text.trim();
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(jsonStr);
|
|
118
|
+
return {
|
|
119
|
+
role,
|
|
120
|
+
needed: Boolean(parsed.needed),
|
|
121
|
+
scope: String(parsed.scope ?? ""),
|
|
122
|
+
suggestedTasks: Array.isArray(parsed.suggestedTasks) ? parsed.suggestedTasks : [],
|
|
123
|
+
dependencies: Array.isArray(parsed.dependencies) ? parsed.dependencies : [],
|
|
124
|
+
risks: Array.isArray(parsed.risks) ? parsed.risks : [],
|
|
125
|
+
questions: Array.isArray(parsed.questions) ? parsed.questions : [],
|
|
126
|
+
};
|
|
127
|
+
} catch {
|
|
128
|
+
return { role, needed: false, scope: `Could not parse: ${text.slice(0, 200)}`, suggestedTasks: [], dependencies: [], risks: [], questions: [] };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
: undefined;
|
|
132
|
+
|
|
93
133
|
const handler = createWorkerHttpHandler(
|
|
94
134
|
{ role: config.role, port: config.port },
|
|
95
135
|
logger,
|
|
@@ -99,6 +139,7 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
|
|
|
99
139
|
reportTaskResult,
|
|
100
140
|
cancelAssignedTask,
|
|
101
141
|
isTaskCancelled,
|
|
142
|
+
kickoffAssessor,
|
|
102
143
|
);
|
|
103
144
|
|
|
104
145
|
if (server) {
|
package/src/workspace-browser.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveTeamClawWorkspaceDir } from "./openclaw-workspace.js";
|
|
4
4
|
|
|
5
5
|
const MAX_PREVIEW_BYTES = 256 * 1024;
|
|
6
6
|
const MAX_TREE_DEPTH = 8;
|
|
@@ -81,15 +81,25 @@ export type WorkspaceFilePayload = {
|
|
|
81
81
|
contentType: string;
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
export async function listWorkspaceTree(): Promise<WorkspaceTreePayload> {
|
|
84
|
+
export async function listWorkspaceTree(maxDepth?: number): Promise<WorkspaceTreePayload> {
|
|
85
85
|
const workspaceDir = await ensureWorkspaceDir();
|
|
86
|
-
const entries = await readTree(workspaceDir, "", 0);
|
|
86
|
+
const entries = await readTree(workspaceDir, "", 0, maxDepth);
|
|
87
87
|
return {
|
|
88
88
|
root: "/",
|
|
89
89
|
entries,
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/** Load children of a single directory (one level). Used for lazy-loading. */
|
|
94
|
+
export async function listWorkspaceSubtree(relativePath: string): Promise<WorkspaceTreeNode[]> {
|
|
95
|
+
const { absolutePath, normalizedPath } = await resolveWorkspacePath(relativePath);
|
|
96
|
+
const stat = await fs.stat(absolutePath);
|
|
97
|
+
if (!stat.isDirectory()) {
|
|
98
|
+
throw new Error("Path is not a directory");
|
|
99
|
+
}
|
|
100
|
+
return readTree(absolutePath, normalizedPath, 0, 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
export async function readWorkspaceFile(relativePath: string): Promise<WorkspaceFilePayload> {
|
|
94
104
|
const { normalizedPath, absolutePath } = await resolveWorkspacePath(relativePath);
|
|
95
105
|
const stat = await fs.stat(absolutePath);
|
|
@@ -147,7 +157,7 @@ export function buildWorkspaceRawUrl(relativePath: string): string {
|
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
async function ensureWorkspaceDir(): Promise<string> {
|
|
150
|
-
const workspaceDir =
|
|
160
|
+
const workspaceDir = resolveTeamClawWorkspaceDir();
|
|
151
161
|
await fs.mkdir(workspaceDir, { recursive: true });
|
|
152
162
|
return workspaceDir;
|
|
153
163
|
}
|
|
@@ -189,8 +199,9 @@ function normalizeWorkspacePath(relativePath: string): string {
|
|
|
189
199
|
return normalized;
|
|
190
200
|
}
|
|
191
201
|
|
|
192
|
-
async function readTree(dirPath: string, relativeDir: string, depth: number): Promise<WorkspaceTreeNode[]> {
|
|
193
|
-
|
|
202
|
+
async function readTree(dirPath: string, relativeDir: string, depth: number, maxDepth?: number): Promise<WorkspaceTreeNode[]> {
|
|
203
|
+
const effectiveMaxDepth = maxDepth ?? MAX_TREE_DEPTH;
|
|
204
|
+
if (depth > effectiveMaxDepth) {
|
|
194
205
|
return [];
|
|
195
206
|
}
|
|
196
207
|
|
|
@@ -209,11 +220,13 @@ async function readTree(dirPath: string, relativeDir: string, depth: number): Pr
|
|
|
209
220
|
const childAbsolutePath = path.join(dirPath, dirent.name);
|
|
210
221
|
|
|
211
222
|
if (dirent.isDirectory()) {
|
|
223
|
+
// At max depth, mark directory as lazy-loadable (no children yet)
|
|
224
|
+
const atLimit = depth + 1 > effectiveMaxDepth;
|
|
212
225
|
nodes.push({
|
|
213
226
|
name: dirent.name,
|
|
214
227
|
path: childRelativePath,
|
|
215
228
|
type: "directory",
|
|
216
|
-
children: await readTree(childAbsolutePath, childRelativePath, depth + 1),
|
|
229
|
+
children: atLimit ? undefined : await readTree(childAbsolutePath, childRelativePath, depth + 1, effectiveMaxDepth),
|
|
217
230
|
});
|
|
218
231
|
continue;
|
|
219
232
|
}
|