@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.
@@ -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 { resolveDefaultOpenClawConfigPath } from "../openclaw-workspace.js";
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
- this.baseDirPromise = fs.mkdtemp(path.join(os.tmpdir(), "teamclaw-provisioned-"));
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.workerProvisioningDockerMounts,
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 fs from "node:fs/promises";
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
- systemPrompt: [
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
- "- If only a commercial or proprietary option would unblock the task, ask the human for approval before assuming it is allowed.",
254
- "- If follow-up work is needed, mention it in your result or use handoff/review tools for this current task only.",
255
- `- Use exact TeamClaw role IDs when collaborating: ${TEAMCLAW_ROLE_IDS_TEXT}.`,
256
- `- If a true follow-up is required after your deliverable, prefer these exact next roles: ${suggestedRoles}.`,
257
- ].join("\n");
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
 
@@ -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 workerProvisioningImage = typeof raw.workerProvisioningImage === "string"
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
  };