@teamclaws/teamclaw 2026.3.26-2 → 2026.4.2-1
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 +52 -8
- package/cli.mjs +538 -224
- package/index.ts +76 -27
- package/openclaw.plugin.json +53 -28
- package/package.json +5 -2
- package/skills/teamclaw/SKILL.md +213 -0
- package/skills/teamclaw/references/api-quick-ref.md +117 -0
- package/skills/teamclaw-setup/SKILL.md +81 -0
- package/skills/teamclaw-setup/references/install-modes.md +136 -0
- package/skills/teamclaw-setup/references/validation-checklist.md +73 -0
- package/src/config.ts +44 -16
- package/src/controller/controller-capacity.ts +2 -2
- package/src/controller/controller-service.ts +193 -47
- package/src/controller/controller-tools.ts +102 -2
- package/src/controller/delivery-report.ts +563 -0
- package/src/controller/http-server.ts +1907 -172
- package/src/controller/kickoff-orchestrator.ts +292 -0
- package/src/controller/managed-gateway-process.ts +330 -0
- package/src/controller/orchestration-manifest.ts +69 -1
- package/src/controller/preview-manager.ts +676 -0
- package/src/controller/prompt-injector.ts +116 -67
- package/src/controller/role-inference.ts +41 -0
- package/src/controller/websocket.ts +3 -1
- package/src/controller/worker-provisioning.ts +429 -74
- package/src/discovery.ts +1 -1
- package/src/git-collaboration.ts +198 -47
- package/src/identity.ts +12 -2
- package/src/interaction-contracts.ts +179 -3
- package/src/networking.ts +99 -0
- package/src/openclaw-workspace.ts +478 -11
- package/src/prompt-policy.ts +381 -0
- package/src/roles.ts +37 -36
- package/src/state.ts +40 -1
- package/src/task-executor.ts +282 -78
- package/src/types.ts +150 -7
- package/src/ui/app.js +1403 -175
- package/src/ui/assets/teamclaw-app-icon.png +0 -0
- package/src/ui/index.html +122 -40
- package/src/ui/style.css +829 -143
- package/src/worker/http-handler.ts +40 -4
- package/src/worker/prompt-injector.ts +9 -38
- package/src/worker/skill-installer.ts +2 -2
- package/src/worker/tools.ts +31 -5
- package/src/worker/worker-service.ts +49 -8
- package/src/workspace-browser.ts +20 -7
- package/src/controller/local-worker-manager.ts +0 -533
package/src/task-executor.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import type { OpenClawPluginApi, PluginLogger } from "../api.js";
|
|
2
|
+
import {
|
|
3
|
+
buildDeliverableMetadataPolicy,
|
|
4
|
+
buildResultContractGuidance,
|
|
5
|
+
buildTaskExecutionRules,
|
|
6
|
+
buildVerificationPolicy,
|
|
7
|
+
buildWorkerMemoryContractRules,
|
|
8
|
+
} from "./prompt-policy.js";
|
|
9
|
+
import {
|
|
10
|
+
buildTeamClawProjectWorkspacePath,
|
|
11
|
+
resolveTeamClawWorkspaceDir,
|
|
12
|
+
resolveTeamClawProjectsDir,
|
|
13
|
+
} from "./openclaw-workspace.js";
|
|
2
14
|
import { getRole } from "./roles.js";
|
|
3
|
-
import type { RoleId, TaskAssignmentPayload, TaskExecutionEventInput } from "./types.js";
|
|
4
|
-
|
|
5
|
-
const TEAMCLAW_ROLE_IDS_TEXT = [
|
|
6
|
-
"pm",
|
|
7
|
-
"architect",
|
|
8
|
-
"developer",
|
|
9
|
-
"qa",
|
|
10
|
-
"release-engineer",
|
|
11
|
-
"infra-engineer",
|
|
12
|
-
"devops",
|
|
13
|
-
"security-engineer",
|
|
14
|
-
"designer",
|
|
15
|
-
"marketing",
|
|
16
|
-
].join(", ");
|
|
15
|
+
import type { RoleId, TaskAssignmentPayload, TaskExecutionEventInput, WorkerTaskResultContract } from "./types.js";
|
|
17
16
|
|
|
18
17
|
const SESSION_PROGRESS_POLL_INTERVAL_MS = 1000;
|
|
19
18
|
const SESSION_PROGRESS_MESSAGE_LIMIT = 200;
|
|
@@ -23,6 +22,7 @@ const RATE_LIMIT_STALL_PROBE_MS = 5 * 60 * 1000;
|
|
|
23
22
|
const RATE_LIMIT_PROBE_TIMEOUT_MS = 60_000;
|
|
24
23
|
const BACKGROUND_WORK_PROBE_MS = 60_000;
|
|
25
24
|
const BACKGROUND_WORK_PROBE_TIMEOUT_MS = 60_000;
|
|
25
|
+
const INACTIVITY_PROBE_TIMEOUT_MS = 60_000;
|
|
26
26
|
const CHILD_SESSION_PROGRESS_POLL_INTERVAL_MS = 5_000;
|
|
27
27
|
const RATE_LIMIT_WAITING_SENTINEL = "TEAMCLAW_STILL_WAITING";
|
|
28
28
|
const TOOL_CALL_BLOCK_TYPES = new Set(["tool_use", "toolcall", "tool_call"]);
|
|
@@ -43,6 +43,7 @@ type SessionProgressSnapshot = {
|
|
|
43
43
|
lastChildPollAt: number;
|
|
44
44
|
lastAssistantMessage: string;
|
|
45
45
|
latestMessages: unknown[];
|
|
46
|
+
lastActivityAt: number;
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
type AssistantTurnSnapshot = {
|
|
@@ -52,6 +53,11 @@ type AssistantTurnSnapshot = {
|
|
|
52
53
|
backgroundPending: boolean;
|
|
53
54
|
};
|
|
54
55
|
|
|
56
|
+
export type TaskExecutorResult = {
|
|
57
|
+
text: string;
|
|
58
|
+
contract?: Record<string, unknown>;
|
|
59
|
+
};
|
|
60
|
+
|
|
55
61
|
export type RoleTaskExecutorDeps = {
|
|
56
62
|
runtime: OpenClawPluginApi["runtime"];
|
|
57
63
|
logger: PluginLogger;
|
|
@@ -69,13 +75,27 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
69
75
|
? roleDef.systemPrompt
|
|
70
76
|
: `You are a ${role} in a virtual software team. Complete the assigned task.`;
|
|
71
77
|
|
|
72
|
-
return async (taskDescription: string, assignment: TaskAssignmentPayload): Promise<
|
|
78
|
+
return async (taskDescription: string, assignment: TaskAssignmentPayload): Promise<TaskExecutorResult> => {
|
|
73
79
|
const taskId = assignment.taskId;
|
|
74
80
|
const sessionKey = getSessionKey(assignment);
|
|
75
81
|
const idempotencyKey = getIdempotencyKey?.(assignment);
|
|
76
|
-
const taskMessage = buildTaskMessage(taskDescription, taskId, roleDef?.label ?? role
|
|
82
|
+
const taskMessage = buildTaskMessage(taskDescription, taskId, roleDef?.label ?? role, {
|
|
83
|
+
inlineContract: true,
|
|
84
|
+
projectDir: assignment.projectDir,
|
|
85
|
+
});
|
|
86
|
+
const workspaceDir = resolveTeamClawWorkspaceDir();
|
|
77
87
|
logger.info(`TeamClaw: executing task ${taskId} as ${role} via subagent`);
|
|
78
88
|
|
|
89
|
+
function buildSubagentRunOptions(
|
|
90
|
+
options: Parameters<typeof runtime.subagent.run>[0],
|
|
91
|
+
): Parameters<typeof runtime.subagent.run>[0] {
|
|
92
|
+
const enrichedOptions: Parameters<typeof runtime.subagent.run>[0] & { workspaceDir?: string } = {
|
|
93
|
+
...options,
|
|
94
|
+
workspaceDir,
|
|
95
|
+
};
|
|
96
|
+
return enrichedOptions;
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
async function emitExecutionEvent(event: TaskExecutionEventInput): Promise<void> {
|
|
80
100
|
if (!reportExecutionEvent) {
|
|
81
101
|
return;
|
|
@@ -92,12 +112,12 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
92
112
|
}
|
|
93
113
|
|
|
94
114
|
try {
|
|
95
|
-
const runResult = await runtime.subagent.run({
|
|
115
|
+
const runResult = await runtime.subagent.run(buildSubagentRunOptions({
|
|
96
116
|
sessionKey,
|
|
97
117
|
message: taskMessage,
|
|
98
118
|
extraSystemPrompt: roleSystemPrompt,
|
|
99
119
|
idempotencyKey,
|
|
100
|
-
});
|
|
120
|
+
}));
|
|
101
121
|
|
|
102
122
|
logger.info(`TeamClaw: subagent run started for task ${taskId}, runId=${runResult.runId}`);
|
|
103
123
|
await emitExecutionEvent({
|
|
@@ -117,8 +137,8 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
117
137
|
lastChildPollAt: 0,
|
|
118
138
|
lastAssistantMessage: "",
|
|
119
139
|
latestMessages: [],
|
|
140
|
+
lastActivityAt: Date.now(),
|
|
120
141
|
};
|
|
121
|
-
const deadline = Date.now() + taskTimeoutMs;
|
|
122
142
|
const rateLimitState: {
|
|
123
143
|
active: boolean;
|
|
124
144
|
visibleAt?: number;
|
|
@@ -128,6 +148,24 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
128
148
|
active: false,
|
|
129
149
|
probeCount: 0,
|
|
130
150
|
};
|
|
151
|
+
const inactivityState: {
|
|
152
|
+
active: boolean;
|
|
153
|
+
visibleAt?: number;
|
|
154
|
+
nextProbeAt?: number;
|
|
155
|
+
probeCount: number;
|
|
156
|
+
} = {
|
|
157
|
+
active: false,
|
|
158
|
+
nextProbeAt: Date.now() + taskTimeoutMs,
|
|
159
|
+
probeCount: 0,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const noteObservedActivity = (): void => {
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
progressSnapshot.lastActivityAt = now;
|
|
165
|
+
inactivityState.active = false;
|
|
166
|
+
inactivityState.visibleAt = undefined;
|
|
167
|
+
inactivityState.nextProbeAt = now + taskTimeoutMs;
|
|
168
|
+
};
|
|
131
169
|
const backgroundWaitState: {
|
|
132
170
|
active: boolean;
|
|
133
171
|
visibleAt?: number;
|
|
@@ -198,6 +236,9 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
198
236
|
const entries = buildSessionProgressEntries(progressSnapshot.latestMessages, taskMessage);
|
|
199
237
|
const newEntries = getNewSessionProgressEntries(entries, progressSnapshot.fingerprints);
|
|
200
238
|
progressSnapshot.fingerprints = entries.map((entry) => entry.fingerprint);
|
|
239
|
+
if (newEntries.length > 0) {
|
|
240
|
+
noteObservedActivity();
|
|
241
|
+
}
|
|
201
242
|
progressSnapshot.childSessionKeys = mergeChildSessionKeys(
|
|
202
243
|
progressSnapshot.childSessionKeys,
|
|
203
244
|
collectChildSessionKeys(progressSnapshot.latestMessages),
|
|
@@ -268,12 +309,12 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
268
309
|
message: `Model rate limit has delayed task progress for over ${formatDuration(RATE_LIMIT_STALL_PROBE_MS)}. Re-checking whether the current task has already completed.`,
|
|
269
310
|
});
|
|
270
311
|
|
|
271
|
-
const probeRun = await runtime.subagent.run({
|
|
312
|
+
const probeRun = await runtime.subagent.run(buildSubagentRunOptions({
|
|
272
313
|
sessionKey,
|
|
273
314
|
message: buildRateLimitProbeMessage(taskId, roleDef?.label ?? role),
|
|
274
315
|
extraSystemPrompt: roleSystemPrompt,
|
|
275
316
|
idempotencyKey: `${idempotencyKey ?? `teamclaw-${taskId}`}:rate-limit-probe:${rateLimitState.probeCount}`,
|
|
276
|
-
});
|
|
317
|
+
}));
|
|
277
318
|
const probeWait = await runtime.subagent.waitForRun({
|
|
278
319
|
runId: probeRun.runId,
|
|
279
320
|
timeoutMs: RATE_LIMIT_PROBE_TIMEOUT_MS,
|
|
@@ -322,18 +363,15 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
322
363
|
message: `Background work has been running for over ${formatDuration(BACKGROUND_WORK_PROBE_MS)}. Re-checking whether the original task is now complete.`,
|
|
323
364
|
});
|
|
324
365
|
|
|
325
|
-
const probeRun = await runtime.subagent.run({
|
|
366
|
+
const probeRun = await runtime.subagent.run(buildSubagentRunOptions({
|
|
326
367
|
sessionKey,
|
|
327
368
|
message: buildBackgroundWorkProbeMessage(taskId, roleDef?.label ?? role),
|
|
328
369
|
extraSystemPrompt: roleSystemPrompt,
|
|
329
370
|
idempotencyKey: `${idempotencyKey ?? `teamclaw-${taskId}`}:background-work-probe:${backgroundWaitState.probeCount}`,
|
|
330
|
-
});
|
|
371
|
+
}));
|
|
331
372
|
const probeWait = await runtime.subagent.waitForRun({
|
|
332
373
|
runId: probeRun.runId,
|
|
333
|
-
timeoutMs:
|
|
334
|
-
BACKGROUND_WORK_PROBE_TIMEOUT_MS,
|
|
335
|
-
Math.max(1_000, deadline - Date.now()),
|
|
336
|
-
),
|
|
374
|
+
timeoutMs: BACKGROUND_WORK_PROBE_TIMEOUT_MS,
|
|
337
375
|
});
|
|
338
376
|
|
|
339
377
|
try {
|
|
@@ -372,6 +410,71 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
372
410
|
return probeTurn;
|
|
373
411
|
};
|
|
374
412
|
|
|
413
|
+
const probeInactiveTaskCompletion = async (): Promise<AssistantTurnSnapshot | null> => {
|
|
414
|
+
inactivityState.probeCount += 1;
|
|
415
|
+
const now = Date.now();
|
|
416
|
+
inactivityState.active = true;
|
|
417
|
+
inactivityState.visibleAt = now;
|
|
418
|
+
inactivityState.nextProbeAt = now + taskTimeoutMs;
|
|
419
|
+
await emitExecutionEvent({
|
|
420
|
+
type: "progress",
|
|
421
|
+
phase: "inactivity_probe",
|
|
422
|
+
source: "worker",
|
|
423
|
+
status: "running",
|
|
424
|
+
runId: runResult.runId,
|
|
425
|
+
sessionKey,
|
|
426
|
+
message: `No new visible task progress has appeared for over ${formatDuration(taskTimeoutMs)}. Re-checking whether the original task is complete or still actively running.`,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const probeRun = await runtime.subagent.run(buildSubagentRunOptions({
|
|
430
|
+
sessionKey,
|
|
431
|
+
message: buildInactivityProbeMessage(taskId, roleDef?.label ?? role, taskTimeoutMs),
|
|
432
|
+
extraSystemPrompt: roleSystemPrompt,
|
|
433
|
+
idempotencyKey: `${idempotencyKey ?? `teamclaw-${taskId}`}:inactivity-probe:${inactivityState.probeCount}`,
|
|
434
|
+
}));
|
|
435
|
+
const probeWait = await runtime.subagent.waitForRun({
|
|
436
|
+
runId: probeRun.runId,
|
|
437
|
+
timeoutMs: INACTIVITY_PROBE_TIMEOUT_MS,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
await syncSessionProgress();
|
|
442
|
+
} catch (err) {
|
|
443
|
+
logger.debug?.(`TeamClaw: failed inactivity probe session sync for ${taskId}: ${String(err)}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (probeWait.status === "error" && isRateLimitMessage(probeWait.error || "")) {
|
|
447
|
+
await markRateLimitWaiting();
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
if (probeWait.status !== "ok") {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const probeTurn = await extractSessionAssistantTurn();
|
|
455
|
+
if (!probeTurn.text || probeTurn.backgroundPending || isRateLimitMessage(probeTurn.text) || isStillWaitingResponse(probeTurn.text)) {
|
|
456
|
+
inactivityState.active = false;
|
|
457
|
+
inactivityState.visibleAt = undefined;
|
|
458
|
+
inactivityState.nextProbeAt = Date.now() + taskTimeoutMs;
|
|
459
|
+
await emitExecutionEvent({
|
|
460
|
+
type: "progress",
|
|
461
|
+
phase: "inactivity_still_waiting",
|
|
462
|
+
source: "worker",
|
|
463
|
+
status: "running",
|
|
464
|
+
runId: runResult.runId,
|
|
465
|
+
sessionKey,
|
|
466
|
+
message: "The task is still actively pending with no final result yet. TeamClaw will continue waiting instead of failing it.",
|
|
467
|
+
});
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (rateLimitState.active) {
|
|
472
|
+
clearRateLimitWaiting();
|
|
473
|
+
}
|
|
474
|
+
noteObservedActivity();
|
|
475
|
+
return probeTurn;
|
|
476
|
+
};
|
|
477
|
+
|
|
375
478
|
let keepPolling = true;
|
|
376
479
|
const pollSessionProgress = (async () => {
|
|
377
480
|
while (keepPolling) {
|
|
@@ -392,12 +495,6 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
392
495
|
let completionOverride: string | null = null;
|
|
393
496
|
try {
|
|
394
497
|
while (true) {
|
|
395
|
-
const remainingMs = deadline - Date.now();
|
|
396
|
-
if (remainingMs <= 0) {
|
|
397
|
-
waitResult = { status: "timeout" as const };
|
|
398
|
-
break;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
498
|
if (rateLimitState.active && (rateLimitState.nextProbeAt ?? Number.POSITIVE_INFINITY) <= Date.now()) {
|
|
402
499
|
completionOverride = await probeRateLimitedTaskCompletion();
|
|
403
500
|
if (completionOverride) {
|
|
@@ -406,10 +503,22 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
406
503
|
}
|
|
407
504
|
}
|
|
408
505
|
|
|
409
|
-
|
|
506
|
+
if (
|
|
507
|
+
!rateLimitState.active
|
|
508
|
+
&& !backgroundWaitState.active
|
|
509
|
+
&& (inactivityState.nextProbeAt ?? Number.POSITIVE_INFINITY) <= Date.now()
|
|
510
|
+
) {
|
|
511
|
+
const inactivityProbeTurn = await probeInactiveTaskCompletion();
|
|
512
|
+
if (inactivityProbeTurn) {
|
|
513
|
+
completionOverride = inactivityProbeTurn.text;
|
|
514
|
+
waitResult = { status: "ok" as const };
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
410
519
|
waitResult = await runtime.subagent.waitForRun({
|
|
411
520
|
runId: runResult.runId,
|
|
412
|
-
timeoutMs:
|
|
521
|
+
timeoutMs: RUN_WAIT_SLICE_MS,
|
|
413
522
|
});
|
|
414
523
|
|
|
415
524
|
if (waitResult.status === "ok") {
|
|
@@ -440,13 +549,8 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
440
549
|
: await extractSessionAssistantTurn();
|
|
441
550
|
while (isBackgroundWorkPendingTurn(assistantTurn)) {
|
|
442
551
|
await markBackgroundWorkWaiting();
|
|
443
|
-
const remainingMs = deadline - Date.now();
|
|
444
|
-
if (remainingMs <= 0) {
|
|
445
|
-
waitResult = { status: "timeout" as const };
|
|
446
|
-
break;
|
|
447
|
-
}
|
|
448
552
|
const nextProbeAt = backgroundWaitState.nextProbeAt ?? (Date.now() + BACKGROUND_WORK_PROBE_MS);
|
|
449
|
-
const delayMs = Math.max(1_000,
|
|
553
|
+
const delayMs = Math.max(1_000, nextProbeAt - Date.now());
|
|
450
554
|
await delay(delayMs);
|
|
451
555
|
const probeTurn = await probeBackgroundTaskCompletion();
|
|
452
556
|
if (probeTurn) {
|
|
@@ -459,34 +563,37 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
459
563
|
if (rateLimitState.active) {
|
|
460
564
|
clearRateLimitWaiting();
|
|
461
565
|
}
|
|
462
|
-
const
|
|
463
|
-
if (
|
|
566
|
+
const rawResult = assistantTurn.text;
|
|
567
|
+
if (rawResult && normalizeComparableText(rawResult) !== normalizeComparableText(progressSnapshot.lastAssistantMessage)) {
|
|
464
568
|
await emitExecutionEvent({
|
|
465
569
|
type: "output",
|
|
466
570
|
phase: "final_output",
|
|
467
571
|
source: "subagent",
|
|
468
|
-
message:
|
|
572
|
+
message: rawResult,
|
|
469
573
|
});
|
|
470
574
|
}
|
|
471
575
|
|
|
472
576
|
clearBackgroundWorkWaiting();
|
|
577
|
+
|
|
578
|
+
// Extract inline result contract if present
|
|
579
|
+
const extracted = extractInlineResultContract(rawResult);
|
|
580
|
+
if (extracted) {
|
|
581
|
+
logger.info(`TeamClaw: task ${taskId} — extracted inline result contract from ${role}`);
|
|
582
|
+
return { text: extracted.cleanedText || rawResult, contract: extracted.contract };
|
|
583
|
+
}
|
|
584
|
+
if (rawResult && isApprovalRequiredResponse(rawResult)) {
|
|
585
|
+
logger.warn(`TeamClaw: task ${taskId} is blocked waiting for exec approval as ${role}`);
|
|
586
|
+
return {
|
|
587
|
+
text: rawResult,
|
|
588
|
+
contract: buildApprovalBlockedContract(rawResult),
|
|
589
|
+
};
|
|
590
|
+
}
|
|
473
591
|
logger.info(`TeamClaw: task ${taskId} completed successfully as ${role}`);
|
|
474
|
-
return
|
|
592
|
+
return { text: rawResult };
|
|
475
593
|
}
|
|
476
594
|
clearBackgroundWorkWaiting();
|
|
477
595
|
}
|
|
478
596
|
|
|
479
|
-
if (waitResult.status === "timeout") {
|
|
480
|
-
await emitExecutionEvent({
|
|
481
|
-
type: "error",
|
|
482
|
-
phase: "timeout",
|
|
483
|
-
source: "subagent",
|
|
484
|
-
status: "failed",
|
|
485
|
-
message: `Task execution timed out after ${formatDuration(taskTimeoutMs)}`,
|
|
486
|
-
});
|
|
487
|
-
throw new Error(`Task execution timed out after ${formatDuration(taskTimeoutMs)}`);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
597
|
await emitExecutionEvent({
|
|
491
598
|
type: "error",
|
|
492
599
|
phase: "run_failed",
|
|
@@ -780,33 +887,80 @@ function safeJsonStringify(value: unknown): string {
|
|
|
780
887
|
}
|
|
781
888
|
}
|
|
782
889
|
|
|
783
|
-
function buildTaskMessage(
|
|
784
|
-
|
|
890
|
+
function buildTaskMessage(
|
|
891
|
+
taskDescription: string,
|
|
892
|
+
taskId: string,
|
|
893
|
+
roleLabel: string,
|
|
894
|
+
options?: { inlineContract?: boolean; projectDir?: string },
|
|
895
|
+
): string {
|
|
896
|
+
const rules = [
|
|
897
|
+
...buildTaskExecutionRules(RATE_LIMIT_WAITING_SENTINEL),
|
|
898
|
+
...buildWorkerMemoryContractRules(),
|
|
899
|
+
...buildVerificationPolicy(),
|
|
900
|
+
...buildDeliverableMetadataPolicy(),
|
|
901
|
+
...buildResultContractGuidance({ inlineContract: Boolean(options?.inlineContract) }),
|
|
902
|
+
];
|
|
903
|
+
|
|
904
|
+
const sections = [
|
|
785
905
|
taskDescription,
|
|
786
906
|
"",
|
|
787
907
|
"## Task Context",
|
|
788
908
|
`Reference: ${taskId}`,
|
|
789
909
|
`Assigned Role: ${roleLabel}`,
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
910
|
+
];
|
|
911
|
+
|
|
912
|
+
if (options?.projectDir) {
|
|
913
|
+
const workspaceProjectPath = buildTeamClawProjectWorkspacePath(options.projectDir);
|
|
914
|
+
const absoluteProjectPath = `${resolveTeamClawProjectsDir()}/${options.projectDir}`.replace(/\/+/gu, "/");
|
|
915
|
+
sections.push(
|
|
916
|
+
"",
|
|
917
|
+
"## Working Directory",
|
|
918
|
+
`This task's project directory is: \`${workspaceProjectPath}/\``,
|
|
919
|
+
`Authoritative absolute path: \`${absoluteProjectPath}/\``,
|
|
920
|
+
"All files you create, read, or modify for this task MUST be inside this directory.",
|
|
921
|
+
"If the directory is empty, create the necessary structure. If it already has files from prior tasks in the same project, build on them.",
|
|
922
|
+
"If the task description mentions a different absolute workspace path, treat that path as stale guidance and use this authoritative project directory instead.",
|
|
923
|
+
"When the task references project-local files such as `ARCHITECTURE.md`, `README.md`, or `package.json`, resolve them inside this project directory first.",
|
|
924
|
+
"Do NOT place files in the workspace root or any other project's directory.",
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
sections.push("", "## Execution Rules", ...rules);
|
|
929
|
+
return sections.join("\n");
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Extract an inline result contract from a fenced ```teamclaw-result-contract block.
|
|
934
|
+
* Returns the parsed contract and the text with the block removed, or null if
|
|
935
|
+
* no valid contract is found.
|
|
936
|
+
*/
|
|
937
|
+
export function extractInlineResultContract(text: string): {
|
|
938
|
+
contract: Record<string, unknown>;
|
|
939
|
+
cleanedText: string;
|
|
940
|
+
} | null {
|
|
941
|
+
// Match ```teamclaw-result-contract ... ``` blocks (greedy last match)
|
|
942
|
+
const pattern = /```teamclaw-result-contract\s*\n([\s\S]*?)```/g;
|
|
943
|
+
let lastMatch: RegExpExecArray | null = null;
|
|
944
|
+
let match: RegExpExecArray | null;
|
|
945
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
946
|
+
lastMatch = match;
|
|
947
|
+
}
|
|
948
|
+
if (!lastMatch) {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
const jsonStr = lastMatch[1]!.trim();
|
|
952
|
+
try {
|
|
953
|
+
const parsed = JSON.parse(jsonStr);
|
|
954
|
+
if (!parsed || typeof parsed !== "object") {
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
// Remove the contract block from the text for a clean result
|
|
958
|
+
const cleanedText = text.slice(0, lastMatch.index).trimEnd()
|
|
959
|
+
+ text.slice(lastMatch.index + lastMatch[0].length).trimStart();
|
|
960
|
+
return { contract: parsed, cleanedText: cleanedText.trim() };
|
|
961
|
+
} catch {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
810
964
|
}
|
|
811
965
|
|
|
812
966
|
function buildRateLimitProbeMessage(taskId: string, roleLabel: string): string {
|
|
@@ -831,6 +985,17 @@ function buildBackgroundWorkProbeMessage(taskId: string, roleLabel: string): str
|
|
|
831
985
|
].join("\n");
|
|
832
986
|
}
|
|
833
987
|
|
|
988
|
+
function buildInactivityProbeMessage(taskId: string, roleLabel: string, inactivityMs: number): string {
|
|
989
|
+
return [
|
|
990
|
+
`This is a follow-up check for task ${taskId} (${roleLabel}).`,
|
|
991
|
+
`There has been no new visible progress for over ${formatDuration(inactivityMs)}.`,
|
|
992
|
+
"Do not restart the task from scratch.",
|
|
993
|
+
"Continue from the existing workspace and session state only.",
|
|
994
|
+
"If the original task is fully complete now, immediately submit the structured result contract and provide the final result for that original task.",
|
|
995
|
+
`If the original task is not complete yet, reply with exactly ${RATE_LIMIT_WAITING_SENTINEL}.`,
|
|
996
|
+
].join("\n");
|
|
997
|
+
}
|
|
998
|
+
|
|
834
999
|
function buildAssistantTurnSnapshot(text: string, toolCalls: string[] = []): AssistantTurnSnapshot {
|
|
835
1000
|
const normalizedText = String(text || "").trim();
|
|
836
1001
|
const normalizedToolCalls = toolCalls
|
|
@@ -902,6 +1067,45 @@ function isStillWaitingResponse(value: string): boolean {
|
|
|
902
1067
|
return /(still waiting|continue waiting|not complete yet|尚未完成|继续等待|仍在等待)/i.test(normalized);
|
|
903
1068
|
}
|
|
904
1069
|
|
|
1070
|
+
function extractPendingApprovalCommands(value: string): string[] {
|
|
1071
|
+
const matches = String(value || "").match(/\/approve\s+[^\s]+\s+(?:allow-once|allow-always|deny)\b/gi) ?? [];
|
|
1072
|
+
return Array.from(new Set(matches.map((entry) => entry.trim()).filter(Boolean)));
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function isApprovalRequiredResponse(value: string): boolean {
|
|
1076
|
+
const normalized = String(value || "").trim();
|
|
1077
|
+
if (!normalized) {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
return /approval required|i need approval to run commands|pending exec commands?|reply with:\s*\/approve|需要批准|等待.*批准/i.test(normalized)
|
|
1081
|
+
|| extractPendingApprovalCommands(normalized).length > 0;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function buildApprovalBlockedContract(rawResult: string): WorkerTaskResultContract {
|
|
1085
|
+
const approvalCommands = extractPendingApprovalCommands(rawResult);
|
|
1086
|
+
const blockingReason = "Pending exec approval is required before this task can continue.";
|
|
1087
|
+
return {
|
|
1088
|
+
version: "1.0",
|
|
1089
|
+
outcome: "blocked",
|
|
1090
|
+
summary: "Task is blocked waiting for exec approval.",
|
|
1091
|
+
deliverables: approvalCommands.map((command) => ({
|
|
1092
|
+
kind: "command",
|
|
1093
|
+
value: command,
|
|
1094
|
+
summary: "Approval command emitted by the worker runtime.",
|
|
1095
|
+
})),
|
|
1096
|
+
keyPoints: approvalCommands,
|
|
1097
|
+
blockers: [blockingReason],
|
|
1098
|
+
followUps: [{
|
|
1099
|
+
type: "clarification",
|
|
1100
|
+
reason: blockingReason,
|
|
1101
|
+
}],
|
|
1102
|
+
questions: [
|
|
1103
|
+
"A worker command needs exec approval before this task can continue. Should TeamClaw retry after the approval policy is fixed or the commands are approved?",
|
|
1104
|
+
],
|
|
1105
|
+
notes: rawResult,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
905
1109
|
function isInternalRetryPrompt(value: string, stream?: string): boolean {
|
|
906
1110
|
if (stream !== "user") {
|
|
907
1111
|
return false;
|