@swarmclawai/swarmclaw 0.6.7 → 0.7.0

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 (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -0,0 +1,44 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import { normalizeTaskQualityGate } from './task-quality-gate.ts'
4
+
5
+ test('normalizeTaskQualityGate uses defaults when unset', () => {
6
+ const gate = normalizeTaskQualityGate(undefined, undefined)
7
+ assert.equal(gate.enabled, true)
8
+ assert.equal(gate.minResultChars, 80)
9
+ assert.equal(gate.minEvidenceItems, 2)
10
+ assert.equal(gate.requireVerification, false)
11
+ assert.equal(gate.requireArtifact, false)
12
+ assert.equal(gate.requireReport, false)
13
+ })
14
+
15
+ test('normalizeTaskQualityGate respects app settings defaults', () => {
16
+ const gate = normalizeTaskQualityGate(null, {
17
+ taskQualityGateEnabled: false,
18
+ taskQualityGateMinResultChars: 120,
19
+ taskQualityGateMinEvidenceItems: 1,
20
+ taskQualityGateRequireVerification: true,
21
+ })
22
+ assert.equal(gate.enabled, false)
23
+ assert.equal(gate.minResultChars, 120)
24
+ assert.equal(gate.minEvidenceItems, 1)
25
+ assert.equal(gate.requireVerification, true)
26
+ })
27
+
28
+ test('normalizeTaskQualityGate allows per-task overrides on top of settings', () => {
29
+ const gate = normalizeTaskQualityGate({
30
+ enabled: true,
31
+ minResultChars: 64,
32
+ minEvidenceItems: 3,
33
+ requireArtifact: true,
34
+ }, {
35
+ taskQualityGateEnabled: false,
36
+ taskQualityGateMinResultChars: 120,
37
+ taskQualityGateMinEvidenceItems: 1,
38
+ taskQualityGateRequireArtifact: false,
39
+ })
40
+ assert.equal(gate.enabled, true)
41
+ assert.equal(gate.minResultChars, 64)
42
+ assert.equal(gate.minEvidenceItems, 3)
43
+ assert.equal(gate.requireArtifact, true)
44
+ })
@@ -0,0 +1,67 @@
1
+ import type { AppSettings, TaskQualityGateConfig } from '@/types'
2
+
3
+ export interface NormalizedTaskQualityGate {
4
+ enabled: boolean
5
+ minResultChars: number
6
+ minEvidenceItems: number
7
+ requireVerification: boolean
8
+ requireArtifact: boolean
9
+ requireReport: boolean
10
+ }
11
+
12
+ export const DEFAULT_TASK_QUALITY_GATE: NormalizedTaskQualityGate = {
13
+ enabled: true,
14
+ minResultChars: 80,
15
+ minEvidenceItems: 2,
16
+ requireVerification: false,
17
+ requireArtifact: false,
18
+ requireReport: false,
19
+ }
20
+
21
+ function normalizeInt(value: unknown, fallback: number, min: number, max: number): number {
22
+ const parsed = typeof value === 'number'
23
+ ? value
24
+ : typeof value === 'string'
25
+ ? Number.parseInt(value, 10)
26
+ : Number.NaN
27
+ if (!Number.isFinite(parsed)) return fallback
28
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
29
+ }
30
+
31
+ function normalizeBool(value: unknown, fallback: boolean): boolean {
32
+ if (typeof value === 'boolean') return value
33
+ if (typeof value === 'string') {
34
+ const normalized = value.trim().toLowerCase()
35
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
36
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false
37
+ }
38
+ return fallback
39
+ }
40
+
41
+ function normalizeSettingsDefaults(settings?: AppSettings | Record<string, unknown> | null): NormalizedTaskQualityGate {
42
+ const raw = settings && typeof settings === 'object' ? settings as Record<string, unknown> : {}
43
+ return {
44
+ enabled: normalizeBool(raw.taskQualityGateEnabled, DEFAULT_TASK_QUALITY_GATE.enabled),
45
+ minResultChars: normalizeInt(raw.taskQualityGateMinResultChars, DEFAULT_TASK_QUALITY_GATE.minResultChars, 10, 2000),
46
+ minEvidenceItems: normalizeInt(raw.taskQualityGateMinEvidenceItems, DEFAULT_TASK_QUALITY_GATE.minEvidenceItems, 0, 8),
47
+ requireVerification: normalizeBool(raw.taskQualityGateRequireVerification, DEFAULT_TASK_QUALITY_GATE.requireVerification),
48
+ requireArtifact: normalizeBool(raw.taskQualityGateRequireArtifact, DEFAULT_TASK_QUALITY_GATE.requireArtifact),
49
+ requireReport: normalizeBool(raw.taskQualityGateRequireReport, DEFAULT_TASK_QUALITY_GATE.requireReport),
50
+ }
51
+ }
52
+
53
+ export function normalizeTaskQualityGate(
54
+ rawGate?: TaskQualityGateConfig | Record<string, unknown> | null,
55
+ settings?: AppSettings | Record<string, unknown> | null,
56
+ ): NormalizedTaskQualityGate {
57
+ const defaults = normalizeSettingsDefaults(settings)
58
+ const raw = rawGate && typeof rawGate === 'object' ? rawGate as Record<string, unknown> : {}
59
+ return {
60
+ enabled: normalizeBool(raw.enabled, defaults.enabled),
61
+ minResultChars: normalizeInt(raw.minResultChars, defaults.minResultChars, 10, 2000),
62
+ minEvidenceItems: normalizeInt(raw.minEvidenceItems, defaults.minEvidenceItems, 0, 8),
63
+ requireVerification: normalizeBool(raw.requireVerification, defaults.requireVerification),
64
+ requireArtifact: normalizeBool(raw.requireArtifact, defaults.requireArtifact),
65
+ requireReport: normalizeBool(raw.requireReport, defaults.requireReport),
66
+ }
67
+ }
@@ -48,3 +48,81 @@ test('validateTaskCompletion still enforces stricter minimum for implementation
48
48
  assert.equal(validation.ok, false)
49
49
  assert.ok(validation.reasons.some((reason) => reason.includes('Result summary is too short')))
50
50
  })
51
+
52
+ test('validateTaskCompletion fails implementation task with unfinished next-step language', () => {
53
+ const validation = validateTaskCompletion({
54
+ title: 'Build weather dashboard',
55
+ description: 'Implement dashboard and run dev server.',
56
+ result: 'I prepared an outline. Next I will run the server once access is granted.',
57
+ error: null,
58
+ } as Partial<BoardTask>)
59
+
60
+ assert.equal(validation.ok, false)
61
+ assert.ok(validation.reasons.some((reason) => reason.includes('unfinished work')))
62
+ })
63
+
64
+ test('validateTaskCompletion fails implementation task that requests shell access', () => {
65
+ const validation = validateTaskCompletion({
66
+ title: 'Create blog and run server',
67
+ description: 'Create markdown blog and serve it.',
68
+ result: 'I created the blog file at data/workspace/blog/swarmclaw-blog.md, but I need access to the shell to proceed. Once the access is granted, I will finish setup.',
69
+ error: null,
70
+ } as Partial<BoardTask>)
71
+
72
+ assert.equal(validation.ok, false)
73
+ assert.ok(validation.reasons.some((reason) => reason.includes('unfinished work')))
74
+ })
75
+
76
+ test('validateTaskCompletion fails untitled tasks with empty metadata', () => {
77
+ const validation = validateTaskCompletion({
78
+ title: 'Untitled Task',
79
+ description: '',
80
+ result: 'Could you provide more information about what you need?',
81
+ error: null,
82
+ } as Partial<BoardTask>)
83
+
84
+ assert.equal(validation.ok, false)
85
+ assert.ok(validation.reasons.some((reason) => reason.includes('metadata is too vague')))
86
+ })
87
+
88
+ test('validateTaskCompletion enforces explicit quality gate evidence requirements', () => {
89
+ const validation = validateTaskCompletion({
90
+ title: 'Ship API migration summary',
91
+ description: 'Summarize the migration outcome.',
92
+ result: 'Migration summary completed successfully with no extra artifacts included.',
93
+ qualityGate: {
94
+ enabled: true,
95
+ minResultChars: 20,
96
+ minEvidenceItems: 2,
97
+ requireArtifact: true,
98
+ },
99
+ error: null,
100
+ } as Partial<BoardTask>)
101
+
102
+ assert.equal(validation.ok, false)
103
+ assert.ok(validation.reasons.some((reason) => reason.includes('insufficient completion evidence')))
104
+ assert.ok(validation.reasons.some((reason) => reason.includes('artifact evidence is required')))
105
+ })
106
+
107
+ test('validateTaskCompletion passes explicit quality gate when evidence checks are met', () => {
108
+ const validation = validateTaskCompletion({
109
+ title: 'Ship API migration summary',
110
+ description: 'Summarize the migration outcome.',
111
+ result: 'Ran npm test and tests passed. Updated src/api/migrate.ts. Uploaded evidence: sandbox:/api/uploads/migration-proof.png.',
112
+ artifacts: [{
113
+ url: 'sandbox:/api/uploads/migration-proof.png',
114
+ type: 'image',
115
+ filename: 'migration-proof.png',
116
+ }],
117
+ qualityGate: {
118
+ enabled: true,
119
+ minResultChars: 20,
120
+ minEvidenceItems: 2,
121
+ requireArtifact: true,
122
+ requireVerification: true,
123
+ },
124
+ error: null,
125
+ } as Partial<BoardTask>)
126
+
127
+ assert.equal(validation.ok, true)
128
+ })
@@ -1,5 +1,6 @@
1
1
  import type { BoardTask } from '@/types'
2
2
  import type { TaskReportArtifact } from './task-reports'
3
+ import { normalizeTaskQualityGate } from './task-quality-gate'
3
4
 
4
5
  export interface TaskCompletionValidation {
5
6
  ok: boolean
@@ -9,6 +10,7 @@ export interface TaskCompletionValidation {
9
10
 
10
11
  interface TaskCompletionValidationOptions {
11
12
  report?: TaskReportArtifact | null
13
+ settings?: Record<string, unknown> | null
12
14
  }
13
15
 
14
16
  const MIN_RESULT_CHARS_IMPLEMENTATION = 40
@@ -24,8 +26,26 @@ const WEAK_RESULT_PATTERNS: RegExp[] = [
24
26
  /\bzero typescript errors\b/i,
25
27
  ]
26
28
 
29
+ const INCOMPLETE_RESULT_PATTERNS: RegExp[] = [
30
+ /\b(?:next|then)\s*,?\s*i\s+(?:will|can|am going to)\b/i,
31
+ /\b(?:i(?:'| a)?ll|let me)\s+(?:start|begin|proceed|continue)\b/i,
32
+ /\b(?:once|when|after)\s+(?:the\s+)?(?:access|approval|permission)\s+(?:is|has been)\s+granted\b/i,
33
+ /\bneed (?:more )?(?:details|information|context)\b/i,
34
+ /\b(?:i|we)\s+(?:need|require)\s+(?:access|approval|permission)\b/i,
35
+ /\brequested\s+(?:access|approval|permission)\b/i,
36
+ /\bneed access to (?:the )?(?:shell|terminal|command line)\b/i,
37
+ /\battempted to\b[^.]{0,120}\b(?:but|however)\b/i,
38
+ /\bcould you provide\b/i,
39
+ /\blet me know once\b/i,
40
+ /\bthere (?:aren't|are not) any specific details\b/i,
41
+ ]
42
+
27
43
  const IMPLEMENTATION_HINT = /\b(add|build|create|fix|implement|integrat|refactor|update|write)\b/i
28
- const EXECUTION_EVIDENCE = /\b(changed|updated|added|modified|files?|commands?|tests?|build|lint|typecheck|verified|report)\b/i
44
+ const EXECUTION_ACTION_HINT = /\b(changed|updated|added|modified|implemented|refactored|fixed|ran|executed|verified)\b/i
45
+ const COMMAND_EVIDENCE_HINT = /\b(npm|pnpm|yarn|bun|node|npx|pytest|vitest|jest|playwright|go test|cargo test|deno test|python|pip|uv|docker|git)\b/i
46
+ const FILE_PATH_EVIDENCE_HINT = /\b[\w./-]+\.(ts|tsx|js|jsx|mjs|cjs|json|md|css|scss|html|yml|yaml|sh|py|go|rs|java|kt|swift|rb|php|sql|txt)\b/i
47
+ const ARTIFACT_EVIDENCE_HINT = /(?:sandbox:)?\/api\/uploads\/[^\s)\]]+|https?:\/\/[^\s)\]]+\.(?:png|jpe?g|webp|gif|pdf|zip)\b/i
48
+ const VERIFICATION_EVIDENCE_HINT = /\b(test|tests|lint|typecheck|build)\b[^.]{0,40}\b(pass(?:ed)?|fail(?:ed)?|ok|success)\b/i
29
49
  const SCREENSHOT_HINT = /\b(screenshot|screen shot|snapshot|capture)\b/i
30
50
  const DELIVERY_HINT = /\b(send|deliver|return|share|upload|post|message)\b/i
31
51
  const SCREENSHOT_ARTIFACT_HINT = /(?:sandbox:)?\/api\/uploads\/[^\s)\]]+|https?:\/\/[^\s)\]]+\.(?:png|jpe?g|webp|gif|pdf)\b/i
@@ -46,9 +66,14 @@ export function validateTaskCompletion(
46
66
  const result = normalizeText(task.result)
47
67
  const error = normalizeText(task.error)
48
68
  const report = options.report || null
69
+ const hasExplicitQualityGate = !!task.qualityGate && typeof task.qualityGate === 'object'
70
+ const qualityGate = normalizeTaskQualityGate(task.qualityGate || null, options.settings || null)
49
71
  const implementationTask = IMPLEMENTATION_HINT.test(title) || IMPLEMENTATION_HINT.test(description)
50
72
 
51
73
  if (error) reasons.push('Task has a non-empty error field.')
74
+ if (/^untitled task$/i.test(title) && !description) {
75
+ reasons.push('Task metadata is too vague (untitled title with empty description).')
76
+ }
52
77
 
53
78
  if (!result) reasons.push('Result summary is empty.')
54
79
  else {
@@ -57,11 +82,20 @@ export function validateTaskCompletion(
57
82
  if (WEAK_RESULT_PATTERNS.some((rx) => rx.test(result))) {
58
83
  reasons.push('Result contains placeholder/planning language instead of completion evidence.')
59
84
  }
85
+ if (INCOMPLETE_RESULT_PATTERNS.some((rx) => rx.test(result))) {
86
+ reasons.push('Result indicates unfinished work or missing inputs instead of completed execution.')
87
+ }
60
88
  }
61
89
 
62
90
  // If task description/title suggests implementation work, require concrete evidence in
63
91
  // the result summary OR task report.
64
- const hasResultEvidence = EXECUTION_EVIDENCE.test(result)
92
+ const hasResultEvidence = (
93
+ COMMAND_EVIDENCE_HINT.test(result)
94
+ || ARTIFACT_EVIDENCE_HINT.test(result)
95
+ || VERIFICATION_EVIDENCE_HINT.test(result)
96
+ || (EXECUTION_ACTION_HINT.test(result)
97
+ && (/\b(command|test|lint|typecheck|build|file|artifact)\b/i.test(result) || FILE_PATH_EVIDENCE_HINT.test(result)))
98
+ )
65
99
  const hasReportEvidence = report?.evidence.hasEvidence === true
66
100
  if (implementationTask && !hasResultEvidence && !hasReportEvidence) {
67
101
  if (report?.relativePath) {
@@ -80,6 +114,37 @@ export function validateTaskCompletion(
80
114
  }
81
115
  }
82
116
 
117
+ if (qualityGate.enabled && (implementationTask || hasExplicitQualityGate)) {
118
+ if (result && result.length < qualityGate.minResultChars) {
119
+ reasons.push(`Quality gate: result summary is shorter than required minimum (${result.length} chars; min ${qualityGate.minResultChars}).`)
120
+ }
121
+
122
+ const hasCommandEvidence = COMMAND_EVIDENCE_HINT.test(result) || (report?.evidence.commandsRun.length || 0) > 0
123
+ const hasFileEvidence = FILE_PATH_EVIDENCE_HINT.test(result) || (report?.evidence.changedFiles.length || 0) > 0
124
+ const hasVerificationEvidence = VERIFICATION_EVIDENCE_HINT.test(result) || (report?.evidence.verification.length || 0) > 0
125
+ const hasArtifactEvidence = ARTIFACT_EVIDENCE_HINT.test(result) || ((task.artifacts?.length || 0) > 0)
126
+
127
+ const evidenceSignals = [
128
+ hasCommandEvidence,
129
+ hasFileEvidence,
130
+ hasVerificationEvidence,
131
+ hasArtifactEvidence,
132
+ ].filter(Boolean).length
133
+
134
+ if (evidenceSignals < qualityGate.minEvidenceItems) {
135
+ reasons.push(`Quality gate: insufficient completion evidence (${evidenceSignals}/${qualityGate.minEvidenceItems} required evidence signals).`)
136
+ }
137
+ if (qualityGate.requireVerification && !hasVerificationEvidence) {
138
+ reasons.push('Quality gate: verification evidence is required (tests/lint/build/check output missing).')
139
+ }
140
+ if (qualityGate.requireArtifact && !hasArtifactEvidence) {
141
+ reasons.push('Quality gate: artifact evidence is required (artifact URL/upload or structured artifacts list missing).')
142
+ }
143
+ if (qualityGate.requireReport && !report?.relativePath) {
144
+ reasons.push('Quality gate: task completion report is required but missing.')
145
+ }
146
+ }
147
+
83
148
  return {
84
149
  ok: reasons.length === 0,
85
150
  reasons,
@@ -0,0 +1,68 @@
1
+ const TOOL_ALIAS_GROUPS: string[][] = [
2
+ ['shell', 'execute_command', 'process_tool', 'process'],
3
+ ['files', 'read_file', 'write_file', 'list_files', 'copy_file', 'move_file', 'delete_file', 'send_file'],
4
+ ['edit_file'],
5
+ ['web', 'web_search', 'web_fetch'],
6
+ ['browser', 'openclaw_browser'],
7
+ ['delegate', 'claude_code', 'codex_cli', 'opencode_cli', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli'],
8
+ ['manage_platform', 'manage_agents', 'manage_tasks', 'manage_schedules', 'manage_skills', 'manage_documents', 'manage_webhooks', 'manage_secrets', 'manage_sessions'],
9
+ ['manage_connectors', 'connectors', 'connector_message_tool'],
10
+ ['manage_chatrooms', 'chatroom'],
11
+ ['spawn_subagent', 'subagent', 'delegate_to_agent'],
12
+ ['manage_sessions', 'session_info', 'sessions_tool', 'whoami_tool', 'search_history_tool'],
13
+ ['schedule_wake', 'schedule'],
14
+ ['http_request', 'http'],
15
+ ['memory', 'memory_tool'],
16
+ ['sandbox', 'sandbox_exec', 'sandbox_list_runtimes'],
17
+ ['wallet', 'wallet_tool'],
18
+ ['monitor', 'monitor_tool'],
19
+ ['sample_ui', 'show_plugin_card'],
20
+ ['context_mgmt', 'context_status', 'context_summarize'],
21
+ ['openclaw_workspace'],
22
+ ['openclaw_nodes'],
23
+ ]
24
+
25
+ const TOOL_ALIAS_MAP = (() => {
26
+ const map = new Map<string, Set<string>>()
27
+ for (const group of TOOL_ALIAS_GROUPS) {
28
+ const normalized = group.map((tool) => tool.trim().toLowerCase()).filter(Boolean)
29
+ for (const tool of normalized) {
30
+ const current = map.get(tool) || new Set<string>()
31
+ for (const alias of normalized) current.add(alias)
32
+ map.set(tool, current)
33
+ }
34
+ }
35
+ return map
36
+ })()
37
+
38
+ export function normalizeToolId(value: unknown): string {
39
+ return typeof value === 'string' ? value.trim().toLowerCase() : ''
40
+ }
41
+
42
+ export function expandToolIds(values: string[] | null | undefined): string[] {
43
+ if (!Array.isArray(values) || values.length === 0) return []
44
+ const expanded = new Set<string>()
45
+ const queue: string[] = values
46
+ .map((tool) => normalizeToolId(tool))
47
+ .filter(Boolean)
48
+
49
+ while (queue.length > 0) {
50
+ const next = queue.shift()!
51
+ if (expanded.has(next)) continue
52
+ expanded.add(next)
53
+ const aliases = TOOL_ALIAS_MAP.get(next)
54
+ if (!aliases) continue
55
+ for (const alias of aliases) {
56
+ if (!expanded.has(alias)) queue.push(alias)
57
+ }
58
+ }
59
+
60
+ return Array.from(expanded)
61
+ }
62
+
63
+ export function toolIdMatches(enabledTools: string[] | null | undefined, toolId: string): boolean {
64
+ const normalized = normalizeToolId(toolId)
65
+ if (!normalized) return false
66
+ return expandToolIds(enabledTools).includes(normalized)
67
+ }
68
+
@@ -32,9 +32,9 @@ interface ToolDescriptor {
32
32
  }
33
33
 
34
34
  const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
35
- shell: { categories: ['execution'], concreteTools: ['execute_command'] },
36
- process: { categories: ['execution'], concreteTools: ['process_tool'] },
37
- files: { categories: ['filesystem'], concreteTools: ['read_file', 'write_file', 'list_files', 'send_file'] },
35
+ shell: { categories: ['execution'], concreteTools: ['shell', 'execute_command'] },
36
+ process: { categories: ['execution'], concreteTools: ['process', 'process_tool'] },
37
+ files: { categories: ['filesystem'], concreteTools: ['files', 'read_file', 'write_file', 'list_files', 'send_file'] },
38
38
  read_file: { categories: ['filesystem'], concreteTools: ['read_file'] },
39
39
  write_file: { categories: ['filesystem'], concreteTools: ['write_file'] },
40
40
  list_files: { categories: ['filesystem'], concreteTools: ['list_files'] },
@@ -43,22 +43,41 @@ const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
43
43
  move_file: { categories: ['filesystem'], concreteTools: ['move_file'] },
44
44
  edit_file: { categories: ['filesystem'], concreteTools: ['edit_file'] },
45
45
  delete_file: { categories: ['filesystem'], concreteTools: ['delete_file'], destructive: true },
46
+ web: { categories: ['network'], concreteTools: ['web', 'web_search', 'web_fetch'] },
46
47
  web_search: { categories: ['network'], concreteTools: ['web_search'] },
47
48
  web_fetch: { categories: ['network'], concreteTools: ['web_fetch'] },
48
- browser: { categories: ['browser', 'network'], concreteTools: ['browser'] },
49
+ browser: { categories: ['browser', 'network'], concreteTools: ['browser', 'openclaw_browser'] },
50
+ delegate: { categories: ['delegation', 'execution'], concreteTools: ['delegate', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli'] },
49
51
  claude_code: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_claude_code'] },
50
52
  codex_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_codex_cli'] },
51
53
  opencode_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_opencode_cli'] },
52
- memory: { categories: ['memory'], concreteTools: ['memory_tool', 'context_status', 'context_summarize'] },
54
+ memory: { categories: ['memory'], concreteTools: ['memory', 'memory_tool', 'context_status', 'context_summarize'] },
55
+ sandbox: { categories: ['execution', 'filesystem'], concreteTools: ['sandbox', 'sandbox_exec', 'sandbox_list_runtimes', 'openclaw_sandbox'] },
56
+ git: { categories: ['execution', 'filesystem'], concreteTools: ['git'] },
57
+ http_request: { categories: ['network'], concreteTools: ['http_request'] },
58
+ canvas: { categories: ['filesystem'], concreteTools: ['canvas'] },
59
+ wallet: { categories: ['outbound'], concreteTools: ['wallet', 'wallet_tool'] },
60
+ monitor: { categories: ['execution'], concreteTools: ['monitor', 'monitor_tool'] },
61
+ openclaw_workspace: { categories: ['filesystem', 'platform'], concreteTools: ['openclaw_workspace'] },
62
+ openclaw_nodes: { categories: ['platform'], concreteTools: ['openclaw_nodes'] },
63
+ manage_platform: { categories: ['platform'], concreteTools: ['manage_platform', 'manage_agents', 'manage_tasks', 'manage_schedules', 'manage_skills', 'manage_documents', 'manage_webhooks', 'manage_connectors', 'manage_sessions', 'manage_secrets'] },
53
64
  manage_agents: { categories: ['platform'], concreteTools: ['manage_agents'] },
54
65
  manage_tasks: { categories: ['platform'], concreteTools: ['manage_tasks'] },
55
66
  manage_schedules: { categories: ['platform'], concreteTools: ['manage_schedules'] },
67
+ schedule_wake: { categories: ['platform'], concreteTools: ['schedule_wake'] },
56
68
  manage_skills: { categories: ['platform'], concreteTools: ['manage_skills'] },
57
69
  manage_documents: { categories: ['platform'], concreteTools: ['manage_documents'] },
58
70
  manage_webhooks: { categories: ['platform', 'network'], concreteTools: ['manage_webhooks'] },
71
+ connectors: { categories: ['platform', 'outbound'], concreteTools: ['connectors', 'connector_message_tool'] },
59
72
  manage_connectors: { categories: ['platform', 'outbound'], concreteTools: ['manage_connectors', 'connector_message_tool'] },
73
+ session_info: { categories: ['platform'], concreteTools: ['session_info', 'sessions_tool', 'search_history_tool', 'whoami_tool'] },
60
74
  manage_sessions: { categories: ['platform'], concreteTools: ['manage_sessions', 'sessions_tool', 'search_history_tool', 'whoami_tool'] },
61
75
  manage_secrets: { categories: ['platform'], concreteTools: ['manage_secrets'] },
76
+ manage_chatrooms: { categories: ['platform'], concreteTools: ['manage_chatrooms', 'chatroom'] },
77
+ spawn_subagent: { categories: ['delegation', 'platform'], concreteTools: ['spawn_subagent', 'delegate_to_agent'] },
78
+ context_mgmt: { categories: ['memory'], concreteTools: ['context_mgmt', 'context_status', 'context_summarize'] },
79
+ plugin_creator: { categories: ['filesystem', 'execution'], concreteTools: ['plugin_creator', 'plugin_creator_tool'] },
80
+ sample_ui: { categories: ['platform'], concreteTools: ['sample_ui', 'show_plugin_card'] },
62
81
  }
63
82
 
64
83
  const CONCRETE_TOOL_TO_SESSION_TOOL = new Map<string, string>()
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Structured retry with exponential backoff for transient tool failures.
3
+ */
4
+
5
+ export interface RetryOptions {
6
+ maxAttempts?: number
7
+ backoffMs?: number
8
+ retryable?: RegExp[]
9
+ }
10
+
11
+ const DEFAULT_RETRYABLE: RegExp[] = [
12
+ /timeout/i,
13
+ /ECONNRESET/i,
14
+ /ENOTFOUND/i,
15
+ /429/,
16
+ /503/,
17
+ /rate.?limit/i,
18
+ ]
19
+
20
+ const DEFAULT_MAX_ATTEMPTS = 3
21
+ const DEFAULT_BACKOFF_MS = 2000
22
+
23
+ function isRetryableError(error: string, patterns: RegExp[]): boolean {
24
+ return patterns.some((p) => p.test(error))
25
+ }
26
+
27
+ function sleep(ms: number): Promise<void> {
28
+ return new Promise((resolve) => setTimeout(resolve, ms))
29
+ }
30
+
31
+ /**
32
+ * Wraps a tool handler function with retry logic for transient failures.
33
+ * The wrapped function must return a string (tool output).
34
+ * Retries only when the returned string matches a retryable pattern
35
+ * (tool handlers typically return error strings rather than throwing).
36
+ */
37
+ export async function withRetry<TArgs>(
38
+ fn: (args: TArgs) => Promise<string>,
39
+ args: TArgs,
40
+ opts?: RetryOptions,
41
+ ): Promise<string> {
42
+ const maxAttempts = opts?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS
43
+ const backoffMs = opts?.backoffMs ?? DEFAULT_BACKOFF_MS
44
+ const retryable = opts?.retryable ?? DEFAULT_RETRYABLE
45
+
46
+ let lastResult = ''
47
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
48
+ lastResult = await fn(args)
49
+
50
+ // Only retry if the result looks like a retryable error
51
+ if (attempt < maxAttempts && isRetryableError(lastResult, retryable)) {
52
+ const delay = backoffMs * Math.pow(2, attempt - 1)
53
+ console.warn(
54
+ `[tool-retry] Attempt ${attempt}/${maxAttempts} matched retryable pattern, retrying in ${delay}ms`,
55
+ )
56
+ await sleep(delay)
57
+ continue
58
+ }
59
+ return lastResult
60
+ }
61
+ return lastResult
62
+ }
@@ -0,0 +1,72 @@
1
+ import type { Message } from '@/types'
2
+
3
+ /**
4
+ * Repairs a conversation transcript by ensuring that tool events remain associated
5
+ * with their parent assistant messages during pruning or manipulation.
6
+ *
7
+ * In SwarmClaw, toolEvents are nested within the Message object, so "orphaning"
8
+ * is less of a structural risk than in OpenClaw, but we still need to ensure
9
+ * consistency during context management.
10
+ */
11
+ export function repairTranscriptConsistency(messages: Message[]): Message[] {
12
+ // SwarmClaw specific: ensure that 'system' messages like [Context Summary]
13
+ // are preserved correctly and that nested toolEvents are valid.
14
+ return messages.map(m => {
15
+ if (m.role === 'assistant' && m.toolEvents) {
16
+ // Filter out empty or malformed tool events that might cause LLM confusion
17
+ const validTools = m.toolEvents.filter(t => t.name && t.input)
18
+ if (validTools.length !== m.toolEvents.length) {
19
+ return { ...m, toolEvents: validTools }
20
+ }
21
+ }
22
+ return m
23
+ })
24
+ }
25
+
26
+ /**
27
+ * Checks for and repairs common transcript issues that cause LLM provider errors.
28
+ * (e.g. consecutive user messages, trailing assistant messages without text).
29
+ */
30
+ export function finalProviderTranscriptSanityCheck(messages: Message[]): Message[] {
31
+ if (messages.length === 0) return []
32
+
33
+ const out: Message[] = []
34
+ for (let i = 0; i < messages.length; i++) {
35
+ const m = messages[i]
36
+
37
+ // 1. Skip messages marked as suppressed
38
+ if (m.suppressed) continue
39
+
40
+ // 2. Prevent consecutive messages of same role (some providers are strict)
41
+ const prev = out.at(-1)
42
+ if (prev && prev.role === m.role) {
43
+ if (m.role === 'user') {
44
+ // Merge consecutive user messages
45
+ prev.text = `${prev.text}\n\n${m.text}`
46
+ if (m.imagePath) prev.imagePath = m.imagePath
47
+ if (m.imageUrl) prev.imageUrl = m.imageUrl
48
+ continue
49
+ } else {
50
+ // Assistant consecutive? Keep the one with tool events or the longer one
51
+ const mTools = m.toolEvents?.length || 0
52
+ const pTools = prev.toolEvents?.length || 0
53
+ if (mTools > pTools || m.text.length > prev.text.length) {
54
+ out[out.length - 1] = m
55
+ }
56
+ continue
57
+ }
58
+ }
59
+
60
+ out.push(m)
61
+ }
62
+
63
+ // 3. Ensure the transcript doesn't end with an empty assistant message
64
+ if (out.length > 0 && out.at(-1)?.role === 'assistant') {
65
+ const last = out.at(-1)!
66
+ if (!last.text.trim() && (!last.toolEvents || last.toolEvents.length === 0)) {
67
+ out.pop()
68
+ }
69
+ }
70
+
71
+ return out
72
+ }
@@ -157,6 +157,7 @@ export const STARTER_AGENT_TOOLS = [
157
157
  'manage_agents',
158
158
  'manage_tasks',
159
159
  'manage_schedules',
160
+ 'schedule_wake',
160
161
  'manage_skills',
161
162
  'manage_connectors',
162
163
  'manage_sessions',
package/src/lib/tasks.ts CHANGED
@@ -4,7 +4,13 @@ import type { BoardTask } from '../types'
4
4
  export const fetchTasks = (includeArchived = false) =>
5
5
  api<Record<string, BoardTask>>('GET', `/tasks${includeArchived ? '?includeArchived=true' : ''}`)
6
6
 
7
- export const createTask = (data: { title: string; description: string; agentId: string; status?: string }) =>
7
+ export const createTask = (data: {
8
+ title: string
9
+ description: string
10
+ agentId: string
11
+ status?: string
12
+ qualityGate?: BoardTask['qualityGate']
13
+ }) =>
8
14
  api<BoardTask>('POST', '/tasks', data)
9
15
 
10
16
  export const updateTask = (id: string, data: Partial<BoardTask>) =>