@alexeiled/pi-fusion 0.2.1 → 0.2.3
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/docs/user-guide.md +4 -3
- package/package.json +1 -1
- package/src/claude-aliases.ts +255 -0
- package/src/commands.ts +27 -158
- package/src/config.ts +12 -18
- package/src/fusion-args.ts +126 -0
- package/src/orchestrator.ts +49 -102
- package/src/panel-completion.ts +85 -0
- package/src/run-store.ts +1 -12
- package/src/types.ts +5 -0
- package/src/utils.ts +19 -0
package/docs/user-guide.md
CHANGED
|
@@ -110,7 +110,8 @@ Panel member:
|
|
|
110
110
|
- `id`: stable machine name
|
|
111
111
|
- `label`: human-readable report label
|
|
112
112
|
- `agent`: subagent name
|
|
113
|
-
- `model`: optional model override; often the main source of panel diversity
|
|
113
|
+
- `model`: optional model override; often the main source of panel diversity. Supports normal Pi model ids, and if `pi-claude-alias` is configured, Claude alias shorthand like `claude-work/opus-4.8`
|
|
114
|
+
- Claude alias handles must be unique across global and project alias files; duplicate handles are rejected.
|
|
114
115
|
- `thinking`: optional `off`, `minimal`, `low`, `medium`, `high`, or `xhigh`
|
|
115
116
|
- `role`: optional perspective hint layered on top of the model
|
|
116
117
|
|
|
@@ -162,7 +163,7 @@ Deliberate review:
|
|
|
162
163
|
"id": "architect",
|
|
163
164
|
"label": "Architect",
|
|
164
165
|
"agent": "pi-fusion.fusion-panelist",
|
|
165
|
-
"model": "
|
|
166
|
+
"model": "claude-work/sonnet-4.6",
|
|
166
167
|
"thinking": "high",
|
|
167
168
|
"role": "architecture and failure modes"
|
|
168
169
|
},
|
|
@@ -177,7 +178,7 @@ Deliberate review:
|
|
|
177
178
|
],
|
|
178
179
|
"judge": {
|
|
179
180
|
"agent": "pi-fusion.fusion-judge",
|
|
180
|
-
"model": "
|
|
181
|
+
"model": "claude-work/sonnet-4.6",
|
|
181
182
|
"thinking": "high"
|
|
182
183
|
},
|
|
183
184
|
"concurrency": 2,
|
package/package.json
CHANGED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { FusionConfig } from "./types.js";
|
|
5
|
+
import { FusionConfigError } from "./errors.js";
|
|
6
|
+
import { isNodeErrorCode, isNonEmptyString, isRecord } from "./utils.js";
|
|
7
|
+
import type { FusionConfigLoadContext } from "./config.js";
|
|
8
|
+
|
|
9
|
+
const CLAUDE_ALIAS_CONFIG_FILE = "claude-alias.json";
|
|
10
|
+
|
|
11
|
+
interface ClaudeAliasDefinition {
|
|
12
|
+
slug: string;
|
|
13
|
+
providerId: string;
|
|
14
|
+
handle: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface AliasFileDeps {
|
|
18
|
+
readTextFile?: (path: string) => Promise<string>;
|
|
19
|
+
agentDir?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface RawAliasConfig {
|
|
23
|
+
aliases?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RawAliasEntry {
|
|
27
|
+
slug?: unknown;
|
|
28
|
+
handle?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function applyClaudeAliasShorthand(
|
|
32
|
+
config: FusionConfig,
|
|
33
|
+
ctx: FusionConfigLoadContext,
|
|
34
|
+
deps: AliasFileDeps = {},
|
|
35
|
+
): Promise<FusionConfig> {
|
|
36
|
+
const aliases = await loadClaudeAliases(ctx, deps);
|
|
37
|
+
if (aliases.length === 0) return config;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...config,
|
|
41
|
+
profiles: Object.fromEntries(
|
|
42
|
+
Object.entries(config.profiles).map(([name, profile]) => [
|
|
43
|
+
name,
|
|
44
|
+
{
|
|
45
|
+
...profile,
|
|
46
|
+
panel: profile.panel.map((member) => ({
|
|
47
|
+
...member,
|
|
48
|
+
...(member.model
|
|
49
|
+
? { model: resolveClaudeAliasModelSpec(member.model, aliases) }
|
|
50
|
+
: {}),
|
|
51
|
+
})),
|
|
52
|
+
judge: {
|
|
53
|
+
...profile.judge,
|
|
54
|
+
...(profile.judge.model
|
|
55
|
+
? { model: resolveClaudeAliasModelSpec(profile.judge.model, aliases) }
|
|
56
|
+
: {}),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
]),
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolveClaudeAliasModelSpec(
|
|
65
|
+
model: string,
|
|
66
|
+
aliases: readonly ClaudeAliasDefinition[],
|
|
67
|
+
): string {
|
|
68
|
+
const trimmed = model.trim();
|
|
69
|
+
const slashIndex = trimmed.indexOf("/");
|
|
70
|
+
if (slashIndex <= 0 || slashIndex === trimmed.length - 1) return trimmed;
|
|
71
|
+
|
|
72
|
+
const handle = trimmed.slice(0, slashIndex).trim().toLowerCase();
|
|
73
|
+
const modelRef = trimmed.slice(slashIndex + 1).trim();
|
|
74
|
+
const alias = aliases.find((item) => item.handle === handle);
|
|
75
|
+
if (!alias) return trimmed;
|
|
76
|
+
|
|
77
|
+
return `${alias.providerId}/${normalizeAnthropicModelRef(modelRef)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function normalizeAnthropicModelRef(modelRef: string): string {
|
|
81
|
+
const normalized = modelRef
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[._\s]+/g, "-")
|
|
85
|
+
.replace(/-+/g, "-")
|
|
86
|
+
.replace(/^-+|-+$/g, "");
|
|
87
|
+
|
|
88
|
+
if (!normalized) return modelRef.trim();
|
|
89
|
+
return normalized.startsWith("claude-") ? normalized : `claude-${normalized}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function loadClaudeAliases(
|
|
93
|
+
ctx: FusionConfigLoadContext,
|
|
94
|
+
deps: AliasFileDeps,
|
|
95
|
+
): Promise<ClaudeAliasDefinition[]> {
|
|
96
|
+
const readTextFile = deps.readTextFile ?? readUtf8File;
|
|
97
|
+
const global = await readOptionalAliasFile(
|
|
98
|
+
getGlobalClaudeAliasConfigPath(deps.agentDir),
|
|
99
|
+
readTextFile,
|
|
100
|
+
);
|
|
101
|
+
const project = ctx.isProjectTrusted()
|
|
102
|
+
? await readOptionalAliasFile(
|
|
103
|
+
getProjectClaudeAliasConfigPath(ctx.cwd),
|
|
104
|
+
readTextFile,
|
|
105
|
+
)
|
|
106
|
+
: undefined;
|
|
107
|
+
|
|
108
|
+
const merged = new Map<string, ClaudeAliasDefinition>();
|
|
109
|
+
for (const alias of global ?? []) {
|
|
110
|
+
merged.set(alias.slug, alias);
|
|
111
|
+
}
|
|
112
|
+
for (const alias of project ?? []) {
|
|
113
|
+
merged.set(alias.slug, alias);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const aliases = [...merged.values()];
|
|
117
|
+
validateUniqueHandles(aliases);
|
|
118
|
+
return aliases;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function readOptionalAliasFile(
|
|
122
|
+
path: string,
|
|
123
|
+
readTextFile: (path: string) => Promise<string>,
|
|
124
|
+
): Promise<ClaudeAliasDefinition[] | undefined> {
|
|
125
|
+
let raw: string;
|
|
126
|
+
try {
|
|
127
|
+
raw = await readTextFile(path);
|
|
128
|
+
} catch (error: unknown) {
|
|
129
|
+
if (isNodeErrorCode(error, "ENOENT")) return undefined;
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
throw new FusionConfigError(
|
|
132
|
+
`Could not read Claude alias config at ${path}: ${message}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return parseAliasFile(raw, path);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseAliasFile(raw: string, source: string): ClaudeAliasDefinition[] {
|
|
140
|
+
let value: unknown;
|
|
141
|
+
try {
|
|
142
|
+
value = JSON.parse(raw);
|
|
143
|
+
} catch (error: unknown) {
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
throw new FusionConfigError(
|
|
146
|
+
`Invalid JSON in Claude alias config at ${source}: ${message}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!isRecord(value) || !Array.isArray((value as RawAliasConfig).aliases)) {
|
|
151
|
+
throw new FusionConfigError(
|
|
152
|
+
`Invalid Claude alias config at ${source}. Expected aliases array.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const aliasValues = (value as { aliases: unknown[] }).aliases;
|
|
157
|
+
const aliases: ClaudeAliasDefinition[] = [];
|
|
158
|
+
for (const [index, entry] of aliasValues.entries()) {
|
|
159
|
+
const parsed = parseAliasEntry(entry);
|
|
160
|
+
if (!parsed) {
|
|
161
|
+
throw new FusionConfigError(
|
|
162
|
+
`Invalid Claude alias entry at ${source} aliases[${index}].`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
aliases.push(parsed);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return dedupeAliases(aliases, source);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseAliasEntry(value: unknown): ClaudeAliasDefinition | undefined {
|
|
172
|
+
if (!isRecord(value)) return undefined;
|
|
173
|
+
const entry = value as RawAliasEntry;
|
|
174
|
+
|
|
175
|
+
const slug = normalizeSlug(entry.slug);
|
|
176
|
+
if (!slug) return undefined;
|
|
177
|
+
|
|
178
|
+
const handle = normalizeHandle(entry.handle) ?? `claude-${slug}`;
|
|
179
|
+
if (!handle) return undefined;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
slug,
|
|
183
|
+
providerId: `anthropic-${slug}`,
|
|
184
|
+
handle,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function dedupeAliases(
|
|
189
|
+
aliases: readonly ClaudeAliasDefinition[],
|
|
190
|
+
source: string,
|
|
191
|
+
): ClaudeAliasDefinition[] {
|
|
192
|
+
const byHandle = new Set<string>();
|
|
193
|
+
const deduped: ClaudeAliasDefinition[] = [];
|
|
194
|
+
|
|
195
|
+
for (const alias of aliases) {
|
|
196
|
+
if (byHandle.has(alias.handle)) {
|
|
197
|
+
throw new FusionConfigError(
|
|
198
|
+
`Duplicate Claude alias handle "${alias.handle}" in ${source}.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
byHandle.add(alias.handle);
|
|
202
|
+
deduped.push(alias);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return deduped;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function validateUniqueHandles(
|
|
209
|
+
aliases: readonly ClaudeAliasDefinition[],
|
|
210
|
+
): void {
|
|
211
|
+
const seen = new Map<string, string>();
|
|
212
|
+
|
|
213
|
+
for (const alias of aliases) {
|
|
214
|
+
const existingSlug = seen.get(alias.handle);
|
|
215
|
+
if (existingSlug && existingSlug !== alias.slug) {
|
|
216
|
+
throw new FusionConfigError(
|
|
217
|
+
`Duplicate Claude alias handle "${alias.handle}" across merged config. Use a unique handle for each alias.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
seen.set(alias.handle, alias.slug);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizeSlug(value: unknown): string | undefined {
|
|
225
|
+
if (!isNonEmptyString(value)) return undefined;
|
|
226
|
+
const slug = value
|
|
227
|
+
.trim()
|
|
228
|
+
.toLowerCase()
|
|
229
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
230
|
+
.replace(/^-+|-+$/g, "");
|
|
231
|
+
return slug || undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeHandle(value: unknown): string | undefined {
|
|
235
|
+
if (value === undefined) return undefined;
|
|
236
|
+
if (!isNonEmptyString(value)) return undefined;
|
|
237
|
+
const handle = value
|
|
238
|
+
.trim()
|
|
239
|
+
.toLowerCase()
|
|
240
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
241
|
+
.replace(/^-+|-+$/g, "");
|
|
242
|
+
return handle || undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getGlobalClaudeAliasConfigPath(agentDir = getAgentDir()): string {
|
|
246
|
+
return join(agentDir, CLAUDE_ALIAS_CONFIG_FILE);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function getProjectClaudeAliasConfigPath(cwd: string): string {
|
|
250
|
+
return join(cwd, CONFIG_DIR_NAME, CLAUDE_ALIAS_CONFIG_FILE);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function readUtf8File(path: string): Promise<string> {
|
|
254
|
+
return readFile(path, "utf8");
|
|
255
|
+
}
|
package/src/commands.ts
CHANGED
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
getProjectFusionConfigPath,
|
|
8
8
|
writeProjectFusionConfigTemplate,
|
|
9
9
|
} from "./config.js";
|
|
10
|
-
import {
|
|
10
|
+
import { FusionConfigError } from "./errors.js";
|
|
11
|
+
import { parseFusionInlineCommand } from "./fusion-args.js";
|
|
12
|
+
import type { ParsedFusionArgs } from "./types.js";
|
|
13
|
+
import { isNodeErrorCode } from "./utils.js";
|
|
11
14
|
|
|
12
|
-
const FUSION_USAGE =
|
|
13
|
-
"Usage: /fusion <prompt> | /fusion --profile <name> <prompt> | /fusion status | /fusion stop | /fusion init.";
|
|
14
15
|
const FUSION_HELP = [
|
|
15
16
|
"Fusion commands",
|
|
16
17
|
"/fusion <prompt>",
|
|
@@ -20,39 +21,11 @@ const FUSION_HELP = [
|
|
|
20
21
|
"/fusion init",
|
|
21
22
|
].join("\n");
|
|
22
23
|
|
|
23
|
-
export type FusionInlineCommand = "init" | "status" | "stop";
|
|
24
|
-
|
|
25
|
-
export interface ParsedFusionArgs {
|
|
26
|
-
prompt: string;
|
|
27
|
-
profile?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface FusionInitContext {
|
|
31
|
-
cwd: string;
|
|
32
|
-
hasUI: boolean;
|
|
33
|
-
isProjectTrusted(): boolean;
|
|
34
|
-
ui: {
|
|
35
|
-
confirm(title: string, message: string): Promise<boolean>;
|
|
36
|
-
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface FusionInitDeps {
|
|
41
|
-
readTextFile?: (path: string) => Promise<string>;
|
|
42
|
-
writeTextFile?: (path: string, content: string) => Promise<void>;
|
|
43
|
-
ensureDir?: (path: string) => Promise<void>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export type FusionInitResult =
|
|
47
|
-
| { status: "written"; path: string }
|
|
48
|
-
| {
|
|
49
|
-
status: "skipped";
|
|
50
|
-
reason: "untrusted" | "exists" | "cancelled";
|
|
51
|
-
path?: string;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
24
|
export interface FusionRuntimeCommandHandler {
|
|
55
|
-
startRun(
|
|
25
|
+
startRun(
|
|
26
|
+
args: string | ParsedFusionArgs,
|
|
27
|
+
ctx: ExtensionCommandContext,
|
|
28
|
+
): Promise<unknown>;
|
|
56
29
|
showStatus(ctx: ExtensionCommandContext): Promise<unknown>;
|
|
57
30
|
cancelActiveRun(ctx: ExtensionCommandContext): Promise<unknown>;
|
|
58
31
|
}
|
|
@@ -124,124 +97,29 @@ export async function runFusionInit(
|
|
|
124
97
|
return { status: "written", path: writtenPath };
|
|
125
98
|
}
|
|
126
99
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return command;
|
|
136
|
-
}
|
|
137
|
-
return undefined;
|
|
100
|
+
interface FusionInitContext {
|
|
101
|
+
cwd: string;
|
|
102
|
+
hasUI: boolean;
|
|
103
|
+
isProjectTrusted(): boolean;
|
|
104
|
+
ui: {
|
|
105
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
106
|
+
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
107
|
+
};
|
|
138
108
|
}
|
|
139
109
|
|
|
140
|
-
export
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
typeof input === "string" ? tokenizeCommandArgs(input) : [...input];
|
|
145
|
-
if (tokens[0] === "/fusion" || tokens[0] === "fusion") tokens.shift();
|
|
146
|
-
|
|
147
|
-
let profile: string | undefined;
|
|
148
|
-
const promptTokens: string[] = [];
|
|
149
|
-
|
|
150
|
-
for (let index = 0; index < tokens.length; index++) {
|
|
151
|
-
const token = tokens[index];
|
|
152
|
-
if (!token) continue;
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
promptTokens.length === 0 &&
|
|
156
|
-
(token === "--profile" || token === "-p")
|
|
157
|
-
) {
|
|
158
|
-
const value = tokens[index + 1];
|
|
159
|
-
if (!value || value.startsWith("-")) {
|
|
160
|
-
throw new FusionArgsError(
|
|
161
|
-
`Missing value for ${token}. ${FUSION_USAGE}`,
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
if (profile)
|
|
165
|
-
throw new FusionArgsError("Profile can only be provided once.");
|
|
166
|
-
profile = value;
|
|
167
|
-
index++;
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (promptTokens.length === 0 && token.startsWith("--profile=")) {
|
|
172
|
-
const value = token.slice("--profile=".length).trim();
|
|
173
|
-
if (!value)
|
|
174
|
-
throw new FusionArgsError(
|
|
175
|
-
`Missing value for --profile. ${FUSION_USAGE}`,
|
|
176
|
-
);
|
|
177
|
-
if (profile)
|
|
178
|
-
throw new FusionArgsError("Profile can only be provided once.");
|
|
179
|
-
profile = value;
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (promptTokens.length === 0 && token.startsWith("-")) {
|
|
184
|
-
throw new FusionArgsError(`Unknown option ${token}. ${FUSION_USAGE}`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
promptTokens.push(token, ...tokens.slice(index + 1));
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const prompt = promptTokens.join(" ").trim();
|
|
192
|
-
if (!prompt) throw new FusionArgsError(FUSION_USAGE);
|
|
193
|
-
return profile ? { prompt, profile } : { prompt };
|
|
110
|
+
export interface FusionInitDeps {
|
|
111
|
+
readTextFile?: (path: string) => Promise<string>;
|
|
112
|
+
writeTextFile?: (path: string, content: string) => Promise<void>;
|
|
113
|
+
ensureDir?: (path: string) => Promise<void>;
|
|
194
114
|
}
|
|
195
115
|
|
|
196
|
-
export
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (escaping) {
|
|
204
|
-
current += char;
|
|
205
|
-
escaping = false;
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (char === "\\") {
|
|
210
|
-
escaping = true;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (quote) {
|
|
215
|
-
if (char === quote) {
|
|
216
|
-
quote = undefined;
|
|
217
|
-
} else {
|
|
218
|
-
current += char;
|
|
219
|
-
}
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (char === "'" || char === '"') {
|
|
224
|
-
quote = char;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (/\s/.test(char)) {
|
|
229
|
-
if (current) {
|
|
230
|
-
tokens.push(current);
|
|
231
|
-
current = "";
|
|
232
|
-
}
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
current += char;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (escaping) current += "\\";
|
|
240
|
-
if (quote)
|
|
241
|
-
throw new FusionArgsError(`Unclosed ${quote} quote in /fusion arguments.`);
|
|
242
|
-
if (current) tokens.push(current);
|
|
243
|
-
return tokens;
|
|
244
|
-
}
|
|
116
|
+
export type FusionInitResult =
|
|
117
|
+
| { status: "written"; path: string }
|
|
118
|
+
| {
|
|
119
|
+
status: "skipped";
|
|
120
|
+
reason: "untrusted" | "exists" | "cancelled";
|
|
121
|
+
path?: string;
|
|
122
|
+
};
|
|
245
123
|
|
|
246
124
|
async function fileExists(
|
|
247
125
|
path: string,
|
|
@@ -259,15 +137,6 @@ async function fileExists(
|
|
|
259
137
|
}
|
|
260
138
|
}
|
|
261
139
|
|
|
262
|
-
function isNodeErrorCode(error: unknown, code: string): boolean {
|
|
263
|
-
return (
|
|
264
|
-
typeof error === "object" &&
|
|
265
|
-
error !== null &&
|
|
266
|
-
"code" in error &&
|
|
267
|
-
error.code === code
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
140
|
async function readUtf8File(path: string): Promise<string> {
|
|
272
141
|
return readFile(path, "utf8");
|
|
273
142
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { applyClaudeAliasShorthand } from "./claude-aliases.js";
|
|
4
5
|
import { FusionConfigError } from "./errors.js";
|
|
5
6
|
import {
|
|
6
7
|
THINKING_LEVELS,
|
|
@@ -11,6 +12,12 @@ import {
|
|
|
11
12
|
type PanelMemberConfig,
|
|
12
13
|
type ThinkingLevel,
|
|
13
14
|
} from "./types.js";
|
|
15
|
+
import {
|
|
16
|
+
isNodeErrorCode,
|
|
17
|
+
isNonEmptyString,
|
|
18
|
+
isPositiveInteger,
|
|
19
|
+
isRecord,
|
|
20
|
+
} from "./utils.js";
|
|
14
21
|
|
|
15
22
|
export const FUSION_CONFIG_FILE = "fusion.json";
|
|
16
23
|
export const DEFAULT_PROFILE_NAME = "quality";
|
|
@@ -98,12 +105,15 @@ export async function loadFusionConfig(
|
|
|
98
105
|
if (ctx.isProjectTrusted()) {
|
|
99
106
|
const projectPath = getProjectFusionConfigPath(ctx.cwd);
|
|
100
107
|
const projectConfig = await readOptionalConfig(projectPath, readTextFile);
|
|
101
|
-
if (projectConfig)
|
|
108
|
+
if (projectConfig) {
|
|
109
|
+
return applyClaudeAliasShorthand(projectConfig, ctx, deps);
|
|
110
|
+
}
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
const globalPath = getGlobalFusionConfigPath(deps.agentDir);
|
|
105
114
|
const globalConfig = await readOptionalConfig(globalPath, readTextFile);
|
|
106
|
-
|
|
115
|
+
const config = globalConfig ?? createDefaultFusionConfig();
|
|
116
|
+
return applyClaudeAliasShorthand(config, ctx, deps);
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
export function resolveProfile(
|
|
@@ -227,22 +237,6 @@ function isFusionContextMode(value: unknown): value is FusionContextMode {
|
|
|
227
237
|
return value === "fresh" || value === "fork";
|
|
228
238
|
}
|
|
229
239
|
|
|
230
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
231
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function isNonEmptyString(value: unknown): value is string {
|
|
235
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function isPositiveInteger(value: unknown): value is number {
|
|
239
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function isNodeErrorCode(error: unknown, code: string): boolean {
|
|
243
|
-
return isRecord(error) && error.code === code;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
240
|
async function readUtf8File(path: string): Promise<string> {
|
|
247
241
|
return readFile(path, "utf8");
|
|
248
242
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { FusionArgsError } from "./errors.js";
|
|
2
|
+
import type { ParsedFusionArgs } from "./types.js";
|
|
3
|
+
|
|
4
|
+
const FUSION_USAGE =
|
|
5
|
+
"Usage: /fusion <prompt> | /fusion --profile <name> <prompt> | /fusion status | /fusion stop | /fusion init.";
|
|
6
|
+
|
|
7
|
+
export type FusionInlineCommand = "init" | "status" | "stop";
|
|
8
|
+
|
|
9
|
+
export function parseFusionInlineCommand(
|
|
10
|
+
input: string | readonly string[],
|
|
11
|
+
): FusionInlineCommand | undefined {
|
|
12
|
+
const tokens =
|
|
13
|
+
typeof input === "string" ? tokenizeCommandArgs(input) : [...input];
|
|
14
|
+
if (tokens.length !== 1) return undefined;
|
|
15
|
+
const command = tokens[0];
|
|
16
|
+
if (command === "init" || command === "status" || command === "stop") {
|
|
17
|
+
return command;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseFusionArgs(
|
|
23
|
+
input: string | readonly string[],
|
|
24
|
+
): ParsedFusionArgs {
|
|
25
|
+
const tokens =
|
|
26
|
+
typeof input === "string" ? tokenizeCommandArgs(input) : [...input];
|
|
27
|
+
if (tokens[0] === "/fusion" || tokens[0] === "fusion") tokens.shift();
|
|
28
|
+
|
|
29
|
+
let profile: string | undefined;
|
|
30
|
+
const promptTokens: string[] = [];
|
|
31
|
+
|
|
32
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
33
|
+
const token = tokens[index];
|
|
34
|
+
if (!token) continue;
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
promptTokens.length === 0 &&
|
|
38
|
+
(token === "--profile" || token === "-p")
|
|
39
|
+
) {
|
|
40
|
+
const value = tokens[index + 1];
|
|
41
|
+
if (!value || value.startsWith("-")) {
|
|
42
|
+
throw new FusionArgsError(
|
|
43
|
+
`Missing value for ${token}. ${FUSION_USAGE}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (profile)
|
|
47
|
+
throw new FusionArgsError("Profile can only be provided once.");
|
|
48
|
+
profile = value;
|
|
49
|
+
index++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (promptTokens.length === 0 && token.startsWith("--profile=")) {
|
|
54
|
+
const value = token.slice("--profile=".length).trim();
|
|
55
|
+
if (!value)
|
|
56
|
+
throw new FusionArgsError(
|
|
57
|
+
`Missing value for --profile. ${FUSION_USAGE}`,
|
|
58
|
+
);
|
|
59
|
+
if (profile)
|
|
60
|
+
throw new FusionArgsError("Profile can only be provided once.");
|
|
61
|
+
profile = value;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (promptTokens.length === 0 && token.startsWith("-")) {
|
|
66
|
+
throw new FusionArgsError(`Unknown option ${token}. ${FUSION_USAGE}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
promptTokens.push(token, ...tokens.slice(index + 1));
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const prompt = promptTokens.join(" ").trim();
|
|
74
|
+
if (!prompt) throw new FusionArgsError(FUSION_USAGE);
|
|
75
|
+
return profile ? { prompt, profile } : { prompt };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function tokenizeCommandArgs(input: string): string[] {
|
|
79
|
+
const tokens: string[] = [];
|
|
80
|
+
let current = "";
|
|
81
|
+
let quote: "'" | '"' | undefined;
|
|
82
|
+
let escaping = false;
|
|
83
|
+
|
|
84
|
+
for (const char of input.trim()) {
|
|
85
|
+
if (escaping) {
|
|
86
|
+
current += char;
|
|
87
|
+
escaping = false;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (char === "\\") {
|
|
92
|
+
escaping = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (quote) {
|
|
97
|
+
if (char === quote) {
|
|
98
|
+
quote = undefined;
|
|
99
|
+
} else {
|
|
100
|
+
current += char;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (char === "'" || char === '"') {
|
|
106
|
+
quote = char;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (/\s/.test(char)) {
|
|
111
|
+
if (current) {
|
|
112
|
+
tokens.push(current);
|
|
113
|
+
current = "";
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
current += char;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (escaping) current += "\\";
|
|
122
|
+
if (quote)
|
|
123
|
+
throw new FusionArgsError(`Unclosed ${quote} quote in /fusion arguments.`);
|
|
124
|
+
if (current) tokens.push(current);
|
|
125
|
+
return tokens;
|
|
126
|
+
}
|
package/src/orchestrator.ts
CHANGED
|
@@ -4,18 +4,17 @@ import {
|
|
|
4
4
|
type ResolvedFusionProfile,
|
|
5
5
|
} from "./config.js";
|
|
6
6
|
import { FusionArgsError } from "./errors.js";
|
|
7
|
+
import { parseFusionArgs } from "./fusion-args.js";
|
|
8
|
+
import { decidePanelCompletion } from "./panel-completion.js";
|
|
7
9
|
import {
|
|
8
10
|
renderCancelledReport,
|
|
9
11
|
renderFailureReport,
|
|
10
12
|
renderJudgeReport,
|
|
11
|
-
renderPanelFailureReport,
|
|
12
|
-
renderSinglePanelReport,
|
|
13
13
|
} from "./report.js";
|
|
14
14
|
import { extractPanelResults } from "./result-extract.js";
|
|
15
15
|
import {
|
|
16
16
|
appendThinkingSuffix,
|
|
17
17
|
buildFusionChainSpawnParams,
|
|
18
|
-
buildJudgeSpawnParams,
|
|
19
18
|
} from "./run-builder.js";
|
|
20
19
|
import { FusionRunStore, FusionRunStoreError } from "./run-store.js";
|
|
21
20
|
import {
|
|
@@ -35,8 +34,8 @@ import type {
|
|
|
35
34
|
FusionProfile,
|
|
36
35
|
FusionRun,
|
|
37
36
|
PanelOutput,
|
|
37
|
+
ParsedFusionArgs,
|
|
38
38
|
} from "./types.js";
|
|
39
|
-
import { parseFusionArgs, type ParsedFusionArgs } from "./commands.js";
|
|
40
39
|
import type { SubagentsTargetParams } from "./subagents-rpc.js";
|
|
41
40
|
|
|
42
41
|
export const SUBAGENT_ASYNC_COMPLETE_EVENT = "subagent:async-complete";
|
|
@@ -124,15 +123,7 @@ export class FusionOrchestrator {
|
|
|
124
123
|
): Promise<FusionCommandResult> {
|
|
125
124
|
this.context = ctx;
|
|
126
125
|
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
args = typeof input === "string" ? parseFusionArgs(input) : input;
|
|
130
|
-
} catch (error: unknown) {
|
|
131
|
-
const message = errorMessage(error);
|
|
132
|
-
this.notify(ctx, message, "error");
|
|
133
|
-
return { status: "failed", error: message };
|
|
134
|
-
}
|
|
135
|
-
|
|
126
|
+
const args = typeof input === "string" ? parseFusionArgs(input) : input;
|
|
136
127
|
const existing = this.runStore.getActiveRun();
|
|
137
128
|
if (existing) {
|
|
138
129
|
const message = `Fusion run ${existing.id} is already active.`;
|
|
@@ -475,61 +466,13 @@ export class FusionOrchestrator {
|
|
|
475
466
|
return this.completeActiveRun(report);
|
|
476
467
|
}
|
|
477
468
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
"No fusion panelists completed successfully.",
|
|
486
|
-
report,
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (extracted.outputs.length === 1) {
|
|
491
|
-
const report = renderSinglePanelReport({
|
|
492
|
-
run: updated,
|
|
493
|
-
output: extracted.outputs[0]!,
|
|
494
|
-
failures: extracted.failures,
|
|
495
|
-
...withJudgeModel(configuredJudgeModel(profile)),
|
|
496
|
-
});
|
|
497
|
-
return this.completeActiveRun(report);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
try {
|
|
501
|
-
const spawnResult = await this.rpc.spawn(
|
|
502
|
-
buildJudgeSpawnParams({
|
|
503
|
-
profile,
|
|
504
|
-
prompt: active.prompt,
|
|
505
|
-
panelOutputs: extracted.outputs,
|
|
506
|
-
failedPanelists: extracted.failures,
|
|
507
|
-
}),
|
|
508
|
-
);
|
|
509
|
-
const judgeRunId = extractSubagentRunId(spawnResult);
|
|
510
|
-
if (!judgeRunId) {
|
|
511
|
-
throw new FusionArgsError(
|
|
512
|
-
"pi-subagents spawn did not return a fallback judge run ID.",
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
const judgeAsyncDir = extractSubagentAsyncDir(spawnResult);
|
|
516
|
-
const nextRun = this.runStore.updateRun(active.id, {
|
|
517
|
-
phase: "judge",
|
|
518
|
-
judgeRunId,
|
|
519
|
-
...(judgeAsyncDir ? { judgeAsyncDir } : {}),
|
|
520
|
-
panelOutputs: extracted.outputs,
|
|
521
|
-
panelFailures: extracted.failures,
|
|
522
|
-
});
|
|
523
|
-
publishFusionStatus(this.context, nextRun);
|
|
524
|
-
this.notify(
|
|
525
|
-
this.context,
|
|
526
|
-
`Fusion fallback judge started: ${judgeRunId}`,
|
|
527
|
-
"info",
|
|
528
|
-
);
|
|
529
|
-
return { status: "started", run: nextRun };
|
|
530
|
-
} catch (error: unknown) {
|
|
531
|
-
return this.failActiveRun(errorMessage(error));
|
|
532
|
-
}
|
|
469
|
+
return this.finishPanelCompletion(
|
|
470
|
+
updated,
|
|
471
|
+
profile,
|
|
472
|
+
extracted.outputs,
|
|
473
|
+
extracted.failures,
|
|
474
|
+
{ fallbackJudge: true },
|
|
475
|
+
);
|
|
533
476
|
}
|
|
534
477
|
|
|
535
478
|
private async handleLegacyPanelComplete(
|
|
@@ -576,53 +519,57 @@ export class FusionOrchestrator {
|
|
|
576
519
|
extracted.failures,
|
|
577
520
|
);
|
|
578
521
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
report,
|
|
588
|
-
);
|
|
589
|
-
}
|
|
522
|
+
return this.finishPanelCompletion(
|
|
523
|
+
updated,
|
|
524
|
+
profile,
|
|
525
|
+
extracted.outputs,
|
|
526
|
+
extracted.failures,
|
|
527
|
+
{ fallbackJudge: false },
|
|
528
|
+
);
|
|
529
|
+
}
|
|
590
530
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
531
|
+
private async finishPanelCompletion(
|
|
532
|
+
run: FusionRun,
|
|
533
|
+
profile: FusionProfile,
|
|
534
|
+
panelOutputs: readonly PanelOutput[],
|
|
535
|
+
panelFailures: readonly FailedPanelSummary[],
|
|
536
|
+
options: { fallbackJudge: boolean },
|
|
537
|
+
): Promise<FusionCommandResult> {
|
|
538
|
+
const decision = decidePanelCompletion({
|
|
539
|
+
run,
|
|
540
|
+
profile,
|
|
541
|
+
panelOutputs,
|
|
542
|
+
panelFailures,
|
|
543
|
+
fallbackJudge: options.fallbackJudge,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
if (decision.kind === "fail") {
|
|
547
|
+
return this.failActiveRun(decision.error, decision.report);
|
|
548
|
+
}
|
|
549
|
+
if (decision.kind === "complete") {
|
|
550
|
+
return this.completeActiveRun(decision.report);
|
|
599
551
|
}
|
|
600
552
|
|
|
601
553
|
try {
|
|
602
|
-
const spawnResult = await this.rpc.spawn(
|
|
603
|
-
buildJudgeSpawnParams({
|
|
604
|
-
profile,
|
|
605
|
-
prompt: active.prompt,
|
|
606
|
-
panelOutputs: extracted.outputs,
|
|
607
|
-
failedPanelists: extracted.failures,
|
|
608
|
-
}),
|
|
609
|
-
);
|
|
554
|
+
const spawnResult = await this.rpc.spawn(decision.params);
|
|
610
555
|
const judgeRunId = extractSubagentRunId(spawnResult);
|
|
611
556
|
if (!judgeRunId) {
|
|
612
|
-
throw new FusionArgsError(
|
|
613
|
-
"pi-subagents spawn did not return a judge run ID.",
|
|
614
|
-
);
|
|
557
|
+
throw new FusionArgsError(decision.missingRunIdError);
|
|
615
558
|
}
|
|
616
559
|
const judgeAsyncDir = extractSubagentAsyncDir(spawnResult);
|
|
617
|
-
const nextRun = this.runStore.updateRun(
|
|
560
|
+
const nextRun = this.runStore.updateRun(run.id, {
|
|
618
561
|
phase: "judge",
|
|
619
562
|
judgeRunId,
|
|
620
563
|
...(judgeAsyncDir ? { judgeAsyncDir } : {}),
|
|
621
|
-
panelOutputs:
|
|
622
|
-
panelFailures:
|
|
564
|
+
panelOutputs: [...panelOutputs],
|
|
565
|
+
panelFailures: [...panelFailures],
|
|
623
566
|
});
|
|
624
567
|
publishFusionStatus(this.context, nextRun);
|
|
625
|
-
this.notify(
|
|
568
|
+
this.notify(
|
|
569
|
+
this.context,
|
|
570
|
+
`${decision.notification}: ${judgeRunId}`,
|
|
571
|
+
"info",
|
|
572
|
+
);
|
|
626
573
|
return { status: "started", run: nextRun };
|
|
627
574
|
} catch (error: unknown) {
|
|
628
575
|
return this.failActiveRun(errorMessage(error));
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { renderPanelFailureReport, renderSinglePanelReport } from "./report.js";
|
|
2
|
+
import {
|
|
3
|
+
appendThinkingSuffix,
|
|
4
|
+
buildJudgeSpawnParams,
|
|
5
|
+
type JudgeSpawnParams,
|
|
6
|
+
} from "./run-builder.js";
|
|
7
|
+
import type {
|
|
8
|
+
FailedPanelSummary,
|
|
9
|
+
FusionProfile,
|
|
10
|
+
FusionRun,
|
|
11
|
+
PanelOutput,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
export type PanelCompletionDecision =
|
|
15
|
+
| { kind: "fail"; error: string; report: string }
|
|
16
|
+
| { kind: "complete"; report: string }
|
|
17
|
+
| {
|
|
18
|
+
kind: "judge";
|
|
19
|
+
params: JudgeSpawnParams;
|
|
20
|
+
missingRunIdError: string;
|
|
21
|
+
notification: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface DecidePanelCompletionInput {
|
|
25
|
+
run: FusionRun;
|
|
26
|
+
profile: FusionProfile;
|
|
27
|
+
panelOutputs: readonly PanelOutput[];
|
|
28
|
+
panelFailures: readonly FailedPanelSummary[];
|
|
29
|
+
fallbackJudge?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function decidePanelCompletion(
|
|
33
|
+
input: DecidePanelCompletionInput,
|
|
34
|
+
): PanelCompletionDecision {
|
|
35
|
+
const judgeModel = configuredJudgeModel(input.profile);
|
|
36
|
+
|
|
37
|
+
if (input.panelOutputs.length === 0) {
|
|
38
|
+
const report = renderPanelFailureReport({
|
|
39
|
+
run: input.run,
|
|
40
|
+
failures: input.panelFailures,
|
|
41
|
+
...withJudgeModel(judgeModel),
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
kind: "fail",
|
|
45
|
+
error: "No fusion panelists completed successfully.",
|
|
46
|
+
report,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (input.panelOutputs.length === 1) {
|
|
51
|
+
const report = renderSinglePanelReport({
|
|
52
|
+
run: input.run,
|
|
53
|
+
output: input.panelOutputs[0]!,
|
|
54
|
+
failures: input.panelFailures,
|
|
55
|
+
...withJudgeModel(judgeModel),
|
|
56
|
+
});
|
|
57
|
+
return { kind: "complete", report };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
kind: "judge",
|
|
62
|
+
params: buildJudgeSpawnParams({
|
|
63
|
+
profile: input.profile,
|
|
64
|
+
prompt: input.run.prompt,
|
|
65
|
+
panelOutputs: input.panelOutputs,
|
|
66
|
+
failedPanelists: input.panelFailures,
|
|
67
|
+
}),
|
|
68
|
+
missingRunIdError: input.fallbackJudge
|
|
69
|
+
? "pi-subagents spawn did not return a fallback judge run ID."
|
|
70
|
+
: "pi-subagents spawn did not return a judge run ID.",
|
|
71
|
+
notification: input.fallbackJudge
|
|
72
|
+
? "Fusion fallback judge started"
|
|
73
|
+
: "Fusion judge started",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function configuredJudgeModel(profile: FusionProfile): string | undefined {
|
|
78
|
+
return appendThinkingSuffix(profile.judge.model, profile.judge.thinking);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function withJudgeModel(
|
|
82
|
+
judgeModel: string | undefined,
|
|
83
|
+
): { judgeModel: string } | Record<string, never> {
|
|
84
|
+
return judgeModel ? { judgeModel } : {};
|
|
85
|
+
}
|
package/src/run-store.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { FusionPhase, FusionRun } from "./types.js";
|
|
3
|
+
import { isFiniteNumber, isNonEmptyString, isRecord } from "./utils.js";
|
|
3
4
|
|
|
4
5
|
export const FUSION_RUN_ENTRY_TYPE = "fusion-run";
|
|
5
6
|
|
|
@@ -423,18 +424,6 @@ function isTerminalPhase(value: unknown): value is FusionTerminalPhase {
|
|
|
423
424
|
return value === "done" || value === "failed" || value === "cancelled";
|
|
424
425
|
}
|
|
425
426
|
|
|
426
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
427
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function isNonEmptyString(value: unknown): value is string {
|
|
431
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function isFiniteNumber(value: unknown): value is number {
|
|
435
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
427
|
function isPanelOutputArray(
|
|
439
428
|
value: unknown,
|
|
440
429
|
): value is NonNullable<FusionRun["panelOutputs"]> {
|
package/src/types.ts
CHANGED
package/src/utils.ts
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isNonEmptyString(value: unknown): value is string {
|
|
6
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isPositiveInteger(value: unknown): value is number {
|
|
10
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isFiniteNumber(value: unknown): value is number {
|
|
14
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isNodeErrorCode(error: unknown, code: string): boolean {
|
|
18
|
+
return isRecord(error) && error.code === code;
|
|
19
|
+
}
|