@alexeiled/pi-fusion 0.5.1 → 0.6.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/README.md +22 -9
- package/agents/fusion-composer.md +49 -0
- package/agents/fusion-judge.md +7 -0
- package/agents/fusion-panelist-full.md +53 -0
- package/agents/fusion-panelist-web.md +41 -0
- package/agents/fusion-panelist.md +2 -0
- package/docs/user-guide.md +200 -19
- package/package.json +1 -1
- package/skills/fusion-review/SKILL.md +69 -18
- package/src/commands.ts +1 -0
- package/src/config.ts +167 -5
- package/src/fusion-args.ts +36 -2
- package/src/index.ts +11 -2
- package/src/orchestrator.ts +158 -44
- package/src/panel-completion.ts +17 -6
- package/src/report.ts +233 -78
- package/src/run-builder.ts +242 -26
- package/src/run-store.ts +31 -0
- package/src/status.ts +31 -0
- package/src/types.ts +89 -1
package/src/orchestrator.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { applyClaudeAliasShorthand } from "./claude-aliases.js";
|
|
1
2
|
import {
|
|
3
|
+
buildInlinePanelProfile,
|
|
2
4
|
loadFusionConfig,
|
|
3
5
|
resolveProfile as resolveFusionProfile,
|
|
4
6
|
type ResolvedFusionProfile,
|
|
@@ -21,6 +23,7 @@ import {
|
|
|
21
23
|
clearFusionUi,
|
|
22
24
|
extractFusionProgressCounts,
|
|
23
25
|
formatProgressCounts,
|
|
26
|
+
isTerminalFusionProgress,
|
|
24
27
|
publishFusionStatus,
|
|
25
28
|
type FusionProgressCounts,
|
|
26
29
|
type FusionUi,
|
|
@@ -29,12 +32,14 @@ import {
|
|
|
29
32
|
readSubagentResultArtifact,
|
|
30
33
|
readSubagentStatusArtifact,
|
|
31
34
|
} from "./subagent-artifacts.js";
|
|
32
|
-
import
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
import {
|
|
36
|
+
memberLabel,
|
|
37
|
+
resolveSynthesisMode,
|
|
38
|
+
type FailedPanelSummary,
|
|
39
|
+
type FusionProfile,
|
|
40
|
+
type FusionRun,
|
|
41
|
+
type PanelOutput,
|
|
42
|
+
type ParsedFusionArgs,
|
|
38
43
|
} from "./types.js";
|
|
39
44
|
import {
|
|
40
45
|
extractRunObservation,
|
|
@@ -149,9 +154,30 @@ export class FusionOrchestrator {
|
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
let resolved: ResolvedFusionProfile;
|
|
157
|
+
let baseProfileName: string | undefined;
|
|
152
158
|
try {
|
|
153
159
|
const config = await this.loadConfig(ctx);
|
|
154
160
|
resolved = this.resolveProfile(config, args.profile);
|
|
161
|
+
baseProfileName = resolved.name;
|
|
162
|
+
if (args.panel?.length) {
|
|
163
|
+
// The named profile still supplies the judge and every other setting;
|
|
164
|
+
// only the panel is replaced. Inline models skip the alias pass that
|
|
165
|
+
// runs at config load, so re-run it over the assembled profile.
|
|
166
|
+
const inlineName = `${resolved.name} (inline panel)`;
|
|
167
|
+
const aliased = await applyClaudeAliasShorthand(
|
|
168
|
+
{
|
|
169
|
+
defaultProfile: inlineName,
|
|
170
|
+
profiles: {
|
|
171
|
+
[inlineName]: buildInlinePanelProfile(
|
|
172
|
+
resolved.profile,
|
|
173
|
+
args.panel,
|
|
174
|
+
),
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
ctx,
|
|
178
|
+
);
|
|
179
|
+
resolved = this.resolveProfile(aliased, inlineName);
|
|
180
|
+
}
|
|
155
181
|
this.configWarning = undefined;
|
|
156
182
|
} catch (error: unknown) {
|
|
157
183
|
const message = errorMessage(error);
|
|
@@ -165,7 +191,12 @@ export class FusionOrchestrator {
|
|
|
165
191
|
run = this.runStore.startRun({
|
|
166
192
|
prompt: args.prompt,
|
|
167
193
|
profileName: resolved.name,
|
|
168
|
-
...(args.
|
|
194
|
+
...(args.panel?.length
|
|
195
|
+
? { inlinePanel: args.panel, baseProfileName: baseProfileName }
|
|
196
|
+
: {}),
|
|
197
|
+
...(args.operationId !== undefined
|
|
198
|
+
? { operationId: args.operationId }
|
|
199
|
+
: {}),
|
|
169
200
|
phase: "panel",
|
|
170
201
|
});
|
|
171
202
|
} catch (error: unknown) {
|
|
@@ -188,6 +219,8 @@ export class FusionOrchestrator {
|
|
|
188
219
|
const spawnResult = await this.rpc.spawn(
|
|
189
220
|
buildPanelSpawnParams(resolved.profile, args.prompt),
|
|
190
221
|
);
|
|
222
|
+
const spawnError = extractSubagentFailure(spawnResult);
|
|
223
|
+
if (spawnError) throw new FusionArgsError(spawnError);
|
|
191
224
|
const panelRunId = extractSubagentRunId(spawnResult);
|
|
192
225
|
if (!panelRunId) {
|
|
193
226
|
throw new FusionArgsError(
|
|
@@ -345,6 +378,12 @@ export class FusionOrchestrator {
|
|
|
345
378
|
? configuredJudgeModel(this.activeProfile)
|
|
346
379
|
: undefined,
|
|
347
380
|
),
|
|
381
|
+
...(this.activeProfile
|
|
382
|
+
? {
|
|
383
|
+
synthesis: resolveSynthesisMode(this.activeProfile),
|
|
384
|
+
panel: this.activeProfile.panel,
|
|
385
|
+
}
|
|
386
|
+
: {}),
|
|
348
387
|
});
|
|
349
388
|
const cancelled = this.runStore.cancelRun(active.id, {
|
|
350
389
|
...(active.chainRunId ? { chainRunId: active.chainRunId } : {}),
|
|
@@ -376,10 +415,19 @@ export class FusionOrchestrator {
|
|
|
376
415
|
|
|
377
416
|
try {
|
|
378
417
|
const config = await this.loadConfig(ctx);
|
|
379
|
-
|
|
418
|
+
// An inline run's profileName is a display name that no config defines,
|
|
419
|
+
// so rebuild it from the base profile plus the persisted entries. Looking
|
|
420
|
+
// the display name up throws, which used to leave activeProfile undefined
|
|
421
|
+
// and fail the run the moment its panel completed.
|
|
422
|
+
const base = this.resolveProfile(
|
|
380
423
|
config,
|
|
381
|
-
active.
|
|
424
|
+
active.inlinePanel?.length
|
|
425
|
+
? active.baseProfileName
|
|
426
|
+
: active.profileName,
|
|
382
427
|
).profile;
|
|
428
|
+
this.activeProfile = active.inlinePanel?.length
|
|
429
|
+
? buildInlinePanelProfile(base, active.inlinePanel)
|
|
430
|
+
: base;
|
|
383
431
|
this.configWarning = undefined;
|
|
384
432
|
} catch (error: unknown) {
|
|
385
433
|
const message = `Could not restore fusion profile "${active.profileName}": ${errorMessage(error)}`;
|
|
@@ -517,13 +565,17 @@ export class FusionOrchestrator {
|
|
|
517
565
|
return { status: "ignored" };
|
|
518
566
|
}
|
|
519
567
|
|
|
520
|
-
const
|
|
521
|
-
snapshot.resultPayload ?? snapshot.statusPayload ?? payload
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
568
|
+
const lifecyclePayload =
|
|
569
|
+
snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
|
|
570
|
+
const lifecycleError = extractSubagentFailure(lifecyclePayload);
|
|
571
|
+
if (!hasLifecycleResults(lifecyclePayload) && lifecycleError) {
|
|
572
|
+
return this.failActiveRun(lifecycleError);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const extracted = extractPanelResults(lifecyclePayload, {
|
|
576
|
+
panel: profile.panel,
|
|
577
|
+
limit: profile.panel.length,
|
|
578
|
+
});
|
|
527
579
|
if (!extracted.ok) {
|
|
528
580
|
return this.failActiveRun(
|
|
529
581
|
`${extracted.error.message} (${extracted.error.path})`,
|
|
@@ -567,6 +619,8 @@ export class FusionOrchestrator {
|
|
|
567
619
|
failures: storedPanelFailures(observed),
|
|
568
620
|
...withJudgeModel(configuredJudgeModel(profile)),
|
|
569
621
|
judgeObservation,
|
|
622
|
+
...(profile.blindPanelLabels ? { blindPanelLabels: true } : {}),
|
|
623
|
+
synthesis: resolveSynthesisMode(profile),
|
|
570
624
|
});
|
|
571
625
|
return this.completeActiveRun(report);
|
|
572
626
|
}
|
|
@@ -628,16 +682,20 @@ export class FusionOrchestrator {
|
|
|
628
682
|
return { status: "ignored" };
|
|
629
683
|
}
|
|
630
684
|
|
|
631
|
-
const
|
|
632
|
-
snapshot.resultPayload ?? snapshot.statusPayload ?? payload
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
685
|
+
const lifecyclePayload =
|
|
686
|
+
snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
|
|
687
|
+
const lifecycleError = extractSubagentFailure(lifecyclePayload);
|
|
688
|
+
if (!hasLifecycleResults(lifecyclePayload) && lifecycleError) {
|
|
689
|
+
return this.failActiveRun(lifecycleError);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const extracted = extractPanelResults(lifecyclePayload, {
|
|
693
|
+
panel: profile.panel,
|
|
694
|
+
limit: profile.panel.length,
|
|
695
|
+
...(active.panelStoppedIndices
|
|
696
|
+
? { stoppedPanelIndices: active.panelStoppedIndices }
|
|
697
|
+
: {}),
|
|
698
|
+
});
|
|
641
699
|
if (!extracted.ok) {
|
|
642
700
|
return this.failActiveRun(
|
|
643
701
|
`${extracted.error.message} (${extracted.error.path})`,
|
|
@@ -742,6 +800,8 @@ export class FusionOrchestrator {
|
|
|
742
800
|
|
|
743
801
|
try {
|
|
744
802
|
const spawnResult = await this.rpc.spawn(decision.params);
|
|
803
|
+
const spawnError = extractSubagentFailure(spawnResult);
|
|
804
|
+
if (spawnError) throw new FusionArgsError(spawnError);
|
|
745
805
|
const judgeRunId = extractSubagentRunId(spawnResult);
|
|
746
806
|
if (!judgeRunId) {
|
|
747
807
|
throw new FusionArgsError(decision.missingRunIdError);
|
|
@@ -789,14 +849,27 @@ export class FusionOrchestrator {
|
|
|
789
849
|
return { status: "ignored" };
|
|
790
850
|
}
|
|
791
851
|
|
|
792
|
-
const
|
|
793
|
-
snapshot.resultPayload ?? snapshot.statusPayload ?? payload
|
|
794
|
-
);
|
|
852
|
+
const lifecyclePayload =
|
|
853
|
+
snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
|
|
854
|
+
const lifecycleError = extractSubagentFailure(lifecyclePayload);
|
|
855
|
+
if (!hasLifecycleResults(lifecyclePayload) && lifecycleError) {
|
|
856
|
+
return this.failActiveRun(lifecycleError);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const output = extractJudgeOutput(lifecyclePayload);
|
|
795
860
|
if (!output.ok) return this.failActiveRun(output.error);
|
|
796
861
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
862
|
+
// The panel and chain handlers already treat a missing profile as fatal.
|
|
863
|
+
// Without the same guard here the run "succeeds" with a degraded report:
|
|
864
|
+
// blind labels are never restored, and the judge model is dropped.
|
|
865
|
+
const profile = this.activeProfile;
|
|
866
|
+
if (!profile) {
|
|
867
|
+
return this.failActiveRun(
|
|
868
|
+
"Fusion judge completed, but the active profile was not available.",
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const judgeModel = configuredJudgeModel(profile);
|
|
800
873
|
const judgeObservation = mergeRunObservations(
|
|
801
874
|
extractRunObservation(
|
|
802
875
|
findStepsArray(snapshot.statusPayload)[0] ?? snapshot.statusPayload,
|
|
@@ -817,6 +890,8 @@ export class FusionOrchestrator {
|
|
|
817
890
|
failures: storedPanelFailures(observed),
|
|
818
891
|
...withJudgeModel(judgeModel),
|
|
819
892
|
judgeObservation,
|
|
893
|
+
...(profile.blindPanelLabels ? { blindPanelLabels: true } : {}),
|
|
894
|
+
synthesis: resolveSynthesisMode(profile),
|
|
820
895
|
});
|
|
821
896
|
return this.completeActiveRun(report);
|
|
822
897
|
}
|
|
@@ -850,12 +925,18 @@ export class FusionOrchestrator {
|
|
|
850
925
|
);
|
|
851
926
|
}
|
|
852
927
|
|
|
928
|
+
const statusIsTerminal =
|
|
929
|
+
isTerminalSubagentState(extractSubagentState(statusPayload)) ||
|
|
930
|
+
isTerminalFusionProgress(statusPayload);
|
|
931
|
+
const eventIsTerminal =
|
|
932
|
+
isTerminalSubagentState(extractSubagentState(input.eventPayload)) ||
|
|
933
|
+
isTerminalFusionProgress(input.eventPayload);
|
|
853
934
|
const eventHasResults = hasResultsArray(input.eventPayload);
|
|
854
935
|
if (eventPayloadMatches && eventHasResults) {
|
|
855
936
|
return {
|
|
856
937
|
statusPayload,
|
|
857
938
|
resultPayload: input.eventPayload,
|
|
858
|
-
resultIsTerminal:
|
|
939
|
+
resultIsTerminal: eventIsTerminal || statusIsTerminal,
|
|
859
940
|
};
|
|
860
941
|
}
|
|
861
942
|
|
|
@@ -872,17 +953,14 @@ export class FusionOrchestrator {
|
|
|
872
953
|
}
|
|
873
954
|
|
|
874
955
|
if (hasResultsArray(statusPayload)) {
|
|
875
|
-
const resultIsTerminal =
|
|
876
|
-
eventPayloadMatches ||
|
|
877
|
-
isTerminalSubagentState(extractSubagentState(statusPayload));
|
|
878
956
|
return {
|
|
879
957
|
statusPayload,
|
|
880
958
|
resultPayload: statusPayload,
|
|
881
|
-
resultIsTerminal,
|
|
959
|
+
resultIsTerminal: statusIsTerminal,
|
|
882
960
|
};
|
|
883
961
|
}
|
|
884
962
|
|
|
885
|
-
if (
|
|
963
|
+
if (statusIsTerminal) {
|
|
886
964
|
return {
|
|
887
965
|
statusPayload,
|
|
888
966
|
resultPayload: statusPayload,
|
|
@@ -894,9 +972,7 @@ export class FusionOrchestrator {
|
|
|
894
972
|
return {
|
|
895
973
|
statusPayload,
|
|
896
974
|
resultPayload: input.eventPayload,
|
|
897
|
-
resultIsTerminal:
|
|
898
|
-
extractSubagentState(input.eventPayload),
|
|
899
|
-
),
|
|
975
|
+
resultIsTerminal: eventIsTerminal,
|
|
900
976
|
};
|
|
901
977
|
}
|
|
902
978
|
|
|
@@ -968,6 +1044,12 @@ export class FusionOrchestrator {
|
|
|
968
1044
|
? configuredJudgeModel(this.activeProfile)
|
|
969
1045
|
: undefined,
|
|
970
1046
|
),
|
|
1047
|
+
...(this.activeProfile
|
|
1048
|
+
? {
|
|
1049
|
+
synthesis: resolveSynthesisMode(this.activeProfile),
|
|
1050
|
+
panel: this.activeProfile.panel,
|
|
1051
|
+
}
|
|
1052
|
+
: {}),
|
|
971
1053
|
});
|
|
972
1054
|
}
|
|
973
1055
|
|
|
@@ -1248,7 +1330,7 @@ function buildFusionStatusDetails(
|
|
|
1248
1330
|
const activity = describeStepActivity(step);
|
|
1249
1331
|
const metrics = describeStepMetrics(step);
|
|
1250
1332
|
return {
|
|
1251
|
-
label: member
|
|
1333
|
+
label: memberLabel(member),
|
|
1252
1334
|
...(member.role ? { role: member.role } : {}),
|
|
1253
1335
|
...(model ? { model } : {}),
|
|
1254
1336
|
status: describePanelStatus(step),
|
|
@@ -1313,7 +1395,7 @@ function buildCompletedPanelStatusLines(
|
|
|
1313
1395
|
);
|
|
1314
1396
|
if (output) {
|
|
1315
1397
|
return {
|
|
1316
|
-
label: member
|
|
1398
|
+
label: memberLabel(member),
|
|
1317
1399
|
...(member.role ? { role: member.role } : {}),
|
|
1318
1400
|
...(model ? { model } : {}),
|
|
1319
1401
|
status: "completed",
|
|
@@ -1323,7 +1405,7 @@ function buildCompletedPanelStatusLines(
|
|
|
1323
1405
|
(item) => item.id === member.id || item.index === index,
|
|
1324
1406
|
);
|
|
1325
1407
|
return {
|
|
1326
|
-
label: member
|
|
1408
|
+
label: memberLabel(member),
|
|
1327
1409
|
...(member.role ? { role: member.role } : {}),
|
|
1328
1410
|
...(model ? { model } : {}),
|
|
1329
1411
|
status: failure ? "failed" : "unknown",
|
|
@@ -1526,6 +1608,38 @@ function isTerminalSubagentState(state: string | undefined): boolean {
|
|
|
1526
1608
|
);
|
|
1527
1609
|
}
|
|
1528
1610
|
|
|
1611
|
+
function hasLifecycleResults(payload: unknown): boolean {
|
|
1612
|
+
return hasResultsArray(payload) || findStepsArray(payload).length > 0;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function extractSubagentFailure(payload: unknown): string | undefined {
|
|
1616
|
+
if (!isRecord(payload)) return undefined;
|
|
1617
|
+
const direct = firstNonBlankString(payload.error, payload.errorMessage);
|
|
1618
|
+
if (direct) return direct;
|
|
1619
|
+
if (isRecord(payload.details)) {
|
|
1620
|
+
const detailsError = firstNonBlankString(
|
|
1621
|
+
payload.details.error,
|
|
1622
|
+
payload.details.errorMessage,
|
|
1623
|
+
);
|
|
1624
|
+
if (detailsError) return detailsError;
|
|
1625
|
+
}
|
|
1626
|
+
if (payload.isError === true && Array.isArray(payload.content)) {
|
|
1627
|
+
for (const item of payload.content) {
|
|
1628
|
+
const contentText = isRecord(item)
|
|
1629
|
+
? firstNonBlankString(item.text)
|
|
1630
|
+
: undefined;
|
|
1631
|
+
if (contentText) return contentText;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
const text = firstNonBlankString(
|
|
1635
|
+
payload.text,
|
|
1636
|
+
isRecord(payload.details) ? payload.details.text : undefined,
|
|
1637
|
+
);
|
|
1638
|
+
if (text && /^error(?:\s|:)/i.test(text)) return text;
|
|
1639
|
+
if (isRecord(payload.data)) return extractSubagentFailure(payload.data);
|
|
1640
|
+
return undefined;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1529
1643
|
function firstNonBlankStringFromPayload(payload: unknown): string | undefined {
|
|
1530
1644
|
if (!isRecord(payload)) return undefined;
|
|
1531
1645
|
const direct = firstNonBlankString(
|
package/src/panel-completion.ts
CHANGED
|
@@ -4,11 +4,12 @@ import {
|
|
|
4
4
|
buildJudgeSpawnParams,
|
|
5
5
|
type JudgeSpawnParams,
|
|
6
6
|
} from "./run-builder.js";
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import {
|
|
8
|
+
resolveSynthesisMode,
|
|
9
|
+
type FailedPanelSummary,
|
|
10
|
+
type FusionProfile,
|
|
11
|
+
type FusionRun,
|
|
12
|
+
type PanelOutput,
|
|
12
13
|
} from "./types.js";
|
|
13
14
|
|
|
14
15
|
export type PanelCompletionDecision =
|
|
@@ -39,6 +40,8 @@ export function decidePanelCompletion(
|
|
|
39
40
|
run: input.run,
|
|
40
41
|
failures: input.panelFailures,
|
|
41
42
|
...withJudgeModel(judgeModel),
|
|
43
|
+
synthesis: resolveSynthesisMode(input.profile),
|
|
44
|
+
panel: input.profile.panel,
|
|
42
45
|
});
|
|
43
46
|
return {
|
|
44
47
|
kind: "fail",
|
|
@@ -47,7 +50,14 @@ export function decidePanelCompletion(
|
|
|
47
50
|
};
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
// Under `select` every panelist answered the whole question, so a lone
|
|
54
|
+
// survivor is a complete if thin answer. Under `merge` it answered ONE facet:
|
|
55
|
+
// returning it as the answer would be wrong, not thin. Run the composer so
|
|
56
|
+
// the report names the facets nobody covered.
|
|
57
|
+
if (
|
|
58
|
+
input.panelOutputs.length === 1 &&
|
|
59
|
+
resolveSynthesisMode(input.profile) !== "merge"
|
|
60
|
+
) {
|
|
51
61
|
const report = renderSinglePanelReport({
|
|
52
62
|
run: input.run,
|
|
53
63
|
output: input.panelOutputs[0]!,
|
|
@@ -64,6 +74,7 @@ export function decidePanelCompletion(
|
|
|
64
74
|
prompt: input.run.prompt,
|
|
65
75
|
panelOutputs: input.panelOutputs,
|
|
66
76
|
failedPanelists: input.panelFailures,
|
|
77
|
+
runId: input.run.id,
|
|
67
78
|
}),
|
|
68
79
|
missingRunIdError: input.fallbackJudge
|
|
69
80
|
? "pi-subagents spawn did not return a fallback judge run ID."
|