@ilya-lesikov/pi-pi 0.4.0 → 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/3p/pi-ask-user/index.ts +65 -49
- package/3p/pi-subagents/src/agent-manager.ts +8 -0
- package/3p/pi-subagents/src/agent-runner.ts +112 -19
- package/3p/pi-subagents/src/index.ts +3 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +32 -19
- package/extensions/orchestrator/agents/code-reviewer.ts +31 -17
- package/extensions/orchestrator/agents/constraints.ts +55 -0
- package/extensions/orchestrator/agents/explore.ts +13 -13
- package/extensions/orchestrator/agents/librarian.ts +12 -9
- package/extensions/orchestrator/agents/plan-reviewer.ts +31 -19
- package/extensions/orchestrator/agents/planner.ts +28 -23
- package/extensions/orchestrator/agents/registry.ts +2 -1
- package/extensions/orchestrator/agents/repo-context.ts +11 -0
- package/extensions/orchestrator/agents/task.ts +14 -11
- package/extensions/orchestrator/agents/tool-routing.ts +17 -32
- package/extensions/orchestrator/ast-search.ts +2 -1
- package/extensions/orchestrator/cbm.test.ts +35 -0
- package/extensions/orchestrator/cbm.ts +43 -13
- package/extensions/orchestrator/command-handlers.test.ts +390 -19
- package/extensions/orchestrator/command-handlers.ts +73 -31
- package/extensions/orchestrator/commands.test.ts +255 -2
- package/extensions/orchestrator/commands.ts +108 -10
- package/extensions/orchestrator/config.test.ts +289 -68
- package/extensions/orchestrator/config.ts +630 -121
- package/extensions/orchestrator/context.test.ts +177 -10
- package/extensions/orchestrator/context.ts +115 -14
- package/extensions/orchestrator/custom-footer.ts +2 -1
- package/extensions/orchestrator/doctor.test.ts +559 -0
- package/extensions/orchestrator/doctor.ts +664 -0
- package/extensions/orchestrator/event-handlers.test.ts +84 -22
- package/extensions/orchestrator/event-handlers.ts +1191 -360
- package/extensions/orchestrator/exa.test.ts +46 -0
- package/extensions/orchestrator/exa.ts +16 -10
- package/extensions/orchestrator/flant-infra.test.ts +224 -0
- package/extensions/orchestrator/flant-infra.ts +110 -41
- package/extensions/orchestrator/index.ts +13 -2
- package/extensions/orchestrator/integration.test.ts +2866 -118
- package/extensions/orchestrator/log.test.ts +219 -0
- package/extensions/orchestrator/log.ts +153 -0
- package/extensions/orchestrator/model-registry.test.ts +238 -0
- package/extensions/orchestrator/model-registry.ts +282 -0
- package/extensions/orchestrator/model-version.test.ts +27 -0
- package/extensions/orchestrator/model-version.ts +19 -0
- package/extensions/orchestrator/orchestrator.test.ts +206 -56
- package/extensions/orchestrator/orchestrator.ts +295 -148
- package/extensions/orchestrator/phases/brainstorm.test.ts +10 -7
- package/extensions/orchestrator/phases/brainstorm.ts +41 -35
- package/extensions/orchestrator/phases/implementation.ts +7 -11
- package/extensions/orchestrator/phases/machine.test.ts +27 -8
- package/extensions/orchestrator/phases/machine.ts +13 -0
- package/extensions/orchestrator/phases/planning.ts +57 -29
- package/extensions/orchestrator/phases/review-task.ts +3 -3
- package/extensions/orchestrator/phases/review.ts +38 -39
- package/extensions/orchestrator/phases/spawn-blocking.test.ts +69 -0
- package/extensions/orchestrator/phases/verdict.test.ts +139 -0
- package/extensions/orchestrator/phases/verdict.ts +82 -0
- package/extensions/orchestrator/plannotator.test.ts +85 -0
- package/extensions/orchestrator/plannotator.ts +24 -6
- package/extensions/orchestrator/pp-menu.test.ts +134 -0
- package/extensions/orchestrator/pp-menu.ts +2631 -392
- package/extensions/orchestrator/repo-utils.test.ts +151 -0
- package/extensions/orchestrator/repo-utils.ts +67 -0
- package/extensions/orchestrator/spawn-cleanup.test.ts +57 -0
- package/extensions/orchestrator/spawn-cleanup.ts +35 -0
- package/extensions/orchestrator/state.test.ts +76 -6
- package/extensions/orchestrator/state.ts +89 -26
- package/extensions/orchestrator/subagent-session-marker.test.ts +36 -0
- package/extensions/orchestrator/test-helpers.ts +217 -0
- package/extensions/orchestrator/tracer.test.ts +132 -0
- package/extensions/orchestrator/tracer.ts +127 -0
- package/extensions/orchestrator/transition-controller.test.ts +207 -0
- package/extensions/orchestrator/transition-controller.ts +259 -0
- package/extensions/orchestrator/usage-tracker.test.ts +435 -0
- package/extensions/orchestrator/usage-tracker.ts +23 -2
- package/extensions/orchestrator/validate-artifacts.test.ts +83 -1
- package/package.json +2 -1
|
@@ -1,27 +1,51 @@
|
|
|
1
1
|
import { existsSync, readdirSync, writeFileSync } from "fs";
|
|
2
2
|
import { join, relative } from "path";
|
|
3
|
-
import {
|
|
3
|
+
import { isDeepStrictEqual } from "util";
|
|
4
|
+
import { askUser, isCancel } from "../../3p/pi-ask-user/index.js";
|
|
4
5
|
import { unregisterAgentDefinitions } from "./agents/registry.js";
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type AfterEditCommandConfig,
|
|
8
|
+
type AfterImplementCommandConfig,
|
|
9
|
+
GLOBAL_CONFIG_PATH,
|
|
10
|
+
getDefaultConfig,
|
|
11
|
+
loadConfig,
|
|
12
|
+
mergeConfigLayers,
|
|
13
|
+
parseDuration,
|
|
14
|
+
readRawConfig,
|
|
15
|
+
removeConfigValue,
|
|
16
|
+
resolvePreset,
|
|
17
|
+
writeConfigValue,
|
|
18
|
+
type PiPiConfig,
|
|
19
|
+
type PresetGroup,
|
|
20
|
+
type OrchestratorRole,
|
|
21
|
+
type VariantConfig,
|
|
22
|
+
} from "./config.js";
|
|
6
23
|
import {
|
|
7
24
|
loadBrainstormReviewOutputs,
|
|
8
25
|
loadCodeReviewOutputs,
|
|
9
26
|
loadPlanReviewOutputs,
|
|
10
27
|
} from "./context.js";
|
|
11
28
|
import { detectDefaultBranch, enterReviewCycle, finalizeReviewCycle } from "./event-handlers.js";
|
|
12
|
-
import { Orchestrator
|
|
29
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
13
30
|
import { cancelPendingPlannotatorWait } from "./plannotator.js";
|
|
14
31
|
import { spawnPlanners, spawnPlanReviewers } from "./phases/planning.js";
|
|
15
32
|
import { spawnCodeReviewers } from "./phases/review.js";
|
|
16
33
|
import { spawnBrainstormReviewers } from "./phases/brainstorm.js";
|
|
34
|
+
import { nextPhase } from "./phases/machine.js";
|
|
35
|
+
import { getAllAliases, getModelFamilies, getModelInfo, resolveModel, updateRegistryFromAvailableModels } from "./model-registry.js";
|
|
36
|
+
import { compareModelVersion } from "./model-version.js";
|
|
17
37
|
|
|
18
38
|
import {
|
|
19
39
|
listTasks,
|
|
40
|
+
getEffectiveMode,
|
|
41
|
+
getEffectivePhaseMode,
|
|
20
42
|
loadTask,
|
|
21
43
|
lockTask,
|
|
22
44
|
saveTask,
|
|
23
45
|
taskAge,
|
|
24
46
|
taskName,
|
|
47
|
+
type AutonomousConfig,
|
|
48
|
+
type TaskMode,
|
|
25
49
|
type TaskInfo,
|
|
26
50
|
type TaskType,
|
|
27
51
|
} from "./state.js";
|
|
@@ -34,6 +58,10 @@ import {
|
|
|
34
58
|
updateFlantInfra,
|
|
35
59
|
type FlantSettings,
|
|
36
60
|
} from "./flant-infra.js";
|
|
61
|
+
import { runDoctor } from "./doctor.js";
|
|
62
|
+
import { normalizeRepoPath, type RepoInfo } from "./repo-utils.js";
|
|
63
|
+
import { getLogger, addTaskDestination, setLogLevel } from "./log.js";
|
|
64
|
+
import { handleSpawnResult } from "./spawn-cleanup.js";
|
|
37
65
|
|
|
38
66
|
type MenuMode = "command" | "tool";
|
|
39
67
|
|
|
@@ -41,6 +69,17 @@ const BACK = "back" as const;
|
|
|
41
69
|
|
|
42
70
|
type OptionInput = string | { title: string; description?: string };
|
|
43
71
|
|
|
72
|
+
type TimeoutKey =
|
|
73
|
+
| "performance.commands.afterEdit"
|
|
74
|
+
| "performance.commands.afterImplement"
|
|
75
|
+
| "performance.internals.subagentStale"
|
|
76
|
+
| "performance.internals.taskLockStale"
|
|
77
|
+
| "performance.internals.taskLockRefresh";
|
|
78
|
+
|
|
79
|
+
function isEnabled(value: { enabled?: boolean } | undefined): boolean {
|
|
80
|
+
return value?.enabled !== false;
|
|
81
|
+
}
|
|
82
|
+
|
|
44
83
|
async function selectOption(ctx: any, question: string, options: OptionInput[]): Promise<string | undefined> {
|
|
45
84
|
const result = await askUser(ctx, {
|
|
46
85
|
question,
|
|
@@ -49,10 +88,125 @@ async function selectOption(ctx: any, question: string, options: OptionInput[]):
|
|
|
49
88
|
allowComment: false,
|
|
50
89
|
allowMultiple: false,
|
|
51
90
|
});
|
|
52
|
-
if (!result || result.kind !== "selection") return undefined;
|
|
91
|
+
if (!result || isCancel(result) || result.kind !== "selection") return undefined;
|
|
53
92
|
return result.selections[0];
|
|
54
93
|
}
|
|
55
94
|
|
|
95
|
+
function opt(title: string, description: string): OptionInput {
|
|
96
|
+
return { title, description };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getRegisteredRepos(orchestrator: Orchestrator): RepoInfo[] {
|
|
100
|
+
const repos = orchestrator.active?.state.repos ?? [];
|
|
101
|
+
if (repos.length > 0) return repos;
|
|
102
|
+
return [{ path: normalizeRepoPath(orchestrator.cwd), isRoot: true }];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validateRepos(repos: RepoInfo[]): RepoInfo[] {
|
|
106
|
+
return repos.filter((repo) => {
|
|
107
|
+
try {
|
|
108
|
+
if (!existsSync(repo.path)) return false;
|
|
109
|
+
return existsSync(join(repo.path, ".git"));
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatRepoLabel(repo: RepoInfo): string {
|
|
117
|
+
return `${repo.path}${repo.isRoot ? " (root)" : ""}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function formatRepoList(repos: RepoInfo[]): string {
|
|
121
|
+
return repos
|
|
122
|
+
.map((repo) => {
|
|
123
|
+
const base = repo.baseBranch ? `, base: ${repo.baseBranch}` : "";
|
|
124
|
+
return `- ${formatRepoLabel(repo)}${base}`;
|
|
125
|
+
})
|
|
126
|
+
.join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function appendSection(content: string, heading: string, body: string): string {
|
|
130
|
+
const normalized = content.trimEnd();
|
|
131
|
+
if (normalized.includes(`${heading}\n`)) return normalized + "\n";
|
|
132
|
+
return `${normalized}\n\n${heading}\n${body}\n`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function appendToSection(content: string, heading: string, body: string): string {
|
|
136
|
+
const normalized = content.trimEnd();
|
|
137
|
+
if (normalized.includes(body)) return normalized + "\n";
|
|
138
|
+
|
|
139
|
+
const lines = normalized.split("\n");
|
|
140
|
+
const sectionIndex = lines.findIndex((line) => line.trim() === heading);
|
|
141
|
+
if (sectionIndex === -1) {
|
|
142
|
+
return appendSection(normalized, heading, body);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let insertIndex = lines.length;
|
|
146
|
+
for (let i = sectionIndex + 1; i < lines.length; i += 1) {
|
|
147
|
+
if (lines[i]?.startsWith("## ")) {
|
|
148
|
+
insertIndex = i;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const insertLines = body.split("\n");
|
|
154
|
+
if (insertIndex > sectionIndex + 1 && lines[insertIndex - 1]?.trim() !== "") {
|
|
155
|
+
insertLines.unshift("");
|
|
156
|
+
}
|
|
157
|
+
lines.splice(insertIndex, 0, ...insertLines);
|
|
158
|
+
return lines.join("\n") + "\n";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function appendRepoContext(content: string, repos: RepoInfo[]): string {
|
|
162
|
+
if (repos.length === 0) return content;
|
|
163
|
+
const lines = repos.map((repo) => `- ${formatRepoLabel(repo)}${repo.baseBranch ? ` (base: ${repo.baseBranch})` : ""}`);
|
|
164
|
+
return appendToSection(content, "## Constraints", `Registered repositories:\n${lines.join("\n")}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function appendResearchOpenQuestions(content: string, text: string): string {
|
|
168
|
+
const normalized = content.trimEnd();
|
|
169
|
+
if (normalized.includes("## Open Questions\n")) {
|
|
170
|
+
return `${normalized}\n${text}\n`;
|
|
171
|
+
}
|
|
172
|
+
return `${normalized}\n\n## Open Questions\n${text}\n`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function pickCommitForRepo(orchestrator: Orchestrator, ctx: any, repo: RepoInfo): Promise<string | null> {
|
|
176
|
+
let commits: Array<{ hash: string; message: string; age: string }> = [];
|
|
177
|
+
try {
|
|
178
|
+
const logResult = await orchestrator.pi.exec(
|
|
179
|
+
"git", ["log", "--oneline", "--format=%h\t%s\t%cr", "-30"],
|
|
180
|
+
{ cwd: repo.path, timeout: 5000 },
|
|
181
|
+
);
|
|
182
|
+
if (logResult.code === 0 && logResult.stdout.trim()) {
|
|
183
|
+
commits = logResult.stdout.trim().split("\n").map((line) => {
|
|
184
|
+
const [hash, message, age] = line.split("\t");
|
|
185
|
+
return { hash: hash || "", message: message || "", age: age || "" };
|
|
186
|
+
}).filter((c) => c.hash);
|
|
187
|
+
}
|
|
188
|
+
} catch {}
|
|
189
|
+
if (commits.length === 0) {
|
|
190
|
+
ctx.ui.notify(`No commits found in ${repo.path}.`, "info");
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const commitOptions: OptionInput[] = commits.map((c) => ({
|
|
194
|
+
title: `${c.hash} ${c.message}`,
|
|
195
|
+
description: c.age,
|
|
196
|
+
}));
|
|
197
|
+
commitOptions.push({ title: "Back", description: "Return to the previous menu" });
|
|
198
|
+
const picked = await selectOption(ctx, `Review changes since (${repo.path}):`, commitOptions);
|
|
199
|
+
if (!picked || picked === "Back") return null;
|
|
200
|
+
const pickedHash = picked.split(" ")[0];
|
|
201
|
+
return pickedHash || null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface RepoPrContext {
|
|
205
|
+
repoPath: string;
|
|
206
|
+
prUrl: string | null;
|
|
207
|
+
prContext: string | null;
|
|
208
|
+
}
|
|
209
|
+
|
|
56
210
|
function setStep(orchestrator: Orchestrator, step: string): void {
|
|
57
211
|
if (!orchestrator.active) return;
|
|
58
212
|
orchestrator.active.state.step = step;
|
|
@@ -75,7 +229,7 @@ function showStatus(orchestrator: Orchestrator, ctx: any): void {
|
|
|
75
229
|
|
|
76
230
|
async function abortCurrentWork(orchestrator: Orchestrator, ctx: any): Promise<void> {
|
|
77
231
|
orchestrator.abortAllSubagents();
|
|
78
|
-
ctx.abort?.();
|
|
232
|
+
orchestrator.transitionController.abortMainAgent(ctx.abort?.bind(ctx));
|
|
79
233
|
await ctx.waitForIdle?.();
|
|
80
234
|
const taskStore = (globalThis as any)[Symbol.for("pi-tasks:store")];
|
|
81
235
|
taskStore?.clearAll?.();
|
|
@@ -87,11 +241,13 @@ async function pauseTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
87
241
|
|
|
88
242
|
cancelPendingPlannotatorWait(orchestrator);
|
|
89
243
|
orchestrator.abortAllSubagents();
|
|
90
|
-
ctx.abort?.();
|
|
244
|
+
orchestrator.transitionController.abortMainAgent(ctx.abort?.bind(ctx));
|
|
91
245
|
await ctx.waitForIdle?.();
|
|
92
246
|
|
|
93
247
|
const name = orchestrator.active.description;
|
|
248
|
+
const type = orchestrator.active.type;
|
|
94
249
|
|
|
250
|
+
orchestrator.active.state.reviewCycle = null;
|
|
95
251
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
96
252
|
unregisterAgentDefinitions(orchestrator.pi);
|
|
97
253
|
await orchestrator.cleanupActive();
|
|
@@ -100,7 +256,15 @@ async function pauseTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
100
256
|
taskStore?.clearAll?.();
|
|
101
257
|
taskStore?.refreshWidget?.(ctx.ui);
|
|
102
258
|
|
|
259
|
+
orchestrator.lastCtx = ctx;
|
|
103
260
|
orchestrator.updateStatus(ctx);
|
|
261
|
+
// Route through the controller as a "done" target (same coordinator as every
|
|
262
|
+
// other compaction). The agent was aborted above, so this compacts via the
|
|
263
|
+
// already-idle path; the awaitable resolves at every terminus.
|
|
264
|
+
await orchestrator.transitionController.requestTransition({
|
|
265
|
+
kind: "done",
|
|
266
|
+
summary: `Task "${name}" (${type}) paused.`,
|
|
267
|
+
});
|
|
104
268
|
ctx.ui.notify(`Task "${name}" paused. Use /pp → Resume to continue.`, "info");
|
|
105
269
|
return `Task "${name}" paused.`;
|
|
106
270
|
}
|
|
@@ -110,17 +274,17 @@ async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
110
274
|
|
|
111
275
|
cancelPendingPlannotatorWait(orchestrator);
|
|
112
276
|
orchestrator.abortAllSubagents();
|
|
113
|
-
ctx.abort?.();
|
|
277
|
+
orchestrator.transitionController.abortMainAgent(ctx.abort?.bind(ctx));
|
|
114
278
|
await ctx.waitForIdle?.();
|
|
115
279
|
|
|
116
280
|
const name = orchestrator.active.description;
|
|
117
281
|
const type = orchestrator.active.type;
|
|
118
282
|
const dir = orchestrator.active.dir;
|
|
119
283
|
|
|
120
|
-
orchestrator.
|
|
121
|
-
orchestrator.taskDoneCompactionSummary = `Task "${name}" (${type}) completed.`;
|
|
284
|
+
orchestrator.lastCtx = ctx;
|
|
122
285
|
|
|
123
286
|
orchestrator.active.state.phase = "done";
|
|
287
|
+
orchestrator.active.state.reviewCycle = null;
|
|
124
288
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
125
289
|
unregisterAgentDefinitions(orchestrator.pi);
|
|
126
290
|
await orchestrator.cleanupActive();
|
|
@@ -130,7 +294,12 @@ async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
130
294
|
taskStore?.refreshWidget?.(ctx.ui);
|
|
131
295
|
|
|
132
296
|
orchestrator.updateStatus(ctx);
|
|
133
|
-
|
|
297
|
+
// Route through the controller as a "done" target (fire-and-forget: the task
|
|
298
|
+
// is over, nothing awaits this compaction).
|
|
299
|
+
void orchestrator.transitionController.requestTransition({
|
|
300
|
+
kind: "done",
|
|
301
|
+
summary: `Task "${name}" (${type}) completed.`,
|
|
302
|
+
});
|
|
134
303
|
|
|
135
304
|
const urExists = existsSync(join(dir, "USER_REQUEST.md"));
|
|
136
305
|
const resExists = existsSync(join(dir, "RESEARCH.md"));
|
|
@@ -150,10 +319,59 @@ async function finishTask(orchestrator: Orchestrator, ctx: any): Promise<string>
|
|
|
150
319
|
|
|
151
320
|
function loadPhaseReviewOutputs(taskDir: string, phase: string, pass: number): { name: string; content: string }[] {
|
|
152
321
|
if (phase === "brainstorm") return loadBrainstormReviewOutputs(taskDir, pass);
|
|
153
|
-
if (phase === "plan") return loadPlanReviewOutputs(taskDir);
|
|
322
|
+
if (phase === "plan") return loadPlanReviewOutputs(taskDir, pass);
|
|
154
323
|
return loadCodeReviewOutputs(taskDir, pass);
|
|
155
324
|
}
|
|
156
325
|
|
|
326
|
+
function getDefaultReviewPresetName(config: PiPiConfig, phase: string): string {
|
|
327
|
+
if (phase === "brainstorm") return config.agents.subagents.presetGroups.brainstormReviewers.default;
|
|
328
|
+
if (phase === "plan") return config.agents.subagents.presetGroups.planReviewers.default;
|
|
329
|
+
return config.agents.subagents.presetGroups.codeReviewers.default;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function isPresetEnabled(preset: { enabled?: boolean } | undefined): boolean {
|
|
333
|
+
return preset?.enabled !== false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function getReviewPresetGroup(phase: string): PresetGroup {
|
|
337
|
+
if (phase === "brainstorm") return "brainstormReviewers";
|
|
338
|
+
if (phase === "plan") return "planReviewers";
|
|
339
|
+
return "codeReviewers";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export async function pickPreset(
|
|
343
|
+
ctx: any,
|
|
344
|
+
orchestrator: Orchestrator,
|
|
345
|
+
group: PresetGroup,
|
|
346
|
+
title: string,
|
|
347
|
+
): Promise<string | null> {
|
|
348
|
+
const presets = orchestrator.config.agents.subagents.presetGroups[group].presets ?? {};
|
|
349
|
+
const defaultPresetName = orchestrator.config.agents.subagents.presetGroups[group].default;
|
|
350
|
+
|
|
351
|
+
const options: OptionInput[] = [];
|
|
352
|
+
const byTitle = new Map<string, string>();
|
|
353
|
+
|
|
354
|
+
for (const [presetName, preset] of Object.entries(presets)) {
|
|
355
|
+
if (!isPresetEnabled(preset)) continue;
|
|
356
|
+
const enabledModels = Object.values(preset.agents)
|
|
357
|
+
.filter((variant) => isEnabled(variant))
|
|
358
|
+
.map((variant) => {
|
|
359
|
+
const info = getModelInfo(variant.model);
|
|
360
|
+
return `${info.vendor}/${info.family} (${variant.thinking})`;
|
|
361
|
+
});
|
|
362
|
+
const description = enabledModels.length > 0 ? enabledModels.join(", ") : "No enabled models";
|
|
363
|
+
const optionTitle = presetName === defaultPresetName ? `${presetName} [default]` : presetName;
|
|
364
|
+
byTitle.set(optionTitle, presetName);
|
|
365
|
+
options.push({ title: optionTitle, description });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
options.push({ title: "Back", description: "Return to the previous menu" });
|
|
369
|
+
|
|
370
|
+
const choice = await selectOption(ctx, title, options);
|
|
371
|
+
if (!choice || choice === "Back") return null;
|
|
372
|
+
return byTitle.get(choice) ?? null;
|
|
373
|
+
}
|
|
374
|
+
|
|
157
375
|
export async function resumeTask(
|
|
158
376
|
orchestrator: Orchestrator,
|
|
159
377
|
ctx: any,
|
|
@@ -171,9 +389,9 @@ export async function resumeTask(
|
|
|
171
389
|
|
|
172
390
|
let release: (() => Promise<void>) | null = null;
|
|
173
391
|
try {
|
|
174
|
-
release = await lockTask(task.dir, orchestrator.config.
|
|
392
|
+
release = await lockTask(task.dir, orchestrator.config.performance.internals);
|
|
175
393
|
} catch {
|
|
176
|
-
const staleSeconds = Math.round(orchestrator.config.
|
|
394
|
+
const staleSeconds = Math.round(orchestrator.config.performance.internals.taskLockStale / 1000);
|
|
177
395
|
const message =
|
|
178
396
|
`Cannot resume: task is locked by another pi session (or a session that crashed less than ${staleSeconds}s ago). ` +
|
|
179
397
|
`Wait ${staleSeconds}s for the lock to expire, or kill the other session first.`;
|
|
@@ -184,6 +402,39 @@ export async function resumeTask(
|
|
|
184
402
|
orchestrator.resetTaskScopedState();
|
|
185
403
|
orchestrator.activeTaskToken++;
|
|
186
404
|
|
|
405
|
+
const normalizedRoot = normalizeRepoPath(orchestrator.cwd);
|
|
406
|
+
if (!task.state.repos || task.state.repos.length === 0) {
|
|
407
|
+
task.state.repos = [{ path: normalizedRoot, isRoot: true }];
|
|
408
|
+
saveTask(task.dir, task.state);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!task.state.repos.some((repo) => repo.isRoot)) {
|
|
412
|
+
const rootByPath = task.state.repos.find((repo) => repo.path === normalizedRoot);
|
|
413
|
+
if (rootByPath) {
|
|
414
|
+
rootByPath.isRoot = true;
|
|
415
|
+
} else if (task.state.repos.length > 0) {
|
|
416
|
+
task.state.repos[0]!.isRoot = true;
|
|
417
|
+
} else {
|
|
418
|
+
task.state.repos = [{ path: normalizedRoot, isRoot: true }];
|
|
419
|
+
}
|
|
420
|
+
saveTask(task.dir, task.state);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const validRepos = validateRepos(task.state.repos ?? []);
|
|
424
|
+
if ((task.state.repos?.length ?? 0) !== validRepos.length) {
|
|
425
|
+
const pruned = (task.state.repos?.length ?? 0) - validRepos.length;
|
|
426
|
+
task.state.repos = validRepos;
|
|
427
|
+
saveTask(task.dir, task.state);
|
|
428
|
+
ctx.ui.notify(`Pruned ${pruned} stale repo(s) that no longer exist.`, "warning");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (!task.state.repos || task.state.repos.length === 0) {
|
|
432
|
+
task.state.repos = [{ path: normalizedRoot, isRoot: true }];
|
|
433
|
+
saveTask(task.dir, task.state);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const needsRepoRegistrationPrompt = task.state.repos.some((repo) => !repo.baseBranch);
|
|
437
|
+
|
|
187
438
|
orchestrator.active = {
|
|
188
439
|
dir: task.dir,
|
|
189
440
|
type: task.type,
|
|
@@ -195,7 +446,11 @@ export async function resumeTask(
|
|
|
195
446
|
description: task.state.description,
|
|
196
447
|
};
|
|
197
448
|
|
|
198
|
-
|
|
449
|
+
addTaskDestination(task.dir);
|
|
450
|
+
setLogLevel(orchestrator.config.general.logLevel);
|
|
451
|
+
getLogger().info({ s: "task", dir: task.dir, type: task.type, phase: task.state.phase }, "task resumed");
|
|
452
|
+
|
|
453
|
+
const modelConfig = orchestrator.config.agents.orchestrators[
|
|
199
454
|
task.type === "debug" ? "debug"
|
|
200
455
|
: task.type === "brainstorm" ? "brainstorm"
|
|
201
456
|
: task.type === "review" ? "review"
|
|
@@ -215,32 +470,52 @@ export async function resumeTask(
|
|
|
215
470
|
|
|
216
471
|
if (orchestrator.active.state.phase === "plan" && orchestrator.active.state.step === "await_planners") {
|
|
217
472
|
const plansDir = join(orchestrator.active.dir, "plans");
|
|
218
|
-
const
|
|
473
|
+
const requestedPlannerPresetName = orchestrator.active.state.activePlannerPreset ?? orchestrator.config.agents.subagents.presetGroups.planners.default;
|
|
474
|
+
const plannerPresetExists = Object.prototype.hasOwnProperty.call(orchestrator.config.agents.subagents.presetGroups.planners.presets ?? {}, requestedPlannerPresetName);
|
|
475
|
+
const plannerPresetName = plannerPresetExists
|
|
476
|
+
? requestedPlannerPresetName
|
|
477
|
+
: (Object.keys(orchestrator.config.agents.subagents.presetGroups.planners.presets ?? {})[0] ?? requestedPlannerPresetName);
|
|
478
|
+
if (orchestrator.active.state.activePlannerPreset !== plannerPresetName) {
|
|
479
|
+
orchestrator.active.state.activePlannerPreset = plannerPresetName;
|
|
480
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
481
|
+
}
|
|
482
|
+
if (!plannerPresetExists && plannerPresetName !== requestedPlannerPresetName) {
|
|
483
|
+
ctx.ui.notify(
|
|
484
|
+
`Planner preset "${requestedPlannerPresetName}" not found. Falling back to "${plannerPresetName}".`,
|
|
485
|
+
"warning",
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
const plannerVariants = resolvePreset(orchestrator.config, "planners", plannerPresetName);
|
|
489
|
+
const enabledVariants = Object.entries(plannerVariants).filter(([, v]) => isEnabled(v));
|
|
219
490
|
const planFiles = existsSync(plansDir)
|
|
220
491
|
? readdirSync(plansDir).filter((f) => f.endsWith(".md") && !f.includes("synthesized") && !f.includes("review_"))
|
|
221
492
|
: [];
|
|
222
|
-
|
|
493
|
+
const completedVariants = new Set(planFiles.map((f) => f.replace(/^\d+_/, "").replace(/\.md$/, "")));
|
|
494
|
+
const hasAllEnabledVariants = enabledVariants.every(([name]) => completedVariants.has(name));
|
|
495
|
+
if (hasAllEnabledVariants) {
|
|
223
496
|
orchestrator.active.state.step = "synthesize";
|
|
224
497
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
225
498
|
} else {
|
|
226
|
-
const completedVariants = new Set(planFiles.map((f) => f.replace(/^\d+_/, "").replace(/\.md$/, "")));
|
|
227
499
|
const missingVariants = enabledVariants.filter(([name]) => !completedVariants.has(name));
|
|
228
500
|
if (missingVariants.length > 0) {
|
|
229
|
-
const missingConfig:
|
|
501
|
+
const missingConfig: typeof plannerVariants = {};
|
|
230
502
|
for (const [name, cfg] of missingVariants) missingConfig[name] = cfg;
|
|
231
|
-
const partialConfig = { ...orchestrator.config, planners: missingConfig };
|
|
232
503
|
orchestrator.pendingSubagentSpawns = missingVariants.length;
|
|
233
504
|
orchestrator.failedPlannerVariants = [];
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
orchestrator.
|
|
241
|
-
|
|
242
|
-
orchestrator.
|
|
243
|
-
|
|
505
|
+
const plannerSpawn = spawnPlanners(
|
|
506
|
+
pi,
|
|
507
|
+
orchestrator.cwd,
|
|
508
|
+
orchestrator.active.dir,
|
|
509
|
+
orchestrator.active.taskId,
|
|
510
|
+
orchestrator.config,
|
|
511
|
+
orchestrator.transitionController.phaseSend,
|
|
512
|
+
missingConfig,
|
|
513
|
+
orchestrator.active?.state.repos ?? [],
|
|
514
|
+
);
|
|
515
|
+
handleSpawnResult(orchestrator, plannerSpawn, {
|
|
516
|
+
kind: "planner",
|
|
517
|
+
logScope: "planner",
|
|
518
|
+
logMessage: "spawnPlanners failed",
|
|
244
519
|
});
|
|
245
520
|
} else {
|
|
246
521
|
orchestrator.active.state.step = "synthesize";
|
|
@@ -251,35 +526,56 @@ export async function resumeTask(
|
|
|
251
526
|
|
|
252
527
|
if (orchestrator.active.state.reviewCycle) {
|
|
253
528
|
const cycle = orchestrator.active.state.reviewCycle;
|
|
254
|
-
const reviewConfig = cycle.kind === "auto-deep" ? deepReviewConfig(orchestrator.config) : orchestrator.config;
|
|
255
529
|
const phase = orchestrator.active.state.phase;
|
|
530
|
+
const requestedReviewPresetName = orchestrator.active.state.activeReviewPreset
|
|
531
|
+
?? getDefaultReviewPresetName(orchestrator.config, phase);
|
|
532
|
+
const group = getReviewPresetGroup(phase);
|
|
533
|
+
const reviewPresetExists = Object.prototype.hasOwnProperty.call(orchestrator.config.agents.subagents.presetGroups[group].presets ?? {}, requestedReviewPresetName);
|
|
534
|
+
const presetName = reviewPresetExists
|
|
535
|
+
? requestedReviewPresetName
|
|
536
|
+
: (Object.keys(orchestrator.config.agents.subagents.presetGroups[group].presets ?? {})[0] ?? requestedReviewPresetName);
|
|
537
|
+
if (orchestrator.active.state.activeReviewPreset !== presetName) {
|
|
538
|
+
orchestrator.active.state.activeReviewPreset = presetName;
|
|
539
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
540
|
+
}
|
|
541
|
+
if (!reviewPresetExists && presetName !== requestedReviewPresetName) {
|
|
542
|
+
ctx.ui.notify(
|
|
543
|
+
`Review preset "${requestedReviewPresetName}" not found. Falling back to "${presetName}".`,
|
|
544
|
+
"warning",
|
|
545
|
+
);
|
|
546
|
+
}
|
|
256
547
|
const reviewers = phase === "brainstorm"
|
|
257
|
-
?
|
|
548
|
+
? resolvePreset(orchestrator.config, "brainstormReviewers", presetName)
|
|
258
549
|
: phase === "plan"
|
|
259
|
-
?
|
|
260
|
-
:
|
|
261
|
-
const reviewerCount = Object.values(reviewers).filter((v) => v
|
|
550
|
+
? resolvePreset(orchestrator.config, "planReviewers", presetName)
|
|
551
|
+
: resolvePreset(orchestrator.config, "codeReviewers", presetName);
|
|
552
|
+
const reviewerCount = Object.values(reviewers).filter((v) => isEnabled(v)).length;
|
|
262
553
|
|
|
263
|
-
if (
|
|
554
|
+
if (cycle.kind === "auto" && (cycle.step === "spawn_reviewers" || cycle.step === "await_reviewers")) {
|
|
264
555
|
const outputs = loadPhaseReviewOutputs(orchestrator.active.dir, phase, cycle.pass);
|
|
265
|
-
if (
|
|
556
|
+
if (reviewerCount === 0) {
|
|
557
|
+
orchestrator.active.state.reviewCycle = null;
|
|
558
|
+
orchestrator.active.state.step = "llm_work";
|
|
559
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
560
|
+
orchestrator.safeSendUserMessage("[PI-PI] No reviewers configured — nothing to review. Continue working.");
|
|
561
|
+
} else if (outputs.length >= reviewerCount) {
|
|
266
562
|
cycle.step = "apply_feedback";
|
|
267
563
|
orchestrator.active.state.step = "apply_feedback";
|
|
268
564
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
269
565
|
const rendered = outputs.map((o) => `=== ${o.name} ===\n${o.content}`).join("\n\n");
|
|
270
|
-
|
|
566
|
+
orchestrator.transitionController.sendCustom(
|
|
271
567
|
{
|
|
272
568
|
customType: "pp-review-ready",
|
|
273
569
|
content: `[PI-PI] Reviewer outputs are ready.\n\n${rendered}`,
|
|
274
570
|
display: false,
|
|
275
571
|
},
|
|
276
|
-
|
|
572
|
+
"context",
|
|
277
573
|
);
|
|
278
574
|
} else {
|
|
279
575
|
const completedVariants = new Set(
|
|
280
576
|
outputs.map((o) => o.name.replace(/^\d+_/, "").replace(/_round-\d+\.md$/, "").replace(/\.md$/, "")),
|
|
281
577
|
);
|
|
282
|
-
const enabledVariants = Object.entries(reviewers).filter(([, v]) => v
|
|
578
|
+
const enabledVariants = Object.entries(reviewers).filter(([, v]) => isEnabled(v));
|
|
283
579
|
const missingVariants = enabledVariants.filter(([name]) => !completedVariants.has(name));
|
|
284
580
|
|
|
285
581
|
if (missingVariants.length === 0) {
|
|
@@ -287,39 +583,69 @@ export async function resumeTask(
|
|
|
287
583
|
orchestrator.active.state.step = "apply_feedback";
|
|
288
584
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
289
585
|
const rendered = outputs.map((o) => `=== ${o.name} ===\n${o.content}`).join("\n\n");
|
|
290
|
-
|
|
586
|
+
orchestrator.transitionController.sendCustom(
|
|
291
587
|
{
|
|
292
588
|
customType: "pp-review-ready",
|
|
293
589
|
content: `[PI-PI] Reviewer outputs are ready.\n\n${rendered}`,
|
|
294
590
|
display: false,
|
|
295
591
|
},
|
|
296
|
-
|
|
592
|
+
"context",
|
|
297
593
|
);
|
|
298
594
|
} else {
|
|
299
|
-
const missingReviewerConfig:
|
|
595
|
+
const missingReviewerConfig: typeof reviewers = {};
|
|
300
596
|
for (const [name, cfg] of missingVariants) missingReviewerConfig[name] = cfg;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
: phase === "plan"
|
|
304
|
-
? { ...reviewConfig, planReviewers: missingReviewerConfig }
|
|
305
|
-
: { ...reviewConfig, codeReviewers: missingReviewerConfig };
|
|
597
|
+
orchestrator.active.state.activeReviewPreset = presetName;
|
|
598
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
306
599
|
orchestrator.pendingSubagentSpawns = missingVariants.length;
|
|
307
600
|
const spawnFn = phase === "brainstorm"
|
|
308
|
-
? () => spawnBrainstormReviewers(
|
|
601
|
+
? () => spawnBrainstormReviewers(
|
|
602
|
+
pi,
|
|
603
|
+
orchestrator.cwd,
|
|
604
|
+
orchestrator.active!.dir,
|
|
605
|
+
orchestrator.active!.taskId,
|
|
606
|
+
orchestrator.config,
|
|
607
|
+
cycle.pass,
|
|
608
|
+
orchestrator.transitionController.phaseSend,
|
|
609
|
+
missingReviewerConfig,
|
|
610
|
+
orchestrator.active?.state.repos ?? [],
|
|
611
|
+
)
|
|
309
612
|
: phase === "plan"
|
|
310
|
-
? () => spawnPlanReviewers(
|
|
311
|
-
|
|
613
|
+
? () => spawnPlanReviewers(
|
|
614
|
+
pi,
|
|
615
|
+
orchestrator.cwd,
|
|
616
|
+
orchestrator.active!.dir,
|
|
617
|
+
orchestrator.active!.taskId,
|
|
618
|
+
orchestrator.config,
|
|
619
|
+
cycle.pass,
|
|
620
|
+
orchestrator.transitionController.phaseSend,
|
|
621
|
+
missingReviewerConfig,
|
|
622
|
+
orchestrator.active?.state.repos ?? [],
|
|
623
|
+
)
|
|
624
|
+
: () => spawnCodeReviewers(
|
|
625
|
+
pi,
|
|
626
|
+
orchestrator.cwd,
|
|
627
|
+
orchestrator.active!.dir,
|
|
628
|
+
orchestrator.active!.taskId,
|
|
629
|
+
orchestrator.config,
|
|
630
|
+
cycle.pass,
|
|
631
|
+
phase,
|
|
632
|
+
orchestrator.transitionController.phaseSend,
|
|
633
|
+
missingReviewerConfig,
|
|
634
|
+
orchestrator.active?.state.repos ?? [],
|
|
635
|
+
);
|
|
312
636
|
orchestrator.failedReviewerVariants = [];
|
|
313
|
-
spawnFn()
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
637
|
+
handleSpawnResult(orchestrator, spawnFn(), {
|
|
638
|
+
kind: "reviewer",
|
|
639
|
+
logScope: "review",
|
|
640
|
+
logMessage: "spawn reviewers failed",
|
|
641
|
+
onSettled: (result) => {
|
|
642
|
+
if (result?.spawned === 0 && orchestrator.active?.state.reviewCycle?.step === "await_reviewers") {
|
|
643
|
+
orchestrator.active.state.reviewCycle = null;
|
|
644
|
+
orchestrator.active.state.step = "llm_work";
|
|
645
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
646
|
+
orchestrator.safeSendUserMessage("[PI-PI] No reviewer outputs were produced — nothing to review. Continue working.");
|
|
647
|
+
}
|
|
648
|
+
},
|
|
323
649
|
});
|
|
324
650
|
cycle.step = "await_reviewers";
|
|
325
651
|
orchestrator.active.state.step = "await_reviewers";
|
|
@@ -329,13 +655,13 @@ export async function resumeTask(
|
|
|
329
655
|
} else if (cycle.step === "apply_feedback") {
|
|
330
656
|
const outputs = loadPhaseReviewOutputs(orchestrator.active.dir, phase, cycle.pass);
|
|
331
657
|
const rendered = outputs.map((o) => `=== ${o.name} ===\n${o.content}`).join("\n\n");
|
|
332
|
-
|
|
658
|
+
orchestrator.transitionController.sendCustom(
|
|
333
659
|
{
|
|
334
660
|
customType: "pp-review-ready",
|
|
335
661
|
content: `[PI-PI] Review cycle is in apply_feedback step.\n\n${rendered}`,
|
|
336
662
|
display: false,
|
|
337
663
|
},
|
|
338
|
-
|
|
664
|
+
"context",
|
|
339
665
|
);
|
|
340
666
|
}
|
|
341
667
|
}
|
|
@@ -344,9 +670,15 @@ export async function resumeTask(
|
|
|
344
670
|
if (step === "await_planners" || step === "await_reviewers") {
|
|
345
671
|
ctx.ui.notify(`Resumed task. Awaiting subagents (${step}).`, "info");
|
|
346
672
|
} else if (step === "apply_feedback") {
|
|
347
|
-
|
|
673
|
+
orchestrator.safeSendUserMessage(`[PI-PI] Resumed ${orchestrator.active.state.phase} phase. Read reviewer outputs and apply feedback.`);
|
|
348
674
|
} else {
|
|
349
|
-
|
|
675
|
+
orchestrator.safeSendUserMessage(`[PI-PI] Resumed ${orchestrator.active.state.phase} phase. Continue working.`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (needsRepoRegistrationPrompt) {
|
|
679
|
+
orchestrator.safeSendUserMessage(
|
|
680
|
+
"[PI-PI] Register your repos using pp_register_repo (including the root) before continuing.",
|
|
681
|
+
);
|
|
350
682
|
}
|
|
351
683
|
|
|
352
684
|
return { ok: true };
|
|
@@ -356,10 +688,11 @@ function listCompletedFromTasks(cwd: string): TaskInfo[] {
|
|
|
356
688
|
const paused = new Set<string>([
|
|
357
689
|
...listTasks(cwd, "brainstorm").map((t) => t.dir),
|
|
358
690
|
...listTasks(cwd, "debug").map((t) => t.dir),
|
|
691
|
+
...listTasks(cwd, "review").map((t) => t.dir),
|
|
359
692
|
]);
|
|
360
693
|
const results: TaskInfo[] = [];
|
|
361
694
|
|
|
362
|
-
for (const type of ["brainstorm", "debug"] as TaskType[]) {
|
|
695
|
+
for (const type of ["brainstorm", "debug", "review"] as TaskType[]) {
|
|
363
696
|
const typeDir = join(cwd, ".pp", "state", type);
|
|
364
697
|
if (!existsSync(typeDir)) continue;
|
|
365
698
|
for (const entry of readdirSync(typeDir, { withFileTypes: true })) {
|
|
@@ -395,34 +728,6 @@ async function showSubagentsMenu(ctx: any): Promise<void> {
|
|
|
395
728
|
await api.showMenu(ctx);
|
|
396
729
|
}
|
|
397
730
|
|
|
398
|
-
async function showLspMenu(ctx: any): Promise<typeof BACK> {
|
|
399
|
-
while (true) {
|
|
400
|
-
const choice = await selectOption(ctx, "LSP", [
|
|
401
|
-
{ title: "Status", description: "Show detected language servers and their state" },
|
|
402
|
-
{ title: "Restart", description: "Stop all servers. They reinitialize on next use" },
|
|
403
|
-
{ title: "Back", description: "Return to the previous menu" },
|
|
404
|
-
]);
|
|
405
|
-
if (!choice || choice === "Back") return BACK;
|
|
406
|
-
|
|
407
|
-
const api = (globalThis as any)[Symbol.for("pi-lsp:api")] as {
|
|
408
|
-
status?: (menuCtx: any) => Promise<void>;
|
|
409
|
-
restart?: (menuCtx: any) => Promise<void>;
|
|
410
|
-
} | undefined;
|
|
411
|
-
|
|
412
|
-
if (!api?.status || !api?.restart) {
|
|
413
|
-
ctx.ui.notify("LSP API is not available.", "warning");
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (choice === "Status") {
|
|
418
|
-
await api.status(ctx);
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
await api.restart(ctx);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
731
|
function countFlantProviders(settings: FlantSettings): { anthropic: number; openai: number } {
|
|
427
732
|
const models = settings.cachedFlantModels ?? [];
|
|
428
733
|
const anthropic = models.filter((m) => m.startsWith("claude-")).length;
|
|
@@ -436,27 +741,36 @@ function collectRoleAssignments(config: Partial<PiPiConfig> | null): string[] {
|
|
|
436
741
|
if (typeof value === "string" && value.length > 0) out.push(`${key} = ${value}`);
|
|
437
742
|
};
|
|
438
743
|
|
|
439
|
-
add("
|
|
440
|
-
add("
|
|
441
|
-
add("
|
|
442
|
-
add("
|
|
744
|
+
add("agents.orchestrators.implement", config.agents?.orchestrators?.implement?.model);
|
|
745
|
+
add("agents.orchestrators.plan", config.agents?.orchestrators?.plan?.model);
|
|
746
|
+
add("agents.orchestrators.debug", config.agents?.orchestrators?.debug?.model);
|
|
747
|
+
add("agents.orchestrators.brainstorm", config.agents?.orchestrators?.brainstorm?.model);
|
|
748
|
+
add("agents.orchestrators.review", config.agents?.orchestrators?.review?.model);
|
|
749
|
+
|
|
750
|
+
const addPresetAssignments = (group: "planners" | "planReviewers" | "codeReviewers" | "brainstormReviewers") => {
|
|
751
|
+
const presets = config.agents?.subagents?.presetGroups?.[group]?.presets;
|
|
752
|
+
if (!presets || typeof presets !== "object") return;
|
|
753
|
+
for (const [presetName, preset] of Object.entries(presets)) {
|
|
754
|
+
if (!preset || typeof preset !== "object") continue;
|
|
755
|
+
const agents = (preset as any).agents;
|
|
756
|
+
if (!agents || typeof agents !== "object") continue;
|
|
757
|
+
if ((preset as any).enabled === false) continue;
|
|
758
|
+
for (const [name, variant] of Object.entries(agents as Record<string, any>)) {
|
|
759
|
+
if (variant?.enabled !== false) {
|
|
760
|
+
add(`agents.subagents.presetGroups.${group}.presets.${presetName}.agents.${name}`, variant.model);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
};
|
|
443
765
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (variant.enabled) add(`planReviewers.${name}`, variant.model);
|
|
449
|
-
}
|
|
450
|
-
for (const [name, variant] of Object.entries(config.codeReviewers ?? {})) {
|
|
451
|
-
if (variant.enabled) add(`codeReviewers.${name}`, variant.model);
|
|
452
|
-
}
|
|
453
|
-
for (const [name, variant] of Object.entries(config.brainstormReviewers ?? {})) {
|
|
454
|
-
if (variant.enabled) add(`brainstormReviewers.${name}`, variant.model);
|
|
455
|
-
}
|
|
766
|
+
addPresetAssignments("planners");
|
|
767
|
+
addPresetAssignments("planReviewers");
|
|
768
|
+
addPresetAssignments("codeReviewers");
|
|
769
|
+
addPresetAssignments("brainstormReviewers");
|
|
456
770
|
|
|
457
|
-
add("agents.explore", config.agents?.explore?.model);
|
|
458
|
-
add("agents.librarian", config.agents?.librarian?.model);
|
|
459
|
-
add("agents.task", config.agents?.task?.model);
|
|
771
|
+
add("agents.subagents.simple.explore", config.agents?.subagents?.simple?.explore?.model);
|
|
772
|
+
add("agents.subagents.simple.librarian", config.agents?.subagents?.simple?.librarian?.model);
|
|
773
|
+
add("agents.subagents.simple.task", config.agents?.subagents?.simple?.task?.model);
|
|
460
774
|
return out;
|
|
461
775
|
}
|
|
462
776
|
|
|
@@ -574,7 +888,7 @@ function formatTokenCount(count: number): string {
|
|
|
574
888
|
return `${Math.round(count / 1000000)}M`;
|
|
575
889
|
}
|
|
576
890
|
|
|
577
|
-
function
|
|
891
|
+
function formatElapsedDuration(ms: number): string {
|
|
578
892
|
const sec = Math.floor(ms / 1000);
|
|
579
893
|
if (sec < 60) return `${sec}s`;
|
|
580
894
|
const min = Math.floor(sec / 60);
|
|
@@ -594,8 +908,8 @@ function showUsage(ctx: any): void {
|
|
|
594
908
|
getMainInputTokens(): number; getMainOutputTokens(): number;
|
|
595
909
|
getMainCacheReadTokens(): number; getMainCacheWriteTokens(): number;
|
|
596
910
|
getMainCost(): number;
|
|
597
|
-
getPerModelUsage(): Record<string, { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; turns: number }>;
|
|
598
|
-
getSubagentList(): Array<{ description: string; agentType: string; modelId: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; cost: number; durationMs: number; toolUses: number }>;
|
|
911
|
+
getPerModelUsage(): Record<string, { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; cacheSupported: boolean; turns: number }>;
|
|
912
|
+
getSubagentList(): Array<{ description: string; agentType: string; modelId: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; cacheSupported: boolean; cost: number; durationMs: number; toolUses: number }>;
|
|
599
913
|
}
|
|
600
914
|
| undefined;
|
|
601
915
|
|
|
@@ -619,7 +933,7 @@ function showUsage(ctx: any): void {
|
|
|
619
933
|
const models = tracker.getPerModelUsage();
|
|
620
934
|
const subagents = tracker.getSubagentList();
|
|
621
935
|
|
|
622
|
-
const byModel = new Map<string, { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }>();
|
|
936
|
+
const byModel = new Map<string, { input: number; output: number; cacheRead: number; cacheWrite: number; cacheSupported: boolean; cost: number }>();
|
|
623
937
|
const mainModelEntries = Object.entries(models);
|
|
624
938
|
const mainTotalTokens = mainModelEntries.reduce((s, [, u]) => s + u.inputTokens + u.outputTokens, 0);
|
|
625
939
|
for (const [modelId, usage] of mainModelEntries) {
|
|
@@ -627,7 +941,7 @@ function showUsage(ctx: any): void {
|
|
|
627
941
|
const modelCostShare = mainTotalTokens > 0 ? mainCost * (modelTokens / mainTotalTokens) : 0;
|
|
628
942
|
byModel.set(modelId, {
|
|
629
943
|
input: usage.inputTokens, output: usage.outputTokens,
|
|
630
|
-
cacheRead: usage.cacheReadTokens, cacheWrite: usage.cacheWriteTokens, cost: modelCostShare,
|
|
944
|
+
cacheRead: usage.cacheReadTokens, cacheWrite: usage.cacheWriteTokens, cacheSupported: usage.cacheSupported, cost: modelCostShare,
|
|
631
945
|
});
|
|
632
946
|
}
|
|
633
947
|
for (const sa of subagents) {
|
|
@@ -638,11 +952,12 @@ function showUsage(ctx: any): void {
|
|
|
638
952
|
existing.output += sa.outputTokens;
|
|
639
953
|
existing.cacheRead += sa.cacheReadTokens;
|
|
640
954
|
existing.cacheWrite += sa.cacheWriteTokens;
|
|
955
|
+
if (sa.cacheSupported) existing.cacheSupported = true;
|
|
641
956
|
existing.cost += sa.cost;
|
|
642
957
|
} else {
|
|
643
958
|
byModel.set(key, {
|
|
644
959
|
input: sa.inputTokens, output: sa.outputTokens,
|
|
645
|
-
cacheRead: sa.cacheReadTokens, cacheWrite: sa.cacheWriteTokens, cost: sa.cost,
|
|
960
|
+
cacheRead: sa.cacheReadTokens, cacheWrite: sa.cacheWriteTokens, cacheSupported: sa.cacheSupported, cost: sa.cost,
|
|
646
961
|
});
|
|
647
962
|
}
|
|
648
963
|
}
|
|
@@ -659,7 +974,7 @@ function showUsage(ctx: any): void {
|
|
|
659
974
|
for (const [modelId, m] of byModel) {
|
|
660
975
|
const cr = (m.cacheRead + m.input) > 0 ? Math.round(m.cacheRead / (m.cacheRead + m.input) * 100) : 0;
|
|
661
976
|
const parts = [`↑${formatTokenCount(m.input)}`, `↓${formatTokenCount(m.output)}`];
|
|
662
|
-
if (
|
|
977
|
+
if (m.cacheSupported) parts.push(`⚡${cr}%`);
|
|
663
978
|
if (m.cost > 0) parts.push(`$${m.cost.toFixed(2)}`);
|
|
664
979
|
lines.push(` ${modelId}: ${parts.join(" ")}`);
|
|
665
980
|
}
|
|
@@ -669,13 +984,14 @@ function showUsage(ctx: any): void {
|
|
|
669
984
|
lines.push("By agent:");
|
|
670
985
|
const agentModelNames = Object.keys(models);
|
|
671
986
|
if (agentModelNames.length > 0) {
|
|
987
|
+
const mainCacheSupported = mainModelEntries.some(([, u]) => u.cacheSupported);
|
|
672
988
|
const mainParts = [`↑${formatTokenCount(mainInput)}`, `↓${formatTokenCount(mainOutput)}`];
|
|
673
989
|
const mainCR = (mainCacheRead + mainInput) > 0 ? Math.round(mainCacheRead / (mainCacheRead + mainInput) * 100) : 0;
|
|
674
|
-
if (
|
|
990
|
+
if (mainCacheSupported) mainParts.push(`⚡${mainCR}%`);
|
|
675
991
|
if (mainCost > 0) mainParts.push(`$${mainCost.toFixed(2)}`);
|
|
676
992
|
lines.push(` Main (${agentModelNames.join(", ")}): ${mainParts.join(" ")}`);
|
|
677
993
|
}
|
|
678
|
-
const byAgentType = new Map<string, { input: number; output: number; cacheRead: number; cost: number; durationMs: number; toolUses: number; count: number }>();
|
|
994
|
+
const byAgentType = new Map<string, { input: number; output: number; cacheRead: number; cacheSupported: boolean; cost: number; durationMs: number; toolUses: number; count: number }>();
|
|
679
995
|
for (const sa of subagents) {
|
|
680
996
|
const key = sa.agentType || sa.description;
|
|
681
997
|
const existing = byAgentType.get(key);
|
|
@@ -683,6 +999,7 @@ function showUsage(ctx: any): void {
|
|
|
683
999
|
existing.input += sa.inputTokens;
|
|
684
1000
|
existing.output += sa.outputTokens;
|
|
685
1001
|
existing.cacheRead += sa.cacheReadTokens;
|
|
1002
|
+
if (sa.cacheSupported) existing.cacheSupported = true;
|
|
686
1003
|
existing.cost += sa.cost;
|
|
687
1004
|
existing.durationMs += sa.durationMs;
|
|
688
1005
|
existing.toolUses += sa.toolUses;
|
|
@@ -690,7 +1007,7 @@ function showUsage(ctx: any): void {
|
|
|
690
1007
|
} else {
|
|
691
1008
|
byAgentType.set(key, {
|
|
692
1009
|
input: sa.inputTokens, output: sa.outputTokens, cacheRead: sa.cacheReadTokens,
|
|
693
|
-
cost: sa.cost, durationMs: sa.durationMs, toolUses: sa.toolUses, count: 1,
|
|
1010
|
+
cacheSupported: sa.cacheSupported, cost: sa.cost, durationMs: sa.durationMs, toolUses: sa.toolUses, count: 1,
|
|
694
1011
|
});
|
|
695
1012
|
}
|
|
696
1013
|
}
|
|
@@ -698,9 +1015,9 @@ function showUsage(ctx: any): void {
|
|
|
698
1015
|
const saCR = (agg.cacheRead + agg.input) > 0
|
|
699
1016
|
? Math.round(agg.cacheRead / (agg.cacheRead + agg.input) * 100) : 0;
|
|
700
1017
|
const parts = [`↑${formatTokenCount(agg.input)}`, `↓${formatTokenCount(agg.output)}`];
|
|
701
|
-
if (
|
|
1018
|
+
if (agg.cacheSupported) parts.push(`⚡${saCR}%`);
|
|
702
1019
|
if (agg.cost > 0) parts.push(`$${agg.cost.toFixed(2)}`);
|
|
703
|
-
if (agg.durationMs > 0) parts.push(
|
|
1020
|
+
if (agg.durationMs > 0) parts.push(formatElapsedDuration(agg.durationMs));
|
|
704
1021
|
if (agg.toolUses > 0) parts.push(`${agg.toolUses} tools`);
|
|
705
1022
|
const countSuffix = agg.count > 1 ? ` (×${agg.count})` : "";
|
|
706
1023
|
lines.push(` ${agentType}${countSuffix}: ${parts.join(" ")}`);
|
|
@@ -709,220 +1026,1909 @@ function showUsage(ctx: any): void {
|
|
|
709
1026
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
710
1027
|
}
|
|
711
1028
|
|
|
712
|
-
async function
|
|
1029
|
+
async function showInfoMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
713
1030
|
while (true) {
|
|
714
1031
|
const options: OptionInput[] = [];
|
|
1032
|
+
options.push({ title: "Subagents", description: "Manage running agents" });
|
|
1033
|
+
options.push({ title: "Usage", description: "Show session token usage and cost breakdown" });
|
|
1034
|
+
options.push({ title: "Doctor", description: "Run diagnostic checks" });
|
|
715
1035
|
if (orchestrator.active) {
|
|
716
1036
|
options.push({ title: "Task status", description: "Show current task phase, step, and timing" });
|
|
717
|
-
|
|
718
|
-
options.push({ title: "Usage", description: "Show session token usage and cost breakdown" });
|
|
719
|
-
options.push({ title: "LSP", description: "Language server status and controls" });
|
|
720
|
-
if (showFlant) {
|
|
721
|
-
options.push({ title: "Flant AI Infrastructure", description: "Configure corporate AI model provider" });
|
|
1037
|
+
options.push({ title: "Repos", description: "Registered repositories and base branches" });
|
|
722
1038
|
}
|
|
723
1039
|
options.push({ title: "Back", description: "Return to the previous menu" });
|
|
724
1040
|
|
|
725
|
-
const choice = await selectOption(ctx, "
|
|
1041
|
+
const choice = await selectOption(ctx, "Info", options);
|
|
726
1042
|
if (!choice || choice === "Back") return BACK;
|
|
727
|
-
if (choice === "
|
|
728
|
-
|
|
1043
|
+
if (choice === "Subagents") {
|
|
1044
|
+
await showSubagentsMenu(ctx);
|
|
729
1045
|
continue;
|
|
730
1046
|
}
|
|
731
1047
|
if (choice === "Usage") {
|
|
732
1048
|
showUsage(ctx);
|
|
733
1049
|
continue;
|
|
734
1050
|
}
|
|
735
|
-
if (choice === "
|
|
736
|
-
await
|
|
1051
|
+
if (choice === "Doctor") {
|
|
1052
|
+
await runDoctor(orchestrator, ctx);
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
if (choice === "Task status") {
|
|
1056
|
+
showStatus(orchestrator, ctx);
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
if (choice === "Repos") {
|
|
1060
|
+
await showReposSettings(orchestrator, ctx);
|
|
737
1061
|
continue;
|
|
738
1062
|
}
|
|
739
|
-
await showFlantInfraMenu(orchestrator, ctx);
|
|
740
1063
|
}
|
|
741
1064
|
}
|
|
742
1065
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1066
|
+
type Scope = "global" | "project";
|
|
1067
|
+
type MainModelRole = keyof PiPiConfig["agents"]["orchestrators"];
|
|
1068
|
+
type AgentRole = keyof PiPiConfig["agents"]["subagents"]["simple"];
|
|
1069
|
+
type TimeoutGroup = "commands" | "internals";
|
|
1070
|
+
type CommandListKey = keyof PiPiConfig["commands"];
|
|
1071
|
+
type TimeoutEntry = { key: TimeoutKey; path: string[]; value: number };
|
|
1072
|
+
|
|
1073
|
+
export interface ConfigSourceInfo {
|
|
1074
|
+
activeValue: any;
|
|
1075
|
+
defaultValue: any;
|
|
1076
|
+
flantValue: any | undefined;
|
|
1077
|
+
globalValue: any | undefined;
|
|
1078
|
+
projectValue: any | undefined;
|
|
1079
|
+
source: "default" | "flant" | "global" | "project";
|
|
748
1080
|
}
|
|
749
1081
|
|
|
750
|
-
|
|
751
|
-
|
|
1082
|
+
const ORCHESTRATOR_ROLES: Array<{ role: MainModelRole; label: string; description: string }> = [
|
|
1083
|
+
{ role: "brainstorm", label: "Brainstormer", description: "agents.orchestrators.brainstorm" },
|
|
1084
|
+
{ role: "implement", label: "Implementer", description: "agents.orchestrators.implement" },
|
|
1085
|
+
{ role: "plan", label: "Planner", description: "agents.orchestrators.plan" },
|
|
1086
|
+
{ role: "debug", label: "Debugger", description: "agents.orchestrators.debug" },
|
|
1087
|
+
{ role: "review", label: "Reviewer", description: "agents.orchestrators.review" },
|
|
1088
|
+
];
|
|
1089
|
+
|
|
1090
|
+
const SUBAGENT_ROLES: Array<{ role: AgentRole; label: string; description: string }> = [
|
|
1091
|
+
{ role: "explore", label: "Explore", description: "agents.subagents.simple.explore" },
|
|
1092
|
+
{ role: "librarian", label: "Librarian", description: "agents.subagents.simple.librarian" },
|
|
1093
|
+
{ role: "task", label: "Task", description: "agents.subagents.simple.task" },
|
|
1094
|
+
];
|
|
1095
|
+
|
|
1096
|
+
const PRESET_GROUP_ITEMS: Array<{ group: PresetGroup; label: string }> = [
|
|
1097
|
+
{ group: "brainstormReviewers", label: "Brainstorm reviewers" },
|
|
1098
|
+
{ group: "planners", label: "Planners" },
|
|
1099
|
+
{ group: "planReviewers", label: "Plan reviewers" },
|
|
1100
|
+
{ group: "codeReviewers", label: "Code reviewers" },
|
|
1101
|
+
];
|
|
1102
|
+
|
|
1103
|
+
const TIMEOUT_LABELS: Record<TimeoutKey, string> = {
|
|
1104
|
+
"performance.commands.afterEdit": "Command after file edit",
|
|
1105
|
+
"performance.commands.afterImplement": "Command after implementation",
|
|
1106
|
+
"performance.internals.subagentStale": "Subagent stale",
|
|
1107
|
+
"performance.internals.taskLockStale": "Lock stale",
|
|
1108
|
+
"performance.internals.taskLockRefresh": "Lock update",
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
const VALID_NAME_RE = /^[A-Za-z0-9-]+$/;
|
|
1112
|
+
|
|
1113
|
+
function getProjectConfigPath(cwd: string): string {
|
|
1114
|
+
return join(cwd, ".pp", "config.json");
|
|
752
1115
|
}
|
|
753
1116
|
|
|
754
|
-
function
|
|
755
|
-
|
|
756
|
-
const phase = t.state.phase === t.type ? t.type : `${t.type}/${t.state.phase}`;
|
|
757
|
-
return `${phase}, ${age}`;
|
|
1117
|
+
function getScopeConfigPath(orchestrator: Orchestrator, scope: Scope): string {
|
|
1118
|
+
return scope === "global" ? GLOBAL_CONFIG_PATH : getProjectConfigPath(orchestrator.cwd);
|
|
758
1119
|
}
|
|
759
1120
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
ctx.ui.notify(emptyMessage, "info");
|
|
770
|
-
return BACK;
|
|
771
|
-
}
|
|
1121
|
+
function hasNestedKey(obj: unknown, keyPath: string[]): boolean {
|
|
1122
|
+
let cursor: any = obj;
|
|
1123
|
+
for (const key of keyPath) {
|
|
1124
|
+
if (!cursor || typeof cursor !== "object") return false;
|
|
1125
|
+
if (!Object.prototype.hasOwnProperty.call(cursor, key)) return false;
|
|
1126
|
+
cursor = cursor[key];
|
|
1127
|
+
}
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
772
1130
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1131
|
+
function getNestedValue(obj: unknown, keyPath: string[]): any {
|
|
1132
|
+
let cursor: any = obj;
|
|
1133
|
+
for (const key of keyPath) {
|
|
1134
|
+
if (!cursor || typeof cursor !== "object") return undefined;
|
|
1135
|
+
if (!Object.prototype.hasOwnProperty.call(cursor, key)) return undefined;
|
|
1136
|
+
cursor = cursor[key];
|
|
1137
|
+
}
|
|
1138
|
+
return cursor;
|
|
1139
|
+
}
|
|
778
1140
|
|
|
779
|
-
|
|
780
|
-
|
|
1141
|
+
function setNestedValue(obj: Record<string, any>, keyPath: string[], value: any): void {
|
|
1142
|
+
if (keyPath.length === 0) return;
|
|
1143
|
+
let cursor: Record<string, any> = obj;
|
|
1144
|
+
for (let i = 0; i < keyPath.length - 1; i += 1) {
|
|
1145
|
+
const key = keyPath[i]!;
|
|
1146
|
+
const current = cursor[key];
|
|
1147
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) cursor[key] = {};
|
|
1148
|
+
cursor = cursor[key] as Record<string, any>;
|
|
1149
|
+
}
|
|
1150
|
+
cursor[keyPath[keyPath.length - 1]!] = value;
|
|
1151
|
+
}
|
|
781
1152
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1153
|
+
function deleteNestedValue(obj: Record<string, any>, keyPath: string[]): void {
|
|
1154
|
+
if (keyPath.length === 0) return;
|
|
1155
|
+
let cursor: Record<string, any> = obj;
|
|
1156
|
+
for (let i = 0; i < keyPath.length - 1; i += 1) {
|
|
1157
|
+
const key = keyPath[i]!;
|
|
1158
|
+
const current = cursor[key];
|
|
1159
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return;
|
|
1160
|
+
cursor = current;
|
|
786
1161
|
}
|
|
1162
|
+
delete cursor[keyPath[keyPath.length - 1]!];
|
|
787
1163
|
}
|
|
788
1164
|
|
|
789
|
-
function
|
|
790
|
-
return
|
|
1165
|
+
function formatInlineValue(value: any): string {
|
|
1166
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
1167
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
|
|
1168
|
+
if (value === undefined) return "undefined";
|
|
1169
|
+
try {
|
|
1170
|
+
return JSON.stringify(value);
|
|
1171
|
+
} catch {
|
|
1172
|
+
return String(value);
|
|
1173
|
+
}
|
|
791
1174
|
}
|
|
792
1175
|
|
|
793
|
-
function
|
|
794
|
-
|
|
795
|
-
const rel = relative(join(cwd, ".pp", "state"), t.dir);
|
|
796
|
-
return `${t.type}, ${age} — ${rel}`;
|
|
1176
|
+
function tagsString(tags: string[]): string {
|
|
1177
|
+
return tags.length > 0 ? `(${tags.join(", ")})` : "";
|
|
797
1178
|
}
|
|
798
1179
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if (tasks.length === 0) {
|
|
803
|
-
ctx.ui.notify("No completed brainstorm/debug tasks with artifacts found.", "info");
|
|
804
|
-
return BACK;
|
|
805
|
-
}
|
|
1180
|
+
function withTags(label: string, tags: string): string {
|
|
1181
|
+
return tags ? `${label} ${tags}` : label;
|
|
1182
|
+
}
|
|
806
1183
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
options.push({ title: "Back", description: "Return to the previous menu" });
|
|
1184
|
+
function getRawScopeValue(orchestrator: Orchestrator, scope: Scope, keyPath: string[]): any {
|
|
1185
|
+
const raw = readRawConfig(getScopeConfigPath(orchestrator, scope));
|
|
1186
|
+
return getNestedValue(raw, keyPath);
|
|
1187
|
+
}
|
|
812
1188
|
|
|
813
|
-
|
|
814
|
-
|
|
1189
|
+
function getRawScopeConfigs(orchestrator: Orchestrator): { globalConfig: Record<string, any>; projectConfig: Record<string, any> } {
|
|
1190
|
+
return {
|
|
1191
|
+
globalConfig: readRawConfig(GLOBAL_CONFIG_PATH),
|
|
1192
|
+
projectConfig: readRawConfig(getProjectConfigPath(orchestrator.cwd)),
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
815
1195
|
|
|
816
|
-
|
|
817
|
-
|
|
1196
|
+
function isEmptyOverride(value: unknown): boolean {
|
|
1197
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value as object).length === 0;
|
|
1198
|
+
}
|
|
818
1199
|
|
|
819
|
-
|
|
820
|
-
|
|
1200
|
+
function hasLayerOverride(orchestrator: Orchestrator, scope: Scope, keyPath: string[]): boolean {
|
|
1201
|
+
const raw = readRawConfig(getScopeConfigPath(orchestrator, scope));
|
|
1202
|
+
if (!hasNestedKey(raw, keyPath)) return false;
|
|
1203
|
+
return !isEmptyOverride(getNestedValue(raw, keyPath));
|
|
1204
|
+
}
|
|
821
1205
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1206
|
+
function getLayerOverrideValue(orchestrator: Orchestrator, scope: Scope, keyPath: string[]): any {
|
|
1207
|
+
const raw = readRawConfig(getScopeConfigPath(orchestrator, scope));
|
|
1208
|
+
return getNestedValue(raw, keyPath);
|
|
825
1209
|
}
|
|
826
1210
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
{ title: "Back", description: "Return to the previous menu" },
|
|
834
|
-
]);
|
|
835
|
-
if (!choice || choice === "Back") return BACK;
|
|
1211
|
+
function getOwnedScopes(orchestrator: Orchestrator, keyPath: string[]): Scope[] {
|
|
1212
|
+
const out: Scope[] = [];
|
|
1213
|
+
if (hasLayerOverride(orchestrator, "global", keyPath)) out.push("global");
|
|
1214
|
+
if (hasLayerOverride(orchestrator, "project", keyPath)) out.push("project");
|
|
1215
|
+
return out;
|
|
1216
|
+
}
|
|
836
1217
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1218
|
+
export function getConfigSourceInfo(orchestrator: Orchestrator, keyPath: string[]): ConfigSourceInfo {
|
|
1219
|
+
const { globalConfig, projectConfig } = getRawScopeConfigs(orchestrator);
|
|
1220
|
+
const flantConfig = getFlantGeneratedConfig() as Record<string, any> | null;
|
|
1221
|
+
const activeValue = getNestedValue(orchestrator.config as Record<string, any>, keyPath);
|
|
1222
|
+
const defaultValue = getNestedValue(getDefaultConfig() as Record<string, any>, keyPath);
|
|
1223
|
+
const flantValue = flantConfig ? getNestedValue(flantConfig, keyPath) : undefined;
|
|
1224
|
+
const globalValue = hasNestedKey(globalConfig, keyPath) ? getNestedValue(globalConfig, keyPath) : undefined;
|
|
1225
|
+
const projectValue = hasNestedKey(projectConfig, keyPath) ? getNestedValue(projectConfig, keyPath) : undefined;
|
|
1226
|
+
const source = hasNestedKey(projectConfig, keyPath)
|
|
1227
|
+
? "project"
|
|
1228
|
+
: hasNestedKey(globalConfig, keyPath)
|
|
1229
|
+
? "global"
|
|
1230
|
+
: flantConfig && hasNestedKey(flantConfig, keyPath)
|
|
1231
|
+
? "flant"
|
|
1232
|
+
: "default";
|
|
1233
|
+
return {
|
|
1234
|
+
activeValue,
|
|
1235
|
+
defaultValue,
|
|
1236
|
+
flantValue,
|
|
1237
|
+
globalValue,
|
|
1238
|
+
projectValue,
|
|
1239
|
+
source,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
843
1242
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1243
|
+
export function formatSourceTags(currentValue: any, info: ConfigSourceInfo): string {
|
|
1244
|
+
const tags: string[] = [];
|
|
1245
|
+
if (isDeepStrictEqual(currentValue, info.activeValue)) tags.push("active");
|
|
1246
|
+
if (isDeepStrictEqual(currentValue, info.defaultValue)) tags.push("default");
|
|
1247
|
+
if (info.flantValue !== undefined && isDeepStrictEqual(currentValue, info.flantValue)) tags.push("flant");
|
|
1248
|
+
if (info.globalValue !== undefined && isDeepStrictEqual(currentValue, info.globalValue)) tags.push("global");
|
|
1249
|
+
if (info.projectValue !== undefined && isDeepStrictEqual(currentValue, info.projectValue)) tags.push("project");
|
|
1250
|
+
return tagsString(tags);
|
|
1251
|
+
}
|
|
849
1252
|
|
|
850
|
-
|
|
851
|
-
|
|
1253
|
+
export function buildResetOptions(orchestrator: Orchestrator, keyPath: string[]): OptionInput[] {
|
|
1254
|
+
const options: OptionInput[] = [];
|
|
1255
|
+
const globalValue = getLayerOverrideValue(orchestrator, "global", keyPath);
|
|
1256
|
+
if (hasLayerOverride(orchestrator, "global", keyPath) && !isEmptyOverride(globalValue)) {
|
|
1257
|
+
options.push(opt("Reset global setting", formatInlineValue(globalValue)));
|
|
852
1258
|
}
|
|
1259
|
+
const projectValue = getLayerOverrideValue(orchestrator, "project", keyPath);
|
|
1260
|
+
if (hasLayerOverride(orchestrator, "project", keyPath) && !isEmptyOverride(projectValue)) {
|
|
1261
|
+
options.push(opt("Reset project setting", formatInlineValue(projectValue)));
|
|
1262
|
+
}
|
|
1263
|
+
return options;
|
|
853
1264
|
}
|
|
854
1265
|
|
|
855
|
-
function
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
? parsed.comments.nodes
|
|
862
|
-
: [];
|
|
863
|
-
const comments: string[] = commentsRaw
|
|
864
|
-
.map((comment: any) => {
|
|
865
|
-
const text = typeof comment?.body === "string"
|
|
866
|
-
? comment.body.trim()
|
|
867
|
-
: typeof comment?.bodyText === "string"
|
|
868
|
-
? comment.bodyText.trim()
|
|
869
|
-
: "";
|
|
870
|
-
return text;
|
|
871
|
-
})
|
|
872
|
-
.filter((text: string): text is string => text.length > 0);
|
|
1266
|
+
export function formatDuration(ms: number): string {
|
|
1267
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
1268
|
+
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
|
|
1269
|
+
if (ms < 3600000) return `${Math.round(ms / 60000)}m`;
|
|
1270
|
+
return `${Math.round(ms / 3600000)}h`;
|
|
1271
|
+
}
|
|
873
1272
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}
|
|
1273
|
+
export function slugify(text: string, maxLen = 40): string {
|
|
1274
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
1275
|
+
const safe = collapsed.replace(/[^A-Za-z0-9 _./:-]/g, "").trim();
|
|
1276
|
+
const base = safe || collapsed || "(empty)";
|
|
1277
|
+
if (base.length <= maxLen) return base;
|
|
1278
|
+
return `${base.slice(0, Math.max(1, maxLen - 1)).trimEnd()}…`;
|
|
1279
|
+
}
|
|
880
1280
|
|
|
881
|
-
|
|
1281
|
+
function tryApplyConfigChange(orchestrator: Orchestrator, scope: Scope, keyPath: string[], value: any): { ok: boolean; error?: string } {
|
|
1282
|
+
try {
|
|
1283
|
+
const nextGlobal = structuredClone(readRawConfig(GLOBAL_CONFIG_PATH));
|
|
1284
|
+
const nextProject = structuredClone(readRawConfig(getProjectConfigPath(orchestrator.cwd)));
|
|
1285
|
+
const target = scope === "global" ? nextGlobal : nextProject;
|
|
1286
|
+
setNestedValue(target, keyPath, value);
|
|
1287
|
+
mergeConfigLayers(nextGlobal, nextProject);
|
|
1288
|
+
writeConfigValue(getScopeConfigPath(orchestrator, scope), keyPath, value);
|
|
1289
|
+
orchestrator.config = loadConfig(orchestrator.cwd);
|
|
1290
|
+
return { ok: true };
|
|
1291
|
+
} catch (err: any) {
|
|
1292
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function tryClearConfigOverride(orchestrator: Orchestrator, scope: Scope, keyPath: string[]): { ok: boolean; error?: string } {
|
|
1297
|
+
try {
|
|
1298
|
+
const nextGlobal = structuredClone(readRawConfig(GLOBAL_CONFIG_PATH));
|
|
1299
|
+
const nextProject = structuredClone(readRawConfig(getProjectConfigPath(orchestrator.cwd)));
|
|
1300
|
+
const target = scope === "global" ? nextGlobal : nextProject;
|
|
1301
|
+
deleteNestedValue(target, keyPath);
|
|
1302
|
+
mergeConfigLayers(nextGlobal, nextProject);
|
|
1303
|
+
removeConfigValue(getScopeConfigPath(orchestrator, scope), keyPath);
|
|
1304
|
+
orchestrator.config = loadConfig(orchestrator.cwd);
|
|
1305
|
+
return { ok: true };
|
|
1306
|
+
} catch (err: any) {
|
|
1307
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function parseCommaSeparated(input: string): string[] {
|
|
1312
|
+
return input
|
|
1313
|
+
.split(",")
|
|
1314
|
+
.map((item) => item.trim())
|
|
1315
|
+
.filter(Boolean);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function makeUniqueTitle(base: string, used: Set<string>): string {
|
|
1319
|
+
if (!used.has(base)) {
|
|
1320
|
+
used.add(base);
|
|
1321
|
+
return base;
|
|
1322
|
+
}
|
|
1323
|
+
let index = 2;
|
|
1324
|
+
while (true) {
|
|
1325
|
+
const candidate = `${base} (${index})`;
|
|
1326
|
+
if (!used.has(candidate)) {
|
|
1327
|
+
used.add(candidate);
|
|
1328
|
+
return candidate;
|
|
1329
|
+
}
|
|
1330
|
+
index += 1;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function normalizeProviderLabel(provider: string): string {
|
|
1335
|
+
if (provider === "anthropic") return "Anthropic";
|
|
1336
|
+
if (provider === "openai") return "OpenAI";
|
|
1337
|
+
if (provider === "google") return "Google";
|
|
1338
|
+
if (provider === "deepseek") return "DeepSeek";
|
|
1339
|
+
if (provider === "x-ai") return "xAI";
|
|
1340
|
+
if (provider === "qwen") return "Qwen";
|
|
1341
|
+
if (provider === "pp-flant-anthropic") return "Flant Anthropic";
|
|
1342
|
+
if (provider === "pp-flant-openai") return "Flant OpenAI";
|
|
1343
|
+
return provider;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function providerOrder(provider: string): number {
|
|
1347
|
+
if (provider === "pp-flant-anthropic") return 0;
|
|
1348
|
+
if (provider === "pp-flant-openai") return 1;
|
|
1349
|
+
if (provider === "anthropic") return 2;
|
|
1350
|
+
if (provider === "openai") return 3;
|
|
1351
|
+
if (provider === "google") return 4;
|
|
1352
|
+
if (provider === "deepseek") return 5;
|
|
1353
|
+
if (provider === "x-ai") return 6;
|
|
1354
|
+
if (provider === "qwen") return 7;
|
|
1355
|
+
return 99;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function listAvailableModels(ctx: any): Array<{ provider: string; id: string; spec: string }> {
|
|
1359
|
+
const available = ctx?.modelRegistry?.getAvailable?.();
|
|
1360
|
+
if (!Array.isArray(available)) return [];
|
|
1361
|
+
|
|
1362
|
+
const seen = new Set<string>();
|
|
1363
|
+
const models: Array<{ provider: string; id: string; spec: string }> = [];
|
|
1364
|
+
for (const model of available) {
|
|
1365
|
+
const provider = typeof model?.provider === "string" ? model.provider.trim() : "";
|
|
1366
|
+
const id = typeof model?.id === "string" ? model.id.trim() : "";
|
|
1367
|
+
if (!provider || !id) continue;
|
|
1368
|
+
const spec = `${provider}/${id}`;
|
|
1369
|
+
const key = spec.toLowerCase();
|
|
1370
|
+
if (seen.has(key)) continue;
|
|
1371
|
+
seen.add(key);
|
|
1372
|
+
models.push({ provider, id, spec });
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
models.sort((a, b) => {
|
|
1376
|
+
const byProviderOrder = providerOrder(a.provider) - providerOrder(b.provider);
|
|
1377
|
+
if (byProviderOrder !== 0) return byProviderOrder;
|
|
1378
|
+
const byProviderName = a.provider.localeCompare(b.provider);
|
|
1379
|
+
if (byProviderName !== 0) return byProviderName;
|
|
1380
|
+
return compareModelVersion(b.id, a.id);
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
return models;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
async function pickScope(ctx: any, orchestrator: Orchestrator): Promise<Scope | null> {
|
|
1387
|
+
const choice = await selectOption(ctx, "Scope", [
|
|
1388
|
+
opt("Set globally", GLOBAL_CONFIG_PATH),
|
|
1389
|
+
opt("Set for project", getProjectConfigPath(orchestrator.cwd)),
|
|
1390
|
+
opt("Back", "Return to the previous menu"),
|
|
1391
|
+
]);
|
|
1392
|
+
if (!choice || choice === "Back") return null;
|
|
1393
|
+
if (choice === "Set globally") return "global";
|
|
1394
|
+
if (choice === "Set for project") return "project";
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
async function pickScopeFromOwned(ctx: any, orchestrator: Orchestrator, keyPath: string[]): Promise<Scope | null> {
|
|
1399
|
+
const scopes = getOwnedScopes(orchestrator, keyPath);
|
|
1400
|
+
if (scopes.length === 0) return null;
|
|
1401
|
+
if (scopes.length === 1) return scopes[0]!;
|
|
1402
|
+
const options: OptionInput[] = [
|
|
1403
|
+
opt("Global override", GLOBAL_CONFIG_PATH),
|
|
1404
|
+
opt("Project override", getProjectConfigPath(orchestrator.cwd)),
|
|
1405
|
+
opt("Back", "Return to the previous menu"),
|
|
1406
|
+
];
|
|
1407
|
+
const choice = await selectOption(ctx, "Choose override scope", options);
|
|
1408
|
+
if (!choice || choice === "Back") return null;
|
|
1409
|
+
return choice === "Global override" ? "global" : "project";
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
async function pickModel(ctx: any, currentModel?: string): Promise<string | null> {
|
|
1413
|
+
const aliasMap = getAllAliases();
|
|
1414
|
+
const families = getModelFamilies();
|
|
1415
|
+
const availableModels = listAvailableModels(ctx);
|
|
1416
|
+
const availableSpecs = new Set(availableModels.map((m) => m.spec));
|
|
1417
|
+
const currentResolved = currentModel ? resolveModel(currentModel) : null;
|
|
1418
|
+
const currentAvailable = currentResolved ? availableSpecs.has(currentResolved) : false;
|
|
1419
|
+
const visibleAliases = new Set(
|
|
1420
|
+
Object.entries(aliasMap)
|
|
1421
|
+
.filter(([, resolved]) => availableSpecs.has(resolved))
|
|
1422
|
+
.map(([alias]) => alias),
|
|
1423
|
+
);
|
|
1424
|
+
const options: OptionInput[] = [];
|
|
1425
|
+
const selectionToModel = new Map<string, string>();
|
|
1426
|
+
const usedTitles = new Set<string>();
|
|
1427
|
+
|
|
1428
|
+
if (currentModel && !visibleAliases.has(currentModel)) {
|
|
1429
|
+
const tags = ["active"];
|
|
1430
|
+
if (!currentAvailable) tags.push("unavailable");
|
|
1431
|
+
const title = makeUniqueTitle(withTags(currentModel, tagsString(tags)), usedTitles);
|
|
1432
|
+
options.push(opt(title, "Current model"));
|
|
1433
|
+
selectionToModel.set(title, currentModel);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const aliasEntries: Array<{ provider: string; displayName: string; alias: string }> = [];
|
|
1437
|
+
for (const family of families) {
|
|
1438
|
+
for (const alias of family.aliases) {
|
|
1439
|
+
if (!visibleAliases.has(alias)) continue;
|
|
1440
|
+
aliasEntries.push({
|
|
1441
|
+
provider: alias.split("/")[0] ?? "",
|
|
1442
|
+
displayName: family.displayName,
|
|
1443
|
+
alias,
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
aliasEntries.sort((a, b) => {
|
|
1449
|
+
const byProviderOrder = providerOrder(a.provider) - providerOrder(b.provider);
|
|
1450
|
+
if (byProviderOrder !== 0) return byProviderOrder;
|
|
1451
|
+
const byProviderName = a.provider.localeCompare(b.provider);
|
|
1452
|
+
if (byProviderName !== 0) return byProviderName;
|
|
1453
|
+
return a.displayName.localeCompare(b.displayName);
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
for (const entry of aliasEntries) {
|
|
1457
|
+
const providerLabel = normalizeProviderLabel(entry.provider);
|
|
1458
|
+
const tags = entry.alias === currentModel ? tagsString(["active"]) : "";
|
|
1459
|
+
const title = makeUniqueTitle(withTags(`${providerLabel} — ${entry.displayName} (latest)`, tags), usedTitles);
|
|
1460
|
+
options.push(opt(title, entry.alias));
|
|
1461
|
+
selectionToModel.set(title, entry.alias);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
if (availableModels.length > 0) {
|
|
1465
|
+
for (const model of availableModels) {
|
|
1466
|
+
if (currentModel && model.spec === currentModel) continue;
|
|
1467
|
+
const providerLabel = normalizeProviderLabel(model.provider);
|
|
1468
|
+
const title = makeUniqueTitle(`${providerLabel} — ${model.id}`, usedTitles);
|
|
1469
|
+
options.push(opt(title, model.spec));
|
|
1470
|
+
selectionToModel.set(title, model.spec);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
1475
|
+
|
|
1476
|
+
while (true) {
|
|
1477
|
+
const choice = await selectOption(ctx, "Model", options);
|
|
1478
|
+
if (!choice || choice === "Back") return null;
|
|
1479
|
+
const selected = selectionToModel.get(choice);
|
|
1480
|
+
if (selected) return selected;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
async function pickThinking(
|
|
1485
|
+
ctx: any,
|
|
1486
|
+
allowXhigh: boolean,
|
|
1487
|
+
orchestrator?: Orchestrator,
|
|
1488
|
+
keyPath?: string[],
|
|
1489
|
+
): Promise<string | null> {
|
|
1490
|
+
const options: OptionInput[] = [];
|
|
1491
|
+
const byTitle = new Map<string, string>();
|
|
1492
|
+
const usedTitles = new Set<string>();
|
|
1493
|
+
const values = allowXhigh ? ["xhigh", "high", "medium", "low", "off"] : ["high", "medium", "low", "off"];
|
|
1494
|
+
const info = orchestrator && keyPath ? getConfigSourceInfo(orchestrator, keyPath) : null;
|
|
1495
|
+
for (const value of values) {
|
|
1496
|
+
const label = thinkingLabel(value);
|
|
1497
|
+
const title = makeUniqueTitle(withTags(label, info ? formatSourceTags(value, info) : ""), usedTitles);
|
|
1498
|
+
options.push(title);
|
|
1499
|
+
byTitle.set(title, value);
|
|
1500
|
+
}
|
|
1501
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
1502
|
+
const choice = await selectOption(ctx, "Thinking level", options);
|
|
1503
|
+
if (!choice || choice === "Back") return null;
|
|
1504
|
+
return byTitle.get(choice) ?? null;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function refreshSubagentDefinitions(orchestrator: Orchestrator, keyPath: string[]): void {
|
|
1508
|
+
if (keyPath[0] !== "agents") return;
|
|
1509
|
+
unregisterAgentDefinitions(orchestrator.pi);
|
|
1510
|
+
orchestrator.registerAgents();
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function applyConfigChange(orchestrator: Orchestrator, scope: Scope, keyPath: string[], value: any): void {
|
|
1514
|
+
const result = tryApplyConfigChange(orchestrator, scope, keyPath, value);
|
|
1515
|
+
if (!result.ok) {
|
|
1516
|
+
orchestrator.lastCtx?.ui?.notify(`Config update rejected: ${result.error}`, "error");
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const available = (orchestrator.lastCtx as any)?.modelRegistry?.getAvailable?.();
|
|
1520
|
+
if (Array.isArray(available)) {
|
|
1521
|
+
const modelIds = available
|
|
1522
|
+
.map((m: any) => {
|
|
1523
|
+
const provider = typeof m?.provider === "string" ? m.provider.trim() : "";
|
|
1524
|
+
const id = typeof m?.id === "string" ? m.id.trim() : "";
|
|
1525
|
+
return provider && id ? `${provider}/${id}` : "";
|
|
1526
|
+
})
|
|
1527
|
+
.filter((id: string) => id.length > 0);
|
|
1528
|
+
updateRegistryFromAvailableModels(modelIds);
|
|
1529
|
+
}
|
|
1530
|
+
refreshSubagentDefinitions(orchestrator, keyPath);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function clearConfigOverride(orchestrator: Orchestrator, scope: Scope, keyPath: string[]): void {
|
|
1534
|
+
const result = tryClearConfigOverride(orchestrator, scope, keyPath);
|
|
1535
|
+
if (!result.ok) {
|
|
1536
|
+
orchestrator.lastCtx?.ui?.notify(`Config update rejected: ${result.error}`, "error");
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
refreshSubagentDefinitions(orchestrator, keyPath);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function thinkingLabel(value: string): string {
|
|
1543
|
+
if (value === "off") return "Off";
|
|
1544
|
+
if (value === "low") return "Low";
|
|
1545
|
+
if (value === "medium") return "Medium";
|
|
1546
|
+
if (value === "high") return "High";
|
|
1547
|
+
if (value === "xhigh") return "Extra High";
|
|
1548
|
+
return value;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function logLevelLabel(value: string): string {
|
|
1552
|
+
if (value === "debug") return "Debug";
|
|
1553
|
+
if (value === "info") return "Info";
|
|
1554
|
+
if (value === "warn") return "Warning";
|
|
1555
|
+
if (value === "error") return "Error";
|
|
1556
|
+
return value;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function applyScopeChoice(orchestrator: Orchestrator, keyPath: string[], value: any, scope: Scope | null): void {
|
|
1560
|
+
if (!scope) return;
|
|
1561
|
+
try {
|
|
1562
|
+
const globalConfig = structuredClone(readRawConfig(GLOBAL_CONFIG_PATH));
|
|
1563
|
+
const projectConfig = structuredClone(readRawConfig(getProjectConfigPath(orchestrator.cwd)));
|
|
1564
|
+
const mergedWithoutScope = scope === "global"
|
|
1565
|
+
? (() => {
|
|
1566
|
+
deleteNestedValue(globalConfig, keyPath);
|
|
1567
|
+
return mergeConfigLayers(globalConfig, null);
|
|
1568
|
+
})()
|
|
1569
|
+
: (() => {
|
|
1570
|
+
deleteNestedValue(projectConfig, keyPath);
|
|
1571
|
+
return mergeConfigLayers(globalConfig, projectConfig);
|
|
1572
|
+
})();
|
|
1573
|
+
const defaultForScope = getNestedValue(mergedWithoutScope, keyPath);
|
|
1574
|
+
if (isDeepStrictEqual(value, defaultForScope)) {
|
|
1575
|
+
if (scope === "global") {
|
|
1576
|
+
clearConfigOverride(orchestrator, "global", keyPath);
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (!hasLayerOverride(orchestrator, "global", keyPath)) {
|
|
1580
|
+
clearConfigOverride(orchestrator, "project", keyPath);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
} catch {}
|
|
1585
|
+
applyConfigChange(orchestrator, scope, keyPath, value);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function enabledPresetSummary(variants: Record<string, VariantConfig>): string {
|
|
1589
|
+
const enabled = Object.entries(variants)
|
|
1590
|
+
.filter(([, variant]) => isEnabled(variant))
|
|
1591
|
+
.map(([name, variant]) => {
|
|
1592
|
+
const info = getModelInfo(variant.model);
|
|
1593
|
+
return `${name}: ${info.vendor}/${info.family} (${variant.thinking})`;
|
|
1594
|
+
});
|
|
1595
|
+
return enabled.length > 0 ? enabled.join(", ") : "No enabled models";
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
async function promptRequiredInput(ctx: any, label: string): Promise<string | null> {
|
|
1599
|
+
const value = await ctx.ui.input(label);
|
|
1600
|
+
if (value === undefined || value === null) return null;
|
|
1601
|
+
const trimmed = String(value).trim();
|
|
1602
|
+
if (!trimmed) return null;
|
|
1603
|
+
return trimmed;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
async function promptSafeName(ctx: any, label: string): Promise<string | null> {
|
|
1607
|
+
const value = await promptRequiredInput(ctx, label);
|
|
1608
|
+
if (!value) return null;
|
|
1609
|
+
if (!VALID_NAME_RE.test(value)) {
|
|
1610
|
+
ctx.ui.notify("Use only letters, numbers, and '-'.", "warning");
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
return value;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
async function maybeHandleResetChoice(orchestrator: Orchestrator, ctx: any, choice: string, keyPath: string[]): Promise<boolean> {
|
|
1617
|
+
if (choice === "Reset global setting") {
|
|
1618
|
+
const confirm = await selectOption(ctx, "Confirm reset?", [
|
|
1619
|
+
opt("Yes, reset", `Reset global override ${formatInlineValue(getLayerOverrideValue(orchestrator, "global", keyPath))}`),
|
|
1620
|
+
opt("Back", "Cancel"),
|
|
1621
|
+
]);
|
|
1622
|
+
if (confirm !== "Yes, reset") return true;
|
|
1623
|
+
clearConfigOverride(orchestrator, "global", keyPath);
|
|
1624
|
+
return true;
|
|
1625
|
+
}
|
|
1626
|
+
if (choice === "Reset project setting") {
|
|
1627
|
+
const confirm = await selectOption(ctx, "Confirm reset?", [
|
|
1628
|
+
opt("Yes, reset", `Reset project override ${formatInlineValue(getLayerOverrideValue(orchestrator, "project", keyPath))}`),
|
|
1629
|
+
opt("Back", "Cancel"),
|
|
1630
|
+
]);
|
|
1631
|
+
if (confirm !== "Yes, reset") return true;
|
|
1632
|
+
clearConfigOverride(orchestrator, "project", keyPath);
|
|
1633
|
+
return true;
|
|
1634
|
+
}
|
|
1635
|
+
return false;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
async function showOrchestratorEditor(
|
|
1639
|
+
orchestrator: Orchestrator,
|
|
1640
|
+
ctx: any,
|
|
1641
|
+
role: MainModelRole,
|
|
1642
|
+
label: string,
|
|
1643
|
+
): Promise<typeof BACK> {
|
|
1644
|
+
while (true) {
|
|
1645
|
+
const current = orchestrator.config.agents.orchestrators[role];
|
|
1646
|
+
const basePath = ["agents", "orchestrators", role];
|
|
1647
|
+
const choice = await selectOption(ctx, label, [
|
|
1648
|
+
opt(`Model: ${current.model}`, "Select model"),
|
|
1649
|
+
opt(`Thinking: ${thinkingLabel(current.thinking)}`, "Select thinking level"),
|
|
1650
|
+
...buildResetOptions(orchestrator, basePath),
|
|
1651
|
+
opt("Back", "Return to the previous menu"),
|
|
1652
|
+
]);
|
|
1653
|
+
if (!choice || choice === "Back") return BACK;
|
|
1654
|
+
if (choice.startsWith("Model:")) {
|
|
1655
|
+
const model = await pickModel(ctx, current.model);
|
|
1656
|
+
if (!model) continue;
|
|
1657
|
+
applyScopeChoice(orchestrator, [...basePath, "model"], model, await pickScope(ctx, orchestrator));
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
if (choice.startsWith("Thinking:")) {
|
|
1661
|
+
const thinking = await pickThinking(ctx, false, orchestrator, [...basePath, "thinking"]);
|
|
1662
|
+
if (!thinking) continue;
|
|
1663
|
+
applyScopeChoice(orchestrator, [...basePath, "thinking"], thinking, await pickScope(ctx, orchestrator));
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
await maybeHandleResetChoice(orchestrator, ctx, choice, basePath);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
async function showOrchestratorsSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
1671
|
+
while (true) {
|
|
1672
|
+
const options: OptionInput[] = ORCHESTRATOR_ROLES.map(({ role, label, description }) => {
|
|
1673
|
+
const current = orchestrator.config.agents.orchestrators[role];
|
|
1674
|
+
return opt(label, `${current.model} / ${thinkingLabel(current.thinking)} — ${description}`);
|
|
1675
|
+
});
|
|
1676
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
1677
|
+
const choice = await selectOption(ctx, "Orchestrators", options);
|
|
1678
|
+
if (!choice || choice === "Back") return BACK;
|
|
1679
|
+
const picked = ORCHESTRATOR_ROLES.find((item) => item.label === choice);
|
|
1680
|
+
if (!picked) continue;
|
|
1681
|
+
await showOrchestratorEditor(orchestrator, ctx, picked.role, picked.label);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
async function showSimpleSubagentEditor(
|
|
1686
|
+
orchestrator: Orchestrator,
|
|
1687
|
+
ctx: any,
|
|
1688
|
+
role: AgentRole,
|
|
1689
|
+
label: string,
|
|
1690
|
+
): Promise<typeof BACK> {
|
|
1691
|
+
while (true) {
|
|
1692
|
+
const current = orchestrator.config.agents.subagents.simple[role];
|
|
1693
|
+
const basePath = ["agents", "subagents", "simple", role];
|
|
1694
|
+
const choice = await selectOption(ctx, label, [
|
|
1695
|
+
opt(`Model: ${current.model}`, "Select model"),
|
|
1696
|
+
opt(`Thinking: ${thinkingLabel(current.thinking)}`, "Select thinking level"),
|
|
1697
|
+
...buildResetOptions(orchestrator, basePath),
|
|
1698
|
+
opt("Back", "Return to the previous menu"),
|
|
1699
|
+
]);
|
|
1700
|
+
if (!choice || choice === "Back") return BACK;
|
|
1701
|
+
if (choice.startsWith("Model:")) {
|
|
1702
|
+
const model = await pickModel(ctx, current.model);
|
|
1703
|
+
if (!model) continue;
|
|
1704
|
+
applyScopeChoice(orchestrator, [...basePath, "model"], model, await pickScope(ctx, orchestrator));
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
if (choice.startsWith("Thinking:")) {
|
|
1708
|
+
const thinking = await pickThinking(ctx, true, orchestrator, [...basePath, "thinking"]);
|
|
1709
|
+
if (!thinking) continue;
|
|
1710
|
+
applyScopeChoice(orchestrator, [...basePath, "thinking"], thinking, await pickScope(ctx, orchestrator));
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
await maybeHandleResetChoice(orchestrator, ctx, choice, basePath);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
async function addPresetVariant(
|
|
1718
|
+
orchestrator: Orchestrator,
|
|
1719
|
+
ctx: any,
|
|
1720
|
+
group: PresetGroup,
|
|
1721
|
+
presetName: string,
|
|
1722
|
+
): Promise<void> {
|
|
1723
|
+
const variantName = await promptSafeName(ctx, "Agent name");
|
|
1724
|
+
if (!variantName) return;
|
|
1725
|
+
if (orchestrator.config.agents.subagents.presetGroups[group].presets?.[presetName]?.agents?.[variantName]) {
|
|
1726
|
+
ctx.ui.notify(`Agent '${variantName}' already exists.`, "warning");
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
const model = await pickModel(ctx);
|
|
1730
|
+
if (!model) return;
|
|
1731
|
+
const thinking = await pickThinking(ctx, true);
|
|
1732
|
+
if (!thinking) return;
|
|
1733
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
1734
|
+
if (!scope) return;
|
|
1735
|
+
applyConfigChange(orchestrator, scope, ["agents", "subagents", "presetGroups", group, "presets", presetName, "agents", variantName], {
|
|
1736
|
+
enabled: true,
|
|
1737
|
+
model,
|
|
1738
|
+
thinking,
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
async function removePresetVariant(
|
|
1743
|
+
orchestrator: Orchestrator,
|
|
1744
|
+
ctx: any,
|
|
1745
|
+
group: PresetGroup,
|
|
1746
|
+
presetName: string,
|
|
1747
|
+
variantName: string,
|
|
1748
|
+
): Promise<void> {
|
|
1749
|
+
const variantPath = ["agents", "subagents", "presetGroups", group, "presets", presetName, "agents", variantName];
|
|
1750
|
+
const presetAgentsPath = ["agents", "subagents", "presetGroups", group, "presets", presetName, "agents"];
|
|
1751
|
+
const scopes = getOwnedScopes(orchestrator, variantPath);
|
|
1752
|
+
if (scopes.length === 0) return;
|
|
1753
|
+
const scope = scopes.length === 1 ? scopes[0]! : await pickScopeFromOwned(ctx, orchestrator, variantPath);
|
|
1754
|
+
if (!scope) return;
|
|
1755
|
+
const rawPresetValue = getRawScopeValue(orchestrator, scope, presetAgentsPath);
|
|
1756
|
+
if (!rawPresetValue || typeof rawPresetValue !== "object" || Array.isArray(rawPresetValue)) {
|
|
1757
|
+
ctx.ui.notify("No override in selected scope.", "info");
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const rawPreset = rawPresetValue as Record<string, VariantConfig>;
|
|
1761
|
+
if (!Object.prototype.hasOwnProperty.call(rawPreset, variantName)) {
|
|
1762
|
+
ctx.ui.notify("Agent is inherited and cannot be removed in selected scope.", "info");
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
if (Object.keys(rawPreset).length <= 1) {
|
|
1766
|
+
try {
|
|
1767
|
+
const nextGlobal = structuredClone(readRawConfig(GLOBAL_CONFIG_PATH));
|
|
1768
|
+
const nextProject = structuredClone(readRawConfig(getProjectConfigPath(orchestrator.cwd)));
|
|
1769
|
+
const target = scope === "global" ? nextGlobal : nextProject;
|
|
1770
|
+
deleteNestedValue(target, presetAgentsPath);
|
|
1771
|
+
mergeConfigLayers(nextGlobal, nextProject);
|
|
1772
|
+
} catch {
|
|
1773
|
+
ctx.ui.notify("Cannot delete the last agent in this preset.", "warning");
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
clearConfigOverride(orchestrator, scope, presetAgentsPath);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
const nextPreset = structuredClone(rawPreset);
|
|
1780
|
+
delete nextPreset[variantName];
|
|
1781
|
+
applyConfigChange(orchestrator, scope, presetAgentsPath, nextPreset);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
async function showPresetVariantEditor(
|
|
1785
|
+
orchestrator: Orchestrator,
|
|
1786
|
+
ctx: any,
|
|
1787
|
+
group: PresetGroup,
|
|
1788
|
+
presetName: string,
|
|
1789
|
+
variantName: string,
|
|
1790
|
+
): Promise<typeof BACK> {
|
|
1791
|
+
while (true) {
|
|
1792
|
+
const variant = orchestrator.config.agents.subagents.presetGroups[group].presets?.[presetName]?.agents?.[variantName];
|
|
1793
|
+
if (!variant) return BACK;
|
|
1794
|
+
const variantPath = ["agents", "subagents", "presetGroups", group, "presets", presetName, "agents", variantName];
|
|
1795
|
+
const options: OptionInput[] = [
|
|
1796
|
+
opt(`Enabled: ${isEnabled(variant) ? "Yes" : "No"}`, "Toggle enabled state"),
|
|
1797
|
+
opt(`Model: ${variant.model}`, "Select model"),
|
|
1798
|
+
opt(`Thinking: ${thinkingLabel(variant.thinking)}`, "Select thinking level"),
|
|
1799
|
+
];
|
|
1800
|
+
if (getOwnedScopes(orchestrator, variantPath).length > 0) {
|
|
1801
|
+
options.push(opt("Delete", "Delete this agent override"));
|
|
1802
|
+
}
|
|
1803
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
1804
|
+
const choice = await selectOption(ctx, `Agent "${variantName}"`, options);
|
|
1805
|
+
if (!choice || choice === "Back") return BACK;
|
|
1806
|
+
if (choice.startsWith("Model:")) {
|
|
1807
|
+
const model = await pickModel(ctx, variant.model);
|
|
1808
|
+
if (!model) continue;
|
|
1809
|
+
applyScopeChoice(orchestrator, [...variantPath, "model"], model, await pickScope(ctx, orchestrator));
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
if (choice.startsWith("Thinking:")) {
|
|
1813
|
+
const thinking = await pickThinking(ctx, true, orchestrator, [...variantPath, "thinking"]);
|
|
1814
|
+
if (!thinking) continue;
|
|
1815
|
+
applyScopeChoice(orchestrator, [...variantPath, "thinking"], thinking, await pickScope(ctx, orchestrator));
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
if (choice.startsWith("Enabled:")) {
|
|
1819
|
+
await showBooleanSetting(orchestrator, ctx, "Enabled", [...variantPath, "enabled"]);
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
const confirm = await selectOption(ctx, "Confirm delete?", [
|
|
1823
|
+
opt("Yes, delete", "This cannot be undone"),
|
|
1824
|
+
opt("Back", "Cancel"),
|
|
1825
|
+
]);
|
|
1826
|
+
if (confirm !== "Yes, delete") continue;
|
|
1827
|
+
await removePresetVariant(orchestrator, ctx, group, presetName, variantName);
|
|
1828
|
+
return BACK;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
async function showPresetAgentsMenu(
|
|
1833
|
+
orchestrator: Orchestrator,
|
|
1834
|
+
ctx: any,
|
|
1835
|
+
group: PresetGroup,
|
|
1836
|
+
presetName: string,
|
|
1837
|
+
): Promise<typeof BACK> {
|
|
1838
|
+
while (true) {
|
|
1839
|
+
const preset = orchestrator.config.agents.subagents.presetGroups[group].presets?.[presetName];
|
|
1840
|
+
if (!preset) return BACK;
|
|
1841
|
+
const agents = preset.agents ?? {};
|
|
1842
|
+
const options: OptionInput[] = [];
|
|
1843
|
+
const byTitle = new Map<string, string>();
|
|
1844
|
+
for (const [variantName, variant] of Object.entries(agents)) {
|
|
1845
|
+
const tag = isEnabled(variant) ? "" : " (disabled)";
|
|
1846
|
+
const title = `Agent "${variantName}"${tag}`;
|
|
1847
|
+
options.push(opt(title, `${variant.model} / ${thinkingLabel(variant.thinking)}`));
|
|
1848
|
+
byTitle.set(title, variantName);
|
|
1849
|
+
}
|
|
1850
|
+
options.push(opt("New agent", "Add a new agent"));
|
|
1851
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
1852
|
+
const choice = await selectOption(ctx, "Agents", options);
|
|
1853
|
+
if (!choice || choice === "Back") return BACK;
|
|
1854
|
+
if (choice === "New agent") {
|
|
1855
|
+
await addPresetVariant(orchestrator, ctx, group, presetName);
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
const variantName = byTitle.get(choice);
|
|
1859
|
+
if (!variantName) continue;
|
|
1860
|
+
await showPresetVariantEditor(orchestrator, ctx, group, presetName, variantName);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
function deletePresetFromScope(orchestrator: Orchestrator, scope: Scope, group: PresetGroup, presetName: string): void {
|
|
1865
|
+
const groupPath = ["agents", "subagents", "presetGroups", group, "presets"];
|
|
1866
|
+
const rawGroup = getRawScopeValue(orchestrator, scope, groupPath);
|
|
1867
|
+
if (!rawGroup || typeof rawGroup !== "object" || Array.isArray(rawGroup)) return;
|
|
1868
|
+
const nextGroup = structuredClone(rawGroup as Record<string, { enabled?: boolean; agents: Record<string, VariantConfig> }>);
|
|
1869
|
+
delete nextGroup[presetName];
|
|
1870
|
+
if (Object.keys(nextGroup).length === 0) {
|
|
1871
|
+
clearConfigOverride(orchestrator, scope, groupPath);
|
|
1872
|
+
} else {
|
|
1873
|
+
applyConfigChange(orchestrator, scope, groupPath, nextGroup);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
function isPresetDisabled(orchestrator: Orchestrator, group: PresetGroup, presetName: string): boolean {
|
|
1878
|
+
const preset = orchestrator.config.agents.subagents.presetGroups[group].presets[presetName];
|
|
1879
|
+
return preset?.enabled === false;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function setPresetDisabled(orchestrator: Orchestrator, group: PresetGroup, presetName: string, disabled: boolean, scope: Scope): void {
|
|
1883
|
+
applyConfigChange(
|
|
1884
|
+
orchestrator,
|
|
1885
|
+
scope,
|
|
1886
|
+
["agents", "subagents", "presetGroups", group, "presets", presetName, "enabled"],
|
|
1887
|
+
!disabled,
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
async function showPresetEditor(
|
|
1892
|
+
orchestrator: Orchestrator,
|
|
1893
|
+
ctx: any,
|
|
1894
|
+
group: PresetGroup,
|
|
1895
|
+
presetName: string,
|
|
1896
|
+
): Promise<typeof BACK> {
|
|
1897
|
+
while (true) {
|
|
1898
|
+
const preset = orchestrator.config.agents.subagents.presetGroups[group].presets?.[presetName];
|
|
1899
|
+
if (!preset) return BACK;
|
|
1900
|
+
const isDefault = orchestrator.config.agents.subagents.presetGroups[group].default === presetName;
|
|
1901
|
+
const disabled = isPresetDisabled(orchestrator, group, presetName);
|
|
1902
|
+
const presetPath = ["agents", "subagents", "presetGroups", group, "presets", presetName];
|
|
1903
|
+
const defaultPath = ["agents", "subagents", "presetGroups", group, "default"];
|
|
1904
|
+
const options: OptionInput[] = [
|
|
1905
|
+
opt(`Enabled: ${disabled ? "No" : "Yes"}`, "Enable or disable this preset"),
|
|
1906
|
+
opt("Agents", `${Object.keys(preset.agents ?? {}).length} agents`),
|
|
1907
|
+
];
|
|
1908
|
+
if (!isDefault) {
|
|
1909
|
+
options.push(opt("Use as default", "Set as default preset"));
|
|
1910
|
+
}
|
|
1911
|
+
options.push(...buildResetOptions(orchestrator, presetPath));
|
|
1912
|
+
if (getOwnedScopes(orchestrator, presetPath).length > 0) {
|
|
1913
|
+
options.push(opt("Delete", "Delete this preset override"));
|
|
1914
|
+
}
|
|
1915
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
1916
|
+
const choice = await selectOption(ctx, `Preset "${presetName}"${isDefault ? " (default)" : ""}`, options);
|
|
1917
|
+
if (!choice || choice === "Back") return BACK;
|
|
1918
|
+
if (choice.startsWith("Enabled:")) {
|
|
1919
|
+
await showPresetEnabledSetting(orchestrator, ctx, group, presetName);
|
|
1920
|
+
continue;
|
|
1921
|
+
}
|
|
1922
|
+
if (choice === "Use as default") {
|
|
1923
|
+
applyScopeChoice(orchestrator, defaultPath, presetName, await pickScope(ctx, orchestrator));
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
if (choice === "Agents") {
|
|
1927
|
+
await showPresetAgentsMenu(orchestrator, ctx, group, presetName);
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
if (choice === "Delete") {
|
|
1931
|
+
if (isDefault) {
|
|
1932
|
+
ctx.ui.notify("Cannot delete the default preset. Change the default first.", "warning");
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
const confirm = await selectOption(ctx, "Confirm delete?", [
|
|
1936
|
+
opt("Yes, delete", "This cannot be undone"),
|
|
1937
|
+
opt("Back", "Cancel"),
|
|
1938
|
+
]);
|
|
1939
|
+
if (confirm !== "Yes, delete") continue;
|
|
1940
|
+
const scope = await pickScopeFromOwned(ctx, orchestrator, presetPath);
|
|
1941
|
+
if (!scope) continue;
|
|
1942
|
+
deletePresetFromScope(orchestrator, scope, group, presetName);
|
|
1943
|
+
return BACK;
|
|
1944
|
+
}
|
|
1945
|
+
await maybeHandleResetChoice(orchestrator, ctx, choice, presetPath);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
async function showPresetEnabledSetting(
|
|
1950
|
+
orchestrator: Orchestrator,
|
|
1951
|
+
ctx: any,
|
|
1952
|
+
group: PresetGroup,
|
|
1953
|
+
presetName: string,
|
|
1954
|
+
): Promise<void> {
|
|
1955
|
+
const enabledPath = ["agents", "subagents", "presetGroups", group, "presets", presetName, "enabled"];
|
|
1956
|
+
const info = getConfigSourceInfo(orchestrator, enabledPath);
|
|
1957
|
+
const yesTitle = withTags("Yes", formatSourceTags(true, info));
|
|
1958
|
+
const noTitle = withTags("No", formatSourceTags(false, info));
|
|
1959
|
+
const choice = await selectOption(ctx, "Enabled", [
|
|
1960
|
+
yesTitle,
|
|
1961
|
+
noTitle,
|
|
1962
|
+
opt("Back", "Return to the previous menu"),
|
|
1963
|
+
]);
|
|
1964
|
+
if (!choice || choice === "Back") return;
|
|
1965
|
+
if (choice === yesTitle) {
|
|
1966
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
1967
|
+
if (!scope) return;
|
|
1968
|
+
setPresetDisabled(orchestrator, group, presetName, false, scope);
|
|
1969
|
+
} else if (choice === noTitle) {
|
|
1970
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
1971
|
+
if (!scope) return;
|
|
1972
|
+
setPresetDisabled(orchestrator, group, presetName, true, scope);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
async function addNewPreset(orchestrator: Orchestrator, ctx: any, group: PresetGroup): Promise<void> {
|
|
1977
|
+
const name = await promptSafeName(ctx, "Preset name");
|
|
1978
|
+
if (!name) return;
|
|
1979
|
+
if (orchestrator.config.agents.subagents.presetGroups[group].presets?.[name]) {
|
|
1980
|
+
ctx.ui.notify(`Preset '${name}' already exists.`, "warning");
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
const variantName = await promptSafeName(ctx, "Initial agent name");
|
|
1984
|
+
if (!variantName) return;
|
|
1985
|
+
const model = await pickModel(ctx);
|
|
1986
|
+
if (!model) return;
|
|
1987
|
+
const thinking = await pickThinking(ctx, true);
|
|
1988
|
+
if (!thinking) return;
|
|
1989
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
1990
|
+
if (!scope) return;
|
|
1991
|
+
applyConfigChange(orchestrator, scope, ["agents", "subagents", "presetGroups", group, "presets", name], {
|
|
1992
|
+
enabled: true,
|
|
1993
|
+
agents: {
|
|
1994
|
+
[variantName]: { enabled: true, model, thinking },
|
|
1995
|
+
},
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
async function showPresetSettings(
|
|
2000
|
+
orchestrator: Orchestrator,
|
|
2001
|
+
ctx: any,
|
|
2002
|
+
group: PresetGroup,
|
|
2003
|
+
title: string,
|
|
2004
|
+
): Promise<typeof BACK> {
|
|
2005
|
+
while (true) {
|
|
2006
|
+
const presets = orchestrator.config.agents.subagents.presetGroups[group].presets ?? {};
|
|
2007
|
+
const defaultName = orchestrator.config.agents.subagents.presetGroups[group].default;
|
|
2008
|
+
const options: OptionInput[] = [];
|
|
2009
|
+
const byTitle = new Map<string, string>();
|
|
2010
|
+
for (const [presetName, preset] of Object.entries(presets)) {
|
|
2011
|
+
const tags: string[] = [];
|
|
2012
|
+
if (presetName === defaultName) tags.push("default");
|
|
2013
|
+
if (isPresetDisabled(orchestrator, group, presetName)) tags.push("disabled");
|
|
2014
|
+
const tagStr = tags.length > 0 ? ` (${tags.join(", ")})` : "";
|
|
2015
|
+
const optionTitle = `Preset "${presetName}"${tagStr}`;
|
|
2016
|
+
options.push(opt(optionTitle, enabledPresetSummary(preset.agents)));
|
|
2017
|
+
byTitle.set(optionTitle, presetName);
|
|
2018
|
+
}
|
|
2019
|
+
options.push(opt("New preset", "Create preset"));
|
|
2020
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
2021
|
+
const choice = await selectOption(ctx, title, options);
|
|
2022
|
+
if (!choice || choice === "Back") return BACK;
|
|
2023
|
+
if (choice === "New preset") {
|
|
2024
|
+
await addNewPreset(orchestrator, ctx, group);
|
|
2025
|
+
continue;
|
|
2026
|
+
}
|
|
2027
|
+
const presetName = byTitle.get(choice);
|
|
2028
|
+
if (!presetName) continue;
|
|
2029
|
+
await showPresetEditor(orchestrator, ctx, group, presetName);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
async function showSubagentSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2034
|
+
while (true) {
|
|
2035
|
+
const options: OptionInput[] = SUBAGENT_ROLES.map(({ role, label, description }) => {
|
|
2036
|
+
const current = orchestrator.config.agents.subagents.simple[role];
|
|
2037
|
+
return opt(label, `${current.model} / ${thinkingLabel(current.thinking)} — ${description}`);
|
|
2038
|
+
});
|
|
2039
|
+
for (const item of PRESET_GROUP_ITEMS) {
|
|
2040
|
+
options.push(opt(item.label, `${Object.keys(orchestrator.config.agents.subagents.presetGroups[item.group].presets ?? {}).length} presets`));
|
|
2041
|
+
}
|
|
2042
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
2043
|
+
const choice = await selectOption(ctx, "Subagents", options);
|
|
2044
|
+
if (!choice || choice === "Back") return BACK;
|
|
2045
|
+
const simple = SUBAGENT_ROLES.find((item) => item.label === choice);
|
|
2046
|
+
if (simple) {
|
|
2047
|
+
await showSimpleSubagentEditor(orchestrator, ctx, simple.role, simple.label);
|
|
2048
|
+
continue;
|
|
2049
|
+
}
|
|
2050
|
+
const group = PRESET_GROUP_ITEMS.find((item) => item.label === choice);
|
|
2051
|
+
if (!group) continue;
|
|
2052
|
+
await showPresetSettings(orchestrator, ctx, group.group, group.label);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function getCommandScope(orchestrator: Orchestrator, keyPath: string[]): Scope {
|
|
2057
|
+
const info = getConfigSourceInfo(orchestrator, keyPath);
|
|
2058
|
+
if (info.source === "project") return "project";
|
|
2059
|
+
if (info.source === "global") return "global";
|
|
2060
|
+
return "project";
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
function ensureUniqueCommandId(existing: Record<string, unknown>, base: string): string {
|
|
2064
|
+
if (!Object.prototype.hasOwnProperty.call(existing, base)) return base;
|
|
2065
|
+
let i = 2;
|
|
2066
|
+
while (Object.prototype.hasOwnProperty.call(existing, `${base}-${i}`)) i += 1;
|
|
2067
|
+
return `${base}-${i}`;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
function commandIdFromRun(run: string): string {
|
|
2071
|
+
const base = run
|
|
2072
|
+
.toLowerCase()
|
|
2073
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
2074
|
+
.replace(/^-+|-+$/g, "")
|
|
2075
|
+
.slice(0, 48);
|
|
2076
|
+
return base || "command";
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
async function showAfterEditCommands(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2080
|
+
while (true) {
|
|
2081
|
+
const commands = orchestrator.config.commands.afterEdit;
|
|
2082
|
+
const entries = Object.entries(commands);
|
|
2083
|
+
const options: OptionInput[] = [];
|
|
2084
|
+
const byTitle = new Map<string, string>();
|
|
2085
|
+
const usedTitles = new Set<string>();
|
|
2086
|
+
for (const [id, cmd] of entries) {
|
|
2087
|
+
const title = makeUniqueTitle(`Command "${id}"`, usedTitles);
|
|
2088
|
+
const globsCount = (cmd.globs ?? []).length;
|
|
2089
|
+
const enabledTag = isEnabled(cmd) ? "" : " [disabled]";
|
|
2090
|
+
options.push(opt(`${title}${enabledTag}`, `${globsCount} patterns — ${cmd.run}`));
|
|
2091
|
+
byTitle.set(`${title}${enabledTag}`, id);
|
|
2092
|
+
}
|
|
2093
|
+
options.push(opt("New command", "Add command"));
|
|
2094
|
+
options.push(...buildResetOptions(orchestrator, ["commands", "afterEdit"]));
|
|
2095
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
2096
|
+
const choice = await selectOption(ctx, `After file edit: ${entries.length} commands`, options);
|
|
2097
|
+
if (!choice || choice === "Back") return BACK;
|
|
2098
|
+
if (choice === "New command") {
|
|
2099
|
+
const run = await promptRequiredInput(ctx, "Command to run");
|
|
2100
|
+
if (!run) continue;
|
|
2101
|
+
const globInput = await promptRequiredInput(ctx, "Glob patterns (comma-separated)");
|
|
2102
|
+
if (!globInput) {
|
|
2103
|
+
ctx.ui.notify("At least one file pattern is required.", "warning");
|
|
2104
|
+
continue;
|
|
2105
|
+
}
|
|
2106
|
+
const patterns = parseCommaSeparated(globInput);
|
|
2107
|
+
if (patterns.length === 0) {
|
|
2108
|
+
ctx.ui.notify("At least one file pattern is required.", "warning");
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
2112
|
+
if (!scope) continue;
|
|
2113
|
+
const baseId = commandIdFromRun(run);
|
|
2114
|
+
const id = ensureUniqueCommandId(commands, baseId);
|
|
2115
|
+
applyConfigChange(orchestrator, scope, ["commands", "afterEdit", id], { run, globs: patterns, enabled: true });
|
|
2116
|
+
continue;
|
|
2117
|
+
}
|
|
2118
|
+
if (await maybeHandleResetChoice(orchestrator, ctx, choice, ["commands", "afterEdit"])) continue;
|
|
2119
|
+
const id = byTitle.get(choice);
|
|
2120
|
+
if (!id) continue;
|
|
2121
|
+
const commandPath = ["commands", "afterEdit", id];
|
|
2122
|
+
while (true) {
|
|
2123
|
+
const command = orchestrator.config.commands.afterEdit[id];
|
|
2124
|
+
if (!command) break;
|
|
2125
|
+
const commandChoice = await selectOption(ctx, `Command "${id}"`, [
|
|
2126
|
+
opt("Edit command", command.run),
|
|
2127
|
+
opt("Triggers", `${(command.globs ?? []).length} patterns`),
|
|
2128
|
+
opt(`Enabled: ${isEnabled(command) ? "Yes" : "No"}`, "Toggle command"),
|
|
2129
|
+
opt("Delete command", "Remove this command"),
|
|
2130
|
+
opt("Back", "Return to previous menu"),
|
|
2131
|
+
]);
|
|
2132
|
+
if (!commandChoice || commandChoice === "Back") break;
|
|
2133
|
+
if (commandChoice === "Edit command") {
|
|
2134
|
+
const run = await promptRequiredInput(ctx, `Command (current: ${command.run})`);
|
|
2135
|
+
if (!run) continue;
|
|
2136
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
2137
|
+
if (!scope) continue;
|
|
2138
|
+
applyConfigChange(orchestrator, scope, [...commandPath, "run"], run);
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
if (commandChoice.startsWith("Enabled:")) {
|
|
2142
|
+
await showBooleanSetting(orchestrator, ctx, "Enabled", [...commandPath, "enabled"]);
|
|
2143
|
+
continue;
|
|
2144
|
+
}
|
|
2145
|
+
if (commandChoice === "Triggers") {
|
|
2146
|
+
while (true) {
|
|
2147
|
+
const current = orchestrator.config.commands.afterEdit[id];
|
|
2148
|
+
if (!current) break;
|
|
2149
|
+
const currentGlobs = current.globs ?? [];
|
|
2150
|
+
const patternUsed = new Set<string>();
|
|
2151
|
+
const patternOptions: OptionInput[] = currentGlobs.map((glob) => {
|
|
2152
|
+
const title = makeUniqueTitle(`Pattern "${slugify(glob, 50)}"`, patternUsed);
|
|
2153
|
+
return opt(title, glob);
|
|
2154
|
+
});
|
|
2155
|
+
patternOptions.push(opt("New pattern", "Add trigger pattern"));
|
|
2156
|
+
patternOptions.push(opt("Back", "Return to previous menu"));
|
|
2157
|
+
const patternChoice = await selectOption(ctx, "File patterns", patternOptions);
|
|
2158
|
+
if (!patternChoice || patternChoice === "Back") break;
|
|
2159
|
+
if (patternChoice === "New pattern") {
|
|
2160
|
+
const value = await promptRequiredInput(ctx, "Pattern");
|
|
2161
|
+
if (!value) continue;
|
|
2162
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
2163
|
+
if (!scope) continue;
|
|
2164
|
+
applyConfigChange(orchestrator, scope, [...commandPath, "globs"], [...currentGlobs, value]);
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
const patternGlob = patternOptions.find((o) => (typeof o === "string" ? o : o.title) === patternChoice);
|
|
2168
|
+
const actualGlob = patternGlob && typeof patternGlob !== "string" ? patternGlob.description : undefined;
|
|
2169
|
+
const patternIndex = actualGlob ? currentGlobs.indexOf(actualGlob) : -1;
|
|
2170
|
+
if (patternIndex < 0) continue;
|
|
2171
|
+
const action = await selectOption(ctx, `Pattern "${slugify(currentGlobs[patternIndex]!, 50)}"`, [
|
|
2172
|
+
opt("Edit", currentGlobs[patternIndex]!),
|
|
2173
|
+
opt("Delete", "Remove this pattern"),
|
|
2174
|
+
opt("Back", "Return to previous menu"),
|
|
2175
|
+
]);
|
|
2176
|
+
if (!action || action === "Back") continue;
|
|
2177
|
+
const nextGlob = [...currentGlobs];
|
|
2178
|
+
if (action === "Delete") {
|
|
2179
|
+
const confirm = await selectOption(ctx, "Confirm delete?", [
|
|
2180
|
+
opt("Yes, delete", "This cannot be undone"),
|
|
2181
|
+
opt("Back", "Cancel"),
|
|
2182
|
+
]);
|
|
2183
|
+
if (confirm !== "Yes, delete") continue;
|
|
2184
|
+
nextGlob.splice(patternIndex, 1);
|
|
2185
|
+
} else {
|
|
2186
|
+
const value = await promptRequiredInput(ctx, `Pattern (current: ${currentGlobs[patternIndex]})`);
|
|
2187
|
+
if (!value) continue;
|
|
2188
|
+
nextGlob[patternIndex] = value;
|
|
2189
|
+
}
|
|
2190
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
2191
|
+
if (!scope) continue;
|
|
2192
|
+
applyConfigChange(orchestrator, scope, [...commandPath, "globs"], nextGlob);
|
|
2193
|
+
}
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
const confirm = await selectOption(ctx, "Confirm delete?", [
|
|
2197
|
+
opt("Yes, delete", "This cannot be undone"),
|
|
2198
|
+
opt("Back", "Cancel"),
|
|
2199
|
+
]);
|
|
2200
|
+
if (confirm !== "Yes, delete") continue;
|
|
2201
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
2202
|
+
if (!scope) continue;
|
|
2203
|
+
clearConfigOverride(orchestrator, scope, commandPath);
|
|
2204
|
+
break;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
async function showAfterImplementCommands(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2210
|
+
while (true) {
|
|
2211
|
+
const commands = orchestrator.config.commands.afterImplement;
|
|
2212
|
+
const entries = Object.entries(commands);
|
|
2213
|
+
const options: OptionInput[] = [];
|
|
2214
|
+
const byTitle = new Map<string, string>();
|
|
2215
|
+
const usedTitles = new Set<string>();
|
|
2216
|
+
for (const [id, cmd] of entries) {
|
|
2217
|
+
const enabledTag = isEnabled(cmd) ? "" : " [disabled]";
|
|
2218
|
+
const title = makeUniqueTitle(`Command "${id}"${enabledTag}`, usedTitles);
|
|
2219
|
+
options.push(opt(title, cmd.run));
|
|
2220
|
+
byTitle.set(title, id);
|
|
2221
|
+
}
|
|
2222
|
+
options.push(opt("New command", "Add command"));
|
|
2223
|
+
options.push(...buildResetOptions(orchestrator, ["commands", "afterImplement"]));
|
|
2224
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
2225
|
+
const choice = await selectOption(ctx, `After implementation: ${entries.length} commands`, options);
|
|
2226
|
+
if (!choice || choice === "Back") return BACK;
|
|
2227
|
+
if (choice === "New command") {
|
|
2228
|
+
const run = await promptRequiredInput(ctx, "Command to run");
|
|
2229
|
+
if (!run) continue;
|
|
2230
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
2231
|
+
if (!scope) continue;
|
|
2232
|
+
const id = ensureUniqueCommandId(commands, commandIdFromRun(run));
|
|
2233
|
+
applyConfigChange(orchestrator, scope, ["commands", "afterImplement", id], { run, enabled: true });
|
|
2234
|
+
continue;
|
|
2235
|
+
}
|
|
2236
|
+
if (await maybeHandleResetChoice(orchestrator, ctx, choice, ["commands", "afterImplement"])) continue;
|
|
2237
|
+
const id = byTitle.get(choice);
|
|
2238
|
+
if (!id) continue;
|
|
2239
|
+
const commandPath = ["commands", "afterImplement", id];
|
|
2240
|
+
while (true) {
|
|
2241
|
+
const command = orchestrator.config.commands.afterImplement[id];
|
|
2242
|
+
if (!command) break;
|
|
2243
|
+
const commandChoice = await selectOption(ctx, `Command "${id}"`, [
|
|
2244
|
+
opt("Edit command", command.run),
|
|
2245
|
+
opt(`Enabled: ${isEnabled(command) ? "Yes" : "No"}`, "Toggle command"),
|
|
2246
|
+
opt("Delete command", "Remove this command"),
|
|
2247
|
+
opt("Back", "Return to previous menu"),
|
|
2248
|
+
]);
|
|
2249
|
+
if (!commandChoice || commandChoice === "Back") break;
|
|
2250
|
+
if (commandChoice === "Edit command") {
|
|
2251
|
+
const run = await promptRequiredInput(ctx, `Command (current: ${command.run})`);
|
|
2252
|
+
if (!run) continue;
|
|
2253
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
2254
|
+
if (!scope) continue;
|
|
2255
|
+
applyConfigChange(orchestrator, scope, [...commandPath, "run"], run);
|
|
2256
|
+
continue;
|
|
2257
|
+
}
|
|
2258
|
+
if (commandChoice.startsWith("Enabled:")) {
|
|
2259
|
+
await showBooleanSetting(orchestrator, ctx, "Enabled", [...commandPath, "enabled"]);
|
|
2260
|
+
continue;
|
|
2261
|
+
}
|
|
2262
|
+
const confirm = await selectOption(ctx, "Confirm delete?", [
|
|
2263
|
+
opt("Yes, delete", "This cannot be undone"),
|
|
2264
|
+
opt("Back", "Cancel"),
|
|
2265
|
+
]);
|
|
2266
|
+
if (confirm !== "Yes, delete") continue;
|
|
2267
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
2268
|
+
if (!scope) continue;
|
|
2269
|
+
clearConfigOverride(orchestrator, scope, commandPath);
|
|
2270
|
+
break;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
async function showCommandsSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2276
|
+
while (true) {
|
|
2277
|
+
const afterEditCount = Object.keys(orchestrator.config.commands.afterEdit).length;
|
|
2278
|
+
const afterImplementCount = Object.keys(orchestrator.config.commands.afterImplement).length;
|
|
2279
|
+
const choice = await selectOption(ctx, "Commands", [
|
|
2280
|
+
opt(
|
|
2281
|
+
`After file edit: ${afterEditCount} commands`,
|
|
2282
|
+
`${afterEditCount} commands`,
|
|
2283
|
+
),
|
|
2284
|
+
opt(
|
|
2285
|
+
`After implementation: ${afterImplementCount} commands`,
|
|
2286
|
+
`${afterImplementCount} commands`,
|
|
2287
|
+
),
|
|
2288
|
+
opt("Back", "Return to the previous menu"),
|
|
2289
|
+
]);
|
|
2290
|
+
if (!choice || choice === "Back") return BACK;
|
|
2291
|
+
if (choice.startsWith("After file edit:")) {
|
|
2292
|
+
await showAfterEditCommands(orchestrator, ctx);
|
|
2293
|
+
continue;
|
|
2294
|
+
}
|
|
2295
|
+
await showAfterImplementCommands(orchestrator, ctx);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
async function showTimeoutsSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2300
|
+
while (true) {
|
|
2301
|
+
const timeoutEntries: TimeoutEntry[] = [
|
|
2302
|
+
{
|
|
2303
|
+
key: "performance.commands.afterEdit",
|
|
2304
|
+
path: ["performance", "commands", "afterEdit"],
|
|
2305
|
+
value: orchestrator.config.performance.commands.afterEdit,
|
|
2306
|
+
},
|
|
2307
|
+
{
|
|
2308
|
+
key: "performance.commands.afterImplement",
|
|
2309
|
+
path: ["performance", "commands", "afterImplement"],
|
|
2310
|
+
value: orchestrator.config.performance.commands.afterImplement,
|
|
2311
|
+
},
|
|
2312
|
+
{
|
|
2313
|
+
key: "performance.internals.subagentStale",
|
|
2314
|
+
path: ["performance", "internals", "subagentStale"],
|
|
2315
|
+
value: orchestrator.config.performance.internals.subagentStale,
|
|
2316
|
+
},
|
|
2317
|
+
{
|
|
2318
|
+
key: "performance.internals.taskLockStale",
|
|
2319
|
+
path: ["performance", "internals", "taskLockStale"],
|
|
2320
|
+
value: orchestrator.config.performance.internals.taskLockStale,
|
|
2321
|
+
},
|
|
2322
|
+
{
|
|
2323
|
+
key: "performance.internals.taskLockRefresh",
|
|
2324
|
+
path: ["performance", "internals", "taskLockRefresh"],
|
|
2325
|
+
value: orchestrator.config.performance.internals.taskLockRefresh,
|
|
2326
|
+
},
|
|
2327
|
+
];
|
|
2328
|
+
const options: OptionInput[] = timeoutEntries.map((entry) => {
|
|
2329
|
+
const value = entry.value;
|
|
2330
|
+
const key = entry.key;
|
|
2331
|
+
return opt(`${TIMEOUT_LABELS[key]}: ${formatDuration(value)}`, "Edit timeout");
|
|
2332
|
+
});
|
|
2333
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
2334
|
+
const choice = await selectOption(ctx, "Timeouts", options);
|
|
2335
|
+
if (!choice || choice === "Back") return BACK;
|
|
2336
|
+
const entry = timeoutEntries.find((item) => choice.startsWith(`${TIMEOUT_LABELS[item.key]}:`));
|
|
2337
|
+
if (!entry) continue;
|
|
2338
|
+
while (true) {
|
|
2339
|
+
const current = getNestedValue(orchestrator.config, entry.path);
|
|
2340
|
+
if (typeof current !== "number") break;
|
|
2341
|
+
const action = await selectOption(ctx, `${TIMEOUT_LABELS[entry.key]}: ${formatDuration(current)}`, [
|
|
2342
|
+
opt("Edit", "Set timeout value"),
|
|
2343
|
+
...buildResetOptions(orchestrator, entry.path),
|
|
2344
|
+
opt("Back", "Return to the previous menu"),
|
|
2345
|
+
]);
|
|
2346
|
+
if (!action || action === "Back") break;
|
|
2347
|
+
if (action === "Edit") {
|
|
2348
|
+
const input = await promptRequiredInput(
|
|
2349
|
+
ctx,
|
|
2350
|
+
`New value (current: ${formatDuration(current)}, e.g. 30s, 5m, 1h, or milliseconds)`,
|
|
2351
|
+
);
|
|
2352
|
+
if (!input) continue;
|
|
2353
|
+
const parsed = parseDuration(input);
|
|
2354
|
+
if (parsed === null) {
|
|
2355
|
+
ctx.ui.notify("Invalid duration format.", "warning");
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
applyScopeChoice(orchestrator, entry.path, parsed, await pickScope(ctx, orchestrator));
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2361
|
+
await maybeHandleResetChoice(orchestrator, ctx, action, entry.path);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
async function showPerformanceSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2367
|
+
while (true) {
|
|
2368
|
+
const choice = await selectOption(ctx, "Performance", [
|
|
2369
|
+
opt("Timeouts", "Timeout configuration"),
|
|
2370
|
+
opt("Back", "Return to the previous menu"),
|
|
2371
|
+
]);
|
|
2372
|
+
if (!choice || choice === "Back") return BACK;
|
|
2373
|
+
await showTimeoutsSettings(orchestrator, ctx);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
async function showBooleanSetting(
|
|
2378
|
+
orchestrator: Orchestrator,
|
|
2379
|
+
ctx: any,
|
|
2380
|
+
title: string,
|
|
2381
|
+
keyPath: string[],
|
|
2382
|
+
): Promise<void> {
|
|
2383
|
+
while (true) {
|
|
2384
|
+
const info = getConfigSourceInfo(orchestrator, keyPath);
|
|
2385
|
+
const yesTitle = withTags("Yes", formatSourceTags(true, info));
|
|
2386
|
+
const noTitle = withTags("No", formatSourceTags(false, info));
|
|
2387
|
+
const choice = await selectOption(ctx, title, [
|
|
2388
|
+
yesTitle,
|
|
2389
|
+
noTitle,
|
|
2390
|
+
...buildResetOptions(orchestrator, keyPath),
|
|
2391
|
+
opt("Back", "Return to the previous menu"),
|
|
2392
|
+
]);
|
|
2393
|
+
if (!choice || choice === "Back") return;
|
|
2394
|
+
if (choice === yesTitle) {
|
|
2395
|
+
applyScopeChoice(orchestrator, keyPath, true, await pickScope(ctx, orchestrator));
|
|
2396
|
+
continue;
|
|
2397
|
+
}
|
|
2398
|
+
if (choice === noTitle) {
|
|
2399
|
+
applyScopeChoice(orchestrator, keyPath, false, await pickScope(ctx, orchestrator));
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
await maybeHandleResetChoice(orchestrator, ctx, choice, keyPath);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
async function showInvertedBooleanSetting(
|
|
2407
|
+
orchestrator: Orchestrator,
|
|
2408
|
+
ctx: any,
|
|
2409
|
+
title: string,
|
|
2410
|
+
keyPath: string[],
|
|
2411
|
+
): Promise<void> {
|
|
2412
|
+
while (true) {
|
|
2413
|
+
const info = getConfigSourceInfo(orchestrator, keyPath);
|
|
2414
|
+
const yesTitle = withTags("Yes", formatSourceTags(false, info));
|
|
2415
|
+
const noTitle = withTags("No", formatSourceTags(true, info));
|
|
2416
|
+
const choice = await selectOption(ctx, title, [
|
|
2417
|
+
yesTitle,
|
|
2418
|
+
noTitle,
|
|
2419
|
+
...buildResetOptions(orchestrator, keyPath),
|
|
2420
|
+
opt("Back", "Return to the previous menu"),
|
|
2421
|
+
]);
|
|
2422
|
+
if (!choice || choice === "Back") return;
|
|
2423
|
+
if (choice === yesTitle) {
|
|
2424
|
+
applyScopeChoice(orchestrator, keyPath, false, await pickScope(ctx, orchestrator));
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
if (choice === noTitle) {
|
|
2428
|
+
applyScopeChoice(orchestrator, keyPath, true, await pickScope(ctx, orchestrator));
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2431
|
+
await maybeHandleResetChoice(orchestrator, ctx, choice, keyPath);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
async function showLogLevelSetting(orchestrator: Orchestrator, ctx: any): Promise<void> {
|
|
2436
|
+
const levels: Array<{ value: PiPiConfig["general"]["logLevel"]; label: string }> = [
|
|
2437
|
+
{ value: "debug", label: "Debug" },
|
|
2438
|
+
{ value: "info", label: "Info" },
|
|
2439
|
+
{ value: "warn", label: "Warning" },
|
|
2440
|
+
{ value: "error", label: "Error" },
|
|
2441
|
+
];
|
|
2442
|
+
while (true) {
|
|
2443
|
+
const info = getConfigSourceInfo(orchestrator, ["general", "logLevel"]);
|
|
2444
|
+
const options: OptionInput[] = levels.map((entry) => withTags(entry.label, formatSourceTags(entry.value, info)));
|
|
2445
|
+
options.push(...buildResetOptions(orchestrator, ["general", "logLevel"]));
|
|
2446
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
2447
|
+
const choice = await selectOption(ctx, "Log level", options);
|
|
2448
|
+
if (!choice || choice === "Back") return;
|
|
2449
|
+
const picked = levels.find((entry) => choice.startsWith(entry.label));
|
|
2450
|
+
if (picked) {
|
|
2451
|
+
const scope = await pickScope(ctx, orchestrator);
|
|
2452
|
+
if (!scope) continue;
|
|
2453
|
+
applyConfigChange(orchestrator, scope, ["general", "logLevel"], picked.value);
|
|
2454
|
+
setLogLevel(orchestrator.config.general.logLevel);
|
|
2455
|
+
continue;
|
|
2456
|
+
}
|
|
2457
|
+
await maybeHandleResetChoice(orchestrator, ctx, choice, ["general", "logLevel"]);
|
|
2458
|
+
setLogLevel(orchestrator.config.general.logLevel);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
async function showGeneralSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2463
|
+
while (true) {
|
|
2464
|
+
const choice = await selectOption(ctx, "General", [
|
|
2465
|
+
opt(`Commit automatically: ${orchestrator.config.general.autoCommit ? "Yes" : "No"}`, "Enable or disable auto commits"),
|
|
2466
|
+
opt(`Ignore configs from other repos: ${orchestrator.config.general.loadExtraRepoConfigs ? "No" : "Yes"}`, "Load only root repo config"),
|
|
2467
|
+
opt(`Log level: ${logLevelLabel(orchestrator.config.general.logLevel)}`, "Logging verbosity"),
|
|
2468
|
+
opt(`Tracing: ${orchestrator.config.general.tracing ? "Yes" : "No"}`, "Capture full session traces to .pp/logs/traces/"),
|
|
2469
|
+
opt("Flant AI Infrastructure", "Configure corporate AI model provider"),
|
|
2470
|
+
opt("Back", "Return to the previous menu"),
|
|
2471
|
+
]);
|
|
2472
|
+
if (!choice || choice === "Back") return BACK;
|
|
2473
|
+
if (choice.startsWith("Commit automatically:")) {
|
|
2474
|
+
await showBooleanSetting(orchestrator, ctx, "Commit automatically", ["general", "autoCommit"]);
|
|
2475
|
+
continue;
|
|
2476
|
+
}
|
|
2477
|
+
if (choice.startsWith("Ignore configs from other repos:")) {
|
|
2478
|
+
await showInvertedBooleanSetting(orchestrator, ctx, "Ignore configs from other repos", ["general", "loadExtraRepoConfigs"]);
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
if (choice.startsWith("Log level:")) {
|
|
2482
|
+
await showLogLevelSetting(orchestrator, ctx);
|
|
2483
|
+
continue;
|
|
2484
|
+
}
|
|
2485
|
+
if (choice.startsWith("Tracing:")) {
|
|
2486
|
+
await showBooleanSetting(orchestrator, ctx, "Tracing", ["general", "tracing"]);
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
await showFlantInfraMenu(orchestrator, ctx);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
async function showAgentsSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2494
|
+
while (true) {
|
|
2495
|
+
const choice = await selectOption(ctx, "Agents", [
|
|
2496
|
+
opt("Orchestrators", "Orchestrator and subagent configuration"),
|
|
2497
|
+
opt("Subagents", "Simple subagents and preset groups"),
|
|
2498
|
+
opt("Back", "Return to the previous menu"),
|
|
2499
|
+
]);
|
|
2500
|
+
if (!choice || choice === "Back") return BACK;
|
|
2501
|
+
if (choice === "Orchestrators") {
|
|
2502
|
+
await showOrchestratorsSettings(orchestrator, ctx);
|
|
2503
|
+
continue;
|
|
2504
|
+
}
|
|
2505
|
+
await showSubagentSettings(orchestrator, ctx);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
async function showReposSettings(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2510
|
+
while (true) {
|
|
2511
|
+
const repos = orchestrator.active?.state.repos ?? [];
|
|
2512
|
+
if (repos.length === 0) {
|
|
2513
|
+
await selectOption(ctx, "No repos registered yet. The agent will register repos when it starts working.", [
|
|
2514
|
+
opt("Back", "Return to the previous menu"),
|
|
2515
|
+
]);
|
|
2516
|
+
return BACK;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
const options: OptionInput[] = repos.map((repo) => ({
|
|
2520
|
+
title: repo.path,
|
|
2521
|
+
description: `base: ${repo.baseBranch ?? "(not set)"}${repo.isRoot ? " (root)" : ""}`,
|
|
2522
|
+
}));
|
|
2523
|
+
options.push(opt("Back", "Return to the previous menu"));
|
|
2524
|
+
|
|
2525
|
+
const choice = await selectOption(ctx, "Repos", options);
|
|
2526
|
+
if (!choice || choice === "Back") return BACK;
|
|
2527
|
+
|
|
2528
|
+
const repo = repos.find((item) => item.path === choice);
|
|
2529
|
+
if (!repo) continue;
|
|
2530
|
+
|
|
2531
|
+
const repoOptions: OptionInput[] = [
|
|
2532
|
+
opt("Change base branch", `currently: ${repo.baseBranch ?? "(not set)"}`),
|
|
2533
|
+
opt("Back", "Return to repo list"),
|
|
2534
|
+
];
|
|
2535
|
+
const repoChoice = await selectOption(ctx, `${repo.path}${repo.isRoot ? " (root)" : ""}`, repoOptions);
|
|
2536
|
+
if (!repoChoice || repoChoice === "Back") continue;
|
|
2537
|
+
|
|
2538
|
+
if (repoChoice === "Change base branch") {
|
|
2539
|
+
const value = await ctx.ui.input("Base branch (e.g. origin/main):");
|
|
2540
|
+
if (value === undefined || value === null) continue;
|
|
2541
|
+
repo.baseBranch = String(value).trim() || undefined;
|
|
2542
|
+
saveTask(orchestrator.active!.dir, orchestrator.active!.state);
|
|
2543
|
+
unregisterAgentDefinitions(orchestrator.pi);
|
|
2544
|
+
orchestrator.registerAgents();
|
|
2545
|
+
ctx.ui.notify(`Base branch set to: ${repo.baseBranch ?? "(cleared)"}`, "info");
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
async function showLspSettings(ctx: any): Promise<typeof BACK> {
|
|
2551
|
+
while (true) {
|
|
2552
|
+
const choice = await selectOption(ctx, "LSP", [
|
|
2553
|
+
opt("Restart all servers", "Stop all servers. They reinitialize on next use"),
|
|
2554
|
+
opt("Back", "Return to the previous menu"),
|
|
2555
|
+
]);
|
|
2556
|
+
if (!choice || choice === "Back") return BACK;
|
|
2557
|
+
|
|
2558
|
+
const api = (globalThis as any)[Symbol.for("pi-lsp:api")] as {
|
|
2559
|
+
restart?: (menuCtx: any) => Promise<void>;
|
|
2560
|
+
} | undefined;
|
|
2561
|
+
if (!api?.restart) {
|
|
2562
|
+
ctx.ui.notify("LSP API is not available.", "warning");
|
|
2563
|
+
continue;
|
|
2564
|
+
}
|
|
2565
|
+
try {
|
|
2566
|
+
await api.restart(ctx);
|
|
2567
|
+
} catch (error: any) {
|
|
2568
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2569
|
+
ctx.ui.notify(`Failed to restart LSP servers: ${message}`, "error");
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
async function showSettingsMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
|
|
2575
|
+
while (true) {
|
|
2576
|
+
const options: OptionInput[] = [
|
|
2577
|
+
opt("General", "Commit, log level, Flant AI, repos"),
|
|
2578
|
+
opt("Agents", "Orchestrator and subagent configuration"),
|
|
2579
|
+
opt("Commands", "After file edit and after implementation"),
|
|
2580
|
+
opt("Performance", "Timeout configuration"),
|
|
2581
|
+
opt("LSP", "Language server controls"),
|
|
2582
|
+
opt("Back", "Return to the previous menu"),
|
|
2583
|
+
];
|
|
2584
|
+
|
|
2585
|
+
const choice = await selectOption(ctx, "Settings", options);
|
|
2586
|
+
if (!choice || choice === "Back") return BACK;
|
|
2587
|
+
|
|
2588
|
+
if (choice === "General") await showGeneralSettings(orchestrator, ctx);
|
|
2589
|
+
else if (choice === "Agents") await showAgentsSettings(orchestrator, ctx);
|
|
2590
|
+
else if (choice === "Commands") await showCommandsSettings(orchestrator, ctx);
|
|
2591
|
+
else if (choice === "Performance") await showPerformanceSettings(orchestrator, ctx);
|
|
2592
|
+
else if (choice === "LSP") await showLspSettings(ctx);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
// First phases (brainstorm/debug/review) are always interactive and can never run
|
|
2597
|
+
// autonomously — autonomous configs only ever cover plan/implement.
|
|
2598
|
+
function autonomousPhasesForTask(type: TaskType): string[] {
|
|
2599
|
+
if (type === "implement" || type === "debug" || type === "review") return ["plan", "implement"];
|
|
2600
|
+
return [];
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
function defaultAutonomousReviewPreset(type: TaskType, phase: string): string {
|
|
2604
|
+
if (type === "review" && phase === "review") return "deep";
|
|
2605
|
+
return "regular";
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
function buildDefaultAutonomousConfig(type: TaskType): AutonomousConfig {
|
|
2609
|
+
const phases = autonomousPhasesForTask(type);
|
|
2610
|
+
const out: AutonomousConfig = { phases: {} };
|
|
2611
|
+
for (const phase of phases) {
|
|
2612
|
+
out.phases[phase] = {
|
|
2613
|
+
reviewPreset: defaultAutonomousReviewPreset(type, phase),
|
|
2614
|
+
maxReviewPasses: 3,
|
|
2615
|
+
...(phase === "plan" ? { plannerPreset: "regular" } : {}),
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
return out;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
async function showTaskModePicker(ctx: any): Promise<TaskMode | "back"> {
|
|
2622
|
+
const mode = await selectOption(ctx, "Mode", [
|
|
2623
|
+
opt("Guided", "User gates at every phase transition"),
|
|
2624
|
+
opt("Autonomous", "Full pipeline with automatic phase transitions"),
|
|
2625
|
+
opt("Back", "Return to the previous menu"),
|
|
2626
|
+
]);
|
|
2627
|
+
if (!mode || mode === "Back") return "back";
|
|
2628
|
+
return mode === "Autonomous" ? "autonomous" : "guided";
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
export async function pickMaxReviewPasses(ctx: any, current: number): Promise<number | null> {
|
|
2632
|
+
const currentLabel = current >= 999 ? "-" : String(current);
|
|
2633
|
+
while (true) {
|
|
2634
|
+
const input = await ctx.ui.input(
|
|
2635
|
+
`Max review passes (enter a positive integer, or "-" for unlimited) [${currentLabel}]`,
|
|
2636
|
+
);
|
|
2637
|
+
if (input === undefined || input === null) return null;
|
|
2638
|
+
const trimmed = String(input).trim();
|
|
2639
|
+
if (trimmed === "") return null;
|
|
2640
|
+
if (trimmed === "-") return 999;
|
|
2641
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
2642
|
+
ctx.ui.notify('Please enter a positive integer, or "-" for unlimited.', "warning");
|
|
2643
|
+
continue;
|
|
2644
|
+
}
|
|
2645
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
2646
|
+
if (parsed <= 0) {
|
|
2647
|
+
ctx.ui.notify('Please enter a positive integer, or "-" for unlimited.', "warning");
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
return parsed;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
async function showAutonomousPhaseSettings(
|
|
2655
|
+
orchestrator: Orchestrator,
|
|
2656
|
+
ctx: any,
|
|
2657
|
+
type: TaskType,
|
|
2658
|
+
phase: string,
|
|
2659
|
+
config: AutonomousConfig,
|
|
2660
|
+
): Promise<void> {
|
|
2661
|
+
const phaseConfig = config.phases[phase] ?? {
|
|
2662
|
+
reviewPreset: defaultAutonomousReviewPreset(type, phase),
|
|
2663
|
+
maxReviewPasses: 3,
|
|
2664
|
+
...(phase === "plan" ? { plannerPreset: "regular" } : {}),
|
|
2665
|
+
};
|
|
2666
|
+
config.phases[phase] = phaseConfig;
|
|
2667
|
+
|
|
2668
|
+
while (true) {
|
|
2669
|
+
const reviewPreset = phaseConfig.reviewPreset ?? defaultAutonomousReviewPreset(type, phase);
|
|
2670
|
+
const maxReview = phaseConfig.maxReviewPasses >= 999 ? "No limit" : String(phaseConfig.maxReviewPasses);
|
|
2671
|
+
const options: OptionInput[] = [
|
|
2672
|
+
opt(`Review preset: ${reviewPreset}`, "Select review preset"),
|
|
2673
|
+
];
|
|
2674
|
+
if (phase === "plan") {
|
|
2675
|
+
options.push(opt(`Planner preset: ${phaseConfig.plannerPreset ?? "regular"}`, "Select planner preset"));
|
|
2676
|
+
}
|
|
2677
|
+
options.push(opt(`Max review passes: ${maxReview}`, "Safety cap for autonomous review loops"));
|
|
2678
|
+
options.push(opt("Back", "Return to autonomous settings"));
|
|
2679
|
+
|
|
2680
|
+
const choice = await selectOption(ctx, `${phase.charAt(0).toUpperCase()}${phase.slice(1)} phase`, options);
|
|
2681
|
+
if (!choice || choice === "Back") return;
|
|
2682
|
+
|
|
2683
|
+
if (choice.startsWith("Review preset:")) {
|
|
2684
|
+
const group = getReviewPresetGroup(phase);
|
|
2685
|
+
const picked = await pickPreset(ctx, orchestrator, group, "Review preset");
|
|
2686
|
+
if (picked) phaseConfig.reviewPreset = picked;
|
|
2687
|
+
continue;
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
if (choice.startsWith("Planner preset:")) {
|
|
2691
|
+
const picked = await pickPreset(ctx, orchestrator, "planners", "Planner preset");
|
|
2692
|
+
if (picked) phaseConfig.plannerPreset = picked;
|
|
2693
|
+
continue;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
const pickedMax = await pickMaxReviewPasses(ctx, phaseConfig.maxReviewPasses);
|
|
2697
|
+
if (pickedMax !== null) phaseConfig.maxReviewPasses = pickedMax;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
async function showAutonomousSettings(
|
|
2702
|
+
orchestrator: Orchestrator,
|
|
2703
|
+
ctx: any,
|
|
2704
|
+
type: TaskType,
|
|
2705
|
+
): Promise<AutonomousConfig | null> {
|
|
2706
|
+
const config = buildDefaultAutonomousConfig(type);
|
|
2707
|
+
const phases = autonomousPhasesForTask(type);
|
|
2708
|
+
|
|
2709
|
+
while (true) {
|
|
2710
|
+
const options: OptionInput[] = [
|
|
2711
|
+
opt("Start", "Begin with current autonomous settings"),
|
|
2712
|
+
...phases.map((phase) => opt(`${phase.charAt(0).toUpperCase()}${phase.slice(1)} phase`, "Configure presets and review passes")),
|
|
2713
|
+
opt("Back", "Return to mode selection"),
|
|
2714
|
+
];
|
|
2715
|
+
const choice = await selectOption(ctx, "Autonomous", options);
|
|
2716
|
+
if (!choice || choice === "Back") return null;
|
|
2717
|
+
if (choice === "Start") return config;
|
|
2718
|
+
|
|
2719
|
+
const phase = choice.replace(/ phase$/, "").toLowerCase();
|
|
2720
|
+
if (!phases.includes(phase)) continue;
|
|
2721
|
+
await showAutonomousPhaseSettings(orchestrator, ctx, type, phase, config);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
async function pickModeForTaskStart(
|
|
2726
|
+
orchestrator: Orchestrator,
|
|
2727
|
+
ctx: any,
|
|
2728
|
+
type: TaskType,
|
|
2729
|
+
): Promise<{ mode: TaskMode; autonomousConfig?: AutonomousConfig } | null> {
|
|
2730
|
+
while (true) {
|
|
2731
|
+
const mode = await showTaskModePicker(ctx);
|
|
2732
|
+
if (mode === "back") return null;
|
|
2733
|
+
if (mode === "guided") return { mode: "guided" };
|
|
2734
|
+
const autonomousConfig = await showAutonomousSettings(orchestrator, ctx, type);
|
|
2735
|
+
if (!autonomousConfig) continue;
|
|
2736
|
+
return { mode: "autonomous", autonomousConfig };
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
function resumeOptionTitle(t: TaskInfo): string {
|
|
2741
|
+
return taskName(t.dir);
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
function resumeOptionDescription(t: TaskInfo): string {
|
|
2745
|
+
const age = taskAge(t.state);
|
|
2746
|
+
const phase = t.state.phase === t.type ? t.type : `${t.type}/${t.state.phase}`;
|
|
2747
|
+
return `${phase}, ${age}`;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
async function showResumeMenu(
|
|
2751
|
+
orchestrator: Orchestrator,
|
|
2752
|
+
ctx: any,
|
|
2753
|
+
type: TaskType | undefined,
|
|
2754
|
+
emptyMessage: string,
|
|
2755
|
+
): Promise<typeof BACK | "started"> {
|
|
2756
|
+
while (true) {
|
|
2757
|
+
const tasks = listTasks(orchestrator.cwd, type);
|
|
2758
|
+
if (tasks.length === 0) {
|
|
2759
|
+
ctx.ui.notify(emptyMessage, "info");
|
|
2760
|
+
return BACK;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
const options: OptionInput[] = tasks.map((t) => ({
|
|
2764
|
+
title: resumeOptionTitle(t),
|
|
2765
|
+
description: resumeOptionDescription(t),
|
|
2766
|
+
}));
|
|
2767
|
+
options.push({ title: "Back", description: "Return to the previous menu" });
|
|
2768
|
+
|
|
2769
|
+
const choice = await selectOption(ctx, "Resume", options);
|
|
2770
|
+
if (!choice || choice === "Back") return BACK;
|
|
2771
|
+
|
|
2772
|
+
const task = tasks.find((t) => resumeOptionTitle(t) === choice);
|
|
2773
|
+
if (!task) continue;
|
|
2774
|
+
const result = await resumeTask(orchestrator, ctx, task);
|
|
2775
|
+
if (result.ok) return "started";
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
function fromOptionTitle(t: TaskInfo): string {
|
|
2780
|
+
return taskName(t.dir);
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
function fromOptionDescription(t: TaskInfo, cwd: string): string {
|
|
2784
|
+
const age = taskAge(t.state);
|
|
2785
|
+
const rel = relative(join(cwd, ".pp", "state"), t.dir);
|
|
2786
|
+
return `${t.type}, ${age} — ${rel}`;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
async function showFromMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK | "started"> {
|
|
2790
|
+
while (true) {
|
|
2791
|
+
const tasks = listCompletedFromTasks(orchestrator.cwd);
|
|
2792
|
+
if (tasks.length === 0) {
|
|
2793
|
+
ctx.ui.notify("No completed brainstorm/debug/review tasks with artifacts found.", "info");
|
|
2794
|
+
return BACK;
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
const options: OptionInput[] = tasks.map((t) => ({
|
|
2798
|
+
title: fromOptionTitle(t),
|
|
2799
|
+
description: fromOptionDescription(t, orchestrator.cwd),
|
|
2800
|
+
}));
|
|
2801
|
+
options.push({ title: "Back", description: "Return to the previous menu" });
|
|
2802
|
+
|
|
2803
|
+
const choice = await selectOption(ctx, "From", options);
|
|
2804
|
+
if (!choice || choice === "Back") return BACK;
|
|
2805
|
+
|
|
2806
|
+
const selected = tasks.find((t) => fromOptionTitle(t) === choice);
|
|
2807
|
+
if (!selected) continue;
|
|
2808
|
+
|
|
2809
|
+
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "implement");
|
|
2810
|
+
if (!modeSelection) continue;
|
|
2811
|
+
await orchestrator.startTask(ctx, "implement", "implement", selected.dir, true, modeSelection.mode);
|
|
2812
|
+
if (orchestrator.active) {
|
|
2813
|
+
orchestrator.active.state.autonomousConfig = modeSelection.autonomousConfig;
|
|
2814
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
2815
|
+
}
|
|
2816
|
+
return "started";
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
async function showImplementMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK | "started"> {
|
|
2821
|
+
while (true) {
|
|
2822
|
+
const choice = await selectOption(ctx, "Implement", [
|
|
2823
|
+
{ title: "New", description: "Start a new implementation from scratch" },
|
|
2824
|
+
{ title: "From", description: "Continue from a completed brainstorm or debug task" },
|
|
2825
|
+
{ title: "Resume", description: "Resume a paused implementation" },
|
|
2826
|
+
{ title: "Back", description: "Return to the previous menu" },
|
|
2827
|
+
]);
|
|
2828
|
+
if (!choice || choice === "Back") return BACK;
|
|
2829
|
+
|
|
2830
|
+
if (choice === "New") {
|
|
2831
|
+
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "implement");
|
|
2832
|
+
if (!modeSelection) continue;
|
|
2833
|
+
await orchestrator.startTask(ctx, "implement", "implement", undefined, undefined, modeSelection.mode);
|
|
2834
|
+
if (orchestrator.active) {
|
|
2835
|
+
orchestrator.active.state.autonomousConfig = modeSelection.autonomousConfig;
|
|
2836
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
2837
|
+
}
|
|
2838
|
+
return "started";
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
if (choice === "From") {
|
|
2842
|
+
const result = await showFromMenu(orchestrator, ctx);
|
|
2843
|
+
if (result === "started") return result;
|
|
2844
|
+
continue;
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
const result = await showResumeMenu(orchestrator, ctx, "implement", "No paused implement tasks found.");
|
|
2848
|
+
if (result === "started") return result;
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
function buildPrContext(parsed: any): { prUrl: string | null; prContext: string | null } {
|
|
2853
|
+
const title = typeof parsed?.title === "string" ? parsed.title.trim() : "";
|
|
2854
|
+
const body = typeof parsed?.body === "string" ? parsed.body.trim() : "";
|
|
2855
|
+
const commentsRaw = Array.isArray(parsed?.comments)
|
|
2856
|
+
? parsed.comments
|
|
2857
|
+
: Array.isArray(parsed?.comments?.nodes)
|
|
2858
|
+
? parsed.comments.nodes
|
|
2859
|
+
: [];
|
|
2860
|
+
const comments: string[] = commentsRaw
|
|
2861
|
+
.map((comment: any) => {
|
|
2862
|
+
const text = typeof comment?.body === "string"
|
|
2863
|
+
? comment.body.trim()
|
|
2864
|
+
: typeof comment?.bodyText === "string"
|
|
2865
|
+
? comment.bodyText.trim()
|
|
2866
|
+
: "";
|
|
2867
|
+
return text;
|
|
2868
|
+
})
|
|
2869
|
+
.filter((text: string): text is string => text.length > 0);
|
|
2870
|
+
|
|
2871
|
+
const parts: string[] = [];
|
|
2872
|
+
if (title) parts.push(`Title: ${title}`);
|
|
2873
|
+
if (body) parts.push(`Body:\n${body}`);
|
|
2874
|
+
if (comments.length > 0) {
|
|
2875
|
+
parts.push(`Comments:\n${comments.map((comment, index) => `${index + 1}. ${comment}`).join("\n\n")}`);
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
const prUrl = typeof parsed?.url === "string" && parsed.url.trim().length > 0 ? parsed.url.trim() : null;
|
|
882
2879
|
return { prUrl, prContext: parts.length > 0 ? parts.join("\n\n") : null };
|
|
883
2880
|
}
|
|
884
2881
|
|
|
885
|
-
async function detectCurrentPrContext(orchestrator: Orchestrator): Promise<
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
2882
|
+
async function detectCurrentPrContext(orchestrator: Orchestrator, repos: RepoInfo[]): Promise<RepoPrContext[]> {
|
|
2883
|
+
const results: RepoPrContext[] = [];
|
|
2884
|
+
for (const repo of repos) {
|
|
2885
|
+
try {
|
|
2886
|
+
const prResult = await orchestrator.pi.exec("gh", ["pr", "view", "--json", "url,title,body,comments"], {
|
|
2887
|
+
cwd: repo.path,
|
|
2888
|
+
timeout: 10000,
|
|
2889
|
+
});
|
|
2890
|
+
if (prResult.code !== 0) continue;
|
|
2891
|
+
const parsed = JSON.parse(prResult.stdout);
|
|
2892
|
+
const pr = buildPrContext(parsed);
|
|
2893
|
+
if (pr.prUrl || pr.prContext) {
|
|
2894
|
+
results.push({ repoPath: repo.path, prUrl: pr.prUrl, prContext: pr.prContext });
|
|
2895
|
+
}
|
|
2896
|
+
} catch {}
|
|
896
2897
|
}
|
|
2898
|
+
return results;
|
|
897
2899
|
}
|
|
898
2900
|
|
|
899
|
-
async function openCodeReviewInPlannotator(
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
2901
|
+
async function openCodeReviewInPlannotator(
|
|
2902
|
+
orchestrator: Orchestrator,
|
|
2903
|
+
payload: { cwd: string; diffType?: string; defaultBranch?: string },
|
|
2904
|
+
): Promise<{ status: "approved" | "needs_changes" | "error"; feedback?: string; error?: string }> {
|
|
2905
|
+
const requestPayload: Record<string, unknown> = { cwd: payload.cwd };
|
|
2906
|
+
if (payload.diffType) requestPayload.diffType = payload.diffType;
|
|
2907
|
+
if (payload.defaultBranch) requestPayload.defaultBranch = payload.defaultBranch;
|
|
903
2908
|
|
|
904
2909
|
return await new Promise((resolve) => {
|
|
905
2910
|
let handled = false;
|
|
2911
|
+
const timer = setTimeout(() => {
|
|
2912
|
+
if (!handled) resolve({ status: "error", error: "Plannotator is not available." });
|
|
2913
|
+
}, 30000);
|
|
906
2914
|
orchestrator.pi.events.emit("plannotator:request", {
|
|
907
2915
|
requestId: crypto.randomUUID(),
|
|
908
2916
|
action: "code-review",
|
|
909
|
-
payload,
|
|
2917
|
+
payload: requestPayload,
|
|
910
2918
|
respond: (response: any) => {
|
|
911
2919
|
handled = true;
|
|
2920
|
+
clearTimeout(timer);
|
|
912
2921
|
if (response?.status !== "handled") {
|
|
913
|
-
resolve(
|
|
2922
|
+
resolve({ status: "error", error: response?.error || "Plannotator is not available." });
|
|
914
2923
|
return;
|
|
915
2924
|
}
|
|
916
2925
|
const approved = !!response?.result?.approved;
|
|
917
2926
|
const feedback = typeof response?.result?.feedback === "string" && response.result.feedback.trim().length > 0
|
|
918
|
-
?
|
|
919
|
-
:
|
|
920
|
-
resolve(approved ? "
|
|
2927
|
+
? response.result.feedback
|
|
2928
|
+
: undefined;
|
|
2929
|
+
resolve({ status: approved ? "approved" : "needs_changes", feedback });
|
|
921
2930
|
},
|
|
922
2931
|
});
|
|
923
|
-
setTimeout(() => {
|
|
924
|
-
if (!handled) resolve("Plannotator is not available.");
|
|
925
|
-
}, 30000);
|
|
926
2932
|
});
|
|
927
2933
|
}
|
|
928
2934
|
|
|
@@ -932,10 +2938,16 @@ async function startReviewTask(
|
|
|
932
2938
|
userRequestContent: string,
|
|
933
2939
|
researchContent: string | null,
|
|
934
2940
|
description: string,
|
|
2941
|
+
mode?: TaskMode,
|
|
2942
|
+
autonomousConfig?: AutonomousConfig,
|
|
935
2943
|
): Promise<"started" | typeof BACK> {
|
|
936
|
-
await orchestrator.startTask(ctx, "review", description);
|
|
2944
|
+
await orchestrator.startTask(ctx, "review", description, undefined, undefined, mode);
|
|
937
2945
|
if (!orchestrator.active || orchestrator.active.type !== "review") return BACK;
|
|
938
|
-
|
|
2946
|
+
orchestrator.active.state.autonomousConfig = autonomousConfig;
|
|
2947
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
2948
|
+
const repos = getRegisteredRepos(orchestrator);
|
|
2949
|
+
const userRequestWithRepos = appendRepoContext(userRequestContent, repos);
|
|
2950
|
+
writeFileSync(join(orchestrator.active.dir, "USER_REQUEST.md"), userRequestWithRepos, "utf-8");
|
|
939
2951
|
if (researchContent) {
|
|
940
2952
|
writeFileSync(join(orchestrator.active.dir, "RESEARCH.md"), researchContent, "utf-8");
|
|
941
2953
|
}
|
|
@@ -944,13 +2956,16 @@ async function startReviewTask(
|
|
|
944
2956
|
|
|
945
2957
|
async function showReviewMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK | "started"> {
|
|
946
2958
|
while (true) {
|
|
947
|
-
const
|
|
2959
|
+
const options: OptionInput[] = [
|
|
948
2960
|
{ title: "Current branch", description: "Review changes on current branch vs base" },
|
|
2961
|
+
{ title: "Last commit", description: "Review changes in the most recent commit" },
|
|
2962
|
+
{ title: "Since commit", description: "Review all changes since a specific commit" },
|
|
949
2963
|
{ title: "Uncommitted changes", description: "Review working directory changes" },
|
|
950
2964
|
{ title: "Describe", description: "Describe what to review and let the agent figure it out" },
|
|
951
2965
|
{ title: "Resume", description: "Resume a previously unfinished review" },
|
|
952
2966
|
{ title: "Back", description: "Return to the previous menu" },
|
|
953
|
-
]
|
|
2967
|
+
];
|
|
2968
|
+
const choice = await selectOption(ctx, "Review", options);
|
|
954
2969
|
if (!choice || choice === "Back") return BACK;
|
|
955
2970
|
|
|
956
2971
|
if (choice === "Resume") {
|
|
@@ -960,38 +2975,132 @@ async function showReviewMenu(orchestrator: Orchestrator, ctx: any): Promise<typ
|
|
|
960
2975
|
}
|
|
961
2976
|
|
|
962
2977
|
if (choice === "Current branch") {
|
|
963
|
-
const
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
2978
|
+
const repos = getRegisteredRepos(orchestrator);
|
|
2979
|
+
const repoRanges = await Promise.all(
|
|
2980
|
+
repos.map(async (repo) => `- ${formatRepoLabel(repo)}: ${await detectDefaultBranch(orchestrator, repos, repo.path)}..HEAD`),
|
|
2981
|
+
);
|
|
2982
|
+
const prContexts = await detectCurrentPrContext(orchestrator, repos);
|
|
2983
|
+
|
|
2984
|
+
const urLines = [
|
|
2985
|
+
"# User Request",
|
|
2986
|
+
"Review current branch changes across registered repositories.",
|
|
2987
|
+
"",
|
|
2988
|
+
"Diff ranges:",
|
|
2989
|
+
...repoRanges,
|
|
2990
|
+
];
|
|
2991
|
+
const prUrls = prContexts
|
|
2992
|
+
.filter((pr) => pr.prUrl)
|
|
2993
|
+
.map((pr) => `- ${pr.repoPath}: ${pr.prUrl}`);
|
|
2994
|
+
if (prUrls.length > 0) {
|
|
2995
|
+
urLines.push("", "Open PRs:", ...prUrls);
|
|
2996
|
+
}
|
|
968
2997
|
urLines.push("", "## Problem", "Review and identify issues in the code changes.", "", "## Constraints", "Focus on correctness, edge cases, style, missing tests, potential bugs.");
|
|
969
2998
|
const urContent = urLines.join("\n") + "\n";
|
|
970
2999
|
|
|
971
|
-
let resContent
|
|
972
|
-
|
|
973
|
-
|
|
3000
|
+
let resContent = [
|
|
3001
|
+
"## Affected Code",
|
|
3002
|
+
"(to be filled during review)",
|
|
3003
|
+
"",
|
|
3004
|
+
"## Architecture Context",
|
|
3005
|
+
"(to be filled during review)",
|
|
3006
|
+
"",
|
|
3007
|
+
"## Constraints & Edge Cases",
|
|
3008
|
+
"- MUST: Review all changed code across the registered repositories",
|
|
3009
|
+
"- RISK: Issues can span repository boundaries",
|
|
3010
|
+
].join("\n") + "\n";
|
|
3011
|
+
if (prContexts.length > 0) {
|
|
3012
|
+
const prContextBlocks = prContexts
|
|
3013
|
+
.map((pr) => {
|
|
3014
|
+
const lines = [`${pr.repoPath}`];
|
|
3015
|
+
if (pr.prUrl) lines.push(`URL: ${pr.prUrl}`);
|
|
3016
|
+
if (pr.prContext) lines.push(pr.prContext);
|
|
3017
|
+
return lines.join("\n");
|
|
3018
|
+
})
|
|
3019
|
+
.join("\n\n");
|
|
3020
|
+
resContent = appendResearchOpenQuestions(resContent, `PR context:\n${prContextBlocks}`);
|
|
974
3021
|
}
|
|
975
3022
|
|
|
976
|
-
const
|
|
977
|
-
if (!
|
|
978
|
-
|
|
3023
|
+
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3024
|
+
if (!modeSelection) continue;
|
|
3025
|
+
const description = "review-current-branch";
|
|
3026
|
+
return startReviewTask(orchestrator, ctx, urContent, resContent, description, modeSelection.mode, modeSelection.autonomousConfig);
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
if (choice === "Last commit") {
|
|
3030
|
+
const repos = getRegisteredRepos(orchestrator);
|
|
3031
|
+
const urContent = [
|
|
3032
|
+
"# User Request",
|
|
3033
|
+
"Review last commit changes across registered repositories",
|
|
3034
|
+
"",
|
|
3035
|
+
"## Problem",
|
|
3036
|
+
"Review and identify issues in the most recent commit.",
|
|
3037
|
+
"",
|
|
3038
|
+
"## Constraints",
|
|
3039
|
+
"Focus on correctness, edge cases, style, missing tests, potential bugs.",
|
|
3040
|
+
"",
|
|
3041
|
+
].join("\n");
|
|
3042
|
+
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3043
|
+
if (!modeSelection) continue;
|
|
3044
|
+
return startReviewTask(orchestrator, ctx, urContent, null, "review-last-commit", modeSelection.mode, modeSelection.autonomousConfig);
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
if (choice === "Since commit") {
|
|
3048
|
+
const repos = getRegisteredRepos(orchestrator);
|
|
3049
|
+
const repoOptions: OptionInput[] = repos.map((repo) => ({
|
|
3050
|
+
title: formatRepoLabel(repo),
|
|
3051
|
+
description: "Choose repository for commit range",
|
|
3052
|
+
}));
|
|
3053
|
+
repoOptions.push({ title: "Back", description: "Return to the previous menu" });
|
|
3054
|
+
const repoChoice = await selectOption(ctx, "Select repository", repoOptions);
|
|
3055
|
+
if (!repoChoice || repoChoice === "Back") continue;
|
|
3056
|
+
const selectedRepo = repos.find((repo) => formatRepoLabel(repo) === repoChoice);
|
|
3057
|
+
if (!selectedRepo) continue;
|
|
3058
|
+
const pickedHash = await pickCommitForRepo(orchestrator, ctx, selectedRepo);
|
|
3059
|
+
if (!pickedHash) continue;
|
|
3060
|
+
|
|
3061
|
+
const urContent = [
|
|
3062
|
+
"# User Request",
|
|
3063
|
+
`Review changes in ${selectedRepo.path} since commit ${pickedHash}`,
|
|
3064
|
+
"",
|
|
3065
|
+
"## Problem",
|
|
3066
|
+
`Review and identify issues in all changes in ${selectedRepo.path} since ${pickedHash}.`,
|
|
3067
|
+
"",
|
|
3068
|
+
"## Constraints",
|
|
3069
|
+
"Focus on correctness, edge cases, style, missing tests, potential bugs.",
|
|
3070
|
+
"",
|
|
3071
|
+
].join("\n");
|
|
3072
|
+
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3073
|
+
if (!modeSelection) continue;
|
|
3074
|
+
return startReviewTask(orchestrator, ctx, urContent, null, "review-since-commit", modeSelection.mode, modeSelection.autonomousConfig);
|
|
979
3075
|
}
|
|
980
3076
|
|
|
981
3077
|
if (choice === "Uncommitted changes") {
|
|
982
|
-
const
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
3078
|
+
const repos = getRegisteredRepos(orchestrator);
|
|
3079
|
+
const urContent = [
|
|
3080
|
+
"# User Request",
|
|
3081
|
+
"Review uncommitted changes across registered repositories",
|
|
3082
|
+
"",
|
|
3083
|
+
"## Problem",
|
|
3084
|
+
"Review and identify issues in uncommitted working directory changes.",
|
|
3085
|
+
"",
|
|
3086
|
+
"## Constraints",
|
|
3087
|
+
"Focus on correctness, edge cases, style, missing tests, potential bugs.",
|
|
3088
|
+
"",
|
|
3089
|
+
].join("\n");
|
|
3090
|
+
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3091
|
+
if (!modeSelection) continue;
|
|
3092
|
+
return startReviewTask(orchestrator, ctx, urContent, null, "review-uncommitted", modeSelection.mode, modeSelection.autonomousConfig);
|
|
986
3093
|
}
|
|
987
3094
|
|
|
988
3095
|
const input = await ctx.ui.input("Describe what to review");
|
|
989
3096
|
if (input === undefined || input === null) continue;
|
|
990
3097
|
const trimmed = String(input).trim();
|
|
991
|
-
|
|
3098
|
+
const description = trimmed || "review";
|
|
992
3099
|
|
|
993
|
-
const urContent = `# User Request\n${
|
|
994
|
-
|
|
3100
|
+
const urContent = `# User Request\n${description}\n\n## Problem\n${description}\n\n## Constraints\nFocus on correctness, edge cases, style, missing tests, potential bugs.\n`;
|
|
3101
|
+
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "review");
|
|
3102
|
+
if (!modeSelection) continue;
|
|
3103
|
+
return startReviewTask(orchestrator, ctx, urContent, null, description, modeSelection.mode, modeSelection.autonomousConfig);
|
|
995
3104
|
}
|
|
996
3105
|
}
|
|
997
3106
|
|
|
@@ -999,7 +3108,6 @@ async function showTaskTypeMenu(
|
|
|
999
3108
|
orchestrator: Orchestrator,
|
|
1000
3109
|
ctx: any,
|
|
1001
3110
|
type: TaskType,
|
|
1002
|
-
inputPrompt: string,
|
|
1003
3111
|
): Promise<typeof BACK | "started"> {
|
|
1004
3112
|
while (true) {
|
|
1005
3113
|
const choice = await selectOption(ctx, type.charAt(0).toUpperCase() + type.slice(1), [
|
|
@@ -1010,9 +3118,19 @@ async function showTaskTypeMenu(
|
|
|
1010
3118
|
if (!choice || choice === "Back") return BACK;
|
|
1011
3119
|
|
|
1012
3120
|
if (choice === "New") {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
3121
|
+
let mode: TaskMode | undefined;
|
|
3122
|
+
let autonomousConfig: AutonomousConfig | undefined;
|
|
3123
|
+
if (type === "debug") {
|
|
3124
|
+
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, type);
|
|
3125
|
+
if (!modeSelection) continue;
|
|
3126
|
+
mode = modeSelection.mode;
|
|
3127
|
+
autonomousConfig = modeSelection.autonomousConfig;
|
|
3128
|
+
}
|
|
3129
|
+
await orchestrator.startTask(ctx, type, type, undefined, undefined, mode);
|
|
3130
|
+
if (orchestrator.active) {
|
|
3131
|
+
orchestrator.active.state.autonomousConfig = autonomousConfig;
|
|
3132
|
+
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
3133
|
+
}
|
|
1016
3134
|
return "started";
|
|
1017
3135
|
}
|
|
1018
3136
|
|
|
@@ -1021,78 +3139,95 @@ async function showTaskTypeMenu(
|
|
|
1021
3139
|
}
|
|
1022
3140
|
}
|
|
1023
3141
|
|
|
1024
|
-
async function
|
|
3142
|
+
async function showTaskMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK | "started"> {
|
|
1025
3143
|
while (true) {
|
|
1026
|
-
const choice = await selectOption(ctx, "
|
|
1027
|
-
{ title: "
|
|
1028
|
-
{ title: "
|
|
1029
|
-
{ title: "
|
|
1030
|
-
{ title: "Review", description: "Review
|
|
3144
|
+
const choice = await selectOption(ctx, "Task", [
|
|
3145
|
+
{ title: "Implement", description: "Want to make some changes? Research any topic or a codebase, brainstorm solutions and implement the chosen one" },
|
|
3146
|
+
{ title: "Debug", description: "Something is broken? Investigate it. If there is an issue — brainstorm solutions and fix it" },
|
|
3147
|
+
{ title: "Brainstorm", description: "No idea where to start? Research any topic or a codebase. If there is a problem to solve — brainstorm solutions and solve it" },
|
|
3148
|
+
{ title: "Review", description: "Want to ensure that some commits or a GitHub PR are good to go? Review it. Even fix it yourself, if you want" },
|
|
3149
|
+
{ title: "Quick", description: "Quick freeform task — no phases, no reviews, just work" },
|
|
1031
3150
|
{ title: "Resume", description: "Resume a previously unfinished task" },
|
|
1032
|
-
{ title: "
|
|
1033
|
-
{ title: "Settings", description: "LSP, Flant AI, and other configuration" },
|
|
1034
|
-
{ title: "Back", description: "Close this menu" },
|
|
3151
|
+
{ title: "Back", description: "Return to the previous menu" },
|
|
1035
3152
|
]);
|
|
1036
|
-
if (!choice || choice === "Back") return
|
|
3153
|
+
if (!choice || choice === "Back") return BACK;
|
|
1037
3154
|
|
|
1038
3155
|
if (choice === "Debug") {
|
|
1039
|
-
const result = await showTaskTypeMenu(orchestrator, ctx, "debug"
|
|
1040
|
-
if (result === "started") return
|
|
3156
|
+
const result = await showTaskTypeMenu(orchestrator, ctx, "debug");
|
|
3157
|
+
if (result === "started") return "started";
|
|
1041
3158
|
continue;
|
|
1042
3159
|
}
|
|
1043
3160
|
|
|
1044
3161
|
if (choice === "Brainstorm") {
|
|
1045
|
-
const result = await showTaskTypeMenu(orchestrator, ctx, "brainstorm"
|
|
1046
|
-
if (result === "started") return
|
|
3162
|
+
const result = await showTaskTypeMenu(orchestrator, ctx, "brainstorm");
|
|
3163
|
+
if (result === "started") return "started";
|
|
1047
3164
|
continue;
|
|
1048
3165
|
}
|
|
1049
3166
|
|
|
1050
3167
|
if (choice === "Implement") {
|
|
1051
3168
|
const result = await showImplementMenu(orchestrator, ctx);
|
|
1052
|
-
if (result === "started") return
|
|
3169
|
+
if (result === "started") return "started";
|
|
1053
3170
|
continue;
|
|
1054
3171
|
}
|
|
1055
3172
|
|
|
1056
3173
|
if (choice === "Review") {
|
|
1057
3174
|
const result = await showReviewMenu(orchestrator, ctx);
|
|
1058
|
-
if (result === "started") return
|
|
3175
|
+
if (result === "started") return "started";
|
|
1059
3176
|
continue;
|
|
1060
3177
|
}
|
|
1061
3178
|
|
|
3179
|
+
if (choice === "Quick") {
|
|
3180
|
+
await orchestrator.startTask(ctx, "quick", "quick");
|
|
3181
|
+
return "started";
|
|
3182
|
+
}
|
|
3183
|
+
|
|
1062
3184
|
if (choice === "Resume") {
|
|
1063
3185
|
const result = await showResumeMenu(orchestrator, ctx, undefined, "No paused tasks found.");
|
|
3186
|
+
if (result === "started") return "started";
|
|
3187
|
+
continue;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
async function showNoActiveMenu(orchestrator: Orchestrator, ctx: any): Promise<string | undefined> {
|
|
3193
|
+
while (true) {
|
|
3194
|
+
const choice = await selectOption(ctx, "/pp", [
|
|
3195
|
+
{ title: "Task", description: "Start a new task or resume a paused one" },
|
|
3196
|
+
{ title: "Info", description: "Subagents, usage, and task status" },
|
|
3197
|
+
{ title: "Settings", description: "Flant AI and other configuration" },
|
|
3198
|
+
{ title: "Back", description: "Close this menu" },
|
|
3199
|
+
]);
|
|
3200
|
+
if (!choice || choice === "Back") return undefined;
|
|
3201
|
+
|
|
3202
|
+
if (choice === "Task") {
|
|
3203
|
+
const result = await showTaskMenu(orchestrator, ctx);
|
|
1064
3204
|
if (result === "started") return undefined;
|
|
1065
3205
|
continue;
|
|
1066
3206
|
}
|
|
1067
3207
|
|
|
1068
|
-
if (choice === "
|
|
1069
|
-
await
|
|
3208
|
+
if (choice === "Info") {
|
|
3209
|
+
await showInfoMenu(orchestrator, ctx);
|
|
1070
3210
|
continue;
|
|
1071
3211
|
}
|
|
1072
3212
|
|
|
1073
|
-
await showSettingsMenu(orchestrator, ctx
|
|
3213
|
+
await showSettingsMenu(orchestrator, ctx);
|
|
1074
3214
|
}
|
|
1075
3215
|
}
|
|
1076
3216
|
|
|
1077
|
-
function getReviewLabels(orchestrator: Orchestrator): { autoLabel: string
|
|
1078
|
-
const
|
|
1079
|
-
const
|
|
1080
|
-
const
|
|
3217
|
+
function getReviewLabels(orchestrator: Orchestrator): { autoLabel: string } {
|
|
3218
|
+
const phase = orchestrator.active?.state.phase;
|
|
3219
|
+
const byPhase = (phase && orchestrator.active?.state.reviewPassByKind?.[phase]) ?? {};
|
|
3220
|
+
const autoCount = byPhase["auto"] ?? 0;
|
|
1081
3221
|
const autoLabel = autoCount > 0 ? `Auto review (pass ${autoCount + 1})` : "Auto review";
|
|
1082
|
-
|
|
1083
|
-
return { autoLabel, deepLabel };
|
|
3222
|
+
return { autoLabel };
|
|
1084
3223
|
}
|
|
1085
3224
|
|
|
1086
|
-
function hasEnabledReviewers(orchestrator: Orchestrator,
|
|
3225
|
+
function hasEnabledReviewers(orchestrator: Orchestrator, presetName?: string): boolean {
|
|
1087
3226
|
if (!orchestrator.active) return false;
|
|
1088
3227
|
const phase = orchestrator.active.state.phase;
|
|
1089
|
-
const
|
|
1090
|
-
const reviewers =
|
|
1091
|
-
|
|
1092
|
-
: phase === "plan"
|
|
1093
|
-
? config.planReviewers
|
|
1094
|
-
: config.codeReviewers;
|
|
1095
|
-
return Object.values(reviewers).some((v) => v.enabled);
|
|
3228
|
+
const group = getReviewPresetGroup(phase);
|
|
3229
|
+
const reviewers = resolvePreset(orchestrator.config, group, presetName);
|
|
3230
|
+
return Object.values(reviewers).some((v) => isEnabled(v));
|
|
1096
3231
|
}
|
|
1097
3232
|
|
|
1098
3233
|
function handleReviewResult(ctx: any, text: string): { continueLoop: boolean; text?: string } {
|
|
@@ -1103,6 +3238,44 @@ function handleReviewResult(ctx: any, text: string): { continueLoop: boolean; te
|
|
|
1103
3238
|
return { continueLoop: false, text };
|
|
1104
3239
|
}
|
|
1105
3240
|
|
|
3241
|
+
async function showQuickTaskMenu(
|
|
3242
|
+
orchestrator: Orchestrator,
|
|
3243
|
+
ctx: any,
|
|
3244
|
+
summary: string,
|
|
3245
|
+
mode: MenuMode,
|
|
3246
|
+
): Promise<string> {
|
|
3247
|
+
while (true) {
|
|
3248
|
+
if (!orchestrator.active) return "No active task.";
|
|
3249
|
+
const task = orchestrator.active;
|
|
3250
|
+
const headerLines = [`/pp\n\nTask: ${task.type}\nPhase: ${task.state.phase}`];
|
|
3251
|
+
if (summary !== "/pp") headerLines.push(`\n\n${summary}`);
|
|
3252
|
+
const menuTitle = headerLines.join("");
|
|
3253
|
+
|
|
3254
|
+
const choice = await selectOption(ctx, menuTitle, [
|
|
3255
|
+
opt("Complete", "Mark task as done and clean up"),
|
|
3256
|
+
opt("Pause", "Suspend task to resume later"),
|
|
3257
|
+
opt("Info", "Subagents, usage, and task status"),
|
|
3258
|
+
opt("Settings", "Flant AI and other configuration"),
|
|
3259
|
+
opt("Back", "Return to the prompt and keep working"),
|
|
3260
|
+
]);
|
|
3261
|
+
if (!choice || choice === "Back") return "";
|
|
3262
|
+
if (choice === "Info") {
|
|
3263
|
+
await showInfoMenu(orchestrator, ctx);
|
|
3264
|
+
continue;
|
|
3265
|
+
}
|
|
3266
|
+
if (choice === "Settings") {
|
|
3267
|
+
await showSettingsMenu(orchestrator, ctx);
|
|
3268
|
+
continue;
|
|
3269
|
+
}
|
|
3270
|
+
if (choice === "Pause") {
|
|
3271
|
+
const text = await pauseTask(orchestrator, ctx);
|
|
3272
|
+
return mode === "tool" ? text : "";
|
|
3273
|
+
}
|
|
3274
|
+
const text = await finishTask(orchestrator, ctx);
|
|
3275
|
+
return mode === "tool" ? text : "";
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
|
|
1106
3279
|
export async function showActiveTaskMenu(
|
|
1107
3280
|
orchestrator: Orchestrator,
|
|
1108
3281
|
ctx: any,
|
|
@@ -1115,47 +3288,78 @@ export async function showActiveTaskMenu(
|
|
|
1115
3288
|
if (!orchestrator.active) return "No active task.";
|
|
1116
3289
|
|
|
1117
3290
|
const task = orchestrator.active;
|
|
3291
|
+
if (task.type === "quick") {
|
|
3292
|
+
return showQuickTaskMenu(orchestrator, ctx, summary, mode);
|
|
3293
|
+
}
|
|
1118
3294
|
const phase = task.state.phase;
|
|
1119
3295
|
const step = task.state.step;
|
|
3296
|
+
const effectiveMode = getEffectivePhaseMode(task.state);
|
|
1120
3297
|
|
|
1121
|
-
|
|
1122
|
-
|
|
3298
|
+
// Suppress phase-advancing/review actions while a transition is in flight
|
|
3299
|
+
// (controller pending/compacting/resuming) OR while awaiting subagents. The
|
|
3300
|
+
// controller's isRunning() predicate already folds in the await_* steps.
|
|
3301
|
+
const waiting = !orchestrator.transitionController.isRunning();
|
|
3302
|
+
const { autoLabel } = getReviewLabels(orchestrator);
|
|
1123
3303
|
const isReviewPhase = phase === "review";
|
|
1124
3304
|
const hasPlannotator = phase === "plan" || phase === "implement" || isReviewPhase;
|
|
1125
3305
|
|
|
1126
3306
|
const opt = (title: string, description: string): OptionInput => ({ title, description });
|
|
1127
3307
|
|
|
3308
|
+
if (effectiveMode === "autonomous") {
|
|
3309
|
+
const autoChoice = await selectOption(ctx, `/pp\n\nTask: ${task.type}\nPhase: ${phase}${summary !== "/pp" ? `\n\n${summary}` : ""}`, [
|
|
3310
|
+
opt("Complete task", "Mark task as done and clean up"),
|
|
3311
|
+
opt("Pause task", "Suspend task to resume later"),
|
|
3312
|
+
opt("Info", "Subagents, usage, and task status"),
|
|
3313
|
+
opt("Settings", "Flant AI and other configuration"),
|
|
3314
|
+
opt("Back", "Return to the prompt and keep working"),
|
|
3315
|
+
]);
|
|
3316
|
+
if (!autoChoice || autoChoice === "Back") return "";
|
|
3317
|
+
if (autoChoice === "Info") {
|
|
3318
|
+
await showInfoMenu(orchestrator, ctx);
|
|
3319
|
+
continue;
|
|
3320
|
+
}
|
|
3321
|
+
if (autoChoice === "Settings") {
|
|
3322
|
+
await showSettingsMenu(orchestrator, ctx);
|
|
3323
|
+
continue;
|
|
3324
|
+
}
|
|
3325
|
+
if (autoChoice === "Complete task") {
|
|
3326
|
+
const text = await finishTask(orchestrator, ctx);
|
|
3327
|
+
return mode === "tool" ? text : "";
|
|
3328
|
+
}
|
|
3329
|
+
const text = await pauseTask(orchestrator, ctx);
|
|
3330
|
+
return mode === "tool" ? text : "";
|
|
3331
|
+
}
|
|
3332
|
+
|
|
1128
3333
|
const options: OptionInput[] = [];
|
|
1129
|
-
options.push(opt("
|
|
3334
|
+
options.push(opt("Next", "Complete, pause, or continue to next phase"));
|
|
1130
3335
|
if (!waiting) {
|
|
1131
3336
|
options.push(opt("Review", "Auto review, Plannotator, or manual review"));
|
|
1132
3337
|
}
|
|
1133
|
-
options.push(opt("
|
|
1134
|
-
options.push(opt("Settings", "
|
|
3338
|
+
options.push(opt("Info", "Subagents, usage, and task status"));
|
|
3339
|
+
options.push(opt("Settings", "Flant AI and other configuration"));
|
|
1135
3340
|
options.push(opt("Back", "Return to the prompt and keep working"));
|
|
1136
3341
|
|
|
1137
3342
|
const headerLines = [`/pp\n\nTask: ${task.type}\nPhase: ${phase}`];
|
|
1138
|
-
if (summary !== "/pp") headerLines.push(`\n${summary}`);
|
|
3343
|
+
if (summary !== "/pp") headerLines.push(`\n\n${summary}`);
|
|
1139
3344
|
const menuTitle = headerLines.join("");
|
|
1140
3345
|
const choice = await selectOption(ctx, menuTitle, options);
|
|
1141
3346
|
if (!choice || choice === "Back") {
|
|
1142
3347
|
return "";
|
|
1143
3348
|
}
|
|
1144
3349
|
|
|
1145
|
-
if (choice === "
|
|
1146
|
-
await
|
|
3350
|
+
if (choice === "Info") {
|
|
3351
|
+
await showInfoMenu(orchestrator, ctx);
|
|
1147
3352
|
continue;
|
|
1148
3353
|
}
|
|
1149
3354
|
if (choice === "Settings") {
|
|
1150
|
-
await showSettingsMenu(orchestrator, ctx
|
|
3355
|
+
await showSettingsMenu(orchestrator, ctx);
|
|
1151
3356
|
continue;
|
|
1152
3357
|
}
|
|
1153
|
-
|
|
1154
3358
|
if (mode === "command") {
|
|
1155
3359
|
await abortCurrentWork(orchestrator, ctx);
|
|
1156
3360
|
}
|
|
1157
3361
|
|
|
1158
|
-
if (choice === "
|
|
3362
|
+
if (choice === "Next") {
|
|
1159
3363
|
const canContinue = phase !== "implement" && !waiting;
|
|
1160
3364
|
const continueLabel = phase === "plan" ? "Continue to implement" : "Continue to plan & implement";
|
|
1161
3365
|
const finishOptions: OptionInput[] = [];
|
|
@@ -1166,7 +3370,7 @@ export async function showActiveTaskMenu(
|
|
|
1166
3370
|
finishOptions.push(opt("Pause", "Suspend task to resume later"));
|
|
1167
3371
|
finishOptions.push(opt("Back", "Return to the previous menu"));
|
|
1168
3372
|
|
|
1169
|
-
const finishChoice = await selectOption(ctx, "
|
|
3373
|
+
const finishChoice = await selectOption(ctx, "Next", finishOptions);
|
|
1170
3374
|
if (!finishChoice || finishChoice === "Back") continue;
|
|
1171
3375
|
if (finishChoice === "Pause") {
|
|
1172
3376
|
const text = await pauseTask(orchestrator, ctx);
|
|
@@ -1176,19 +3380,39 @@ export async function showActiveTaskMenu(
|
|
|
1176
3380
|
const text = await finishTask(orchestrator, ctx);
|
|
1177
3381
|
return mode === "tool" ? text : "";
|
|
1178
3382
|
}
|
|
3383
|
+
const next = nextPhase(task.type, phase);
|
|
3384
|
+
|
|
3385
|
+
if (
|
|
3386
|
+
next === "plan" &&
|
|
3387
|
+
task.type === "brainstorm"
|
|
3388
|
+
) {
|
|
3389
|
+
const modeSelection = await pickModeForTaskStart(orchestrator, ctx, "implement");
|
|
3390
|
+
if (!modeSelection) continue;
|
|
3391
|
+
task.state.mode = modeSelection.mode;
|
|
3392
|
+
task.state.effectiveMode = undefined;
|
|
3393
|
+
task.state.autonomousConfig = modeSelection.autonomousConfig;
|
|
3394
|
+
saveTask(task.dir, task.state);
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
let plannerPreset: string | undefined;
|
|
3398
|
+
if (next === "plan") {
|
|
3399
|
+
if (getEffectiveMode(task.state) !== "autonomous") {
|
|
3400
|
+
const pickedPlannerPreset = await pickPreset(ctx, orchestrator, "planners", "Planner preset");
|
|
3401
|
+
if (!pickedPlannerPreset) continue;
|
|
3402
|
+
plannerPreset = pickedPlannerPreset;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
1179
3405
|
finalizeReviewCycle(task);
|
|
1180
|
-
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
3406
|
+
const result = await orchestrator.transitionToNextPhase(ctx, plannerPreset);
|
|
1181
3407
|
if (!result.ok) return `Transition blocked: ${result.error}`;
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
if (curStep === "await_planners" || curStep === "await_reviewers") return "";
|
|
3408
|
+
// Transition is now owned by the controller (state ≠ running) or we're
|
|
3409
|
+
// awaiting subagents; either way the menu returns empty.
|
|
1185
3410
|
return "";
|
|
1186
3411
|
}
|
|
1187
3412
|
|
|
1188
3413
|
if (choice === "Review") {
|
|
1189
3414
|
const reviewOptions: OptionInput[] = [
|
|
1190
3415
|
opt(autoLabel, "Run automated review with configured reviewers"),
|
|
1191
|
-
opt(deepLabel, "Run automated review with higher thinking level"),
|
|
1192
3416
|
];
|
|
1193
3417
|
if (hasPlannotator) {
|
|
1194
3418
|
reviewOptions.push(opt("Review in Plannotator", phase === "plan" ? "Open plan review in browser" : "Open code diff review in browser"));
|
|
@@ -1209,56 +3433,70 @@ export async function showActiveTaskMenu(
|
|
|
1209
3433
|
if (handled.continueLoop) continue;
|
|
1210
3434
|
return handled.text ?? text;
|
|
1211
3435
|
}
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
{ cwd: orchestrator.cwd, timeout: 5000 },
|
|
1233
|
-
);
|
|
1234
|
-
if (logResult.code === 0 && logResult.stdout.trim()) {
|
|
1235
|
-
commits = logResult.stdout.trim().split("\n").map((line) => {
|
|
1236
|
-
const [hash, message, age] = line.split("\t");
|
|
1237
|
-
return { hash: hash || "", message: message || "", age: age || "" };
|
|
1238
|
-
}).filter((c) => c.hash);
|
|
3436
|
+
const repos = getRegisteredRepos(orchestrator);
|
|
3437
|
+
const summaries: string[] = [];
|
|
3438
|
+
let stopReviewing = false;
|
|
3439
|
+
|
|
3440
|
+
for (const repo of repos) {
|
|
3441
|
+
if (stopReviewing) break;
|
|
3442
|
+
|
|
3443
|
+
while (true) {
|
|
3444
|
+
const diffChoice = await selectOption(ctx, `Review: ${formatRepoLabel(repo)}`, [
|
|
3445
|
+
opt("All branch changes", "Committed changes vs base branch"),
|
|
3446
|
+
opt("Last commit", "Changes in the most recent commit"),
|
|
3447
|
+
opt("Since commit", "Review all changes since a specific commit"),
|
|
3448
|
+
opt("Uncommitted changes", "Working directory changes"),
|
|
3449
|
+
opt("Skip this repo", "Move to the next repository"),
|
|
3450
|
+
opt("Done (stop reviewing)", "Stop iterating repositories"),
|
|
3451
|
+
]);
|
|
3452
|
+
|
|
3453
|
+
if (!diffChoice || diffChoice === "Done (stop reviewing)") {
|
|
3454
|
+
stopReviewing = true;
|
|
3455
|
+
break;
|
|
1239
3456
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
3457
|
+
if (diffChoice === "Skip this repo") {
|
|
3458
|
+
summaries.push(`${formatRepoLabel(repo)}: SKIPPED`);
|
|
3459
|
+
break;
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
let diffType: string | undefined;
|
|
3463
|
+
let defaultBranch: string | undefined;
|
|
3464
|
+
|
|
3465
|
+
if (diffChoice === "All branch changes") {
|
|
3466
|
+
diffType = "branch";
|
|
3467
|
+
defaultBranch = await detectDefaultBranch(orchestrator, repos, repo.path);
|
|
3468
|
+
} else if (diffChoice === "Last commit") {
|
|
3469
|
+
diffType = "last-commit";
|
|
3470
|
+
} else if (diffChoice === "Since commit") {
|
|
3471
|
+
const pickedHash = await pickCommitForRepo(orchestrator, ctx, repo);
|
|
3472
|
+
if (!pickedHash) continue;
|
|
3473
|
+
diffType = "branch";
|
|
3474
|
+
defaultBranch = pickedHash;
|
|
3475
|
+
} else {
|
|
3476
|
+
diffType = "uncommitted";
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
const result = await openCodeReviewInPlannotator(orchestrator, {
|
|
3480
|
+
cwd: repo.path,
|
|
3481
|
+
diffType,
|
|
3482
|
+
defaultBranch,
|
|
3483
|
+
});
|
|
3484
|
+
if (result.status === "error") {
|
|
3485
|
+
summaries.push(`${formatRepoLabel(repo)}: ERROR${result.error ? ` — ${result.error}` : ""}`);
|
|
3486
|
+
} else if (result.status === "approved") {
|
|
3487
|
+
summaries.push(`${formatRepoLabel(repo)}: APPROVED`);
|
|
3488
|
+
} else {
|
|
3489
|
+
summaries.push(`${formatRepoLabel(repo)}: NEEDS_CHANGES${result.feedback ? `\nFeedback: ${result.feedback}` : ""}`);
|
|
3490
|
+
}
|
|
3491
|
+
break;
|
|
1244
3492
|
}
|
|
1245
|
-
const commitOptions: OptionInput[] = commits.map((c) => ({
|
|
1246
|
-
title: `${c.hash} ${c.message}`,
|
|
1247
|
-
description: c.age,
|
|
1248
|
-
}));
|
|
1249
|
-
commitOptions.push(opt("Back", "Return to the previous menu"));
|
|
1250
|
-
const picked = await selectOption(ctx, "Review changes since:", commitOptions);
|
|
1251
|
-
if (!picked || picked === "Back") continue;
|
|
1252
|
-
const pickedHash = picked.split(" ")[0];
|
|
1253
|
-
if (!pickedHash) continue;
|
|
1254
|
-
diffType = "branch";
|
|
1255
|
-
defaultBranch = pickedHash;
|
|
1256
|
-
} else {
|
|
1257
|
-
diffType = "uncommitted";
|
|
1258
3493
|
}
|
|
1259
3494
|
|
|
1260
|
-
|
|
1261
|
-
|
|
3495
|
+
if (summaries.length > 0) {
|
|
3496
|
+
ctx.ui.notify(`Plannotator review summary:\n\n${summaries.join("\n\n")}`, "info");
|
|
3497
|
+
} else {
|
|
3498
|
+
ctx.ui.notify("No repositories were reviewed in Plannotator.", "info");
|
|
3499
|
+
}
|
|
1262
3500
|
continue;
|
|
1263
3501
|
}
|
|
1264
3502
|
|
|
@@ -1271,14 +3509,15 @@ export async function showActiveTaskMenu(
|
|
|
1271
3509
|
return continueMessage;
|
|
1272
3510
|
}
|
|
1273
3511
|
|
|
3512
|
+
const reviewPreset = await pickPreset(ctx, orchestrator, getReviewPresetGroup(phase), "Review preset");
|
|
3513
|
+
if (!reviewPreset) continue;
|
|
1274
3514
|
finalizeReviewCycle(task);
|
|
1275
|
-
|
|
1276
|
-
if (!hasEnabledReviewers(orchestrator, kind)) {
|
|
3515
|
+
if (!hasEnabledReviewers(orchestrator, reviewPreset)) {
|
|
1277
3516
|
const label = phase === "brainstorm" ? "brainstorm" : phase === "plan" ? "plan" : "code";
|
|
1278
3517
|
ctx.ui.notify(`No ${label} reviewers enabled.`, "info");
|
|
1279
3518
|
continue;
|
|
1280
3519
|
}
|
|
1281
|
-
const text = await enterReviewCycle(orchestrator, ctx,
|
|
3520
|
+
const text = await enterReviewCycle(orchestrator, ctx, reviewPreset);
|
|
1282
3521
|
const curStep = orchestrator.active?.state.step;
|
|
1283
3522
|
if (curStep === "await_reviewers") return "";
|
|
1284
3523
|
const handled = handleReviewResult(ctx, text);
|