@teamclaws/teamclaw 2026.4.2-3 → 2026.4.2-5
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 +10 -1
- package/cli.mjs +5 -5
- package/openclaw.plugin.json +2 -2
- package/package.json +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 +1 -1
- package/src/controller/controller-service.ts +1 -0
- package/src/controller/controller-tools.ts +13 -0
- package/src/controller/http-server.ts +362 -60
- package/src/controller/orchestration-manifest.ts +20 -1
- package/src/controller/prompt-injector.ts +10 -1
- package/src/controller/worker-provisioning.ts +1 -1
- package/src/openclaw-workspace.ts +7 -3
- package/src/state.ts +6 -0
- package/src/types.ts +32 -0
package/README.md
CHANGED
|
@@ -71,6 +71,15 @@ For maintainers, TeamClaw now uses two release paths:
|
|
|
71
71
|
1. **npm package** — published by GitHub Actions via `.github/workflows/teamclaw-plugin-npm-release.yml`
|
|
72
72
|
2. **ClawHub code plugin + bundled skills** — published manually from the CLI
|
|
73
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
|
+
|
|
74
83
|
Before either release path, sync and validate the generated plugin manifest:
|
|
75
84
|
|
|
76
85
|
```bash
|
|
@@ -101,7 +110,7 @@ For a first-time setup, the safest path is:
|
|
|
101
110
|
2. Validate the workflow with a small smoke-test task
|
|
102
111
|
3. Expand to external workers, Docker, or Kubernetes after the basics are working
|
|
103
112
|
|
|
104
|
-
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.
|
|
105
114
|
|
|
106
115
|
## Documentation
|
|
107
116
|
|
package/cli.mjs
CHANGED
|
@@ -64,7 +64,7 @@ const INSTALL_MODE_OPTIONS = [
|
|
|
64
64
|
{
|
|
65
65
|
value: "controller-manual",
|
|
66
66
|
label: "Local controller + default on-demand workers",
|
|
67
|
-
hint: "Lean same-host setup;
|
|
67
|
+
hint: "Lean same-host setup; writes workerProvisioningRoles=[] and workerProvisioningMaxPerRole=1.",
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
value: "controller-docker",
|
|
@@ -1363,7 +1363,7 @@ async function collectInstallChoices(configPath, config, prompter, options) {
|
|
|
1363
1363
|
|
|
1364
1364
|
const provisioningRoles = await promptOptionalRoleList(
|
|
1365
1365
|
prompter,
|
|
1366
|
-
"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)",
|
|
1367
1367
|
resolveDefaultProvisioningRoles(existingTeamClaw),
|
|
1368
1368
|
);
|
|
1369
1369
|
const maxPerRole = await prompter.number({
|
|
@@ -1371,7 +1371,7 @@ async function collectInstallChoices(configPath, config, prompter, options) {
|
|
|
1371
1371
|
defaultValue:
|
|
1372
1372
|
typeof existingTeamClaw.workerProvisioningMaxPerRole === "number" && existingTeamClaw.workerProvisioningMaxPerRole >= 1
|
|
1373
1373
|
? existingTeamClaw.workerProvisioningMaxPerRole
|
|
1374
|
-
:
|
|
1374
|
+
: 10,
|
|
1375
1375
|
min: 1,
|
|
1376
1376
|
max: 50,
|
|
1377
1377
|
});
|
|
@@ -1677,7 +1677,7 @@ function applyInstallerChoices(config, choices, configPath) {
|
|
|
1677
1677
|
teamclawConfig.workerProvisioningDisabled = true;
|
|
1678
1678
|
teamclawConfig.workerProvisioningControllerUrl = "";
|
|
1679
1679
|
teamclawConfig.workerProvisioningRoles = [];
|
|
1680
|
-
teamclawConfig.workerProvisioningMaxPerRole =
|
|
1680
|
+
teamclawConfig.workerProvisioningMaxPerRole = 10;
|
|
1681
1681
|
teamclawConfig.workerProvisioningImage = "";
|
|
1682
1682
|
teamclawConfig.workerProvisioningPassEnv = [];
|
|
1683
1683
|
teamclawConfig.workerProvisioningExtraEnv = {};
|
|
@@ -1697,7 +1697,7 @@ function applyInstallerChoices(config, choices, configPath) {
|
|
|
1697
1697
|
teamclawConfig.workerProvisioningDisabled = false;
|
|
1698
1698
|
teamclawConfig.workerProvisioningControllerUrl = "";
|
|
1699
1699
|
teamclawConfig.workerProvisioningRoles = [];
|
|
1700
|
-
teamclawConfig.workerProvisioningMaxPerRole =
|
|
1700
|
+
teamclawConfig.workerProvisioningMaxPerRole = 10;
|
|
1701
1701
|
teamclawConfig.workerProvisioningImage = "";
|
|
1702
1702
|
teamclawConfig.workerProvisioningPassEnv = [];
|
|
1703
1703
|
teamclawConfig.workerProvisioningExtraEnv = {};
|
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-5",
|
|
6
6
|
"skills": [
|
|
7
7
|
"./skills"
|
|
8
8
|
],
|
|
@@ -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": [
|
package/package.json
CHANGED
|
@@ -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
|
@@ -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,
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
import { buildControllerNoWorkersMessage, hasOnDemandWorkerProvisioning, shouldBlockControllerWithoutWorkers } from "./controller-capacity.js";
|
|
11
11
|
import {
|
|
12
12
|
normalizeClarificationQuestionSchemas,
|
|
13
|
+
normalizeManifestCompletionOpportunities,
|
|
13
14
|
normalizeManifestCreatedTasks,
|
|
14
15
|
normalizeManifestDeferredTasks,
|
|
15
16
|
normalizeManifestRoleList,
|
|
@@ -130,6 +131,7 @@ export function createControllerTools(deps: ControllerToolsDeps) {
|
|
|
130
131
|
description: Type.String({ description: "Execution-ready task description with scope, expected deliverable, constraints, resolved clarifications, and no unmet predecessor dependency" }),
|
|
131
132
|
priority: Type.Optional(Type.String({ description: "Priority: low, medium, high, critical" })),
|
|
132
133
|
assignedRole: Type.Optional(Type.String({ description: "Exact target role ID (pm, architect, developer, qa, release-engineer, infra-engineer, devops, security-engineer, designer, marketing)" })),
|
|
134
|
+
projectName: Type.Optional(Type.String({ description: "Stable project key to reuse or create for this task (for example: ggcode, todo-rest-api)." })),
|
|
133
135
|
recommendedSkills: Type.Optional(
|
|
134
136
|
Type.Array(
|
|
135
137
|
Type.String({
|
|
@@ -184,6 +186,7 @@ export function createControllerTools(deps: ControllerToolsDeps) {
|
|
|
184
186
|
description,
|
|
185
187
|
priority: params.priority ?? "medium",
|
|
186
188
|
assignedRole: params.assignedRole ?? undefined,
|
|
189
|
+
projectName: typeof params.projectName === "string" ? params.projectName : undefined,
|
|
187
190
|
recommendedSkills: Array.isArray(params.recommendedSkills) ? params.recommendedSkills : undefined,
|
|
188
191
|
createdBy: "controller",
|
|
189
192
|
controllerSessionKey: normalizedSessionKey || undefined,
|
|
@@ -277,6 +280,15 @@ export function createControllerTools(deps: ControllerToolsDeps) {
|
|
|
277
280
|
),
|
|
278
281
|
handoffPlan: Type.Optional(Type.String({ description: "Brief note about how workers should report progress/handoffs across this flow" })),
|
|
279
282
|
notes: Type.Optional(Type.String({ description: "Additional orchestration notes for the human/controller log" })),
|
|
283
|
+
completionOpportunities: Type.Optional(
|
|
284
|
+
Type.Array(
|
|
285
|
+
Type.Object({
|
|
286
|
+
title: Type.String({ description: "Human-facing label for an adjacent optional next step after delivery" }),
|
|
287
|
+
value: Type.String({ description: "Stable option value for this next step" }),
|
|
288
|
+
summary: Type.String({ description: "What TeamClaw could continue doing if selected" }),
|
|
289
|
+
}),
|
|
290
|
+
),
|
|
291
|
+
),
|
|
280
292
|
requirementFullyComplete: Type.Optional(Type.Boolean({ description: "Set to true when the entire human requirement is fully satisfied — all tasks completed, no deferred tasks remaining, no follow-ups needed" })),
|
|
281
293
|
}),
|
|
282
294
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
@@ -309,6 +321,7 @@ export function createControllerTools(deps: ControllerToolsDeps) {
|
|
|
309
321
|
deferredTasks: normalizeManifestDeferredTasks(params.deferredTasks),
|
|
310
322
|
handoffPlan: normalizeOptionalManifestText(params.handoffPlan),
|
|
311
323
|
notes: normalizeOptionalManifestText(params.notes),
|
|
324
|
+
completionOpportunities: normalizeManifestCompletionOpportunities(params.completionOpportunities),
|
|
312
325
|
requirementFullyComplete: Boolean(params.requirementFullyComplete),
|
|
313
326
|
};
|
|
314
327
|
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
resolveTeamClawAgentWorkspaceRootDir,
|
|
64
64
|
resolveTeamClawWorkspaceDir,
|
|
65
65
|
resolveTeamClawProjectsDir,
|
|
66
|
+
deriveStableProjectKey,
|
|
66
67
|
deriveProjectSlug,
|
|
67
68
|
} from "../openclaw-workspace.js";
|
|
68
69
|
import { resolvePreferredLanAddress } from "../networking.js";
|
|
@@ -92,6 +93,7 @@ const CONTROLLER_INTAKE_SESSION_PREFIX = "teamclaw-controller-web:";
|
|
|
92
93
|
const CONTROLLER_INTAKE_AGENT_SESSION_RE = /^agent:[^:]+:(teamclaw-controller-web:[a-zA-Z0-9:_-]{1,120})$/;
|
|
93
94
|
const CONTROLLER_RUN_WAIT_SLICE_MS = 30_000;
|
|
94
95
|
const EXISTING_PROJECT_REUSE_HINT_RE = /\b(existing|optimi(?:s|z)e|optimi(?:s|z)ation|improvement|enhanc(?:e|ement)|follow[- ]?up|extend|update|bugfix|bug fix)\b/iu;
|
|
96
|
+
const GENERIC_PROJECT_FOLLOW_UP_RE = /\b(continue|continuing|next step|what else|what's next|follow[- ]?up|more|remaining)\b|继续|接下来|下一步|还有哪些|还可以做什么/u;
|
|
95
97
|
const PROJECT_MATCH_STOPWORDS = new Set([
|
|
96
98
|
"a",
|
|
97
99
|
"an",
|
|
@@ -459,15 +461,19 @@ function createControllerRun(
|
|
|
459
461
|
): ControllerRunInfo {
|
|
460
462
|
const now = Date.now();
|
|
461
463
|
const existingState = deps.getTeamState();
|
|
462
|
-
const
|
|
463
|
-
?
|
|
464
|
-
:
|
|
465
|
-
??
|
|
464
|
+
const inheritedProject = options?.sourceTaskId
|
|
465
|
+
? resolveProjectIdentityForTaskId(options.sourceTaskId, existingState)
|
|
466
|
+
: resolveProjectIdentityForSession(sessionKey, existingState)
|
|
467
|
+
?? resolveExistingProjectIdentityFromMessage(message, existingState);
|
|
468
|
+
const explicitProjectId = extractExplicitProjectNameHint(message);
|
|
469
|
+
const projectId = inheritedProject?.projectId ?? explicitProjectId;
|
|
470
|
+
const projectDir = inheritedProject?.projectDir ?? projectId ?? deriveProjectSlug(message);
|
|
466
471
|
const run: ControllerRunInfo = {
|
|
467
472
|
id: generateId(),
|
|
468
473
|
title: buildControllerRunTitle(message, options?.source ?? "human", options?.sourceTaskTitle),
|
|
469
474
|
sessionKey,
|
|
470
|
-
|
|
475
|
+
projectId: projectId || undefined,
|
|
476
|
+
projectDir,
|
|
471
477
|
source: options?.source ?? "human",
|
|
472
478
|
sourceTaskId: options?.sourceTaskId,
|
|
473
479
|
sourceTaskTitle: options?.sourceTaskTitle,
|
|
@@ -479,6 +485,13 @@ function createControllerRun(
|
|
|
479
485
|
};
|
|
480
486
|
|
|
481
487
|
const state = deps.updateTeamState((teamState) => {
|
|
488
|
+
syncProjectRegistryEntry(teamState, {
|
|
489
|
+
projectId: run.projectId,
|
|
490
|
+
projectDir: run.projectDir,
|
|
491
|
+
aliases: [run.projectId, extractExplicitProjectNameHint(message)],
|
|
492
|
+
summary: run.title,
|
|
493
|
+
updatedAt: now,
|
|
494
|
+
});
|
|
482
495
|
teamState.controllerRuns[run.id] = run;
|
|
483
496
|
trimControllerRuns(teamState);
|
|
484
497
|
});
|
|
@@ -487,20 +500,17 @@ function createControllerRun(
|
|
|
487
500
|
return createdRun;
|
|
488
501
|
}
|
|
489
502
|
|
|
490
|
-
function
|
|
503
|
+
function resolveExistingProjectIdentityFromMessage(
|
|
491
504
|
message: string,
|
|
492
505
|
state: TeamState | null,
|
|
493
|
-
): string | undefined {
|
|
494
|
-
if (!EXISTING_PROJECT_REUSE_HINT_RE.test(message)) {
|
|
495
|
-
return undefined;
|
|
496
|
-
}
|
|
506
|
+
): { projectId?: string; projectDir?: string } | undefined {
|
|
497
507
|
const normalizedMessage = normalizeProjectMatchingText(message);
|
|
498
508
|
if (!normalizedMessage) {
|
|
499
509
|
return undefined;
|
|
500
510
|
}
|
|
501
|
-
const explicitAlias = normalizeProjectMatchingText(
|
|
511
|
+
const explicitAlias = normalizeProjectMatchingText(extractExplicitProjectNameHint(message) ?? "");
|
|
502
512
|
|
|
503
|
-
let best: { projectDir: string; score: number; updatedAt: number } | null = null;
|
|
513
|
+
let best: { projectId?: string; projectDir: string; score: number; updatedAt: number } | null = null;
|
|
504
514
|
const candidates = collectExistingProjectCandidates(state);
|
|
505
515
|
for (const candidate of candidates) {
|
|
506
516
|
const normalizedAliases = Array.from(candidate.aliases)
|
|
@@ -532,27 +542,47 @@ function resolveExistingProjectDirFromMessage(
|
|
|
532
542
|
}
|
|
533
543
|
if (!best || totalScore > best.score || (totalScore === best.score && candidate.updatedAt > best.updatedAt)) {
|
|
534
544
|
best = {
|
|
545
|
+
projectId: candidate.projectId,
|
|
535
546
|
projectDir: candidate.projectDir,
|
|
536
547
|
score: totalScore,
|
|
537
548
|
updatedAt: candidate.updatedAt,
|
|
538
549
|
};
|
|
539
550
|
}
|
|
540
551
|
}
|
|
541
|
-
|
|
552
|
+
if (best) {
|
|
553
|
+
return { projectId: best.projectId, projectDir: best.projectDir };
|
|
554
|
+
}
|
|
555
|
+
if ((EXISTING_PROJECT_REUSE_HINT_RE.test(message) || GENERIC_PROJECT_FOLLOW_UP_RE.test(message)) && candidates.length === 1) {
|
|
556
|
+
return {
|
|
557
|
+
projectId: candidates[0]?.projectId,
|
|
558
|
+
projectDir: candidates[0]?.projectDir,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return undefined;
|
|
542
562
|
}
|
|
543
563
|
|
|
544
564
|
function collectExistingProjectCandidates(state: TeamState | null): Array<{
|
|
565
|
+
projectId?: string;
|
|
545
566
|
projectDir: string;
|
|
546
567
|
aliases: Set<string>;
|
|
547
568
|
searchText: string;
|
|
548
569
|
updatedAt: number;
|
|
549
570
|
}> {
|
|
550
|
-
const byProjectDir = new Map<string, { aliases: Set<string>; texts: string[]; updatedAt: number }>();
|
|
551
|
-
const addCandidate = (
|
|
571
|
+
const byProjectDir = new Map<string, { projectId?: string; aliases: Set<string>; texts: string[]; updatedAt: number }>();
|
|
572
|
+
const addCandidate = (
|
|
573
|
+
projectDir: string | undefined,
|
|
574
|
+
text: string | undefined,
|
|
575
|
+
updatedAt: number,
|
|
576
|
+
alias?: string | null,
|
|
577
|
+
projectId?: string,
|
|
578
|
+
) => {
|
|
552
579
|
if (!projectDir) {
|
|
553
580
|
return;
|
|
554
581
|
}
|
|
555
|
-
const entry = byProjectDir.get(projectDir) ?? { aliases: new Set<string>(), texts: [], updatedAt: 0 };
|
|
582
|
+
const entry = byProjectDir.get(projectDir) ?? { projectId, aliases: new Set<string>(), texts: [], updatedAt: 0 };
|
|
583
|
+
if (!entry.projectId && projectId) {
|
|
584
|
+
entry.projectId = projectId;
|
|
585
|
+
}
|
|
556
586
|
if (text && text.trim()) {
|
|
557
587
|
entry.texts.push(text);
|
|
558
588
|
}
|
|
@@ -563,17 +593,25 @@ function collectExistingProjectCandidates(state: TeamState | null): Array<{
|
|
|
563
593
|
byProjectDir.set(projectDir, entry);
|
|
564
594
|
};
|
|
565
595
|
|
|
596
|
+
for (const project of Object.values(state?.projects ?? {})) {
|
|
597
|
+
addCandidate(project.projectDir, project.summary, project.updatedAt, project.id, project.id);
|
|
598
|
+
for (const alias of project.aliases ?? []) {
|
|
599
|
+
addCandidate(project.projectDir, alias, project.updatedAt, alias, project.id);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
566
602
|
for (const run of Object.values(state?.controllerRuns ?? {})) {
|
|
567
|
-
addCandidate(run.projectDir, run.request, run.updatedAt, extractProjectAliasFromText(run.request));
|
|
568
|
-
addCandidate(run.projectDir, run.title, run.updatedAt, extractProjectAliasFromText(run.title));
|
|
603
|
+
addCandidate(run.projectDir, run.request, run.updatedAt, extractProjectAliasFromText(run.request), run.projectId);
|
|
604
|
+
addCandidate(run.projectDir, run.title, run.updatedAt, extractProjectAliasFromText(run.title), run.projectId);
|
|
605
|
+
addCandidate(run.projectDir, run.manifest?.projectName, run.updatedAt, run.manifest?.projectName, run.projectId);
|
|
569
606
|
}
|
|
570
607
|
for (const task of Object.values(state?.tasks ?? {})) {
|
|
571
|
-
addCandidate(task.projectDir, task.title, task.updatedAt, extractProjectAliasFromText(task.title));
|
|
572
|
-
addCandidate(task.projectDir, task.description, task.updatedAt, extractProjectAliasFromText(task.description));
|
|
573
|
-
addCandidate(task.projectDir, task.resultContract?.summary, task.updatedAt);
|
|
608
|
+
addCandidate(task.projectDir, task.title, task.updatedAt, extractProjectAliasFromText(task.title), task.projectId);
|
|
609
|
+
addCandidate(task.projectDir, task.description, task.updatedAt, extractProjectAliasFromText(task.description), task.projectId);
|
|
610
|
+
addCandidate(task.projectDir, task.resultContract?.summary, task.updatedAt, null, task.projectId);
|
|
574
611
|
}
|
|
575
612
|
|
|
576
613
|
return Array.from(byProjectDir.entries()).map(([projectDir, entry]) => ({
|
|
614
|
+
projectId: entry.projectId,
|
|
577
615
|
projectDir,
|
|
578
616
|
aliases: entry.aliases,
|
|
579
617
|
searchText: entry.texts.join("\n"),
|
|
@@ -581,6 +619,119 @@ function collectExistingProjectCandidates(state: TeamState | null): Array<{
|
|
|
581
619
|
}));
|
|
582
620
|
}
|
|
583
621
|
|
|
622
|
+
function extractExplicitProjectNameHint(text: string | undefined): string | undefined {
|
|
623
|
+
const value = String(text ?? "");
|
|
624
|
+
const namedMatch = value.match(/(?:品牌|brand|project\s*name|产品名|名字)\s*[::]?\s*[`"'“”]?([a-z][a-z0-9_-]{1,39})[`"'“”]?/iu);
|
|
625
|
+
if (namedMatch?.[1]) {
|
|
626
|
+
return deriveStableProjectKey(namedMatch[1]);
|
|
627
|
+
}
|
|
628
|
+
const inlineNames = Array.from(value.matchAll(/`([a-z][a-z0-9_-]{1,39})`/giu))
|
|
629
|
+
.map((match) => deriveStableProjectKey(match[1]))
|
|
630
|
+
.filter(Boolean);
|
|
631
|
+
return inlineNames.length === 1 ? inlineNames[0] : undefined;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function findProjectRecordByDir(state: TeamState | null, projectDir: string | undefined) {
|
|
635
|
+
if (!state || !projectDir) {
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
return Object.values(state.projects ?? {}).find((project) => project.projectDir === projectDir);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function syncProjectRegistryEntry(
|
|
642
|
+
state: TeamState,
|
|
643
|
+
input: {
|
|
644
|
+
projectId?: string;
|
|
645
|
+
projectDir?: string;
|
|
646
|
+
aliases?: Array<string | undefined | null>;
|
|
647
|
+
summary?: string;
|
|
648
|
+
updatedAt: number;
|
|
649
|
+
},
|
|
650
|
+
): { projectId?: string; projectDir?: string } {
|
|
651
|
+
state.projects = state.projects && typeof state.projects === "object" ? state.projects : {};
|
|
652
|
+
let normalizedProjectId = deriveStableProjectKey(input.projectId ?? "");
|
|
653
|
+
let existing = normalizedProjectId ? state.projects[normalizedProjectId] : undefined;
|
|
654
|
+
const byDir = findProjectRecordByDir(state, input.projectDir);
|
|
655
|
+
if (!existing && byDir) {
|
|
656
|
+
existing = byDir;
|
|
657
|
+
}
|
|
658
|
+
if (!existing && input.projectDir) {
|
|
659
|
+
normalizedProjectId = normalizedProjectId || deriveStableProjectKey(input.projectDir);
|
|
660
|
+
if (normalizedProjectId) {
|
|
661
|
+
existing = {
|
|
662
|
+
id: normalizedProjectId,
|
|
663
|
+
projectDir: input.projectDir,
|
|
664
|
+
aliases: [],
|
|
665
|
+
createdAt: input.updatedAt,
|
|
666
|
+
updatedAt: input.updatedAt,
|
|
667
|
+
lastUsedAt: input.updatedAt,
|
|
668
|
+
};
|
|
669
|
+
state.projects[normalizedProjectId] = existing;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (!existing) {
|
|
673
|
+
return { projectId: normalizedProjectId || undefined, projectDir: input.projectDir };
|
|
674
|
+
}
|
|
675
|
+
if (normalizedProjectId && existing.id !== normalizedProjectId && !state.projects[normalizedProjectId]) {
|
|
676
|
+
delete state.projects[existing.id];
|
|
677
|
+
existing.id = normalizedProjectId;
|
|
678
|
+
state.projects[normalizedProjectId] = existing;
|
|
679
|
+
}
|
|
680
|
+
if (!existing.projectDir && input.projectDir) {
|
|
681
|
+
existing.projectDir = input.projectDir;
|
|
682
|
+
}
|
|
683
|
+
const aliasSet = new Set(existing.aliases ?? []);
|
|
684
|
+
aliasSet.add(existing.id);
|
|
685
|
+
if (existing.projectDir) {
|
|
686
|
+
aliasSet.add(existing.projectDir);
|
|
687
|
+
}
|
|
688
|
+
for (const alias of input.aliases ?? []) {
|
|
689
|
+
const normalizedAlias = deriveStableProjectKey(String(alias ?? ""));
|
|
690
|
+
if (normalizedAlias) {
|
|
691
|
+
aliasSet.add(normalizedAlias);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
existing.aliases = Array.from(aliasSet);
|
|
695
|
+
existing.summary = input.summary || existing.summary;
|
|
696
|
+
existing.updatedAt = Math.max(existing.updatedAt || 0, input.updatedAt);
|
|
697
|
+
existing.lastUsedAt = Math.max(existing.lastUsedAt || 0, input.updatedAt);
|
|
698
|
+
return { projectId: existing.id, projectDir: existing.projectDir };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function resolveProjectIdentityForSession(
|
|
702
|
+
sessionKey: string,
|
|
703
|
+
state: TeamState | null,
|
|
704
|
+
): { projectId?: string; projectDir?: string } | undefined {
|
|
705
|
+
const runId = findLatestControllerRunIdForSession(sessionKey, state, { preferActive: true });
|
|
706
|
+
if (!runId) {
|
|
707
|
+
return undefined;
|
|
708
|
+
}
|
|
709
|
+
const run = state?.controllerRuns[runId];
|
|
710
|
+
if (!run) {
|
|
711
|
+
return undefined;
|
|
712
|
+
}
|
|
713
|
+
const project = findProjectRecordByDir(state, run.projectDir);
|
|
714
|
+
return {
|
|
715
|
+
projectId: run.projectId ?? project?.id,
|
|
716
|
+
projectDir: run.projectDir ?? project?.projectDir,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function resolveProjectIdentityForTaskId(
|
|
721
|
+
taskId: string,
|
|
722
|
+
state: TeamState | null,
|
|
723
|
+
): { projectId?: string; projectDir?: string } | undefined {
|
|
724
|
+
const task = state?.tasks?.[taskId];
|
|
725
|
+
if (!task) {
|
|
726
|
+
return undefined;
|
|
727
|
+
}
|
|
728
|
+
const project = findProjectRecordByDir(state, task.projectDir);
|
|
729
|
+
return {
|
|
730
|
+
projectId: task.projectId ?? project?.id,
|
|
731
|
+
projectDir: task.projectDir ?? project?.projectDir,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
584
735
|
function extractProjectAliasFromText(text: string | undefined): string | null {
|
|
585
736
|
const firstMeaningfulLine = String(text ?? "")
|
|
586
737
|
.split(/\n+/u)
|
|
@@ -946,8 +1097,7 @@ function resolveProjectDirForSession(
|
|
|
946
1097
|
sessionKey: string,
|
|
947
1098
|
state: TeamState | null,
|
|
948
1099
|
): string | undefined {
|
|
949
|
-
|
|
950
|
-
return runId ? state?.controllerRuns[runId]?.projectDir : undefined;
|
|
1100
|
+
return resolveProjectIdentityForSession(sessionKey, state)?.projectDir;
|
|
951
1101
|
}
|
|
952
1102
|
|
|
953
1103
|
function resolveControllerWorkflowSessionKey(task: TaskInfo, state: TeamState | null): string | undefined {
|
|
@@ -1121,15 +1271,16 @@ function buildBackfilledControllerManifest(
|
|
|
1121
1271
|
for (const roleId of inferManifestRolesFromText(request)) {
|
|
1122
1272
|
inferredRoles.add(roleId);
|
|
1123
1273
|
}
|
|
1124
|
-
// When no roles could be inferred at all
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1274
|
+
// When no roles could be inferred at all, default the first-pass requirement analysis
|
|
1275
|
+
// to architect rather than developer. This keeps repository analysis, feasibility
|
|
1276
|
+
// assessment, and technical decomposition out of implementation-only developer tasks.
|
|
1127
1277
|
if (inferredRoles.size === 0) {
|
|
1128
|
-
inferredRoles.add("
|
|
1278
|
+
inferredRoles.add("architect");
|
|
1129
1279
|
}
|
|
1130
1280
|
const clarificationQuestions = inferClarificationQuestionsFromReply(rawReply);
|
|
1131
1281
|
return {
|
|
1132
1282
|
version: "1.0",
|
|
1283
|
+
projectName: actualCreatedTasks.find((task) => task.projectId)?.projectId,
|
|
1133
1284
|
requirementSummary: request.replace(/\s+/g, " ").trim() || "Controller requirement summary unavailable.",
|
|
1134
1285
|
requiredRoles: Array.from(inferredRoles),
|
|
1135
1286
|
clarificationsNeeded: clarificationQuestions.length > 0 && actualCreatedTasks.length === 0,
|
|
@@ -1158,6 +1309,30 @@ function looksLikeSoftwareRequirement(request: string): boolean {
|
|
|
1158
1309
|
return /(api|backend|frontend|fastapi|react|vue|node|python|typescript|javascript|sql|database|service|app|web|mobile|docker|kubernetes|deploy|测试|系统|平台|接口|服务|数据库|应用|前端|后端)/.test(normalized);
|
|
1159
1310
|
}
|
|
1160
1311
|
|
|
1312
|
+
function isArchitectureFirstRequirement(request: string): boolean {
|
|
1313
|
+
const normalized = request.toLowerCase();
|
|
1314
|
+
return /(analy[sz]e|analysis|assess|feasibility|architecture|architect|design|plan|decompose|migration|port|rewrite|refactor|audit|review|repo|repository|codebase|golang|go\b|typescript|可行性|评估|架构|设计|分析|拆分|迁移|移植|重写|审计|仓库|代码库)/.test(normalized);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function chooseFallbackAssignedRole(manifest: ControllerOrchestrationManifest, request: string): RoleId {
|
|
1318
|
+
const requiredRoles = manifest.requiredRoles;
|
|
1319
|
+
if (requiredRoles.length === 0) {
|
|
1320
|
+
return isArchitectureFirstRequirement(request) ? "architect" : "developer";
|
|
1321
|
+
}
|
|
1322
|
+
if (isArchitectureFirstRequirement(request)) {
|
|
1323
|
+
if (requiredRoles.includes("architect")) {
|
|
1324
|
+
return "architect";
|
|
1325
|
+
}
|
|
1326
|
+
if (requiredRoles.includes("pm")) {
|
|
1327
|
+
return "pm";
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (requiredRoles.includes("developer") && requiredRoles.length === 1) {
|
|
1331
|
+
return "developer";
|
|
1332
|
+
}
|
|
1333
|
+
return requiredRoles[0] ?? (isArchitectureFirstRequirement(request) ? "architect" : "developer");
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1161
1336
|
function buildFallbackControllerTaskTitle(request: string, assignedRole?: RoleId): string {
|
|
1162
1337
|
const firstMeaningfulLine = request
|
|
1163
1338
|
.split(/\n+/)
|
|
@@ -1172,6 +1347,9 @@ function buildFallbackControllerTaskTitle(request: string, assignedRole?: RoleId
|
|
|
1172
1347
|
: `Implement ${capped}`;
|
|
1173
1348
|
}
|
|
1174
1349
|
}
|
|
1350
|
+
if (assignedRole === "architect") {
|
|
1351
|
+
return "Analyze the repository and produce an architecture/feasibility plan";
|
|
1352
|
+
}
|
|
1175
1353
|
return assignedRole === "developer"
|
|
1176
1354
|
? "Implement the requested software deliverable"
|
|
1177
1355
|
: `Perform the requested ${assignedRole || "software"} work`;
|
|
@@ -1185,6 +1363,7 @@ async function createControllerManagedTask(
|
|
|
1185
1363
|
assignedRole?: RoleId;
|
|
1186
1364
|
createdBy: string;
|
|
1187
1365
|
controllerSessionKey?: string;
|
|
1366
|
+
projectName?: string;
|
|
1188
1367
|
recommendedSkills?: string[];
|
|
1189
1368
|
},
|
|
1190
1369
|
deps: ControllerHttpDeps,
|
|
@@ -1196,15 +1375,12 @@ async function createControllerManagedTask(
|
|
|
1196
1375
|
? normalizeControllerIntakeSessionKey(input.controllerSessionKey)
|
|
1197
1376
|
: undefined;
|
|
1198
1377
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
if (!projectDir) {
|
|
1206
|
-
projectDir = deriveProjectSlug(input.title);
|
|
1207
|
-
}
|
|
1378
|
+
const inheritedProject = normalizedSessionKey
|
|
1379
|
+
? resolveProjectIdentityForSession(normalizedSessionKey, deps.getTeamState())
|
|
1380
|
+
: undefined;
|
|
1381
|
+
const explicitProjectId = deriveStableProjectKey(input.projectName ?? "");
|
|
1382
|
+
const projectId = inheritedProject?.projectId ?? explicitProjectId || undefined;
|
|
1383
|
+
const projectDir = inheritedProject?.projectDir ?? projectId ?? deriveProjectSlug(input.title);
|
|
1208
1384
|
|
|
1209
1385
|
const normalizedRecommendedSkills = normalizeRecommendedSkills(input.recommendedSkills ?? []);
|
|
1210
1386
|
const task: TaskInfo = {
|
|
@@ -1217,12 +1393,22 @@ async function createControllerManagedTask(
|
|
|
1217
1393
|
createdBy: input.createdBy,
|
|
1218
1394
|
recommendedSkills: normalizedRecommendedSkills.length > 0 ? normalizedRecommendedSkills : undefined,
|
|
1219
1395
|
controllerSessionKey: normalizedSessionKey,
|
|
1396
|
+
projectId,
|
|
1220
1397
|
projectDir,
|
|
1221
1398
|
createdAt: now,
|
|
1222
1399
|
updatedAt: now,
|
|
1223
1400
|
};
|
|
1224
1401
|
|
|
1225
1402
|
deps.updateTeamState((state) => {
|
|
1403
|
+
const project = syncProjectRegistryEntry(state, {
|
|
1404
|
+
projectId: task.projectId,
|
|
1405
|
+
projectDir: task.projectDir,
|
|
1406
|
+
aliases: [input.projectName, task.projectId],
|
|
1407
|
+
summary: task.title,
|
|
1408
|
+
updatedAt: now,
|
|
1409
|
+
});
|
|
1410
|
+
task.projectId = project.projectId;
|
|
1411
|
+
task.projectDir = project.projectDir;
|
|
1226
1412
|
state.tasks[taskId] = task;
|
|
1227
1413
|
});
|
|
1228
1414
|
recordTaskExecutionEvent(taskId, {
|
|
@@ -1283,9 +1469,7 @@ async function maybeBackfillExecutionReadyTask(
|
|
|
1283
1469
|
return [];
|
|
1284
1470
|
}
|
|
1285
1471
|
|
|
1286
|
-
const assignedRole = manifest
|
|
1287
|
-
? "developer"
|
|
1288
|
-
: manifest.requiredRoles[0];
|
|
1472
|
+
const assignedRole = chooseFallbackAssignedRole(manifest, request);
|
|
1289
1473
|
const task = await createControllerManagedTask({
|
|
1290
1474
|
title: buildFallbackControllerTaskTitle(request, assignedRole),
|
|
1291
1475
|
description: request,
|
|
@@ -1528,17 +1712,43 @@ function normalizeComparableText(value: string): string {
|
|
|
1528
1712
|
function buildManifestClarificationEntries(
|
|
1529
1713
|
manifest: ControllerOrchestrationManifest,
|
|
1530
1714
|
): Array<{ question: string; questionSchema?: ClarificationRequest["questionSchema"] }> {
|
|
1715
|
+
const opportunities = Array.isArray(manifest.completionOpportunities)
|
|
1716
|
+
? manifest.completionOpportunities.filter((entry) => entry.title && entry.value && entry.summary)
|
|
1717
|
+
: [];
|
|
1718
|
+
const completionEntries = manifest.requirementFullyComplete && opportunities.length > 0
|
|
1719
|
+
? [{
|
|
1720
|
+
question: "Would you like TeamClaw to continue with one of these adjacent next steps?",
|
|
1721
|
+
questionSchema: {
|
|
1722
|
+
kind: "single-select" as const,
|
|
1723
|
+
title: "What should TeamClaw do next?",
|
|
1724
|
+
description: opportunities.map((entry) => `- ${entry.title}: ${entry.summary}`).join("\n"),
|
|
1725
|
+
required: false,
|
|
1726
|
+
options: opportunities.map((entry) => ({
|
|
1727
|
+
value: entry.value,
|
|
1728
|
+
label: entry.title,
|
|
1729
|
+
hint: entry.summary,
|
|
1730
|
+
})),
|
|
1731
|
+
allowOther: true,
|
|
1732
|
+
},
|
|
1733
|
+
}]
|
|
1734
|
+
: [];
|
|
1531
1735
|
const normalizedQuestions = manifest.clarificationQuestions
|
|
1532
1736
|
.map((entry) => String(entry || "").trim())
|
|
1533
1737
|
.filter(Boolean);
|
|
1534
1738
|
const schemas = Array.isArray(manifest.clarificationSchemas) ? manifest.clarificationSchemas : [];
|
|
1535
1739
|
if (schemas.length > 0) {
|
|
1536
|
-
return
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1740
|
+
return [
|
|
1741
|
+
...schemas.map((schema, index) => ({
|
|
1742
|
+
question: normalizedQuestions[index] || schema.title,
|
|
1743
|
+
questionSchema: schema,
|
|
1744
|
+
})),
|
|
1745
|
+
...completionEntries,
|
|
1746
|
+
];
|
|
1540
1747
|
}
|
|
1541
|
-
return
|
|
1748
|
+
return [
|
|
1749
|
+
...normalizedQuestions.map((question) => ({ question })),
|
|
1750
|
+
...completionEntries,
|
|
1751
|
+
];
|
|
1542
1752
|
}
|
|
1543
1753
|
|
|
1544
1754
|
function syncControllerRunClarifications(
|
|
@@ -1548,19 +1758,33 @@ function syncControllerRunClarifications(
|
|
|
1548
1758
|
deps: ControllerHttpDeps,
|
|
1549
1759
|
): ClarificationRequest[] {
|
|
1550
1760
|
const entries = buildManifestClarificationEntries(manifest);
|
|
1551
|
-
if (entries.length === 0) {
|
|
1552
|
-
return [];
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
1761
|
const created: ClarificationRequest[] = [];
|
|
1762
|
+
const superseded: ClarificationRequest[] = [];
|
|
1556
1763
|
const now = Date.now();
|
|
1557
1764
|
deps.updateTeamState((state) => {
|
|
1765
|
+
const desiredQuestionKeys = new Set(
|
|
1766
|
+
entries.map((entry) => normalizeComparableText(entry.question)),
|
|
1767
|
+
);
|
|
1558
1768
|
const existingQuestions = new Map(
|
|
1559
1769
|
Object.values(state.clarifications)
|
|
1560
1770
|
.filter((item) => item.controllerRunId === controllerRunId)
|
|
1561
1771
|
.map((item) => [normalizeComparableText(item.question), item]),
|
|
1562
1772
|
);
|
|
1563
1773
|
|
|
1774
|
+
for (const [key, clarification] of existingQuestions.entries()) {
|
|
1775
|
+
if (
|
|
1776
|
+
clarification.status === "pending"
|
|
1777
|
+
&& !desiredQuestionKeys.has(key)
|
|
1778
|
+
) {
|
|
1779
|
+
clarification.status = "answered";
|
|
1780
|
+
clarification.answer = "Automatically superseded by the latest controller clarification set.";
|
|
1781
|
+
clarification.answeredBy = "system";
|
|
1782
|
+
clarification.answeredAt = now;
|
|
1783
|
+
clarification.updatedAt = now;
|
|
1784
|
+
superseded.push({ ...clarification });
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1564
1788
|
for (const entry of entries) {
|
|
1565
1789
|
const key = normalizeComparableText(entry.question);
|
|
1566
1790
|
if (existingQuestions.has(key)) {
|
|
@@ -1588,6 +1812,9 @@ function syncControllerRunClarifications(
|
|
|
1588
1812
|
for (const clarification of created) {
|
|
1589
1813
|
deps.wsServer.broadcastUpdate({ type: "clarification:requested", data: clarification });
|
|
1590
1814
|
}
|
|
1815
|
+
for (const clarification of superseded) {
|
|
1816
|
+
deps.wsServer.broadcastUpdate({ type: "clarification:answered", data: clarification });
|
|
1817
|
+
}
|
|
1591
1818
|
return created;
|
|
1592
1819
|
}
|
|
1593
1820
|
|
|
@@ -1597,6 +1824,7 @@ function reconcileControllerClarifications(deps: ControllerHttpDeps): TeamState
|
|
|
1597
1824
|
return null;
|
|
1598
1825
|
}
|
|
1599
1826
|
|
|
1827
|
+
const superseded: ClarificationRequest[] = [];
|
|
1600
1828
|
deps.updateTeamState((draft) => {
|
|
1601
1829
|
const runsBySession = new Map<string, ControllerRunInfo[]>();
|
|
1602
1830
|
for (const run of Object.values(draft.controllerRuns)) {
|
|
@@ -1627,9 +1855,14 @@ function reconcileControllerClarifications(deps: ControllerHttpDeps): TeamState
|
|
|
1627
1855
|
clar.answeredBy = "system";
|
|
1628
1856
|
clar.answeredAt = supersedingRun.updatedAt;
|
|
1629
1857
|
clar.updatedAt = supersedingRun.updatedAt;
|
|
1858
|
+
superseded.push({ ...clar });
|
|
1630
1859
|
}
|
|
1631
1860
|
});
|
|
1632
1861
|
|
|
1862
|
+
for (const clarification of superseded) {
|
|
1863
|
+
deps.wsServer.broadcastUpdate({ type: "clarification:answered", data: clarification });
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1633
1866
|
for (const run of Object.values(state.controllerRuns)) {
|
|
1634
1867
|
if (!run.manifest?.clarificationsNeeded) {
|
|
1635
1868
|
continue;
|
|
@@ -2263,6 +2496,49 @@ function buildRecommendedSkillsContext(task: TaskInfo): string {
|
|
|
2263
2496
|
].join("\n");
|
|
2264
2497
|
}
|
|
2265
2498
|
|
|
2499
|
+
function buildTaskContextSnapshot(task: TaskInfo, state: TeamState | null): TaskAssignmentPayload["teamContext"] | undefined {
|
|
2500
|
+
if (!state || !task.controllerSessionKey) {
|
|
2501
|
+
return undefined;
|
|
2502
|
+
}
|
|
2503
|
+
const normalizedSessionKey = normalizeControllerIntakeSessionKey(task.controllerSessionKey);
|
|
2504
|
+
const summarize = (candidate: TaskInfo) => ({
|
|
2505
|
+
id: candidate.id,
|
|
2506
|
+
title: candidate.title,
|
|
2507
|
+
assignedRole: candidate.assignedRole,
|
|
2508
|
+
status: candidate.status,
|
|
2509
|
+
summary: summarizeTaskForAssignment(candidate),
|
|
2510
|
+
});
|
|
2511
|
+
const sameSessionTasks = Object.values(state.tasks)
|
|
2512
|
+
.filter((candidate) => candidate.id !== task.id)
|
|
2513
|
+
.filter((candidate) => normalizeControllerIntakeSessionKey(candidate.controllerSessionKey) === normalizedSessionKey);
|
|
2514
|
+
const latestRun = Object.values(state.controllerRuns)
|
|
2515
|
+
.filter((run) => normalizeControllerIntakeSessionKey(run.sessionKey) === normalizedSessionKey)
|
|
2516
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
2517
|
+
return {
|
|
2518
|
+
requirementSummary: latestRun?.manifest?.requirementSummary || latestRun?.request,
|
|
2519
|
+
projectName: latestRun?.manifest?.projectName,
|
|
2520
|
+
requiredRoles: latestRun?.manifest?.requiredRoles,
|
|
2521
|
+
activeTasks: sameSessionTasks.filter((candidate) => candidate.status === "assigned" || candidate.status === "in_progress").slice(0, 8).map(summarize),
|
|
2522
|
+
blockedTasks: sameSessionTasks.filter((candidate) => candidate.status === "blocked").slice(0, 6).map(summarize),
|
|
2523
|
+
recentCompletedTasks: sameSessionTasks
|
|
2524
|
+
.filter((candidate) => candidate.status === "completed")
|
|
2525
|
+
.sort((a, b) => (b.completedAt ?? b.updatedAt) - (a.completedAt ?? a.updatedAt))
|
|
2526
|
+
.slice(0, 8)
|
|
2527
|
+
.map(summarize),
|
|
2528
|
+
pendingClarifications: Object.values(state.clarifications)
|
|
2529
|
+
.filter((clarification) => clarification.status === "pending")
|
|
2530
|
+
.filter((clarification) => normalizeControllerIntakeSessionKey(clarification.controllerSessionKey) === normalizedSessionKey)
|
|
2531
|
+
.slice(0, 5)
|
|
2532
|
+
.map((clarification) => ({
|
|
2533
|
+
question: clarification.question,
|
|
2534
|
+
blockingReason: clarification.blockingReason,
|
|
2535
|
+
})),
|
|
2536
|
+
handoffPlan: latestRun?.manifest?.handoffPlan,
|
|
2537
|
+
notes: latestRun?.manifest?.notes,
|
|
2538
|
+
kickoffSummary: latestRun?.manifest?.kickoffPlan?.summary,
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2266
2542
|
function buildTaskAssignmentDescription(task: TaskInfo, state: TeamState | null, repoInfo?: RepoSyncInfo): string {
|
|
2267
2543
|
const parts = [task.description];
|
|
2268
2544
|
const projectContext = buildProjectDirectoryContext(task.projectDir);
|
|
@@ -3326,6 +3602,7 @@ async function dispatchTaskToWorker(
|
|
|
3326
3602
|
executionSessionKey: executionIdentity.executionSessionKey,
|
|
3327
3603
|
executionIdempotencyKey: executionIdentity.executionIdempotencyKey,
|
|
3328
3604
|
repo: repoInfo,
|
|
3605
|
+
teamContext: buildTaskContextSnapshot(task, state ?? null),
|
|
3329
3606
|
};
|
|
3330
3607
|
|
|
3331
3608
|
const res = await fetch(`${worker.url}/api/v1/tasks/assign`, {
|
|
@@ -3757,6 +4034,9 @@ async function handleRequest(
|
|
|
3757
4034
|
const controllerSessionKey = createdBy === "controller" && typeof body.controllerSessionKey === "string" && body.controllerSessionKey.trim()
|
|
3758
4035
|
? normalizeControllerIntakeSessionKey(body.controllerSessionKey)
|
|
3759
4036
|
: undefined;
|
|
4037
|
+
const explicitProjectName = typeof body.projectName === "string" && body.projectName.trim()
|
|
4038
|
+
? deriveStableProjectKey(body.projectName)
|
|
4039
|
+
: undefined;
|
|
3760
4040
|
const recommendedSkills = normalizeRecommendedSkills(
|
|
3761
4041
|
Array.isArray(body.recommendedSkills) ? body.recommendedSkills.map((entry) => String(entry ?? "")) : [],
|
|
3762
4042
|
);
|
|
@@ -3781,16 +4061,11 @@ async function handleRequest(
|
|
|
3781
4061
|
const now = Date.now();
|
|
3782
4062
|
const repoState = await refreshControllerRepoState(deps);
|
|
3783
4063
|
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
projectDir = parentRun?.projectDir;
|
|
3790
|
-
}
|
|
3791
|
-
if (!projectDir) {
|
|
3792
|
-
projectDir = deriveProjectSlug(title);
|
|
3793
|
-
}
|
|
4064
|
+
const inheritedProject = controllerSessionKey
|
|
4065
|
+
? resolveProjectIdentityForSession(controllerSessionKey, getTeamState())
|
|
4066
|
+
: undefined;
|
|
4067
|
+
const projectId = inheritedProject?.projectId ?? explicitProjectName || undefined;
|
|
4068
|
+
const projectDir = inheritedProject?.projectDir ?? projectId ?? deriveProjectSlug(title);
|
|
3794
4069
|
|
|
3795
4070
|
const task: TaskInfo = {
|
|
3796
4071
|
id: taskId,
|
|
@@ -3802,12 +4077,22 @@ async function handleRequest(
|
|
|
3802
4077
|
createdBy,
|
|
3803
4078
|
recommendedSkills: recommendedSkills.length > 0 ? recommendedSkills : undefined,
|
|
3804
4079
|
controllerSessionKey,
|
|
4080
|
+
projectId,
|
|
3805
4081
|
projectDir,
|
|
3806
4082
|
createdAt: now,
|
|
3807
4083
|
updatedAt: now,
|
|
3808
4084
|
};
|
|
3809
4085
|
|
|
3810
4086
|
updateTeamState((s) => {
|
|
4087
|
+
const project = syncProjectRegistryEntry(s, {
|
|
4088
|
+
projectId: task.projectId,
|
|
4089
|
+
projectDir: task.projectDir,
|
|
4090
|
+
aliases: [explicitProjectName, task.projectId],
|
|
4091
|
+
summary: task.title,
|
|
4092
|
+
updatedAt: now,
|
|
4093
|
+
});
|
|
4094
|
+
task.projectId = project.projectId;
|
|
4095
|
+
task.projectDir = project.projectDir;
|
|
3811
4096
|
s.tasks[taskId] = task;
|
|
3812
4097
|
});
|
|
3813
4098
|
recordTaskExecutionEvent(taskId, {
|
|
@@ -4409,14 +4694,31 @@ async function handleRequest(
|
|
|
4409
4694
|
|
|
4410
4695
|
const updatedRun = updateControllerRun(runId, deps, (run) => {
|
|
4411
4696
|
run.manifest = manifest;
|
|
4697
|
+
const state = deps.getTeamState();
|
|
4698
|
+
if (state) {
|
|
4699
|
+
const project = syncProjectRegistryEntry(state, {
|
|
4700
|
+
projectId: manifest.projectName ?? run.projectId,
|
|
4701
|
+
projectDir: run.projectDir ?? manifest.projectName,
|
|
4702
|
+
aliases: [manifest.projectName, run.projectId],
|
|
4703
|
+
summary: manifest.requirementSummary,
|
|
4704
|
+
updatedAt: Date.now(),
|
|
4705
|
+
});
|
|
4706
|
+
run.projectId = project.projectId;
|
|
4707
|
+
run.projectDir = project.projectDir;
|
|
4708
|
+
}
|
|
4412
4709
|
|
|
4413
4710
|
if (run.projectDir) {
|
|
4414
4711
|
const state = deps.getTeamState();
|
|
4415
4712
|
if (state) {
|
|
4416
4713
|
for (const taskId of run.createdTaskIds) {
|
|
4417
4714
|
const task = state.tasks[taskId];
|
|
4418
|
-
if (task
|
|
4419
|
-
task.projectDir
|
|
4715
|
+
if (task) {
|
|
4716
|
+
if (!task.projectDir) {
|
|
4717
|
+
task.projectDir = run.projectDir;
|
|
4718
|
+
}
|
|
4719
|
+
if (!task.projectId) {
|
|
4720
|
+
task.projectId = run.projectId;
|
|
4721
|
+
}
|
|
4420
4722
|
}
|
|
4421
4723
|
}
|
|
4422
4724
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ClarificationQuestionOption,
|
|
3
3
|
ClarificationQuestionSchema,
|
|
4
|
+
ControllerManifestCompletionOpportunity,
|
|
4
5
|
ControllerManifestCreatedTask,
|
|
5
6
|
ControllerManifestDeferredTask,
|
|
6
7
|
ControllerOrchestrationManifest,
|
|
7
8
|
RoleId,
|
|
8
9
|
} from "../types.js";
|
|
10
|
+
import { deriveStableProjectKey } from "../openclaw-workspace.js";
|
|
9
11
|
|
|
10
12
|
const TEAMCLAW_ROLE_IDS = new Set<RoleId>([
|
|
11
13
|
"pm",
|
|
@@ -145,6 +147,20 @@ export function normalizeManifestDeferredTasks(raw: unknown): ControllerManifest
|
|
|
145
147
|
.filter((entry) => entry.title && entry.blockedBy && entry.whenReady);
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
export function normalizeManifestCompletionOpportunities(raw: unknown): ControllerManifestCompletionOpportunity[] {
|
|
151
|
+
if (!Array.isArray(raw)) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
return raw
|
|
155
|
+
.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === "object")
|
|
156
|
+
.map((entry) => ({
|
|
157
|
+
title: typeof entry.title === "string" ? entry.title.trim() : "",
|
|
158
|
+
value: typeof entry.value === "string" ? entry.value.trim() : "",
|
|
159
|
+
summary: typeof entry.summary === "string" ? entry.summary.trim() : "",
|
|
160
|
+
}))
|
|
161
|
+
.filter((entry) => entry.title && entry.value && entry.summary);
|
|
162
|
+
}
|
|
163
|
+
|
|
148
164
|
export function normalizeControllerManifest(raw: unknown): ControllerOrchestrationManifest | null {
|
|
149
165
|
if (!raw || typeof raw !== "object") {
|
|
150
166
|
return null;
|
|
@@ -158,7 +174,9 @@ export function normalizeControllerManifest(raw: unknown): ControllerOrchestrati
|
|
|
158
174
|
const clarificationQuestions = normalizeManifestStringList(input.clarificationQuestions);
|
|
159
175
|
return {
|
|
160
176
|
version: typeof input.version === "string" && input.version.trim() ? input.version.trim() : "1.0",
|
|
161
|
-
projectName: typeof input.projectName === "string" && input.projectName.trim()
|
|
177
|
+
projectName: typeof input.projectName === "string" && input.projectName.trim()
|
|
178
|
+
? (deriveStableProjectKey(input.projectName.trim()) || input.projectName.trim())
|
|
179
|
+
: undefined,
|
|
162
180
|
requirementSummary,
|
|
163
181
|
requiredRoles: normalizeManifestRoleList(input.requiredRoles),
|
|
164
182
|
clarificationsNeeded: Boolean(input.clarificationsNeeded),
|
|
@@ -168,6 +186,7 @@ export function normalizeControllerManifest(raw: unknown): ControllerOrchestrati
|
|
|
168
186
|
deferredTasks: normalizeManifestDeferredTasks(input.deferredTasks),
|
|
169
187
|
handoffPlan: normalizeOptionalManifestText(input.handoffPlan),
|
|
170
188
|
notes: normalizeOptionalManifestText(input.notes),
|
|
189
|
+
completionOpportunities: normalizeManifestCompletionOpportunities(input.completionOpportunities),
|
|
171
190
|
requirementFullyComplete: Boolean(input.requirementFullyComplete),
|
|
172
191
|
};
|
|
173
192
|
}
|
|
@@ -129,6 +129,7 @@ export function createControllerPromptInjector(deps: ControllerPromptDeps) {
|
|
|
129
129
|
parts.push("");
|
|
130
130
|
parts.push("## Out-of-Scope Requests");
|
|
131
131
|
parts.push("- TeamClaw is a software development team. You handle: coding, architecture, design, testing, deployment, documentation, security review, and related technical work.");
|
|
132
|
+
parts.push("- Do not infer that the user lacks rights to work on a codebase just because it contains binaries, bundles, minified code, generated artifacts, proprietary branding, or unusual repository structure. Unless the user explicitly states a restriction, keep the decision technical rather than ownership-based.");
|
|
132
133
|
parts.push("- If the human asks for something clearly non-technical (cooking, weather, personal advice, general knowledge, creative writing unrelated to software), politely decline in your reply text AND still call teamclaw_submit_manifest with createdTasks=[], requiredRoles=[], and requirementSummary explaining why you declined.");
|
|
133
134
|
parts.push("- If the request is borderline (e.g. 'write a blog post about our API'), lean toward accepting it and assigning to the appropriate role (marketing, pm).");
|
|
134
135
|
parts.push("- REMEMBER: You must ALWAYS call teamclaw_submit_manifest, even when declining. The system cannot record your decision without it.");
|
|
@@ -169,9 +170,17 @@ type ExistingProjectInfo = { dir: string; summary: string };
|
|
|
169
170
|
function listExistingProjects(state: TeamState | null): ExistingProjectInfo[] {
|
|
170
171
|
const projects: ExistingProjectInfo[] = [];
|
|
171
172
|
|
|
172
|
-
// Gather from completed tasks with projectDir
|
|
173
173
|
const seenDirs = new Set<string>();
|
|
174
174
|
if (state) {
|
|
175
|
+
for (const project of Object.values(state.projects ?? {})) {
|
|
176
|
+
if (project.projectDir && !seenDirs.has(project.projectDir)) {
|
|
177
|
+
seenDirs.add(project.projectDir);
|
|
178
|
+
const aliases = project.aliases.filter(Boolean);
|
|
179
|
+
const aliasSummary = aliases.length > 0 ? `aliases: ${aliases.join(", ")}` : "";
|
|
180
|
+
const summary = [project.summary, aliasSummary].filter(Boolean).join(" | ") || "(registered project)";
|
|
181
|
+
projects.push({ dir: project.projectDir, summary });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
175
184
|
for (const task of Object.values(state.tasks)) {
|
|
176
185
|
if (task.projectDir && !seenDirs.has(task.projectDir)) {
|
|
177
186
|
seenDirs.add(task.projectDir);
|
|
@@ -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 = "";
|
|
@@ -438,13 +438,17 @@ export function buildTeamClawProjectAgentRelativePath(
|
|
|
438
438
|
*
|
|
439
439
|
* Example: "Build a payment system with Stripe" → "build-a-payment-system-with-stripe-k3f9m2"
|
|
440
440
|
*/
|
|
441
|
-
export function
|
|
442
|
-
|
|
441
|
+
export function deriveStableProjectKey(text: string): string {
|
|
442
|
+
return text
|
|
443
443
|
.toLowerCase()
|
|
444
444
|
.replace(/[^a-z0-9]+/g, "-")
|
|
445
445
|
.replace(/^-+|-+$/g, "")
|
|
446
446
|
.slice(0, 50)
|
|
447
|
-
.replace(
|
|
447
|
+
.replace(/-+$/g, "");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function deriveProjectSlug(text: string): string {
|
|
451
|
+
const slug = deriveStableProjectKey(text);
|
|
448
452
|
const suffix = randomSuffix(6);
|
|
449
453
|
return slug ? `${slug}-${suffix}` : suffix;
|
|
450
454
|
}
|
package/src/state.ts
CHANGED
|
@@ -100,6 +100,9 @@ async function loadTeamState(teamName: string): Promise<TeamState | null> {
|
|
|
100
100
|
if (!parsed.controllerRuns || typeof parsed.controllerRuns !== "object") {
|
|
101
101
|
parsed.controllerRuns = {};
|
|
102
102
|
}
|
|
103
|
+
if (!parsed.projects || typeof parsed.projects !== "object") {
|
|
104
|
+
parsed.projects = {};
|
|
105
|
+
}
|
|
103
106
|
if (!Array.isArray(parsed.messages)) {
|
|
104
107
|
parsed.messages = [];
|
|
105
108
|
}
|
|
@@ -136,6 +139,9 @@ async function saveTeamState(state: TeamState): Promise<void> {
|
|
|
136
139
|
state.controllerRuns = state.controllerRuns && typeof state.controllerRuns === "object"
|
|
137
140
|
? state.controllerRuns
|
|
138
141
|
: {};
|
|
142
|
+
state.projects = state.projects && typeof state.projects === "object"
|
|
143
|
+
? state.projects
|
|
144
|
+
: {};
|
|
139
145
|
await enqueueAtomicWrite(filePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
140
146
|
}
|
|
141
147
|
|
package/src/types.ts
CHANGED
|
@@ -110,6 +110,7 @@ export type TaskInfo = {
|
|
|
110
110
|
createdBy: string;
|
|
111
111
|
recommendedSkills?: string[];
|
|
112
112
|
controllerSessionKey?: string;
|
|
113
|
+
projectId?: string;
|
|
113
114
|
projectDir?: string;
|
|
114
115
|
createdAt: number;
|
|
115
116
|
updatedAt: number;
|
|
@@ -147,6 +148,18 @@ export type TaskAssignmentPayload = {
|
|
|
147
148
|
executionSessionKey?: string;
|
|
148
149
|
executionIdempotencyKey?: string;
|
|
149
150
|
repo?: RepoSyncInfo;
|
|
151
|
+
teamContext?: {
|
|
152
|
+
requirementSummary?: string;
|
|
153
|
+
projectName?: string;
|
|
154
|
+
requiredRoles?: RoleId[];
|
|
155
|
+
activeTasks?: Array<{ id: string; title: string; assignedRole?: RoleId; status: TaskStatus; summary?: string }>;
|
|
156
|
+
blockedTasks?: Array<{ id: string; title: string; assignedRole?: RoleId; status: TaskStatus; summary?: string }>;
|
|
157
|
+
recentCompletedTasks?: Array<{ id: string; title: string; assignedRole?: RoleId; status: TaskStatus; summary?: string }>;
|
|
158
|
+
pendingClarifications?: Array<{ question: string; blockingReason?: string }>;
|
|
159
|
+
handoffPlan?: string;
|
|
160
|
+
notes?: string;
|
|
161
|
+
kickoffSummary?: string;
|
|
162
|
+
};
|
|
150
163
|
};
|
|
151
164
|
|
|
152
165
|
export type ControllerRunSource = "human" | "task_follow_up";
|
|
@@ -274,6 +287,12 @@ export type ControllerManifestDeferredTask = {
|
|
|
274
287
|
whenReady: string;
|
|
275
288
|
};
|
|
276
289
|
|
|
290
|
+
export type ControllerManifestCompletionOpportunity = {
|
|
291
|
+
title: string;
|
|
292
|
+
value: string;
|
|
293
|
+
summary: string;
|
|
294
|
+
};
|
|
295
|
+
|
|
277
296
|
export type ClarificationQuestionKind = "single-select" | "multi-select" | "number" | "text";
|
|
278
297
|
|
|
279
298
|
export type ClarificationQuestionOption = {
|
|
@@ -309,6 +328,7 @@ export type ControllerOrchestrationManifest = {
|
|
|
309
328
|
deferredTasks: ControllerManifestDeferredTask[];
|
|
310
329
|
handoffPlan?: string;
|
|
311
330
|
notes?: string;
|
|
331
|
+
completionOpportunities?: ControllerManifestCompletionOpportunity[];
|
|
312
332
|
/** Controller signals that the entire human requirement is fully satisfied — no more tasks or follow-ups needed. */
|
|
313
333
|
requirementFullyComplete?: boolean;
|
|
314
334
|
/** Team kickoff plan — populated when controller uses collaborative planning. */
|
|
@@ -326,6 +346,7 @@ export type ControllerRunInfo = {
|
|
|
326
346
|
request: string;
|
|
327
347
|
reply?: string;
|
|
328
348
|
error?: string;
|
|
349
|
+
projectId?: string;
|
|
329
350
|
projectDir?: string;
|
|
330
351
|
createdTaskIds: string[];
|
|
331
352
|
manifest?: ControllerOrchestrationManifest;
|
|
@@ -492,6 +513,7 @@ export type TeamState = {
|
|
|
492
513
|
workers: Record<string, WorkerInfo>;
|
|
493
514
|
tasks: Record<string, TaskInfo>;
|
|
494
515
|
controllerRuns: Record<string, ControllerRunInfo>;
|
|
516
|
+
projects: Record<string, TeamProjectInfo>;
|
|
495
517
|
messages: TeamMessage[];
|
|
496
518
|
clarifications: Record<string, ClarificationRequest>;
|
|
497
519
|
repo?: GitRepoState;
|
|
@@ -502,6 +524,16 @@ export type TeamState = {
|
|
|
502
524
|
updatedAt: number;
|
|
503
525
|
};
|
|
504
526
|
|
|
527
|
+
export type TeamProjectInfo = {
|
|
528
|
+
id: string;
|
|
529
|
+
projectDir: string;
|
|
530
|
+
aliases: string[];
|
|
531
|
+
summary?: string;
|
|
532
|
+
createdAt: number;
|
|
533
|
+
updatedAt: number;
|
|
534
|
+
lastUsedAt: number;
|
|
535
|
+
};
|
|
536
|
+
|
|
505
537
|
export type DeliveryReportRecord = {
|
|
506
538
|
id: string;
|
|
507
539
|
sessionKey: string;
|