@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/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OpenClawPluginApi } from "./api.js";
1
+ import { definePluginEntry, type OpenClawPluginApi } from "./api.js";
2
2
  import { parsePluginConfig } from "./src/types.js";
3
3
  import type { TaskExecutionEventInput, WorkerIdentity } from "./src/types.js";
4
4
  import { buildConfigSchema } from "./src/config.js";
@@ -13,16 +13,16 @@ import { LocalWorkerManager } from "./src/controller/local-worker-manager.js";
13
13
  import { createControllerPromptInjector } from "./src/controller/prompt-injector.js";
14
14
  import { createControllerTools } from "./src/controller/controller-tools.js";
15
15
  import { publishWorkerRepo, syncWorkerRepo } from "./src/git-collaboration.js";
16
+ import { installRecommendedSkills } from "./src/worker/skill-installer.js";
16
17
 
17
- const plugin = {
18
+ export default definePluginEntry({
18
19
  id: "teamclaw",
19
20
  name: "TeamClaw",
20
21
  description:
21
22
  "Virtual team collaboration - multiple OpenClaw instances form a virtual software company with role-based task routing.",
22
- configSchema: buildConfigSchema(),
23
+ configSchema: buildConfigSchema,
23
24
  register(api: OpenClawPluginApi) {
24
25
  const config = parsePluginConfig(api.pluginConfig as Record<string, unknown>);
25
- const logger = api.logger;
26
26
 
27
27
  if (config.mode === "controller") {
28
28
  registerController(api, config);
@@ -30,9 +30,7 @@ const plugin = {
30
30
  registerWorker(api, config);
31
31
  }
32
32
  },
33
- };
34
-
35
- export default plugin;
33
+ });
36
34
 
37
35
  function registerController(api: OpenClawPluginApi, config: ReturnType<typeof parsePluginConfig>) {
38
36
  const logger = api.logger;
@@ -146,6 +144,25 @@ function registerWorker(api: OpenClawPluginApi, config: ReturnType<typeof parseP
146
144
  currentWorkerId = identity.workerId;
147
145
  },
148
146
  prepareTaskAssignment: async (assignment) => {
147
+ if (assignment.recommendedSkills?.length) {
148
+ try {
149
+ const skillInstall = await installRecommendedSkills(assignment, logger);
150
+ for (const event of skillInstall.events) {
151
+ await reportExecutionEvent(assignment.taskId, event);
152
+ }
153
+ } catch (err) {
154
+ const message = err instanceof Error ? err.message : String(err);
155
+ await reportExecutionEvent(assignment.taskId, {
156
+ type: "error",
157
+ phase: "skills_preflight_failed",
158
+ source: "worker",
159
+ status: "running",
160
+ message,
161
+ });
162
+ logger.warn(`Worker: skill preflight failed for ${assignment.taskId}: ${message}`);
163
+ }
164
+ }
165
+
149
166
  if (!assignment.repo?.enabled || !currentControllerUrl) {
150
167
  return;
151
168
  }
@@ -1,13 +1,151 @@
1
1
  {
2
2
  "id": "teamclaw",
3
3
  "name": "TeamClaw",
4
- "description": "Virtual team collaboration plugin - multiple OpenClaw instances form a virtual software company with role-based task routing.",
4
+ "description": "Virtual team collaboration - multiple OpenClaw instances form a virtual software company with role-based task routing.",
5
+ "version": "2026.3.25",
6
+ "uiHints": {
7
+ "mode": {
8
+ "label": "Mode",
9
+ "help": "controller manages the team, worker executes tasks"
10
+ },
11
+ "port": {
12
+ "label": "Port",
13
+ "help": "HTTP server port for this instance"
14
+ },
15
+ "role": {
16
+ "label": "Role",
17
+ "help": "Worker role (worker mode only)"
18
+ },
19
+ "controllerUrl": {
20
+ "label": "Controller URL",
21
+ "help": "Manual fallback if mDNS discovery fails"
22
+ },
23
+ "teamName": {
24
+ "label": "Team Name",
25
+ "help": "Team identifier for mDNS"
26
+ },
27
+ "heartbeatIntervalMs": {
28
+ "label": "Heartbeat Interval",
29
+ "help": "In milliseconds, minimum 1000"
30
+ },
31
+ "taskTimeoutMs": {
32
+ "label": "Task Timeout",
33
+ "help": "Maximum time to wait for a role task to finish before marking it failed (in milliseconds)"
34
+ },
35
+ "gitEnabled": {
36
+ "label": "Git Collaboration",
37
+ "help": "Enable automatic git-backed workspace bootstrapping and worker repo sync"
38
+ },
39
+ "gitRemoteUrl": {
40
+ "label": "Git Remote URL",
41
+ "help": "Optional remote repository URL; when empty, distributed workers use controller-hosted git bundles"
42
+ },
43
+ "gitDefaultBranch": {
44
+ "label": "Git Default Branch",
45
+ "help": "Default branch name for the TeamClaw workspace repository"
46
+ },
47
+ "gitAuthorName": {
48
+ "label": "Git Author Name",
49
+ "help": "Author name for TeamClaw-managed workspace commits"
50
+ },
51
+ "gitAuthorEmail": {
52
+ "label": "Git Author Email",
53
+ "help": "Author email for TeamClaw-managed workspace commits"
54
+ },
55
+ "localRoles": {
56
+ "label": "Local Roles",
57
+ "help": "Controller mode only: run these roles as local virtual workers inside the same OpenClaw instance"
58
+ },
59
+ "workerProvisioningType": {
60
+ "label": "On-demand Worker Provider",
61
+ "help": "Launch missing workers on demand using process, Docker, or Kubernetes"
62
+ },
63
+ "workerProvisioningControllerUrl": {
64
+ "label": "Provisioned Worker Controller URL",
65
+ "help": "URL that launched workers use to call back into the controller"
66
+ },
67
+ "workerProvisioningRoles": {
68
+ "label": "Provisioned Roles",
69
+ "help": "Only launch these roles on demand; leave empty for all roles"
70
+ },
71
+ "workerProvisioningMinPerRole": {
72
+ "label": "Warm Workers Per Role",
73
+ "help": "Minimum idle workers to keep warm per role"
74
+ },
75
+ "workerProvisioningMaxPerRole": {
76
+ "label": "Max Workers Per Role",
77
+ "help": "Maximum concurrent on-demand workers per role"
78
+ },
79
+ "workerProvisioningIdleTtlMs": {
80
+ "label": "Idle TTL",
81
+ "help": "Terminate an idle provisioned worker after this many milliseconds"
82
+ },
83
+ "workerProvisioningStartupTimeoutMs": {
84
+ "label": "Startup Timeout",
85
+ "help": "Fail a launch if the worker does not register in time"
86
+ },
87
+ "workerProvisioningImage": {
88
+ "label": "Provisioning Image",
89
+ "help": "Container image for docker/kubernetes provisioners"
90
+ },
91
+ "workerProvisioningPassEnv": {
92
+ "label": "Pass-through Env",
93
+ "help": "Environment variable names copied from controller to provisioned workers"
94
+ },
95
+ "workerProvisioningExtraEnv": {
96
+ "label": "Extra Env",
97
+ "help": "Static environment variables injected into provisioned workers"
98
+ },
99
+ "workerProvisioningDockerNetwork": {
100
+ "label": "Docker Network",
101
+ "help": "Optional Docker network for launched worker containers"
102
+ },
103
+ "workerProvisioningDockerMounts": {
104
+ "label": "Docker Mounts",
105
+ "help": "Optional Docker bind mounts for launched worker containers"
106
+ },
107
+ "workerProvisioningWorkspaceRoot": {
108
+ "label": "Workspace Root",
109
+ "help": "Optional persistent workspace root path inside docker/kubernetes workers; defaults to /workspace-root when persistence is configured"
110
+ },
111
+ "workerProvisioningDockerWorkspaceVolume": {
112
+ "label": "Docker Workspace Volume",
113
+ "help": "Optional Docker named volume or host path mounted as the persistent workspace root"
114
+ },
115
+ "workerProvisioningKubernetesNamespace": {
116
+ "label": "Kubernetes Namespace",
117
+ "help": "Namespace for launched worker pods"
118
+ },
119
+ "workerProvisioningKubernetesContext": {
120
+ "label": "Kubernetes Context",
121
+ "help": "Optional kubectl context for the Kubernetes provider"
122
+ },
123
+ "workerProvisioningKubernetesServiceAccount": {
124
+ "label": "Kubernetes Service Account",
125
+ "help": "Optional service account for launched worker pods"
126
+ },
127
+ "workerProvisioningKubernetesWorkspacePersistentVolumeClaim": {
128
+ "label": "Kubernetes Workspace PVC",
129
+ "help": "Optional PVC mounted as the persistent workspace root for launched worker pods"
130
+ },
131
+ "workerProvisioningKubernetesLabels": {
132
+ "label": "Kubernetes Labels",
133
+ "help": "Extra labels applied to launched worker pods"
134
+ },
135
+ "workerProvisioningKubernetesAnnotations": {
136
+ "label": "Kubernetes Annotations",
137
+ "help": "Extra annotations applied to launched worker pods"
138
+ }
139
+ },
5
140
  "configSchema": {
6
141
  "type": "object",
7
142
  "properties": {
8
143
  "mode": {
9
144
  "type": "string",
10
- "enum": ["controller", "worker"],
145
+ "enum": [
146
+ "controller",
147
+ "worker"
148
+ ],
11
149
  "default": "worker",
12
150
  "description": "Plugin mode: controller manages the team, worker executes tasks"
13
151
  },
@@ -35,6 +173,192 @@
35
173
  "type": "number",
36
174
  "default": 10000,
37
175
  "description": "Heartbeat interval in milliseconds"
176
+ },
177
+ "taskTimeoutMs": {
178
+ "type": "number",
179
+ "default": 1800000,
180
+ "description": "Maximum time in milliseconds to wait for a role task to finish"
181
+ },
182
+ "gitEnabled": {
183
+ "type": "boolean",
184
+ "default": true,
185
+ "description": "Enable TeamClaw git-backed workspace collaboration"
186
+ },
187
+ "gitRemoteUrl": {
188
+ "type": "string",
189
+ "default": "",
190
+ "description": "Optional remote repository URL for distributed worker clone/pull/push"
191
+ },
192
+ "gitDefaultBranch": {
193
+ "type": "string",
194
+ "default": "main",
195
+ "description": "Default branch name for the shared TeamClaw workspace repository"
196
+ },
197
+ "gitAuthorName": {
198
+ "type": "string",
199
+ "default": "TeamClaw",
200
+ "description": "Git author name used for TeamClaw-managed workspace commits"
201
+ },
202
+ "gitAuthorEmail": {
203
+ "type": "string",
204
+ "default": "teamclaw@local",
205
+ "description": "Git author email used for TeamClaw-managed workspace commits"
206
+ },
207
+ "localRoles": {
208
+ "type": "array",
209
+ "default": [],
210
+ "description": "Controller-local roles executed in this same OpenClaw instance",
211
+ "items": {
212
+ "type": "string",
213
+ "enum": [
214
+ "pm",
215
+ "architect",
216
+ "developer",
217
+ "qa",
218
+ "release-engineer",
219
+ "infra-engineer",
220
+ "devops",
221
+ "security-engineer",
222
+ "designer",
223
+ "marketing"
224
+ ]
225
+ }
226
+ },
227
+ "workerProvisioningType": {
228
+ "type": "string",
229
+ "enum": [
230
+ "none",
231
+ "process",
232
+ "docker",
233
+ "kubernetes"
234
+ ],
235
+ "default": "none",
236
+ "description": "Controller-only on-demand worker launch backend"
237
+ },
238
+ "workerProvisioningControllerUrl": {
239
+ "type": "string",
240
+ "default": "",
241
+ "description": "Controller URL injected into provisioned workers; required for docker/kubernetes"
242
+ },
243
+ "workerProvisioningRoles": {
244
+ "type": "array",
245
+ "default": [],
246
+ "description": "Restrict on-demand launches to specific roles; empty means all roles",
247
+ "items": {
248
+ "type": "string",
249
+ "enum": [
250
+ "pm",
251
+ "architect",
252
+ "developer",
253
+ "qa",
254
+ "release-engineer",
255
+ "infra-engineer",
256
+ "devops",
257
+ "security-engineer",
258
+ "designer",
259
+ "marketing"
260
+ ]
261
+ }
262
+ },
263
+ "workerProvisioningMinPerRole": {
264
+ "type": "number",
265
+ "default": 0,
266
+ "description": "Minimum number of ready workers to keep warm per role"
267
+ },
268
+ "workerProvisioningMaxPerRole": {
269
+ "type": "number",
270
+ "default": 1,
271
+ "description": "Maximum on-demand workers to launch per role"
272
+ },
273
+ "workerProvisioningIdleTtlMs": {
274
+ "type": "number",
275
+ "default": 120000,
276
+ "description": "Terminate provisioned idle workers after this many milliseconds"
277
+ },
278
+ "workerProvisioningStartupTimeoutMs": {
279
+ "type": "number",
280
+ "default": 120000,
281
+ "description": "Fail a launch if the worker does not register within this many milliseconds"
282
+ },
283
+ "workerProvisioningImage": {
284
+ "type": "string",
285
+ "default": "ghcr.io/topcheer/teamclaw-openclaw:latest",
286
+ "description": "Container image used by docker/kubernetes provisioners"
287
+ },
288
+ "workerProvisioningPassEnv": {
289
+ "type": "array",
290
+ "default": [],
291
+ "description": "Environment variable names copied from the controller into provisioned workers",
292
+ "items": {
293
+ "type": "string"
294
+ }
295
+ },
296
+ "workerProvisioningExtraEnv": {
297
+ "type": "object",
298
+ "default": {},
299
+ "description": "Extra environment variables injected into provisioned workers",
300
+ "additionalProperties": {
301
+ "type": "string"
302
+ }
303
+ },
304
+ "workerProvisioningDockerNetwork": {
305
+ "type": "string",
306
+ "default": "",
307
+ "description": "Optional Docker network name for launched worker containers"
308
+ },
309
+ "workerProvisioningDockerMounts": {
310
+ "type": "array",
311
+ "default": [],
312
+ "description": "Optional Docker bind mounts for launched worker containers",
313
+ "items": {
314
+ "type": "string"
315
+ }
316
+ },
317
+ "workerProvisioningWorkspaceRoot": {
318
+ "type": "string",
319
+ "default": "",
320
+ "description": "Optional persistent workspace root path inside docker/kubernetes workers; defaults to /workspace-root when a Docker volume or PVC is configured"
321
+ },
322
+ "workerProvisioningDockerWorkspaceVolume": {
323
+ "type": "string",
324
+ "default": "",
325
+ "description": "Optional Docker named volume or host path mounted as the persistent workspace root"
326
+ },
327
+ "workerProvisioningKubernetesNamespace": {
328
+ "type": "string",
329
+ "default": "default",
330
+ "description": "Kubernetes namespace for launched worker pods"
331
+ },
332
+ "workerProvisioningKubernetesContext": {
333
+ "type": "string",
334
+ "default": "",
335
+ "description": "Optional kubectl context used by the Kubernetes provisioner"
336
+ },
337
+ "workerProvisioningKubernetesServiceAccount": {
338
+ "type": "string",
339
+ "default": "",
340
+ "description": "Optional service account name for launched worker pods"
341
+ },
342
+ "workerProvisioningKubernetesWorkspacePersistentVolumeClaim": {
343
+ "type": "string",
344
+ "default": "",
345
+ "description": "Optional PVC mounted as the persistent workspace root for launched worker pods"
346
+ },
347
+ "workerProvisioningKubernetesLabels": {
348
+ "type": "object",
349
+ "default": {},
350
+ "description": "Extra labels applied to launched worker pods",
351
+ "additionalProperties": {
352
+ "type": "string"
353
+ }
354
+ },
355
+ "workerProvisioningKubernetesAnnotations": {
356
+ "type": "object",
357
+ "default": {},
358
+ "description": "Extra annotations applied to launched worker pods",
359
+ "additionalProperties": {
360
+ "type": "string"
361
+ }
38
362
  }
39
363
  }
40
364
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamclaws/teamclaw",
3
- "version": "2026.3.21",
3
+ "version": "2026.3.25",
4
4
  "description": "OpenClaw virtual software team orchestration plugin",
5
5
  "private": false,
6
6
  "keywords": [
@@ -20,9 +20,13 @@
20
20
  "directory": "src"
21
21
  },
22
22
  "type": "module",
23
+ "bin": {
24
+ "teamclaw": "./cli.mjs"
25
+ },
23
26
  "files": [
24
27
  "README.md",
25
28
  "api.ts",
29
+ "cli.mjs",
26
30
  "index.ts",
27
31
  "openclaw.plugin.json",
28
32
  "src/",
@@ -36,19 +40,12 @@
36
40
  "@sinclair/typebox": "0.34.48",
37
41
  "bonjour-service": "^1.3.0",
38
42
  "json5": "^2.2.3",
43
+ "openclaw": "2026.3.23-2",
39
44
  "ws": "^8.19.0"
40
45
  },
41
46
  "devDependencies": {
42
47
  "typescript": "^5.9.0"
43
48
  },
44
- "peerDependencies": {
45
- "openclaw": ">=2026.3.14"
46
- },
47
- "peerDependenciesMeta": {
48
- "openclaw": {
49
- "optional": true
50
- }
51
- },
52
49
  "publishConfig": {
53
50
  "access": "public"
54
51
  },
package/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { PluginConfig } from "./types.js";
2
2
  import { parsePluginConfig } from "./types.js";
3
3
  import { ROLE_IDS } from "./roles.js";
4
+ import { TEAMCLAW_PUBLISHED_RUNTIME_IMAGE } from "./install-defaults.js";
4
5
 
5
6
  function buildConfigSchema() {
6
7
  return {
@@ -119,7 +120,7 @@ function buildConfigSchema() {
119
120
  },
120
121
  workerProvisioningImage: {
121
122
  type: "string" as const,
122
- default: "",
123
+ default: TEAMCLAW_PUBLISHED_RUNTIME_IMAGE,
123
124
  description: "Container image used by docker/kubernetes provisioners",
124
125
  },
125
126
  workerProvisioningPassEnv: {
@@ -151,6 +152,16 @@ function buildConfigSchema() {
151
152
  type: "string" as const,
152
153
  },
153
154
  },
155
+ workerProvisioningWorkspaceRoot: {
156
+ type: "string" as const,
157
+ default: "",
158
+ description: "Optional persistent workspace root path inside docker/kubernetes workers; defaults to /workspace-root when a Docker volume or PVC is configured",
159
+ },
160
+ workerProvisioningDockerWorkspaceVolume: {
161
+ type: "string" as const,
162
+ default: "",
163
+ description: "Optional Docker named volume or host path mounted as the persistent workspace root",
164
+ },
154
165
  workerProvisioningKubernetesNamespace: {
155
166
  type: "string" as const,
156
167
  default: "default",
@@ -166,6 +177,11 @@ function buildConfigSchema() {
166
177
  default: "",
167
178
  description: "Optional service account name for launched worker pods",
168
179
  },
180
+ workerProvisioningKubernetesWorkspacePersistentVolumeClaim: {
181
+ type: "string" as const,
182
+ default: "",
183
+ description: "Optional PVC mounted as the persistent workspace root for launched worker pods",
184
+ },
169
185
  workerProvisioningKubernetesLabels: {
170
186
  type: "object" as const,
171
187
  default: {},
@@ -267,6 +283,14 @@ function buildConfigSchema() {
267
283
  label: "Docker Mounts",
268
284
  help: "Optional Docker bind mounts for launched worker containers",
269
285
  },
286
+ workerProvisioningWorkspaceRoot: {
287
+ label: "Workspace Root",
288
+ help: "Optional persistent workspace root path inside docker/kubernetes workers; defaults to /workspace-root when persistence is configured",
289
+ },
290
+ workerProvisioningDockerWorkspaceVolume: {
291
+ label: "Docker Workspace Volume",
292
+ help: "Optional Docker named volume or host path mounted as the persistent workspace root",
293
+ },
270
294
  workerProvisioningKubernetesNamespace: {
271
295
  label: "Kubernetes Namespace",
272
296
  help: "Namespace for launched worker pods",
@@ -279,6 +303,10 @@ function buildConfigSchema() {
279
303
  label: "Kubernetes Service Account",
280
304
  help: "Optional service account for launched worker pods",
281
305
  },
306
+ workerProvisioningKubernetesWorkspacePersistentVolumeClaim: {
307
+ label: "Kubernetes Workspace PVC",
308
+ help: "Optional PVC mounted as the persistent workspace root for launched worker pods",
309
+ },
282
310
  workerProvisioningKubernetesLabels: {
283
311
  label: "Kubernetes Labels",
284
312
  help: "Extra labels applied to launched worker pods",
@@ -46,6 +46,7 @@ export function createControllerService(deps: ControllerServiceDeps): OpenClawPl
46
46
  teamName: config.teamName,
47
47
  workers: {},
48
48
  tasks: {},
49
+ controllerRuns: {},
49
50
  messages: [],
50
51
  clarifications: {},
51
52
  repo: repoState ?? undefined,
@@ -31,6 +31,13 @@ export function createControllerTools(deps: ControllerToolsDeps) {
31
31
  description: Type.String({ description: "Execution-ready task description with scope, expected deliverable, constraints, resolved clarifications, and no unmet predecessor dependency" }),
32
32
  priority: Type.Optional(Type.String({ description: "Priority: low, medium, high, critical" })),
33
33
  assignedRole: Type.Optional(Type.String({ description: "Exact target role ID (pm, architect, developer, qa, release-engineer, infra-engineer, devops, security-engineer, designer, marketing)" })),
34
+ recommendedSkills: Type.Optional(
35
+ Type.Array(
36
+ Type.String({
37
+ description: "Exact OpenClaw/ClawHub skill slug when known; otherwise a short skill-discovery query",
38
+ }),
39
+ ),
40
+ ),
34
41
  }),
35
42
  async execute(_id: string, params: Record<string, unknown>) {
36
43
  const title = String(params.title ?? "");
@@ -58,6 +65,7 @@ export function createControllerTools(deps: ControllerToolsDeps) {
58
65
  description,
59
66
  priority: params.priority ?? "medium",
60
67
  assignedRole: params.assignedRole ?? undefined,
68
+ recommendedSkills: Array.isArray(params.recommendedSkills) ? params.recommendedSkills : undefined,
61
69
  createdBy: "controller",
62
70
  }),
63
71
  });
@@ -74,11 +82,14 @@ export function createControllerTools(deps: ControllerToolsDeps) {
74
82
  : task.status === "pending"
75
83
  ? " (pending - no available worker)"
76
84
  : "";
85
+ const recommended = Array.isArray(task.recommendedSkills) && task.recommendedSkills.length > 0
86
+ ? ` | skills: ${task.recommendedSkills.join(", ")}`
87
+ : "";
77
88
 
78
89
  return {
79
90
  content: [{
80
91
  type: "text" as const,
81
- text: `Task created: ${task.title} [${task.id}] [${task.priority}]${assigned}`,
92
+ text: `Task created: ${task.title} [${task.id}] [${task.priority}]${assigned}${recommended}`,
82
93
  }],
83
94
  };
84
95
  } catch (err) {