@swarmclawai/swarmclaw 1.9.9 → 1.9.11
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 +18 -0
- package/electron-dist/main.js +218 -0
- package/package.json +2 -2
- package/src/app/api/tasks/[id]/execution-policy/route.ts +46 -0
- package/src/app/api/tasks/[id]/handoff/route.ts +73 -0
- package/src/app/api/tasks/handoffs/route.ts +50 -0
- package/src/app/api/tasks/task-workspace-route.test.ts +187 -0
- package/src/cli/index.js +5 -0
- package/src/cli/index.test.js +63 -1
- package/src/cli/spec.js +5 -0
- package/src/components/tasks/task-sheet.tsx +297 -2
- package/src/features/tasks/queries.ts +18 -0
- package/src/lib/server/tasks/task-execution-policy.test.ts +120 -0
- package/src/lib/server/tasks/task-execution-policy.ts +320 -0
- package/src/lib/server/tasks/task-execution-workspace.ts +2 -0
- package/src/lib/server/tasks/task-handoff.test.ts +86 -0
- package/src/lib/server/tasks/task-handoff.ts +421 -0
- package/src/lib/server/tasks/task-lifecycle.ts +2 -0
- package/src/lib/server/tasks/task-route-service.ts +86 -0
- package/src/lib/server/tasks/task-service.ts +30 -0
- package/src/lib/tasks.ts +47 -1
- package/src/lib/validation/schemas.ts +14 -0
- package/src/types/task.ts +153 -0
package/README.md
CHANGED
|
@@ -399,6 +399,24 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.9.11 Highlights
|
|
403
|
+
|
|
404
|
+
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.
|
|
405
|
+
|
|
406
|
+
- **Task execution policies.** Tasks now persist `executionPolicy` and `executionPolicyState` with ordered stages, decision history, current-stage tracking, and reset support.
|
|
407
|
+
- **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.
|
|
408
|
+
- **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.
|
|
409
|
+
- **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.
|
|
410
|
+
|
|
411
|
+
### v1.9.10 Highlights
|
|
412
|
+
|
|
413
|
+
Task handoff release: operators can package task state, readiness, workspace context, dependencies, outputs, and resume handles into a shareable packet before continuing work.
|
|
414
|
+
|
|
415
|
+
- **Task handoff packets.** `GET /api/tasks/:id/handoff` returns a structured packet with owner, liveness, workspace, runtime links, dependencies, quality checks, outputs, run summary, and recommended actions.
|
|
416
|
+
- **Workspace snapshots.** `POST /api/tasks/:id/handoff` prepares a workspace when needed and writes `handoff.md` plus `handoff.json` beside the task context files.
|
|
417
|
+
- **Board-level triage.** `GET /api/tasks/handoffs` lists readiness packets with ready, needs-attention, and blocked counts so operators can scan handoff risk across the board.
|
|
418
|
+
- **CLI and UI access.** `swarmclaw tasks handoff`, `swarmclaw tasks handoff-save`, and `swarmclaw tasks handoffs` expose the workflow for scripts, while the task sheet can copy, open, or save packets.
|
|
419
|
+
|
|
402
420
|
### v1.9.9 Highlights
|
|
403
421
|
|
|
404
422
|
Schedule revision timeline release: schedule edits, lifecycle changes, and run evidence now stay inspectable from UI, API, and CLI surfaces.
|
|
@@ -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.
|
|
3
|
+
"version": "1.9.11",
|
|
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",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"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
88
|
"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
89
|
"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-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",
|
|
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-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
91
|
"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
92
|
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
|
|
93
93
|
"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
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { buildTaskHandoffPacket, formatTaskHandoffMarkdown } from '@/lib/server/tasks/task-handoff'
|
|
5
|
+
import { prepareTaskExecutionWorkspace } from '@/lib/server/tasks/task-execution-workspace'
|
|
6
|
+
import { loadTasks, saveTask } from '@/lib/server/tasks/task-repository'
|
|
7
|
+
|
|
8
|
+
export const dynamic = 'force-dynamic'
|
|
9
|
+
|
|
10
|
+
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
11
|
+
const { id } = await params
|
|
12
|
+
const tasks = loadTasks()
|
|
13
|
+
const task = tasks[id]
|
|
14
|
+
if (!task) return NextResponse.json({ error: 'Task not found' }, { status: 404 })
|
|
15
|
+
|
|
16
|
+
const packet = buildTaskHandoffPacket(task, tasks)
|
|
17
|
+
const { searchParams } = new URL(req.url)
|
|
18
|
+
if (searchParams.get('format') === 'markdown') {
|
|
19
|
+
return new Response(formatTaskHandoffMarkdown(packet), {
|
|
20
|
+
headers: {
|
|
21
|
+
'content-type': 'text/markdown; charset=utf-8',
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return NextResponse.json(packet)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
30
|
+
const { id } = await params
|
|
31
|
+
const tasks = loadTasks()
|
|
32
|
+
const task = tasks[id]
|
|
33
|
+
if (!task) return NextResponse.json({ error: 'Task not found' }, { status: 404 })
|
|
34
|
+
|
|
35
|
+
let body: Record<string, unknown> = {}
|
|
36
|
+
try {
|
|
37
|
+
body = await req.json() as Record<string, unknown>
|
|
38
|
+
} catch {
|
|
39
|
+
body = {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!task.executionWorkspace || body.prepareWorkspace !== false) {
|
|
43
|
+
Object.assign(task, prepareTaskExecutionWorkspace(task, {
|
|
44
|
+
now: Date.now(),
|
|
45
|
+
actor: 'user',
|
|
46
|
+
tasks,
|
|
47
|
+
}))
|
|
48
|
+
task.updatedAt = Date.now()
|
|
49
|
+
tasks[id] = task
|
|
50
|
+
saveTask(id, task)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const workspacePath = task.executionWorkspace?.path
|
|
54
|
+
if (!workspacePath) {
|
|
55
|
+
return NextResponse.json({ error: 'Task workspace is not available' }, { status: 409 })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fs.mkdirSync(workspacePath, { recursive: true })
|
|
59
|
+
const packet = buildTaskHandoffPacket(task, tasks)
|
|
60
|
+
const markdown = formatTaskHandoffMarkdown(packet)
|
|
61
|
+
const markdownPath = path.join(workspacePath, 'handoff.md')
|
|
62
|
+
const jsonPath = path.join(workspacePath, 'handoff.json')
|
|
63
|
+
fs.writeFileSync(markdownPath, markdown, 'utf8')
|
|
64
|
+
fs.writeFileSync(jsonPath, `${JSON.stringify(packet, null, 2)}\n`, 'utf8')
|
|
65
|
+
|
|
66
|
+
return NextResponse.json({
|
|
67
|
+
packet,
|
|
68
|
+
files: {
|
|
69
|
+
markdownPath,
|
|
70
|
+
jsonPath,
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { buildTaskHandoffPacket } from '@/lib/server/tasks/task-handoff'
|
|
3
|
+
import { loadTasks } from '@/lib/server/tasks/task-repository'
|
|
4
|
+
import type { TaskHandoffReadinessStatus } from '@/types'
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
const READINESS_STATUSES: TaskHandoffReadinessStatus[] = ['ready', 'needs_attention', 'blocked']
|
|
9
|
+
|
|
10
|
+
function normalizeLimit(value: string | null): number {
|
|
11
|
+
const parsed = value ? Number.parseInt(value, 10) : 50
|
|
12
|
+
if (!Number.isFinite(parsed)) return 50
|
|
13
|
+
return Math.max(1, Math.min(200, Math.trunc(parsed)))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function GET(req: Request) {
|
|
17
|
+
const { searchParams } = new URL(req.url)
|
|
18
|
+
const status = searchParams.get('status') as TaskHandoffReadinessStatus | null
|
|
19
|
+
const includeArchived = searchParams.get('includeArchived') === 'true'
|
|
20
|
+
const limit = normalizeLimit(searchParams.get('limit'))
|
|
21
|
+
const now = Date.now()
|
|
22
|
+
const tasks = loadTasks()
|
|
23
|
+
const packets = Object.values(tasks)
|
|
24
|
+
.filter((task) => includeArchived || task.status !== 'archived')
|
|
25
|
+
.map((task) => buildTaskHandoffPacket(task, tasks, { now, runBrief: null }))
|
|
26
|
+
.sort((left, right) => {
|
|
27
|
+
const statusRank: Record<TaskHandoffReadinessStatus, number> = {
|
|
28
|
+
blocked: 0,
|
|
29
|
+
needs_attention: 1,
|
|
30
|
+
ready: 2,
|
|
31
|
+
}
|
|
32
|
+
return statusRank[left.readiness.status] - statusRank[right.readiness.status] || right.updatedAt - left.updatedAt
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const filtered = status && READINESS_STATUSES.includes(status)
|
|
36
|
+
? packets.filter((packet) => packet.readiness.status === status)
|
|
37
|
+
: packets
|
|
38
|
+
const counts: Record<TaskHandoffReadinessStatus, number> = {
|
|
39
|
+
ready: 0,
|
|
40
|
+
needs_attention: 0,
|
|
41
|
+
blocked: 0,
|
|
42
|
+
}
|
|
43
|
+
for (const packet of packets) counts[packet.readiness.status] += 1
|
|
44
|
+
|
|
45
|
+
return NextResponse.json({
|
|
46
|
+
generatedAt: now,
|
|
47
|
+
counts,
|
|
48
|
+
items: filtered.slice(0, limit),
|
|
49
|
+
})
|
|
50
|
+
}
|
|
@@ -15,6 +15,11 @@ const originalEnv = {
|
|
|
15
15
|
|
|
16
16
|
let tempDir = ''
|
|
17
17
|
let putTask: typeof import('./[id]/route')['PUT']
|
|
18
|
+
let getTaskHandoff: typeof import('./[id]/handoff/route')['GET']
|
|
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']
|
|
22
|
+
let getTaskHandoffs: typeof import('./handoffs/route')['GET']
|
|
18
23
|
let getTasks: typeof import('./route')['GET']
|
|
19
24
|
let storage: typeof import('@/lib/server/storage')
|
|
20
25
|
|
|
@@ -46,6 +51,13 @@ before(async () => {
|
|
|
46
51
|
process.env.SWARMCLAW_DAEMON_AUTOSTART = '0'
|
|
47
52
|
storage = await import('@/lib/server/storage')
|
|
48
53
|
putTask = (await import('./[id]/route')).PUT
|
|
54
|
+
const handoffRoute = await import('./[id]/handoff/route')
|
|
55
|
+
getTaskHandoff = handoffRoute.GET
|
|
56
|
+
postTaskHandoff = handoffRoute.POST
|
|
57
|
+
const policyRoute = await import('./[id]/execution-policy/route')
|
|
58
|
+
getTaskExecutionPolicy = policyRoute.GET
|
|
59
|
+
postTaskExecutionPolicy = policyRoute.POST
|
|
60
|
+
getTaskHandoffs = (await import('./handoffs/route')).GET
|
|
49
61
|
getTasks = (await import('./route')).GET
|
|
50
62
|
})
|
|
51
63
|
|
|
@@ -114,3 +126,178 @@ test('GET /api/tasks returns computed blocked liveness without persisting a task
|
|
|
114
126
|
assert.equal(body['task-blocked']?.liveness?.state, 'blocked')
|
|
115
127
|
assert.deepEqual(body['task-blocked']?.liveness?.blockerTaskIds, ['dep-route'])
|
|
116
128
|
})
|
|
129
|
+
|
|
130
|
+
test('GET /api/tasks/:id/handoff returns readiness and markdown packets', async () => {
|
|
131
|
+
seedTask('task-handoff', {
|
|
132
|
+
title: 'Handoff Route Task',
|
|
133
|
+
description: 'Prepare a packet.',
|
|
134
|
+
blockedBy: ['dep-handoff'],
|
|
135
|
+
qualityGate: {
|
|
136
|
+
enabled: true,
|
|
137
|
+
minResultChars: 50,
|
|
138
|
+
minEvidenceItems: 1,
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
const tasks = storage.loadTasks()
|
|
142
|
+
tasks['dep-handoff'] = {
|
|
143
|
+
id: 'dep-handoff',
|
|
144
|
+
title: 'Dependency',
|
|
145
|
+
description: '',
|
|
146
|
+
status: 'running',
|
|
147
|
+
agentId: 'agent-1',
|
|
148
|
+
createdAt: Date.now(),
|
|
149
|
+
updatedAt: Date.now(),
|
|
150
|
+
} as BoardTask
|
|
151
|
+
storage.saveTasks(tasks)
|
|
152
|
+
|
|
153
|
+
const jsonResponse = await getTaskHandoff(
|
|
154
|
+
new Request('http://local/api/tasks/task-handoff/handoff'),
|
|
155
|
+
routeParams('task-handoff'),
|
|
156
|
+
)
|
|
157
|
+
assert.equal(jsonResponse.status, 200)
|
|
158
|
+
const packet = await jsonResponse.json()
|
|
159
|
+
assert.equal(packet.taskId, 'task-handoff')
|
|
160
|
+
assert.equal(packet.readiness.status, 'blocked')
|
|
161
|
+
assert.equal(packet.dependencies.blockedBy[0]?.id, 'dep-handoff')
|
|
162
|
+
|
|
163
|
+
const markdownResponse = await getTaskHandoff(
|
|
164
|
+
new Request('http://local/api/tasks/task-handoff/handoff?format=markdown'),
|
|
165
|
+
routeParams('task-handoff'),
|
|
166
|
+
)
|
|
167
|
+
assert.equal(markdownResponse.status, 200)
|
|
168
|
+
assert.match(markdownResponse.headers.get('content-type') || '', /text\/markdown/)
|
|
169
|
+
const markdown = await markdownResponse.text()
|
|
170
|
+
assert.match(markdown, /# Task Handoff: Handoff Route Task/)
|
|
171
|
+
assert.match(markdown, /Readiness: blocked/)
|
|
172
|
+
})
|
|
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
|
+
|
|
258
|
+
test('POST /api/tasks/:id/handoff saves markdown and JSON snapshots into the workspace', async () => {
|
|
259
|
+
seedTask('task-handoff-save', {
|
|
260
|
+
title: 'Saved Handoff Task',
|
|
261
|
+
cwd: '/source/repo',
|
|
262
|
+
result: 'Ready for the next operator.',
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
const response = await postTaskHandoff(
|
|
266
|
+
new Request('http://local/api/tasks/task-handoff-save/handoff', {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
headers: { 'content-type': 'application/json' },
|
|
269
|
+
body: JSON.stringify({ prepareWorkspace: true }),
|
|
270
|
+
}),
|
|
271
|
+
routeParams('task-handoff-save'),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
assert.equal(response.status, 200)
|
|
275
|
+
const body = await response.json()
|
|
276
|
+
assert.equal(body.packet.taskId, 'task-handoff-save')
|
|
277
|
+
assert.equal(fs.existsSync(body.files.markdownPath), true)
|
|
278
|
+
assert.equal(fs.existsSync(body.files.jsonPath), true)
|
|
279
|
+
assert.match(fs.readFileSync(body.files.markdownPath, 'utf8'), /# Task Handoff: Saved Handoff Task/)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('GET /api/tasks/handoffs lists board-level readiness packets with counts', async () => {
|
|
283
|
+
seedTask('task-ready', {
|
|
284
|
+
title: 'Ready Task',
|
|
285
|
+
executionWorkspace: {
|
|
286
|
+
path: '/tmp/ready',
|
|
287
|
+
mode: 'task',
|
|
288
|
+
preparedAt: Date.now(),
|
|
289
|
+
previewLinks: [],
|
|
290
|
+
runtimeServices: [],
|
|
291
|
+
},
|
|
292
|
+
})
|
|
293
|
+
seedTask('task-needs-attention', {
|
|
294
|
+
title: 'Needs Workspace',
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const response = await getTaskHandoffs(new Request('http://local/api/tasks/handoffs?status=needs_attention&limit=10'))
|
|
298
|
+
assert.equal(response.status, 200)
|
|
299
|
+
const body = await response.json()
|
|
300
|
+
assert.equal(body.counts.ready >= 1, true)
|
|
301
|
+
assert.equal(body.counts.needs_attention >= 1, true)
|
|
302
|
+
assert.equal(body.items.every((packet: { readiness: { status: string } }) => packet.readiness.status === 'needs_attention'), true)
|
|
303
|
+
})
|
package/src/cli/index.js
CHANGED
|
@@ -735,6 +735,11 @@ const COMMAND_GROUPS = [
|
|
|
735
735
|
commands: [
|
|
736
736
|
cmd('list', 'GET', '/tasks', 'List tasks'),
|
|
737
737
|
cmd('get', 'GET', '/tasks/:id', 'Get task'),
|
|
738
|
+
cmd('handoff', 'GET', '/tasks/:id/handoff', 'Get task handoff packet'),
|
|
739
|
+
cmd('handoff-save', 'POST', '/tasks/:id/handoff', 'Save task handoff packet into the task workspace', { expectsJsonBody: true }),
|
|
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 }),
|
|
738
743
|
cmd('create', 'POST', '/tasks', 'Create task', { expectsJsonBody: true }),
|
|
739
744
|
cmd('bulk', 'POST', '/tasks/bulk', 'Bulk update tasks (status/agent/project)', { expectsJsonBody: true }),
|
|
740
745
|
cmd('update', 'PUT', '/tasks/:id', 'Update task', { expectsJsonBody: true }),
|