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.
- package/dist/commands/doctor.d.ts +32 -0
- package/dist/commands/doctor.test.d.ts +1 -0
- package/dist/commands/init.d.ts +32 -0
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/resume.d.ts +32 -0
- package/dist/commands/resume.test.d.ts +1 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.test.d.ts +1 -0
- package/dist/commands/tui.d.ts +4 -0
- package/dist/commands/upgrade.d.ts +1 -0
- package/dist/commands/upgrade.test.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +285 -41
- package/dist/lib.d.ts +7 -8
- package/dist/lib.test.d.ts +1 -0
- package/dist/loader/loadConfig.d.ts +2 -0
- package/dist/loader/loadConfig.test.d.ts +1 -0
- package/dist/reporters/ConsoleReporter.d.ts +5 -0
- package/dist/reporters/ConsoleReporter.test.d.ts +1 -0
- package/dist/reporters/FileLogWriter.d.ts +29 -0
- package/dist/reporters/FileLogWriter.test.d.ts +1 -0
- package/package.json +8 -8
|
@@ -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 @@
|
|
|
1
|
+
export declare function upgradeCommand(): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
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
|
|
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(
|
|
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
|
|
260
|
-
const sep =
|
|
261
|
-
return `agents.${id}({ ${
|
|
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
|
|
410
|
-
import
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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((
|
|
719
|
+
(s) => new Promise((resolve4) => {
|
|
621
720
|
if (s.destroyed || s.writableEnded) {
|
|
622
|
-
|
|
721
|
+
resolve4();
|
|
623
722
|
return;
|
|
624
723
|
}
|
|
625
|
-
s.end(() =>
|
|
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(
|
|
638
|
-
const config = await loadConfig(options.config,
|
|
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
|
|
643
|
-
|
|
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 ??
|
|
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 =
|
|
714
|
-
var pkg = JSON.parse(
|
|
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
|
|
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
|
|
2
|
-
export { AgentDefinition, MultiClawConfig
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
34
|
-
"@multiclawcli/dashboard": "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.
|
|
39
|
+
"@types/node": "^26.6.5",
|
|
40
40
|
"tsx": "^4.7.0",
|
|
41
|
-
"typescript": "^
|
|
42
|
-
"vitest": "^
|
|
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",
|