@swarmclawai/swarmclaw 0.2.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 (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,122 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import type { BoardTask } from '@/types'
4
+
5
+ import { DATA_DIR } from './data-dir'
6
+
7
+ const REPORTS_DIR = path.join(DATA_DIR, 'task-reports')
8
+ const MAX_REPORT_BODY = 6_000
9
+
10
+ const COMMAND_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
11
+ const FILE_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))\b/i
12
+ const VERIFICATION_HINT = /\b(test|tests|passed|failed|failing|lint|typecheck|build|verified|verification)\b/i
13
+
14
+ export interface TaskReportEvidence {
15
+ changedFiles: string[]
16
+ commandsRun: string[]
17
+ verification: string[]
18
+ hasEvidence: boolean
19
+ }
20
+
21
+ export interface TaskReportArtifact {
22
+ absolutePath: string
23
+ relativePath: string
24
+ evidence: TaskReportEvidence
25
+ }
26
+
27
+ function normalizeLine(value: string): string {
28
+ return value.replace(/\s+/g, ' ').trim()
29
+ }
30
+
31
+ function toLines(value: string): string[] {
32
+ return value
33
+ .split(/\r?\n/)
34
+ .map((line) => normalizeLine(line.replace(/^[-*]\s+/, '')))
35
+ .filter(Boolean)
36
+ }
37
+
38
+ function uniqueTop(values: string[], limit = 8): string[] {
39
+ const out: string[] = []
40
+ const seen = new Set<string>()
41
+ for (const value of values) {
42
+ const key = value.toLowerCase()
43
+ if (seen.has(key)) continue
44
+ seen.add(key)
45
+ out.push(value)
46
+ if (out.length >= limit) break
47
+ }
48
+ return out
49
+ }
50
+
51
+ function extractEvidence(result: string): TaskReportEvidence {
52
+ const lines = toLines(result)
53
+ const changedFiles = uniqueTop(lines.filter((line) => FILE_HINT.test(line)))
54
+ const commandsRun = uniqueTop(lines.filter((line) => COMMAND_HINT.test(line)))
55
+ const verification = uniqueTop(lines.filter((line) => VERIFICATION_HINT.test(line)))
56
+
57
+ return {
58
+ changedFiles,
59
+ commandsRun,
60
+ verification,
61
+ hasEvidence: changedFiles.length > 0 || commandsRun.length > 0 || verification.length > 0,
62
+ }
63
+ }
64
+
65
+ function bullets(title: string, values: string[]): string[] {
66
+ if (!values.length) return [`## ${title}`, '- Not provided', '']
67
+ return [`## ${title}`, ...values.map((value) => `- ${value}`), '']
68
+ }
69
+
70
+ export function ensureTaskCompletionReport(task: Partial<BoardTask>): TaskReportArtifact | null {
71
+ const id = typeof task.id === 'string' ? task.id.trim() : ''
72
+ if (!id) return null
73
+
74
+ const title = typeof task.title === 'string' && task.title.trim() ? task.title.trim() : 'Untitled Task'
75
+ const description = typeof task.description === 'string' ? task.description.trim() : ''
76
+ const result = typeof task.result === 'string' ? task.result.trim() : ''
77
+ const evidence = extractEvidence(result)
78
+
79
+ const reportPath = path.join(REPORTS_DIR, `${id}.md`)
80
+ const relativePath = path.relative(process.cwd(), reportPath)
81
+ const reportLines: string[] = [
82
+ `# Task ${id}: ${title}`,
83
+ '',
84
+ `- Status: ${task.status || 'unknown'}`,
85
+ `- Agent: ${task.agentId || 'unassigned'}`,
86
+ `- Session: ${task.sessionId || 'none'}`,
87
+ '',
88
+ ]
89
+
90
+ if (description) {
91
+ reportLines.push('## Description')
92
+ reportLines.push(description)
93
+ reportLines.push('')
94
+ }
95
+
96
+ if (result) {
97
+ reportLines.push('## Result Summary')
98
+ reportLines.push(result.slice(0, MAX_REPORT_BODY))
99
+ reportLines.push('')
100
+ } else {
101
+ reportLines.push('## Result Summary')
102
+ reportLines.push('No result summary provided.')
103
+ reportLines.push('')
104
+ }
105
+
106
+ reportLines.push(...bullets('Changed Files', evidence.changedFiles))
107
+ reportLines.push(...bullets('Commands Run', evidence.commandsRun))
108
+ reportLines.push(...bullets('Verification', evidence.verification))
109
+
110
+ const content = `${reportLines.join('\n').trim()}\n`
111
+ fs.mkdirSync(REPORTS_DIR, { recursive: true })
112
+ const current = fs.existsSync(reportPath) ? fs.readFileSync(reportPath, 'utf8') : null
113
+ if (current !== content) {
114
+ fs.writeFileSync(reportPath, content, 'utf8')
115
+ }
116
+
117
+ return {
118
+ absolutePath: reportPath,
119
+ relativePath,
120
+ evidence,
121
+ }
122
+ }
@@ -0,0 +1,161 @@
1
+ import { z } from 'zod'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Zod schemas for structured task result extraction
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export const ArtifactSchema = z.object({
8
+ url: z.string(),
9
+ type: z.enum(['image', 'video', 'pdf', 'file']),
10
+ filename: z.string(),
11
+ })
12
+
13
+ export const TaskResultSchema = z.object({
14
+ summary: z.string(),
15
+ artifacts: z.array(ArtifactSchema),
16
+ })
17
+
18
+ export type Artifact = z.infer<typeof ArtifactSchema>
19
+ export type TaskResult = z.infer<typeof TaskResultSchema>
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const SANDBOX_RE = /^sandbox:/
26
+ const UPLOAD_URL_RE = /(?:sandbox:)?\/api\/uploads\/[^\s)"'>\]]+/gi
27
+
28
+ function stripSandbox(url: string): string {
29
+ return url.replace(SANDBOX_RE, '')
30
+ }
31
+
32
+ function classifyArtifact(filename: string): Artifact['type'] {
33
+ if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(filename)) return 'image'
34
+ if (/\.(mp4|webm|mov|avi)$/i.test(filename)) return 'video'
35
+ if (/\.pdf$/i.test(filename)) return 'pdf'
36
+ return 'file'
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Session message types (loose to avoid coupling to full types)
41
+ // ---------------------------------------------------------------------------
42
+
43
+ interface MessageLike {
44
+ role?: string
45
+ text?: string
46
+ imageUrl?: string
47
+ imagePath?: string
48
+ toolEvents?: Array<{ name?: string; output?: string }>
49
+ }
50
+
51
+ interface SessionLike {
52
+ messages?: MessageLike[]
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Core extraction
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /**
60
+ * Walk a session's messages and extract all artifacts + a clean summary.
61
+ * Replaces the old regex-based `extractLatestUploadUrl` and
62
+ * `summarizeScheduleTaskResult` with a single Zod-validated pass.
63
+ */
64
+ export function extractTaskResult(
65
+ session: SessionLike | null | undefined,
66
+ rawResultText: string | null | undefined,
67
+ ): TaskResult {
68
+ const seen = new Set<string>()
69
+ const artifacts: Artifact[] = []
70
+
71
+ function addUrl(raw: string) {
72
+ const url = stripSandbox(raw)
73
+ if (seen.has(url)) return
74
+ seen.add(url)
75
+ const filename = url.split('/').pop()?.split('?')[0] || 'file'
76
+ artifacts.push({ url, type: classifyArtifact(filename), filename })
77
+ }
78
+
79
+ // Walk session messages to collect all artifact URLs
80
+ if (Array.isArray(session?.messages)) {
81
+ for (const msg of session.messages) {
82
+ // Explicit image fields
83
+ if (msg.imageUrl) addUrl(msg.imageUrl)
84
+ if (msg.imagePath) {
85
+ const basename = String(msg.imagePath).split('/').pop()
86
+ if (basename) addUrl(`/api/uploads/${basename}`)
87
+ }
88
+
89
+ // Scan message text
90
+ const text = typeof msg.text === 'string' ? msg.text : ''
91
+ for (const m of text.matchAll(UPLOAD_URL_RE)) addUrl(m[0])
92
+
93
+ // Scan tool event outputs
94
+ if (Array.isArray(msg.toolEvents)) {
95
+ for (const ev of msg.toolEvents) {
96
+ const output = typeof ev.output === 'string' ? ev.output : ''
97
+ for (const m of output.matchAll(UPLOAD_URL_RE)) addUrl(m[0])
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ // Clean summary: strip sandbox: prefixes from the raw text
104
+ const summary = (typeof rawResultText === 'string' ? rawResultText.trim() : '')
105
+ .replace(/sandbox:\/api\/uploads\//g, '/api/uploads/')
106
+
107
+ return TaskResultSchema.parse({ summary, artifacts })
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Formatting helpers for thread / main-chat messages
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /**
115
+ * Build the markdown body for a task result notification.
116
+ * Uses the same markdown patterns the chat bubble renderer already handles:
117
+ * - `![alt](url)` for images and videos → rendered as <img> / <video>
118
+ * - `[filename](url)` for PDFs and other files → rendered as download link
119
+ */
120
+ export function formatResultBody(result: TaskResult): string {
121
+ const parts: string[] = []
122
+
123
+ if (result.summary) {
124
+ // Remove any existing markdown image/link references to artifacts
125
+ // we'll re-add them properly below
126
+ let clean = result.summary
127
+ for (const a of result.artifacts) {
128
+ // Remove ![...](url) and [...](url) patterns for this artifact
129
+ clean = clean
130
+ .replace(new RegExp(`!\\[[^\\]]*\\]\\(${escapeRegex(a.url)}\\)`, 'g'), '')
131
+ .replace(new RegExp(`\\[[^\\]]*\\]\\(${escapeRegex(a.url)}\\)`, 'g'), '')
132
+ }
133
+ clean = clean.replace(/\n{3,}/g, '\n\n').trim()
134
+ if (clean) parts.push(clean)
135
+ }
136
+
137
+ // Add artifacts with proper markdown for each type
138
+ for (const artifact of result.artifacts) {
139
+ switch (artifact.type) {
140
+ case 'image':
141
+ parts.push(`![${artifact.filename}](${artifact.url})`)
142
+ break
143
+ case 'video':
144
+ // Markdown img with video extension → chat renderer uses <video>
145
+ parts.push(`![${artifact.filename}](${artifact.url})`)
146
+ break
147
+ case 'pdf':
148
+ parts.push(`[${artifact.filename}](${artifact.url})`)
149
+ break
150
+ case 'file':
151
+ parts.push(`[${artifact.filename}](${artifact.url})`)
152
+ break
153
+ }
154
+ }
155
+
156
+ return parts.join('\n\n')
157
+ }
158
+
159
+ function escapeRegex(s: string): string {
160
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
161
+ }
@@ -0,0 +1,27 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import { validateTaskCompletion } from './task-validation.ts'
4
+ import type { BoardTask } from '@/types'
5
+
6
+ test('validateTaskCompletion fails screenshot delivery tasks without artifact evidence', () => {
7
+ const validation = validateTaskCompletion({
8
+ title: 'Take screenshot and send it every minute',
9
+ description: 'Schedule a screenshot capture and deliver it to the user.',
10
+ result: 'Existing schedule verified for taking screenshots every minute. Waiting for next run.',
11
+ error: null,
12
+ } as Partial<BoardTask>)
13
+
14
+ assert.equal(validation.ok, false)
15
+ assert.ok(validation.reasons.some((reason) => reason.includes('Screenshot delivery task is missing artifact evidence')))
16
+ })
17
+
18
+ test('validateTaskCompletion accepts screenshot delivery tasks with upload artifact evidence', () => {
19
+ const validation = validateTaskCompletion({
20
+ title: 'Take screenshot and send it',
21
+ description: 'Capture Wikipedia and return the file to the user.',
22
+ result: 'Captured and sent screenshot successfully: sandbox:/api/uploads/1234-wikipedia.png',
23
+ error: null,
24
+ } as Partial<BoardTask>)
25
+
26
+ assert.equal(validation.ok, true)
27
+ })
@@ -0,0 +1,90 @@
1
+ import type { BoardTask } from '@/types'
2
+ import type { TaskReportArtifact } from './task-reports'
3
+
4
+ export interface TaskCompletionValidation {
5
+ ok: boolean
6
+ reasons: string[]
7
+ checkedAt: number
8
+ }
9
+
10
+ interface TaskCompletionValidationOptions {
11
+ report?: TaskReportArtifact | null
12
+ }
13
+
14
+ const MIN_RESULT_CHARS = 40
15
+
16
+ const WEAK_RESULT_PATTERNS: RegExp[] = [
17
+ /what can i help you with/i,
18
+ /waiting for approval/i,
19
+ /now let me write/i,
20
+ /what'?s the play/i,
21
+ /\bthe plan covers\b/i,
22
+ /now update the agent/i,
23
+ /\bzero typescript errors\b/i,
24
+ ]
25
+
26
+ const IMPLEMENTATION_HINT = /\b(add|build|create|fix|implement|integrat|refactor|update|write)\b/i
27
+ const EXECUTION_EVIDENCE = /\b(changed|updated|added|modified|files?|commands?|tests?|build|lint|typecheck|verified|report)\b/i
28
+ const SCREENSHOT_HINT = /\b(screenshot|screen shot|snapshot|capture)\b/i
29
+ const DELIVERY_HINT = /\b(send|deliver|return|share|upload|post|message)\b/i
30
+ const SCREENSHOT_ARTIFACT_HINT = /(?:sandbox:)?\/api\/uploads\/[^\s)\]]+|https?:\/\/[^\s)\]]+\.(?:png|jpe?g|webp|gif|pdf)\b/i
31
+ const SENT_SCREENSHOT_HINT = /\b(sent|shared|uploaded|returned)\b[^.]*\b(screenshot|snapshot|image)\b/i
32
+
33
+ function normalizeText(value: unknown): string {
34
+ if (typeof value !== 'string') return ''
35
+ return value.replace(/\s+/g, ' ').trim()
36
+ }
37
+
38
+ export function validateTaskCompletion(
39
+ task: Partial<BoardTask>,
40
+ options: TaskCompletionValidationOptions = {},
41
+ ): TaskCompletionValidation {
42
+ const reasons: string[] = []
43
+ const title = normalizeText(task.title)
44
+ const description = normalizeText(task.description)
45
+ const result = normalizeText(task.result)
46
+ const error = normalizeText(task.error)
47
+ const report = options.report || null
48
+
49
+ if (error) reasons.push('Task has a non-empty error field.')
50
+
51
+ if (!result) reasons.push('Result summary is empty.')
52
+ else {
53
+ if (result.length < MIN_RESULT_CHARS) reasons.push(`Result summary is too short (${result.length} chars).`)
54
+ if (WEAK_RESULT_PATTERNS.some((rx) => rx.test(result))) {
55
+ reasons.push('Result contains placeholder/planning language instead of completion evidence.')
56
+ }
57
+ }
58
+
59
+ // If task description/title suggests implementation work, require concrete evidence in
60
+ // the result summary OR task report.
61
+ const implementationTask = IMPLEMENTATION_HINT.test(title) || IMPLEMENTATION_HINT.test(description)
62
+ const hasResultEvidence = EXECUTION_EVIDENCE.test(result)
63
+ const hasReportEvidence = report?.evidence.hasEvidence === true
64
+ if (implementationTask && !hasResultEvidence && !hasReportEvidence) {
65
+ if (report?.relativePath) {
66
+ reasons.push(`Implementation task is missing concrete execution evidence in result or ${report.relativePath}.`)
67
+ } else {
68
+ reasons.push('Implementation task is missing concrete execution evidence in result.')
69
+ }
70
+ }
71
+
72
+ const screenshotTask = SCREENSHOT_HINT.test(title) || SCREENSHOT_HINT.test(description)
73
+ const screenshotDeliveryTask = screenshotTask && (DELIVERY_HINT.test(title) || DELIVERY_HINT.test(description))
74
+ if (screenshotDeliveryTask) {
75
+ const hasScreenshotArtifact = SCREENSHOT_ARTIFACT_HINT.test(result) || SENT_SCREENSHOT_HINT.test(result)
76
+ if (!hasScreenshotArtifact) {
77
+ reasons.push('Screenshot delivery task is missing artifact evidence (upload link or explicit sent screenshot confirmation).')
78
+ }
79
+ }
80
+
81
+ return {
82
+ ok: reasons.length === 0,
83
+ reasons,
84
+ checkedAt: Date.now(),
85
+ }
86
+ }
87
+
88
+ export function formatValidationFailure(reasons: string[]): string {
89
+ return `Completion validation failed: ${reasons.join(' ')}`
90
+ }
@@ -0,0 +1,58 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import {
4
+ resolveConcreteToolPolicyBlock,
5
+ resolveSessionToolPolicy,
6
+ } from './tool-capability-policy.ts'
7
+
8
+ test('capability policy permissive mode allows non-blocked tools', () => {
9
+ const decision = resolveSessionToolPolicy(['shell', 'web_search'], { capabilityPolicyMode: 'permissive' })
10
+ assert.deepEqual(decision.enabledTools, ['shell', 'web_search'])
11
+ assert.equal(decision.blockedTools.length, 0)
12
+ })
13
+
14
+ test('capability policy balanced mode blocks destructive delete_file', () => {
15
+ const decision = resolveSessionToolPolicy(['files', 'delete_file'], { capabilityPolicyMode: 'balanced' })
16
+ assert.deepEqual(decision.enabledTools, ['files'])
17
+ assert.equal(decision.blockedTools.length, 1)
18
+ assert.equal(decision.blockedTools[0].tool, 'delete_file')
19
+ })
20
+
21
+ test('capability policy strict mode blocks execution/platform families', () => {
22
+ const decision = resolveSessionToolPolicy(
23
+ ['shell', 'manage_tasks', 'web_search', 'memory'],
24
+ { capabilityPolicyMode: 'strict' },
25
+ )
26
+ assert.deepEqual(decision.enabledTools, ['web_search', 'memory'])
27
+ assert.equal(decision.blockedTools.some((entry) => entry.tool === 'shell'), true)
28
+ assert.equal(decision.blockedTools.some((entry) => entry.tool === 'manage_tasks'), true)
29
+ })
30
+
31
+ test('capability policy respects explicit allow overrides', () => {
32
+ const decision = resolveSessionToolPolicy(
33
+ ['shell', 'web_search'],
34
+ {
35
+ capabilityPolicyMode: 'strict',
36
+ capabilityAllowedTools: ['shell'],
37
+ },
38
+ )
39
+ assert.deepEqual(decision.enabledTools, ['shell', 'web_search'])
40
+ })
41
+
42
+ test('concrete tool checks inherit blocked family rules', () => {
43
+ const decision = resolveSessionToolPolicy(
44
+ ['claude_code', 'codex_cli'],
45
+ {
46
+ safetyBlockedTools: ['delegate_to_codex_cli'],
47
+ },
48
+ )
49
+
50
+ assert.equal(
51
+ resolveConcreteToolPolicyBlock('delegate_to_codex_cli', decision, { safetyBlockedTools: ['delegate_to_codex_cli'] }),
52
+ 'blocked by safety policy',
53
+ )
54
+ assert.equal(
55
+ resolveConcreteToolPolicyBlock('delegate_to_claude_code', decision, { safetyBlockedTools: ['delegate_to_codex_cli'] }),
56
+ null,
57
+ )
58
+ })