multiclaw 0.4.0 → 0.5.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.
@@ -0,0 +1,32 @@
1
+ import { type DetectedProvider, type ModelSelectionMode, type MultiClawConfig, type ProviderSource, type ProviderStatus } from "@multiclawcli/core";
2
+ export interface DoctorProviderRow {
3
+ id: string;
4
+ label: string;
5
+ status: ProviderStatus;
6
+ command: string;
7
+ source?: ProviderSource;
8
+ resolvedPath?: string;
9
+ version?: string;
10
+ modelCount: number;
11
+ modelSelection: ModelSelectionMode;
12
+ authHint: string;
13
+ installHint?: string;
14
+ }
15
+ export interface DoctorReport {
16
+ providers: DoctorProviderRow[];
17
+ installedCount: number;
18
+ config?: {
19
+ name?: string;
20
+ defaultsProvider?: string;
21
+ defaultsProviderOk: boolean;
22
+ agentsWithMissingProvider: string[];
23
+ agentsWithUnsupportedTools: string[];
24
+ };
25
+ runnable: boolean;
26
+ }
27
+ /** Build the diagnostic model from detection results and an optional config. */
28
+ export declare function buildDoctorReport(detected: DetectedProvider[], config?: MultiClawConfig): DoctorReport;
29
+ /** Render the report as human-readable text for stdout. */
30
+ export declare function formatDoctorReport(report: DoctorReport, useColor?: boolean): string;
31
+ /** Run the `doctor` command: detect providers, print the report, set exit code. */
32
+ export declare function doctorCommand(configPath?: string): Promise<void>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The agents each preset actually contains, by their `agents.<id>()` name.
3
+ *
4
+ * Kept adjacent to {@link AGENT_BLOCKS} because the two must stay in step: the
5
+ * override checkbox offers exactly these ids, and the regex that injects an
6
+ * override only matches an id that really appears in the block. A mismatch is a
7
+ * silently dropped override, which is what this list exists to prevent. The
8
+ * `init.test.ts` consistency check asserts every id here has a matching
9
+ * `agents.<id>(` in the block string.
10
+ */
11
+ export declare const PRESET_AGENT_IDS: Record<string, string[]>;
12
+ /**
13
+ * The override choices for a preset: the leader (which belongs to no preset)
14
+ * plus the preset's own agents. Filtering by preset is what stops an override
15
+ * for an agent the preset does not contain from being offered and then dropped.
16
+ */
17
+ export declare function overrideChoices(preset: string): Array<{
18
+ value: string;
19
+ name: string;
20
+ }>;
21
+ export declare function generateConfig(opts: {
22
+ preset: string;
23
+ workDir: string;
24
+ projectName: string;
25
+ provider: string;
26
+ model?: string;
27
+ overrides?: Array<{
28
+ id: string;
29
+ model: string;
30
+ }>;
31
+ }): string;
32
+ export declare function initCommand(): Promise<void>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import type { MultiClawConfig, OrchestratorResult } from "@multiclawcli/core";
2
+ /** A previous run's report, as much of it as resume needs. */
3
+ export interface ResumeTarget {
4
+ runDir: string;
5
+ report: OrchestratorResult;
6
+ }
7
+ /**
8
+ * Resolve `--resume` into the run directory it names.
9
+ *
10
+ * `true` (a bare `--resume`) and `"latest"` both mean "the newest run". Run
11
+ * directories are named `run-<ISO stamp>` with a fixed-width stamp, so a plain
12
+ * lexicographic maximum is the most recent one — deterministic, and testable
13
+ * without a clock.
14
+ *
15
+ * Any other value is treated as a path: either the run directory itself or its
16
+ * `logs/` subdirectory, since that is where the report lives and a caller may
17
+ * well point at it.
18
+ */
19
+ export declare function resolveRunDir(runsRoot: string, target: string | boolean): string;
20
+ /** Read a run's `report.json`. */
21
+ export declare function readReport(runDir: string): OrchestratorResult;
22
+ /**
23
+ * Turn a report (plus an optional requirement override) into run config.
24
+ *
25
+ * The requirement is the one thing a report cannot always supply — an older
26
+ * report predates the field — so an explicit override wins, and its absence on
27
+ * both sides is an error rather than a silently empty run.
28
+ */
29
+ export declare function resumeOptionsFromReport(report: OrchestratorResult, requirementOverride?: string): {
30
+ requirement: string;
31
+ resume: NonNullable<MultiClawConfig["resume"]>;
32
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ export interface RunOptions {
2
+ config?: string;
3
+ noLeader?: boolean;
4
+ /**
5
+ * `true` for a bare `--resume` (latest run), or a path to a specific run.
6
+ * Commander only ever produces `undefined`, `true` or a string, so `false` is
7
+ * not part of the domain.
8
+ */
9
+ resume?: string | true;
10
+ dryRun?: boolean;
11
+ }
12
+ export declare function runCommand(requirementArg: string | undefined, options: RunOptions): Promise<void>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export interface TuiCommandOptions {
2
+ config?: string;
3
+ }
4
+ export declare function tuiCommand(options: TuiCommandOptions): Promise<void>;
@@ -0,0 +1 @@
1
+ export declare function upgradeCommand(): Promise<void>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync } from "fs";
5
- import { dirname as dirname2, join as join3 } from "path";
4
+ import { readFileSync as readFileSync2 } from "fs";
5
+ import { dirname as dirname3, join as join4 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { Command } from "commander";
8
8
 
@@ -10,7 +10,9 @@ import { Command } from "commander";
10
10
  import {
11
11
  detectProviders,
12
12
  presentProviders,
13
+ resolveCommand,
13
14
  resolveProvider,
15
+ specForCommand,
14
16
  specForProvider
15
17
  } from "@multiclawcli/core";
16
18
 
@@ -96,6 +98,8 @@ function buildDoctorReport(detected, config) {
96
98
  label: d.label,
97
99
  status: d.status,
98
100
  command: d.command,
101
+ source: d.source,
102
+ resolvedPath: d.resolvedPath,
99
103
  version: d.version,
100
104
  modelCount: spec?.models.length ?? 0,
101
105
  modelSelection: spec?.modelSelection ?? "none",
@@ -111,11 +115,17 @@ function buildDoctorReport(detected, config) {
111
115
  const agents2 = [...config.agents];
112
116
  if (config.leader) agents2.push(config.leader);
113
117
  const agentsWithMissingProvider = agents2.map((a) => ({ id: a.id, intended: resolveProvider(a, config) })).filter((a) => !present.includes(a.intended)).map((a) => a.id);
118
+ const agentsWithUnsupportedTools = agents2.map((a) => {
119
+ const command = resolveCommand(a, config);
120
+ const spec = specForCommand(command) ?? specForProvider(resolveProvider(a, config));
121
+ return { id: a.id, spec, tools: a.tools };
122
+ }).filter((a) => a.spec && !a.spec.tools && a.tools !== void 0).map((a) => a.id);
114
123
  configReport = {
115
124
  name: config.name,
116
125
  defaultsProvider,
117
126
  defaultsProviderOk,
118
- agentsWithMissingProvider
127
+ agentsWithMissingProvider,
128
+ agentsWithUnsupportedTools
119
129
  };
120
130
  }
121
131
  return {
@@ -145,16 +155,23 @@ function formatDoctorReport(report, useColor = true) {
145
155
  lines.push("");
146
156
  lines.push(c(DIM, "multiclaw doctor \u2014 environment check"));
147
157
  lines.push("");
148
- lines.push(`${cell("provider", 12)}${cell("status", 10)}${cell("version", 14)}models auth`);
158
+ lines.push(
159
+ `${cell("provider", 12)}${cell("status", 10)}${cell("version", 14)}${cell("source", 9)}models auth`
160
+ );
149
161
  for (const p of report.providers) {
150
162
  const mark = p.status === "present" ? "\u2713 present" : "\u2717 missing";
151
163
  const markCode = p.status === "present" ? GREEN : RED;
152
164
  const version = p.version ?? (p.status === "present" ? "" : "\u2014");
165
+ const source = p.source ?? "\u2014";
166
+ const sourceCode = p.source === "bundled" ? YELLOW : void 0;
153
167
  const models = p.modelSelection === "none" ? "n/a" : String(p.modelCount);
154
168
  const auth = p.authHint ? c(DIM, p.authHint) : "";
155
169
  lines.push(
156
- `${cell(p.label, 12)}${cell(mark, 10, markCode)}${cell(version, 14)}${cell(models, 7)}${auth}`
170
+ `${cell(p.label, 12)}${cell(mark, 10, markCode)}${cell(version, 14)}${cell(source, 9, sourceCode)}${cell(models, 7)}${auth}`
157
171
  );
172
+ if (p.status === "present" && p.source === "bundled" && p.resolvedPath) {
173
+ lines.push(c(DIM, ` bundled bridge: ${p.resolvedPath}`));
174
+ }
158
175
  if (p.status === "missing" && p.installHint) {
159
176
  lines.push(c(YELLOW, ` install: ${p.installHint}`));
160
177
  }
@@ -178,6 +195,14 @@ function formatDoctorReport(report, useColor = true) {
178
195
  } else {
179
196
  lines.push(c(GREEN, " all agents resolve to an installed provider \u2713"));
180
197
  }
198
+ if (report.config.agentsWithUnsupportedTools.length > 0) {
199
+ lines.push(
200
+ c(
201
+ YELLOW,
202
+ ` ${report.config.agentsWithUnsupportedTools.length} agent(s) use a provider that cannot enforce tool allowlists: ${report.config.agentsWithUnsupportedTools.join(", ")}`
203
+ )
204
+ );
205
+ }
181
206
  }
182
207
  lines.push("");
183
208
  if (report.runnable) {
@@ -244,21 +269,75 @@ var PRESET_DESCRIPTIONS = {
244
269
  frontend: "Architect \u2192 UI Designer \u2192 UI Design Reviewer \u2192 Frontend Developer \u2192 Tester \u2192 Code Reviewer (6 agents)",
245
270
  fullstack: "Product Manager \u2192 Architect \u2192 Arch Design Reviewer \u2192 Backend Developer + UI Designer \u2192 UI Design Reviewer \u2192 Frontend Developer \u2192 Tester \u2192 Code Reviewer + DevOps (10 agents)"
246
271
  };
272
+ var PRESET_AGENT_IDS = {
273
+ backend: [
274
+ "productManager",
275
+ "architect",
276
+ "archDesignReviewer",
277
+ "backendDeveloper",
278
+ "tester",
279
+ "codeReviewer",
280
+ "devops"
281
+ ],
282
+ frontend: [
283
+ "architect",
284
+ "uiDesigner",
285
+ "uiDesignReviewer",
286
+ "frontendDeveloper",
287
+ "tester",
288
+ "codeReviewer"
289
+ ],
290
+ fullstack: [
291
+ "productManager",
292
+ "architect",
293
+ "archDesignReviewer",
294
+ "backendDeveloper",
295
+ "uiDesigner",
296
+ "uiDesignReviewer",
297
+ "frontendDeveloper",
298
+ "tester",
299
+ "codeReviewer",
300
+ "devops"
301
+ ]
302
+ };
303
+ var OVERRIDE_LABELS = {
304
+ leader: "\u2605 Leader",
305
+ productManager: "Product Manager",
306
+ architect: "Architect",
307
+ archDesignReviewer: "Arch Design Reviewer",
308
+ backendDeveloper: "Backend Developer",
309
+ frontendDeveloper: "Frontend Developer",
310
+ uiDesigner: "UI Designer",
311
+ uiDesignReviewer: "UI Design Reviewer",
312
+ tester: "Tester",
313
+ codeReviewer: "Code Reviewer",
314
+ devops: "DevOps"
315
+ };
316
+ function overrideChoices(preset) {
317
+ const ids = PRESET_AGENT_IDS[preset] ?? [];
318
+ return [
319
+ { value: "leader", name: OVERRIDE_LABELS.leader },
320
+ ...ids.map((id) => ({ value: id, name: OVERRIDE_LABELS[id] ?? id }))
321
+ ];
322
+ }
247
323
  function generateConfig(opts) {
248
324
  const name = JSON.stringify(opts.projectName);
249
325
  const workDir = JSON.stringify(opts.workDir);
250
326
  const defaultsParts = [`provider: ${JSON.stringify(opts.provider)}`];
251
327
  if (opts.model) defaultsParts.push(`model: ${JSON.stringify(opts.model)}`);
328
+ const leaderOverride = opts.overrides?.find((o) => o.id === "leader");
329
+ const leaderLine = leaderOverride ? ` leader: agents.leader({ model: ${JSON.stringify(leaderOverride.model)} }),` : ` leader: agents.leader(),`;
252
330
  let agentBlock = AGENT_BLOCKS[opts.preset];
253
331
  if (opts.overrides && opts.overrides.length > 0) {
254
332
  for (const ov of opts.overrides) {
255
333
  const id = ov.id;
334
+ if (id === "leader") continue;
256
335
  const model = JSON.stringify(ov.model);
257
336
  const re = new RegExp(`(agents\\.${id}\\([^)]*\\))`, "g");
258
337
  agentBlock = agentBlock.replace(re, (match) => {
259
- const inner = match.slice(id.length + 8, -1);
260
- const sep = inner.trim() ? ", " : "";
261
- return `agents.${id}({ ${inner}${sep}model: ${model} })`;
338
+ const body = match.slice(id.length + 8, -1).trim().replace(/^\{/, "").replace(/\}$/, "").trim();
339
+ const sep = body ? ", " : "";
340
+ return `agents.${id}({ ${body}${sep}model: ${model} })`;
262
341
  });
263
342
  }
264
343
  }
@@ -267,6 +346,7 @@ function generateConfig(opts) {
267
346
  export default defineConfig({
268
347
  name: ${name},
269
348
  workDir: ${workDir},
349
+ ${leaderLine}
270
350
  context: {
271
351
  projectName: ${name},
272
352
  },
@@ -331,19 +411,7 @@ async function initCommand() {
331
411
  const modelDefault = modelAnswer || void 0;
332
412
  const overrideIds = await checkbox({
333
413
  message: "Override model for specific agents? (space to select, enter to confirm)",
334
- choices: [
335
- { value: "leader", name: "\u2605 Leader" },
336
- { value: "productManager", name: "Product Manager" },
337
- { value: "architect", name: "Architect" },
338
- { value: "archDesignReviewer", name: "Arch Design Reviewer" },
339
- { value: "backendDeveloper", name: "Backend Developer" },
340
- { value: "frontendDeveloper", name: "Frontend Developer" },
341
- { value: "uiDesigner", name: "UI Designer" },
342
- { value: "uiDesignReviewer", name: "UI Design Reviewer" },
343
- { value: "tester", name: "Tester" },
344
- { value: "codeReviewer", name: "Code Reviewer" },
345
- { value: "devops", name: "DevOps" }
346
- ]
414
+ choices: overrideChoices(preset)
347
415
  });
348
416
  const overrides = [];
349
417
  for (const id of overrideIds) {
@@ -406,14 +474,18 @@ async function initCommand() {
406
474
  }
407
475
 
408
476
  // src/commands/run.ts
409
- import * as path4 from "path";
410
- import { Orchestrator as OrchestratorImpl } from "@multiclawcli/core";
477
+ import * as fs5 from "fs";
478
+ import * as path5 from "path";
479
+ import {
480
+ describeResumePlan,
481
+ Orchestrator as OrchestratorImpl
482
+ } from "@multiclawcli/core";
411
483
 
412
484
  // src/lib.ts
413
485
  import {
414
486
  detectProviders as detectProviders3,
415
487
  presentProviders as presentProviders2,
416
- resolveCommand,
488
+ resolveCommand as resolveCommand2,
417
489
  runtimeRegistry
418
490
  } from "@multiclawcli/core";
419
491
  import { agents, defineConfig } from "@multiclawcli/core";
@@ -426,7 +498,7 @@ async function assertRuntimesAvailable(config) {
426
498
  console.error(`\x1B[31mError: Unknown runtime "${name}". Supported: ${supported}\x1B[0m`);
427
499
  process.exit(1);
428
500
  }
429
- const command = resolveCommand(agent, config);
501
+ const command = resolveCommand2(agent, config);
430
502
  const key = `${name}\0${command}`;
431
503
  if (checked.has(key)) continue;
432
504
  checked.add(key);
@@ -463,6 +535,11 @@ var C = isFancy ? {
463
535
  red: "",
464
536
  purple: ""
465
537
  };
538
+ function markFor(status) {
539
+ if (status === "success") return { icon: S.ok, color: C.green };
540
+ if (status === "cancelled") return { icon: S.skip, color: C.yellow };
541
+ return { icon: S.fail, color: C.red };
542
+ }
466
543
  var ConsoleReporter = class {
467
544
  attach(eventBus) {
468
545
  return eventBus.subscribe((e) => this.handle(e));
@@ -480,6 +557,10 @@ ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
480
557
  `${C.cyan}${S.bullet} Agents: ${e.payload.totalAgents} Stages: ${e.payload.stages.length}${C.reset}
481
558
  `
482
559
  );
560
+ if (e.payload.selectionReason) {
561
+ console.log(`${C.dim}${S.bullet} Why: ${e.payload.selectionReason}${C.reset}
562
+ `);
563
+ }
483
564
  break;
484
565
  }
485
566
  case "orchestration:stages": {
@@ -498,6 +579,7 @@ ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
498
579
  break;
499
580
  }
500
581
  case "agent:output": {
582
+ if (e.payload.kind && e.payload.kind !== "text") break;
501
583
  process.stdout.write(` ${e.payload.chunk}`);
502
584
  break;
503
585
  }
@@ -507,6 +589,10 @@ ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
507
589
  break;
508
590
  }
509
591
  case "agent:failed": {
592
+ if (e.payload.status === "cancelled") {
593
+ console.log(`${C.yellow}${S.skip} ${e.payload.agentName} cancelled${C.reset}`);
594
+ break;
595
+ }
510
596
  console.log(`${C.red}${S.fail} ${e.payload.agentName} failed: ${e.payload.error}${C.reset}`);
511
597
  break;
512
598
  }
@@ -533,22 +619,33 @@ ${C.cyan}${mark} ${reply.message}${C.reset}`);
533
619
  }
534
620
  const line = (isFancy ? "\u2550" : "=").repeat(50);
535
621
  const edge = isFancy ? ["\u2554", "\u2551", "\u255A", "\u2557", "\u255D"] : ["+", "|", "+", "+", "+"];
536
- const title = reply ? reply.mode === "ask" ? `${S.warn} Needs your input` : `${S.ok} Answered` : success ? `${S.ok} All done` : `${S.warn} Done (with failures)`;
622
+ const title = reply ? reply.mode === "ask" ? `${S.warn} Needs your input` : `${S.ok} Answered` : e.payload.cancelled ? `${S.skip} Cancelled` : success ? `${S.ok} All done` : `${S.warn} Done (with failures)`;
537
623
  console.log(`
538
624
  ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
539
625
  console.log(`${edge[1]} ${title.padEnd(48)}${edge[1]}`);
540
626
  console.log(`${edge[2]}${line}${edge[4]}${C.reset}`);
541
627
  const nameWidth = Math.max(8, ...agentResults.map((r) => r.agentName.length));
542
628
  const totalStr = `${(totalDuration / 1e3).toFixed(1)}s`;
543
- console.log(` ~ ${"Total".padEnd(nameWidth)} ${totalStr.padStart(7)}`);
629
+ const cost = e.payload.totalCostUsd;
630
+ const costStr = cost === void 0 ? "" : ` $${cost.toFixed(2)}`;
631
+ console.log(` ~ ${"Total".padEnd(nameWidth)} ${totalStr.padStart(7)}${costStr}`);
544
632
  for (const r of agentResults) {
545
- const icon = r.status === "success" ? S.ok : S.fail;
546
- const color2 = r.status === "success" ? C.green : C.red;
633
+ const { icon, color: color2 } = markFor(r.status);
547
634
  const dur = `${(r.duration / 1e3).toFixed(1)}s`;
548
635
  console.log(
549
636
  ` ${color2}${icon}${C.reset} ${r.agentName.padEnd(nameWidth)} ${dur.padStart(7)}`
550
637
  );
551
638
  }
639
+ if (e.payload.deliverables && e.payload.deliverables.length > 0) {
640
+ console.log(`
641
+ Deliverables:`);
642
+ for (const file of e.payload.deliverables.slice(0, 5)) {
643
+ console.log(` ${S.bullet} ${file.path} (${file.status}, ${file.size} bytes)`);
644
+ }
645
+ if (e.payload.deliverables.length > 5) {
646
+ console.log(` ${S.bullet} \u2026 ${e.payload.deliverables.length - 5} more`);
647
+ }
648
+ }
552
649
  console.log();
553
650
  break;
554
651
  }
@@ -587,7 +684,9 @@ var FileLogWriter = class {
587
684
  }
588
685
  handle(e) {
589
686
  if (e.type === "agent:output") {
590
- this.getStream(e.payload.agentId)?.write(e.payload.chunk);
687
+ const { agentId, chunk, kind } = e.payload;
688
+ if (kind && kind !== "text") return;
689
+ this.getStream(agentId)?.write(chunk);
591
690
  }
592
691
  if (e.type === "orchestration:complete") {
593
692
  try {
@@ -617,12 +716,12 @@ var FileLogWriter = class {
617
716
  this.streams.clear();
618
717
  return Promise.all(
619
718
  streams.map(
620
- (s) => new Promise((resolve3) => {
719
+ (s) => new Promise((resolve4) => {
621
720
  if (s.destroyed || s.writableEnded) {
622
- resolve3();
721
+ resolve4();
623
722
  return;
624
723
  }
625
- s.end(() => resolve3());
724
+ s.end(() => resolve4());
626
725
  })
627
726
  )
628
727
  ).then(() => void 0);
@@ -632,19 +731,108 @@ function errorMessage(err) {
632
731
  return err instanceof Error ? err.message : String(err);
633
732
  }
634
733
 
734
+ // src/commands/resume.ts
735
+ import * as fs4 from "fs";
736
+ import * as path4 from "path";
737
+ function resolveRunDir(runsRoot, target) {
738
+ if (typeof target !== "string" || target === "latest") {
739
+ return latestRunDir(runsRoot);
740
+ }
741
+ const resolved = path4.resolve(target);
742
+ const runDir = path4.basename(resolved) === "logs" ? path4.dirname(resolved) : resolved;
743
+ if (!fs4.existsSync(runDir)) {
744
+ throw new Error(`Run directory not found: ${runDir}`);
745
+ }
746
+ return runDir;
747
+ }
748
+ function latestRunDir(runsRoot) {
749
+ const names = fs4.existsSync(runsRoot) ? fs4.readdirSync(runsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("run-")).map((entry) => entry.name).sort() : [];
750
+ const latest = names[names.length - 1];
751
+ if (!latest) {
752
+ throw new Error(`No runs found to resume \u2014 looked in ${runsRoot}`);
753
+ }
754
+ return path4.join(runsRoot, latest);
755
+ }
756
+ function readReport(runDir) {
757
+ const reportPath = path4.join(runDir, "logs", "report.json");
758
+ let raw;
759
+ try {
760
+ raw = fs4.readFileSync(reportPath, "utf-8");
761
+ } catch {
762
+ throw new Error(`No report.json in ${runDir} \u2014 it may not be a complete run`);
763
+ }
764
+ try {
765
+ return JSON.parse(raw);
766
+ } catch (err) {
767
+ const detail = err instanceof Error ? err.message : String(err);
768
+ throw new Error(`Could not parse ${reportPath}: ${detail}`);
769
+ }
770
+ }
771
+ function resumeOptionsFromReport(report, requirementOverride) {
772
+ const agents2 = [...new Set(report.agentResults.map((result) => result.agentId))];
773
+ if (agents2.length === 0) {
774
+ throw new Error("The previous run has no agent results \u2014 nothing to resume");
775
+ }
776
+ const sessions = {};
777
+ for (const result of report.agentResults) {
778
+ if (result.sessionId) sessions[result.agentId] = result.sessionId;
779
+ }
780
+ const requirement = requirementOverride ?? report.requirement;
781
+ if (requirement === void 0) {
782
+ throw new Error("The previous run recorded no requirement \u2014 pass one explicitly to resume");
783
+ }
784
+ return {
785
+ requirement,
786
+ resume: {
787
+ agents: agents2,
788
+ // Absent, not `{}`: core reads a missing `sessions` as "start every agent
789
+ // fresh", which is exactly the single-agent retry of a failed agent.
790
+ ...Object.keys(sessions).length > 0 ? { sessions } : {}
791
+ }
792
+ };
793
+ }
794
+
635
795
  // src/commands/run.ts
636
796
  var EXIT_CANCELLED = 130;
637
- async function runCommand(requirement, options) {
638
- const config = await loadConfig(options.config, requirement);
797
+ async function runCommand(requirementArg, options) {
798
+ const config = await loadConfig(options.config, requirementArg);
639
799
  if (options.noLeader) {
640
800
  config.useLeader = false;
641
801
  }
642
- const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
643
- config.workDir = path4.join(config.workDir, ".multiclaw", "runs", `run-${ts}`);
802
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
803
+ if (options.resume !== void 0) {
804
+ const runsRoot = path5.join(config.workDir, ".multiclaw", "runs");
805
+ const runDir = resolveRunDir(runsRoot, options.resume);
806
+ const report = readReport(runDir);
807
+ const { requirement, resume } = resumeOptionsFromReport(report, requirementArg);
808
+ config.workDir = runDir;
809
+ config.resume = resume;
810
+ config.context = { ...config.context, requirement };
811
+ config.logDir = path5.join(runDir, `logs-resume-${stamp}`);
812
+ const plan = describeResumePlan({
813
+ agents: resume.agents,
814
+ sessions: resume.sessions,
815
+ team: config.agents
816
+ });
817
+ console.log(`\x1B[2mResuming ${runDir} (${resume.agents.length} agents) \u2014 ${plan}\x1B[0m`);
818
+ } else {
819
+ config.workDir = path5.join(config.workDir, ".multiclaw", "runs", `run-${stamp}`);
820
+ }
821
+ if (options.dryRun) {
822
+ const preview = await new OrchestratorImpl(config).preview();
823
+ printDryRun(preview, config.workDir);
824
+ process.exitCode = 0;
825
+ return;
826
+ }
644
827
  await assertRuntimesAvailable(config);
828
+ const before = snapshotFiles(config.workDir);
645
829
  const orchestrator = new OrchestratorImpl(config);
830
+ orchestrator.eventBus.subscribe((event) => {
831
+ if (event.type !== "orchestration:complete") return;
832
+ event.payload.deliverables = collectDeliverables(config.workDir, before);
833
+ });
646
834
  new ConsoleReporter().attach(orchestrator.eventBus);
647
- const logDir = config.logDir ?? path4.join(config.workDir, "logs");
835
+ const logDir = config.logDir ?? path5.join(config.workDir, "logs");
648
836
  const logs = new FileLogWriter(logDir);
649
837
  logs.attach(orchestrator.eventBus);
650
838
  console.log(`\x1B[2mLogs: ${logDir}\x1B[0m`);
@@ -662,6 +850,59 @@ async function runCommand(requirement, options) {
662
850
  process.off("SIGINT", onSigint);
663
851
  }
664
852
  }
853
+ function printDryRun(preview, workDir) {
854
+ console.log("Dry run \u2014 no pipeline agents will execute.");
855
+ console.log(`Project: ${preview.name}`);
856
+ console.log(`Work dir: ${workDir}`);
857
+ if (preview.requirement) console.log(`Requirement: ${preview.requirement}`);
858
+ if (preview.reply) {
859
+ console.log(`${preview.reply.mode === "ask" ? "Question" : "Reply"}: ${preview.reply.message}`);
860
+ return;
861
+ }
862
+ console.log(`Agents: ${preview.totalAgents}`);
863
+ if (preview.selectionReason) console.log(`Why: ${preview.selectionReason}`);
864
+ for (const stage of preview.stages) {
865
+ const agents2 = stage.agents.map(
866
+ (agent) => `${agent.id}${agent.model ? ` (${agent.model})` : ""}`
867
+ );
868
+ console.log(`Stage ${stage.stageIndex + 1}: ${agents2.join(", ") || "\u2014"}`);
869
+ }
870
+ }
871
+ function snapshotFiles(root) {
872
+ const snapshot = /* @__PURE__ */ new Map();
873
+ walkFiles(root, (_absolute, relative2, stat) => {
874
+ snapshot.set(relative2, { mtimeMs: stat.mtimeMs, size: stat.size });
875
+ });
876
+ return snapshot;
877
+ }
878
+ function collectDeliverables(root, before) {
879
+ const deliverables = [];
880
+ walkFiles(root, (_absolute, relative2, stat) => {
881
+ const previous = before.get(relative2);
882
+ if (previous && previous.mtimeMs === stat.mtimeMs && previous.size === stat.size) return;
883
+ deliverables.push({
884
+ path: relative2,
885
+ status: previous ? "modified" : "created",
886
+ size: stat.size,
887
+ modifiedAt: stat.mtime.toISOString()
888
+ });
889
+ });
890
+ return deliverables.sort((a, b) => a.path.localeCompare(b.path)).slice(0, 20);
891
+ }
892
+ function walkFiles(root, visit) {
893
+ if (!fs5.existsSync(root)) return;
894
+ const stack = [root];
895
+ while (stack.length > 0) {
896
+ const dir = stack.pop();
897
+ for (const name of fs5.readdirSync(dir)) {
898
+ if (name === ".multiclaw") continue;
899
+ const absolute = path5.join(dir, name);
900
+ const stat = fs5.statSync(absolute);
901
+ if (stat.isDirectory()) stack.push(absolute);
902
+ else if (stat.isFile()) visit(absolute, path5.relative(root, absolute), stat);
903
+ }
904
+ }
905
+ }
665
906
 
666
907
  // src/commands/tui.ts
667
908
  async function tuiCommand(options) {
@@ -710,12 +951,15 @@ async function upgradeCommand() {
710
951
 
711
952
  // src/index.ts
712
953
  var __filename2 = fileURLToPath(import.meta.url);
713
- var __dirname2 = dirname2(__filename2);
714
- var pkg = JSON.parse(readFileSync(join3(__dirname2, "../package.json"), "utf-8"));
954
+ var __dirname2 = dirname3(__filename2);
955
+ var pkg = JSON.parse(readFileSync2(join4(__dirname2, "../package.json"), "utf-8"));
715
956
  var program = new Command();
716
957
  program.name("multiclaw").description("Multi-agent orchestration CLI").version(pkg.version, "-v, --version", "Display version number").action((options) => tuiCommand(options));
717
958
  program.command("init").description("Scaffold a config file (interactive)").action(() => initCommand());
718
- program.command("run <requirement>").description("Run an orchestration headlessly").option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)").option("--no-leader", "Batch mode only: skip routing and run the full team").action((requirement, opts) => runCommand(requirement, opts));
959
+ program.command("run [requirement]").description("Run an orchestration headlessly").option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)").option("--no-leader", "Batch mode only: skip routing and run the full team").option("--dry-run", "Preview the routed plan without running the pipeline").option(
960
+ "--resume [path]",
961
+ "Resume a previous run, continuing agent sessions (default: the latest)"
962
+ ).action((requirement, opts) => runCommand(requirement, opts));
719
963
  program.command("upgrade").description("Upgrade multiclaw to the latest version").action(() => upgradeCommand());
720
964
  program.command("doctor").description("Check which agent providers are installed and ready").option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)").action((opts) => doctorCommand(opts.config));
721
965
  program.parseAsync(process.argv).catch((err) => {
package/dist/lib.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { MultiClawConfig } from '@multiclawcli/core';
2
- export { AgentDefinition, MultiClawConfig, agents, defineConfig, detectProviders, presentProviders } from '@multiclawcli/core';
3
-
1
+ import { detectProviders, type MultiClawConfig, presentProviders } from "@multiclawcli/core";
2
+ export type { AgentDefinition, MultiClawConfig } from "@multiclawcli/core";
3
+ export { agents, defineConfig } from "@multiclawcli/core";
4
4
  /**
5
5
  * Hard gate for `run`/`tui`: every runtime the config names must exist and
6
6
  * report itself as installed, or the process exits before any work starts.
@@ -10,12 +10,11 @@ export { AgentDefinition, MultiClawConfig, agents, defineConfig, detectProviders
10
10
  * receives the agent's configured command because an external-process runtime
11
11
  * (e.g. ACP) is only as available as the binary it was told to launch.
12
12
  */
13
- declare function assertRuntimesAvailable(config: MultiClawConfig): Promise<void>;
14
-
13
+ export declare function assertRuntimesAvailable(config: MultiClawConfig): Promise<void>;
14
+ /** Re-export for consumers that need full detection details. */
15
+ export { detectProviders, presentProviders };
15
16
  /**
16
17
  * Legacy alias: returns runtime names that have at least one provider present.
17
18
  * Prefer `detectProviders()` for new code.
18
19
  */
19
- declare function availableRuntimes(): Promise<string[]>;
20
-
21
- export { assertRuntimesAvailable, availableRuntimes };
20
+ export declare function availableRuntimes(): Promise<string[]>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { MultiClawConfig } from "@multiclawcli/core";
2
+ export declare function loadConfig(configPath?: string, requirement?: string): Promise<MultiClawConfig>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { EventBus } from "@multiclawcli/core";
2
+ export declare class ConsoleReporter {
3
+ attach(eventBus: EventBus): () => void;
4
+ private handle;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import type { EventBus } from "@multiclawcli/core";
2
+ /**
3
+ * Streams each agent's output to its own log file, plus a `report.json` per turn.
4
+ *
5
+ * Logging is diagnostics, so nothing in here is allowed to take the run down: a
6
+ * failed log write is reported and then ignored.
7
+ */
8
+ export declare class FileLogWriter {
9
+ private logDir;
10
+ private onError;
11
+ private streams;
12
+ /** Agent ids whose log could not be opened — never retried, so a broken
13
+ * directory does not produce one failing stream per output chunk. */
14
+ private failed;
15
+ private closing;
16
+ constructor(logDir: string, onError?: (message: string) => void);
17
+ attach(eventBus: EventBus): () => void;
18
+ /**
19
+ * Flush every open stream.
20
+ *
21
+ * `stream.end()` only *starts* the flush, so a caller that exits the moment
22
+ * `orchestration:complete` arrives can lose the tail of the last agent's log.
23
+ * Awaiting this is what makes that tail durable.
24
+ */
25
+ drain(): Promise<void>;
26
+ private handle;
27
+ private getStream;
28
+ private flush;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multiclaw",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multiclaw": "dist/index.js"
@@ -27,22 +27,22 @@
27
27
  "url": "https://github.com/shipengqi/multiclaw/issues"
28
28
  },
29
29
  "dependencies": {
30
- "@agentclientprotocol/claude-agent-acp": "^0.89.1",
30
+ "@agentclientprotocol/claude-agent-acp": "^0.93.0",
31
31
  "@agentclientprotocol/codex-acp": "^2.2.2",
32
32
  "@inquirer/prompts": "^8.7.3",
33
- "@multiclawcli/core": "0.4.0",
34
- "@multiclawcli/dashboard": "0.4.0",
33
+ "@multiclawcli/core": "0.5.0",
34
+ "@multiclawcli/dashboard": "0.5.0",
35
35
  "commander": "^15.0.0",
36
36
  "jiti": "^2.7.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/node": "^26.6.4",
39
+ "@types/node": "^26.6.5",
40
40
  "tsx": "^4.7.0",
41
- "typescript": "^5.4.0",
42
- "vitest": "^4.1.11"
41
+ "typescript": "^7.0.2",
42
+ "vitest": "^5.0.4"
43
43
  },
44
44
  "scripts": {
45
- "build": "tsup",
45
+ "build": "tsup && tsc -p tsconfig.json --emitDeclarationOnly --outDir dist --composite false --incremental false",
46
46
  "typecheck": "tsc -p tsconfig.json --noEmit",
47
47
  "test": "vitest run",
48
48
  "test:coverage": "vitest run --coverage",