@swarmclawai/swarmclaw 0.6.8 → 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 (166) hide show
  1. package/README.md +70 -45
  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 +18 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  9. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  10. package/src/app/api/memory/route.ts +36 -5
  11. package/src/app/api/notifications/route.ts +3 -0
  12. package/src/app/api/plugins/install/route.ts +57 -5
  13. package/src/app/api/plugins/marketplace/route.ts +73 -22
  14. package/src/app/api/plugins/route.ts +61 -1
  15. package/src/app/api/plugins/ui/route.ts +34 -0
  16. package/src/app/api/settings/route.ts +62 -0
  17. package/src/app/api/setup/doctor/route.ts +22 -5
  18. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  19. package/src/app/api/tasks/[id]/route.ts +11 -3
  20. package/src/app/api/tasks/route.ts +8 -2
  21. package/src/app/globals.css +27 -0
  22. package/src/app/page.tsx +10 -5
  23. package/src/cli/index.js +13 -0
  24. package/src/components/activity/activity-feed.tsx +9 -2
  25. package/src/components/agents/agent-avatar.tsx +5 -1
  26. package/src/components/agents/agent-card.tsx +55 -9
  27. package/src/components/agents/agent-sheet.tsx +86 -29
  28. package/src/components/agents/inspector-panel.tsx +1 -1
  29. package/src/components/auth/access-key-gate.tsx +63 -54
  30. package/src/components/auth/user-picker.tsx +37 -32
  31. package/src/components/chat/chat-area.tsx +11 -0
  32. package/src/components/chat/chat-header.tsx +69 -25
  33. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  34. package/src/components/chat/code-block.tsx +3 -1
  35. package/src/components/chat/exec-approval-card.tsx +8 -1
  36. package/src/components/chat/message-bubble.tsx +164 -4
  37. package/src/components/chat/message-list.tsx +30 -4
  38. package/src/components/chat/session-approval-card.tsx +80 -0
  39. package/src/components/chat/streaming-bubble.tsx +6 -5
  40. package/src/components/chat/thinking-indicator.tsx +48 -12
  41. package/src/components/chat/tool-request-banner.tsx +39 -20
  42. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  43. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  44. package/src/components/connectors/connector-list.tsx +33 -11
  45. package/src/components/connectors/connector-sheet.tsx +29 -6
  46. package/src/components/home/home-view.tsx +20 -14
  47. package/src/components/input/chat-input.tsx +22 -1
  48. package/src/components/knowledge/knowledge-list.tsx +17 -18
  49. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  50. package/src/components/layout/app-layout.tsx +73 -21
  51. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  52. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  53. package/src/components/memory/memory-list.tsx +20 -13
  54. package/src/components/plugins/plugin-list.tsx +213 -59
  55. package/src/components/plugins/plugin-sheet.tsx +119 -24
  56. package/src/components/projects/project-list.tsx +17 -9
  57. package/src/components/providers/provider-list.tsx +21 -6
  58. package/src/components/providers/provider-sheet.tsx +42 -25
  59. package/src/components/runs/run-list.tsx +17 -13
  60. package/src/components/schedules/schedule-card.tsx +10 -3
  61. package/src/components/schedules/schedule-list.tsx +2 -2
  62. package/src/components/schedules/schedule-sheet.tsx +19 -7
  63. package/src/components/secrets/secret-sheet.tsx +7 -2
  64. package/src/components/secrets/secrets-list.tsx +18 -5
  65. package/src/components/sessions/new-session-sheet.tsx +183 -376
  66. package/src/components/sessions/session-card.tsx +10 -2
  67. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  68. package/src/components/shared/command-palette.tsx +13 -5
  69. package/src/components/shared/empty-state.tsx +20 -8
  70. package/src/components/shared/notification-center.tsx +134 -86
  71. package/src/components/shared/profile-sheet.tsx +4 -0
  72. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  73. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  74. package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
  75. package/src/components/skills/clawhub-browser.tsx +1 -0
  76. package/src/components/skills/skill-list.tsx +31 -12
  77. package/src/components/skills/skill-sheet.tsx +20 -7
  78. package/src/components/tasks/approvals-panel.tsx +170 -66
  79. package/src/components/tasks/task-board.tsx +20 -12
  80. package/src/components/tasks/task-card.tsx +21 -7
  81. package/src/components/tasks/task-column.tsx +4 -3
  82. package/src/components/tasks/task-list.tsx +1 -1
  83. package/src/components/tasks/task-sheet.tsx +130 -1
  84. package/src/components/ui/dialog.tsx +1 -0
  85. package/src/components/ui/sheet.tsx +1 -0
  86. package/src/components/usage/metrics-dashboard.tsx +66 -64
  87. package/src/components/wallets/wallet-panel.tsx +65 -41
  88. package/src/components/wallets/wallet-section.tsx +9 -3
  89. package/src/components/webhooks/webhook-list.tsx +21 -12
  90. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  91. package/src/lib/approval-display.test.ts +45 -0
  92. package/src/lib/approval-display.ts +62 -0
  93. package/src/lib/clipboard.ts +38 -0
  94. package/src/lib/memory.ts +8 -0
  95. package/src/lib/providers/claude-cli.ts +5 -3
  96. package/src/lib/providers/index.ts +67 -21
  97. package/src/lib/runtime-loop.ts +3 -2
  98. package/src/lib/server/approvals.ts +150 -0
  99. package/src/lib/server/chat-execution.ts +223 -62
  100. package/src/lib/server/clawhub-client.ts +82 -6
  101. package/src/lib/server/connectors/manager.ts +27 -1
  102. package/src/lib/server/cost.test.ts +73 -0
  103. package/src/lib/server/cost.ts +165 -34
  104. package/src/lib/server/daemon-state.ts +42 -0
  105. package/src/lib/server/data-dir.ts +18 -1
  106. package/src/lib/server/integrity-monitor.ts +208 -0
  107. package/src/lib/server/llm-response-cache.test.ts +102 -0
  108. package/src/lib/server/llm-response-cache.ts +227 -0
  109. package/src/lib/server/main-agent-loop.ts +1 -1
  110. package/src/lib/server/main-session.ts +6 -3
  111. package/src/lib/server/mcp-conformance.test.ts +18 -0
  112. package/src/lib/server/mcp-conformance.ts +233 -0
  113. package/src/lib/server/memory-db.ts +180 -17
  114. package/src/lib/server/memory-retrieval.test.ts +56 -0
  115. package/src/lib/server/orchestrator-lg.ts +4 -1
  116. package/src/lib/server/orchestrator.ts +4 -3
  117. package/src/lib/server/plugins.ts +650 -142
  118. package/src/lib/server/process-manager.ts +18 -0
  119. package/src/lib/server/queue.ts +253 -11
  120. package/src/lib/server/runtime-settings.ts +9 -0
  121. package/src/lib/server/session-run-manager.test.ts +23 -0
  122. package/src/lib/server/session-run-manager.ts +11 -1
  123. package/src/lib/server/session-tools/canvas.ts +85 -50
  124. package/src/lib/server/session-tools/chatroom.ts +130 -127
  125. package/src/lib/server/session-tools/connector.ts +233 -454
  126. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  127. package/src/lib/server/session-tools/crud.ts +84 -7
  128. package/src/lib/server/session-tools/delegate.ts +351 -752
  129. package/src/lib/server/session-tools/discovery.ts +198 -0
  130. package/src/lib/server/session-tools/edit_file.ts +82 -0
  131. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  132. package/src/lib/server/session-tools/file.ts +257 -425
  133. package/src/lib/server/session-tools/git.ts +87 -47
  134. package/src/lib/server/session-tools/http.ts +85 -33
  135. package/src/lib/server/session-tools/index.ts +205 -160
  136. package/src/lib/server/session-tools/memory.ts +152 -265
  137. package/src/lib/server/session-tools/monitor.ts +126 -0
  138. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  139. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  140. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  141. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  142. package/src/lib/server/session-tools/platform.ts +86 -0
  143. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  144. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  145. package/src/lib/server/session-tools/sandbox.ts +175 -148
  146. package/src/lib/server/session-tools/schedule.ts +66 -31
  147. package/src/lib/server/session-tools/session-info.ts +104 -410
  148. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  149. package/src/lib/server/session-tools/shell.ts +171 -143
  150. package/src/lib/server/session-tools/subagent.ts +77 -77
  151. package/src/lib/server/session-tools/wallet.ts +182 -106
  152. package/src/lib/server/session-tools/web.ts +179 -349
  153. package/src/lib/server/storage.ts +24 -0
  154. package/src/lib/server/stream-agent-chat.ts +301 -244
  155. package/src/lib/server/task-quality-gate.test.ts +44 -0
  156. package/src/lib/server/task-quality-gate.ts +67 -0
  157. package/src/lib/server/task-validation.test.ts +78 -0
  158. package/src/lib/server/task-validation.ts +67 -2
  159. package/src/lib/server/tool-aliases.ts +68 -0
  160. package/src/lib/server/tool-capability-policy.ts +23 -5
  161. package/src/lib/tasks.ts +7 -1
  162. package/src/lib/tool-definitions.ts +23 -23
  163. package/src/lib/validation/schemas.ts +12 -0
  164. package/src/lib/view-routes.ts +2 -24
  165. package/src/stores/use-app-store.ts +23 -1
  166. package/src/types/index.ts +121 -7
@@ -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,13 +43,24 @@ 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'] },
@@ -57,9 +68,16 @@ const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
57
68
  manage_skills: { categories: ['platform'], concreteTools: ['manage_skills'] },
58
69
  manage_documents: { categories: ['platform'], concreteTools: ['manage_documents'] },
59
70
  manage_webhooks: { categories: ['platform', 'network'], concreteTools: ['manage_webhooks'] },
71
+ connectors: { categories: ['platform', 'outbound'], concreteTools: ['connectors', 'connector_message_tool'] },
60
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'] },
61
74
  manage_sessions: { categories: ['platform'], concreteTools: ['manage_sessions', 'sessions_tool', 'search_history_tool', 'whoami_tool'] },
62
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'] },
63
81
  }
64
82
 
65
83
  const CONCRETE_TOOL_TO_SESSION_TOOL = new Map<string, string>()
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>) =>
@@ -4,40 +4,39 @@ export interface ToolDefinition {
4
4
  description: string
5
5
  }
6
6
 
7
+ /**
8
+ * Standard dynamic tools.
9
+ * Many granular tools (read_file, write_file, etc.) are now unified under 'files'.
10
+ */
7
11
  export const AVAILABLE_TOOLS: ToolDefinition[] = [
8
- { id: 'shell', label: 'Shell', description: 'Execute commands in the working directory' },
9
- { id: 'files', label: 'Files', description: 'Read, write, list, move, copy, and send files' },
10
- { id: 'copy_file', label: 'Copy File', description: 'Copy files within the working directory' },
11
- { id: 'move_file', label: 'Move File', description: 'Move/rename files within the working directory' },
12
- { id: 'delete_file', label: 'Delete File', description: 'Delete files/directories (disabled by default)' },
13
- { id: 'edit_file', label: 'Edit File', description: 'Search-and-replace editing within files' },
14
- { id: 'process', label: 'Process', description: 'Monitor and control long-running shell commands' },
15
- { id: 'web_search', label: 'Web Search', description: 'Search the web via DuckDuckGo' },
16
- { id: 'web_fetch', label: 'Web Fetch', description: 'Fetch and extract text from URLs' },
17
- { id: 'claude_code', label: 'Claude Code', description: 'Delegate complex tasks to Claude Code CLI' },
18
- { id: 'codex_cli', label: 'Codex CLI', description: 'Delegate complex tasks to OpenAI Codex CLI' },
19
- { id: 'opencode_cli', label: 'OpenCode CLI', description: 'Delegate complex tasks to OpenCode CLI' },
12
+ { id: 'shell', label: 'Shell', description: 'Execute commands in the working directory and manage background processes' },
13
+ { id: 'files', label: 'Files', description: 'Complete file management: read, write, list, move, copy, delete, and send' },
14
+ { id: 'edit_file', label: 'Edit File', description: 'Surgical search-and-replace within files' },
15
+ { id: 'web', label: 'Web', description: 'Search the web via DuckDuckGo and fetch text from URLs' },
16
+ { id: 'delegate', label: 'Delegate', description: 'Delegate complex tasks to specialized backends (Claude Code, Codex, OpenCode)' },
20
17
  { id: 'browser', label: 'Browser', description: 'Playwright — browse, scrape, interact with web pages' },
21
18
  { id: 'memory', label: 'Memory', description: 'Store and retrieve long-term memories across conversations' },
22
- { id: 'sandbox', label: 'Sandbox', description: 'Run JS/TS/Python code in an isolated Deno sandbox' },
19
+ { id: 'sandbox', label: 'Sandbox', description: 'Secure isolated code execution for JS, TS, and Python' },
23
20
  { id: 'create_document', label: 'Create Document', description: 'Render markdown to PDF, HTML, or image' },
24
21
  { id: 'create_spreadsheet', label: 'Create Spreadsheet', description: 'Create Excel or CSV files from structured data' },
25
- { id: 'http_request', label: 'HTTP Request', description: 'Make HTTP API calls (GET, POST, PUT, DELETE, etc.)' },
22
+ { id: 'http_request', label: 'HTTP Request', description: 'Make direct HTTP API calls with custom methods, headers, and bodies' },
26
23
  { id: 'git', label: 'Git', description: 'Run structured git operations (status, commit, push, diff, etc.)' },
27
24
  { id: 'wallet', label: 'Wallet', description: 'Manage agent crypto wallet — check balance, send SOL, view transactions' },
25
+ { id: 'monitor', label: 'Monitor', description: 'System observability: check resource usage, watch logs, and ping endpoints' },
26
+ { id: 'plugin_creator', label: 'Plugin Creator', description: 'Design, write, and test custom SwarmClaw plugins dynamically' },
27
+ { id: 'sample_ui', label: 'Sample UI', description: 'Demonstration of dynamic UI injection into Sidebar and Chat Header' },
28
28
  ]
29
29
 
30
+ /**
31
+ * Platform capability tools.
32
+ * Granular CRUD tools are now unified under 'manage_platform'.
33
+ */
30
34
  export const PLATFORM_TOOLS: ToolDefinition[] = [
31
- { id: 'manage_agents', label: 'Agents', description: 'Create, edit, and delete agents' },
32
- { id: 'manage_tasks', label: 'Tasks', description: 'Create, edit, and delete tasks' },
33
- { id: 'manage_schedules', label: 'Schedules', description: 'Create, edit, and delete schedules' },
35
+ { id: 'manage_platform', label: 'Platform', description: 'Unified management of agents, tasks, schedules, skills, documents, and secrets' },
36
+ { id: 'manage_connectors', label: 'Connectors', description: 'Manage chat platform bridges and send outbound messages' },
37
+ { id: 'manage_chatrooms', label: 'Chatrooms', description: 'Manage SwarmClaw routing rules and multi-agent chatrooms' },
38
+ { id: 'delegate_to_agent', label: 'Assign Agent', description: 'Delegate a task to another specific agent' },
34
39
  { id: 'schedule_wake', label: 'Reminders', description: 'Schedule a proactive wake event in the current chat' },
35
- { id: 'manage_skills', label: 'Skills', description: 'Create, edit, and delete skills' },
36
- { id: 'manage_documents', label: 'Documents', description: 'Upload, search, and delete indexed documents' },
37
- { id: 'manage_webhooks', label: 'Webhooks', description: 'Register webhooks that trigger agent workflows' },
38
- { id: 'manage_connectors', label: 'Connectors', description: 'Create, edit, and delete connectors' },
39
- { id: 'manage_sessions', label: 'Chats', description: 'List chats, send messages, and spawn agent work' },
40
- { id: 'manage_secrets', label: 'Secrets', description: 'Store and retrieve encrypted service secrets' },
41
40
  ]
42
41
 
43
42
  export const ALL_TOOLS: ToolDefinition[] = [...AVAILABLE_TOOLS, ...PLATFORM_TOOLS]
@@ -46,3 +45,4 @@ export const ALL_TOOLS: ToolDefinition[] = [...AVAILABLE_TOOLS, ...PLATFORM_TOOL
46
45
  export const TOOL_LABELS: Record<string, string> = Object.fromEntries(
47
46
  ALL_TOOLS.map((t) => [t.id, t.label]),
48
47
  )
48
+
@@ -15,6 +15,10 @@ export const AgentCreateSchema = z.object({
15
15
  thinkingLevel: z.string().optional(),
16
16
  soul: z.string().optional(),
17
17
  autoRecovery: z.boolean().optional().default(false),
18
+ monthlyBudget: z.number().positive().nullable().optional().default(null),
19
+ dailyBudget: z.number().positive().nullable().optional().default(null),
20
+ hourlyBudget: z.number().positive().nullable().optional().default(null),
21
+ budgetAction: z.enum(['warn', 'block']).optional().default('warn'),
18
22
  })
19
23
 
20
24
  export const ConnectorCreateSchema = z.object({
@@ -46,6 +50,14 @@ export const TaskCreateSchema = z.object({
46
50
  retryBackoffSec: z.number().optional(),
47
51
  priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
48
52
  dueAt: z.number().nullable().optional(),
53
+ qualityGate: z.object({
54
+ enabled: z.boolean().optional(),
55
+ minResultChars: z.number().optional(),
56
+ minEvidenceItems: z.number().optional(),
57
+ requireVerification: z.boolean().optional(),
58
+ requireArtifact: z.boolean().optional(),
59
+ requireReport: z.boolean().optional(),
60
+ }).nullable().optional(),
49
61
  })
50
62
 
51
63
  export const ChatroomCreateSchema = z.object({
@@ -1,31 +1,9 @@
1
1
  import type { AppView } from '@/types'
2
+ import { VIEW_ROUTE_PATHS } from '../../view-route-paths'
2
3
 
3
4
  export const DEFAULT_VIEW: AppView = 'home'
4
5
 
5
- export const VIEW_TO_PATH: Record<AppView, string> = {
6
- home: '/',
7
- agents: '/agents',
8
- chatrooms: '/chatrooms',
9
- schedules: '/schedules',
10
- memory: '/memory',
11
- tasks: '/tasks',
12
- approvals: '/approvals',
13
- secrets: '/secrets',
14
- providers: '/providers',
15
- skills: '/skills',
16
- connectors: '/connectors',
17
- webhooks: '/webhooks',
18
- mcp_servers: '/mcp-servers',
19
- knowledge: '/knowledge',
20
- plugins: '/plugins',
21
- usage: '/usage',
22
- wallets: '/wallets',
23
- runs: '/runs',
24
- logs: '/logs',
25
- settings: '/settings',
26
- projects: '/projects',
27
- activity: '/activity',
28
- }
6
+ export const VIEW_TO_PATH: Record<AppView, string> = VIEW_ROUTE_PATHS
29
7
 
30
8
  const entries = Object.entries(VIEW_TO_PATH) as [AppView, string][]
31
9
  export const PATH_TO_VIEW: Record<string, AppView> = Object.fromEntries(
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { create } from 'zustand'
4
- import type { Sessions, Session, NetworkInfo, Directory, ProviderInfo, Credentials, Agent, Schedule, AppView, BoardTask, AppSettings, OrchestratorSecret, ProviderConfig, Skill, Connector, Webhook, McpServerConfig, PluginMeta, Project, FleetFilter, ActivityEntry, AppNotification } from '../types'
4
+ import type { Sessions, Session, NetworkInfo, Directory, ProviderInfo, Credentials, Agent, Schedule, AppView, BoardTask, AppSettings, OrchestratorSecret, ProviderConfig, Skill, Connector, Webhook, McpServerConfig, PluginMeta, Project, FleetFilter, ActivityEntry, AppNotification, ApprovalRequest } from '../types'
5
5
  import { fetchSessions, fetchDirs, fetchProviders, fetchCredentials } from '../lib/sessions'
6
6
  import { fetchAgents } from '../lib/agents'
7
7
  import { fetchSchedules } from '../lib/schedules'
@@ -198,6 +198,11 @@ interface AppState {
198
198
  lastReadTimestamps: Record<string, number>
199
199
  markChatRead: (id: string) => void
200
200
 
201
+ // Approvals
202
+ approvals: Record<string, ApprovalRequest>
203
+ loadApprovals: () => Promise<void>
204
+ submitApprovalDecision: (id: string, approved: boolean) => Promise<void>
205
+
201
206
  // Notifications
202
207
  notifications: AppNotification[]
203
208
  unreadNotificationCount: number
@@ -643,6 +648,23 @@ export const useAppStore = create<AppState>((set, get) => ({
643
648
  safeStorageSet('sc_last_read', JSON.stringify(ts))
644
649
  },
645
650
 
651
+ // Approvals
652
+ approvals: {},
653
+ loadApprovals: async () => {
654
+ try {
655
+ const list = await api<ApprovalRequest[]>('GET', '/approvals')
656
+ const approvals: Record<string, ApprovalRequest> = {}
657
+ for (const a of list) approvals[a.id] = a
658
+ set({ approvals })
659
+ } catch { /* ignore */ }
660
+ },
661
+ submitApprovalDecision: async (id, approved) => {
662
+ try {
663
+ await api('POST', '/approvals', { id, approved })
664
+ await get().loadApprovals()
665
+ } catch { /* ignore */ }
666
+ },
667
+
646
668
  // Notifications
647
669
  notifications: [],
648
670
  unreadNotificationCount: 0,