@oh-my-pi/pi-coding-agent 13.2.1 → 13.3.1
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/CHANGELOG.md +43 -2
- package/package.json +7 -7
- package/scripts/generate-docs-index.ts +2 -2
- package/src/cli/args.ts +2 -1
- package/src/cli/config-cli.ts +32 -20
- package/src/config/settings-schema.ts +96 -14
- package/src/config/settings.ts +10 -0
- package/src/discovery/claude.ts +24 -6
- package/src/discovery/helpers.ts +9 -2
- package/src/ipy/runtime.ts +1 -0
- package/src/mcp/config.ts +1 -1
- package/src/modes/components/settings-defs.ts +53 -1
- package/src/modes/components/status-line.ts +7 -5
- package/src/modes/controllers/mcp-command-controller.ts +4 -3
- package/src/modes/controllers/selector-controller.ts +46 -0
- package/src/modes/interactive-mode.ts +9 -0
- package/src/modes/oauth-manual-input.ts +42 -0
- package/src/modes/types.ts +2 -0
- package/src/patch/hashline.ts +19 -1
- package/src/patch/index.ts +7 -8
- package/src/prompts/system/commit-message-system.md +2 -0
- package/src/prompts/system/subagent-submit-reminder.md +3 -3
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/system-prompt.md +13 -0
- package/src/prompts/tools/hashline.md +45 -1
- package/src/prompts/tools/task-summary.md +4 -4
- package/src/prompts/tools/task.md +1 -1
- package/src/sdk.ts +8 -0
- package/src/slash-commands/builtin-registry.ts +26 -1
- package/src/system-prompt.ts +4 -0
- package/src/task/index.ts +211 -70
- package/src/task/render.ts +44 -16
- package/src/task/types.ts +6 -1
- package/src/task/worktree.ts +394 -31
- package/src/tools/review.ts +50 -1
- package/src/tools/submit-result.ts +22 -23
- package/src/utils/commit-message-generator.ts +132 -0
- package/src/web/search/providers/exa.ts +41 -4
- package/src/web/search/providers/perplexity.ts +20 -8
package/src/task/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type:
|
|
|
29
29
|
import { formatBytes, formatDuration } from "../tools/render-utils";
|
|
30
30
|
// Import review tools for side effects (registers subagent tool handlers)
|
|
31
31
|
import "../tools/review";
|
|
32
|
+
import { generateCommitMessage } from "../utils/commit-message-generator";
|
|
32
33
|
import { discoverAgents, getAgent } from "./discovery";
|
|
33
34
|
import { runSubprocess } from "./executor";
|
|
34
35
|
import { AgentOutputManager } from "./output-manager";
|
|
@@ -47,11 +48,17 @@ import {
|
|
|
47
48
|
} from "./types";
|
|
48
49
|
import {
|
|
49
50
|
applyBaseline,
|
|
51
|
+
applyNestedPatches,
|
|
50
52
|
captureBaseline,
|
|
51
53
|
captureDeltaPatch,
|
|
54
|
+
cleanupFuseOverlay,
|
|
55
|
+
cleanupTaskBranches,
|
|
52
56
|
cleanupWorktree,
|
|
57
|
+
commitToBranch,
|
|
58
|
+
ensureFuseOverlay,
|
|
53
59
|
ensureWorktree,
|
|
54
60
|
getRepoRoot,
|
|
61
|
+
mergeTaskBranches,
|
|
55
62
|
type WorktreeBaseline,
|
|
56
63
|
} from "./worktree";
|
|
57
64
|
|
|
@@ -145,11 +152,11 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
145
152
|
get description(): string {
|
|
146
153
|
const disabledAgents = this.session.settings.get("task.disabledAgents") as string[];
|
|
147
154
|
const maxConcurrency = this.session.settings.get("task.maxConcurrency");
|
|
148
|
-
const
|
|
155
|
+
const isolationMode = this.session.settings.get("task.isolation.mode");
|
|
149
156
|
return renderDescription(
|
|
150
157
|
this.#discoveredAgents,
|
|
151
158
|
maxConcurrency,
|
|
152
|
-
|
|
159
|
+
isolationMode !== "none",
|
|
153
160
|
this.session.settings.get("async.enabled"),
|
|
154
161
|
disabledAgents,
|
|
155
162
|
);
|
|
@@ -168,9 +175,9 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
168
175
|
* Create a TaskTool instance with async agent discovery.
|
|
169
176
|
*/
|
|
170
177
|
static async create(session: ToolSession): Promise<TaskTool> {
|
|
171
|
-
const
|
|
178
|
+
const isolationMode = session.settings.get("task.isolation.mode");
|
|
172
179
|
const { agents } = await discoverAgents(session.cwd);
|
|
173
|
-
return new TaskTool(session, agents,
|
|
180
|
+
return new TaskTool(session, agents, isolationMode !== "none");
|
|
174
181
|
}
|
|
175
182
|
|
|
176
183
|
async execute(
|
|
@@ -422,18 +429,20 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
422
429
|
const startTime = Date.now();
|
|
423
430
|
const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
|
|
424
431
|
const { agent: agentName, context, schema: outputSchema } = params;
|
|
425
|
-
const
|
|
432
|
+
const isolationMode = this.session.settings.get("task.isolation.mode");
|
|
426
433
|
const isolationRequested = "isolated" in params ? params.isolated === true : false;
|
|
427
|
-
const isIsolated =
|
|
434
|
+
const isIsolated = isolationMode !== "none" && isolationRequested;
|
|
435
|
+
const mergeMode = this.session.settings.get("task.isolation.merge");
|
|
436
|
+
const commitStyle = this.session.settings.get("task.isolation.commits");
|
|
428
437
|
const maxConcurrency = this.session.settings.get("task.maxConcurrency");
|
|
429
438
|
const taskDepth = this.session.taskDepth ?? 0;
|
|
430
439
|
|
|
431
|
-
if (
|
|
440
|
+
if (isolationMode === "none" && "isolated" in params) {
|
|
432
441
|
return {
|
|
433
442
|
content: [
|
|
434
443
|
{
|
|
435
444
|
type: "text",
|
|
436
|
-
text: "Task isolation is disabled. Remove the isolated argument to
|
|
445
|
+
text: "Task isolation is disabled. Remove the isolated argument or set task.isolation.mode to 'worktree' or 'fuse-overlay'.",
|
|
437
446
|
},
|
|
438
447
|
],
|
|
439
448
|
details: {
|
|
@@ -789,16 +798,23 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
789
798
|
}
|
|
790
799
|
|
|
791
800
|
const taskStart = Date.now();
|
|
792
|
-
let
|
|
801
|
+
let isolationDir: string | undefined;
|
|
793
802
|
try {
|
|
794
803
|
if (!repoRoot || !baseline) {
|
|
795
804
|
throw new Error("Isolated task execution not initialized.");
|
|
796
805
|
}
|
|
797
|
-
|
|
798
|
-
|
|
806
|
+
const taskBaseline = structuredClone(baseline);
|
|
807
|
+
|
|
808
|
+
if (isolationMode === "fuse-overlay") {
|
|
809
|
+
isolationDir = await ensureFuseOverlay(repoRoot, task.id);
|
|
810
|
+
} else {
|
|
811
|
+
isolationDir = await ensureWorktree(repoRoot, task.id);
|
|
812
|
+
await applyBaseline(isolationDir, taskBaseline);
|
|
813
|
+
}
|
|
814
|
+
|
|
799
815
|
const result = await runSubprocess({
|
|
800
816
|
cwd: this.session.cwd,
|
|
801
|
-
worktree:
|
|
817
|
+
worktree: isolationDir,
|
|
802
818
|
agent,
|
|
803
819
|
task: task.task,
|
|
804
820
|
description: task.description,
|
|
@@ -830,13 +846,56 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
830
846
|
preloadedSkills: task.preloadedSkills,
|
|
831
847
|
promptTemplates,
|
|
832
848
|
});
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
849
|
+
if (mergeMode === "branch" && result.exitCode === 0) {
|
|
850
|
+
try {
|
|
851
|
+
const commitMsg =
|
|
852
|
+
commitStyle === "ai" && this.session.modelRegistry
|
|
853
|
+
? async (diff: string) => {
|
|
854
|
+
const smolModel = this.session.settings.getModelRole("smol");
|
|
855
|
+
return generateCommitMessage(
|
|
856
|
+
diff,
|
|
857
|
+
this.session.modelRegistry!,
|
|
858
|
+
smolModel,
|
|
859
|
+
this.session.getSessionId?.() ?? undefined,
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
: undefined;
|
|
863
|
+
const commitResult = await commitToBranch(
|
|
864
|
+
isolationDir,
|
|
865
|
+
taskBaseline,
|
|
866
|
+
task.id,
|
|
867
|
+
task.description,
|
|
868
|
+
commitMsg,
|
|
869
|
+
);
|
|
870
|
+
return {
|
|
871
|
+
...result,
|
|
872
|
+
branchName: commitResult?.branchName,
|
|
873
|
+
nestedPatches: commitResult?.nestedPatches,
|
|
874
|
+
};
|
|
875
|
+
} catch (mergeErr) {
|
|
876
|
+
// Agent succeeded but branch commit failed — clean up stale branch
|
|
877
|
+
const branchName = `omp/task/${task.id}`;
|
|
878
|
+
await $`git branch -D ${branchName}`.cwd(repoRoot).quiet().nothrow();
|
|
879
|
+
const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
880
|
+
return { ...result, error: `Merge failed: ${msg}` };
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (result.exitCode === 0) {
|
|
884
|
+
try {
|
|
885
|
+
const delta = await captureDeltaPatch(isolationDir, taskBaseline);
|
|
886
|
+
const patchPath = path.join(effectiveArtifactsDir, `${task.id}.patch`);
|
|
887
|
+
await Bun.write(patchPath, delta.rootPatch);
|
|
888
|
+
return {
|
|
889
|
+
...result,
|
|
890
|
+
patchPath,
|
|
891
|
+
nestedPatches: delta.nestedPatches,
|
|
892
|
+
};
|
|
893
|
+
} catch (patchErr) {
|
|
894
|
+
const msg = patchErr instanceof Error ? patchErr.message : String(patchErr);
|
|
895
|
+
return { ...result, error: `Patch capture failed: ${msg}` };
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return result;
|
|
840
899
|
} catch (err) {
|
|
841
900
|
const message = err instanceof Error ? err.message : String(err);
|
|
842
901
|
return {
|
|
@@ -856,8 +915,12 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
856
915
|
error: message,
|
|
857
916
|
};
|
|
858
917
|
} finally {
|
|
859
|
-
if (
|
|
860
|
-
|
|
918
|
+
if (isolationDir) {
|
|
919
|
+
if (isolationMode === "fuse-overlay") {
|
|
920
|
+
await cleanupFuseOverlay(isolationDir);
|
|
921
|
+
} else {
|
|
922
|
+
await cleanupWorktree(isolationDir);
|
|
923
|
+
}
|
|
861
924
|
}
|
|
862
925
|
}
|
|
863
926
|
};
|
|
@@ -917,74 +980,152 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
917
980
|
}
|
|
918
981
|
}
|
|
919
982
|
|
|
920
|
-
let
|
|
921
|
-
let
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
983
|
+
let mergeSummary = "";
|
|
984
|
+
let changesApplied: boolean | null = null;
|
|
985
|
+
let mergedBranchesForNestedPatches: Set<string> | null = null;
|
|
986
|
+
if (isIsolated && repoRoot) {
|
|
987
|
+
if (mergeMode === "branch") {
|
|
988
|
+
// Branch mode: merge task branches sequentially
|
|
989
|
+
const branchEntries = results
|
|
990
|
+
.filter(r => r.branchName && r.exitCode === 0 && !r.aborted)
|
|
991
|
+
.map(r => ({ branchName: r.branchName!, taskId: r.id, description: r.description }));
|
|
992
|
+
|
|
993
|
+
if (branchEntries.length === 0) {
|
|
994
|
+
changesApplied = true;
|
|
995
|
+
} else {
|
|
996
|
+
const mergeResult = await mergeTaskBranches(repoRoot, branchEntries);
|
|
997
|
+
mergedBranchesForNestedPatches = new Set(mergeResult.merged);
|
|
998
|
+
changesApplied = mergeResult.failed.length === 0;
|
|
999
|
+
|
|
1000
|
+
if (changesApplied) {
|
|
1001
|
+
mergeSummary = `\n\nMerged ${mergeResult.merged.length} branch${mergeResult.merged.length === 1 ? "" : "es"}: ${mergeResult.merged.join(", ")}`;
|
|
1002
|
+
} else {
|
|
1003
|
+
const mergedPart =
|
|
1004
|
+
mergeResult.merged.length > 0 ? `Merged: ${mergeResult.merged.join(", ")}.\n` : "";
|
|
1005
|
+
const failedPart = `Failed: ${mergeResult.failed.join(", ")}.`;
|
|
1006
|
+
const conflictPart = mergeResult.conflict ? `\nConflict: ${mergeResult.conflict}` : "";
|
|
1007
|
+
mergeSummary = `\n\n<system-notification>Branch merge failed. ${mergedPart}${failedPart}${conflictPart}\nUnmerged branches remain for manual resolution.</system-notification>`;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Clean up merged branches (keep failed ones for manual resolution)
|
|
1012
|
+
const allBranches = branchEntries.map(b => b.branchName);
|
|
1013
|
+
if (changesApplied) {
|
|
1014
|
+
await cleanupTaskBranches(repoRoot, allBranches);
|
|
1015
|
+
}
|
|
927
1016
|
} else {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
);
|
|
934
|
-
const nonEmptyPatches = patchStats.filter(patch => patch.size > 0).map(patch => patch.patchPath);
|
|
935
|
-
if (nonEmptyPatches.length === 0) {
|
|
936
|
-
patchesApplied = true;
|
|
1017
|
+
// Patch mode: combine and apply patches
|
|
1018
|
+
const patchesInOrder = results.map(result => result.patchPath).filter(Boolean) as string[];
|
|
1019
|
+
const missingPatch = results.some(result => !result.patchPath);
|
|
1020
|
+
if (missingPatch) {
|
|
1021
|
+
changesApplied = false;
|
|
937
1022
|
} else {
|
|
938
|
-
const
|
|
939
|
-
|
|
1023
|
+
const patchStats = await Promise.all(
|
|
1024
|
+
patchesInOrder.map(async patchPath => ({
|
|
1025
|
+
patchPath,
|
|
1026
|
+
size: (await fs.stat(patchPath)).size,
|
|
1027
|
+
})),
|
|
940
1028
|
);
|
|
941
|
-
const
|
|
942
|
-
if (
|
|
943
|
-
|
|
1029
|
+
const nonEmptyPatches = patchStats.filter(patch => patch.size > 0).map(patch => patch.patchPath);
|
|
1030
|
+
if (nonEmptyPatches.length === 0) {
|
|
1031
|
+
changesApplied = true;
|
|
944
1032
|
} else {
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const
|
|
1033
|
+
const patchTexts = await Promise.all(
|
|
1034
|
+
nonEmptyPatches.map(async patchPath => Bun.file(patchPath).text()),
|
|
1035
|
+
);
|
|
1036
|
+
const combinedPatch = patchTexts.map(text => (text.endsWith("\n") ? text : `${text}\n`)).join("");
|
|
1037
|
+
if (!combinedPatch.trim()) {
|
|
1038
|
+
changesApplied = true;
|
|
1039
|
+
} else {
|
|
1040
|
+
const combinedPatchPath = path.join(os.tmpdir(), `omp-task-combined-${Snowflake.next()}.patch`);
|
|
1041
|
+
try {
|
|
1042
|
+
await Bun.write(combinedPatchPath, combinedPatch);
|
|
1043
|
+
const checkResult = await $`git apply --check --binary ${combinedPatchPath}`
|
|
956
1044
|
.cwd(repoRoot)
|
|
957
1045
|
.quiet()
|
|
958
1046
|
.nothrow();
|
|
959
|
-
|
|
1047
|
+
if (checkResult.exitCode !== 0) {
|
|
1048
|
+
changesApplied = false;
|
|
1049
|
+
} else {
|
|
1050
|
+
const applyResult = await $`git apply --binary ${combinedPatchPath}`
|
|
1051
|
+
.cwd(repoRoot)
|
|
1052
|
+
.quiet()
|
|
1053
|
+
.nothrow();
|
|
1054
|
+
changesApplied = applyResult.exitCode === 0;
|
|
1055
|
+
}
|
|
1056
|
+
} finally {
|
|
1057
|
+
await fs.rm(combinedPatchPath, { force: true });
|
|
960
1058
|
}
|
|
961
|
-
} finally {
|
|
962
|
-
await fs.rm(combinedPatchPath, { force: true });
|
|
963
1059
|
}
|
|
964
1060
|
}
|
|
965
1061
|
}
|
|
1062
|
+
|
|
1063
|
+
if (changesApplied) {
|
|
1064
|
+
mergeSummary = "\n\nApplied patches: yes";
|
|
1065
|
+
} else {
|
|
1066
|
+
const notification =
|
|
1067
|
+
"<system-notification>Patches were not applied and must be handled manually.</system-notification>";
|
|
1068
|
+
const patchList =
|
|
1069
|
+
patchPaths.length > 0
|
|
1070
|
+
? `\n\nPatch artifacts:\n${patchPaths.map(patch => `- ${patch}`).join("\n")}`
|
|
1071
|
+
: "";
|
|
1072
|
+
mergeSummary = `\n\n${notification}${patchList}`;
|
|
1073
|
+
}
|
|
966
1074
|
}
|
|
1075
|
+
}
|
|
967
1076
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1077
|
+
// Apply nested repo patches (separate from parent git)
|
|
1078
|
+
if (isIsolated && repoRoot && (mergeMode === "branch" || changesApplied !== false)) {
|
|
1079
|
+
const allNestedPatches = results
|
|
1080
|
+
.filter(r => {
|
|
1081
|
+
if (!r.nestedPatches || r.nestedPatches.length === 0 || r.exitCode !== 0 || r.aborted) {
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
if (mergeMode !== "branch") {
|
|
1085
|
+
return true;
|
|
1086
|
+
}
|
|
1087
|
+
if (!r.branchName || !mergedBranchesForNestedPatches) {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
return mergedBranchesForNestedPatches.has(r.branchName);
|
|
1091
|
+
})
|
|
1092
|
+
.flatMap(r => r.nestedPatches!);
|
|
1093
|
+
if (allNestedPatches.length > 0) {
|
|
1094
|
+
try {
|
|
1095
|
+
const commitMsg =
|
|
1096
|
+
commitStyle === "ai" && this.session.modelRegistry
|
|
1097
|
+
? async (diff: string) => {
|
|
1098
|
+
const smolModel = this.session.settings.getModelRole("smol");
|
|
1099
|
+
return generateCommitMessage(
|
|
1100
|
+
diff,
|
|
1101
|
+
this.session.modelRegistry!,
|
|
1102
|
+
smolModel,
|
|
1103
|
+
this.session.getSessionId?.() ?? undefined,
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
: undefined;
|
|
1107
|
+
await applyNestedPatches(repoRoot, allNestedPatches, commitMsg);
|
|
1108
|
+
} catch {
|
|
1109
|
+
// Nested patch failures are non-fatal to the parent merge
|
|
1110
|
+
mergeSummary +=
|
|
1111
|
+
"\n\n<system-notification>Some nested repository patches failed to apply.</system-notification>";
|
|
1112
|
+
}
|
|
978
1113
|
}
|
|
979
1114
|
}
|
|
980
1115
|
|
|
981
1116
|
// Build final output - match plugin format
|
|
982
|
-
const successCount = results.filter(r => r.exitCode === 0).length;
|
|
1117
|
+
const successCount = results.filter(r => r.exitCode === 0 && !r.error).length;
|
|
983
1118
|
const cancelledCount = results.filter(r => r.aborted).length;
|
|
984
1119
|
const totalDuration = Date.now() - startTime;
|
|
985
1120
|
|
|
986
1121
|
const summaries = results.map(r => {
|
|
987
|
-
const status = r.aborted
|
|
1122
|
+
const status = r.aborted
|
|
1123
|
+
? "cancelled"
|
|
1124
|
+
: r.exitCode === 0 && r.error
|
|
1125
|
+
? "merge failed"
|
|
1126
|
+
: r.exitCode === 0
|
|
1127
|
+
? "completed"
|
|
1128
|
+
: `failed (exit ${r.exitCode})`;
|
|
988
1129
|
const output = r.output.trim() || r.stderr.trim() || "(no output)";
|
|
989
1130
|
const outputCharCount = r.outputMeta?.charCount ?? output.length;
|
|
990
1131
|
const fullOutputThreshold = 5000;
|
|
@@ -1021,12 +1162,12 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
1021
1162
|
summaries,
|
|
1022
1163
|
outputIds,
|
|
1023
1164
|
agentName,
|
|
1024
|
-
|
|
1165
|
+
mergeSummary,
|
|
1025
1166
|
});
|
|
1026
1167
|
|
|
1027
1168
|
// Cleanup temp directory if used
|
|
1028
1169
|
const shouldCleanupTempArtifacts =
|
|
1029
|
-
tempArtifactsDir && (!isIsolated ||
|
|
1170
|
+
tempArtifactsDir && (!isIsolated || changesApplied === true || changesApplied === null);
|
|
1030
1171
|
if (shouldCleanupTempArtifacts) {
|
|
1031
1172
|
await fs.rm(tempArtifactsDir, { recursive: true, force: true });
|
|
1032
1173
|
}
|
package/src/task/render.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type FindingPriority,
|
|
23
23
|
getPriorityInfo,
|
|
24
24
|
PRIORITY_LABELS,
|
|
25
|
+
parseReportFindingDetails,
|
|
25
26
|
type ReportFindingDetails,
|
|
26
27
|
type SubmitReviewDetails,
|
|
27
28
|
} from "../tools/review";
|
|
@@ -68,6 +69,16 @@ function formatFindingSummary(findings: ReportFindingDetails[], theme: Theme): s
|
|
|
68
69
|
return `${theme.fg("dim", "Findings:")} ${parts.join(theme.sep.dot)}`;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function normalizeReportFindings(value: unknown): ReportFindingDetails[] {
|
|
73
|
+
if (!Array.isArray(value)) return [];
|
|
74
|
+
const findings: ReportFindingDetails[] = [];
|
|
75
|
+
for (const item of value) {
|
|
76
|
+
const finding = parseReportFindingDetails(item);
|
|
77
|
+
if (finding) findings.push(finding);
|
|
78
|
+
}
|
|
79
|
+
return findings;
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
72
83
|
if (value === null) return "null";
|
|
73
84
|
if (typeof value === "string") {
|
|
@@ -569,13 +580,13 @@ function renderAgentProgress(
|
|
|
569
580
|
// For completed tasks, check for review verdict from submit_result tool
|
|
570
581
|
if (progress.status === "completed") {
|
|
571
582
|
const completeData = progress.extractedToolData.submit_result as Array<{ data: unknown }> | undefined;
|
|
572
|
-
const reportFindingData = progress.extractedToolData.report_finding
|
|
583
|
+
const reportFindingData = normalizeReportFindings(progress.extractedToolData.report_finding);
|
|
573
584
|
const reviewData = completeData
|
|
574
585
|
?.map(c => c.data as SubmitReviewDetails)
|
|
575
586
|
.filter(d => d && typeof d === "object" && "overall_correctness" in d);
|
|
576
587
|
if (reviewData && reviewData.length > 0) {
|
|
577
588
|
const summary = reviewData[reviewData.length - 1];
|
|
578
|
-
const findings = reportFindingData
|
|
589
|
+
const findings = reportFindingData;
|
|
579
590
|
lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
|
|
580
591
|
return lines; // Review result handles its own rendering
|
|
581
592
|
}
|
|
@@ -583,8 +594,9 @@ function renderAgentProgress(
|
|
|
583
594
|
|
|
584
595
|
for (const [toolName, dataArray] of Object.entries(progress.extractedToolData)) {
|
|
585
596
|
// Handle report_finding with tree formatting
|
|
586
|
-
if (toolName === "report_finding"
|
|
587
|
-
const findings = dataArray
|
|
597
|
+
if (toolName === "report_finding") {
|
|
598
|
+
const findings = normalizeReportFindings(dataArray);
|
|
599
|
+
if (findings.length === 0) continue;
|
|
588
600
|
lines.push(`${continuePrefix}${formatFindingSummary(findings, theme)}`);
|
|
589
601
|
lines.push(...renderFindings(findings, continuePrefix, expanded, theme));
|
|
590
602
|
continue;
|
|
@@ -693,7 +705,7 @@ function renderFindings(
|
|
|
693
705
|
|
|
694
706
|
const { color } = getPriorityInfo(finding.priority);
|
|
695
707
|
const titleText = finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled";
|
|
696
|
-
const loc = `${path.basename(finding.file_path)}:${finding.line_start}`;
|
|
708
|
+
const loc = `${path.basename(finding.file_path || "<unknown>")}:${finding.line_start}`;
|
|
697
709
|
|
|
698
710
|
lines.push(
|
|
699
711
|
`${continuePrefix}${findingPrefix} ${theme.fg(color, `[${finding.priority}]`)} ${titleText} ${theme.fg("dim", loc)}`,
|
|
@@ -728,7 +740,8 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
728
740
|
result.output,
|
|
729
741
|
);
|
|
730
742
|
const aborted = result.aborted ?? false;
|
|
731
|
-
const
|
|
743
|
+
const mergeFailed = !aborted && result.exitCode === 0 && !!result.error;
|
|
744
|
+
const success = !aborted && result.exitCode === 0 && !result.error;
|
|
732
745
|
const needsWarning = Boolean(missingCompleteWarning) && success;
|
|
733
746
|
const icon = aborted
|
|
734
747
|
? theme.status.aborted
|
|
@@ -737,8 +750,16 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
737
750
|
: success
|
|
738
751
|
? theme.status.success
|
|
739
752
|
: theme.status.error;
|
|
740
|
-
const iconColor = needsWarning ? "warning" : success ? "success" : "error";
|
|
741
|
-
const statusText = aborted
|
|
753
|
+
const iconColor = needsWarning ? "warning" : success ? "success" : mergeFailed ? "warning" : "error";
|
|
754
|
+
const statusText = aborted
|
|
755
|
+
? "aborted"
|
|
756
|
+
: needsWarning
|
|
757
|
+
? "warning"
|
|
758
|
+
: success
|
|
759
|
+
? "done"
|
|
760
|
+
: mergeFailed
|
|
761
|
+
? "merge failed"
|
|
762
|
+
: "failed";
|
|
742
763
|
|
|
743
764
|
// Main status line: id: description [status] · stats · ⟨agent⟩
|
|
744
765
|
const description = result.description?.trim();
|
|
@@ -764,7 +785,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
764
785
|
|
|
765
786
|
// Check for review result (submit_result with review schema + report_finding)
|
|
766
787
|
const completeData = result.extractedToolData?.submit_result as Array<{ data: unknown }> | undefined;
|
|
767
|
-
const reportFindingData = result.extractedToolData?.report_finding
|
|
788
|
+
const reportFindingData = normalizeReportFindings(result.extractedToolData?.report_finding);
|
|
768
789
|
|
|
769
790
|
// Extract review verdict from submit_result tool's data field if it matches SubmitReviewDetails
|
|
770
791
|
const reviewData = completeData
|
|
@@ -775,11 +796,11 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
775
796
|
if (submitReviewData && submitReviewData.length > 0) {
|
|
776
797
|
// Use combined review renderer
|
|
777
798
|
const summary = submitReviewData[submitReviewData.length - 1];
|
|
778
|
-
const findings = reportFindingData
|
|
799
|
+
const findings = reportFindingData;
|
|
779
800
|
lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
|
|
780
801
|
return lines;
|
|
781
802
|
}
|
|
782
|
-
if (reportFindingData
|
|
803
|
+
if (reportFindingData.length > 0) {
|
|
783
804
|
const hasCompleteData = completeData && completeData.length > 0;
|
|
784
805
|
const message = hasCompleteData
|
|
785
806
|
? "Review verdict missing expected fields"
|
|
@@ -847,11 +868,13 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
847
868
|
|
|
848
869
|
if (result.patchPath && !aborted && result.exitCode === 0) {
|
|
849
870
|
lines.push(`${continuePrefix}${theme.fg("dim", `Patch: ${result.patchPath}`)}`);
|
|
871
|
+
} else if (result.branchName && !aborted && result.exitCode === 0) {
|
|
872
|
+
lines.push(`${continuePrefix}${theme.fg("dim", `Branch: ${result.branchName}`)}`);
|
|
850
873
|
}
|
|
851
874
|
|
|
852
875
|
// Error message
|
|
853
|
-
if (result.error && !success) {
|
|
854
|
-
lines.push(`${continuePrefix}${theme.fg("error", truncateToWidth(result.error, 70))}`);
|
|
876
|
+
if (result.error && (!success || mergeFailed)) {
|
|
877
|
+
lines.push(`${continuePrefix}${theme.fg(mergeFailed ? "warning" : "error", truncateToWidth(result.error, 70))}`);
|
|
855
878
|
}
|
|
856
879
|
|
|
857
880
|
return lines;
|
|
@@ -902,15 +925,20 @@ export function renderResult(
|
|
|
902
925
|
});
|
|
903
926
|
|
|
904
927
|
const abortedCount = details.results.filter(r => r.aborted).length;
|
|
905
|
-
const
|
|
906
|
-
const
|
|
928
|
+
const mergeFailedCount = details.results.filter(r => !r.aborted && r.exitCode === 0 && r.error).length;
|
|
929
|
+
const successCount = details.results.filter(r => !r.aborted && r.exitCode === 0 && !r.error).length;
|
|
930
|
+
const failCount = details.results.length - successCount - mergeFailedCount - abortedCount;
|
|
907
931
|
let summary = `${theme.fg("dim", "Total:")} `;
|
|
908
932
|
if (abortedCount > 0) {
|
|
909
933
|
summary += theme.fg("error", `${abortedCount} aborted`);
|
|
910
|
-
if (successCount > 0 || failCount > 0) summary += theme.sep.dot;
|
|
934
|
+
if (successCount > 0 || mergeFailedCount > 0 || failCount > 0) summary += theme.sep.dot;
|
|
911
935
|
}
|
|
912
936
|
if (successCount > 0) {
|
|
913
937
|
summary += theme.fg("success", `${successCount} succeeded`);
|
|
938
|
+
if (mergeFailedCount > 0 || failCount > 0) summary += theme.sep.dot;
|
|
939
|
+
}
|
|
940
|
+
if (mergeFailedCount > 0) {
|
|
941
|
+
summary += theme.fg("warning", `${mergeFailedCount} merge failed`);
|
|
914
942
|
if (failCount > 0) summary += theme.sep.dot;
|
|
915
943
|
}
|
|
916
944
|
if (failCount > 0) {
|
package/src/task/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { type Static, Type } from "@sinclair/typebox";
|
|
5
|
+
import type { NestedRepoPatch } from "./worktree";
|
|
5
6
|
|
|
6
7
|
/** Source of an agent definition */
|
|
7
8
|
export type AgentSource = "bundled" | "user" | "project";
|
|
@@ -77,7 +78,7 @@ const createTaskSchema = (options: { isolationEnabled: boolean }) => {
|
|
|
77
78
|
...properties,
|
|
78
79
|
isolated: Type.Optional(
|
|
79
80
|
Type.Boolean({
|
|
80
|
-
description: "Run in isolated
|
|
81
|
+
description: "Run in isolated environment; returns patches. Use when tasks edit overlapping files.",
|
|
81
82
|
}),
|
|
82
83
|
),
|
|
83
84
|
});
|
|
@@ -179,6 +180,10 @@ export interface SingleResult {
|
|
|
179
180
|
outputPath?: string;
|
|
180
181
|
/** Patch path for isolated worktree output */
|
|
181
182
|
patchPath?: string;
|
|
183
|
+
/** Branch name for isolated branch-mode output */
|
|
184
|
+
branchName?: string;
|
|
185
|
+
/** Nested repo patches to apply after parent merge */
|
|
186
|
+
nestedPatches?: NestedRepoPatch[];
|
|
182
187
|
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|
|
183
188
|
extractedToolData?: Record<string, unknown[]>;
|
|
184
189
|
/** Output metadata for agent:// URL integration */
|