@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.
Files changed (46) hide show
  1. package/README.md +52 -8
  2. package/cli.mjs +538 -224
  3. package/index.ts +76 -27
  4. package/openclaw.plugin.json +53 -28
  5. package/package.json +5 -2
  6. package/skills/teamclaw/SKILL.md +213 -0
  7. package/skills/teamclaw/references/api-quick-ref.md +117 -0
  8. package/skills/teamclaw-setup/SKILL.md +81 -0
  9. package/skills/teamclaw-setup/references/install-modes.md +136 -0
  10. package/skills/teamclaw-setup/references/validation-checklist.md +73 -0
  11. package/src/config.ts +44 -16
  12. package/src/controller/controller-capacity.ts +2 -2
  13. package/src/controller/controller-service.ts +193 -47
  14. package/src/controller/controller-tools.ts +102 -2
  15. package/src/controller/delivery-report.ts +563 -0
  16. package/src/controller/http-server.ts +1907 -172
  17. package/src/controller/kickoff-orchestrator.ts +292 -0
  18. package/src/controller/managed-gateway-process.ts +330 -0
  19. package/src/controller/orchestration-manifest.ts +69 -1
  20. package/src/controller/preview-manager.ts +676 -0
  21. package/src/controller/prompt-injector.ts +116 -67
  22. package/src/controller/role-inference.ts +41 -0
  23. package/src/controller/websocket.ts +3 -1
  24. package/src/controller/worker-provisioning.ts +429 -74
  25. package/src/discovery.ts +1 -1
  26. package/src/git-collaboration.ts +198 -47
  27. package/src/identity.ts +12 -2
  28. package/src/interaction-contracts.ts +179 -3
  29. package/src/networking.ts +99 -0
  30. package/src/openclaw-workspace.ts +478 -11
  31. package/src/prompt-policy.ts +381 -0
  32. package/src/roles.ts +37 -36
  33. package/src/state.ts +40 -1
  34. package/src/task-executor.ts +282 -78
  35. package/src/types.ts +150 -7
  36. package/src/ui/app.js +1403 -175
  37. package/src/ui/assets/teamclaw-app-icon.png +0 -0
  38. package/src/ui/index.html +122 -40
  39. package/src/ui/style.css +829 -143
  40. package/src/worker/http-handler.ts +40 -4
  41. package/src/worker/prompt-injector.ts +9 -38
  42. package/src/worker/skill-installer.ts +2 -2
  43. package/src/worker/tools.ts +31 -5
  44. package/src/worker/worker-service.ts +49 -8
  45. package/src/workspace-browser.ts +20 -7
  46. package/src/controller/local-worker-manager.ts +0 -533
@@ -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<string> => {
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: Math.min(
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
- const sliceTimeoutMs = Math.max(1_000, Math.min(RUN_WAIT_SLICE_MS, remainingMs));
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: sliceTimeoutMs,
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, Math.min(nextProbeAt - Date.now(), remainingMs));
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 result = assistantTurn.text;
463
- if (result && normalizeComparableText(result) !== normalizeComparableText(progressSnapshot.lastAssistantMessage)) {
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: result,
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 result;
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(taskDescription: string, taskId: string, roleLabel: string): string {
784
- return [
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
- "## Execution Rules",
792
- "- Deliver exactly the artifact requested by this task.",
793
- "- Follow the task verb literally: if the task asks for a brief, plan, matrix, review, package, positioning, or design artifact, produce that artifact and stop there.",
794
- "- Do NOT scaffold code, project structure, configs, or files unless the task explicitly asks for implementation work.",
795
- "- Do NOT create additional tasks, task trees, or duplicate follow-up work.",
796
- "- Do NOT re-scope this into a multi-role coordination workflow.",
797
- "- Do NOT delegate the core work of this task away to another role.",
798
- "- If Task Context includes recent completed deliverables, treat them as upstream inputs and search the shared workspace for any referenced task IDs or filenames before requesting clarification.",
799
- "- Do NOT attempt to inspect or resolve another worker's OpenClaw session or session key; those sessions are isolated per worker.",
800
- "- If the task includes a Recommended Skills section, use those skills first and prefer the exact listed slugs when searching for additional help.",
801
- "- Do NOT mark the task completed or failed via progress tools. Return the final deliverable (or raise an error) and let TeamClaw close the task.",
802
- "- If critical information is missing and you cannot proceed safely, request clarification and wait instead of guessing.",
803
- "- If more work is needed, mention it briefly in your result or use a handoff/review tool on this same task.",
804
- "- Before your final reply, submit a structured worker result contract with teamclaw_submit_result_contract so TeamClaw can route the next step without parsing prose.",
805
- `- Do NOT use sessions_yield or end your turn while background work, coding agents, or process sessions are still running; if the task is not complete yet, reply with exactly ${RATE_LIMIT_WAITING_SENTINEL}.`,
806
- "- Never return 'running in background' as the final result for a TeamClaw task. If you spawn a helper session, keep monitoring it and only return after you have the actual deliverable.",
807
- "- Use structured fields on progress, review, handoff, and messaging tools whenever coordination is needed.",
808
- `- When naming a role, use exact TeamClaw role IDs: ${TEAMCLAW_ROLE_IDS_TEXT}.`,
809
- ].join("\n");
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;