@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.
- package/README.md +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- 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
|
+
* - `` 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  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(``)
|
|
142
|
+
break
|
|
143
|
+
case 'video':
|
|
144
|
+
// Markdown img with video extension → chat renderer uses <video>
|
|
145
|
+
parts.push(``)
|
|
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
|
+
})
|