@teamclaws/teamclaw 2026.4.2-2 → 2026.4.2-4
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 +17 -2
- package/cli.mjs +13 -10
- package/openclaw.plugin.json +5 -5
- package/package.json +1 -1
- package/skills/teamclaw/SKILL.md +1 -1
- package/skills/teamclaw/references/api-quick-ref.md +1 -1
- package/skills/teamclaw-setup/references/install-modes.md +10 -4
- package/skills/teamclaw-setup/references/validation-checklist.md +3 -1
- package/src/config.ts +4 -4
- package/src/controller/controller-capacity.ts +3 -3
- package/src/controller/http-server.ts +191 -9
- package/src/controller/worker-provisioning.ts +1 -1
- package/src/prompt-policy.ts +2 -0
- package/src/state.ts +22 -2
- package/src/types.ts +7 -1
- package/src/worker/tools.ts +94 -10
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ It supports:
|
|
|
6
6
|
|
|
7
7
|
- `controller` / `worker` modes
|
|
8
8
|
- externally registered workers
|
|
9
|
-
-
|
|
9
|
+
- adaptive kickoff planning for medium/complex work, with per-role assessments before task creation
|
|
10
|
+
- clarifications, workspace browsing, controller UI visibility, and a matching desktop client
|
|
10
11
|
- Git-based collaboration
|
|
11
12
|
- on-demand worker provisioning with `process`, `docker`, and `kubernetes`
|
|
12
13
|
|
|
@@ -25,9 +26,12 @@ This installer can:
|
|
|
25
26
|
- install/update the TeamClaw plugin in OpenClaw
|
|
26
27
|
- detect your local `openclaw.json`
|
|
27
28
|
- let you choose the installation mode
|
|
29
|
+
- support dedicated worker-only installs with `--install-mode worker`
|
|
28
30
|
- let you choose a model from the models already defined in OpenClaw
|
|
29
31
|
- let you choose the OpenClaw workspace directory
|
|
30
32
|
- default TeamClaw to a dedicated `teamclaw` agent/workspace instead of reusing `main`
|
|
33
|
+
- copy the effective host model into the dedicated TeamClaw agent config when independent mode is used
|
|
34
|
+
- bootstrap TeamClaw auth profiles from the host OpenClaw auth store when available
|
|
31
35
|
- prefill Docker/Kubernetes provisioning with the published TeamClaw runtime image
|
|
32
36
|
- prefill Docker workspace persistence with a named volume and Kubernetes persistence with a PVC name
|
|
33
37
|
|
|
@@ -36,6 +40,8 @@ By default, guided install now uses an independent TeamClaw agent/workspace layo
|
|
|
36
40
|
- `agent:teamclaw:*` sessions
|
|
37
41
|
- sibling workspace such as `~/.openclaw/workspace-teamclaw`
|
|
38
42
|
|
|
43
|
+
In practice, this means TeamClaw keeps its own agent state, workspace, and auth-profile copy instead of reusing the host `main` agent. Use this default unless you explicitly need legacy compatibility with a shared `main` workspace.
|
|
44
|
+
|
|
39
45
|
For advanced compatibility only, you can force the legacy shared-`main` layout:
|
|
40
46
|
|
|
41
47
|
```bash
|
|
@@ -65,6 +71,15 @@ For maintainers, TeamClaw now uses two release paths:
|
|
|
65
71
|
1. **npm package** — published by GitHub Actions via `.github/workflows/teamclaw-plugin-npm-release.yml`
|
|
66
72
|
2. **ClawHub code plugin + bundled skills** — published manually from the CLI
|
|
67
73
|
|
|
74
|
+
The npm publish workflow runs automatically when:
|
|
75
|
+
|
|
76
|
+
- a tag matching `v*` is pushed
|
|
77
|
+
- or relevant package files change on `main` (`src/**`, `.github/workflows/teamclaw-plugin-npm-release.yml`, `scripts/sync-teamclaw-plugin-manifest.mjs`, `scripts/teamclaw-package-check.mjs`, `scripts/teamclaw-npm-publish.sh`)
|
|
78
|
+
|
|
79
|
+
It also supports `workflow_dispatch` for a specific commit SHA on `main`.
|
|
80
|
+
|
|
81
|
+
Before the first real npm publish, GitHub must already be configured for npm trusted publishing with the `npm-release` environment and this workflow file.
|
|
82
|
+
|
|
68
83
|
Before either release path, sync and validate the generated plugin manifest:
|
|
69
84
|
|
|
70
85
|
```bash
|
|
@@ -95,7 +110,7 @@ For a first-time setup, the safest path is:
|
|
|
95
110
|
2. Validate the workflow with a small smoke-test task
|
|
96
111
|
3. Expand to external workers, Docker, or Kubernetes after the basics are working
|
|
97
112
|
|
|
98
|
-
When on-demand provisioning is enabled, TeamClaw now treats controller startup as a readiness phase, not just a process-up check. During that warm-up, `/api/v1/health` returns a non-OK status until the controller has verified writable runtime paths and successfully brought the configured startup worker roles online.
|
|
113
|
+
When on-demand provisioning is enabled, TeamClaw now treats controller startup as a readiness phase, not just a process-up check. During that warm-up, `/api/v1/health` returns a non-OK status until the controller has verified writable runtime paths and successfully brought the configured startup worker roles online. If `workerProvisioningRoles` is empty, startup readiness defaults to waiting for a warm `developer` worker.
|
|
99
114
|
|
|
100
115
|
## Documentation
|
|
101
116
|
|
package/cli.mjs
CHANGED
|
@@ -63,8 +63,8 @@ const INSTALL_MODE_OPTIONS = [
|
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
value: "controller-manual",
|
|
66
|
-
label: "
|
|
67
|
-
hint: "
|
|
66
|
+
label: "Local controller + default on-demand workers",
|
|
67
|
+
hint: "Lean same-host setup; writes workerProvisioningRoles=[] and workerProvisioningMaxPerRole=1.",
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
value: "controller-docker",
|
|
@@ -776,7 +776,10 @@ function isControllerInstallMode(installMode) {
|
|
|
776
776
|
}
|
|
777
777
|
|
|
778
778
|
function isOnDemandControllerInstallMode(installMode) {
|
|
779
|
-
return installMode === "controller-
|
|
779
|
+
return installMode === "controller-manual" ||
|
|
780
|
+
installMode === "controller-process" ||
|
|
781
|
+
installMode === "controller-docker" ||
|
|
782
|
+
installMode === "controller-kubernetes";
|
|
780
783
|
}
|
|
781
784
|
|
|
782
785
|
function describeProvisioningRoles(roles) {
|
|
@@ -1360,7 +1363,7 @@ async function collectInstallChoices(configPath, config, prompter, options) {
|
|
|
1360
1363
|
|
|
1361
1364
|
const provisioningRoles = await promptOptionalRoleList(
|
|
1362
1365
|
prompter,
|
|
1363
|
-
"Preferred on-demand roles (comma-separated, leave empty
|
|
1366
|
+
"Preferred on-demand roles (comma-separated, leave empty to keep workerProvisioningRoles empty and let startup readiness default to a warm developer worker)",
|
|
1364
1367
|
resolveDefaultProvisioningRoles(existingTeamClaw),
|
|
1365
1368
|
);
|
|
1366
1369
|
const maxPerRole = await prompter.number({
|
|
@@ -1368,7 +1371,7 @@ async function collectInstallChoices(configPath, config, prompter, options) {
|
|
|
1368
1371
|
defaultValue:
|
|
1369
1372
|
typeof existingTeamClaw.workerProvisioningMaxPerRole === "number" && existingTeamClaw.workerProvisioningMaxPerRole >= 1
|
|
1370
1373
|
? existingTeamClaw.workerProvisioningMaxPerRole
|
|
1371
|
-
:
|
|
1374
|
+
: 10,
|
|
1372
1375
|
min: 1,
|
|
1373
1376
|
max: 50,
|
|
1374
1377
|
});
|
|
@@ -1674,7 +1677,7 @@ function applyInstallerChoices(config, choices, configPath) {
|
|
|
1674
1677
|
teamclawConfig.workerProvisioningDisabled = true;
|
|
1675
1678
|
teamclawConfig.workerProvisioningControllerUrl = "";
|
|
1676
1679
|
teamclawConfig.workerProvisioningRoles = [];
|
|
1677
|
-
teamclawConfig.workerProvisioningMaxPerRole =
|
|
1680
|
+
teamclawConfig.workerProvisioningMaxPerRole = 10;
|
|
1678
1681
|
teamclawConfig.workerProvisioningImage = "";
|
|
1679
1682
|
teamclawConfig.workerProvisioningPassEnv = [];
|
|
1680
1683
|
teamclawConfig.workerProvisioningExtraEnv = {};
|
|
@@ -1690,11 +1693,11 @@ function applyInstallerChoices(config, choices, configPath) {
|
|
|
1690
1693
|
delete teamclawConfig.role;
|
|
1691
1694
|
|
|
1692
1695
|
if (choices.installMode === "controller-manual") {
|
|
1693
|
-
teamclawConfig.workerProvisioningType = "
|
|
1694
|
-
teamclawConfig.workerProvisioningDisabled =
|
|
1696
|
+
teamclawConfig.workerProvisioningType = "process";
|
|
1697
|
+
teamclawConfig.workerProvisioningDisabled = false;
|
|
1695
1698
|
teamclawConfig.workerProvisioningControllerUrl = "";
|
|
1696
1699
|
teamclawConfig.workerProvisioningRoles = [];
|
|
1697
|
-
teamclawConfig.workerProvisioningMaxPerRole =
|
|
1700
|
+
teamclawConfig.workerProvisioningMaxPerRole = 10;
|
|
1698
1701
|
teamclawConfig.workerProvisioningImage = "";
|
|
1699
1702
|
teamclawConfig.workerProvisioningPassEnv = [];
|
|
1700
1703
|
teamclawConfig.workerProvisioningExtraEnv = {};
|
|
@@ -1963,7 +1966,7 @@ async function runInstall(options) {
|
|
|
1963
1966
|
} else if (choices.installMode === "controller-kubernetes") {
|
|
1964
1967
|
prompter.note("Before using Kubernetes provisioning, make sure kubectl, namespace access, and the worker image are ready.");
|
|
1965
1968
|
} else if (choices.installMode === "controller-manual") {
|
|
1966
|
-
prompter.note("Next step:
|
|
1969
|
+
prompter.note("Next step: open the local TeamClaw UI and let it provision local workers on demand.");
|
|
1967
1970
|
} else if (choices.installMode === "worker") {
|
|
1968
1971
|
prompter.note("Next step: start this worker node so it can register with the controller.");
|
|
1969
1972
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.4.2-
|
|
5
|
+
"version": "2026.4.2-4",
|
|
6
6
|
"skills": [
|
|
7
7
|
"./skills"
|
|
8
8
|
],
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"processModel": {
|
|
63
63
|
"label": "Process Model",
|
|
64
|
-
"help": "TeamClaw runs workers as
|
|
64
|
+
"help": "TeamClaw runs workers as local or provisioned gateway processes"
|
|
65
65
|
},
|
|
66
66
|
"workerProvisioningType": {
|
|
67
67
|
"label": "On-demand Worker Provider",
|
|
@@ -234,7 +234,7 @@
|
|
|
234
234
|
"multi"
|
|
235
235
|
],
|
|
236
236
|
"default": "multi",
|
|
237
|
-
"description": "Worker execution model: TeamClaw runs workers as
|
|
237
|
+
"description": "Worker execution model: TeamClaw runs workers as local or provisioned gateway processes"
|
|
238
238
|
},
|
|
239
239
|
"workerProvisioningType": {
|
|
240
240
|
"type": "string",
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
"workerProvisioningRoles": {
|
|
261
261
|
"type": "array",
|
|
262
262
|
"default": [],
|
|
263
|
-
"description": "Preferred on-demand roles; task-required roles can still launch automatically. Empty means
|
|
263
|
+
"description": "Preferred on-demand roles; task-required roles can still launch automatically. Empty means no preferred startup role list, so startup readiness falls back to a warm developer worker",
|
|
264
264
|
"items": {
|
|
265
265
|
"type": "string",
|
|
266
266
|
"enum": [
|
|
@@ -284,7 +284,7 @@
|
|
|
284
284
|
},
|
|
285
285
|
"workerProvisioningMaxPerRole": {
|
|
286
286
|
"type": "number",
|
|
287
|
-
"default":
|
|
287
|
+
"default": 10,
|
|
288
288
|
"description": "Maximum on-demand workers to launch per role"
|
|
289
289
|
},
|
|
290
290
|
"workerProvisioningIdleTtlMs": {
|
package/package.json
CHANGED
package/skills/teamclaw/SKILL.md
CHANGED
|
@@ -203,7 +203,7 @@ Mention this to users who prefer a visual overview.
|
|
|
203
203
|
| Issue | Solution |
|
|
204
204
|
|-------|----------|
|
|
205
205
|
| Health check fails | Ensure TeamClaw plugin is enabled in controller mode: `openclaw plugins list` |
|
|
206
|
-
| No workers available | Check
|
|
206
|
+
| No workers available | Check `workerProvisioningType` / `workerProvisioningDisabled`; local installs should usually use process provisioning |
|
|
207
207
|
| Intake times out | Increase `taskTimeoutMs` in TeamClaw config; ensure AI model is responsive |
|
|
208
208
|
| Task stuck in pending | No idle worker for the assigned role; check `GET /api/v1/workers` |
|
|
209
209
|
| Clarification blocking | Answer pending clarifications via `POST /api/v1/clarifications/:id/answer` |
|
|
@@ -10,7 +10,7 @@ Default: `http://127.0.0.1:9527`
|
|
|
10
10
|
|
|
11
11
|
| Method | Path | Description |
|
|
12
12
|
|--------|------|-------------|
|
|
13
|
-
| GET | `/api/v1/health` | Health check
|
|
13
|
+
| GET | `/api/v1/health` | Health/readiness check; may report a non-OK startup readiness state until required warm workers are online |
|
|
14
14
|
| GET | `/api/v1/team/status` | Full team snapshot (workers, tasks, runs, clarifications) |
|
|
15
15
|
| GET | `/api/v1/roles` | List all available roles |
|
|
16
16
|
|
|
@@ -48,7 +48,7 @@ Minimal TeamClaw plugin block:
|
|
|
48
48
|
"workerProvisioningType": "process",
|
|
49
49
|
"workerProvisioningRoles": [],
|
|
50
50
|
"workerProvisioningMinPerRole": 0,
|
|
51
|
-
"workerProvisioningMaxPerRole":
|
|
51
|
+
"workerProvisioningMaxPerRole": 10,
|
|
52
52
|
"workerProvisioningIdleTtlMs": 120000,
|
|
53
53
|
"workerProvisioningStartupTimeoutMs": 120000
|
|
54
54
|
}
|
|
@@ -56,6 +56,12 @@ Minimal TeamClaw plugin block:
|
|
|
56
56
|
|
|
57
57
|
Use this first because it avoids multi-machine networking while still exercising the real controller, provisioned workers, UI, messages, clarifications, and git-backed workspace flow.
|
|
58
58
|
|
|
59
|
+
If the user is specifically choosing install modes through the guided installer, remember the resulting config differs by mode:
|
|
60
|
+
|
|
61
|
+
- `controller-manual` writes `workerProvisioningType: "process"` and `workerProvisioningRoles: []`, so startup readiness falls back to a warm `developer` worker
|
|
62
|
+
- `controller-process` writes `workerProvisioningType: "process"` and uses the chosen roles/max-per-role
|
|
63
|
+
- `worker` disables provisioning on that node
|
|
64
|
+
|
|
59
65
|
## 4. Worker-only topology
|
|
60
66
|
|
|
61
67
|
Use this only when a controller already exists elsewhere:
|
|
@@ -83,7 +89,7 @@ Use this when the controller should launch same-machine workers only as needed:
|
|
|
83
89
|
"teamName": "my-team",
|
|
84
90
|
"workerProvisioningType": "process",
|
|
85
91
|
"workerProvisioningMinPerRole": 0,
|
|
86
|
-
"workerProvisioningMaxPerRole":
|
|
92
|
+
"workerProvisioningMaxPerRole": 10,
|
|
87
93
|
"workerProvisioningIdleTtlMs": 120000,
|
|
88
94
|
"workerProvisioningStartupTimeoutMs": 120000
|
|
89
95
|
}
|
|
@@ -103,7 +109,7 @@ Use this when the user already has Docker and wants containerized workers:
|
|
|
103
109
|
"workerProvisioningImage": "ghcr.io/topcheer/teamclaw-openclaw:latest",
|
|
104
110
|
"workerProvisioningWorkspaceRoot": "/workspace-root",
|
|
105
111
|
"workerProvisioningDockerWorkspaceVolume": "teamclaw-workspaces",
|
|
106
|
-
"workerProvisioningMaxPerRole":
|
|
112
|
+
"workerProvisioningMaxPerRole": 10
|
|
107
113
|
}
|
|
108
114
|
```
|
|
109
115
|
|
|
@@ -126,7 +132,7 @@ Use this only when the user already runs the controller in or behind a reachable
|
|
|
126
132
|
"workerProvisioningImage": "ghcr.io/topcheer/teamclaw-openclaw:latest",
|
|
127
133
|
"workerProvisioningWorkspaceRoot": "/workspace-root",
|
|
128
134
|
"workerProvisioningKubernetesWorkspacePersistentVolumeClaim": "teamclaw-workspace",
|
|
129
|
-
"workerProvisioningMaxPerRole":
|
|
135
|
+
"workerProvisioningMaxPerRole": 10
|
|
130
136
|
}
|
|
131
137
|
```
|
|
132
138
|
|
|
@@ -14,6 +14,8 @@ Expected shape:
|
|
|
14
14
|
{"status":"ok","mode":"controller", ...}
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
If on-demand provisioning is enabled, `/api/v1/health` can temporarily return a non-OK readiness state during warm-up. When `workerProvisioningRoles` is empty, readiness defaults to waiting for a warm `developer` worker.
|
|
18
|
+
|
|
17
19
|
Also point them to:
|
|
18
20
|
|
|
19
21
|
```text
|
|
@@ -67,7 +69,7 @@ If OpenClaw times out first, users often think TeamClaw is broken when the real
|
|
|
67
69
|
|
|
68
70
|
Treat the install as successful only when:
|
|
69
71
|
|
|
70
|
-
1. controller health is `ok`
|
|
72
|
+
1. controller health is `ok` after startup readiness finishes
|
|
71
73
|
2. expected workers appear in the UI or status view
|
|
72
74
|
3. the smoke-test task completes
|
|
73
75
|
4. files appear in the workspace
|
package/src/config.ts
CHANGED
|
@@ -79,7 +79,7 @@ function buildConfigSchema() {
|
|
|
79
79
|
type: "string" as const,
|
|
80
80
|
enum: ["multi"],
|
|
81
81
|
default: "multi",
|
|
82
|
-
description: "Worker execution model: TeamClaw runs workers as
|
|
82
|
+
description: "Worker execution model: TeamClaw runs workers as local or provisioned gateway processes",
|
|
83
83
|
},
|
|
84
84
|
workerProvisioningType: {
|
|
85
85
|
type: "string" as const,
|
|
@@ -100,7 +100,7 @@ function buildConfigSchema() {
|
|
|
100
100
|
workerProvisioningRoles: {
|
|
101
101
|
type: "array" as const,
|
|
102
102
|
default: [],
|
|
103
|
-
description: "Preferred on-demand roles; task-required roles can still launch automatically. Empty means
|
|
103
|
+
description: "Preferred on-demand roles; task-required roles can still launch automatically. Empty means no preferred startup role list, so startup readiness falls back to a warm developer worker",
|
|
104
104
|
items: {
|
|
105
105
|
type: "string" as const,
|
|
106
106
|
enum: ROLE_IDS,
|
|
@@ -113,7 +113,7 @@ function buildConfigSchema() {
|
|
|
113
113
|
},
|
|
114
114
|
workerProvisioningMaxPerRole: {
|
|
115
115
|
type: "number" as const,
|
|
116
|
-
default:
|
|
116
|
+
default: 10,
|
|
117
117
|
description: "Maximum on-demand workers to launch per role",
|
|
118
118
|
},
|
|
119
119
|
workerProvisioningIdleTtlMs: {
|
|
@@ -253,7 +253,7 @@ function buildConfigSchema() {
|
|
|
253
253
|
},
|
|
254
254
|
processModel: {
|
|
255
255
|
label: "Process Model",
|
|
256
|
-
help: "TeamClaw runs workers as
|
|
256
|
+
help: "TeamClaw runs workers as local or provisioned gateway processes",
|
|
257
257
|
},
|
|
258
258
|
workerProvisioningType: {
|
|
259
259
|
label: "On-demand Worker Provider",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { PluginConfig, TeamState } from "../types.js";
|
|
2
2
|
|
|
3
3
|
export function hasOnDemandWorkerProvisioning(
|
|
4
|
-
config: Pick<PluginConfig, "workerProvisioningType" | "processModel">,
|
|
4
|
+
config: Pick<PluginConfig, "workerProvisioningType" | "workerProvisioningDisabled" | "processModel">,
|
|
5
5
|
): boolean {
|
|
6
|
-
return config.workerProvisioningType !== "none";
|
|
6
|
+
return config.workerProvisioningType !== "none" && config.workerProvisioningDisabled !== true;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function shouldBlockControllerWithoutWorkers(
|
|
10
|
-
config: Pick<PluginConfig, "workerProvisioningType" | "processModel">,
|
|
10
|
+
config: Pick<PluginConfig, "workerProvisioningType" | "workerProvisioningDisabled" | "processModel">,
|
|
11
11
|
state: TeamState | null,
|
|
12
12
|
): boolean {
|
|
13
13
|
return !!state && Object.keys(state.workers).length === 0 && !hasOnDemandWorkerProvisioning(config);
|
|
@@ -1427,6 +1427,9 @@ function buildControllerFollowUpMessage(task: TaskInfo, state: TeamState | null)
|
|
|
1427
1427
|
"Continue orchestrating this same requirement.",
|
|
1428
1428
|
"Review the current TeamClaw state before acting.",
|
|
1429
1429
|
"Create only the next execution-ready task(s) whose prerequisites are now satisfied.",
|
|
1430
|
+
"For any large-scale requirement, prefer parallel fan-out over serial mega-phases: if multiple independent developer workstreams are ready, create all of them now instead of a single umbrella developer task.",
|
|
1431
|
+
"Decompose developer work by module or subsystem with clear file ownership and interfaces so multiple developer workers can collaborate safely in parallel.",
|
|
1432
|
+
"Use TeamClaw's available same-role capacity: create multiple developer tasks when the work can proceed concurrently rather than forcing one developer to carry the whole rewrite alone.",
|
|
1430
1433
|
"Do not duplicate tasks that already exist, are active, or are already completed.",
|
|
1431
1434
|
"If this task produced a web application with a live preview URL, include it in your reply so the human can verify the result.",
|
|
1432
1435
|
"If all planned phases are complete and no follow-ups remain, set requirementFullyComplete=true in the manifest and provide a final delivery summary.",
|
|
@@ -1436,6 +1439,37 @@ function buildControllerFollowUpMessage(task: TaskInfo, state: TeamState | null)
|
|
|
1436
1439
|
return parts.filter(Boolean).join("\n");
|
|
1437
1440
|
}
|
|
1438
1441
|
|
|
1442
|
+
function buildControllerParallelHelpMessage(
|
|
1443
|
+
task: TaskInfo,
|
|
1444
|
+
state: TeamState | null,
|
|
1445
|
+
input: {
|
|
1446
|
+
requestedBy: string;
|
|
1447
|
+
requestedByRole?: RoleId;
|
|
1448
|
+
targetRole?: RoleId;
|
|
1449
|
+
reason: string;
|
|
1450
|
+
requestedWorkerCount?: number;
|
|
1451
|
+
suggestedWorkstreams: string[];
|
|
1452
|
+
},
|
|
1453
|
+
): string {
|
|
1454
|
+
const targetRole = input.targetRole || input.requestedByRole || task.assignedRole || "developer";
|
|
1455
|
+
return [
|
|
1456
|
+
buildControllerFollowUpMessage(task, state),
|
|
1457
|
+
"",
|
|
1458
|
+
"## Parallel Help Request",
|
|
1459
|
+
`Current worker ${input.requestedBy} has asked TeamClaw to expand parallel help for role ${targetRole}.`,
|
|
1460
|
+
input.requestedByRole ? `Requesting worker role: ${input.requestedByRole}` : "",
|
|
1461
|
+
typeof input.requestedWorkerCount === "number"
|
|
1462
|
+
? `Desired same-role worker capacity for this requirement: ${input.requestedWorkerCount}`
|
|
1463
|
+
: "",
|
|
1464
|
+
`Why more parallel help is needed: ${input.reason}`,
|
|
1465
|
+
input.suggestedWorkstreams.length > 0
|
|
1466
|
+
? `Suggested parallel workstreams:\n${input.suggestedWorkstreams.map((item) => `- ${item}`).join("\n")}`
|
|
1467
|
+
: "",
|
|
1468
|
+
"If the suggested workstreams are genuinely independent, create multiple execution-ready tasks for that role now instead of keeping one giant serial task.",
|
|
1469
|
+
"Reuse active tasks when possible, but if no matching tasks already exist, fan out the work into distinct module- or subsystem-scoped tasks that can run concurrently.",
|
|
1470
|
+
].filter(Boolean).join("\n");
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1439
1473
|
function buildControllerClarificationAnswerMessage(
|
|
1440
1474
|
clarification: ClarificationRequest,
|
|
1441
1475
|
answer: string,
|
|
@@ -2644,6 +2678,8 @@ function enrichWithFilesystemHtmlScan(
|
|
|
2644
2678
|
|
|
2645
2679
|
const MEANINGFUL_PROJECT_CHANGE_EXTENSIONS = new Set([
|
|
2646
2680
|
".js", ".jsx", ".ts", ".tsx", ".json", ".html", ".css", ".scss", ".md", ".txt", ".yml", ".yaml",
|
|
2681
|
+
".go", ".mod", ".sum", ".py", ".rb", ".rs", ".java", ".kt", ".swift", ".c", ".cc", ".cpp", ".h", ".hpp",
|
|
2682
|
+
".cs", ".php", ".sh", ".bash", ".zsh", ".sql", ".toml", ".ini",
|
|
2647
2683
|
]);
|
|
2648
2684
|
|
|
2649
2685
|
const IGNORED_PROJECT_CHANGE_DIRS = new Set([
|
|
@@ -2668,7 +2704,57 @@ function taskRequiresMeaningfulProjectChangeGate(task: TaskInfo): boolean {
|
|
|
2668
2704
|
return /\b(implement|build|fix|rework|update|add|enhanc|deliver|write|create)\b/u.test(text);
|
|
2669
2705
|
}
|
|
2670
2706
|
|
|
2671
|
-
function
|
|
2707
|
+
function projectHasMeaningfulDeliverableEvidence(
|
|
2708
|
+
task: TaskInfo,
|
|
2709
|
+
contract: WorkerTaskResultContract | undefined,
|
|
2710
|
+
): boolean {
|
|
2711
|
+
if (!task.projectDir || !contract) {
|
|
2712
|
+
return false;
|
|
2713
|
+
}
|
|
2714
|
+
const projectRoot = path.join(resolveTeamClawProjectsDir(), task.projectDir);
|
|
2715
|
+
for (const deliverable of contract.deliverables) {
|
|
2716
|
+
if (deliverable.kind !== "file" && deliverable.kind !== "directory") {
|
|
2717
|
+
continue;
|
|
2718
|
+
}
|
|
2719
|
+
const rawValue = deliverable.value.trim().replace(/\\/g, "/");
|
|
2720
|
+
if (!rawValue) {
|
|
2721
|
+
continue;
|
|
2722
|
+
}
|
|
2723
|
+
const normalizedValue = rawValue.startsWith("projects/")
|
|
2724
|
+
? rawValue.slice("projects/".length)
|
|
2725
|
+
: rawValue;
|
|
2726
|
+
if (
|
|
2727
|
+
normalizedValue !== task.projectDir
|
|
2728
|
+
&& !normalizedValue.startsWith(`${task.projectDir}/`)
|
|
2729
|
+
) {
|
|
2730
|
+
continue;
|
|
2731
|
+
}
|
|
2732
|
+
const relativePath = normalizedValue === task.projectDir
|
|
2733
|
+
? ""
|
|
2734
|
+
: normalizedValue.slice(task.projectDir.length + 1);
|
|
2735
|
+
const fullPath = relativePath ? path.join(projectRoot, relativePath) : projectRoot;
|
|
2736
|
+
try {
|
|
2737
|
+
const stats = fs.statSync(fullPath);
|
|
2738
|
+
if (stats.isFile()) {
|
|
2739
|
+
return true;
|
|
2740
|
+
}
|
|
2741
|
+
if (stats.isDirectory()) {
|
|
2742
|
+
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
2743
|
+
if (entries.some((entry) => entry.isFile() || entry.isDirectory())) {
|
|
2744
|
+
return true;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
} catch {
|
|
2748
|
+
continue;
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
return false;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
function projectHasMeaningfulFileChanges(
|
|
2755
|
+
task: TaskInfo,
|
|
2756
|
+
contract?: WorkerTaskResultContract,
|
|
2757
|
+
): boolean {
|
|
2672
2758
|
if (!task.projectDir) {
|
|
2673
2759
|
return false;
|
|
2674
2760
|
}
|
|
@@ -2714,7 +2800,7 @@ function projectHasMeaningfulFileChanges(task: TaskInfo): boolean {
|
|
|
2714
2800
|
}
|
|
2715
2801
|
}
|
|
2716
2802
|
}
|
|
2717
|
-
return
|
|
2803
|
+
return projectHasMeaningfulDeliverableEvidence(task, contract);
|
|
2718
2804
|
}
|
|
2719
2805
|
|
|
2720
2806
|
function allowsNoChangeCompletion(
|
|
@@ -2982,6 +3068,25 @@ function ensureTaskResultContract(
|
|
|
2982
3068
|
return contract;
|
|
2983
3069
|
}
|
|
2984
3070
|
|
|
3071
|
+
function buildEffectiveTaskResultContract(
|
|
3072
|
+
task: TaskInfo,
|
|
3073
|
+
result: string,
|
|
3074
|
+
error: string | undefined,
|
|
3075
|
+
submittedContract?: WorkerTaskResultContract,
|
|
3076
|
+
): WorkerTaskResultContract {
|
|
3077
|
+
let contract = submittedContract
|
|
3078
|
+
? filterStaleDeliverables(submittedContract, task.projectDir)
|
|
3079
|
+
: backfillWorkerTaskResultContract(task, result, error);
|
|
3080
|
+
if (!error) {
|
|
3081
|
+
const enriched = enrichDeliverablesWithPreviewInference(contract, result)
|
|
3082
|
+
?? enrichWithFilesystemHtmlScan(contract, task.projectDir);
|
|
3083
|
+
if (enriched) {
|
|
3084
|
+
contract = enriched;
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
return contract;
|
|
3088
|
+
}
|
|
3089
|
+
|
|
2985
3090
|
function buildResultContractSection(task: TaskInfo): string {
|
|
2986
3091
|
const contract = task.resultContract;
|
|
2987
3092
|
if (!contract) {
|
|
@@ -4078,20 +4183,40 @@ async function handleRequest(
|
|
|
4078
4183
|
}, deps);
|
|
4079
4184
|
}
|
|
4080
4185
|
|
|
4081
|
-
|
|
4186
|
+
const effectiveContract = buildEffectiveTaskResultContract(currentTask, result, error, submittedContract);
|
|
4187
|
+
|
|
4188
|
+
if (!error && effectiveContract.outcome === "blocked") {
|
|
4189
|
+
updateTeamState((teamState) => {
|
|
4190
|
+
const task = teamState.tasks[taskId];
|
|
4191
|
+
if (!task) {
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4194
|
+
task.resultContract = effectiveContract;
|
|
4195
|
+
task.updatedAt = Date.now();
|
|
4196
|
+
});
|
|
4197
|
+
if (!submittedContract) {
|
|
4198
|
+
recordTaskExecutionEvent(taskId, {
|
|
4199
|
+
type: "lifecycle",
|
|
4200
|
+
phase: "result_contract_backfilled",
|
|
4201
|
+
source: "controller",
|
|
4202
|
+
message: "Worker did not submit a structured result contract; TeamClaw backfilled one from the recorded task result.",
|
|
4203
|
+
workerId: currentTask.assignedWorkerId,
|
|
4204
|
+
role: currentTask.assignedRole,
|
|
4205
|
+
}, deps);
|
|
4206
|
+
}
|
|
4082
4207
|
const requested = await requestTaskClarification({
|
|
4083
4208
|
taskId,
|
|
4084
4209
|
requestedBy: workerId ?? "worker",
|
|
4085
4210
|
requestedByWorkerId: workerId,
|
|
4086
4211
|
requestedByRole: currentTask.assignedRole,
|
|
4087
|
-
question:
|
|
4212
|
+
question: effectiveContract.questions[0]
|
|
4088
4213
|
?? "This task is blocked and needs a human decision before work can continue. What should TeamClaw do next?",
|
|
4089
|
-
blockingReason:
|
|
4214
|
+
blockingReason: effectiveContract.blockers[0] ?? effectiveContract.summary,
|
|
4090
4215
|
context: [
|
|
4091
|
-
|
|
4216
|
+
effectiveContract.notes,
|
|
4092
4217
|
result.trim(),
|
|
4093
|
-
|
|
4094
|
-
? `Worker-provided commands/details:\n${
|
|
4218
|
+
effectiveContract.keyPoints.length > 0
|
|
4219
|
+
? `Worker-provided commands/details:\n${effectiveContract.keyPoints.join("\n")}`
|
|
4095
4220
|
: "",
|
|
4096
4221
|
].filter(Boolean).join("\n\n"),
|
|
4097
4222
|
}, deps);
|
|
@@ -4116,7 +4241,7 @@ async function handleRequest(
|
|
|
4116
4241
|
!error
|
|
4117
4242
|
&& gatedTask
|
|
4118
4243
|
&& taskRequiresMeaningfulProjectChangeGate(gatedTask)
|
|
4119
|
-
&& !projectHasMeaningfulFileChanges(gatedTask)
|
|
4244
|
+
&& !projectHasMeaningfulFileChanges(gatedTask, effectiveContract)
|
|
4120
4245
|
&& !allowsNoChangeCompletion(gatedTask, submittedContract, result)
|
|
4121
4246
|
) {
|
|
4122
4247
|
error = "Task reported completion but no meaningful project file changes were detected in the assigned project directory.";
|
|
@@ -4205,6 +4330,63 @@ async function handleRequest(
|
|
|
4205
4330
|
return;
|
|
4206
4331
|
}
|
|
4207
4332
|
|
|
4333
|
+
// POST /api/v1/tasks/:id/parallel-help
|
|
4334
|
+
if (req.method === "POST" && pathname.match(/^\/api\/v1\/tasks\/[^/]+\/parallel-help$/)) {
|
|
4335
|
+
const taskId = pathname.split("/")[4]!;
|
|
4336
|
+
const body = await parseJsonBody(req);
|
|
4337
|
+
const currentTask = getTeamState()?.tasks[taskId];
|
|
4338
|
+
if (!currentTask) {
|
|
4339
|
+
sendError(res, 404, "Task not found");
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
const requestedBy = typeof body.requestedBy === "string" ? body.requestedBy : "";
|
|
4343
|
+
const reason = typeof body.reason === "string" ? body.reason.trim() : "";
|
|
4344
|
+
const requestedByRole = typeof body.requestedByRole === "string" ? body.requestedByRole as RoleId : undefined;
|
|
4345
|
+
const targetRole = typeof body.targetRole === "string" ? body.targetRole as RoleId : undefined;
|
|
4346
|
+
const requestedWorkerCount = typeof body.requestedWorkerCount === "number"
|
|
4347
|
+
? Math.max(2, Math.min(10, Math.floor(body.requestedWorkerCount)))
|
|
4348
|
+
: undefined;
|
|
4349
|
+
const suggestedWorkstreams = Array.isArray(body.suggestedWorkstreams)
|
|
4350
|
+
? body.suggestedWorkstreams.map((entry: unknown) => String(entry ?? "").trim()).filter(Boolean)
|
|
4351
|
+
: [];
|
|
4352
|
+
if (!requestedBy || !reason) {
|
|
4353
|
+
sendError(res, 400, "requestedBy and reason are required");
|
|
4354
|
+
return;
|
|
4355
|
+
}
|
|
4356
|
+
const sessionKey = resolveControllerWorkflowSessionKey(currentTask, getTeamState());
|
|
4357
|
+
if (!sessionKey) {
|
|
4358
|
+
sendError(res, 409, "Task is not linked to a controller session");
|
|
4359
|
+
return;
|
|
4360
|
+
}
|
|
4361
|
+
recordTaskExecutionEvent(taskId, {
|
|
4362
|
+
type: "progress",
|
|
4363
|
+
phase: "parallel_help_requested",
|
|
4364
|
+
source: "worker",
|
|
4365
|
+
message: `Worker requested more ${targetRole || requestedByRole || currentTask.assignedRole || "developer"} capacity for parallel work: ${reason}`,
|
|
4366
|
+
workerId: requestedBy,
|
|
4367
|
+
role: requestedByRole,
|
|
4368
|
+
}, deps);
|
|
4369
|
+
const result = await runControllerIntake(
|
|
4370
|
+
buildControllerParallelHelpMessage(currentTask, getTeamState(), {
|
|
4371
|
+
requestedBy,
|
|
4372
|
+
requestedByRole,
|
|
4373
|
+
targetRole,
|
|
4374
|
+
reason,
|
|
4375
|
+
requestedWorkerCount,
|
|
4376
|
+
suggestedWorkstreams,
|
|
4377
|
+
}),
|
|
4378
|
+
sessionKey,
|
|
4379
|
+
deps,
|
|
4380
|
+
{
|
|
4381
|
+
source: "task_follow_up",
|
|
4382
|
+
sourceTaskId: currentTask.id,
|
|
4383
|
+
sourceTaskTitle: currentTask.title,
|
|
4384
|
+
},
|
|
4385
|
+
);
|
|
4386
|
+
sendJson(res, 201, result);
|
|
4387
|
+
return;
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4208
4390
|
// ==================== Message Routing ====================
|
|
4209
4391
|
|
|
4210
4392
|
// POST /api/v1/controller/manifest
|
|
@@ -1416,7 +1416,7 @@ function buildProvisionedWorkerConfig(
|
|
|
1416
1416
|
teamclawConfig.workerProvisioningControllerUrl = "";
|
|
1417
1417
|
teamclawConfig.workerProvisioningRoles = [];
|
|
1418
1418
|
teamclawConfig.workerProvisioningMinPerRole = 0;
|
|
1419
|
-
teamclawConfig.workerProvisioningMaxPerRole =
|
|
1419
|
+
teamclawConfig.workerProvisioningMaxPerRole = 10;
|
|
1420
1420
|
teamclawConfig.workerProvisioningIdleTtlMs = controllerConfig.workerProvisioningIdleTtlMs;
|
|
1421
1421
|
teamclawConfig.workerProvisioningStartupTimeoutMs = controllerConfig.workerProvisioningStartupTimeoutMs;
|
|
1422
1422
|
teamclawConfig.workerProvisioningImage = "";
|
package/src/prompt-policy.ts
CHANGED
|
@@ -45,6 +45,7 @@ export function buildRoleOperatingRules(options: {
|
|
|
45
45
|
"- You are a team member, not the controller. Complete the current task yourself.",
|
|
46
46
|
"- Stay within your assigned role. Do not switch roles unless the task explicitly asks for cross-role analysis.",
|
|
47
47
|
"- Do not create new tasks, parallel workstreams, or extra backlog items on your own.",
|
|
48
|
+
"- If the assigned task is clearly too large for one worker but can be split into independent same-role workstreams, ask the controller to expand parallel help instead of silently carrying the whole backlog alone.",
|
|
48
49
|
"- Do not delegate the core work of your current task to another role.",
|
|
49
50
|
"- Respect the requested deliverable shape: if the task asks for a brief, plan, matrix, review, or design artifact, do that artifact instead of expanding it into full implementation work.",
|
|
50
51
|
"- If required information or a product/technical decision is missing, request clarification instead of guessing.",
|
|
@@ -81,6 +82,7 @@ export function buildWorkerSessionRules(): string[] {
|
|
|
81
82
|
"1. Complete only the task assigned to this session.",
|
|
82
83
|
"2. Pending team messages are context, not permission to widen scope.",
|
|
83
84
|
"3. Do NOT create new tasks, duplicate an existing task, or start a parallel task tree.",
|
|
85
|
+
"3a. If the task is too large for one worker and can be safely split into independent same-role workstreams, use the controller-facing parallel-help tool instead of silently continuing as one giant serial task.",
|
|
84
86
|
"4. If you are blocked by missing information, raise a clarification request and stop instead of guessing.",
|
|
85
87
|
"5. If required infrastructure, credentials, or external tool access are unavailable in this runtime, raise a clarification request and stop instead of faking completion.",
|
|
86
88
|
"6. Respect the task's requested deliverable: briefs, plans, matrices, reviews, and design artifacts are not implementation requests unless the task explicitly asks you to build code.",
|
package/src/state.ts
CHANGED
|
@@ -20,6 +20,7 @@ function resolvePluginStateDir(): string {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const STATE_DIR = resolvePluginStateDir();
|
|
23
|
+
const writeQueues = new Map<string, Promise<void>>();
|
|
23
24
|
|
|
24
25
|
function createEmptyProvisioningState(): TeamProvisioningState {
|
|
25
26
|
return {
|
|
@@ -63,6 +64,25 @@ async function ensureDir(dir: string): Promise<void> {
|
|
|
63
64
|
await fs.mkdir(dir, { recursive: true });
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
async function writeFileAtomically(filePath: string, contents: string): Promise<void> {
|
|
68
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
69
|
+
await fs.writeFile(tmpPath, contents, "utf8");
|
|
70
|
+
await fs.rename(tmpPath, filePath);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function enqueueAtomicWrite(filePath: string, contents: string): Promise<void> {
|
|
74
|
+
const previous = writeQueues.get(filePath) ?? Promise.resolve();
|
|
75
|
+
const next = previous
|
|
76
|
+
.catch(() => {})
|
|
77
|
+
.then(() => writeFileAtomically(filePath, contents));
|
|
78
|
+
writeQueues.set(filePath, next);
|
|
79
|
+
return next.finally(() => {
|
|
80
|
+
if (writeQueues.get(filePath) === next) {
|
|
81
|
+
writeQueues.delete(filePath);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
66
86
|
async function loadTeamState(teamName: string): Promise<TeamState | null> {
|
|
67
87
|
const filePath = path.join(STATE_DIR, `${teamName}-team-state.json`);
|
|
68
88
|
try {
|
|
@@ -116,7 +136,7 @@ async function saveTeamState(state: TeamState): Promise<void> {
|
|
|
116
136
|
state.controllerRuns = state.controllerRuns && typeof state.controllerRuns === "object"
|
|
117
137
|
? state.controllerRuns
|
|
118
138
|
: {};
|
|
119
|
-
await
|
|
139
|
+
await enqueueAtomicWrite(filePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
120
140
|
}
|
|
121
141
|
|
|
122
142
|
async function loadWorkerIdentity(): Promise<WorkerIdentity | null> {
|
|
@@ -141,7 +161,7 @@ async function loadWorkerIdentity(): Promise<WorkerIdentity | null> {
|
|
|
141
161
|
async function saveWorkerIdentity(identity: WorkerIdentity): Promise<void> {
|
|
142
162
|
await ensureDir(STATE_DIR);
|
|
143
163
|
const filePath = path.join(STATE_DIR, "worker-identity.json");
|
|
144
|
-
await
|
|
164
|
+
await enqueueAtomicWrite(filePath, `${JSON.stringify(identity, null, 2)}\n`);
|
|
145
165
|
}
|
|
146
166
|
|
|
147
167
|
async function clearWorkerIdentity(): Promise<void> {
|
package/src/types.ts
CHANGED
|
@@ -562,7 +562,7 @@ export function parsePluginConfig(raw: Record<string, unknown> = {}): PluginConf
|
|
|
562
562
|
? raw.heartbeatIntervalMs
|
|
563
563
|
: 10000;
|
|
564
564
|
|
|
565
|
-
const processModel
|
|
565
|
+
const processModel = parseProcessModel(raw.processModel);
|
|
566
566
|
|
|
567
567
|
const taskTimeoutMs = typeof raw.taskTimeoutMs === "number" && raw.taskTimeoutMs >= 1000
|
|
568
568
|
? raw.taskTimeoutMs
|
|
@@ -717,6 +717,12 @@ function parseProvisioningType(raw: unknown): WorkerProvisioningType {
|
|
|
717
717
|
: "none";
|
|
718
718
|
}
|
|
719
719
|
|
|
720
|
+
function parseProcessModel(raw: unknown): ProcessModel {
|
|
721
|
+
return typeof raw === "string" && raw.trim() === "multi"
|
|
722
|
+
? "multi"
|
|
723
|
+
: "multi";
|
|
724
|
+
}
|
|
725
|
+
|
|
720
726
|
function parseStringArray(raw: unknown): string[] {
|
|
721
727
|
return Array.isArray(raw)
|
|
722
728
|
? [...new Set(raw
|
package/src/worker/tools.ts
CHANGED
|
@@ -10,11 +10,28 @@ import {
|
|
|
10
10
|
normalizeWorkerTaskResultContract,
|
|
11
11
|
renderWorkerProgressText,
|
|
12
12
|
} from "../interaction-contracts.js";
|
|
13
|
+
import { loadWorkerIdentity } from "../state.js";
|
|
13
14
|
import type { PluginConfig, WorkerIdentity } from "../types.js";
|
|
14
15
|
import { normalizeClarificationQuestionSchema } from "../controller/orchestration-manifest.js";
|
|
15
16
|
|
|
16
17
|
const ALLOWED_PROGRESS_STATUSES = new Set(["in_progress", "review"]);
|
|
17
18
|
|
|
19
|
+
function normalizeProgressText(params: Record<string, unknown>): string {
|
|
20
|
+
if (typeof params.progress === "string" && params.progress.trim()) {
|
|
21
|
+
return params.progress.trim();
|
|
22
|
+
}
|
|
23
|
+
if (typeof params.summary === "string" && params.summary.trim()) {
|
|
24
|
+
return params.summary.trim();
|
|
25
|
+
}
|
|
26
|
+
if (typeof params.currentStep === "string" && params.currentStep.trim()) {
|
|
27
|
+
return params.currentStep.trim();
|
|
28
|
+
}
|
|
29
|
+
if (typeof params.message === "string" && params.message.trim()) {
|
|
30
|
+
return params.message.trim();
|
|
31
|
+
}
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
export type WorkerToolsDeps = {
|
|
19
36
|
config: PluginConfig;
|
|
20
37
|
getIdentity: () => WorkerIdentity | null;
|
|
@@ -23,6 +40,10 @@ export type WorkerToolsDeps = {
|
|
|
23
40
|
export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
24
41
|
const { config, getIdentity } = deps;
|
|
25
42
|
|
|
43
|
+
async function resolveIdentity(): Promise<WorkerIdentity | null> {
|
|
44
|
+
return getIdentity() ?? await loadWorkerIdentity();
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
return [
|
|
27
48
|
{
|
|
28
49
|
name: "teamclaw_ask_peer",
|
|
@@ -38,7 +59,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
38
59
|
references: Type.Optional(Type.Array(Type.String({ description: "Relevant task IDs, file paths, or artifact references" }))),
|
|
39
60
|
}),
|
|
40
61
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
41
|
-
const identity =
|
|
62
|
+
const identity = await resolveIdentity();
|
|
42
63
|
if (!identity) {
|
|
43
64
|
return { content: [{ type: "text" as const, text: "Not registered with a team. Cannot send messages." }] };
|
|
44
65
|
}
|
|
@@ -101,7 +122,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
101
122
|
references: Type.Optional(Type.Array(Type.String({ description: "Relevant task IDs, file paths, or artifact references" }))),
|
|
102
123
|
}),
|
|
103
124
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
104
|
-
const identity =
|
|
125
|
+
const identity = await resolveIdentity();
|
|
105
126
|
if (!identity) {
|
|
106
127
|
return { content: [{ type: "text" as const, text: "Not registered with a team." }] };
|
|
107
128
|
}
|
|
@@ -158,7 +179,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
158
179
|
references: Type.Optional(Type.Array(Type.String({ description: "Relevant file paths, artifacts, or checks to review" }))),
|
|
159
180
|
}),
|
|
160
181
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
161
|
-
const identity =
|
|
182
|
+
const identity = await resolveIdentity();
|
|
162
183
|
if (!identity) {
|
|
163
184
|
return { content: [{ type: "text" as const, text: "Not registered with a team." }] };
|
|
164
185
|
}
|
|
@@ -219,7 +240,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
219
240
|
artifacts: Type.Optional(Type.Array(Type.String({ description: "Files, task IDs, or artifacts the next role should inspect first" }))),
|
|
220
241
|
}),
|
|
221
242
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
222
|
-
const identity =
|
|
243
|
+
const identity = await resolveIdentity();
|
|
223
244
|
if (!identity) {
|
|
224
245
|
return { content: [{ type: "text" as const, text: "Not registered with a team." }] };
|
|
225
246
|
}
|
|
@@ -298,7 +319,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
298
319
|
notes: Type.Optional(Type.String({ description: "Optional extra delivery notes" })),
|
|
299
320
|
}),
|
|
300
321
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
301
|
-
const identity =
|
|
322
|
+
const identity = await resolveIdentity();
|
|
302
323
|
if (!identity) {
|
|
303
324
|
return { content: [{ type: "text" as const, text: "Not registered with a team." }] };
|
|
304
325
|
}
|
|
@@ -344,6 +365,68 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
344
365
|
}
|
|
345
366
|
},
|
|
346
367
|
},
|
|
368
|
+
{
|
|
369
|
+
name: "teamclaw_request_parallel_help",
|
|
370
|
+
label: "Request Parallel Help",
|
|
371
|
+
description: "Ask the controller to spawn more same-role or target-role workers for parallel work on this requirement",
|
|
372
|
+
parameters: Type.Object({
|
|
373
|
+
taskId: Type.String({ description: "Current task ID" }),
|
|
374
|
+
reason: Type.String({ description: "Why this task should be split across more workers now" }),
|
|
375
|
+
requestedWorkerCount: Type.Optional(Type.Number({ description: "Desired total worker count for this role after expansion" })),
|
|
376
|
+
targetRole: Type.Optional(Type.String({ description: "Role that should receive more workers; defaults to the current worker role" })),
|
|
377
|
+
suggestedWorkstreams: Type.Optional(Type.Array(Type.String({ description: "Concrete parallel workstreams or module slices the controller should fan out" }))),
|
|
378
|
+
}),
|
|
379
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
380
|
+
const identity = await resolveIdentity();
|
|
381
|
+
if (!identity) {
|
|
382
|
+
return { content: [{ type: "text" as const, text: "Not registered with a team." }] };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const taskId = String(params.taskId ?? "");
|
|
386
|
+
const reason = String(params.reason ?? "").trim();
|
|
387
|
+
const targetRole = typeof params.targetRole === "string" && params.targetRole.trim()
|
|
388
|
+
? params.targetRole.trim()
|
|
389
|
+
: identity.role;
|
|
390
|
+
const requestedWorkerCount = typeof params.requestedWorkerCount === "number"
|
|
391
|
+
? Math.max(2, Math.min(10, Math.floor(params.requestedWorkerCount)))
|
|
392
|
+
: undefined;
|
|
393
|
+
const suggestedWorkstreams = Array.isArray(params.suggestedWorkstreams)
|
|
394
|
+
? params.suggestedWorkstreams.map((entry) => String(entry ?? "").trim()).filter(Boolean)
|
|
395
|
+
: [];
|
|
396
|
+
|
|
397
|
+
if (!taskId || !reason) {
|
|
398
|
+
return { content: [{ type: "text" as const, text: "taskId and reason are required." }] };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const res = await fetch(`${identity.controllerUrl}/api/v1/tasks/${taskId}/parallel-help`, {
|
|
403
|
+
method: "POST",
|
|
404
|
+
headers: { "Content-Type": "application/json" },
|
|
405
|
+
body: JSON.stringify({
|
|
406
|
+
requestedBy: identity.workerId,
|
|
407
|
+
requestedByRole: identity.role,
|
|
408
|
+
targetRole,
|
|
409
|
+
reason,
|
|
410
|
+
requestedWorkerCount,
|
|
411
|
+
suggestedWorkstreams,
|
|
412
|
+
}),
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
if (!res.ok) {
|
|
416
|
+
return { content: [{ type: "text" as const, text: `Failed to request parallel help: ${res.status}` }] };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
content: [{
|
|
421
|
+
type: "text" as const,
|
|
422
|
+
text: `Parallel help requested for ${taskId}${requestedWorkerCount ? ` (target ${targetRole} workers: ${requestedWorkerCount})` : ""}.`,
|
|
423
|
+
}],
|
|
424
|
+
};
|
|
425
|
+
} catch (err) {
|
|
426
|
+
return { content: [{ type: "text" as const, text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
},
|
|
347
430
|
{
|
|
348
431
|
name: "teamclaw_request_clarification",
|
|
349
432
|
label: "Request Clarification",
|
|
@@ -372,7 +455,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
372
455
|
})),
|
|
373
456
|
}),
|
|
374
457
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
375
|
-
const identity =
|
|
458
|
+
const identity = await resolveIdentity();
|
|
376
459
|
if (!identity) {
|
|
377
460
|
return { content: [{ type: "text" as const, text: "Not registered with a team." }] };
|
|
378
461
|
}
|
|
@@ -419,7 +502,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
419
502
|
description: "Get current team status including all workers and tasks",
|
|
420
503
|
parameters: Type.Object({}),
|
|
421
504
|
async execute(_id: string) {
|
|
422
|
-
const identity =
|
|
505
|
+
const identity = await resolveIdentity();
|
|
423
506
|
if (!identity) {
|
|
424
507
|
return { content: [{ type: "text" as const, text: "Not registered with a team." }] };
|
|
425
508
|
}
|
|
@@ -443,6 +526,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
443
526
|
parameters: Type.Object({
|
|
444
527
|
taskId: Type.String({ description: "Task ID" }),
|
|
445
528
|
progress: Type.Optional(Type.String({ description: "Progress update message" })),
|
|
529
|
+
message: Type.Optional(Type.String({ description: "Alias for progress when the runtime sends a generic message field" })),
|
|
446
530
|
status: Type.Optional(Type.String({ description: "Optional non-terminal status: in_progress or review. Do not use completed or failed here." })),
|
|
447
531
|
summary: Type.Optional(Type.String({ description: "Short structured progress summary" })),
|
|
448
532
|
currentStep: Type.Optional(Type.String({ description: "What the worker is doing right now" })),
|
|
@@ -450,19 +534,19 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
450
534
|
blockers: Type.Optional(Type.Array(Type.String({ description: "Any blockers slowing progress" }))),
|
|
451
535
|
}),
|
|
452
536
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
453
|
-
const identity =
|
|
537
|
+
const identity = await resolveIdentity();
|
|
454
538
|
if (!identity) {
|
|
455
539
|
return { content: [{ type: "text" as const, text: "Not registered with a team." }] };
|
|
456
540
|
}
|
|
457
541
|
|
|
458
542
|
const taskId = String(params.taskId ?? "");
|
|
459
|
-
const progress =
|
|
543
|
+
const progress = normalizeProgressText(params);
|
|
460
544
|
const status = typeof params.status === "string" ? params.status : undefined;
|
|
461
545
|
|
|
462
546
|
if (!taskId) {
|
|
463
547
|
return { content: [{ type: "text" as const, text: "taskId is required." }] };
|
|
464
548
|
}
|
|
465
|
-
if (!progress
|
|
549
|
+
if (!progress) {
|
|
466
550
|
return { content: [{ type: "text" as const, text: "progress or summary is required." }] };
|
|
467
551
|
}
|
|
468
552
|
if (status && !ALLOWED_PROGRESS_STATUSES.has(status)) {
|