chainlesschain 0.45.80 → 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.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{Analytics--dpzs0oZ.js → Analytics-C1AnPdMx.js} +2 -2
  4. package/src/assets/web-panel/assets/{AppLayout-DWXapZbP.js → AppLayout-BnvARObz.js} +1 -1
  5. package/src/assets/web-panel/assets/{Backup-BoUUzGFw.js → Backup-D31iZX3l.js} +1 -1
  6. package/src/assets/web-panel/assets/{Chat-CkSlXBzN.js → Chat-DiXJ3TuK.js} +1 -1
  7. package/src/assets/web-panel/assets/Cowork-B8ZDdRm4.js +7 -0
  8. package/src/assets/web-panel/assets/Cowork-CXuhlHew.css +1 -0
  9. package/src/assets/web-panel/assets/{Cron-xiBL6XfP.js → Cron-DBt1ueXh.js} +2 -2
  10. package/src/assets/web-panel/assets/{Dashboard-CmgEtUKl.js → Dashboard-jt6XPIjB.js} +1 -1
  11. package/src/assets/web-panel/assets/{Git-DCDjvp5Z.js → Git-hwQ1oZHj.js} +2 -2
  12. package/src/assets/web-panel/assets/{Logs-Qz-GLplC.js → Logs-4D9p6PRM.js} +1 -1
  13. package/src/assets/web-panel/assets/{McpTools-qYf_sT-Y.js → McpTools-CyAUjbbs.js} +1 -1
  14. package/src/assets/web-panel/assets/{Memory-BxoM2XNZ.js → Memory-BMqOR7S-.js} +2 -2
  15. package/src/assets/web-panel/assets/{Notes-DltR8wq4.js → Notes-Cmas8i4E.js} +2 -2
  16. package/src/assets/web-panel/assets/{Organization-7m_PX3yo.js → Organization-DnSa58Tl.js} +4 -4
  17. package/src/assets/web-panel/assets/{P2P-e88KqFBm.js → P2P-BxksIBWs.js} +2 -2
  18. package/src/assets/web-panel/assets/{Permissions-DAY4Xy1h.js → Permissions-Bq5Qn2s3.js} +4 -4
  19. package/src/assets/web-panel/assets/{Projects-ylUhg9th.js → Projects-B7EM0uPg.js} +1 -1
  20. package/src/assets/web-panel/assets/{Providers-DNIlBWLm.js → Providers-DAwgG5KV.js} +2 -2
  21. package/src/assets/web-panel/assets/{RssFeed-Dr_6vD69.js → RssFeed-HSZoRXvS.js} +2 -2
  22. package/src/assets/web-panel/assets/{Security-U57Q-VOj.js → Security-Cz17qBny.js} +3 -3
  23. package/src/assets/web-panel/assets/{Services-BUfO-jvr.js → Services-D2EsLq-v.js} +1 -1
  24. package/src/assets/web-panel/assets/{Skills-D0NYT7Q8.js → Skills-C9v-f3vZ.js} +1 -1
  25. package/src/assets/web-panel/assets/{Tasks-WXqKX58l.js → Tasks-yMEcU0n7.js} +1 -1
  26. package/src/assets/web-panel/assets/{Templates-B1zfqNTe.js → Templates-l7SvlKuB.js} +1 -1
  27. package/src/assets/web-panel/assets/{Wallet-CUSPGN3F.js → Wallet-BHWhLWn9.js} +4 -4
  28. package/src/assets/web-panel/assets/{WebAuthn-ZGz__UJi.js → WebAuthn-kWhFYaUK.js} +4 -4
  29. package/src/assets/web-panel/assets/{antd-BQNxIyr-.js → antd-D6h4fDFf.js} +82 -82
  30. package/src/assets/web-panel/assets/{index-BYqeR6ME.js → index-C1SPm_5l.js} +2 -2
  31. package/src/assets/web-panel/assets/{markdown-BeVIhIzs.js → markdown-BZsB-Dsv.js} +1 -1
  32. package/src/assets/web-panel/index.html +2 -2
  33. package/src/commands/cowork.js +695 -0
  34. package/src/gateways/ws/action-protocol.js +143 -2
  35. package/src/gateways/ws/message-dispatcher.js +3 -0
  36. package/src/gateways/ws/ws-server.js +18 -0
  37. package/src/lib/cowork-cron.js +474 -0
  38. package/src/lib/cowork-learning.js +438 -0
  39. package/src/lib/cowork-mcp-tools.js +182 -0
  40. package/src/lib/cowork-share.js +218 -0
  41. package/src/lib/cowork-task-runner.js +364 -4
  42. package/src/lib/cowork-task-templates.js +203 -12
  43. package/src/lib/cowork-template-marketplace.js +205 -0
  44. package/src/lib/cowork-workflow.js +571 -0
  45. package/src/lib/sub-agent-context.js +66 -0
  46. package/src/lib/workflow-expr.js +318 -0
  47. package/src/assets/web-panel/assets/Cowork-CPqYhoMI.css +0 -1
  48. package/src/assets/web-panel/assets/Cowork-DjAJ5ymV.js +0 -48
@@ -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,8 +7,16 @@
7
7
  * @module cowork-task-runner
8
8
  */
9
9
 
10
+ import { existsSync, mkdirSync, appendFileSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
10
12
  import { SubAgentContext } from "./sub-agent-context.js";
11
- 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";
16
+
17
+ // ─── Dependencies (overridable for testing) ──────────────────────────────────
18
+
19
+ export const _deps = { existsSync, mkdirSync, appendFileSync, readFileSync };
12
20
 
13
21
  // ─── Constants ────────────────────────────────────────────────────────────────
14
22
 
@@ -41,24 +49,69 @@ export async function runCoworkTask(options = {}) {
41
49
  llmOptions = {},
42
50
  maxIterations = DEFAULT_MAX_ITERATIONS,
43
51
  tokenBudget = DEFAULT_TOKEN_BUDGET,
52
+ onProgress = null,
53
+ signal = null,
44
54
  } = options;
45
55
 
46
56
  if (!userMessage || typeof userMessage !== "string") {
47
57
  throw new Error("userMessage is required");
48
58
  }
49
59
 
60
+ // Validate file paths before starting
61
+ if (files.length > 0) {
62
+ const missing = files.filter((f) => !_deps.existsSync(f));
63
+ if (missing.length > 0) {
64
+ throw new Error(`File(s) not found: ${missing.join(", ")}`);
65
+ }
66
+ }
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
+
50
75
  // Resolve template
51
76
  const template = getTemplate(templateId);
52
77
 
53
78
  // Build the task prompt with template context + files
54
79
  const taskParts = [template.systemPromptExtension];
55
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
+
56
94
  if (files.length > 0) {
57
95
  taskParts.push(`\n## 用户提供的文件\n${files.join("\n")}`);
58
96
  }
59
97
 
60
98
  const task = taskParts.join("\n");
61
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
+
62
115
  // Create isolated sub-agent context
63
116
  const subAgent = SubAgentContext.create({
64
117
  role: `cowork-${template.id}`,
@@ -69,28 +122,39 @@ export async function runCoworkTask(options = {}) {
69
122
  db,
70
123
  llmOptions,
71
124
  cwd,
125
+ onProgress,
126
+ signal,
127
+ extraToolDefinitions: mcp.extraToolDefinitions,
128
+ externalToolDescriptors: mcp.externalToolDescriptors,
129
+ externalToolExecutors: mcp.externalToolExecutors,
130
+ mcpClient: mcp.mcpClient,
72
131
  });
73
132
 
74
133
  const taskId = subAgent.id;
75
134
 
76
135
  // Build loop options — pass shell policy overrides if template declares them
77
136
  const loopOptions = {};
78
- if (Array.isArray(template.shellPolicyOverrides) && template.shellPolicyOverrides.length) {
137
+ if (
138
+ Array.isArray(template.shellPolicyOverrides) &&
139
+ template.shellPolicyOverrides.length
140
+ ) {
79
141
  loopOptions.shellPolicyOverrides = template.shellPolicyOverrides;
80
142
  }
81
143
 
82
144
  // Run the agent with the user's message
83
145
  try {
84
146
  const result = await subAgent.run(userMessage, loopOptions);
85
- return {
147
+ const entry = {
86
148
  taskId,
87
149
  status: subAgent.status,
88
150
  templateId: template.id,
89
151
  templateName: template.name,
90
152
  result,
91
153
  };
154
+ _appendHistory(cwd, entry, userMessage);
155
+ return entry;
92
156
  } catch (err) {
93
- return {
157
+ const entry = {
94
158
  taskId,
95
159
  status: "failed",
96
160
  templateId: template.id,
@@ -103,5 +167,301 @@ export async function runCoworkTask(options = {}) {
103
167
  iterationCount: 0,
104
168
  },
105
169
  };
170
+ _appendHistory(cwd, entry, userMessage);
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;
445
+ }
446
+ }
447
+
448
+ // ─── History Persistence ─────────────────────────────────────────────────────
449
+
450
+ function _appendHistory(cwd, entry, userMessage) {
451
+ try {
452
+ const histDir = join(cwd, ".chainlesschain", "cowork");
453
+ _deps.mkdirSync(histDir, { recursive: true });
454
+ const record = {
455
+ ...entry,
456
+ userMessage,
457
+ timestamp: new Date().toISOString(),
458
+ };
459
+ _deps.appendFileSync(
460
+ join(histDir, "history.jsonl"),
461
+ JSON.stringify(record) + "\n",
462
+ "utf-8",
463
+ );
464
+ } catch (_e) {
465
+ // Best-effort — don't fail the task for history write errors
106
466
  }
107
467
  }