chainlesschain 0.45.81 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/chainlesschain.js +0 -0
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-YdvJBMHH.js → AppLayout-BnvARObz.js} +1 -1
- package/src/assets/web-panel/assets/Cowork-B8ZDdRm4.js +7 -0
- package/src/assets/web-panel/assets/Cowork-CXuhlHew.css +1 -0
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
- package/src/assets/web-panel/assets/{Dashboard-HPh9FcPt.js → Dashboard-jt6XPIjB.js} +2 -2
- package/src/assets/web-panel/assets/{index-ByUk2Wmr.js → index-C1SPm_5l.js} +2 -2
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/cowork.js +695 -0
- package/src/gateways/ws/action-protocol.js +42 -2
- package/src/lib/cowork-cron.js +474 -0
- package/src/lib/cowork-learning.js +438 -0
- package/src/lib/cowork-mcp-tools.js +182 -0
- package/src/lib/cowork-share.js +218 -0
- package/src/lib/cowork-task-runner.js +317 -3
- package/src/lib/cowork-task-templates.js +101 -13
- package/src/lib/cowork-template-marketplace.js +205 -0
- package/src/lib/cowork-workflow.js +571 -0
- package/src/lib/sub-agent-context.js +40 -0
- package/src/lib/workflow-expr.js +318 -0
- package/src/assets/web-panel/assets/Cowork-BnrHWwZw.js +0 -7
- package/src/assets/web-panel/assets/Cowork-CcSoS3eX.css +0 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cowork Share — export/import signed packets for templates and task results.
|
|
3
|
+
*
|
|
4
|
+
* Produces a verifiable JSON packet that can be transferred by any channel
|
|
5
|
+
* (P2P, email, file drop). The packet contains:
|
|
6
|
+
* - `kind`: "template" or "result"
|
|
7
|
+
* - `payload`: the shareable object (template JSON or history record)
|
|
8
|
+
* - `meta`: { author, createdAt, cliVersion }
|
|
9
|
+
* - `checksum`: sha256 hex over the canonicalized payload+meta
|
|
10
|
+
*
|
|
11
|
+
* Import validates the checksum before returning the payload. This is not an
|
|
12
|
+
* identity signature — anyone can produce a packet — but it protects against
|
|
13
|
+
* accidental corruption during transfer.
|
|
14
|
+
*
|
|
15
|
+
* @module cowork-share
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import {
|
|
22
|
+
toShareableTemplate,
|
|
23
|
+
saveUserTemplate,
|
|
24
|
+
} from "./cowork-template-marketplace.js";
|
|
25
|
+
|
|
26
|
+
export const _deps = {
|
|
27
|
+
existsSync,
|
|
28
|
+
mkdirSync,
|
|
29
|
+
readFileSync,
|
|
30
|
+
writeFileSync,
|
|
31
|
+
now: () => new Date().toISOString(),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const PACKET_VERSION = 1;
|
|
35
|
+
const PACKET_KINDS = ["template", "result"];
|
|
36
|
+
|
|
37
|
+
// ─── Canonical JSON (stable key ordering for checksum) ───────────────────────
|
|
38
|
+
|
|
39
|
+
export function canonicalize(value) {
|
|
40
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
return "[" + value.map(canonicalize).join(",") + "]";
|
|
43
|
+
}
|
|
44
|
+
const keys = Object.keys(value).sort();
|
|
45
|
+
return (
|
|
46
|
+
"{" +
|
|
47
|
+
keys
|
|
48
|
+
.map((k) => JSON.stringify(k) + ":" + canonicalize(value[k]))
|
|
49
|
+
.join(",") +
|
|
50
|
+
"}"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sha256Hex(s) {
|
|
55
|
+
return createHash("sha256").update(s, "utf-8").digest("hex");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Packet builders ─────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build a share packet. `kind` and `payload` are validated; `meta` gets
|
|
62
|
+
* filled with createdAt/cliVersion defaults; checksum is computed over the
|
|
63
|
+
* canonical form of `{ kind, version, payload, meta }`.
|
|
64
|
+
*/
|
|
65
|
+
export function buildPacket({ kind, payload, author, cliVersion } = {}) {
|
|
66
|
+
if (!PACKET_KINDS.includes(kind)) {
|
|
67
|
+
throw new Error(`kind must be one of ${PACKET_KINDS.join(", ")}`);
|
|
68
|
+
}
|
|
69
|
+
if (!payload || typeof payload !== "object") {
|
|
70
|
+
throw new Error("payload must be an object");
|
|
71
|
+
}
|
|
72
|
+
const meta = {
|
|
73
|
+
author: author || "anonymous",
|
|
74
|
+
createdAt: _deps.now(),
|
|
75
|
+
cliVersion: cliVersion || "unknown",
|
|
76
|
+
};
|
|
77
|
+
const body = { kind, version: PACKET_VERSION, payload, meta };
|
|
78
|
+
const checksum = sha256Hex(canonicalize(body));
|
|
79
|
+
return { ...body, checksum };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Verify a packet: checks shape, version, kind, recomputes checksum.
|
|
84
|
+
* Returns `{ valid, errors }`.
|
|
85
|
+
*/
|
|
86
|
+
export function verifyPacket(packet) {
|
|
87
|
+
const errors = [];
|
|
88
|
+
if (!packet || typeof packet !== "object") {
|
|
89
|
+
return { valid: false, errors: ["packet must be an object"] };
|
|
90
|
+
}
|
|
91
|
+
if (packet.version !== PACKET_VERSION) {
|
|
92
|
+
errors.push(`unsupported packet version ${packet.version}`);
|
|
93
|
+
}
|
|
94
|
+
if (!PACKET_KINDS.includes(packet.kind)) {
|
|
95
|
+
errors.push(`unknown kind '${packet.kind}'`);
|
|
96
|
+
}
|
|
97
|
+
if (!packet.payload || typeof packet.payload !== "object") {
|
|
98
|
+
errors.push("payload missing or not an object");
|
|
99
|
+
}
|
|
100
|
+
if (!packet.meta || typeof packet.meta !== "object") {
|
|
101
|
+
errors.push("meta missing");
|
|
102
|
+
}
|
|
103
|
+
if (!packet.checksum) errors.push("checksum missing");
|
|
104
|
+
if (errors.length > 0) return { valid: false, errors };
|
|
105
|
+
|
|
106
|
+
const { checksum, ...body } = packet;
|
|
107
|
+
const expected = sha256Hex(canonicalize(body));
|
|
108
|
+
if (expected !== checksum) {
|
|
109
|
+
errors.push("checksum mismatch (packet may be corrupted or tampered with)");
|
|
110
|
+
}
|
|
111
|
+
return { valid: errors.length === 0, errors };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Higher-level helpers ────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build a packet from a full Cowork template object. The template is reduced
|
|
118
|
+
* to its shareable fields first.
|
|
119
|
+
*/
|
|
120
|
+
export function exportTemplatePacket(template, { author, cliVersion } = {}) {
|
|
121
|
+
const payload = toShareableTemplate(template);
|
|
122
|
+
return buildPacket({ kind: "template", payload, author, cliVersion });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build a packet from a history record (one line of history.jsonl).
|
|
127
|
+
* Irrelevant internal fields are dropped.
|
|
128
|
+
*/
|
|
129
|
+
export function exportResultPacket(historyRecord, { author, cliVersion } = {}) {
|
|
130
|
+
if (!historyRecord || typeof historyRecord !== "object") {
|
|
131
|
+
throw new Error("historyRecord required");
|
|
132
|
+
}
|
|
133
|
+
const payload = {
|
|
134
|
+
taskId: historyRecord.taskId,
|
|
135
|
+
status: historyRecord.status,
|
|
136
|
+
templateId: historyRecord.templateId,
|
|
137
|
+
templateName: historyRecord.templateName,
|
|
138
|
+
userMessage: historyRecord.userMessage,
|
|
139
|
+
timestamp: historyRecord.timestamp,
|
|
140
|
+
result: historyRecord.result,
|
|
141
|
+
};
|
|
142
|
+
return buildPacket({ kind: "result", payload, author, cliVersion });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Find a history record by taskId in `.chainlesschain/cowork/history.jsonl`.
|
|
147
|
+
* Returns null if missing. The last matching line wins.
|
|
148
|
+
*/
|
|
149
|
+
export function findHistoryRecord(cwd, taskId) {
|
|
150
|
+
const file = join(cwd, ".chainlesschain", "cowork", "history.jsonl");
|
|
151
|
+
if (!_deps.existsSync(file)) return null;
|
|
152
|
+
const raw = _deps.readFileSync(file, "utf-8");
|
|
153
|
+
let match = null;
|
|
154
|
+
for (const line of raw.split("\n")) {
|
|
155
|
+
const trimmed = line.trim();
|
|
156
|
+
if (!trimmed) continue;
|
|
157
|
+
try {
|
|
158
|
+
const rec = JSON.parse(trimmed);
|
|
159
|
+
if (rec.taskId === taskId) match = rec;
|
|
160
|
+
} catch (_e) {
|
|
161
|
+
// skip malformed
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return match;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Write a packet to disk as pretty-printed JSON.
|
|
169
|
+
*/
|
|
170
|
+
export function writePacket(filePath, packet) {
|
|
171
|
+
_deps.writeFileSync(filePath, JSON.stringify(packet, null, 2), "utf-8");
|
|
172
|
+
return filePath;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Read + verify a packet from disk. Throws on verification failure.
|
|
177
|
+
*/
|
|
178
|
+
export function readPacket(filePath) {
|
|
179
|
+
if (!_deps.existsSync(filePath)) {
|
|
180
|
+
throw new Error(`Packet not found: ${filePath}`);
|
|
181
|
+
}
|
|
182
|
+
const body = _deps.readFileSync(filePath, "utf-8");
|
|
183
|
+
let packet;
|
|
184
|
+
try {
|
|
185
|
+
packet = JSON.parse(body);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
throw new Error(`Packet is not valid JSON: ${err.message}`);
|
|
188
|
+
}
|
|
189
|
+
const { valid, errors } = verifyPacket(packet);
|
|
190
|
+
if (!valid) throw new Error(`Invalid packet: ${errors.join("; ")}`);
|
|
191
|
+
return packet;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Import a template packet into the local marketplace.
|
|
196
|
+
* Returns the installed template.
|
|
197
|
+
*/
|
|
198
|
+
export function importTemplatePacket(cwd, packet) {
|
|
199
|
+
if (packet.kind !== "template") {
|
|
200
|
+
throw new Error(`Expected template packet, got '${packet.kind}'`);
|
|
201
|
+
}
|
|
202
|
+
return saveUserTemplate(cwd, packet.payload);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Import a result packet into a local `.chainlesschain/cowork/shared-results/`
|
|
207
|
+
* directory. Produces one JSON file per result, keyed by taskId.
|
|
208
|
+
*/
|
|
209
|
+
export function importResultPacket(cwd, packet) {
|
|
210
|
+
if (packet.kind !== "result") {
|
|
211
|
+
throw new Error(`Expected result packet, got '${packet.kind}'`);
|
|
212
|
+
}
|
|
213
|
+
const dir = join(cwd, ".chainlesschain", "cowork", "shared-results");
|
|
214
|
+
_deps.mkdirSync(dir, { recursive: true });
|
|
215
|
+
const file = join(dir, `${packet.payload.taskId}.json`);
|
|
216
|
+
_deps.writeFileSync(file, JSON.stringify(packet, null, 2), "utf-8");
|
|
217
|
+
return { file, taskId: packet.payload.taskId };
|
|
218
|
+
}
|
|
@@ -7,14 +7,16 @@
|
|
|
7
7
|
* @module cowork-task-runner
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { existsSync, mkdirSync, appendFileSync } from "node:fs";
|
|
10
|
+
import { existsSync, mkdirSync, appendFileSync, readFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { SubAgentContext } from "./sub-agent-context.js";
|
|
13
|
-
import { getTemplate } from "./cowork-task-templates.js";
|
|
13
|
+
import { getTemplate, setUserTemplates } from "./cowork-task-templates.js";
|
|
14
|
+
import { mountTemplateMcpTools } from "./cowork-mcp-tools.js";
|
|
15
|
+
import { listUserTemplates } from "./cowork-template-marketplace.js";
|
|
14
16
|
|
|
15
17
|
// ─── Dependencies (overridable for testing) ──────────────────────────────────
|
|
16
18
|
|
|
17
|
-
export const _deps = { existsSync, mkdirSync, appendFileSync };
|
|
19
|
+
export const _deps = { existsSync, mkdirSync, appendFileSync, readFileSync };
|
|
18
20
|
|
|
19
21
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
20
22
|
|
|
@@ -63,18 +65,53 @@ export async function runCoworkTask(options = {}) {
|
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
// Merge user-installed templates (marketplace) into the registry before resolving
|
|
69
|
+
try {
|
|
70
|
+
setUserTemplates(listUserTemplates(cwd));
|
|
71
|
+
} catch (_e) {
|
|
72
|
+
// Non-fatal — marketplace absence should not break task execution
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
// Resolve template
|
|
67
76
|
const template = getTemplate(templateId);
|
|
68
77
|
|
|
69
78
|
// Build the task prompt with template context + files
|
|
70
79
|
const taskParts = [template.systemPromptExtension];
|
|
71
80
|
|
|
81
|
+
// N2: apply learning-layer patch for this template if one exists
|
|
82
|
+
try {
|
|
83
|
+
const { loadUserTemplate } = await import("./cowork-learning.js");
|
|
84
|
+
const override = loadUserTemplate(cwd, template.id);
|
|
85
|
+
if (override?.systemPromptExtension) {
|
|
86
|
+
taskParts.push(
|
|
87
|
+
`\n## 历史学习补丁 (learning patch)\n${override.systemPromptExtension}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
} catch (_e) {
|
|
91
|
+
// Non-fatal — learning overrides are optional
|
|
92
|
+
}
|
|
93
|
+
|
|
72
94
|
if (files.length > 0) {
|
|
73
95
|
taskParts.push(`\n## 用户提供的文件\n${files.join("\n")}`);
|
|
74
96
|
}
|
|
75
97
|
|
|
76
98
|
const task = taskParts.join("\n");
|
|
77
99
|
|
|
100
|
+
// Mount template-declared MCP servers (best-effort, failures are tolerated)
|
|
101
|
+
const mcp = await mountTemplateMcpTools(template, {
|
|
102
|
+
onWarn: (msg) => {
|
|
103
|
+
if (onProgress) onProgress({ type: "mcp-warning", message: msg });
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
if (onProgress && (mcp.mounted.length > 0 || mcp.skipped.length > 0)) {
|
|
107
|
+
onProgress({
|
|
108
|
+
type: "mcp-mounted",
|
|
109
|
+
mounted: mcp.mounted,
|
|
110
|
+
skipped: mcp.skipped.map((s) => s.name),
|
|
111
|
+
toolCount: mcp.extraToolDefinitions.length,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
78
115
|
// Create isolated sub-agent context
|
|
79
116
|
const subAgent = SubAgentContext.create({
|
|
80
117
|
role: `cowork-${template.id}`,
|
|
@@ -87,6 +124,10 @@ export async function runCoworkTask(options = {}) {
|
|
|
87
124
|
cwd,
|
|
88
125
|
onProgress,
|
|
89
126
|
signal,
|
|
127
|
+
extraToolDefinitions: mcp.extraToolDefinitions,
|
|
128
|
+
externalToolDescriptors: mcp.externalToolDescriptors,
|
|
129
|
+
externalToolExecutors: mcp.externalToolExecutors,
|
|
130
|
+
mcpClient: mcp.mcpClient,
|
|
90
131
|
});
|
|
91
132
|
|
|
92
133
|
const taskId = subAgent.id;
|
|
@@ -128,6 +169,279 @@ export async function runCoworkTask(options = {}) {
|
|
|
128
169
|
};
|
|
129
170
|
_appendHistory(cwd, entry, userMessage);
|
|
130
171
|
return entry;
|
|
172
|
+
} finally {
|
|
173
|
+
await mcp.cleanup();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Parallel Runner (Orchestrator) ──────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Run a cowork task using the Orchestrator for multi-agent parallel execution.
|
|
181
|
+
*
|
|
182
|
+
* @param {object} options - Same as runCoworkTask, plus:
|
|
183
|
+
* @param {number} [options.agents] - Number of parallel agents (default 3, max 10)
|
|
184
|
+
* @param {string} [options.strategy] - Routing strategy (default "round-robin")
|
|
185
|
+
* @param {function} [options.onProgress] - Progress callback
|
|
186
|
+
* @param {AbortSignal} [options.signal] - Cancellation signal
|
|
187
|
+
* @returns {Promise<{ taskId: string, status: string, result: object }>}
|
|
188
|
+
*/
|
|
189
|
+
export async function runCoworkTaskParallel(options = {}) {
|
|
190
|
+
const {
|
|
191
|
+
templateId = null,
|
|
192
|
+
userMessage,
|
|
193
|
+
files = [],
|
|
194
|
+
cwd = process.cwd(),
|
|
195
|
+
agents = 3,
|
|
196
|
+
strategy,
|
|
197
|
+
onProgress = null,
|
|
198
|
+
signal = null,
|
|
199
|
+
} = options;
|
|
200
|
+
|
|
201
|
+
if (!userMessage || typeof userMessage !== "string") {
|
|
202
|
+
throw new Error("userMessage is required");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (files.length > 0) {
|
|
206
|
+
const missing = files.filter((f) => !_deps.existsSync(f));
|
|
207
|
+
if (missing.length > 0) {
|
|
208
|
+
throw new Error(`File(s) not found: ${missing.join(", ")}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const template = getTemplate(templateId);
|
|
213
|
+
|
|
214
|
+
// Build full task description for the orchestrator
|
|
215
|
+
const taskParts = [
|
|
216
|
+
`[Cowork Template: ${template.name}]`,
|
|
217
|
+
template.systemPromptExtension,
|
|
218
|
+
`\n## 用户需求\n${userMessage}`,
|
|
219
|
+
];
|
|
220
|
+
if (files.length > 0) {
|
|
221
|
+
taskParts.push(`\n## 用户提供的文件\n${files.join("\n")}`);
|
|
222
|
+
}
|
|
223
|
+
const fullTask = taskParts.join("\n");
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const { Orchestrator, TASK_SOURCE } = await import("./orchestrator.js");
|
|
227
|
+
|
|
228
|
+
const orch = new Orchestrator({
|
|
229
|
+
cwd,
|
|
230
|
+
maxParallel: Math.min(parseInt(agents, 10) || 3, 10),
|
|
231
|
+
ciCommand: "echo ok",
|
|
232
|
+
agents: strategy ? { strategy } : undefined,
|
|
233
|
+
verbose: false,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Wire progress events
|
|
237
|
+
if (onProgress) {
|
|
238
|
+
orch.on("task:added", (t) =>
|
|
239
|
+
onProgress({
|
|
240
|
+
type: "orchestrator-started",
|
|
241
|
+
taskId: t.id,
|
|
242
|
+
subtaskCount: 0,
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
orch.on("task:decomposed", (t) =>
|
|
246
|
+
onProgress({
|
|
247
|
+
type: "orchestrator-decomposed",
|
|
248
|
+
taskId: t.id,
|
|
249
|
+
subtaskCount: t.subtasks?.length || 0,
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
orch.on("agents:dispatched", (ev) =>
|
|
253
|
+
onProgress({
|
|
254
|
+
type: "agents-dispatched",
|
|
255
|
+
agentCount: ev.agents?.length || 0,
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
orch.on("agent:output", (ev) =>
|
|
259
|
+
onProgress({
|
|
260
|
+
type: "agent-progress",
|
|
261
|
+
agentIndex: ev.agentIndex,
|
|
262
|
+
status: ev.status,
|
|
263
|
+
output: ev.output?.slice(0, 200),
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Handle cancellation
|
|
269
|
+
if (signal) {
|
|
270
|
+
signal.addEventListener(
|
|
271
|
+
"abort",
|
|
272
|
+
() => {
|
|
273
|
+
orch.stopCronWatch();
|
|
274
|
+
},
|
|
275
|
+
{ once: true },
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const orchResult = await orch.addTask(fullTask, {
|
|
280
|
+
source: TASK_SOURCE.CLI,
|
|
281
|
+
cwd,
|
|
282
|
+
runCI: false,
|
|
283
|
+
notify: false,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const entry = {
|
|
287
|
+
taskId: orchResult.id,
|
|
288
|
+
status: orchResult.status === "completed" ? "completed" : "failed",
|
|
289
|
+
templateId: template.id,
|
|
290
|
+
templateName: template.name,
|
|
291
|
+
parallel: true,
|
|
292
|
+
agentCount: agents,
|
|
293
|
+
result: {
|
|
294
|
+
summary:
|
|
295
|
+
orchResult.agentResults
|
|
296
|
+
?.map((r) => r.output?.slice(0, 500))
|
|
297
|
+
.join("\n---\n") || "Parallel execution completed",
|
|
298
|
+
artifacts: [],
|
|
299
|
+
tokenCount: 0,
|
|
300
|
+
toolsUsed: [],
|
|
301
|
+
iterationCount: orchResult.retries || 0,
|
|
302
|
+
subtaskCount: orchResult.subtasks?.length || 0,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
_appendHistory(cwd, entry, userMessage);
|
|
306
|
+
return entry;
|
|
307
|
+
} catch (err) {
|
|
308
|
+
const entry = {
|
|
309
|
+
taskId: `cowork-parallel-${Date.now()}`,
|
|
310
|
+
status: "failed",
|
|
311
|
+
templateId: template.id,
|
|
312
|
+
templateName: template.name,
|
|
313
|
+
parallel: true,
|
|
314
|
+
result: {
|
|
315
|
+
summary: `Parallel task failed: ${err.message}`,
|
|
316
|
+
artifacts: [],
|
|
317
|
+
tokenCount: 0,
|
|
318
|
+
toolsUsed: [],
|
|
319
|
+
iterationCount: 0,
|
|
320
|
+
subtaskCount: 0,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
_appendHistory(cwd, entry, userMessage);
|
|
324
|
+
return entry;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ─── Debate Runner (Multi-perspective Review) ───────────────────────────────
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Run a cowork task in debate mode — multiple reviewer perspectives converge
|
|
332
|
+
* into a final verdict via moderator synthesis.
|
|
333
|
+
*
|
|
334
|
+
* @param {object} options
|
|
335
|
+
* @param {string|null} options.templateId - Should be "code-review" or null
|
|
336
|
+
* @param {string} options.userMessage - Target description / review instructions
|
|
337
|
+
* @param {string[]} [options.files] - File paths to review (concatenated as code body)
|
|
338
|
+
* @param {string[]} [options.perspectives] - Override template perspectives
|
|
339
|
+
* @param {string} [options.cwd] - Working directory for history
|
|
340
|
+
* @param {object} [options.llmOptions] - LLM provider/model/key
|
|
341
|
+
* @param {function} [options.onProgress] - Progress callback
|
|
342
|
+
* @returns {Promise<{ taskId, status, result }>}
|
|
343
|
+
*/
|
|
344
|
+
export async function runCoworkDebate(options = {}) {
|
|
345
|
+
const {
|
|
346
|
+
templateId = "code-review",
|
|
347
|
+
userMessage,
|
|
348
|
+
files = [],
|
|
349
|
+
perspectives,
|
|
350
|
+
cwd = process.cwd(),
|
|
351
|
+
llmOptions = {},
|
|
352
|
+
onProgress = null,
|
|
353
|
+
} = options;
|
|
354
|
+
|
|
355
|
+
if (!userMessage || typeof userMessage !== "string") {
|
|
356
|
+
throw new Error("userMessage is required");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (files.length > 0) {
|
|
360
|
+
const missing = files.filter((f) => !_deps.existsSync(f));
|
|
361
|
+
if (missing.length > 0) {
|
|
362
|
+
throw new Error(`File(s) not found: ${missing.join(", ")}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const template = getTemplate(templateId);
|
|
367
|
+
const reviewPerspectives = perspectives ||
|
|
368
|
+
template.debatePerspectives || [
|
|
369
|
+
"performance",
|
|
370
|
+
"security",
|
|
371
|
+
"maintainability",
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
// Build code body from files (or from userMessage if no files provided)
|
|
375
|
+
let code = "";
|
|
376
|
+
if (files.length > 0) {
|
|
377
|
+
const chunks = files.map((f) => {
|
|
378
|
+
try {
|
|
379
|
+
return `// ===== ${f} =====\n${_deps.readFileSync(f, "utf-8")}`;
|
|
380
|
+
} catch (err) {
|
|
381
|
+
return `// ===== ${f} (read error: ${err.message}) =====`;
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
code = chunks.join("\n\n");
|
|
385
|
+
} else {
|
|
386
|
+
code = userMessage;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const taskId = `cowork-debate-${Date.now()}`;
|
|
390
|
+
|
|
391
|
+
if (onProgress) {
|
|
392
|
+
onProgress({ type: "debate-started", perspectives: reviewPerspectives });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const { startDebate } = await import("./cowork/debate-review-cli.js");
|
|
397
|
+
const debateResult = await startDebate({
|
|
398
|
+
target: userMessage,
|
|
399
|
+
code,
|
|
400
|
+
perspectives: reviewPerspectives,
|
|
401
|
+
llmOptions,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (onProgress) {
|
|
405
|
+
onProgress({ type: "debate-completed", verdict: debateResult.verdict });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const entry = {
|
|
409
|
+
taskId,
|
|
410
|
+
status: "completed",
|
|
411
|
+
templateId: template.id,
|
|
412
|
+
templateName: template.name,
|
|
413
|
+
mode: "debate",
|
|
414
|
+
result: {
|
|
415
|
+
summary: debateResult.summary,
|
|
416
|
+
verdict: debateResult.verdict,
|
|
417
|
+
consensusScore: debateResult.consensusScore,
|
|
418
|
+
reviews: debateResult.reviews,
|
|
419
|
+
perspectives: debateResult.perspectives,
|
|
420
|
+
artifacts: [],
|
|
421
|
+
tokenCount: 0,
|
|
422
|
+
toolsUsed: [],
|
|
423
|
+
iterationCount: debateResult.reviews.length + 1,
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
_appendHistory(cwd, entry, userMessage);
|
|
427
|
+
return entry;
|
|
428
|
+
} catch (err) {
|
|
429
|
+
const entry = {
|
|
430
|
+
taskId,
|
|
431
|
+
status: "failed",
|
|
432
|
+
templateId: template.id,
|
|
433
|
+
templateName: template.name,
|
|
434
|
+
mode: "debate",
|
|
435
|
+
result: {
|
|
436
|
+
summary: `Debate failed: ${err.message}`,
|
|
437
|
+
artifacts: [],
|
|
438
|
+
tokenCount: 0,
|
|
439
|
+
toolsUsed: [],
|
|
440
|
+
iterationCount: 0,
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
_appendHistory(cwd, entry, userMessage);
|
|
444
|
+
return entry;
|
|
131
445
|
}
|
|
132
446
|
}
|
|
133
447
|
|