@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
|
@@ -2,7 +2,6 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import https from "node:https";
|
|
4
4
|
import net from "node:net";
|
|
5
|
-
import os from "node:os";
|
|
6
5
|
import path from "node:path";
|
|
7
6
|
import readline from "node:readline";
|
|
8
7
|
import { createHash } from "node:crypto";
|
|
@@ -10,7 +9,10 @@ import { spawn, type ChildProcess } from "node:child_process";
|
|
|
10
9
|
import JSON5 from "json5";
|
|
11
10
|
import type { PluginLogger } from "../../api.js";
|
|
12
11
|
import { generateId } from "../protocol.js";
|
|
13
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
resolveDefaultOpenClawConfigPath,
|
|
14
|
+
resolveDefaultTeamClawRuntimeRootDir,
|
|
15
|
+
} from "../openclaw-workspace.js";
|
|
14
16
|
import { ROLES } from "../roles.js";
|
|
15
17
|
import type {
|
|
16
18
|
PluginConfig,
|
|
@@ -45,6 +47,7 @@ type LaunchSpec = {
|
|
|
45
47
|
controllerUrl: string;
|
|
46
48
|
workerPort: number;
|
|
47
49
|
gatewayPort: number;
|
|
50
|
+
workspaceDir?: string;
|
|
48
51
|
env: Record<string, string>;
|
|
49
52
|
configJson: string;
|
|
50
53
|
};
|
|
@@ -377,6 +380,7 @@ export class WorkerProvisioningManager {
|
|
|
377
380
|
controllerUrl,
|
|
378
381
|
workerPort,
|
|
379
382
|
gatewayPort,
|
|
383
|
+
workspaceDir: buildProvisionedWorkspaceDir(this.backend.type, this.deps.config, role, workerId),
|
|
380
384
|
});
|
|
381
385
|
const launchResult = await this.backend.launch({
|
|
382
386
|
workerId,
|
|
@@ -385,6 +389,7 @@ export class WorkerProvisioningManager {
|
|
|
385
389
|
controllerUrl,
|
|
386
390
|
workerPort,
|
|
387
391
|
gatewayPort,
|
|
392
|
+
workspaceDir: getConfiguredWorkerWorkspaceDir(workerConfig),
|
|
388
393
|
env: this.buildForwardedEnv(),
|
|
389
394
|
configJson: `${JSON.stringify(workerConfig, null, 2)}\n`,
|
|
390
395
|
});
|
|
@@ -620,7 +625,9 @@ class ProcessProvisioner implements WorkerProvisionerBackend {
|
|
|
620
625
|
|
|
621
626
|
constructor(logger: PluginLogger) {
|
|
622
627
|
this.logger = logger;
|
|
623
|
-
|
|
628
|
+
const provisionedRoot = path.join(resolveDefaultTeamClawRuntimeRootDir(), "provisioned-workers");
|
|
629
|
+
this.baseDirPromise = fs.mkdir(provisionedRoot, { recursive: true })
|
|
630
|
+
.then(() => fs.mkdtemp(path.join(provisionedRoot, "session-")));
|
|
624
631
|
}
|
|
625
632
|
|
|
626
633
|
async launch(spec: LaunchSpec): Promise<LaunchResult> {
|
|
@@ -733,6 +740,7 @@ class DockerProvisioner implements WorkerProvisionerBackend {
|
|
|
733
740
|
TEAMCLAW_BOOTSTRAP_CONFIG_B64: Buffer.from(spec.configJson, "utf8").toString("base64"),
|
|
734
741
|
TEAMCLAW_WORKER_ID: spec.workerId,
|
|
735
742
|
TEAMCLAW_LAUNCH_TOKEN: spec.launchToken,
|
|
743
|
+
...(spec.workspaceDir ? { TEAMCLAW_WORKSPACE_DIR: spec.workspaceDir } : {}),
|
|
736
744
|
};
|
|
737
745
|
|
|
738
746
|
const script = buildContainerBootstrapScript();
|
|
@@ -750,7 +758,7 @@ class DockerProvisioner implements WorkerProvisionerBackend {
|
|
|
750
758
|
"teamclaw.worker_id": spec.workerId,
|
|
751
759
|
},
|
|
752
760
|
HostConfig: {
|
|
753
|
-
Binds: this.config
|
|
761
|
+
Binds: buildDockerBinds(this.config),
|
|
754
762
|
NetworkMode: this.config.workerProvisioningDockerNetwork || undefined,
|
|
755
763
|
},
|
|
756
764
|
},
|
|
@@ -806,7 +814,12 @@ class KubernetesProvisioner implements WorkerProvisionerBackend {
|
|
|
806
814
|
TEAMCLAW_BOOTSTRAP_CONFIG_B64: Buffer.from(spec.configJson, "utf8").toString("base64"),
|
|
807
815
|
TEAMCLAW_WORKER_ID: spec.workerId,
|
|
808
816
|
TEAMCLAW_LAUNCH_TOKEN: spec.launchToken,
|
|
817
|
+
...(spec.workspaceDir ? { TEAMCLAW_WORKSPACE_DIR: spec.workspaceDir } : {}),
|
|
809
818
|
};
|
|
819
|
+
const workspaceRoot = this.config.workerProvisioningWorkspaceRoot;
|
|
820
|
+
const hasPersistentWorkspace = Boolean(
|
|
821
|
+
workspaceRoot && this.config.workerProvisioningKubernetesWorkspacePersistentVolumeClaim,
|
|
822
|
+
);
|
|
810
823
|
|
|
811
824
|
const manifest = {
|
|
812
825
|
apiVersion: "v1",
|
|
@@ -829,6 +842,23 @@ class KubernetesProvisioner implements WorkerProvisionerBackend {
|
|
|
829
842
|
restartPolicy: "Never",
|
|
830
843
|
hostname: buildManagedHostname(this.config.teamName, spec.role, spec.workerId),
|
|
831
844
|
serviceAccountName: this.config.workerProvisioningKubernetesServiceAccount || undefined,
|
|
845
|
+
securityContext: hasPersistentWorkspace
|
|
846
|
+
? {
|
|
847
|
+
runAsUser: 1000,
|
|
848
|
+
runAsGroup: 1000,
|
|
849
|
+
fsGroup: 1000,
|
|
850
|
+
}
|
|
851
|
+
: undefined,
|
|
852
|
+
volumes: hasPersistentWorkspace
|
|
853
|
+
? [
|
|
854
|
+
{
|
|
855
|
+
name: "workspace",
|
|
856
|
+
persistentVolumeClaim: {
|
|
857
|
+
claimName: this.config.workerProvisioningKubernetesWorkspacePersistentVolumeClaim,
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
]
|
|
861
|
+
: undefined,
|
|
832
862
|
containers: [
|
|
833
863
|
{
|
|
834
864
|
name: "worker",
|
|
@@ -836,6 +866,14 @@ class KubernetesProvisioner implements WorkerProvisionerBackend {
|
|
|
836
866
|
command: ["sh", "-lc"],
|
|
837
867
|
args: [buildContainerBootstrapScript()],
|
|
838
868
|
env: Object.entries(env).map(([name, value]) => ({ name, value })),
|
|
869
|
+
volumeMounts: hasPersistentWorkspace
|
|
870
|
+
? [
|
|
871
|
+
{
|
|
872
|
+
name: "workspace",
|
|
873
|
+
mountPath: workspaceRoot,
|
|
874
|
+
},
|
|
875
|
+
]
|
|
876
|
+
: undefined,
|
|
839
877
|
},
|
|
840
878
|
],
|
|
841
879
|
},
|
|
@@ -976,9 +1014,21 @@ function buildProvisionedWorkerConfig(
|
|
|
976
1014
|
controllerUrl: string;
|
|
977
1015
|
workerPort: number;
|
|
978
1016
|
gatewayPort: number;
|
|
1017
|
+
workspaceDir?: string;
|
|
979
1018
|
},
|
|
980
1019
|
): Record<string, unknown> {
|
|
981
1020
|
const config = cloneJson(baseConfig);
|
|
1021
|
+
const agents = ensureRecord(config.agents);
|
|
1022
|
+
const agentDefaults = ensureRecord(agents.defaults);
|
|
1023
|
+
delete agentDefaults.repoRoot;
|
|
1024
|
+
if (spec.workspaceDir) {
|
|
1025
|
+
agentDefaults.workspace = spec.workspaceDir;
|
|
1026
|
+
} else {
|
|
1027
|
+
delete agentDefaults.workspace;
|
|
1028
|
+
}
|
|
1029
|
+
agents.defaults = agentDefaults;
|
|
1030
|
+
config.agents = agents;
|
|
1031
|
+
|
|
982
1032
|
const gateway = ensureRecord(config.gateway);
|
|
983
1033
|
gateway.mode = "local";
|
|
984
1034
|
gateway.bind = "loopback";
|
|
@@ -1016,9 +1066,12 @@ function buildProvisionedWorkerConfig(
|
|
|
1016
1066
|
teamclawConfig.workerProvisioningExtraEnv = {};
|
|
1017
1067
|
teamclawConfig.workerProvisioningDockerNetwork = "";
|
|
1018
1068
|
teamclawConfig.workerProvisioningDockerMounts = [];
|
|
1069
|
+
teamclawConfig.workerProvisioningWorkspaceRoot = "";
|
|
1070
|
+
teamclawConfig.workerProvisioningDockerWorkspaceVolume = "";
|
|
1019
1071
|
teamclawConfig.workerProvisioningKubernetesNamespace = "default";
|
|
1020
1072
|
teamclawConfig.workerProvisioningKubernetesContext = "";
|
|
1021
1073
|
teamclawConfig.workerProvisioningKubernetesServiceAccount = "";
|
|
1074
|
+
teamclawConfig.workerProvisioningKubernetesWorkspacePersistentVolumeClaim = "";
|
|
1022
1075
|
teamclawConfig.workerProvisioningKubernetesLabels = {};
|
|
1023
1076
|
teamclawConfig.workerProvisioningKubernetesAnnotations = {};
|
|
1024
1077
|
teamclawEntry.config = teamclawConfig;
|
|
@@ -1205,11 +1258,47 @@ function buildContainerBootstrapScript(): string {
|
|
|
1205
1258
|
return [
|
|
1206
1259
|
"set -eu",
|
|
1207
1260
|
"mkdir -p \"$OPENCLAW_STATE_DIR\"",
|
|
1261
|
+
"if [ -n \"${TEAMCLAW_WORKSPACE_DIR:-}\" ]; then mkdir -p \"$TEAMCLAW_WORKSPACE_DIR\"; fi",
|
|
1208
1262
|
"node -e 'const fs=require(\"fs\"); const configPath=process.env.OPENCLAW_CONFIG_PATH; const raw=Buffer.from(process.env.TEAMCLAW_BOOTSTRAP_CONFIG_B64||\"\", \"base64\").toString(\"utf8\"); fs.mkdirSync(require(\"path\").dirname(configPath), { recursive: true }); fs.writeFileSync(configPath, raw);'",
|
|
1209
1263
|
"exec node dist/index.js gateway --allow-unconfigured",
|
|
1210
1264
|
].join("\n");
|
|
1211
1265
|
}
|
|
1212
1266
|
|
|
1267
|
+
function buildDockerBinds(config: PluginConfig): string[] {
|
|
1268
|
+
const binds = [...config.workerProvisioningDockerMounts];
|
|
1269
|
+
if (config.workerProvisioningDockerWorkspaceVolume && config.workerProvisioningWorkspaceRoot) {
|
|
1270
|
+
binds.unshift(`${config.workerProvisioningDockerWorkspaceVolume}:${config.workerProvisioningWorkspaceRoot}`);
|
|
1271
|
+
}
|
|
1272
|
+
return [...new Set(binds)];
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function buildProvisionedWorkspaceDir(
|
|
1276
|
+
provider: WorkerProvisioningType,
|
|
1277
|
+
config: PluginConfig,
|
|
1278
|
+
role: RoleId,
|
|
1279
|
+
workerId: string,
|
|
1280
|
+
): string {
|
|
1281
|
+
if (
|
|
1282
|
+
(provider !== "docker" && provider !== "kubernetes") ||
|
|
1283
|
+
!config.workerProvisioningWorkspaceRoot
|
|
1284
|
+
) {
|
|
1285
|
+
return "";
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
return path.posix.join(
|
|
1289
|
+
config.workerProvisioningWorkspaceRoot,
|
|
1290
|
+
sanitizePathSegment(config.teamName),
|
|
1291
|
+
sanitizePathSegment(role),
|
|
1292
|
+
sanitizePathSegment(workerId),
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function getConfiguredWorkerWorkspaceDir(config: Record<string, unknown>): string {
|
|
1297
|
+
const agents = ensureRecord(config.agents);
|
|
1298
|
+
const defaults = ensureRecord(agents.defaults);
|
|
1299
|
+
return typeof defaults.workspace === "string" ? defaults.workspace : "";
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1213
1302
|
function resolveDockerEndpoint(): DockerEndpoint {
|
|
1214
1303
|
const dockerHost = process.env.DOCKER_HOST?.trim();
|
|
1215
1304
|
if (!dockerHost) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const TEAMCLAW_PUBLISHED_RUNTIME_IMAGE = "ghcr.io/topcheer/teamclaw-openclaw:latest";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
1
3
|
import os from "node:os";
|
|
2
4
|
import path from "node:path";
|
|
3
|
-
import
|
|
5
|
+
import JSON5 from "json5";
|
|
4
6
|
import type { PluginLogger } from "../api.js";
|
|
5
7
|
|
|
6
8
|
const DEFAULT_AGENTS_MD = `# AGENTS.md
|
|
@@ -29,6 +31,49 @@ const DEFAULT_HEARTBEAT_MD = `# HEARTBEAT.md
|
|
|
29
31
|
# Keep this file empty (or with only comments) to skip heartbeat API calls.
|
|
30
32
|
`;
|
|
31
33
|
|
|
34
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function expandUserPath(
|
|
39
|
+
value: string,
|
|
40
|
+
homedir: () => string = os.homedir,
|
|
41
|
+
): string {
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
if (!trimmed) {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
if (trimmed === "~") {
|
|
47
|
+
return homedir();
|
|
48
|
+
}
|
|
49
|
+
if (trimmed.startsWith("~/")) {
|
|
50
|
+
return path.join(homedir(), trimmed.slice(2));
|
|
51
|
+
}
|
|
52
|
+
return path.resolve(trimmed);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveConfiguredOpenClawWorkspaceDir(
|
|
56
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
57
|
+
homedir: () => string = os.homedir,
|
|
58
|
+
): string {
|
|
59
|
+
const configPath = resolveDefaultOpenClawConfigPath(env, homedir);
|
|
60
|
+
try {
|
|
61
|
+
const raw = readFileSync(configPath, "utf8");
|
|
62
|
+
const parsed = JSON5.parse(raw);
|
|
63
|
+
if (!isRecord(parsed)) {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
const agents = isRecord(parsed.agents) ? parsed.agents : null;
|
|
67
|
+
const defaults = agents && isRecord(agents.defaults) ? agents.defaults : null;
|
|
68
|
+
if (defaults && typeof defaults.workspace === "string" && defaults.workspace.trim()) {
|
|
69
|
+
return expandUserPath(defaults.workspace, homedir);
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Fall back to the legacy state-dir-derived workspace path below.
|
|
73
|
+
}
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
|
|
32
77
|
export function resolveDefaultOpenClawHomeDir(
|
|
33
78
|
env: NodeJS.ProcessEnv = process.env,
|
|
34
79
|
homedir: () => string = os.homedir,
|
|
@@ -65,6 +110,10 @@ export function resolveDefaultOpenClawWorkspaceDir(
|
|
|
65
110
|
env: NodeJS.ProcessEnv = process.env,
|
|
66
111
|
homedir: () => string = os.homedir,
|
|
67
112
|
): string {
|
|
113
|
+
const configuredWorkspaceDir = resolveConfiguredOpenClawWorkspaceDir(env, homedir);
|
|
114
|
+
if (configuredWorkspaceDir) {
|
|
115
|
+
return configuredWorkspaceDir;
|
|
116
|
+
}
|
|
68
117
|
const stateDir = resolveDefaultOpenClawStateDir(env, homedir);
|
|
69
118
|
const profile = env.OPENCLAW_PROFILE?.trim();
|
|
70
119
|
if (profile && profile.toLowerCase() !== "default") {
|
|
@@ -73,6 +122,13 @@ export function resolveDefaultOpenClawWorkspaceDir(
|
|
|
73
122
|
return path.join(stateDir, "workspace");
|
|
74
123
|
}
|
|
75
124
|
|
|
125
|
+
export function resolveDefaultTeamClawRuntimeRootDir(
|
|
126
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
127
|
+
homedir: () => string = os.homedir,
|
|
128
|
+
): string {
|
|
129
|
+
return path.join(path.dirname(resolveDefaultOpenClawWorkspaceDir(env, homedir)), "teamclaw-runtimes");
|
|
130
|
+
}
|
|
131
|
+
|
|
76
132
|
export async function ensureOpenClawWorkspaceMemoryDir(logger: PluginLogger): Promise<string> {
|
|
77
133
|
const workspaceDir = resolveDefaultOpenClawWorkspaceDir();
|
|
78
134
|
const memoryDir = path.join(workspaceDir, "memory");
|
package/src/roles.ts
CHANGED
|
@@ -10,6 +10,7 @@ const ROLES: RoleDefinition[] = [
|
|
|
10
10
|
"requirements-analysis", "user-stories", "product-specification",
|
|
11
11
|
"priority-planning", "stakeholder-communication",
|
|
12
12
|
],
|
|
13
|
+
recommendedSkills: ["find-skills"],
|
|
13
14
|
systemPrompt: [
|
|
14
15
|
"You are a Product Manager in a virtual software team.",
|
|
15
16
|
"Your responsibilities include analyzing requirements, writing user stories,",
|
|
@@ -28,6 +29,7 @@ const ROLES: RoleDefinition[] = [
|
|
|
28
29
|
"system-design", "api-design", "database-schema",
|
|
29
30
|
"technology-selection", "code-review-architecture",
|
|
30
31
|
],
|
|
32
|
+
recommendedSkills: ["find-skills"],
|
|
31
33
|
systemPrompt: [
|
|
32
34
|
"You are a Software Architect in a virtual software team.",
|
|
33
35
|
"Your responsibilities include system design, API design, database schema design,",
|
|
@@ -46,6 +48,7 @@ const ROLES: RoleDefinition[] = [
|
|
|
46
48
|
"coding", "debugging", "feature-implementation",
|
|
47
49
|
"code-refactoring", "unit-testing",
|
|
48
50
|
],
|
|
51
|
+
recommendedSkills: ["find-skills"],
|
|
49
52
|
systemPrompt: [
|
|
50
53
|
"You are a Developer in a virtual software team.",
|
|
51
54
|
"Your responsibilities include implementing features, fixing bugs, refactoring code,",
|
|
@@ -64,6 +67,7 @@ const ROLES: RoleDefinition[] = [
|
|
|
64
67
|
"test-planning", "test-case-writing", "bug-reporting",
|
|
65
68
|
"regression-testing", "quality-assurance",
|
|
66
69
|
],
|
|
70
|
+
recommendedSkills: ["find-skills"],
|
|
67
71
|
systemPrompt: [
|
|
68
72
|
"You are a QA Engineer in a virtual software team.",
|
|
69
73
|
"Your responsibilities include test planning, writing test cases, reporting bugs,",
|
|
@@ -82,6 +86,7 @@ const ROLES: RoleDefinition[] = [
|
|
|
82
86
|
"release-management", "deployment", "version-control",
|
|
83
87
|
"ci-cd-pipeline", "release-notes",
|
|
84
88
|
],
|
|
89
|
+
recommendedSkills: ["find-skills"],
|
|
85
90
|
systemPrompt: [
|
|
86
91
|
"You are a Release Engineer in a virtual software team.",
|
|
87
92
|
"Your responsibilities include managing releases, deployment pipelines,",
|
|
@@ -100,6 +105,7 @@ const ROLES: RoleDefinition[] = [
|
|
|
100
105
|
"storage-design", "compute-provisioning", "cost-optimization",
|
|
101
106
|
"disaster-recovery", "capacity-planning", "iac-terraform",
|
|
102
107
|
],
|
|
108
|
+
recommendedSkills: ["find-skills"],
|
|
103
109
|
systemPrompt: [
|
|
104
110
|
"You are an Infrastructure Engineer in a virtual software team.",
|
|
105
111
|
"Your core responsibilities include designing, provisioning, and maintaining cloud infrastructure.",
|
|
@@ -137,7 +143,8 @@ const ROLES: RoleDefinition[] = [
|
|
|
137
143
|
"infrastructure", "ci-cd", "monitoring",
|
|
138
144
|
"docker-kubernetes", "automation",
|
|
139
145
|
],
|
|
140
|
-
|
|
146
|
+
recommendedSkills: ["find-skills"],
|
|
147
|
+
systemPrompt: [
|
|
141
148
|
"You are a DevOps Engineer in a virtual software team.",
|
|
142
149
|
"Your responsibilities include infrastructure management, CI/CD pipelines,",
|
|
143
150
|
"monitoring, and automation.",
|
|
@@ -158,6 +165,7 @@ const ROLES: RoleDefinition[] = [
|
|
|
158
165
|
"code-security-review", "secrets-management", "auth-design",
|
|
159
166
|
"data-protection",
|
|
160
167
|
],
|
|
168
|
+
recommendedSkills: ["find-skills"],
|
|
161
169
|
systemPrompt: [
|
|
162
170
|
"You are a Security Engineer in a virtual software team.",
|
|
163
171
|
"Your core responsibility is ensuring the security posture of all software, infrastructure, and data.",
|
|
@@ -193,6 +201,7 @@ const ROLES: RoleDefinition[] = [
|
|
|
193
201
|
"ui-design", "ux-research", "wireframing",
|
|
194
202
|
"prototyping", "design-systems",
|
|
195
203
|
],
|
|
204
|
+
recommendedSkills: ["ui-ux-pro-max", "find-skills"],
|
|
196
205
|
systemPrompt: [
|
|
197
206
|
"You are a UI/UX Designer in a virtual software team.",
|
|
198
207
|
"Your responsibilities include user interface design, UX research,",
|
|
@@ -210,6 +219,7 @@ const ROLES: RoleDefinition[] = [
|
|
|
210
219
|
"product-marketing", "content-creation",
|
|
211
220
|
"launch-strategy", "user-acquisition", "analytics",
|
|
212
221
|
],
|
|
222
|
+
recommendedSkills: ["find-skills"],
|
|
213
223
|
systemPrompt: [
|
|
214
224
|
"You are a Marketing Specialist in a virtual software team.",
|
|
215
225
|
"Your responsibilities include product marketing, content creation,",
|
|
@@ -235,6 +245,7 @@ const TEAMCLAW_ROLE_IDS_TEXT = [
|
|
|
235
245
|
|
|
236
246
|
for (const role of ROLES) {
|
|
237
247
|
const suggestedRoles = role.suggestedNextRoles.length > 0 ? role.suggestedNextRoles.join(", ") : "none";
|
|
248
|
+
const recommendedSkills = role.recommendedSkills.length > 0 ? role.recommendedSkills.join(", ") : "none";
|
|
238
249
|
role.systemPrompt = [
|
|
239
250
|
role.systemPrompt,
|
|
240
251
|
"",
|
|
@@ -250,11 +261,12 @@ for (const role of ROLES) {
|
|
|
250
261
|
"- Treat file paths from plans, docs, and teammate messages as hints, not facts. Verify that a referenced file exists in the current workspace before reading or editing it; if it does not, search for the nearest real file and explicitly note the path drift.",
|
|
251
262
|
"- Treat other workers' OpenClaw sessions and session keys as unavailable; use the shared workspace, the current task context, and teammate messages instead of trying cross-session inspection.",
|
|
252
263
|
"- Do not mark a task completed or failed via progress updates. Finish by returning the deliverable or raising the blocking error so TeamClaw can close the task correctly.",
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
264
|
+
"- If only a commercial or proprietary option would unblock the task, ask the human for approval before assuming it is allowed.",
|
|
265
|
+
"- If follow-up work is needed, mention it in your result or use handoff/review tools for this current task only.",
|
|
266
|
+
`- Use exact TeamClaw role IDs when collaborating: ${TEAMCLAW_ROLE_IDS_TEXT}.`,
|
|
267
|
+
`- If a true follow-up is required after your deliverable, prefer these exact next roles: ${suggestedRoles}.`,
|
|
268
|
+
`- Default starter skills for this role: ${recommendedSkills}. If the task includes more specific recommended skills, prefer those.`,
|
|
269
|
+
].join("\n");
|
|
258
270
|
}
|
|
259
271
|
|
|
260
272
|
const ROLE_MAP = new Map<string, RoleDefinition>(ROLES.map((r) => [r.id, r]));
|
|
@@ -264,6 +276,29 @@ function getRole(id: RoleId): RoleDefinition | undefined {
|
|
|
264
276
|
return ROLE_MAP.get(id);
|
|
265
277
|
}
|
|
266
278
|
|
|
279
|
+
function normalizeRecommendedSkills(skills: string[] = []): string[] {
|
|
280
|
+
const seen = new Set<string>();
|
|
281
|
+
const normalized: string[] = [];
|
|
282
|
+
for (const entry of skills) {
|
|
283
|
+
const value = String(entry || "").trim();
|
|
284
|
+
if (!value) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const key = value.toLowerCase();
|
|
288
|
+
if (seen.has(key)) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
seen.add(key);
|
|
292
|
+
normalized.push(value);
|
|
293
|
+
}
|
|
294
|
+
return normalized;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function resolveRecommendedSkillsForRole(roleId?: RoleId, taskSkills: string[] = []): string[] {
|
|
298
|
+
const roleSkills = roleId ? (getRole(roleId)?.recommendedSkills ?? []) : [];
|
|
299
|
+
return normalizeRecommendedSkills([...roleSkills, ...taskSkills]);
|
|
300
|
+
}
|
|
301
|
+
|
|
267
302
|
function buildRolePrompt(role: RoleDefinition, teamContext?: string): string {
|
|
268
303
|
const parts = [role.systemPrompt];
|
|
269
304
|
if (teamContext) {
|
|
@@ -272,4 +307,4 @@ function buildRolePrompt(role: RoleDefinition, teamContext?: string): string {
|
|
|
272
307
|
return parts.join("\n");
|
|
273
308
|
}
|
|
274
309
|
|
|
275
|
-
export { ROLES, ROLE_IDS, getRole, buildRolePrompt };
|
|
310
|
+
export { ROLES, ROLE_IDS, getRole, buildRolePrompt, normalizeRecommendedSkills, resolveRecommendedSkillsForRole };
|
package/src/state.ts
CHANGED
|
@@ -40,6 +40,9 @@ async function loadTeamState(teamName: string): Promise<TeamState | null> {
|
|
|
40
40
|
) {
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
|
+
if (!parsed.controllerRuns || typeof parsed.controllerRuns !== "object") {
|
|
44
|
+
parsed.controllerRuns = {};
|
|
45
|
+
}
|
|
43
46
|
if (!Array.isArray(parsed.messages)) {
|
|
44
47
|
parsed.messages = [];
|
|
45
48
|
}
|
|
@@ -71,6 +74,9 @@ async function saveTeamState(state: TeamState): Promise<void> {
|
|
|
71
74
|
state.provisioning.workers = state.provisioning.workers && typeof state.provisioning.workers === "object"
|
|
72
75
|
? state.provisioning.workers
|
|
73
76
|
: {};
|
|
77
|
+
state.controllerRuns = state.controllerRuns && typeof state.controllerRuns === "object"
|
|
78
|
+
? state.controllerRuns
|
|
79
|
+
: {};
|
|
74
80
|
await fs.writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
75
81
|
}
|
|
76
82
|
|
package/src/task-executor.ts
CHANGED
|
@@ -438,6 +438,7 @@ function buildTaskMessage(taskDescription: string, taskId: string, roleLabel: st
|
|
|
438
438
|
"- Do NOT delegate the core work of this task away to another role.",
|
|
439
439
|
"- If Task Context includes recent completed deliverables, treat them as upstream inputs and search the shared workspace for any referenced task IDs or filenames before requesting clarification.",
|
|
440
440
|
"- Do NOT attempt to inspect or resolve another worker's OpenClaw session or session key; those sessions are isolated per worker.",
|
|
441
|
+
"- If the task includes a Recommended Skills section, use those skills first and prefer the exact listed slugs when searching for additional help.",
|
|
441
442
|
"- Do NOT mark the task completed or failed via progress tools. Return the final deliverable (or raise an error) and let TeamClaw close the task.",
|
|
442
443
|
"- If critical information is missing and you cannot proceed safely, request clarification and wait instead of guessing.",
|
|
443
444
|
"- If more work is needed, mention it briefly in your result or use a handoff/review tool on this same task.",
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { TEAMCLAW_PUBLISHED_RUNTIME_IMAGE } from "./install-defaults.js";
|
|
2
|
+
|
|
1
3
|
export type TeamClawMode = "controller" | "worker";
|
|
2
4
|
|
|
3
5
|
export type WorkerStatus = "idle" | "busy" | "offline";
|
|
@@ -70,6 +72,7 @@ export type RoleDefinition = {
|
|
|
70
72
|
icon: string;
|
|
71
73
|
description: string;
|
|
72
74
|
capabilities: string[];
|
|
75
|
+
recommendedSkills: string[];
|
|
73
76
|
systemPrompt: string;
|
|
74
77
|
suggestedNextRoles: RoleId[];
|
|
75
78
|
};
|
|
@@ -96,6 +99,7 @@ export type TaskInfo = {
|
|
|
96
99
|
assignedRole?: RoleId;
|
|
97
100
|
assignedWorkerId?: string;
|
|
98
101
|
createdBy: string;
|
|
102
|
+
recommendedSkills?: string[];
|
|
99
103
|
controllerSessionKey?: string;
|
|
100
104
|
createdAt: number;
|
|
101
105
|
updatedAt: number;
|
|
@@ -125,9 +129,32 @@ export type TaskAssignmentPayload = {
|
|
|
125
129
|
title: string;
|
|
126
130
|
description: string;
|
|
127
131
|
priority?: TaskPriority;
|
|
132
|
+
recommendedSkills?: string[];
|
|
128
133
|
repo?: RepoSyncInfo;
|
|
129
134
|
};
|
|
130
135
|
|
|
136
|
+
export type ControllerRunSource = "human" | "task_follow_up";
|
|
137
|
+
|
|
138
|
+
export type ControllerRunInfo = {
|
|
139
|
+
id: string;
|
|
140
|
+
title: string;
|
|
141
|
+
sessionKey: string;
|
|
142
|
+
runId?: string;
|
|
143
|
+
source: ControllerRunSource;
|
|
144
|
+
sourceTaskId?: string;
|
|
145
|
+
sourceTaskTitle?: string;
|
|
146
|
+
request: string;
|
|
147
|
+
reply?: string;
|
|
148
|
+
error?: string;
|
|
149
|
+
createdTaskIds: string[];
|
|
150
|
+
status: TaskExecutionStatus;
|
|
151
|
+
createdAt: number;
|
|
152
|
+
updatedAt: number;
|
|
153
|
+
startedAt?: number;
|
|
154
|
+
completedAt?: number;
|
|
155
|
+
execution?: TaskExecution;
|
|
156
|
+
};
|
|
157
|
+
|
|
131
158
|
export type TaskExecution = {
|
|
132
159
|
status: TaskExecutionStatus;
|
|
133
160
|
runId?: string;
|
|
@@ -239,9 +266,12 @@ export type PluginConfig = {
|
|
|
239
266
|
workerProvisioningExtraEnv: Record<string, string>;
|
|
240
267
|
workerProvisioningDockerNetwork: string;
|
|
241
268
|
workerProvisioningDockerMounts: string[];
|
|
269
|
+
workerProvisioningWorkspaceRoot: string;
|
|
270
|
+
workerProvisioningDockerWorkspaceVolume: string;
|
|
242
271
|
workerProvisioningKubernetesNamespace: string;
|
|
243
272
|
workerProvisioningKubernetesContext: string;
|
|
244
273
|
workerProvisioningKubernetesServiceAccount: string;
|
|
274
|
+
workerProvisioningKubernetesWorkspacePersistentVolumeClaim: string;
|
|
245
275
|
workerProvisioningKubernetesLabels: Record<string, string>;
|
|
246
276
|
workerProvisioningKubernetesAnnotations: Record<string, string>;
|
|
247
277
|
};
|
|
@@ -257,6 +287,7 @@ export type TeamState = {
|
|
|
257
287
|
teamName: string;
|
|
258
288
|
workers: Record<string, WorkerInfo>;
|
|
259
289
|
tasks: Record<string, TaskInfo>;
|
|
290
|
+
controllerRuns: Record<string, ControllerRunInfo>;
|
|
260
291
|
messages: TeamMessage[];
|
|
261
292
|
clarifications: Record<string, ClarificationRequest>;
|
|
262
293
|
repo?: GitRepoState;
|
|
@@ -356,15 +387,25 @@ export function parsePluginConfig(raw: Record<string, unknown> = {}): PluginConf
|
|
|
356
387
|
typeof raw.workerProvisioningStartupTimeoutMs === "number" && raw.workerProvisioningStartupTimeoutMs >= 1000
|
|
357
388
|
? raw.workerProvisioningStartupTimeoutMs
|
|
358
389
|
: 120_000;
|
|
359
|
-
const
|
|
390
|
+
const rawWorkerProvisioningImage = typeof raw.workerProvisioningImage === "string"
|
|
360
391
|
? raw.workerProvisioningImage.trim()
|
|
361
392
|
: "";
|
|
393
|
+
const workerProvisioningImage = rawWorkerProvisioningImage ||
|
|
394
|
+
(workerProvisioningType === "docker" || workerProvisioningType === "kubernetes"
|
|
395
|
+
? TEAMCLAW_PUBLISHED_RUNTIME_IMAGE
|
|
396
|
+
: "");
|
|
362
397
|
const workerProvisioningPassEnv = parseStringArray(raw.workerProvisioningPassEnv);
|
|
363
398
|
const workerProvisioningExtraEnv = parseStringRecord(raw.workerProvisioningExtraEnv);
|
|
364
399
|
const workerProvisioningDockerNetwork = typeof raw.workerProvisioningDockerNetwork === "string"
|
|
365
400
|
? raw.workerProvisioningDockerNetwork.trim()
|
|
366
401
|
: "";
|
|
367
402
|
const workerProvisioningDockerMounts = parseStringArray(raw.workerProvisioningDockerMounts);
|
|
403
|
+
const rawWorkerProvisioningWorkspaceRoot = typeof raw.workerProvisioningWorkspaceRoot === "string"
|
|
404
|
+
? raw.workerProvisioningWorkspaceRoot.trim()
|
|
405
|
+
: "";
|
|
406
|
+
const workerProvisioningDockerWorkspaceVolume = typeof raw.workerProvisioningDockerWorkspaceVolume === "string"
|
|
407
|
+
? raw.workerProvisioningDockerWorkspaceVolume.trim()
|
|
408
|
+
: "";
|
|
368
409
|
const workerProvisioningKubernetesNamespace = typeof raw.workerProvisioningKubernetesNamespace === "string" &&
|
|
369
410
|
raw.workerProvisioningKubernetesNamespace.trim()
|
|
370
411
|
? raw.workerProvisioningKubernetesNamespace.trim()
|
|
@@ -375,6 +416,14 @@ export function parsePluginConfig(raw: Record<string, unknown> = {}): PluginConf
|
|
|
375
416
|
const workerProvisioningKubernetesServiceAccount = typeof raw.workerProvisioningKubernetesServiceAccount === "string"
|
|
376
417
|
? raw.workerProvisioningKubernetesServiceAccount.trim()
|
|
377
418
|
: "";
|
|
419
|
+
const workerProvisioningKubernetesWorkspacePersistentVolumeClaim =
|
|
420
|
+
typeof raw.workerProvisioningKubernetesWorkspacePersistentVolumeClaim === "string"
|
|
421
|
+
? raw.workerProvisioningKubernetesWorkspacePersistentVolumeClaim.trim()
|
|
422
|
+
: "";
|
|
423
|
+
const workerProvisioningWorkspaceRoot = rawWorkerProvisioningWorkspaceRoot ||
|
|
424
|
+
(workerProvisioningDockerWorkspaceVolume || workerProvisioningKubernetesWorkspacePersistentVolumeClaim
|
|
425
|
+
? "/workspace-root"
|
|
426
|
+
: "");
|
|
378
427
|
const workerProvisioningKubernetesLabels = parseStringRecord(raw.workerProvisioningKubernetesLabels);
|
|
379
428
|
const workerProvisioningKubernetesAnnotations = parseStringRecord(raw.workerProvisioningKubernetesAnnotations);
|
|
380
429
|
|
|
@@ -404,9 +453,12 @@ export function parsePluginConfig(raw: Record<string, unknown> = {}): PluginConf
|
|
|
404
453
|
workerProvisioningExtraEnv,
|
|
405
454
|
workerProvisioningDockerNetwork,
|
|
406
455
|
workerProvisioningDockerMounts,
|
|
456
|
+
workerProvisioningWorkspaceRoot,
|
|
457
|
+
workerProvisioningDockerWorkspaceVolume,
|
|
407
458
|
workerProvisioningKubernetesNamespace,
|
|
408
459
|
workerProvisioningKubernetesContext,
|
|
409
460
|
workerProvisioningKubernetesServiceAccount,
|
|
461
|
+
workerProvisioningKubernetesWorkspacePersistentVolumeClaim,
|
|
410
462
|
workerProvisioningKubernetesLabels,
|
|
411
463
|
workerProvisioningKubernetesAnnotations,
|
|
412
464
|
};
|