@alexeiled/pi-fusion 0.2.0 → 0.2.2
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/README.md +95 -48
- package/docs/user-guide.md +6 -4
- package/package.json +5 -8
- package/src/commands.ts +27 -158
- package/src/config.ts +6 -16
- 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/AGENTS.md +0 -7
- package/tsconfig.json +0 -19
package/README.md
CHANGED
|
@@ -1,75 +1,121 @@
|
|
|
1
1
|
# pi-fusion
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@alexeiled/pi-fusion)
|
|
4
|
+
[](https://github.com/alexei-led/pi-fusion/actions/workflows/test.yml?query=branch%3Amaster)
|
|
4
5
|
[](https://nodejs.org/)
|
|
5
6
|
[](./LICENSE)
|
|
6
7
|
|
|
7
|
-
> Parallel
|
|
8
|
+
> Parallel models. One judge. Better answers.
|
|
8
9
|
|
|
9
|
-
`pi-fusion` is a Pi extension for
|
|
10
|
-
It
|
|
11
|
-
to
|
|
10
|
+
`pi-fusion` is a Pi extension for hard technical questions.
|
|
11
|
+
It uses `pi-subagents` to send the same prompt through a small parallel model panel,
|
|
12
|
+
then asks a judge agent to compare the outputs and return the best realistic answer.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
CI covers lint, typecheck, unit tests, integration tests, package smoke tests,
|
|
15
|
+
and `npm pack --dry-run`.
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+

|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
- panelists work independently in parallel
|
|
19
|
-
- the judge reconciles evidence, not votes
|
|
20
|
-
- if the judge result is missing but the panel still produced enough signal,
|
|
21
|
-
Fusion retries only the judge step
|
|
19
|
+
## Why Fusion exists
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
Hard questions are often bottlenecked by one model's search path.
|
|
22
|
+
`pi-fusion` trades latency for diversity:
|
|
23
|
+
|
|
24
|
+
- the same prompt fans out to several model runs in parallel
|
|
25
|
+
- each model explores the problem from a different training prior and reasoning path
|
|
26
|
+
- overlap raises confidence
|
|
27
|
+
- disagreement exposes risk
|
|
28
|
+
- the judge keeps the strongest parts and drops weak, partial, or conflicting ones
|
|
29
|
+
|
|
30
|
+
This is evidence selection, not majority vote.
|
|
25
31
|
|
|
26
32
|
```mermaid
|
|
33
|
+
%%{init: {"theme": "base", "flowchart": {"curve": "basis", "nodeSpacing": 28, "rankSpacing": 48}, "themeVariables": {"background": "#050816", "fontFamily": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", "primaryTextColor": "#E5F0FF", "lineColor": "#38bdf8", "tertiaryColor": "#0b1220"}}}%%
|
|
27
34
|
flowchart LR
|
|
28
|
-
classDef
|
|
29
|
-
classDef
|
|
30
|
-
classDef
|
|
31
|
-
classDef
|
|
32
|
-
classDef
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
classDef prompt fill:#071321,stroke:#38bdf8,color:#dbeafe,stroke-width:2px;
|
|
36
|
+
classDef modelA fill:#081223,stroke:#38bdf8,color:#dbeafe,stroke-width:2px;
|
|
37
|
+
classDef modelB fill:#120826,stroke:#a855f7,color:#f3e8ff,stroke-width:2px;
|
|
38
|
+
classDef modelC fill:#08180f,stroke:#22c55e,color:#dcfce7,stroke-width:2px;
|
|
39
|
+
classDef modelD fill:#241307,stroke:#f59e0b,color:#fef3c7,stroke-width:2px;
|
|
40
|
+
classDef modelE fill:#260712,stroke:#ff4d8d,color:#ffd1e7,stroke-width:2px;
|
|
41
|
+
classDef judge fill:#2b1905,stroke:#f59e0b,color:#fef3c7,stroke-width:3px;
|
|
42
|
+
classDef answer fill:#062814,stroke:#22c55e,color:#dcfce7,stroke-width:3px;
|
|
43
|
+
classDef note fill:#0b1220,stroke:#475569,color:#cbd5e1,stroke-width:1px;
|
|
44
|
+
|
|
45
|
+
P["input prompt<br/>same question to every model"]:::prompt
|
|
46
|
+
|
|
47
|
+
subgraph PANEL[parallel model panel]
|
|
48
|
+
direction TB
|
|
49
|
+
A["model A<br/>strong baseline candidate"]:::modelA
|
|
50
|
+
B["model B<br/>finds contradiction"]:::modelB
|
|
51
|
+
C["model C<br/>adds unique insight"]:::modelC
|
|
52
|
+
D["model D<br/>fast practical path"]:::modelD
|
|
53
|
+
E["model E<br/>catches edge case"]:::modelE
|
|
41
54
|
end
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
J["judge<br/>consensus • contradictions • blind spots"]:::judge
|
|
57
|
+
R["best answer<br/>selected or synthesized"]:::answer
|
|
58
|
+
N["not majority vote<br/>best evidence wins"]:::note
|
|
59
|
+
|
|
60
|
+
P --> A
|
|
61
|
+
P --> B
|
|
62
|
+
P --> C
|
|
63
|
+
P --> D
|
|
64
|
+
P --> E
|
|
65
|
+
|
|
66
|
+
A --> J
|
|
67
|
+
B --> J
|
|
68
|
+
C --> J
|
|
69
|
+
D --> J
|
|
70
|
+
E --> J
|
|
71
|
+
|
|
72
|
+
J --> R
|
|
73
|
+
J -.-> N
|
|
74
|
+
|
|
75
|
+
linkStyle 0,5 stroke:#38bdf8,stroke-width:3px;
|
|
76
|
+
linkStyle 1,6 stroke:#a855f7,stroke-width:3px;
|
|
77
|
+
linkStyle 2,7 stroke:#22c55e,stroke-width:3px;
|
|
78
|
+
linkStyle 3,8 stroke:#f59e0b,stroke-width:3px;
|
|
79
|
+
linkStyle 4,9 stroke:#ff4d8d,stroke-width:3px;
|
|
80
|
+
linkStyle 10 stroke:#22c55e,stroke-width:4px;
|
|
81
|
+
linkStyle 11 stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5;
|
|
52
82
|
```
|
|
53
83
|
|
|
54
|
-
##
|
|
84
|
+
## Why a panel can beat one model
|
|
85
|
+
|
|
86
|
+
Single-model answers are brittle on hard tasks. They are limited by one model's
|
|
87
|
+
priors, one reasoning path, and one failure mode.
|
|
88
|
+
|
|
89
|
+
A panel helps because:
|
|
90
|
+
|
|
91
|
+
- different models are trained differently and make different bets
|
|
92
|
+
- errors are less correlated, so blind spots do not line up perfectly
|
|
93
|
+
- consensus is a useful confidence signal without pretending certainty
|
|
94
|
+
- disagreement tells you where the answer is fragile
|
|
95
|
+
- a judge can select or synthesize the best realistic answer from the set
|
|
96
|
+
|
|
97
|
+
The result is slower, but usually better for design choices, risk review,
|
|
98
|
+
tricky debugging, and research-heavy questions.
|
|
99
|
+
|
|
100
|
+
## What the judge actually does
|
|
55
101
|
|
|
56
102
|
The judge gets:
|
|
57
103
|
|
|
58
104
|
- the original prompt
|
|
59
|
-
-
|
|
60
|
-
-
|
|
105
|
+
- every panel output
|
|
106
|
+
- panel failures and blind spots
|
|
61
107
|
- the configured judge model
|
|
62
108
|
|
|
63
|
-
It
|
|
109
|
+
It then:
|
|
64
110
|
|
|
65
|
-
- consensus
|
|
66
|
-
- disagreements
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
- next step
|
|
111
|
+
- finds consensus
|
|
112
|
+
- preserves real disagreements
|
|
113
|
+
- spots weak or incomplete answers
|
|
114
|
+
- pulls forward unique insights worth keeping
|
|
115
|
+
- returns one clear recommendation and next step
|
|
70
116
|
|
|
71
|
-
It does not edit files or spawn more subagents. It does one job:
|
|
72
|
-
|
|
117
|
+
It does not edit files or spawn more subagents. It does one job: choose or
|
|
118
|
+
synthesize the best realistic answer.
|
|
73
119
|
|
|
74
120
|
## Good fit
|
|
75
121
|
|
|
@@ -89,6 +135,7 @@ Do not use it for trivial edits, formatting, or obvious one-step fixes.
|
|
|
89
135
|
/fusion
|
|
90
136
|
/fusion <prompt>
|
|
91
137
|
/fusion --profile <name> <prompt>
|
|
138
|
+
/fusion -p <name> <prompt>
|
|
92
139
|
/fusion status
|
|
93
140
|
/fusion stop
|
|
94
141
|
/fusion init
|
|
@@ -113,7 +160,7 @@ Then reload Pi:
|
|
|
113
160
|
/reload
|
|
114
161
|
```
|
|
115
162
|
|
|
116
|
-
For
|
|
163
|
+
For commands, config, and troubleshooting details, see [`docs/user-guide.md`](./docs/user-guide.md).
|
|
117
164
|
|
|
118
165
|
## Notes
|
|
119
166
|
|
package/docs/user-guide.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# pi-fusion user guide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
README covers the why. This guide covers commands, config, and troubleshooting.
|
|
4
4
|
|
|
5
5
|
## Mental model
|
|
6
6
|
|
|
7
|
-
`pi-fusion` turns one hard question into a small
|
|
7
|
+
`pi-fusion` turns one hard question into a small parallel panel:
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
10
|
prompt → parallel panel → judge synthesis → final report
|
|
@@ -12,6 +12,8 @@ prompt → parallel panel → judge synthesis → final report
|
|
|
12
12
|
|
|
13
13
|
Normal execution is a single `pi-subagents` async chain. If that chain completes without a judge result but at least two panelists still produced usable answers, `pi-fusion` runs one fallback judge pass instead of losing the review.
|
|
14
14
|
|
|
15
|
+
Panel diversity can come from different model choices, different perspective prompts, or both. In practice, mixing models is usually the main lever.
|
|
16
|
+
|
|
15
17
|
The base Pi session stays in control. Fusion is a tool for decisions, not a replacement for normal coding.
|
|
16
18
|
|
|
17
19
|
## Commands
|
|
@@ -108,9 +110,9 @@ Panel member:
|
|
|
108
110
|
- `id`: stable machine name
|
|
109
111
|
- `label`: human-readable report label
|
|
110
112
|
- `agent`: subagent name
|
|
111
|
-
- `model`: optional model override
|
|
113
|
+
- `model`: optional model override; often the main source of panel diversity
|
|
112
114
|
- `thinking`: optional `off`, `minimal`, `low`, `medium`, `high`, or `xhigh`
|
|
113
|
-
- `role`:
|
|
115
|
+
- `role`: optional perspective hint layered on top of the model
|
|
114
116
|
|
|
115
117
|
Judge:
|
|
116
118
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexeiled/pi-fusion",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Stronger answers for hard Pi questions via a parallel model panel + judge, built on pi-subagents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -22,11 +22,7 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"agents/*.md",
|
|
24
24
|
"src/*.ts",
|
|
25
|
-
"
|
|
26
|
-
"LICENSE",
|
|
27
|
-
"AGENTS.md",
|
|
28
|
-
"docs/user-guide.md",
|
|
29
|
-
"tsconfig.json"
|
|
25
|
+
"docs/user-guide.md"
|
|
30
26
|
],
|
|
31
27
|
"scripts": {
|
|
32
28
|
"check": "tsc --noEmit",
|
|
@@ -47,7 +43,8 @@
|
|
|
47
43
|
"agents": [
|
|
48
44
|
"./agents"
|
|
49
45
|
]
|
|
50
|
-
}
|
|
46
|
+
},
|
|
47
|
+
"image": "https://raw.githubusercontent.com/alexei-led/pi-fusion/master/docs/assets/fusion-icon.png"
|
|
51
48
|
},
|
|
52
49
|
"peerDependencies": {
|
|
53
50
|
"@earendil-works/pi-coding-agent": "*",
|
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
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
type PanelMemberConfig,
|
|
12
12
|
type ThinkingLevel,
|
|
13
13
|
} from "./types.js";
|
|
14
|
+
import {
|
|
15
|
+
isNodeErrorCode,
|
|
16
|
+
isNonEmptyString,
|
|
17
|
+
isPositiveInteger,
|
|
18
|
+
isRecord,
|
|
19
|
+
} from "./utils.js";
|
|
14
20
|
|
|
15
21
|
export const FUSION_CONFIG_FILE = "fusion.json";
|
|
16
22
|
export const DEFAULT_PROFILE_NAME = "quality";
|
|
@@ -227,22 +233,6 @@ function isFusionContextMode(value: unknown): value is FusionContextMode {
|
|
|
227
233
|
return value === "fresh" || value === "fork";
|
|
228
234
|
}
|
|
229
235
|
|
|
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
236
|
async function readUtf8File(path: string): Promise<string> {
|
|
247
237
|
return readFile(path, "utf8");
|
|
248
238
|
}
|
|
@@ -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
|
+
}
|
package/AGENTS.md
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# Contributor notes
|
|
2
|
-
|
|
3
|
-
- Keep the extension small, typed, and testable.
|
|
4
|
-
- Do not own or replace the Pi footer. Publish only the `fusion` status key.
|
|
5
|
-
- Use `pi-subagents` RPC for panel and judge execution. Do not import its internals.
|
|
6
|
-
- Keep bundled panel agents read-only by default.
|
|
7
|
-
- Do not add runtime dependencies unless the task needs them.
|
package/tsconfig.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2023",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"noUncheckedIndexedAccess": true,
|
|
8
|
-
"exactOptionalPropertyTypes": true,
|
|
9
|
-
"useUnknownInCatchVariables": true,
|
|
10
|
-
"noImplicitOverride": true,
|
|
11
|
-
"noImplicitReturns": true,
|
|
12
|
-
"noFallthroughCasesInSwitch": true,
|
|
13
|
-
"skipLibCheck": true
|
|
14
|
-
},
|
|
15
|
-
"include": [
|
|
16
|
-
"src/**/*.ts",
|
|
17
|
-
"test/**/*.ts"
|
|
18
|
-
]
|
|
19
|
-
}
|