@teamclaws/teamclaw 2026.3.24-6 → 2026.3.24-7

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/cli.mjs CHANGED
@@ -26,7 +26,7 @@ const DEFAULT_TEAM_NAME = "default";
26
26
  const DEFAULT_TASK_TIMEOUT_MS = 1_800_000;
27
27
  const DEFAULT_AGENT_TIMEOUT_SECONDS = 2_400;
28
28
  const DEFAULT_LOCAL_ROLES = ["architect", "developer", "qa"];
29
- const DEFAULT_PROVISIONING_ROLES = ["architect", "developer", "qa"];
29
+ const LEGACY_DEFAULT_PROVISIONING_ROLES = ["architect", "developer", "qa"];
30
30
 
31
31
  const ROLE_OPTIONS = [
32
32
  { value: "pm", label: "Product Manager" },
@@ -347,6 +347,27 @@ function dedupeStrings(values) {
347
347
  return Array.from(new Set(values.filter((value) => typeof value === "string" && value.trim()).map((value) => value.trim())));
348
348
  }
349
349
 
350
+ function hasSameStringSet(left, right) {
351
+ const normalizedLeft = dedupeStrings(left).slice().sort();
352
+ const normalizedRight = dedupeStrings(right).slice().sort();
353
+ if (normalizedLeft.length !== normalizedRight.length) {
354
+ return false;
355
+ }
356
+ return normalizedLeft.every((value, index) => value === normalizedRight[index]);
357
+ }
358
+
359
+ function normalizeConfiguredRoleList(raw) {
360
+ return Array.isArray(raw) ? dedupeStrings(raw) : [];
361
+ }
362
+
363
+ function resolveDefaultProvisioningRoles(existingTeamClaw) {
364
+ const existingRoles = normalizeConfiguredRoleList(existingTeamClaw.workerProvisioningRoles);
365
+ if (existingRoles.length === 0) {
366
+ return [];
367
+ }
368
+ return hasSameStringSet(existingRoles, LEGACY_DEFAULT_PROVISIONING_ROLES) ? [] : existingRoles;
369
+ }
370
+
350
371
  function extractModelOptions(config) {
351
372
  const currentModel = getCurrentModel(config);
352
373
  const models = [];
@@ -553,6 +574,28 @@ async function promptRoleList(prompter, message, defaultRoles) {
553
574
  return parseRoleList(raw).values;
554
575
  }
555
576
 
577
+ async function promptOptionalRoleList(prompter, message, defaultRoles) {
578
+ const defaultValue = defaultRoles.join(",");
579
+ if (!prompter.yes) {
580
+ console.log(
581
+ `Available roles: ${ROLE_OPTIONS.map((option) => `${option.value} (${option.label})`).join(", ")}. Leave empty to allow all roles.`,
582
+ );
583
+ }
584
+ const raw = await prompter.text({
585
+ message,
586
+ defaultValue,
587
+ allowEmpty: true,
588
+ validate: (value) => {
589
+ const parsed = parseRoleList(value);
590
+ if (parsed.invalid.length > 0) {
591
+ return `Unknown role ids: ${parsed.invalid.join(", ")}`;
592
+ }
593
+ return "";
594
+ },
595
+ });
596
+ return parseRoleList(raw).values;
597
+ }
598
+
556
599
  function buildStartCommand(configPath) {
557
600
  const defaultPath = resolveDefaultOpenClawConfigPath();
558
601
  if (path.resolve(configPath) === path.resolve(defaultPath)) {
@@ -572,6 +615,16 @@ function isControllerInstallMode(installMode) {
572
615
  return installMode !== "worker";
573
616
  }
574
617
 
618
+ function isOnDemandControllerInstallMode(installMode) {
619
+ return installMode === "controller-process" || installMode === "controller-docker" || installMode === "controller-kubernetes";
620
+ }
621
+
622
+ function describeProvisioningRoles(roles) {
623
+ return Array.isArray(roles) && roles.length > 0
624
+ ? roles.join(", ")
625
+ : "all TeamClaw roles (controller decides at runtime)";
626
+ }
627
+
575
628
  function getLocalUiUrl(port) {
576
629
  return `http://127.0.0.1:${port}/ui`;
577
630
  }
@@ -1097,12 +1150,10 @@ async function collectInstallChoices(configPath, config, prompter) {
1097
1150
  };
1098
1151
  }
1099
1152
 
1100
- const provisioningRoles = await promptRoleList(
1153
+ const provisioningRoles = await promptOptionalRoleList(
1101
1154
  prompter,
1102
- "On-demand roles to launch (comma-separated)",
1103
- Array.isArray(existingTeamClaw.workerProvisioningRoles) && existingTeamClaw.workerProvisioningRoles.length > 0
1104
- ? existingTeamClaw.workerProvisioningRoles
1105
- : DEFAULT_PROVISIONING_ROLES,
1155
+ "On-demand roles to allow (comma-separated, leave empty for all roles)",
1156
+ resolveDefaultProvisioningRoles(existingTeamClaw),
1106
1157
  );
1107
1158
  const maxPerRole = await prompter.number({
1108
1159
  message: "Maximum on-demand workers per role",
@@ -1469,6 +1520,9 @@ function buildSummaryLines(params) {
1469
1520
  if (params.choices.installMode === "controller-docker" || params.choices.installMode === "controller-kubernetes") {
1470
1521
  lines.push(`Provisioning image: ${params.choices.workerImage}`);
1471
1522
  }
1523
+ if (isOnDemandControllerInstallMode(params.choices.installMode)) {
1524
+ lines.push(`On-demand roles: ${describeProvisioningRoles(params.choices.provisioningRoles)}`);
1525
+ }
1472
1526
  if (params.choices.installMode === "controller-docker" && params.choices.dockerWorkspaceVolume) {
1473
1527
  lines.push(`Docker workspace volume: ${params.choices.dockerWorkspaceVolume}`);
1474
1528
  }
@@ -2,7 +2,7 @@
2
2
  "id": "teamclaw",
3
3
  "name": "TeamClaw",
4
4
  "description": "Virtual team collaboration - multiple OpenClaw instances form a virtual software company with role-based task routing.",
5
- "version": "2026.3.24-6",
5
+ "version": "2026.3.24-7",
6
6
  "uiHints": {
7
7
  "mode": {
8
8
  "label": "Mode",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamclaws/teamclaw",
3
- "version": "2026.3.24-6",
3
+ "version": "2026.3.24-7",
4
4
  "description": "OpenClaw virtual software team orchestration plugin",
5
5
  "private": false,
6
6
  "keywords": [
@@ -0,0 +1,23 @@
1
+ import type { PluginConfig, TeamState } from "../types.js";
2
+
3
+ export function hasOnDemandWorkerProvisioning(
4
+ config: Pick<PluginConfig, "workerProvisioningType">,
5
+ ): boolean {
6
+ return config.workerProvisioningType !== "none";
7
+ }
8
+
9
+ export function shouldBlockControllerWithoutWorkers(
10
+ config: Pick<PluginConfig, "workerProvisioningType">,
11
+ state: TeamState | null,
12
+ ): boolean {
13
+ return !!state && Object.keys(state.workers).length === 0 && !hasOnDemandWorkerProvisioning(config);
14
+ }
15
+
16
+ export function buildControllerNoWorkersMessage(): string {
17
+ return [
18
+ "No TeamClaw workers are registered and on-demand provisioning is disabled.",
19
+ "You may analyze the requirement and identify the roles that would be needed,",
20
+ "but do not create TeamClaw tasks and do not do the worker-role work yourself.",
21
+ "Ask the human to bring workers online or enable process/docker/kubernetes provisioning first.",
22
+ ].join(" ");
23
+ }
@@ -1,5 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import type { PluginConfig, TaskInfo, TeamState } from "../types.js";
3
+ import { buildControllerNoWorkersMessage, hasOnDemandWorkerProvisioning, shouldBlockControllerWithoutWorkers } from "./controller-capacity.js";
3
4
 
4
5
  export type ControllerToolsDeps = {
5
6
  config: PluginConfig;
@@ -46,6 +47,16 @@ export function createControllerTools(deps: ControllerToolsDeps) {
46
47
  return { content: [{ type: "text" as const, text: "title is required." }] };
47
48
  }
48
49
 
50
+ const state = getTeamState();
51
+ if (shouldBlockControllerWithoutWorkers(config, state)) {
52
+ return {
53
+ content: [{
54
+ type: "text" as const,
55
+ text: `${buildControllerNoWorkersMessage()} Stop after reporting this block to the human.`,
56
+ }],
57
+ };
58
+ }
59
+
49
60
  const blocker = detectExecutionReadyBlocker(description);
50
61
  if (blocker) {
51
62
  return {
@@ -80,7 +91,9 @@ export function createControllerTools(deps: ControllerToolsDeps) {
80
91
  const assigned = task.assignedWorkerId
81
92
  ? ` -> assigned to ${task.assignedWorkerId}`
82
93
  : task.status === "pending"
83
- ? " (pending - no available worker)"
94
+ ? hasOnDemandWorkerProvisioning(config)
95
+ ? " (pending - waiting for worker provisioning or an available worker)"
96
+ : " (pending - no registered/available worker)"
84
97
  : "";
85
98
  const recommended = Array.isArray(task.recommendedSkills) && task.recommendedSkills.length > 0
86
99
  ? ` | skills: ${task.recommendedSkills.join(", ")}`
@@ -39,6 +39,7 @@ import { MessageRouter } from "./message-router.js";
39
39
  import { TeamWebSocketServer } from "./websocket.js";
40
40
  import type { WorkerProvisioningManager } from "./worker-provisioning.js";
41
41
  import { createControllerPromptInjector } from "./prompt-injector.js";
42
+ import { buildControllerNoWorkersMessage, shouldBlockControllerWithoutWorkers } from "./controller-capacity.js";
42
43
 
43
44
  export type ControllerHttpDeps = {
44
45
  config: PluginConfig;
@@ -1475,6 +1476,10 @@ async function handleRequest(
1475
1476
  sendError(res, 400, "title is required");
1476
1477
  return;
1477
1478
  }
1479
+ if (createdBy === "controller" && shouldBlockControllerWithoutWorkers(deps.config, getTeamState())) {
1480
+ sendError(res, 409, buildControllerNoWorkersMessage());
1481
+ return;
1482
+ }
1478
1483
 
1479
1484
  const taskId = generateId();
1480
1485
  const now = Date.now();
@@ -1,5 +1,6 @@
1
1
  import type { PluginConfig, TeamState } from "../types.js";
2
2
  import { ROLES } from "../roles.js";
3
+ import { hasOnDemandWorkerProvisioning, shouldBlockControllerWithoutWorkers } from "./controller-capacity.js";
3
4
 
4
5
  const TEAMCLAW_ROLE_IDS_TEXT = [
5
6
  "pm",
@@ -47,7 +48,14 @@ export function createControllerPromptInjector(deps: ControllerPromptDeps) {
47
48
  if (!state) {
48
49
  parts.push("- Team state is not loaded yet; treat this as a fresh controller intake and establish execution-ready tasks from the human requirement.");
49
50
  } else if (workers.length === 0) {
50
- parts.push("- No workers registered yet");
51
+ if (shouldBlockControllerWithoutWorkers(deps.config, state)) {
52
+ parts.push("- No workers are registered and on-demand provisioning is disabled.");
53
+ parts.push("- Blocking rule: you may analyze the requirement and identify the needed roles, but do not create TeamClaw tasks yet.");
54
+ parts.push("- Do not start doing the worker-role work yourself. Tell the human to bring workers online or enable process/docker/kubernetes provisioning first.");
55
+ } else {
56
+ parts.push("- No workers are registered yet, but on-demand provisioning is enabled.");
57
+ parts.push("- You may still create execution-ready TeamClaw tasks for the required roles; the controller will provision workers on demand.");
58
+ }
51
59
  } else {
52
60
  for (const w of workers) {
53
61
  const roleDef = ROLES.find((r) => r.id === w.role);
@@ -86,6 +94,13 @@ export function createControllerPromptInjector(deps: ControllerPromptDeps) {
86
94
  parts.push(`- ${role.icon} ${role.label}: ${role.description}.${skillLine}`);
87
95
  }
88
96
 
97
+ parts.push("");
98
+ parts.push("## Controller Workflow");
99
+ parts.push("- First determine which TeamClaw roles are needed for the human requirement.");
100
+ parts.push("- Then translate the requirement into the minimum execution-ready TeamClaw tasks owned by those roles.");
101
+ parts.push("- TeamClaw workers, not the controller, do the specialist work in the shared repo/workspace.");
102
+ parts.push("- After workers report progress, results, or handoffs, create only the next tasks whose prerequisites are now satisfied.");
103
+
89
104
  parts.push("");
90
105
  parts.push("## Requirement Intake Rules");
91
106
  parts.push("- Human messages are the initial requirement, not an already-decomposed task tree.");
@@ -111,6 +126,13 @@ export function createControllerPromptInjector(deps: ControllerPromptDeps) {
111
126
  parts.push("- Do not let a worker task turn itself into a controller/coordinator workflow.");
112
127
  parts.push("- If the correct role is busy, prefer waiting, messaging, or explicit reassignment over routing core work to an unrelated role.");
113
128
  parts.push("- If a task is blocked by missing information, keep it in the clarification queue until the human answers; do not guess on the user's behalf.");
129
+ parts.push("- You are never a substitute worker. Do not personally perform architecture, implementation, QA, release, infra, design, marketing, research, or other specialist work.");
130
+ parts.push("- Your own reply must stay at the orchestration layer: clarification, role selection, task decomposition, assignment decisions, and concise status updates.");
131
+ if (hasOnDemandWorkerProvisioning(deps.config)) {
132
+ parts.push("- If no workers are currently registered but on-demand provisioning is enabled, you may still create execution-ready tasks so the required roles can be provisioned.");
133
+ } else {
134
+ parts.push("- If no workers are registered, you may mention which roles would be needed, but stop there and report the worker-capacity block to the human.");
135
+ }
114
136
  parts.push("- Use the controller itself for requirement analysis; use the PM role only for PM-owned deliverables after intake is clear.");
115
137
  parts.push(`- Use exact TeamClaw role IDs only: ${TEAMCLAW_ROLE_IDS_TEXT}.`);
116
138