@swarmclawai/swarmclaw 1.9.10 → 1.9.12

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 (36) hide show
  1. package/README.md +19 -1
  2. package/electron-dist/main.js +218 -0
  3. package/package.json +3 -2
  4. package/src/app/api/tasks/[id]/execution-policy/route.ts +46 -0
  5. package/src/app/api/tasks/task-workspace-route.test.ts +89 -0
  6. package/src/app/home/page.tsx +1 -1
  7. package/src/cli/index.js +2 -0
  8. package/src/cli/index.test.js +31 -1
  9. package/src/cli/index.ts +1 -1
  10. package/src/cli/spec.js +2 -0
  11. package/src/components/connectors/connector-sheet.tsx +36 -5
  12. package/src/components/shared/command-palette.tsx +1 -1
  13. package/src/components/shared/connector-platform-icon.test.ts +4 -0
  14. package/src/components/shared/connector-platform-icon.tsx +1 -0
  15. package/src/components/tasks/task-sheet.tsx +205 -2
  16. package/src/features/tasks/queries.ts +18 -0
  17. package/src/lib/connectors/connector-readiness.ts +17 -7
  18. package/src/lib/server/connectors/connector-lifecycle.ts +2 -1
  19. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  20. package/src/lib/server/connectors/connector-service.ts +1 -0
  21. package/src/lib/server/connectors/email.test.ts +1 -0
  22. package/src/lib/server/connectors/filequeue.test.ts +141 -0
  23. package/src/lib/server/connectors/filequeue.ts +324 -0
  24. package/src/lib/server/session-tools/crud.ts +1 -0
  25. package/src/lib/server/storage.ts +1 -1
  26. package/src/lib/server/tasks/task-execution-policy.test.ts +120 -0
  27. package/src/lib/server/tasks/task-execution-policy.ts +320 -0
  28. package/src/lib/server/tasks/task-execution-workspace.ts +2 -0
  29. package/src/lib/server/tasks/task-handoff.ts +56 -0
  30. package/src/lib/server/tasks/task-lifecycle.ts +2 -0
  31. package/src/lib/server/tasks/task-route-service.ts +86 -0
  32. package/src/lib/server/tasks/task-service.ts +30 -0
  33. package/src/lib/tasks.ts +28 -1
  34. package/src/lib/validation/schemas.ts +15 -1
  35. package/src/types/connector.ts +1 -0
  36. package/src/types/task.ts +62 -0
package/README.md CHANGED
@@ -183,7 +183,7 @@ Full hosted deployment guides live at https://swarmclaw.ai/docs/deployment
183
183
  - **Structured Sessions**: reusable bounded runs with templates, facilitators, participants, hidden live rooms, chatroom `/breakout`, durable transcripts, outputs, operator controls, and a visible protocols template gallery plus visual builder.
184
184
  - **Memory**: hybrid recall, graph traversal, journaling, durable documents, project-scoped context, automatic reflection memory, communication preferences, profile and boundary memory, significant events, and open follow-up loops.
185
185
  - **Wallets**: linked Base wallet generation, address management, approval-oriented limits, and agent payout identity.
186
- - **Connectors**: Discord, Slack, Telegram, WhatsApp, Teams, Matrix, OpenClaw, SwarmDock, SwarmFeed, and more.
186
+ - **Connectors**: Discord, Slack, Telegram, WhatsApp, Teams, Matrix, email, local file queues, OpenClaw, SwarmDock, SwarmFeed, and more.
187
187
  - **MCP Servers**: connect any Model Context Protocol server (stdio, SSE, or streamable HTTP) and inject its tools into agents alongside built-ins. Configure, test, and assign per-agent from the MCP Servers panel.
188
188
  - **Extensions**: external tool extensions, UI modules, hooks, install/update flows, and managed resource manifests for extension-owned agents, routines, local folders, gateways, and setup checks.
189
189
 
@@ -399,6 +399,24 @@ Operational docs: https://swarmclaw.ai/docs/observability
399
399
 
400
400
  ## Releases
401
401
 
402
+ ### v1.9.12 Highlights
403
+
404
+ Local file-queue connector release: operators can bridge SwarmClaw to filesystem inbox, outbox, archive, and error folders without a hosted message bus.
405
+
406
+ - **File Queue connector.** Configure root, inbox, outbox, archive, and error folders from the connector sheet or CLI.
407
+ - **JSON command ingress.** External tools can drop command envelopes into the inbox, then SwarmClaw normalizes them into connector messages for the selected agent or chatroom.
408
+ - **Durable file handling.** Processed commands move to archive, malformed commands move to errors with diagnostic sidecars, and replies are written to outbox as structured JSON.
409
+ - **Connector runtime parity.** Queue traffic uses the existing connector session, policy, health, readiness, CLI, and follow-up delivery paths.
410
+
411
+ ### v1.9.11 Highlights
412
+
413
+ Task execution policy release: operators can attach ordered review, approval, and verification stages to board tasks, record decisions, and block premature completion until required stages clear.
414
+
415
+ - **Task execution policies.** Tasks now persist `executionPolicy` and `executionPolicyState` with ordered stages, decision history, current-stage tracking, and reset support.
416
+ - **Completion guardrails.** `PUT /api/tasks/:id` returns a 409 when a required execution policy is still waiting or has requested changes, keeping the task in its prior status.
417
+ - **Policy API and CLI.** `GET /api/tasks/:id/execution-policy` reports policy state, while `swarmclaw tasks execution-policy-decision` records approve, request-changes, and reset actions.
418
+ - **Operator UI and handoffs.** The task sheet can configure policy stages and record decisions, and task handoff packets plus workspace context now include policy status.
419
+
402
420
  ### v1.9.10 Highlights
403
421
 
404
422
  Task handoff release: operators can package task state, readiness, workspace context, dependencies, outputs, and resume handles into a shareable packet before continuing work.
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const electron_1 = require("electron");
40
+ const node_fs_1 = __importDefault(require("node:fs"));
41
+ const node_path_1 = __importDefault(require("node:path"));
42
+ const paths_1 = require("./paths");
43
+ const server_lifecycle_1 = require("./server-lifecycle");
44
+ const menu_1 = require("./menu");
45
+ const DEV_URL_DEFAULT = 'http://127.0.0.1:3456';
46
+ const LOG_TAIL_BYTES = 1500;
47
+ let mainWindow = null;
48
+ let serverHandle = null;
49
+ let serverLogFile = null;
50
+ let isQuitting = false;
51
+ const gotLock = electron_1.app.requestSingleInstanceLock();
52
+ if (!gotLock) {
53
+ electron_1.app.quit();
54
+ }
55
+ else {
56
+ electron_1.app.on('second-instance', () => {
57
+ if (mainWindow) {
58
+ if (mainWindow.isMinimized())
59
+ mainWindow.restore();
60
+ mainWindow.focus();
61
+ }
62
+ });
63
+ electron_1.app.on('ready', () => void onReady());
64
+ electron_1.app.on('window-all-closed', () => {
65
+ if (process.platform !== 'darwin')
66
+ electron_1.app.quit();
67
+ });
68
+ electron_1.app.on('activate', () => {
69
+ if (mainWindow !== null)
70
+ return;
71
+ if (serverHandle) {
72
+ createMainWindow(serverHandle.url);
73
+ }
74
+ else if (!electron_1.app.isPackaged) {
75
+ createMainWindow(process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT);
76
+ }
77
+ });
78
+ electron_1.app.on('before-quit', () => {
79
+ isQuitting = true;
80
+ });
81
+ electron_1.app.on('will-quit', async (event) => {
82
+ if (!serverHandle)
83
+ return;
84
+ event.preventDefault();
85
+ try {
86
+ await serverHandle.stop();
87
+ }
88
+ finally {
89
+ serverHandle = null;
90
+ electron_1.app.exit(0);
91
+ }
92
+ });
93
+ }
94
+ async function onReady() {
95
+ const paths = (0, paths_1.resolveRuntimePaths)();
96
+ (0, menu_1.buildAppMenu)(paths, () => mainWindow);
97
+ const iconPath = resolveIconPath();
98
+ if (process.platform === 'darwin' && iconPath && electron_1.app.dock) {
99
+ const img = electron_1.nativeImage.createFromPath(iconPath);
100
+ if (!img.isEmpty())
101
+ electron_1.app.dock.setIcon(img);
102
+ }
103
+ if (!electron_1.app.isPackaged) {
104
+ const devUrl = process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT;
105
+ console.log(`[swarmclaw] dev mode, loading ${devUrl}`);
106
+ createMainWindow(devUrl);
107
+ return;
108
+ }
109
+ serverLogFile = node_path_1.default.join(electron_1.app.getPath('userData'), 'logs', 'server.log');
110
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(serverLogFile), { recursive: true });
111
+ try {
112
+ serverHandle = await (0, server_lifecycle_1.startEmbeddedServer)({
113
+ paths,
114
+ logFile: serverLogFile,
115
+ onStdout: (c) => process.stdout.write(`[swarmclaw] ${c}`),
116
+ onStderr: (c) => process.stderr.write(`[swarmclaw] ${c}`),
117
+ onExit: (code, signal) => {
118
+ if (!isQuitting) {
119
+ console.error(`[swarmclaw] server exited unexpectedly (code=${code}, signal=${signal ?? 'none'})`);
120
+ void showServerCrashDialog(code, signal);
121
+ }
122
+ },
123
+ });
124
+ }
125
+ catch (err) {
126
+ await showStartupFailureDialog(err, paths);
127
+ electron_1.app.exit(1);
128
+ return;
129
+ }
130
+ createMainWindow(serverHandle.url);
131
+ void Promise.resolve().then(() => __importStar(require('./updater'))).then((m) => m.initAutoUpdater());
132
+ }
133
+ function resolveIconPath() {
134
+ const candidate = electron_1.app.isPackaged
135
+ ? node_path_1.default.join(process.resourcesPath, 'icon.png')
136
+ : node_path_1.default.join(__dirname, '..', 'resources', 'icon.png');
137
+ return node_fs_1.default.existsSync(candidate) ? candidate : undefined;
138
+ }
139
+ function createMainWindow(startUrl) {
140
+ const iconPath = resolveIconPath();
141
+ mainWindow = new electron_1.BrowserWindow({
142
+ width: 1440,
143
+ height: 900,
144
+ minWidth: 1024,
145
+ minHeight: 640,
146
+ backgroundColor: '#0b0b0f',
147
+ show: true,
148
+ ...(iconPath ? { icon: iconPath } : {}),
149
+ webPreferences: {
150
+ contextIsolation: true,
151
+ nodeIntegration: false,
152
+ sandbox: false,
153
+ },
154
+ });
155
+ const wc = mainWindow.webContents;
156
+ if (!electron_1.app.isPackaged)
157
+ wc.openDevTools({ mode: 'detach' });
158
+ wc.on('did-start-loading', () => console.log('[swarmclaw] did-start-loading'));
159
+ wc.on('did-finish-load', () => console.log('[swarmclaw] did-finish-load'));
160
+ wc.on('did-fail-load', (_e, code, desc, url) => console.error(`[swarmclaw] did-fail-load code=${code} desc=${desc} url=${url}`));
161
+ wc.on('render-process-gone', (_e, details) => console.error(`[swarmclaw] render-process-gone reason=${details.reason}`));
162
+ wc.on('unresponsive', () => console.error('[swarmclaw] webContents unresponsive'));
163
+ mainWindow.on('closed', () => {
164
+ mainWindow = null;
165
+ });
166
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
167
+ if (url.startsWith(startUrl))
168
+ return { action: 'allow' };
169
+ void electron_1.shell.openExternal(url);
170
+ return { action: 'deny' };
171
+ });
172
+ void mainWindow.loadURL(startUrl).catch((err) => {
173
+ console.error('[swarmclaw] loadURL rejected:', err);
174
+ });
175
+ }
176
+ async function showServerCrashDialog(code, signal) {
177
+ const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
178
+ const quitButtonId = buttons.length - 1;
179
+ const detail = buildLogDetail(`code=${code ?? 'null'} signal=${signal ?? 'none'}`);
180
+ const res = await electron_1.dialog.showMessageBox({
181
+ type: 'error',
182
+ buttons,
183
+ defaultId: quitButtonId,
184
+ cancelId: quitButtonId,
185
+ title: 'SwarmClaw stopped',
186
+ message: 'The SwarmClaw server exited unexpectedly.',
187
+ detail,
188
+ });
189
+ if (serverLogFile && res.response === 0)
190
+ electron_1.shell.showItemInFolder(serverLogFile);
191
+ electron_1.app.exit(1);
192
+ }
193
+ async function showStartupFailureDialog(err, paths) {
194
+ const message = err instanceof Error ? err.message : String(err);
195
+ const base = `${message}\n\nStandalone entry: ${paths.standaloneEntry}\nData dir: ${paths.dataDir}`;
196
+ const detail = buildLogDetail(base);
197
+ const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
198
+ const quitButtonId = buttons.length - 1;
199
+ const res = await electron_1.dialog.showMessageBox({
200
+ type: 'error',
201
+ buttons,
202
+ defaultId: quitButtonId,
203
+ cancelId: quitButtonId,
204
+ title: 'SwarmClaw failed to start',
205
+ message: 'The embedded server did not start.',
206
+ detail,
207
+ });
208
+ if (serverLogFile && res.response === 0)
209
+ electron_1.shell.showItemInFolder(serverLogFile);
210
+ }
211
+ function buildLogDetail(base) {
212
+ if (!serverLogFile)
213
+ return base;
214
+ const tail = (0, server_lifecycle_1.tailLogFile)(serverLogFile, LOG_TAIL_BYTES).trim();
215
+ if (!tail)
216
+ return `${base}\n\nLog file: ${serverLogFile}\n(no output captured yet)`;
217
+ return `${base}\n\nLog tail (${serverLogFile}):\n${tail}`;
218
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.9.10",
3
+ "version": "1.9.12",
4
4
  "description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
5
5
  "main": "electron-dist/main.js",
6
6
  "license": "MIT",
@@ -29,6 +29,7 @@
29
29
  "clawd",
30
30
  "clawdbot",
31
31
  "moltbot",
32
+ "file-queue",
32
33
  "openclaw-skill",
33
34
  "openclaw-dashboard",
34
35
  "openclaw-gateway",
@@ -87,7 +88,7 @@
87
88
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
88
89
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
89
90
  "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
90
- "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
91
+ "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
91
92
  "test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
92
93
  "test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
93
94
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
@@ -0,0 +1,46 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+ import { loadTask } from '@/lib/server/tasks/task-repository'
5
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
6
+ import {
7
+ describeTaskExecutionPolicy,
8
+ normalizeTaskExecutionPolicy,
9
+ syncTaskExecutionPolicyState,
10
+ } from '@/lib/server/tasks/task-execution-policy'
11
+ import { decideTaskExecutionPolicyFromRoute } from '@/lib/server/tasks/task-route-service'
12
+ import { formatZodError } from '@/lib/validation/schemas'
13
+
14
+ const TaskExecutionPolicyDecisionSchema = z.object({
15
+ action: z.enum(['approve', 'request_changes', 'reset']).optional().default('approve'),
16
+ stageId: z.string().optional(),
17
+ actor: z.string().optional(),
18
+ note: z.string().optional(),
19
+ })
20
+
21
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
22
+ const { id } = await params
23
+ const task = loadTask(id)
24
+ if (!task) return notFound()
25
+ const policy = normalizeTaskExecutionPolicy(task.executionPolicy)
26
+ const state = syncTaskExecutionPolicyState(policy, task.executionPolicyState)
27
+ return NextResponse.json({
28
+ taskId: id,
29
+ policy,
30
+ state,
31
+ summary: describeTaskExecutionPolicy({ executionPolicy: policy, executionPolicyState: state }),
32
+ })
33
+ }
34
+
35
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
36
+ const { id } = await params
37
+ const { data: raw, error } = await safeParseBody<Record<string, unknown>>(req)
38
+ if (error) return error
39
+ const parsed = TaskExecutionPolicyDecisionSchema.safeParse(raw || {})
40
+ if (!parsed.success) return NextResponse.json(formatZodError(parsed.error), { status: 400 })
41
+ const result = decideTaskExecutionPolicyFromRoute(id, parsed.data)
42
+ if (!result.ok && result.status === 404) return notFound()
43
+ return result.ok
44
+ ? NextResponse.json(result.payload)
45
+ : NextResponse.json(result.payload, { status: result.status })
46
+ }
@@ -17,6 +17,8 @@ let tempDir = ''
17
17
  let putTask: typeof import('./[id]/route')['PUT']
18
18
  let getTaskHandoff: typeof import('./[id]/handoff/route')['GET']
19
19
  let postTaskHandoff: typeof import('./[id]/handoff/route')['POST']
20
+ let getTaskExecutionPolicy: typeof import('./[id]/execution-policy/route')['GET']
21
+ let postTaskExecutionPolicy: typeof import('./[id]/execution-policy/route')['POST']
20
22
  let getTaskHandoffs: typeof import('./handoffs/route')['GET']
21
23
  let getTasks: typeof import('./route')['GET']
22
24
  let storage: typeof import('@/lib/server/storage')
@@ -52,6 +54,9 @@ before(async () => {
52
54
  const handoffRoute = await import('./[id]/handoff/route')
53
55
  getTaskHandoff = handoffRoute.GET
54
56
  postTaskHandoff = handoffRoute.POST
57
+ const policyRoute = await import('./[id]/execution-policy/route')
58
+ getTaskExecutionPolicy = policyRoute.GET
59
+ postTaskExecutionPolicy = policyRoute.POST
55
60
  getTaskHandoffs = (await import('./handoffs/route')).GET
56
61
  getTasks = (await import('./route')).GET
57
62
  })
@@ -166,6 +171,90 @@ test('GET /api/tasks/:id/handoff returns readiness and markdown packets', async
166
171
  assert.match(markdown, /Readiness: blocked/)
167
172
  })
168
173
 
174
+ test('PUT /api/tasks/:id blocks completion until execution policy stages are approved', async () => {
175
+ seedTask('task-policy-complete', {
176
+ title: 'Policy Completion Task',
177
+ result: 'Implemented the requested feature, updated src/app/example.ts, and verified with npm run test.',
178
+ executionPolicy: {
179
+ enabled: true,
180
+ mode: 'before_completion',
181
+ stages: [{ id: 'review', title: 'Review', kind: 'review', requiredDecisions: 1 }],
182
+ },
183
+ })
184
+
185
+ const blocked = await putTask(new Request('http://local/api/tasks/task-policy-complete', {
186
+ method: 'PUT',
187
+ headers: { 'content-type': 'application/json' },
188
+ body: JSON.stringify({ status: 'completed' }),
189
+ }), routeParams('task-policy-complete'))
190
+
191
+ assert.equal(blocked.status, 409)
192
+ assert.equal(storage.loadTasks()['task-policy-complete']?.status, 'backlog')
193
+
194
+ const policyResponse = await postTaskExecutionPolicy(new Request('http://local/api/tasks/task-policy-complete/execution-policy', {
195
+ method: 'POST',
196
+ headers: { 'content-type': 'application/json' },
197
+ body: JSON.stringify({ action: 'approve', actor: 'QA' }),
198
+ }), routeParams('task-policy-complete'))
199
+ assert.equal(policyResponse.status, 200)
200
+ const policyBody = await policyResponse.json()
201
+ assert.equal(policyBody.state.status, 'completed')
202
+
203
+ const completed = await putTask(new Request('http://local/api/tasks/task-policy-complete', {
204
+ method: 'PUT',
205
+ headers: { 'content-type': 'application/json' },
206
+ body: JSON.stringify({ status: 'completed' }),
207
+ }), routeParams('task-policy-complete'))
208
+
209
+ assert.equal(completed.status, 200)
210
+ const body = await completed.json() as BoardTask
211
+ assert.equal(body.status, 'completed')
212
+ })
213
+
214
+ test('PUT /api/tasks/:id allows edits to already completed tasks without re-requesting completion', async () => {
215
+ seedTask('task-policy-completed-edit', {
216
+ title: 'Completed Policy Edit Task',
217
+ status: 'completed',
218
+ result: 'Completed with tests passed and build passed.',
219
+ executionPolicy: {
220
+ enabled: true,
221
+ mode: 'before_completion',
222
+ stages: [{ id: 'review', title: 'Review', kind: 'review', requiredDecisions: 1 }],
223
+ },
224
+ })
225
+
226
+ const response = await putTask(new Request('http://local/api/tasks/task-policy-completed-edit', {
227
+ method: 'PUT',
228
+ headers: { 'content-type': 'application/json' },
229
+ body: JSON.stringify({ title: 'Completed Policy Edit Task Updated' }),
230
+ }), routeParams('task-policy-completed-edit'))
231
+
232
+ assert.equal(response.status, 200)
233
+ const body = await response.json() as BoardTask
234
+ assert.equal(body.status, 'completed')
235
+ assert.equal(body.title, 'Completed Policy Edit Task Updated')
236
+ })
237
+
238
+ test('GET /api/tasks/:id/execution-policy returns policy summary', async () => {
239
+ seedTask('task-policy-summary', {
240
+ title: 'Policy Summary Task',
241
+ executionPolicy: {
242
+ enabled: true,
243
+ mode: 'before_completion',
244
+ stages: [{ id: 'review', title: 'Review', kind: 'review' }],
245
+ },
246
+ })
247
+
248
+ const response = await getTaskExecutionPolicy(
249
+ new Request('http://local/api/tasks/task-policy-summary/execution-policy'),
250
+ routeParams('task-policy-summary'),
251
+ )
252
+ assert.equal(response.status, 200)
253
+ const body = await response.json()
254
+ assert.equal(body.summary.enabled, true)
255
+ assert.equal(body.summary.status, 'waiting')
256
+ })
257
+
169
258
  test('POST /api/tasks/:id/handoff saves markdown and JSON snapshots into the workspace', async () => {
170
259
  seedTask('task-handoff-save', {
171
260
  title: 'Saved Handoff Task',
@@ -429,7 +429,7 @@ export default function HomePage() {
429
429
  <StatCard label="Agents" value={String(agentCount)} hint="Total active agents configured in your dashboard" index={0} />
430
430
  <StatCard label="Active Tasks" value={String(activeTaskCount)} accent={activeTaskCount > 0} hint="Tasks currently running or queued for execution" index={1} />
431
431
  <StatCard label="Today's Cost" value={`$${todayCost.toFixed(2)}`} hint="Estimated API cost for today across all providers" index={2} />
432
- <StatCard label="Connectors" value={`${activeConnectorCount}/${allConnectors.length}`} accent={activeConnectorCount > 0} hint="Active bridges to chat platforms (Discord, Slack, etc.)" index={3} />
432
+ <StatCard label="Connectors" value={`${activeConnectorCount}/${allConnectors.length}`} accent={activeConnectorCount > 0} hint="Active bridges to chat platforms, local queues, and agent channels." index={3} />
433
433
  </div>
434
434
 
435
435
  {/* Cost trend sparkline */}
package/src/cli/index.js CHANGED
@@ -738,6 +738,8 @@ const COMMAND_GROUPS = [
738
738
  cmd('handoff', 'GET', '/tasks/:id/handoff', 'Get task handoff packet'),
739
739
  cmd('handoff-save', 'POST', '/tasks/:id/handoff', 'Save task handoff packet into the task workspace', { expectsJsonBody: true }),
740
740
  cmd('handoffs', 'GET', '/tasks/handoffs', 'List task handoff readiness packets'),
741
+ cmd('execution-policy', 'GET', '/tasks/:id/execution-policy', 'Get task execution policy state'),
742
+ cmd('execution-policy-decision', 'POST', '/tasks/:id/execution-policy', 'Approve, request changes, or reset a task policy stage', { expectsJsonBody: true }),
741
743
  cmd('create', 'POST', '/tasks', 'Create task', { expectsJsonBody: true }),
742
744
  cmd('bulk', 'POST', '/tasks/bulk', 'Bulk update tasks (status/agent/project)', { expectsJsonBody: true }),
743
745
  cmd('update', 'PUT', '/tasks/:id', 'Update task', { expectsJsonBody: true }),
@@ -12,7 +12,7 @@ const {
12
12
  getApiCoveragePairs,
13
13
  parseArgv,
14
14
  runCli,
15
- } = require('./index')
15
+ } = require('./index.js')
16
16
 
17
17
  function collectApiRoutePairs() {
18
18
  const root = path.join(process.cwd(), 'src', 'app', 'api')
@@ -195,6 +195,36 @@ test('tasks handoff command can request markdown packets', async () => {
195
195
  assert.equal(stderr.toString(), '')
196
196
  })
197
197
 
198
+ test('tasks execution-policy-decision posts policy decisions', async () => {
199
+ const stdout = makeWritable()
200
+ const stderr = makeWritable()
201
+ const calls = []
202
+
203
+ const fetchImpl = async (url, init) => {
204
+ calls.push({ url: String(url), init })
205
+ return jsonResponse({ state: { status: 'completed' } })
206
+ }
207
+
208
+ const exitCode = await runCli(
209
+ ['tasks', 'execution-policy-decision', 'task-1', '--data', '{"action":"approve","actor":"QA"}', '--json'],
210
+ {
211
+ fetchImpl,
212
+ stdout,
213
+ stderr,
214
+ env: {},
215
+ cwd: process.cwd(),
216
+ }
217
+ )
218
+
219
+ assert.equal(exitCode, 0)
220
+ assert.equal(calls.length, 1)
221
+ assert.match(calls[0].url, /\/api\/tasks\/task-1\/execution-policy$/)
222
+ assert.equal(calls[0].init.method, 'POST')
223
+ assert.deepEqual(JSON.parse(calls[0].init.body), { action: 'approve', actor: 'QA' })
224
+ assert.match(stdout.toString(), /"completed"/)
225
+ assert.equal(stderr.toString(), '')
226
+ })
227
+
198
228
  test('openclaw deploy bundle command merges action with provided JSON body', async () => {
199
229
  const stdout = makeWritable()
200
230
  const stderr = makeWritable()
package/src/cli/index.ts CHANGED
@@ -1177,7 +1177,7 @@ export function buildProgram(): Command {
1177
1177
  connectors
1178
1178
  .command('create')
1179
1179
  .description('Create connector')
1180
- .requiredOption('--platform <platform>', 'Connector platform (discord|telegram|slack|whatsapp|openclaw|bluebubbles|signal|teams|googlechat|matrix)')
1180
+ .requiredOption('--platform <platform>', 'Connector platform (discord|telegram|slack|whatsapp|openclaw|bluebubbles|signal|teams|googlechat|matrix|email|filequeue|swarmdock)')
1181
1181
  .requiredOption('--agent-id <agentId>', 'Agent id')
1182
1182
  .option('--name <name>', 'Connector name')
1183
1183
  .option('--credential-id <credentialId>', 'Credential id')
package/src/cli/spec.js CHANGED
@@ -532,6 +532,8 @@ const COMMAND_GROUPS = {
532
532
  handoff: { description: 'Get task handoff packet', method: 'GET', path: '/tasks/:id/handoff', params: ['id'] },
533
533
  'handoff-save': { description: 'Save task handoff packet into the task workspace', method: 'POST', path: '/tasks/:id/handoff', params: ['id'] },
534
534
  handoffs: { description: 'List task handoff readiness packets', method: 'GET', path: '/tasks/handoffs' },
535
+ 'execution-policy': { description: 'Get task execution policy state', method: 'GET', path: '/tasks/:id/execution-policy', params: ['id'] },
536
+ 'execution-policy-decision': { description: 'Approve, request changes, or reset a task policy stage', method: 'POST', path: '/tasks/:id/execution-policy', params: ['id'] },
535
537
  create: { description: 'Create task', method: 'POST', path: '/tasks' },
536
538
  bulk: { description: 'Bulk update tasks (status/agent/project)', method: 'POST', path: '/tasks/bulk' },
537
539
  update: { description: 'Update task', method: 'PUT', path: '/tasks/:id', params: ['id'] },
@@ -86,6 +86,12 @@ interface ConnectorConfigField {
86
86
  }
87
87
 
88
88
  const FIELD_HINTS: Record<string, string> = {
89
+ rootDir: 'Root folder for the queue. Defaults to a managed folder under SwarmClaw data.',
90
+ inboxDir: 'Folder where external tools drop inbound JSON commands. Relative paths are resolved under Root Directory.',
91
+ outboxDir: 'Folder where SwarmClaw writes outbound JSON replies. Relative paths are resolved under Root Directory.',
92
+ archiveDir: 'Processed inbound JSON commands are moved here after routing.',
93
+ errorDir: 'Malformed or failed inbound JSON commands are moved here with an error sidecar.',
94
+ pollIntervalMs: 'How often SwarmClaw checks the inbox folder. Minimum 250 ms.',
89
95
  channelIds: "Find these in your platform's developer settings. Leave empty to allow all channels",
90
96
  chatIds: "Find these in your platform's developer settings. Leave empty to allow all chats",
91
97
  roomIds: 'Leave empty to allow all rooms visible to the bot',
@@ -366,6 +372,30 @@ const PLATFORMS: {
366
372
  { key: 'maxBudget', label: 'Max Budget (USDC micro-units)', placeholder: '5000000', help: '$1 = 1000000, $5 = 5000000' },
367
373
  ],
368
374
  },
375
+ {
376
+ id: 'filequeue',
377
+ label: 'File Queue',
378
+ color: '#22C55E',
379
+ setupSteps: [
380
+ 'Choose a root directory or let SwarmClaw create a managed one',
381
+ 'External tools write JSON commands into the inbox folder',
382
+ 'SwarmClaw archives processed commands and writes JSON replies into the outbox folder',
383
+ 'Use an agent or chatroom route to decide who handles inbound commands',
384
+ ],
385
+ tokenLabel: '',
386
+ tokenHelp: '',
387
+ configFields: [
388
+ { key: 'rootDir', label: 'Root Directory', placeholder: '~/swarmclaw-command-queue', help: 'Optional. Defaults to data/connectors/<id>/filequeue.', section: 'basic' },
389
+ { key: 'inboxDir', label: 'Inbox Directory', placeholder: 'inbox', help: 'Inbound JSON command directory. Relative paths resolve under Root Directory.', section: 'basic' },
390
+ { key: 'outboxDir', label: 'Outbox Directory', placeholder: 'outbox', help: 'Outbound JSON reply directory. Relative paths resolve under Root Directory.', section: 'basic' },
391
+ { key: 'archiveDir', label: 'Archive Directory', placeholder: 'archive', help: 'Processed command archive directory.', section: 'advanced' },
392
+ { key: 'errorDir', label: 'Error Directory', placeholder: 'errors', help: 'Malformed command quarantine directory.', section: 'advanced' },
393
+ { key: 'pollIntervalMs', label: 'Poll Interval (ms)', placeholder: '1000', help: 'How often to scan the inbox. Minimum 250 ms.', section: 'advanced' },
394
+ { key: 'defaultChannelId', label: 'Default Channel ID', placeholder: 'ops', help: 'Used when an inbound command omits channelId.', section: 'advanced' },
395
+ { key: 'defaultSenderId', label: 'Default Sender ID', placeholder: 'queue', help: 'Used when an inbound command omits senderId.', section: 'advanced' },
396
+ { key: 'defaultSenderName', label: 'Default Sender Name', placeholder: 'Queue', help: 'Used when an inbound command omits senderName.', section: 'advanced' },
397
+ ],
398
+ },
369
399
  ]
370
400
 
371
401
  const COMMON_CONFIG_FIELDS: ConnectorConfigField[] = [
@@ -780,10 +810,10 @@ export function ConnectorSheet() {
780
810
  await saveConnectorMutation.mutateAsync({
781
811
  id: editing?.id,
782
812
  payload: {
783
- name: name || `${platformConfig?.label} Bot`,
813
+ name: name || (platform === 'filequeue' ? `${platformConfig?.label} Connector` : `${platformConfig?.label} Bot`),
784
814
  platform,
785
815
  ...routePayload,
786
- credentialId: credentialId || null,
816
+ credentialId: showCredentialSection ? (credentialId || null) : null,
787
817
  config,
788
818
  },
789
819
  })
@@ -848,6 +878,7 @@ export function ConnectorSheet() {
848
878
  const credList = Object.values(credentials)
849
879
  const basicPlatformFields = platformConfig.configFields.filter((field) => field.section !== 'advanced')
850
880
  const advancedPlatformFields = platformConfig.configFields.filter((field) => field.section === 'advanced')
881
+ const showCredentialSection = Boolean(platformConfig.tokenLabel.trim())
851
882
  const basicAccessFields = ACCESS_CONTROL_FIELDS.filter((field) => field.section !== 'advanced')
852
883
  const advancedAccessFields = ACCESS_CONTROL_FIELDS.filter((field) => field.section === 'advanced')
853
884
  const hasConfiguredValue = useCallback((key: string) => Boolean(config[key]?.trim()), [config])
@@ -1014,7 +1045,7 @@ export function ConnectorSheet() {
1014
1045
  <div>
1015
1046
  <div className={`text-[14px] font-600 ${platform === p.id ? 'text-text' : 'text-text-2'}`}>{p.label}</div>
1016
1047
  <div className="text-[11px] text-text-3 mt-0.5">
1017
- {p.id === 'whatsapp' ? 'QR code pairing' : p.id === 'openclaw' ? 'WebSocket gateway' : p.id === 'bluebubbles' ? 'iMessage bridge' : p.id === 'signal' ? 'signal-cli binary' : p.id === 'matrix' ? 'Access token' : p.id === 'googlechat' ? 'Service account' : p.id === 'teams' ? 'Bot Framework' : 'Bot token'}
1048
+ {p.id === 'whatsapp' ? 'QR code pairing' : p.id === 'openclaw' ? 'WebSocket gateway' : p.id === 'bluebubbles' ? 'iMessage bridge' : p.id === 'signal' ? 'signal-cli binary' : p.id === 'matrix' ? 'Access token' : p.id === 'googlechat' ? 'Service account' : p.id === 'teams' ? 'Bot Framework' : p.id === 'filequeue' ? 'Local queue' : 'Bot token'}
1018
1049
  </div>
1019
1050
  </div>
1020
1051
  </button>
@@ -1074,7 +1105,7 @@ export function ConnectorSheet() {
1074
1105
  <input
1075
1106
  value={name}
1076
1107
  onChange={(e) => setName(e.target.value)}
1077
- placeholder={`My ${platformConfig.label} Bot`}
1108
+ placeholder={`My ${platformConfig.label} ${platform === 'filequeue' ? 'Connector' : 'Bot'}`}
1078
1109
  className={inputClass}
1079
1110
  style={{ fontFamily: 'inherit' }}
1080
1111
  />
@@ -1128,7 +1159,7 @@ export function ConnectorSheet() {
1128
1159
  </div>
1129
1160
 
1130
1161
  {/* Bot token credential */}
1131
- {platform !== 'whatsapp' && (
1162
+ {showCredentialSection && (
1132
1163
  <div className="mb-6">
1133
1164
  <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">{platformConfig.tokenLabel}</label>
1134
1165
  <p className="text-[12px] text-text-3/60 mb-2">{platformConfig.tokenHelp}</p>
@@ -92,7 +92,7 @@ function CommandPaletteInner({ setOpen }: { setOpen: (v: boolean) => void }) {
92
92
  { id: 'projects', label: 'Projects', description: 'Scoped workspaces for agents and tasks', keywords: ['workspace', 'scope'] },
93
93
  { id: 'chatrooms', label: 'Chatrooms', description: 'Shared multi-agent conversations', keywords: ['group', 'room', 'mentions'] },
94
94
  { id: 'schedules', label: 'Schedules', description: 'Recurring and timed automations', keywords: ['cron', 'automation', 'interval'] },
95
- { id: 'connectors', label: 'Connectors', description: 'Bridges to Slack, Discord, Telegram, and more', keywords: ['discord', 'slack', 'telegram', 'whatsapp'] },
95
+ { id: 'connectors', label: 'Connectors', description: 'Bridges to Slack, Discord, Telegram, file queues, and more', keywords: ['discord', 'slack', 'telegram', 'whatsapp', 'file queue'] },
96
96
  { id: 'memory', label: 'Memory', description: 'Stored agent memory and retrieval', keywords: ['knowledge', 'vector', 'retrieval'] },
97
97
  { id: 'knowledge', label: 'Knowledge', description: 'Shared knowledge base', keywords: ['docs', 'entries', 'facts'] },
98
98
  { id: 'providers', label: 'Providers', description: 'Model providers and endpoints', keywords: ['openai', 'anthropic', 'ollama', 'endpoint'] },